Skip to content

Commit 62d1552

Browse files
Merge branch 'modelcontextprotocol:main' into fix-notification-handler
2 parents ad9bc30 + aacc631 commit 62d1552

File tree

13 files changed

+604
-35
lines changed

13 files changed

+604
-35
lines changed

README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ This SDK enables Java applications to interact with AI models and tools through
77
## 📚 Reference Documentation
88

99
#### MCP Java SDK documentation
10-
For comprehensive guides and SDK API documentation, visit the [MCP Java SDK Reference Documentation](https://modelcontextprotocol.io/sdk/java/mcp-overview).
10+
For comprehensive guides and SDK API documentation
11+
12+
- [Features](https://modelcontextprotocol.io/sdk/java/mcp-overview#features) - Overview the features provided by the Java MCP SDK
13+
- [Acrchitecture](https://modelcontextprotocol.io/sdk/java/mcp-overview#architecture) - Java MCP SDK architecture overview.
14+
- [Java Dependencies / BOM](https://modelcontextprotocol.io/sdk/java/mcp-overview#dependencies) - Java dependencies and BOM.
15+
- [Java MCP Client](https://modelcontextprotocol.io/sdk/java/mcp-client) - Learn how to use the MCP client to interact with MCP servers.
16+
- [Java MCP Server](https://modelcontextprotocol.io/sdk/java/mcp-server) - Learn how to implement and configure a MCP servers.
1117

1218
#### Spring AI MCP documentation
1319
[Spring AI MCP](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-overview.html) extends the MCP Java SDK with Spring Boot integration, providing both [client](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-client-boot-starter-docs.html) and [server](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-server-boot-starter-docs.html) starters. Bootstrap your AI applications with MCP support using [Spring Initializer](https://start.spring.io).

mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/client/transport/WebClientStreamableHttpTransport.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -125,13 +125,14 @@ public Mono<Void> connect(Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchem
125125
}
126126

127127
private DefaultMcpTransportSession createTransportSession() {
128-
Supplier<Publisher<Void>> onClose = () -> {
129-
DefaultMcpTransportSession transportSession = this.activeSession.get();
130-
return transportSession.sessionId().isEmpty() ? Mono.empty()
131-
: webClient.delete().uri(this.endpoint).headers(httpHeaders -> {
132-
httpHeaders.add("mcp-session-id", transportSession.sessionId().get());
133-
}).retrieve().toBodilessEntity().doOnError(e -> logger.info("Got response {}", e)).then();
134-
};
128+
Function<String, Publisher<Void>> onClose = sessionId -> sessionId == null ? Mono.empty()
129+
: webClient.delete().uri(this.endpoint).headers(httpHeaders -> {
130+
httpHeaders.add("mcp-session-id", sessionId);
131+
})
132+
.retrieve()
133+
.toBodilessEntity()
134+
.doOnError(e -> logger.warn("Got error when closing transport", e))
135+
.then();
135136
return new DefaultMcpTransportSession(onClose);
136137
}
137138

@@ -192,6 +193,7 @@ private Mono<Disposable> reconnect(McpTransportStream<Disposable> stream) {
192193
})
193194
.exchangeToFlux(response -> {
194195
if (isEventStream(response)) {
196+
logger.debug("Established SSE stream via GET");
195197
return eventStream(stream, response);
196198
}
197199
else if (isNotAllowed(response)) {
@@ -208,6 +210,7 @@ else if (isNotFound(response)) {
208210
}).flux();
209211
}
210212
})
213+
.flatMap(jsonrpcMessage -> this.handler.get().apply(Mono.just(jsonrpcMessage)))
211214
.onErrorComplete(t -> {
212215
this.handleException(t);
213216
return true;
@@ -274,6 +277,7 @@ public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
274277
else {
275278
MediaType mediaType = contentType.get();
276279
if (mediaType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM)) {
280+
logger.debug("Established SSE stream via POST");
277281
// communicate to caller that the message was delivered
278282
sink.success();
279283
// starting a stream

mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/client/WebClientStreamableHttpSyncClientTests.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
package io.modelcontextprotocol.client;
22

3-
import com.fasterxml.jackson.databind.ObjectMapper;
43
import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport;
54
import io.modelcontextprotocol.spec.McpClientTransport;
65
import org.junit.jupiter.api.Timeout;
7-
import org.springframework.web.reactive.function.client.WebClient;
86
import org.testcontainers.containers.GenericContainer;
97
import org.testcontainers.containers.wait.strategy.Wait;
108

9+
import org.springframework.web.reactive.function.client.WebClient;
10+
1111
@Timeout(15)
1212
public class WebClientStreamableHttpSyncClientTests extends AbstractMcpSyncClientTests {
1313

mcp-test/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@
5454
<artifactId>junit-jupiter-api</artifactId>
5555
<version>${junit.version}</version>
5656
</dependency>
57+
<dependency>
58+
<groupId>org.junit.jupiter</groupId>
59+
<artifactId>junit-jupiter-params</artifactId>
60+
<version>${junit.version}</version>
61+
</dependency>
5762
<dependency>
5863
<groupId>org.mockito</groupId>
5964
<artifactId>mockito-core</artifactId>

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

Lines changed: 135 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.Map;
99
import java.util.Objects;
1010
import java.util.concurrent.atomic.AtomicBoolean;
11+
import java.util.concurrent.atomic.AtomicInteger;
1112
import java.util.concurrent.atomic.AtomicReference;
1213
import java.util.function.Consumer;
1314
import java.util.function.Function;
@@ -19,6 +20,8 @@
1920
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
2021
import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
2122
import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
23+
import io.modelcontextprotocol.spec.McpSchema.ElicitRequest;
24+
import io.modelcontextprotocol.spec.McpSchema.ElicitResult;
2225
import io.modelcontextprotocol.spec.McpSchema.GetPromptRequest;
2326
import io.modelcontextprotocol.spec.McpSchema.Prompt;
2427
import io.modelcontextprotocol.spec.McpSchema.Resource;
@@ -31,13 +34,17 @@
3134
import org.junit.jupiter.api.BeforeEach;
3235
import org.junit.jupiter.api.Disabled;
3336
import org.junit.jupiter.api.Test;
37+
import org.junit.jupiter.params.ParameterizedTest;
38+
import org.junit.jupiter.params.provider.ValueSource;
3439
import reactor.core.publisher.Flux;
3540
import reactor.core.publisher.Mono;
3641
import reactor.test.StepVerifier;
3742

3843
import static org.assertj.core.api.Assertions.assertThat;
3944
import static org.assertj.core.api.Assertions.assertThatCode;
4045
import static org.assertj.core.api.Assertions.assertThatThrownBy;
46+
import static org.assertj.core.api.Assertions.fail;
47+
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
4148

4249
/**
4350
* Test suite for the {@link McpAsyncClient} that can be used with different
@@ -77,7 +84,9 @@ McpAsyncClient client(McpClientTransport transport, Function<McpClient.AsyncSpec
7784
McpClient.AsyncSpec builder = McpClient.async(transport)
7885
.requestTimeout(getRequestTimeout())
7986
.initializationTimeout(getInitializationTimeout())
80-
.capabilities(ClientCapabilities.builder().roots(true).build());
87+
.sampling(req -> Mono.just(new CreateMessageResult(McpSchema.Role.USER,
88+
new McpSchema.TextContent("Oh, hi!"), "modelId", CreateMessageResult.StopReason.END_TURN)))
89+
.capabilities(ClientCapabilities.builder().roots(true).sampling().build());
8190
builder = customizer.apply(builder);
8291
client.set(builder.build());
8392
}).doesNotThrowAnyException();
@@ -202,6 +211,64 @@ void testCallToolWithInvalidTool() {
202211
});
203212
}
204213

214+
@ParameterizedTest
215+
@ValueSource(strings = { "success", "error", "debug" })
216+
void testCallToolWithMessageAnnotations(String messageType) {
217+
McpClientTransport transport = createMcpTransport();
218+
219+
withClient(transport, mcpAsyncClient -> {
220+
StepVerifier.create(mcpAsyncClient.initialize()
221+
.then(mcpAsyncClient.callTool(new McpSchema.CallToolRequest("annotatedMessage",
222+
Map.of("messageType", messageType, "includeImage", true)))))
223+
.consumeNextWith(result -> {
224+
assertThat(result).isNotNull();
225+
assertThat(result.isError()).isNotEqualTo(true);
226+
assertThat(result.content()).isNotEmpty();
227+
assertThat(result.content()).allSatisfy(content -> {
228+
switch (content.type()) {
229+
case "text":
230+
McpSchema.TextContent textContent = assertInstanceOf(McpSchema.TextContent.class,
231+
content);
232+
assertThat(textContent.text()).isNotEmpty();
233+
assertThat(textContent.annotations()).isNotNull();
234+
235+
switch (messageType) {
236+
case "error":
237+
assertThat(textContent.annotations().priority()).isEqualTo(1.0);
238+
assertThat(textContent.annotations().audience())
239+
.containsOnly(McpSchema.Role.USER, McpSchema.Role.ASSISTANT);
240+
break;
241+
case "success":
242+
assertThat(textContent.annotations().priority()).isEqualTo(0.7);
243+
assertThat(textContent.annotations().audience())
244+
.containsExactly(McpSchema.Role.USER);
245+
break;
246+
case "debug":
247+
assertThat(textContent.annotations().priority()).isEqualTo(0.3);
248+
assertThat(textContent.annotations().audience())
249+
.containsExactly(McpSchema.Role.ASSISTANT);
250+
break;
251+
default:
252+
throw new IllegalStateException("Unexpected value: " + content.type());
253+
}
254+
break;
255+
case "image":
256+
McpSchema.ImageContent imageContent = assertInstanceOf(McpSchema.ImageContent.class,
257+
content);
258+
assertThat(imageContent.data()).isNotEmpty();
259+
assertThat(imageContent.annotations()).isNotNull();
260+
assertThat(imageContent.annotations().priority()).isEqualTo(0.5);
261+
assertThat(imageContent.annotations().audience()).containsExactly(McpSchema.Role.USER);
262+
break;
263+
default:
264+
fail("Unexpected content type: " + content.type());
265+
}
266+
});
267+
})
268+
.verifyComplete();
269+
});
270+
}
271+
205272
@Test
206273
void testListResourcesWithoutInitialization() {
207274
verifyCallSucceedsWithImplicitInitialization(client -> client.listResources(null), "listing resources");
@@ -424,6 +491,20 @@ void testInitializeWithSamplingCapability() {
424491
});
425492
}
426493

494+
@Test
495+
void testInitializeWithElicitationCapability() {
496+
ClientCapabilities capabilities = ClientCapabilities.builder().elicitation().build();
497+
ElicitResult elicitResult = ElicitResult.builder()
498+
.message(ElicitResult.Action.ACCEPT)
499+
.content(Map.of("foo", "bar"))
500+
.build();
501+
withClient(createMcpTransport(),
502+
builder -> builder.capabilities(capabilities).elicitation(request -> Mono.just(elicitResult)),
503+
client -> {
504+
StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete();
505+
});
506+
}
507+
427508
@Test
428509
void testInitializeWithAllCapabilities() {
429510
var capabilities = ClientCapabilities.builder()
@@ -435,7 +516,11 @@ void testInitializeWithAllCapabilities() {
435516
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
436517
.just(CreateMessageResult.builder().message("test").model("test-model").build());
437518

438-
withClient(createMcpTransport(), builder -> builder.capabilities(capabilities).sampling(samplingHandler),
519+
Function<ElicitRequest, Mono<ElicitResult>> elicitationHandler = request -> Mono
520+
.just(ElicitResult.builder().message(ElicitResult.Action.ACCEPT).content(Map.of("foo", "bar")).build());
521+
522+
withClient(createMcpTransport(),
523+
builder -> builder.capabilities(capabilities).sampling(samplingHandler).elicitation(elicitationHandler),
439524
client ->
440525

441526
StepVerifier.create(client.initialize()).assertNext(result -> {
@@ -487,4 +572,52 @@ void testLoggingWithNullNotification() {
487572
});
488573
}
489574

575+
@Test
576+
void testSampling() {
577+
McpClientTransport transport = createMcpTransport();
578+
579+
final String message = "Hello, world!";
580+
final String response = "Goodbye, world!";
581+
final int maxTokens = 100;
582+
583+
AtomicReference<String> receivedPrompt = new AtomicReference<>();
584+
AtomicReference<String> receivedMessage = new AtomicReference<>();
585+
AtomicInteger receivedMaxTokens = new AtomicInteger();
586+
587+
withClient(transport, spec -> spec.capabilities(McpSchema.ClientCapabilities.builder().sampling().build())
588+
.sampling(request -> {
589+
McpSchema.TextContent messageText = assertInstanceOf(McpSchema.TextContent.class,
590+
request.messages().get(0).content());
591+
receivedPrompt.set(request.systemPrompt());
592+
receivedMessage.set(messageText.text());
593+
receivedMaxTokens.set(request.maxTokens());
594+
595+
return Mono
596+
.just(new McpSchema.CreateMessageResult(McpSchema.Role.USER, new McpSchema.TextContent(response),
597+
"modelId", McpSchema.CreateMessageResult.StopReason.END_TURN));
598+
}), client -> {
599+
StepVerifier.create(client.initialize()).expectNextMatches(Objects::nonNull).verifyComplete();
600+
601+
StepVerifier.create(client.callTool(
602+
new McpSchema.CallToolRequest("sampleLLM", Map.of("prompt", message, "maxTokens", maxTokens))))
603+
.consumeNextWith(result -> {
604+
// Verify tool response to ensure our sampling response was passed
605+
// through
606+
assertThat(result.content()).hasAtLeastOneElementOfType(McpSchema.TextContent.class);
607+
assertThat(result.content()).allSatisfy(content -> {
608+
if (!(content instanceof McpSchema.TextContent text))
609+
return;
610+
611+
assertThat(text.text()).endsWith(response); // Prefixed
612+
});
613+
614+
// Verify sampling request parameters received in our callback
615+
assertThat(receivedPrompt.get()).isNotEmpty();
616+
assertThat(receivedMessage.get()).endsWith(message); // Prefixed
617+
assertThat(receivedMaxTokens.get()).isEqualTo(maxTokens);
618+
})
619+
.verifyComplete();
620+
});
621+
}
622+
490623
}

0 commit comments

Comments
 (0)