Skip to content

Commit 895e7a1

Browse files
committed
fix(conformance): advertise raw 2020-12 schema in json-schema-2020-12 fixture tool
The json-schema-2020-12 server scenario asserts that $schema, $defs/$anchor, additionalProperties, composition (allOf/anyOf), and conditional (if/then/else) keywords are preserved verbatim in tools/list. The fixture registered the tool with a zod schema that never contained those keywords, so the scenario failed with 'field was likely stripped'. Register the exact scenario schema as raw JSON Schema via fromJsonSchema() instead; all 7 checks now pass. Also note the per-tool outputSchema compile-failure scoping in the SEP-2106 changeset.
1 parent c2533e1 commit 895e7a1

2 files changed

Lines changed: 41 additions & 13 deletions

File tree

.changeset/sep-2106-json-schema-2020-12.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,9 @@ Implement SEP-2106: tool `inputSchema`/`outputSchema` conform to JSON Schema 202
1313
- `McpServer.registerTool` type-checks a handler's returned `structuredContent` against the tool's `outputSchema` inferred output.
1414
- Servers returning array or primitive `structuredContent` automatically also emit a serialized `TextContent` block, so pre-SEP clients can fall back to the text content.
1515
- Built-in validators refuse to dereference non-same-document `$ref`/`$dynamicRef` (SSRF guard) and reject schemas exceeding depth / subschema-count bounds (composition-DoS guard).
16-
- The default Node validator now uses `Ajv2020`, so the 2020-12 dialect is honored by default (previously `new Ajv()` ran draft-07 semantics and silently ignored keywords such as `prefixItems`). Both built-in validators now default to the `2020-12` dialect (`MCP_DEFAULT_SCHEMA_DIALECT`).
17-
- New opt-in `resolveExternalSchemaRefs(schema, options)` helper (the SEP's optional external-`$ref` mode): fetches and inlines non-local `$ref`s ahead of time into a self-contained schema. Disabled by default, enforces a host allowlist (and rejects loopback/link-local/private targets otherwise), `https`-only by default, with fetch timeout / response-size / document-count limits, dereference logging, and fail-closed on unresolved references.
16+
- `Client.listTools()` no longer rejects when a single advertised tool's `outputSchema` fails to compile (e.g. it trips the safety guards above): the failure is scoped to the offending tool. Every other tool stays listable and callable; calling the offending tool throws a
17+
descriptive error instead of silently skipping output validation.
18+
- The default Node validator now uses `Ajv2020`, so the 2020-12 dialect is honored by default (previously `new Ajv()` ran draft-07 semantics and silently ignored keywords such as `prefixItems`). Both built-in validators now default to the `2020-12` dialect
19+
(`MCP_DEFAULT_SCHEMA_DIALECT`).
20+
- New opt-in `resolveExternalSchemaRefs(schema, options)` helper (the SEP's optional external-`$ref` mode): fetches and inlines non-local `$ref`s ahead of time into a self-contained schema. Disabled by default, enforces a host allowlist (and rejects loopback/link-local/private
21+
targets otherwise), `https`-only by default, with fetch timeout / response-size / document-count limits, dereference logging, and fail-closed on unresolved references.

test/conformance/src/everythingServer.ts

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { randomUUID } from 'node:crypto';
1212
import { localhostHostValidation } from '@modelcontextprotocol/express';
1313
import { NodeStreamableHTTPServerTransport } from '@modelcontextprotocol/node';
1414
import type { CallToolResult, EventId, EventStore, GetPromptResult, ReadResourceResult, StreamId } from '@modelcontextprotocol/server';
15-
import { isInitializeRequest, McpServer, ResourceTemplate } from '@modelcontextprotocol/server';
15+
import { fromJsonSchema, isInitializeRequest, McpServer, ResourceTemplate } from '@modelcontextprotocol/server';
1616
import cors from 'cors';
1717
import type { Request, Response } from 'express';
1818
import express from 'express';
@@ -597,19 +597,43 @@ function createMcpServer() {
597597
}
598598
);
599599
600-
// SEP-1613: JSON Schema 2020-12 conformance test tool
600+
// SEP-1613 / SEP-2106: JSON Schema 2020-12 conformance test tool. The `json-schema-2020-12`
601+
// scenario asserts that `$schema`, `$defs`/`$anchor`, `additionalProperties`, composition
602+
// (`allOf`/`anyOf`), and conditional (`if`/`then`/`else`) keywords are preserved verbatim in
603+
// the tools/list response, so the schema is registered as raw JSON Schema via fromJsonSchema().
601604
mcpServer.registerTool(
602605
'json_schema_2020_12_tool',
603606
{
604-
description: 'Tool with JSON Schema 2020-12 features for conformance testing (SEP-1613)',
605-
inputSchema: z.object({
606-
name: z.string().optional(),
607-
address: z
608-
.object({
609-
street: z.string().optional(),
610-
city: z.string().optional()
611-
})
612-
.optional()
607+
description: 'Tool with JSON Schema 2020-12 features for conformance testing (SEP-1613, SEP-2106)',
608+
inputSchema: fromJsonSchema<{ name?: string; address?: { street?: string; city?: string } }>({
609+
$schema: 'https://json-schema.org/draft/2020-12/schema',
610+
type: 'object',
611+
$defs: {
612+
address: {
613+
$anchor: 'addressDef',
614+
type: 'object',
615+
properties: {
616+
street: { type: 'string' },
617+
city: { type: 'string' }
618+
}
619+
}
620+
},
621+
properties: {
622+
name: { type: 'string' },
623+
address: { $ref: '#/$defs/address' },
624+
contactMethod: { type: 'string', enum: ['phone', 'email'] },
625+
phone: { type: 'string' },
626+
email: { type: 'string' }
627+
},
628+
allOf: [{ anyOf: [{ required: ['phone'] }, { required: ['email'] }] }],
629+
if: {
630+
properties: { contactMethod: { const: 'phone' } },
631+
required: ['contactMethod']
632+
},
633+
// eslint-disable-next-line unicorn/no-thenable -- JSON Schema conditional keyword, not a Promise
634+
then: { required: ['phone'] },
635+
else: { required: ['email'] },
636+
additionalProperties: false
613637
})
614638
},
615639
async (args: { name?: string; address?: { street?: string; city?: string } }): Promise<CallToolResult> => {

0 commit comments

Comments
 (0)