Skip to content

Commit b521c7a

Browse files
committed
Address review findings in SEP-2106 scenarios
- json-schema-ref-deref: make getChecks() idempotent (fresh array per call, matching the http-standard-headers convention) and guard the async /mcp handler like the example servers do - json-schema-2020-12: include the SEP-2106 keywords ($anchor, allOf/anyOf, if/then/else) in the description's example inputSchema so a server that copies it verbatim passes the new checks - sep-2106-stripped-schema: default to port 3007 to match negative.test.ts and avoid sharing a default with sep-2549-no-caching-hints
1 parent 8589972 commit b521c7a

3 files changed

Lines changed: 73 additions & 52 deletions

File tree

examples/servers/typescript/sep-2106-stripped-schema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ app.post('/mcp', async (req, res) => {
7676
}
7777
});
7878

79-
const PORT = parseInt(process.env.PORT || '3006', 10);
79+
const PORT = parseInt(process.env.PORT || '3007', 10);
8080
app.listen(PORT, '127.0.0.1', () => {
8181
console.log(
8282
`SEP-2106 negative test server running on http://localhost:${PORT}/mcp`

src/scenarios/client/json-schema-ref-deref.ts

Lines changed: 55 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,10 @@ The scenario advertises a tool whose inputSchema contains a \`$ref\` pointing at
7979

8080
private app: express.Application | null = null;
8181
private httpServer: ReturnType<express.Application['listen']> | null = null;
82-
private checks: ConformanceCheck[] = [];
8382
private canaryRequests: Array<{ method: string; userAgent?: string }> = [];
8483
private toolsListed = false;
8584

8685
async start(): Promise<ScenarioUrls> {
87-
this.checks = [];
8886
this.canaryRequests = [];
8987
this.toolsListed = false;
9088

@@ -107,15 +105,28 @@ The scenario advertises a tool whose inputSchema contains a \`$ref\` pointing at
107105
});
108106

109107
app.post('/mcp', async (req: Request, res: Response) => {
110-
// Stateless: fresh server and transport per request
111-
const server = createMcpServer(this.canaryUrl(), () => {
112-
this.toolsListed = true;
113-
});
114-
const transport = new StreamableHTTPServerTransport({
115-
sessionIdGenerator: undefined
116-
});
117-
await server.connect(transport);
118-
await transport.handleRequest(req, res, req.body);
108+
try {
109+
// Stateless: fresh server and transport per request
110+
const server = createMcpServer(this.canaryUrl(), () => {
111+
this.toolsListed = true;
112+
});
113+
const transport = new StreamableHTTPServerTransport({
114+
sessionIdGenerator: undefined
115+
});
116+
await server.connect(transport);
117+
await transport.handleRequest(req, res, req.body);
118+
} catch (error) {
119+
if (!res.headersSent) {
120+
res.status(500).json({
121+
jsonrpc: '2.0',
122+
error: {
123+
code: -32603,
124+
message: `Internal error: ${error instanceof Error ? error.message : String(error)}`
125+
},
126+
id: null
127+
});
128+
}
129+
}
119130
});
120131

121132
this.app = app;
@@ -144,44 +155,47 @@ The scenario advertises a tool whose inputSchema contains a \`$ref\` pointing at
144155
}
145156

146157
getChecks(): ConformanceCheck[] {
158+
// Built fresh on every call so getChecks() is idempotent — the runner may
159+
// call it more than once and we must not accumulate duplicates.
147160
const timestamp = new Date().toISOString();
148161
const fetched = this.canaryRequests.length > 0;
149162

150163
if (!this.toolsListed) {
151-
this.checks.push({
164+
return [
165+
{
166+
id: CHECK_ID,
167+
name: 'NoNetworkRefDereference',
168+
description:
169+
'Client never requested tools/list, so $ref handling could not be evaluated',
170+
status: 'FAILURE',
171+
timestamp,
172+
errorMessage:
173+
'Client did not call tools/list against a server advertising a tool with a network $ref',
174+
specReferences: SPEC_REFERENCES,
175+
details: { toolsListed: false }
176+
}
177+
];
178+
}
179+
180+
return [
181+
{
152182
id: CHECK_ID,
153183
name: 'NoNetworkRefDereference',
154-
description:
155-
'Client never requested tools/list, so $ref handling could not be evaluated',
156-
status: 'FAILURE',
184+
description: fetched
185+
? 'Client automatically dereferenced a network-URI $ref in a tool inputSchema. Implementations MUST NOT automatically dereference $ref values that resolve to a network URI (SEP-2106).'
186+
: 'Client did not dereference the network-URI $ref in the tool inputSchema',
187+
status: fetched ? 'FAILURE' : 'SUCCESS',
157188
timestamp,
158-
errorMessage:
159-
'Client did not call tools/list against a server advertising a tool with a network $ref',
189+
errorMessage: fetched
190+
? `Canary URL ${CANARY_PATH} was fetched ${this.canaryRequests.length} time(s)`
191+
: undefined,
160192
specReferences: SPEC_REFERENCES,
161-
details: { toolsListed: false }
162-
});
163-
return this.checks;
164-
}
165-
166-
this.checks.push({
167-
id: CHECK_ID,
168-
name: 'NoNetworkRefDereference',
169-
description: fetched
170-
? 'Client automatically dereferenced a network-URI $ref in a tool inputSchema. Implementations MUST NOT automatically dereference $ref values that resolve to a network URI (SEP-2106).'
171-
: 'Client did not dereference the network-URI $ref in the tool inputSchema',
172-
status: fetched ? 'FAILURE' : 'SUCCESS',
173-
timestamp,
174-
errorMessage: fetched
175-
? `Canary URL ${CANARY_PATH} was fetched ${this.canaryRequests.length} time(s)`
176-
: undefined,
177-
specReferences: SPEC_REFERENCES,
178-
details: {
179-
toolsListed: true,
180-
canaryRequestCount: this.canaryRequests.length,
181-
canaryRequests: this.canaryRequests
193+
details: {
194+
toolsListed: true,
195+
canaryRequestCount: this.canaryRequests.length,
196+
canaryRequests: this.canaryRequests
197+
}
182198
}
183-
});
184-
185-
return this.checks;
199+
];
186200
}
187201
}

src/scenarios/server/json-schema-2020-12.ts

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ const EXPECTED_SCHEMA_DIALECT = 'https://json-schema.org/draft/2020-12/schema';
2020
export class JsonSchema2020_12Scenario implements ClientScenario {
2121
name = 'json-schema-2020-12';
2222
readonly source = { introducedIn: '2025-11-25' } as const;
23-
description = `Validates JSON Schema 2020-12 keyword preservation (SEP-1613).
23+
description = `Validates JSON Schema 2020-12 keyword preservation (SEP-1613, SEP-2106).
2424
2525
**Server Implementation Requirements:**
2626
27-
Implement tool \`${EXPECTED_TOOL_NAME}\` with inputSchema containing JSON Schema 2020-12 features:
27+
Implement tool \`${EXPECTED_TOOL_NAME}\` with inputSchema containing JSON Schema 2020-12 features, including the broader vocabulary permitted by SEP-2106 (an \`$anchor\` inside \`$defs\`, composition keywords \`allOf\`/\`anyOf\`, and conditional keywords \`if\`/\`then\`/\`else\`):
2828
2929
\`\`\`json
3030
{
@@ -35,6 +35,7 @@ Implement tool \`${EXPECTED_TOOL_NAME}\` with inputSchema containing JSON Schema
3535
"type": "object",
3636
"$defs": {
3737
"address": {
38+
"$anchor": "addressDef",
3839
"type": "object",
3940
"properties": {
4041
"street": { "type": "string" },
@@ -44,20 +45,26 @@ Implement tool \`${EXPECTED_TOOL_NAME}\` with inputSchema containing JSON Schema
4445
},
4546
"properties": {
4647
"name": { "type": "string" },
47-
"address": { "$ref": "#/$defs/address" }
48+
"address": { "$ref": "#/$defs/address" },
49+
"contactMethod": { "type": "string", "enum": ["phone", "email"] },
50+
"phone": { "type": "string" },
51+
"email": { "type": "string" }
4852
},
53+
"allOf": [
54+
{ "anyOf": [{ "required": ["phone"] }, { "required": ["email"] }] }
55+
],
56+
"if": {
57+
"properties": { "contactMethod": { "const": "phone" } },
58+
"required": ["contactMethod"]
59+
},
60+
"then": { "required": ["phone"] },
61+
"else": { "required": ["email"] },
4962
"additionalProperties": false
5063
}
5164
}
5265
\`\`\`
5366
54-
The \`inputSchema\` should also exercise the broader JSON Schema 2020-12 vocabulary permitted by SEP-2106:
55-
56-
- a \`$defs\` subschema with an \`$anchor\`
57-
- composition keywords (\`allOf\` containing \`anyOf\`)
58-
- conditional keywords (\`if\`/\`then\`/\`else\`)
59-
60-
**Verification**: The test verifies that \`$schema\`, \`$defs\`, and \`additionalProperties\` are preserved (SEP-1613), and that the composition, conditional, and \`$anchor\` keywords are preserved (SEP-2106), in the tool listing response.`;
67+
**Verification**: The test verifies that \`$schema\`, \`$defs\`, and \`additionalProperties\` are preserved (SEP-1613), and that the composition (\`allOf\`/\`anyOf\`), conditional (\`if\`/\`then\`/\`else\`), and \`$anchor\` keywords are preserved (SEP-2106), in the tool listing response.`;
6168

6269
async run(serverUrl: string): Promise<ConformanceCheck[]> {
6370
const checks: ConformanceCheck[] = [];

0 commit comments

Comments
 (0)