Skip to content

Commit 3987ea7

Browse files
committed
fix(server): add structured content fallback without content
1 parent 057b632 commit 3987ea7

2 files changed

Lines changed: 49 additions & 5 deletions

File tree

packages/server/src/server/mcp.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import type {
2929
import {
3030
assertCompleteRequestPrompt,
3131
assertCompleteRequestResourceTemplate,
32-
isCallToolResult,
3332
normalizeRawShapeSchema,
3433
promptArgumentsFromStandardSchema,
3534
ProtocolError,
@@ -177,7 +176,7 @@ export class McpServer {
177176
// Per SEP-2106, a server returning array or primitive structuredContent MUST also emit a
178177
// TextContent block with the serialized JSON, so pre-SEP clients that only understand
179178
// object-typed structuredContent can fall back to the text content.
180-
return isCallToolResult(result) ? withStructuredContentTextFallback(result) : result;
179+
return withStructuredContentTextFallback(result);
181180
} catch (error) {
182181
if (error instanceof ProtocolError && error.code === ProtocolErrorCode.UrlElicitationRequired) {
183182
throw error; // Return the error to the caller without wrapping in CallToolResult
@@ -1191,10 +1190,11 @@ function withStructuredContentTextFallback(result: CallToolResult): CallToolResu
11911190
if (isPlainObject) {
11921191
return result;
11931192
}
1194-
if (result.content.some(block => block.type === 'text')) {
1193+
const content = Array.isArray(result.content) ? result.content : [];
1194+
if (content.some(block => block.type === 'text')) {
11951195
return result;
11961196
}
1197-
return { ...result, content: [...result.content, { type: 'text', text: JSON.stringify(structuredContent) }] };
1197+
return { ...result, content: [...content, { type: 'text', text: JSON.stringify(structuredContent) }] };
11981198
}
11991199

12001200
/**

packages/server/test/server/mcp.compat.test.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { JSONRPCMessage } from '@modelcontextprotocol/core';
1+
import type { CallToolResultWithStructuredContent, JSONRPCMessage } from '@modelcontextprotocol/core';
22
import { InMemoryTransport, isStandardSchema, LATEST_PROTOCOL_VERSION } from '@modelcontextprotocol/core';
33
import { describe, expect, expectTypeOf, it, vi } from 'vitest';
44
import * as z from 'zod/v4';
@@ -119,6 +119,50 @@ describe('registerTool/registerPrompt accept raw Zod shape (auto-wrapped)', () =
119119

120120
await server.close();
121121
});
122+
123+
it('adds text fallback for structuredContent when an untyped handler omits content', async () => {
124+
const server = new McpServer({ name: 't', version: '1.0.0' });
125+
const untypedHandler = (async () => ({ structuredContent: [1, 2, 3] })) as unknown as () => Promise<
126+
CallToolResultWithStructuredContent<number[]>
127+
>;
128+
129+
server.registerTool('forecast', { outputSchema: z.array(z.number()) }, untypedHandler);
130+
131+
const [client, srv] = InMemoryTransport.createLinkedPair();
132+
await server.connect(srv);
133+
await client.start();
134+
135+
const responses: JSONRPCMessage[] = [];
136+
client.onmessage = m => responses.push(m);
137+
138+
await client.send({
139+
jsonrpc: '2.0',
140+
id: 1,
141+
method: 'initialize',
142+
params: {
143+
protocolVersion: LATEST_PROTOCOL_VERSION,
144+
capabilities: {},
145+
clientInfo: { name: 'c', version: '1.0.0' }
146+
}
147+
} as JSONRPCMessage);
148+
await client.send({ jsonrpc: '2.0', method: 'notifications/initialized' } as JSONRPCMessage);
149+
await client.send({
150+
jsonrpc: '2.0',
151+
id: 2,
152+
method: 'tools/call',
153+
params: { name: 'forecast', arguments: {} }
154+
} as JSONRPCMessage);
155+
156+
await vi.waitFor(() => expect(responses.some(r => 'id' in r && r.id === 2)).toBe(true));
157+
158+
const response = responses.find(r => 'id' in r && r.id === 2) as {
159+
result?: { content?: Array<{ type: string; text?: string }>; structuredContent?: unknown };
160+
};
161+
expect(response.result?.structuredContent).toEqual([1, 2, 3]);
162+
expect(response.result?.content).toEqual([{ type: 'text', text: '[1,2,3]' }]);
163+
164+
await server.close();
165+
});
122166
});
123167

124168
describe('InferRawShape', () => {

0 commit comments

Comments
 (0)