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

@ -10,6 +10,7 @@ import java.util.Arrays;
import java.util.List;
import java.util.Map;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerSession;
@ -461,6 +462,75 @@ class McpAsyncServerExchangeTests {
});
}
@Test
void testCreateElicitationWithInvalidRequestedSchema() {
McpSchema.ClientCapabilities capabilitiesWithElicitation = McpSchema.ClientCapabilities.builder()
.elicitation()
.build();
JsonSchemaValidator rejectingValidator = new JsonSchemaValidator() {
@Override
public ValidationResponse validate(Map<String, Object> schema, Object content) {
return ValidationResponse.asValid(null);
}
@Override
public ValidationResponse validateSchema(Map<String, Object> schema) {
return ValidationResponse.asInvalid("bad schema");
}
};
McpAsyncServerExchange exchangeWithValidator = new McpAsyncServerExchange("testSessionId", mockSession,
capabilitiesWithElicitation, clientInfo, McpTransportContext.EMPTY, rejectingValidator);
McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest
.builder("Provide info", Map.of("type", "invalid-type"))
.build();
StepVerifier.create(exchangeWithValidator.createElicitation(elicitRequest)).verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("SEP-1613")
.hasMessageContaining("ElicitRequest requestedSchema");
});
verify(mockSession, never()).sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), any(), any(TypeRef.class));
}
@Test
void testCreateElicitationWithValidSchemaPassesThroughToSession() {
McpSchema.ClientCapabilities capabilitiesWithElicitation = McpSchema.ClientCapabilities.builder()
.elicitation()
.build();
JsonSchemaValidator acceptingValidator = new JsonSchemaValidator() {
@Override
public ValidationResponse validate(Map<String, Object> schema, Object content) {
return ValidationResponse.asValid(null);
}
@Override
public ValidationResponse validateSchema(Map<String, Object> schema) {
return ValidationResponse.asValid(null);
}
};
McpAsyncServerExchange exchangeWithValidator = new McpAsyncServerExchange("testSessionId", mockSession,
capabilitiesWithElicitation, clientInfo, McpTransportContext.EMPTY, acceptingValidator);
Map<String, Object> validSchema = Map.of("type", "object");
McpSchema.ElicitRequest elicitRequest = McpSchema.ElicitRequest.builder("Provide info", validSchema).build();
when(mockSession.sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest), any(TypeRef.class)))
.thenReturn(Mono.just(McpSchema.ElicitResult.builder(McpSchema.ElicitResult.Action.ACCEPT).build()));
StepVerifier.create(exchangeWithValidator.createElicitation(elicitRequest)).assertNext(result -> {
assertThat(result.action()).isEqualTo(McpSchema.ElicitResult.Action.ACCEPT);
}).verifyComplete();
verify(mockSession, times(1)).sendRequest(eq(McpSchema.METHOD_ELICITATION_CREATE), eq(elicitRequest),
any(TypeRef.class));
}
// ---------------------------------------
// Create Message Tests
// ---------------------------------------