diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransport.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransport.java deleted file mode 100644 index fb0b581e0..000000000 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransport.java +++ /dev/null @@ -1,413 +0,0 @@ -package io.modelcontextprotocol.server.transport; - -import java.io.IOException; -import java.time.Duration; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import io.modelcontextprotocol.util.Assert; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.Sinks; - -import org.springframework.http.HttpStatus; -import org.springframework.http.MediaType; -import org.springframework.http.codec.ServerSentEvent; -import org.springframework.web.reactive.function.server.RouterFunction; -import org.springframework.web.reactive.function.server.RouterFunctions; -import org.springframework.web.reactive.function.server.ServerRequest; -import org.springframework.web.reactive.function.server.ServerResponse; - -/** - * Server-side implementation of the MCP (Model Context Protocol) HTTP transport using - * Server-Sent Events (SSE). This implementation provides a bidirectional communication - * channel between MCP clients and servers using HTTP POST for client-to-server messages - * and SSE for server-to-client messages. - * - *

- * Key features: - *

- * - *

- * The transport sets up two main endpoints: - *

- * - *

- * This implementation is thread-safe and can handle multiple concurrent client - * connections. It uses {@link ConcurrentHashMap} for session management and Reactor's - * {@link Sinks} for thread-safe message broadcasting. - * - * @author Christian Tzolov - * @author Alexandros Pappas - * @see ServerMcpTransport - * @see ServerSentEvent - * @deprecated This class will be removed in 0.9.0. Use - * {@link WebFluxSseServerTransportProvider}. - */ -@Deprecated -public class WebFluxSseServerTransport implements ServerMcpTransport { - - private static final Logger logger = LoggerFactory.getLogger(WebFluxSseServerTransport.class); - - /** - * Event type for JSON-RPC messages sent through the SSE connection. - */ - public static final String MESSAGE_EVENT_TYPE = "message"; - - /** - * Event type for sending the message endpoint URI to clients. - */ - public static final String ENDPOINT_EVENT_TYPE = "endpoint"; - - /** - * Default SSE endpoint path as specified by the MCP transport specification. - */ - public static final String DEFAULT_SSE_ENDPOINT = "/sse"; - - private final ObjectMapper objectMapper; - - private final String messageEndpoint; - - private final String sseEndpoint; - - private final RouterFunction routerFunction; - - /** - * Map of active client sessions, keyed by session ID. - */ - private final ConcurrentHashMap sessions = new ConcurrentHashMap<>(); - - /** - * Flag indicating if the transport is shutting down. - */ - private volatile boolean isClosing = false; - - private Function, Mono> connectHandler; - - /** - * Constructs a new WebFlux SSE server transport instance. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization - * of MCP messages. Must not be null. - * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC - * messages. This endpoint will be communicated to clients during SSE connection - * setup. Must not be null. - * @throws IllegalArgumentException if either parameter is null - */ - public WebFluxSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - Assert.notNull(messageEndpoint, "Message endpoint must not be null"); - Assert.notNull(sseEndpoint, "SSE endpoint must not be null"); - - this.objectMapper = objectMapper; - this.messageEndpoint = messageEndpoint; - this.sseEndpoint = sseEndpoint; - this.routerFunction = RouterFunctions.route() - .GET(this.sseEndpoint, this::handleSseConnection) - .POST(this.messageEndpoint, this::handleMessage) - .build(); - } - - /** - * Constructs a new WebFlux SSE server transport instance with the default SSE - * endpoint. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization - * of MCP messages. Must not be null. - * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC - * messages. This endpoint will be communicated to clients during SSE connection - * setup. Must not be null. - * @throws IllegalArgumentException if either parameter is null - */ - public WebFluxSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) { - this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT); - } - - /** - * Configures the message handler for this transport. In the WebFlux SSE - * implementation, this method stores the handler for processing incoming messages but - * doesn't establish any connections since the server accepts connections rather than - * initiating them. - * @param handler A function that processes incoming JSON-RPC messages and returns - * responses. This handler will be called for each message received through the - * message endpoint. - * @return An empty Mono since the server doesn't initiate connections - */ - @Override - public Mono connect(Function, Mono> handler) { - this.connectHandler = handler; - // Server-side transport doesn't initiate connections - return Mono.empty().then(); - } - - /** - * Broadcasts a JSON-RPC message to all connected clients through their SSE - * connections. The message is serialized to JSON and sent as a server-sent event to - * each active session. - * - *

- * The method: - *

- * @param message The JSON-RPC message to broadcast - * @return A Mono that completes when the message has been sent to all sessions, or - * errors if any session fails to receive the message - */ - @Override - public Mono sendMessage(McpSchema.JSONRPCMessage message) { - if (sessions.isEmpty()) { - logger.debug("No active sessions to broadcast message to"); - return Mono.empty(); - } - - return Mono.create(sink -> { - try {// @formatter:off - String jsonText = objectMapper.writeValueAsString(message); - ServerSentEvent event = ServerSentEvent.builder() - .event(MESSAGE_EVENT_TYPE) - .data(jsonText) - .build(); - - logger.debug("Attempting to broadcast message to {} active sessions", sessions.size()); - - List failedSessions = sessions.values().stream() - .filter(session -> session.messageSink.tryEmitNext(event).isFailure()) - .map(session -> session.id) - .toList(); - - if (failedSessions.isEmpty()) { - logger.debug("Successfully broadcast message to all sessions"); - sink.success(); - } - else { - String error = "Failed to broadcast message to sessions: " + String.join(", ", failedSessions); - logger.error(error); - sink.error(new RuntimeException(error)); - } // @formatter:on - } - catch (IOException e) { - logger.error("Failed to serialize message: {}", e.getMessage()); - sink.error(e); - } - }); - } - - /** - * Converts data from one type to another using the configured ObjectMapper. This - * method is primarily used for converting between different representations of - * JSON-RPC message data. - * @param The target type to convert to - * @param data The source data to convert - * @param typeRef Type reference describing the target type - * @return The converted data - * @throws IllegalArgumentException if the conversion fails - */ - @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return this.objectMapper.convertValue(data, typeRef); - } - - /** - * Initiates a graceful shutdown of the transport. This method ensures all active - * sessions are properly closed and cleaned up. - * - *

- * The shutdown process: - *

    - *
  • Marks the transport as closing to prevent new connections
  • - *
  • Closes each active session
  • - *
  • Removes closed sessions from the sessions map
  • - *
  • Times out after 5 seconds if shutdown takes too long
  • - *
- * @return A Mono that completes when all sessions have been closed - */ - @Override - public Mono closeGracefully() { - return Mono.fromRunnable(() -> { - isClosing = true; - logger.debug("Initiating graceful shutdown with {} active sessions", sessions.size()); - }).then(Mono.when(sessions.values().stream().map(session -> { - String sessionId = session.id; - return Mono.fromRunnable(() -> session.close()) - .then(Mono.delay(Duration.ofMillis(100))) - .then(Mono.fromRunnable(() -> sessions.remove(sessionId))); - }).toList())) - .timeout(Duration.ofSeconds(5)) - .doOnSuccess(v -> logger.debug("Graceful shutdown completed")) - .doOnError(e -> logger.error("Error during graceful shutdown: {}", e.getMessage())); - } - - /** - * Returns the WebFlux router function that defines the transport's HTTP endpoints. - * This router function should be integrated into the application's web configuration. - * - *

- * The router function defines two endpoints: - *

    - *
  • GET {sseEndpoint} - For establishing SSE connections
  • - *
  • POST {messageEndpoint} - For receiving client messages
  • - *
- * @return The configured {@link RouterFunction} for handling HTTP requests - */ - public RouterFunction getRouterFunction() { - return this.routerFunction; - } - - /** - * Handles new SSE connection requests from clients. Creates a new session for each - * connection and sets up the SSE event stream. - * - *

- * The handler performs the following steps: - *

    - *
  • Generates a unique session ID
  • - *
  • Creates a new ClientSession instance
  • - *
  • Sends the message endpoint URI as an initial event
  • - *
  • Sets up message forwarding for the session
  • - *
  • Handles connection cleanup on completion or errors
  • - *
- * @param request The incoming server request - * @return A response with the SSE event stream - */ - private Mono handleSseConnection(ServerRequest request) { - if (isClosing) { - return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down"); - } - String sessionId = UUID.randomUUID().toString(); - logger.debug("Creating new SSE connection for session: {}", sessionId); - ClientSession session = new ClientSession(sessionId); - this.sessions.put(sessionId, session); - - return ServerResponse.ok() - .contentType(MediaType.TEXT_EVENT_STREAM) - .body(Flux.>create(sink -> { - // Send initial endpoint event - logger.debug("Sending initial endpoint event to session: {}", sessionId); - sink.next(ServerSentEvent.builder().event(ENDPOINT_EVENT_TYPE).data(messageEndpoint).build()); - - // Subscribe to session messages - session.messageSink.asFlux() - .doOnSubscribe(s -> logger.debug("Session {} subscribed to message sink", sessionId)) - .doOnComplete(() -> { - logger.debug("Session {} completed", sessionId); - sessions.remove(sessionId); - }) - .doOnError(error -> { - logger.error("Error in session {}: {}", sessionId, error.getMessage()); - sessions.remove(sessionId); - }) - .doOnCancel(() -> { - logger.debug("Session {} cancelled", sessionId); - sessions.remove(sessionId); - }) - .subscribe(event -> { - logger.debug("Forwarding event to session {}: {}", sessionId, event); - sink.next(event); - }, sink::error, sink::complete); - - sink.onCancel(() -> { - logger.debug("Session {} cancelled", sessionId); - sessions.remove(sessionId); - }); - }), ServerSentEvent.class); - } - - /** - * Handles incoming JSON-RPC messages from clients. Deserializes the message and - * processes it through the configured message handler. - * - *

- * The handler: - *

    - *
  • Deserializes the incoming JSON-RPC message
  • - *
  • Passes it through the message handler chain
  • - *
  • Returns appropriate HTTP responses based on processing results
  • - *
  • Handles various error conditions with appropriate error responses
  • - *
- * @param request The incoming server request containing the JSON-RPC message - * @return A response indicating the message processing result - */ - private Mono handleMessage(ServerRequest request) { - if (isClosing) { - return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down"); - } - - return request.bodyToMono(String.class).flatMap(body -> { - try { - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body); - return Mono.just(message) - .transform(this.connectHandler) - .flatMap(response -> ServerResponse.ok().build()) - .onErrorResume(error -> { - logger.error("Error processing message: {}", error.getMessage()); - return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR) - .bodyValue(new McpError(error.getMessage())); - }); - } - catch (IllegalArgumentException | IOException e) { - logger.error("Failed to deserialize message: {}", e.getMessage()); - return ServerResponse.badRequest().bodyValue(new McpError("Invalid message format")); - } - }); - } - - /** - * Represents an active client SSE connection session. Manages the message sink for - * sending events to the client and handles session lifecycle. - * - *

- * Each session: - *

    - *
  • Has a unique identifier
  • - *
  • Maintains its own message sink for event broadcasting
  • - *
  • Supports clean shutdown through the close method
  • - *
- */ - private static class ClientSession { - - private final String id; - - private final Sinks.Many> messageSink; - - ClientSession(String id) { - this.id = id; - logger.debug("Creating new session: {}", id); - this.messageSink = Sinks.many().replay().latest(); - logger.debug("Session {} initialized with replay sink", id); - } - - void close() { - logger.debug("Closing session: {}", id); - Sinks.EmitResult result = messageSink.tryEmitComplete(); - if (result.isFailure()) { - logger.warn("Failed to complete message sink for session {}: {}", id, result); - } - else { - logger.debug("Successfully completed message sink for session {}", id); - } - } - - } - -} \ No newline at end of file diff --git a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java index cf3eeae03..4e5d2fafb 100644 --- a/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java +++ b/mcp-spring/mcp-spring-webflux/src/main/java/io/modelcontextprotocol/server/transport/WebFluxSseServerTransportProvider.java @@ -8,10 +8,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpServerSession; import io.modelcontextprotocol.spec.McpServerTransport; import io.modelcontextprotocol.spec.McpServerTransportProvider; -import io.modelcontextprotocol.spec.McpServerSession; -import io.modelcontextprotocol.spec.ServerMcpTransport; import io.modelcontextprotocol.util.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,7 +18,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; -import reactor.core.publisher.Sinks; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpAsyncServerDeprecatedTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpAsyncServerDeprecatedTests.java deleted file mode 100644 index b460284ee..000000000 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpAsyncServerDeprecatedTests.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.server; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.server.transport.WebFluxSseServerTransport; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import org.junit.jupiter.api.Timeout; -import reactor.netty.DisposableServer; -import reactor.netty.http.server.HttpServer; - -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; -import org.springframework.web.reactive.function.server.RouterFunctions; - -/** - * Tests for {@link McpAsyncServer} using {@link WebFluxSseServerTransport}. - * - * @author Christian Tzolov - */ -@Deprecated -@Timeout(15) // Giving extra time beyond the client timeout -class WebFluxSseMcpAsyncServerDeprecatedTests extends AbstractMcpAsyncServerDeprecatedTests { - - private static final int PORT = 8181; - - private static final String MESSAGE_ENDPOINT = "/mcp/message"; - - private DisposableServer httpServer; - - @Override - protected ServerMcpTransport createMcpTransport() { - var transport = new WebFluxSseServerTransport(new ObjectMapper(), MESSAGE_ENDPOINT); - - HttpHandler httpHandler = RouterFunctions.toHttpHandler(transport.getRouterFunction()); - ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler); - httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow(); - - return transport; - } - - @Override - protected void onStart() { - } - - @Override - protected void onClose() { - if (httpServer != null) { - httpServer.disposeNow(); - } - } - -} diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpSyncServerDeprecatecTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpSyncServerDeprecatecTests.java deleted file mode 100644 index be2bf6c7f..000000000 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/WebFluxSseMcpSyncServerDeprecatecTests.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.server; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.server.transport.WebFluxSseServerTransport; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import org.junit.jupiter.api.Timeout; -import reactor.netty.DisposableServer; -import reactor.netty.http.server.HttpServer; - -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; -import org.springframework.web.reactive.function.server.RouterFunctions; - -/** - * Tests for {@link McpSyncServer} using {@link WebFluxSseServerTransport}. - * - * @author Christian Tzolov - */ -@Deprecated -@Timeout(15) // Giving extra time beyond the client timeout -class WebFluxSseMcpSyncServerDeprecatecTests extends AbstractMcpSyncServerDeprecatedTests { - - private static final int PORT = 8182; - - private static final String MESSAGE_ENDPOINT = "/mcp/message"; - - private DisposableServer httpServer; - - private WebFluxSseServerTransport transport; - - @Override - protected ServerMcpTransport createMcpTransport() { - transport = new WebFluxSseServerTransport(new ObjectMapper(), MESSAGE_ENDPOINT); - return transport; - } - - @Override - protected void onStart() { - HttpHandler httpHandler = RouterFunctions.toHttpHandler(transport.getRouterFunction()); - ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler); - httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow(); - } - - @Override - protected void onClose() { - if (httpServer != null) { - httpServer.disposeNow(); - } - } - -} diff --git a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/legacy/WebFluxSseIntegrationTests.java b/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/legacy/WebFluxSseIntegrationTests.java deleted file mode 100644 index 981e114c9..000000000 --- a/mcp-spring/mcp-spring-webflux/src/test/java/io/modelcontextprotocol/server/legacy/WebFluxSseIntegrationTests.java +++ /dev/null @@ -1,459 +0,0 @@ -/* - * Copyright 2024 - 2024 the original author or authors. - */ -package io.modelcontextprotocol.server.legacy; - -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.client.McpClient; -import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; -import io.modelcontextprotocol.client.transport.WebFluxSseClientTransport; -import io.modelcontextprotocol.server.McpServer; -import io.modelcontextprotocol.server.McpServerFeatures; -import io.modelcontextprotocol.server.transport.WebFluxSseServerTransport; -import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; -import io.modelcontextprotocol.spec.McpSchema.InitializeResult; -import io.modelcontextprotocol.spec.McpSchema.Role; -import io.modelcontextprotocol.spec.McpSchema.Root; -import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; -import io.modelcontextprotocol.spec.McpSchema.Tool; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import reactor.netty.DisposableServer; -import reactor.netty.http.server.HttpServer; -import reactor.test.StepVerifier; - -import org.springframework.http.server.reactive.HttpHandler; -import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter; -import org.springframework.web.client.RestClient; -import org.springframework.web.reactive.function.client.WebClient; -import org.springframework.web.reactive.function.server.RouterFunctions; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.awaitility.Awaitility.await; - -public class WebFluxSseIntegrationTests { - - private static final int PORT = 8182; - - private static final String MESSAGE_ENDPOINT = "/mcp/message"; - - private DisposableServer httpServer; - - private WebFluxSseServerTransport mcpServerTransport; - - ConcurrentHashMap clientBulders = new ConcurrentHashMap<>(); - - @BeforeEach - public void before() { - - this.mcpServerTransport = new WebFluxSseServerTransport(new ObjectMapper(), MESSAGE_ENDPOINT); - - HttpHandler httpHandler = RouterFunctions.toHttpHandler(mcpServerTransport.getRouterFunction()); - ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler); - this.httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow(); - - clientBulders.put("httpclient", McpClient.sync(new HttpClientSseClientTransport("http://localhost:" + PORT))); - clientBulders.put("webflux", - McpClient.sync(new WebFluxSseClientTransport(WebClient.builder().baseUrl("http://localhost:" + PORT)))); - - } - - @AfterEach - public void after() { - if (httpServer != null) { - httpServer.disposeNow(); - } - } - - // --------------------------------------- - // Sampling Tests - // --------------------------------------- - @Test - void testCreateMessageWithoutInitialization() { - var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build(); - - var messages = List.of(new McpSchema.SamplingMessage(Role.USER, new McpSchema.TextContent("Test message"))); - var modelPrefs = new McpSchema.ModelPreferences(List.of(), 1.0, 1.0, 1.0); - - var request = new CreateMessageRequest(messages, modelPrefs, null, - CreateMessageRequest.ContextInclusionStrategy.NONE, null, 100, List.of(), Map.of()); - - StepVerifier.create(mcpAsyncServer.createMessage(request)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Client must be initialized. Call the initialize method first!"); - }); - } - - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient", "webflux" }) - void testCreateMessageWithoutSamplingCapabilities(String clientType) { - - var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build(); - - var clientBuilder = clientBulders.get(clientType); - - var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")).build(); - - InitializeResult initResult = client.initialize(); - assertThat(initResult).isNotNull(); - - var messages = List.of(new McpSchema.SamplingMessage(Role.USER, new McpSchema.TextContent("Test message"))); - var modelPrefs = new McpSchema.ModelPreferences(List.of(), 1.0, 1.0, 1.0); - - var request = new CreateMessageRequest(messages, modelPrefs, null, - CreateMessageRequest.ContextInclusionStrategy.NONE, null, 100, List.of(), Map.of()); - - StepVerifier.create(mcpAsyncServer.createMessage(request)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Client must be configured with sampling capabilities"); - }); - } - - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient", "webflux" }) - void testCreateMessageSuccess(String clientType) throws InterruptedException { - - var clientBuilder = clientBulders.get(clientType); - - var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build(); - - Function samplingHandler = request -> { - assertThat(request.messages()).hasSize(1); - assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class); - - return new CreateMessageResult(Role.USER, new McpSchema.TextContent("Test message"), "MockModelName", - CreateMessageResult.StopReason.STOP_SEQUENCE); - }; - - var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) - .capabilities(ClientCapabilities.builder().sampling().build()) - .sampling(samplingHandler) - .build(); - - InitializeResult initResult = client.initialize(); - assertThat(initResult).isNotNull(); - - var messages = List.of(new McpSchema.SamplingMessage(Role.USER, new McpSchema.TextContent("Test message"))); - var modelPrefs = new McpSchema.ModelPreferences(List.of(), 1.0, 1.0, 1.0); - - var request = new CreateMessageRequest(messages, modelPrefs, null, - CreateMessageRequest.ContextInclusionStrategy.NONE, null, 100, List.of(), Map.of()); - - StepVerifier.create(mcpAsyncServer.createMessage(request)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.role()).isEqualTo(Role.USER); - assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); - assertThat(result.model()).isEqualTo("MockModelName"); - assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); - }).verifyComplete(); - } - - // --------------------------------------- - // Roots Tests - // --------------------------------------- - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient", "webflux" }) - void testRootsSuccess(String clientType) { - var clientBuilder = clientBulders.get(clientType); - - List roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2")); - - AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransport) - .rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate)) - .build(); - - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - assertThat(rootsRef.get()).isNull(); - - assertThat(mcpServer.listRoots().roots()).containsAll(roots); - - mcpClient.rootsListChangedNotification(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(roots); - }); - - // Remove a root - mcpClient.removeRoot(roots.get(0).uri()); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(roots.get(1))); - }); - - // Add a new root - var root3 = new Root("uri3://", "root3"); - mcpClient.addRoot(root3); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3)); - }); - - mcpClient.close(); - mcpServer.close(); - } - - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient", "webflux" }) - void testRootsWithoutCapability(String clientType) { - var clientBuilder = clientBulders.get(clientType); - - var mcpServer = McpServer.sync(mcpServerTransport).rootsChangeConsumer(rootsUpdate -> { - }).build(); - - // Create client without roots capability - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()) // No - // roots - // capability - .build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - // Attempt to list roots should fail - assertThatThrownBy(() -> mcpServer.listRoots().roots()).isInstanceOf(McpError.class) - .hasMessage("Roots not supported"); - - mcpClient.close(); - mcpServer.close(); - } - - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient", "webflux" }) - void testRootsWithEmptyRootsList(String clientType) { - var clientBuilder = clientBulders.get(clientType); - - AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransport) - .rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate)) - .build(); - - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(List.of()) // Empty roots list - .build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - mcpClient.rootsListChangedNotification(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).isEmpty(); - }); - - mcpClient.close(); - mcpServer.close(); - } - - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient", "webflux" }) - void testRootsWithMultipleConsumers(String clientType) { - var clientBuilder = clientBulders.get(clientType); - - List roots = List.of(new Root("uri1://", "root1")); - - AtomicReference> rootsRef1 = new AtomicReference<>(); - AtomicReference> rootsRef2 = new AtomicReference<>(); - - var mcpServer = McpServer.sync(mcpServerTransport) - .rootsChangeConsumer(rootsUpdate -> rootsRef1.set(rootsUpdate)) - .rootsChangeConsumer(rootsUpdate -> rootsRef2.set(rootsUpdate)) - .build(); - - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - mcpClient.rootsListChangedNotification(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef1.get()).containsAll(roots); - assertThat(rootsRef2.get()).containsAll(roots); - }); - - mcpClient.close(); - mcpServer.close(); - } - - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient", "webflux" }) - void testRootsServerCloseWithActiveSubscription(String clientType) { - - var clientBuilder = clientBulders.get(clientType); - - List roots = List.of(new Root("uri1://", "root1")); - - AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransport) - .rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate)) - .build(); - - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - mcpClient.rootsListChangedNotification(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(roots); - }); - - // Close server while subscription is active - mcpServer.close(); - - // Verify client can handle server closure gracefully - mcpClient.close(); - } - - // --------------------------------------- - // Tools Tests - // --------------------------------------- - - String emptyJsonSchema = """ - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": {} - } - """; - - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient", "webflux" }) - void testToolCallSuccess(String clientType) { - - var clientBuilder = clientBulders.get(clientType); - - var callResponse = new CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.SyncToolRegistration tool1 = new McpServerFeatures.SyncToolRegistration( - new Tool("tool1", "tool1 description", emptyJsonSchema), request -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - return callResponse; - }); - - var mcpServer = McpServer.sync(mcpServerTransport) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); - - var mcpClient = clientBuilder.build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); - - CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - - assertThat(response).isNotNull(); - assertThat(response).isEqualTo(callResponse); - - mcpClient.close(); - mcpServer.close(); - } - - @ParameterizedTest(name = "{0} : {displayName} ") - @ValueSource(strings = { "httpclient", "webflux" }) - void testToolListChangeHandlingSuccess(String clientType) { - - var clientBuilder = clientBulders.get(clientType); - - var callResponse = new CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.SyncToolRegistration tool1 = new McpServerFeatures.SyncToolRegistration( - new Tool("tool1", "tool1 description", emptyJsonSchema), request -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - return callResponse; - }); - - var mcpServer = McpServer.sync(mcpServerTransport) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); - - AtomicReference> rootsRef = new AtomicReference<>(); - var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - rootsRef.set(toolsUpdate); - }).build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - assertThat(rootsRef.get()).isNull(); - - assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); - - mcpServer.notifyToolsListChanged(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(tool1.tool())); - }); - - // Remove a tool - mcpServer.removeTool("tool1"); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).isEmpty(); - }); - - // Add a new tool - McpServerFeatures.SyncToolRegistration tool2 = new McpServerFeatures.SyncToolRegistration( - new Tool("tool2", "tool2 description", emptyJsonSchema), request -> callResponse); - - mcpServer.addTool(tool2); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(tool2.tool())); - }); - - mcpClient.close(); - mcpServer.close(); - } - -} diff --git a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransport.java b/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransport.java deleted file mode 100644 index 23193d106..000000000 --- a/mcp-spring/mcp-spring-webmvc/src/main/java/io/modelcontextprotocol/server/transport/WebMvcSseServerTransport.java +++ /dev/null @@ -1,385 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.server.transport; - -import java.io.IOException; -import java.time.Duration; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import io.modelcontextprotocol.util.Assert; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; - -import org.springframework.http.HttpStatus; -import org.springframework.web.servlet.function.RouterFunction; -import org.springframework.web.servlet.function.RouterFunctions; -import org.springframework.web.servlet.function.ServerRequest; -import org.springframework.web.servlet.function.ServerResponse; -import org.springframework.web.servlet.function.ServerResponse.SseBuilder; - -/** - * Server-side implementation of the Model Context Protocol (MCP) transport layer using - * HTTP with Server-Sent Events (SSE) through Spring WebMVC. This implementation provides - * a bridge between synchronous WebMVC operations and reactive programming patterns to - * maintain compatibility with the reactive transport interface. - * - * @deprecated This class will be removed in 0.9.0. Use - * {@link WebMvcSseServerTransportProvider}. - * - *

- * Key features: - *

    - *
  • Implements bidirectional communication using HTTP POST for client-to-server - * messages and SSE for server-to-client messages
  • - *
  • Manages client sessions with unique IDs for reliable message delivery
  • - *
  • Supports graceful shutdown with proper session cleanup
  • - *
  • Provides JSON-RPC message handling through configured endpoints
  • - *
  • Includes built-in error handling and logging
  • - *
- * - *

- * The transport operates on two main endpoints: - *

    - *
  • {@code /sse} - The SSE endpoint where clients establish their event stream - * connection
  • - *
  • A configurable message endpoint where clients send their JSON-RPC messages via HTTP - * POST
  • - *
- * - *

- * This implementation uses {@link ConcurrentHashMap} to safely manage multiple client - * sessions in a thread-safe manner. Each client session is assigned a unique ID and - * maintains its own SSE connection. - * @author Christian Tzolov - * @author Alexandros Pappas - * @see ServerMcpTransport - * @see RouterFunction - */ -@Deprecated -public class WebMvcSseServerTransport implements ServerMcpTransport { - - private static final Logger logger = LoggerFactory.getLogger(WebMvcSseServerTransport.class); - - /** - * Event type for JSON-RPC messages sent through the SSE connection. - */ - public static final String MESSAGE_EVENT_TYPE = "message"; - - /** - * Event type for sending the message endpoint URI to clients. - */ - public static final String ENDPOINT_EVENT_TYPE = "endpoint"; - - /** - * Default SSE endpoint path as specified by the MCP transport specification. - */ - public static final String DEFAULT_SSE_ENDPOINT = "/sse"; - - private final ObjectMapper objectMapper; - - private final String messageEndpoint; - - private final String sseEndpoint; - - private final RouterFunction routerFunction; - - /** - * Map of active client sessions, keyed by session ID. - */ - private final ConcurrentHashMap sessions = new ConcurrentHashMap<>(); - - /** - * Flag indicating if the transport is shutting down. - */ - private volatile boolean isClosing = false; - - /** - * The function to process incoming JSON-RPC messages and produce responses. - */ - private Function, Mono> connectHandler; - - /** - * Constructs a new WebMvcSseServerTransport instance. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization - * of messages. - * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC - * messages via HTTP POST. This endpoint will be communicated to clients through the - * SSE connection's initial endpoint event. - * @throws IllegalArgumentException if either objectMapper or messageEndpoint is null - */ - public WebMvcSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) { - Assert.notNull(objectMapper, "ObjectMapper must not be null"); - Assert.notNull(messageEndpoint, "Message endpoint must not be null"); - Assert.notNull(sseEndpoint, "SSE endpoint must not be null"); - - this.objectMapper = objectMapper; - this.messageEndpoint = messageEndpoint; - this.sseEndpoint = sseEndpoint; - this.routerFunction = RouterFunctions.route() - .GET(this.sseEndpoint, this::handleSseConnection) - .POST(this.messageEndpoint, this::handleMessage) - .build(); - } - - /** - * Constructs a new WebMvcSseServerTransport instance with the default SSE endpoint. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization - * of messages. - * @param messageEndpoint The endpoint URI where clients should send their JSON-RPC - * messages via HTTP POST. This endpoint will be communicated to clients through the - * SSE connection's initial endpoint event. - * @throws IllegalArgumentException if either objectMapper or messageEndpoint is null - */ - public WebMvcSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) { - this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT); - } - - /** - * Sets up the message handler for this transport. In the WebMVC SSE implementation, - * this method only stores the handler for later use, as connections are initiated by - * clients rather than the server. - * @param connectionHandler The function to process incoming JSON-RPC messages and - * produce responses - * @return An empty Mono since the server doesn't initiate connections - */ - @Override - public Mono connect( - Function, Mono> connectionHandler) { - this.connectHandler = connectionHandler; - // Server-side transport doesn't initiate connections - return Mono.empty(); - } - - /** - * Broadcasts a message to all connected clients through their SSE connections. The - * message is serialized to JSON and sent as an SSE event with type "message". If any - * errors occur during sending to a particular client, they are logged but don't - * prevent sending to other clients. - * @param message The JSON-RPC message to broadcast to all connected clients - * @return A Mono that completes when the broadcast attempt is finished - */ - @Override - public Mono sendMessage(McpSchema.JSONRPCMessage message) { - return Mono.fromRunnable(() -> { - if (sessions.isEmpty()) { - logger.debug("No active sessions to broadcast message to"); - return; - } - - try { - String jsonText = objectMapper.writeValueAsString(message); - logger.debug("Attempting to broadcast message to {} active sessions", sessions.size()); - - sessions.values().forEach(session -> { - try { - session.sseBuilder.id(session.id).event(MESSAGE_EVENT_TYPE).data(jsonText); - } - catch (Exception e) { - logger.error("Failed to send message to session {}: {}", session.id, e.getMessage()); - session.sseBuilder.error(e); - } - }); - } - catch (IOException e) { - logger.error("Failed to serialize message: {}", e.getMessage()); - } - }); - } - - /** - * Handles new SSE connection requests from clients by creating a new session and - * establishing an SSE connection. This method: - *

    - *
  • Generates a unique session ID
  • - *
  • Creates a new ClientSession with an SSE builder
  • - *
  • Sends an initial endpoint event to inform the client where to send - * messages
  • - *
  • Maintains the session in the sessions map
  • - *
- * @param request The incoming server request - * @return A ServerResponse configured for SSE communication, or an error response if - * the server is shutting down or the connection fails - */ - private ServerResponse handleSseConnection(ServerRequest request) { - if (this.isClosing) { - return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down"); - } - - String sessionId = UUID.randomUUID().toString(); - logger.debug("Creating new SSE connection for session: {}", sessionId); - - // Send initial endpoint event - try { - return ServerResponse.sse(sseBuilder -> { - sseBuilder.onComplete(() -> { - logger.debug("SSE connection completed for session: {}", sessionId); - sessions.remove(sessionId); - }); - sseBuilder.onTimeout(() -> { - logger.debug("SSE connection timed out for session: {}", sessionId); - sessions.remove(sessionId); - }); - - ClientSession session = new ClientSession(sessionId, sseBuilder); - this.sessions.put(sessionId, session); - - try { - session.sseBuilder.id(session.id).event(ENDPOINT_EVENT_TYPE).data(messageEndpoint); - } - catch (Exception e) { - logger.error("Failed to poll event from session queue: {}", e.getMessage()); - sseBuilder.error(e); - } - }, Duration.ZERO); - } - catch (Exception e) { - logger.error("Failed to send initial endpoint event to session {}: {}", sessionId, e.getMessage()); - sessions.remove(sessionId); - return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); - } - } - - /** - * Handles incoming JSON-RPC messages from clients. This method: - *
    - *
  • Deserializes the request body into a JSON-RPC message
  • - *
  • Processes the message through the configured connect handler
  • - *
  • Returns appropriate HTTP responses based on the processing result
  • - *
- * @param request The incoming server request containing the JSON-RPC message - * @return A ServerResponse indicating success (200 OK) or appropriate error status - * with error details in case of failures - */ - private ServerResponse handleMessage(ServerRequest request) { - if (this.isClosing) { - return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).body("Server is shutting down"); - } - - try { - String body = request.body(String.class); - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body); - - // Convert the message to a Mono, apply the handler, and block for the - // response - @SuppressWarnings("unused") - McpSchema.JSONRPCMessage response = Mono.just(message).transform(connectHandler).block(); - - return ServerResponse.ok().build(); - } - catch (IllegalArgumentException | IOException e) { - logger.error("Failed to deserialize message: {}", e.getMessage()); - return ServerResponse.badRequest().body(new McpError("Invalid message format")); - } - catch (Exception e) { - logger.error("Error handling message: {}", e.getMessage()); - return ServerResponse.status(HttpStatus.INTERNAL_SERVER_ERROR).body(new McpError(e.getMessage())); - } - } - - /** - * Represents an active client session with its associated SSE connection. Each - * session maintains: - *
    - *
  • A unique session identifier
  • - *
  • An SSE builder for sending server events to the client
  • - *
  • Logging of session lifecycle events
  • - *
- */ - private static class ClientSession { - - private final String id; - - private final SseBuilder sseBuilder; - - /** - * Creates a new client session with the specified ID and SSE builder. - * @param id The unique identifier for this session - * @param sseBuilder The SSE builder for sending server events to the client - */ - ClientSession(String id, SseBuilder sseBuilder) { - this.id = id; - this.sseBuilder = sseBuilder; - logger.debug("Session {} initialized with SSE emitter", id); - } - - /** - * Closes this session by completing the SSE connection. Any errors during - * completion are logged but do not prevent the session from being marked as - * closed. - */ - void close() { - logger.debug("Closing session: {}", id); - try { - sseBuilder.complete(); - logger.debug("Successfully completed SSE emitter for session {}", id); - } - catch (Exception e) { - logger.warn("Failed to complete SSE emitter for session {}: {}", id, e.getMessage()); - // sseBuilder.error(e); - } - } - - } - - /** - * Converts data from one type to another using the configured ObjectMapper. This is - * particularly useful for handling complex JSON-RPC parameter types. - * @param data The source data object to convert - * @param typeRef The target type reference - * @return The converted object of type T - * @param The target type - */ - @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return this.objectMapper.convertValue(data, typeRef); - } - - /** - * Initiates a graceful shutdown of the transport. This method: - *
    - *
  • Sets the closing flag to prevent new connections
  • - *
  • Closes all active SSE connections
  • - *
  • Removes all session records
  • - *
- * @return A Mono that completes when all cleanup operations are finished - */ - @Override - public Mono closeGracefully() { - return Mono.fromRunnable(() -> { - this.isClosing = true; - logger.debug("Initiating graceful shutdown with {} active sessions", sessions.size()); - - sessions.values().forEach(session -> { - String sessionId = session.id; - session.close(); - sessions.remove(sessionId); - }); - - logger.debug("Graceful shutdown completed"); - }); - } - - /** - * Returns the RouterFunction that defines the HTTP endpoints for this transport. The - * router function handles two endpoints: - *
    - *
  • GET /sse - For establishing SSE connections
  • - *
  • POST [messageEndpoint] - For receiving JSON-RPC messages from clients
  • - *
- * @return The configured RouterFunction for handling HTTP requests - */ - public RouterFunction getRouterFunction() { - return this.routerFunction; - } - -} diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportDeprecatedTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportDeprecatedTests.java deleted file mode 100644 index c3f0e3220..000000000 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseAsyncServerTransportDeprecatedTests.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.server; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.server.transport.WebMvcSseServerTransport; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import org.apache.catalina.Context; -import org.apache.catalina.LifecycleException; -import org.apache.catalina.startup.Tomcat; -import org.junit.jupiter.api.Timeout; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.function.RouterFunction; -import org.springframework.web.servlet.function.ServerResponse; - -@Deprecated -@Timeout(15) -class WebMvcSseAsyncServerTransportDeprecatedTests extends AbstractMcpAsyncServerDeprecatedTests { - - private static final String MESSAGE_ENDPOINT = "/mcp/message"; - - private static final int PORT = 8181; - - private Tomcat tomcat; - - private WebMvcSseServerTransport transport; - - @Configuration - @EnableWebMvc - static class TestConfig { - - @Bean - public WebMvcSseServerTransport webMvcSseServerTransport() { - return new WebMvcSseServerTransport(new ObjectMapper(), MESSAGE_ENDPOINT); - } - - @Bean - public RouterFunction routerFunction(WebMvcSseServerTransport transport) { - return transport.getRouterFunction(); - } - - } - - private AnnotationConfigWebApplicationContext appContext; - - @Override - protected ServerMcpTransport createMcpTransport() { - // Set up Tomcat first - tomcat = new Tomcat(); - tomcat.setPort(PORT); - - // Set Tomcat base directory to java.io.tmpdir to avoid permission issues - String baseDir = System.getProperty("java.io.tmpdir"); - tomcat.setBaseDir(baseDir); - - // Use the same directory for document base - Context context = tomcat.addContext("", baseDir); - - // Create and configure Spring WebMvc context - appContext = new AnnotationConfigWebApplicationContext(); - appContext.register(TestConfig.class); - appContext.setServletContext(context.getServletContext()); - appContext.refresh(); - - // Get the transport from Spring context - transport = appContext.getBean(WebMvcSseServerTransport.class); - - // Create DispatcherServlet with our Spring context - DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext); - // dispatcherServlet.setThrowExceptionIfNoHandlerFound(true); - - // Add servlet to Tomcat and get the wrapper - var wrapper = Tomcat.addServlet(context, "dispatcherServlet", dispatcherServlet); - wrapper.setLoadOnStartup(1); - context.addServletMappingDecoded("/*", "dispatcherServlet"); - - try { - tomcat.start(); - tomcat.getConnector(); // Create and start the connector - } - catch (LifecycleException e) { - throw new RuntimeException("Failed to start Tomcat", e); - } - - return transport; - } - - @Override - protected void onStart() { - } - - @Override - protected void onClose() { - if (transport != null) { - transport.closeGracefully().block(); - } - if (appContext != null) { - appContext.close(); - } - if (tomcat != null) { - try { - tomcat.stop(); - tomcat.destroy(); - } - catch (LifecycleException e) { - throw new RuntimeException("Failed to stop Tomcat", e); - } - } - } - -} diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationDeprecatedTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationDeprecatedTests.java deleted file mode 100644 index f2b593d8d..000000000 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseIntegrationDeprecatedTests.java +++ /dev/null @@ -1,508 +0,0 @@ -/* - * Copyright 2024 - 2024 the original author or authors. - */ -package io.modelcontextprotocol.server; - -import java.time.Duration; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Function; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.client.McpClient; -import io.modelcontextprotocol.client.transport.HttpClientSseClientTransport; -import io.modelcontextprotocol.server.transport.WebMvcSseServerTransport; -import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageRequest; -import io.modelcontextprotocol.spec.McpSchema.CreateMessageResult; -import io.modelcontextprotocol.spec.McpSchema.InitializeResult; -import io.modelcontextprotocol.spec.McpSchema.Role; -import io.modelcontextprotocol.spec.McpSchema.Root; -import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; -import io.modelcontextprotocol.spec.McpSchema.Tool; -import org.apache.catalina.Context; -import org.apache.catalina.LifecycleException; -import org.apache.catalina.LifecycleState; -import org.apache.catalina.startup.Tomcat; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import reactor.test.StepVerifier; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.client.RestClient; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.function.RouterFunction; -import org.springframework.web.servlet.function.ServerResponse; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.awaitility.Awaitility.await; - -@Deprecated -public class WebMvcSseIntegrationDeprecatedTests { - - private static final int PORT = 8183; - - private static final String MESSAGE_ENDPOINT = "/mcp/message"; - - private WebMvcSseServerTransport mcpServerTransport; - - McpClient.SyncSpec clientBuilder; - - @Configuration - @EnableWebMvc - static class TestConfig { - - @Bean - public WebMvcSseServerTransport webMvcSseServerTransport() { - return new WebMvcSseServerTransport(new ObjectMapper(), MESSAGE_ENDPOINT); - } - - @Bean - public RouterFunction routerFunction(WebMvcSseServerTransport transport) { - return transport.getRouterFunction(); - } - - } - - private Tomcat tomcat; - - private AnnotationConfigWebApplicationContext appContext; - - @BeforeEach - public void before() { - - // Set up Tomcat first - tomcat = new Tomcat(); - tomcat.setPort(PORT); - - // Set Tomcat base directory to java.io.tmpdir to avoid permission issues - String baseDir = System.getProperty("java.io.tmpdir"); - tomcat.setBaseDir(baseDir); - - // Use the same directory for document base - Context context = tomcat.addContext("", baseDir); - - // Create and configure Spring WebMvc context - appContext = new AnnotationConfigWebApplicationContext(); - appContext.register(TestConfig.class); - appContext.setServletContext(context.getServletContext()); - appContext.refresh(); - - // Get the transport from Spring context - mcpServerTransport = appContext.getBean(WebMvcSseServerTransport.class); - - // Create DispatcherServlet with our Spring context - DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext); - // dispatcherServlet.setThrowExceptionIfNoHandlerFound(true); - - // Add servlet to Tomcat and get the wrapper - var wrapper = Tomcat.addServlet(context, "dispatcherServlet", dispatcherServlet); - wrapper.setLoadOnStartup(1); - wrapper.setAsyncSupported(true); - context.addServletMappingDecoded("/*", "dispatcherServlet"); - - try { - // Configure and start the connector with async support - var connector = tomcat.getConnector(); - connector.setAsyncTimeout(3000); // 3 seconds timeout for async requests - tomcat.start(); - assertThat(tomcat.getServer().getState() == LifecycleState.STARTED); - } - catch (Exception e) { - throw new RuntimeException("Failed to start Tomcat", e); - } - - this.clientBuilder = McpClient.sync(new HttpClientSseClientTransport("http://localhost:" + PORT)); - } - - @AfterEach - public void after() { - if (mcpServerTransport != null) { - mcpServerTransport.closeGracefully().block(); - } - if (appContext != null) { - appContext.close(); - } - if (tomcat != null) { - try { - tomcat.stop(); - tomcat.destroy(); - } - catch (LifecycleException e) { - throw new RuntimeException("Failed to stop Tomcat", e); - } - } - } - - // --------------------------------------- - // Sampling Tests - // --------------------------------------- - @Test - void testCreateMessageWithoutInitialization() { - var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build(); - - var messages = List - .of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Test message"))); - var modelPrefs = new McpSchema.ModelPreferences(List.of(), 1.0, 1.0, 1.0); - - var request = new McpSchema.CreateMessageRequest(messages, modelPrefs, null, - McpSchema.CreateMessageRequest.ContextInclusionStrategy.NONE, null, 100, List.of(), Map.of()); - - StepVerifier.create(mcpAsyncServer.createMessage(request)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Client must be initialized. Call the initialize method first!"); - }); - } - - @Test - void testCreateMessageWithoutSamplingCapabilities() { - - var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build(); - - var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample " + "client", "0.0.0")).build(); - - InitializeResult initResult = client.initialize(); - assertThat(initResult).isNotNull(); - - var messages = List - .of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Test message"))); - var modelPrefs = new McpSchema.ModelPreferences(List.of(), 1.0, 1.0, 1.0); - - var request = new McpSchema.CreateMessageRequest(messages, modelPrefs, null, - McpSchema.CreateMessageRequest.ContextInclusionStrategy.NONE, null, 100, List.of(), Map.of()); - - StepVerifier.create(mcpAsyncServer.createMessage(request)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Client must be configured with sampling capabilities"); - }); - } - - @Test - void testCreateMessageSuccess() throws InterruptedException { - - var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build(); - - Function samplingHandler = request -> { - assertThat(request.messages()).hasSize(1); - assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class); - - return new CreateMessageResult(Role.USER, new McpSchema.TextContent("Test message"), "MockModelName", - CreateMessageResult.StopReason.STOP_SEQUENCE); - }; - - var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) - .capabilities(ClientCapabilities.builder().sampling().build()) - .sampling(samplingHandler) - .build(); - - InitializeResult initResult = client.initialize(); - assertThat(initResult).isNotNull(); - - var messages = List - .of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Test message"))); - var modelPrefs = new McpSchema.ModelPreferences(List.of(), 1.0, 1.0, 1.0); - - var request = new McpSchema.CreateMessageRequest(messages, modelPrefs, null, - McpSchema.CreateMessageRequest.ContextInclusionStrategy.NONE, null, 100, List.of(), Map.of()); - - StepVerifier.create(mcpAsyncServer.createMessage(request)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.role()).isEqualTo(Role.USER); - assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); - assertThat(result.model()).isEqualTo("MockModelName"); - assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); - }).verifyComplete(); - } - - // --------------------------------------- - // Roots Tests - // --------------------------------------- - @Test - void testRootsSuccess() { - List roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2")); - - AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransport) - .rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate)) - .build(); - - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - assertThat(rootsRef.get()).isNull(); - - assertThat(mcpServer.listRoots().roots()).containsAll(roots); - - mcpClient.rootsListChangedNotification(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(roots); - }); - - // Remove a root - mcpClient.removeRoot(roots.get(0).uri()); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(roots.get(1))); - }); - - // Add a new root - var root3 = new Root("uri3://", "root3"); - mcpClient.addRoot(root3); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(roots.get(1), root3)); - }); - - mcpClient.close(); - mcpServer.close(); - } - - @Test - void testRootsWithoutCapability() { - var mcpServer = McpServer.sync(mcpServerTransport).rootsChangeConsumer(rootsUpdate -> { - }).build(); - - // Create client without roots capability - // No roots capability - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().build()).build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - // Attempt to list roots should fail - assertThatThrownBy(() -> mcpServer.listRoots().roots()).isInstanceOf(McpError.class) - .hasMessage("Roots not supported"); - - mcpClient.close(); - mcpServer.close(); - } - - @Test - void testRootsWithEmptyRootsList() { - AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransport) - .rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate)) - .build(); - - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(List.of()) // Empty roots list - .build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - mcpClient.rootsListChangedNotification(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).isEmpty(); - }); - - mcpClient.close(); - mcpServer.close(); - } - - @Test - void testRootsWithMultipleConsumers() { - List roots = List.of(new Root("uri1://", "root1")); - - AtomicReference> rootsRef1 = new AtomicReference<>(); - AtomicReference> rootsRef2 = new AtomicReference<>(); - - var mcpServer = McpServer.sync(mcpServerTransport) - .rootsChangeConsumer(rootsUpdate -> rootsRef1.set(rootsUpdate)) - .rootsChangeConsumer(rootsUpdate -> rootsRef2.set(rootsUpdate)) - .build(); - - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - mcpClient.rootsListChangedNotification(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef1.get()).containsAll(roots); - assertThat(rootsRef2.get()).containsAll(roots); - }); - - mcpClient.close(); - mcpServer.close(); - } - - @Test - void testRootsServerCloseWithActiveSubscription() { - List roots = List.of(new Root("uri1://", "root1")); - - AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransport) - .rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate)) - .build(); - - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - mcpClient.rootsListChangedNotification(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(roots); - }); - - // Close server while subscription is active - mcpServer.close(); - - // Verify client can handle server closure gracefully - mcpClient.close(); - } - - // --------------------------------------- - // Tools Tests - // --------------------------------------- - - String emptyJsonSchema = """ - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": {} - } - """; - - @Test - void testToolCallSuccess() { - - var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.SyncToolRegistration tool1 = new McpServerFeatures.SyncToolRegistration( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), request -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - return callResponse; - }); - - var mcpServer = McpServer.sync(mcpServerTransport) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); - - var mcpClient = clientBuilder.build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); - - CallToolResult response = mcpClient.callTool(new McpSchema.CallToolRequest("tool1", Map.of())); - - assertThat(response).isNotNull(); - assertThat(response).isEqualTo(callResponse); - - mcpClient.close(); - mcpServer.close(); - } - - @Test - void testToolListChangeHandlingSuccess() { - - var callResponse = new McpSchema.CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null); - McpServerFeatures.SyncToolRegistration tool1 = new McpServerFeatures.SyncToolRegistration( - new McpSchema.Tool("tool1", "tool1 description", emptyJsonSchema), request -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - return callResponse; - }); - - var mcpServer = McpServer.sync(mcpServerTransport) - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tools(tool1) - .build(); - - AtomicReference> rootsRef = new AtomicReference<>(); - var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { - // perform a blocking call to a remote service - String response = RestClient.create() - .get() - .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - rootsRef.set(toolsUpdate); - }).build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - assertThat(rootsRef.get()).isNull(); - - assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); - - mcpServer.notifyToolsListChanged(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(tool1.tool())); - }); - - // Remove a tool - mcpServer.removeTool("tool1"); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).isEmpty(); - }); - - // Add a new tool - McpServerFeatures.SyncToolRegistration tool2 = new McpServerFeatures.SyncToolRegistration( - new McpSchema.Tool("tool2", "tool2 description", emptyJsonSchema), request -> callResponse); - - mcpServer.addTool(tool2); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(List.of(tool2.tool())); - }); - - mcpClient.close(); - mcpServer.close(); - } - - @Test - void testInitialize() { - - var mcpServer = McpServer.sync(mcpServerTransport).build(); - - var mcpClient = clientBuilder.build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - mcpClient.close(); - mcpServer.close(); - } - -} diff --git a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportDeprecatedTests.java b/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportDeprecatedTests.java deleted file mode 100644 index 8656665ed..000000000 --- a/mcp-spring/mcp-spring-webmvc/src/test/java/io/modelcontextprotocol/server/WebMvcSseSyncServerTransportDeprecatedTests.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.server; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.server.transport.WebMvcSseServerTransport; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import org.apache.catalina.Context; -import org.apache.catalina.LifecycleException; -import org.apache.catalina.startup.Tomcat; -import org.junit.jupiter.api.Timeout; - -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.web.context.support.AnnotationConfigWebApplicationContext; -import org.springframework.web.servlet.DispatcherServlet; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; -import org.springframework.web.servlet.function.RouterFunction; -import org.springframework.web.servlet.function.ServerResponse; - -@Deprecated -@Timeout(15) -class WebMvcSseSyncServerTransportDeprecatedTests extends AbstractMcpSyncServerDeprecatedTests { - - private static final String MESSAGE_ENDPOINT = "/mcp/message"; - - private static final int PORT = 8181; - - private Tomcat tomcat; - - private WebMvcSseServerTransport transport; - - @Configuration - @EnableWebMvc - static class TestConfig { - - @Bean - public WebMvcSseServerTransport webMvcSseServerTransport() { - return new WebMvcSseServerTransport(new ObjectMapper(), MESSAGE_ENDPOINT); - } - - @Bean - public RouterFunction routerFunction(WebMvcSseServerTransport transport) { - return transport.getRouterFunction(); - } - - } - - private AnnotationConfigWebApplicationContext appContext; - - @Override - protected ServerMcpTransport createMcpTransport() { - // Set up Tomcat first - tomcat = new Tomcat(); - tomcat.setPort(PORT); - - // Set Tomcat base directory to java.io.tmpdir to avoid permission issues - String baseDir = System.getProperty("java.io.tmpdir"); - tomcat.setBaseDir(baseDir); - - // Use the same directory for document base - Context context = tomcat.addContext("", baseDir); - - // Create and configure Spring WebMvc context - appContext = new AnnotationConfigWebApplicationContext(); - appContext.register(TestConfig.class); - appContext.setServletContext(context.getServletContext()); - appContext.refresh(); - - // Get the transport from Spring context - transport = appContext.getBean(WebMvcSseServerTransport.class); - - // Create DispatcherServlet with our Spring context - DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext); - // dispatcherServlet.setThrowExceptionIfNoHandlerFound(true); - - // Add servlet to Tomcat and get the wrapper - var wrapper = Tomcat.addServlet(context, "dispatcherServlet", dispatcherServlet); - wrapper.setLoadOnStartup(1); - context.addServletMappingDecoded("/*", "dispatcherServlet"); - - try { - tomcat.start(); - tomcat.getConnector(); // Create and start the connector - } - catch (LifecycleException e) { - throw new RuntimeException("Failed to start Tomcat", e); - } - - return transport; - } - - @Override - protected void onStart() { - } - - @Override - protected void onClose() { - if (transport != null) { - transport.closeGracefully().block(); - } - if (appContext != null) { - appContext.close(); - } - if (tomcat != null) { - try { - tomcat.stop(); - tomcat.destroy(); - } - catch (LifecycleException e) { - throw new RuntimeException("Failed to stop Tomcat", e); - } - } - } - -} diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java b/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java index cef3fb9fa..5484a63c2 100644 --- a/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java +++ b/mcp-test/src/main/java/io/modelcontextprotocol/MockMcpTransport.java @@ -15,15 +15,18 @@ import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification; import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; -import io.modelcontextprotocol.spec.ServerMcpTransport; +import io.modelcontextprotocol.spec.McpServerTransport; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; /** - * A mock implementation of the {@link McpClientTransport} and {@link ServerMcpTransport} + * A mock implementation of the {@link McpClientTransport} and {@link McpServerTransport} * interfaces. + * + * @deprecated not used. to be removed in the future. */ -public class MockMcpTransport implements McpClientTransport, ServerMcpTransport { +@Deprecated +public class MockMcpTransport implements McpClientTransport, McpServerTransport { private final Sinks.Many inbound = Sinks.many().unicast().onBackpressureBuffer(); diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerDeprecatedTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerDeprecatedTests.java deleted file mode 100644 index 005d78f25..000000000 --- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerDeprecatedTests.java +++ /dev/null @@ -1,465 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.server; - -import java.time.Duration; -import java.util.List; - -import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; -import io.modelcontextprotocol.spec.McpSchema.Prompt; -import io.modelcontextprotocol.spec.McpSchema.PromptMessage; -import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; -import io.modelcontextprotocol.spec.McpSchema.Resource; -import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; -import io.modelcontextprotocol.spec.McpSchema.Tool; -import io.modelcontextprotocol.spec.McpTransport; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * Test suite for the {@link McpAsyncServer} that can be used with different - * {@link McpTransport} implementations. - * - * @author Christian Tzolov - */ -@Deprecated -public abstract class AbstractMcpAsyncServerDeprecatedTests { - - private static final String TEST_TOOL_NAME = "test-tool"; - - private static final String TEST_RESOURCE_URI = "test://resource"; - - private static final String TEST_PROMPT_NAME = "test-prompt"; - - abstract protected ServerMcpTransport createMcpTransport(); - - protected void onStart() { - } - - protected void onClose() { - } - - @BeforeEach - void setUp() { - } - - @AfterEach - void tearDown() { - onClose(); - } - - // --------------------------------------- - // Server Lifecycle Tests - // --------------------------------------- - - @Test - void testConstructorWithInvalidArguments() { - assertThatThrownBy(() -> McpServer.async((ServerMcpTransport) null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Transport must not be null"); - - assertThatThrownBy(() -> McpServer.async(createMcpTransport()).serverInfo((McpSchema.Implementation) null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Server info must not be null"); - } - - @Test - void testGracefulShutdown() { - var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - StepVerifier.create(mcpAsyncServer.closeGracefully()).verifyComplete(); - } - - @Test - void testImmediateClose() { - var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatCode(() -> mcpAsyncServer.close()).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Tools Tests - // --------------------------------------- - String emptyJsonSchema = """ - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": {} - } - """; - - @Test - void testAddTool() { - Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .build(); - - StepVerifier.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolRegistration(newTool, - args -> Mono.just(new CallToolResult(List.of(), false))))) - .verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddDuplicateTool() { - Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); - - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(duplicateTool, args -> Mono.just(new CallToolResult(List.of(), false))) - .build(); - - StepVerifier.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolRegistration(duplicateTool, - args -> Mono.just(new CallToolResult(List.of(), false))))) - .verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists"); - }); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testRemoveTool() { - Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); - - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(too, args -> Mono.just(new CallToolResult(List.of(), false))) - .build(); - - StepVerifier.create(mcpAsyncServer.removeTool(TEST_TOOL_NAME)).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testRemoveNonexistentTool() { - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .build(); - - StepVerifier.create(mcpAsyncServer.removeTool("nonexistent-tool")).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class).hasMessage("Tool with name 'nonexistent-tool' not found"); - }); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testNotifyToolsListChanged() { - Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); - - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(too, args -> Mono.just(new CallToolResult(List.of(), false))) - .build(); - - StepVerifier.create(mcpAsyncServer.notifyToolsListChanged()).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Resources Tests - // --------------------------------------- - - @Test - void testNotifyResourcesListChanged() { - var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - StepVerifier.create(mcpAsyncServer.notifyResourcesListChanged()).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddResource() { - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description", - null); - McpServerFeatures.AsyncResourceRegistration registration = new McpServerFeatures.AsyncResourceRegistration( - resource, req -> Mono.just(new ReadResourceResult(List.of()))); - - StepVerifier.create(mcpAsyncServer.addResource(registration)).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddResourceWithNullRegistration() { - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - StepVerifier.create(mcpAsyncServer.addResource((McpServerFeatures.AsyncResourceRegistration) null)) - .verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class).hasMessage("Resource must not be null"); - }); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddResourceWithoutCapability() { - // Create a server without resource capabilities - McpAsyncServer serverWithoutResources = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .build(); - - Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description", - null); - McpServerFeatures.AsyncResourceRegistration registration = new McpServerFeatures.AsyncResourceRegistration( - resource, req -> Mono.just(new ReadResourceResult(List.of()))); - - StepVerifier.create(serverWithoutResources.addResource(registration)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with resource capabilities"); - }); - } - - @Test - void testRemoveResourceWithoutCapability() { - // Create a server without resource capabilities - McpAsyncServer serverWithoutResources = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .build(); - - StepVerifier.create(serverWithoutResources.removeResource(TEST_RESOURCE_URI)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with resource capabilities"); - }); - } - - // --------------------------------------- - // Prompts Tests - // --------------------------------------- - - @Test - void testNotifyPromptsListChanged() { - var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - StepVerifier.create(mcpAsyncServer.notifyPromptsListChanged()).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddPromptWithNullRegistration() { - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(false).build()) - .build(); - - StepVerifier.create(mcpAsyncServer.addPrompt((McpServerFeatures.AsyncPromptRegistration) null)) - .verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class).hasMessage("Prompt registration must not be null"); - }); - } - - @Test - void testAddPromptWithoutCapability() { - // Create a server without prompt capabilities - McpAsyncServer serverWithoutPrompts = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .build(); - - Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of()); - McpServerFeatures.AsyncPromptRegistration registration = new McpServerFeatures.AsyncPromptRegistration(prompt, - req -> Mono.just(new GetPromptResult("Test prompt description", List - .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))))); - - StepVerifier.create(serverWithoutPrompts.addPrompt(registration)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with prompt capabilities"); - }); - } - - @Test - void testRemovePromptWithoutCapability() { - // Create a server without prompt capabilities - McpAsyncServer serverWithoutPrompts = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .build(); - - StepVerifier.create(serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with prompt capabilities"); - }); - } - - @Test - void testRemovePrompt() { - String TEST_PROMPT_NAME_TO_REMOVE = "TEST_PROMPT_NAME678"; - - Prompt prompt = new Prompt(TEST_PROMPT_NAME_TO_REMOVE, "Test Prompt", List.of()); - McpServerFeatures.AsyncPromptRegistration registration = new McpServerFeatures.AsyncPromptRegistration(prompt, - req -> Mono.just(new GetPromptResult("Test prompt description", List - .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))))); - - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(true).build()) - .prompts(registration) - .build(); - - StepVerifier.create(mcpAsyncServer.removePrompt(TEST_PROMPT_NAME_TO_REMOVE)).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testRemoveNonexistentPrompt() { - var mcpAsyncServer2 = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(true).build()) - .build(); - - StepVerifier.create(mcpAsyncServer2.removePrompt("nonexistent-prompt")).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Prompt with name 'nonexistent-prompt' not found"); - }); - - assertThatCode(() -> mcpAsyncServer2.closeGracefully().block(Duration.ofSeconds(10))) - .doesNotThrowAnyException(); - } - - // --------------------------------------- - // Roots Tests - // --------------------------------------- - - @Test - void testRootsChangeConsumers() { - // Test with single consumer - var rootsReceived = new McpSchema.Root[1]; - var consumerCalled = new boolean[1]; - - var singleConsumerServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .rootsChangeConsumers(List.of(roots -> Mono.fromRunnable(() -> { - consumerCalled[0] = true; - if (!roots.isEmpty()) { - rootsReceived[0] = roots.get(0); - } - }))) - .build(); - - assertThat(singleConsumerServer).isNotNull(); - assertThatCode(() -> singleConsumerServer.closeGracefully().block(Duration.ofSeconds(10))) - .doesNotThrowAnyException(); - onClose(); - - // Test with multiple consumers - var consumer1Called = new boolean[1]; - var consumer2Called = new boolean[1]; - var rootsContent = new List[1]; - - var multipleConsumersServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .rootsChangeConsumers(List.of(roots -> Mono.fromRunnable(() -> { - consumer1Called[0] = true; - rootsContent[0] = roots; - }), roots -> Mono.fromRunnable(() -> consumer2Called[0] = true))) - .build(); - - assertThat(multipleConsumersServer).isNotNull(); - assertThatCode(() -> multipleConsumersServer.closeGracefully().block(Duration.ofSeconds(10))) - .doesNotThrowAnyException(); - onClose(); - - // Test error handling - var errorHandlingServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .rootsChangeConsumers(List.of(roots -> { - throw new RuntimeException("Test error"); - })) - .build(); - - assertThat(errorHandlingServer).isNotNull(); - assertThatCode(() -> errorHandlingServer.closeGracefully().block(Duration.ofSeconds(10))) - .doesNotThrowAnyException(); - onClose(); - - // Test without consumers - var noConsumersServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThat(noConsumersServer).isNotNull(); - assertThatCode(() -> noConsumersServer.closeGracefully().block(Duration.ofSeconds(10))) - .doesNotThrowAnyException(); - } - - // --------------------------------------- - // Logging Tests - // --------------------------------------- - - @Test - void testLoggingLevels() { - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - // Test all logging levels - for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { - var notification = McpSchema.LoggingMessageNotification.builder() - .level(level) - .logger("test-logger") - .data("Test message with level " + level) - .build(); - - StepVerifier.create(mcpAsyncServer.loggingNotification(notification)).verifyComplete(); - } - } - - @Test - void testLoggingWithoutCapability() { - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().build()) // No logging capability - .build(); - - var notification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.INFO) - .logger("test-logger") - .data("Test log message") - .build(); - - StepVerifier.create(mcpAsyncServer.loggingNotification(notification)).verifyComplete(); - } - - @Test - void testLoggingWithNullNotification() { - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - StepVerifier.create(mcpAsyncServer.loggingNotification(null)).verifyError(McpError.class); - } - -} diff --git a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerDeprecatedTests.java b/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerDeprecatedTests.java deleted file mode 100644 index c6625acaa..000000000 --- a/mcp-test/src/main/java/io/modelcontextprotocol/server/AbstractMcpSyncServerDeprecatedTests.java +++ /dev/null @@ -1,431 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.server; - -import java.util.List; - -import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.GetPromptResult; -import io.modelcontextprotocol.spec.McpSchema.Prompt; -import io.modelcontextprotocol.spec.McpSchema.PromptMessage; -import io.modelcontextprotocol.spec.McpSchema.ReadResourceResult; -import io.modelcontextprotocol.spec.McpSchema.Resource; -import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; -import io.modelcontextprotocol.spec.McpSchema.Tool; -import io.modelcontextprotocol.spec.McpTransport; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatCode; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * Test suite for the {@link McpSyncServer} that can be used with different - * {@link McpTransport} implementations. - * - * @author Christian Tzolov - */ -public abstract class AbstractMcpSyncServerDeprecatedTests { - - private static final String TEST_TOOL_NAME = "test-tool"; - - private static final String TEST_RESOURCE_URI = "test://resource"; - - private static final String TEST_PROMPT_NAME = "test-prompt"; - - abstract protected ServerMcpTransport createMcpTransport(); - - protected void onStart() { - } - - protected void onClose() { - } - - @BeforeEach - void setUp() { - // onStart(); - } - - @AfterEach - void tearDown() { - onClose(); - } - - // --------------------------------------- - // Server Lifecycle Tests - // --------------------------------------- - - @Test - void testConstructorWithInvalidArguments() { - assertThatThrownBy(() -> McpServer.sync((ServerMcpTransport) null)).isInstanceOf(IllegalArgumentException.class) - .hasMessage("Transport must not be null"); - - assertThatThrownBy(() -> McpServer.sync(createMcpTransport()).serverInfo(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Server info must not be null"); - } - - @Test - void testGracefulShutdown() { - var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testImmediateClose() { - var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatCode(() -> mcpSyncServer.close()).doesNotThrowAnyException(); - } - - @Test - void testGetAsyncServer() { - var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThat(mcpSyncServer.getAsyncServer()).isNotNull(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Tools Tests - // --------------------------------------- - - String emptyJsonSchema = """ - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": {} - } - """; - - @Test - void testAddTool() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .build(); - - Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); - assertThatCode(() -> mcpSyncServer - .addTool(new McpServerFeatures.SyncToolRegistration(newTool, args -> new CallToolResult(List.of(), false)))) - .doesNotThrowAnyException(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testAddDuplicateTool() { - Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); - - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(duplicateTool, args -> new CallToolResult(List.of(), false)) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolRegistration(duplicateTool, - args -> new CallToolResult(List.of(), false)))) - .isInstanceOf(McpError.class) - .hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists"); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testRemoveTool() { - Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", emptyJsonSchema); - - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(tool, args -> new CallToolResult(List.of(), false)) - .build(); - - assertThatCode(() -> mcpSyncServer.removeTool(TEST_TOOL_NAME)).doesNotThrowAnyException(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testRemoveNonexistentTool() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.removeTool("nonexistent-tool")).isInstanceOf(McpError.class) - .hasMessage("Tool with name 'nonexistent-tool' not found"); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testNotifyToolsListChanged() { - var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatCode(() -> mcpSyncServer.notifyToolsListChanged()).doesNotThrowAnyException(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Resources Tests - // --------------------------------------- - - @Test - void testNotifyResourcesListChanged() { - var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatCode(() -> mcpSyncServer.notifyResourcesListChanged()).doesNotThrowAnyException(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testAddResource() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description", - null); - McpServerFeatures.SyncResourceRegistration registration = new McpServerFeatures.SyncResourceRegistration( - resource, req -> new ReadResourceResult(List.of())); - - assertThatCode(() -> mcpSyncServer.addResource(registration)).doesNotThrowAnyException(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testAddResourceWithNullRegistration() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.addResource((McpServerFeatures.SyncResourceRegistration) null)) - .isInstanceOf(McpError.class) - .hasMessage("Resource must not be null"); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testAddResourceWithoutCapability() { - var serverWithoutResources = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description", - null); - McpServerFeatures.SyncResourceRegistration registration = new McpServerFeatures.SyncResourceRegistration( - resource, req -> new ReadResourceResult(List.of())); - - assertThatThrownBy(() -> serverWithoutResources.addResource(registration)).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with resource capabilities"); - } - - @Test - void testRemoveResourceWithoutCapability() { - var serverWithoutResources = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatThrownBy(() -> serverWithoutResources.removeResource(TEST_RESOURCE_URI)).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with resource capabilities"); - } - - // --------------------------------------- - // Prompts Tests - // --------------------------------------- - - @Test - void testNotifyPromptsListChanged() { - var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatCode(() -> mcpSyncServer.notifyPromptsListChanged()).doesNotThrowAnyException(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testAddPromptWithNullRegistration() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(false).build()) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.addPrompt((McpServerFeatures.SyncPromptRegistration) null)) - .isInstanceOf(McpError.class) - .hasMessage("Prompt registration must not be null"); - } - - @Test - void testAddPromptWithoutCapability() { - var serverWithoutPrompts = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of()); - McpServerFeatures.SyncPromptRegistration registration = new McpServerFeatures.SyncPromptRegistration(prompt, - req -> new GetPromptResult("Test prompt description", List - .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))); - - assertThatThrownBy(() -> serverWithoutPrompts.addPrompt(registration)).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with prompt capabilities"); - } - - @Test - void testRemovePromptWithoutCapability() { - var serverWithoutPrompts = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatThrownBy(() -> serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with prompt capabilities"); - } - - @Test - void testRemovePrompt() { - Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of()); - McpServerFeatures.SyncPromptRegistration registration = new McpServerFeatures.SyncPromptRegistration(prompt, - req -> new GetPromptResult("Test prompt description", List - .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))); - - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(true).build()) - .prompts(registration) - .build(); - - assertThatCode(() -> mcpSyncServer.removePrompt(TEST_PROMPT_NAME)).doesNotThrowAnyException(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testRemoveNonexistentPrompt() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(true).build()) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.removePrompt("nonexistent-prompt")).isInstanceOf(McpError.class) - .hasMessage("Prompt with name 'nonexistent-prompt' not found"); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Roots Tests - // --------------------------------------- - - @Test - void testRootsChangeConsumers() { - // Test with single consumer - var rootsReceived = new McpSchema.Root[1]; - var consumerCalled = new boolean[1]; - - var singleConsumerServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .rootsChangeConsumers(List.of(roots -> { - consumerCalled[0] = true; - if (!roots.isEmpty()) { - rootsReceived[0] = roots.get(0); - } - })) - .build(); - - assertThat(singleConsumerServer).isNotNull(); - assertThatCode(() -> singleConsumerServer.closeGracefully()).doesNotThrowAnyException(); - onClose(); - - // Test with multiple consumers - var consumer1Called = new boolean[1]; - var consumer2Called = new boolean[1]; - var rootsContent = new List[1]; - - var multipleConsumersServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .rootsChangeConsumers(List.of(roots -> { - consumer1Called[0] = true; - rootsContent[0] = roots; - }, roots -> consumer2Called[0] = true)) - .build(); - - assertThat(multipleConsumersServer).isNotNull(); - assertThatCode(() -> multipleConsumersServer.closeGracefully()).doesNotThrowAnyException(); - onClose(); - - // Test error handling - var errorHandlingServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .rootsChangeConsumers(List.of(roots -> { - throw new RuntimeException("Test error"); - })) - .build(); - - assertThat(errorHandlingServer).isNotNull(); - assertThatCode(() -> errorHandlingServer.closeGracefully()).doesNotThrowAnyException(); - onClose(); - - // Test without consumers - var noConsumersServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThat(noConsumersServer).isNotNull(); - assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Logging Tests - // --------------------------------------- - - @Test - void testLoggingLevels() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - // Test all logging levels - for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { - var notification = McpSchema.LoggingMessageNotification.builder() - .level(level) - .logger("test-logger") - .data("Test message with level " + level) - .build(); - - assertThatCode(() -> mcpSyncServer.loggingNotification(notification)).doesNotThrowAnyException(); - } - } - - @Test - void testLoggingWithoutCapability() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().build()) // No logging capability - .build(); - - var notification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.INFO) - .logger("test-logger") - .data("Test log message") - .build(); - - assertThatCode(() -> mcpSyncServer.loggingNotification(notification)).doesNotThrowAnyException(); - } - - @Test - void testLoggingWithNullNotification() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.loggingNotification(null)).isInstanceOf(McpError.class); - } - -} diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java index 9cbef0500..379b47e23 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpAsyncClient.java @@ -14,10 +14,10 @@ import java.util.function.Function; import com.fasterxml.jackson.core.type.TypeReference; -import io.modelcontextprotocol.spec.ClientMcpTransport; import io.modelcontextprotocol.spec.McpClientSession; import io.modelcontextprotocol.spec.McpClientSession.NotificationHandler; import io.modelcontextprotocol.spec.McpClientSession.RequestHandler; +import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; @@ -153,7 +153,7 @@ public class McpAsyncClient { * @param initializationTimeout the max timeout to await for the client-server * @param features the MCP Client supported features. */ - McpAsyncClient(ClientMcpTransport transport, Duration requestTimeout, Duration initializationTimeout, + McpAsyncClient(McpClientTransport transport, Duration requestTimeout, Duration initializationTimeout, McpClientFeatures.Async features) { Assert.notNull(transport, "Transport must not be null"); diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java index 9c5f7b015..f7b179616 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpClient.java @@ -12,7 +12,6 @@ import java.util.function.Consumer; import java.util.function.Function; -import io.modelcontextprotocol.spec.ClientMcpTransport; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpTransport; @@ -102,26 +101,6 @@ */ public interface McpClient { - /** - * Start building a synchronous MCP client with the specified transport layer. The - * synchronous MCP client provides blocking operations. Synchronous clients wait for - * each operation to complete before returning, making them simpler to use but - * potentially less performant for concurrent operations. The transport layer handles - * the low-level communication between client and server using protocols like stdio or - * Server-Sent Events (SSE). - * @param transport The transport layer implementation for MCP communication. Common - * implementations include {@code StdioClientTransport} for stdio-based communication - * and {@code SseClientTransport} for SSE-based communication. - * @return A new builder instance for configuring the client - * @throws IllegalArgumentException if transport is null - * @deprecated This method will be removed in 0.9.0. Use - * {@link #sync(McpClientTransport)} - */ - @Deprecated - static SyncSpec sync(ClientMcpTransport transport) { - return new SyncSpec(transport); - } - /** * Start building a synchronous MCP client with the specified transport layer. The * synchronous MCP client provides blocking operations. Synchronous clients wait for @@ -139,26 +118,6 @@ static SyncSpec sync(McpClientTransport transport) { return new SyncSpec(transport); } - /** - * Start building an asynchronous MCP client with the specified transport layer. The - * asynchronous MCP client provides non-blocking operations. Asynchronous clients - * return reactive primitives (Mono/Flux) immediately, allowing for concurrent - * operations and reactive programming patterns. The transport layer handles the - * low-level communication between client and server using protocols like stdio or - * Server-Sent Events (SSE). - * @param transport The transport layer implementation for MCP communication. Common - * implementations include {@code StdioClientTransport} for stdio-based communication - * and {@code SseClientTransport} for SSE-based communication. - * @return A new builder instance for configuring the client - * @throws IllegalArgumentException if transport is null - * @deprecated This method will be removed in 0.9.0. Use - * {@link #async(McpClientTransport)} - */ - @Deprecated - static AsyncSpec async(ClientMcpTransport transport) { - return new AsyncSpec(transport); - } - /** * Start building an asynchronous MCP client with the specified transport layer. The * asynchronous MCP client provides non-blocking operations. Asynchronous clients @@ -194,7 +153,7 @@ static AsyncSpec async(McpClientTransport transport) { */ class SyncSpec { - private final ClientMcpTransport transport; + private final McpClientTransport transport; private Duration requestTimeout = Duration.ofSeconds(20); // Default timeout @@ -216,7 +175,7 @@ class SyncSpec { private Function samplingHandler; - private SyncSpec(ClientMcpTransport transport) { + private SyncSpec(McpClientTransport transport) { Assert.notNull(transport, "Transport must not be null"); this.transport = transport; } @@ -433,7 +392,7 @@ public McpSyncClient build() { */ class AsyncSpec { - private final ClientMcpTransport transport; + private final McpClientTransport transport; private Duration requestTimeout = Duration.ofSeconds(20); // Default timeout @@ -455,7 +414,7 @@ class AsyncSpec { private Function> samplingHandler; - private AsyncSpec(ClientMcpTransport transport) { + private AsyncSpec(McpClientTransport transport) { Assert.notNull(transport, "Transport must not be null"); this.transport = transport; } diff --git a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java index ec0a0dfdb..071d76462 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java +++ b/mcp/src/main/java/io/modelcontextprotocol/client/McpSyncClient.java @@ -66,12 +66,8 @@ public class McpSyncClient implements AutoCloseable { * Create a new McpSyncClient with the given delegate. * @param delegate the asynchronous kernel on top of which this synchronous client * provides a blocking API. - * @deprecated This method will be removed in 0.9.0. Use - * {@link McpClient#sync(McpClientTransport)} to obtain an instance. */ - @Deprecated - // TODO make the constructor package private post-deprecation - public McpSyncClient(McpAsyncClient delegate) { + McpSyncClient(McpAsyncClient delegate) { Assert.notNull(delegate, "The delegate can not be null"); this.delegate = delegate; } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index ef69539ad..188b0f48e 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -4,9 +4,7 @@ package io.modelcontextprotocol.server; -import java.time.Duration; import java.util.HashMap; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -14,21 +12,18 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.BiFunction; -import java.util.function.Function; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.spec.McpClientSession; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpServerTransportProvider; -import io.modelcontextprotocol.spec.McpServerSession; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; -import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; import io.modelcontextprotocol.spec.McpSchema.LoggingLevel; import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification; import io.modelcontextprotocol.spec.McpSchema.Tool; -import io.modelcontextprotocol.spec.ServerMcpTransport; +import io.modelcontextprotocol.spec.McpServerSession; +import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.util.Utils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -86,19 +81,6 @@ public class McpAsyncServer { this.delegate = null; } - /** - * Create a new McpAsyncServer with the given transport and capabilities. - * @param mcpTransport The transport layer implementation for MCP communication. - * @param features The MCP server supported features. - * @deprecated This constructor will beremoved in 0.9.0. Use - * {@link #McpAsyncServer(McpServerTransportProvider, ObjectMapper, McpServerFeatures.Async)} - * instead. - */ - @Deprecated - McpAsyncServer(ServerMcpTransport mcpTransport, McpServerFeatures.Async features) { - this.delegate = new LegacyAsyncServer(mcpTransport, features); - } - /** * Create a new McpAsyncServer with the given transport provider and capabilities. * @param mcpTransportProvider The transport layer implementation for MCP @@ -127,28 +109,6 @@ public McpSchema.Implementation getServerInfo() { return this.delegate.getServerInfo(); } - /** - * Get the client capabilities that define the supported features and functionality. - * @return The client capabilities - * @deprecated This will be removed in 0.9.0. Use - * {@link McpAsyncServerExchange#getClientCapabilities()}. - */ - @Deprecated - public ClientCapabilities getClientCapabilities() { - return this.delegate.getClientCapabilities(); - } - - /** - * Get the client implementation information. - * @return The client implementation details - * @deprecated This will be removed in 0.9.0. Use - * {@link McpAsyncServerExchange#getClientInfo()}. - */ - @Deprecated - public McpSchema.Implementation getClientInfo() { - return this.delegate.getClientInfo(); - } - /** * Gracefully closes the server, allowing any in-progress operations to complete. * @return A Mono that completes when the server has been closed @@ -164,45 +124,9 @@ public void close() { this.delegate.close(); } - /** - * Retrieves the list of all roots provided by the client. - * @return A Mono that emits the list of roots result. - * @deprecated This will be removed in 0.9.0. Use - * {@link McpAsyncServerExchange#listRoots()}. - */ - @Deprecated - public Mono listRoots() { - return this.delegate.listRoots(null); - } - - /** - * Retrieves a paginated list of roots provided by the server. - * @param cursor Optional pagination cursor from a previous list request - * @return A Mono that emits the list of roots result containing - * @deprecated This will be removed in 0.9.0. Use - * {@link McpAsyncServerExchange#listRoots(String)}. - */ - @Deprecated - public Mono listRoots(String cursor) { - return this.delegate.listRoots(cursor); - } - // --------------------------------------- // Tool Management // --------------------------------------- - - /** - * Add a new tool registration at runtime. - * @param toolRegistration The tool registration to add - * @return Mono that completes when clients have been notified of the change - * @deprecated This method will be removed in 0.9.0. Use - * {@link #addTool(McpServerFeatures.AsyncToolSpecification)}. - */ - @Deprecated - public Mono addTool(McpServerFeatures.AsyncToolRegistration toolRegistration) { - return this.delegate.addTool(toolRegistration); - } - /** * Add a new tool specification at runtime. * @param toolSpecification The tool specification to add @@ -232,19 +156,6 @@ public Mono notifyToolsListChanged() { // --------------------------------------- // Resource Management // --------------------------------------- - - /** - * Add a new resource handler at runtime. - * @param resourceHandler The resource handler to add - * @return Mono that completes when clients have been notified of the change - * @deprecated This method will be removed in 0.9.0. Use - * {@link #addResource(McpServerFeatures.AsyncResourceSpecification)}. - */ - @Deprecated - public Mono addResource(McpServerFeatures.AsyncResourceRegistration resourceHandler) { - return this.delegate.addResource(resourceHandler); - } - /** * Add a new resource handler at runtime. * @param resourceHandler The resource handler to add @@ -274,19 +185,6 @@ public Mono notifyResourcesListChanged() { // --------------------------------------- // Prompt Management // --------------------------------------- - - /** - * Add a new prompt handler at runtime. - * @param promptRegistration The prompt handler to add - * @return Mono that completes when clients have been notified of the change - * @deprecated This method will be removed in 0.9.0. Use - * {@link #addPrompt(McpServerFeatures.AsyncPromptSpecification)}. - */ - @Deprecated - public Mono addPrompt(McpServerFeatures.AsyncPromptRegistration promptRegistration) { - return this.delegate.addPrompt(promptRegistration); - } - /** * Add a new prompt handler at runtime. * @param promptSpecification The prompt handler to add @@ -330,33 +228,6 @@ public Mono loggingNotification(LoggingMessageNotification loggingMessageN // --------------------------------------- // Sampling // --------------------------------------- - - /** - * Create a new message using the sampling capabilities of the client. The Model - * Context Protocol (MCP) provides a standardized way for servers to request LLM - * sampling (“completions” or “generations”) from language models via clients. This - * flow allows clients to maintain control over model access, selection, and - * permissions while enabling servers to leverage AI capabilities—with no server API - * keys necessary. Servers can request text or image-based interactions and optionally - * include context from MCP servers in their prompts. - * @param createMessageRequest The request to create a new message - * @return A Mono that completes when the message has been created - * @throws McpError if the client has not been initialized or does not support - * sampling capabilities - * @throws McpError if the client does not support the createMessage method - * @see McpSchema.CreateMessageRequest - * @see McpSchema.CreateMessageResult - * @see Sampling - * Specification - * @deprecated This will be removed in 0.9.0. Use - * {@link McpAsyncServerExchange#createMessage(McpSchema.CreateMessageRequest)}. - */ - @Deprecated - public Mono createMessage(McpSchema.CreateMessageRequest createMessageRequest) { - return this.delegate.createMessage(createMessageRequest); - } - /** * This method is package-private and used for test only. Should not be called by user * code. @@ -492,18 +363,6 @@ public McpSchema.Implementation getServerInfo() { return this.serverInfo; } - @Override - @Deprecated - public ClientCapabilities getClientCapabilities() { - throw new IllegalStateException("This method is deprecated and should not be called"); - } - - @Override - @Deprecated - public McpSchema.Implementation getClientInfo() { - throw new IllegalStateException("This method is deprecated and should not be called"); - } - @Override public Mono closeGracefully() { return this.mcpTransportProvider.closeGracefully(); @@ -514,18 +373,6 @@ public void close() { this.mcpTransportProvider.close(); } - @Override - @Deprecated - public Mono listRoots() { - return this.listRoots(null); - } - - @Override - @Deprecated - public Mono listRoots(String cursor) { - return Mono.error(new RuntimeException("Not implemented")); - } - private McpServerSession.NotificationHandler asyncRootsListChangedNotificationHandler( List, Mono>> rootsChangeConsumers) { return (exchange, params) -> exchange.listRoots() @@ -574,11 +421,6 @@ public Mono addTool(McpServerFeatures.AsyncToolSpecification toolSpecifica }); } - @Override - public Mono addTool(McpServerFeatures.AsyncToolRegistration toolRegistration) { - return this.addTool(toolRegistration.toSpecification()); - } - @Override public Mono removeTool(String toolName) { if (toolName == null) { @@ -661,11 +503,6 @@ public Mono addResource(McpServerFeatures.AsyncResourceSpecification resou }); } - @Override - public Mono addResource(McpServerFeatures.AsyncResourceRegistration resourceHandler) { - return this.addResource(resourceHandler.toSpecification()); - } - @Override public Mono removeResource(String resourceUri) { if (resourceUri == null) { @@ -756,11 +593,6 @@ public Mono addPrompt(McpServerFeatures.AsyncPromptSpecification promptSpe }); } - @Override - public Mono addPrompt(McpServerFeatures.AsyncPromptRegistration promptRegistration) { - return this.addPrompt(promptRegistration.toSpecification()); - } - @Override public Mono removePrompt(String promptName) { if (promptName == null) { @@ -859,648 +691,6 @@ private McpServerSession.RequestHandler setLoggerRequestHandler() { // --------------------------------------- @Override - @Deprecated - public Mono createMessage(McpSchema.CreateMessageRequest createMessageRequest) { - return Mono.error(new RuntimeException("Not implemented")); - } - - @Override - void setProtocolVersions(List protocolVersions) { - this.protocolVersions = protocolVersions; - } - - } - - private static final class LegacyAsyncServer extends McpAsyncServer { - - /** - * The MCP session implementation that manages bidirectional JSON-RPC - * communication between clients and servers. - */ - private final McpClientSession mcpSession; - - private final ServerMcpTransport transport; - - private final McpSchema.ServerCapabilities serverCapabilities; - - private final McpSchema.Implementation serverInfo; - - private McpSchema.ClientCapabilities clientCapabilities; - - private McpSchema.Implementation clientInfo; - - /** - * Thread-safe list of tool handlers that can be modified at runtime. - */ - private final CopyOnWriteArrayList tools = new CopyOnWriteArrayList<>(); - - private final CopyOnWriteArrayList resourceTemplates = new CopyOnWriteArrayList<>(); - - private final ConcurrentHashMap resources = new ConcurrentHashMap<>(); - - private final ConcurrentHashMap prompts = new ConcurrentHashMap<>(); - - private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG; - - /** - * Supported protocol versions. - */ - private List protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION); - - /** - * Create a new McpAsyncServer with the given transport and capabilities. - * @param mcpTransport The transport layer implementation for MCP communication. - * @param features The MCP server supported features. - */ - LegacyAsyncServer(ServerMcpTransport mcpTransport, McpServerFeatures.Async features) { - - this.serverInfo = features.serverInfo(); - this.serverCapabilities = features.serverCapabilities(); - this.tools.addAll(features.tools()); - this.resources.putAll(features.resources()); - this.resourceTemplates.addAll(features.resourceTemplates()); - this.prompts.putAll(features.prompts()); - - Map> requestHandlers = new HashMap<>(); - - // Initialize request handlers for standard MCP methods - requestHandlers.put(McpSchema.METHOD_INITIALIZE, asyncInitializeRequestHandler()); - - // Ping MUST respond with an empty data, but not NULL response. - requestHandlers.put(McpSchema.METHOD_PING, (params) -> Mono.just(Map.of())); - - // Add tools API handlers if the tool capability is enabled - if (this.serverCapabilities.tools() != null) { - requestHandlers.put(McpSchema.METHOD_TOOLS_LIST, toolsListRequestHandler()); - requestHandlers.put(McpSchema.METHOD_TOOLS_CALL, toolsCallRequestHandler()); - } - - // Add resources API handlers if provided - if (this.serverCapabilities.resources() != null) { - requestHandlers.put(McpSchema.METHOD_RESOURCES_LIST, resourcesListRequestHandler()); - requestHandlers.put(McpSchema.METHOD_RESOURCES_READ, resourcesReadRequestHandler()); - requestHandlers.put(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, resourceTemplateListRequestHandler()); - } - - // Add prompts API handlers if provider exists - if (this.serverCapabilities.prompts() != null) { - requestHandlers.put(McpSchema.METHOD_PROMPT_LIST, promptsListRequestHandler()); - requestHandlers.put(McpSchema.METHOD_PROMPT_GET, promptsGetRequestHandler()); - } - - // Add logging API handlers if the logging capability is enabled - if (this.serverCapabilities.logging() != null) { - requestHandlers.put(McpSchema.METHOD_LOGGING_SET_LEVEL, setLoggerRequestHandler()); - } - - Map notificationHandlers = new HashMap<>(); - - notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (params) -> Mono.empty()); - - List, Mono>> rootsChangeHandlers = features - .rootsChangeConsumers(); - - List, Mono>> rootsChangeConsumers = rootsChangeHandlers.stream() - .map(handler -> (Function, Mono>) (roots) -> handler.apply(null, roots)) - .toList(); - - if (Utils.isEmpty(rootsChangeConsumers)) { - rootsChangeConsumers = List.of((roots) -> Mono.fromRunnable(() -> logger.warn( - "Roots list changed notification, but no consumers provided. Roots list changed: {}", roots))); - } - - notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED, - asyncRootsListChangedNotificationHandler(rootsChangeConsumers)); - - this.transport = mcpTransport; - this.mcpSession = new McpClientSession(Duration.ofSeconds(10), mcpTransport, requestHandlers, - notificationHandlers); - } - - @Override - public Mono addTool(McpServerFeatures.AsyncToolSpecification toolSpecification) { - throw new IllegalArgumentException( - "McpAsyncServer configured with legacy " + "transport. Use McpServerTransportProvider instead."); - } - - @Override - public Mono addResource(McpServerFeatures.AsyncResourceSpecification resourceHandler) { - throw new IllegalArgumentException( - "McpAsyncServer configured with legacy " + "transport. Use McpServerTransportProvider instead."); - } - - @Override - public Mono addPrompt(McpServerFeatures.AsyncPromptSpecification promptSpecification) { - throw new IllegalArgumentException( - "McpAsyncServer configured with legacy " + "transport. Use McpServerTransportProvider instead."); - } - - // --------------------------------------- - // Lifecycle Management - // --------------------------------------- - private McpClientSession.RequestHandler asyncInitializeRequestHandler() { - return params -> { - McpSchema.InitializeRequest initializeRequest = transport.unmarshalFrom(params, - new TypeReference() { - }); - this.clientCapabilities = initializeRequest.capabilities(); - this.clientInfo = initializeRequest.clientInfo(); - logger.info("Client initialize request - Protocol: {}, Capabilities: {}, Info: {}", - initializeRequest.protocolVersion(), initializeRequest.capabilities(), - initializeRequest.clientInfo()); - - // The server MUST respond with the highest protocol version it supports - // if - // it does not support the requested (e.g. Client) version. - String serverProtocolVersion = this.protocolVersions.get(this.protocolVersions.size() - 1); - - if (this.protocolVersions.contains(initializeRequest.protocolVersion())) { - // If the server supports the requested protocol version, it MUST - // respond - // with the same version. - serverProtocolVersion = initializeRequest.protocolVersion(); - } - else { - logger.warn( - "Client requested unsupported protocol version: {}, so the server will sugggest the {} version instead", - initializeRequest.protocolVersion(), serverProtocolVersion); - } - - return Mono.just(new McpSchema.InitializeResult(serverProtocolVersion, this.serverCapabilities, - this.serverInfo, null)); - }; - } - - /** - * Get the server capabilities that define the supported features and - * functionality. - * @return The server capabilities - */ - public McpSchema.ServerCapabilities getServerCapabilities() { - return this.serverCapabilities; - } - - /** - * Get the server implementation information. - * @return The server implementation details - */ - public McpSchema.Implementation getServerInfo() { - return this.serverInfo; - } - - /** - * Get the client capabilities that define the supported features and - * functionality. - * @return The client capabilities - */ - public ClientCapabilities getClientCapabilities() { - return this.clientCapabilities; - } - - /** - * Get the client implementation information. - * @return The client implementation details - */ - public McpSchema.Implementation getClientInfo() { - return this.clientInfo; - } - - /** - * Gracefully closes the server, allowing any in-progress operations to complete. - * @return A Mono that completes when the server has been closed - */ - public Mono closeGracefully() { - return this.mcpSession.closeGracefully(); - } - - /** - * Close the server immediately. - */ - public void close() { - this.mcpSession.close(); - } - - private static final TypeReference LIST_ROOTS_RESULT_TYPE_REF = new TypeReference<>() { - }; - - /** - * Retrieves the list of all roots provided by the client. - * @return A Mono that emits the list of roots result. - */ - public Mono listRoots() { - return this.listRoots(null); - } - - /** - * Retrieves a paginated list of roots provided by the server. - * @param cursor Optional pagination cursor from a previous list request - * @return A Mono that emits the list of roots result containing - */ - public Mono listRoots(String cursor) { - return this.mcpSession.sendRequest(McpSchema.METHOD_ROOTS_LIST, new McpSchema.PaginatedRequest(cursor), - LIST_ROOTS_RESULT_TYPE_REF); - } - - private McpClientSession.NotificationHandler asyncRootsListChangedNotificationHandler( - List, Mono>> rootsChangeConsumers) { - return params -> listRoots().flatMap(listRootsResult -> Flux.fromIterable(rootsChangeConsumers) - .flatMap(consumer -> consumer.apply(listRootsResult.roots())) - .onErrorResume(error -> { - logger.error("Error handling roots list change notification", error); - return Mono.empty(); - }) - .then()); - } - - // --------------------------------------- - // Tool Management - // --------------------------------------- - - /** - * Add a new tool registration at runtime. - * @param toolRegistration The tool registration to add - * @return Mono that completes when clients have been notified of the change - */ - @Override - public Mono addTool(McpServerFeatures.AsyncToolRegistration toolRegistration) { - if (toolRegistration == null) { - return Mono.error(new McpError("Tool registration must not be null")); - } - if (toolRegistration.tool() == null) { - return Mono.error(new McpError("Tool must not be null")); - } - if (toolRegistration.call() == null) { - return Mono.error(new McpError("Tool call handler must not be null")); - } - if (this.serverCapabilities.tools() == null) { - return Mono.error(new McpError("Server must be configured with tool capabilities")); - } - - return Mono.defer(() -> { - // Check for duplicate tool names - if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolRegistration.tool().name()))) { - return Mono - .error(new McpError("Tool with name '" + toolRegistration.tool().name() + "' already exists")); - } - - this.tools.add(toolRegistration.toSpecification()); - logger.debug("Added tool handler: {}", toolRegistration.tool().name()); - - if (this.serverCapabilities.tools().listChanged()) { - return notifyToolsListChanged(); - } - return Mono.empty(); - }); - } - - /** - * Remove a tool handler at runtime. - * @param toolName The name of the tool handler to remove - * @return Mono that completes when clients have been notified of the change - */ - public Mono removeTool(String toolName) { - if (toolName == null) { - return Mono.error(new McpError("Tool name must not be null")); - } - if (this.serverCapabilities.tools() == null) { - return Mono.error(new McpError("Server must be configured with tool capabilities")); - } - - return Mono.defer(() -> { - boolean removed = this.tools - .removeIf(toolRegistration -> toolRegistration.tool().name().equals(toolName)); - if (removed) { - logger.debug("Removed tool handler: {}", toolName); - if (this.serverCapabilities.tools().listChanged()) { - return notifyToolsListChanged(); - } - return Mono.empty(); - } - return Mono.error(new McpError("Tool with name '" + toolName + "' not found")); - }); - } - - /** - * Notifies clients that the list of available tools has changed. - * @return A Mono that completes when all clients have been notified - */ - public Mono notifyToolsListChanged() { - return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null); - } - - private McpClientSession.RequestHandler toolsListRequestHandler() { - return params -> { - List tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList(); - - return Mono.just(new McpSchema.ListToolsResult(tools, null)); - }; - } - - private McpClientSession.RequestHandler toolsCallRequestHandler() { - return params -> { - McpSchema.CallToolRequest callToolRequest = transport.unmarshalFrom(params, - new TypeReference() { - }); - - Optional toolRegistration = this.tools.stream() - .filter(tr -> callToolRequest.name().equals(tr.tool().name())) - .findAny(); - - if (toolRegistration.isEmpty()) { - return Mono.error(new McpError("Tool not found: " + callToolRequest.name())); - } - - return toolRegistration.map(tool -> tool.call().apply(null, callToolRequest.arguments())) - .orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name()))); - }; - } - - // --------------------------------------- - // Resource Management - // --------------------------------------- - - /** - * Add a new resource handler at runtime. - * @param resourceHandler The resource handler to add - * @return Mono that completes when clients have been notified of the change - */ - @Override - public Mono addResource(McpServerFeatures.AsyncResourceRegistration resourceHandler) { - if (resourceHandler == null || resourceHandler.resource() == null) { - return Mono.error(new McpError("Resource must not be null")); - } - - if (this.serverCapabilities.resources() == null) { - return Mono.error(new McpError("Server must be configured with resource capabilities")); - } - - return Mono.defer(() -> { - if (this.resources.putIfAbsent(resourceHandler.resource().uri(), - resourceHandler.toSpecification()) != null) { - return Mono.error(new McpError( - "Resource with URI '" + resourceHandler.resource().uri() + "' already exists")); - } - logger.debug("Added resource handler: {}", resourceHandler.resource().uri()); - if (this.serverCapabilities.resources().listChanged()) { - return notifyResourcesListChanged(); - } - return Mono.empty(); - }); - } - - /** - * Remove a resource handler at runtime. - * @param resourceUri The URI of the resource handler to remove - * @return Mono that completes when clients have been notified of the change - */ - public Mono removeResource(String resourceUri) { - if (resourceUri == null) { - return Mono.error(new McpError("Resource URI must not be null")); - } - if (this.serverCapabilities.resources() == null) { - return Mono.error(new McpError("Server must be configured with resource capabilities")); - } - - return Mono.defer(() -> { - McpServerFeatures.AsyncResourceSpecification removed = this.resources.remove(resourceUri); - if (removed != null) { - logger.debug("Removed resource handler: {}", resourceUri); - if (this.serverCapabilities.resources().listChanged()) { - return notifyResourcesListChanged(); - } - return Mono.empty(); - } - return Mono.error(new McpError("Resource with URI '" + resourceUri + "' not found")); - }); - } - - /** - * Notifies clients that the list of available resources has changed. - * @return A Mono that completes when all clients have been notified - */ - public Mono notifyResourcesListChanged() { - return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED, null); - } - - private McpClientSession.RequestHandler resourcesListRequestHandler() { - return params -> { - var resourceList = this.resources.values() - .stream() - .map(McpServerFeatures.AsyncResourceSpecification::resource) - .toList(); - return Mono.just(new McpSchema.ListResourcesResult(resourceList, null)); - }; - } - - private McpClientSession.RequestHandler resourceTemplateListRequestHandler() { - return params -> Mono.just(new McpSchema.ListResourceTemplatesResult(this.resourceTemplates, null)); - - } - - private McpClientSession.RequestHandler resourcesReadRequestHandler() { - return params -> { - McpSchema.ReadResourceRequest resourceRequest = transport.unmarshalFrom(params, - new TypeReference() { - }); - var resourceUri = resourceRequest.uri(); - McpServerFeatures.AsyncResourceSpecification registration = this.resources.get(resourceUri); - if (registration != null) { - return registration.readHandler().apply(null, resourceRequest); - } - return Mono.error(new McpError("Resource not found: " + resourceUri)); - }; - } - - // --------------------------------------- - // Prompt Management - // --------------------------------------- - - /** - * Add a new prompt handler at runtime. - * @param promptRegistration The prompt handler to add - * @return Mono that completes when clients have been notified of the change - */ - @Override - public Mono addPrompt(McpServerFeatures.AsyncPromptRegistration promptRegistration) { - if (promptRegistration == null) { - return Mono.error(new McpError("Prompt registration must not be null")); - } - if (this.serverCapabilities.prompts() == null) { - return Mono.error(new McpError("Server must be configured with prompt capabilities")); - } - - return Mono.defer(() -> { - McpServerFeatures.AsyncPromptSpecification registration = this.prompts - .putIfAbsent(promptRegistration.prompt().name(), promptRegistration.toSpecification()); - if (registration != null) { - return Mono.error(new McpError( - "Prompt with name '" + promptRegistration.prompt().name() + "' already exists")); - } - - logger.debug("Added prompt handler: {}", promptRegistration.prompt().name()); - - // Servers that declared the listChanged capability SHOULD send a - // notification, - // when the list of available prompts changes - if (this.serverCapabilities.prompts().listChanged()) { - return notifyPromptsListChanged(); - } - return Mono.empty(); - }); - } - - /** - * Remove a prompt handler at runtime. - * @param promptName The name of the prompt handler to remove - * @return Mono that completes when clients have been notified of the change - */ - public Mono removePrompt(String promptName) { - if (promptName == null) { - return Mono.error(new McpError("Prompt name must not be null")); - } - if (this.serverCapabilities.prompts() == null) { - return Mono.error(new McpError("Server must be configured with prompt capabilities")); - } - - return Mono.defer(() -> { - McpServerFeatures.AsyncPromptSpecification removed = this.prompts.remove(promptName); - - if (removed != null) { - logger.debug("Removed prompt handler: {}", promptName); - // Servers that declared the listChanged capability SHOULD send a - // notification, when the list of available prompts changes - if (this.serverCapabilities.prompts().listChanged()) { - return this.notifyPromptsListChanged(); - } - return Mono.empty(); - } - return Mono.error(new McpError("Prompt with name '" + promptName + "' not found")); - }); - } - - /** - * Notifies clients that the list of available prompts has changed. - * @return A Mono that completes when all clients have been notified - */ - public Mono notifyPromptsListChanged() { - return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null); - } - - private McpClientSession.RequestHandler promptsListRequestHandler() { - return params -> { - // TODO: Implement pagination - // McpSchema.PaginatedRequest request = transport.unmarshalFrom(params, - // new TypeReference() { - // }); - - var promptList = this.prompts.values() - .stream() - .map(McpServerFeatures.AsyncPromptSpecification::prompt) - .toList(); - - return Mono.just(new McpSchema.ListPromptsResult(promptList, null)); - }; - } - - private McpClientSession.RequestHandler promptsGetRequestHandler() { - return params -> { - McpSchema.GetPromptRequest promptRequest = transport.unmarshalFrom(params, - new TypeReference() { - }); - - // Implement prompt retrieval logic here - McpServerFeatures.AsyncPromptSpecification registration = this.prompts.get(promptRequest.name()); - if (registration == null) { - return Mono.error(new McpError("Prompt not found: " + promptRequest.name())); - } - - return registration.promptHandler().apply(null, promptRequest); - }; - } - - // --------------------------------------- - // Logging Management - // --------------------------------------- - - /** - * Send a logging message notification to all connected clients. Messages below - * the current minimum logging level will be filtered out. - * @param loggingMessageNotification The logging message to send - * @return A Mono that completes when the notification has been sent - */ - public Mono loggingNotification(LoggingMessageNotification loggingMessageNotification) { - - if (loggingMessageNotification == null) { - return Mono.error(new McpError("Logging message must not be null")); - } - - Map params = this.transport.unmarshalFrom(loggingMessageNotification, - new TypeReference>() { - }); - - if (loggingMessageNotification.level().level() < minLoggingLevel.level()) { - return Mono.empty(); - } - - return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_MESSAGE, params); - } - - /** - * Handles requests to set the minimum logging level. Messages below this level - * will not be sent. - * @return A handler that processes logging level change requests - */ - private McpClientSession.RequestHandler setLoggerRequestHandler() { - return params -> { - this.minLoggingLevel = transport.unmarshalFrom(params, new TypeReference() { - }); - - return Mono.empty(); - }; - } - - // --------------------------------------- - // Sampling - // --------------------------------------- - private static final TypeReference CREATE_MESSAGE_RESULT_TYPE_REF = new TypeReference<>() { - }; - - /** - * Create a new message using the sampling capabilities of the client. The Model - * Context Protocol (MCP) provides a standardized way for servers to request LLM - * sampling (“completions” or “generations”) from language models via clients. - * This flow allows clients to maintain control over model access, selection, and - * permissions while enabling servers to leverage AI capabilities—with no server - * API keys necessary. Servers can request text or image-based interactions and - * optionally include context from MCP servers in their prompts. - * @param createMessageRequest The request to create a new message - * @return A Mono that completes when the message has been created - * @throws McpError if the client has not been initialized or does not support - * sampling capabilities - * @throws McpError if the client does not support the createMessage method - * @see McpSchema.CreateMessageRequest - * @see McpSchema.CreateMessageResult - * @see Sampling - * Specification - */ - public Mono createMessage(McpSchema.CreateMessageRequest createMessageRequest) { - - if (this.clientCapabilities == null) { - return Mono.error(new McpError("Client must be initialized. Call the initialize method first!")); - } - if (this.clientCapabilities.sampling() == null) { - return Mono.error(new McpError("Client must be configured with sampling capabilities")); - } - return this.mcpSession.sendRequest(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, createMessageRequest, - CREATE_MESSAGE_RESULT_TYPE_REF); - } - - /** - * This method is package-private and used for test only. Should not be called by - * user code. - * @param protocolVersions the Client supported protocol versions. - */ void setProtocolVersions(List protocolVersions) { this.protocolVersions = protocolVersions; } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java index d8dfcb018..091efac2f 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServer.java @@ -11,16 +11,12 @@ import java.util.Map; import java.util.function.BiConsumer; import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpServerTransportProvider; -import io.modelcontextprotocol.spec.ServerMcpTransport; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate; +import io.modelcontextprotocol.spec.McpServerTransportProvider; import io.modelcontextprotocol.util.Assert; import reactor.core.publisher.Mono; @@ -136,21 +132,6 @@ static SyncSpecification sync(McpServerTransportProvider transportProvider) { return new SyncSpecification(transportProvider); } - /** - * Starts building a synchronous MCP server that provides blocking operations. - * Synchronous servers block the current Thread's execution upon each request before - * giving the control back to the caller, making them simpler to implement but - * potentially less scalable for concurrent operations. - * @param transport The transport layer implementation for MCP communication - * @return A new instance of {@link SyncSpec} for configuring the server. - * @deprecated This method will be removed in 0.9.0. Use - * {@link #sync(McpServerTransportProvider)} instead. - */ - @Deprecated - static SyncSpec sync(ServerMcpTransport transport) { - return new SyncSpec(transport); - } - /** * Starts building an asynchronous MCP server that provides non-blocking operations. * Asynchronous servers can handle multiple requests concurrently on a single Thread @@ -163,21 +144,6 @@ static AsyncSpecification async(McpServerTransportProvider transportProvider) { return new AsyncSpecification(transportProvider); } - /** - * Starts building an asynchronous MCP server that provides non-blocking operations. - * Asynchronous servers can handle multiple requests concurrently on a single Thread - * using a functional paradigm with non-blocking server transports, making them more - * scalable for high-concurrency scenarios but more complex to implement. - * @param transport The transport layer implementation for MCP communication - * @return A new instance of {@link AsyncSpec} for configuring the server. - * @deprecated This method will be removed in 0.9.0. Use - * {@link #async(McpServerTransportProvider)} instead. - */ - @Deprecated - static AsyncSpec async(ServerMcpTransport transport) { - return new AsyncSpec(transport); - } - /** * Asynchronous server specification. */ @@ -1004,819 +970,4 @@ public McpSyncServer build() { } - /** - * Asynchronous server specification. - * - * @deprecated - */ - @Deprecated - class AsyncSpec { - - private static final McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("mcp-server", - "1.0.0"); - - private final ServerMcpTransport transport; - - private ObjectMapper objectMapper; - - private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO; - - private McpSchema.ServerCapabilities serverCapabilities; - - /** - * The Model Context Protocol (MCP) allows servers to expose tools that can be - * invoked by language models. Tools enable models to interact with external - * systems, such as querying databases, calling APIs, or performing computations. - * Each tool is uniquely identified by a name and includes metadata describing its - * schema. - */ - private final List tools = new ArrayList<>(); - - /** - * The Model Context Protocol (MCP) provides a standardized way for servers to - * expose resources to clients. Resources allow servers to share data that - * provides context to language models, such as files, database schemas, or - * application-specific information. Each resource is uniquely identified by a - * URI. - */ - private final Map resources = new HashMap<>(); - - private final List resourceTemplates = new ArrayList<>(); - - /** - * The Model Context Protocol (MCP) provides a standardized way for servers to - * expose prompt templates to clients. Prompts allow servers to provide structured - * messages and instructions for interacting with language models. Clients can - * discover available prompts, retrieve their contents, and provide arguments to - * customize them. - */ - private final Map prompts = new HashMap<>(); - - private final List, Mono>> rootsChangeConsumers = new ArrayList<>(); - - private AsyncSpec(ServerMcpTransport transport) { - Assert.notNull(transport, "Transport must not be null"); - this.transport = transport; - } - - /** - * Sets the server implementation information that will be shared with clients - * during connection initialization. This helps with version compatibility, - * debugging, and server identification. - * @param serverInfo The server implementation details including name and version. - * Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if serverInfo is null - */ - public AsyncSpec serverInfo(McpSchema.Implementation serverInfo) { - Assert.notNull(serverInfo, "Server info must not be null"); - this.serverInfo = serverInfo; - return this; - } - - /** - * Sets the server implementation information using name and version strings. This - * is a convenience method alternative to - * {@link #serverInfo(McpSchema.Implementation)}. - * @param name The server name. Must not be null or empty. - * @param version The server version. Must not be null or empty. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if name or version is null or empty - * @see #serverInfo(McpSchema.Implementation) - */ - public AsyncSpec serverInfo(String name, String version) { - Assert.hasText(name, "Name must not be null or empty"); - Assert.hasText(version, "Version must not be null or empty"); - this.serverInfo = new McpSchema.Implementation(name, version); - return this; - } - - /** - * Sets the server capabilities that will be advertised to clients during - * connection initialization. Capabilities define what features the server - * supports, such as: - *
    - *
  • Tool execution - *
  • Resource access - *
  • Prompt handling - *
  • Streaming responses - *
  • Batch operations - *
- * @param serverCapabilities The server capabilities configuration. Must not be - * null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if serverCapabilities is null - */ - public AsyncSpec capabilities(McpSchema.ServerCapabilities serverCapabilities) { - this.serverCapabilities = serverCapabilities; - return this; - } - - /** - * Adds a single tool with its implementation handler to the server. This is a - * convenience method for registering individual tools without creating a - * {@link McpServerFeatures.AsyncToolRegistration} explicitly. - * - *

- * Example usage:

{@code
-		 * .tool(
-		 *     new Tool("calculator", "Performs calculations", schema),
-		 *     args -> Mono.just(new CallToolResult("Result: " + calculate(args)))
-		 * )
-		 * }
- * @param tool The tool definition including name, description, and schema. Must - * not be null. - * @param handler The function that implements the tool's logic. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if tool or handler is null - */ - public AsyncSpec tool(McpSchema.Tool tool, Function, Mono> handler) { - Assert.notNull(tool, "Tool must not be null"); - Assert.notNull(handler, "Handler must not be null"); - - this.tools.add(new McpServerFeatures.AsyncToolRegistration(tool, handler)); - - return this; - } - - /** - * Adds multiple tools with their handlers to the server using a List. This method - * is useful when tools are dynamically generated or loaded from a configuration - * source. - * @param toolRegistrations The list of tool registrations to add. Must not be - * null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if toolRegistrations is null - * @see #tools(McpServerFeatures.AsyncToolRegistration...) - */ - public AsyncSpec tools(List toolRegistrations) { - Assert.notNull(toolRegistrations, "Tool handlers list must not be null"); - this.tools.addAll(toolRegistrations); - return this; - } - - /** - * Adds multiple tools with their handlers to the server using varargs. This - * method provides a convenient way to register multiple tools inline. - * - *

- * Example usage:

{@code
-		 * .tools(
-		 *     new McpServerFeatures.AsyncToolRegistration(calculatorTool, calculatorHandler),
-		 *     new McpServerFeatures.AsyncToolRegistration(weatherTool, weatherHandler),
-		 *     new McpServerFeatures.AsyncToolRegistration(fileManagerTool, fileManagerHandler)
-		 * )
-		 * }
- * @param toolRegistrations The tool registrations to add. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if toolRegistrations is null - * @see #tools(List) - */ - public AsyncSpec tools(McpServerFeatures.AsyncToolRegistration... toolRegistrations) { - for (McpServerFeatures.AsyncToolRegistration tool : toolRegistrations) { - this.tools.add(tool); - } - return this; - } - - /** - * Registers multiple resources with their handlers using a Map. This method is - * useful when resources are dynamically generated or loaded from a configuration - * source. - * @param resourceRegsitrations Map of resource name to registration. Must not be - * null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if resourceRegsitrations is null - * @see #resources(McpServerFeatures.AsyncResourceRegistration...) - */ - public AsyncSpec resources(Map resourceRegsitrations) { - Assert.notNull(resourceRegsitrations, "Resource handlers map must not be null"); - this.resources.putAll(resourceRegsitrations); - return this; - } - - /** - * Registers multiple resources with their handlers using a List. This method is - * useful when resources need to be added in bulk from a collection. - * @param resourceRegsitrations List of resource registrations. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if resourceRegsitrations is null - * @see #resources(McpServerFeatures.AsyncResourceRegistration...) - */ - public AsyncSpec resources(List resourceRegsitrations) { - Assert.notNull(resourceRegsitrations, "Resource handlers list must not be null"); - for (McpServerFeatures.AsyncResourceRegistration resource : resourceRegsitrations) { - this.resources.put(resource.resource().uri(), resource); - } - return this; - } - - /** - * Registers multiple resources with their handlers using varargs. This method - * provides a convenient way to register multiple resources inline. - * - *

- * Example usage:

{@code
-		 * .resources(
-		 *     new McpServerFeatures.AsyncResourceRegistration(fileResource, fileHandler),
-		 *     new McpServerFeatures.AsyncResourceRegistration(dbResource, dbHandler),
-		 *     new McpServerFeatures.AsyncResourceRegistration(apiResource, apiHandler)
-		 * )
-		 * }
- * @param resourceRegistrations The resource registrations to add. Must not be - * null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if resourceRegistrations is null - */ - public AsyncSpec resources(McpServerFeatures.AsyncResourceRegistration... resourceRegistrations) { - Assert.notNull(resourceRegistrations, "Resource handlers list must not be null"); - for (McpServerFeatures.AsyncResourceRegistration resource : resourceRegistrations) { - this.resources.put(resource.resource().uri(), resource); - } - return this; - } - - /** - * Sets the resource templates that define patterns for dynamic resource access. - * Templates use URI patterns with placeholders that can be filled at runtime. - * - *

- * Example usage:

{@code
-		 * .resourceTemplates(
-		 *     new ResourceTemplate("file://{path}", "Access files by path"),
-		 *     new ResourceTemplate("db://{table}/{id}", "Access database records")
-		 * )
-		 * }
- * @param resourceTemplates List of resource templates. If null, clears existing - * templates. - * @return This builder instance for method chaining - * @see #resourceTemplates(ResourceTemplate...) - */ - public AsyncSpec resourceTemplates(List resourceTemplates) { - this.resourceTemplates.addAll(resourceTemplates); - return this; - } - - /** - * Sets the resource templates using varargs for convenience. This is an - * alternative to {@link #resourceTemplates(List)}. - * @param resourceTemplates The resource templates to set. - * @return This builder instance for method chaining - * @see #resourceTemplates(List) - */ - public AsyncSpec resourceTemplates(ResourceTemplate... resourceTemplates) { - for (ResourceTemplate resourceTemplate : resourceTemplates) { - this.resourceTemplates.add(resourceTemplate); - } - return this; - } - - /** - * Registers multiple prompts with their handlers using a Map. This method is - * useful when prompts are dynamically generated or loaded from a configuration - * source. - * - *

- * Example usage:

{@code
-		 * .prompts(Map.of("analysis", new McpServerFeatures.AsyncPromptRegistration(
-		 *     new Prompt("analysis", "Code analysis template"),
-		 *     request -> Mono.just(new GetPromptResult(generateAnalysisPrompt(request)))
-		 * )));
-		 * }
- * @param prompts Map of prompt name to registration. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if prompts is null - */ - public AsyncSpec prompts(Map prompts) { - this.prompts.putAll(prompts); - return this; - } - - /** - * Registers multiple prompts with their handlers using a List. This method is - * useful when prompts need to be added in bulk from a collection. - * @param prompts List of prompt registrations. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if prompts is null - * @see #prompts(McpServerFeatures.AsyncPromptRegistration...) - */ - public AsyncSpec prompts(List prompts) { - for (McpServerFeatures.AsyncPromptRegistration prompt : prompts) { - this.prompts.put(prompt.prompt().name(), prompt); - } - return this; - } - - /** - * Registers multiple prompts with their handlers using varargs. This method - * provides a convenient way to register multiple prompts inline. - * - *

- * Example usage:

{@code
-		 * .prompts(
-		 *     new McpServerFeatures.AsyncPromptRegistration(analysisPrompt, analysisHandler),
-		 *     new McpServerFeatures.AsyncPromptRegistration(summaryPrompt, summaryHandler),
-		 *     new McpServerFeatures.AsyncPromptRegistration(reviewPrompt, reviewHandler)
-		 * )
-		 * }
- * @param prompts The prompt registrations to add. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if prompts is null - */ - public AsyncSpec prompts(McpServerFeatures.AsyncPromptRegistration... prompts) { - for (McpServerFeatures.AsyncPromptRegistration prompt : prompts) { - this.prompts.put(prompt.prompt().name(), prompt); - } - return this; - } - - /** - * Registers a consumer that will be notified when the list of roots changes. This - * is useful for updating resource availability dynamically, such as when new - * files are added or removed. - * @param consumer The consumer to register. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if consumer is null - */ - public AsyncSpec rootsChangeConsumer(Function, Mono> consumer) { - Assert.notNull(consumer, "Consumer must not be null"); - this.rootsChangeConsumers.add(consumer); - return this; - } - - /** - * Registers multiple consumers that will be notified when the list of roots - * changes. This method is useful when multiple consumers need to be registered at - * once. - * @param consumers The list of consumers to register. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if consumers is null - */ - public AsyncSpec rootsChangeConsumers(List, Mono>> consumers) { - Assert.notNull(consumers, "Consumers list must not be null"); - this.rootsChangeConsumers.addAll(consumers); - return this; - } - - /** - * Registers multiple consumers that will be notified when the list of roots - * changes using varargs. This method provides a convenient way to register - * multiple consumers inline. - * @param consumers The consumers to register. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if consumers is null - */ - public AsyncSpec rootsChangeConsumers( - @SuppressWarnings("unchecked") Function, Mono>... consumers) { - for (Function, Mono> consumer : consumers) { - this.rootsChangeConsumers.add(consumer); - } - return this; - } - - /** - * Builds an asynchronous MCP server that provides non-blocking operations. - * @return A new instance of {@link McpAsyncServer} configured with this builder's - * settings - */ - public McpAsyncServer build() { - var tools = this.tools.stream().map(McpServerFeatures.AsyncToolRegistration::toSpecification).toList(); - - var resources = this.resources.entrySet() - .stream() - .map(entry -> Map.entry(entry.getKey(), entry.getValue().toSpecification())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - var prompts = this.prompts.entrySet() - .stream() - .map(entry -> Map.entry(entry.getKey(), entry.getValue().toSpecification())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - var rootsChangeHandlers = this.rootsChangeConsumers.stream() - .map(consumer -> (BiFunction, Mono>) (exchange, - roots) -> consumer.apply(roots)) - .toList(); - - var features = new McpServerFeatures.Async(this.serverInfo, this.serverCapabilities, tools, resources, - this.resourceTemplates, prompts, rootsChangeHandlers); - - return new McpAsyncServer(this.transport, features); - } - - } - - /** - * Synchronous server specification. - * - * @deprecated - */ - @Deprecated - class SyncSpec { - - private static final McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("mcp-server", - "1.0.0"); - - private final ServerMcpTransport transport; - - private final McpServerTransportProvider transportProvider; - - private ObjectMapper objectMapper; - - private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO; - - private McpSchema.ServerCapabilities serverCapabilities; - - /** - * The Model Context Protocol (MCP) allows servers to expose tools that can be - * invoked by language models. Tools enable models to interact with external - * systems, such as querying databases, calling APIs, or performing computations. - * Each tool is uniquely identified by a name and includes metadata describing its - * schema. - */ - private final List tools = new ArrayList<>(); - - /** - * The Model Context Protocol (MCP) provides a standardized way for servers to - * expose resources to clients. Resources allow servers to share data that - * provides context to language models, such as files, database schemas, or - * application-specific information. Each resource is uniquely identified by a - * URI. - */ - private final Map resources = new HashMap<>(); - - private final List resourceTemplates = new ArrayList<>(); - - /** - * The Model Context Protocol (MCP) provides a standardized way for servers to - * expose prompt templates to clients. Prompts allow servers to provide structured - * messages and instructions for interacting with language models. Clients can - * discover available prompts, retrieve their contents, and provide arguments to - * customize them. - */ - private final Map prompts = new HashMap<>(); - - private final List>> rootsChangeConsumers = new ArrayList<>(); - - private SyncSpec(McpServerTransportProvider transportProvider) { - Assert.notNull(transportProvider, "Transport provider must not be null"); - this.transportProvider = transportProvider; - this.transport = null; - } - - private SyncSpec(ServerMcpTransport transport) { - Assert.notNull(transport, "Transport must not be null"); - this.transport = transport; - this.transportProvider = null; - } - - /** - * Sets the server implementation information that will be shared with clients - * during connection initialization. This helps with version compatibility, - * debugging, and server identification. - * @param serverInfo The server implementation details including name and version. - * Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if serverInfo is null - */ - public SyncSpec serverInfo(McpSchema.Implementation serverInfo) { - Assert.notNull(serverInfo, "Server info must not be null"); - this.serverInfo = serverInfo; - return this; - } - - /** - * Sets the server implementation information using name and version strings. This - * is a convenience method alternative to - * {@link #serverInfo(McpSchema.Implementation)}. - * @param name The server name. Must not be null or empty. - * @param version The server version. Must not be null or empty. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if name or version is null or empty - * @see #serverInfo(McpSchema.Implementation) - */ - public SyncSpec serverInfo(String name, String version) { - Assert.hasText(name, "Name must not be null or empty"); - Assert.hasText(version, "Version must not be null or empty"); - this.serverInfo = new McpSchema.Implementation(name, version); - return this; - } - - /** - * Sets the server capabilities that will be advertised to clients during - * connection initialization. Capabilities define what features the server - * supports, such as: - *
    - *
  • Tool execution - *
  • Resource access - *
  • Prompt handling - *
  • Streaming responses - *
  • Batch operations - *
- * @param serverCapabilities The server capabilities configuration. Must not be - * null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if serverCapabilities is null - */ - public SyncSpec capabilities(McpSchema.ServerCapabilities serverCapabilities) { - this.serverCapabilities = serverCapabilities; - return this; - } - - /** - * Adds a single tool with its implementation handler to the server. This is a - * convenience method for registering individual tools without creating a - * {@link McpServerFeatures.SyncToolRegistration} explicitly. - * - *

- * Example usage:

{@code
-		 * .tool(
-		 *     new Tool("calculator", "Performs calculations", schema),
-		 *     args -> new CallToolResult("Result: " + calculate(args))
-		 * )
-		 * }
- * @param tool The tool definition including name, description, and schema. Must - * not be null. - * @param handler The function that implements the tool's logic. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if tool or handler is null - */ - public SyncSpec tool(McpSchema.Tool tool, Function, McpSchema.CallToolResult> handler) { - Assert.notNull(tool, "Tool must not be null"); - Assert.notNull(handler, "Handler must not be null"); - - this.tools.add(new McpServerFeatures.SyncToolRegistration(tool, handler)); - - return this; - } - - /** - * Adds multiple tools with their handlers to the server using a List. This method - * is useful when tools are dynamically generated or loaded from a configuration - * source. - * @param toolRegistrations The list of tool registrations to add. Must not be - * null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if toolRegistrations is null - * @see #tools(McpServerFeatures.SyncToolRegistration...) - */ - public SyncSpec tools(List toolRegistrations) { - Assert.notNull(toolRegistrations, "Tool handlers list must not be null"); - this.tools.addAll(toolRegistrations); - return this; - } - - /** - * Adds multiple tools with their handlers to the server using varargs. This - * method provides a convenient way to register multiple tools inline. - * - *

- * Example usage:

{@code
-		 * .tools(
-		 *     new ToolRegistration(calculatorTool, calculatorHandler),
-		 *     new ToolRegistration(weatherTool, weatherHandler),
-		 *     new ToolRegistration(fileManagerTool, fileManagerHandler)
-		 * )
-		 * }
- * @param toolRegistrations The tool registrations to add. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if toolRegistrations is null - * @see #tools(List) - */ - public SyncSpec tools(McpServerFeatures.SyncToolRegistration... toolRegistrations) { - for (McpServerFeatures.SyncToolRegistration tool : toolRegistrations) { - this.tools.add(tool); - } - return this; - } - - /** - * Registers multiple resources with their handlers using a Map. This method is - * useful when resources are dynamically generated or loaded from a configuration - * source. - * @param resourceRegsitrations Map of resource name to registration. Must not be - * null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if resourceRegsitrations is null - * @see #resources(McpServerFeatures.SyncResourceRegistration...) - */ - public SyncSpec resources(Map resourceRegsitrations) { - Assert.notNull(resourceRegsitrations, "Resource handlers map must not be null"); - this.resources.putAll(resourceRegsitrations); - return this; - } - - /** - * Registers multiple resources with their handlers using a List. This method is - * useful when resources need to be added in bulk from a collection. - * @param resourceRegsitrations List of resource registrations. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if resourceRegsitrations is null - * @see #resources(McpServerFeatures.SyncResourceRegistration...) - */ - public SyncSpec resources(List resourceRegsitrations) { - Assert.notNull(resourceRegsitrations, "Resource handlers list must not be null"); - for (McpServerFeatures.SyncResourceRegistration resource : resourceRegsitrations) { - this.resources.put(resource.resource().uri(), resource); - } - return this; - } - - /** - * Registers multiple resources with their handlers using varargs. This method - * provides a convenient way to register multiple resources inline. - * - *

- * Example usage:

{@code
-		 * .resources(
-		 *     new ResourceRegistration(fileResource, fileHandler),
-		 *     new ResourceRegistration(dbResource, dbHandler),
-		 *     new ResourceRegistration(apiResource, apiHandler)
-		 * )
-		 * }
- * @param resourceRegistrations The resource registrations to add. Must not be - * null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if resourceRegistrations is null - */ - public SyncSpec resources(McpServerFeatures.SyncResourceRegistration... resourceRegistrations) { - Assert.notNull(resourceRegistrations, "Resource handlers list must not be null"); - for (McpServerFeatures.SyncResourceRegistration resource : resourceRegistrations) { - this.resources.put(resource.resource().uri(), resource); - } - return this; - } - - /** - * Sets the resource templates that define patterns for dynamic resource access. - * Templates use URI patterns with placeholders that can be filled at runtime. - * - *

- * Example usage:

{@code
-		 * .resourceTemplates(
-		 *     new ResourceTemplate("file://{path}", "Access files by path"),
-		 *     new ResourceTemplate("db://{table}/{id}", "Access database records")
-		 * )
-		 * }
- * @param resourceTemplates List of resource templates. If null, clears existing - * templates. - * @return This builder instance for method chaining - * @see #resourceTemplates(ResourceTemplate...) - */ - public SyncSpec resourceTemplates(List resourceTemplates) { - this.resourceTemplates.addAll(resourceTemplates); - return this; - } - - /** - * Sets the resource templates using varargs for convenience. This is an - * alternative to {@link #resourceTemplates(List)}. - * @param resourceTemplates The resource templates to set. - * @return This builder instance for method chaining - * @see #resourceTemplates(List) - */ - public SyncSpec resourceTemplates(ResourceTemplate... resourceTemplates) { - for (ResourceTemplate resourceTemplate : resourceTemplates) { - this.resourceTemplates.add(resourceTemplate); - } - return this; - } - - /** - * Registers multiple prompts with their handlers using a Map. This method is - * useful when prompts are dynamically generated or loaded from a configuration - * source. - * - *

- * Example usage:

{@code
-		 * Map prompts = new HashMap<>();
-		 * prompts.put("analysis", new PromptRegistration(
-		 *     new Prompt("analysis", "Code analysis template"),
-		 *     request -> new GetPromptResult(generateAnalysisPrompt(request))
-		 * ));
-		 * .prompts(prompts)
-		 * }
- * @param prompts Map of prompt name to registration. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if prompts is null - */ - public SyncSpec prompts(Map prompts) { - this.prompts.putAll(prompts); - return this; - } - - /** - * Registers multiple prompts with their handlers using a List. This method is - * useful when prompts need to be added in bulk from a collection. - * @param prompts List of prompt registrations. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if prompts is null - * @see #prompts(McpServerFeatures.SyncPromptRegistration...) - */ - public SyncSpec prompts(List prompts) { - for (McpServerFeatures.SyncPromptRegistration prompt : prompts) { - this.prompts.put(prompt.prompt().name(), prompt); - } - return this; - } - - /** - * Registers multiple prompts with their handlers using varargs. This method - * provides a convenient way to register multiple prompts inline. - * - *

- * Example usage:

{@code
-		 * .prompts(
-		 *     new PromptRegistration(analysisPrompt, analysisHandler),
-		 *     new PromptRegistration(summaryPrompt, summaryHandler),
-		 *     new PromptRegistration(reviewPrompt, reviewHandler)
-		 * )
-		 * }
- * @param prompts The prompt registrations to add. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if prompts is null - */ - public SyncSpec prompts(McpServerFeatures.SyncPromptRegistration... prompts) { - for (McpServerFeatures.SyncPromptRegistration prompt : prompts) { - this.prompts.put(prompt.prompt().name(), prompt); - } - return this; - } - - /** - * Registers a consumer that will be notified when the list of roots changes. This - * is useful for updating resource availability dynamically, such as when new - * files are added or removed. - * @param consumer The consumer to register. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if consumer is null - */ - public SyncSpec rootsChangeConsumer(Consumer> consumer) { - Assert.notNull(consumer, "Consumer must not be null"); - this.rootsChangeConsumers.add(consumer); - return this; - } - - /** - * Registers multiple consumers that will be notified when the list of roots - * changes. This method is useful when multiple consumers need to be registered at - * once. - * @param consumers The list of consumers to register. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if consumers is null - */ - public SyncSpec rootsChangeConsumers(List>> consumers) { - Assert.notNull(consumers, "Consumers list must not be null"); - this.rootsChangeConsumers.addAll(consumers); - return this; - } - - /** - * Registers multiple consumers that will be notified when the list of roots - * changes using varargs. This method provides a convenient way to register - * multiple consumers inline. - * @param consumers The consumers to register. Must not be null. - * @return This builder instance for method chaining - * @throws IllegalArgumentException if consumers is null - */ - public SyncSpec rootsChangeConsumers(Consumer>... consumers) { - for (Consumer> consumer : consumers) { - this.rootsChangeConsumers.add(consumer); - } - return this; - } - - /** - * Builds a synchronous MCP server that provides blocking operations. - * @return A new instance of {@link McpSyncServer} configured with this builder's - * settings - */ - public McpSyncServer build() { - var tools = this.tools.stream().map(McpServerFeatures.SyncToolRegistration::toSpecification).toList(); - - var resources = this.resources.entrySet() - .stream() - .map(entry -> Map.entry(entry.getKey(), entry.getValue().toSpecification())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - var prompts = this.prompts.entrySet() - .stream() - .map(entry -> Map.entry(entry.getKey(), entry.getValue().toSpecification())) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - var rootsChangeHandlers = this.rootsChangeConsumers.stream() - .map(consumer -> (BiConsumer>) (exchange, roots) -> consumer - .accept(roots)) - .toList(); - - McpServerFeatures.Sync syncFeatures = new McpServerFeatures.Sync(this.serverInfo, this.serverCapabilities, - tools, resources, this.resourceTemplates, prompts, rootsChangeHandlers); - - McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures); - var asyncServer = new McpAsyncServer(this.transport, asyncFeatures); - - return new McpSyncServer(asyncServer); - } - - } - } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java index 5aeeadd77..8c110027c 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.java @@ -10,7 +10,6 @@ import java.util.Map; import java.util.function.BiConsumer; import java.util.function.BiFunction; -import java.util.function.Function; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.util.Assert; @@ -423,272 +422,4 @@ public record SyncPromptSpecification(McpSchema.Prompt prompt, BiFunction promptHandler) { } - // --------------------------------------- - // Deprecated registrations - // --------------------------------------- - - /** - * Registration of a tool with its asynchronous handler function. Tools are the - * primary way for MCP servers to expose functionality to AI models. Each tool - * represents a specific capability, such as: - *
    - *
  • Performing calculations - *
  • Accessing external APIs - *
  • Querying databases - *
  • Manipulating files - *
  • Executing system commands - *
- * - *

- * Example tool registration:

{@code
-	 * new McpServerFeatures.AsyncToolRegistration(
-	 *     new Tool(
-	 *         "calculator",
-	 *         "Performs mathematical calculations",
-	 *         new JsonSchemaObject()
-	 *             .required("expression")
-	 *             .property("expression", JsonSchemaType.STRING)
-	 *     ),
-	 *     args -> {
-	 *         String expr = (String) args.get("expression");
-	 *         return Mono.just(new CallToolResult("Result: " + evaluate(expr)));
-	 *     }
-	 * )
-	 * }
- * - * @param tool The tool definition including name, description, and parameter schema - * @param call The function that implements the tool's logic, receiving arguments and - * returning results - * @deprecated This class is deprecated and will be removed in 0.9.0. Use - * {@link AsyncToolSpecification}. - */ - @Deprecated - public record AsyncToolRegistration(McpSchema.Tool tool, - Function, Mono> call) { - - static AsyncToolRegistration fromSync(SyncToolRegistration tool) { - // FIXME: This is temporary, proper validation should be implemented - if (tool == null) { - return null; - } - return new AsyncToolRegistration(tool.tool(), - map -> Mono.fromCallable(() -> tool.call().apply(map)).subscribeOn(Schedulers.boundedElastic())); - } - - public AsyncToolSpecification toSpecification() { - return new AsyncToolSpecification(tool(), (exchange, map) -> call.apply(map)); - } - } - - /** - * Registration of a resource with its asynchronous handler function. Resources - * provide context to AI models by exposing data such as: - *
    - *
  • File contents - *
  • Database records - *
  • API responses - *
  • System information - *
  • Application state - *
- * - *

- * Example resource registration:

{@code
-	 * new McpServerFeatures.AsyncResourceRegistration(
-	 *     new Resource("docs", "Documentation files", "text/markdown"),
-	 *     request -> {
-	 *         String content = readFile(request.getPath());
-	 *         return Mono.just(new ReadResourceResult(content));
-	 *     }
-	 * )
-	 * }
- * - * @param resource The resource definition including name, description, and MIME type - * @param readHandler The function that handles resource read requests - * @deprecated This class is deprecated and will be removed in 0.9.0. Use - * {@link AsyncResourceSpecification}. - */ - @Deprecated - public record AsyncResourceRegistration(McpSchema.Resource resource, - Function> readHandler) { - - static AsyncResourceRegistration fromSync(SyncResourceRegistration resource) { - // FIXME: This is temporary, proper validation should be implemented - if (resource == null) { - return null; - } - return new AsyncResourceRegistration(resource.resource(), - req -> Mono.fromCallable(() -> resource.readHandler().apply(req)) - .subscribeOn(Schedulers.boundedElastic())); - } - - public AsyncResourceSpecification toSpecification() { - return new AsyncResourceSpecification(resource(), (exchange, request) -> readHandler.apply(request)); - } - } - - /** - * Registration of a prompt template with its asynchronous handler function. Prompts - * provide structured templates for AI model interactions, supporting: - *
    - *
  • Consistent message formatting - *
  • Parameter substitution - *
  • Context injection - *
  • Response formatting - *
  • Instruction templating - *
- * - *

- * Example prompt registration:

{@code
-	 * new McpServerFeatures.AsyncPromptRegistration(
-	 *     new Prompt("analyze", "Code analysis template"),
-	 *     request -> {
-	 *         String code = request.getArguments().get("code");
-	 *         return Mono.just(new GetPromptResult(
-	 *             "Analyze this code:\n\n" + code + "\n\nProvide feedback on:"
-	 *         ));
-	 *     }
-	 * )
-	 * }
- * - * @param prompt The prompt definition including name and description - * @param promptHandler The function that processes prompt requests and returns - * formatted templates - * @deprecated This class is deprecated and will be removed in 0.9.0. Use - * {@link AsyncPromptSpecification}. - */ - @Deprecated - public record AsyncPromptRegistration(McpSchema.Prompt prompt, - Function> promptHandler) { - - static AsyncPromptRegistration fromSync(SyncPromptRegistration prompt) { - // FIXME: This is temporary, proper validation should be implemented - if (prompt == null) { - return null; - } - return new AsyncPromptRegistration(prompt.prompt(), - req -> Mono.fromCallable(() -> prompt.promptHandler().apply(req)) - .subscribeOn(Schedulers.boundedElastic())); - } - - public AsyncPromptSpecification toSpecification() { - return new AsyncPromptSpecification(prompt(), (exchange, request) -> promptHandler.apply(request)); - } - } - - /** - * Registration of a tool with its synchronous handler function. Tools are the primary - * way for MCP servers to expose functionality to AI models. Each tool represents a - * specific capability, such as: - *
    - *
  • Performing calculations - *
  • Accessing external APIs - *
  • Querying databases - *
  • Manipulating files - *
  • Executing system commands - *
- * - *

- * Example tool registration:

{@code
-	 * new McpServerFeatures.SyncToolRegistration(
-	 *     new Tool(
-	 *         "calculator",
-	 *         "Performs mathematical calculations",
-	 *         new JsonSchemaObject()
-	 *             .required("expression")
-	 *             .property("expression", JsonSchemaType.STRING)
-	 *     ),
-	 *     args -> {
-	 *         String expr = (String) args.get("expression");
-	 *         return new CallToolResult("Result: " + evaluate(expr));
-	 *     }
-	 * )
-	 * }
- * - * @param tool The tool definition including name, description, and parameter schema - * @param call The function that implements the tool's logic, receiving arguments and - * returning results - * @deprecated This class is deprecated and will be removed in 0.9.0. Use - * {@link SyncToolSpecification}. - */ - @Deprecated - public record SyncToolRegistration(McpSchema.Tool tool, - Function, McpSchema.CallToolResult> call) { - public SyncToolSpecification toSpecification() { - return new SyncToolSpecification(tool, (exchange, map) -> call.apply(map)); - } - } - - /** - * Registration of a resource with its synchronous handler function. Resources provide - * context to AI models by exposing data such as: - *
    - *
  • File contents - *
  • Database records - *
  • API responses - *
  • System information - *
  • Application state - *
- * - *

- * Example resource registration:

{@code
-	 * new McpServerFeatures.SyncResourceRegistration(
-	 *     new Resource("docs", "Documentation files", "text/markdown"),
-	 *     request -> {
-	 *         String content = readFile(request.getPath());
-	 *         return new ReadResourceResult(content);
-	 *     }
-	 * )
-	 * }
- * - * @param resource The resource definition including name, description, and MIME type - * @param readHandler The function that handles resource read requests - * @deprecated This class is deprecated and will be removed in 0.9.0. Use - * {@link SyncResourceSpecification}. - */ - @Deprecated - public record SyncResourceRegistration(McpSchema.Resource resource, - Function readHandler) { - public SyncResourceSpecification toSpecification() { - return new SyncResourceSpecification(resource, (exchange, request) -> readHandler.apply(request)); - } - } - - /** - * Registration of a prompt template with its synchronous handler function. Prompts - * provide structured templates for AI model interactions, supporting: - *
    - *
  • Consistent message formatting - *
  • Parameter substitution - *
  • Context injection - *
  • Response formatting - *
  • Instruction templating - *
- * - *

- * Example prompt registration:

{@code
-	 * new McpServerFeatures.SyncPromptRegistration(
-	 *     new Prompt("analyze", "Code analysis template"),
-	 *     request -> {
-	 *         String code = request.getArguments().get("code");
-	 *         return new GetPromptResult(
-	 *             "Analyze this code:\n\n" + code + "\n\nProvide feedback on:"
-	 *         );
-	 *     }
-	 * )
-	 * }
- * - * @param prompt The prompt definition including name and description - * @param promptHandler The function that processes prompt requests and returns - * formatted templates - * @deprecated This class is deprecated and will be removed in 0.9.0. Use - * {@link SyncPromptSpecification}. - */ - @Deprecated - public record SyncPromptRegistration(McpSchema.Prompt prompt, - Function promptHandler) { - public SyncPromptSpecification toSpecification() { - return new SyncPromptSpecification(prompt, (exchange, request) -> promptHandler.apply(request)); - } - } - } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java index 60662d98d..72eba8b86 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java +++ b/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java @@ -65,40 +65,6 @@ public McpSyncServer(McpAsyncServer asyncServer) { this.asyncServer = asyncServer; } - /** - * Retrieves the list of all roots provided by the client. - * @return The list of roots - * @deprecated This method will be removed in 0.9.0. Use - * {@link McpSyncServerExchange#listRoots()}. - */ - @Deprecated - public McpSchema.ListRootsResult listRoots() { - return this.listRoots(null); - } - - /** - * Retrieves a paginated list of roots provided by the server. - * @param cursor Optional pagination cursor from a previous list request - * @return The list of roots - * @deprecated This method will be removed in 0.9.0. Use - * {@link McpSyncServerExchange#listRoots(String)}. - */ - @Deprecated - public McpSchema.ListRootsResult listRoots(String cursor) { - return this.asyncServer.listRoots(cursor).block(); - } - - /** - * Add a new tool handler. - * @param toolHandler The tool handler to add - * @deprecated This method will be removed in 0.9.0. Use - * {@link #addTool(McpServerFeatures.SyncToolSpecification)}. - */ - @Deprecated - public void addTool(McpServerFeatures.SyncToolRegistration toolHandler) { - this.asyncServer.addTool(McpServerFeatures.AsyncToolRegistration.fromSync(toolHandler)).block(); - } - /** * Add a new tool handler. * @param toolHandler The tool handler to add @@ -115,17 +81,6 @@ public void removeTool(String toolName) { this.asyncServer.removeTool(toolName).block(); } - /** - * Add a new resource handler. - * @param resourceHandler The resource handler to add - * @deprecated This method will be removed in 0.9.0. Use - * {@link #addResource(McpServerFeatures.SyncResourceSpecification)}. - */ - @Deprecated - public void addResource(McpServerFeatures.SyncResourceRegistration resourceHandler) { - this.asyncServer.addResource(McpServerFeatures.AsyncResourceRegistration.fromSync(resourceHandler)).block(); - } - /** * Add a new resource handler. * @param resourceHandler The resource handler to add @@ -142,17 +97,6 @@ public void removeResource(String resourceUri) { this.asyncServer.removeResource(resourceUri).block(); } - /** - * Add a new prompt handler. - * @param promptRegistration The prompt registration to add - * @deprecated This method will be removed in 0.9.0. Use - * {@link #addPrompt(McpServerFeatures.SyncPromptSpecification)}. - */ - @Deprecated - public void addPrompt(McpServerFeatures.SyncPromptRegistration promptRegistration) { - this.asyncServer.addPrompt(McpServerFeatures.AsyncPromptRegistration.fromSync(promptRegistration)).block(); - } - /** * Add a new prompt handler. * @param promptSpecification The prompt specification to add @@ -192,28 +136,6 @@ public McpSchema.Implementation getServerInfo() { return this.asyncServer.getServerInfo(); } - /** - * Get the client capabilities that define the supported features and functionality. - * @return The client capabilities - * @deprecated This method will be removed in 0.9.0. Use - * {@link McpSyncServerExchange#getClientCapabilities()}. - */ - @Deprecated - public ClientCapabilities getClientCapabilities() { - return this.asyncServer.getClientCapabilities(); - } - - /** - * Get the client implementation information. - * @return The client implementation details - * @deprecated This method will be removed in 0.9.0. Use - * {@link McpSyncServerExchange#getClientInfo()}. - */ - @Deprecated - public McpSchema.Implementation getClientInfo() { - return this.asyncServer.getClientInfo(); - } - /** * Notify clients that the list of available resources has changed. */ @@ -258,36 +180,4 @@ public McpAsyncServer getAsyncServer() { return this.asyncServer; } - /** - * Create a new message using the sampling capabilities of the client. The Model - * Context Protocol (MCP) provides a standardized way for servers to request LLM - * sampling ("completions" or "generations") from language models via clients. - * - *

- * This flow allows clients to maintain control over model access, selection, and - * permissions while enabling servers to leverage AI capabilities—with no server API - * keys necessary. Servers can request text or image-based interactions and optionally - * include context from MCP servers in their prompts. - * - *

- * Unlike its async counterpart, this method blocks until the message creation is - * complete, making it easier to use in synchronous code paths. - * @param createMessageRequest The request to create a new message - * @return The result of the message creation - * @throws McpError if the client has not been initialized or does not support - * sampling capabilities - * @throws McpError if the client does not support the createMessage method - * @see McpSchema.CreateMessageRequest - * @see McpSchema.CreateMessageResult - * @see Sampling - * Specification - * @deprecated This method will be removed in 0.9.0. Use - * {@link McpSyncServerExchange#createMessage(McpSchema.CreateMessageRequest)}. - */ - @Deprecated - public McpSchema.CreateMessageResult createMessage(McpSchema.CreateMessageRequest createMessageRequest) { - return this.asyncServer.createMessage(createMessageRequest).block(); - } - } diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransport.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransport.java deleted file mode 100644 index fa5dcf1c1..000000000 --- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransport.java +++ /dev/null @@ -1,419 +0,0 @@ -/* -* Copyright 2024 - 2024 the original author or authors. -*/ -package io.modelcontextprotocol.server.transport; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.PrintWriter; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Function; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.McpError; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import jakarta.servlet.AsyncContext; -import jakarta.servlet.ServletException; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Mono; - -/** - * A Servlet-based implementation of the MCP HTTP with Server-Sent Events (SSE) transport - * specification. This implementation provides similar functionality to - * WebFluxSseServerTransport but uses the traditional Servlet API instead of WebFlux. - * - * @deprecated This class will be removed in 0.9.0. Use - * {@link HttpServletSseServerTransportProvider}. - * - *

- * The transport handles two types of endpoints: - *

    - *
  • SSE endpoint (/sse) - Establishes a long-lived connection for server-to-client - * events
  • - *
  • Message endpoint (configurable) - Handles client-to-server message requests
  • - *
- * - *

- * Features: - *

    - *
  • Asynchronous message handling using Servlet 6.0 async support
  • - *
  • Session management for multiple client connections
  • - *
  • Graceful shutdown support
  • - *
  • Error handling and response formatting
  • - *
- * @author Christian Tzolov - * @author Alexandros Pappas - * @see ServerMcpTransport - * @see HttpServlet - */ - -@WebServlet(asyncSupported = true) -@Deprecated -public class HttpServletSseServerTransport extends HttpServlet implements ServerMcpTransport { - - /** Logger for this class */ - private static final Logger logger = LoggerFactory.getLogger(HttpServletSseServerTransport.class); - - public static final String UTF_8 = "UTF-8"; - - public static final String APPLICATION_JSON = "application/json"; - - public static final String FAILED_TO_SEND_ERROR_RESPONSE = "Failed to send error response: {}"; - - /** Default endpoint path for SSE connections */ - public static final String DEFAULT_SSE_ENDPOINT = "/sse"; - - /** Event type for regular messages */ - public static final String MESSAGE_EVENT_TYPE = "message"; - - /** Event type for endpoint information */ - public static final String ENDPOINT_EVENT_TYPE = "endpoint"; - - /** JSON object mapper for serialization/deserialization */ - private final ObjectMapper objectMapper; - - /** The endpoint path for handling client messages */ - private final String messageEndpoint; - - /** The endpoint path for handling SSE connections */ - private final String sseEndpoint; - - /** Map of active client sessions, keyed by session ID */ - private final Map sessions = new ConcurrentHashMap<>(); - - /** Flag indicating if the transport is in the process of shutting down */ - private final AtomicBoolean isClosing = new AtomicBoolean(false); - - /** Handler for processing incoming messages */ - private Function, Mono> connectHandler; - - /** - * Creates a new HttpServletSseServerTransport instance with a custom SSE endpoint. - * @param objectMapper The JSON object mapper to use for message - * serialization/deserialization - * @param messageEndpoint The endpoint path where clients will send their messages - * @param sseEndpoint The endpoint path where clients will establish SSE connections - */ - public HttpServletSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) { - this.objectMapper = objectMapper; - this.messageEndpoint = messageEndpoint; - this.sseEndpoint = sseEndpoint; - } - - /** - * Creates a new HttpServletSseServerTransport instance with the default SSE endpoint. - * @param objectMapper The JSON object mapper to use for message - * serialization/deserialization - * @param messageEndpoint The endpoint path where clients will send their messages - */ - public HttpServletSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) { - this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT); - } - - /** - * Handles GET requests to establish SSE connections. - *

- * This method sets up a new SSE connection when a client connects to the SSE - * endpoint. It configures the response headers for SSE, creates a new session, and - * sends the initial endpoint information to the client. - * @param request The HTTP servlet request - * @param response The HTTP servlet response - * @throws ServletException If a servlet-specific error occurs - * @throws IOException If an I/O error occurs - */ - @Override - protected void doGet(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - String pathInfo = request.getPathInfo(); - if (!sseEndpoint.equals(pathInfo)) { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - if (isClosing.get()) { - response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Server is shutting down"); - return; - } - - response.setContentType("text/event-stream"); - response.setCharacterEncoding(UTF_8); - response.setHeader("Cache-Control", "no-cache"); - response.setHeader("Connection", "keep-alive"); - response.setHeader("Access-Control-Allow-Origin", "*"); - - String sessionId = UUID.randomUUID().toString(); - AsyncContext asyncContext = request.startAsync(); - asyncContext.setTimeout(0); - - PrintWriter writer = response.getWriter(); - ClientSession session = new ClientSession(sessionId, asyncContext, writer); - this.sessions.put(sessionId, session); - - // Send initial endpoint event - this.sendEvent(writer, ENDPOINT_EVENT_TYPE, messageEndpoint); - } - - /** - * Handles POST requests for client messages. - *

- * This method processes incoming messages from clients, routes them through the - * connect handler if configured, and sends back the appropriate response. It handles - * error cases and formats error responses according to the MCP specification. - * @param request The HTTP servlet request - * @param response The HTTP servlet response - * @throws ServletException If a servlet-specific error occurs - * @throws IOException If an I/O error occurs - */ - @Override - protected void doPost(HttpServletRequest request, HttpServletResponse response) - throws ServletException, IOException { - - if (isClosing.get()) { - response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Server is shutting down"); - return; - } - - String pathInfo = request.getPathInfo(); - if (!messageEndpoint.equals(pathInfo)) { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - return; - } - - try { - BufferedReader reader = request.getReader(); - StringBuilder body = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) { - body.append(line); - } - - McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body.toString()); - - if (connectHandler != null) { - connectHandler.apply(Mono.just(message)).subscribe(responseMessage -> { - try { - response.setContentType(APPLICATION_JSON); - response.setCharacterEncoding(UTF_8); - String jsonResponse = objectMapper.writeValueAsString(responseMessage); - PrintWriter writer = response.getWriter(); - writer.write(jsonResponse); - writer.flush(); - } - catch (Exception e) { - logger.error("Error sending response: {}", e.getMessage()); - try { - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, - "Error processing response: " + e.getMessage()); - } - catch (IOException ex) { - logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage()); - } - } - }, error -> { - try { - logger.error("Error processing message: {}", error.getMessage()); - McpError mcpError = new McpError(error.getMessage()); - response.setContentType(APPLICATION_JSON); - response.setCharacterEncoding(UTF_8); - response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - String jsonError = objectMapper.writeValueAsString(mcpError); - PrintWriter writer = response.getWriter(); - writer.write(jsonError); - writer.flush(); - } - catch (IOException e) { - logger.error(FAILED_TO_SEND_ERROR_RESPONSE, e.getMessage()); - try { - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, - "Error sending error response: " + e.getMessage()); - } - catch (IOException ex) { - logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage()); - } - } - }); - } - else { - response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "No message handler configured"); - } - } - catch (Exception e) { - logger.error("Invalid message format: {}", e.getMessage()); - try { - McpError mcpError = new McpError("Invalid message format: " + e.getMessage()); - response.setContentType(APPLICATION_JSON); - response.setCharacterEncoding(UTF_8); - response.setStatus(HttpServletResponse.SC_BAD_REQUEST); - String jsonError = objectMapper.writeValueAsString(mcpError); - PrintWriter writer = response.getWriter(); - writer.write(jsonError); - writer.flush(); - } - catch (IOException ex) { - logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage()); - response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid message format"); - } - } - } - - /** - * Sets up the message handler for processing client requests. - * @param handler The function to process incoming messages and produce responses - * @return A Mono that completes when the handler is set up - */ - @Override - public Mono connect(Function, Mono> handler) { - this.connectHandler = handler; - return Mono.empty(); - } - - /** - * Broadcasts a message to all connected clients. - *

- * This method serializes the message and sends it to all active client sessions. If a - * client is disconnected, its session is removed. - * @param message The message to broadcast - * @return A Mono that completes when the message has been sent to all clients - */ - @Override - public Mono sendMessage(McpSchema.JSONRPCMessage message) { - if (sessions.isEmpty()) { - logger.debug("No active sessions to broadcast message to"); - return Mono.empty(); - } - - return Mono.create(sink -> { - try { - String jsonText = objectMapper.writeValueAsString(message); - - sessions.values().forEach(session -> { - try { - this.sendEvent(session.writer, MESSAGE_EVENT_TYPE, jsonText); - } - catch (IOException e) { - logger.error("Failed to send message to session {}: {}", session.id, e.getMessage()); - removeSession(session); - } - }); - - sink.success(); - } - catch (Exception e) { - logger.error("Failed to process message: {}", e.getMessage()); - sink.error(new McpError("Failed to process message: " + e.getMessage())); - } - }); - } - - /** - * Closes the transport. - *

- * This implementation delegates to the super class's close method. - */ - @Override - public void close() { - ServerMcpTransport.super.close(); - } - - /** - * Unmarshals data from one type to another using the object mapper. - * @param The target type - * @param data The source data - * @param typeRef The type reference for the target type - * @return The unmarshaled data - */ - @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return objectMapper.convertValue(data, typeRef); - } - - /** - * Initiates a graceful shutdown of the transport. - *

- * This method marks the transport as closing and closes all active client sessions. - * New connection attempts will be rejected during shutdown. - * @return A Mono that completes when all sessions have been closed - */ - @Override - public Mono closeGracefully() { - isClosing.set(true); - logger.debug("Initiating graceful shutdown with {} active sessions", sessions.size()); - - return Mono.create(sink -> { - sessions.values().forEach(this::removeSession); - sink.success(); - }); - } - - /** - * Sends an SSE event to a client. - * @param writer The writer to send the event through - * @param eventType The type of event (message or endpoint) - * @param data The event data - * @throws IOException If an error occurs while writing the event - */ - private void sendEvent(PrintWriter writer, String eventType, String data) throws IOException { - writer.write("event: " + eventType + "\n"); - writer.write("data: " + data + "\n\n"); - writer.flush(); - - if (writer.checkError()) { - throw new IOException("Client disconnected"); - } - } - - /** - * Removes a client session and completes its async context. - * @param session The session to remove - */ - private void removeSession(ClientSession session) { - sessions.remove(session.id); - session.asyncContext.complete(); - } - - /** - * Represents a client connection session. - *

- * This class holds the necessary information about a client's SSE connection, - * including its ID, async context, and output writer. - */ - private static class ClientSession { - - private final String id; - - private final AsyncContext asyncContext; - - private final PrintWriter writer; - - ClientSession(String id, AsyncContext asyncContext, PrintWriter writer) { - this.id = id; - this.asyncContext = asyncContext; - this.writer = writer; - } - - } - - /** - * Cleans up resources when the servlet is being destroyed. - *

- * This method ensures a graceful shutdown by closing all client connections before - * calling the parent's destroy method. - */ - @Override - public void destroy() { - closeGracefully().block(); - super.destroy(); - } - -} diff --git a/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransport.java b/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransport.java deleted file mode 100644 index 78264ca32..000000000 --- a/mcp/src/main/java/io/modelcontextprotocol/server/transport/StdioServerTransport.java +++ /dev/null @@ -1,259 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.server.transport; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.nio.charset.StandardCharsets; -import java.util.concurrent.Executors; -import java.util.function.Function; - -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import io.modelcontextprotocol.util.Assert; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.Sinks; -import reactor.core.scheduler.Scheduler; -import reactor.core.scheduler.Schedulers; - -/** - * Implementation of the MCP Stdio transport for servers that communicates using standard - * input/output streams. Messages are exchanged as newline-delimited JSON-RPC messages - * over stdin/stdout, with errors and debug information sent to stderr. - * - * @author Christian Tzolov - * @deprecated This method will be removed in 0.9.0. Use - * {@link io.modelcontextprotocol.server.transport.StdioServerTransportProvider} instead. - */ -@Deprecated -public class StdioServerTransport implements ServerMcpTransport { - - private static final Logger logger = LoggerFactory.getLogger(StdioServerTransport.class); - - private final Sinks.Many inboundSink; - - private final Sinks.Many outboundSink; - - private ObjectMapper objectMapper; - - /** Scheduler for handling inbound messages */ - private Scheduler inboundScheduler; - - /** Scheduler for handling outbound messages */ - private Scheduler outboundScheduler; - - private volatile boolean isClosing = false; - - private final InputStream inputStream; - - private final OutputStream outputStream; - - private final Sinks.One inboundReady = Sinks.one(); - - private final Sinks.One outboundReady = Sinks.one(); - - /** - * Creates a new StdioServerTransport with a default ObjectMapper and System streams. - */ - public StdioServerTransport() { - this(new ObjectMapper()); - } - - /** - * Creates a new StdioServerTransport with the specified ObjectMapper and System - * streams. - * @param objectMapper The ObjectMapper to use for JSON serialization/deserialization - */ - public StdioServerTransport(ObjectMapper objectMapper) { - - Assert.notNull(objectMapper, "The ObjectMapper can not be null"); - - this.inboundSink = Sinks.many().unicast().onBackpressureBuffer(); - this.outboundSink = Sinks.many().unicast().onBackpressureBuffer(); - - this.objectMapper = objectMapper; - this.inputStream = System.in; - this.outputStream = System.out; - - // Use bounded schedulers for better resource management - this.inboundScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "inbound"); - this.outboundScheduler = Schedulers.fromExecutorService(Executors.newSingleThreadExecutor(), "outbound"); - } - - @Override - public Mono connect(Function, Mono> handler) { - return Mono.fromRunnable(() -> { - handleIncomingMessages(handler); - - // Start threads - startInboundProcessing(); - startOutboundProcessing(); - }).subscribeOn(Schedulers.boundedElastic()); - } - - private void handleIncomingMessages(Function, Mono> inboundMessageHandler) { - this.inboundSink.asFlux() - .flatMap(message -> Mono.just(message) - .transform(inboundMessageHandler) - .contextWrite(ctx -> ctx.put("observation", "myObservation"))) - .doOnTerminate(() -> { - // The outbound processing will dispose its scheduler upon completion - this.outboundSink.tryEmitComplete(); - this.inboundScheduler.dispose(); - }) - .subscribe(); - } - - @Override - public Mono sendMessage(JSONRPCMessage message) { - return Mono.zip(inboundReady.asMono(), outboundReady.asMono()).then(Mono.defer(() -> { - if (this.outboundSink.tryEmitNext(message).isSuccess()) { - return Mono.empty(); - } - else { - return Mono.error(new RuntimeException("Failed to enqueue message")); - } - })); - } - - /** - * Starts the inbound processing thread that reads JSON-RPC messages from stdin. - * Messages are deserialized and emitted to the inbound sink. - */ - private void startInboundProcessing() { - this.inboundScheduler.schedule(() -> { - inboundReady.tryEmitValue(null); - BufferedReader reader = null; - try { - reader = new BufferedReader(new InputStreamReader(inputStream)); - while (!isClosing) { - try { - String line = reader.readLine(); - if (line == null || isClosing) { - break; - } - - logger.debug("Received JSON message: {}", line); - - try { - JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(this.objectMapper, line); - if (!this.inboundSink.tryEmitNext(message).isSuccess()) { - logIfNotClosing("Failed to enqueue message"); - break; - } - } - catch (Exception e) { - logIfNotClosing("Error processing inbound message", e); - break; - } - } - catch (IOException e) { - logIfNotClosing("Error reading from stdin", e); - break; - } - } - } - catch (Exception e) { - logIfNotClosing("Error in inbound processing", e); - } - finally { - isClosing = true; - inboundSink.tryEmitComplete(); - } - }); - } - - /** - * Starts the outbound processing thread that writes JSON-RPC messages to stdout. - * Messages are serialized to JSON and written with a newline delimiter. - */ - private void startOutboundProcessing() { - Function, Flux> outboundConsumer = messages -> messages // @formatter:off - .doOnSubscribe(subscription -> outboundReady.tryEmitValue(null)) - .publishOn(outboundScheduler) - .handle((message, sink) -> { - if (message != null && !isClosing) { - try { - String jsonMessage = objectMapper.writeValueAsString(message); - // Escape any embedded newlines in the JSON message as per spec - jsonMessage = jsonMessage.replace("\r\n", "\\n").replace("\n", "\\n").replace("\r", "\\n"); - - synchronized (outputStream) { - outputStream.write(jsonMessage.getBytes(StandardCharsets.UTF_8)); - outputStream.write("\n".getBytes(StandardCharsets.UTF_8)); - outputStream.flush(); - } - sink.next(message); - } - catch (IOException e) { - if (!isClosing) { - logger.error("Error writing message", e); - sink.error(new RuntimeException(e)); - } - else { - logger.debug("Stream closed during shutdown", e); - } - } - } - else if (isClosing) { - sink.complete(); - } - }) - .doOnComplete(() -> { - isClosing = true; - outboundScheduler.dispose(); - }) - .doOnError(e -> { - if (!isClosing) { - logger.error("Error in outbound processing", e); - isClosing = true; - outboundScheduler.dispose(); - } - }) - .map(msg -> (JSONRPCMessage) msg); - - outboundConsumer.apply(outboundSink.asFlux()).subscribe(); - } // @formatter:on - - @Override - public Mono closeGracefully() { - return Mono.defer(() -> { - isClosing = true; - logger.debug("Initiating graceful shutdown"); - // Completing the inbound causes the outbound to be completed as well, so - // we only close the inbound. - inboundSink.tryEmitComplete(); - logger.debug("Graceful shutdown complete"); - return Mono.empty(); - }).subscribeOn(Schedulers.boundedElastic()); - } - - @Override - public T unmarshalFrom(Object data, TypeReference typeRef) { - return this.objectMapper.convertValue(data, typeRef); - } - - private void logIfNotClosing(String message, Exception e) { - if (!this.isClosing) { - logger.error(message, e); - } - } - - private void logIfNotClosing(String message) { - if (!this.isClosing) { - logger.error(message); - } - } - -} diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/ClientMcpTransport.java b/mcp/src/main/java/io/modelcontextprotocol/spec/ClientMcpTransport.java deleted file mode 100644 index 8464b6ae7..000000000 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/ClientMcpTransport.java +++ /dev/null @@ -1,15 +0,0 @@ -/* -* Copyright 2024 - 2024 the original author or authors. -*/ -package io.modelcontextprotocol.spec; - -/** - * Marker interface for the client-side MCP transport. - * - * @author Christian Tzolov - * @deprecated This class will be removed in 0.9.0. Use {@link McpClientTransport}. - */ -@Deprecated -public interface ClientMcpTransport extends McpTransport { - -} diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/DefaultMcpSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/DefaultMcpSession.java deleted file mode 100644 index 83de4c094..000000000 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/DefaultMcpSession.java +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.spec; - -import java.time.Duration; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.atomic.AtomicLong; - -import com.fasterxml.jackson.core.type.TypeReference; -import io.modelcontextprotocol.util.Assert; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import reactor.core.Disposable; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoSink; - -/** - * Default implementation of the MCP (Model Context Protocol) session that manages - * bidirectional JSON-RPC communication between clients and servers. This implementation - * follows the MCP specification for message exchange and transport handling. - * - *

- * The session manages: - *

    - *
  • Request/response handling with unique message IDs
  • - *
  • Notification processing
  • - *
  • Message timeout management
  • - *
  • Transport layer abstraction
  • - *
- * - * @author Christian Tzolov - * @author Dariusz Jędrzejczyk - * @deprecated This method will be removed in 0.9.0. Use {@link McpClientSession} instead - */ -@Deprecated - -public class DefaultMcpSession implements McpSession { - - /** Logger for this class */ - private static final Logger logger = LoggerFactory.getLogger(DefaultMcpSession.class); - - /** Duration to wait for request responses before timing out */ - private final Duration requestTimeout; - - /** Transport layer implementation for message exchange */ - private final McpTransport transport; - - /** Map of pending responses keyed by request ID */ - private final ConcurrentHashMap> pendingResponses = new ConcurrentHashMap<>(); - - /** Map of request handlers keyed by method name */ - private final ConcurrentHashMap> requestHandlers = new ConcurrentHashMap<>(); - - /** Map of notification handlers keyed by method name */ - private final ConcurrentHashMap notificationHandlers = new ConcurrentHashMap<>(); - - /** Session-specific prefix for request IDs */ - private final String sessionPrefix = UUID.randomUUID().toString().substring(0, 8); - - /** Atomic counter for generating unique request IDs */ - private final AtomicLong requestCounter = new AtomicLong(0); - - private final Disposable connection; - - /** - * Functional interface for handling incoming JSON-RPC requests. Implementations - * should process the request parameters and return a response. - * - * @param Response type - */ - @FunctionalInterface - public interface RequestHandler { - - /** - * Handles an incoming request with the given parameters. - * @param params The request parameters - * @return A Mono containing the response object - */ - Mono handle(Object params); - - } - - /** - * Functional interface for handling incoming JSON-RPC notifications. Implementations - * should process the notification parameters without returning a response. - */ - @FunctionalInterface - public interface NotificationHandler { - - /** - * Handles an incoming notification with the given parameters. - * @param params The notification parameters - * @return A Mono that completes when the notification is processed - */ - Mono handle(Object params); - - } - - /** - * Creates a new DefaultMcpSession with the specified configuration and handlers. - * @param requestTimeout Duration to wait for responses - * @param transport Transport implementation for message exchange - * @param requestHandlers Map of method names to request handlers - * @param notificationHandlers Map of method names to notification handlers - */ - public DefaultMcpSession(Duration requestTimeout, McpTransport transport, - Map> requestHandlers, Map notificationHandlers) { - - Assert.notNull(requestTimeout, "The requstTimeout can not be null"); - Assert.notNull(transport, "The transport can not be null"); - Assert.notNull(requestHandlers, "The requestHandlers can not be null"); - Assert.notNull(notificationHandlers, "The notificationHandlers can not be null"); - - this.requestTimeout = requestTimeout; - this.transport = transport; - this.requestHandlers.putAll(requestHandlers); - this.notificationHandlers.putAll(notificationHandlers); - - // TODO: consider mono.transformDeferredContextual where the Context contains - // the - // Observation associated with the individual message - it can be used to - // create child Observation and emit it together with the message to the - // consumer - this.connection = this.transport.connect(mono -> mono.doOnNext(message -> { - if (message instanceof McpSchema.JSONRPCResponse response) { - logger.debug("Received Response: {}", response); - var sink = pendingResponses.remove(response.id()); - if (sink == null) { - logger.warn("Unexpected response for unkown id {}", response.id()); - } - else { - sink.success(response); - } - } - else if (message instanceof McpSchema.JSONRPCRequest request) { - logger.debug("Received request: {}", request); - handleIncomingRequest(request).subscribe(response -> transport.sendMessage(response).subscribe(), - error -> { - var errorResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), - null, new McpSchema.JSONRPCResponse.JSONRPCError( - McpSchema.ErrorCodes.INTERNAL_ERROR, error.getMessage(), null)); - transport.sendMessage(errorResponse).subscribe(); - }); - } - else if (message instanceof McpSchema.JSONRPCNotification notification) { - logger.debug("Received notification: {}", notification); - handleIncomingNotification(notification).subscribe(null, - error -> logger.error("Error handling notification: {}", error.getMessage())); - } - })).subscribe(); - } - - /** - * Handles an incoming JSON-RPC request by routing it to the appropriate handler. - * @param request The incoming JSON-RPC request - * @return A Mono containing the JSON-RPC response - */ - private Mono handleIncomingRequest(McpSchema.JSONRPCRequest request) { - return Mono.defer(() -> { - var handler = this.requestHandlers.get(request.method()); - if (handler == null) { - MethodNotFoundError error = getMethodNotFoundError(request.method()); - return Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null, - new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND, - error.message(), error.data()))); - } - - return handler.handle(request.params()) - .map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null)) - .onErrorResume(error -> Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), - null, new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR, - error.getMessage(), null)))); // TODO: add error message - // through the data field - }); - } - - record MethodNotFoundError(String method, String message, Object data) { - } - - public static MethodNotFoundError getMethodNotFoundError(String method) { - switch (method) { - case McpSchema.METHOD_ROOTS_LIST: - return new MethodNotFoundError(method, "Roots not supported", - Map.of("reason", "Client does not have roots capability")); - default: - return new MethodNotFoundError(method, "Method not found: " + method, null); - } - } - - /** - * Handles an incoming JSON-RPC notification by routing it to the appropriate handler. - * @param notification The incoming JSON-RPC notification - * @return A Mono that completes when the notification is processed - */ - private Mono handleIncomingNotification(McpSchema.JSONRPCNotification notification) { - return Mono.defer(() -> { - var handler = notificationHandlers.get(notification.method()); - if (handler == null) { - logger.error("No handler registered for notification method: {}", notification.method()); - return Mono.empty(); - } - return handler.handle(notification.params()); - }); - } - - /** - * Generates a unique request ID in a non-blocking way. Combines a session-specific - * prefix with an atomic counter to ensure uniqueness. - * @return A unique request ID string - */ - private String generateRequestId() { - return this.sessionPrefix + "-" + this.requestCounter.getAndIncrement(); - } - - /** - * Sends a JSON-RPC request and returns the response. - * @param The expected response type - * @param method The method name to call - * @param requestParams The request parameters - * @param typeRef Type reference for response deserialization - * @return A Mono containing the response - */ - @Override - public Mono sendRequest(String method, Object requestParams, TypeReference typeRef) { - String requestId = this.generateRequestId(); - - return Mono.create(sink -> { - this.pendingResponses.put(requestId, sink); - McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest(McpSchema.JSONRPC_VERSION, method, - requestId, requestParams); - this.transport.sendMessage(jsonrpcRequest) - // TODO: It's most efficient to create a dedicated Subscriber here - .subscribe(v -> { - }, error -> { - this.pendingResponses.remove(requestId); - sink.error(error); - }); - }).timeout(this.requestTimeout).handle((jsonRpcResponse, sink) -> { - if (jsonRpcResponse.error() != null) { - sink.error(new McpError(jsonRpcResponse.error())); - } - else { - if (typeRef.getType().equals(Void.class)) { - sink.complete(); - } - else { - sink.next(this.transport.unmarshalFrom(jsonRpcResponse.result(), typeRef)); - } - } - }); - } - - /** - * Sends a JSON-RPC notification. - * @param method The method name for the notification - * @param params The notification parameters - * @return A Mono that completes when the notification is sent - */ - @Override - public Mono sendNotification(String method, Map params) { - McpSchema.JSONRPCNotification jsonrpcNotification = new McpSchema.JSONRPCNotification(McpSchema.JSONRPC_VERSION, - method, params); - return this.transport.sendMessage(jsonrpcNotification); - } - - /** - * Closes the session gracefully, allowing pending operations to complete. - * @return A Mono that completes when the session is closed - */ - @Override - public Mono closeGracefully() { - return Mono.defer(() -> { - this.connection.dispose(); - return transport.closeGracefully(); - }); - } - - /** - * Closes the session immediately, potentially interrupting pending operations. - */ - @Override - public void close() { - this.connection.dispose(); - transport.close(); - } - -} diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java index 6657e3622..e29646e6a 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientSession.java @@ -44,7 +44,7 @@ public class McpClientSession implements McpSession { private final Duration requestTimeout; /** Transport layer implementation for message exchange */ - private final McpTransport transport; + private final McpClientTransport transport; /** Map of pending responses keyed by request ID */ private final ConcurrentHashMap> pendingResponses = new ConcurrentHashMap<>(); @@ -104,7 +104,7 @@ public interface NotificationHandler { * @param requestHandlers Map of method names to request handlers * @param notificationHandlers Map of method names to notification handlers */ - public McpClientSession(Duration requestTimeout, McpTransport transport, + public McpClientSession(Duration requestTimeout, McpClientTransport transport, Map> requestHandlers, Map notificationHandlers) { Assert.notNull(requestTimeout, "The requstTimeout can not be null"); diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java index 458979651..f29091248 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpClientTransport.java @@ -13,9 +13,8 @@ * @author Christian Tzolov * @author Dariusz Jędrzejczyk */ -public interface McpClientTransport extends ClientMcpTransport { +public interface McpClientTransport extends McpTransport { - @Override Mono connect(Function, Mono> handler); } diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java b/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java index f698d8789..40d9ba7ac 100644 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java +++ b/mcp/src/main/java/io/modelcontextprotocol/spec/McpTransport.java @@ -4,8 +4,6 @@ package io.modelcontextprotocol.spec; -import java.util.function.Function; - import com.fasterxml.jackson.core.type.TypeReference; import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage; import reactor.core.publisher.Mono; @@ -39,21 +37,6 @@ */ public interface McpTransport { - /** - * Initializes and starts the transport connection. - * - *

- * This method should be called before any message exchange can occur. It sets up the - * necessary resources and establishes the connection to the server. - *

- * @deprecated This is only relevant for client-side transports and will be removed - * from this interface in 0.9.0. - */ - @Deprecated - default Mono connect(Function, Mono> handler) { - return Mono.empty(); - } - /** * Closes the transport connection and releases any associated resources. * diff --git a/mcp/src/main/java/io/modelcontextprotocol/spec/ServerMcpTransport.java b/mcp/src/main/java/io/modelcontextprotocol/spec/ServerMcpTransport.java deleted file mode 100644 index 704daee0f..000000000 --- a/mcp/src/main/java/io/modelcontextprotocol/spec/ServerMcpTransport.java +++ /dev/null @@ -1,15 +0,0 @@ -/* -* Copyright 2024 - 2024 the original author or authors. -*/ -package io.modelcontextprotocol.spec; - -/** - * Marker interface for the server-side MCP transport. - * - * @author Christian Tzolov - * @deprecated This class will be removed in 0.9.0. Use {@link McpServerTransport}. - */ -@Deprecated -public interface ServerMcpTransport extends McpTransport { - -} diff --git a/mcp/src/test/java/io/modelcontextprotocol/MockMcpTransport.java b/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java similarity index 84% rename from mcp/src/test/java/io/modelcontextprotocol/MockMcpTransport.java rename to mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java index 12f30d12f..482d0aac6 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/MockMcpTransport.java +++ b/mcp/src/test/java/io/modelcontextprotocol/MockMcpClientTransport.java @@ -13,30 +13,28 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.modelcontextprotocol.spec.McpClientTransport; import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.ServerMcpTransport; import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification; import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; /** - * A mock implementation of the {@link McpClientTransport} and {@link ServerMcpTransport} - * interfaces. + * A mock implementation of the {@link McpClientTransport} interfaces. */ -public class MockMcpTransport implements McpClientTransport, ServerMcpTransport { +public class MockMcpClientTransport implements McpClientTransport { private final Sinks.Many inbound = Sinks.many().unicast().onBackpressureBuffer(); private final List sent = new ArrayList<>(); - private final BiConsumer interceptor; + private final BiConsumer interceptor; - public MockMcpTransport() { + public MockMcpClientTransport() { this((t, msg) -> { }); } - public MockMcpTransport(BiConsumer interceptor) { + public MockMcpClientTransport(BiConsumer interceptor) { this.interceptor = interceptor; } diff --git a/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java b/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java new file mode 100644 index 000000000..4be680e11 --- /dev/null +++ b/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransport.java @@ -0,0 +1,66 @@ +/* + * Copyright 2024-2024 the original author or authors. + */ + +package io.modelcontextprotocol; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.BiConsumer; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.JSONRPCNotification; +import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; +import io.modelcontextprotocol.spec.McpServerTransport; +import reactor.core.publisher.Mono; + +/** + * A mock implementation of the {@link McpServerTransport} interfaces. + */ +public class MockMcpServerTransport implements McpServerTransport { + + private final List sent = new ArrayList<>(); + + private final BiConsumer interceptor; + + public MockMcpServerTransport() { + this((t, msg) -> { + }); + } + + public MockMcpServerTransport(BiConsumer interceptor) { + this.interceptor = interceptor; + } + + @Override + public Mono sendMessage(McpSchema.JSONRPCMessage message) { + sent.add(message); + interceptor.accept(this, message); + return Mono.empty(); + } + + public McpSchema.JSONRPCRequest getLastSentMessageAsRequest() { + return (JSONRPCRequest) getLastSentMessage(); + } + + public McpSchema.JSONRPCNotification getLastSentMessageAsNotification() { + return (JSONRPCNotification) getLastSentMessage(); + } + + public McpSchema.JSONRPCMessage getLastSentMessage() { + return !sent.isEmpty() ? sent.get(sent.size() - 1) : null; + } + + @Override + public Mono closeGracefully() { + return Mono.empty(); + } + + @Override + public T unmarshalFrom(Object data, TypeReference typeRef) { + return new ObjectMapper().convertValue(data, typeRef); + } + +} diff --git a/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java b/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java new file mode 100644 index 000000000..3fb19180b --- /dev/null +++ b/mcp/src/test/java/io/modelcontextprotocol/MockMcpServerTransportProvider.java @@ -0,0 +1,63 @@ +/* +* Copyright 2025 - 2025 the original author or authors. +* +* Licensed under the Apache License, Version 2.0 (the "License"); +* you may not use this file except in compliance with the License. +* You may obtain a copy of the License at +* +* https://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +*/ +package io.modelcontextprotocol; + +import java.util.Map; + +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpServerSession; +import io.modelcontextprotocol.spec.McpServerSession.Factory; +import io.modelcontextprotocol.spec.McpServerTransportProvider; +import reactor.core.publisher.Mono; + +/** + * @author Christian Tzolov + */ +public class MockMcpServerTransportProvider implements McpServerTransportProvider { + + private McpServerSession session; + + private final MockMcpServerTransport transport; + + public MockMcpServerTransportProvider(MockMcpServerTransport transport) { + this.transport = transport; + } + + public MockMcpServerTransport getTransport() { + return transport; + } + + @Override + public void setSessionFactory(Factory sessionFactory) { + + session = sessionFactory.create(transport); + } + + @Override + public Mono notifyClients(String method, Map params) { + return session.sendNotification(method, params); + } + + @Override + public Mono closeGracefully() { + return session.closeGracefully(); + } + + public void simulateIncomingMessage(McpSchema.JSONRPCMessage message) { + session.handle(message).subscribe(); + } + +} diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java index b1e82b748..4510b1529 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/McpAsyncClientResponseHandlerTests.java @@ -12,7 +12,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.MockMcpTransport; +import io.modelcontextprotocol.MockMcpClientTransport; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities; @@ -34,16 +34,16 @@ class McpAsyncClientResponseHandlerTests { .resources(true, true) // Enable both resources and resource templates .build(); - private static MockMcpTransport initializationEnabledTransport() { + private static MockMcpClientTransport initializationEnabledTransport() { return initializationEnabledTransport(SERVER_CAPABILITIES, SERVER_INFO); } - private static MockMcpTransport initializationEnabledTransport(McpSchema.ServerCapabilities mockServerCapabilities, - McpSchema.Implementation mockServerInfo) { + private static MockMcpClientTransport initializationEnabledTransport( + McpSchema.ServerCapabilities mockServerCapabilities, McpSchema.Implementation mockServerInfo) { McpSchema.InitializeResult mockInitResult = new McpSchema.InitializeResult(McpSchema.LATEST_PROTOCOL_VERSION, mockServerCapabilities, mockServerInfo, "Test instructions"); - return new MockMcpTransport((t, message) -> { + return new MockMcpClientTransport((t, message) -> { if (message instanceof McpSchema.JSONRPCRequest r && METHOD_INITIALIZE.equals(r.method())) { McpSchema.JSONRPCResponse initResponse = new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, r.id(), mockInitResult, null); @@ -59,7 +59,7 @@ void testSuccessfulInitialization() { .tools(false) .resources(true, true) // Enable both resources and resource templates .build(); - MockMcpTransport transport = initializationEnabledTransport(serverCapabilities, serverInfo); + MockMcpClientTransport transport = initializationEnabledTransport(serverCapabilities, serverInfo); McpAsyncClient asyncMcpClient = McpClient.async(transport).build(); // Verify client is not initialized initially @@ -91,7 +91,7 @@ void testSuccessfulInitialization() { @Test void testToolsChangeNotificationHandling() throws JsonProcessingException { - MockMcpTransport transport = initializationEnabledTransport(); + MockMcpClientTransport transport = initializationEnabledTransport(); // Create a list to store received tools for verification List receivedTools = new ArrayList<>(); @@ -134,7 +134,7 @@ void testToolsChangeNotificationHandling() throws JsonProcessingException { @Test void testRootsListRequestHandling() { - MockMcpTransport transport = initializationEnabledTransport(); + MockMcpClientTransport transport = initializationEnabledTransport(); McpAsyncClient asyncMcpClient = McpClient.async(transport) .roots(new Root("file:///test/path", "test-root")) @@ -162,7 +162,7 @@ void testRootsListRequestHandling() { @Test void testResourcesChangeNotificationHandling() { - MockMcpTransport transport = initializationEnabledTransport(); + MockMcpClientTransport transport = initializationEnabledTransport(); // Create a list to store received resources for verification List receivedResources = new ArrayList<>(); @@ -208,7 +208,7 @@ void testResourcesChangeNotificationHandling() { @Test void testPromptsChangeNotificationHandling() { - MockMcpTransport transport = initializationEnabledTransport(); + MockMcpClientTransport transport = initializationEnabledTransport(); // Create a list to store received prompts for verification List receivedPrompts = new ArrayList<>(); @@ -252,7 +252,7 @@ void testPromptsChangeNotificationHandling() { @Test void testSamplingCreateMessageRequestHandling() { - MockMcpTransport transport = initializationEnabledTransport(); + MockMcpClientTransport transport = initializationEnabledTransport(); // Create a test sampling handler that echoes back the input Function> samplingHandler = request -> { @@ -306,7 +306,7 @@ void testSamplingCreateMessageRequestHandling() { @Test void testSamplingCreateMessageRequestHandlingWithoutCapability() { - MockMcpTransport transport = initializationEnabledTransport(); + MockMcpClientTransport transport = initializationEnabledTransport(); // Create client without sampling capability McpAsyncClient asyncMcpClient = McpClient.async(transport) @@ -340,7 +340,7 @@ void testSamplingCreateMessageRequestHandlingWithoutCapability() { @Test void testSamplingCreateMessageRequestHandlingWithNullHandler() { - MockMcpTransport transport = new MockMcpTransport(); + MockMcpClientTransport transport = new MockMcpClientTransport(); // Create client with sampling capability but null handler assertThatThrownBy( diff --git a/mcp/src/test/java/io/modelcontextprotocol/client/McpClientProtocolVersionTests.java b/mcp/src/test/java/io/modelcontextprotocol/client/McpClientProtocolVersionTests.java index 58e486e19..bf4738496 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/client/McpClientProtocolVersionTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/client/McpClientProtocolVersionTests.java @@ -7,7 +7,7 @@ import java.time.Duration; import java.util.List; -import io.modelcontextprotocol.MockMcpTransport; +import io.modelcontextprotocol.MockMcpClientTransport; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.InitializeResult; @@ -28,7 +28,7 @@ class McpClientProtocolVersionTests { @Test void shouldUseLatestVersionByDefault() { - MockMcpTransport transport = new MockMcpTransport(); + MockMcpClientTransport transport = new MockMcpClientTransport(); McpAsyncClient client = McpClient.async(transport) .clientInfo(CLIENT_INFO) .requestTimeout(REQUEST_TIMEOUT) @@ -61,7 +61,7 @@ void shouldUseLatestVersionByDefault() { @Test void shouldNegotiateSpecificVersion() { String oldVersion = "0.1.0"; - MockMcpTransport transport = new MockMcpTransport(); + MockMcpClientTransport transport = new MockMcpClientTransport(); McpAsyncClient client = McpClient.async(transport) .clientInfo(CLIENT_INFO) .requestTimeout(REQUEST_TIMEOUT) @@ -94,7 +94,7 @@ void shouldNegotiateSpecificVersion() { @Test void shouldFailForUnsupportedVersion() { String unsupportedVersion = "999.999.999"; - MockMcpTransport transport = new MockMcpTransport(); + MockMcpClientTransport transport = new MockMcpClientTransport(); McpAsyncClient client = McpClient.async(transport) .clientInfo(CLIENT_INFO) .requestTimeout(REQUEST_TIMEOUT) @@ -124,7 +124,7 @@ void shouldUseHighestVersionWhenMultipleSupported() { String middleVersion = "0.2.0"; String latestVersion = McpSchema.LATEST_PROTOCOL_VERSION; - MockMcpTransport transport = new MockMcpTransport(); + MockMcpClientTransport transport = new MockMcpClientTransport(); McpAsyncClient client = McpClient.async(transport) .clientInfo(CLIENT_INFO) .requestTimeout(REQUEST_TIMEOUT) diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerDeprecatedTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerDeprecatedTests.java deleted file mode 100644 index b9a19de6c..000000000 --- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpAsyncServerDeprecatedTests.java +++ /dev/null @@ -1,466 +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 - */ -// KEEP IN SYNC with the class in mcp-test module -@Deprecated -public abstract class AbstractMcpAsyncServerDeprecatedTests { - - private static final String TEST_TOOL_NAME = "test-tool"; - - private static final String TEST_RESOURCE_URI = "test://resource"; - - private static final String TEST_PROMPT_NAME = "test-prompt"; - - abstract protected ServerMcpTransport createMcpTransport(); - - protected void onStart() { - } - - protected void onClose() { - } - - @BeforeEach - void setUp() { - } - - @AfterEach - void tearDown() { - onClose(); - } - - // --------------------------------------- - // Server Lifecycle Tests - // --------------------------------------- - - @Test - void testConstructorWithInvalidArguments() { - assertThatThrownBy(() -> McpServer.async((ServerMcpTransport) null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Transport must not be null"); - - assertThatThrownBy(() -> McpServer.async(createMcpTransport()).serverInfo((McpSchema.Implementation) null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Server info must not be null"); - } - - @Test - void testGracefulShutdown() { - var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - StepVerifier.create(mcpAsyncServer.closeGracefully()).verifyComplete(); - } - - @Test - void testImmediateClose() { - var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatCode(() -> mcpAsyncServer.close()).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Tools Tests - // --------------------------------------- - String emptyJsonSchema = """ - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": {} - } - """; - - @Test - void testAddTool() { - Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .build(); - - StepVerifier.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolRegistration(newTool, - args -> Mono.just(new CallToolResult(List.of(), false))))) - .verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddDuplicateTool() { - Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); - - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(duplicateTool, args -> Mono.just(new CallToolResult(List.of(), false))) - .build(); - - StepVerifier.create(mcpAsyncServer.addTool(new McpServerFeatures.AsyncToolRegistration(duplicateTool, - args -> Mono.just(new CallToolResult(List.of(), false))))) - .verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists"); - }); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testRemoveTool() { - Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); - - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(too, args -> Mono.just(new CallToolResult(List.of(), false))) - .build(); - - StepVerifier.create(mcpAsyncServer.removeTool(TEST_TOOL_NAME)).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testRemoveNonexistentTool() { - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .build(); - - StepVerifier.create(mcpAsyncServer.removeTool("nonexistent-tool")).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class).hasMessage("Tool with name 'nonexistent-tool' not found"); - }); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testNotifyToolsListChanged() { - Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); - - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(too, args -> Mono.just(new CallToolResult(List.of(), false))) - .build(); - - StepVerifier.create(mcpAsyncServer.notifyToolsListChanged()).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Resources Tests - // --------------------------------------- - - @Test - void testNotifyResourcesListChanged() { - var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - StepVerifier.create(mcpAsyncServer.notifyResourcesListChanged()).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddResource() { - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description", - null); - McpServerFeatures.AsyncResourceRegistration registration = new McpServerFeatures.AsyncResourceRegistration( - resource, req -> Mono.just(new ReadResourceResult(List.of()))); - - StepVerifier.create(mcpAsyncServer.addResource(registration)).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddResourceWithNullRegistration() { - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - StepVerifier.create(mcpAsyncServer.addResource((McpServerFeatures.AsyncResourceRegistration) null)) - .verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class).hasMessage("Resource must not be null"); - }); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddResourceWithoutCapability() { - // Create a server without resource capabilities - McpAsyncServer serverWithoutResources = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .build(); - - Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description", - null); - McpServerFeatures.AsyncResourceRegistration registration = new McpServerFeatures.AsyncResourceRegistration( - resource, req -> Mono.just(new ReadResourceResult(List.of()))); - - StepVerifier.create(serverWithoutResources.addResource(registration)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with resource capabilities"); - }); - } - - @Test - void testRemoveResourceWithoutCapability() { - // Create a server without resource capabilities - McpAsyncServer serverWithoutResources = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .build(); - - StepVerifier.create(serverWithoutResources.removeResource(TEST_RESOURCE_URI)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with resource capabilities"); - }); - } - - // --------------------------------------- - // Prompts Tests - // --------------------------------------- - - @Test - void testNotifyPromptsListChanged() { - var mcpAsyncServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - StepVerifier.create(mcpAsyncServer.notifyPromptsListChanged()).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testAddPromptWithNullRegistration() { - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(false).build()) - .build(); - - StepVerifier.create(mcpAsyncServer.addPrompt((McpServerFeatures.AsyncPromptRegistration) null)) - .verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class).hasMessage("Prompt registration must not be null"); - }); - } - - @Test - void testAddPromptWithoutCapability() { - // Create a server without prompt capabilities - McpAsyncServer serverWithoutPrompts = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .build(); - - Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of()); - McpServerFeatures.AsyncPromptRegistration registration = new McpServerFeatures.AsyncPromptRegistration(prompt, - req -> Mono.just(new GetPromptResult("Test prompt description", List - .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))))); - - StepVerifier.create(serverWithoutPrompts.addPrompt(registration)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with prompt capabilities"); - }); - } - - @Test - void testRemovePromptWithoutCapability() { - // Create a server without prompt capabilities - McpAsyncServer serverWithoutPrompts = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .build(); - - StepVerifier.create(serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with prompt capabilities"); - }); - } - - @Test - void testRemovePrompt() { - String TEST_PROMPT_NAME_TO_REMOVE = "TEST_PROMPT_NAME678"; - - Prompt prompt = new Prompt(TEST_PROMPT_NAME_TO_REMOVE, "Test Prompt", List.of()); - McpServerFeatures.AsyncPromptRegistration registration = new McpServerFeatures.AsyncPromptRegistration(prompt, - req -> Mono.just(new GetPromptResult("Test prompt description", List - .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))))); - - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(true).build()) - .prompts(registration) - .build(); - - StepVerifier.create(mcpAsyncServer.removePrompt(TEST_PROMPT_NAME_TO_REMOVE)).verifyComplete(); - - assertThatCode(() -> mcpAsyncServer.closeGracefully().block(Duration.ofSeconds(10))).doesNotThrowAnyException(); - } - - @Test - void testRemoveNonexistentPrompt() { - var mcpAsyncServer2 = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(true).build()) - .build(); - - StepVerifier.create(mcpAsyncServer2.removePrompt("nonexistent-prompt")).verifyErrorSatisfies(error -> { - assertThat(error).isInstanceOf(McpError.class) - .hasMessage("Prompt with name 'nonexistent-prompt' not found"); - }); - - assertThatCode(() -> mcpAsyncServer2.closeGracefully().block(Duration.ofSeconds(10))) - .doesNotThrowAnyException(); - } - - // --------------------------------------- - // Roots Tests - // --------------------------------------- - - @Test - void testRootsChangeConsumers() { - // Test with single consumer - var rootsReceived = new McpSchema.Root[1]; - var consumerCalled = new boolean[1]; - - var singleConsumerServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .rootsChangeConsumers(List.of(roots -> Mono.fromRunnable(() -> { - consumerCalled[0] = true; - if (!roots.isEmpty()) { - rootsReceived[0] = roots.get(0); - } - }))) - .build(); - - assertThat(singleConsumerServer).isNotNull(); - assertThatCode(() -> singleConsumerServer.closeGracefully().block(Duration.ofSeconds(10))) - .doesNotThrowAnyException(); - onClose(); - - // Test with multiple consumers - var consumer1Called = new boolean[1]; - var consumer2Called = new boolean[1]; - var rootsContent = new List[1]; - - var multipleConsumersServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .rootsChangeConsumers(List.of(roots -> Mono.fromRunnable(() -> { - consumer1Called[0] = true; - rootsContent[0] = roots; - }), roots -> Mono.fromRunnable(() -> consumer2Called[0] = true))) - .build(); - - assertThat(multipleConsumersServer).isNotNull(); - assertThatCode(() -> multipleConsumersServer.closeGracefully().block(Duration.ofSeconds(10))) - .doesNotThrowAnyException(); - onClose(); - - // Test error handling - var errorHandlingServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .rootsChangeConsumers(List.of(roots -> { - throw new RuntimeException("Test error"); - })) - .build(); - - assertThat(errorHandlingServer).isNotNull(); - assertThatCode(() -> errorHandlingServer.closeGracefully().block(Duration.ofSeconds(10))) - .doesNotThrowAnyException(); - onClose(); - - // Test without consumers - var noConsumersServer = McpServer.async(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThat(noConsumersServer).isNotNull(); - assertThatCode(() -> noConsumersServer.closeGracefully().block(Duration.ofSeconds(10))) - .doesNotThrowAnyException(); - } - - // --------------------------------------- - // Logging Tests - // --------------------------------------- - - @Test - void testLoggingLevels() { - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - // Test all logging levels - for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { - var notification = McpSchema.LoggingMessageNotification.builder() - .level(level) - .logger("test-logger") - .data("Test message with level " + level) - .build(); - - StepVerifier.create(mcpAsyncServer.loggingNotification(notification)).verifyComplete(); - } - } - - @Test - void testLoggingWithoutCapability() { - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().build()) // No logging capability - .build(); - - var notification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.INFO) - .logger("test-logger") - .data("Test log message") - .build(); - - StepVerifier.create(mcpAsyncServer.loggingNotification(notification)).verifyComplete(); - } - - @Test - void testLoggingWithNullNotification() { - var mcpAsyncServer = McpServer.async(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - StepVerifier.create(mcpAsyncServer.loggingNotification(null)).verifyError(McpError.class); - } - -} diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerDeprecatedTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerDeprecatedTests.java deleted file mode 100644 index 16bc2d6e4..000000000 --- a/mcp/src/test/java/io/modelcontextprotocol/server/AbstractMcpSyncServerDeprecatedTests.java +++ /dev/null @@ -1,433 +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 - */ -// KEEP IN SYNC with the class in mcp-test module -@Deprecated -public abstract class AbstractMcpSyncServerDeprecatedTests { - - private static final String TEST_TOOL_NAME = "test-tool"; - - private static final String TEST_RESOURCE_URI = "test://resource"; - - private static final String TEST_PROMPT_NAME = "test-prompt"; - - abstract protected ServerMcpTransport createMcpTransport(); - - protected void onStart() { - } - - protected void onClose() { - } - - @BeforeEach - void setUp() { - // onStart(); - } - - @AfterEach - void tearDown() { - onClose(); - } - - // --------------------------------------- - // Server Lifecycle Tests - // --------------------------------------- - - @Test - void testConstructorWithInvalidArguments() { - assertThatThrownBy(() -> McpServer.sync((ServerMcpTransport) null)).isInstanceOf(IllegalArgumentException.class) - .hasMessage("Transport must not be null"); - - assertThatThrownBy(() -> McpServer.sync(createMcpTransport()).serverInfo(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("Server info must not be null"); - } - - @Test - void testGracefulShutdown() { - var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testImmediateClose() { - var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatCode(() -> mcpSyncServer.close()).doesNotThrowAnyException(); - } - - @Test - void testGetAsyncServer() { - var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThat(mcpSyncServer.getAsyncServer()).isNotNull(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Tools Tests - // --------------------------------------- - - String emptyJsonSchema = """ - { - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": {} - } - """; - - @Test - void testAddTool() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .build(); - - Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema); - assertThatCode(() -> mcpSyncServer - .addTool(new McpServerFeatures.SyncToolRegistration(newTool, args -> new CallToolResult(List.of(), false)))) - .doesNotThrowAnyException(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testAddDuplicateTool() { - Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema); - - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(duplicateTool, args -> new CallToolResult(List.of(), false)) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.addTool(new McpServerFeatures.SyncToolRegistration(duplicateTool, - args -> new CallToolResult(List.of(), false)))) - .isInstanceOf(McpError.class) - .hasMessage("Tool with name '" + TEST_TOOL_NAME + "' already exists"); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testRemoveTool() { - Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", emptyJsonSchema); - - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .tool(tool, args -> new CallToolResult(List.of(), false)) - .build(); - - assertThatCode(() -> mcpSyncServer.removeTool(TEST_TOOL_NAME)).doesNotThrowAnyException(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testRemoveNonexistentTool() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().tools(true).build()) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.removeTool("nonexistent-tool")).isInstanceOf(McpError.class) - .hasMessage("Tool with name 'nonexistent-tool' not found"); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testNotifyToolsListChanged() { - var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatCode(() -> mcpSyncServer.notifyToolsListChanged()).doesNotThrowAnyException(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Resources Tests - // --------------------------------------- - - @Test - void testNotifyResourcesListChanged() { - var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatCode(() -> mcpSyncServer.notifyResourcesListChanged()).doesNotThrowAnyException(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testAddResource() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description", - null); - McpServerFeatures.SyncResourceRegistration registration = new McpServerFeatures.SyncResourceRegistration( - resource, req -> new ReadResourceResult(List.of())); - - assertThatCode(() -> mcpSyncServer.addResource(registration)).doesNotThrowAnyException(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testAddResourceWithNullRegistration() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().resources(true, false).build()) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.addResource((McpServerFeatures.SyncResourceRegistration) null)) - .isInstanceOf(McpError.class) - .hasMessage("Resource must not be null"); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testAddResourceWithoutCapability() { - var serverWithoutResources = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description", - null); - McpServerFeatures.SyncResourceRegistration registration = new McpServerFeatures.SyncResourceRegistration( - resource, req -> new ReadResourceResult(List.of())); - - assertThatThrownBy(() -> serverWithoutResources.addResource(registration)).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with resource capabilities"); - } - - @Test - void testRemoveResourceWithoutCapability() { - var serverWithoutResources = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatThrownBy(() -> serverWithoutResources.removeResource(TEST_RESOURCE_URI)).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with resource capabilities"); - } - - // --------------------------------------- - // Prompts Tests - // --------------------------------------- - - @Test - void testNotifyPromptsListChanged() { - var mcpSyncServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatCode(() -> mcpSyncServer.notifyPromptsListChanged()).doesNotThrowAnyException(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testAddPromptWithNullRegistration() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(false).build()) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.addPrompt((McpServerFeatures.SyncPromptRegistration) null)) - .isInstanceOf(McpError.class) - .hasMessage("Prompt registration must not be null"); - } - - @Test - void testAddPromptWithoutCapability() { - var serverWithoutPrompts = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of()); - McpServerFeatures.SyncPromptRegistration registration = new McpServerFeatures.SyncPromptRegistration(prompt, - req -> new GetPromptResult("Test prompt description", List - .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))); - - assertThatThrownBy(() -> serverWithoutPrompts.addPrompt(registration)).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with prompt capabilities"); - } - - @Test - void testRemovePromptWithoutCapability() { - var serverWithoutPrompts = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThatThrownBy(() -> serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).isInstanceOf(McpError.class) - .hasMessage("Server must be configured with prompt capabilities"); - } - - @Test - void testRemovePrompt() { - Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", List.of()); - McpServerFeatures.SyncPromptRegistration registration = new McpServerFeatures.SyncPromptRegistration(prompt, - req -> new GetPromptResult("Test prompt description", List - .of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))); - - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(true).build()) - .prompts(registration) - .build(); - - assertThatCode(() -> mcpSyncServer.removePrompt(TEST_PROMPT_NAME)).doesNotThrowAnyException(); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - @Test - void testRemoveNonexistentPrompt() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().prompts(true).build()) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.removePrompt("nonexistent-prompt")).isInstanceOf(McpError.class) - .hasMessage("Prompt with name 'nonexistent-prompt' not found"); - - assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Roots Tests - // --------------------------------------- - - @Test - void testRootsChangeConsumers() { - // Test with single consumer - var rootsReceived = new McpSchema.Root[1]; - var consumerCalled = new boolean[1]; - - var singleConsumerServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .rootsChangeConsumers(List.of(roots -> { - consumerCalled[0] = true; - if (!roots.isEmpty()) { - rootsReceived[0] = roots.get(0); - } - })) - .build(); - - assertThat(singleConsumerServer).isNotNull(); - assertThatCode(() -> singleConsumerServer.closeGracefully()).doesNotThrowAnyException(); - onClose(); - - // Test with multiple consumers - var consumer1Called = new boolean[1]; - var consumer2Called = new boolean[1]; - var rootsContent = new List[1]; - - var multipleConsumersServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .rootsChangeConsumers(List.of(roots -> { - consumer1Called[0] = true; - rootsContent[0] = roots; - }, roots -> consumer2Called[0] = true)) - .build(); - - assertThat(multipleConsumersServer).isNotNull(); - assertThatCode(() -> multipleConsumersServer.closeGracefully()).doesNotThrowAnyException(); - onClose(); - - // Test error handling - var errorHandlingServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .rootsChangeConsumers(List.of(roots -> { - throw new RuntimeException("Test error"); - })) - .build(); - - assertThat(errorHandlingServer).isNotNull(); - assertThatCode(() -> errorHandlingServer.closeGracefully()).doesNotThrowAnyException(); - onClose(); - - // Test without consumers - var noConsumersServer = McpServer.sync(createMcpTransport()).serverInfo("test-server", "1.0.0").build(); - - assertThat(noConsumersServer).isNotNull(); - assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException(); - } - - // --------------------------------------- - // Logging Tests - // --------------------------------------- - - @Test - void testLoggingLevels() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - // Test all logging levels - for (McpSchema.LoggingLevel level : McpSchema.LoggingLevel.values()) { - var notification = McpSchema.LoggingMessageNotification.builder() - .level(level) - .logger("test-logger") - .data("Test message with level " + level) - .build(); - - assertThatCode(() -> mcpSyncServer.loggingNotification(notification)).doesNotThrowAnyException(); - } - } - - @Test - void testLoggingWithoutCapability() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().build()) // No logging capability - .build(); - - var notification = McpSchema.LoggingMessageNotification.builder() - .level(McpSchema.LoggingLevel.INFO) - .logger("test-logger") - .data("Test log message") - .build(); - - assertThatCode(() -> mcpSyncServer.loggingNotification(notification)).doesNotThrowAnyException(); - } - - @Test - void testLoggingWithNullNotification() { - var mcpSyncServer = McpServer.sync(createMcpTransport()) - .serverInfo("test-server", "1.0.0") - .capabilities(ServerCapabilities.builder().logging().build()) - .build(); - - assertThatThrownBy(() -> mcpSyncServer.loggingNotification(null)).isInstanceOf(McpError.class); - } - -} diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java index 97358723f..f643f1ba3 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/McpServerProtocolVersionTests.java @@ -7,7 +7,8 @@ import java.util.List; import java.util.UUID; -import io.modelcontextprotocol.MockMcpTransport; +import io.modelcontextprotocol.MockMcpServerTransport; +import io.modelcontextprotocol.MockMcpServerTransportProvider; import io.modelcontextprotocol.spec.McpSchema; import org.junit.jupiter.api.Test; @@ -29,14 +30,16 @@ private McpSchema.JSONRPCRequest jsonRpcInitializeRequest(String requestId, Stri @Test void shouldUseLatestVersionByDefault() { - MockMcpTransport transport = new MockMcpTransport(); - McpAsyncServer server = McpServer.async(transport).serverInfo(SERVER_INFO).build(); + MockMcpServerTransport serverTransport = new MockMcpServerTransport(); + var transportProvider = new MockMcpServerTransportProvider(serverTransport); + McpAsyncServer server = McpServer.async(transportProvider).serverInfo(SERVER_INFO).build(); String requestId = UUID.randomUUID().toString(); - transport.simulateIncomingMessage(jsonRpcInitializeRequest(requestId, McpSchema.LATEST_PROTOCOL_VERSION)); + transportProvider + .simulateIncomingMessage(jsonRpcInitializeRequest(requestId, McpSchema.LATEST_PROTOCOL_VERSION)); - McpSchema.JSONRPCMessage response = transport.getLastSentMessage(); + McpSchema.JSONRPCMessage response = serverTransport.getLastSentMessage(); assertThat(response).isInstanceOf(McpSchema.JSONRPCResponse.class); McpSchema.JSONRPCResponse jsonResponse = (McpSchema.JSONRPCResponse) response; assertThat(jsonResponse.id()).isEqualTo(requestId); @@ -50,16 +53,18 @@ void shouldUseLatestVersionByDefault() { @Test void shouldNegotiateSpecificVersion() { String oldVersion = "0.1.0"; - MockMcpTransport transport = new MockMcpTransport(); - McpAsyncServer server = McpServer.async(transport).serverInfo(SERVER_INFO).build(); + MockMcpServerTransport serverTransport = new MockMcpServerTransport(); + var transportProvider = new MockMcpServerTransportProvider(serverTransport); + + McpAsyncServer server = McpServer.async(transportProvider).serverInfo(SERVER_INFO).build(); server.setProtocolVersions(List.of(oldVersion, McpSchema.LATEST_PROTOCOL_VERSION)); String requestId = UUID.randomUUID().toString(); - transport.simulateIncomingMessage(jsonRpcInitializeRequest(requestId, oldVersion)); + transportProvider.simulateIncomingMessage(jsonRpcInitializeRequest(requestId, oldVersion)); - McpSchema.JSONRPCMessage response = transport.getLastSentMessage(); + McpSchema.JSONRPCMessage response = serverTransport.getLastSentMessage(); assertThat(response).isInstanceOf(McpSchema.JSONRPCResponse.class); McpSchema.JSONRPCResponse jsonResponse = (McpSchema.JSONRPCResponse) response; assertThat(jsonResponse.id()).isEqualTo(requestId); @@ -73,14 +78,16 @@ void shouldNegotiateSpecificVersion() { @Test void shouldSuggestLatestVersionForUnsupportedVersion() { String unsupportedVersion = "999.999.999"; - MockMcpTransport transport = new MockMcpTransport(); - McpAsyncServer server = McpServer.async(transport).serverInfo(SERVER_INFO).build(); + MockMcpServerTransport serverTransport = new MockMcpServerTransport(); + var transportProvider = new MockMcpServerTransportProvider(serverTransport); + + McpAsyncServer server = McpServer.async(transportProvider).serverInfo(SERVER_INFO).build(); String requestId = UUID.randomUUID().toString(); - transport.simulateIncomingMessage(jsonRpcInitializeRequest(requestId, unsupportedVersion)); + transportProvider.simulateIncomingMessage(jsonRpcInitializeRequest(requestId, unsupportedVersion)); - McpSchema.JSONRPCMessage response = transport.getLastSentMessage(); + McpSchema.JSONRPCMessage response = serverTransport.getLastSentMessage(); assertThat(response).isInstanceOf(McpSchema.JSONRPCResponse.class); McpSchema.JSONRPCResponse jsonResponse = (McpSchema.JSONRPCResponse) response; assertThat(jsonResponse.id()).isEqualTo(requestId); @@ -97,15 +104,17 @@ void shouldUseHighestVersionWhenMultipleSupported() { String middleVersion = "0.2.0"; String latestVersion = McpSchema.LATEST_PROTOCOL_VERSION; - MockMcpTransport transport = new MockMcpTransport(); - McpAsyncServer server = McpServer.async(transport).serverInfo(SERVER_INFO).build(); + MockMcpServerTransport serverTransport = new MockMcpServerTransport(); + var transportProvider = new MockMcpServerTransportProvider(serverTransport); + + McpAsyncServer server = McpServer.async(transportProvider).serverInfo(SERVER_INFO).build(); server.setProtocolVersions(List.of(oldVersion, middleVersion, latestVersion)); String requestId = UUID.randomUUID().toString(); - transport.simulateIncomingMessage(jsonRpcInitializeRequest(requestId, latestVersion)); + transportProvider.simulateIncomingMessage(jsonRpcInitializeRequest(requestId, latestVersion)); - McpSchema.JSONRPCMessage response = transport.getLastSentMessage(); + McpSchema.JSONRPCMessage response = serverTransport.getLastSentMessage(); assertThat(response).isInstanceOf(McpSchema.JSONRPCResponse.class); McpSchema.JSONRPCResponse jsonResponse = (McpSchema.JSONRPCResponse) response; assertThat(jsonResponse.id()).isEqualTo(requestId); diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpAsyncServerDeprecatedTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpAsyncServerDeprecatedTests.java deleted file mode 100644 index 2c80d45c6..000000000 --- a/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpAsyncServerDeprecatedTests.java +++ /dev/null @@ -1,26 +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.HttpServletSseServerTransport; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import org.junit.jupiter.api.Timeout; - -/** - * Tests for {@link McpAsyncServer} using {@link HttpServletSseServerTransport}. - * - * @author Christian Tzolov - */ -@Deprecated -@Timeout(15) // Giving extra time beyond the client timeout -class ServletSseMcpAsyncServerDeprecatedTests extends AbstractMcpAsyncServerDeprecatedTests { - - @Override - protected ServerMcpTransport createMcpTransport() { - return new HttpServletSseServerTransport(new ObjectMapper(), "/mcp/message"); - } - -} diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpSyncServerDeprecatedTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpSyncServerDeprecatedTests.java deleted file mode 100644 index 8cdd08c5d..000000000 --- a/mcp/src/test/java/io/modelcontextprotocol/server/ServletSseMcpSyncServerDeprecatedTests.java +++ /dev/null @@ -1,26 +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.HttpServletSseServerTransport; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import org.junit.jupiter.api.Timeout; - -/** - * Tests for {@link McpSyncServer} using {@link HttpServletSseServerTransport}. - * - * @author Christian Tzolov - */ -@Deprecated -@Timeout(15) // Giving extra time beyond the client timeout -class ServletSseMcpSyncServerDeprecatedTests extends AbstractMcpSyncServerDeprecatedTests { - - @Override - protected ServerMcpTransport createMcpTransport() { - return new HttpServletSseServerTransport(new ObjectMapper(), "/mcp/message"); - } - -} diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerDeprecatedTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerDeprecatedTests.java deleted file mode 100644 index db95db07b..000000000 --- a/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerDeprecatedTests.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.server; - -import io.modelcontextprotocol.server.transport.StdioServerTransport; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import org.junit.jupiter.api.Timeout; - -/** - * Tests for {@link McpAsyncServer} using {@link StdioServerTransport}. - * - * @author Christian Tzolov - */ -@Deprecated -@Timeout(15) // Giving extra time beyond the client timeout -class StdioMcpAsyncServerDeprecatedTests extends AbstractMcpAsyncServerDeprecatedTests { - - @Override - protected ServerMcpTransport createMcpTransport() { - return new StdioServerTransport(); - } - -} diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java index 27ff53c93..0381a43bd 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpAsyncServerTests.java @@ -4,7 +4,6 @@ package io.modelcontextprotocol.server; -import io.modelcontextprotocol.server.transport.StdioServerTransport; import io.modelcontextprotocol.server.transport.StdioServerTransportProvider; import io.modelcontextprotocol.spec.McpServerTransportProvider; import org.junit.jupiter.api.Timeout; diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpSyncServerDeprecatedTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpSyncServerDeprecatedTests.java deleted file mode 100644 index 149f72819..000000000 --- a/mcp/src/test/java/io/modelcontextprotocol/server/StdioMcpSyncServerDeprecatedTests.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.server; - -import io.modelcontextprotocol.server.transport.StdioServerTransport; -import io.modelcontextprotocol.spec.ServerMcpTransport; -import org.junit.jupiter.api.Timeout; - -/** - * Tests for {@link McpSyncServer} using {@link StdioServerTransport}. - * - * @author Christian Tzolov - */ -@Deprecated -@Timeout(15) // Giving extra time beyond the client timeout -class StdioMcpSyncServerDeprecatedTests extends AbstractMcpSyncServerDeprecatedTests { - - @Override - protected ServerMcpTransport createMcpTransport() { - return new StdioServerTransport(); - } - -} diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportIntegrationTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportIntegrationTests.java deleted file mode 100644 index 4a292da31..000000000 --- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/HttpServletSseServerTransportIntegrationTests.java +++ /dev/null @@ -1,328 +0,0 @@ -/* - * Copyright 2024 - 2024 the original author or authors. - */ -package io.modelcontextprotocol.server.transport; - -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.McpServer; -import io.modelcontextprotocol.server.McpServerFeatures; -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.web.client.RestClient; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.awaitility.Awaitility.await; - -public class HttpServletSseServerTransportIntegrationTests { - - private static final int PORT = 8184; - - private static final String MESSAGE_ENDPOINT = "/mcp/message"; - - private HttpServletSseServerTransport mcpServerTransport; - - McpClient.SyncSpec clientBuilder; - - private Tomcat tomcat; - - @BeforeEach - public void before() { - tomcat = new Tomcat(); - tomcat.setPort(PORT); - - String baseDir = System.getProperty("java.io.tmpdir"); - tomcat.setBaseDir(baseDir); - - Context context = tomcat.addContext("", baseDir); - - // Create and configure the transport - mcpServerTransport = new HttpServletSseServerTransport(new ObjectMapper(), MESSAGE_ENDPOINT); - - // Add transport servlet to Tomcat - org.apache.catalina.Wrapper wrapper = context.createWrapper(); - wrapper.setName("mcpServlet"); - wrapper.setServlet(mcpServerTransport); - wrapper.setLoadOnStartup(1); - wrapper.setAsyncSupported(true); - context.addChild(wrapper); - context.addServletMappingDecoded("/*", "mcpServlet"); - - try { - var connector = tomcat.getConnector(); - connector.setAsyncTimeout(3000); - 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 (tomcat != null) { - try { - tomcat.stop(); - tomcat.destroy(); - } - catch (LifecycleException e) { - throw new RuntimeException("Failed to stop Tomcat", e); - } - } - } - - @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() { - var mcpAsyncServer = McpServer.async(mcpServerTransport).serverInfo("test-server", "1.0.0").build(); - - Function samplingHandler = request -> { - assertThat(request.messages()).hasSize(1); - assertThat(request.messages().get(0).content()).isInstanceOf(McpSchema.TextContent.class); - - return new CreateMessageResult(Role.USER, new McpSchema.TextContent("Test message"), "MockModelName", - CreateMessageResult.StopReason.STOP_SEQUENCE); - }; - - var client = clientBuilder.clientInfo(new McpSchema.Implementation("Sample client", "0.0.0")) - .capabilities(ClientCapabilities.builder().sampling().build()) - .sampling(samplingHandler) - .build(); - - InitializeResult initResult = client.initialize(); - assertThat(initResult).isNotNull(); - - var messages = List - .of(new McpSchema.SamplingMessage(McpSchema.Role.USER, new McpSchema.TextContent("Test message"))); - var modelPrefs = new McpSchema.ModelPreferences(List.of(), 1.0, 1.0, 1.0); - - var request = new McpSchema.CreateMessageRequest(messages, modelPrefs, null, - McpSchema.CreateMessageRequest.ContextInclusionStrategy.NONE, null, 100, List.of(), Map.of()); - - StepVerifier.create(mcpAsyncServer.createMessage(request)).consumeNextWith(result -> { - assertThat(result).isNotNull(); - assertThat(result.role()).isEqualTo(Role.USER); - assertThat(result.content()).isInstanceOf(McpSchema.TextContent.class); - assertThat(((McpSchema.TextContent) result.content()).text()).isEqualTo("Test message"); - assertThat(result.model()).isEqualTo("MockModelName"); - assertThat(result.stopReason()).isEqualTo(CreateMessageResult.StopReason.STOP_SEQUENCE); - }).verifyComplete(); - } - - @Test - void testRootsSuccess() { - List roots = List.of(new Root("uri1://", "root1"), new Root("uri2://", "root2")); - - AtomicReference> rootsRef = new AtomicReference<>(); - var mcpServer = McpServer.sync(mcpServerTransport) - .rootsChangeConsumer(rootsUpdate -> rootsRef.set(rootsUpdate)) - .build(); - - var mcpClient = clientBuilder.capabilities(ClientCapabilities.builder().roots(true).build()) - .roots(roots) - .build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - assertThat(rootsRef.get()).isNull(); - - assertThat(mcpServer.listRoots().roots()).containsAll(roots); - - mcpClient.rootsListChangedNotification(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(rootsRef.get()).containsAll(roots); - }); - - mcpClient.close(); - mcpServer.close(); - } - - 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 -> { - 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 -> { - 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> toolsRef = new AtomicReference<>(); - var mcpClient = clientBuilder.toolsChangeConsumer(toolsUpdate -> { - String response = RestClient.create() - .get() - .uri("https://github.com/modelcontextprotocol/specification/blob/main/README.md") - .retrieve() - .body(String.class); - assertThat(response).isNotBlank(); - toolsRef.set(toolsUpdate); - }).build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - assertThat(toolsRef.get()).isNull(); - - assertThat(mcpClient.listTools().tools()).contains(tool1.tool()); - - mcpServer.notifyToolsListChanged(); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(toolsRef.get()).containsAll(List.of(tool1.tool())); - }); - - mcpServer.removeTool("tool1"); - - await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> { - assertThat(toolsRef.get()).isEmpty(); - }); - - 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(toolsRef.get()).containsAll(List.of(tool2.tool())); - }); - - mcpClient.close(); - mcpServer.close(); - } - - @Test - void testInitialize() { - var mcpServer = McpServer.sync(mcpServerTransport).build(); - var mcpClient = clientBuilder.build(); - - InitializeResult initResult = mcpClient.initialize(); - assertThat(initResult).isNotNull(); - - mcpClient.close(); - mcpServer.close(); - } - -} diff --git a/mcp/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportTests.java b/mcp/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportTests.java deleted file mode 100644 index 43e5019fc..000000000 --- a/mcp/src/test/java/io/modelcontextprotocol/server/transport/StdioServerTransportTests.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2024-2024 the original author or authors. - */ - -package io.modelcontextprotocol.server.transport; - -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.io.PrintStream; -import java.nio.charset.StandardCharsets; -import java.util.Map; - -import com.fasterxml.jackson.databind.ObjectMapper; -import io.modelcontextprotocol.spec.McpSchema; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCRequest; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link StdioServerTransport}. - * - * @author Christian Tzolov - */ -class StdioServerTransportTests { - - private final InputStream originalIn = System.in; - - private final PrintStream originalOut = System.out; - - private final PrintStream originalErr = System.err; - - private ByteArrayOutputStream testOut; - - private ByteArrayOutputStream testErr; - - private PrintStream testOutPrintStream; - - private StdioServerTransport transport; - - private ObjectMapper objectMapper; - - @BeforeEach - void setUp() { - testOut = new ByteArrayOutputStream(); - testErr = new ByteArrayOutputStream(); - testOutPrintStream = new PrintStream(testOut, true); - System.setOut(testOutPrintStream); - System.setErr(new PrintStream(testErr)); - - objectMapper = new ObjectMapper(); - } - - @AfterEach - void tearDown() { - if (transport != null) { - transport.closeGracefully().block(); - } - if (testOutPrintStream != null) { - testOutPrintStream.close(); - } - System.setIn(originalIn); - System.setOut(originalOut); - System.setErr(originalErr); - } - - @Test - void shouldHandleIncomingMessages() throws Exception { - // Prepare test input - String jsonMessage = "{\"jsonrpc\":\"2.0\",\"method\":\"test\",\"params\":{},\"id\":1}"; - - // Create transport with test streams - transport = new StdioServerTransport(objectMapper); - - // Parse expected message - McpSchema.JSONRPCRequest expected = objectMapper.readValue(jsonMessage, McpSchema.JSONRPCRequest.class); - - // Connect transport with message handler and verify message - StepVerifier.create(transport.connect(message -> message.doOnNext(msg -> { - McpSchema.JSONRPCRequest received = (McpSchema.JSONRPCRequest) msg; - assertThat(received.id()).isEqualTo(expected.id()); - assertThat(received.method()).isEqualTo(expected.method()); - }))).verifyComplete(); - } - - @Test - @Disabled - void shouldHandleOutgoingMessages() throws Exception { - // Create transport with test streams - transport = new StdioServerTransport(objectMapper); - // transport = new StdioServerTransport(objectMapper, new BlockingInputStream(), - // testOutPrintStream); - - // Create test messages - JSONRPCRequest initMessage = new JSONRPCRequest(McpSchema.JSONRPC_VERSION, "init", "init-id", - Map.of("init", "true")); - JSONRPCRequest testMessage = new JSONRPCRequest(McpSchema.JSONRPC_VERSION, "test", "test-id", - Map.of("key", "value")); - - // Connect transport, send messages, and verify output in a reactive chain - StepVerifier.create(transport.connect(message -> message) - .then(transport.sendMessage(initMessage)) - // .then(Mono.fromRunnable(() -> testOut.reset())) // Clear buffer after init - // message - .then(transport.sendMessage(testMessage)) - .then(Mono.fromCallable(() -> { - String output = testOut.toString(StandardCharsets.UTF_8); - assertThat(output).contains("\"jsonrpc\":\"2.0\""); - assertThat(output).contains("\"method\":\"test\""); - assertThat(output).contains("\"id\":\"test-id\""); - return null; - }))).verifyComplete(); - } - - @Test - void shouldWaitForProcessorsBeforeSendingMessage() { - // Create transport with test streams - transport = new StdioServerTransport(objectMapper); - - // Create test message - JSONRPCRequest testMessage = new JSONRPCRequest(McpSchema.JSONRPC_VERSION, "test", "test-id", - Map.of("key", "value")); - - // Try to send message before connecting (before processors are ready) - StepVerifier.create(transport.sendMessage(testMessage)).verifyTimeout(java.time.Duration.ofMillis(100)); - - // Connect transport and verify message can be sent - StepVerifier.create(transport.connect(message -> message).then(transport.sendMessage(testMessage))) - .verifyComplete(); - } - - @Test - void shouldCloseGracefully() { - // Create transport with test streams - transport = new StdioServerTransport(objectMapper); - - // Create test message - JSONRPCRequest initMessage = new JSONRPCRequest(McpSchema.JSONRPC_VERSION, "init", "init-id", - Map.of("init", "true")); - - // Connect transport, send message, and close gracefully in a reactive chain - StepVerifier - .create(transport.connect(message -> message) - .then(transport.sendMessage(initMessage)) - .then(transport.closeGracefully())) - .verifyComplete(); - - // Verify error log is empty - assertThat(testErr.toString()).doesNotContain("Error"); - } - -} diff --git a/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java b/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java index 79a1d0d92..715d6651e 100644 --- a/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java +++ b/mcp/src/test/java/io/modelcontextprotocol/spec/McpClientSessionTests.java @@ -8,7 +8,7 @@ import java.util.Map; import com.fasterxml.jackson.core.type.TypeReference; -import io.modelcontextprotocol.MockMcpTransport; +import io.modelcontextprotocol.MockMcpClientTransport; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -41,11 +41,11 @@ class McpClientSessionTests { private McpClientSession session; - private MockMcpTransport transport; + private MockMcpClientTransport transport; @BeforeEach void setUp() { - transport = new MockMcpTransport(); + transport = new MockMcpClientTransport(); session = new McpClientSession(TIMEOUT, transport, Map.of(), Map.of(TEST_NOTIFICATION, params -> Mono.fromRunnable(() -> logger.info("Status update: " + params)))); } @@ -139,7 +139,7 @@ void testRequestHandling() { String echoMessage = "Hello MCP!"; Map> requestHandlers = Map.of(ECHO_METHOD, params -> Mono.just(params)); - transport = new MockMcpTransport(); + transport = new MockMcpClientTransport(); session = new McpClientSession(TIMEOUT, transport, requestHandlers, Map.of()); // Simulate incoming request @@ -159,7 +159,7 @@ void testRequestHandling() { void testNotificationHandling() { Sinks.One receivedParams = Sinks.one(); - transport = new MockMcpTransport(); + transport = new MockMcpClientTransport(); session = new McpClientSession(TIMEOUT, transport, Map.of(), Map.of(TEST_NOTIFICATION, params -> Mono.fromRunnable(() -> receivedParams.tryEmitValue(params))));