feat: add Streamable HTTP Server abstractions and implement WebFlux transport provider (#420)

- Add WebFluxStreamableServerTransportProvider for streamable HTTP sessions
- Add WebFluxStatelessServerTransport for stateless HTTP MCP servers
  - Add McpStatelessServerFeatures, McpStatelessServerHandler, McpStatelessRequestHandler, McpStatelessNotificationHandler
- Refactor server architecture to support multiple transport categories:
   streamable, single-session, stateless
- Introduce McpTransportContext for transport-level metadata extraction
- Add session management capabilities for streamable HTTP connections
- Update MCP protocol version to 2025-03-26
- Add test coverage for new transport implementations
- Implement integration tests for both stateless and streamable transports

Signed-off-by: Dariusz Jędrzejczyk <dariusz.jedrzejczyk@broadcom.com>
Signed-off-by: Christian Tzolov <christian.tzolov@broadcom.com>
Co-authored-by: Christian Tzolov <christian.tzolov@broadcom.com>
This commit is contained in:
Dariusz Jędrzejczyk 2025-07-30 13:41:18 +02:00 • committed by GitHub
commit a8f5a3fb4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 6578 additions and 704 deletions

View file

@ -341,7 +341,8 @@ public class WebClientStreamableHttpTransport implements McpClientTransport {
McpSchema.JSONRPCResponse jsonRpcResponse = objectMapper.readValue(body,
McpSchema.JSONRPCResponse.class);
jsonRpcError = jsonRpcResponse.error();
toPropagate = new McpError(jsonRpcError);
toPropagate = jsonRpcError != null ? new McpError(jsonRpcError)
: new McpError("Can't parse the jsonResponse " + jsonRpcResponse);
}
catch (IOException ex) {
toPropagate = new RuntimeException("Sending request failed", e);

View file

@ -0,0 +1,213 @@
package io.modelcontextprotocol.server.transport;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.McpStatelessServerHandler;
import io.modelcontextprotocol.server.DefaultMcpTransportContext;
import io.modelcontextprotocol.server.McpTransportContextExtractor;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpStatelessServerTransport;
import io.modelcontextprotocol.server.McpTransportContext;
import io.modelcontextprotocol.util.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
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;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.util.List;
import java.util.function.Function;
/**
* Implementation of a WebFlux based {@link McpStatelessServerTransport}.
*
* @author Dariusz Jędrzejczyk
*/
public class WebFluxStatelessServerTransport implements McpStatelessServerTransport {
private static final Logger logger = LoggerFactory.getLogger(WebFluxStatelessServerTransport.class);
private final ObjectMapper objectMapper;
private final String mcpEndpoint;
private final RouterFunction<?> routerFunction;
private McpStatelessServerHandler mcpHandler;
private McpTransportContextExtractor<ServerRequest> contextExtractor;
private volatile boolean isClosing = false;
private WebFluxStatelessServerTransport(ObjectMapper objectMapper, String mcpEndpoint,
McpTransportContextExtractor<ServerRequest> contextExtractor) {
Assert.notNull(objectMapper, "objectMapper must not be null");
Assert.notNull(mcpEndpoint, "mcpEndpoint must not be null");
Assert.notNull(contextExtractor, "contextExtractor must not be null");
this.objectMapper = objectMapper;
this.mcpEndpoint = mcpEndpoint;
this.contextExtractor = contextExtractor;
this.routerFunction = RouterFunctions.route()
.GET(this.mcpEndpoint, this::handleGet)
.POST(this.mcpEndpoint, this::handlePost)
.build();
}
@Override
public void setMcpHandler(McpStatelessServerHandler mcpHandler) {
this.mcpHandler = mcpHandler;
}
@Override
public Mono<Void> closeGracefully() {
return Mono.fromRunnable(() -> this.isClosing = true);
}
/**
* Returns the WebFlux router function that defines the transport's HTTP endpoints.
* This router function should be integrated into the application's web configuration.
*
* <p>
* The router function defines one endpoint handling two HTTP methods:
* <ul>
* <li>GET {messageEndpoint} - Unsupported, returns 405 METHOD NOT ALLOWED</li>
* <li>POST {messageEndpoint} - For handling client requests and notifications</li>
* </ul>
* @return The configured {@link RouterFunction} for handling HTTP requests
*/
public RouterFunction<?> getRouterFunction() {
return this.routerFunction;
}
private Mono<ServerResponse> handleGet(ServerRequest request) {
return ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED).build();
}
private Mono<ServerResponse> handlePost(ServerRequest request) {
if (isClosing) {
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down");
}
McpTransportContext transportContext = this.contextExtractor.extract(request, new DefaultMcpTransportContext());
List<MediaType> acceptHeaders = request.headers().asHttpHeaders().getAccept();
if (!(acceptHeaders.contains(MediaType.APPLICATION_JSON)
&& acceptHeaders.contains(MediaType.TEXT_EVENT_STREAM))) {
return ServerResponse.badRequest().build();
}
return request.bodyToMono(String.class).<ServerResponse>flatMap(body -> {
try {
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body);
if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
return this.mcpHandler.handleRequest(transportContext, jsonrpcRequest)
.flatMap(jsonrpcResponse -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(jsonrpcResponse));
}
else if (message instanceof McpSchema.JSONRPCNotification jsonrpcNotification) {
return this.mcpHandler.handleNotification(transportContext, jsonrpcNotification)
.then(ServerResponse.accepted().build());
}
else {
return ServerResponse.badRequest()
.bodyValue(new McpError("The server accepts either requests or notifications"));
}
}
catch (IllegalArgumentException | IOException e) {
logger.error("Failed to deserialize message: {}", e.getMessage());
return ServerResponse.badRequest().bodyValue(new McpError("Invalid message format"));
}
}).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext));
}
/**
* Create a builder for the server.
* @return a fresh {@link Builder} instance.
*/
public static Builder builder() {
return new Builder();
}
/**
* Builder for creating instances of {@link WebFluxStatelessServerTransport}.
* <p>
* This builder provides a fluent API for configuring and creating instances of
* WebFluxSseServerTransportProvider with custom settings.
*/
public static class Builder {
private ObjectMapper objectMapper;
private String mcpEndpoint = "/mcp";
private McpTransportContextExtractor<ServerRequest> contextExtractor = (serverRequest, context) -> context;
private Builder() {
// used by a static method
}
/**
* Sets the ObjectMapper to use for JSON serialization/deserialization of MCP
* messages.
* @param objectMapper The ObjectMapper instance. Must not be null.
* @return this builder instance
* @throws IllegalArgumentException if objectMapper is null
*/
public Builder objectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
this.objectMapper = objectMapper;
return this;
}
/**
* Sets the endpoint URI where clients should send their JSON-RPC messages.
* @param messageEndpoint The message endpoint URI. Must not be null.
* @return this builder instance
* @throws IllegalArgumentException if messageEndpoint is null
*/
public Builder messageEndpoint(String messageEndpoint) {
Assert.notNull(messageEndpoint, "Message endpoint must not be null");
this.mcpEndpoint = messageEndpoint;
return this;
}
/**
* Sets the context extractor that allows providing the MCP feature
* implementations to inspect HTTP transport level metadata that was present at
* HTTP request processing time. This allows to extract custom headers and other
* useful data for use during execution later on in the process.
* @param contextExtractor The contextExtractor to fill in a
* {@link McpTransportContext}.
* @return this builder instance
* @throws IllegalArgumentException if contextExtractor is null
*/
public Builder contextExtractor(McpTransportContextExtractor<ServerRequest> contextExtractor) {
Assert.notNull(contextExtractor, "Context extractor must not be null");
this.contextExtractor = contextExtractor;
return this;
}
/**
* Builds a new instance of {@link WebFluxStatelessServerTransport} with the
* configured settings.
* @return A new WebFluxSseServerTransportProvider instance
* @throws IllegalStateException if required parameters are not set
*/
public WebFluxStatelessServerTransport build() {
Assert.notNull(objectMapper, "ObjectMapper must be set");
Assert.notNull(mcpEndpoint, "Message endpoint must be set");
return new WebFluxStatelessServerTransport(objectMapper, mcpEndpoint, contextExtractor);
}
}
}

View file

@ -0,0 +1,443 @@
package io.modelcontextprotocol.server.transport;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.DefaultMcpTransportContext;
import io.modelcontextprotocol.server.McpTransportContextExtractor;
import io.modelcontextprotocol.spec.HttpHeaders;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpStreamableServerSession;
import io.modelcontextprotocol.spec.McpStreamableServerTransport;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
import io.modelcontextprotocol.server.McpTransportContext;
import io.modelcontextprotocol.util.Assert;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
import reactor.core.Disposable;
import reactor.core.Exceptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.FluxSink;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
/**
* Implementation of a WebFlux based {@link McpStreamableServerTransportProvider}.
*
* @author Dariusz Jędrzejczyk
*/
public class WebFluxStreamableServerTransportProvider implements McpStreamableServerTransportProvider {
private static final Logger logger = LoggerFactory.getLogger(WebFluxStreamableServerTransportProvider.class);
public static final String MESSAGE_EVENT_TYPE = "message";
private final ObjectMapper objectMapper;
private final String mcpEndpoint;
private final boolean disallowDelete;
private final RouterFunction<?> routerFunction;
private McpStreamableServerSession.Factory sessionFactory;
private final ConcurrentHashMap<String, McpStreamableServerSession> sessions = new ConcurrentHashMap<>();
private McpTransportContextExtractor<ServerRequest> contextExtractor;
private volatile boolean isClosing = false;
private WebFluxStreamableServerTransportProvider(ObjectMapper objectMapper, String mcpEndpoint,
McpTransportContextExtractor<ServerRequest> contextExtractor, boolean disallowDelete) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
Assert.notNull(mcpEndpoint, "Message endpoint must not be null");
Assert.notNull(contextExtractor, "Context extractor must not be null");
this.objectMapper = objectMapper;
this.mcpEndpoint = mcpEndpoint;
this.contextExtractor = contextExtractor;
this.disallowDelete = disallowDelete;
this.routerFunction = RouterFunctions.route()
.GET(this.mcpEndpoint, this::handleGet)
.POST(this.mcpEndpoint, this::handlePost)
.DELETE(this.mcpEndpoint, this::handleDelete)
.build();
}
@Override
public void setSessionFactory(McpStreamableServerSession.Factory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@Override
public Mono<Void> notifyClients(String method, Object params) {
if (sessions.isEmpty()) {
logger.debug("No active sessions to broadcast message to");
return Mono.empty();
}
logger.debug("Attempting to broadcast message to {} active sessions", sessions.size());
return Flux.fromIterable(sessions.values())
.flatMap(session -> session.sendNotification(method, params)
.doOnError(
e -> logger.error("Failed to send message to session {}: {}", session.getId(), e.getMessage()))
.onErrorComplete())
.then();
}
@Override
public Mono<Void> closeGracefully() {
return Mono.defer(() -> {
this.isClosing = true;
return Flux.fromIterable(sessions.values())
.doFirst(() -> logger.debug("Initiating graceful shutdown with {} active sessions", sessions.size()))
.flatMap(McpStreamableServerSession::closeGracefully)
.then();
});
}
/**
* Returns the WebFlux router function that defines the transport's HTTP endpoints.
* This router function should be integrated into the application's web configuration.
*
* <p>
* The router function defines one endpoint with three methods:
* <ul>
* <li>GET {messageEndpoint} - For the client listening SSE stream</li>
* <li>POST {messageEndpoint} - For receiving client messages</li>
* <li>DELETE {messageEndpoint} - For removing sessions</li>
* </ul>
* @return The configured {@link RouterFunction} for handling HTTP requests
*/
public RouterFunction<?> getRouterFunction() {
return this.routerFunction;
}
/**
* Opens the listening SSE streams for clients.
* @param request The incoming server request
* @return A Mono which emits a response with the SSE event stream
*/
private Mono<ServerResponse> handleGet(ServerRequest request) {
if (isClosing) {
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down");
}
McpTransportContext transportContext = this.contextExtractor.extract(request, new DefaultMcpTransportContext());
return Mono.defer(() -> {
List<MediaType> acceptHeaders = request.headers().asHttpHeaders().getAccept();
if (!acceptHeaders.contains(MediaType.TEXT_EVENT_STREAM)) {
return ServerResponse.badRequest().build();
}
if (!request.headers().asHttpHeaders().containsKey(HttpHeaders.MCP_SESSION_ID)) {
return ServerResponse.badRequest().build(); // TODO: say we need a session
// id
}
String sessionId = request.headers().asHttpHeaders().getFirst(HttpHeaders.MCP_SESSION_ID);
McpStreamableServerSession session = this.sessions.get(sessionId);
if (session == null) {
return ServerResponse.notFound().build();
}
if (request.headers().asHttpHeaders().containsKey(HttpHeaders.LAST_EVENT_ID)) {
String lastId = request.headers().asHttpHeaders().getFirst(HttpHeaders.LAST_EVENT_ID);
return ServerResponse.ok()
.contentType(MediaType.TEXT_EVENT_STREAM)
.body(session.replay(lastId), ServerSentEvent.class);
}
return ServerResponse.ok()
.contentType(MediaType.TEXT_EVENT_STREAM)
.body(Flux.<ServerSentEvent<?>>create(sink -> {
WebFluxStreamableMcpSessionTransport sessionTransport = new WebFluxStreamableMcpSessionTransport(
sink);
McpStreamableServerSession.McpStreamableServerSessionStream listeningStream = session
.listeningStream(sessionTransport);
sink.onDispose(listeningStream::close);
}), ServerSentEvent.class);
}).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext));
}
/**
* Handles incoming JSON-RPC messages from clients.
* @param request The incoming server request containing the JSON-RPC message
* @return A Mono with the response appropriate to a particular Streamable HTTP flow.
*/
private Mono<ServerResponse> handlePost(ServerRequest request) {
if (isClosing) {
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down");
}
McpTransportContext transportContext = this.contextExtractor.extract(request, new DefaultMcpTransportContext());
List<MediaType> acceptHeaders = request.headers().asHttpHeaders().getAccept();
if (!(acceptHeaders.contains(MediaType.APPLICATION_JSON)
&& acceptHeaders.contains(MediaType.TEXT_EVENT_STREAM))) {
return ServerResponse.badRequest().build();
}
return request.bodyToMono(String.class).<ServerResponse>flatMap(body -> {
try {
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body);
if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest
&& jsonrpcRequest.method().equals(McpSchema.METHOD_INITIALIZE)) {
McpSchema.InitializeRequest initializeRequest = objectMapper.convertValue(jsonrpcRequest.params(),
new TypeReference<McpSchema.InitializeRequest>() {
});
McpStreamableServerSession.McpStreamableServerSessionInit init = this.sessionFactory
.startSession(initializeRequest);
sessions.put(init.session().getId(), init.session());
return init.initResult().map(initializeResult -> {
McpSchema.JSONRPCResponse jsonrpcResponse = new McpSchema.JSONRPCResponse(
McpSchema.JSONRPC_VERSION, jsonrpcRequest.id(), initializeResult, null);
try {
return this.objectMapper.writeValueAsString(jsonrpcResponse);
}
catch (IOException e) {
logger.warn("Failed to serialize initResponse", e);
throw Exceptions.propagate(e);
}
})
.flatMap(initResult -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.header(HttpHeaders.MCP_SESSION_ID, init.session().getId())
.bodyValue(initResult));
}
if (!request.headers().asHttpHeaders().containsKey(HttpHeaders.MCP_SESSION_ID)) {
return ServerResponse.badRequest().bodyValue(new McpError("Session ID missing"));
}
String sessionId = request.headers().asHttpHeaders().getFirst(HttpHeaders.MCP_SESSION_ID);
McpStreamableServerSession session = sessions.get(sessionId);
if (session == null) {
return ServerResponse.status(HttpStatus.NOT_FOUND)
.bodyValue(new McpError("Session not found: " + sessionId));
}
if (message instanceof McpSchema.JSONRPCResponse jsonrpcResponse) {
return session.accept(jsonrpcResponse).then(ServerResponse.accepted().build());
}
else if (message instanceof McpSchema.JSONRPCNotification jsonrpcNotification) {
return session.accept(jsonrpcNotification).then(ServerResponse.accepted().build());
}
else if (message instanceof McpSchema.JSONRPCRequest jsonrpcRequest) {
return ServerResponse.ok()
.contentType(MediaType.TEXT_EVENT_STREAM)
.body(Flux.<ServerSentEvent<?>>create(sink -> {
WebFluxStreamableMcpSessionTransport st = new WebFluxStreamableMcpSessionTransport(sink);
Mono<Void> stream = session.responseStream(jsonrpcRequest, st);
Disposable streamSubscription = stream.onErrorComplete(err -> {
sink.error(err);
return true;
}).contextWrite(sink.contextView()).subscribe();
sink.onCancel(streamSubscription);
}), ServerSentEvent.class);
}
else {
return ServerResponse.badRequest().bodyValue(new McpError("Unknown message type"));
}
}
catch (IllegalArgumentException | IOException e) {
logger.error("Failed to deserialize message: {}", e.getMessage());
return ServerResponse.badRequest().bodyValue(new McpError("Invalid message format"));
}
})
.switchIfEmpty(ServerResponse.badRequest().build())
.contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext));
}
private Mono<ServerResponse> handleDelete(ServerRequest request) {
if (isClosing) {
return ServerResponse.status(HttpStatus.SERVICE_UNAVAILABLE).bodyValue("Server is shutting down");
}
McpTransportContext transportContext = this.contextExtractor.extract(request, new DefaultMcpTransportContext());
return Mono.defer(() -> {
if (!request.headers().asHttpHeaders().containsKey(HttpHeaders.MCP_SESSION_ID)) {
return ServerResponse.badRequest().build(); // TODO: say we need a session
// id
}
if (this.disallowDelete) {
return ServerResponse.status(HttpStatus.METHOD_NOT_ALLOWED).build();
}
String sessionId = request.headers().asHttpHeaders().getFirst(HttpHeaders.MCP_SESSION_ID);
McpStreamableServerSession session = this.sessions.get(sessionId);
if (session == null) {
return ServerResponse.notFound().build();
}
return session.delete().then(ServerResponse.ok().build());
}).contextWrite(ctx -> ctx.put(McpTransportContext.KEY, transportContext));
}
private class WebFluxStreamableMcpSessionTransport implements McpStreamableServerTransport {
private final FluxSink<ServerSentEvent<?>> sink;
public WebFluxStreamableMcpSessionTransport(FluxSink<ServerSentEvent<?>> sink) {
this.sink = sink;
}
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
return this.sendMessage(message, null);
}
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message, String messageId) {
return Mono.fromSupplier(() -> {
try {
return objectMapper.writeValueAsString(message);
}
catch (IOException e) {
throw Exceptions.propagate(e);
}
}).doOnNext(jsonText -> {
ServerSentEvent<Object> event = ServerSentEvent.builder()
.id(messageId)
.event(MESSAGE_EVENT_TYPE)
.data(jsonText)
.build();
sink.next(event);
}).doOnError(e -> {
// TODO log with sessionid
Throwable exception = Exceptions.unwrap(e);
sink.error(exception);
}).then();
}
@Override
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return objectMapper.convertValue(data, typeRef);
}
@Override
public Mono<Void> closeGracefully() {
return Mono.fromRunnable(sink::complete);
}
@Override
public void close() {
sink.complete();
}
}
public static Builder builder() {
return new Builder();
}
/**
* Builder for creating instances of {@link WebFluxStreamableServerTransportProvider}.
* <p>
* This builder provides a fluent API for configuring and creating instances of
* WebFluxStreamableServerTransportProvider with custom settings.
*/
public static class Builder {
private ObjectMapper objectMapper;
private String mcpEndpoint = "/mcp";
private McpTransportContextExtractor<ServerRequest> contextExtractor = (serverRequest, context) -> context;
private boolean disallowDelete;
private Builder() {
// used by a static method
}
/**
* Sets the ObjectMapper to use for JSON serialization/deserialization of MCP
* messages.
* @param objectMapper The ObjectMapper instance. Must not be null.
* @return this builder instance
* @throws IllegalArgumentException if objectMapper is null
*/
public Builder objectMapper(ObjectMapper objectMapper) {
Assert.notNull(objectMapper, "ObjectMapper must not be null");
this.objectMapper = objectMapper;
return this;
}
/**
* Sets the endpoint URI where clients should send their JSON-RPC messages.
* @param messageEndpoint The message endpoint URI. Must not be null.
* @return this builder instance
* @throws IllegalArgumentException if messageEndpoint is null
*/
public Builder messageEndpoint(String messageEndpoint) {
Assert.notNull(messageEndpoint, "Message endpoint must not be null");
this.mcpEndpoint = messageEndpoint;
return this;
}
/**
* Sets the context extractor that allows providing the MCP feature
* implementations to inspect HTTP transport level metadata that was present at
* HTTP request processing time. This allows to extract custom headers and other
* useful data for use during execution later on in the process.
* @param contextExtractor The contextExtractor to fill in a
* {@link McpTransportContext}.
* @return this builder instance
* @throws IllegalArgumentException if contextExtractor is null
*/
public Builder contextExtractor(McpTransportContextExtractor<ServerRequest> contextExtractor) {
Assert.notNull(contextExtractor, "contextExtractor must not be null");
this.contextExtractor = contextExtractor;
return this;
}
/**
* Sets whether the session removal capability is disabled.
* @param disallowDelete if {@code true}, the DELETE endpoint will not be
* supported and sessions won't be deleted.
* @return this builder instance
*/
public Builder disallowDelete(boolean disallowDelete) {
this.disallowDelete = disallowDelete;
return this;
}
/**
* Builds a new instance of {@link WebFluxStreamableServerTransportProvider} with
* the configured settings.
* @return A new WebFluxStreamableServerTransportProvider instance
* @throws IllegalStateException if required parameters are not set
*/
public WebFluxStreamableServerTransportProvider build() {
Assert.notNull(objectMapper, "ObjectMapper must be set");
Assert.notNull(mcpEndpoint, "Message endpoint must be set");
return new WebFluxStreamableServerTransportProvider(objectMapper, mcpEndpoint, contextExtractor,
disallowDelete);
}
}
}

View file

@ -0,0 +1,472 @@
/*
* Copyright 2024 - 2024 the original author or authors.
*/
package io.modelcontextprotocol;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.client.McpClient;
import io.modelcontextprotocol.client.transport.HttpClientStreamableHttpTransport;
import io.modelcontextprotocol.client.transport.WebClientStreamableHttpTransport;
import io.modelcontextprotocol.server.McpServer;
import io.modelcontextprotocol.server.McpStatelessServerFeatures;
import io.modelcontextprotocol.server.TestUtil;
import io.modelcontextprotocol.server.transport.WebFluxStatelessServerTransport;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.CompleteRequest;
import io.modelcontextprotocol.spec.McpSchema.CompleteResult;
import io.modelcontextprotocol.spec.McpSchema.InitializeResult;
import io.modelcontextprotocol.spec.McpSchema.Prompt;
import io.modelcontextprotocol.spec.McpSchema.PromptArgument;
import io.modelcontextprotocol.spec.McpSchema.PromptReference;
import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities;
import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.server.McpTransportContext;
import net.javacrumbs.jsonunit.core.Option;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
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 reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
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.BiFunction;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
class WebFluxStatelessIntegrationTests {
private static final int PORT = TestUtil.findAvailablePort();
private static final String CUSTOM_MESSAGE_ENDPOINT = "/otherPath/mcp/message";
private DisposableServer httpServer;
private WebFluxStatelessServerTransport mcpStreamableServerTransport;
ConcurrentHashMap<String, McpClient.SyncSpec> clientBuilders = new ConcurrentHashMap<>();
@BeforeEach
public void before() {
this.mcpStreamableServerTransport = WebFluxStatelessServerTransport.builder()
.objectMapper(new ObjectMapper())
.messageEndpoint(CUSTOM_MESSAGE_ENDPOINT)
.build();
HttpHandler httpHandler = RouterFunctions.toHttpHandler(mcpStreamableServerTransport.getRouterFunction());
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
this.httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow();
clientBuilders
.put("httpclient",
McpClient.sync(HttpClientStreamableHttpTransport.builder("http://localhost:" + PORT)
.endpoint(CUSTOM_MESSAGE_ENDPOINT)
.build()).initializationTimeout(Duration.ofHours(10)).requestTimeout(Duration.ofHours(10)));
clientBuilders
.put("webflux", McpClient
.sync(WebClientStreamableHttpTransport.builder(WebClient.builder().baseUrl("http://localhost:" + PORT))
.endpoint(CUSTOM_MESSAGE_ENDPOINT)
.build())
.initializationTimeout(Duration.ofHours(10))
.requestTimeout(Duration.ofHours(10)));
}
@AfterEach
public void after() {
if (httpServer != null) {
httpServer.disposeNow();
}
}
// ---------------------------------------
// 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 = clientBuilders.get(clientType);
var callResponse = new CallToolResult(List.of(new McpSchema.TextContent("CALL RESPONSE")), null);
McpStatelessServerFeatures.SyncToolSpecification tool1 = new McpStatelessServerFeatures.SyncToolSpecification(
new Tool("tool1", "tool1 description", emptyJsonSchema), (transportContext, request) -> {
// perform a blocking call to a remote service
String response = RestClient.create()
.get()
.uri("https://raw.githubusercontent.com/modelcontextprotocol/java-sdk/refs/heads/main/README.md")
.retrieve()
.body(String.class);
assertThat(response).isNotBlank();
return callResponse;
});
var mcpServer = McpServer.sync(mcpStreamableServerTransport)
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(tool1)
.build();
try (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);
}
mcpServer.close();
}
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
void testInitialize(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
var mcpServer = McpServer.sync(mcpStreamableServerTransport).build();
try (var mcpClient = clientBuilder.build()) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
}
mcpServer.close();
}
// ---------------------------------------
// Completion Tests
// ---------------------------------------
@ParameterizedTest(name = "{0} : Completion call")
@ValueSource(strings = { "httpclient", "webflux" })
void testCompletionShouldReturnExpectedSuggestions(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
var expectedValues = List.of("python", "pytorch", "pyside");
var completionResponse = new CompleteResult(new CompleteResult.CompleteCompletion(expectedValues, 10, // total
true // hasMore
));
AtomicReference<CompleteRequest> samplingRequest = new AtomicReference<>();
BiFunction<McpTransportContext, CompleteRequest, CompleteResult> completionHandler = (transportContext,
request) -> {
samplingRequest.set(request);
return completionResponse;
};
var mcpServer = McpServer.sync(mcpStreamableServerTransport)
.capabilities(ServerCapabilities.builder().completions().build())
.prompts(new McpStatelessServerFeatures.SyncPromptSpecification(
new Prompt("code_review", "Code review", "this is code review prompt",
List.of(new PromptArgument("language", "Language", "string", false))),
(transportContext, getPromptRequest) -> null))
.completions(new McpStatelessServerFeatures.SyncCompletionSpecification(
new PromptReference("ref/prompt", "code_review", "Code review"), completionHandler))
.build();
try (var mcpClient = clientBuilder.build()) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
CompleteRequest request = new CompleteRequest(
new PromptReference("ref/prompt", "code_review", "Code review"),
new CompleteRequest.CompleteArgument("language", "py"));
CompleteResult result = mcpClient.completeCompletion(request);
assertThat(result).isNotNull();
assertThat(samplingRequest.get().argument().name()).isEqualTo("language");
assertThat(samplingRequest.get().argument().value()).isEqualTo("py");
assertThat(samplingRequest.get().ref().type()).isEqualTo("ref/prompt");
}
mcpServer.close();
}
// ---------------------------------------
// Tool Structured Output Schema Tests
// ---------------------------------------
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
void testStructuredOutputValidationSuccess(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
// Create a tool with output schema
Map<String, Object> outputSchema = Map.of(
"type", "object", "properties", Map.of("result", Map.of("type", "number"), "operation",
Map.of("type", "string"), "timestamp", Map.of("type", "string")),
"required", List.of("result", "operation"));
Tool calculatorTool = Tool.builder()
.name("calculator")
.description("Performs mathematical calculations")
.outputSchema(outputSchema)
.build();
McpStatelessServerFeatures.SyncToolSpecification tool = new McpStatelessServerFeatures.SyncToolSpecification(
calculatorTool, (transportContext, request) -> {
String expression = (String) request.arguments().getOrDefault("expression", "2 + 3");
double result = evaluateExpression(expression);
return CallToolResult.builder()
.structuredContent(
Map.of("result", result, "operation", expression, "timestamp", "2024-01-01T10:00:00Z"))
.build();
});
var mcpServer = McpServer.sync(mcpStreamableServerTransport)
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(tool)
.build();
try (var mcpClient = clientBuilder.build()) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
// Verify tool is listed with output schema
var toolsList = mcpClient.listTools();
assertThat(toolsList.tools()).hasSize(1);
assertThat(toolsList.tools().get(0).name()).isEqualTo("calculator");
// Note: outputSchema might be null in sync server, but validation still works
// Call tool with valid structured output
CallToolResult response = mcpClient
.callTool(new McpSchema.CallToolRequest("calculator", Map.of("expression", "2 + 3")));
assertThat(response).isNotNull();
assertThat(response.isError()).isFalse();
assertThat(response.content()).hasSize(1);
assertThat(response.content().get(0)).isInstanceOf(McpSchema.TextContent.class);
assertThatJson(((McpSchema.TextContent) response.content().get(0)).text()).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"result":5.0,"operation":"2 + 3","timestamp":"2024-01-01T10:00:00Z"}"""));
assertThat(response.structuredContent()).isNotNull();
assertThatJson(response.structuredContent()).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"result":5.0,"operation":"2 + 3","timestamp":"2024-01-01T10:00:00Z"}"""));
}
mcpServer.close();
}
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
void testStructuredOutputValidationFailure(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
// Create a tool with output schema
Map<String, Object> outputSchema = Map.of("type", "object", "properties",
Map.of("result", Map.of("type", "number"), "operation", Map.of("type", "string")), "required",
List.of("result", "operation"));
Tool calculatorTool = Tool.builder()
.name("calculator")
.description("Performs mathematical calculations")
.outputSchema(outputSchema)
.build();
McpStatelessServerFeatures.SyncToolSpecification tool = new McpStatelessServerFeatures.SyncToolSpecification(
calculatorTool, (transportContext, request) -> {
// Return invalid structured output. Result should be number, missing
// operation
return CallToolResult.builder()
.addTextContent("Invalid calculation")
.structuredContent(Map.of("result", "not-a-number", "extra", "field"))
.build();
});
var mcpServer = McpServer.sync(mcpStreamableServerTransport)
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(tool)
.build();
try (var mcpClient = clientBuilder.build()) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
// Call tool with invalid structured output
CallToolResult response = mcpClient
.callTool(new McpSchema.CallToolRequest("calculator", Map.of("expression", "2 + 3")));
assertThat(response).isNotNull();
assertThat(response.isError()).isTrue();
assertThat(response.content()).hasSize(1);
assertThat(response.content().get(0)).isInstanceOf(McpSchema.TextContent.class);
String errorMessage = ((McpSchema.TextContent) response.content().get(0)).text();
assertThat(errorMessage).contains("Validation failed");
}
mcpServer.close();
}
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
void testStructuredOutputMissingStructuredContent(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
// Create a tool with output schema
Map<String, Object> outputSchema = Map.of("type", "object", "properties",
Map.of("result", Map.of("type", "number")), "required", List.of("result"));
Tool calculatorTool = Tool.builder()
.name("calculator")
.description("Performs mathematical calculations")
.outputSchema(outputSchema)
.build();
McpStatelessServerFeatures.SyncToolSpecification tool = new McpStatelessServerFeatures.SyncToolSpecification(
calculatorTool, (transportContext, request) -> {
// Return result without structured content but tool has output schema
return CallToolResult.builder().addTextContent("Calculation completed").build();
});
var mcpServer = McpServer.sync(mcpStreamableServerTransport)
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.instructions("bla")
.tools(tool)
.build();
try (var mcpClient = clientBuilder.build()) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
// Call tool that should return structured content but doesn't
CallToolResult response = mcpClient
.callTool(new McpSchema.CallToolRequest("calculator", Map.of("expression", "2 + 3")));
assertThat(response).isNotNull();
assertThat(response.isError()).isTrue();
assertThat(response.content()).hasSize(1);
assertThat(response.content().get(0)).isInstanceOf(McpSchema.TextContent.class);
String errorMessage = ((McpSchema.TextContent) response.content().get(0)).text();
assertThat(errorMessage).isEqualTo(
"Response missing structured content which is expected when calling tool with non-empty outputSchema");
}
mcpServer.close();
}
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "httpclient", "webflux" })
void testStructuredOutputRuntimeToolAddition(String clientType) {
var clientBuilder = clientBuilders.get(clientType);
// Start server without tools
var mcpServer = McpServer.sync(mcpStreamableServerTransport)
.serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.build();
try (var mcpClient = clientBuilder.build()) {
InitializeResult initResult = mcpClient.initialize();
assertThat(initResult).isNotNull();
// Initially no tools
assertThat(mcpClient.listTools().tools()).isEmpty();
// Add tool with output schema at runtime
Map<String, Object> outputSchema = Map.of("type", "object", "properties",
Map.of("message", Map.of("type", "string"), "count", Map.of("type", "integer")), "required",
List.of("message", "count"));
Tool dynamicTool = Tool.builder()
.name("dynamic-tool")
.description("Dynamically added tool")
.outputSchema(outputSchema)
.build();
McpStatelessServerFeatures.SyncToolSpecification toolSpec = new McpStatelessServerFeatures.SyncToolSpecification(
dynamicTool, (transportContext, request) -> {
int count = (Integer) request.arguments().getOrDefault("count", 1);
return CallToolResult.builder()
.addTextContent("Dynamic tool executed " + count + " times")
.structuredContent(Map.of("message", "Dynamic execution", "count", count))
.build();
});
// Add tool to server
mcpServer.addTool(toolSpec);
// Wait for tool list change notification
await().atMost(Duration.ofSeconds(5)).untilAsserted(() -> {
assertThat(mcpClient.listTools().tools()).hasSize(1);
});
// Verify tool was added with output schema
var toolsList = mcpClient.listTools();
assertThat(toolsList.tools()).hasSize(1);
assertThat(toolsList.tools().get(0).name()).isEqualTo("dynamic-tool");
// Note: outputSchema might be null in sync server, but validation still works
// Call dynamically added tool
CallToolResult response = mcpClient
.callTool(new McpSchema.CallToolRequest("dynamic-tool", Map.of("count", 3)));
assertThat(response).isNotNull();
assertThat(response.isError()).isFalse();
assertThat(response.content()).hasSize(1);
assertThat(response.content().get(0)).isInstanceOf(McpSchema.TextContent.class);
assertThat(((McpSchema.TextContent) response.content().get(0)).text())
.isEqualTo("Dynamic tool executed 3 times");
assertThat(response.structuredContent()).isNotNull();
assertThatJson(response.structuredContent()).when(Option.IGNORING_ARRAY_ORDER)
.when(Option.IGNORING_EXTRA_ARRAY_ITEMS)
.isObject()
.isEqualTo(json("""
{"count":3,"message":"Dynamic execution"}"""));
}
mcpServer.close();
}
private double evaluateExpression(String expression) {
// Simple expression evaluator for testing
return switch (expression) {
case "2 + 3" -> 5.0;
case "10 * 2" -> 20.0;
case "7 + 8" -> 15.0;
case "5 + 3" -> 8.0;
default -> 0.0;
};
}
}

View file

@ -29,8 +29,7 @@ class WebFluxSseMcpAsyncServerTests extends AbstractMcpAsyncServerTests {
private DisposableServer httpServer;
@Override
protected McpServerTransportProvider createMcpTransportProvider() {
private McpServerTransportProvider createMcpTransportProvider() {
var transportProvider = new WebFluxSseServerTransportProvider.Builder().objectMapper(new ObjectMapper())
.messageEndpoint(MESSAGE_ENDPOINT)
.build();
@ -41,6 +40,11 @@ class WebFluxSseMcpAsyncServerTests extends AbstractMcpAsyncServerTests {
return transportProvider;
}
@Override
protected McpServer.AsyncSpecification<?> prepareAsyncServerBuilder() {
return McpServer.async(createMcpTransportProvider());
}
@Override
protected void onStart() {
}

View file

@ -32,7 +32,11 @@ class WebFluxSseMcpSyncServerTests extends AbstractMcpSyncServerTests {
private WebFluxSseServerTransportProvider transportProvider;
@Override
protected McpServerTransportProvider createMcpTransportProvider() {
protected McpServer.SyncSpecification<?> prepareSyncServerBuilder() {
return McpServer.sync(createMcpTransportProvider());
}
private McpServerTransportProvider createMcpTransportProvider() {
transportProvider = new WebFluxSseServerTransportProvider.Builder().objectMapper(new ObjectMapper())
.messageEndpoint(MESSAGE_ENDPOINT)
.build();

View file

@ -0,0 +1,63 @@
/*
* Copyright 2024-2024 the original author or authors.
*/
package io.modelcontextprotocol.server;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider;
import io.modelcontextprotocol.server.transport.WebFluxStreamableServerTransportProvider;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
import org.junit.jupiter.api.Timeout;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
import org.springframework.web.reactive.function.server.RouterFunctions;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
/**
* Tests for {@link McpAsyncServer} using
* {@link WebFluxStreamableServerTransportProvider}.
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/
@Timeout(15) // Giving extra time beyond the client timeout
class WebFluxStreamableMcpAsyncServerTests extends AbstractMcpAsyncServerTests {
private static final int PORT = TestUtil.findAvailablePort();
private static final String MESSAGE_ENDPOINT = "/mcp/message";
private DisposableServer httpServer;
private McpStreamableServerTransportProvider createMcpTransportProvider() {
var transportProvider = WebFluxStreamableServerTransportProvider.builder()
.objectMapper(new ObjectMapper())
.messageEndpoint(MESSAGE_ENDPOINT)
.build();
HttpHandler httpHandler = RouterFunctions.toHttpHandler(transportProvider.getRouterFunction());
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow();
return transportProvider;
}
@Override
protected McpServer.AsyncSpecification<?> prepareAsyncServerBuilder() {
return McpServer.async(createMcpTransportProvider());
}
@Override
protected void onStart() {
}
@Override
protected void onClose() {
if (httpServer != null) {
httpServer.disposeNow();
}
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright 2024-2024 the original author or authors.
*/
package io.modelcontextprotocol.server;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider;
import io.modelcontextprotocol.server.transport.WebFluxStreamableServerTransportProvider;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
import org.junit.jupiter.api.Timeout;
import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
import org.springframework.web.reactive.function.server.RouterFunctions;
import reactor.netty.DisposableServer;
import reactor.netty.http.server.HttpServer;
/**
* Tests for {@link McpAsyncServer} using
* {@link WebFluxStreamableServerTransportProvider}.
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/
@Timeout(15) // Giving extra time beyond the client timeout
class WebFluxStreamableMcpSyncServerTests extends AbstractMcpSyncServerTests {
private static final int PORT = TestUtil.findAvailablePort();
private static final String MESSAGE_ENDPOINT = "/mcp/message";
private DisposableServer httpServer;
private McpStreamableServerTransportProvider createMcpTransportProvider() {
var transportProvider = WebFluxStreamableServerTransportProvider.builder()
.objectMapper(new ObjectMapper())
.messageEndpoint(MESSAGE_ENDPOINT)
.build();
HttpHandler httpHandler = RouterFunctions.toHttpHandler(transportProvider.getRouterFunction());
ReactorHttpHandlerAdapter adapter = new ReactorHttpHandlerAdapter(httpHandler);
httpServer = HttpServer.create().port(PORT).handle(adapter).bindNow();
return transportProvider;
}
@Override
protected McpServer.SyncSpecification<?> prepareSyncServerBuilder() {
return McpServer.sync(createMcpTransportProvider());
}
@Override
protected void onStart() {
}
@Override
protected void onClose() {
if (httpServer != null) {
httpServer.disposeNow();
}
}
}

View file

@ -49,8 +49,7 @@ class WebMvcSseAsyncServerTransportTests extends AbstractMcpAsyncServerTests {
private AnnotationConfigWebApplicationContext appContext;
@Override
protected McpServerTransportProvider createMcpTransportProvider() {
private McpServerTransportProvider createMcpTransportProvider() {
// Set up Tomcat first
tomcat = new Tomcat();
tomcat.setPort(PORT);
@ -90,6 +89,11 @@ class WebMvcSseAsyncServerTransportTests extends AbstractMcpAsyncServerTests {
return transportProvider;
}
@Override
protected McpServer.AsyncSpecification<?> prepareAsyncServerBuilder() {
return McpServer.async(createMcpTransportProvider());
}
@Override
protected void onStart() {
}

View file

@ -49,7 +49,11 @@ class WebMvcSseSyncServerTransportTests extends AbstractMcpSyncServerTests {
private AnnotationConfigWebApplicationContext appContext;
@Override
protected WebMvcSseServerTransportProvider createMcpTransportProvider() {
protected McpServer.SyncSpecification<?> prepareSyncServerBuilder() {
return McpServer.sync(createMcpTransportProvider());
}
private WebMvcSseServerTransportProvider createMcpTransportProvider() {
// Set up Tomcat first
tomcat = new Tomcat();
tomcat.setPort(PORT);

View file

@ -21,6 +21,8 @@ import io.modelcontextprotocol.spec.McpServerTransportProvider;
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.core.publisher.Mono;
import reactor.test.StepVerifier;
@ -42,7 +44,7 @@ public abstract class AbstractMcpAsyncServerTests {
private static final String TEST_PROMPT_NAME = "test-prompt";
abstract protected McpServerTransportProvider createMcpTransportProvider();
abstract protected McpServer.AsyncSpecification<?> prepareAsyncServerBuilder();
protected void onStart() {
}
@ -63,28 +65,29 @@ public abstract class AbstractMcpAsyncServerTests {
// Server Lifecycle Tests
// ---------------------------------------
@Test
void testConstructorWithInvalidArguments() {
@ParameterizedTest(name = "{0} : {displayName} ")
@ValueSource(strings = { "sse", "streamable" })
void testConstructorWithInvalidArguments(String serverType) {
assertThatThrownBy(() -> McpServer.async((McpServerTransportProvider) null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Transport provider must not be null");
assertThatThrownBy(
() -> McpServer.async(createMcpTransportProvider()).serverInfo((McpSchema.Implementation) null))
assertThatThrownBy(() -> prepareAsyncServerBuilder().serverInfo((McpSchema.Implementation) null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Server info must not be null");
}
@Test
void testGracefulShutdown() {
var mcpAsyncServer = McpServer.async(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build();
McpServer.AsyncSpecification<?> builder = prepareAsyncServerBuilder();
var mcpAsyncServer = builder.serverInfo("test-server", "1.0.0").build();
StepVerifier.create(mcpAsyncServer.closeGracefully()).verifyComplete();
}
@Test
void testImmediateClose() {
var mcpAsyncServer = McpServer.async(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build();
var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build();
assertThatCode(() -> mcpAsyncServer.close()).doesNotThrowAnyException();
}
@ -104,8 +107,7 @@ public abstract class AbstractMcpAsyncServerTests {
@Deprecated
void testAddTool() {
Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.build();
@ -119,8 +121,7 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testAddToolCall() {
Tool newTool = new McpSchema.Tool("new-tool", "New test tool", emptyJsonSchema);
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.build();
@ -137,8 +138,7 @@ public abstract class AbstractMcpAsyncServerTests {
void testAddDuplicateTool() {
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tool(duplicateTool, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false)))
.build();
@ -158,8 +158,7 @@ public abstract class AbstractMcpAsyncServerTests {
void testAddDuplicateToolCall() {
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.toolCall(duplicateTool, (exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))
.build();
@ -180,8 +179,7 @@ public abstract class AbstractMcpAsyncServerTests {
Tool duplicateTool = new Tool("duplicate-build-toolcall", "Duplicate toolcall during building",
emptyJsonSchema);
assertThatThrownBy(() -> McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
assertThatThrownBy(() -> prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.toolCall(duplicateTool, (exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))
.toolCall(duplicateTool, (exchange, request) -> Mono.just(new CallToolResult(List.of(), false))) // Duplicate!
@ -203,8 +201,7 @@ public abstract class AbstractMcpAsyncServerTests {
.build() // Duplicate!
);
assertThatThrownBy(() -> McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
assertThatThrownBy(() -> prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(specs)
.build()).isInstanceOf(IllegalArgumentException.class)
@ -215,8 +212,7 @@ public abstract class AbstractMcpAsyncServerTests {
void testDuplicateToolsInBatchVarargsRegistration() {
Tool duplicateTool = new Tool("batch-varargs-tool", "Duplicate tool in batch varargs", emptyJsonSchema);
assertThatThrownBy(() -> McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
assertThatThrownBy(() -> prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(McpServerFeatures.AsyncToolSpecification.builder()
.tool(duplicateTool)
@ -235,8 +231,7 @@ public abstract class AbstractMcpAsyncServerTests {
void testRemoveTool() {
Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.toolCall(too, (exchange, request) -> Mono.just(new CallToolResult(List.of(), false)))
.build();
@ -248,8 +243,7 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testRemoveNonexistentTool() {
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.build();
@ -264,8 +258,7 @@ public abstract class AbstractMcpAsyncServerTests {
void testNotifyToolsListChanged() {
Tool too = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.toolCall(too, (exchange, args) -> Mono.just(new CallToolResult(List.of(), false)))
.build();
@ -281,7 +274,7 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testNotifyResourcesListChanged() {
var mcpAsyncServer = McpServer.async(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build();
var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build();
StepVerifier.create(mcpAsyncServer.notifyResourcesListChanged()).verifyComplete();
@ -290,7 +283,7 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testNotifyResourcesUpdated() {
var mcpAsyncServer = McpServer.async(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build();
var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build();
StepVerifier
.create(mcpAsyncServer
@ -302,8 +295,7 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testAddResource() {
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().resources(true, false).build())
.build();
@ -319,8 +311,7 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testAddResourceWithNullSpecification() {
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().resources(true, false).build())
.build();
@ -335,9 +326,7 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testAddResourceWithoutCapability() {
// Create a server without resource capabilities
McpAsyncServer serverWithoutResources = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.build();
McpAsyncServer serverWithoutResources = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build();
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
null);
@ -353,9 +342,7 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testRemoveResourceWithoutCapability() {
// Create a server without resource capabilities
McpAsyncServer serverWithoutResources = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.build();
McpAsyncServer serverWithoutResources = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build();
StepVerifier.create(serverWithoutResources.removeResource(TEST_RESOURCE_URI)).verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class)
@ -369,7 +356,7 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testNotifyPromptsListChanged() {
var mcpAsyncServer = McpServer.async(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build();
var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build();
StepVerifier.create(mcpAsyncServer.notifyPromptsListChanged()).verifyComplete();
@ -378,8 +365,7 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testAddPromptWithNullSpecification() {
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(false).build())
.build();
@ -392,9 +378,7 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testAddPromptWithoutCapability() {
// Create a server without prompt capabilities
McpAsyncServer serverWithoutPrompts = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.build();
McpAsyncServer serverWithoutPrompts = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build();
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", "Test Prompt", List.of());
McpServerFeatures.AsyncPromptSpecification specification = new McpServerFeatures.AsyncPromptSpecification(
@ -410,9 +394,7 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testRemovePromptWithoutCapability() {
// Create a server without prompt capabilities
McpAsyncServer serverWithoutPrompts = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.build();
McpAsyncServer serverWithoutPrompts = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build();
StepVerifier.create(serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).verifyErrorSatisfies(error -> {
assertThat(error).isInstanceOf(McpError.class)
@ -429,8 +411,7 @@ public abstract class AbstractMcpAsyncServerTests {
prompt, (exchange, req) -> Mono.just(new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content"))))));
var mcpAsyncServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpAsyncServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(true).build())
.prompts(specification)
.build();
@ -442,8 +423,7 @@ public abstract class AbstractMcpAsyncServerTests {
@Test
void testRemoveNonexistentPrompt() {
var mcpAsyncServer2 = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpAsyncServer2 = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(true).build())
.build();
@ -466,8 +446,7 @@ public abstract class AbstractMcpAsyncServerTests {
var rootsReceived = new McpSchema.Root[1];
var consumerCalled = new boolean[1];
var singleConsumerServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var singleConsumerServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.rootsChangeHandlers(List.of((exchange, roots) -> Mono.fromRunnable(() -> {
consumerCalled[0] = true;
if (!roots.isEmpty()) {
@ -486,8 +465,7 @@ public abstract class AbstractMcpAsyncServerTests {
var consumer2Called = new boolean[1];
var rootsContent = new List[1];
var multipleConsumersServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var multipleConsumersServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.rootsChangeHandlers(List.of((exchange, roots) -> Mono.fromRunnable(() -> {
consumer1Called[0] = true;
rootsContent[0] = roots;
@ -500,8 +478,7 @@ public abstract class AbstractMcpAsyncServerTests {
onClose();
// Test error handling
var errorHandlingServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var errorHandlingServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0")
.rootsChangeHandlers(List.of((exchange, roots) -> {
throw new RuntimeException("Test error");
}))
@ -513,9 +490,7 @@ public abstract class AbstractMcpAsyncServerTests {
onClose();
// Test without consumers
var noConsumersServer = McpServer.async(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.build();
var noConsumersServer = prepareAsyncServerBuilder().serverInfo("test-server", "1.0.0").build();
assertThat(noConsumersServer).isNotNull();
assertThatCode(() -> noConsumersServer.closeGracefully().block(Duration.ofSeconds(10)))

View file

@ -28,7 +28,7 @@ import io.modelcontextprotocol.spec.McpServerTransportProvider;
/**
* Test suite for the {@link McpSyncServer} that can be used with different
* {@link McpTransportProvider} implementations.
* {@link McpServerTransportProvider} implementations.
*
* @author Christian Tzolov
*/
@ -40,7 +40,7 @@ public abstract class AbstractMcpSyncServerTests {
private static final String TEST_PROMPT_NAME = "test-prompt";
abstract protected McpServerTransportProvider createMcpTransportProvider();
abstract protected McpServer.SyncSpecification<?> prepareSyncServerBuilder();
protected void onStart() {
}
@ -68,28 +68,28 @@ public abstract class AbstractMcpSyncServerTests {
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Transport provider must not be null");
assertThatThrownBy(() -> McpServer.sync(createMcpTransportProvider()).serverInfo(null))
assertThatThrownBy(() -> prepareSyncServerBuilder().serverInfo(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("Server info must not be null");
}
@Test
void testGracefulShutdown() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build();
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build();
assertThatCode(() -> mcpSyncServer.closeGracefully()).doesNotThrowAnyException();
}
@Test
void testImmediateClose() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build();
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build();
assertThatCode(() -> mcpSyncServer.close()).doesNotThrowAnyException();
}
@Test
void testGetAsyncServer() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build();
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build();
assertThat(mcpSyncServer.getAsyncServer()).isNotNull();
@ -111,8 +111,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
@Deprecated
void testAddTool() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.build();
@ -126,8 +125,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testAddToolCall() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.build();
@ -145,8 +143,7 @@ public abstract class AbstractMcpSyncServerTests {
void testAddDuplicateTool() {
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tool(duplicateTool, (exchange, args) -> new CallToolResult(List.of(), false))
.build();
@ -163,8 +160,7 @@ public abstract class AbstractMcpSyncServerTests {
void testAddDuplicateToolCall() {
Tool duplicateTool = new McpSchema.Tool(TEST_TOOL_NAME, "Duplicate tool", emptyJsonSchema);
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false))
.build();
@ -183,8 +179,7 @@ public abstract class AbstractMcpSyncServerTests {
Tool duplicateTool = new Tool("duplicate-build-toolcall", "Duplicate toolcall during building",
emptyJsonSchema);
assertThatThrownBy(() -> McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
assertThatThrownBy(() -> prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false))
.toolCall(duplicateTool, (exchange, request) -> new CallToolResult(List.of(), false)) // Duplicate!
@ -206,8 +201,7 @@ public abstract class AbstractMcpSyncServerTests {
.build() // Duplicate!
);
assertThatThrownBy(() -> McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
assertThatThrownBy(() -> prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(specs)
.build()).isInstanceOf(IllegalArgumentException.class)
@ -218,8 +212,7 @@ public abstract class AbstractMcpSyncServerTests {
void testDuplicateToolsInBatchVarargsRegistration() {
Tool duplicateTool = new Tool("batch-varargs-tool", "Duplicate tool in batch varargs", emptyJsonSchema);
assertThatThrownBy(() -> McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
assertThatThrownBy(() -> prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.tools(McpServerFeatures.SyncToolSpecification.builder()
.tool(duplicateTool)
@ -238,8 +231,7 @@ public abstract class AbstractMcpSyncServerTests {
void testRemoveTool() {
Tool tool = new McpSchema.Tool(TEST_TOOL_NAME, "Test tool", emptyJsonSchema);
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.toolCall(tool, (exchange, args) -> new CallToolResult(List.of(), false))
.build();
@ -251,8 +243,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testRemoveNonexistentTool() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().tools(true).build())
.build();
@ -264,7 +255,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testNotifyToolsListChanged() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build();
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build();
assertThatCode(() -> mcpSyncServer.notifyToolsListChanged()).doesNotThrowAnyException();
@ -277,7 +268,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testNotifyResourcesListChanged() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build();
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build();
assertThatCode(() -> mcpSyncServer.notifyResourcesListChanged()).doesNotThrowAnyException();
@ -286,7 +277,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testNotifyResourcesUpdated() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build();
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build();
assertThatCode(() -> mcpSyncServer
.notifyResourcesUpdated(new McpSchema.ResourcesUpdatedNotification(TEST_RESOURCE_URI)))
@ -297,8 +288,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testAddResource() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().resources(true, false).build())
.build();
@ -314,8 +304,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testAddResourceWithNullSpecification() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().resources(true, false).build())
.build();
@ -328,9 +317,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testAddResourceWithoutCapability() {
var serverWithoutResources = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.build();
var serverWithoutResources = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build();
Resource resource = new Resource(TEST_RESOURCE_URI, "Test Resource", "text/plain", "Test resource description",
null);
@ -343,9 +330,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testRemoveResourceWithoutCapability() {
var serverWithoutResources = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.build();
var serverWithoutResources = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build();
assertThatThrownBy(() -> serverWithoutResources.removeResource(TEST_RESOURCE_URI)).isInstanceOf(McpError.class)
.hasMessage("Server must be configured with resource capabilities");
@ -357,7 +342,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testNotifyPromptsListChanged() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build();
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build();
assertThatCode(() -> mcpSyncServer.notifyPromptsListChanged()).doesNotThrowAnyException();
@ -366,8 +351,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testAddPromptWithNullSpecification() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(false).build())
.build();
@ -378,9 +362,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testAddPromptWithoutCapability() {
var serverWithoutPrompts = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.build();
var serverWithoutPrompts = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build();
Prompt prompt = new Prompt(TEST_PROMPT_NAME, "Test Prompt", "Test Prompt", List.of());
McpServerFeatures.SyncPromptSpecification specification = new McpServerFeatures.SyncPromptSpecification(prompt,
@ -393,9 +375,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testRemovePromptWithoutCapability() {
var serverWithoutPrompts = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
.build();
var serverWithoutPrompts = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build();
assertThatThrownBy(() -> serverWithoutPrompts.removePrompt(TEST_PROMPT_NAME)).isInstanceOf(McpError.class)
.hasMessage("Server must be configured with prompt capabilities");
@ -408,8 +388,7 @@ public abstract class AbstractMcpSyncServerTests {
(exchange, req) -> new GetPromptResult("Test prompt description", List
.of(new PromptMessage(McpSchema.Role.ASSISTANT, new McpSchema.TextContent("Test content")))));
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(true).build())
.prompts(specification)
.build();
@ -421,8 +400,7 @@ public abstract class AbstractMcpSyncServerTests {
@Test
void testRemoveNonexistentPrompt() {
var mcpSyncServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var mcpSyncServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.capabilities(ServerCapabilities.builder().prompts(true).build())
.build();
@ -442,8 +420,7 @@ public abstract class AbstractMcpSyncServerTests {
var rootsReceived = new McpSchema.Root[1];
var consumerCalled = new boolean[1];
var singleConsumerServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var singleConsumerServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.rootsChangeHandlers(List.of((exchange, roots) -> {
consumerCalled[0] = true;
if (!roots.isEmpty()) {
@ -461,8 +438,7 @@ public abstract class AbstractMcpSyncServerTests {
var consumer2Called = new boolean[1];
var rootsContent = new List[1];
var multipleConsumersServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var multipleConsumersServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.rootsChangeHandlers(List.of((exchange, roots) -> {
consumer1Called[0] = true;
rootsContent[0] = roots;
@ -474,8 +450,7 @@ public abstract class AbstractMcpSyncServerTests {
onClose();
// Test error handling
var errorHandlingServer = McpServer.sync(createMcpTransportProvider())
.serverInfo("test-server", "1.0.0")
var errorHandlingServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0")
.rootsChangeHandlers(List.of((exchange, roots) -> {
throw new RuntimeException("Test error");
}))
@ -486,7 +461,7 @@ public abstract class AbstractMcpSyncServerTests {
onClose();
// Test without consumers
var noConsumersServer = McpServer.sync(createMcpTransportProvider()).serverInfo("test-server", "1.0.0").build();
var noConsumersServer = prepareSyncServerBuilder().serverInfo("test-server", "1.0.0").build();
assertThat(noConsumersServer).isNotNull();
assertThatCode(() -> noConsumersServer.closeGracefully()).doesNotThrowAnyException();

View file

@ -0,0 +1,50 @@
package io.modelcontextprotocol.server;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import java.util.Map;
class DefaultMcpStatelessServerHandler implements McpStatelessServerHandler {
private static final Logger logger = LoggerFactory.getLogger(DefaultMcpStatelessServerHandler.class);
Map<String, McpStatelessRequestHandler<?>> requestHandlers;
Map<String, McpStatelessNotificationHandler> notificationHandlers;
public DefaultMcpStatelessServerHandler(Map<String, McpStatelessRequestHandler<?>> requestHandlers,
Map<String, McpStatelessNotificationHandler> notificationHandlers) {
this.requestHandlers = requestHandlers;
this.notificationHandlers = notificationHandlers;
}
@Override
public Mono<McpSchema.JSONRPCResponse> handleRequest(McpTransportContext transportContext,
McpSchema.JSONRPCRequest request) {
McpStatelessRequestHandler<?> requestHandler = this.requestHandlers.get(request.method());
if (requestHandler == null) {
return Mono.error(new McpError("Missing handler for request type: " + request.method()));
}
return requestHandler.handle(transportContext, request.params())
.map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null))
.onErrorResume(t -> Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null,
new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR, t.getMessage(),
null))));
}
@Override
public Mono<Void> handleNotification(McpTransportContext transportContext,
McpSchema.JSONRPCNotification notification) {
McpStatelessNotificationHandler notificationHandler = this.notificationHandlers.get(notification.method());
if (notificationHandler == null) {
logger.warn("Missing handler for notification type: {}", notification.method());
return Mono.empty();
}
return notificationHandler.handle(transportContext, notification.params());
}
}

View file

@ -0,0 +1,45 @@
package io.modelcontextprotocol.server;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Default implementation for {@link McpTransportContext} which uses a Thread-safe map.
* Objects of this kind are mutable.
*
* @author Dariusz Jędrzejczyk
*/
public class DefaultMcpTransportContext implements McpTransportContext {
private final Map<String, Object> storage;
/**
* Create an empty instance.
*/
public DefaultMcpTransportContext() {
this.storage = new ConcurrentHashMap<>();
}
DefaultMcpTransportContext(Map<String, Object> storage) {
this.storage = storage;
}
@Override
public Object get(String key) {
return this.storage.get(key);
}
@Override
public void put(String key, Object value) {
this.storage.put(key, value);
}
/**
* Allows copying the contents.
* @return new instance with the copy of the underlying map
*/
public McpTransportContext copy() {
return new DefaultMcpTransportContext(new ConcurrentHashMap<>(this.storage));
}
}

View file

@ -15,6 +15,9 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction;
import io.modelcontextprotocol.spec.DefaultMcpStreamableServerSessionFactory;
import io.modelcontextprotocol.spec.McpServerTransportProviderBase;
import io.modelcontextprotocol.spec.McpStreamableServerTransportProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -86,7 +89,7 @@ public class McpAsyncServer {
private static final Logger logger = LoggerFactory.getLogger(McpAsyncServer.class);
private final McpServerTransportProvider mcpTransportProvider;
private final McpServerTransportProviderBase mcpTransportProvider;
private final ObjectMapper objectMapper;
@ -139,7 +142,56 @@ public class McpAsyncServer {
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
this.jsonSchemaValidator = jsonSchemaValidator;
Map<String, McpServerSession.RequestHandler<?>> requestHandlers = new HashMap<>();
Map<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
Map<String, McpNotificationHandler> notificationHandlers = prepareNotificationHandlers(features);
mcpTransportProvider.setSessionFactory(transport -> new McpServerSession(UUID.randomUUID().toString(),
requestTimeout, transport, this::asyncInitializeRequestHandler, requestHandlers, notificationHandlers));
}
McpAsyncServer(McpStreamableServerTransportProvider mcpTransportProvider, ObjectMapper objectMapper,
McpServerFeatures.Async features, Duration requestTimeout,
McpUriTemplateManagerFactory uriTemplateManagerFactory, JsonSchemaValidator jsonSchemaValidator) {
this.mcpTransportProvider = mcpTransportProvider;
this.objectMapper = objectMapper;
this.serverInfo = features.serverInfo();
this.serverCapabilities = features.serverCapabilities();
this.instructions = features.instructions();
this.tools.addAll(withStructuredOutputHandling(jsonSchemaValidator, features.tools()));
this.resources.putAll(features.resources());
this.resourceTemplates.addAll(features.resourceTemplates());
this.prompts.putAll(features.prompts());
this.completions.putAll(features.completions());
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
this.jsonSchemaValidator = jsonSchemaValidator;
Map<String, McpRequestHandler<?>> requestHandlers = prepareRequestHandlers();
Map<String, McpNotificationHandler> notificationHandlers = prepareNotificationHandlers(features);
mcpTransportProvider.setSessionFactory(new DefaultMcpStreamableServerSessionFactory(requestTimeout,
this::asyncInitializeRequestHandler, requestHandlers, notificationHandlers));
}
private Map<String, McpNotificationHandler> prepareNotificationHandlers(McpServerFeatures.Async features) {
Map<String, McpNotificationHandler> notificationHandlers = new HashMap<>();
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (exchange, params) -> Mono.empty());
List<BiFunction<McpAsyncServerExchange, List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers = features
.rootsChangeConsumers();
if (Utils.isEmpty(rootsChangeConsumers)) {
rootsChangeConsumers = List.of((exchange, 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));
return notificationHandlers;
}
private Map<String, McpRequestHandler<?>> prepareRequestHandlers() {
Map<String, McpRequestHandler<?>> requestHandlers = new HashMap<>();
// Initialize request handlers for standard MCP methods
@ -174,25 +226,7 @@ public class McpAsyncServer {
if (this.serverCapabilities.completions() != null) {
requestHandlers.put(McpSchema.METHOD_COMPLETION_COMPLETE, completionCompleteRequestHandler());
}
Map<String, McpServerSession.NotificationHandler> notificationHandlers = new HashMap<>();
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (exchange, params) -> Mono.empty());
List<BiFunction<McpAsyncServerExchange, List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers = features
.rootsChangeConsumers();
if (Utils.isEmpty(rootsChangeConsumers)) {
rootsChangeConsumers = List.of((exchange, 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));
mcpTransportProvider.setSessionFactory(
transport -> new McpServerSession(UUID.randomUUID().toString(), requestTimeout, transport,
this::asyncInitializeRequestHandler, Mono::empty, requestHandlers, notificationHandlers));
return requestHandlers;
}
// ---------------------------------------
@ -258,7 +292,7 @@ public class McpAsyncServer {
this.mcpTransportProvider.close();
}
private McpServerSession.NotificationHandler asyncRootsListChangedNotificationHandler(
private McpNotificationHandler asyncRootsListChangedNotificationHandler(
List<BiFunction<McpAsyncServerExchange, List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers) {
return (exchange, params) -> exchange.listRoots()
.flatMap(listRootsResult -> Flux.fromIterable(rootsChangeConsumers)
@ -450,7 +484,7 @@ public class McpAsyncServer {
return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null);
}
private McpServerSession.RequestHandler<McpSchema.ListToolsResult> toolsListRequestHandler() {
private McpRequestHandler<McpSchema.ListToolsResult> toolsListRequestHandler() {
return (exchange, params) -> {
List<Tool> tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList();
@ -458,7 +492,7 @@ public class McpAsyncServer {
};
}
private McpServerSession.RequestHandler<CallToolResult> toolsCallRequestHandler() {
private McpRequestHandler<CallToolResult> toolsCallRequestHandler() {
return (exchange, params) -> {
McpSchema.CallToolRequest callToolRequest = objectMapper.convertValue(params,
new TypeReference<McpSchema.CallToolRequest>() {
@ -551,7 +585,7 @@ public class McpAsyncServer {
resourcesUpdatedNotification);
}
private McpServerSession.RequestHandler<McpSchema.ListResourcesResult> resourcesListRequestHandler() {
private McpRequestHandler<McpSchema.ListResourcesResult> resourcesListRequestHandler() {
return (exchange, params) -> {
var resourceList = this.resources.values()
.stream()
@ -561,7 +595,7 @@ public class McpAsyncServer {
};
}
private McpServerSession.RequestHandler<McpSchema.ListResourceTemplatesResult> resourceTemplateListRequestHandler() {
private McpRequestHandler<McpSchema.ListResourceTemplatesResult> resourceTemplateListRequestHandler() {
return (exchange, params) -> Mono
.just(new McpSchema.ListResourceTemplatesResult(this.getResourceTemplates(), null));
@ -585,7 +619,7 @@ public class McpAsyncServer {
return list;
}
private McpServerSession.RequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
private McpRequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
return (exchange, params) -> {
McpSchema.ReadResourceRequest resourceRequest = objectMapper.convertValue(params,
new TypeReference<McpSchema.ReadResourceRequest>() {
@ -678,7 +712,7 @@ public class McpAsyncServer {
return this.mcpTransportProvider.notifyClients(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null);
}
private McpServerSession.RequestHandler<McpSchema.ListPromptsResult> promptsListRequestHandler() {
private McpRequestHandler<McpSchema.ListPromptsResult> promptsListRequestHandler() {
return (exchange, params) -> {
// TODO: Implement pagination
// McpSchema.PaginatedRequest request = objectMapper.convertValue(params,
@ -694,7 +728,7 @@ public class McpAsyncServer {
};
}
private McpServerSession.RequestHandler<McpSchema.GetPromptResult> promptsGetRequestHandler() {
private McpRequestHandler<McpSchema.GetPromptResult> promptsGetRequestHandler() {
return (exchange, params) -> {
McpSchema.GetPromptRequest promptRequest = objectMapper.convertValue(params,
new TypeReference<McpSchema.GetPromptRequest>() {
@ -740,7 +774,7 @@ public class McpAsyncServer {
loggingMessageNotification);
}
private McpServerSession.RequestHandler<Object> setLoggerRequestHandler() {
private McpRequestHandler<Object> setLoggerRequestHandler() {
return (exchange, params) -> {
return Mono.defer(() -> {
@ -759,7 +793,7 @@ public class McpAsyncServer {
};
}
private McpServerSession.RequestHandler<McpSchema.CompleteResult> completionCompleteRequestHandler() {
private McpRequestHandler<McpSchema.CompleteResult> completionCompleteRequestHandler() {
return (exchange, params) -> {
McpSchema.CompleteRequest request = parseCompletionParams(params);

View file

@ -9,10 +9,11 @@ import java.util.Collections;
import com.fasterxml.jackson.core.type.TypeReference;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpLoggableSession;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpSession;
import io.modelcontextprotocol.util.Assert;
import reactor.core.publisher.Mono;
@ -25,13 +26,15 @@ import reactor.core.publisher.Mono;
*/
public class McpAsyncServerExchange {
private final McpServerSession session;
private final String sessionId;
private final McpLoggableSession session;
private final McpSchema.ClientCapabilities clientCapabilities;
private final McpSchema.Implementation clientInfo;
private volatile LoggingLevel minLoggingLevel = LoggingLevel.INFO;
private final McpTransportContext transportContext;
private static final TypeReference<McpSchema.CreateMessageResult> CREATE_MESSAGE_RESULT_TYPE_REF = new TypeReference<>() {
};
@ -51,12 +54,39 @@ public class McpAsyncServerExchange {
* @param clientCapabilities The client capabilities that define the supported
* features and functionality.
* @param clientInfo The client implementation information.
* @deprecated Use
* {@link #McpAsyncServerExchange(String, McpLoggableSession, McpSchema.ClientCapabilities, McpSchema.Implementation, McpTransportContext)}
*/
public McpAsyncServerExchange(McpServerSession session, McpSchema.ClientCapabilities clientCapabilities,
@Deprecated
public McpAsyncServerExchange(McpSession session, McpSchema.ClientCapabilities clientCapabilities,
McpSchema.Implementation clientInfo) {
this.sessionId = null;
if (!(session instanceof McpLoggableSession)) {
throw new IllegalArgumentException("Expecting session to be a McpLoggableSession instance");
}
this.session = (McpLoggableSession) session;
this.clientCapabilities = clientCapabilities;
this.clientInfo = clientInfo;
this.transportContext = McpTransportContext.EMPTY;
}
/**
* Create a new asynchronous exchange with the client.
* @param session The server session representing a 1-1 interaction.
* @param clientCapabilities The client capabilities that define the supported
* features and functionality.
* @param clientInfo The client implementation information.
* @param transportContext context associated with the client as extracted from the
* transport
*/
public McpAsyncServerExchange(String sessionId, McpLoggableSession session,
McpSchema.ClientCapabilities clientCapabilities, McpSchema.Implementation clientInfo,
McpTransportContext transportContext) {
this.sessionId = sessionId;
this.session = session;
this.clientCapabilities = clientCapabilities;
this.clientInfo = clientInfo;
this.transportContext = transportContext;
}
/**
@ -75,6 +105,24 @@ public class McpAsyncServerExchange {
return this.clientInfo;
}
/**
* Provides the {@link McpTransportContext} associated with the transport layer. For
* HTTP transports it can contain the metadata associated with the HTTP request that
* triggered the processing.
* @return the transport context object
*/
public McpTransportContext transportContext() {
return this.transportContext;
}
/**
* Provides the Session ID.
* @return session ID string
*/
public String sessionId() {
return this.sessionId;
}
/**
* 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
@ -170,7 +218,7 @@ public class McpAsyncServerExchange {
}
return Mono.defer(() -> {
if (this.isNotificationForLevelAllowed(loggingMessageNotification.level())) {
if (this.session.isNotificationForLevelAllowed(loggingMessageNotification.level())) {
return this.session.sendNotification(McpSchema.METHOD_NOTIFICATION_MESSAGE, loggingMessageNotification);
}
return Mono.empty();
@ -205,11 +253,7 @@ public class McpAsyncServerExchange {
*/
void setMinLoggingLevel(LoggingLevel minLoggingLevel) {
Assert.notNull(minLoggingLevel, "minLoggingLevel must not be null");
this.minLoggingLevel = minLoggingLevel;
}
private boolean isNotificationForLevelAllowed(LoggingLevel loggingLevel) {
return loggingLevel.level() >= this.minLoggingLevel.level();
this.session.setMinLoggingLevel(minLoggingLevel);
}
}

View file

@ -0,0 +1,18 @@
package io.modelcontextprotocol.server;
import io.modelcontextprotocol.spec.McpSchema;
import reactor.core.publisher.Mono;
/**
* Request handler for the initialization request.
*/
public interface McpInitRequestHandler {
/**
* Handles the initialization request.
* @param initializeRequest the initialization request by the client
* @return a Mono that will emit the result of the initialization
*/
Mono<McpSchema.InitializeResult> handle(McpSchema.InitializeRequest initializeRequest);
}

View file

@ -0,0 +1,19 @@
package io.modelcontextprotocol.server;
import reactor.core.publisher.Mono;
/**
* A handler for client-initiated notifications.
*/
public interface McpNotificationHandler {
/**
* Handles a notification from the client.
* @param exchange the exchange associated with the client that allows calling back to
* the connected client or inspecting its capabilities.
* @param params the parameters of the notification.
* @return a Mono that completes once the notification is handled.
*/
Mono<Void> handle(McpAsyncServerExchange exchange, Object params);
}

View file

@ -0,0 +1,22 @@
package io.modelcontextprotocol.server;
import reactor.core.publisher.Mono;
/**
* A handler for client-initiated requests.
*
* @param <T> the type of the response that is expected as a result of handling the
* request.
*/
public interface McpRequestHandler<T> {
/**
* Handles a request from the client.
* @param exchange the exchange associated with the client that allows calling back to
* the connected client or inspecting its capabilities.
* @param params the parameters of the request.
* @return a Mono that will emit the response to the request.
*/
Mono<T> handle(McpAsyncServerExchange exchange, Object params);
}

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