From 2e071b62d324e1a0e4d242812131989e4206b650 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Thu, 1 Oct 2026 14:38:10 -0700 Subject: [PATCH] fix: repeat MCP tool data as JSON text for clients without structured content --- docs/TOOLS.md | 3 ++- src/tools/mcp/mcp.test.mjs | 5 ++++- src/tools/mcp/protocol.js | 3 +++ src/tools/queries/media.test.mjs | 2 +- 4 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/TOOLS.md b/docs/TOOLS.md index 45a4f22e8..ed9f8c280 100644 --- a/docs/TOOLS.md +++ b/docs/TOOLS.md @@ -32,7 +32,8 @@ checks completely; unsupported keywords are rejected at definition time. `run(args, { services, signal })` resolves to `{ summary, data }`: one sentence for people and a structured object for programs. A tool may also return `images`, each `{ mimeType, data }` with base64 data; the MCP adapter sends them as image -content. +content. MCP results carry the summary and the data as JSON text, plus the data +as `structuredContent`, for clients that read only one of them. `composeCatalog({ tools, services, replace, interceptors })` builds a catalog: diff --git a/src/tools/mcp/mcp.test.mjs b/src/tools/mcp/mcp.test.mjs index 61a2ff63f..54269ab2e 100644 --- a/src/tools/mcp/mcp.test.mjs +++ b/src/tools/mcp/mcp.test.mjs @@ -125,7 +125,10 @@ test('tool results carry text and structured content; failures stay generic', as (await request('tools/call', { name: 'count', arguments: { n: 3 } })) .result, { - content: [{ type: 'text', text: '3 things.' }], + content: [ + { type: 'text', text: '3 things.' }, + { type: 'text', text: '{"n":3}' }, + ], structuredContent: { n: 3 }, isError: false, }, diff --git a/src/tools/mcp/protocol.js b/src/tools/mcp/protocol.js index d86a0c6b8..44e3e778c 100644 --- a/src/tools/mcp/protocol.js +++ b/src/tools/mcp/protocol.js @@ -80,8 +80,11 @@ export function createMcpServer({ signal, }); return { + // The data is repeated as JSON text for clients that do not read + // structuredContent. content: [ { type: 'text', text: result.summary }, + { type: 'text', text: JSON.stringify(result.data) }, ...(result.images || []).map((item) => ({ type: 'image', data: item.data, diff --git a/src/tools/queries/media.test.mjs b/src/tools/queries/media.test.mjs index b99e9e781..a1916c111 100644 --- a/src/tools/queries/media.test.mjs +++ b/src/tools/queries/media.test.mjs @@ -97,7 +97,7 @@ test('a snapshot returns the frame as an image with its credit', async () => { method: 'tools/call', params: { name: 'get_cctv_snapshot', arguments: { camera_id: 'austin-1' } }, }); - assert.deepEqual(response.result.content[1], { + assert.deepEqual(response.result.content[2], { type: 'image', data: 'iVBORw0KGgo=', mimeType: 'image/png',