Skip to content

make content nonoptional, add structured output creation shim #521

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 13 additions & 20 deletions src/client/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
import { Transport } from "../shared/transport.js";
import { Server } from "../server/index.js";
import { InMemoryTransport } from "../inMemory.js";
import { createCallToolStructuredResult } from "../server/mcp.js";

/***
* Test: Initialize with Matching Protocol Version
Expand Down Expand Up @@ -841,9 +842,7 @@ describe('outputSchema validation', () => {

server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'test-tool') {
return {
structuredContent: { result: 'success', count: 42 },
};
return createCallToolStructuredResult({ result: 'success', count: 42 });
}
throw new Error('Unknown tool');
});
Expand Down Expand Up @@ -916,9 +915,7 @@ describe('outputSchema validation', () => {
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'test-tool') {
// Return invalid structured content (count is string instead of number)
return {
structuredContent: { result: 'success', count: 'not a number' },
};
return createCallToolStructuredResult({ result: 'success', count: 'not a number' });
}
throw new Error('Unknown tool');
});
Expand Down Expand Up @@ -1145,17 +1142,15 @@ describe('outputSchema validation', () => {

server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'complex-tool') {
return {
structuredContent: {
name: 'John Doe',
age: 30,
active: true,
tags: ['user', 'admin'],
metadata: {
created: '2023-01-01T00:00:00Z',
},
return createCallToolStructuredResult({
name: 'John Doe',
age: 30,
active: true,
tags: ['user', 'admin'],
metadata: {
created: '2023-01-01T00:00:00Z',
},
};
});
}
throw new Error('Unknown tool');
});
Expand Down Expand Up @@ -1230,12 +1225,10 @@ describe('outputSchema validation', () => {
server.setRequestHandler(CallToolRequestSchema, async (request) => {
if (request.params.name === 'strict-tool') {
// Return structured content with extra property
return {
structuredContent: {
return createCallToolStructuredResult({
name: 'John',
extraField: 'not allowed',
},
};
});
}
throw new Error('Unknown tool');
});
Expand Down
26 changes: 12 additions & 14 deletions src/examples/server/mcpServerOutputSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This demonstrates how to easily create tools with structured output
*/

import { McpServer } from "../../server/mcp.js";
import { createCallToolStructuredResult, McpServer } from "../../server/mcp.js";
import { StdioServerTransport } from "../../server/stdio.js";
import { z } from "zod";

Expand Down Expand Up @@ -45,20 +45,18 @@ server.registerTool(
const temp_c = Math.round((Math.random() * 35 - 5) * 10) / 10;
const conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"][Math.floor(Math.random() * 5)];

return {
structuredContent: {
temperature: {
celsius: temp_c,
fahrenheit: Math.round((temp_c * 9/5 + 32) * 10) / 10
},
conditions,
humidity: Math.round(Math.random() * 100),
wind: {
speed_kmh: Math.round(Math.random() * 50),
direction: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][Math.floor(Math.random() * 8)]
}
return createCallToolStructuredResult({
temperature: {
celsius: temp_c,
fahrenheit: Math.round((temp_c * 9/5 + 32) * 10) / 10
},
conditions,
humidity: Math.round(Math.random() * 100),
wind: {
speed_kmh: Math.round(Math.random() * 50),
direction: ["N", "NE", "E", "SE", "S", "SW", "W", "NW"][Math.floor(Math.random() * 8)]
}
};
})
}
);

Expand Down
9 changes: 3 additions & 6 deletions src/server/mcp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { ResourceTemplate } from "./mcp.js";
import { completable } from "./completable.js";
import { UriTemplate } from "../shared/uriTemplate.js";
import { createCallToolStructuredResult } from "./mcp.js";

describe("McpServer", () => {
/***
Expand Down Expand Up @@ -1082,12 +1083,10 @@ describe("tool()", () => {
timestamp: z.string()
},
},
async ({ input }) => ({
structuredContent: {
async ({ input }) => createCallToolStructuredResult({
processedInput: input,
resultType: "structured",
timestamp: "2023-01-01T00:00:00Z"
},
})
);

Expand Down Expand Up @@ -1185,13 +1184,11 @@ describe("tool()", () => {
timestamp: z.string()
},
},
async ({ input }) => ({
structuredContent: {
async ({ input }) => createCallToolStructuredResult({
processedInput: input,
resultType: "structured",
// Missing required 'timestamp' field
someExtraField: "unexpected" // Extra field not in schema
},
})
);

Expand Down
34 changes: 34 additions & 0 deletions src/server/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1161,3 +1161,37 @@ const EMPTY_COMPLETION_RESULT: CompleteResult = {
hasMore: false,
},
};

/**
* The spec encourages structured tools to return a `content` field
* containing a stringified version of the structured content for backward
* compatibility.
*
* Use this function to create a CallToolStructuredResult with a content
* field from your structured content, like so:
*
* async ({ input }) => createCallToolStructuredResult({
* firstProp: "first",
* secondProp: 2,
* // ...
* })
*
* Note: in SDK versions 1.11.*, the content field is required for all tool
* call results, in the interest of backward compatibility. Later versions
* of the SDK will allow tools to omit the content field if they return
* structured content.
*/
export function createCallToolStructuredResult(
structuredContent: CallToolResult["structuredContent"]
): CallToolResult {
return {
structuredContent,
content: [
{
type: "text",
text: JSON.stringify(structuredContent, null, 2),
},
],
};
}

8 changes: 4 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -924,14 +924,14 @@ export const CallToolStructuredResultSchema = ResultSchema.extend({
/**
* A list of content objects that represent the result of the tool call.
*
* If the Tool defines an outputSchema, this field MAY be present in the result.
* Per the spec, if the Tool defines an outputSchema, this field MAY be present in the result.
*
* Tools may use this field to provide compatibility with older clients that
* do not support structured content.
* In this SDK we automatically generate backwards-compatible `content` for older clients,
* so this field can be defined as non-optional.
*
* Clients that support structured content should ignore this field.
*/
content: z.optional(ContentListSchema),
content: ContentListSchema,

/**
* Whether the tool call ended in an error.
Expand Down