Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 26 additions & 6 deletions conformance-tests/VALIDATION_RESULTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,31 @@

## Summary

**Server Tests:** 40/40 passed (100%)
**Server Tests (active suite):** 44/44 passed (31 scenarios, 100%)
**Server Tests (spec 2025-11-25):** 4/4 passed — SEP-1613 `json-schema-2020-12` scenario ✨
**Client Tests:** 3/4 scenarios passed (9/10 checks passed)
**Auth Tests:** 14/15 scenarios fully passing (196 passed, 0 failed, 1 warning, 93.3% scenarios, 99.5% checks)

## Server Test Results

### Passing (40/40)
### Active Suite — Passing (31/31 scenarios, 44/44 checks)

- **Lifecycle & Utilities (4/4):** initialize, ping, logging-set-level, completion-complete
- **Tools (11/11):** All scenarios including progress notifications ✨
- **Tools (13/13):** All scenarios including progress notifications, sampling, elicitation
- **Elicitation (10/10):** SEP-1034 defaults (5 checks), SEP-1330 enums (5 checks)
- **Resources (6/6):** list, read-text, read-binary, templates-read, subscribe, unsubscribe
- **Prompts (4/4):** list, simple, with-args, embedded-resource, with-image
- **Resources (7/7):** list, read-text, read-binary, templates-read, subscribe, unsubscribe, SEP-2164 resource-not-found
- **Prompts (5/5):** list, simple, with-args, embedded-resource, with-image
- **SSE Transport (2/2):** Multiple streams
- **Security (2/2):** Localhost validation passes, DNS rebinding protection

### Spec 2025-11-25 Scenarios — Passing (1/1 scenario, 4/4 checks)

- **JSON Schema 2020-12 (SEP-1613) (4/4):** ✨
- `json_schema_2020_12_tool` found
- `inputSchema.$schema` field preserved
- `inputSchema.$defs` field preserved
- `inputSchema.additionalProperties` field preserved

## Client Test Results

### Passing (3/4 scenarios, 9/10 checks)
Expand Down Expand Up @@ -69,7 +78,7 @@ Uses the `client-spring-http-client` module with Spring Security OAuth2 and the

## Running Tests

### Server
### Server (active suite)
```bash
# Start server
./mvnw compile -pl conformance-tests/server-servlet -am exec:java
Expand All @@ -78,6 +87,17 @@ Uses the `client-spring-http-client` module with Spring Security OAuth2 and the
npx @modelcontextprotocol/conformance server --url http://localhost:8080/mcp --suite active
```

### Server (spec 2025-11-25 scenarios — SEP-1613)
```bash
# Start server (if not already running)
./mvnw compile -pl conformance-tests/server-servlet -am exec:java

# Run json-schema-2020-12 scenario
cd ../conformance && node --import tsx/esm src/index.ts server \
--url http://localhost:8080/mcp \
--scenario json-schema-2020-12
```

### Client
```bash
# Build
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import io.modelcontextprotocol.server.McpServerFeatures;
import io.modelcontextprotocol.server.transport.DefaultServerTransportSecurityValidator;
import io.modelcontextprotocol.server.transport.HttpServletStreamableServerTransportProvider;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.AudioContent;
import io.modelcontextprotocol.spec.McpSchema.BlobResourceContents;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
Expand Down Expand Up @@ -402,6 +403,29 @@ private static List<McpServerFeatures.SyncToolSpecification> createToolSpecs() {
})
.build(),

// json_schema_2020_12_tool - SEP-1613 dialect/keyword preservation
McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool
.builder("json_schema_2020_12_tool", 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))
.description("Tool with JSON Schema 2020-12 features (SEP-1613)")
.build())
.callHandler((exchange, request) -> {
logger.info("Tool 'json_schema_2020_12_tool' called");
return CallToolResult.builder()
.content(List.of(TextContent.builder("ok").build()))
.isError(false)
.build();
})
.build(),

// test_elicitation_sep1330_enums - Tool with enum schema improvements
McpServerFeatures.SyncToolSpecification.builder()
.tool(Tool.builder("test_elicitation_sep1330_enums", EMPTY_JSON_SCHEMA)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,29 @@
*/
public interface JsonSchemaValidator {

/**
* Asserts that the given schema document is a structurally valid JSON Schema. Schemas
* without an explicit {@code $schema} declaration, or those that declare JSON Schema
* 2020-12, are validated against the 2020-12 meta-schema. Schemas that explicitly
* declare a different dialect are accepted without meta-schema validation. Throws
* {@link IllegalArgumentException} if validation fails. Silently returns on null
* schema. The default implementation delegates to {@link #validateSchema}.
* @param context human-readable description of the schema's location (used in error
* messages)
* @param schema the schema document to validate, or {@code null} (no-op)
* @throws IllegalArgumentException if the schema is structurally invalid
*/
default void assertConforms(String context, Map<String, Object> schema) {
if (schema == null) {
return;
}
var result = validateSchema(schema);
if (!result.valid()) {
throw new IllegalArgumentException(
context + " is not a valid JSON Schema 2020-12 document (SEP-1613): " + result.errorMessage());
}
}

/**
* Represents the result of a validation operation.
*
Expand Down Expand Up @@ -41,4 +64,15 @@ public static ValidationResponse asInvalid(String message) {
*/
ValidationResponse validate(Map<String, Object> schema, Object structuredContent);

/**
* Validates that the given schema document itself conforms to JSON Schema 2020-12
* (SEP-1613). Schemas that declare an explicit non-2020-12 {@code $schema} dialect
* are skipped and considered valid. The default implementation is a no-op.
* @param schema the schema document to check
* @return a ValidationResponse indicating conformance
*/
default ValidationResponse validateSchema(Map<String, Object> schema) {
return ValidationResponse.asValid(null);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,8 @@ public class McpAsyncServer {
mcpTransportProvider.setSessionFactory(transport -> {
String sessionId = UUID.randomUUID().toString();
return new McpServerSession(sessionId, requestTimeout, transport, this::asyncInitializeRequestHandler,
requestHandlers, notificationHandlers, () -> this.cleanupForSession(sessionId));
requestHandlers, notificationHandlers, () -> this.cleanupForSession(sessionId),
this.jsonSchemaValidator);
});
}

Expand All @@ -183,9 +184,9 @@ public class McpAsyncServer {

this.protocolVersions = mcpTransportProvider.protocolVersions();

mcpTransportProvider.setSessionFactory(
new DefaultMcpStreamableServerSessionFactory(requestTimeout, this::asyncInitializeRequestHandler,
requestHandlers, notificationHandlers, sessionId -> this.cleanupForSession(sessionId)));
mcpTransportProvider.setSessionFactory(new DefaultMcpStreamableServerSessionFactory(requestTimeout,
this::asyncInitializeRequestHandler, requestHandlers, notificationHandlers,
sessionId -> this.cleanupForSession(sessionId), this.jsonSchemaValidator));
}

private Map<String, McpNotificationHandler> prepareNotificationHandlers(McpServerFeatures.Async features) {
Expand Down Expand Up @@ -347,6 +348,15 @@ public Mono<Void> addTool(McpServerFeatures.AsyncToolSpecification toolSpecifica
return Mono.error(new IllegalStateException("Server must be configured with tool capabilities"));
}

try {
var t = toolSpecification.tool();
this.jsonSchemaValidator.assertConforms("Tool '" + t.name() + "' inputSchema", t.inputSchema());
this.jsonSchemaValidator.assertConforms("Tool '" + t.name() + "' outputSchema", t.outputSchema());
}
catch (IllegalArgumentException e) {
return Mono.error(e);
}

var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator, toolSpecification);

return Mono.defer(() -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.util.Collections;

import io.modelcontextprotocol.json.TypeRef;
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpLoggableSession;
import io.modelcontextprotocol.spec.McpSchema;
Expand Down Expand Up @@ -37,6 +38,8 @@ public class McpAsyncServerExchange {

private final McpTransportContext transportContext;

private final JsonSchemaValidator jsonSchemaValidator;

private static final TypeRef<McpSchema.CreateMessageResult> CREATE_MESSAGE_RESULT_TYPE_REF = new TypeRef<>() {
};

Expand All @@ -51,21 +54,40 @@ public class McpAsyncServerExchange {

/**
* Create a new asynchronous exchange with the client.
* @param sessionId the session ID
* @param session The server session representing a 1-1 interaction.
* @param clientCapabilities The client capabilities that define the supported
* features and functionality.
* @param clientInfo The client implementation information.
* @param transportContext context associated with the client as extracted from the
* transport
* @param jsonSchemaValidator optional validator used to verify elicitation schemas
*/
public McpAsyncServerExchange(String sessionId, McpLoggableSession session,
McpSchema.ClientCapabilities clientCapabilities, McpSchema.Implementation clientInfo,
McpTransportContext transportContext) {
McpTransportContext transportContext, JsonSchemaValidator jsonSchemaValidator) {
this.sessionId = sessionId;
this.session = session;
this.clientCapabilities = clientCapabilities;
this.clientInfo = clientInfo;
this.transportContext = transportContext;
this.jsonSchemaValidator = jsonSchemaValidator;
}

/**
* Create a new asynchronous exchange with the client.
* @param sessionId the session ID
* @param session The server session representing a 1-1 interaction.
* @param clientCapabilities The client capabilities that define the supported
* features and functionality.
* @param clientInfo The client implementation information.
* @param transportContext context associated with the client as extracted from the
* transport
*/
public McpAsyncServerExchange(String sessionId, McpLoggableSession session,
McpSchema.ClientCapabilities clientCapabilities, McpSchema.Implementation clientInfo,
McpTransportContext transportContext) {
this(sessionId, session, clientCapabilities, clientInfo, transportContext, null);
}

/**
Expand Down Expand Up @@ -152,6 +174,15 @@ public Mono<McpSchema.ElicitResult> createElicitation(McpSchema.ElicitRequest el
if (this.clientCapabilities.elicitation() == null) {
return Mono.error(new IllegalStateException("Client must be configured with elicitation capabilities"));
}
if (this.jsonSchemaValidator != null) {
try {
this.jsonSchemaValidator.assertConforms("ElicitRequest requestedSchema",
elicitRequest.requestedSchema());
}
catch (IllegalArgumentException e) {
return Mono.error(e);
}
}
return this.session.sendRequest(McpSchema.METHOD_ELICITATION_CREATE, elicitRequest,
ELICITATION_RESULT_TYPE_REF);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,8 @@ public McpAsyncServer build() {
var jsonSchemaValidator = (this.jsonSchemaValidator != null) ? this.jsonSchemaValidator
: McpJsonDefaults.getSchemaValidator();

validateAsyncToolSchemas(jsonSchemaValidator, this.tools);

return new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
}
Expand All @@ -269,6 +271,9 @@ public McpAsyncServer build() {
this.instructions);
var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
: McpJsonDefaults.getSchemaValidator();

validateAsyncToolSchemas(jsonSchemaValidator, this.tools);

return new McpAsyncServer(transportProvider, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
}
Expand Down Expand Up @@ -829,11 +834,14 @@ public McpSyncServer build() {
McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures,
this.immediateExecution);

var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
: McpJsonDefaults.getSchemaValidator();

validateSyncToolSchemas(jsonSchemaValidator, this.tools);

var asyncServer = new McpAsyncServer(transportProvider,
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, requestTimeout,
uriTemplateManagerFactory,
jsonSchemaValidator != null ? jsonSchemaValidator : McpJsonDefaults.getSchemaValidator(),
validateToolInputs);
uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
return new McpSyncServer(asyncServer, this.immediateExecution);
}

Expand Down Expand Up @@ -862,6 +870,9 @@ public McpSyncServer build() {
this.immediateExecution);
var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
: McpJsonDefaults.getSchemaValidator();

validateSyncToolSchemas(jsonSchemaValidator, this.tools);

var asyncServer = new McpAsyncServer(transportProvider,
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, this.requestTimeout,
this.uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
Expand Down Expand Up @@ -1898,10 +1909,13 @@ public StatelessAsyncSpecification jsonSchemaValidator(JsonSchemaValidator jsonS
public McpStatelessAsyncServer build() {
var features = new McpStatelessServerFeatures.Async(this.serverInfo, this.serverCapabilities, this.tools,
this.resources, this.resourceTemplates, this.prompts, this.completions, this.instructions);
var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
: McpJsonDefaults.getSchemaValidator();

validateStatelessAsyncToolSchemas(jsonSchemaValidator, this.tools);

return new McpStatelessAsyncServer(transport, jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper,
features, requestTimeout, uriTemplateManagerFactory,
jsonSchemaValidator != null ? jsonSchemaValidator : McpJsonDefaults.getSchemaValidator(),
validateToolInputs);
features, requestTimeout, uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
}

}
Expand Down Expand Up @@ -2412,14 +2426,42 @@ public McpStatelessSyncServer build() {
var syncFeatures = new McpStatelessServerFeatures.Sync(this.serverInfo, this.serverCapabilities, this.tools,
this.resources, this.resourceTemplates, this.prompts, this.completions, this.instructions);
var asyncFeatures = McpStatelessServerFeatures.Async.fromSync(syncFeatures, this.immediateExecution);
var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
: McpJsonDefaults.getSchemaValidator();

validateStatelessSyncToolSchemas(jsonSchemaValidator, this.tools);

var asyncServer = new McpStatelessAsyncServer(transport,
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, requestTimeout,
uriTemplateManagerFactory,
this.jsonSchemaValidator != null ? this.jsonSchemaValidator : McpJsonDefaults.getSchemaValidator(),
validateToolInputs);
uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
return new McpStatelessSyncServer(asyncServer, this.immediateExecution);
}

}

private static void validateAsyncToolSchemas(JsonSchemaValidator validator,
List<McpServerFeatures.AsyncToolSpecification> tools) {
tools.forEach(spec -> validateToolSchema(validator, spec.tool()));
}

private static void validateSyncToolSchemas(JsonSchemaValidator validator,
List<McpServerFeatures.SyncToolSpecification> tools) {
tools.forEach(spec -> validateToolSchema(validator, spec.tool()));
}

private static void validateStatelessAsyncToolSchemas(JsonSchemaValidator validator,
List<McpStatelessServerFeatures.AsyncToolSpecification> tools) {
tools.forEach(spec -> validateToolSchema(validator, spec.tool()));
}

private static void validateStatelessSyncToolSchemas(JsonSchemaValidator validator,
List<McpStatelessServerFeatures.SyncToolSpecification> tools) {
tools.forEach(spec -> validateToolSchema(validator, spec.tool()));
}

private static void validateToolSchema(JsonSchemaValidator validator, McpSchema.Tool tool) {
validator.assertConforms("Tool '" + tool.name() + "' inputSchema", tool.inputSchema());
validator.assertConforms("Tool '" + tool.name() + "' outputSchema", tool.outputSchema());
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,15 @@ public Mono<Void> addTool(McpStatelessServerFeatures.AsyncToolSpecification tool
return Mono.error(new IllegalStateException("Server must be configured with tool capabilities"));
}

try {
var t = toolSpecification.tool();
this.jsonSchemaValidator.assertConforms("Tool '" + t.name() + "' inputSchema", t.inputSchema());
this.jsonSchemaValidator.assertConforms("Tool '" + t.name() + "' outputSchema", t.outputSchema());
}
catch (IllegalArgumentException e) {
return Mono.error(e);
}

var wrappedToolSpecification = withStructuredOutputHandling(this.jsonSchemaValidator, toolSpecification);

return Mono.defer(() -> {
Expand Down
Loading
Loading