Skip to content

Commit 917dd96

Browse files
committed
Merge branch 'main' of https://github.com/modelcontextprotocol/java-sdk into fix/content-annotations
# Conflicts: # mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java # mcp-test/src/main/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java # mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java # mcp/src/test/java/io/modelcontextprotocol/client/AbstractMcpSyncClientTests.java
2 parents 8526daa + f3b0774 commit 917dd96

File tree

10 files changed

+280
-26
lines changed

10 files changed

+280
-26
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/src/main/java/io/modelcontextprotocol/client/AbstractMcpAsyncClientTests.java

Lines changed: 74 additions & 3 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;
@@ -40,8 +43,8 @@
4043
import static org.assertj.core.api.Assertions.assertThat;
4144
import static org.assertj.core.api.Assertions.assertThatCode;
4245
import static org.assertj.core.api.Assertions.assertThatThrownBy;
43-
import static org.assertj.core.api.Assertions.fail;
4446
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
47+
import static org.assertj.core.api.Assertions.fail;
4548

4649
/**
4750
* Test suite for the {@link McpAsyncClient} that can be used with different
@@ -81,7 +84,9 @@ McpAsyncClient client(McpClientTransport transport, Function<McpClient.AsyncSpec
8184
McpClient.AsyncSpec builder = McpClient.async(transport)
8285
.requestTimeout(getRequestTimeout())
8386
.initializationTimeout(getInitializationTimeout())
84-
.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());
8590
builder = customizer.apply(builder);
8691
client.set(builder.build());
8792
}).doesNotThrowAnyException();
@@ -486,6 +491,20 @@ void testInitializeWithSamplingCapability() {
486491
});
487492
}
488493

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+
489508
@Test
490509
void testInitializeWithAllCapabilities() {
491510
var capabilities = ClientCapabilities.builder()
@@ -497,7 +516,11 @@ void testInitializeWithAllCapabilities() {
497516
Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler = request -> Mono
498517
.just(CreateMessageResult.builder().message("test").model("test-model").build());
499518

500-
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),
501524
client ->
502525

503526
StepVerifier.create(client.initialize()).assertNext(result -> {
@@ -549,4 +572,52 @@ void testLoggingWithNullNotification() {
549572
});
550573
}
551574

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+
552623
}

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

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.List;
99
import java.util.Map;
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;
@@ -33,15 +34,13 @@
3334
import org.junit.jupiter.params.ParameterizedTest;
3435
import org.junit.jupiter.params.provider.ValueSource;
3536
import reactor.core.publisher.Mono;
36-
import reactor.core.scheduler.Scheduler;
37-
import reactor.core.scheduler.Schedulers;
3837
import reactor.test.StepVerifier;
3938

4039
import static org.assertj.core.api.Assertions.assertThat;
4140
import static org.assertj.core.api.Assertions.assertThatCode;
4241
import static org.assertj.core.api.Assertions.assertThatThrownBy;
43-
import static org.assertj.core.api.Assertions.fail;
4442
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
43+
import static org.assertj.core.api.Assertions.fail;
4544

4645
/**
4746
* Unit tests for MCP Client Session functionality.
@@ -496,4 +495,48 @@ void testLoggingWithNullNotification() {
496495
.hasMessageContaining("Logging level must not be null"));
497496
}
498497

498+
@Test
499+
void testSampling() {
500+
McpClientTransport transport = createMcpTransport();
501+
502+
final String message = "Hello, world!";
503+
final String response = "Goodbye, world!";
504+
final int maxTokens = 100;
505+
506+
AtomicReference<String> receivedPrompt = new AtomicReference<>();
507+
AtomicReference<String> receivedMessage = new AtomicReference<>();
508+
AtomicInteger receivedMaxTokens = new AtomicInteger();
509+
510+
withClient(transport, spec -> spec.capabilities(McpSchema.ClientCapabilities.builder().sampling().build())
511+
.sampling(request -> {
512+
McpSchema.TextContent messageText = assertInstanceOf(McpSchema.TextContent.class,
513+
request.messages().get(0).content());
514+
receivedPrompt.set(request.systemPrompt());
515+
receivedMessage.set(messageText.text());
516+
receivedMaxTokens.set(request.maxTokens());
517+
518+
return new McpSchema.CreateMessageResult(McpSchema.Role.USER, new McpSchema.TextContent(response),
519+
"modelId", McpSchema.CreateMessageResult.StopReason.END_TURN);
520+
}), client -> {
521+
client.initialize();
522+
523+
McpSchema.CallToolResult result = client.callTool(
524+
new McpSchema.CallToolRequest("sampleLLM", Map.of("prompt", message, "maxTokens", maxTokens)));
525+
526+
// Verify tool response to ensure our sampling response was passed through
527+
assertThat(result.content()).hasAtLeastOneElementOfType(McpSchema.TextContent.class);
528+
assertThat(result.content()).allSatisfy(content -> {
529+
if (!(content instanceof McpSchema.TextContent text))
530+
return;
531+
532+
assertThat(text.text()).endsWith(response); // Prefixed
533+
});
534+
535+
// Verify sampling request parameters received in our callback
536+
assertThat(receivedPrompt.get()).isNotEmpty();
537+
assertThat(receivedMessage.get()).endsWith(message); // Prefixed
538+
assertThat(receivedMaxTokens.get()).isEqualTo(maxTokens);
539+
});
540+
}
541+
499542
}

mcp/src/main/java/io/modelcontextprotocol/spec/DefaultMcpTransportSession.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import java.util.Optional;
1111
import java.util.concurrent.atomic.AtomicBoolean;
1212
import java.util.concurrent.atomic.AtomicReference;
13-
import java.util.function.Supplier;
13+
import java.util.function.Function;
1414

1515
/**
1616
* Default implementation of {@link McpTransportSession} which manages the open
@@ -29,9 +29,9 @@ public class DefaultMcpTransportSession implements McpTransportSession<Disposabl
2929

3030
private final AtomicReference<String> sessionId = new AtomicReference<>();
3131

32-
private final Supplier<Publisher<Void>> onClose;
32+
private final Function<String, Publisher<Void>> onClose;
3333

34-
public DefaultMcpTransportSession(Supplier<Publisher<Void>> onClose) {
34+
public DefaultMcpTransportSession(Function<String, Publisher<Void>> onClose) {
3535
this.onClose = onClose;
3636
}
3737

@@ -73,7 +73,8 @@ public void close() {
7373

7474
@Override
7575
public Mono<Void> closeGracefully() {
76-
return Mono.from(this.onClose.get()).then(Mono.fromRunnable(this.openConnections::dispose));
76+
return Mono.from(this.onClose.apply(this.sessionId.get()))
77+
.then(Mono.fromRunnable(this.openConnections::dispose));
7778
}
7879

7980
}

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

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1359,8 +1359,9 @@ public record CompleteCompletion(
13591359
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
13601360
@JsonSubTypes({ @JsonSubTypes.Type(value = TextContent.class, name = "text"),
13611361
@JsonSubTypes.Type(value = ImageContent.class, name = "image"),
1362+
@JsonSubTypes.Type(value = AudioContent.class, name = "audio"),
13621363
@JsonSubTypes.Type(value = EmbeddedResource.class, name = "resource") })
1363-
public sealed interface Content permits TextContent, ImageContent, EmbeddedResource {
1364+
public sealed interface Content permits TextContent, ImageContent, AudioContent, EmbeddedResource {
13641365

13651366
default String type() {
13661367
if (this instanceof TextContent) {
@@ -1369,6 +1370,9 @@ default String type() {
13691370
else if (this instanceof ImageContent) {
13701371
return "image";
13711372
}
1373+
else if (this instanceof AudioContent) {
1374+
return "audio";
1375+
}
13721376
else if (this instanceof EmbeddedResource) {
13731377
return "resource";
13741378
}
@@ -1444,6 +1448,14 @@ public Double priority() {
14441448
}
14451449
}
14461450

1451+
@JsonInclude(JsonInclude.Include.NON_ABSENT)
1452+
@JsonIgnoreProperties(ignoreUnknown = true)
1453+
public record AudioContent( // @formatter:off
1454+
@JsonProperty("annotations") Annotations annotations,
1455+
@JsonProperty("data") String data,
1456+
@JsonProperty("mimeType") String mimeType) implements Annotated, Content { // @formatter:on
1457+
}
1458+
14471459
@JsonInclude(JsonInclude.Include.NON_ABSENT)
14481460
@JsonIgnoreProperties(ignoreUnknown = true)
14491461
public record EmbeddedResource( // @formatter:off

0 commit comments

Comments
 (0)