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

@ -8,10 +8,13 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.networknt.schema.Schema;
import com.networknt.schema.SchemaLocation;
import com.networknt.schema.SchemaRegistry;
import com.networknt.schema.Error;
import com.networknt.schema.dialect.Dialects;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.util.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -37,14 +40,18 @@ public class DefaultJsonSchemaValidator implements JsonSchemaValidator {
// TODO: Implement a strategy to purge the cache (TTL, size limit, etc.)
private final ConcurrentHashMap<String, Schema> schemaCache;
private final Schema metaSchema202012;
public DefaultJsonSchemaValidator() {
this(JsonMapper.shared());
}
public DefaultJsonSchemaValidator(JsonMapper jsonMapper) {
this.jsonMapper = jsonMapper;
this.schemaFactory = SchemaRegistry.withDialect(Dialects.getDraft202012());
this.schemaFactory = SchemaRegistry.withDefaultDialect(Dialects.getDraft202012());
this.schemaCache = new ConcurrentHashMap<>();
this.metaSchema202012 = schemaFactory
.getSchema(SchemaLocation.of("https://json-schema.org/draft/2020-12/schema"));
}
@Override
@ -85,6 +92,31 @@ public class DefaultJsonSchemaValidator implements JsonSchemaValidator {
}
}
@Override
public ValidationResponse validateSchema(Map<String, Object> schema) {
Assert.notNull(schema, "schema must not be null");
Object declaredDialect = schema.get("$schema");
if (declaredDialect != null && !McpSchema.JSON_SCHEMA_DIALECT_2020_12.equals(declaredDialect.toString())) {
return ValidationResponse.asValid(null);
}
if (this.metaSchema202012 == null) {
return ValidationResponse.asValid(null);
}
try {
JsonNode schemaNode = this.jsonMapper.valueToTree(schema);
List<Error> errors = this.metaSchema202012.validate(schemaNode);
if (!errors.isEmpty()) {
return ValidationResponse
.asInvalid("Schema does not conform to JSON Schema 2020-12 (SEP-1613): " + errors);
}
return ValidationResponse.asValid(null);
}
catch (Exception e) {
logger.error("Failed to validate schema definition: {}", e.getMessage());
return ValidationResponse.asInvalid("Failed to validate schema definition: " + e.getMessage());
}
}
/**
* Gets a cached Schema or creates and caches a new one.
* @param schema the schema map to convert