diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransport.java
deleted file mode 100644
index fb0b581e0..000000000
--- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransport.java
+++ /dev/null
@@ -1,413 +0,0 @@
-package io.modelcontextprotocol.server.transport;
-
-import java.io.IOException;
-import java.time.Duration;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Function;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import io.modelcontextprotocol.spec.McpError;
-import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
-import io.modelcontextprotocol.util.Assert;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-import reactor.core.publisher.Sinks;
-
-import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.http.codec.ServerSentEvent;
-import org.springframework.web.reactive.function.server.RouterFunction;
-import org.springframework.web.reactive.function.server.RouterFunctions;
-import org.springframework.web.reactive.function.server.ServerRequest;
-import org.springframework.web.reactive.function.server.ServerResponse;
-
-/**
- * Server-side implementation of the MCP (Model Context Protocol) HTTP transport using
- * Server-Sent Events (SSE). This implementation provides a bidirectional communication
- * channel between MCP clients and servers using HTTP POST for client-to-server messages
- * and SSE for server-to-client messages.
- *
- *
- * Key features:
- *
- * Implements the {@link ServerMcpTransport} interface for MCP server transport
- * functionality
- * Uses WebFlux for non-blocking request handling and SSE support
- * Maintains client sessions for reliable message delivery
- * Supports graceful shutdown with session cleanup
- * Thread-safe message broadcasting to multiple clients
- *
- *
- *
- * The transport sets up two main endpoints:
- *
- * SSE endpoint (/sse) - For establishing SSE connections with clients
- * Message endpoint (configurable) - For receiving JSON-RPC messages from clients
- *
- *
- *
- * This implementation is thread-safe and can handle multiple concurrent client
- * connections. It uses {@link ConcurrentHashMap} for session management and Reactor's
- * {@link Sinks} for thread-safe message broadcasting.
- *
- * @author Christian Tzolov
- * @author Alexandros Pappas
- * @see ServerMcpTransport
- * @see ServerSentEvent
- * @deprecated This class will be removed in 0.9.0. Use
- * {@link WebFluxSseServerTransportProvider}.
- */
-@Deprecated
-public class WebFluxSseServerTransport implements ServerMcpTransport {
-
- private static final Logger logger = LoggerFactory.getLogger(WebFluxSseServerTransport.class);
-
- /**
- * Event type for JSON-RPC messages sent through the SSE connection.
- */
- public static final String MESSAGE_EVENT_TYPE = "message";
-
- /**
- * Event type for sending the message endpoint URI to clients.
- */
- public static final String ENDPOINT_EVENT_TYPE = "endpoint";
-
- /**
- * Default SSE endpoint path as specified by the MCP transport specification.
- */
- public static final String DEFAULT_SSE_ENDPOINT = "/sse";
-
- private final ObjectMapper objectMapper;
-
- private final String messageEndpoint;
-
- private final String sseEndpoint;
-
- private final RouterFunction> routerFunction;
-
- /**
- * Map of active client sessions, keyed by session ID.
- */
- private final ConcurrentHashMap sessions = new ConcurrentHashMap<>();
-
- /**
- * Flag indicating if the transport is shutting down.
- */
- private volatile boolean isClosing = false;
-
- private Function, Mono> connectHandler;
-
- /**
- * Constructs a new WebFlux SSE server transport instance.
- * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
- * of MCP messages. Must not be null.
- * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC
- * messages. This endpoint will be communicated to clients during SSE connection
- * setup. Must not be null.
- * @throws IllegalArgumentException if either parameter is null
- */
- public WebFluxSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
- Assert.notNull(objectMapper, "ObjectMapper must not be null");
- Assert.notNull(messageEndpoint, "Message endpoint must not be null");
- Assert.notNull(sseEndpoint, "SSE endpoint must not be null");
-
- this.objectMapper = objectMapper;
- this.messageEndpoint = messageEndpoint;
- this.sseEndpoint = sseEndpoint;
- this.routerFunction = RouterFunctions.route()
- .GET(this.sseEndpoint, this::handleSseConnection)
- .POST(this.messageEndpoint, this::handleMessage)
- .build();
- }
-
- /**
- * Constructs a new WebFlux SSE server transport instance with the default SSE
- * endpoint.
- * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
- * of MCP messages. Must not be null.
- * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC
- * messages. This endpoint will be communicated to clients during SSE connection
- * setup. Must not be null.
- * @throws IllegalArgumentException if either parameter is null
- */
- public WebFluxSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
- this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT);
- }
-
- /**
- * Configures the message handler for this transport. In the WebFlux SSE
- * implementation, this method stores the handler for processing incoming messages but
- * doesn't establish any connections since the server accepts connections rather than
- * initiating them.
- * @param handler A function that processes incoming JSON-RPC messages and returns
- * responses. This handler will be called for each message received through the
- * message endpoint.
- * @return An empty Mono since the server doesn't initiate connections
- */
- @Override
- public Mono connect(Function, Mono> handler) {
- this.connectHandler = handler;
- // Server-side transport doesn't initiate connections
- return Mono.empty().then();
- }
-
- /**
- * Broadcasts a JSON-RPC message to all connected clients through their SSE
- * connections. The message is serialized to JSON and sent as a server-sent event to
- * each active session.
- *
- *
- * The method:
- *
- * Serializes the message to JSON
- * Creates a server-sent event with the message data
- * Attempts to send the event to all active sessions
- * Tracks and reports any delivery failures
- *
- * @param message The JSON-RPC message to broadcast
- * @return A Mono that completes when the message has been sent to all sessions, or
- * errors if any session fails to receive the message
- */
- @Override
- public Mono sendMessage(McpSchema.JSONRPCMessage message) {
- if (sessions.isEmpty()) {
- logger.debug("No active sessions to broadcast message to");
- return Mono.empty();
- }
-
- return Mono.create(sink -> {
- try {// @formatter:off
- String jsonText = objectMapper.writeValueAsString(message);
- ServerSentEvent event = ServerSentEvent.builder()
- .event(MESSAGE_EVENT_TYPE)
- .data(jsonText)
- .build();
-
- logger.debug("Attempting to broadcast message to {} active sessions", sessions.size());
-
- List failedSessions = sessions.values().stream()
- .filter(session -> session.messageSink.tryEmitNext(event).isFailure())
- .map(session -> session.id)
- .toList();
-
- if (failedSessions.isEmpty()) {
- logger.debug("Successfully broadcast message to all sessions");
- sink.success();
- }
- else {
- String error = "Failed to broadcast message to sessions: " + String.join(", ", failedSessions);
- logger.error(error);
- sink.error(new RuntimeException(error));
- } // @formatter:on
- }
- catch (IOException e) {
- logger.error("Failed to serialize message: {}", e.getMessage());
- sink.error(e);
- }
- });
- }
-
- /**
- * Converts data from one type to another using the configured ObjectMapper. This
- * method is primarily used for converting between different representations of
- * JSON-RPC message data.
- * @param The target type to convert to
- * @param data The source data to convert
- * @param typeRef Type reference describing the target type
- * @return The converted data
- * @throws IllegalArgumentException if the conversion fails
- */
- @Override
- public T unmarshalFrom(Object data, TypeReference typeRef) {
- return this.objectMapper.convertValue(data, typeRef);
- }
-
- /**
- * Initiates a graceful shutdown of the transport. This method ensures all active
- * sessions are properly closed and cleaned up.
- *
- *
- * The shutdown process:
- *
- * Marks the transport as closing to prevent new connections
- * Closes each active session
- * Removes closed sessions from the sessions map
- * Times out after 5 seconds if shutdown takes too long
- *
- * @return A Mono that completes when all sessions have been closed
- */
- @Override
- public Mono closeGracefully() {
- return Mono.fromRunnable(() -> {
- isClosing = true;
- logger.debug("Initiating graceful shutdown with {} active sessions", sessions.size());
- }).then(Mono.when(sessions.values().stream().map(session -> {
- String sessionId = session.id;
- return Mono.fromRunnable(() -> session.close())
- .then(Mono.delay(Duration.ofMillis(100)))
- .then(Mono.fromRunnable(() -> sessions.remove(sessionId)));
- }).toList()))
- .timeout(Duration.ofSeconds(5))
- .doOnSuccess(v -> logger.debug("Graceful shutdown completed"))
- .doOnError(e -> logger.error("Error during graceful shutdown: {}", e.getMessage()));
- }
-
- /**
- * Returns the WebFlux router function that defines the transport's HTTP endpoints.
- * This router function should be integrated into the application's web configuration.
- *
- *
- * The router function defines two endpoints:
- *
- * GET {sseEndpoint} - For establishing SSE connections
- * POST {messageEndpoint} - For receiving client messages
- *
- * @return The configured {@link RouterFunction} for handling HTTP requests
- */
- public RouterFunction> getRouterFunction() {
- return this.routerFunction;
- }
-
- /**
- * Handles new SSE connection requests from clients. Creates a new session for each
- * connection and sets up the SSE event stream.
- *
- *
- * The handler performs the following steps:
- *
- * Generates a unique session ID
- * Creates a new ClientSession instance
- * Sends the message endpoint URI as an initial event
- * Sets up message forwarding for the session
- * Handles connection cleanup on completion or errors
- *
- * @param request The incoming server request
- * @return A response with the SSE event stream
- */
- private Mono handleSseConnection(ServerRequest request) {
- if (isClosing) {
- return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down");
- }
- String sessionId = UUID.randomUUID().toString();
- logger.debug("Creating new SSE connection for session: {}", sessionId);
- ClientSession session = new ClientSession(sessionId);
- this.sessions.put(sessionId, session);
-
- return ServerResponse.ok()
- .contentType(MediaType.TEXT_EVENT_STREAM)
- .body(Flux.>create(sink -> {
- // Send initial endpoint event
- logger.debug("Sending initial endpoint event to session: {}", sessionId);
- sink.next(ServerSentEvent.builder().event(ENDPOINT_EVENT_TYPE).data(messageEndpoint).build());
-
- // Subscribe to session messages
- session.messageSink.asFlux()
- .doOnSubscribe(s -> logger.debug("Session {} subscribed to message sink", sessionId))
- .doOnComplete(() -> {
- logger.debug("Session {} completed", sessionId);
- sessions.remove(sessionId);
- })
- .doOnError(error -> {
- logger.error("Error in session {}: {}", sessionId, error.getMessage());
- sessions.remove(sessionId);
- })
- .doOnCancel(() -> {
- logger.debug("Session {} cancelled", sessionId);
- sessions.remove(sessionId);
- })
- .subscribe(event -> {
- logger.debug("Forwarding event to session {}: {}", sessionId, event);
- sink.next(event);
- }, sink::error, sink::complete);
-
- sink.onCancel(() -> {
- logger.debug("Session {} cancelled", sessionId);
- sessions.remove(sessionId);
- });
- }), ServerSentEvent.class);
- }
-
- /**
- * Handles incoming JSON-RPC messages from clients. Deserializes the message and
- * processes it through the configured message handler.
- *
- *
- * The handler:
- *
- * Deserializes the incoming JSON-RPC message
- * Passes it through the message handler chain
- * Returns appropriate HTTP responses based on processing results
- * Handles various error conditions with appropriate error responses
- *
- * @param request The incoming server request containing the JSON-RPC message
- * @return A response indicating the message processing result
- */
- private Mono handleMessage(ServerRequest request) {
- if (isClosing) {
- return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down");
- }
-
- return request.bodyToMono(String.class).flatMap(body -> {
- try {
- McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body);
- return Mono.just(message)
- .transform(this.connectHandler)
- .flatMap(response -> ServerResponse.ok().build())
- .onErrorResume(error -> {
- logger.error("Error processing message: {}", error.getMessage());
- return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR)
- .bodyValue(new McpError(error.getMessage()));
- });
- }
- catch (IllegalArgumentException | IOException e) {
- logger.error("Failed to deserialize message: {}", e.getMessage());
- return ServerResponse.badRequest().bodyValue(new McpError("Invalid message format"));
- }
- });
- }
-
- /**
- * Represents an active client SSE connection session. Manages the message sink for
- * sending events to the client and handles session lifecycle.
- *
- *
- * Each session:
- *
- * Has a unique identifier
- * Maintains its own message sink for event broadcasting
- * Supports clean shutdown through the close method
- *
- */
- private static class ClientSession {
-
- private final String id;
-
- private final Sinks.Many> messageSink;
-
- ClientSession(String id) {
- this.id = id;
- logger.debug("Creating new session: {}", id);
- this.messageSink = Sinks.many().replay().latest();
- logger.debug("Session {} initialized with replay sink", id);
- }
-
- void close() {
- logger.debug("Closing session: {}", id);
- Sinks.EmitResult result = messageSink.tryEmitComplete();
- if (result.isFailure()) {
- logger.warn("Failed to complete message sink for session {}: {}", id, result);
- }
- else {
- logger.debug("Successfully completed message sink for session {}", id);
- }
- }
-
- }
-
-}
\ No newline at end of file
diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java
index cf3eeae03..4e5d2fafb 100644
--- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java
+++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java
@@ -8,10 +8,9 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransport;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
-import io.modelcontextprotocol.spec.McpServerSession;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
import io.modelcontextprotocol.util.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -19,7 +18,6 @@
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Mono;
-import reactor.core.publisher.Sinks;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpAsyncServerDeprecatedTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpAsyncServerDeprecatedTests.java
deleted file mode 100644
index b460284ee..000000000
--- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpAsyncServerDeprecatedTests.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 2024-2024 the original author or authors.
- */
-
-package io.modelcontextprotocol.server;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import io.modelcontextprotocol.server.transport.WebFluxSseServerTransport;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
-import org.junit.jupiter.api.Timeout;
-import reactor.netty.DisposableServer;
-import reactor.netty.http.server.HttpServer;
-
-import org.springframework.http.server.reactive.HttpHandler;
-import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
-import org.springframework.web.reactive.function.server.RouterFunctions;
-
-/**
- * Tests for {@link McpAsyncServer} using {@link WebFluxSseServerTransport}.
- *
- * @author Christian Tzolov
- */
-@Deprecated
-@Timeout(15) // Giving extra time beyond the client timeout
-class WebFluxSseMcpAsyncServerDeprecatedTests extends AbstractMcpAsyncServerDeprecatedTests {
-
- private static final int PORT = 8181;
-
- private static final String MESSAGE_ENDPOINT = "/mcp/message";
-
- private DisposableServer httpServer;
-
- @Override
- protected ServerMcpTransport createMcpTransport() {
- var transport = new WebFluxSseServerTransport(new ObjectMapper(), MESSAGE_ENDPOINT);
-
- HttpHandler httpHandler = RouterFunctions.toHttpHandler(transport.getRouterFunction());
- ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
- httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow();
-
- return transport;
- }
-
- @Override
- protected void onStart() {
- }
-
- @Override
- protected void onClose() {
- if (httpServer != null) {
- httpServer.disposeNow();
- }
- }
-
-}
diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpSyncServerDeprecatecTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpSyncServerDeprecatecTests.java
deleted file mode 100644
index be2bf6c7f..000000000
--- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpSyncServerDeprecatecTests.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright 2024-2024 the original author or authors.
- */
-
-package io.modelcontextprotocol.server;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import io.modelcontextprotocol.server.transport.WebFluxSseServerTransport;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
-import org.junit.jupiter.api.Timeout;
-import reactor.netty.DisposableServer;
-import reactor.netty.http.server.HttpServer;
-
-import org.springframework.http.server.reactive.HttpHandler;
-import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
-import org.springframework.web.reactive.function.server.RouterFunctions;
-
-/**
- * Tests for {@link McpSyncServer} using {@link WebFluxSseServerTransport}.
- *
- * @author Christian Tzolov
- */
-@Deprecated
-@Timeout(15) // Giving extra time beyond the client timeout
-class WebFluxSseMcpSyncServerDeprecatecTests extends AbstractMcpSyncServerDeprecatedTests {
-
- private static final int PORT = 8182;
-
- private static final String MESSAGE_ENDPOINT = "/mcp/message";
-
- private DisposableServer httpServer;
-
- private WebFluxSseServerTransport transport;
-
- @Override
- protected ServerMcpTransport createMcpTransport() {
- transport = new WebFluxSseServerTransport(new ObjectMapper(), MESSAGE_ENDPOINT);
- return transport;
- }
-
- @Override
- protected void onStart() {
- HttpHandler httpHandler = RouterFunctions.toHttpHandler(transport.getRouterFunction());
- ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
- httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow();
- }
-
- @Override
- protected void onClose() {
- if (httpServer != null) {
- httpServer.disposeNow();
- }
- }
-
-}
diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/legacy/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/legacy/WebFluxSseIntegrationTests.java
deleted file mode 100644
index 981e114c9..000000000
--- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/legacy/WebFluxSseIntegrationTests.java
+++ /dev/null
@@ -1,459 +0,0 @@
-/*
- * Copyright 2024 - 2024 the original author or authors.
- */
-package io.modelcontextprotocol.server.legacy;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Function;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import io.modelcontextprotocol.client.McpClient;
-import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
-import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport;
-import io.modelcontextprotocol.server.McpServer;
-import io.modelcontextprotocol.server.McpServerFeatures;
-import io.modelcontextprotocol.server.transport.WebFluxSseServerTransport;
-import io.modelcontextprotocol.spec.McpError;
-import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
-import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
-import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
-import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
-import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
-import io.modelcontextprotocol.spec.McpSchema.Role;
-import io.modelcontextprotocol.spec.McpSchema.Root;
-import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
-import io.modelcontextprotocol.spec.McpSchema.Tool;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.ValueSource;
-import reactor.netty.DisposableServer;
-import reactor.netty.http.server.HttpServer;
-import reactor.test.StepVerifier;
-
-import org.springframework.http.server.reactive.HttpHandler;
-import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
-import org.springframework.web.client.RestClient;
-import org.springframework.web.reactive.function.client.WebClient;
-import org.springframework.web.reactive.function.server.RouterFunctions;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.awaitility.Awaitility.await;
-
-public class WebFluxSseIntegrationTests {
-
- private static final int PORT = 8182;
-
- private static final String MESSAGE_ENDPOINT = "/mcp/message";
-
- private DisposableServer httpServer;
-
- private WebFluxSseServerTransport mcpServerTransport;
-
- ConcurrentHashMap clientBulders = new ConcurrentHashMap<>();
-
- @BeforeEach
- public void before() {
-
- this.mcpServerTransport = new WebFluxSseServerTransport(new ObjectMapper(), MESSAGE_ENDPOINT);
-
- HttpHandler httpHandler = RouterFunctions.toHttpHandler(mcpServerTransport.getRouterFunction());
- ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
- this.httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow();
-
- clientBulders.put("httpclient", McpClient.sync(new HttpClientSseClientTransport("http://localhost:" + PORT)));
- clientBulders.put("webflux",
- McpClient.sync(new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:" + PORT))));
-
- }
-
- @AfterEach
- public void after() {
- if (httpServer != null) {
- httpServer.disposeNow();
- }
- }
-
- // ---------------------------------------
- // Sampling Tests
- // ---------------------------------------
- @Test
- void testCreateMessageWithoutInitialization() {
- var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build();
-
- var messages = List.of(new McpSchema.SamplingMessage(Role.USER, new McpSchema.TextContent("Test message")));
- var modelPrefs = new McpSchema.ModelPreferences(List.of(), 1.0, 1.0, 1.0);
-
- var request = new CreateMessageRequest(messages, modelPrefs, null,
- CreateMessageRequest.ContextInclusionStrategy.NONE, null, 100, List.of(), Map.of());
-
- StepVerifier.create(mcpAsyncServer.createMessage(request)).verifyErrorSatisfies(error -> {
- assertThat(error).isInstanceOf(McpError.class)
- .hasMessage("Client must be initialized. Call the initialize method first!");
- });
- }
-
- @ParameterizedTest(name = "{0} : {displayName} ")
- @ValueSource(strings = { "httpclient", "webflux" })
- void testCreateMessageWithoutSamplingCapabilities(String clientType) {
-
- var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build();
-
- var clientBuilder = clientBulders.get(clientType);
-
- var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")).build();
-
- InitializeResult initResult = client.initialize();
- assertThat(initResult).isNotNull();
-
- var messages = List.of(new McpSchema.SamplingMessage(Role.USER, new McpSchema.TextContent("Test message")));
- var modelPrefs = new McpSchema.ModelPreferences(List.of(), 1.0, 1.0, 1.0);
-
- var request = new CreateMessageRequest(messages, modelPrefs, null,
- CreateMessageRequest.ContextInclusionStrategy.NONE, null, 100, List.of(), Map.of());
-
- StepVerifier.create(mcpAsyncServer.createMessage(request)).verifyErrorSatisfies(error -> {
- assertThat(error).isInstanceOf(McpError.class)
- .hasMessage("Client must be configured with sampling capabilities");
- });
- }
-
- @ParameterizedTest(name = "{0} : {displayName} ")
- @ValueSource(strings = { "httpclient", "webflux" })
- void testCreateMessageSuccess(String clientType) throws InterruptedException {
-
- var clientBuilder = clientBulders.get(clientType);
-
- var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build();
-
- Function samplingHandler = request -> {
- assertThat(request.messages()).hasSize(1);
- assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class);
-
- return new CreateMessageResult(Role.USER, new McpSchema.TextContent("Test message"), "MockModelName",
- CreateMessageResult.StopReason.STOP_SEQUENCE);
- };
-
- var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
- .capabilities(ClientCapabilities.builder().sampling().build())
- .sampling(samplingHandler)
- .build();
-
- InitializeResult initResult = client.initialize();
- assertThat(initResult).isNotNull();
-
- var messages = List.of(new McpSchema.SamplingMessage(Role.USER, new McpSchema.TextContent("Test message")));
- var modelPrefs = new McpSchema.ModelPreferences(List.of(), 1.0, 1.0, 1.0);
-
- var request = new CreateMessageRequest(messages, modelPrefs, null,
- CreateMessageRequest.ContextInclusionStrategy.NONE, null, 100, List.of(), Map.of());
-
- StepVerifier.create(mcpAsyncServer.createMessage(request)).consumeNextWith(result -> {
- assertThat(result).isNotNull();
- assertThat(result.role()).isEqualTo(Role.USER);
- assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
- assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message");
- assertThat(result.model()).isEqualTo("MockModelName");
- assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE);
- }).verifyComplete();
- }
-
- // ---------------------------------------
- // Roots Tests
- // ---------------------------------------
- @ParameterizedTest(name = "{0} : {displayName} ")
- @ValueSource(strings = { "httpclient", "webflux" })
- void testRootsSuccess(String clientType) {
- var clientBuilder = clientBulders.get(clientType);
-
- List roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
-
- AtomicReference> rootsRef = new AtomicReference<>();
- var mcpServer = McpServer.sync(mcpServerTransport)
- .rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
- .build();
-
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
- .roots(roots)
- .build();
-
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
-
- assertThat(rootsRef.get()).isNull();
-
- assertThat(mcpServer.listRoots().roots()).containsAll(roots);
-
- mcpClient.rootsListChangedNotification();
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(roots);
- });
-
- // Remove a root
- mcpClient.removeRoot(roots.get(0).uri());
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(roots.get(1)));
- });
-
- // Add a new root
- var root3 = new Root("uri3://", "root3");
- mcpClient.addRoot(root3);
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3));
- });
-
- mcpClient.close();
- mcpServer.close();
- }
-
- @ParameterizedTest(name = "{0} : {displayName} ")
- @ValueSource(strings = { "httpclient", "webflux" })
- void testRootsWithoutCapability(String clientType) {
- var clientBuilder = clientBulders.get(clientType);
-
- var mcpServer = McpServer.sync(mcpServerTransport).rootsChangeConsumer(rootsUpdate -> {
- }).build();
-
- // Create client without roots capability
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()) // No
- // roots
- // capability
- .build();
-
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
-
- // Attempt to list roots should fail
- assertThatThrownBy(() -> mcpServer.listRoots().roots()).isInstanceOf(McpError.class)
- .hasMessage("Roots not supported");
-
- mcpClient.close();
- mcpServer.close();
- }
-
- @ParameterizedTest(name = "{0} : {displayName} ")
- @ValueSource(strings = { "httpclient", "webflux" })
- void testRootsWithEmptyRootsList(String clientType) {
- var clientBuilder = clientBulders.get(clientType);
-
- AtomicReference> rootsRef = new AtomicReference<>();
- var mcpServer = McpServer.sync(mcpServerTransport)
- .rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
- .build();
-
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
- .roots(List.of()) // Empty roots list
- .build();
-
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
-
- mcpClient.rootsListChangedNotification();
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).isEmpty();
- });
-
- mcpClient.close();
- mcpServer.close();
- }
-
- @ParameterizedTest(name = "{0} : {displayName} ")
- @ValueSource(strings = { "httpclient", "webflux" })
- void testRootsWithMultipleConsumers(String clientType) {
- var clientBuilder = clientBulders.get(clientType);
-
- List roots = List.of(new Root("uri1://", "root1"));
-
- AtomicReference> rootsRef1 = new AtomicReference<>();
- AtomicReference> rootsRef2 = new AtomicReference<>();
-
- var mcpServer = McpServer.sync(mcpServerTransport)
- .rootsChangeConsumer(rootsUpdate -> rootsRef1.set(rootsUpdate))
- .rootsChangeConsumer(rootsUpdate -> rootsRef2.set(rootsUpdate))
- .build();
-
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
- .roots(roots)
- .build();
-
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
-
- mcpClient.rootsListChangedNotification();
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef1.get()).containsAll(roots);
- assertThat(rootsRef2.get()).containsAll(roots);
- });
-
- mcpClient.close();
- mcpServer.close();
- }
-
- @ParameterizedTest(name = "{0} : {displayName} ")
- @ValueSource(strings = { "httpclient", "webflux" })
- void testRootsServerCloseWithActiveSubscription(String clientType) {
-
- var clientBuilder = clientBulders.get(clientType);
-
- List roots = List.of(new Root("uri1://", "root1"));
-
- AtomicReference> rootsRef = new AtomicReference<>();
- var mcpServer = McpServer.sync(mcpServerTransport)
- .rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
- .build();
-
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
- .roots(roots)
- .build();
-
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
-
- mcpClient.rootsListChangedNotification();
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(roots);
- });
-
- // Close server while subscription is active
- mcpServer.close();
-
- // Verify client can handle server closure gracefully
- mcpClient.close();
- }
-
- // ---------------------------------------
- // Tools Tests
- // ---------------------------------------
-
- String emptyJsonSchema = """
- {
- "$schema": "http://json-schema.org/draft-07/schema#",
- "type": "object",
- "properties": {}
- }
- """;
-
- @ParameterizedTest(name = "{0} : {displayName} ")
- @ValueSource(strings = { "httpclient", "webflux" })
- void testToolCallSuccess(String clientType) {
-
- var clientBuilder = clientBulders.get(clientType);
-
- var callResponse = new CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
- McpServerFeatures.SyncToolRegistration tool1 = new McpServerFeatures.SyncToolRegistration(
- new Tool("tool1", "tool1 description", emptyJsonSchema), request -> {
- // perform a blocking call to a remote service
- String response = RestClient.create()
- .get()
- .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
- .retrieve()
- .body(String.class);
- assertThat(response).isNotBlank();
- return callResponse;
- });
-
- var mcpServer = McpServer.sync(mcpServerTransport)
- .capabilities(ServerCapabilities.builder().tools(true).build())
- .tools(tool1)
- .build();
-
- var mcpClient = clientBuilder.build();
-
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
-
- assertThat(mcpClient.listTools().tools()).contains(tool1.tool());
-
- CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
-
- assertThat(response).isNotNull();
- assertThat(response).isEqualTo(callResponse);
-
- mcpClient.close();
- mcpServer.close();
- }
-
- @ParameterizedTest(name = "{0} : {displayName} ")
- @ValueSource(strings = { "httpclient", "webflux" })
- void testToolListChangeHandlingSuccess(String clientType) {
-
- var clientBuilder = clientBulders.get(clientType);
-
- var callResponse = new CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
- McpServerFeatures.SyncToolRegistration tool1 = new McpServerFeatures.SyncToolRegistration(
- new Tool("tool1", "tool1 description", emptyJsonSchema), request -> {
- // perform a blocking call to a remote service
- String response = RestClient.create()
- .get()
- .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
- .retrieve()
- .body(String.class);
- assertThat(response).isNotBlank();
- return callResponse;
- });
-
- var mcpServer = McpServer.sync(mcpServerTransport)
- .capabilities(ServerCapabilities.builder().tools(true).build())
- .tools(tool1)
- .build();
-
- AtomicReference> rootsRef = new AtomicReference<>();
- var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
- // perform a blocking call to a remote service
- String response = RestClient.create()
- .get()
- .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
- .retrieve()
- .body(String.class);
- assertThat(response).isNotBlank();
- rootsRef.set(toolsUpdate);
- }).build();
-
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
-
- assertThat(rootsRef.get()).isNull();
-
- assertThat(mcpClient.listTools().tools()).contains(tool1.tool());
-
- mcpServer.notifyToolsListChanged();
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(tool1.tool()));
- });
-
- // Remove a tool
- mcpServer.removeTool("tool1");
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).isEmpty();
- });
-
- // Add a new tool
- McpServerFeatures.SyncToolRegistration tool2 = new McpServerFeatures.SyncToolRegistration(
- new Tool("tool2", "tool2 description", emptyJsonSchema), request -> callResponse);
-
- mcpServer.addTool(tool2);
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(tool2.tool()));
- });
-
- mcpClient.close();
- mcpServer.close();
- }
-
-}
diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransport.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransport.java
deleted file mode 100644
index 23193d106..000000000
--- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransport.java
+++ /dev/null
@@ -1,385 +0,0 @@
-/*
- * Copyright 2024-2024 the original author or authors.
- */
-
-package io.modelcontextprotocol.server.transport;
-
-import java.io.IOException;
-import java.time.Duration;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.function.Function;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import io.modelcontextprotocol.spec.McpError;
-import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
-import io.modelcontextprotocol.util.Assert;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import reactor.core.publisher.Mono;
-
-import org.springframework.http.HttpStatus;
-import org.springframework.web.servlet.function.RouterFunction;
-import org.springframework.web.servlet.function.RouterFunctions;
-import org.springframework.web.servlet.function.ServerRequest;
-import org.springframework.web.servlet.function.ServerResponse;
-import org.springframework.web.servlet.function.ServerResponse.SseBuilder;
-
-/**
- * Server-side implementation of the Model Context Protocol (MCP) transport layer using
- * HTTP with Server-Sent Events (SSE) through Spring WebMVC. This implementation provides
- * a bridge between synchronous WebMVC operations and reactive programming patterns to
- * maintain compatibility with the reactive transport interface.
- *
- * @deprecated This class will be removed in 0.9.0. Use
- * {@link WebMvcSseServerTransportProvider}.
- *
- *
- * Key features:
- *
- * Implements bidirectional communication using HTTP POST for client-to-server
- * messages and SSE for server-to-client messages
- * Manages client sessions with unique IDs for reliable message delivery
- * Supports graceful shutdown with proper session cleanup
- * Provides JSON-RPC message handling through configured endpoints
- * Includes built-in error handling and logging
- *
- *
- *
- * The transport operates on two main endpoints:
- *
- * {@code /sse} - The SSE endpoint where clients establish their event stream
- * connection
- * A configurable message endpoint where clients send their JSON-RPC messages via HTTP
- * POST
- *
- *
- *
- * This implementation uses {@link ConcurrentHashMap} to safely manage multiple client
- * sessions in a thread-safe manner. Each client session is assigned a unique ID and
- * maintains its own SSE connection.
- * @author Christian Tzolov
- * @author Alexandros Pappas
- * @see ServerMcpTransport
- * @see RouterFunction
- */
-@Deprecated
-public class WebMvcSseServerTransport implements ServerMcpTransport {
-
- private static final Logger logger = LoggerFactory.getLogger(WebMvcSseServerTransport.class);
-
- /**
- * Event type for JSON-RPC messages sent through the SSE connection.
- */
- public static final String MESSAGE_EVENT_TYPE = "message";
-
- /**
- * Event type for sending the message endpoint URI to clients.
- */
- public static final String ENDPOINT_EVENT_TYPE = "endpoint";
-
- /**
- * Default SSE endpoint path as specified by the MCP transport specification.
- */
- public static final String DEFAULT_SSE_ENDPOINT = "/sse";
-
- private final ObjectMapper objectMapper;
-
- private final String messageEndpoint;
-
- private final String sseEndpoint;
-
- private final RouterFunction routerFunction;
-
- /**
- * Map of active client sessions, keyed by session ID.
- */
- private final ConcurrentHashMap sessions = new ConcurrentHashMap<>();
-
- /**
- * Flag indicating if the transport is shutting down.
- */
- private volatile boolean isClosing = false;
-
- /**
- * The function to process incoming JSON-RPC messages and produce responses.
- */
- private Function, Mono> connectHandler;
-
- /**
- * Constructs a new WebMvcSseServerTransport instance.
- * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
- * of messages.
- * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC
- * messages via HTTP POST. This endpoint will be communicated to clients through the
- * SSE connection's initial endpoint event.
- * @throws IllegalArgumentException if either objectMapper or messageEndpoint is null
- */
- public WebMvcSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
- Assert.notNull(objectMapper, "ObjectMapper must not be null");
- Assert.notNull(messageEndpoint, "Message endpoint must not be null");
- Assert.notNull(sseEndpoint, "SSE endpoint must not be null");
-
- this.objectMapper = objectMapper;
- this.messageEndpoint = messageEndpoint;
- this.sseEndpoint = sseEndpoint;
- this.routerFunction = RouterFunctions.route()
- .GET(this.sseEndpoint, this::handleSseConnection)
- .POST(this.messageEndpoint, this::handleMessage)
- .build();
- }
-
- /**
- * Constructs a new WebMvcSseServerTransport instance with the default SSE endpoint.
- * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
- * of messages.
- * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC
- * messages via HTTP POST. This endpoint will be communicated to clients through the
- * SSE connection's initial endpoint event.
- * @throws IllegalArgumentException if either objectMapper or messageEndpoint is null
- */
- public WebMvcSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
- this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT);
- }
-
- /**
- * Sets up the message handler for this transport. In the WebMVC SSE implementation,
- * this method only stores the handler for later use, as connections are initiated by
- * clients rather than the server.
- * @param connectionHandler The function to process incoming JSON-RPC messages and
- * produce responses
- * @return An empty Mono since the server doesn't initiate connections
- */
- @Override
- public Mono connect(
- Function, Mono> connectionHandler) {
- this.connectHandler = connectionHandler;
- // Server-side transport doesn't initiate connections
- return Mono.empty();
- }
-
- /**
- * Broadcasts a message to all connected clients through their SSE connections. The
- * message is serialized to JSON and sent as an SSE event with type "message". If any
- * errors occur during sending to a particular client, they are logged but don't
- * prevent sending to other clients.
- * @param message The JSON-RPC message to broadcast to all connected clients
- * @return A Mono that completes when the broadcast attempt is finished
- */
- @Override
- public Mono sendMessage(McpSchema.JSONRPCMessage message) {
- return Mono.fromRunnable(() -> {
- if (sessions.isEmpty()) {
- logger.debug("No active sessions to broadcast message to");
- return;
- }
-
- try {
- String jsonText = objectMapper.writeValueAsString(message);
- logger.debug("Attempting to broadcast message to {} active sessions", sessions.size());
-
- sessions.values().forEach(session -> {
- try {
- session.sseBuilder.id(session.id).event(MESSAGE_EVENT_TYPE).data(jsonText);
- }
- catch (Exception e) {
- logger.error("Failed to send message to session {}: {}", session.id, e.getMessage());
- session.sseBuilder.error(e);
- }
- });
- }
- catch (IOException e) {
- logger.error("Failed to serialize message: {}", e.getMessage());
- }
- });
- }
-
- /**
- * Handles new SSE connection requests from clients by creating a new session and
- * establishing an SSE connection. This method:
- *
- * Generates a unique session ID
- * Creates a new ClientSession with an SSE builder
- * Sends an initial endpoint event to inform the client where to send
- * messages
- * Maintains the session in the sessions map
- *
- * @param request The incoming server request
- * @return A ServerResponse configured for SSE communication, or an error response if
- * the server is shutting down or the connection fails
- */
- private ServerResponse handleSseConnection(ServerRequest request) {
- if (this.isClosing) {
- return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
- }
-
- String sessionId = UUID.randomUUID().toString();
- logger.debug("Creating new SSE connection for session: {}", sessionId);
-
- // Send initial endpoint event
- try {
- return ServerResponse.sse(sseBuilder -> {
- sseBuilder.onComplete(() -> {
- logger.debug("SSE connection completed for session: {}", sessionId);
- sessions.remove(sessionId);
- });
- sseBuilder.onTimeout(() -> {
- logger.debug("SSE connection timed out for session: {}", sessionId);
- sessions.remove(sessionId);
- });
-
- ClientSession session = new ClientSession(sessionId, sseBuilder);
- this.sessions.put(sessionId, session);
-
- try {
- session.sseBuilder.id(session.id).event(ENDPOINT_EVENT_TYPE).data(messageEndpoint);
- }
- catch (Exception e) {
- logger.error("Failed to poll event from session queue: {}", e.getMessage());
- sseBuilder.error(e);
- }
- }, Duration.ZERO);
- }
- catch (Exception e) {
- logger.error("Failed to send initial endpoint event to session {}: {}", sessionId, e.getMessage());
- sessions.remove(sessionId);
- return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
- }
- }
-
- /**
- * Handles incoming JSON-RPC messages from clients. This method:
- *
- * Deserializes the request body into a JSON-RPC message
- * Processes the message through the configured connect handler
- * Returns appropriate HTTP responses based on the processing result
- *
- * @param request The incoming server request containing the JSON-RPC message
- * @return A ServerResponse indicating success (200 OK) or appropriate error status
- * with error details in case of failures
- */
- private ServerResponse handleMessage(ServerRequest request) {
- if (this.isClosing) {
- return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down");
- }
-
- try {
- String body = request.body(String.class);
- McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body);
-
- // Convert the message to a Mono, apply the handler, and block for the
- // response
- @SuppressWarnings("unused")
- McpSchema.JSONRPCMessage response = Mono.just(message).transform(connectHandler).block();
-
- return ServerResponse.ok().build();
- }
- catch (IllegalArgumentException | IOException e) {
- logger.error("Failed to deserialize message: {}", e.getMessage());
- return ServerResponse.badRequest().body(new McpError("Invalid message format"));
- }
- catch (Exception e) {
- logger.error("Error handling message: {}", e.getMessage());
- return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new McpError(e.getMessage()));
- }
- }
-
- /**
- * Represents an active client session with its associated SSE connection. Each
- * session maintains:
- *
- * A unique session identifier
- * An SSE builder for sending server events to the client
- * Logging of session lifecycle events
- *
- */
- private static class ClientSession {
-
- private final String id;
-
- private final SseBuilder sseBuilder;
-
- /**
- * Creates a new client session with the specified ID and SSE builder.
- * @param id The unique identifier for this session
- * @param sseBuilder The SSE builder for sending server events to the client
- */
- ClientSession(String id, SseBuilder sseBuilder) {
- this.id = id;
- this.sseBuilder = sseBuilder;
- logger.debug("Session {} initialized with SSE emitter", id);
- }
-
- /**
- * Closes this session by completing the SSE connection. Any errors during
- * completion are logged but do not prevent the session from being marked as
- * closed.
- */
- void close() {
- logger.debug("Closing session: {}", id);
- try {
- sseBuilder.complete();
- logger.debug("Successfully completed SSE emitter for session {}", id);
- }
- catch (Exception e) {
- logger.warn("Failed to complete SSE emitter for session {}: {}", id, e.getMessage());
- // sseBuilder.error(e);
- }
- }
-
- }
-
- /**
- * Converts data from one type to another using the configured ObjectMapper. This is
- * particularly useful for handling complex JSON-RPC parameter types.
- * @param data The source data object to convert
- * @param typeRef The target type reference
- * @return The converted object of type T
- * @param The target type
- */
- @Override
- public T unmarshalFrom(Object data, TypeReference typeRef) {
- return this.objectMapper.convertValue(data, typeRef);
- }
-
- /**
- * Initiates a graceful shutdown of the transport. This method:
- *
- * Sets the closing flag to prevent new connections
- * Closes all active SSE connections
- * Removes all session records
- *
- * @return A Mono that completes when all cleanup operations are finished
- */
- @Override
- public Mono closeGracefully() {
- return Mono.fromRunnable(() -> {
- this.isClosing = true;
- logger.debug("Initiating graceful shutdown with {} active sessions", sessions.size());
-
- sessions.values().forEach(session -> {
- String sessionId = session.id;
- session.close();
- sessions.remove(sessionId);
- });
-
- logger.debug("Graceful shutdown completed");
- });
- }
-
- /**
- * Returns the RouterFunction that defines the HTTP endpoints for this transport. The
- * router function handles two endpoints:
- *
- * GET /sse - For establishing SSE connections
- * POST [messageEndpoint] - For receiving JSON-RPC messages from clients
- *
- * @return The configured RouterFunction for handling HTTP requests
- */
- public RouterFunction getRouterFunction() {
- return this.routerFunction;
- }
-
-}
diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportDeprecatedTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportDeprecatedTests.java
deleted file mode 100644
index c3f0e3220..000000000
--- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportDeprecatedTests.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright 2024-2024 the original author or authors.
- */
-
-package io.modelcontextprotocol.server;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import io.modelcontextprotocol.server.transport.WebMvcSseServerTransport;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
-import org.apache.catalina.Context;
-import org.apache.catalina.LifecycleException;
-import org.apache.catalina.startup.Tomcat;
-import org.junit.jupiter.api.Timeout;
-
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
-import org.springframework.web.servlet.DispatcherServlet;
-import org.springframework.web.servlet.config.annotation.EnableWebMvc;
-import org.springframework.web.servlet.function.RouterFunction;
-import org.springframework.web.servlet.function.ServerResponse;
-
-@Deprecated
-@Timeout(15)
-class WebMvcSseAsyncServerTransportDeprecatedTests extends AbstractMcpAsyncServerDeprecatedTests {
-
- private static final String MESSAGE_ENDPOINT = "/mcp/message";
-
- private static final int PORT = 8181;
-
- private Tomcat tomcat;
-
- private WebMvcSseServerTransport transport;
-
- @Configuration
- @EnableWebMvc
- static class TestConfig {
-
- @Bean
- public WebMvcSseServerTransport webMvcSseServerTransport() {
- return new WebMvcSseServerTransport(new ObjectMapper(), MESSAGE_ENDPOINT);
- }
-
- @Bean
- public RouterFunction routerFunction(WebMvcSseServerTransport transport) {
- return transport.getRouterFunction();
- }
-
- }
-
- private AnnotationConfigWebApplicationContext appContext;
-
- @Override
- protected ServerMcpTransport createMcpTransport() {
- // Set up Tomcat first
- tomcat = new Tomcat();
- tomcat.setPort(PORT);
-
- // Set Tomcat base directory to java.io.tmpdir to avoid permission issues
- String baseDir = System.getProperty("java.io.tmpdir");
- tomcat.setBaseDir(baseDir);
-
- // Use the same directory for document base
- Context context = tomcat.addContext("", baseDir);
-
- // Create and configure Spring WebMvc context
- appContext = new AnnotationConfigWebApplicationContext();
- appContext.register(TestConfig.class);
- appContext.setServletContext(context.getServletContext());
- appContext.refresh();
-
- // Get the transport from Spring context
- transport = appContext.getBean(WebMvcSseServerTransport.class);
-
- // Create DispatcherServlet with our Spring context
- DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext);
- // dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
-
- // Add servlet to Tomcat and get the wrapper
- var wrapper = Tomcat.addServlet(context, "dispatcherServlet", dispatcherServlet);
- wrapper.setLoadOnStartup(1);
- context.addServletMappingDecoded("/*", "dispatcherServlet");
-
- try {
- tomcat.start();
- tomcat.getConnector(); // Create and start the connector
- }
- catch (LifecycleException e) {
- throw new RuntimeException("Failed to start Tomcat", e);
- }
-
- return transport;
- }
-
- @Override
- protected void onStart() {
- }
-
- @Override
- protected void onClose() {
- if (transport != null) {
- transport.closeGracefully().block();
- }
- if (appContext != null) {
- appContext.close();
- }
- if (tomcat != null) {
- try {
- tomcat.stop();
- tomcat.destroy();
- }
- catch (LifecycleException e) {
- throw new RuntimeException("Failed to stop Tomcat", e);
- }
- }
- }
-
-}
diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationDeprecatedTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationDeprecatedTests.java
deleted file mode 100644
index f2b593d8d..000000000
--- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationDeprecatedTests.java
+++ /dev/null
@@ -1,508 +0,0 @@
-/*
- * Copyright 2024 - 2024 the original author or authors.
- */
-package io.modelcontextprotocol.server;
-
-import java.time.Duration;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.function.Function;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import io.modelcontextprotocol.client.McpClient;
-import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport;
-import io.modelcontextprotocol.server.transport.WebMvcSseServerTransport;
-import io.modelcontextprotocol.spec.McpError;
-import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
-import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
-import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest;
-import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult;
-import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
-import io.modelcontextprotocol.spec.McpSchema.Role;
-import io.modelcontextprotocol.spec.McpSchema.Root;
-import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
-import io.modelcontextprotocol.spec.McpSchema.Tool;
-import org.apache.catalina.Context;
-import org.apache.catalina.LifecycleException;
-import org.apache.catalina.LifecycleState;
-import org.apache.catalina.startup.Tomcat;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import reactor.test.StepVerifier;
-
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.web.client.RestClient;
-import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
-import org.springframework.web.servlet.DispatcherServlet;
-import org.springframework.web.servlet.config.annotation.EnableWebMvc;
-import org.springframework.web.servlet.function.RouterFunction;
-import org.springframework.web.servlet.function.ServerResponse;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-import static org.awaitility.Awaitility.await;
-
-@Deprecated
-public class WebMvcSseIntegrationDeprecatedTests {
-
- private static final int PORT = 8183;
-
- private static final String MESSAGE_ENDPOINT = "/mcp/message";
-
- private WebMvcSseServerTransport mcpServerTransport;
-
- McpClient.SyncSpec clientBuilder;
-
- @Configuration
- @EnableWebMvc
- static class TestConfig {
-
- @Bean
- public WebMvcSseServerTransport webMvcSseServerTransport() {
- return new WebMvcSseServerTransport(new ObjectMapper(), MESSAGE_ENDPOINT);
- }
-
- @Bean
- public RouterFunction routerFunction(WebMvcSseServerTransport transport) {
- return transport.getRouterFunction();
- }
-
- }
-
- private Tomcat tomcat;
-
- private AnnotationConfigWebApplicationContext appContext;
-
- @BeforeEach
- public void before() {
-
- // Set up Tomcat first
- tomcat = new Tomcat();
- tomcat.setPort(PORT);
-
- // Set Tomcat base directory to java.io.tmpdir to avoid permission issues
- String baseDir = System.getProperty("java.io.tmpdir");
- tomcat.setBaseDir(baseDir);
-
- // Use the same directory for document base
- Context context = tomcat.addContext("", baseDir);
-
- // Create and configure Spring WebMvc context
- appContext = new AnnotationConfigWebApplicationContext();
- appContext.register(TestConfig.class);
- appContext.setServletContext(context.getServletContext());
- appContext.refresh();
-
- // Get the transport from Spring context
- mcpServerTransport = appContext.getBean(WebMvcSseServerTransport.class);
-
- // Create DispatcherServlet with our Spring context
- DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext);
- // dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
-
- // Add servlet to Tomcat and get the wrapper
- var wrapper = Tomcat.addServlet(context, "dispatcherServlet", dispatcherServlet);
- wrapper.setLoadOnStartup(1);
- wrapper.setAsyncSupported(true);
- context.addServletMappingDecoded("/*", "dispatcherServlet");
-
- try {
- // Configure and start the connector with async support
- var connector = tomcat.getConnector();
- connector.setAsyncTimeout(3000); // 3 seconds timeout for async requests
- tomcat.start();
- assertThat(tomcat.getServer().getState() == LifecycleState.STARTED);
- }
- catch (Exception e) {
- throw new RuntimeException("Failed to start Tomcat", e);
- }
-
- this.clientBuilder = McpClient.sync(new HttpClientSseClientTransport("http://localhost:" + PORT));
- }
-
- @AfterEach
- public void after() {
- if (mcpServerTransport != null) {
- mcpServerTransport.closeGracefully().block();
- }
- if (appContext != null) {
- appContext.close();
- }
- if (tomcat != null) {
- try {
- tomcat.stop();
- tomcat.destroy();
- }
- catch (LifecycleException e) {
- throw new RuntimeException("Failed to stop Tomcat", e);
- }
- }
- }
-
- // ---------------------------------------
- // Sampling Tests
- // ---------------------------------------
- @Test
- void testCreateMessageWithoutInitialization() {
- var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build();
-
- var messages = List
- .of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Test message")));
- var modelPrefs = new McpSchema.ModelPreferences(List.of(), 1.0, 1.0, 1.0);
-
- var request = new McpSchema.CreateMessageRequest(messages, modelPrefs, null,
- McpSchema.CreateMessageRequest.ContextInclusionStrategy.NONE, null, 100, List.of(), Map.of());
-
- StepVerifier.create(mcpAsyncServer.createMessage(request)).verifyErrorSatisfies(error -> {
- assertThat(error).isInstanceOf(McpError.class)
- .hasMessage("Client must be initialized. Call the initialize method first!");
- });
- }
-
- @Test
- void testCreateMessageWithoutSamplingCapabilities() {
-
- var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build();
-
- var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")).build();
-
- InitializeResult initResult = client.initialize();
- assertThat(initResult).isNotNull();
-
- var messages = List
- .of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Test message")));
- var modelPrefs = new McpSchema.ModelPreferences(List.of(), 1.0, 1.0, 1.0);
-
- var request = new McpSchema.CreateMessageRequest(messages, modelPrefs, null,
- McpSchema.CreateMessageRequest.ContextInclusionStrategy.NONE, null, 100, List.of(), Map.of());
-
- StepVerifier.create(mcpAsyncServer.createMessage(request)).verifyErrorSatisfies(error -> {
- assertThat(error).isInstanceOf(McpError.class)
- .hasMessage("Client must be configured with sampling capabilities");
- });
- }
-
- @Test
- void testCreateMessageSuccess() throws InterruptedException {
-
- var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build();
-
- Function samplingHandler = request -> {
- assertThat(request.messages()).hasSize(1);
- assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class);
-
- return new CreateMessageResult(Role.USER, new McpSchema.TextContent("Test message"), "MockModelName",
- CreateMessageResult.StopReason.STOP_SEQUENCE);
- };
-
- var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0"))
- .capabilities(ClientCapabilities.builder().sampling().build())
- .sampling(samplingHandler)
- .build();
-
- InitializeResult initResult = client.initialize();
- assertThat(initResult).isNotNull();
-
- var messages = List
- .of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Test message")));
- var modelPrefs = new McpSchema.ModelPreferences(List.of(), 1.0, 1.0, 1.0);
-
- var request = new McpSchema.CreateMessageRequest(messages, modelPrefs, null,
- McpSchema.CreateMessageRequest.ContextInclusionStrategy.NONE, null, 100, List.of(), Map.of());
-
- StepVerifier.create(mcpAsyncServer.createMessage(request)).consumeNextWith(result -> {
- assertThat(result).isNotNull();
- assertThat(result.role()).isEqualTo(Role.USER);
- assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class);
- assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message");
- assertThat(result.model()).isEqualTo("MockModelName");
- assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE);
- }).verifyComplete();
- }
-
- // ---------------------------------------
- // Roots Tests
- // ---------------------------------------
- @Test
- void testRootsSuccess() {
- List roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
-
- AtomicReference> rootsRef = new AtomicReference<>();
- var mcpServer = McpServer.sync(mcpServerTransport)
- .rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
- .build();
-
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
- .roots(roots)
- .build();
-
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
-
- assertThat(rootsRef.get()).isNull();
-
- assertThat(mcpServer.listRoots().roots()).containsAll(roots);
-
- mcpClient.rootsListChangedNotification();
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(roots);
- });
-
- // Remove a root
- mcpClient.removeRoot(roots.get(0).uri());
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(roots.get(1)));
- });
-
- // Add a new root
- var root3 = new Root("uri3://", "root3");
- mcpClient.addRoot(root3);
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3));
- });
-
- mcpClient.close();
- mcpServer.close();
- }
-
- @Test
- void testRootsWithoutCapability() {
- var mcpServer = McpServer.sync(mcpServerTransport).rootsChangeConsumer(rootsUpdate -> {
- }).build();
-
- // Create client without roots capability
- // No roots capability
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build();
-
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
-
- // Attempt to list roots should fail
- assertThatThrownBy(() -> mcpServer.listRoots().roots()).isInstanceOf(McpError.class)
- .hasMessage("Roots not supported");
-
- mcpClient.close();
- mcpServer.close();
- }
-
- @Test
- void testRootsWithEmptyRootsList() {
- AtomicReference> rootsRef = new AtomicReference<>();
- var mcpServer = McpServer.sync(mcpServerTransport)
- .rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
- .build();
-
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
- .roots(List.of()) // Empty roots list
- .build();
-
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
-
- mcpClient.rootsListChangedNotification();
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).isEmpty();
- });
-
- mcpClient.close();
- mcpServer.close();
- }
-
- @Test
- void testRootsWithMultipleConsumers() {
- List roots = List.of(new Root("uri1://", "root1"));
-
- AtomicReference> rootsRef1 = new AtomicReference<>();
- AtomicReference> rootsRef2 = new AtomicReference<>();
-
- var mcpServer = McpServer.sync(mcpServerTransport)
- .rootsChangeConsumer(rootsUpdate -> rootsRef1.set(rootsUpdate))
- .rootsChangeConsumer(rootsUpdate -> rootsRef2.set(rootsUpdate))
- .build();
-
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
- .roots(roots)
- .build();
-
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
-
- mcpClient.rootsListChangedNotification();
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef1.get()).containsAll(roots);
- assertThat(rootsRef2.get()).containsAll(roots);
- });
-
- mcpClient.close();
- mcpServer.close();
- }
-
- @Test
- void testRootsServerCloseWithActiveSubscription() {
- List roots = List.of(new Root("uri1://", "root1"));
-
- AtomicReference> rootsRef = new AtomicReference<>();
- var mcpServer = McpServer.sync(mcpServerTransport)
- .rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate))
- .build();
-
- var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build())
- .roots(roots)
- .build();
-
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
-
- mcpClient.rootsListChangedNotification();
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(roots);
- });
-
- // Close server while subscription is active
- mcpServer.close();
-
- // Verify client can handle server closure gracefully
- mcpClient.close();
- }
-
- // ---------------------------------------
- // Tools Tests
- // ---------------------------------------
-
- String emptyJsonSchema = """
- {
- "$schema": "http://json-schema.org/draft-07/schema#",
- "type": "object",
- "properties": {}
- }
- """;
-
- @Test
- void testToolCallSuccess() {
-
- var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
- McpServerFeatures.SyncToolRegistration tool1 = new McpServerFeatures.SyncToolRegistration(
- new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), request -> {
- // perform a blocking call to a remote service
- String response = RestClient.create()
- .get()
- .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
- .retrieve()
- .body(String.class);
- assertThat(response).isNotBlank();
- return callResponse;
- });
-
- var mcpServer = McpServer.sync(mcpServerTransport)
- .capabilities(ServerCapabilities.builder().tools(true).build())
- .tools(tool1)
- .build();
-
- var mcpClient = clientBuilder.build();
-
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
-
- assertThat(mcpClient.listTools().tools()).contains(tool1.tool());
-
- CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of()));
-
- assertThat(response).isNotNull();
- assertThat(response).isEqualTo(callResponse);
-
- mcpClient.close();
- mcpServer.close();
- }
-
- @Test
- void testToolListChangeHandlingSuccess() {
-
- var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
- McpServerFeatures.SyncToolRegistration tool1 = new McpServerFeatures.SyncToolRegistration(
- new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), request -> {
- // perform a blocking call to a remote service
- String response = RestClient.create()
- .get()
- .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
- .retrieve()
- .body(String.class);
- assertThat(response).isNotBlank();
- return callResponse;
- });
-
- var mcpServer = McpServer.sync(mcpServerTransport)
- .capabilities(ServerCapabilities.builder().tools(true).build())
- .tools(tool1)
- .build();
-
- AtomicReference> rootsRef = new AtomicReference<>();
- var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> {
- // perform a blocking call to a remote service
- String response = RestClient.create()
- .get()
- .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md")
- .retrieve()
- .body(String.class);
- assertThat(response).isNotBlank();
- rootsRef.set(toolsUpdate);
- }).build();
-
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
-
- assertThat(rootsRef.get()).isNull();
-
- assertThat(mcpClient.listTools().tools()).contains(tool1.tool());
-
- mcpServer.notifyToolsListChanged();
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(tool1.tool()));
- });
-
- // Remove a tool
- mcpServer.removeTool("tool1");
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).isEmpty();
- });
-
- // Add a new tool
- McpServerFeatures.SyncToolRegistration tool2 = new McpServerFeatures.SyncToolRegistration(
- new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), request -> callResponse);
-
- mcpServer.addTool(tool2);
-
- await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
- assertThat(rootsRef.get()).containsAll(List.of(tool2.tool()));
- });
-
- mcpClient.close();
- mcpServer.close();
- }
-
- @Test
- void testInitialize() {
-
- var mcpServer = McpServer.sync(mcpServerTransport).build();
-
- var mcpClient = clientBuilder.build();
-
- InitializeResult initResult = mcpClient.initialize();
- assertThat(initResult).isNotNull();
-
- mcpClient.close();
- mcpServer.close();
- }
-
-}
diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportDeprecatedTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportDeprecatedTests.java
deleted file mode 100644
index 8656665ed..000000000
--- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportDeprecatedTests.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * Copyright 2024-2024 the original author or authors.
- */
-
-package io.modelcontextprotocol.server;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import io.modelcontextprotocol.server.transport.WebMvcSseServerTransport;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
-import org.apache.catalina.Context;
-import org.apache.catalina.LifecycleException;
-import org.apache.catalina.startup.Tomcat;
-import org.junit.jupiter.api.Timeout;
-
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
-import org.springframework.web.servlet.DispatcherServlet;
-import org.springframework.web.servlet.config.annotation.EnableWebMvc;
-import org.springframework.web.servlet.function.RouterFunction;
-import org.springframework.web.servlet.function.ServerResponse;
-
-@Deprecated
-@Timeout(15)
-class WebMvcSseSyncServerTransportDeprecatedTests extends AbstractMcpSyncServerDeprecatedTests {
-
- private static final String MESSAGE_ENDPOINT = "/mcp/message";
-
- private static final int PORT = 8181;
-
- private Tomcat tomcat;
-
- private WebMvcSseServerTransport transport;
-
- @Configuration
- @EnableWebMvc
- static class TestConfig {
-
- @Bean
- public WebMvcSseServerTransport webMvcSseServerTransport() {
- return new WebMvcSseServerTransport(new ObjectMapper(), MESSAGE_ENDPOINT);
- }
-
- @Bean
- public RouterFunction routerFunction(WebMvcSseServerTransport transport) {
- return transport.getRouterFunction();
- }
-
- }
-
- private AnnotationConfigWebApplicationContext appContext;
-
- @Override
- protected ServerMcpTransport createMcpTransport() {
- // Set up Tomcat first
- tomcat = new Tomcat();
- tomcat.setPort(PORT);
-
- // Set Tomcat base directory to java.io.tmpdir to avoid permission issues
- String baseDir = System.getProperty("java.io.tmpdir");
- tomcat.setBaseDir(baseDir);
-
- // Use the same directory for document base
- Context context = tomcat.addContext("", baseDir);
-
- // Create and configure Spring WebMvc context
- appContext = new AnnotationConfigWebApplicationContext();
- appContext.register(TestConfig.class);
- appContext.setServletContext(context.getServletContext());
- appContext.refresh();
-
- // Get the transport from Spring context
- transport = appContext.getBean(WebMvcSseServerTransport.class);
-
- // Create DispatcherServlet with our Spring context
- DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext);
- // dispatcherServlet.setThrowExceptionIfNoHandlerFound(true);
-
- // Add servlet to Tomcat and get the wrapper
- var wrapper = Tomcat.addServlet(context, "dispatcherServlet", dispatcherServlet);
- wrapper.setLoadOnStartup(1);
- context.addServletMappingDecoded("/*", "dispatcherServlet");
-
- try {
- tomcat.start();
- tomcat.getConnector(); // Create and start the connector
- }
- catch (LifecycleException e) {
- throw new RuntimeException("Failed to start Tomcat", e);
- }
-
- return transport;
- }
-
- @Override
- protected void onStart() {
- }
-
- @Override
- protected void onClose() {
- if (transport != null) {
- transport.closeGracefully().block();
- }
- if (appContext != null) {
- appContext.close();
- }
- if (tomcat != null) {
- try {
- tomcat.stop();
- tomcat.destroy();
- }
- catch (LifecycleException e) {
- throw new RuntimeException("Failed to stop Tomcat", e);
- }
- }
- }
-
-}
diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java b/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java
index cef3fb9fa..5484a63c2 100644
--- a/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java
+++ b/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java
@@ -15,15 +15,18 @@
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
+import io.modelcontextprotocol.spec.McpServerTransport;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
/**
- * A mock implementation of the {@link McpClientTransport} and {@link ServerMcpTransport}
+ * A mock implementation of the {@link McpClientTransport} and {@link McpServerTransport}
* interfaces.
+ *
+ * @deprecated not used. to be removed in the future.
*/
-public class MockMcpTransport implements McpClientTransport, ServerMcpTransport {
+@Deprecated
+public class MockMcpTransport implements McpClientTransport, McpServerTransport {
private final Sinks.Many inbound = Sinks.many().unicast().onBackpressureBuffer();
diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerDeprecatedTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerDeprecatedTests.java
deleted file mode 100644
index 005d78f25..000000000
--- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerDeprecatedTests.java
+++ /dev/null
@@ -1,465 +0,0 @@
-/*
- * Copyright 2024-2024 the original author or authors.
- */
-
-package io.modelcontextprotocol.server;
-
-import java.time.Duration;
-import java.util.List;
-
-import io.modelcontextprotocol.spec.McpError;
-import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
-import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
-import io.modelcontextprotocol.spec.McpSchema.Prompt;
-import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
-import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
-import io.modelcontextprotocol.spec.McpSchema.Resource;
-import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
-import io.modelcontextprotocol.spec.McpSchema.Tool;
-import io.modelcontextprotocol.spec.McpTransport;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import reactor.core.publisher.Mono;
-import reactor.test.StepVerifier;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatCode;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-/**
- * Test suite for the {@link McpAsyncServer} that can be used with different
- * {@link McpTransport} implementations.
- *
- * @author Christian Tzolov
- */
-@Deprecated
-public abstract class AbstractMcpAsyncServerDeprecatedTests {
-
- private static final String TEST_TOOL_NAME = "test-tool";
-
- private static final String TEST_RESOURCE_URI = "test://resource";
-
- private static final String TEST_PROMPT_NAME = "test-prompt";
-
- abstract protected ServerMcpTransport createMcpTransport();
-
- protected void onStart() {
- }
-
- protected void onClose() {
- }
-
- @BeforeEach
- void setUp() {
- }
-
- @AfterEach
- void tearDown() {
- onClose();
- }
-
- // ---------------------------------------
- // Server Lifecycle Tests
- // ---------------------------------------
-
- @Test
- void testConstructorWithInvalidArguments() {
- assertThatThrownBy(() -> McpServer.async((ServerMcpTransport) null))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Transport must not be null");
-
- assertThatThrownBy(() -> McpServer.async(createMcpTransport()).serverInfo((McpSchema.Implementation) null))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Server info must not be null");
- }
-
- @Test
- void testGracefulShutdown() {
- var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- StepVerifier.create(mcpAsyncServer.closeGracefully()).verifyComplete();
- }
-
- @Test
- void testImmediateClose() {
- var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- assertThatCode(() -> mcpAsyncServer.close()).doesNotThrowAnyException();
- }
-
- // ---------------------------------------
- // Tools Tests
- // ---------------------------------------
- String emptyJsonSchema = """
- {
- "$schema": "http://json-schema.org/draft-07/schema#",
- "type": "object",
- "properties": {}
- }
- """;
-
- @Test
- void testAddTool() {
- Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
- var mcpAsyncServer = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().tools(true).build())
- .build();
-
- StepVerifier.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolRegistration(newTool,
- args -> Mono.just(new CallToolResult(List.of(), false)))))
- .verifyComplete();
-
- assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
- }
-
- @Test
- void testAddDuplicateTool() {
- Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
-
- var mcpAsyncServer = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().tools(true).build())
- .tool(duplicateTool, args -> Mono.just(new CallToolResult(List.of(), false)))
- .build();
-
- StepVerifier.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolRegistration(duplicateTool,
- args -> Mono.just(new CallToolResult(List.of(), false)))))
- .verifyErrorSatisfies(error -> {
- assertThat(error).isInstanceOf(McpError.class)
- .hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
- });
-
- assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
- }
-
- @Test
- void testRemoveTool() {
- Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
-
- var mcpAsyncServer = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().tools(true).build())
- .tool(too, args -> Mono.just(new CallToolResult(List.of(), false)))
- .build();
-
- StepVerifier.create(mcpAsyncServer.removeTool(TEST_TOOL_NAME)).verifyComplete();
-
- assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
- }
-
- @Test
- void testRemoveNonexistentTool() {
- var mcpAsyncServer = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().tools(true).build())
- .build();
-
- StepVerifier.create(mcpAsyncServer.removeTool("nonexistent-tool")).verifyErrorSatisfies(error -> {
- assertThat(error).isInstanceOf(McpError.class).hasMessage("Tool with name 'nonexistent-tool' not found");
- });
-
- assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
- }
-
- @Test
- void testNotifyToolsListChanged() {
- Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
-
- var mcpAsyncServer = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().tools(true).build())
- .tool(too, args -> Mono.just(new CallToolResult(List.of(), false)))
- .build();
-
- StepVerifier.create(mcpAsyncServer.notifyToolsListChanged()).verifyComplete();
-
- assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
- }
-
- // ---------------------------------------
- // Resources Tests
- // ---------------------------------------
-
- @Test
- void testNotifyResourcesListChanged() {
- var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- StepVerifier.create(mcpAsyncServer.notifyResourcesListChanged()).verifyComplete();
-
- assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
- }
-
- @Test
- void testAddResource() {
- var mcpAsyncServer = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().resources(true, false).build())
- .build();
-
- Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
- null);
- McpServerFeatures.AsyncResourceRegistration registration = new McpServerFeatures.AsyncResourceRegistration(
- resource, req -> Mono.just(new ReadResourceResult(List.of())));
-
- StepVerifier.create(mcpAsyncServer.addResource(registration)).verifyComplete();
-
- assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
- }
-
- @Test
- void testAddResourceWithNullRegistration() {
- var mcpAsyncServer = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().resources(true, false).build())
- .build();
-
- StepVerifier.create(mcpAsyncServer.addResource((McpServerFeatures.AsyncResourceRegistration) null))
- .verifyErrorSatisfies(error -> {
- assertThat(error).isInstanceOf(McpError.class).hasMessage("Resource must not be null");
- });
-
- assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
- }
-
- @Test
- void testAddResourceWithoutCapability() {
- // Create a server without resource capabilities
- McpAsyncServer serverWithoutResources = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .build();
-
- Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
- null);
- McpServerFeatures.AsyncResourceRegistration registration = new McpServerFeatures.AsyncResourceRegistration(
- resource, req -> Mono.just(new ReadResourceResult(List.of())));
-
- StepVerifier.create(serverWithoutResources.addResource(registration)).verifyErrorSatisfies(error -> {
- assertThat(error).isInstanceOf(McpError.class)
- .hasMessage("Server must be configured with resource capabilities");
- });
- }
-
- @Test
- void testRemoveResourceWithoutCapability() {
- // Create a server without resource capabilities
- McpAsyncServer serverWithoutResources = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .build();
-
- StepVerifier.create(serverWithoutResources.removeResource(TEST_RESOURCE_URI)).verifyErrorSatisfies(error -> {
- assertThat(error).isInstanceOf(McpError.class)
- .hasMessage("Server must be configured with resource capabilities");
- });
- }
-
- // ---------------------------------------
- // Prompts Tests
- // ---------------------------------------
-
- @Test
- void testNotifyPromptsListChanged() {
- var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- StepVerifier.create(mcpAsyncServer.notifyPromptsListChanged()).verifyComplete();
-
- assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
- }
-
- @Test
- void testAddPromptWithNullRegistration() {
- var mcpAsyncServer = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().prompts(false).build())
- .build();
-
- StepVerifier.create(mcpAsyncServer.addPrompt((McpServerFeatures.AsyncPromptRegistration) null))
- .verifyErrorSatisfies(error -> {
- assertThat(error).isInstanceOf(McpError.class).hasMessage("Prompt registration must not be null");
- });
- }
-
- @Test
- void testAddPromptWithoutCapability() {
- // Create a server without prompt capabilities
- McpAsyncServer serverWithoutPrompts = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .build();
-
- Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
- McpServerFeatures.AsyncPromptRegistration registration = new McpServerFeatures.AsyncPromptRegistration(prompt,
- req -> Mono.just(new GetPromptResult("Test prompt description", List
- .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))));
-
- StepVerifier.create(serverWithoutPrompts.addPrompt(registration)).verifyErrorSatisfies(error -> {
- assertThat(error).isInstanceOf(McpError.class)
- .hasMessage("Server must be configured with prompt capabilities");
- });
- }
-
- @Test
- void testRemovePromptWithoutCapability() {
- // Create a server without prompt capabilities
- McpAsyncServer serverWithoutPrompts = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .build();
-
- StepVerifier.create(serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).verifyErrorSatisfies(error -> {
- assertThat(error).isInstanceOf(McpError.class)
- .hasMessage("Server must be configured with prompt capabilities");
- });
- }
-
- @Test
- void testRemovePrompt() {
- String TEST_PROMPT_NAME_TO_REMOVE = "TEST_PROMPT_NAME678";
-
- Prompt prompt = new Prompt(TEST_PROMPT_NAME_TO_REMOVE, "Test Prompt", List.of());
- McpServerFeatures.AsyncPromptRegistration registration = new McpServerFeatures.AsyncPromptRegistration(prompt,
- req -> Mono.just(new GetPromptResult("Test prompt description", List
- .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))));
-
- var mcpAsyncServer = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().prompts(true).build())
- .prompts(registration)
- .build();
-
- StepVerifier.create(mcpAsyncServer.removePrompt(TEST_PROMPT_NAME_TO_REMOVE)).verifyComplete();
-
- assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException();
- }
-
- @Test
- void testRemoveNonexistentPrompt() {
- var mcpAsyncServer2 = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().prompts(true).build())
- .build();
-
- StepVerifier.create(mcpAsyncServer2.removePrompt("nonexistent-prompt")).verifyErrorSatisfies(error -> {
- assertThat(error).isInstanceOf(McpError.class)
- .hasMessage("Prompt with name 'nonexistent-prompt' not found");
- });
-
- assertThatCode(() -> mcpAsyncServer2.closeGracefully().block(Duration.ofSeconds(10)))
- .doesNotThrowAnyException();
- }
-
- // ---------------------------------------
- // Roots Tests
- // ---------------------------------------
-
- @Test
- void testRootsChangeConsumers() {
- // Test with single consumer
- var rootsReceived = new McpSchema.Root[1];
- var consumerCalled = new boolean[1];
-
- var singleConsumerServer = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .rootsChangeConsumers(List.of(roots -> Mono.fromRunnable(() -> {
- consumerCalled[0] = true;
- if (!roots.isEmpty()) {
- rootsReceived[0] = roots.get(0);
- }
- })))
- .build();
-
- assertThat(singleConsumerServer).isNotNull();
- assertThatCode(() -> singleConsumerServer.closeGracefully().block(Duration.ofSeconds(10)))
- .doesNotThrowAnyException();
- onClose();
-
- // Test with multiple consumers
- var consumer1Called = new boolean[1];
- var consumer2Called = new boolean[1];
- var rootsContent = new List[1];
-
- var multipleConsumersServer = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .rootsChangeConsumers(List.of(roots -> Mono.fromRunnable(() -> {
- consumer1Called[0] = true;
- rootsContent[0] = roots;
- }), roots -> Mono.fromRunnable(() -> consumer2Called[0] = true)))
- .build();
-
- assertThat(multipleConsumersServer).isNotNull();
- assertThatCode(() -> multipleConsumersServer.closeGracefully().block(Duration.ofSeconds(10)))
- .doesNotThrowAnyException();
- onClose();
-
- // Test error handling
- var errorHandlingServer = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .rootsChangeConsumers(List.of(roots -> {
- throw new RuntimeException("Test error");
- }))
- .build();
-
- assertThat(errorHandlingServer).isNotNull();
- assertThatCode(() -> errorHandlingServer.closeGracefully().block(Duration.ofSeconds(10)))
- .doesNotThrowAnyException();
- onClose();
-
- // Test without consumers
- var noConsumersServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- assertThat(noConsumersServer).isNotNull();
- assertThatCode(() -> noConsumersServer.closeGracefully().block(Duration.ofSeconds(10)))
- .doesNotThrowAnyException();
- }
-
- // ---------------------------------------
- // Logging Tests
- // ---------------------------------------
-
- @Test
- void testLoggingLevels() {
- var mcpAsyncServer = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().logging().build())
- .build();
-
- // Test all logging levels
- for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
- var notification = McpSchema.LoggingMessageNotification.builder()
- .level(level)
- .logger("test-logger")
- .data("Test message with level " + level)
- .build();
-
- StepVerifier.create(mcpAsyncServer.loggingNotification(notification)).verifyComplete();
- }
- }
-
- @Test
- void testLoggingWithoutCapability() {
- var mcpAsyncServer = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().build()) // No logging capability
- .build();
-
- var notification = McpSchema.LoggingMessageNotification.builder()
- .level(McpSchema.LoggingLevel.INFO)
- .logger("test-logger")
- .data("Test log message")
- .build();
-
- StepVerifier.create(mcpAsyncServer.loggingNotification(notification)).verifyComplete();
- }
-
- @Test
- void testLoggingWithNullNotification() {
- var mcpAsyncServer = McpServer.async(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().logging().build())
- .build();
-
- StepVerifier.create(mcpAsyncServer.loggingNotification(null)).verifyError(McpError.class);
- }
-
-}
diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerDeprecatedTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerDeprecatedTests.java
deleted file mode 100644
index c6625acaa..000000000
--- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerDeprecatedTests.java
+++ /dev/null
@@ -1,431 +0,0 @@
-/*
- * Copyright 2024-2024 the original author or authors.
- */
-
-package io.modelcontextprotocol.server;
-
-import java.util.List;
-
-import io.modelcontextprotocol.spec.McpError;
-import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
-import io.modelcontextprotocol.spec.McpSchema.GetPromptResult;
-import io.modelcontextprotocol.spec.McpSchema.Prompt;
-import io.modelcontextprotocol.spec.McpSchema.PromptMessage;
-import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult;
-import io.modelcontextprotocol.spec.McpSchema.Resource;
-import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
-import io.modelcontextprotocol.spec.McpSchema.Tool;
-import io.modelcontextprotocol.spec.McpTransport;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatCode;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-/**
- * Test suite for the {@link McpSyncServer} that can be used with different
- * {@link McpTransport} implementations.
- *
- * @author Christian Tzolov
- */
-public abstract class AbstractMcpSyncServerDeprecatedTests {
-
- private static final String TEST_TOOL_NAME = "test-tool";
-
- private static final String TEST_RESOURCE_URI = "test://resource";
-
- private static final String TEST_PROMPT_NAME = "test-prompt";
-
- abstract protected ServerMcpTransport createMcpTransport();
-
- protected void onStart() {
- }
-
- protected void onClose() {
- }
-
- @BeforeEach
- void setUp() {
- // onStart();
- }
-
- @AfterEach
- void tearDown() {
- onClose();
- }
-
- // ---------------------------------------
- // Server Lifecycle Tests
- // ---------------------------------------
-
- @Test
- void testConstructorWithInvalidArguments() {
- assertThatThrownBy(() -> McpServer.sync((ServerMcpTransport) null)).isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Transport must not be null");
-
- assertThatThrownBy(() -> McpServer.sync(createMcpTransport()).serverInfo(null))
- .isInstanceOf(IllegalArgumentException.class)
- .hasMessage("Server info must not be null");
- }
-
- @Test
- void testGracefulShutdown() {
- var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
- }
-
- @Test
- void testImmediateClose() {
- var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- assertThatCode(() -> mcpSyncServer.close()).doesNotThrowAnyException();
- }
-
- @Test
- void testGetAsyncServer() {
- var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- assertThat(mcpSyncServer.getAsyncServer()).isNotNull();
-
- assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
- }
-
- // ---------------------------------------
- // Tools Tests
- // ---------------------------------------
-
- String emptyJsonSchema = """
- {
- "$schema": "http://json-schema.org/draft-07/schema#",
- "type": "object",
- "properties": {}
- }
- """;
-
- @Test
- void testAddTool() {
- var mcpSyncServer = McpServer.sync(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().tools(true).build())
- .build();
-
- Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
- assertThatCode(() -> mcpSyncServer
- .addTool(new McpServerFeatures.SyncToolRegistration(newTool, args -> new CallToolResult(List.of(), false))))
- .doesNotThrowAnyException();
-
- assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
- }
-
- @Test
- void testAddDuplicateTool() {
- Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
-
- var mcpSyncServer = McpServer.sync(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().tools(true).build())
- .tool(duplicateTool, args -> new CallToolResult(List.of(), false))
- .build();
-
- assertThatThrownBy(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolRegistration(duplicateTool,
- args -> new CallToolResult(List.of(), false))))
- .isInstanceOf(McpError.class)
- .hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists");
-
- assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
- }
-
- @Test
- void testRemoveTool() {
- Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", emptyJsonSchema);
-
- var mcpSyncServer = McpServer.sync(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().tools(true).build())
- .tool(tool, args -> new CallToolResult(List.of(), false))
- .build();
-
- assertThatCode(() -> mcpSyncServer.removeTool(TEST_TOOL_NAME)).doesNotThrowAnyException();
-
- assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
- }
-
- @Test
- void testRemoveNonexistentTool() {
- var mcpSyncServer = McpServer.sync(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().tools(true).build())
- .build();
-
- assertThatThrownBy(() -> mcpSyncServer.removeTool("nonexistent-tool")).isInstanceOf(McpError.class)
- .hasMessage("Tool with name 'nonexistent-tool' not found");
-
- assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
- }
-
- @Test
- void testNotifyToolsListChanged() {
- var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- assertThatCode(() -> mcpSyncServer.notifyToolsListChanged()).doesNotThrowAnyException();
-
- assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
- }
-
- // ---------------------------------------
- // Resources Tests
- // ---------------------------------------
-
- @Test
- void testNotifyResourcesListChanged() {
- var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- assertThatCode(() -> mcpSyncServer.notifyResourcesListChanged()).doesNotThrowAnyException();
-
- assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
- }
-
- @Test
- void testAddResource() {
- var mcpSyncServer = McpServer.sync(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().resources(true, false).build())
- .build();
-
- Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
- null);
- McpServerFeatures.SyncResourceRegistration registration = new McpServerFeatures.SyncResourceRegistration(
- resource, req -> new ReadResourceResult(List.of()));
-
- assertThatCode(() -> mcpSyncServer.addResource(registration)).doesNotThrowAnyException();
-
- assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
- }
-
- @Test
- void testAddResourceWithNullRegistration() {
- var mcpSyncServer = McpServer.sync(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().resources(true, false).build())
- .build();
-
- assertThatThrownBy(() -> mcpSyncServer.addResource((McpServerFeatures.SyncResourceRegistration) null))
- .isInstanceOf(McpError.class)
- .hasMessage("Resource must not be null");
-
- assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
- }
-
- @Test
- void testAddResourceWithoutCapability() {
- var serverWithoutResources = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
- null);
- McpServerFeatures.SyncResourceRegistration registration = new McpServerFeatures.SyncResourceRegistration(
- resource, req -> new ReadResourceResult(List.of()));
-
- assertThatThrownBy(() -> serverWithoutResources.addResource(registration)).isInstanceOf(McpError.class)
- .hasMessage("Server must be configured with resource capabilities");
- }
-
- @Test
- void testRemoveResourceWithoutCapability() {
- var serverWithoutResources = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- assertThatThrownBy(() -> serverWithoutResources.removeResource(TEST_RESOURCE_URI)).isInstanceOf(McpError.class)
- .hasMessage("Server must be configured with resource capabilities");
- }
-
- // ---------------------------------------
- // Prompts Tests
- // ---------------------------------------
-
- @Test
- void testNotifyPromptsListChanged() {
- var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- assertThatCode(() -> mcpSyncServer.notifyPromptsListChanged()).doesNotThrowAnyException();
-
- assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
- }
-
- @Test
- void testAddPromptWithNullRegistration() {
- var mcpSyncServer = McpServer.sync(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().prompts(false).build())
- .build();
-
- assertThatThrownBy(() -> mcpSyncServer.addPrompt((McpServerFeatures.SyncPromptRegistration) null))
- .isInstanceOf(McpError.class)
- .hasMessage("Prompt registration must not be null");
- }
-
- @Test
- void testAddPromptWithoutCapability() {
- var serverWithoutPrompts = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
- McpServerFeatures.SyncPromptRegistration registration = new McpServerFeatures.SyncPromptRegistration(prompt,
- req -> new GetPromptResult("Test prompt description", List
- .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
-
- assertThatThrownBy(() -> serverWithoutPrompts.addPrompt(registration)).isInstanceOf(McpError.class)
- .hasMessage("Server must be configured with prompt capabilities");
- }
-
- @Test
- void testRemovePromptWithoutCapability() {
- var serverWithoutPrompts = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- assertThatThrownBy(() -> serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).isInstanceOf(McpError.class)
- .hasMessage("Server must be configured with prompt capabilities");
- }
-
- @Test
- void testRemovePrompt() {
- Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of());
- McpServerFeatures.SyncPromptRegistration registration = new McpServerFeatures.SyncPromptRegistration(prompt,
- req -> new GetPromptResult("Test prompt description", List
- .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
-
- var mcpSyncServer = McpServer.sync(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().prompts(true).build())
- .prompts(registration)
- .build();
-
- assertThatCode(() -> mcpSyncServer.removePrompt(TEST_PROMPT_NAME)).doesNotThrowAnyException();
-
- assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
- }
-
- @Test
- void testRemoveNonexistentPrompt() {
- var mcpSyncServer = McpServer.sync(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().prompts(true).build())
- .build();
-
- assertThatThrownBy(() -> mcpSyncServer.removePrompt("nonexistent-prompt")).isInstanceOf(McpError.class)
- .hasMessage("Prompt with name 'nonexistent-prompt' not found");
-
- assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
- }
-
- // ---------------------------------------
- // Roots Tests
- // ---------------------------------------
-
- @Test
- void testRootsChangeConsumers() {
- // Test with single consumer
- var rootsReceived = new McpSchema.Root[1];
- var consumerCalled = new boolean[1];
-
- var singleConsumerServer = McpServer.sync(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .rootsChangeConsumers(List.of(roots -> {
- consumerCalled[0] = true;
- if (!roots.isEmpty()) {
- rootsReceived[0] = roots.get(0);
- }
- }))
- .build();
-
- assertThat(singleConsumerServer).isNotNull();
- assertThatCode(() -> singleConsumerServer.closeGracefully()).doesNotThrowAnyException();
- onClose();
-
- // Test with multiple consumers
- var consumer1Called = new boolean[1];
- var consumer2Called = new boolean[1];
- var rootsContent = new List[1];
-
- var multipleConsumersServer = McpServer.sync(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .rootsChangeConsumers(List.of(roots -> {
- consumer1Called[0] = true;
- rootsContent[0] = roots;
- }, roots -> consumer2Called[0] = true))
- .build();
-
- assertThat(multipleConsumersServer).isNotNull();
- assertThatCode(() -> multipleConsumersServer.closeGracefully()).doesNotThrowAnyException();
- onClose();
-
- // Test error handling
- var errorHandlingServer = McpServer.sync(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .rootsChangeConsumers(List.of(roots -> {
- throw new RuntimeException("Test error");
- }))
- .build();
-
- assertThat(errorHandlingServer).isNotNull();
- assertThatCode(() -> errorHandlingServer.closeGracefully()).doesNotThrowAnyException();
- onClose();
-
- // Test without consumers
- var noConsumersServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build();
-
- assertThat(noConsumersServer).isNotNull();
- assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException();
- }
-
- // ---------------------------------------
- // Logging Tests
- // ---------------------------------------
-
- @Test
- void testLoggingLevels() {
- var mcpSyncServer = McpServer.sync(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().logging().build())
- .build();
-
- // Test all logging levels
- for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) {
- var notification = McpSchema.LoggingMessageNotification.builder()
- .level(level)
- .logger("test-logger")
- .data("Test message with level " + level)
- .build();
-
- assertThatCode(() -> mcpSyncServer.loggingNotification(notification)).doesNotThrowAnyException();
- }
- }
-
- @Test
- void testLoggingWithoutCapability() {
- var mcpSyncServer = McpServer.sync(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().build()) // No logging capability
- .build();
-
- var notification = McpSchema.LoggingMessageNotification.builder()
- .level(McpSchema.LoggingLevel.INFO)
- .logger("test-logger")
- .data("Test log message")
- .build();
-
- assertThatCode(() -> mcpSyncServer.loggingNotification(notification)).doesNotThrowAnyException();
- }
-
- @Test
- void testLoggingWithNullNotification() {
- var mcpSyncServer = McpServer.sync(createMcpTransport())
- .serverInfo("test-server", "1.0.0")
- .capabilities(ServerCapabilities.builder().logging().build())
- .build();
-
- assertThatThrownBy(() -> mcpSyncServer.loggingNotification(null)).isInstanceOf(McpError.class);
- }
-
-}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
index 9cbef0500..379b47e23 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java
@@ -14,10 +14,10 @@
import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
-import io.modelcontextprotocol.spec.ClientMcpTransport;
import io.modelcontextprotocol.spec.McpClientSession;
import io.modelcontextprotocol.spec.McpClientSession.NotificationHandler;
import io.modelcontextprotocol.spec.McpClientSession.RequestHandler;
+import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
@@ -153,7 +153,7 @@ public class McpAsyncClient {
* @param initializationTimeout the max timeout to await for the client-server
* @param features the MCP Client supported features.
*/
- McpAsyncClient(ClientMcpTransport transport, Duration requestTimeout, Duration initializationTimeout,
+ McpAsyncClient(McpClientTransport transport, Duration requestTimeout, Duration initializationTimeout,
McpClientFeatures.Async features) {
Assert.notNull(transport, "Transport must not be null");
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java
index 9c5f7b015..f7b179616 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java
@@ -12,7 +12,6 @@
import java.util.function.Consumer;
import java.util.function.Function;
-import io.modelcontextprotocol.spec.ClientMcpTransport;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpTransport;
@@ -102,26 +101,6 @@
*/
public interface McpClient {
- /**
- * Start building a synchronous MCP client with the specified transport layer. The
- * synchronous MCP client provides blocking operations. Synchronous clients wait for
- * each operation to complete before returning, making them simpler to use but
- * potentially less performant for concurrent operations. The transport layer handles
- * the low-level communication between client and server using protocols like stdio or
- * Server-Sent Events (SSE).
- * @param transport The transport layer implementation for MCP communication. Common
- * implementations include {@code StdioClientTransport} for stdio-based communication
- * and {@code SseClientTransport} for SSE-based communication.
- * @return A new builder instance for configuring the client
- * @throws IllegalArgumentException if transport is null
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link #sync(McpClientTransport)}
- */
- @Deprecated
- static SyncSpec sync(ClientMcpTransport transport) {
- return new SyncSpec(transport);
- }
-
/**
* Start building a synchronous MCP client with the specified transport layer. The
* synchronous MCP client provides blocking operations. Synchronous clients wait for
@@ -139,26 +118,6 @@ static SyncSpec sync(McpClientTransport transport) {
return new SyncSpec(transport);
}
- /**
- * Start building an asynchronous MCP client with the specified transport layer. The
- * asynchronous MCP client provides non-blocking operations. Asynchronous clients
- * return reactive primitives (Mono/Flux) immediately, allowing for concurrent
- * operations and reactive programming patterns. The transport layer handles the
- * low-level communication between client and server using protocols like stdio or
- * Server-Sent Events (SSE).
- * @param transport The transport layer implementation for MCP communication. Common
- * implementations include {@code StdioClientTransport} for stdio-based communication
- * and {@code SseClientTransport} for SSE-based communication.
- * @return A new builder instance for configuring the client
- * @throws IllegalArgumentException if transport is null
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link #async(McpClientTransport)}
- */
- @Deprecated
- static AsyncSpec async(ClientMcpTransport transport) {
- return new AsyncSpec(transport);
- }
-
/**
* Start building an asynchronous MCP client with the specified transport layer. The
* asynchronous MCP client provides non-blocking operations. Asynchronous clients
@@ -194,7 +153,7 @@ static AsyncSpec async(McpClientTransport transport) {
*/
class SyncSpec {
- private final ClientMcpTransport transport;
+ private final McpClientTransport transport;
private Duration requestTimeout = Duration.ofSeconds(20); // Default timeout
@@ -216,7 +175,7 @@ class SyncSpec {
private Function samplingHandler;
- private SyncSpec(ClientMcpTransport transport) {
+ private SyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
}
@@ -433,7 +392,7 @@ public McpSyncClient build() {
*/
class AsyncSpec {
- private final ClientMcpTransport transport;
+ private final McpClientTransport transport;
private Duration requestTimeout = Duration.ofSeconds(20); // Default timeout
@@ -455,7 +414,7 @@ class AsyncSpec {
private Function> samplingHandler;
- private AsyncSpec(ClientMcpTransport transport) {
+ private AsyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
index ec0a0dfdb..071d76462 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java
@@ -66,12 +66,8 @@ public class McpSyncClient implements AutoCloseable {
* Create a new McpSyncClient with the given delegate.
* @param delegate the asynchronous kernel on top of which this synchronous client
* provides a blocking API.
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link McpClient#sync(McpClientTransport)} to obtain an instance.
*/
- @Deprecated
- // TODO make the constructor package private post-deprecation
- public McpSyncClient(McpAsyncClient delegate) {
+ McpSyncClient(McpAsyncClient delegate) {
Assert.notNull(delegate, "The delegate can not be null");
this.delegate = delegate;
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
index ef69539ad..188b0f48e 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java
@@ -4,9 +4,7 @@
package io.modelcontextprotocol.server;
-import java.time.Duration;
import java.util.HashMap;
-import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -14,21 +12,18 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction;
-import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpClientSession;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpServerTransportProvider;
-import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
-import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
import io.modelcontextprotocol.spec.McpSchema.Tool;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
+import io.modelcontextprotocol.spec.McpServerSession;
+import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -86,19 +81,6 @@ public class McpAsyncServer {
this.delegate = null;
}
- /**
- * Create a new McpAsyncServer with the given transport and capabilities.
- * @param mcpTransport The transport layer implementation for MCP communication.
- * @param features The MCP server supported features.
- * @deprecated This constructor will beremoved in 0.9.0. Use
- * {@link #McpAsyncServer(McpServerTransportProvider, ObjectMapper, McpServerFeatures.Async)}
- * instead.
- */
- @Deprecated
- McpAsyncServer(ServerMcpTransport mcpTransport, McpServerFeatures.Async features) {
- this.delegate = new LegacyAsyncServer(mcpTransport, features);
- }
-
/**
* Create a new McpAsyncServer with the given transport provider and capabilities.
* @param mcpTransportProvider The transport layer implementation for MCP
@@ -127,28 +109,6 @@ public McpSchema.Implementation getServerInfo() {
return this.delegate.getServerInfo();
}
- /**
- * Get the client capabilities that define the supported features and functionality.
- * @return The client capabilities
- * @deprecated This will be removed in 0.9.0. Use
- * {@link McpAsyncServerExchange#getClientCapabilities()}.
- */
- @Deprecated
- public ClientCapabilities getClientCapabilities() {
- return this.delegate.getClientCapabilities();
- }
-
- /**
- * Get the client implementation information.
- * @return The client implementation details
- * @deprecated This will be removed in 0.9.0. Use
- * {@link McpAsyncServerExchange#getClientInfo()}.
- */
- @Deprecated
- public McpSchema.Implementation getClientInfo() {
- return this.delegate.getClientInfo();
- }
-
/**
* Gracefully closes the server, allowing any in-progress operations to complete.
* @return A Mono that completes when the server has been closed
@@ -164,45 +124,9 @@ public void close() {
this.delegate.close();
}
- /**
- * Retrieves the list of all roots provided by the client.
- * @return A Mono that emits the list of roots result.
- * @deprecated This will be removed in 0.9.0. Use
- * {@link McpAsyncServerExchange#listRoots()}.
- */
- @Deprecated
- public Mono listRoots() {
- return this.delegate.listRoots(null);
- }
-
- /**
- * Retrieves a paginated list of roots provided by the server.
- * @param cursor Optional pagination cursor from a previous list request
- * @return A Mono that emits the list of roots result containing
- * @deprecated This will be removed in 0.9.0. Use
- * {@link McpAsyncServerExchange#listRoots(String)}.
- */
- @Deprecated
- public Mono listRoots(String cursor) {
- return this.delegate.listRoots(cursor);
- }
-
// ---------------------------------------
// Tool Management
// ---------------------------------------
-
- /**
- * Add a new tool registration at runtime.
- * @param toolRegistration The tool registration to add
- * @return Mono that completes when clients have been notified of the change
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link #addTool(McpServerFeatures.AsyncToolSpecification)}.
- */
- @Deprecated
- public Mono addTool(McpServerFeatures.AsyncToolRegistration toolRegistration) {
- return this.delegate.addTool(toolRegistration);
- }
-
/**
* Add a new tool specification at runtime.
* @param toolSpecification The tool specification to add
@@ -232,19 +156,6 @@ public Mono notifyToolsListChanged() {
// ---------------------------------------
// Resource Management
// ---------------------------------------
-
- /**
- * Add a new resource handler at runtime.
- * @param resourceHandler The resource handler to add
- * @return Mono that completes when clients have been notified of the change
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link #addResource(McpServerFeatures.AsyncResourceSpecification)}.
- */
- @Deprecated
- public Mono addResource(McpServerFeatures.AsyncResourceRegistration resourceHandler) {
- return this.delegate.addResource(resourceHandler);
- }
-
/**
* Add a new resource handler at runtime.
* @param resourceHandler The resource handler to add
@@ -274,19 +185,6 @@ public Mono notifyResourcesListChanged() {
// ---------------------------------------
// Prompt Management
// ---------------------------------------
-
- /**
- * Add a new prompt handler at runtime.
- * @param promptRegistration The prompt handler to add
- * @return Mono that completes when clients have been notified of the change
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link #addPrompt(McpServerFeatures.AsyncPromptSpecification)}.
- */
- @Deprecated
- public Mono addPrompt(McpServerFeatures.AsyncPromptRegistration promptRegistration) {
- return this.delegate.addPrompt(promptRegistration);
- }
-
/**
* Add a new prompt handler at runtime.
* @param promptSpecification The prompt handler to add
@@ -330,33 +228,6 @@ public Mono loggingNotification(LoggingMessageNotification loggingMessageN
// ---------------------------------------
// Sampling
// ---------------------------------------
-
- /**
- * Create a new message using the sampling capabilities of the client. The Model
- * Context Protocol (MCP) provides a standardized way for servers to request LLM
- * sampling (“completions” or “generations”) from language models via clients. This
- * flow allows clients to maintain control over model access, selection, and
- * permissions while enabling servers to leverage AI capabilities—with no server API
- * keys necessary. Servers can request text or image-based interactions and optionally
- * include context from MCP servers in their prompts.
- * @param createMessageRequest The request to create a new message
- * @return A Mono that completes when the message has been created
- * @throws McpError if the client has not been initialized or does not support
- * sampling capabilities
- * @throws McpError if the client does not support the createMessage method
- * @see McpSchema.CreateMessageRequest
- * @see McpSchema.CreateMessageResult
- * @see Sampling
- * Specification
- * @deprecated This will be removed in 0.9.0. Use
- * {@link McpAsyncServerExchange#createMessage(McpSchema.CreateMessageRequest)}.
- */
- @Deprecated
- public Mono createMessage(McpSchema.CreateMessageRequest createMessageRequest) {
- return this.delegate.createMessage(createMessageRequest);
- }
-
/**
* This method is package-private and used for test only. Should not be called by user
* code.
@@ -492,18 +363,6 @@ public McpSchema.Implementation getServerInfo() {
return this.serverInfo;
}
- @Override
- @Deprecated
- public ClientCapabilities getClientCapabilities() {
- throw new IllegalStateException("This method is deprecated and should not be called");
- }
-
- @Override
- @Deprecated
- public McpSchema.Implementation getClientInfo() {
- throw new IllegalStateException("This method is deprecated and should not be called");
- }
-
@Override
public Mono closeGracefully() {
return this.mcpTransportProvider.closeGracefully();
@@ -514,18 +373,6 @@ public void close() {
this.mcpTransportProvider.close();
}
- @Override
- @Deprecated
- public Mono listRoots() {
- return this.listRoots(null);
- }
-
- @Override
- @Deprecated
- public Mono listRoots(String cursor) {
- return Mono.error(new RuntimeException("Not implemented"));
- }
-
private McpServerSession.NotificationHandler asyncRootsListChangedNotificationHandler(
List, Mono>> rootsChangeConsumers) {
return (exchange, params) -> exchange.listRoots()
@@ -574,11 +421,6 @@ public Mono addTool(McpServerFeatures.AsyncToolSpecification toolSpecifica
});
}
- @Override
- public Mono addTool(McpServerFeatures.AsyncToolRegistration toolRegistration) {
- return this.addTool(toolRegistration.toSpecification());
- }
-
@Override
public Mono removeTool(String toolName) {
if (toolName == null) {
@@ -661,11 +503,6 @@ public Mono addResource(McpServerFeatures.AsyncResourceSpecification resou
});
}
- @Override
- public Mono addResource(McpServerFeatures.AsyncResourceRegistration resourceHandler) {
- return this.addResource(resourceHandler.toSpecification());
- }
-
@Override
public Mono removeResource(String resourceUri) {
if (resourceUri == null) {
@@ -756,11 +593,6 @@ public Mono addPrompt(McpServerFeatures.AsyncPromptSpecification promptSpe
});
}
- @Override
- public Mono addPrompt(McpServerFeatures.AsyncPromptRegistration promptRegistration) {
- return this.addPrompt(promptRegistration.toSpecification());
- }
-
@Override
public Mono removePrompt(String promptName) {
if (promptName == null) {
@@ -859,648 +691,6 @@ private McpServerSession.RequestHandler setLoggerRequestHandler() {
// ---------------------------------------
@Override
- @Deprecated
- public Mono createMessage(McpSchema.CreateMessageRequest createMessageRequest) {
- return Mono.error(new RuntimeException("Not implemented"));
- }
-
- @Override
- void setProtocolVersions(List protocolVersions) {
- this.protocolVersions = protocolVersions;
- }
-
- }
-
- private static final class LegacyAsyncServer extends McpAsyncServer {
-
- /**
- * The MCP session implementation that manages bidirectional JSON-RPC
- * communication between clients and servers.
- */
- private final McpClientSession mcpSession;
-
- private final ServerMcpTransport transport;
-
- private final McpSchema.ServerCapabilities serverCapabilities;
-
- private final McpSchema.Implementation serverInfo;
-
- private McpSchema.ClientCapabilities clientCapabilities;
-
- private McpSchema.Implementation clientInfo;
-
- /**
- * Thread-safe list of tool handlers that can be modified at runtime.
- */
- private final CopyOnWriteArrayList tools = new CopyOnWriteArrayList<>();
-
- private final CopyOnWriteArrayList resourceTemplates = new CopyOnWriteArrayList<>();
-
- private final ConcurrentHashMap resources = new ConcurrentHashMap<>();
-
- private final ConcurrentHashMap prompts = new ConcurrentHashMap<>();
-
- private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
-
- /**
- * Supported protocol versions.
- */
- private List protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
-
- /**
- * Create a new McpAsyncServer with the given transport and capabilities.
- * @param mcpTransport The transport layer implementation for MCP communication.
- * @param features The MCP server supported features.
- */
- LegacyAsyncServer(ServerMcpTransport mcpTransport, McpServerFeatures.Async features) {
-
- this.serverInfo = features.serverInfo();
- this.serverCapabilities = features.serverCapabilities();
- this.tools.addAll(features.tools());
- this.resources.putAll(features.resources());
- this.resourceTemplates.addAll(features.resourceTemplates());
- this.prompts.putAll(features.prompts());
-
- Map> requestHandlers = new HashMap<>();
-
- // Initialize request handlers for standard MCP methods
- requestHandlers.put(McpSchema.METHOD_INITIALIZE, asyncInitializeRequestHandler());
-
- // Ping MUST respond with an empty data, but not NULL response.
- requestHandlers.put(McpSchema.METHOD_PING, (params) -> Mono.just(Map.of()));
-
- // Add tools API handlers if the tool capability is enabled
- if (this.serverCapabilities.tools() != null) {
- requestHandlers.put(McpSchema.METHOD_TOOLS_LIST, toolsListRequestHandler());
- requestHandlers.put(McpSchema.METHOD_TOOLS_CALL, toolsCallRequestHandler());
- }
-
- // Add resources API handlers if provided
- if (this.serverCapabilities.resources() != null) {
- requestHandlers.put(McpSchema.METHOD_RESOURCES_LIST, resourcesListRequestHandler());
- requestHandlers.put(McpSchema.METHOD_RESOURCES_READ, resourcesReadRequestHandler());
- requestHandlers.put(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, resourceTemplateListRequestHandler());
- }
-
- // Add prompts API handlers if provider exists
- if (this.serverCapabilities.prompts() != null) {
- requestHandlers.put(McpSchema.METHOD_PROMPT_LIST, promptsListRequestHandler());
- requestHandlers.put(McpSchema.METHOD_PROMPT_GET, promptsGetRequestHandler());
- }
-
- // Add logging API handlers if the logging capability is enabled
- if (this.serverCapabilities.logging() != null) {
- requestHandlers.put(McpSchema.METHOD_LOGGING_SET_LEVEL, setLoggerRequestHandler());
- }
-
- Map notificationHandlers = new HashMap<>();
-
- notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (params) -> Mono.empty());
-
- List, Mono>> rootsChangeHandlers = features
- .rootsChangeConsumers();
-
- List, Mono>> rootsChangeConsumers = rootsChangeHandlers.stream()
- .map(handler -> (Function, Mono>) (roots) -> handler.apply(null, roots))
- .toList();
-
- if (Utils.isEmpty(rootsChangeConsumers)) {
- rootsChangeConsumers = List.of((roots) -> Mono.fromRunnable(() -> logger.warn(
- "Roots list changed notification, but no consumers provided. Roots list changed: {}", roots)));
- }
-
- notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED,
- asyncRootsListChangedNotificationHandler(rootsChangeConsumers));
-
- this.transport = mcpTransport;
- this.mcpSession = new McpClientSession(Duration.ofSeconds(10), mcpTransport, requestHandlers,
- notificationHandlers);
- }
-
- @Override
- public Mono addTool(McpServerFeatures.AsyncToolSpecification toolSpecification) {
- throw new IllegalArgumentException(
- "McpAsyncServer configured with legacy " + "transport. Use McpServerTransportProvider instead.");
- }
-
- @Override
- public Mono addResource(McpServerFeatures.AsyncResourceSpecification resourceHandler) {
- throw new IllegalArgumentException(
- "McpAsyncServer configured with legacy " + "transport. Use McpServerTransportProvider instead.");
- }
-
- @Override
- public Mono addPrompt(McpServerFeatures.AsyncPromptSpecification promptSpecification) {
- throw new IllegalArgumentException(
- "McpAsyncServer configured with legacy " + "transport. Use McpServerTransportProvider instead.");
- }
-
- // ---------------------------------------
- // Lifecycle Management
- // ---------------------------------------
- private McpClientSession.RequestHandler asyncInitializeRequestHandler() {
- return params -> {
- McpSchema.InitializeRequest initializeRequest = transport.unmarshalFrom(params,
- new TypeReference() {
- });
- this.clientCapabilities = initializeRequest.capabilities();
- this.clientInfo = initializeRequest.clientInfo();
- logger.info("Client initialize request - Protocol: {}, Capabilities: {}, Info: {}",
- initializeRequest.protocolVersion(), initializeRequest.capabilities(),
- initializeRequest.clientInfo());
-
- // The server MUST respond with the highest protocol version it supports
- // if
- // it does not support the requested (e.g. Client) version.
- String serverProtocolVersion = this.protocolVersions.get(this.protocolVersions.size() - 1);
-
- if (this.protocolVersions.contains(initializeRequest.protocolVersion())) {
- // If the server supports the requested protocol version, it MUST
- // respond
- // with the same version.
- serverProtocolVersion = initializeRequest.protocolVersion();
- }
- else {
- logger.warn(
- "Client requested unsupported protocol version: {}, so the server will sugggest the {} version instead",
- initializeRequest.protocolVersion(), serverProtocolVersion);
- }
-
- return Mono.just(new McpSchema.InitializeResult(serverProtocolVersion, this.serverCapabilities,
- this.serverInfo, null));
- };
- }
-
- /**
- * Get the server capabilities that define the supported features and
- * functionality.
- * @return The server capabilities
- */
- public McpSchema.ServerCapabilities getServerCapabilities() {
- return this.serverCapabilities;
- }
-
- /**
- * Get the server implementation information.
- * @return The server implementation details
- */
- public McpSchema.Implementation getServerInfo() {
- return this.serverInfo;
- }
-
- /**
- * Get the client capabilities that define the supported features and
- * functionality.
- * @return The client capabilities
- */
- public ClientCapabilities getClientCapabilities() {
- return this.clientCapabilities;
- }
-
- /**
- * Get the client implementation information.
- * @return The client implementation details
- */
- public McpSchema.Implementation getClientInfo() {
- return this.clientInfo;
- }
-
- /**
- * Gracefully closes the server, allowing any in-progress operations to complete.
- * @return A Mono that completes when the server has been closed
- */
- public Mono closeGracefully() {
- return this.mcpSession.closeGracefully();
- }
-
- /**
- * Close the server immediately.
- */
- public void close() {
- this.mcpSession.close();
- }
-
- private static final TypeReference LIST_ROOTS_RESULT_TYPE_REF = new TypeReference<>() {
- };
-
- /**
- * Retrieves the list of all roots provided by the client.
- * @return A Mono that emits the list of roots result.
- */
- public Mono listRoots() {
- return this.listRoots(null);
- }
-
- /**
- * Retrieves a paginated list of roots provided by the server.
- * @param cursor Optional pagination cursor from a previous list request
- * @return A Mono that emits the list of roots result containing
- */
- public Mono listRoots(String cursor) {
- return this.mcpSession.sendRequest(McpSchema.METHOD_ROOTS_LIST, new McpSchema.PaginatedRequest(cursor),
- LIST_ROOTS_RESULT_TYPE_REF);
- }
-
- private McpClientSession.NotificationHandler asyncRootsListChangedNotificationHandler(
- List, Mono>> rootsChangeConsumers) {
- return params -> listRoots().flatMap(listRootsResult -> Flux.fromIterable(rootsChangeConsumers)
- .flatMap(consumer -> consumer.apply(listRootsResult.roots()))
- .onErrorResume(error -> {
- logger.error("Error handling roots list change notification", error);
- return Mono.empty();
- })
- .then());
- }
-
- // ---------------------------------------
- // Tool Management
- // ---------------------------------------
-
- /**
- * Add a new tool registration at runtime.
- * @param toolRegistration The tool registration to add
- * @return Mono that completes when clients have been notified of the change
- */
- @Override
- public Mono addTool(McpServerFeatures.AsyncToolRegistration toolRegistration) {
- if (toolRegistration == null) {
- return Mono.error(new McpError("Tool registration must not be null"));
- }
- if (toolRegistration.tool() == null) {
- return Mono.error(new McpError("Tool must not be null"));
- }
- if (toolRegistration.call() == null) {
- return Mono.error(new McpError("Tool call handler must not be null"));
- }
- if (this.serverCapabilities.tools() == null) {
- return Mono.error(new McpError("Server must be configured with tool capabilities"));
- }
-
- return Mono.defer(() -> {
- // Check for duplicate tool names
- if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolRegistration.tool().name()))) {
- return Mono
- .error(new McpError("Tool with name '" + toolRegistration.tool().name() + "' already exists"));
- }
-
- this.tools.add(toolRegistration.toSpecification());
- logger.debug("Added tool handler: {}", toolRegistration.tool().name());
-
- if (this.serverCapabilities.tools().listChanged()) {
- return notifyToolsListChanged();
- }
- return Mono.empty();
- });
- }
-
- /**
- * Remove a tool handler at runtime.
- * @param toolName The name of the tool handler to remove
- * @return Mono that completes when clients have been notified of the change
- */
- public Mono removeTool(String toolName) {
- if (toolName == null) {
- return Mono.error(new McpError("Tool name must not be null"));
- }
- if (this.serverCapabilities.tools() == null) {
- return Mono.error(new McpError("Server must be configured with tool capabilities"));
- }
-
- return Mono.defer(() -> {
- boolean removed = this.tools
- .removeIf(toolRegistration -> toolRegistration.tool().name().equals(toolName));
- if (removed) {
- logger.debug("Removed tool handler: {}", toolName);
- if (this.serverCapabilities.tools().listChanged()) {
- return notifyToolsListChanged();
- }
- return Mono.empty();
- }
- return Mono.error(new McpError("Tool with name '" + toolName + "' not found"));
- });
- }
-
- /**
- * Notifies clients that the list of available tools has changed.
- * @return A Mono that completes when all clients have been notified
- */
- public Mono notifyToolsListChanged() {
- return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null);
- }
-
- private McpClientSession.RequestHandler toolsListRequestHandler() {
- return params -> {
- List tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList();
-
- return Mono.just(new McpSchema.ListToolsResult(tools, null));
- };
- }
-
- private McpClientSession.RequestHandler toolsCallRequestHandler() {
- return params -> {
- McpSchema.CallToolRequest callToolRequest = transport.unmarshalFrom(params,
- new TypeReference() {
- });
-
- Optional toolRegistration = this.tools.stream()
- .filter(tr -> callToolRequest.name().equals(tr.tool().name()))
- .findAny();
-
- if (toolRegistration.isEmpty()) {
- return Mono.error(new McpError("Tool not found: " + callToolRequest.name()));
- }
-
- return toolRegistration.map(tool -> tool.call().apply(null, callToolRequest.arguments()))
- .orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name())));
- };
- }
-
- // ---------------------------------------
- // Resource Management
- // ---------------------------------------
-
- /**
- * Add a new resource handler at runtime.
- * @param resourceHandler The resource handler to add
- * @return Mono that completes when clients have been notified of the change
- */
- @Override
- public Mono addResource(McpServerFeatures.AsyncResourceRegistration resourceHandler) {
- if (resourceHandler == null || resourceHandler.resource() == null) {
- return Mono.error(new McpError("Resource must not be null"));
- }
-
- if (this.serverCapabilities.resources() == null) {
- return Mono.error(new McpError("Server must be configured with resource capabilities"));
- }
-
- return Mono.defer(() -> {
- if (this.resources.putIfAbsent(resourceHandler.resource().uri(),
- resourceHandler.toSpecification()) != null) {
- return Mono.error(new McpError(
- "Resource with URI '" + resourceHandler.resource().uri() + "' already exists"));
- }
- logger.debug("Added resource handler: {}", resourceHandler.resource().uri());
- if (this.serverCapabilities.resources().listChanged()) {
- return notifyResourcesListChanged();
- }
- return Mono.empty();
- });
- }
-
- /**
- * Remove a resource handler at runtime.
- * @param resourceUri The URI of the resource handler to remove
- * @return Mono that completes when clients have been notified of the change
- */
- public Mono removeResource(String resourceUri) {
- if (resourceUri == null) {
- return Mono.error(new McpError("Resource URI must not be null"));
- }
- if (this.serverCapabilities.resources() == null) {
- return Mono.error(new McpError("Server must be configured with resource capabilities"));
- }
-
- return Mono.defer(() -> {
- McpServerFeatures.AsyncResourceSpecification removed = this.resources.remove(resourceUri);
- if (removed != null) {
- logger.debug("Removed resource handler: {}", resourceUri);
- if (this.serverCapabilities.resources().listChanged()) {
- return notifyResourcesListChanged();
- }
- return Mono.empty();
- }
- return Mono.error(new McpError("Resource with URI '" + resourceUri + "' not found"));
- });
- }
-
- /**
- * Notifies clients that the list of available resources has changed.
- * @return A Mono that completes when all clients have been notified
- */
- public Mono notifyResourcesListChanged() {
- return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED, null);
- }
-
- private McpClientSession.RequestHandler resourcesListRequestHandler() {
- return params -> {
- var resourceList = this.resources.values()
- .stream()
- .map(McpServerFeatures.AsyncResourceSpecification::resource)
- .toList();
- return Mono.just(new McpSchema.ListResourcesResult(resourceList, null));
- };
- }
-
- private McpClientSession.RequestHandler resourceTemplateListRequestHandler() {
- return params -> Mono.just(new McpSchema.ListResourceTemplatesResult(this.resourceTemplates, null));
-
- }
-
- private McpClientSession.RequestHandler resourcesReadRequestHandler() {
- return params -> {
- McpSchema.ReadResourceRequest resourceRequest = transport.unmarshalFrom(params,
- new TypeReference() {
- });
- var resourceUri = resourceRequest.uri();
- McpServerFeatures.AsyncResourceSpecification registration = this.resources.get(resourceUri);
- if (registration != null) {
- return registration.readHandler().apply(null, resourceRequest);
- }
- return Mono.error(new McpError("Resource not found: " + resourceUri));
- };
- }
-
- // ---------------------------------------
- // Prompt Management
- // ---------------------------------------
-
- /**
- * Add a new prompt handler at runtime.
- * @param promptRegistration The prompt handler to add
- * @return Mono that completes when clients have been notified of the change
- */
- @Override
- public Mono addPrompt(McpServerFeatures.AsyncPromptRegistration promptRegistration) {
- if (promptRegistration == null) {
- return Mono.error(new McpError("Prompt registration must not be null"));
- }
- if (this.serverCapabilities.prompts() == null) {
- return Mono.error(new McpError("Server must be configured with prompt capabilities"));
- }
-
- return Mono.defer(() -> {
- McpServerFeatures.AsyncPromptSpecification registration = this.prompts
- .putIfAbsent(promptRegistration.prompt().name(), promptRegistration.toSpecification());
- if (registration != null) {
- return Mono.error(new McpError(
- "Prompt with name '" + promptRegistration.prompt().name() + "' already exists"));
- }
-
- logger.debug("Added prompt handler: {}", promptRegistration.prompt().name());
-
- // Servers that declared the listChanged capability SHOULD send a
- // notification,
- // when the list of available prompts changes
- if (this.serverCapabilities.prompts().listChanged()) {
- return notifyPromptsListChanged();
- }
- return Mono.empty();
- });
- }
-
- /**
- * Remove a prompt handler at runtime.
- * @param promptName The name of the prompt handler to remove
- * @return Mono that completes when clients have been notified of the change
- */
- public Mono removePrompt(String promptName) {
- if (promptName == null) {
- return Mono.error(new McpError("Prompt name must not be null"));
- }
- if (this.serverCapabilities.prompts() == null) {
- return Mono.error(new McpError("Server must be configured with prompt capabilities"));
- }
-
- return Mono.defer(() -> {
- McpServerFeatures.AsyncPromptSpecification removed = this.prompts.remove(promptName);
-
- if (removed != null) {
- logger.debug("Removed prompt handler: {}", promptName);
- // Servers that declared the listChanged capability SHOULD send a
- // notification, when the list of available prompts changes
- if (this.serverCapabilities.prompts().listChanged()) {
- return this.notifyPromptsListChanged();
- }
- return Mono.empty();
- }
- return Mono.error(new McpError("Prompt with name '" + promptName + "' not found"));
- });
- }
-
- /**
- * Notifies clients that the list of available prompts has changed.
- * @return A Mono that completes when all clients have been notified
- */
- public Mono notifyPromptsListChanged() {
- return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null);
- }
-
- private McpClientSession.RequestHandler promptsListRequestHandler() {
- return params -> {
- // TODO: Implement pagination
- // McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
- // new TypeReference() {
- // });
-
- var promptList = this.prompts.values()
- .stream()
- .map(McpServerFeatures.AsyncPromptSpecification::prompt)
- .toList();
-
- return Mono.just(new McpSchema.ListPromptsResult(promptList, null));
- };
- }
-
- private McpClientSession.RequestHandler promptsGetRequestHandler() {
- return params -> {
- McpSchema.GetPromptRequest promptRequest = transport.unmarshalFrom(params,
- new TypeReference() {
- });
-
- // Implement prompt retrieval logic here
- McpServerFeatures.AsyncPromptSpecification registration = this.prompts.get(promptRequest.name());
- if (registration == null) {
- return Mono.error(new McpError("Prompt not found: " + promptRequest.name()));
- }
-
- return registration.promptHandler().apply(null, promptRequest);
- };
- }
-
- // ---------------------------------------
- // Logging Management
- // ---------------------------------------
-
- /**
- * Send a logging message notification to all connected clients. Messages below
- * the current minimum logging level will be filtered out.
- * @param loggingMessageNotification The logging message to send
- * @return A Mono that completes when the notification has been sent
- */
- public Mono loggingNotification(LoggingMessageNotification loggingMessageNotification) {
-
- if (loggingMessageNotification == null) {
- return Mono.error(new McpError("Logging message must not be null"));
- }
-
- Map params = this.transport.unmarshalFrom(loggingMessageNotification,
- new TypeReference>() {
- });
-
- if (loggingMessageNotification.level().level() < minLoggingLevel.level()) {
- return Mono.empty();
- }
-
- return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_MESSAGE, params);
- }
-
- /**
- * Handles requests to set the minimum logging level. Messages below this level
- * will not be sent.
- * @return A handler that processes logging level change requests
- */
- private McpClientSession.RequestHandler setLoggerRequestHandler() {
- return params -> {
- this.minLoggingLevel = transport.unmarshalFrom(params, new TypeReference() {
- });
-
- return Mono.empty();
- };
- }
-
- // ---------------------------------------
- // Sampling
- // ---------------------------------------
- private static final TypeReference CREATE_MESSAGE_RESULT_TYPE_REF = new TypeReference<>() {
- };
-
- /**
- * Create a new message using the sampling capabilities of the client. The Model
- * Context Protocol (MCP) provides a standardized way for servers to request LLM
- * sampling (“completions” or “generations”) from language models via clients.
- * This flow allows clients to maintain control over model access, selection, and
- * permissions while enabling servers to leverage AI capabilities—with no server
- * API keys necessary. Servers can request text or image-based interactions and
- * optionally include context from MCP servers in their prompts.
- * @param createMessageRequest The request to create a new message
- * @return A Mono that completes when the message has been created
- * @throws McpError if the client has not been initialized or does not support
- * sampling capabilities
- * @throws McpError if the client does not support the createMessage method
- * @see McpSchema.CreateMessageRequest
- * @see McpSchema.CreateMessageResult
- * @see Sampling
- * Specification
- */
- public Mono createMessage(McpSchema.CreateMessageRequest createMessageRequest) {
-
- if (this.clientCapabilities == null) {
- return Mono.error(new McpError("Client must be initialized. Call the initialize method first!"));
- }
- if (this.clientCapabilities.sampling() == null) {
- return Mono.error(new McpError("Client must be configured with sampling capabilities"));
- }
- return this.mcpSession.sendRequest(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, createMessageRequest,
- CREATE_MESSAGE_RESULT_TYPE_REF);
- }
-
- /**
- * This method is package-private and used for test only. Should not be called by
- * user code.
- * @param protocolVersions the Client supported protocol versions.
- */
void setProtocolVersions(List protocolVersions) {
this.protocolVersions = protocolVersions;
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
index d8dfcb018..091efac2f 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java
@@ -11,16 +11,12 @@
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
-import java.util.function.Consumer;
-import java.util.function.Function;
-import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpServerTransportProvider;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
+import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.util.Assert;
import reactor.core.publisher.Mono;
@@ -136,21 +132,6 @@ static SyncSpecification sync(McpServerTransportProvider transportProvider) {
return new SyncSpecification(transportProvider);
}
- /**
- * Starts building a synchronous MCP server that provides blocking operations.
- * Synchronous servers block the current Thread's execution upon each request before
- * giving the control back to the caller, making them simpler to implement but
- * potentially less scalable for concurrent operations.
- * @param transport The transport layer implementation for MCP communication
- * @return A new instance of {@link SyncSpec} for configuring the server.
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link #sync(McpServerTransportProvider)} instead.
- */
- @Deprecated
- static SyncSpec sync(ServerMcpTransport transport) {
- return new SyncSpec(transport);
- }
-
/**
* Starts building an asynchronous MCP server that provides non-blocking operations.
* Asynchronous servers can handle multiple requests concurrently on a single Thread
@@ -163,21 +144,6 @@ static AsyncSpecification async(McpServerTransportProvider transportProvider) {
return new AsyncSpecification(transportProvider);
}
- /**
- * Starts building an asynchronous MCP server that provides non-blocking operations.
- * Asynchronous servers can handle multiple requests concurrently on a single Thread
- * using a functional paradigm with non-blocking server transports, making them more
- * scalable for high-concurrency scenarios but more complex to implement.
- * @param transport The transport layer implementation for MCP communication
- * @return A new instance of {@link AsyncSpec} for configuring the server.
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link #async(McpServerTransportProvider)} instead.
- */
- @Deprecated
- static AsyncSpec async(ServerMcpTransport transport) {
- return new AsyncSpec(transport);
- }
-
/**
* Asynchronous server specification.
*/
@@ -1004,819 +970,4 @@ public McpSyncServer build() {
}
- /**
- * Asynchronous server specification.
- *
- * @deprecated
- */
- @Deprecated
- class AsyncSpec {
-
- private static final McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("mcp-server",
- "1.0.0");
-
- private final ServerMcpTransport transport;
-
- private ObjectMapper objectMapper;
-
- private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
-
- private McpSchema.ServerCapabilities serverCapabilities;
-
- /**
- * The Model Context Protocol (MCP) allows servers to expose tools that can be
- * invoked by language models. Tools enable models to interact with external
- * systems, such as querying databases, calling APIs, or performing computations.
- * Each tool is uniquely identified by a name and includes metadata describing its
- * schema.
- */
- private final List tools = new ArrayList<>();
-
- /**
- * The Model Context Protocol (MCP) provides a standardized way for servers to
- * expose resources to clients. Resources allow servers to share data that
- * provides context to language models, such as files, database schemas, or
- * application-specific information. Each resource is uniquely identified by a
- * URI.
- */
- private final Map resources = new HashMap<>();
-
- private final List resourceTemplates = new ArrayList<>();
-
- /**
- * The Model Context Protocol (MCP) provides a standardized way for servers to
- * expose prompt templates to clients. Prompts allow servers to provide structured
- * messages and instructions for interacting with language models. Clients can
- * discover available prompts, retrieve their contents, and provide arguments to
- * customize them.
- */
- private final Map prompts = new HashMap<>();
-
- private final List, Mono>> rootsChangeConsumers = new ArrayList<>();
-
- private AsyncSpec(ServerMcpTransport transport) {
- Assert.notNull(transport, "Transport must not be null");
- this.transport = transport;
- }
-
- /**
- * Sets the server implementation information that will be shared with clients
- * during connection initialization. This helps with version compatibility,
- * debugging, and server identification.
- * @param serverInfo The server implementation details including name and version.
- * Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if serverInfo is null
- */
- public AsyncSpec serverInfo(McpSchema.Implementation serverInfo) {
- Assert.notNull(serverInfo, "Server info must not be null");
- this.serverInfo = serverInfo;
- return this;
- }
-
- /**
- * Sets the server implementation information using name and version strings. This
- * is a convenience method alternative to
- * {@link #serverInfo(McpSchema.Implementation)}.
- * @param name The server name. Must not be null or empty.
- * @param version The server version. Must not be null or empty.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if name or version is null or empty
- * @see #serverInfo(McpSchema.Implementation)
- */
- public AsyncSpec serverInfo(String name, String version) {
- Assert.hasText(name, "Name must not be null or empty");
- Assert.hasText(version, "Version must not be null or empty");
- this.serverInfo = new McpSchema.Implementation(name, version);
- return this;
- }
-
- /**
- * Sets the server capabilities that will be advertised to clients during
- * connection initialization. Capabilities define what features the server
- * supports, such as:
- *
- * Tool execution
- * Resource access
- * Prompt handling
- * Streaming responses
- * Batch operations
- *
- * @param serverCapabilities The server capabilities configuration. Must not be
- * null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if serverCapabilities is null
- */
- public AsyncSpec capabilities(McpSchema.ServerCapabilities serverCapabilities) {
- this.serverCapabilities = serverCapabilities;
- return this;
- }
-
- /**
- * Adds a single tool with its implementation handler to the server. This is a
- * convenience method for registering individual tools without creating a
- * {@link McpServerFeatures.AsyncToolRegistration} explicitly.
- *
- *
- * Example usage:
{@code
- * .tool(
- * new Tool("calculator", "Performs calculations", schema),
- * args -> Mono.just(new CallToolResult("Result: " + calculate(args)))
- * )
- * }
- * @param tool The tool definition including name, description, and schema. Must
- * not be null.
- * @param handler The function that implements the tool's logic. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if tool or handler is null
- */
- public AsyncSpec tool(McpSchema.Tool tool, Function, Mono> handler) {
- Assert.notNull(tool, "Tool must not be null");
- Assert.notNull(handler, "Handler must not be null");
-
- this.tools.add(new McpServerFeatures.AsyncToolRegistration(tool, handler));
-
- return this;
- }
-
- /**
- * Adds multiple tools with their handlers to the server using a List. This method
- * is useful when tools are dynamically generated or loaded from a configuration
- * source.
- * @param toolRegistrations The list of tool registrations to add. Must not be
- * null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if toolRegistrations is null
- * @see #tools(McpServerFeatures.AsyncToolRegistration...)
- */
- public AsyncSpec tools(List toolRegistrations) {
- Assert.notNull(toolRegistrations, "Tool handlers list must not be null");
- this.tools.addAll(toolRegistrations);
- return this;
- }
-
- /**
- * Adds multiple tools with their handlers to the server using varargs. This
- * method provides a convenient way to register multiple tools inline.
- *
- *
- * Example usage:
{@code
- * .tools(
- * new McpServerFeatures.AsyncToolRegistration(calculatorTool, calculatorHandler),
- * new McpServerFeatures.AsyncToolRegistration(weatherTool, weatherHandler),
- * new McpServerFeatures.AsyncToolRegistration(fileManagerTool, fileManagerHandler)
- * )
- * }
- * @param toolRegistrations The tool registrations to add. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if toolRegistrations is null
- * @see #tools(List)
- */
- public AsyncSpec tools(McpServerFeatures.AsyncToolRegistration... toolRegistrations) {
- for (McpServerFeatures.AsyncToolRegistration tool : toolRegistrations) {
- this.tools.add(tool);
- }
- return this;
- }
-
- /**
- * Registers multiple resources with their handlers using a Map. This method is
- * useful when resources are dynamically generated or loaded from a configuration
- * source.
- * @param resourceRegsitrations Map of resource name to registration. Must not be
- * null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if resourceRegsitrations is null
- * @see #resources(McpServerFeatures.AsyncResourceRegistration...)
- */
- public AsyncSpec resources(Map resourceRegsitrations) {
- Assert.notNull(resourceRegsitrations, "Resource handlers map must not be null");
- this.resources.putAll(resourceRegsitrations);
- return this;
- }
-
- /**
- * Registers multiple resources with their handlers using a List. This method is
- * useful when resources need to be added in bulk from a collection.
- * @param resourceRegsitrations List of resource registrations. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if resourceRegsitrations is null
- * @see #resources(McpServerFeatures.AsyncResourceRegistration...)
- */
- public AsyncSpec resources(List resourceRegsitrations) {
- Assert.notNull(resourceRegsitrations, "Resource handlers list must not be null");
- for (McpServerFeatures.AsyncResourceRegistration resource : resourceRegsitrations) {
- this.resources.put(resource.resource().uri(), resource);
- }
- return this;
- }
-
- /**
- * Registers multiple resources with their handlers using varargs. This method
- * provides a convenient way to register multiple resources inline.
- *
- *
- * Example usage:
{@code
- * .resources(
- * new McpServerFeatures.AsyncResourceRegistration(fileResource, fileHandler),
- * new McpServerFeatures.AsyncResourceRegistration(dbResource, dbHandler),
- * new McpServerFeatures.AsyncResourceRegistration(apiResource, apiHandler)
- * )
- * }
- * @param resourceRegistrations The resource registrations to add. Must not be
- * null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if resourceRegistrations is null
- */
- public AsyncSpec resources(McpServerFeatures.AsyncResourceRegistration... resourceRegistrations) {
- Assert.notNull(resourceRegistrations, "Resource handlers list must not be null");
- for (McpServerFeatures.AsyncResourceRegistration resource : resourceRegistrations) {
- this.resources.put(resource.resource().uri(), resource);
- }
- return this;
- }
-
- /**
- * Sets the resource templates that define patterns for dynamic resource access.
- * Templates use URI patterns with placeholders that can be filled at runtime.
- *
- *
- * Example usage:
{@code
- * .resourceTemplates(
- * new ResourceTemplate("file://{path}", "Access files by path"),
- * new ResourceTemplate("db://{table}/{id}", "Access database records")
- * )
- * }
- * @param resourceTemplates List of resource templates. If null, clears existing
- * templates.
- * @return This builder instance for method chaining
- * @see #resourceTemplates(ResourceTemplate...)
- */
- public AsyncSpec resourceTemplates(List resourceTemplates) {
- this.resourceTemplates.addAll(resourceTemplates);
- return this;
- }
-
- /**
- * Sets the resource templates using varargs for convenience. This is an
- * alternative to {@link #resourceTemplates(List)}.
- * @param resourceTemplates The resource templates to set.
- * @return This builder instance for method chaining
- * @see #resourceTemplates(List)
- */
- public AsyncSpec resourceTemplates(ResourceTemplate... resourceTemplates) {
- for (ResourceTemplate resourceTemplate : resourceTemplates) {
- this.resourceTemplates.add(resourceTemplate);
- }
- return this;
- }
-
- /**
- * Registers multiple prompts with their handlers using a Map. This method is
- * useful when prompts are dynamically generated or loaded from a configuration
- * source.
- *
- *
- * Example usage:
{@code
- * .prompts(Map.of("analysis", new McpServerFeatures.AsyncPromptRegistration(
- * new Prompt("analysis", "Code analysis template"),
- * request -> Mono.just(new GetPromptResult(generateAnalysisPrompt(request)))
- * )));
- * }
- * @param prompts Map of prompt name to registration. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if prompts is null
- */
- public AsyncSpec prompts(Map prompts) {
- this.prompts.putAll(prompts);
- return this;
- }
-
- /**
- * Registers multiple prompts with their handlers using a List. This method is
- * useful when prompts need to be added in bulk from a collection.
- * @param prompts List of prompt registrations. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if prompts is null
- * @see #prompts(McpServerFeatures.AsyncPromptRegistration...)
- */
- public AsyncSpec prompts(List prompts) {
- for (McpServerFeatures.AsyncPromptRegistration prompt : prompts) {
- this.prompts.put(prompt.prompt().name(), prompt);
- }
- return this;
- }
-
- /**
- * Registers multiple prompts with their handlers using varargs. This method
- * provides a convenient way to register multiple prompts inline.
- *
- *
- * Example usage:
{@code
- * .prompts(
- * new McpServerFeatures.AsyncPromptRegistration(analysisPrompt, analysisHandler),
- * new McpServerFeatures.AsyncPromptRegistration(summaryPrompt, summaryHandler),
- * new McpServerFeatures.AsyncPromptRegistration(reviewPrompt, reviewHandler)
- * )
- * }
- * @param prompts The prompt registrations to add. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if prompts is null
- */
- public AsyncSpec prompts(McpServerFeatures.AsyncPromptRegistration... prompts) {
- for (McpServerFeatures.AsyncPromptRegistration prompt : prompts) {
- this.prompts.put(prompt.prompt().name(), prompt);
- }
- return this;
- }
-
- /**
- * Registers a consumer that will be notified when the list of roots changes. This
- * is useful for updating resource availability dynamically, such as when new
- * files are added or removed.
- * @param consumer The consumer to register. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if consumer is null
- */
- public AsyncSpec rootsChangeConsumer(Function, Mono> consumer) {
- Assert.notNull(consumer, "Consumer must not be null");
- this.rootsChangeConsumers.add(consumer);
- return this;
- }
-
- /**
- * Registers multiple consumers that will be notified when the list of roots
- * changes. This method is useful when multiple consumers need to be registered at
- * once.
- * @param consumers The list of consumers to register. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if consumers is null
- */
- public AsyncSpec rootsChangeConsumers(List, Mono>> consumers) {
- Assert.notNull(consumers, "Consumers list must not be null");
- this.rootsChangeConsumers.addAll(consumers);
- return this;
- }
-
- /**
- * Registers multiple consumers that will be notified when the list of roots
- * changes using varargs. This method provides a convenient way to register
- * multiple consumers inline.
- * @param consumers The consumers to register. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if consumers is null
- */
- public AsyncSpec rootsChangeConsumers(
- @SuppressWarnings("unchecked") Function, Mono>... consumers) {
- for (Function, Mono> consumer : consumers) {
- this.rootsChangeConsumers.add(consumer);
- }
- return this;
- }
-
- /**
- * Builds an asynchronous MCP server that provides non-blocking operations.
- * @return A new instance of {@link McpAsyncServer} configured with this builder's
- * settings
- */
- public McpAsyncServer build() {
- var tools = this.tools.stream().map(McpServerFeatures.AsyncToolRegistration::toSpecification).toList();
-
- var resources = this.resources.entrySet()
- .stream()
- .map(entry -> Map.entry(entry.getKey(), entry.getValue().toSpecification()))
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
-
- var prompts = this.prompts.entrySet()
- .stream()
- .map(entry -> Map.entry(entry.getKey(), entry.getValue().toSpecification()))
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
-
- var rootsChangeHandlers = this.rootsChangeConsumers.stream()
- .map(consumer -> (BiFunction, Mono>) (exchange,
- roots) -> consumer.apply(roots))
- .toList();
-
- var features = new McpServerFeatures.Async(this.serverInfo, this.serverCapabilities, tools, resources,
- this.resourceTemplates, prompts, rootsChangeHandlers);
-
- return new McpAsyncServer(this.transport, features);
- }
-
- }
-
- /**
- * Synchronous server specification.
- *
- * @deprecated
- */
- @Deprecated
- class SyncSpec {
-
- private static final McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("mcp-server",
- "1.0.0");
-
- private final ServerMcpTransport transport;
-
- private final McpServerTransportProvider transportProvider;
-
- private ObjectMapper objectMapper;
-
- private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
-
- private McpSchema.ServerCapabilities serverCapabilities;
-
- /**
- * The Model Context Protocol (MCP) allows servers to expose tools that can be
- * invoked by language models. Tools enable models to interact with external
- * systems, such as querying databases, calling APIs, or performing computations.
- * Each tool is uniquely identified by a name and includes metadata describing its
- * schema.
- */
- private final List tools = new ArrayList<>();
-
- /**
- * The Model Context Protocol (MCP) provides a standardized way for servers to
- * expose resources to clients. Resources allow servers to share data that
- * provides context to language models, such as files, database schemas, or
- * application-specific information. Each resource is uniquely identified by a
- * URI.
- */
- private final Map resources = new HashMap<>();
-
- private final List resourceTemplates = new ArrayList<>();
-
- /**
- * The Model Context Protocol (MCP) provides a standardized way for servers to
- * expose prompt templates to clients. Prompts allow servers to provide structured
- * messages and instructions for interacting with language models. Clients can
- * discover available prompts, retrieve their contents, and provide arguments to
- * customize them.
- */
- private final Map prompts = new HashMap<>();
-
- private final List>> rootsChangeConsumers = new ArrayList<>();
-
- private SyncSpec(McpServerTransportProvider transportProvider) {
- Assert.notNull(transportProvider, "Transport provider must not be null");
- this.transportProvider = transportProvider;
- this.transport = null;
- }
-
- private SyncSpec(ServerMcpTransport transport) {
- Assert.notNull(transport, "Transport must not be null");
- this.transport = transport;
- this.transportProvider = null;
- }
-
- /**
- * Sets the server implementation information that will be shared with clients
- * during connection initialization. This helps with version compatibility,
- * debugging, and server identification.
- * @param serverInfo The server implementation details including name and version.
- * Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if serverInfo is null
- */
- public SyncSpec serverInfo(McpSchema.Implementation serverInfo) {
- Assert.notNull(serverInfo, "Server info must not be null");
- this.serverInfo = serverInfo;
- return this;
- }
-
- /**
- * Sets the server implementation information using name and version strings. This
- * is a convenience method alternative to
- * {@link #serverInfo(McpSchema.Implementation)}.
- * @param name The server name. Must not be null or empty.
- * @param version The server version. Must not be null or empty.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if name or version is null or empty
- * @see #serverInfo(McpSchema.Implementation)
- */
- public SyncSpec serverInfo(String name, String version) {
- Assert.hasText(name, "Name must not be null or empty");
- Assert.hasText(version, "Version must not be null or empty");
- this.serverInfo = new McpSchema.Implementation(name, version);
- return this;
- }
-
- /**
- * Sets the server capabilities that will be advertised to clients during
- * connection initialization. Capabilities define what features the server
- * supports, such as:
- *
- * Tool execution
- * Resource access
- * Prompt handling
- * Streaming responses
- * Batch operations
- *
- * @param serverCapabilities The server capabilities configuration. Must not be
- * null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if serverCapabilities is null
- */
- public SyncSpec capabilities(McpSchema.ServerCapabilities serverCapabilities) {
- this.serverCapabilities = serverCapabilities;
- return this;
- }
-
- /**
- * Adds a single tool with its implementation handler to the server. This is a
- * convenience method for registering individual tools without creating a
- * {@link McpServerFeatures.SyncToolRegistration} explicitly.
- *
- *
- * Example usage:
{@code
- * .tool(
- * new Tool("calculator", "Performs calculations", schema),
- * args -> new CallToolResult("Result: " + calculate(args))
- * )
- * }
- * @param tool The tool definition including name, description, and schema. Must
- * not be null.
- * @param handler The function that implements the tool's logic. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if tool or handler is null
- */
- public SyncSpec tool(McpSchema.Tool tool, Function, McpSchema.CallToolResult> handler) {
- Assert.notNull(tool, "Tool must not be null");
- Assert.notNull(handler, "Handler must not be null");
-
- this.tools.add(new McpServerFeatures.SyncToolRegistration(tool, handler));
-
- return this;
- }
-
- /**
- * Adds multiple tools with their handlers to the server using a List. This method
- * is useful when tools are dynamically generated or loaded from a configuration
- * source.
- * @param toolRegistrations The list of tool registrations to add. Must not be
- * null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if toolRegistrations is null
- * @see #tools(McpServerFeatures.SyncToolRegistration...)
- */
- public SyncSpec tools(List toolRegistrations) {
- Assert.notNull(toolRegistrations, "Tool handlers list must not be null");
- this.tools.addAll(toolRegistrations);
- return this;
- }
-
- /**
- * Adds multiple tools with their handlers to the server using varargs. This
- * method provides a convenient way to register multiple tools inline.
- *
- *
- * Example usage:
{@code
- * .tools(
- * new ToolRegistration(calculatorTool, calculatorHandler),
- * new ToolRegistration(weatherTool, weatherHandler),
- * new ToolRegistration(fileManagerTool, fileManagerHandler)
- * )
- * }
- * @param toolRegistrations The tool registrations to add. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if toolRegistrations is null
- * @see #tools(List)
- */
- public SyncSpec tools(McpServerFeatures.SyncToolRegistration... toolRegistrations) {
- for (McpServerFeatures.SyncToolRegistration tool : toolRegistrations) {
- this.tools.add(tool);
- }
- return this;
- }
-
- /**
- * Registers multiple resources with their handlers using a Map. This method is
- * useful when resources are dynamically generated or loaded from a configuration
- * source.
- * @param resourceRegsitrations Map of resource name to registration. Must not be
- * null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if resourceRegsitrations is null
- * @see #resources(McpServerFeatures.SyncResourceRegistration...)
- */
- public SyncSpec resources(Map resourceRegsitrations) {
- Assert.notNull(resourceRegsitrations, "Resource handlers map must not be null");
- this.resources.putAll(resourceRegsitrations);
- return this;
- }
-
- /**
- * Registers multiple resources with their handlers using a List. This method is
- * useful when resources need to be added in bulk from a collection.
- * @param resourceRegsitrations List of resource registrations. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if resourceRegsitrations is null
- * @see #resources(McpServerFeatures.SyncResourceRegistration...)
- */
- public SyncSpec resources(List resourceRegsitrations) {
- Assert.notNull(resourceRegsitrations, "Resource handlers list must not be null");
- for (McpServerFeatures.SyncResourceRegistration resource : resourceRegsitrations) {
- this.resources.put(resource.resource().uri(), resource);
- }
- return this;
- }
-
- /**
- * Registers multiple resources with their handlers using varargs. This method
- * provides a convenient way to register multiple resources inline.
- *
- *
- * Example usage:
{@code
- * .resources(
- * new ResourceRegistration(fileResource, fileHandler),
- * new ResourceRegistration(dbResource, dbHandler),
- * new ResourceRegistration(apiResource, apiHandler)
- * )
- * }
- * @param resourceRegistrations The resource registrations to add. Must not be
- * null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if resourceRegistrations is null
- */
- public SyncSpec resources(McpServerFeatures.SyncResourceRegistration... resourceRegistrations) {
- Assert.notNull(resourceRegistrations, "Resource handlers list must not be null");
- for (McpServerFeatures.SyncResourceRegistration resource : resourceRegistrations) {
- this.resources.put(resource.resource().uri(), resource);
- }
- return this;
- }
-
- /**
- * Sets the resource templates that define patterns for dynamic resource access.
- * Templates use URI patterns with placeholders that can be filled at runtime.
- *
- *
- * Example usage:
{@code
- * .resourceTemplates(
- * new ResourceTemplate("file://{path}", "Access files by path"),
- * new ResourceTemplate("db://{table}/{id}", "Access database records")
- * )
- * }
- * @param resourceTemplates List of resource templates. If null, clears existing
- * templates.
- * @return This builder instance for method chaining
- * @see #resourceTemplates(ResourceTemplate...)
- */
- public SyncSpec resourceTemplates(List resourceTemplates) {
- this.resourceTemplates.addAll(resourceTemplates);
- return this;
- }
-
- /**
- * Sets the resource templates using varargs for convenience. This is an
- * alternative to {@link #resourceTemplates(List)}.
- * @param resourceTemplates The resource templates to set.
- * @return This builder instance for method chaining
- * @see #resourceTemplates(List)
- */
- public SyncSpec resourceTemplates(ResourceTemplate... resourceTemplates) {
- for (ResourceTemplate resourceTemplate : resourceTemplates) {
- this.resourceTemplates.add(resourceTemplate);
- }
- return this;
- }
-
- /**
- * Registers multiple prompts with their handlers using a Map. This method is
- * useful when prompts are dynamically generated or loaded from a configuration
- * source.
- *
- *
- * Example usage:
{@code
- * Map prompts = new HashMap<>();
- * prompts.put("analysis", new PromptRegistration(
- * new Prompt("analysis", "Code analysis template"),
- * request -> new GetPromptResult(generateAnalysisPrompt(request))
- * ));
- * .prompts(prompts)
- * }
- * @param prompts Map of prompt name to registration. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if prompts is null
- */
- public SyncSpec prompts(Map prompts) {
- this.prompts.putAll(prompts);
- return this;
- }
-
- /**
- * Registers multiple prompts with their handlers using a List. This method is
- * useful when prompts need to be added in bulk from a collection.
- * @param prompts List of prompt registrations. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if prompts is null
- * @see #prompts(McpServerFeatures.SyncPromptRegistration...)
- */
- public SyncSpec prompts(List prompts) {
- for (McpServerFeatures.SyncPromptRegistration prompt : prompts) {
- this.prompts.put(prompt.prompt().name(), prompt);
- }
- return this;
- }
-
- /**
- * Registers multiple prompts with their handlers using varargs. This method
- * provides a convenient way to register multiple prompts inline.
- *
- *
- * Example usage:
{@code
- * .prompts(
- * new PromptRegistration(analysisPrompt, analysisHandler),
- * new PromptRegistration(summaryPrompt, summaryHandler),
- * new PromptRegistration(reviewPrompt, reviewHandler)
- * )
- * }
- * @param prompts The prompt registrations to add. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if prompts is null
- */
- public SyncSpec prompts(McpServerFeatures.SyncPromptRegistration... prompts) {
- for (McpServerFeatures.SyncPromptRegistration prompt : prompts) {
- this.prompts.put(prompt.prompt().name(), prompt);
- }
- return this;
- }
-
- /**
- * Registers a consumer that will be notified when the list of roots changes. This
- * is useful for updating resource availability dynamically, such as when new
- * files are added or removed.
- * @param consumer The consumer to register. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if consumer is null
- */
- public SyncSpec rootsChangeConsumer(Consumer> consumer) {
- Assert.notNull(consumer, "Consumer must not be null");
- this.rootsChangeConsumers.add(consumer);
- return this;
- }
-
- /**
- * Registers multiple consumers that will be notified when the list of roots
- * changes. This method is useful when multiple consumers need to be registered at
- * once.
- * @param consumers The list of consumers to register. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if consumers is null
- */
- public SyncSpec rootsChangeConsumers(List>> consumers) {
- Assert.notNull(consumers, "Consumers list must not be null");
- this.rootsChangeConsumers.addAll(consumers);
- return this;
- }
-
- /**
- * Registers multiple consumers that will be notified when the list of roots
- * changes using varargs. This method provides a convenient way to register
- * multiple consumers inline.
- * @param consumers The consumers to register. Must not be null.
- * @return This builder instance for method chaining
- * @throws IllegalArgumentException if consumers is null
- */
- public SyncSpec rootsChangeConsumers(Consumer>... consumers) {
- for (Consumer> consumer : consumers) {
- this.rootsChangeConsumers.add(consumer);
- }
- return this;
- }
-
- /**
- * Builds a synchronous MCP server that provides blocking operations.
- * @return A new instance of {@link McpSyncServer} configured with this builder's
- * settings
- */
- public McpSyncServer build() {
- var tools = this.tools.stream().map(McpServerFeatures.SyncToolRegistration::toSpecification).toList();
-
- var resources = this.resources.entrySet()
- .stream()
- .map(entry -> Map.entry(entry.getKey(), entry.getValue().toSpecification()))
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
-
- var prompts = this.prompts.entrySet()
- .stream()
- .map(entry -> Map.entry(entry.getKey(), entry.getValue().toSpecification()))
- .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
-
- var rootsChangeHandlers = this.rootsChangeConsumers.stream()
- .map(consumer -> (BiConsumer>) (exchange, roots) -> consumer
- .accept(roots))
- .toList();
-
- McpServerFeatures.Sync syncFeatures = new McpServerFeatures.Sync(this.serverInfo, this.serverCapabilities,
- tools, resources, this.resourceTemplates, prompts, rootsChangeHandlers);
-
- McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures);
- var asyncServer = new McpAsyncServer(this.transport, asyncFeatures);
-
- return new McpSyncServer(asyncServer);
- }
-
- }
-
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java
index 5aeeadd77..8c110027c 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java
@@ -10,7 +10,6 @@
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
-import java.util.function.Function;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.util.Assert;
@@ -423,272 +422,4 @@ public record SyncPromptSpecification(McpSchema.Prompt prompt,
BiFunction promptHandler) {
}
- // ---------------------------------------
- // Deprecated registrations
- // ---------------------------------------
-
- /**
- * Registration of a tool with its asynchronous handler function. Tools are the
- * primary way for MCP servers to expose functionality to AI models. Each tool
- * represents a specific capability, such as:
- *
- * Performing calculations
- * Accessing external APIs
- * Querying databases
- * Manipulating files
- * Executing system commands
- *
- *
- *
- * Example tool registration:
{@code
- * new McpServerFeatures.AsyncToolRegistration(
- * new Tool(
- * "calculator",
- * "Performs mathematical calculations",
- * new JsonSchemaObject()
- * .required("expression")
- * .property("expression", JsonSchemaType.STRING)
- * ),
- * args -> {
- * String expr = (String) args.get("expression");
- * return Mono.just(new CallToolResult("Result: " + evaluate(expr)));
- * }
- * )
- * }
- *
- * @param tool The tool definition including name, description, and parameter schema
- * @param call The function that implements the tool's logic, receiving arguments and
- * returning results
- * @deprecated This class is deprecated and will be removed in 0.9.0. Use
- * {@link AsyncToolSpecification}.
- */
- @Deprecated
- public record AsyncToolRegistration(McpSchema.Tool tool,
- Function, Mono> call) {
-
- static AsyncToolRegistration fromSync(SyncToolRegistration tool) {
- // FIXME: This is temporary, proper validation should be implemented
- if (tool == null) {
- return null;
- }
- return new AsyncToolRegistration(tool.tool(),
- map -> Mono.fromCallable(() -> tool.call().apply(map)).subscribeOn(Schedulers.boundedElastic()));
- }
-
- public AsyncToolSpecification toSpecification() {
- return new AsyncToolSpecification(tool(), (exchange, map) -> call.apply(map));
- }
- }
-
- /**
- * Registration of a resource with its asynchronous handler function. Resources
- * provide context to AI models by exposing data such as:
- *
- * File contents
- * Database records
- * API responses
- * System information
- * Application state
- *
- *
- *
- * Example resource registration:
{@code
- * new McpServerFeatures.AsyncResourceRegistration(
- * new Resource("docs", "Documentation files", "text/markdown"),
- * request -> {
- * String content = readFile(request.getPath());
- * return Mono.just(new ReadResourceResult(content));
- * }
- * )
- * }
- *
- * @param resource The resource definition including name, description, and MIME type
- * @param readHandler The function that handles resource read requests
- * @deprecated This class is deprecated and will be removed in 0.9.0. Use
- * {@link AsyncResourceSpecification}.
- */
- @Deprecated
- public record AsyncResourceRegistration(McpSchema.Resource resource,
- Function> readHandler) {
-
- static AsyncResourceRegistration fromSync(SyncResourceRegistration resource) {
- // FIXME: This is temporary, proper validation should be implemented
- if (resource == null) {
- return null;
- }
- return new AsyncResourceRegistration(resource.resource(),
- req -> Mono.fromCallable(() -> resource.readHandler().apply(req))
- .subscribeOn(Schedulers.boundedElastic()));
- }
-
- public AsyncResourceSpecification toSpecification() {
- return new AsyncResourceSpecification(resource(), (exchange, request) -> readHandler.apply(request));
- }
- }
-
- /**
- * Registration of a prompt template with its asynchronous handler function. Prompts
- * provide structured templates for AI model interactions, supporting:
- *
- * Consistent message formatting
- * Parameter substitution
- * Context injection
- * Response formatting
- * Instruction templating
- *
- *
- *
- * Example prompt registration:
{@code
- * new McpServerFeatures.AsyncPromptRegistration(
- * new Prompt("analyze", "Code analysis template"),
- * request -> {
- * String code = request.getArguments().get("code");
- * return Mono.just(new GetPromptResult(
- * "Analyze this code:\n\n" + code + "\n\nProvide feedback on:"
- * ));
- * }
- * )
- * }
- *
- * @param prompt The prompt definition including name and description
- * @param promptHandler The function that processes prompt requests and returns
- * formatted templates
- * @deprecated This class is deprecated and will be removed in 0.9.0. Use
- * {@link AsyncPromptSpecification}.
- */
- @Deprecated
- public record AsyncPromptRegistration(McpSchema.Prompt prompt,
- Function> promptHandler) {
-
- static AsyncPromptRegistration fromSync(SyncPromptRegistration prompt) {
- // FIXME: This is temporary, proper validation should be implemented
- if (prompt == null) {
- return null;
- }
- return new AsyncPromptRegistration(prompt.prompt(),
- req -> Mono.fromCallable(() -> prompt.promptHandler().apply(req))
- .subscribeOn(Schedulers.boundedElastic()));
- }
-
- public AsyncPromptSpecification toSpecification() {
- return new AsyncPromptSpecification(prompt(), (exchange, request) -> promptHandler.apply(request));
- }
- }
-
- /**
- * Registration of a tool with its synchronous handler function. Tools are the primary
- * way for MCP servers to expose functionality to AI models. Each tool represents a
- * specific capability, such as:
- *
- * Performing calculations
- * Accessing external APIs
- * Querying databases
- * Manipulating files
- * Executing system commands
- *
- *
- *
- * Example tool registration:
{@code
- * new McpServerFeatures.SyncToolRegistration(
- * new Tool(
- * "calculator",
- * "Performs mathematical calculations",
- * new JsonSchemaObject()
- * .required("expression")
- * .property("expression", JsonSchemaType.STRING)
- * ),
- * args -> {
- * String expr = (String) args.get("expression");
- * return new CallToolResult("Result: " + evaluate(expr));
- * }
- * )
- * }
- *
- * @param tool The tool definition including name, description, and parameter schema
- * @param call The function that implements the tool's logic, receiving arguments and
- * returning results
- * @deprecated This class is deprecated and will be removed in 0.9.0. Use
- * {@link SyncToolSpecification}.
- */
- @Deprecated
- public record SyncToolRegistration(McpSchema.Tool tool,
- Function, McpSchema.CallToolResult> call) {
- public SyncToolSpecification toSpecification() {
- return new SyncToolSpecification(tool, (exchange, map) -> call.apply(map));
- }
- }
-
- /**
- * Registration of a resource with its synchronous handler function. Resources provide
- * context to AI models by exposing data such as:
- *
- * File contents
- * Database records
- * API responses
- * System information
- * Application state
- *
- *
- *
- * Example resource registration:
{@code
- * new McpServerFeatures.SyncResourceRegistration(
- * new Resource("docs", "Documentation files", "text/markdown"),
- * request -> {
- * String content = readFile(request.getPath());
- * return new ReadResourceResult(content);
- * }
- * )
- * }
- *
- * @param resource The resource definition including name, description, and MIME type
- * @param readHandler The function that handles resource read requests
- * @deprecated This class is deprecated and will be removed in 0.9.0. Use
- * {@link SyncResourceSpecification}.
- */
- @Deprecated
- public record SyncResourceRegistration(McpSchema.Resource resource,
- Function readHandler) {
- public SyncResourceSpecification toSpecification() {
- return new SyncResourceSpecification(resource, (exchange, request) -> readHandler.apply(request));
- }
- }
-
- /**
- * Registration of a prompt template with its synchronous handler function. Prompts
- * provide structured templates for AI model interactions, supporting:
- *
- * Consistent message formatting
- * Parameter substitution
- * Context injection
- * Response formatting
- * Instruction templating
- *
- *
- *
- * Example prompt registration:
{@code
- * new McpServerFeatures.SyncPromptRegistration(
- * new Prompt("analyze", "Code analysis template"),
- * request -> {
- * String code = request.getArguments().get("code");
- * return new GetPromptResult(
- * "Analyze this code:\n\n" + code + "\n\nProvide feedback on:"
- * );
- * }
- * )
- * }
- *
- * @param prompt The prompt definition including name and description
- * @param promptHandler The function that processes prompt requests and returns
- * formatted templates
- * @deprecated This class is deprecated and will be removed in 0.9.0. Use
- * {@link SyncPromptSpecification}.
- */
- @Deprecated
- public record SyncPromptRegistration(McpSchema.Prompt prompt,
- Function promptHandler) {
- public SyncPromptSpecification toSpecification() {
- return new SyncPromptSpecification(prompt, (exchange, request) -> promptHandler.apply(request));
- }
- }
-
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java
index 60662d98d..72eba8b86 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java
@@ -65,40 +65,6 @@ public McpSyncServer(McpAsyncServer asyncServer) {
this.asyncServer = asyncServer;
}
- /**
- * Retrieves the list of all roots provided by the client.
- * @return The list of roots
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link McpSyncServerExchange#listRoots()}.
- */
- @Deprecated
- public McpSchema.ListRootsResult listRoots() {
- return this.listRoots(null);
- }
-
- /**
- * Retrieves a paginated list of roots provided by the server.
- * @param cursor Optional pagination cursor from a previous list request
- * @return The list of roots
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link McpSyncServerExchange#listRoots(String)}.
- */
- @Deprecated
- public McpSchema.ListRootsResult listRoots(String cursor) {
- return this.asyncServer.listRoots(cursor).block();
- }
-
- /**
- * Add a new tool handler.
- * @param toolHandler The tool handler to add
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link #addTool(McpServerFeatures.SyncToolSpecification)}.
- */
- @Deprecated
- public void addTool(McpServerFeatures.SyncToolRegistration toolHandler) {
- this.asyncServer.addTool(McpServerFeatures.AsyncToolRegistration.fromSync(toolHandler)).block();
- }
-
/**
* Add a new tool handler.
* @param toolHandler The tool handler to add
@@ -115,17 +81,6 @@ public void removeTool(String toolName) {
this.asyncServer.removeTool(toolName).block();
}
- /**
- * Add a new resource handler.
- * @param resourceHandler The resource handler to add
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link #addResource(McpServerFeatures.SyncResourceSpecification)}.
- */
- @Deprecated
- public void addResource(McpServerFeatures.SyncResourceRegistration resourceHandler) {
- this.asyncServer.addResource(McpServerFeatures.AsyncResourceRegistration.fromSync(resourceHandler)).block();
- }
-
/**
* Add a new resource handler.
* @param resourceHandler The resource handler to add
@@ -142,17 +97,6 @@ public void removeResource(String resourceUri) {
this.asyncServer.removeResource(resourceUri).block();
}
- /**
- * Add a new prompt handler.
- * @param promptRegistration The prompt registration to add
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link #addPrompt(McpServerFeatures.SyncPromptSpecification)}.
- */
- @Deprecated
- public void addPrompt(McpServerFeatures.SyncPromptRegistration promptRegistration) {
- this.asyncServer.addPrompt(McpServerFeatures.AsyncPromptRegistration.fromSync(promptRegistration)).block();
- }
-
/**
* Add a new prompt handler.
* @param promptSpecification The prompt specification to add
@@ -192,28 +136,6 @@ public McpSchema.Implementation getServerInfo() {
return this.asyncServer.getServerInfo();
}
- /**
- * Get the client capabilities that define the supported features and functionality.
- * @return The client capabilities
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link McpSyncServerExchange#getClientCapabilities()}.
- */
- @Deprecated
- public ClientCapabilities getClientCapabilities() {
- return this.asyncServer.getClientCapabilities();
- }
-
- /**
- * Get the client implementation information.
- * @return The client implementation details
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link McpSyncServerExchange#getClientInfo()}.
- */
- @Deprecated
- public McpSchema.Implementation getClientInfo() {
- return this.asyncServer.getClientInfo();
- }
-
/**
* Notify clients that the list of available resources has changed.
*/
@@ -258,36 +180,4 @@ public McpAsyncServer getAsyncServer() {
return this.asyncServer;
}
- /**
- * Create a new message using the sampling capabilities of the client. The Model
- * Context Protocol (MCP) provides a standardized way for servers to request LLM
- * sampling ("completions" or "generations") from language models via clients.
- *
- *
- * This flow allows clients to maintain control over model access, selection, and
- * permissions while enabling servers to leverage AI capabilities—with no server API
- * keys necessary. Servers can request text or image-based interactions and optionally
- * include context from MCP servers in their prompts.
- *
- *
- * Unlike its async counterpart, this method blocks until the message creation is
- * complete, making it easier to use in synchronous code paths.
- * @param createMessageRequest The request to create a new message
- * @return The result of the message creation
- * @throws McpError if the client has not been initialized or does not support
- * sampling capabilities
- * @throws McpError if the client does not support the createMessage method
- * @see McpSchema.CreateMessageRequest
- * @see McpSchema.CreateMessageResult
- * @see Sampling
- * Specification
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link McpSyncServerExchange#createMessage(McpSchema.CreateMessageRequest)}.
- */
- @Deprecated
- public McpSchema.CreateMessageResult createMessage(McpSchema.CreateMessageRequest createMessageRequest) {
- return this.asyncServer.createMessage(createMessageRequest).block();
- }
-
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransport.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransport.java
deleted file mode 100644
index fa5dcf1c1..000000000
--- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransport.java
+++ /dev/null
@@ -1,419 +0,0 @@
-/*
-* Copyright 2024 - 2024 the original author or authors.
-*/
-package io.modelcontextprotocol.server.transport;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Function;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import io.modelcontextprotocol.spec.McpError;
-import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
-import jakarta.servlet.AsyncContext;
-import jakarta.servlet.ServletException;
-import jakarta.servlet.annotation.WebServlet;
-import jakarta.servlet.http.HttpServlet;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import reactor.core.publisher.Mono;
-
-/**
- * A Servlet-based implementation of the MCP HTTP with Server-Sent Events (SSE) transport
- * specification. This implementation provides similar functionality to
- * WebFluxSseServerTransport but uses the traditional Servlet API instead of WebFlux.
- *
- * @deprecated This class will be removed in 0.9.0. Use
- * {@link HttpServletSseServerTransportProvider}.
- *
- *
- * The transport handles two types of endpoints:
- *
- * SSE endpoint (/sse) - Establishes a long-lived connection for server-to-client
- * events
- * Message endpoint (configurable) - Handles client-to-server message requests
- *
- *
- *
- * Features:
- *
- * Asynchronous message handling using Servlet 6.0 async support
- * Session management for multiple client connections
- * Graceful shutdown support
- * Error handling and response formatting
- *
- * @author Christian Tzolov
- * @author Alexandros Pappas
- * @see ServerMcpTransport
- * @see HttpServlet
- */
-
-@WebServlet(asyncSupported = true)
-@Deprecated
-public class HttpServletSseServerTransport extends HttpServlet implements ServerMcpTransport {
-
- /** Logger for this class */
- private static final Logger logger = LoggerFactory.getLogger(HttpServletSseServerTransport.class);
-
- public static final String UTF_8 = "UTF-8";
-
- public static final String APPLICATION_JSON = "application/json";
-
- public static final String FAILED_TO_SEND_ERROR_RESPONSE = "Failed to send error response: {}";
-
- /** Default endpoint path for SSE connections */
- public static final String DEFAULT_SSE_ENDPOINT = "/sse";
-
- /** Event type for regular messages */
- public static final String MESSAGE_EVENT_TYPE = "message";
-
- /** Event type for endpoint information */
- public static final String ENDPOINT_EVENT_TYPE = "endpoint";
-
- /** JSON object mapper for serialization/deserialization */
- private final ObjectMapper objectMapper;
-
- /** The endpoint path for handling client messages */
- private final String messageEndpoint;
-
- /** The endpoint path for handling SSE connections */
- private final String sseEndpoint;
-
- /** Map of active client sessions, keyed by session ID */
- private final Map sessions = new ConcurrentHashMap<>();
-
- /** Flag indicating if the transport is in the process of shutting down */
- private final AtomicBoolean isClosing = new AtomicBoolean(false);
-
- /** Handler for processing incoming messages */
- private Function, Mono> connectHandler;
-
- /**
- * Creates a new HttpServletSseServerTransport instance with a custom SSE endpoint.
- * @param objectMapper The JSON object mapper to use for message
- * serialization/deserialization
- * @param messageEndpoint The endpoint path where clients will send their messages
- * @param sseEndpoint The endpoint path where clients will establish SSE connections
- */
- public HttpServletSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
- this.objectMapper = objectMapper;
- this.messageEndpoint = messageEndpoint;
- this.sseEndpoint = sseEndpoint;
- }
-
- /**
- * Creates a new HttpServletSseServerTransport instance with the default SSE endpoint.
- * @param objectMapper The JSON object mapper to use for message
- * serialization/deserialization
- * @param messageEndpoint The endpoint path where clients will send their messages
- */
- public HttpServletSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
- this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT);
- }
-
- /**
- * Handles GET requests to establish SSE connections.
- *
- * This method sets up a new SSE connection when a client connects to the SSE
- * endpoint. It configures the response headers for SSE, creates a new session, and
- * sends the initial endpoint information to the client.
- * @param request The HTTP servlet request
- * @param response The HTTP servlet response
- * @throws ServletException If a servlet-specific error occurs
- * @throws IOException If an I/O error occurs
- */
- @Override
- protected void doGet(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
-
- String pathInfo = request.getPathInfo();
- if (!sseEndpoint.equals(pathInfo)) {
- response.sendError(HttpServletResponse.SC_NOT_FOUND);
- return;
- }
-
- if (isClosing.get()) {
- response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Server is shutting down");
- return;
- }
-
- response.setContentType("text/event-stream");
- response.setCharacterEncoding(UTF_8);
- response.setHeader("Cache-Control", "no-cache");
- response.setHeader("Connection", "keep-alive");
- response.setHeader("Access-Control-Allow-Origin", "*");
-
- String sessionId = UUID.randomUUID().toString();
- AsyncContext asyncContext = request.startAsync();
- asyncContext.setTimeout(0);
-
- PrintWriter writer = response.getWriter();
- ClientSession session = new ClientSession(sessionId, asyncContext, writer);
- this.sessions.put(sessionId, session);
-
- // Send initial endpoint event
- this.sendEvent(writer, ENDPOINT_EVENT_TYPE, messageEndpoint);
- }
-
- /**
- * Handles POST requests for client messages.
- *
- * This method processes incoming messages from clients, routes them through the
- * connect handler if configured, and sends back the appropriate response. It handles
- * error cases and formats error responses according to the MCP specification.
- * @param request The HTTP servlet request
- * @param response The HTTP servlet response
- * @throws ServletException If a servlet-specific error occurs
- * @throws IOException If an I/O error occurs
- */
- @Override
- protected void doPost(HttpServletRequest request, HttpServletResponse response)
- throws ServletException, IOException {
-
- if (isClosing.get()) {
- response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Server is shutting down");
- return;
- }
-
- String pathInfo = request.getPathInfo();
- if (!messageEndpoint.equals(pathInfo)) {
- response.sendError(HttpServletResponse.SC_NOT_FOUND);
- return;
- }
-
- try {
- BufferedReader reader = request.getReader();
- StringBuilder body = new StringBuilder();
- String line;
- while ((line = reader.readLine()) != null) {
- body.append(line);
- }
-
- McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body.toString());
-
- if (connectHandler != null) {
- connectHandler.apply(Mono.just(message)).subscribe(responseMessage -> {
- try {
- response.setContentType(APPLICATION_JSON);
- response.setCharacterEncoding(UTF_8);
- String jsonResponse = objectMapper.writeValueAsString(responseMessage);
- PrintWriter writer = response.getWriter();
- writer.write(jsonResponse);
- writer.flush();
- }
- catch (Exception e) {
- logger.error("Error sending response: {}", e.getMessage());
- try {
- response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
- "Error processing response: " + e.getMessage());
- }
- catch (IOException ex) {
- logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage());
- }
- }
- }, error -> {
- try {
- logger.error("Error processing message: {}", error.getMessage());
- McpError mcpError = new McpError(error.getMessage());
- response.setContentType(APPLICATION_JSON);
- response.setCharacterEncoding(UTF_8);
- response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- String jsonError = objectMapper.writeValueAsString(mcpError);
- PrintWriter writer = response.getWriter();
- writer.write(jsonError);
- writer.flush();
- }
- catch (IOException e) {
- logger.error(FAILED_TO_SEND_ERROR_RESPONSE, e.getMessage());
- try {
- response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
- "Error sending error response: " + e.getMessage());
- }
- catch (IOException ex) {
- logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage());
- }
- }
- });
- }
- else {
- response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "No message handler configured");
- }
- }
- catch (Exception e) {
- logger.error("Invalid message format: {}", e.getMessage());
- try {
- McpError mcpError = new McpError("Invalid message format: " + e.getMessage());
- response.setContentType(APPLICATION_JSON);
- response.setCharacterEncoding(UTF_8);
- response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
- String jsonError = objectMapper.writeValueAsString(mcpError);
- PrintWriter writer = response.getWriter();
- writer.write(jsonError);
- writer.flush();
- }
- catch (IOException ex) {
- logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage());
- response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid message format");
- }
- }
- }
-
- /**
- * Sets up the message handler for processing client requests.
- * @param handler The function to process incoming messages and produce responses
- * @return A Mono that completes when the handler is set up
- */
- @Override
- public Mono connect(Function, Mono> handler) {
- this.connectHandler = handler;
- return Mono.empty();
- }
-
- /**
- * Broadcasts a message to all connected clients.
- *
- * This method serializes the message and sends it to all active client sessions. If a
- * client is disconnected, its session is removed.
- * @param message The message to broadcast
- * @return A Mono that completes when the message has been sent to all clients
- */
- @Override
- public Mono sendMessage(McpSchema.JSONRPCMessage message) {
- if (sessions.isEmpty()) {
- logger.debug("No active sessions to broadcast message to");
- return Mono.empty();
- }
-
- return Mono.create(sink -> {
- try {
- String jsonText = objectMapper.writeValueAsString(message);
-
- sessions.values().forEach(session -> {
- try {
- this.sendEvent(session.writer, MESSAGE_EVENT_TYPE, jsonText);
- }
- catch (IOException e) {
- logger.error("Failed to send message to session {}: {}", session.id, e.getMessage());
- removeSession(session);
- }
- });
-
- sink.success();
- }
- catch (Exception e) {
- logger.error("Failed to process message: {}", e.getMessage());
- sink.error(new McpError("Failed to process message: " + e.getMessage()));
- }
- });
- }
-
- /**
- * Closes the transport.
- *
- * This implementation delegates to the super class's close method.
- */
- @Override
- public void close() {
- ServerMcpTransport.super.close();
- }
-
- /**
- * Unmarshals data from one type to another using the object mapper.
- * @param The target type
- * @param data The source data
- * @param typeRef The type reference for the target type
- * @return The unmarshaled data
- */
- @Override
- public T unmarshalFrom(Object data, TypeReference typeRef) {
- return objectMapper.convertValue(data, typeRef);
- }
-
- /**
- * Initiates a graceful shutdown of the transport.
- *
- * This method marks the transport as closing and closes all active client sessions.
- * New connection attempts will be rejected during shutdown.
- * @return A Mono that completes when all sessions have been closed
- */
- @Override
- public Mono closeGracefully() {
- isClosing.set(true);
- logger.debug("Initiating graceful shutdown with {} active sessions", sessions.size());
-
- return Mono.create(sink -> {
- sessions.values().forEach(this::removeSession);
- sink.success();
- });
- }
-
- /**
- * Sends an SSE event to a client.
- * @param writer The writer to send the event through
- * @param eventType The type of event (message or endpoint)
- * @param data The event data
- * @throws IOException If an error occurs while writing the event
- */
- private void sendEvent(PrintWriter writer, String eventType, String data) throws IOException {
- writer.write("event: " + eventType + "\n");
- writer.write("data: " + data + "\n\n");
- writer.flush();
-
- if (writer.checkError()) {
- throw new IOException("Client disconnected");
- }
- }
-
- /**
- * Removes a client session and completes its async context.
- * @param session The session to remove
- */
- private void removeSession(ClientSession session) {
- sessions.remove(session.id);
- session.asyncContext.complete();
- }
-
- /**
- * Represents a client connection session.
- *
- * This class holds the necessary information about a client's SSE connection,
- * including its ID, async context, and output writer.
- */
- private static class ClientSession {
-
- private final String id;
-
- private final AsyncContext asyncContext;
-
- private final PrintWriter writer;
-
- ClientSession(String id, AsyncContext asyncContext, PrintWriter writer) {
- this.id = id;
- this.asyncContext = asyncContext;
- this.writer = writer;
- }
-
- }
-
- /**
- * Cleans up resources when the servlet is being destroyed.
- *
- * This method ensures a graceful shutdown by closing all client connections before
- * calling the parent's destroy method.
- */
- @Override
- public void destroy() {
- closeGracefully().block();
- super.destroy();
- }
-
-}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransport.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransport.java
deleted file mode 100644
index 78264ca32..000000000
--- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransport.java
+++ /dev/null
@@ -1,259 +0,0 @@
-/*
- * Copyright 2024-2024 the original author or authors.
- */
-
-package io.modelcontextprotocol.server.transport;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.nio.charset.StandardCharsets;
-import java.util.concurrent.Executors;
-import java.util.function.Function;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-import com.fasterxml.jackson.databind.ObjectMapper;
-import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
-import io.modelcontextprotocol.util.Assert;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import reactor.core.publisher.Flux;
-import reactor.core.publisher.Mono;
-import reactor.core.publisher.Sinks;
-import reactor.core.scheduler.Scheduler;
-import reactor.core.scheduler.Schedulers;
-
-/**
- * Implementation of the MCP Stdio transport for servers that communicates using standard
- * input/output streams. Messages are exchanged as newline-delimited JSON-RPC messages
- * over stdin/stdout, with errors and debug information sent to stderr.
- *
- * @author Christian Tzolov
- * @deprecated This method will be removed in 0.9.0. Use
- * {@link io.modelcontextprotocol.server.transport.StdioServerTransportProvider} instead.
- */
-@Deprecated
-public class StdioServerTransport implements ServerMcpTransport {
-
- private static final Logger logger = LoggerFactory.getLogger(StdioServerTransport.class);
-
- private final Sinks.Many inboundSink;
-
- private final Sinks.Many outboundSink;
-
- private ObjectMapper objectMapper;
-
- /** Scheduler for handling inbound messages */
- private Scheduler inboundScheduler;
-
- /** Scheduler for handling outbound messages */
- private Scheduler outboundScheduler;
-
- private volatile boolean isClosing = false;
-
- private final InputStream inputStream;
-
- private final OutputStream outputStream;
-
- private final Sinks.One inboundReady = Sinks.one();
-
- private final Sinks.One outboundReady = Sinks.one();
-
- /**
- * Creates a new StdioServerTransport with a default ObjectMapper and System streams.
- */
- public StdioServerTransport() {
- this(new ObjectMapper());
- }
-
- /**
- * Creates a new StdioServerTransport with the specified ObjectMapper and System
- * streams.
- * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization
- */
- public StdioServerTransport(ObjectMapper objectMapper) {
-
- Assert.notNull(objectMapper, "The ObjectMapper can not be null");
-
- this.inboundSink = Sinks.many().unicast().onBackpressureBuffer();
- this.outboundSink = Sinks.many().unicast().onBackpressureBuffer();
-
- this.objectMapper = objectMapper;
- this.inputStream = System.in;
- this.outputStream = System.out;
-
- // Use bounded schedulers for better resource management
- this.inboundScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "inbound");
- this.outboundScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "outbound");
- }
-
- @Override
- public Mono connect(Function, Mono> handler) {
- return Mono.fromRunnable(() -> {
- handleIncomingMessages(handler);
-
- // Start threads
- startInboundProcessing();
- startOutboundProcessing();
- }).subscribeOn(Schedulers.boundedElastic());
- }
-
- private void handleIncomingMessages(Function, Mono> inboundMessageHandler) {
- this.inboundSink.asFlux()
- .flatMap(message -> Mono.just(message)
- .transform(inboundMessageHandler)
- .contextWrite(ctx -> ctx.put("observation", "myObservation")))
- .doOnTerminate(() -> {
- // The outbound processing will dispose its scheduler upon completion
- this.outboundSink.tryEmitComplete();
- this.inboundScheduler.dispose();
- })
- .subscribe();
- }
-
- @Override
- public Mono sendMessage(JSONRPCMessage message) {
- return Mono.zip(inboundReady.asMono(), outboundReady.asMono()).then(Mono.defer(() -> {
- if (this.outboundSink.tryEmitNext(message).isSuccess()) {
- return Mono.empty();
- }
- else {
- return Mono.error(new RuntimeException("Failed to enqueue message"));
- }
- }));
- }
-
- /**
- * Starts the inbound processing thread that reads JSON-RPC messages from stdin.
- * Messages are deserialized and emitted to the inbound sink.
- */
- private void startInboundProcessing() {
- this.inboundScheduler.schedule(() -> {
- inboundReady.tryEmitValue(null);
- BufferedReader reader = null;
- try {
- reader = new BufferedReader(new InputStreamReader(inputStream));
- while (!isClosing) {
- try {
- String line = reader.readLine();
- if (line == null || isClosing) {
- break;
- }
-
- logger.debug("Received JSON message: {}", line);
-
- try {
- JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, line);
- if (!this.inboundSink.tryEmitNext(message).isSuccess()) {
- logIfNotClosing("Failed to enqueue message");
- break;
- }
- }
- catch (Exception e) {
- logIfNotClosing("Error processing inbound message", e);
- break;
- }
- }
- catch (IOException e) {
- logIfNotClosing("Error reading from stdin", e);
- break;
- }
- }
- }
- catch (Exception e) {
- logIfNotClosing("Error in inbound processing", e);
- }
- finally {
- isClosing = true;
- inboundSink.tryEmitComplete();
- }
- });
- }
-
- /**
- * Starts the outbound processing thread that writes JSON-RPC messages to stdout.
- * Messages are serialized to JSON and written with a newline delimiter.
- */
- private void startOutboundProcessing() {
- Function, Flux> outboundConsumer = messages -> messages // @formatter:off
- .doOnSubscribe(subscription -> outboundReady.tryEmitValue(null))
- .publishOn(outboundScheduler)
- .handle((message, sink) -> {
- if (message != null && !isClosing) {
- try {
- String jsonMessage = objectMapper.writeValueAsString(message);
- // Escape any embedded newlines in the JSON message as per spec
- jsonMessage = jsonMessage.replace("\r\n", "\\n").replace("\n", "\\n").replace("\r", "\\n");
-
- synchronized (outputStream) {
- outputStream.write(jsonMessage.getBytes(StandardCharsets.UTF_8));
- outputStream.write("\n".getBytes(StandardCharsets.UTF_8));
- outputStream.flush();
- }
- sink.next(message);
- }
- catch (IOException e) {
- if (!isClosing) {
- logger.error("Error writing message", e);
- sink.error(new RuntimeException(e));
- }
- else {
- logger.debug("Stream closed during shutdown", e);
- }
- }
- }
- else if (isClosing) {
- sink.complete();
- }
- })
- .doOnComplete(() -> {
- isClosing = true;
- outboundScheduler.dispose();
- })
- .doOnError(e -> {
- if (!isClosing) {
- logger.error("Error in outbound processing", e);
- isClosing = true;
- outboundScheduler.dispose();
- }
- })
- .map(msg -> (JSONRPCMessage) msg);
-
- outboundConsumer.apply(outboundSink.asFlux()).subscribe();
- } // @formatter:on
-
- @Override
- public Mono closeGracefully() {
- return Mono.defer(() -> {
- isClosing = true;
- logger.debug("Initiating graceful shutdown");
- // Completing the inbound causes the outbound to be completed as well, so
- // we only close the inbound.
- inboundSink.tryEmitComplete();
- logger.debug("Graceful shutdown complete");
- return Mono.empty();
- }).subscribeOn(Schedulers.boundedElastic());
- }
-
- @Override
- public T unmarshalFrom(Object data, TypeReference typeRef) {
- return this.objectMapper.convertValue(data, typeRef);
- }
-
- private void logIfNotClosing(String message, Exception e) {
- if (!this.isClosing) {
- logger.error(message, e);
- }
- }
-
- private void logIfNotClosing(String message) {
- if (!this.isClosing) {
- logger.error(message);
- }
- }
-
-}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/ClientMcpTransport.java b/mcp/src/main/java/io/modelcontextprotocol/spec/ClientMcpTransport.java
deleted file mode 100644
index 8464b6ae7..000000000
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/ClientMcpTransport.java
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
-* Copyright 2024 - 2024 the original author or authors.
-*/
-package io.modelcontextprotocol.spec;
-
-/**
- * Marker interface for the client-side MCP transport.
- *
- * @author Christian Tzolov
- * @deprecated This class will be removed in 0.9.0. Use {@link McpClientTransport}.
- */
-@Deprecated
-public interface ClientMcpTransport extends McpTransport {
-
-}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/DefaultMcpSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/DefaultMcpSession.java
deleted file mode 100644
index 83de4c094..000000000
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/DefaultMcpSession.java
+++ /dev/null
@@ -1,291 +0,0 @@
-/*
- * Copyright 2024-2024 the original author or authors.
- */
-
-package io.modelcontextprotocol.spec;
-
-import java.time.Duration;
-import java.util.Map;
-import java.util.UUID;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicLong;
-
-import com.fasterxml.jackson.core.type.TypeReference;
-import io.modelcontextprotocol.util.Assert;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import reactor.core.Disposable;
-import reactor.core.publisher.Mono;
-import reactor.core.publisher.MonoSink;
-
-/**
- * Default implementation of the MCP (Model Context Protocol) session that manages
- * bidirectional JSON-RPC communication between clients and servers. This implementation
- * follows the MCP specification for message exchange and transport handling.
- *
- *
- * The session manages:
- *
- * Request/response handling with unique message IDs
- * Notification processing
- * Message timeout management
- * Transport layer abstraction
- *
- *
- * @author Christian Tzolov
- * @author Dariusz Jędrzejczyk
- * @deprecated This method will be removed in 0.9.0. Use {@link McpClientSession} instead
- */
-@Deprecated
-
-public class DefaultMcpSession implements McpSession {
-
- /** Logger for this class */
- private static final Logger logger = LoggerFactory.getLogger(DefaultMcpSession.class);
-
- /** Duration to wait for request responses before timing out */
- private final Duration requestTimeout;
-
- /** Transport layer implementation for message exchange */
- private final McpTransport transport;
-
- /** Map of pending responses keyed by request ID */
- private final ConcurrentHashMap> pendingResponses = new ConcurrentHashMap<>();
-
- /** Map of request handlers keyed by method name */
- private final ConcurrentHashMap> requestHandlers = new ConcurrentHashMap<>();
-
- /** Map of notification handlers keyed by method name */
- private final ConcurrentHashMap notificationHandlers = new ConcurrentHashMap<>();
-
- /** Session-specific prefix for request IDs */
- private final String sessionPrefix = UUID.randomUUID().toString().substring(0, 8);
-
- /** Atomic counter for generating unique request IDs */
- private final AtomicLong requestCounter = new AtomicLong(0);
-
- private final Disposable connection;
-
- /**
- * Functional interface for handling incoming JSON-RPC requests. Implementations
- * should process the request parameters and return a response.
- *
- * @param Response type
- */
- @FunctionalInterface
- public interface RequestHandler {
-
- /**
- * Handles an incoming request with the given parameters.
- * @param params The request parameters
- * @return A Mono containing the response object
- */
- Mono handle(Object params);
-
- }
-
- /**
- * Functional interface for handling incoming JSON-RPC notifications. Implementations
- * should process the notification parameters without returning a response.
- */
- @FunctionalInterface
- public interface NotificationHandler {
-
- /**
- * Handles an incoming notification with the given parameters.
- * @param params The notification parameters
- * @return A Mono that completes when the notification is processed
- */
- Mono handle(Object params);
-
- }
-
- /**
- * Creates a new DefaultMcpSession with the specified configuration and handlers.
- * @param requestTimeout Duration to wait for responses
- * @param transport Transport implementation for message exchange
- * @param requestHandlers Map of method names to request handlers
- * @param notificationHandlers Map of method names to notification handlers
- */
- public DefaultMcpSession(Duration requestTimeout, McpTransport transport,
- Map> requestHandlers, Map notificationHandlers) {
-
- Assert.notNull(requestTimeout, "The requstTimeout can not be null");
- Assert.notNull(transport, "The transport can not be null");
- Assert.notNull(requestHandlers, "The requestHandlers can not be null");
- Assert.notNull(notificationHandlers, "The notificationHandlers can not be null");
-
- this.requestTimeout = requestTimeout;
- this.transport = transport;
- this.requestHandlers.putAll(requestHandlers);
- this.notificationHandlers.putAll(notificationHandlers);
-
- // TODO: consider mono.transformDeferredContextual where the Context contains
- // the
- // Observation associated with the individual message - it can be used to
- // create child Observation and emit it together with the message to the
- // consumer
- this.connection = this.transport.connect(mono -> mono.doOnNext(message -> {
- if (message instanceof McpSchema.JSONRPCResponse response) {
- logger.debug("Received Response: {}", response);
- var sink = pendingResponses.remove(response.id());
- if (sink == null) {
- logger.warn("Unexpected response for unkown id {}", response.id());
- }
- else {
- sink.success(response);
- }
- }
- else if (message instanceof McpSchema.JSONRPCRequest request) {
- logger.debug("Received request: {}", request);
- handleIncomingRequest(request).subscribe(response -> transport.sendMessage(response).subscribe(),
- error -> {
- var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(),
- null, new McpSchema.JSONRPCResponse.JSONRPCError(
- McpSchema.ErrorCodes.INTERNAL_ERROR, error.getMessage(), null));
- transport.sendMessage(errorResponse).subscribe();
- });
- }
- else if (message instanceof McpSchema.JSONRPCNotification notification) {
- logger.debug("Received notification: {}", notification);
- handleIncomingNotification(notification).subscribe(null,
- error -> logger.error("Error handling notification: {}", error.getMessage()));
- }
- })).subscribe();
- }
-
- /**
- * Handles an incoming JSON-RPC request by routing it to the appropriate handler.
- * @param request The incoming JSON-RPC request
- * @return A Mono containing the JSON-RPC response
- */
- private Mono handleIncomingRequest(McpSchema.JSONRPCRequest request) {
- return Mono.defer(() -> {
- var handler = this.requestHandlers.get(request.method());
- if (handler == null) {
- MethodNotFoundError error = getMethodNotFoundError(request.method());
- return Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
- new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND,
- error.message(), error.data())));
- }
-
- return handler.handle(request.params())
- .map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null))
- .onErrorResume(error -> Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(),
- null, new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR,
- error.getMessage(), null)))); // TODO: add error message
- // through the data field
- });
- }
-
- record MethodNotFoundError(String method, String message, Object data) {
- }
-
- public static MethodNotFoundError getMethodNotFoundError(String method) {
- switch (method) {
- case McpSchema.METHOD_ROOTS_LIST:
- return new MethodNotFoundError(method, "Roots not supported",
- Map.of("reason", "Client does not have roots capability"));
- default:
- return new MethodNotFoundError(method, "Method not found: " + method, null);
- }
- }
-
- /**
- * Handles an incoming JSON-RPC notification by routing it to the appropriate handler.
- * @param notification The incoming JSON-RPC notification
- * @return A Mono that completes when the notification is processed
- */
- private Mono handleIncomingNotification(McpSchema.JSONRPCNotification notification) {
- return Mono.defer(() -> {
- var handler = notificationHandlers.get(notification.method());
- if (handler == null) {
- logger.error("No handler registered for notification method: {}", notification.method());
- return Mono.empty();
- }
- return handler.handle(notification.params());
- });
- }
-
- /**
- * Generates a unique request ID in a non-blocking way. Combines a session-specific
- * prefix with an atomic counter to ensure uniqueness.
- * @return A unique request ID string
- */
- private String generateRequestId() {
- return this.sessionPrefix + "-" + this.requestCounter.getAndIncrement();
- }
-
- /**
- * Sends a JSON-RPC request and returns the response.
- * @param The expected response type
- * @param method The method name to call
- * @param requestParams The request parameters
- * @param typeRef Type reference for response deserialization
- * @return A Mono containing the response
- */
- @Override
- public Mono sendRequest(String method, Object requestParams, TypeReference typeRef) {
- String requestId = this.generateRequestId();
-
- return Mono.create(sink -> {
- this.pendingResponses.put(requestId, sink);
- McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, method,
- requestId, requestParams);
- this.transport.sendMessage(jsonrpcRequest)
- // TODO: It's most efficient to create a dedicated Subscriber here
- .subscribe(v -> {
- }, error -> {
- this.pendingResponses.remove(requestId);
- sink.error(error);
- });
- }).timeout(this.requestTimeout).handle((jsonRpcResponse, sink) -> {
- if (jsonRpcResponse.error() != null) {
- sink.error(new McpError(jsonRpcResponse.error()));
- }
- else {
- if (typeRef.getType().equals(Void.class)) {
- sink.complete();
- }
- else {
- sink.next(this.transport.unmarshalFrom(jsonRpcResponse.result(), typeRef));
- }
- }
- });
- }
-
- /**
- * Sends a JSON-RPC notification.
- * @param method The method name for the notification
- * @param params The notification parameters
- * @return A Mono that completes when the notification is sent
- */
- @Override
- public Mono sendNotification(String method, Map params) {
- McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION,
- method, params);
- return this.transport.sendMessage(jsonrpcNotification);
- }
-
- /**
- * Closes the session gracefully, allowing pending operations to complete.
- * @return A Mono that completes when the session is closed
- */
- @Override
- public Mono closeGracefully() {
- return Mono.defer(() -> {
- this.connection.dispose();
- return transport.closeGracefully();
- });
- }
-
- /**
- * Closes the session immediately, potentially interrupting pending operations.
- */
- @Override
- public void close() {
- this.connection.dispose();
- transport.close();
- }
-
-}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
index 6657e3622..e29646e6a 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java
@@ -44,7 +44,7 @@ public class McpClientSession implements McpSession {
private final Duration requestTimeout;
/** Transport layer implementation for message exchange */
- private final McpTransport transport;
+ private final McpClientTransport transport;
/** Map of pending responses keyed by request ID */
private final ConcurrentHashMap> pendingResponses = new ConcurrentHashMap<>();
@@ -104,7 +104,7 @@ public interface NotificationHandler {
* @param requestHandlers Map of method names to request handlers
* @param notificationHandlers Map of method names to notification handlers
*/
- public McpClientSession(Duration requestTimeout, McpTransport transport,
+ public McpClientSession(Duration requestTimeout, McpClientTransport transport,
Map> requestHandlers, Map notificationHandlers) {
Assert.notNull(requestTimeout, "The requstTimeout can not be null");
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java
index 458979651..f29091248 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java
@@ -13,9 +13,8 @@
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/
-public interface McpClientTransport extends ClientMcpTransport {
+public interface McpClientTransport extends McpTransport {
- @Override
Mono connect(Function, Mono> handler);
}
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java
index f698d8789..40d9ba7ac 100644
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java
+++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java
@@ -4,8 +4,6 @@
package io.modelcontextprotocol.spec;
-import java.util.function.Function;
-
import com.fasterxml.jackson.core.type.TypeReference;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
import reactor.core.publisher.Mono;
@@ -39,21 +37,6 @@
*/
public interface McpTransport {
- /**
- * Initializes and starts the transport connection.
- *
- *
- * This method should be called before any message exchange can occur. It sets up the
- * necessary resources and establishes the connection to the server.
- *
- * @deprecated This is only relevant for client-side transports and will be removed
- * from this interface in 0.9.0.
- */
- @Deprecated
- default Mono connect(Function, Mono> handler) {
- return Mono.empty();
- }
-
/**
* Closes the transport connection and releases any associated resources.
*
diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/ServerMcpTransport.java b/mcp/src/main/java/io/modelcontextprotocol/spec/ServerMcpTransport.java
deleted file mode 100644
index 704daee0f..000000000
--- a/mcp/src/main/java/io/modelcontextprotocol/spec/ServerMcpTransport.java
+++ /dev/null
@@ -1,15 +0,0 @@
-/*
-* Copyright 2024 - 2024 the original author or authors.
-*/
-package io.modelcontextprotocol.spec;
-
-/**
- * Marker interface for the server-side MCP transport.
- *
- * @author Christian Tzolov
- * @deprecated This class will be removed in 0.9.0. Use {@link McpServerTransport}.
- */
-@Deprecated
-public interface ServerMcpTransport extends McpTransport {
-
-}
diff --git a/mcp/src/test/java/io/modelcontextprotocol/MockMcpTransport.java b/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java
similarity index 84%
rename from mcp/src/test/java/io/modelcontextprotocol/MockMcpTransport.java
rename to mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java
index 12f30d12f..482d0aac6 100644
--- a/mcp/src/test/java/io/modelcontextprotocol/MockMcpTransport.java
+++ b/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java
@@ -13,30 +13,28 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
-import io.modelcontextprotocol.spec.ServerMcpTransport;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest;
import reactor.core.publisher.Mono;
import reactor.core.publisher.Sinks;
/**
- * A mock implementation of the {@link McpClientTransport} and {@link ServerMcpTransport}
- * interfaces.
+ * A mock implementation of the {@link McpClientTransport} interfaces.
*/
-public class MockMcpTransport implements McpClientTransport, ServerMcpTransport {
+public class MockMcpClientTransport implements McpClientTransport {
private final Sinks.Many inbound = Sinks.many().unicast().onBackpressureBuffer();
private final List sent = new ArrayList<>();
- private final BiConsumer interceptor;
+ private final BiConsumer