Skip to content

Commit 5c4f1fb

Browse files
authored
Merge branch 'main' into fweinberger/sep-1036
2 parents 8bbc5af + f087e44 commit 5c4f1fb

File tree

3 files changed

+325
-1
lines changed

3 files changed

+325
-1
lines changed

examples/servers/typescript/everything-server.ts

Lines changed: 134 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ import {
1717
ElicitResultSchema,
1818
McpError,
1919
ErrorCode
20+
ListToolsRequestSchema,
21+
type ListToolsResult,
22+
type Tool
2023
} from '@modelcontextprotocol/sdk/types.js';
24+
import { zodToJsonSchema } from 'zod-to-json-schema';
2125
import { z } from 'zod';
2226
import express from 'express';
2327
import cors from 'cors';
@@ -39,6 +43,28 @@ const TEST_IMAGE_BASE64 =
3943
const TEST_AUDIO_BASE64 =
4044
'UklGRiYAAABXQVZFZm10IBAAAAABAAEAQB8AAAB9AAACABAAZGF0YQIAAAA=';
4145

46+
// SEP-1613: Raw JSON Schema 2020-12 definition for conformance testing
47+
// This schema includes $schema, $defs, and additionalProperties to test
48+
// that SDKs correctly preserve these fields
49+
const JSON_SCHEMA_2020_12_INPUT_SCHEMA = {
50+
$schema: 'https://json-schema.org/draft/2020-12/schema',
51+
type: 'object' as const,
52+
$defs: {
53+
address: {
54+
type: 'object',
55+
properties: {
56+
street: { type: 'string' },
57+
city: { type: 'string' }
58+
}
59+
}
60+
},
61+
properties: {
62+
name: { type: 'string' },
63+
address: { $ref: '#/$defs/address' }
64+
},
65+
additionalProperties: false
66+
};
67+
4268
// Function to create a new MCP server instance (one per session)
4369
function createMcpServer() {
4470
const mcpServer = new McpServer(
@@ -662,7 +688,6 @@ function createMcpServer() {
662688
mcpServer.server.createElicitationCompletionNotifier(elicitationId);
663689
await notifier();
664690
}
665-
666691
return {
667692
content: [
668693
{
@@ -674,6 +699,40 @@ function createMcpServer() {
674699
}
675700
);
676701

702+
// SEP-1613: JSON Schema 2020-12 conformance test tool
703+
// This tool is registered with a Zod schema for tools/call validation,
704+
// but the tools/list handler (below) returns the raw JSON Schema 2020-12
705+
// definition to test that SDKs preserve $schema, $defs, additionalProperties
706+
mcpServer.registerTool(
707+
'json_schema_2020_12_tool',
708+
{
709+
description:
710+
'Tool with JSON Schema 2020-12 features for conformance testing (SEP-1613)',
711+
inputSchema: {
712+
name: z.string().optional(),
713+
address: z
714+
.object({
715+
street: z.string().optional(),
716+
city: z.string().optional()
717+
})
718+
.optional()
719+
}
720+
},
721+
async (args: {
722+
name?: string;
723+
address?: { street?: string; city?: string };
724+
}) => {
725+
return {
726+
content: [
727+
{
728+
type: 'text',
729+
text: `JSON Schema 2020-12 tool called with: ${JSON.stringify(args)}`
730+
}
731+
]
732+
};
733+
}
734+
);
735+
677736
// Dynamic tool (registered later via timer)
678737

679738
// ===== RESOURCES =====
@@ -937,6 +996,80 @@ function createMcpServer() {
937996
}
938997
);
939998

999+
// ===== SEP-1613: Override tools/list to return raw JSON Schema 2020-12 =====
1000+
// This override is necessary because registerTool converts Zod schemas to
1001+
// JSON Schema without preserving $schema, $defs, and additionalProperties.
1002+
// We need to return the raw JSON Schema for our test tool while using the
1003+
// SDK's conversion for other tools.
1004+
mcpServer.server.setRequestHandler(
1005+
ListToolsRequestSchema,
1006+
(): ListToolsResult => {
1007+
// Access internal registered tools (this is internal SDK API but stable)
1008+
const registeredTools = (mcpServer as any)._registeredTools as Record<
1009+
string,
1010+
{
1011+
enabled: boolean;
1012+
title?: string;
1013+
description?: string;
1014+
inputSchema?: any;
1015+
outputSchema?: any;
1016+
annotations?: any;
1017+
_meta?: any;
1018+
}
1019+
>;
1020+
1021+
return {
1022+
tools: Object.entries(registeredTools)
1023+
.filter(([, tool]) => tool.enabled)
1024+
.map(([name, tool]): Tool => {
1025+
// For our SEP-1613 test tool, return raw JSON Schema 2020-12
1026+
if (name === 'json_schema_2020_12_tool') {
1027+
return {
1028+
name,
1029+
description: tool.description,
1030+
inputSchema: JSON_SCHEMA_2020_12_INPUT_SCHEMA
1031+
};
1032+
}
1033+
1034+
// For other tools, convert Zod to JSON Schema
1035+
// Handle different inputSchema formats:
1036+
// - undefined/null: use empty object schema
1037+
// - Zod schema (has _def): convert directly
1038+
// - Raw shape (object with Zod values): wrap in z.object first
1039+
let inputSchema: Tool['inputSchema'];
1040+
if (!tool.inputSchema) {
1041+
inputSchema = { type: 'object' as const, properties: {} };
1042+
} else if ('_def' in tool.inputSchema) {
1043+
// Already a Zod schema
1044+
inputSchema = zodToJsonSchema(tool.inputSchema, {
1045+
strictUnions: true
1046+
}) as Tool['inputSchema'];
1047+
} else if (
1048+
typeof tool.inputSchema === 'object' &&
1049+
Object.keys(tool.inputSchema).length > 0
1050+
) {
1051+
// Raw shape with Zod values
1052+
inputSchema = zodToJsonSchema(z.object(tool.inputSchema), {
1053+
strictUnions: true
1054+
}) as Tool['inputSchema'];
1055+
} else {
1056+
// Empty object or unknown format
1057+
inputSchema = { type: 'object' as const, properties: {} };
1058+
}
1059+
1060+
return {
1061+
name,
1062+
title: tool.title,
1063+
description: tool.description,
1064+
inputSchema,
1065+
annotations: tool.annotations,
1066+
_meta: tool._meta
1067+
};
1068+
})
1069+
};
1070+
}
1071+
);
1072+
9401073
return mcpServer;
9411074
}
9421075

src/scenarios/index.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import {
2525
ToolsCallEmbeddedResourceScenario
2626
} from './server/tools.js';
2727

28+
import { JsonSchema2020_12Scenario } from './server/json-schema-2020-12.js';
29+
2830
import { ElicitationDefaultsScenario } from './server/elicitation-defaults.js';
2931
import { ElicitationEnumsScenario } from './server/elicitation-enums.js';
3032
import { ElicitationUrlModeScenario } from './server/elicitation-url.js';
@@ -55,6 +57,11 @@ const pendingClientScenariosList: ClientScenario[] = [
5557
new ElicitationEnumsScenario(),
5658
// Elicitation scenarios (SEP-1036) - URL mode (pending SDK release)
5759
new ElicitationUrlModeScenario()
60+
61+
// JSON Schema 2020-12 (SEP-1613)
62+
// This test is pending until the SDK includes PR #1135 which preserves
63+
// $schema, $defs, and additionalProperties fields in tool schemas.
64+
new JsonSchema2020_12Scenario()
5865
];
5966

6067
// All client scenarios
@@ -79,6 +86,9 @@ const allClientScenariosList: ClientScenario[] = [
7986
new ToolsCallSamplingScenario(),
8087
new ToolsCallElicitationScenario(),
8188

89+
// JSON Schema 2020-12 support (SEP-1613)
90+
new JsonSchema2020_12Scenario(),
91+
8292
// Elicitation scenarios (SEP-1034)
8393
new ElicitationDefaultsScenario(),
8494

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
/**
2+
* JSON Schema 2020-12 conformance test scenario (SEP-1613)
3+
*
4+
* Validates that MCP servers correctly preserve JSON Schema 2020-12 keywords
5+
* in tool definitions, ensuring implementations don't strip $schema, $defs,
6+
* or additionalProperties fields.
7+
*/
8+
9+
import { ClientScenario, ConformanceCheck } from '../../types.js';
10+
import { connectToServer } from './client-helper.js';
11+
12+
const EXPECTED_TOOL_NAME = 'json_schema_2020_12_tool';
13+
const EXPECTED_SCHEMA_DIALECT = 'https://json-schema.org/draft/2020-12/schema';
14+
15+
export class JsonSchema2020_12Scenario implements ClientScenario {
16+
name = 'json-schema-2020-12';
17+
description = `Validates JSON Schema 2020-12 keyword preservation (SEP-1613).
18+
19+
**Server Implementation Requirements:**
20+
21+
Implement tool \`${EXPECTED_TOOL_NAME}\` with inputSchema containing JSON Schema 2020-12 features:
22+
23+
\`\`\`json
24+
{
25+
"name": "${EXPECTED_TOOL_NAME}",
26+
"description": "Tool with JSON Schema 2020-12 features",
27+
"inputSchema": {
28+
"$schema": "${EXPECTED_SCHEMA_DIALECT}",
29+
"type": "object",
30+
"$defs": {
31+
"address": {
32+
"type": "object",
33+
"properties": {
34+
"street": { "type": "string" },
35+
"city": { "type": "string" }
36+
}
37+
}
38+
},
39+
"properties": {
40+
"name": { "type": "string" },
41+
"address": { "$ref": "#/$defs/address" }
42+
},
43+
"additionalProperties": false
44+
}
45+
}
46+
\`\`\`
47+
48+
**Verification**: The test verifies that \`$schema\`, \`$defs\`, and \`additionalProperties\` are preserved in the tool listing response.`;
49+
50+
async run(serverUrl: string): Promise<ConformanceCheck[]> {
51+
const checks: ConformanceCheck[] = [];
52+
const specReferences = [
53+
{
54+
id: 'SEP-1613',
55+
url: 'https://github.com/modelcontextprotocol/specification/pull/655'
56+
}
57+
];
58+
59+
try {
60+
const connection = await connectToServer(serverUrl);
61+
const result = await connection.client.listTools();
62+
63+
// Find the test tool
64+
const tool = result.tools?.find((t) => t.name === EXPECTED_TOOL_NAME);
65+
66+
// Check 1: Tool exists
67+
checks.push({
68+
id: 'json-schema-2020-12-tool-found',
69+
name: 'JsonSchema2020_12ToolFound',
70+
description: `Server advertises tool '${EXPECTED_TOOL_NAME}'`,
71+
status: tool ? 'SUCCESS' : 'FAILURE',
72+
timestamp: new Date().toISOString(),
73+
errorMessage: tool
74+
? undefined
75+
: `Tool '${EXPECTED_TOOL_NAME}' not found. Available tools: ${result.tools?.map((t) => t.name).join(', ') || 'none'}`,
76+
specReferences,
77+
details: {
78+
toolFound: !!tool,
79+
availableTools: result.tools?.map((t) => t.name) || []
80+
}
81+
});
82+
83+
if (!tool) {
84+
await connection.close();
85+
return checks;
86+
}
87+
88+
const inputSchema = tool.inputSchema as Record<string, unknown>;
89+
90+
// Check 2: $schema field preserved
91+
const hasSchema = '$schema' in inputSchema;
92+
const schemaValue = inputSchema['$schema'];
93+
const schemaCorrect = schemaValue === EXPECTED_SCHEMA_DIALECT;
94+
95+
checks.push({
96+
id: 'json-schema-2020-12-$schema',
97+
name: 'JsonSchema2020_12$Schema',
98+
description: `inputSchema.$schema field preserved with value '${EXPECTED_SCHEMA_DIALECT}'`,
99+
status: hasSchema && schemaCorrect ? 'SUCCESS' : 'FAILURE',
100+
timestamp: new Date().toISOString(),
101+
errorMessage: !hasSchema
102+
? '$schema field missing from inputSchema - field was likely stripped'
103+
: !schemaCorrect
104+
? `$schema has unexpected value: ${JSON.stringify(schemaValue)}`
105+
: undefined,
106+
specReferences,
107+
details: {
108+
hasSchema,
109+
schemaValue,
110+
expected: EXPECTED_SCHEMA_DIALECT
111+
}
112+
});
113+
114+
// Check 3: $defs field preserved
115+
const hasDefs = '$defs' in inputSchema;
116+
const defsValue = inputSchema['$defs'] as
117+
| Record<string, unknown>
118+
| undefined;
119+
const defsHasAddress = defsValue && 'address' in defsValue;
120+
121+
checks.push({
122+
id: 'json-schema-2020-12-$defs',
123+
name: 'JsonSchema2020_12$Defs',
124+
description:
125+
'inputSchema.$defs field preserved with expected structure',
126+
status: hasDefs && defsHasAddress ? 'SUCCESS' : 'FAILURE',
127+
timestamp: new Date().toISOString(),
128+
errorMessage: !hasDefs
129+
? '$defs field missing from inputSchema - field was likely stripped'
130+
: !defsHasAddress
131+
? '$defs exists but missing expected "address" definition'
132+
: undefined,
133+
specReferences,
134+
details: {
135+
hasDefs,
136+
defsKeys: defsValue ? Object.keys(defsValue) : [],
137+
defsValue
138+
}
139+
});
140+
141+
// Check 4: additionalProperties field preserved
142+
const hasAdditionalProps = 'additionalProperties' in inputSchema;
143+
const additionalPropsValue = inputSchema['additionalProperties'];
144+
const additionalPropsCorrect = additionalPropsValue === false;
145+
146+
checks.push({
147+
id: 'json-schema-2020-12-additionalProperties',
148+
name: 'JsonSchema2020_12AdditionalProperties',
149+
description: 'inputSchema.additionalProperties field preserved',
150+
status:
151+
hasAdditionalProps && additionalPropsCorrect ? 'SUCCESS' : 'FAILURE',
152+
timestamp: new Date().toISOString(),
153+
errorMessage: !hasAdditionalProps
154+
? 'additionalProperties field missing from inputSchema - field was likely stripped'
155+
: !additionalPropsCorrect
156+
? `additionalProperties has unexpected value: ${JSON.stringify(additionalPropsValue)}, expected: false`
157+
: undefined,
158+
specReferences,
159+
details: {
160+
hasAdditionalProps,
161+
additionalPropsValue,
162+
expected: false
163+
}
164+
});
165+
166+
await connection.close();
167+
} catch (error) {
168+
checks.push({
169+
id: 'json-schema-2020-12-error',
170+
name: 'JsonSchema2020_12Error',
171+
description: 'JSON Schema 2020-12 conformance test',
172+
status: 'FAILURE',
173+
timestamp: new Date().toISOString(),
174+
errorMessage: `Failed: ${error instanceof Error ? error.message : String(error)}`,
175+
specReferences
176+
});
177+
}
178+
179+
return checks;
180+
}
181+
}

0 commit comments

Comments
 (0)