SEP-2106: traceability YAML + JSON Schema 2020-12 conformance checks (#295)

* chore: add SEP-2106 requirement-traceability YAML (tools JSON Schema 2020-12)

* Drop structured-content text-fallback row from SEP-2106

That SHOULD predates SEP-2106 (pre-existing context line in tools.mdx); 2106
only relaxes structuredContent to allow non-object values (permissive). Point
spec_url at the SEP doc security section where the remaining obligations live.

* feat: SEP-2106 JSON Schema 2020-12 vocabulary preservation checks

Extend the json-schema-2020-12 server scenario (previously SEP-1613 only) with
three checks that verify the broader JSON Schema 2020-12 vocabulary SEP-2106
permits in a tool inputSchema survives tools/list rather than being stripped:
composition (allOf/anyOf), conditional (if/then/else), and reference ($anchor)
keywords.

- everything-server: add these keywords to json_schema_2020_12_tool's raw schema
- add a negative-test server (sep-2106-stripped-schema.ts) plus a negative test
  asserting the checks emit FAILURE when the vocabulary is stripped

* feat: SEP-2106 client scenario for network $ref dereferencing

Add json-schema-ref-no-deref, a client-suite scenario that advertises a tool
whose inputSchema contains a $ref pointing at a canary URL hosted by the
scenario. The sep-2106-no-network-ref-deref check fails if the client fetches
the canary while processing the schema, covering the SEP-2106 requirement that
implementations MUST NOT automatically dereference network-URI $ref values.

- register the scenario in the client scenarios list
- everything-client: handle the scenario with the basic connect+listTools flow
- tests: compliant client passes, a deliberately dereferencing client is
  caught, and a client that never lists tools cannot pass

* 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

* Soft-gate SEP-2106 keyword checks on the negotiated protocol version

The json-schema-2020-12 scenario is introducedIn 2025-11-25 (SEP-1613) but the
three SEP-2106 keyword-preservation checks test draft-era behavior. Rather than
failing servers that only negotiated 2025-11-25, gate the failure on the wire
protocolVersion from the initialize handshake: preserved keywords are SUCCESS at
any version, stripped keywords are FAILURE only when the server negotiated the
draft version and SKIPPED otherwise.

The published SDK does not advertise the draft version yet, so the negative test
asserts SKIPPED for the stripped-schema server; the FAILURE branch is covered by
unit tests on sep2106KeywordCheckStatus.
This commit is contained in:
Paul Carleton 2026-05-23 00:24:03 +01:00 • committed by GitHub
commit ed1ff0ca03
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 680 additions and 9 deletions

View file

@ -93,6 +93,12 @@ async function runBasicClient(serverUrl: string): Promise<void> {
registerScenarios(['initialize', 'tools-call'], runBasicClient);
// SEP-2106: json-schema-ref-no-deref advertises a tool whose inputSchema
// contains a network-URI $ref. A conformant client lists tools normally and
// simply never fetches that URI, so the basic connect+listTools flow is the
// correct behavior here.
registerScenario('json-schema-ref-no-deref', runBasicClient);
// ============================================================================
// request-metadata scenario (SEP-2575)
// ============================================================================

View file

@ -123,6 +123,8 @@ const JSON_SCHEMA_2020_12_INPUT_SCHEMA = {
type: 'object' as const,
$defs: {
address: {
// SEP-2106: reference keyword ($anchor) must be preserved
$anchor: 'addressDef',
type: 'object',
properties: {
street: { type: 'string' },
@ -132,8 +134,25 @@ const JSON_SCHEMA_2020_12_INPUT_SCHEMA = {
},
properties: {
name: { type: 'string' },
address: { $ref: '#/$defs/address' }
address: { $ref: '#/$defs/address' },
contactMethod: { type: 'string', enum: ['phone', 'email'] },
phone: { type: 'string' },
email: { type: 'string' }
},
// SEP-2106: the full JSON Schema 2020-12 vocabulary is permitted in
// inputSchema (alongside the required root `type: "object"`). These keywords
// exercise that SDKs preserve them through tools/list rather than stripping
// them down to properties/required.
//
// Composition keywords (allOf / anyOf):
allOf: [{ anyOf: [{ required: ['phone'] }, { required: ['email'] }] }],
// Conditional keywords (if / then / else):
if: {
properties: { contactMethod: { const: 'phone' } },
required: ['contactMethod']
},
then: { required: ['phone'] },
else: { required: ['email'] },
additionalProperties: false
};

View file

@ -0,0 +1,84 @@
#!/usr/bin/env node
/**
* SEP-2106 Negative Test Server
*
* Advertises `json_schema_2020_12_tool` but strips its inputSchema down to a
* bare `type: "object"` with simple properties — dropping the JSON Schema
* 2020-12 vocabulary (no $schema, $defs, additionalProperties, composition
* (allOf/anyOf), conditional (if/then/else), or $anchor keywords).
*
* The json-schema-2020-12 scenario should emit FAILURE for the SEP-2106
* composition / conditional / $anchor preservation checks (as well as the
* SEP-1613 $schema / $defs / additionalProperties checks) against this server.
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
import express from 'express';
function createServer() {
const server = new Server(
{ name: 'sep-2106-stripped-schema', version: '1.0.0' },
{ capabilities: { tools: {} } }
);
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'json_schema_2020_12_tool',
description:
'Tool whose JSON Schema 2020-12 keywords have been stripped',
// Stripped: only type + plain properties survive.
inputSchema: {
type: 'object',
properties: {
name: { type: 'string' },
address: {
type: 'object',
properties: {
street: { type: 'string' },
city: { type: 'string' }
}
}
}
}
}
]
}));
return server;
}
const app = express();
app.use(express.json());
app.post('/mcp', async (req, res) => {
try {
const server = createServer();
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: `Internal error: ${error instanceof Error ? error.message : String(error)}`
},
id: null
});
}
}
});
const PORT = parseInt(process.env.PORT || '3007', 10);
app.listen(PORT, '127.0.0.1', () => {
console.log(
`SEP-2106 negative test server running on http://localhost:${PORT}/mcp`
);
});