diff --git a/src/client/index.ts b/src/client/index.ts index 08640d45..98a0190c 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -442,7 +442,7 @@ export class Client< try { // Validate the structured content (which is already an object) against the schema const validationResult = outputSchema.safeParse(result.structuredContent); - + if (!validationResult.success) { throw new McpError( ErrorCode.InvalidParams, @@ -485,10 +485,10 @@ export class Client< // Cache the tools and their output schemas for future validation this._cachedTools.clear(); this._cachedToolOutputSchemas.clear(); - + for (const tool of result.tools) { this._cachedTools.set(tool.name, tool); - + // If the tool has an outputSchema, create and cache the Zod schema if (tool.outputSchema) { try { diff --git a/src/examples/server/mcpServerOutputSchema.ts b/src/examples/server/mcpServerOutputSchema.ts index 87820b6a..cb845851 100644 --- a/src/examples/server/mcpServerOutputSchema.ts +++ b/src/examples/server/mcpServerOutputSchema.ts @@ -34,7 +34,7 @@ server.tool( }, required: ["celsius", "fahrenheit"] }, - conditions: { + conditions: { type: "string", enum: ["sunny", "cloudy", "rainy", "stormy", "snowy"] }, @@ -57,7 +57,7 @@ server.tool( // 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 { structuredContent: { temperature: { @@ -118,15 +118,15 @@ server.tool( const lines = csv_data.trim().split('\n'); const headers = lines[0].split(delimiter).map(h => h.trim()); const data = lines.slice(1).map(line => line.split(delimiter).map(cell => cell.trim())); - + // Infer data types const dataTypes: { [key: string]: string } = {}; const summary: { [key: string]: unknown } = {}; - + headers.forEach((header, idx) => { const values = data.map(row => row[idx]); const numericValues = values.filter(v => !isNaN(Number(v)) && v !== ''); - + if (numericValues.length === values.length) { dataTypes[header] = "number"; const numbers = numericValues.map(Number); @@ -140,7 +140,7 @@ server.tool( dataTypes[header] = "string"; } }); - + return { structuredContent: { row_count: data.length, @@ -174,7 +174,7 @@ server.tool( // Tool that can return both structured and unstructured content server.tool( - "hybrid_tool", + "hybrid_tool", "Tool that returns both structured and readable content", { data: z.array(z.number()).describe("Array of numbers to analyze") @@ -196,12 +196,12 @@ server.tool( async ({ data }) => { const mean = data.reduce((a, b) => a + b, 0) / data.length; const sorted = [...data].sort((a, b) => a - b); - const median = sorted.length % 2 === 0 + const median = sorted.length % 2 === 0 ? (sorted[sorted.length / 2 - 1] + sorted[sorted.length / 2]) / 2 : sorted[Math.floor(sorted.length / 2)]; const variance = data.reduce((acc, val) => acc + Math.pow(val - mean, 2), 0) / data.length; const std_dev = Math.sqrt(variance); - + return { structuredContent: { stats: { diff --git a/src/examples/server/outputSchema.ts b/src/examples/server/outputSchema.ts index cb17b4b6..8530a760 100644 --- a/src/examples/server/outputSchema.ts +++ b/src/examples/server/outputSchema.ts @@ -38,8 +38,8 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({ type: "object", properties: { bmi: { type: "number", description: "Body Mass Index" }, - category: { - type: "string", + category: { + type: "string", enum: ["underweight", "normal", "overweight", "obese"], description: "BMI category" }, @@ -102,22 +102,22 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) switch (request.params.name) { case "calculate_bmi": { const { height_cm, weight_kg } = request.params.arguments as { height_cm: number; weight_kg: number }; - + const height_m = height_cm / 100; const bmi = weight_kg / (height_m * height_m); - + let category: string; if (bmi < 18.5) category = "underweight"; else if (bmi < 25) category = "normal"; else if (bmi < 30) category = "overweight"; else category = "obese"; - + // Calculate healthy weight range for normal BMI (18.5-24.9) const min_healthy_bmi = 18.5; const max_healthy_bmi = 24.9; const min_healthy_weight = min_healthy_bmi * height_m * height_m; const max_healthy_weight = max_healthy_bmi * height_m * height_m; - + // Return structured content matching the outputSchema return { structuredContent: { @@ -130,31 +130,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) } }; } - + case "analyze_text": { const { text } = request.params.arguments as { text: string }; - + // Simple text analysis const words = text.trim().split(/\s+/); const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 0); const wordsPerMinute = 200; // Average reading speed - + // Very simple sentiment analysis (for demo purposes) const positiveWords = ["good", "great", "excellent", "happy", "positive", "amazing"]; const negativeWords = ["bad", "poor", "terrible", "sad", "negative", "awful"]; - + let positiveCount = 0; let negativeCount = 0; words.forEach(word => { if (positiveWords.includes(word.toLowerCase())) positiveCount++; if (negativeWords.includes(word.toLowerCase())) negativeCount++; }); - + let sentiment: string; if (positiveCount > negativeCount) sentiment = "positive"; else if (negativeCount > positiveCount) sentiment = "negative"; else sentiment = "neutral"; - + // Extract key phrases (simple approach - just common bigrams) const keyPhrases: string[] = []; for (let i = 0; i < words.length - 1; i++) { @@ -162,7 +162,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) keyPhrases.push(`${words[i]} ${words[i + 1]}`); } } - + return { structuredContent: { word_count: words.length, @@ -174,21 +174,21 @@ server.setRequestHandler(CallToolRequestSchema, async (request: CallToolRequest) } }; } - + case "traditional_tool": { const { message } = request.params.arguments as { message: string }; - + // Traditional tool returns content array return { content: [ - { - type: "text", - text: `Processed message: ${message.toUpperCase()}` + { + type: "text", + text: `Processed message: ${message.toUpperCase()}` } ] }; } - + default: throw new McpError( ErrorCode.MethodNotFound, diff --git a/src/server/mcp-outputschema.test.ts b/src/server/mcp-outputschema.test.ts index 62ea74d3..e59bd563 100644 --- a/src/server/mcp-outputschema.test.ts +++ b/src/server/mcp-outputschema.test.ts @@ -12,7 +12,7 @@ describe('McpServer outputSchema support', () => { beforeEach(async () => { server = new McpServer({ name: 'test', version: '1.0' }); client = new Client({ name: 'test-client', version: '1.0' }); - + [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); }); @@ -85,7 +85,7 @@ describe('McpServer outputSchema support', () => { 'structured-tool', { input: z.string() }, outputSchema, - () => ({ + () => ({ structuredContent: { result: 'test', count: 42 } }) ); @@ -118,7 +118,7 @@ describe('McpServer outputSchema support', () => { 'structured-tool', { input: z.string() }, outputSchema, - () => ({ + () => ({ structuredContent: { result: 'test' }, content: [{ type: 'text', text: 'Custom text' }] }) @@ -149,7 +149,7 @@ describe('McpServer outputSchema support', () => { 'broken-tool', { input: z.string() }, outputSchema, - () => ({ + () => ({ content: [{ type: 'text', text: 'No structured content' }] }) ); @@ -169,7 +169,7 @@ describe('McpServer outputSchema support', () => { server.tool( 'broken-tool', { input: z.string() }, - () => ({ + () => ({ structuredContent: { result: 'test' } }) ); diff --git a/src/server/mcp.test.ts b/src/server/mcp.test.ts index c617b16f..b9561739 100644 --- a/src/server/mcp.test.ts +++ b/src/server/mcp.test.ts @@ -860,8 +860,8 @@ describe("tool()", () => { expect(result.structuredContent).toBeDefined(); // For backward compatibility, content is auto-generated from structuredContent expect(result.content).toBeDefined(); - - const structuredContent = result.structuredContent as { + + const structuredContent = result.structuredContent as { processedInput: string; resultType: string; timestamp: string; diff --git a/src/server/mcp.ts b/src/server/mcp.ts index 5fa8db97..faf8faaf 100644 --- a/src/server/mcp.ts +++ b/src/server/mcp.ts @@ -121,12 +121,12 @@ export class McpServer { : EMPTY_OBJECT_JSON_SCHEMA, annotations: tool.annotations, }; - + // Only include outputSchema if it's defined if (tool.outputSchema) { toolDefinition.outputSchema = tool.outputSchema; } - + return toolDefinition; }, ), @@ -205,7 +205,7 @@ export class McpServer { `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) { @@ -224,7 +224,7 @@ export class McpServer { `Tool ${request.params.name} has no outputSchema but returned structuredContent`, ); } - + // Tool must have content if no outputSchema if (!result.content && !result.isError) { throw new McpError( @@ -774,11 +774,11 @@ export class McpServer { // Check if the next arg is potentially annotations or outputSchema if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null) { const nextArg = rest[0]; - + // Check if it's a JSON Schema (outputSchema) if (typeof nextArg === "object" && "type" in nextArg) { outputSchema = rest.shift() as Tool["outputSchema"]; - + // Check if there's still an annotations object if (rest.length > 1 && typeof rest[0] === "object" && rest[0] !== null && !(isZodRawShape(rest[0]))) { annotations = rest.shift() as ToolAnnotations; @@ -1011,7 +1011,7 @@ export class ResourceTemplate { * Callback for a tool handler registered with Server.tool(). * * Parameters will include tool arguments, if applicable, as well as other request handler context. - * + * * The callback should return: * - `structuredContent` if the tool has an outputSchema defined * - `content` if the tool does not have an outputSchema diff --git a/src/types.test.ts b/src/types.test.ts index 9347acf0..0fbc003d 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -4,7 +4,7 @@ describe("Types", () => { test("should have correct latest protocol version", () => { expect(LATEST_PROTOCOL_VERSION).toBeDefined(); - expect(LATEST_PROTOCOL_VERSION).toBe("DRAFT-2025-v2"); + expect(LATEST_PROTOCOL_VERSION).toBe("2025-03-26"); }); test("should have correct supported protocol versions", () => { expect(SUPPORTED_PROTOCOL_VERSIONS).toBeDefined(); diff --git a/src/types.ts b/src/types.ts index 710212c4..dc9ab5df 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,9 +1,8 @@ import { z, ZodTypeAny } from "zod"; -export const LATEST_PROTOCOL_VERSION = "DRAFT-2025-v2"; +export const LATEST_PROTOCOL_VERSION = "2025-03-26"; export const SUPPORTED_PROTOCOL_VERSIONS = [ LATEST_PROTOCOL_VERSION, - "2025-03-26", "2024-11-05", "2024-10-07", ]; @@ -889,12 +888,12 @@ export const CallToolUnstructuredResultSchema = ResultSchema.extend({ * If the Tool does not define an outputSchema, this field MUST be present in the result. */ content: ContentListSchema, - + /** * Structured output must not be provided in an unstructured tool result. */ structuredContent: z.never().optional(), - + /** * Whether the tool call ended in an error. * @@ -910,7 +909,7 @@ export const CallToolStructuredResultSchema = ResultSchema.extend({ * If the Tool defines an outputSchema, this field MUST be present in the result, and contain a JSON object that matches the schema. */ structuredContent: z.object({}).passthrough(), - + /** * A list of content objects that represent the result of the tool call. * @@ -919,7 +918,7 @@ export const CallToolStructuredResultSchema = ResultSchema.extend({ * Clients that support structured content should ignore this field. */ content: z.optional(ContentListSchema), - + /** * Whether the tool call ended in an error. *