Skip to content

Commit d622756

Browse files
authored
Merge branch 'main' into allow-extend-of-transports
2 parents 838ab0e + f7a460f commit d622756

File tree

7 files changed

+197
-14
lines changed

7 files changed

+197
-14
lines changed

mcp-core/src/main/java/io/modelcontextprotocol/client/McpClient.java

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,8 @@
44

55
package io.modelcontextprotocol.client;
66

7-
import java.time.Duration;
8-
import java.util.ArrayList;
9-
import java.util.HashMap;
10-
import java.util.List;
11-
import java.util.Map;
12-
import java.util.function.Consumer;
13-
import java.util.function.Function;
14-
import java.util.function.Supplier;
15-
16-
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
177
import io.modelcontextprotocol.common.McpTransportContext;
8+
import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
189
import io.modelcontextprotocol.spec.McpClientTransport;
1910
import io.modelcontextprotocol.spec.McpSchema;
2011
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
@@ -28,6 +19,15 @@
2819
import io.modelcontextprotocol.util.Assert;
2920
import reactor.core.publisher.Mono;
3021

22+
import java.time.Duration;
23+
import java.util.ArrayList;
24+
import java.util.HashMap;
25+
import java.util.List;
26+
import java.util.Map;
27+
import java.util.function.Consumer;
28+
import java.util.function.Function;
29+
import java.util.function.Supplier;
30+
3131
/**
3232
* Factory class for creating Model Context Protocol (MCP) clients. MCP is a protocol that
3333
* enables AI models to interact with external tools and resources through a standardized
@@ -75,6 +75,7 @@
7575
* .resourcesChangeConsumer(resources -> Mono.fromRunnable(() -> System.out.println("Resources updated: " + resources)))
7676
* .promptsChangeConsumer(prompts -> Mono.fromRunnable(() -> System.out.println("Prompts updated: " + prompts)))
7777
* .loggingConsumer(message -> Mono.fromRunnable(() -> System.out.println("Log message: " + message)))
78+
* .resourcesUpdateConsumer(resourceContents -> Mono.fromRunnable(() -> System.out.println("Resources contents updated: " + resourceContents)))
7879
* .build();
7980
* }</pre>
8081
*
@@ -346,6 +347,22 @@ public SyncSpec resourcesChangeConsumer(Consumer<List<McpSchema.Resource>> resou
346347
return this;
347348
}
348349

350+
/**
351+
* Adds a consumer to be notified when a specific resource is updated. This allows
352+
* the client to react to changes in individual resources, such as updates to
353+
* their content or metadata.
354+
* @param resourcesUpdateConsumer A consumer function that processes the updated
355+
* resource and returns a Mono indicating the completion of the processing. Must
356+
* not be null.
357+
* @return This builder instance for method chaining.
358+
* @throws IllegalArgumentException If the resourcesUpdateConsumer is null.
359+
*/
360+
public SyncSpec resourcesUpdateConsumer(Consumer<List<McpSchema.ResourceContents>> resourcesUpdateConsumer) {
361+
Assert.notNull(resourcesUpdateConsumer, "Resources update consumer must not be null");
362+
this.resourcesUpdateConsumers.add(resourcesUpdateConsumer);
363+
return this;
364+
}
365+
349366
/**
350367
* Adds a consumer to be notified when the available prompts change. This allows
351368
* the client to react to changes in the server's prompt templates, such as new

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/main/java/io/modelcontextprotocol/spec/ProtocolVersions.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,10 @@ public interface ProtocolVersions {
2020
*/
2121
String MCP_2025_06_18 = "2025-06-18";
2222

23+
/**
24+
* MCP protocol version for 2025-11-25.
25+
* https://modelcontextprotocol.io/specification/2025-11-25
26+
*/
27+
String MCP_2025_11_25 = "2025-11-25";
28+
2329
}

mcp-core/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,11 +536,13 @@ void testNotificationHandlers() {
536536
AtomicBoolean toolsNotificationReceived = new AtomicBoolean(false);
537537
AtomicBoolean resourcesNotificationReceived = new AtomicBoolean(false);
538538
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
539+
AtomicBoolean resourcesUpdatedNotificationReceived = new AtomicBoolean(false);
539540

540541
withClient(createMcpTransport(),
541542
builder -> builder.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
542543
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
543-
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true)),
544+
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true))
545+
.resourcesUpdateConsumer(resources -> resourcesUpdatedNotificationReceived.set(true)),
544546
client -> {
545547

546548
assertThatCode(() -> {

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

mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,11 +535,13 @@ void testNotificationHandlers() {
535535
AtomicBoolean toolsNotificationReceived = new AtomicBoolean(false);
536536
AtomicBoolean resourcesNotificationReceived = new AtomicBoolean(false);
537537
AtomicBoolean promptsNotificationReceived = new AtomicBoolean(false);
538+
AtomicBoolean resourcesUpdatedNotificationReceived = new AtomicBoolean(false);
538539

539540
withClient(createMcpTransport(),
540541
builder -> builder.toolsChangeConsumer(tools -> toolsNotificationReceived.set(true))
541542
.resourcesChangeConsumer(resources -> resourcesNotificationReceived.set(true))
542-
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true)),
543+
.promptsChangeConsumer(prompts -> promptsNotificationReceived.set(true))
544+
.resourcesUpdateConsumer(resources -> resourcesUpdatedNotificationReceived.set(true)),
543545
client -> {
544546

545547
assertThatCode(() -> {

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
<assert4j.version>3.27.6</assert4j.version>
6363
<junit.version>5.10.2</junit.version>
6464
<mockito.version>5.20.0</mockito.version>
65-
<testcontainers.version>1.20.4</testcontainers.version>
65+
<testcontainers.version>1.21.4</testcontainers.version>
6666
<byte-buddy.version>1.17.8</byte-buddy.version>
6767
<toxiproxy.version>1.21.0</toxiproxy.version>
6868

0 commit comments

Comments
 (0)