Skip to content

Commit df05e73

Browse files
committed
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 fa9dac8 commit df05e73

File tree

2 files changed

+157
-1
lines changed

2 files changed

+157
-1
lines changed

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

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

424462
public static Builder builder() {
@@ -450,11 +488,28 @@ public Builder sampling() {
450488
return this;
451489
}
452490

491+
/**
492+
* Enables elicitation capability with default settings (backward compatible,
493+
* produces empty JSON object).
494+
* @return this builder
495+
*/
453496
public Builder elicitation() {
454497
this.elicitation = new Elicitation();
455498
return this;
456499
}
457500

501+
/**
502+
* Enables elicitation capability with explicit form and/or url mode support.
503+
* @param form whether to support form-based elicitation
504+
* @param url whether to support URL-based elicitation
505+
* @return this builder
506+
*/
507+
public Builder elicitation(boolean form, boolean url) {
508+
this.elicitation = new Elicitation(form ? new Elicitation.Form() : null,
509+
url ? new Elicitation.Url() : null);
510+
return this;
511+
}
512+
458513
public ClientCapabilities build() {
459514
return new ClientCapabilities(experimental, roots, sampling, elicitation);
460515
}

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

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1618,6 +1618,107 @@ void testListRootsResult() throws Exception {
16181618

16191619
}
16201620

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

16231724
@Test

0 commit comments

Comments
 (0)