Skip to content

Commit 8fe3ce9

Browse files
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.
1 parent 2e67eb5 commit 8fe3ce9

File tree

4 files changed

+149
-14
lines changed

4 files changed

+149
-14
lines changed

src/types.test.ts

Lines changed: 128 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
ContentBlockSchema,
66
PromptMessageSchema,
77
CallToolResultSchema,
8-
CompleteRequestSchema
8+
CompleteRequestSchema,
9+
ToolSchema
910
} from './types.js';
1011

1112
describe('Types', () => {
@@ -311,4 +312,130 @@ describe('Types', () => {
311312
}
312313
});
313314
});
315+
316+
describe('ToolSchema - SEP-1613 JSON Schema 2020-12 support', () => {
317+
test('should accept inputSchema with $schema field', () => {
318+
const tool = {
319+
name: 'test',
320+
inputSchema: {
321+
$schema: 'https://json-schema.org/draft/2020-12/schema',
322+
type: 'object',
323+
properties: { name: { type: 'string' } }
324+
}
325+
};
326+
const result = ToolSchema.safeParse(tool);
327+
expect(result.success).toBe(true);
328+
});
329+
330+
test('should accept inputSchema with additionalProperties', () => {
331+
const tool = {
332+
name: 'test',
333+
inputSchema: {
334+
type: 'object',
335+
properties: { name: { type: 'string' } },
336+
additionalProperties: false
337+
}
338+
};
339+
const result = ToolSchema.safeParse(tool);
340+
expect(result.success).toBe(true);
341+
});
342+
343+
test('should accept inputSchema with composition keywords', () => {
344+
const tool = {
345+
name: 'test',
346+
inputSchema: {
347+
type: 'object',
348+
allOf: [{ properties: { a: { type: 'string' } } }, { properties: { b: { type: 'number' } } }]
349+
}
350+
};
351+
const result = ToolSchema.safeParse(tool);
352+
expect(result.success).toBe(true);
353+
});
354+
355+
test('should accept inputSchema with $ref and $defs', () => {
356+
const tool = {
357+
name: 'test',
358+
inputSchema: {
359+
type: 'object',
360+
properties: { user: { $ref: '#/$defs/User' } },
361+
$defs: {
362+
User: { type: 'object', properties: { name: { type: 'string' } } }
363+
}
364+
}
365+
};
366+
const result = ToolSchema.safeParse(tool);
367+
expect(result.success).toBe(true);
368+
});
369+
370+
test('should accept inputSchema with metadata keywords', () => {
371+
const tool = {
372+
name: 'test',
373+
inputSchema: {
374+
type: 'object',
375+
title: 'User Input',
376+
description: 'Input parameters for user creation',
377+
deprecated: false,
378+
examples: [{ name: 'John' }],
379+
properties: { name: { type: 'string' } }
380+
}
381+
};
382+
const result = ToolSchema.safeParse(tool);
383+
expect(result.success).toBe(true);
384+
});
385+
386+
test('should accept outputSchema with full JSON Schema features', () => {
387+
const tool = {
388+
name: 'test',
389+
inputSchema: { type: 'object' },
390+
outputSchema: {
391+
type: 'object',
392+
properties: {
393+
id: { type: 'string' },
394+
tags: { type: 'array' }
395+
},
396+
required: ['id'],
397+
additionalProperties: false,
398+
minProperties: 1
399+
}
400+
};
401+
const result = ToolSchema.safeParse(tool);
402+
expect(result.success).toBe(true);
403+
});
404+
405+
test('should still require type: object at root for inputSchema', () => {
406+
const tool = {
407+
name: 'test',
408+
inputSchema: {
409+
type: 'string'
410+
}
411+
};
412+
const result = ToolSchema.safeParse(tool);
413+
expect(result.success).toBe(false);
414+
});
415+
416+
test('should still require type: object at root for outputSchema', () => {
417+
const tool = {
418+
name: 'test',
419+
inputSchema: { type: 'object' },
420+
outputSchema: {
421+
type: 'array'
422+
}
423+
};
424+
const result = ToolSchema.safeParse(tool);
425+
expect(result.success).toBe(false);
426+
});
427+
428+
test('should accept simple minimal schema (backward compatibility)', () => {
429+
const tool = {
430+
name: 'test',
431+
inputSchema: {
432+
type: 'object',
433+
properties: { name: { type: 'string' } },
434+
required: ['name']
435+
}
436+
};
437+
const result = ToolSchema.safeParse(tool);
438+
expect(result.success).toBe(true);
439+
});
440+
});
314441
});

src/types.ts

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -955,27 +955,30 @@ export const ToolSchema = BaseMetadataSchema.extend({
955955
*/
956956
description: z.string().optional(),
957957
/**
958-
* A JSON Schema object defining the expected parameters for the tool.
958+
* A JSON Schema 2020-12 object defining the expected parameters for the tool.
959+
* Must have type: 'object' at the root level per MCP spec.
960+
* Accepts all JSON Schema 2020-12 keywords (the default dialect per SEP-1613).
959961
*/
960-
inputSchema: z.object({
961-
type: z.literal('object'),
962-
properties: z.record(z.string(), AssertObjectSchema).optional(),
963-
required: z.optional(z.array(z.string()))
964-
}),
962+
inputSchema: z
963+
.object({
964+
type: z.literal('object'),
965+
properties: z.record(z.string(), AssertObjectSchema).optional(),
966+
required: z.array(z.string()).optional()
967+
})
968+
.passthrough(),
965969
/**
966-
* An optional JSON Schema object defining the structure of the tool's output returned in
967-
* the structuredContent field of a CallToolResult.
970+
* An optional JSON Schema 2020-12 object defining the structure of the tool's output
971+
* returned in the structuredContent field of a CallToolResult.
972+
* Must have type: 'object' at the root level per MCP spec.
973+
* Accepts all JSON Schema 2020-12 keywords (the default dialect per SEP-1613).
968974
*/
969975
outputSchema: z
970976
.object({
971977
type: z.literal('object'),
972978
properties: z.record(z.string(), AssertObjectSchema).optional(),
973-
required: z.optional(z.array(z.string())),
974-
/**
975-
* Not in the MCP specification, but added to support the Ajv validator while removing .passthrough() which previously allowed additionalProperties to be passed through.
976-
*/
977-
additionalProperties: z.optional(z.boolean())
979+
required: z.array(z.string()).optional()
978980
})
981+
.passthrough()
979982
.optional(),
980983
/**
981984
* Optional additional tool information.

src/validation/ajv-provider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
/**
22
* AJV-based JSON Schema validator provider
3+
*
4+
* Defaults to JSON Schema 2020-12 (the default dialect per SEP-1613/MCP specification).
5+
* Schemas without an explicit $schema field are validated as 2020-12.
36
*/
47

58
import { Ajv } from 'ajv';

src/validation/cfworker-provider.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
* making it compatible with edge runtimes like Cloudflare Workers that restrict
66
* eval and new Function.
77
*
8+
* Defaults to JSON Schema 2020-12 (the default dialect per SEP-1613/MCP specification).
9+
* Schemas without an explicit $schema field are validated as 2020-12.
810
*/
911

1012
import { type Schema, Validator } from '@cfworker/json-schema';

0 commit comments

Comments
 (0)