Skip to content

Commit e48c538

Browse files
authored
Merge pull request #1755 from QwenLM/fix/mcp-multipart-tool-results
fix(core): properly handle MCP multi-part tool results in OpenAI converter
2 parents cc55d78 + 0d026a5 commit e48c538

3 files changed

Lines changed: 463 additions & 8 deletions

File tree

packages/core/src/core/coreToolScheduler.test.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -889,15 +889,22 @@ describe('convertToFunctionResponse', () => {
889889
{ text: 'Another text part' },
890890
];
891891
const result = convertToFunctionResponse(toolName, callId, llmContent);
892+
// All content should be inside the FunctionResponse:
893+
// - text parts joined into response.output
894+
// - media parts in response.parts
892895
expect(result).toEqual([
893896
{
894897
functionResponse: {
895898
name: toolName,
896899
id: callId,
897-
response: { output: 'Tool execution succeeded.' },
900+
response: {
901+
output: 'Some textual description\nAnother text part',
902+
},
903+
parts: [
904+
{ inlineData: { mimeType: 'image/jpeg', data: 'base64data...' } },
905+
],
898906
},
899907
},
900-
...llmContent,
901908
]);
902909
});
903910

packages/core/src/core/coreToolScheduler.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,12 +190,26 @@ export function convertToFunctionResponse(
190190
}
191191

192192
if (Array.isArray(contentToProcess)) {
193-
const functionResponse = createFunctionResponsePart(
194-
callId,
195-
toolName,
196-
'Tool execution succeeded.',
197-
);
198-
return [functionResponse, ...toParts(contentToProcess)];
193+
// Extract text and media from all parts so that EVERYTHING is inside
194+
// the FunctionResponse.
195+
const textParts: string[] = [];
196+
const mediaParts: FunctionResponsePart[] = [];
197+
198+
for (const part of toParts(contentToProcess)) {
199+
if (part.text !== undefined) {
200+
textParts.push(part.text);
201+
} else if (part.inlineData) {
202+
mediaParts.push({ inlineData: part.inlineData });
203+
} else if (part.fileData) {
204+
mediaParts.push({ fileData: part.fileData });
205+
}
206+
// Other exotic part types (e.g. functionCall) are intentionally
207+
// dropped here – they should not appear inside tool results.
208+
}
209+
210+
const output =
211+
textParts.length > 0 ? textParts.join('\n') : 'Tool execution succeeded.';
212+
return [createFunctionResponsePart(callId, toolName, output, mediaParts)];
199213
}
200214

201215
// After this point, contentToProcess is a single Part object.

0 commit comments

Comments
 (0)