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:
parent
5d4a1cce42
commit
87e2c7d4de
1 changed files with 903 additions and 35 deletions
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue