update to track spec changes

This commit is contained in:
Basil Hosmer 2025-05-21 12:19:03 -04:00
commit 7636c63f39
No known key found for this signature in database
6 changed files with 47 additions and 281 deletions

View file

@ -1,89 +0,0 @@
#!/usr/bin/env node
/**
* Client to test outputSchema servers
* This client connects to either the high-level or low-level outputSchema server
* and calls the get_weather tool to demonstrate structured output
*/
import { Client } from "../../client/index.js";
import { StdioClientTransport } from "../../client/stdio.js";
async function main() {
const serverPath = process.argv[2];
if (!serverPath) {
console.error("Usage: npx tsx src/examples/client/testOutputSchemaServers.ts <server-path>");
console.error("Example: npx tsx src/examples/client/testOutputSchemaServers.ts ../server/mcpServerOutputSchema.ts");
process.exit(1);
}
console.log(`Connecting to ${serverPath}...`);
// Create transport that spawns the server process
const transport = new StdioClientTransport({
command: "npx",
args: ["tsx", serverPath]
});
const client = new Client({
name: "output-schema-test-client",
version: "1.0.0"
}, {
capabilities: {}
});
try {
await client.connect(transport);
console.log("Connected to server\n");
// List available tools
console.log("Listing available tools...");
const toolsResult = await client.listTools();
console.log("Available tools:");
for (const tool of toolsResult.tools) {
console.log(`- ${tool.name}: ${tool.description}`);
if (tool.outputSchema) {
console.log(" Has outputSchema: Yes");
console.log(" Output schema:", JSON.stringify(tool.outputSchema, null, 2));
} else {
console.log(" Has outputSchema: No");
}
}
// Call the weather tool
console.log("\nCalling get_weather tool...");
// Note: Output schema validation only works when using the high-level Client API
// methods like callTool(). Low-level protocol requests do not validate the response.
const weatherResult = await client.callTool({
name: "get_weather",
arguments: {
city: "London",
country: "UK"
}
});
console.log("\nWeather tool result:");
if (weatherResult.structuredContent) {
console.log("Structured content:");
console.log(JSON.stringify(weatherResult.structuredContent, null, 2));
}
if (weatherResult.content && Array.isArray(weatherResult.content)) {
console.log("Unstructured content:");
weatherResult.content.forEach(content => {
if (content.type === "text") {
console.log(content.text);
}
});
}
await client.close();
console.log("\nDisconnected from server");
} catch (error) {
console.error("Error:", error);
process.exit(1);
}
}
main();

View file

@ -1,116 +0,0 @@
#!/usr/bin/env node
/**
* Example MCP server demonstrating tool outputSchema support using the low-level Server API
* This server manually handles tool listing and invocation requests to return structured data
* For a simpler high-level API approach, see mcpServerOutputSchema.ts
*/
import { Server } from "../../server/index.js";
import { StdioServerTransport } from "../../server/stdio.js";
import { CallToolRequest, CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError } from "../../types.js";
const server = new Server(
{
name: "output-schema-low-level-example",
version: "1.0.0",
},
{
capabilities: {
tools: {},
},
}
);
// Tool with structured output
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: "get_weather",
description: "Get weather information for a city",
inputSchema: {
type: "object",
properties: {
city: { type: "string", description: "City name" },
country: { type: "string", description: "Country code (e.g., US, UK)" }
},
required: ["city", "country"]
},
outputSchema: {
type: "object",
properties: {
temperature: {
type: "object",
properties: {
celsius: { type: "number" },
fahrenheit: { type: "number" }
},
required: ["celsius", "fahrenheit"]
},
conditions: {
type: "string",
enum: ["sunny", "cloudy", "rainy", "stormy", "snowy"]
},
humidity: { type: "number", minimum: 0, maximum: 100 },
wind: {
type: "object",
properties: {
speed_kmh: { type: "number" },
direction: { type: "string" }
},
required: ["speed_kmh", "direction"]
}
},
required: ["temperature", "conditions", "humidity", "wind"]
}
}
]
}));
server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) => {
switch (request.params.name) {
case "get_weather": {
const { city, country } = request.params.arguments as { city: string; country: string };
// Parameters are available but not used in this example
void city;
void country;
// Simulate weather API call
const temp_c = Math.round((Math.random() * 35 - 5) * 10) / 10;
const conditions = ["sunny", "cloudy", "rainy", "stormy", "snowy"][Math.floor(Math.random() * 5)];
// Return structured content matching the outputSchema
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)]
}
}
};
}
default:
throw new McpError(
ErrorCode.MethodNotFound,
`Unknown tool: ${request.params.name}`
);
}
});
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Low-level Output Schema Example Server running on stdio");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});

View file

@ -45,21 +45,26 @@ 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 {
content: [],
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)]
}
const 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 {
content: [{
type: "text",
text: JSON.stringify(structuredContent, null, 2)
}],
structuredContent
};
}
);

View file

@ -1224,10 +1224,7 @@ describe("tool()", () => {
mcpServer.server.connect(serverTransport),
]);
// First call listTools to cache the outputSchema in the client
await client.listTools();
// Call the tool and expect it to throw a validation error
// Call the tool and expect it to throw a server-side validation error
await expect(
client.callTool({
name: "test",
@ -1235,7 +1232,7 @@ describe("tool()", () => {
input: "hello",
},
}),
).rejects.toThrow(/Structured content does not match the tool's output schema/);
).rejects.toThrow(/Invalid structured content for tool test/);
});
/***

View file

@ -198,40 +198,15 @@ export class McpServer {
}
}
// Handle structured output and backward compatibility
if (tool.outputSchema) {
// Tool has outputSchema, so result must have structuredContent (unless it's an error)
if (!result.structuredContent && !result.isError) {
if (tool.outputSchema && result.structuredContent) {
// if the tool has an output schema, validate structured content
const parseResult = await tool.outputSchema.safeParseAsync(
result.structuredContent,
);
if (!parseResult.success) {
throw new McpError(
ErrorCode.InternalError,
`Tool ${request.params.name} has outputSchema but returned no structuredContent`,
);
}
// For backward compatibility, if structuredContent is provided but no content,
// automatically serialize the structured content to text
if (result.structuredContent && !result.content) {
result.content = [
{
type: "text",
text: JSON.stringify(result.structuredContent, null, 2),
},
];
}
} else {
// Tool must have content if no outputSchema
if (!result.content && !result.isError) {
throw new McpError(
ErrorCode.InternalError,
`Tool ${request.params.name} has no outputSchema and must return content`,
);
}
// If structuredContent is provided, it's an error
if (result.structuredContent) {
throw new McpError(
ErrorCode.InternalError,
`Tool ${request.params.name} has no outputSchema but returned structuredContent`,
ErrorCode.InvalidParams,
`Invalid structured content for tool ${request.params.name}: ${parseResult.error.message}`,
);
}
}

View file

@ -837,10 +837,8 @@ export const ToolSchema = z
})
.passthrough(),
/**
* An optional JSON Schema object defining the structure of the tool's output.
*
* If set, a CallToolResult for this Tool MUST contain a structuredContent field whose contents validate against this schema.
* If not set, a CallToolResult for this Tool MUST NOT contain a structuredContent field and MUST contain a content field.
* An optional JSON Schema object defining the structure of the tool's output returned in
* the structuredContent field of a CallToolResult.
*/
outputSchema: z.optional(
z.object({
@ -848,7 +846,7 @@ export const ToolSchema = z
properties: z.optional(z.object({}).passthrough()),
required: z.optional(z.array(z.string())),
})
.passthrough()
.passthrough()
),
/**
* Optional additional tool information.
@ -873,25 +871,7 @@ export const ListToolsResultSchema = PaginatedResultSchema.extend({
/**
* The server's response to a tool call.
*
* Any errors that originate from the tool SHOULD be reported inside the result
* object, with `isError` set to true, _not_ as an MCP protocol-level error
* response. Otherwise, the LLM would not be able to see that an error occurred
* and self-correct.
*
* However, any errors in _finding_ the tool, an error indicating that the
* server does not support tool calls, or any other exceptional conditions,
* should be reported as an MCP error response.
*/
export const ContentListSchema = z.array(
z.union([
TextContentSchema,
ImageContentSchema,
AudioContentSchema,
EmbeddedResourceSchema,
]),
);
export const CallToolResultSchema = ResultSchema.extend({
/**
* A list of content objects that represent the result of the tool call.
@ -899,7 +879,13 @@ export const CallToolResultSchema = ResultSchema.extend({
* If the Tool does not define an outputSchema, this field MUST be present in the result.
* For backwards compatibility, this field is always present, but it may be empty.
*/
content: ContentListSchema.default([]),
content: z.array(
z.union([
TextContentSchema,
ImageContentSchema,
AudioContentSchema,
EmbeddedResourceSchema,
])).default([]),
/**
* An object containing structured tool output.
@ -912,6 +898,15 @@ export const CallToolResultSchema = ResultSchema.extend({
* Whether the tool call ended in an error.
*
* If not set, this is assumed to be false (the call was successful).
*
* Any errors that originate from the tool SHOULD be reported inside the result
* object, with `isError` set to true, _not_ as an MCP protocol-level error
* response. Otherwise, the LLM would not be able to see that an error occurred
* and self-correct.
*
* However, any errors in _finding_ the tool, an error indicating that the
* server does not support tool calls, or any other exceptional conditions,
* should be reported as an MCP error response.
*/
isError: z.optional(z.boolean()),
});
@ -1366,7 +1361,6 @@ export type ToolAnnotations = Infer<typeof ToolAnnotationsSchema>;
export type Tool = Infer<typeof ToolSchema>;
export type ListToolsRequest = Infer<typeof ListToolsRequestSchema>;
export type ListToolsResult = Infer<typeof ListToolsResultSchema>;
export type ContentList = Infer<typeof ContentListSchema>;
export type CallToolResult = Infer<typeof CallToolResultSchema>;
export type CompatibilityCallToolResult = Infer<typeof CompatibilityCallToolResultSchema>;
export type CallToolRequest = Infer<typeof CallToolRequestSchema>;