From ceb2348e990ed01e7ac40acc93efe922b85040d1 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 13 Dec 2025 11:10:30 +0100 Subject: [PATCH 1/4] fix modelcontextprotocol/java-sdk#724 --- .../modelcontextprotocol/spec/McpSchema.java | 1 + .../spec/McpSchemaTests.java | 20 +++++++++++++++++++ 2 files changed, 21 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 734cff237..ac075ba54 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -418,6 +418,7 @@ public record Sampling() { * data from users with optional JSON schemas to validate responses. */ @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) public record Elicitation() { } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index 0926eebae..9f8b6fd9f 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -10,6 +10,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -353,6 +354,25 @@ void testInitializeResult() throws Exception { {"protocolVersion":"2024-11-05","capabilities":{"logging":{},"prompts":{"listChanged":true},"resources":{"subscribe":true,"listChanged":true},"tools":{"listChanged":true}},"serverInfo":{"name":"test-server","version":"1.0.0"},"instructions":"Server initialized successfully"}""")); } + @Test + // see https://github.com/modelcontextprotocol/java-sdk/issues/724 + void testParseInitializeRequest() throws IOException { + String serialized = """ + {"protocolVersion":"2024-11-05","capabilities":{"elicitation":{"form":{}}},"clientInfo":{"name":"test-client","version":"1.0.0"},"_meta":{"metaKey":"metaValue"}} + """; + + McpSchema.InitializeRequest deserialized = JSON_MAPPER.readValue(serialized, McpSchema.InitializeRequest.class); + + McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder() + .elicitation() + .build(); + McpSchema.Implementation clientInfo = new McpSchema.Implementation("test-client", "1.0.0"); + Map meta = Map.of("metaKey", "metaValue"); + McpSchema.InitializeRequest expected = new McpSchema.InitializeRequest(ProtocolVersions.MCP_2024_11_05, + capabilities, clientInfo, meta); + assertThat(deserialized).isEqualTo(expected); + } + // Resource Tests @Test From 0c7dadc8e0a2dc89ace520d1e54ed9e718ea5ed0 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Sat, 13 Dec 2025 11:14:00 +0100 Subject: [PATCH 2/4] ran formatter --- .../java/io/modelcontextprotocol/spec/McpSchemaTests.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index 9f8b6fd9f..a6a9868ca 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -363,9 +363,7 @@ void testParseInitializeRequest() throws IOException { McpSchema.InitializeRequest deserialized = JSON_MAPPER.readValue(serialized, McpSchema.InitializeRequest.class); - McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder() - .elicitation() - .build(); + McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder().elicitation().build(); McpSchema.Implementation clientInfo = new McpSchema.Implementation("test-client", "1.0.0"); Map meta = Map.of("metaKey", "metaValue"); McpSchema.InitializeRequest expected = new McpSchema.InitializeRequest(ProtocolVersions.MCP_2024_11_05, From 27446049726c42457034f1597b65e28ef3b56a71 Mon Sep 17 00:00:00 2001 From: Sebastian Stenzel Date: Thu, 18 Dec 2025 12:24:04 +0100 Subject: [PATCH 3/4] also ignore unknown properties in `capabilities.sampling` --- .../main/java/io/modelcontextprotocol/spec/McpSchema.java | 1 + .../java/io/modelcontextprotocol/spec/McpSchemaTests.java | 7 +++++-- 2 files changed, 6 insertions(+), 2 deletions(-) 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 ac075ba54..dd9888ff7 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -407,6 +407,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() { } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index a6a9868ca..574410e56 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -358,12 +358,15 @@ void testInitializeResult() throws Exception { // see https://github.com/modelcontextprotocol/java-sdk/issues/724 void testParseInitializeRequest() throws IOException { String serialized = """ - {"protocolVersion":"2024-11-05","capabilities":{"elicitation":{"form":{}}},"clientInfo":{"name":"test-client","version":"1.0.0"},"_meta":{"metaKey":"metaValue"}} + {"protocolVersion":"2024-11-05","capabilities":{"elicitation":{"form":{}},"sampling":{"tools": {}}},"clientInfo":{"name":"test-client","version":"1.0.0"},"_meta":{"metaKey":"metaValue"}} """; McpSchema.InitializeRequest deserialized = JSON_MAPPER.readValue(serialized, McpSchema.InitializeRequest.class); - McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder().elicitation().build(); + McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder() + .elicitation() + .sampling() + .build(); McpSchema.Implementation clientInfo = new McpSchema.Implementation("test-client", "1.0.0"); Map meta = Map.of("metaKey", "metaValue"); McpSchema.InitializeRequest expected = new McpSchema.InitializeRequest(ProtocolVersions.MCP_2024_11_05, From 806048ebeb9941de3bff8e6c1bf67bb7cddd0392 Mon Sep 17 00:00:00 2001 From: Rohit Date: Fri, 19 Dec 2025 21:55:23 -0800 Subject: [PATCH 4/4] fix: Support form and url fields in Elicitation capability per 2025-11-25 spec Update the ClientCapabilities.Elicitation record to accept optional "form" and "url" fields as defined in the MCP 2025-11-25 specification. Previously, deserializing an InitializeRequest with `{"capabilities":{"elicitation":{"form":{}}}}` would fail with UnrecognizedPropertyException because the Elicitation record was empty. Changes: - Add nested Form and Url marker records to Elicitation - Add no-arg constructor for backward compatibility (serializes to {}) - Add elicitation(boolean form, boolean url) builder method - Add comprehensive tests for deserialization and serialization Fixes #724 --- .../modelcontextprotocol/spec/McpSchema.java | 59 +++++++++- .../spec/McpSchemaTests.java | 104 +++++++++++++++++- 2 files changed, 161 insertions(+), 2 deletions(-) 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 dd9888ff7..bde97a57e 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java @@ -417,10 +417,50 @@ public record Sampling() { * maintain control over user interactions and data sharing while enabling servers * to gather necessary information dynamically. Servers can request structured * data from users with optional JSON schemas to validate responses. + * + *

+ * Per the 2025-11-25 spec, clients can declare support for specific elicitation + * modes: + *

    + *
  • {@code form} - In-band structured data collection with optional schema + * validation
  • + *
  • {@code url} - Out-of-band interaction via URL navigation
  • + *
+ * + *

+ * For backward compatibility, an empty elicitation object {@code {}} is + * equivalent to declaring support for form mode only. + * + * @param form support for in-band form-based elicitation + * @param url support for out-of-band URL-based elicitation */ @JsonInclude(JsonInclude.Include.NON_ABSENT) @JsonIgnoreProperties(ignoreUnknown = true) - public record Elicitation() { + 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() { + } + + /** + * Marker record indicating support for URL-based elicitation mode. + */ + @JsonInclude(JsonInclude.Include.NON_ABSENT) + @JsonIgnoreProperties(ignoreUnknown = true) + public record Url() { + } + + /** + * Creates an Elicitation with default settings (backward compatible, produces + * empty JSON object). + */ + public Elicitation() { + this(null, null); + } } public static Builder builder() { @@ -452,11 +492,28 @@ public Builder sampling() { return this; } + /** + * Enables elicitation capability with default settings (backward compatible, + * produces empty JSON object). + * @return this builder + */ public Builder elicitation() { this.elicitation = new Elicitation(); return this; } + /** + * Enables elicitation capability with explicit form and/or url mode support. + * @param form whether to support form-based elicitation + * @param url whether to support URL-based elicitation + * @return this builder + */ + public Builder elicitation(boolean form, boolean url) { + this.elicitation = new Elicitation(form ? new Elicitation.Form() : null, + url ? new Elicitation.Url() : null); + return this; + } + public ClientCapabilities build() { return new ClientCapabilities(experimental, roots, sampling, elicitation); } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java index 574410e56..a7efd976f 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java @@ -363,8 +363,9 @@ void testParseInitializeRequest() throws IOException { McpSchema.InitializeRequest deserialized = JSON_MAPPER.readValue(serialized, McpSchema.InitializeRequest.class); + // The JSON includes form:{} so we need to use elicitation(true, false) to match McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder() - .elicitation() + .elicitation(true, false) .sampling() .build(); McpSchema.Implementation clientInfo = new McpSchema.Implementation("test-client", "1.0.0"); @@ -1639,6 +1640,107 @@ void testListRootsResult() throws Exception { } + // Elicitation Capability Tests (Issue #724) + + @Test + void testElicitationCapabilityWithFormField() throws Exception { + // Test that elicitation with "form" field can be deserialized (2025-11-25 spec) + String json = """ + {"protocolVersion":"2024-11-05","capabilities":{"elicitation":{"form":{}}},"clientInfo":{"name":"test-client","version":"1.0.0"}} + """; + + McpSchema.InitializeRequest request = JSON_MAPPER.readValue(json, McpSchema.InitializeRequest.class); + + assertThat(request).isNotNull(); + assertThat(request.capabilities()).isNotNull(); + assertThat(request.capabilities().elicitation()).isNotNull(); + } + + @Test + void testElicitationCapabilityWithFormAndUrlFields() throws Exception { + // Test that elicitation with both "form" and "url" fields can be deserialized + String json = """ + {"protocolVersion":"2024-11-05","capabilities":{"elicitation":{"form":{},"url":{}}},"clientInfo":{"name":"test-client","version":"1.0.0"}} + """; + + McpSchema.InitializeRequest request = JSON_MAPPER.readValue(json, McpSchema.InitializeRequest.class); + + assertThat(request).isNotNull(); + assertThat(request.capabilities()).isNotNull(); + assertThat(request.capabilities().elicitation()).isNotNull(); + } + + @Test + void testElicitationCapabilityBackwardCompatibilityEmptyObject() throws Exception { + // Test backward compatibility: empty elicitation {} should still work + String json = """ + {"protocolVersion":"2024-11-05","capabilities":{"elicitation":{}},"clientInfo":{"name":"test-client","version":"1.0.0"}} + """; + + McpSchema.InitializeRequest request = JSON_MAPPER.readValue(json, McpSchema.InitializeRequest.class); + + assertThat(request).isNotNull(); + assertThat(request.capabilities()).isNotNull(); + assertThat(request.capabilities().elicitation()).isNotNull(); + } + + @Test + void testElicitationCapabilityBuilderBackwardCompatibility() throws Exception { + // Test that the existing builder API still works and produces valid JSON + McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder().elicitation().build(); + + assertThat(capabilities.elicitation()).isNotNull(); + + // Serialize and verify it produces valid JSON (should be {} for backward compat) + String json = JSON_MAPPER.writeValueAsString(capabilities); + assertThat(json).contains("\"elicitation\""); + } + + @Test + void testElicitationCapabilitySerializationRoundTrip() throws Exception { + // Test that serialization and deserialization round-trip works + McpSchema.ClientCapabilities original = McpSchema.ClientCapabilities.builder().elicitation().build(); + + String json = JSON_MAPPER.writeValueAsString(original); + McpSchema.ClientCapabilities deserialized = JSON_MAPPER.readValue(json, McpSchema.ClientCapabilities.class); + + assertThat(deserialized.elicitation()).isNotNull(); + } + + @Test + void testElicitationCapabilityBuilderWithFormAndUrl() throws Exception { + // Test the new builder method that explicitly sets form and url support + McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder() + .elicitation(true, true) + .build(); + + assertThat(capabilities.elicitation()).isNotNull(); + assertThat(capabilities.elicitation().form()).isNotNull(); + assertThat(capabilities.elicitation().url()).isNotNull(); + + // Verify serialization produces the expected JSON + String json = JSON_MAPPER.writeValueAsString(capabilities); + assertThatJson(json).when(Option.IGNORING_ARRAY_ORDER).isObject().containsKey("elicitation"); + assertThat(json).contains("\"form\""); + assertThat(json).contains("\"url\""); + } + + @Test + void testElicitationCapabilityBuilderFormOnly() throws Exception { + // Test builder with form only + McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder() + .elicitation(true, false) + .build(); + + assertThat(capabilities.elicitation()).isNotNull(); + assertThat(capabilities.elicitation().form()).isNotNull(); + assertThat(capabilities.elicitation().url()).isNull(); + + String json = JSON_MAPPER.writeValueAsString(capabilities); + assertThat(json).contains("\"form\""); + assertThat(json).doesNotContain("\"url\""); + } + // Progress Notification Tests @Test