fix: Support form and url fields in Elicitation capability per 2025-11-25 spec (#731)
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
This commit is contained in:
parent
fa9dac88c5
commit
e7901da010
2 changed files with 157 additions and 1 deletions
|
|
@ -416,9 +416,47 @@ public final class McpSchema {
|
||||||
* maintain control over user interactions and data sharing while enabling servers
|
* maintain control over user interactions and data sharing while enabling servers
|
||||||
* to gather necessary information dynamically. Servers can request structured
|
* to gather necessary information dynamically. Servers can request structured
|
||||||
* data from users with optional JSON schemas to validate responses.
|
* data from users with optional JSON schemas to validate responses.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* Per the 2025-11-25 spec, clients can declare support for specific elicitation
|
||||||
|
* modes:
|
||||||
|
* <ul>
|
||||||
|
* <li>{@code form} - In-band structured data collection with optional schema
|
||||||
|
* validation</li>
|
||||||
|
* <li>{@code url} - Out-of-band interaction via URL navigation</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* 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)
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
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)
|
||||||
|
public record Form() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marker record indicating support for URL-based elicitation mode.
|
||||||
|
*/
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_ABSENT)
|
||||||
|
public record Url() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Elicitation with default settings (backward compatible, produces
|
||||||
|
* empty JSON object).
|
||||||
|
*/
|
||||||
|
public Elicitation() {
|
||||||
|
this(null, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Builder builder() {
|
public static Builder builder() {
|
||||||
|
|
@ -450,11 +488,28 @@ public final class McpSchema {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables elicitation capability with default settings (backward compatible,
|
||||||
|
* produces empty JSON object).
|
||||||
|
* @return this builder
|
||||||
|
*/
|
||||||
public Builder elicitation() {
|
public Builder elicitation() {
|
||||||
this.elicitation = new Elicitation();
|
this.elicitation = new Elicitation();
|
||||||
return this;
|
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() {
|
public ClientCapabilities build() {
|
||||||
return new ClientCapabilities(experimental, roots, sampling, elicitation);
|
return new ClientCapabilities(experimental, roots, sampling, elicitation);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1618,6 +1618,107 @@ public class McpSchemaTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Progress Notification Tests
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue