Skip to content

Commit 3ec29bd

Browse files
bugerclaude
andcommitted
fix: use last step output instead of longest in MCP response extraction
extractResponseText() was picking the longest text across all workflow steps, which could return routing/intent classification output instead of the final generate-response output. Changed to pick the last step's text, matching the natural execution order. This fixes a bug where MCP clients would receive intent classifications (e.g. "engineering-task, skills: api-gateway") instead of the actual AI response when the routing step produced longer output than the final response. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent fa523b7 commit 3ec29bd

2 files changed

Lines changed: 69 additions & 15 deletions

File tree

src/runners/mcp-server-runner.ts

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -576,41 +576,42 @@ export class McpServerRunner implements Runner {
576576

577577
// The output history (reviewSummary.history) contains outputs keyed by step ID.
578578
// For assistant workflows the steps are e.g. chat.route-intent, chat.build-config,
579-
// chat.generate-response. The final AI response is the LAST step that has a .text
580-
// field with substantial content (not a short routing label).
579+
// chat.generate-response. The final AI response is the LAST step that produced
580+
// a text output — not the longest, since routing/classification steps can produce
581+
// longer text than the actual response.
581582
const history = result?.reviewSummary?.history;
582583
if (history && typeof history === 'object') {
583-
// Collect all candidate text outputs, keeping the last (deepest) one
584-
let bestText = '';
584+
// Pick the last step's text output (iteration order preserves insertion order)
585+
let lastText = '';
585586
for (const [, outputs] of Object.entries(history)) {
586587
if (!Array.isArray(outputs)) continue;
587588
for (const item of outputs as any[]) {
588589
const text = item?.text ?? item?.output?.text;
589-
if (typeof text === 'string' && text.length > bestText.length) {
590-
bestText = text;
590+
if (typeof text === 'string' && text.trim().length > 0) {
591+
lastText = text;
591592
}
592593
}
593594
}
594-
if (bestText) return bestText;
595+
if (lastText) return lastText;
595596
}
596597

597-
// Grouped results from execution statistics
598+
// Grouped results from execution statistics — pick the last text output
598599
const grouped = result?.executionStatistics?.groupedResults;
599600
if (grouped && typeof grouped === 'object') {
600-
let bestText = '';
601+
let lastText = '';
601602
for (const checkResults of Object.values(grouped)) {
602603
if (!Array.isArray(checkResults)) continue;
603604
for (const cr of checkResults as any[]) {
604605
const text =
605606
cr?.output?.text ??
606607
(typeof cr?.output === 'string' ? cr.output : null) ??
607608
(typeof cr?.content === 'string' && cr.content.trim() ? cr.content : null);
608-
if (typeof text === 'string' && text.length > bestText.length) {
609-
bestText = text;
609+
if (typeof text === 'string' && text.trim().length > 0) {
610+
lastText = text;
610611
}
611612
}
612613
}
613-
if (bestText) return bestText;
614+
if (lastText) return lastText;
614615
}
615616

616617
// Direct properties on result

tests/unit/runners/mcp-server-runner.test.ts

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ describe('extractResponseText', () => {
213213
expect(extractResponseText(undefined)).toBe('No response from workflow.');
214214
});
215215

216-
it('picks the longest text from history (final AI response, not intent)', () => {
216+
it('picks the last step text from history (final AI response, not routing)', () => {
217217
const result = {
218218
reviewSummary: {
219219
issues: [],
@@ -243,7 +243,7 @@ describe('extractResponseText', () => {
243243
expect(text).not.toBe('Short routing label');
244244
});
245245

246-
it('does not pick short intent/routing text over long response', () => {
246+
it('does not pick short intent/routing text over last response', () => {
247247
const result = {
248248
reviewSummary: {
249249
issues: [],
@@ -261,6 +261,36 @@ describe('extractResponseText', () => {
261261
expect(text).not.toBe('chat');
262262
});
263263

264+
it('picks last step even when routing output is longer than final response', () => {
265+
const result = {
266+
reviewSummary: {
267+
issues: [],
268+
history: {
269+
'chat.route-intent': [
270+
{
271+
text:
272+
'Based on analysis of the user query, I have determined this is a request about ' +
273+
'API gateway configuration. The user wants to understand how to set up rate limiting ' +
274+
'with multiple policies across different API endpoints. Classifying as: engineering-task. ' +
275+
'Relevant skills: api-gateway, rate-limiting, policy-management.',
276+
},
277+
],
278+
'chat.build-config': [{ mcp_servers: {}, text: 'config built' }],
279+
'chat.generate-response': [
280+
{
281+
text: 'To set up rate limiting, use the Tyk Dashboard.',
282+
},
283+
],
284+
},
285+
},
286+
};
287+
288+
const text = extractResponseText(result);
289+
// Should pick the last step (generate-response), NOT the longest (route-intent)
290+
expect(text).toBe('To set up rate limiting, use the Tyk Dashboard.');
291+
expect(text).not.toContain('Based on analysis');
292+
});
293+
264294
it('falls back to grouped results when history has no text', () => {
265295
const result = {
266296
reviewSummary: { issues: [], history: {} },
@@ -307,7 +337,7 @@ describe('extractResponseText', () => {
307337
expect(() => JSON.parse(text)).not.toThrow();
308338
});
309339

310-
it('handles multi-step workflow with multiple text outputs, picks longest', () => {
340+
it('handles multi-step workflow with multiple text outputs, picks last', () => {
311341
const result = {
312342
reviewSummary: {
313343
issues: [],
@@ -322,6 +352,29 @@ describe('extractResponseText', () => {
322352
const text = extractResponseText(result);
323353
expect(text).toContain('comprehensive response');
324354
});
355+
356+
it('picks last grouped result, not longest', () => {
357+
const result = {
358+
reviewSummary: { issues: [], history: {} },
359+
executionStatistics: {
360+
groupedResults: {
361+
routing: [
362+
{
363+
checkName: 'route',
364+
output: {
365+
text: 'Detailed routing analysis with lots of context about the user intent and classification',
366+
},
367+
issues: [],
368+
},
369+
],
370+
response: [{ checkName: 'respond', output: { text: 'Short final answer.' }, issues: [] }],
371+
},
372+
},
373+
};
374+
375+
const text = extractResponseText(result);
376+
expect(text).toBe('Short final answer.');
377+
});
325378
});
326379

327380
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)