feat: validate embedded JSON Schema documents against 2020-12 meta-schema (SEP-1613) (#949)

MCP embeds JSON Schema documents in three places — Tool.inputSchema,
Tool.outputSchema, and ElicitRequest.requestedSchema — and SEP-1613
mandates that these documents conform to JSON Schema 2020-12 by default.

Servers now reject malformed schemas at both build time (McpServer.build())
and runtime (McpAsyncServer/McpStatelessAsyncServer.addTool()), returning
an IllegalArgumentException that identifies the offending field and
references SEP-1613.  Elicitation requests whose requestedSchema violates
the meta-schema are rejected before being sent to the client.

Schemas that explicitly declare a different dialect via $schema are accepted
without meta-schema validation, since 2020-12 is the default, not the only
permitted dialect.

Refine meta-schema loading and deprecate orphan JsonSchemaValidator

Resolves #700

Signed-off-by: Dariusz Jędrzejczyk <2554306+chemicL@users.noreply.github.com>
This commit is contained in:
Dariusz Jędrzejczyk 2026-05-07 14:14:53 +02:00 • committed by GitHub
commit 87e2c7d4de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -1184,6 +1184,58 @@ public class McpSchemaTests {
assertThat(tool.annotations().returnDirect()).isFalse();
}
@Test
void testToolInputSchemaWithExplicitDialect() throws Exception {
Map<String, Object> inputSchema = new HashMap<>();
inputSchema.put("$schema", "http://json-schema.org/draft-07/schema#");
inputSchema.put("type", "object");
inputSchema.put("properties", Map.of("a", Map.of("type", "number")));
McpSchema.Tool tool = McpSchema.Tool.builder("calc", inputSchema).description("draft-07 tool").build();
String json = JSON_MAPPER.writeValueAsString(tool);
assertThatJson(json).inPath("$.inputSchema.$schema").isEqualTo("http://json-schema.org/draft-07/schema#");
McpSchema.Tool parsed = JSON_MAPPER.readValue(json, McpSchema.Tool.class);
assertThat(parsed.inputSchema()).containsEntry("$schema", "http://json-schema.org/draft-07/schema#");
}
@Test
void testToolOutputSchemaWithExplicitDialect() throws Exception {
Map<String, Object> inputSchema = Map.of("type", "object");
Map<String, Object> outputSchema = new HashMap<>();
outputSchema.put("$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12);
outputSchema.put("type", "object");
outputSchema.put("properties", Map.of("count", Map.of("type", "integer")));
McpSchema.Tool tool = McpSchema.Tool.builder("counter", inputSchema).outputSchema(outputSchema).build();
String json = JSON_MAPPER.writeValueAsString(tool);
assertThatJson(json).inPath("$.outputSchema.$schema").isEqualTo(McpSchema.JSON_SCHEMA_DIALECT_2020_12);
McpSchema.Tool parsed = JSON_MAPPER.readValue(json, McpSchema.Tool.class);
assertThat(parsed.outputSchema()).containsEntry("$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12);
}
@Test
void testToolPreserves2020_12Keywords() throws Exception {
Map<String, Object> inputSchema = Map.of("$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12, "type", "object",
"$defs",
Map.of("address",
Map.of("type", "object", "properties",
Map.of("street", Map.of("type", "string"), "city", Map.of("type", "string")))),
"properties", Map.of("name", Map.of("type", "string"), "address", Map.of("$ref", "#/$defs/address")),
"additionalProperties", false);
McpSchema.Tool tool = McpSchema.Tool.builder("addr_tool", inputSchema).build();
McpSchema.Tool parsed = JSON_MAPPER.readValue(JSON_MAPPER.writeValueAsString(tool), McpSchema.Tool.class);
Map<String, Object> rt = parsed.inputSchema();
assertThat(rt).containsEntry("$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12);
assertThat(rt).containsKey("$defs");
assertThat(rt).containsEntry("additionalProperties", false);
}
@Test
void testToolDeserializationWithoutOutputSchema() throws Exception {
String toolJson = """
@ -1567,6 +1619,24 @@ public class McpSchemaTests {
assertThat(request.progressToken()).isEqualTo("elicit-token-789");
}
@Test
void testElicitRequestSchemaWithExplicitDialect() throws Exception {
Map<String, Object> requestedSchema = new HashMap<>();
requestedSchema.put("$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12);
requestedSchema.put("type", "object");
requestedSchema.put("properties", Map.of("name", Map.of("type", "string")));
requestedSchema.put("required", List.of("name"));
McpSchema.ElicitRequest request = McpSchema.ElicitRequest.builder("Please provide name", requestedSchema)
.build();
String json = JSON_MAPPER.writeValueAsString(request);
assertThatJson(json).inPath("$.requestedSchema.$schema").isEqualTo(McpSchema.JSON_SCHEMA_DIALECT_2020_12);
McpSchema.ElicitRequest parsed = JSON_MAPPER.readValue(json, McpSchema.ElicitRequest.class);
assertThat(parsed.requestedSchema()).containsEntry("$schema", McpSchema.JSON_SCHEMA_DIALECT_2020_12);
}
// Pagination Tests
@Test