Skip to content

Commit 806048e

Browse files
rhtnroverheadhunter
authored andcommitted
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
1 parent 2744604 commit 806048e

File tree

2 files changed

+161
-2
lines changed

2 files changed

+161
-2
lines changed

mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,10 +417,50 @@ public record Sampling() {
417417
* maintain control over user interactions and data sharing while enabling servers
418418
* to gather necessary information dynamically. Servers can request structured
419419
* data from users with optional JSON schemas to validate responses.
420+
*
421+
* <p>
422+
* Per the 2025-11-25 spec, clients can declare support for specific elicitation
423+
* modes:
424+
* <ul>
425+
* <li>{@code form} - In-band structured data collection with optional schema
426+
* validation</li>
427+
* <li>{@code url} - Out-of-band interaction via URL navigation</li>
428+
* </ul>
429+
*
430+
* <p>
431+
* For backward compatibility, an empty elicitation object {@code {}} is
432+
* equivalent to declaring support for form mode only.
433+
*
434+
* @param form support for in-band form-based elicitation
435+
* @param url support for out-of-band URL-based elicitation
420436
*/
421437
@JsonInclude(JsonInclude.Include.NON_ABSENT)
422438
@JsonIgnoreProperties(ignoreUnknown = true)
423-
public record Elicitation() {
439+
public record Elicitation(@JsonProperty("form") Form form, @JsonProperty("url") Url url) {
440+
441+
/**
442+
* Marker record indicating support for form-based elicitation mode.
443+
*/
444+
@JsonInclude(JsonInclude.Include.NON_ABSENT)
445+
@JsonIgnoreProperties(ignoreUnknown = true)
446+
public record Form() {
447+
}
448+
449+
/**
450+
* Marker record indicating support for URL-based elicitation mode.
451+
*/
452+
@JsonInclude(JsonInclude.Include.NON_ABSENT)
453+
@JsonIgnoreProperties(ignoreUnknown = true)
454+
public record Url() {
455+
}
456+
457+
/**
458+
* Creates an Elicitation with default settings (backward compatible, produces
459+
* empty JSON object).
460+
*/
461+
public Elicitation() {
462+
this(null, null);
463+
}
424464
}
425465

426466
public static Builder builder() {
@@ -452,11 +492,28 @@ public Builder sampling() {
452492
return this;
453493
}
454494

495+
/**
496+
* Enables elicitation capability with default settings (backward compatible,
497+
* produces empty JSON object).
498+
* @return this builder
499+
*/
455500
public Builder elicitation() {
456501
this.elicitation = new Elicitation();
457502
return this;
458503
}
459504

505+
/**
506+
* Enables elicitation capability with explicit form and/or url mode support.
507+
* @param form whether to support form-based elicitation
508+
* @param url whether to support URL-based elicitation
509+
* @return this builder
510+
*/
511+
public Builder elicitation(boolean form, boolean url) {
512+
this.elicitation = new Elicitation(form ? new Elicitation.Form() : null,
513+
url ? new Elicitation.Url() : null);
514+
return this;
515+
}
516+
460517
public ClientCapabilities build() {
461518
return new ClientCapabilities(experimental, roots, sampling, elicitation);
462519
}

mcp-core/src/test/java/io/modelcontextprotocol/spec/McpSchemaTests.java

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,8 +363,9 @@ void testParseInitializeRequest() throws IOException {
363363

364364
McpSchema.InitializeRequest deserialized = JSON_MAPPER.readValue(serialized, McpSchema.InitializeRequest.class);
365365

366+
// The JSON includes form:{} so we need to use elicitation(true, false) to match
366367
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder()
367-
.elicitation()
368+
.elicitation(true, false)
368369
.sampling()
369370
.build();
370371
McpSchema.Implementation clientInfo = new McpSchema.Implementation("test-client", "1.0.0");
@@ -1639,6 +1640,107 @@ void testListRootsResult() throws Exception {
16391640

16401641
}
16411642

1643+
// Elicitation Capability Tests (Issue #724)
1644+
1645+
@Test
1646+
void testElicitationCapabilityWithFormField() throws Exception {
1647+
// Test that elicitation with "form" field can be deserialized (2025-11-25 spec)
1648+
String json = """
1649+
{"protocolVersion":"2024-11-05","capabilities":{"elicitation":{"form":{}}},"clientInfo":{"name":"test-client","version":"1.0.0"}}
1650+
""";
1651+
1652+
McpSchema.InitializeRequest request = JSON_MAPPER.readValue(json, McpSchema.InitializeRequest.class);
1653+
1654+
assertThat(request).isNotNull();
1655+
assertThat(request.capabilities()).isNotNull();
1656+
assertThat(request.capabilities().elicitation()).isNotNull();
1657+
}
1658+
1659+
@Test
1660+
void testElicitationCapabilityWithFormAndUrlFields() throws Exception {
1661+
// Test that elicitation with both "form" and "url" fields can be deserialized
1662+
String json = """
1663+
{"protocolVersion":"2024-11-05","capabilities":{"elicitation":{"form":{},"url":{}}},"clientInfo":{"name":"test-client","version":"1.0.0"}}
1664+
""";
1665+
1666+
McpSchema.InitializeRequest request = JSON_MAPPER.readValue(json, McpSchema.InitializeRequest.class);
1667+
1668+
assertThat(request).isNotNull();
1669+
assertThat(request.capabilities()).isNotNull();
1670+
assertThat(request.capabilities().elicitation()).isNotNull();
1671+
}
1672+
1673+
@Test
1674+
void testElicitationCapabilityBackwardCompatibilityEmptyObject() throws Exception {
1675+
// Test backward compatibility: empty elicitation {} should still work
1676+
String json = """
1677+
{"protocolVersion":"2024-11-05","capabilities":{"elicitation":{}},"clientInfo":{"name":"test-client","version":"1.0.0"}}
1678+
""";
1679+
1680+
McpSchema.InitializeRequest request = JSON_MAPPER.readValue(json, McpSchema.InitializeRequest.class);
1681+
1682+
assertThat(request).isNotNull();
1683+
assertThat(request.capabilities()).isNotNull();
1684+
assertThat(request.capabilities().elicitation()).isNotNull();
1685+
}
1686+
1687+
@Test
1688+
void testElicitationCapabilityBuilderBackwardCompatibility() throws Exception {
1689+
// Test that the existing builder API still works and produces valid JSON
1690+
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder().elicitation().build();
1691+
1692+
assertThat(capabilities.elicitation()).isNotNull();
1693+
1694+
// Serialize and verify it produces valid JSON (should be {} for backward compat)
1695+
String json = JSON_MAPPER.writeValueAsString(capabilities);
1696+
assertThat(json).contains("\"elicitation\"");
1697+
}
1698+
1699+
@Test
1700+
void testElicitationCapabilitySerializationRoundTrip() throws Exception {
1701+
// Test that serialization and deserialization round-trip works
1702+
McpSchema.ClientCapabilities original = McpSchema.ClientCapabilities.builder().elicitation().build();
1703+
1704+
String json = JSON_MAPPER.writeValueAsString(original);
1705+
McpSchema.ClientCapabilities deserialized = JSON_MAPPER.readValue(json, McpSchema.ClientCapabilities.class);
1706+
1707+
assertThat(deserialized.elicitation()).isNotNull();
1708+
}
1709+
1710+
@Test
1711+
void testElicitationCapabilityBuilderWithFormAndUrl() throws Exception {
1712+
// Test the new builder method that explicitly sets form and url support
1713+
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder()
1714+
.elicitation(true, true)
1715+
.build();
1716+
1717+
assertThat(capabilities.elicitation()).isNotNull();
1718+
assertThat(capabilities.elicitation().form()).isNotNull();
1719+
assertThat(capabilities.elicitation().url()).isNotNull();
1720+
1721+
// Verify serialization produces the expected JSON
1722+
String json = JSON_MAPPER.writeValueAsString(capabilities);
1723+
assertThatJson(json).when(Option.IGNORING_ARRAY_ORDER).isObject().containsKey("elicitation");
1724+
assertThat(json).contains("\"form\"");
1725+
assertThat(json).contains("\"url\"");
1726+
}
1727+
1728+
@Test
1729+
void testElicitationCapabilityBuilderFormOnly() throws Exception {
1730+
// Test builder with form only
1731+
McpSchema.ClientCapabilities capabilities = McpSchema.ClientCapabilities.builder()
1732+
.elicitation(true, false)
1733+
.build();
1734+
1735+
assertThat(capabilities.elicitation()).isNotNull();
1736+
assertThat(capabilities.elicitation().form()).isNotNull();
1737+
assertThat(capabilities.elicitation().url()).isNull();
1738+
1739+
String json = JSON_MAPPER.writeValueAsString(capabilities);
1740+
assertThat(json).contains("\"form\"");
1741+
assertThat(json).doesNotContain("\"url\"");
1742+
}
1743+
16421744
// Progress Notification Tests
16431745

16441746
@Test

0 commit comments

Comments
 (0)