From 37a9f3a5e1a956e9c735f22a43090c8b052e484f Mon Sep 17 00:00:00 2001 From: RameshReddy Adutla Date: Wed, 4 Mar 2026 21:15:23 +0000 Subject: [PATCH] Add @JsonIgnoreProperties(ignoreUnknown = true) to capability sub-records The top-level ClientCapabilities and ServerCapabilities records have @JsonIgnoreProperties(ignoreUnknown = true) but their nested sub-records do not. Since ObjectMapper defaults to FAIL_ON_UNKNOWN_PROPERTIES = true, unknown fields on sub-objects cause deserialization failures. This already caused a real breakage when elicitation gained form/url fields (#724). The MCP spec explicitly does not close capability objects (additionalProperties is never set to false), so SDKs must tolerate unknown fields for forward compatibility. Add the annotation to: - ClientCapabilities.Sampling - ClientCapabilities.Elicitation, Elicitation.Form, Elicitation.Url - ServerCapabilities.CompletionCapabilities - ServerCapabilities.LoggingCapabilities - ServerCapabilities.PromptCapabilities - ServerCapabilities.ResourceCapabilities - ServerCapabilities.ToolCapabilities Fixes modelcontextprotocol#766 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../modelcontextprotocol/spec/McpSchema.java | 9 +++ .../spec/McpSchemaTests.java | 71 +++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java index bb9cead7e..d765b57ad 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -404,6 +404,7 @@ public record RootCapabilities(@JsonProperty("listChanged") Boolean listChanged) * from MCP servers in their prompts. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) public record Sampling() { } @@ -431,12 +432,14 @@ public record Sampling() { * @param url support for out-of-band URL-based elicitation */ @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) public record Elicitation(@JsonProperty("form") Form form, @JsonProperty("url") Url url) { /** * Marker record indicating support for form-based elicitation mode. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) public record Form() { } @@ -444,6 +447,7 @@ public record Form() { * Marker record indicating support for URL-based elicitation mode. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) public record Url() { } @@ -542,6 +546,7 @@ public record ServerCapabilities( // @formatter:off * Present if the server supports argument autocompletion suggestions. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) public record CompletionCapabilities() { } @@ -549,6 +554,7 @@ public record CompletionCapabilities() { * Present if the server supports sending log messages to the client. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) public record LoggingCapabilities() { } @@ -559,6 +565,7 @@ public record LoggingCapabilities() { * the prompt list */ @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) public record PromptCapabilities(@JsonProperty("listChanged") Boolean listChanged) { } @@ -570,6 +577,7 @@ public record PromptCapabilities(@JsonProperty("listChanged") Boolean listChange * the resource list */ @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) public record ResourceCapabilities(@JsonProperty("subscribe") Boolean subscribe, @JsonProperty("listChanged") Boolean listChanged) { } @@ -581,6 +589,7 @@ public record ResourceCapabilities(@JsonProperty("subscribe") Boolean subscribe, * the tool list */ @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) public record ToolCapabilities(@JsonProperty("listChanged") Boolean listChanged) { } diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index 942e0a6e2..a156ea81b 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -1760,4 +1760,75 @@ void testProgressNotificationWithoutMessage() throws Exception { {"progressToken":"progress-token-789","progress":0.25}""")); } + // Capability sub-records: unknown fields should be silently ignored (#766) + + @Test + void testSamplingIgnoresUnknownFields() throws Exception { + McpSchema.ClientCapabilities.Sampling sampling = JSON_MAPPER.readValue(""" + {"futureField": true}""", McpSchema.ClientCapabilities.Sampling.class); + assertThat(sampling).isNotNull(); + } + + @Test + void testElicitationIgnoresUnknownFields() throws Exception { + McpSchema.ClientCapabilities.Elicitation elicitation = JSON_MAPPER.readValue(""" + {"form": {}, "url": {}, "futureField": "value"}""", McpSchema.ClientCapabilities.Elicitation.class); + assertThat(elicitation).isNotNull(); + } + + @Test + void testElicitationFormIgnoresUnknownFields() throws Exception { + McpSchema.ClientCapabilities.Elicitation.Form form = JSON_MAPPER.readValue(""" + {"futureField": 42}""", McpSchema.ClientCapabilities.Elicitation.Form.class); + assertThat(form).isNotNull(); + } + + @Test + void testElicitationUrlIgnoresUnknownFields() throws Exception { + McpSchema.ClientCapabilities.Elicitation.Url url = JSON_MAPPER.readValue(""" + {"futureField": "value"}""", McpSchema.ClientCapabilities.Elicitation.Url.class); + assertThat(url).isNotNull(); + } + + @Test + void testCompletionCapabilitiesIgnoresUnknownFields() throws Exception { + McpSchema.ServerCapabilities.CompletionCapabilities completions = JSON_MAPPER.readValue(""" + {"futureField": true}""", McpSchema.ServerCapabilities.CompletionCapabilities.class); + assertThat(completions).isNotNull(); + } + + @Test + void testLoggingCapabilitiesIgnoresUnknownFields() throws Exception { + McpSchema.ServerCapabilities.LoggingCapabilities logging = JSON_MAPPER.readValue(""" + {"futureField": "value"}""", McpSchema.ServerCapabilities.LoggingCapabilities.class); + assertThat(logging).isNotNull(); + } + + @Test + void testPromptCapabilitiesIgnoresUnknownFields() throws Exception { + McpSchema.ServerCapabilities.PromptCapabilities prompts = JSON_MAPPER.readValue(""" + {"listChanged": true, "futureField": "value"}""", + McpSchema.ServerCapabilities.PromptCapabilities.class); + assertThat(prompts).isNotNull(); + assertThat(prompts.listChanged()).isTrue(); + } + + @Test + void testResourceCapabilitiesIgnoresUnknownFields() throws Exception { + McpSchema.ServerCapabilities.ResourceCapabilities resources = JSON_MAPPER.readValue(""" + {"subscribe": true, "listChanged": false, "futureField": 123}""", + McpSchema.ServerCapabilities.ResourceCapabilities.class); + assertThat(resources).isNotNull(); + assertThat(resources.subscribe()).isTrue(); + assertThat(resources.listChanged()).isFalse(); + } + + @Test + void testToolCapabilitiesIgnoresUnknownFields() throws Exception { + McpSchema.ServerCapabilities.ToolCapabilities tools = JSON_MAPPER.readValue(""" + {"listChanged": true, "futureField": "value"}""", McpSchema.ServerCapabilities.ToolCapabilities.class); + assertThat(tools).isNotNull(); + assertThat(tools.listChanged()).isTrue(); + } + }