Skip to content

Commit 58680c8

Browse files
authored
Merge branch 'main' into fix/fix_close_wait
2 parents 872d5ce + 0a8cb1e commit 58680c8

File tree

58 files changed

+2845
-187
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

58 files changed

+2845
-187
lines changed

mcp-core/pom.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@
8080
<dependency>
8181
<groupId>com.fasterxml.jackson.core</groupId>
8282
<artifactId>jackson-annotations</artifactId>
83-
<version>${jackson.version}</version>
83+
<version>${jackson-annotations.version}</version>
8484
</dependency>
8585

8686
<dependency>
@@ -100,7 +100,7 @@
100100
<!-- Test dependencies -->
101101
<dependency>
102102
<groupId>io.modelcontextprotocol.sdk</groupId>
103-
<artifactId>mcp-json-jackson2</artifactId>
103+
<artifactId>mcp-json-jackson3</artifactId>
104104
<version>0.18.0-SNAPSHOT</version>
105105
<scope>test</scope>
106106
</dependency>

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/client/transport/HttpClientStreamableHttpTransport.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -491,7 +491,9 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage sentMessage) {
491491
.firstValue(HttpHeaders.CONTENT_LENGTH)
492492
.orElse(null);
493493

494-
if (contentType.isBlank() || "0".equals(contentLength)) {
494+
// For empty content or HTTP code 202 (ACCEPTED), assume success
495+
if (contentType.isBlank() || "0".equals(contentLength) || statusCode == 202) {
496+
// if (contentType.isBlank() || "0".equals(contentLength)) {
495497
logger.debug("No body returned for POST in session {}", sessionRepresentation);
496498
// No content type means no response body, so we can just
497499
// return an empty stream

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/AbstractMcpAsyncClientResiliencyTests.java

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,9 @@ public abstract class AbstractMcpAsyncClientResiliencyTests {
4848
static Network network = Network.newNetwork();
4949
static String host = "http://localhost:3001";
5050

51-
// Uses the https://github.com/tzolov/mcp-everything-server-docker-image
5251
@SuppressWarnings("resource")
53-
static GenericContainer<?> container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v3")
54-
.withCommand("node dist/index.js streamableHttp")
52+
static GenericContainer<?> container = new GenericContainer<>("docker.io/node:lts-alpine3.23")
53+
.withCommand("npx -y @modelcontextprotocol/server-everything@2025.12.18 streamableHttp")
5554
.withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
5655
.withNetwork(network)
5756
.withNetworkAliases("everything-server")

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

Lines changed: 50 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ protected Duration getRequestTimeout() {
7272
}
7373

7474
protected Duration getInitializationTimeout() {
75-
return Duration.ofSeconds(2);
75+
return Duration.ofSeconds(20);
7676
}
7777

7878
McpAsyncClient client(McpClientTransport transport) {
@@ -503,57 +503,64 @@ void testRemoveNonExistentRoot() {
503503

504504
@Test
505505
void testReadResource() {
506+
AtomicInteger resourceCount = new AtomicInteger();
506507
withClient(createMcpTransport(), client -> {
507508
Flux<McpSchema.ReadResourceResult> resources = client.initialize()
508509
.then(client.listResources(null))
509-
.flatMapMany(r -> Flux.fromIterable(r.resources()))
510+
.flatMapMany(r -> {
511+
List<Resource> l = r.resources();
512+
resourceCount.set(l.size());
513+
return Flux.fromIterable(l);
514+
})
510515
.flatMap(r -> client.readResource(r));
511516

512-
StepVerifier.create(resources).recordWith(ArrayList::new).consumeRecordedWith(readResourceResults -> {
513-
514-
for (ReadResourceResult result : readResourceResults) {
515-
516-
assertThat(result).isNotNull();
517-
assertThat(result.contents()).isNotNull().isNotEmpty();
518-
519-
// Validate each content item
520-
for (ResourceContents content : result.contents()) {
521-
assertThat(content).isNotNull();
522-
assertThat(content.uri()).isNotNull().isNotEmpty();
523-
assertThat(content.mimeType()).isNotNull().isNotEmpty();
524-
525-
// Validate content based on its type with more comprehensive
526-
// checks
527-
switch (content.mimeType()) {
528-
case "text/plain" -> {
529-
TextResourceContents textContent = assertInstanceOf(TextResourceContents.class,
530-
content);
531-
assertThat(textContent.text()).isNotNull().isNotEmpty();
532-
assertThat(textContent.uri()).isNotEmpty();
533-
}
534-
case "application/octet-stream" -> {
535-
BlobResourceContents blobContent = assertInstanceOf(BlobResourceContents.class,
536-
content);
537-
assertThat(blobContent.blob()).isNotNull().isNotEmpty();
538-
assertThat(blobContent.uri()).isNotNull().isNotEmpty();
539-
// Validate base64 encoding format
540-
assertThat(blobContent.blob()).matches("^[A-Za-z0-9+/]*={0,2}$");
541-
}
542-
default -> {
543-
544-
// Still validate basic properties
545-
if (content instanceof TextResourceContents textContent) {
546-
assertThat(textContent.text()).isNotNull();
517+
StepVerifier.create(resources)
518+
.recordWith(ArrayList::new)
519+
.thenConsumeWhile(res -> true)
520+
.consumeRecordedWith(readResourceResults -> {
521+
assertThat(readResourceResults.size()).isEqualTo(resourceCount.get());
522+
for (ReadResourceResult result : readResourceResults) {
523+
524+
assertThat(result).isNotNull();
525+
assertThat(result.contents()).isNotNull().isNotEmpty();
526+
527+
// Validate each content item
528+
for (ResourceContents content : result.contents()) {
529+
assertThat(content).isNotNull();
530+
assertThat(content.uri()).isNotNull().isNotEmpty();
531+
assertThat(content.mimeType()).isNotNull().isNotEmpty();
532+
533+
// Validate content based on its type with more comprehensive
534+
// checks
535+
switch (content.mimeType()) {
536+
case "text/plain" -> {
537+
TextResourceContents textContent = assertInstanceOf(TextResourceContents.class,
538+
content);
539+
assertThat(textContent.text()).isNotNull().isNotEmpty();
540+
assertThat(textContent.uri()).isNotEmpty();
541+
}
542+
case "application/octet-stream" -> {
543+
BlobResourceContents blobContent = assertInstanceOf(BlobResourceContents.class,
544+
content);
545+
assertThat(blobContent.blob()).isNotNull().isNotEmpty();
546+
assertThat(blobContent.uri()).isNotNull().isNotEmpty();
547+
// Validate base64 encoding format
548+
assertThat(blobContent.blob()).matches("^[A-Za-z0-9+/]*={0,2}$");
547549
}
548-
else if (content instanceof BlobResourceContents blobContent) {
549-
assertThat(blobContent.blob()).isNotNull();
550+
default -> {
551+
552+
// Still validate basic properties
553+
if (content instanceof TextResourceContents textContent) {
554+
assertThat(textContent.text()).isNotNull();
555+
}
556+
else if (content instanceof BlobResourceContents blobContent) {
557+
assertThat(blobContent.blob()).isNotNull();
558+
}
550559
}
551560
}
552561
}
553562
}
554-
}
555-
})
556-
.expectNextCount(10) // Expect 10 elements
563+
})
557564
.verifyComplete();
558565
});
559566
}
@@ -693,7 +700,6 @@ void testInitializeWithAllCapabilities() {
693700
assertThat(result.capabilities()).isNotNull();
694701
}).verifyComplete());
695702
}
696-
697703
// ---------------------------------------
698704
// Logging Tests
699705
// ---------------------------------------
@@ -773,7 +779,7 @@ void testSampling() {
773779
if (!(content instanceof McpSchema.TextContent text))
774780
return;
775781

776-
assertThat(text.text()).endsWith(response); // Prefixed
782+
assertThat(text.text()).contains(response);
777783
});
778784

779785
// Verify sampling request parameters received in our callback

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

Lines changed: 4 additions & 2 deletions
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(() -> {
@@ -623,7 +625,7 @@ void testSampling() {
623625
if (!(content instanceof McpSchema.TextContent text))
624626
return;
625627

626-
assertThat(text.text()).endsWith(response); // Prefixed
628+
assertThat(text.text()).contains(response);
627629
});
628630

629631
// Verify sampling request parameters received in our callback

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,9 @@ public class HttpClientStreamableHttpAsyncClientTests extends AbstractMcpAsyncCl
1717

1818
private static String host = "http://localhost:3001";
1919

20-
// Uses the https://github.com/tzolov/mcp-everything-server-docker-image
2120
@SuppressWarnings("resource")
22-
static GenericContainer<?> container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v3")
23-
.withCommand("node dist/index.js streamableHttp")
21+
static GenericContainer<?> container = new GenericContainer<>("docker.io/node:lts-alpine3.23")
22+
.withCommand("npx -y @modelcontextprotocol/server-everything@2025.12.18 streamableHttp")
2423
.withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
2524
.withExposedPorts(3001)
2625
.waitingFor(Wait.forHttp("/").forStatusCode(404));

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,9 @@ public class HttpClientStreamableHttpSyncClientTests extends AbstractMcpSyncClie
3030

3131
static String host = "http://localhost:3001";
3232

33-
// Uses the https://github.com/tzolov/mcp-everything-server-docker-image
3433
@SuppressWarnings("resource")
35-
static GenericContainer<?> container = new GenericContainer<>("docker.io/tzolov/mcp-everything-server:v3")
36-
.withCommand("node dist/index.js streamableHttp")
34+
static GenericContainer<?> container = new GenericContainer<>("docker.io/node:lts-alpine3.23")
35+
.withCommand("npx -y @modelcontextprotocol/server-everything@2025.12.18 streamableHttp")
3736
.withLogConsumer(outputFrame -> System.out.println(outputFrame.getUtf8String()))
3837
.withExposedPorts(3001)
3938
.waitingFor(Wait.forHttp("/").forStatusCode(404));

0 commit comments

Comments
 (0)