refactor: remove deprecated 0.7.0 code

These changes are part of the planned deprecation cycle announced in 0.8.0,
with the deprecated classes scheduled for removal in 0.9.0

- Delete WebFluxSseServerTransport, WebMvcSseServerTransport, StdioServerTransport, and HttpServletSseServerTransport
- Remove deprecated interfaces: ServerMcpTransport, ClientMcpTransport
- Delete DefaultMcpSession implementation
- Remove all deprecated test classes for the removed implementations
- Update references to use McpServerTransport and McpClientTransport interfaces
- Split MockMcpTransport into client and server implementations
  * Rename MockMcpTransport to MockMcpClientTransport in mcp/src/test
  * Create new MockMcpServerTransport implementation
  * Add MockMcpServerTransportProvider for server tests
  * Mark MockMcpTransport in mcp-test module as deprecated
  * Update all test classes to use the new implementations

Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Christian Tzolov 2025-03-22 16:50:33 +01:00
commit 25f3bad68d
43 changed files with 203 additions and 7660 deletions

View file

@ -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.
*
* <p>
* Key features:
* <ul>
* <li>Implements the {@link ServerMcpTransport} interface for MCP server transport
* functionality</li>
* <li>Uses WebFlux for non-blocking request handling and SSE support</li>
* <li>Maintains client sessions for reliable message delivery</li>
* <li>Supports graceful shutdown with session cleanup</li>
* <li>Thread-safe message broadcasting to multiple clients</li>
* </ul>
*
* <p>
* The transport sets up two main endpoints:
* <ul>
* <li>SSE endpoint (/sse) - For establishing SSE connections with clients</li>
* <li>Message endpoint (configurable) - For receiving JSON-RPC messages from clients</li>
* </ul>
*
* <p>
* 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<String, ClientSession> sessions = new ConcurrentHashMap<>();
/**
* Flag indicating if the transport is shutting down.
*/
private volatile boolean isClosing = false;
private Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> 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<Void> connect(Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> 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.
*
* <p>
* The method:
* <ul>
* <li>Serializes the message to JSON</li>
* <li>Creates a server-sent event with the message data</li>
* <li>Attempts to send the event to all active sessions</li>
* <li>Tracks and reports any delivery failures</li>
* </ul>
* @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<Void> sendMessage(McpSchema.JSONRPCMessage message) {
if (sessions.isEmpty()) {
logger.debug("No active sessions to broadcast message to");
return Mono.empty();
}
return Mono.<Void>create(sink -> {
try {// @formatter:off
String jsonText = objectMapper.writeValueAsString(message);
ServerSentEvent<Object> event = ServerSentEvent.builder()
.event(MESSAGE_EVENT_TYPE)
.data(jsonText)
.build();
logger.debug("Attempting to broadcast message to {} active sessions", sessions.size());
List<String> 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 <T> 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> T unmarshalFrom(Object data, TypeReference<T> 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.
*
* <p>
* The shutdown process:
* <ul>
* <li>Marks the transport as closing to prevent new connections</li>
* <li>Closes each active session</li>
* <li>Removes closed sessions from the sessions map</li>
* <li>Times out after 5 seconds if shutdown takes too long</li>
* </ul>
* @return A Mono that completes when all sessions have been closed
*/
@Override
public Mono<Void> 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.
*
* <p>
* The router function defines two endpoints:
* <ul>
* <li>GET {sseEndpoint} - For establishing SSE connections</li>
* <li>POST {messageEndpoint} - For receiving client messages</li>
* </ul>
* @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.
*
* <p>
* The handler performs the following steps:
* <ul>
* <li>Generates a unique session ID</li>
* <li>Creates a new ClientSession instance</li>
* <li>Sends the message endpoint URI as an initial event</li>
* <li>Sets up message forwarding for the session</li>
* <li>Handles connection cleanup on completion or errors</li>
* </ul>
* @param request The incoming server request
* @return A response with the SSE event stream
*/
private Mono<ServerResponse> 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.<ServerSentEvent<?>>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.
*
* <p>
* The handler:
* <ul>
* <li>Deserializes the incoming JSON-RPC message</li>
* <li>Passes it through the message handler chain</li>
* <li>Returns appropriate HTTP responses based on processing results</li>
* <li>Handles various error conditions with appropriate error responses</li>
* </ul>
* @param request The incoming server request containing the JSON-RPC message
* @return A response indicating the message processing result
*/
private Mono<ServerResponse> 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.
*
* <p>
* Each session:
* <ul>
* <li>Has a unique identifier</li>
* <li>Maintains its own message sink for event broadcasting</li>
* <li>Supports clean shutdown through the close method</li>
* </ul>
*/
private static class ClientSession {
private final String id;
private final Sinks.Many<ServerSentEvent<?>> 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);
}
}
}
}

View file

@ -8,10 +8,9 @@ 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.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.Exceptions;
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;

View file

@ -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();
}
}
}

View file

@ -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();
}
}
}

View file

@ -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<String, McpClient.SyncSpec> 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<CreateMessageRequest, CreateMessageResult> 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<Root> roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
AtomicReference<List<Root>> 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<List<Root>> 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<Root> roots = List.of(new Root("uri1://", "root1"));
AtomicReference<List<Root>> rootsRef1 = new AtomicReference<>();
AtomicReference<List<Root>> 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<Root> roots = List.of(new Root("uri1://", "root1"));
AtomicReference<List<Root>> 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<List<Tool>> 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();
}
}

View file

@ -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}.
*
* <p>
* Key features:
* <ul>
* <li>Implements bidirectional communication using HTTP POST for client-to-server
* messages and SSE for server-to-client messages</li>
* <li>Manages client sessions with unique IDs for reliable message delivery</li>
* <li>Supports graceful shutdown with proper session cleanup</li>
* <li>Provides JSON-RPC message handling through configured endpoints</li>
* <li>Includes built-in error handling and logging</li>
* </ul>
*
* <p>
* The transport operates on two main endpoints:
* <ul>
* <li>{@code /sse} - The SSE endpoint where clients establish their event stream
* connection</li>
* <li>A configurable message endpoint where clients send their JSON-RPC messages via HTTP
* POST</li>
* </ul>
*
* <p>
* 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<ServerResponse> routerFunction;
/**
* Map of active client sessions, keyed by session ID.
*/
private final ConcurrentHashMap<String, ClientSession> 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<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> 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<Void> connect(
Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> 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<Void> 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:
* <ul>
* <li>Generates a unique session ID</li>
* <li>Creates a new ClientSession with an SSE builder</li>
* <li>Sends an initial endpoint event to inform the client where to send
* messages</li>
* <li>Maintains the session in the sessions map</li>
* </ul>
* @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:
* <ul>
* <li>Deserializes the request body into a JSON-RPC message</li>
* <li>Processes the message through the configured connect handler</li>
* <li>Returns appropriate HTTP responses based on the processing result</li>
* </ul>
* @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:
* <ul>
* <li>A unique session identifier</li>
* <li>An SSE builder for sending server events to the client</li>
* <li>Logging of session lifecycle events</li>
* </ul>
*/
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 <T> The target type
*/
@Override
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return this.objectMapper.convertValue(data, typeRef);
}
/**
* Initiates a graceful shutdown of the transport. This method:
* <ul>
* <li>Sets the closing flag to prevent new connections</li>
* <li>Closes all active SSE connections</li>
* <li>Removes all session records</li>
* </ul>
* @return A Mono that completes when all cleanup operations are finished
*/
@Override
public Mono<Void> 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:
* <ul>
* <li>GET /sse - For establishing SSE connections</li>
* <li>POST [messageEndpoint] - For receiving JSON-RPC messages from clients</li>
* </ul>
* @return The configured RouterFunction for handling HTTP requests
*/
public RouterFunction<ServerResponse> getRouterFunction() {
return this.routerFunction;
}
}

View file

@ -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<ServerResponse> 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);
}
}
}
}

View file

@ -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<ServerResponse> 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<CreateMessageRequest, CreateMessageResult> 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<Root> roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2"));
AtomicReference<List<Root>> 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<List<Root>> 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<Root> roots = List.of(new Root("uri1://", "root1"));
AtomicReference<List<Root>> rootsRef1 = new AtomicReference<>();
AtomicReference<List<Root>> 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<Root> roots = List.of(new Root("uri1://", "root1"));
AtomicReference<List<Root>> 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<List<Tool>> 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();
}
}

View file

@ -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<ServerResponse> 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);
}
}
}
}

View file

@ -15,15 +15,18 @@ import io.modelcontextprotocol.spec.McpClientTransport;
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<McpSchema.JSONRPCMessage> inbound = Sinks.many().unicast().onBackpressureBuffer();

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -14,10 +14,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
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");

View file

@ -12,7 +12,6 @@ import java.util.Map;
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 @@ import reactor.core.publisher.Mono;
*/
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 @@ public interface McpClient {
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 @@ public interface McpClient {
*/
class SyncSpec {
private final ClientMcpTransport transport;
private final McpClientTransport transport;
private Duration requestTimeout = Duration.ofSeconds(20); // Default timeout
@ -216,7 +175,7 @@ public interface McpClient {
private Function<CreateMessageRequest, CreateMessageResult> 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 interface McpClient {
*/
class AsyncSpec {
private final ClientMcpTransport transport;
private final McpClientTransport transport;
private Duration requestTimeout = Duration.ofSeconds(20); // Default timeout
@ -455,7 +414,7 @@ public interface McpClient {
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
private AsyncSpec(ClientMcpTransport transport) {
private AsyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
}

View file

@ -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;
}

View file

@ -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.UUID;
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 class McpAsyncServer {
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 class McpAsyncServer {
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<McpSchema.ListRootsResult> 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<McpSchema.ListRootsResult> 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<Void> 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 class McpAsyncServer {
// ---------------------------------------
// 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<Void> 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 class McpAsyncServer {
// ---------------------------------------
// 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<Void> 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 class McpAsyncServer {
// ---------------------------------------
// 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 <a href=
* "https://spec.modelcontextprotocol.io/specification/client/sampling/">Sampling
* Specification</a>
* @deprecated This will be removed in 0.9.0. Use
* {@link McpAsyncServerExchange#createMessage(McpSchema.CreateMessageRequest)}.
*/
@Deprecated
public Mono<McpSchema.CreateMessageResult> 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 class McpAsyncServer {
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<Void> closeGracefully() {
return this.mcpTransportProvider.closeGracefully();
@ -514,18 +373,6 @@ public class McpAsyncServer {
this.mcpTransportProvider.close();
}
@Override
@Deprecated
public Mono<McpSchema.ListRootsResult> listRoots() {
return this.listRoots(null);
}
@Override
@Deprecated
public Mono<McpSchema.ListRootsResult> listRoots(String cursor) {
return Mono.error(new RuntimeException("Not implemented"));
}
private McpServerSession.NotificationHandler asyncRootsListChangedNotificationHandler(
List<BiFunction<McpAsyncServerExchange, List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers) {
return (exchange, params) -> exchange.listRoots()
@ -574,11 +421,6 @@ public class McpAsyncServer {
});
}
@Override
public Mono<Void> addTool(McpServerFeatures.AsyncToolRegistration toolRegistration) {
return this.addTool(toolRegistration.toSpecification());
}
@Override
public Mono<Void> removeTool(String toolName) {
if (toolName == null) {
@ -661,11 +503,6 @@ public class McpAsyncServer {
});
}
@Override
public Mono<Void> addResource(McpServerFeatures.AsyncResourceRegistration resourceHandler) {
return this.addResource(resourceHandler.toSpecification());
}
@Override
public Mono<Void> removeResource(String resourceUri) {
if (resourceUri == null) {
@ -756,11 +593,6 @@ public class McpAsyncServer {
});
}
@Override
public Mono<Void> addPrompt(McpServerFeatures.AsyncPromptRegistration promptRegistration) {
return this.addPrompt(promptRegistration.toSpecification());
}
@Override
public Mono<Void> removePrompt(String promptName) {
if (promptName == null) {
@ -859,648 +691,6 @@ public class McpAsyncServer {
// ---------------------------------------
@Override
@Deprecated
public Mono<McpSchema.CreateMessageResult> createMessage(McpSchema.CreateMessageRequest createMessageRequest) {
return Mono.error(new RuntimeException("Not implemented"));
}
@Override
void setProtocolVersions(List<String> 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<McpServerFeatures.AsyncToolSpecification> tools = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<McpSchema.ResourceTemplate> resourceTemplates = new CopyOnWriteArrayList<>();
private final ConcurrentHashMap<String, McpServerFeatures.AsyncResourceSpecification> resources = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, McpServerFeatures.AsyncPromptSpecification> prompts = new ConcurrentHashMap<>();
private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
/**
* Supported protocol versions.
*/
private List<String> 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<String, McpClientSession.RequestHandler<?>> 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<String, McpClientSession.NotificationHandler> notificationHandlers = new HashMap<>();
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (params) -> Mono.empty());
List<BiFunction<McpAsyncServerExchange, List<McpSchema.Root>, Mono<Void>>> rootsChangeHandlers = features
.rootsChangeConsumers();
List<Function<List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers = rootsChangeHandlers.stream()
.map(handler -> (Function<List<McpSchema.Root>, Mono<Void>>) (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<Void> addTool(McpServerFeatures.AsyncToolSpecification toolSpecification) {
throw new IllegalArgumentException(
"McpAsyncServer configured with legacy " + "transport. Use McpServerTransportProvider instead.");
}
@Override
public Mono<Void> addResource(McpServerFeatures.AsyncResourceSpecification resourceHandler) {
throw new IllegalArgumentException(
"McpAsyncServer configured with legacy " + "transport. Use McpServerTransportProvider instead.");
}
@Override
public Mono<Void> addPrompt(McpServerFeatures.AsyncPromptSpecification promptSpecification) {
throw new IllegalArgumentException(
"McpAsyncServer configured with legacy " + "transport. Use McpServerTransportProvider instead.");
}
// ---------------------------------------
// Lifecycle Management
// ---------------------------------------
private McpClientSession.RequestHandler<McpSchema.InitializeResult> asyncInitializeRequestHandler() {
return params -> {
McpSchema.InitializeRequest initializeRequest = transport.unmarshalFrom(params,
new TypeReference<McpSchema.InitializeRequest>() {
});
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<Void> closeGracefully() {
return this.mcpSession.closeGracefully();
}
/**
* Close the server immediately.
*/
public void close() {
this.mcpSession.close();
}
private static final TypeReference<McpSchema.ListRootsResult> 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<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 A Mono that emits the list of roots result containing
*/
public Mono<McpSchema.ListRootsResult> 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<Function<List<McpSchema.Root>, Mono<Void>>> 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<Void> 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<Void> 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<Void> notifyToolsListChanged() {
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null);
}
private McpClientSession.RequestHandler<McpSchema.ListToolsResult> toolsListRequestHandler() {
return params -> {
List<Tool> tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList();
return Mono.just(new McpSchema.ListToolsResult(tools, null));
};
}
private McpClientSession.RequestHandler<CallToolResult> toolsCallRequestHandler() {
return params -> {
McpSchema.CallToolRequest callToolRequest = transport.unmarshalFrom(params,
new TypeReference<McpSchema.CallToolRequest>() {
});
Optional<McpServerFeatures.AsyncToolSpecification> 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<Void> 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<Void> 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<Void> notifyResourcesListChanged() {
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED, null);
}
private McpClientSession.RequestHandler<McpSchema.ListResourcesResult> 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<McpSchema.ListResourceTemplatesResult> resourceTemplateListRequestHandler() {
return params -> Mono.just(new McpSchema.ListResourceTemplatesResult(this.resourceTemplates, null));
}
private McpClientSession.RequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
return params -> {
McpSchema.ReadResourceRequest resourceRequest = transport.unmarshalFrom(params,
new TypeReference<McpSchema.ReadResourceRequest>() {
});
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<Void> 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<Void> 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<Void> notifyPromptsListChanged() {
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null);
}
private McpClientSession.RequestHandler<McpSchema.ListPromptsResult> promptsListRequestHandler() {
return params -> {
// TODO: Implement pagination
// McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
// new TypeReference<McpSchema.PaginatedRequest>() {
// });
var promptList = this.prompts.values()
.stream()
.map(McpServerFeatures.AsyncPromptSpecification::prompt)
.toList();
return Mono.just(new McpSchema.ListPromptsResult(promptList, null));
};
}
private McpClientSession.RequestHandler<McpSchema.GetPromptResult> promptsGetRequestHandler() {
return params -> {
McpSchema.GetPromptRequest promptRequest = transport.unmarshalFrom(params,
new TypeReference<McpSchema.GetPromptRequest>() {
});
// 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<Void> loggingNotification(LoggingMessageNotification loggingMessageNotification) {
if (loggingMessageNotification == null) {
return Mono.error(new McpError("Logging message must not be null"));
}
Map<String, Object> params = this.transport.unmarshalFrom(loggingMessageNotification,
new TypeReference<Map<String, Object>>() {
});
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<Void> setLoggerRequestHandler() {
return params -> {
this.minLoggingLevel = transport.unmarshalFrom(params, new TypeReference<LoggingLevel>() {
});
return Mono.empty();
};
}
// ---------------------------------------
// Sampling
// ---------------------------------------
private static final TypeReference<McpSchema.CreateMessageResult> 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 <a href=
* "https://spec.modelcontextprotocol.io/specification/client/sampling/">Sampling
* Specification</a>
*/
public Mono<McpSchema.CreateMessageResult> 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<String> protocolVersions) {
this.protocolVersions = protocolVersions;
}

View file

@ -11,16 +11,12 @@ import java.util.List;
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 @@ public interface McpServer {
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 @@ public interface McpServer {
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 interface McpServer {
}
/**
* 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<McpServerFeatures.AsyncToolRegistration> 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<String, McpServerFeatures.AsyncResourceRegistration> resources = new HashMap<>();
private final List<ResourceTemplate> 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<String, McpServerFeatures.AsyncPromptRegistration> prompts = new HashMap<>();
private final List<Function<List<McpSchema.Root>, Mono<Void>>> 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:
* <ul>
* <li>Tool execution
* <li>Resource access
* <li>Prompt handling
* <li>Streaming responses
* <li>Batch operations
* </ul>
* @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.
*
* <p>
* Example usage: <pre>{@code
* .tool(
* new Tool("calculator", "Performs calculations", schema),
* args -> Mono.just(new CallToolResult("Result: " + calculate(args)))
* )
* }</pre>
* @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<Map<String, Object>, Mono<CallToolResult>> 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<McpServerFeatures.AsyncToolRegistration> 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.
*
* <p>
* Example usage: <pre>{@code
* .tools(
* new McpServerFeatures.AsyncToolRegistration(calculatorTool, calculatorHandler),
* new McpServerFeatures.AsyncToolRegistration(weatherTool, weatherHandler),
* new McpServerFeatures.AsyncToolRegistration(fileManagerTool, fileManagerHandler)
* )
* }</pre>
* @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<String, McpServerFeatures.AsyncResourceRegistration> 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<McpServerFeatures.AsyncResourceRegistration> 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.
*
* <p>
* Example usage: <pre>{@code
* .resources(
* new McpServerFeatures.AsyncResourceRegistration(fileResource, fileHandler),
* new McpServerFeatures.AsyncResourceRegistration(dbResource, dbHandler),
* new McpServerFeatures.AsyncResourceRegistration(apiResource, apiHandler)
* )
* }</pre>
* @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.
*
* <p>
* Example usage: <pre>{@code
* .resourceTemplates(
* new ResourceTemplate("file://{path}", "Access files by path"),
* new ResourceTemplate("db://{table}/{id}", "Access database records")
* )
* }</pre>
* @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<ResourceTemplate> 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.
*
* <p>
* Example usage: <pre>{@code
* .prompts(Map.of("analysis", new McpServerFeatures.AsyncPromptRegistration(
* new Prompt("analysis", "Code analysis template"),
* request -> Mono.just(new GetPromptResult(generateAnalysisPrompt(request)))
* )));
* }</pre>
* @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<String, McpServerFeatures.AsyncPromptRegistration> 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<McpServerFeatures.AsyncPromptRegistration> 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.
*
* <p>
* Example usage: <pre>{@code
* .prompts(
* new McpServerFeatures.AsyncPromptRegistration(analysisPrompt, analysisHandler),
* new McpServerFeatures.AsyncPromptRegistration(summaryPrompt, summaryHandler),
* new McpServerFeatures.AsyncPromptRegistration(reviewPrompt, reviewHandler)
* )
* }</pre>
* @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<List<McpSchema.Root>, Mono<Void>> 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<Function<List<McpSchema.Root>, Mono<Void>>> 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<List<McpSchema.Root>, Mono<Void>>... consumers) {
for (Function<List<McpSchema.Root>, Mono<Void>> 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<McpAsyncServerExchange, List<McpSchema.Root>, Mono<Void>>) (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<McpServerFeatures.SyncToolRegistration> 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<String, McpServerFeatures.SyncResourceRegistration> resources = new HashMap<>();
private final List<ResourceTemplate> 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<String, McpServerFeatures.SyncPromptRegistration> prompts = new HashMap<>();
private final List<Consumer<List<McpSchema.Root>>> 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:
* <ul>
* <li>Tool execution
* <li>Resource access
* <li>Prompt handling
* <li>Streaming responses
* <li>Batch operations
* </ul>
* @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.
*
* <p>
* Example usage: <pre>{@code
* .tool(
* new Tool("calculator", "Performs calculations", schema),
* args -> new CallToolResult("Result: " + calculate(args))
* )
* }</pre>
* @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<Map<String, Object>, 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<McpServerFeatures.SyncToolRegistration> 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.
*
* <p>
* Example usage: <pre>{@code
* .tools(
* new ToolRegistration(calculatorTool, calculatorHandler),
* new ToolRegistration(weatherTool, weatherHandler),
* new ToolRegistration(fileManagerTool, fileManagerHandler)
* )
* }</pre>
* @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<String, McpServerFeatures.SyncResourceRegistration> 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<McpServerFeatures.SyncResourceRegistration> 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.
*
* <p>
* Example usage: <pre>{@code
* .resources(
* new ResourceRegistration(fileResource, fileHandler),
* new ResourceRegistration(dbResource, dbHandler),
* new ResourceRegistration(apiResource, apiHandler)
* )
* }</pre>
* @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.
*
* <p>
* Example usage: <pre>{@code
* .resourceTemplates(
* new ResourceTemplate("file://{path}", "Access files by path"),
* new ResourceTemplate("db://{table}/{id}", "Access database records")
* )
* }</pre>
* @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<ResourceTemplate> 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.
*
* <p>
* Example usage: <pre>{@code
* Map<String, PromptRegistration> prompts = new HashMap<>();
* prompts.put("analysis", new PromptRegistration(
* new Prompt("analysis", "Code analysis template"),
* request -> new GetPromptResult(generateAnalysisPrompt(request))
* ));
* .prompts(prompts)
* }</pre>
* @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<String, McpServerFeatures.SyncPromptRegistration> 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<McpServerFeatures.SyncPromptRegistration> 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.
*
* <p>
* Example usage: <pre>{@code
* .prompts(
* new PromptRegistration(analysisPrompt, analysisHandler),
* new PromptRegistration(summaryPrompt, summaryHandler),
* new PromptRegistration(reviewPrompt, reviewHandler)
* )
* }</pre>
* @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<List<McpSchema.Root>> 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<Consumer<List<McpSchema.Root>>> 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<List<McpSchema.Root>>... consumers) {
for (Consumer<List<McpSchema.Root>> 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<McpSyncServerExchange, List<McpSchema.Root>>) (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);
}
}
}

View file

@ -10,7 +10,6 @@ import java.util.List;
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 class McpServerFeatures {
BiFunction<McpSyncServerExchange, McpSchema.GetPromptRequest, McpSchema.GetPromptResult> 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:
* <ul>
* <li>Performing calculations
* <li>Accessing external APIs
* <li>Querying databases
* <li>Manipulating files
* <li>Executing system commands
* </ul>
*
* <p>
* Example tool registration: <pre>{@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)));
* }
* )
* }</pre>
*
* @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<Map<String, Object>, Mono<McpSchema.CallToolResult>> 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:
* <ul>
* <li>File contents
* <li>Database records
* <li>API responses
* <li>System information
* <li>Application state
* </ul>
*
* <p>
* Example resource registration: <pre>{@code
* new McpServerFeatures.AsyncResourceRegistration(
* new Resource("docs", "Documentation files", "text/markdown"),
* request -> {
* String content = readFile(request.getPath());
* return Mono.just(new ReadResourceResult(content));
* }
* )
* }</pre>
*
* @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<McpSchema.ReadResourceRequest, Mono<McpSchema.ReadResourceResult>> 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:
* <ul>
* <li>Consistent message formatting
* <li>Parameter substitution
* <li>Context injection
* <li>Response formatting
* <li>Instruction templating
* </ul>
*
* <p>
* Example prompt registration: <pre>{@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:"
* ));
* }
* )
* }</pre>
*
* @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<McpSchema.GetPromptRequest, Mono<McpSchema.GetPromptResult>> 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:
* <ul>
* <li>Performing calculations
* <li>Accessing external APIs
* <li>Querying databases
* <li>Manipulating files
* <li>Executing system commands
* </ul>
*
* <p>
* Example tool registration: <pre>{@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));
* }
* )
* }</pre>
*
* @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<Map<String, Object>, 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:
* <ul>
* <li>File contents
* <li>Database records
* <li>API responses
* <li>System information
* <li>Application state
* </ul>
*
* <p>
* Example resource registration: <pre>{@code
* new McpServerFeatures.SyncResourceRegistration(
* new Resource("docs", "Documentation files", "text/markdown"),
* request -> {
* String content = readFile(request.getPath());
* return new ReadResourceResult(content);
* }
* )
* }</pre>
*
* @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<McpSchema.ReadResourceRequest, McpSchema.ReadResourceResult> 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:
* <ul>
* <li>Consistent message formatting
* <li>Parameter substitution
* <li>Context injection
* <li>Response formatting
* <li>Instruction templating
* </ul>
*
* <p>
* Example prompt registration: <pre>{@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:"
* );
* }
* )
* }</pre>
*
* @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<McpSchema.GetPromptRequest, McpSchema.GetPromptResult> promptHandler) {
public SyncPromptSpecification toSpecification() {
return new SyncPromptSpecification(prompt, (exchange, request) -> promptHandler.apply(request));
}
}
}

View file

@ -65,40 +65,6 @@ public class McpSyncServer {
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 class McpSyncServer {
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 class McpSyncServer {
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 class McpSyncServer {
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 class McpSyncServer {
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.
*
* <p>
* 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.
*
* <p>
* 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 <a href=
* "https://spec.modelcontextprotocol.io/specification/client/sampling/">Sampling
* Specification</a>
* @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();
}
}

View file

@ -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}.
*
* <p>
* The transport handles two types of endpoints:
* <ul>
* <li>SSE endpoint (/sse) - Establishes a long-lived connection for server-to-client
* events</li>
* <li>Message endpoint (configurable) - Handles client-to-server message requests</li>
* </ul>
*
* <p>
* Features:
* <ul>
* <li>Asynchronous message handling using Servlet 6.0 async support</li>
* <li>Session management for multiple client connections</li>
* <li>Graceful shutdown support</li>
* <li>Error handling and response formatting</li>
* </ul>
* @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<String, ClientSession> 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<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> 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.
* <p>
* 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.
* <p>
* 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<Void> connect(Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler) {
this.connectHandler = handler;
return Mono.empty();
}
/**
* Broadcasts a message to all connected clients.
* <p>
* 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<Void> 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.
* <p>
* 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 <T> The target type
* @param data The source data
* @param typeRef The type reference for the target type
* @return The unmarshaled data
*/
@Override
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return objectMapper.convertValue(data, typeRef);
}
/**
* Initiates a graceful shutdown of the transport.
* <p>
* 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<Void> 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.
* <p>
* 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.
* <p>
* 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();
}
}

Some files were not shown because too many files have changed in this diff Show more