From 2d3b1a5d1ea4ce6924bf8602ad8c2b51563a6ff6 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Tue, 18 Nov 2025 22:47:18 +0000 Subject: [PATCH 1/3] SEP-1613: Support full JSON Schema 2020-12 in ToolSchema Add .passthrough() to inputSchema and outputSchema to accept all JSON Schema 2020-12 keywords. This is the correct approach because: - inputSchema/outputSchema are embedded external specs (JSON Schema), not MCP protocol fields that should be strictly validated - The SDK's role is to transport schemas, not validate JSON Schema structure - Enumeration approach would silently drop unrecognized keywords (data loss) Changes: - Add .passthrough() to inputSchema and outputSchema in ToolSchema - Update JSDoc to document SEP-1613/2020-12 as default dialect - Add comprehensive tests for JSON Schema 2020-12 keyword support Backwards compatible: existing typed properties (type, properties, required) remain explicitly typed for TypeScript autocomplete. --- src/types.test.ts | 129 +++++++++++++++++++++++++++- src/types.ts | 27 +++--- src/validation/ajv-provider.ts | 3 + src/validation/cfworker-provider.ts | 2 + 4 files changed, 147 insertions(+), 14 deletions(-) diff --git a/src/types.test.ts b/src/types.test.ts index cd8cc0711..8bb965189 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -5,7 +5,8 @@ import { ContentBlockSchema, PromptMessageSchema, CallToolResultSchema, - CompleteRequestSchema + CompleteRequestSchema, + ToolSchema } from './types.js'; describe('Types', () => { @@ -311,4 +312,130 @@ describe('Types', () => { } }); }); + + describe('ToolSchema - JSON Schema 2020-12 support', () => { + test('should accept inputSchema with $schema field', () => { + const tool = { + name: 'test', + inputSchema: { + $schema: 'https://json-schema.org/draft/2020-12/schema', + type: 'object', + properties: { name: { type: 'string' } } + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + }); + + test('should accept inputSchema with additionalProperties', () => { + const tool = { + name: 'test', + inputSchema: { + type: 'object', + properties: { name: { type: 'string' } }, + additionalProperties: false + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + }); + + test('should accept inputSchema with composition keywords', () => { + const tool = { + name: 'test', + inputSchema: { + type: 'object', + allOf: [{ properties: { a: { type: 'string' } } }, { properties: { b: { type: 'number' } } }] + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + }); + + test('should accept inputSchema with $ref and $defs', () => { + const tool = { + name: 'test', + inputSchema: { + type: 'object', + properties: { user: { $ref: '#/$defs/User' } }, + $defs: { + User: { type: 'object', properties: { name: { type: 'string' } } } + } + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + }); + + test('should accept inputSchema with metadata keywords', () => { + const tool = { + name: 'test', + inputSchema: { + type: 'object', + title: 'User Input', + description: 'Input parameters for user creation', + deprecated: false, + examples: [{ name: 'John' }], + properties: { name: { type: 'string' } } + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + }); + + test('should accept outputSchema with full JSON Schema features', () => { + const tool = { + name: 'test', + inputSchema: { type: 'object' }, + outputSchema: { + type: 'object', + properties: { + id: { type: 'string' }, + tags: { type: 'array' } + }, + required: ['id'], + additionalProperties: false, + minProperties: 1 + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + }); + + test('should still require type: object at root for inputSchema', () => { + const tool = { + name: 'test', + inputSchema: { + type: 'string' + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(false); + }); + + test('should still require type: object at root for outputSchema', () => { + const tool = { + name: 'test', + inputSchema: { type: 'object' }, + outputSchema: { + type: 'array' + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(false); + }); + + test('should accept simple minimal schema (backward compatibility)', () => { + const tool = { + name: 'test', + inputSchema: { + type: 'object', + properties: { name: { type: 'string' } }, + required: ['name'] + } + }; + const result = ToolSchema.safeParse(tool); + expect(result.success).toBe(true); + }); + }); }); diff --git a/src/types.ts b/src/types.ts index 78fa81d54..bceb6d493 100644 --- a/src/types.ts +++ b/src/types.ts @@ -955,27 +955,28 @@ export const ToolSchema = BaseMetadataSchema.extend({ */ description: z.string().optional(), /** - * A JSON Schema object defining the expected parameters for the tool. + * A JSON Schema 2020-12 object defining the expected parameters for the tool. + * Must have type: 'object' at the root level per MCP spec. */ - inputSchema: z.object({ - type: z.literal('object'), - properties: z.record(z.string(), AssertObjectSchema).optional(), - required: z.optional(z.array(z.string())) - }), + inputSchema: z + .object({ + type: z.literal('object'), + properties: z.record(z.string(), AssertObjectSchema).optional(), + required: z.array(z.string()).optional() + }) + .passthrough(), /** - * An optional JSON Schema object defining the structure of the tool's output returned in - * the structuredContent field of a CallToolResult. + * An optional JSON Schema 2020-12 object defining the structure of the tool's output + * returned in the structuredContent field of a CallToolResult. + * Must have type: 'object' at the root level per MCP spec. */ outputSchema: z .object({ type: z.literal('object'), properties: z.record(z.string(), AssertObjectSchema).optional(), - required: z.optional(z.array(z.string())), - /** - * Not in the MCP specification, but added to support the Ajv validator while removing .passthrough() which previously allowed additionalProperties to be passed through. - */ - additionalProperties: z.optional(z.boolean()) + required: z.array(z.string()).optional() }) + .passthrough() .optional(), /** * Optional additional tool information. diff --git a/src/validation/ajv-provider.ts b/src/validation/ajv-provider.ts index 115a98521..1ee38fc4e 100644 --- a/src/validation/ajv-provider.ts +++ b/src/validation/ajv-provider.ts @@ -1,5 +1,8 @@ /** * AJV-based JSON Schema validator provider + * + * Defaults to JSON Schema 2020-12 (the default dialect per MCP specification). + * Schemas without an explicit $schema field are validated as 2020-12. */ import { Ajv } from 'ajv'; diff --git a/src/validation/cfworker-provider.ts b/src/validation/cfworker-provider.ts index 60ec3f06e..7ec02403c 100644 --- a/src/validation/cfworker-provider.ts +++ b/src/validation/cfworker-provider.ts @@ -5,6 +5,8 @@ * making it compatible with edge runtimes like Cloudflare Workers that restrict * eval and new Function. * + * Defaults to JSON Schema 2020-12 (the default dialect per MCP specification). + * Schemas without an explicit $schema field are validated as 2020-12. */ import { type Schema, Validator } from '@cfworker/json-schema'; From 913ca2ad77953f7be0a9b060d686435b2629a726 Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 20 Nov 2025 14:26:24 +0000 Subject: [PATCH 2/3] Address review: use .catchall(z.unknown()) instead of .passthrough() More semantically correct for JSON Schema objects where additional properties are expected but not validated by Zod. --- src/types.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types.ts b/src/types.ts index bceb6d493..bb465265b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -964,7 +964,7 @@ export const ToolSchema = BaseMetadataSchema.extend({ properties: z.record(z.string(), AssertObjectSchema).optional(), required: z.array(z.string()).optional() }) - .passthrough(), + .catchall(z.unknown()), /** * An optional JSON Schema 2020-12 object defining the structure of the tool's output * returned in the structuredContent field of a CallToolResult. @@ -976,7 +976,7 @@ export const ToolSchema = BaseMetadataSchema.extend({ properties: z.record(z.string(), AssertObjectSchema).optional(), required: z.array(z.string()).optional() }) - .passthrough() + .catchall(z.unknown()) .optional(), /** * Optional additional tool information. From 6116af34719f3ac96e23d2d6c26d9d53bef36a2e Mon Sep 17 00:00:00 2001 From: Felix Weinberger Date: Thu, 20 Nov 2025 21:05:51 +0000 Subject: [PATCH 3/3] Remove unnecessary comment additions from validation providers --- src/validation/ajv-provider.ts | 3 --- src/validation/cfworker-provider.ts | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/validation/ajv-provider.ts b/src/validation/ajv-provider.ts index 1ee38fc4e..115a98521 100644 --- a/src/validation/ajv-provider.ts +++ b/src/validation/ajv-provider.ts @@ -1,8 +1,5 @@ /** * AJV-based JSON Schema validator provider - * - * Defaults to JSON Schema 2020-12 (the default dialect per MCP specification). - * Schemas without an explicit $schema field are validated as 2020-12. */ import { Ajv } from 'ajv'; diff --git a/src/validation/cfworker-provider.ts b/src/validation/cfworker-provider.ts index 7ec02403c..adb102037 100644 --- a/src/validation/cfworker-provider.ts +++ b/src/validation/cfworker-provider.ts @@ -4,9 +4,6 @@ * This provider uses @cfworker/json-schema for validation without code generation, * making it compatible with edge runtimes like Cloudflare Workers that restrict * eval and new Function. - * - * Defaults to JSON Schema 2020-12 (the default dialect per MCP specification). - * Schemas without an explicit $schema field are validated as 2020-12. */ import { type Schema, Validator } from '@cfworker/json-schema';