refactor: remove deprecated 0.7.0 code

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

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

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

View file

@ -14,10 +14,10 @@ import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
import io.modelcontextprotocol.spec.ClientMcpTransport;
import io.modelcontextprotocol.spec.McpClientSession;
import io.modelcontextprotocol.spec.McpClientSession.NotificationHandler;
import io.modelcontextprotocol.spec.McpClientSession.RequestHandler;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
@ -153,7 +153,7 @@ public class McpAsyncClient {
* @param initializationTimeout the max timeout to await for the client-server
* @param features the MCP Client supported features.
*/
McpAsyncClient(ClientMcpTransport transport, Duration requestTimeout, Duration initializationTimeout,
McpAsyncClient(McpClientTransport transport, Duration requestTimeout, Duration initializationTimeout,
McpClientFeatures.Async features) {
Assert.notNull(transport, "Transport must not be null");

View file

@ -12,7 +12,6 @@ import java.util.Map;
import java.util.function.Consumer;
import java.util.function.Function;
import io.modelcontextprotocol.spec.ClientMcpTransport;
import io.modelcontextprotocol.spec.McpClientTransport;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpTransport;
@ -102,26 +101,6 @@ import reactor.core.publisher.Mono;
*/
public interface McpClient {
/**
* Start building a synchronous MCP client with the specified transport layer. The
* synchronous MCP client provides blocking operations. Synchronous clients wait for
* each operation to complete before returning, making them simpler to use but
* potentially less performant for concurrent operations. The transport layer handles
* the low-level communication between client and server using protocols like stdio or
* Server-Sent Events (SSE).
* @param transport The transport layer implementation for MCP communication. Common
* implementations include {@code StdioClientTransport} for stdio-based communication
* and {@code SseClientTransport} for SSE-based communication.
* @return A new builder instance for configuring the client
* @throws IllegalArgumentException if transport is null
* @deprecated This method will be removed in 0.9.0. Use
* {@link #sync(McpClientTransport)}
*/
@Deprecated
static SyncSpec sync(ClientMcpTransport transport) {
return new SyncSpec(transport);
}
/**
* Start building a synchronous MCP client with the specified transport layer. The
* synchronous MCP client provides blocking operations. Synchronous clients wait for
@ -139,26 +118,6 @@ public interface McpClient {
return new SyncSpec(transport);
}
/**
* Start building an asynchronous MCP client with the specified transport layer. The
* asynchronous MCP client provides non-blocking operations. Asynchronous clients
* return reactive primitives (Mono/Flux) immediately, allowing for concurrent
* operations and reactive programming patterns. The transport layer handles the
* low-level communication between client and server using protocols like stdio or
* Server-Sent Events (SSE).
* @param transport The transport layer implementation for MCP communication. Common
* implementations include {@code StdioClientTransport} for stdio-based communication
* and {@code SseClientTransport} for SSE-based communication.
* @return A new builder instance for configuring the client
* @throws IllegalArgumentException if transport is null
* @deprecated This method will be removed in 0.9.0. Use
* {@link #async(McpClientTransport)}
*/
@Deprecated
static AsyncSpec async(ClientMcpTransport transport) {
return new AsyncSpec(transport);
}
/**
* Start building an asynchronous MCP client with the specified transport layer. The
* asynchronous MCP client provides non-blocking operations. Asynchronous clients
@ -194,7 +153,7 @@ public interface McpClient {
*/
class SyncSpec {
private final ClientMcpTransport transport;
private final McpClientTransport transport;
private Duration requestTimeout = Duration.ofSeconds(20); // Default timeout
@ -216,7 +175,7 @@ public interface McpClient {
private Function<CreateMessageRequest, CreateMessageResult> samplingHandler;
private SyncSpec(ClientMcpTransport transport) {
private SyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
}
@ -433,7 +392,7 @@ public interface McpClient {
*/
class AsyncSpec {
private final ClientMcpTransport transport;
private final McpClientTransport transport;
private Duration requestTimeout = Duration.ofSeconds(20); // Default timeout
@ -455,7 +414,7 @@ public interface McpClient {
private Function<CreateMessageRequest, Mono<CreateMessageResult>> samplingHandler;
private AsyncSpec(ClientMcpTransport transport) {
private AsyncSpec(McpClientTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
}

View file

@ -66,12 +66,8 @@ public class McpSyncClient implements AutoCloseable {
* Create a new McpSyncClient with the given delegate.
* @param delegate the asynchronous kernel on top of which this synchronous client
* provides a blocking API.
* @deprecated This method will be removed in 0.9.0. Use
* {@link McpClient#sync(McpClientTransport)} to obtain an instance.
*/
@Deprecated
// TODO make the constructor package private post-deprecation
public McpSyncClient(McpAsyncClient delegate) {
McpSyncClient(McpAsyncClient delegate) {
Assert.notNull(delegate, "The delegate can not be null");
this.delegate = delegate;
}

View file

@ -4,9 +4,7 @@
package io.modelcontextprotocol.server;
import java.time.Duration;
import java.util.HashMap;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@ -14,21 +12,18 @@ import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction;
import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpClientSession;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.ClientCapabilities;
import io.modelcontextprotocol.spec.McpSchema.LoggingLevel;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
import io.modelcontextprotocol.spec.McpSchema.Tool;
import io.modelcontextprotocol.spec.ServerMcpTransport;
import io.modelcontextprotocol.spec.McpServerSession;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.util.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -86,19 +81,6 @@ public class McpAsyncServer {
this.delegate = null;
}
/**
* Create a new McpAsyncServer with the given transport and capabilities.
* @param mcpTransport The transport layer implementation for MCP communication.
* @param features The MCP server supported features.
* @deprecated This constructor will beremoved in 0.9.0. Use
* {@link #McpAsyncServer(McpServerTransportProvider, ObjectMapper, McpServerFeatures.Async)}
* instead.
*/
@Deprecated
McpAsyncServer(ServerMcpTransport mcpTransport, McpServerFeatures.Async features) {
this.delegate = new LegacyAsyncServer(mcpTransport, features);
}
/**
* Create a new McpAsyncServer with the given transport provider and capabilities.
* @param mcpTransportProvider The transport layer implementation for MCP
@ -127,28 +109,6 @@ public class McpAsyncServer {
return this.delegate.getServerInfo();
}
/**
* Get the client capabilities that define the supported features and functionality.
* @return The client capabilities
* @deprecated This will be removed in 0.9.0. Use
* {@link McpAsyncServerExchange#getClientCapabilities()}.
*/
@Deprecated
public ClientCapabilities getClientCapabilities() {
return this.delegate.getClientCapabilities();
}
/**
* Get the client implementation information.
* @return The client implementation details
* @deprecated This will be removed in 0.9.0. Use
* {@link McpAsyncServerExchange#getClientInfo()}.
*/
@Deprecated
public McpSchema.Implementation getClientInfo() {
return this.delegate.getClientInfo();
}
/**
* Gracefully closes the server, allowing any in-progress operations to complete.
* @return A Mono that completes when the server has been closed
@ -164,45 +124,9 @@ public class McpAsyncServer {
this.delegate.close();
}
/**
* Retrieves the list of all roots provided by the client.
* @return A Mono that emits the list of roots result.
* @deprecated This will be removed in 0.9.0. Use
* {@link McpAsyncServerExchange#listRoots()}.
*/
@Deprecated
public Mono<McpSchema.ListRootsResult> listRoots() {
return this.delegate.listRoots(null);
}
/**
* Retrieves a paginated list of roots provided by the server.
* @param cursor Optional pagination cursor from a previous list request
* @return A Mono that emits the list of roots result containing
* @deprecated This will be removed in 0.9.0. Use
* {@link McpAsyncServerExchange#listRoots(String)}.
*/
@Deprecated
public Mono<McpSchema.ListRootsResult> listRoots(String cursor) {
return this.delegate.listRoots(cursor);
}
// ---------------------------------------
// Tool Management
// ---------------------------------------
/**
* Add a new tool registration at runtime.
* @param toolRegistration The tool registration to add
* @return Mono that completes when clients have been notified of the change
* @deprecated This method will be removed in 0.9.0. Use
* {@link #addTool(McpServerFeatures.AsyncToolSpecification)}.
*/
@Deprecated
public Mono<Void> addTool(McpServerFeatures.AsyncToolRegistration toolRegistration) {
return this.delegate.addTool(toolRegistration);
}
/**
* Add a new tool specification at runtime.
* @param toolSpecification The tool specification to add
@ -232,19 +156,6 @@ public class McpAsyncServer {
// ---------------------------------------
// Resource Management
// ---------------------------------------
/**
* Add a new resource handler at runtime.
* @param resourceHandler The resource handler to add
* @return Mono that completes when clients have been notified of the change
* @deprecated This method will be removed in 0.9.0. Use
* {@link #addResource(McpServerFeatures.AsyncResourceSpecification)}.
*/
@Deprecated
public Mono<Void> addResource(McpServerFeatures.AsyncResourceRegistration resourceHandler) {
return this.delegate.addResource(resourceHandler);
}
/**
* Add a new resource handler at runtime.
* @param resourceHandler The resource handler to add
@ -274,19 +185,6 @@ public class McpAsyncServer {
// ---------------------------------------
// Prompt Management
// ---------------------------------------
/**
* Add a new prompt handler at runtime.
* @param promptRegistration The prompt handler to add
* @return Mono that completes when clients have been notified of the change
* @deprecated This method will be removed in 0.9.0. Use
* {@link #addPrompt(McpServerFeatures.AsyncPromptSpecification)}.
*/
@Deprecated
public Mono<Void> addPrompt(McpServerFeatures.AsyncPromptRegistration promptRegistration) {
return this.delegate.addPrompt(promptRegistration);
}
/**
* Add a new prompt handler at runtime.
* @param promptSpecification The prompt handler to add
@ -330,33 +228,6 @@ public class McpAsyncServer {
// ---------------------------------------
// Sampling
// ---------------------------------------
/**
* Create a new message using the sampling capabilities of the client. The Model
* Context Protocol (MCP) provides a standardized way for servers to request LLM
* sampling (“completions” or “generations”) from language models via clients. This
* flow allows clients to maintain control over model access, selection, and
* permissions while enabling servers to leverage AI capabilities—with no server API
* keys necessary. Servers can request text or image-based interactions and optionally
* include context from MCP servers in their prompts.
* @param createMessageRequest The request to create a new message
* @return A Mono that completes when the message has been created
* @throws McpError if the client has not been initialized or does not support
* sampling capabilities
* @throws McpError if the client does not support the createMessage method
* @see McpSchema.CreateMessageRequest
* @see McpSchema.CreateMessageResult
* @see <a href=
* "https://spec.modelcontextprotocol.io/specification/client/sampling/">Sampling
* Specification</a>
* @deprecated This will be removed in 0.9.0. Use
* {@link McpAsyncServerExchange#createMessage(McpSchema.CreateMessageRequest)}.
*/
@Deprecated
public Mono<McpSchema.CreateMessageResult> createMessage(McpSchema.CreateMessageRequest createMessageRequest) {
return this.delegate.createMessage(createMessageRequest);
}
/**
* This method is package-private and used for test only. Should not be called by user
* code.
@ -492,18 +363,6 @@ public class McpAsyncServer {
return this.serverInfo;
}
@Override
@Deprecated
public ClientCapabilities getClientCapabilities() {
throw new IllegalStateException("This method is deprecated and should not be called");
}
@Override
@Deprecated
public McpSchema.Implementation getClientInfo() {
throw new IllegalStateException("This method is deprecated and should not be called");
}
@Override
public Mono<Void> closeGracefully() {
return this.mcpTransportProvider.closeGracefully();
@ -514,18 +373,6 @@ public class McpAsyncServer {
this.mcpTransportProvider.close();
}
@Override
@Deprecated
public Mono<McpSchema.ListRootsResult> listRoots() {
return this.listRoots(null);
}
@Override
@Deprecated
public Mono<McpSchema.ListRootsResult> listRoots(String cursor) {
return Mono.error(new RuntimeException("Not implemented"));
}
private McpServerSession.NotificationHandler asyncRootsListChangedNotificationHandler(
List<BiFunction<McpAsyncServerExchange, List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers) {
return (exchange, params) -> exchange.listRoots()
@ -574,11 +421,6 @@ public class McpAsyncServer {
});
}
@Override
public Mono<Void> addTool(McpServerFeatures.AsyncToolRegistration toolRegistration) {
return this.addTool(toolRegistration.toSpecification());
}
@Override
public Mono<Void> removeTool(String toolName) {
if (toolName == null) {
@ -661,11 +503,6 @@ public class McpAsyncServer {
});
}
@Override
public Mono<Void> addResource(McpServerFeatures.AsyncResourceRegistration resourceHandler) {
return this.addResource(resourceHandler.toSpecification());
}
@Override
public Mono<Void> removeResource(String resourceUri) {
if (resourceUri == null) {
@ -756,11 +593,6 @@ public class McpAsyncServer {
});
}
@Override
public Mono<Void> addPrompt(McpServerFeatures.AsyncPromptRegistration promptRegistration) {
return this.addPrompt(promptRegistration.toSpecification());
}
@Override
public Mono<Void> removePrompt(String promptName) {
if (promptName == null) {
@ -859,648 +691,6 @@ public class McpAsyncServer {
// ---------------------------------------
@Override
@Deprecated
public Mono<McpSchema.CreateMessageResult> createMessage(McpSchema.CreateMessageRequest createMessageRequest) {
return Mono.error(new RuntimeException("Not implemented"));
}
@Override
void setProtocolVersions(List<String> protocolVersions) {
this.protocolVersions = protocolVersions;
}
}
private static final class LegacyAsyncServer extends McpAsyncServer {
/**
* The MCP session implementation that manages bidirectional JSON-RPC
* communication between clients and servers.
*/
private final McpClientSession mcpSession;
private final ServerMcpTransport transport;
private final McpSchema.ServerCapabilities serverCapabilities;
private final McpSchema.Implementation serverInfo;
private McpSchema.ClientCapabilities clientCapabilities;
private McpSchema.Implementation clientInfo;
/**
* Thread-safe list of tool handlers that can be modified at runtime.
*/
private final CopyOnWriteArrayList<McpServerFeatures.AsyncToolSpecification> tools = new CopyOnWriteArrayList<>();
private final CopyOnWriteArrayList<McpSchema.ResourceTemplate> resourceTemplates = new CopyOnWriteArrayList<>();
private final ConcurrentHashMap<String, McpServerFeatures.AsyncResourceSpecification> resources = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, McpServerFeatures.AsyncPromptSpecification> prompts = new ConcurrentHashMap<>();
private LoggingLevel minLoggingLevel = LoggingLevel.DEBUG;
/**
* Supported protocol versions.
*/
private List<String> protocolVersions = List.of(McpSchema.LATEST_PROTOCOL_VERSION);
/**
* Create a new McpAsyncServer with the given transport and capabilities.
* @param mcpTransport The transport layer implementation for MCP communication.
* @param features The MCP server supported features.
*/
LegacyAsyncServer(ServerMcpTransport mcpTransport, McpServerFeatures.Async features) {
this.serverInfo = features.serverInfo();
this.serverCapabilities = features.serverCapabilities();
this.tools.addAll(features.tools());
this.resources.putAll(features.resources());
this.resourceTemplates.addAll(features.resourceTemplates());
this.prompts.putAll(features.prompts());
Map<String, McpClientSession.RequestHandler<?>> requestHandlers = new HashMap<>();
// Initialize request handlers for standard MCP methods
requestHandlers.put(McpSchema.METHOD_INITIALIZE, asyncInitializeRequestHandler());
// Ping MUST respond with an empty data, but not NULL response.
requestHandlers.put(McpSchema.METHOD_PING, (params) -> Mono.just(Map.of()));
// Add tools API handlers if the tool capability is enabled
if (this.serverCapabilities.tools() != null) {
requestHandlers.put(McpSchema.METHOD_TOOLS_LIST, toolsListRequestHandler());
requestHandlers.put(McpSchema.METHOD_TOOLS_CALL, toolsCallRequestHandler());
}
// Add resources API handlers if provided
if (this.serverCapabilities.resources() != null) {
requestHandlers.put(McpSchema.METHOD_RESOURCES_LIST, resourcesListRequestHandler());
requestHandlers.put(McpSchema.METHOD_RESOURCES_READ, resourcesReadRequestHandler());
requestHandlers.put(McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, resourceTemplateListRequestHandler());
}
// Add prompts API handlers if provider exists
if (this.serverCapabilities.prompts() != null) {
requestHandlers.put(McpSchema.METHOD_PROMPT_LIST, promptsListRequestHandler());
requestHandlers.put(McpSchema.METHOD_PROMPT_GET, promptsGetRequestHandler());
}
// Add logging API handlers if the logging capability is enabled
if (this.serverCapabilities.logging() != null) {
requestHandlers.put(McpSchema.METHOD_LOGGING_SET_LEVEL, setLoggerRequestHandler());
}
Map<String, McpClientSession.NotificationHandler> notificationHandlers = new HashMap<>();
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_INITIALIZED, (params) -> Mono.empty());
List<BiFunction<McpAsyncServerExchange, List<McpSchema.Root>, Mono<Void>>> rootsChangeHandlers = features
.rootsChangeConsumers();
List<Function<List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers = rootsChangeHandlers.stream()
.map(handler -> (Function<List<McpSchema.Root>, Mono<Void>>) (roots) -> handler.apply(null, roots))
.toList();
if (Utils.isEmpty(rootsChangeConsumers)) {
rootsChangeConsumers = List.of((roots) -> Mono.fromRunnable(() -> logger.warn(
"Roots list changed notification, but no consumers provided. Roots list changed: {}", roots)));
}
notificationHandlers.put(McpSchema.METHOD_NOTIFICATION_ROOTS_LIST_CHANGED,
asyncRootsListChangedNotificationHandler(rootsChangeConsumers));
this.transport = mcpTransport;
this.mcpSession = new McpClientSession(Duration.ofSeconds(10), mcpTransport, requestHandlers,
notificationHandlers);
}
@Override
public Mono<Void> addTool(McpServerFeatures.AsyncToolSpecification toolSpecification) {
throw new IllegalArgumentException(
"McpAsyncServer configured with legacy " + "transport. Use McpServerTransportProvider instead.");
}
@Override
public Mono<Void> addResource(McpServerFeatures.AsyncResourceSpecification resourceHandler) {
throw new IllegalArgumentException(
"McpAsyncServer configured with legacy " + "transport. Use McpServerTransportProvider instead.");
}
@Override
public Mono<Void> addPrompt(McpServerFeatures.AsyncPromptSpecification promptSpecification) {
throw new IllegalArgumentException(
"McpAsyncServer configured with legacy " + "transport. Use McpServerTransportProvider instead.");
}
// ---------------------------------------
// Lifecycle Management
// ---------------------------------------
private McpClientSession.RequestHandler<McpSchema.InitializeResult> asyncInitializeRequestHandler() {
return params -> {
McpSchema.InitializeRequest initializeRequest = transport.unmarshalFrom(params,
new TypeReference<McpSchema.InitializeRequest>() {
});
this.clientCapabilities = initializeRequest.capabilities();
this.clientInfo = initializeRequest.clientInfo();
logger.info("Client initialize request - Protocol: {}, Capabilities: {}, Info: {}",
initializeRequest.protocolVersion(), initializeRequest.capabilities(),
initializeRequest.clientInfo());
// The server MUST respond with the highest protocol version it supports
// if
// it does not support the requested (e.g. Client) version.
String serverProtocolVersion = this.protocolVersions.get(this.protocolVersions.size() - 1);
if (this.protocolVersions.contains(initializeRequest.protocolVersion())) {
// If the server supports the requested protocol version, it MUST
// respond
// with the same version.
serverProtocolVersion = initializeRequest.protocolVersion();
}
else {
logger.warn(
"Client requested unsupported protocol version: {}, so the server will sugggest the {} version instead",
initializeRequest.protocolVersion(), serverProtocolVersion);
}
return Mono.just(new McpSchema.InitializeResult(serverProtocolVersion, this.serverCapabilities,
this.serverInfo, null));
};
}
/**
* Get the server capabilities that define the supported features and
* functionality.
* @return The server capabilities
*/
public McpSchema.ServerCapabilities getServerCapabilities() {
return this.serverCapabilities;
}
/**
* Get the server implementation information.
* @return The server implementation details
*/
public McpSchema.Implementation getServerInfo() {
return this.serverInfo;
}
/**
* Get the client capabilities that define the supported features and
* functionality.
* @return The client capabilities
*/
public ClientCapabilities getClientCapabilities() {
return this.clientCapabilities;
}
/**
* Get the client implementation information.
* @return The client implementation details
*/
public McpSchema.Implementation getClientInfo() {
return this.clientInfo;
}
/**
* Gracefully closes the server, allowing any in-progress operations to complete.
* @return A Mono that completes when the server has been closed
*/
public Mono<Void> closeGracefully() {
return this.mcpSession.closeGracefully();
}
/**
* Close the server immediately.
*/
public void close() {
this.mcpSession.close();
}
private static final TypeReference<McpSchema.ListRootsResult> LIST_ROOTS_RESULT_TYPE_REF = new TypeReference<>() {
};
/**
* Retrieves the list of all roots provided by the client.
* @return A Mono that emits the list of roots result.
*/
public Mono<McpSchema.ListRootsResult> listRoots() {
return this.listRoots(null);
}
/**
* Retrieves a paginated list of roots provided by the server.
* @param cursor Optional pagination cursor from a previous list request
* @return A Mono that emits the list of roots result containing
*/
public Mono<McpSchema.ListRootsResult> listRoots(String cursor) {
return this.mcpSession.sendRequest(McpSchema.METHOD_ROOTS_LIST, new McpSchema.PaginatedRequest(cursor),
LIST_ROOTS_RESULT_TYPE_REF);
}
private McpClientSession.NotificationHandler asyncRootsListChangedNotificationHandler(
List<Function<List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers) {
return params -> listRoots().flatMap(listRootsResult -> Flux.fromIterable(rootsChangeConsumers)
.flatMap(consumer -> consumer.apply(listRootsResult.roots()))
.onErrorResume(error -> {
logger.error("Error handling roots list change notification", error);
return Mono.empty();
})
.then());
}
// ---------------------------------------
// Tool Management
// ---------------------------------------
/**
* Add a new tool registration at runtime.
* @param toolRegistration The tool registration to add
* @return Mono that completes when clients have been notified of the change
*/
@Override
public Mono<Void> addTool(McpServerFeatures.AsyncToolRegistration toolRegistration) {
if (toolRegistration == null) {
return Mono.error(new McpError("Tool registration must not be null"));
}
if (toolRegistration.tool() == null) {
return Mono.error(new McpError("Tool must not be null"));
}
if (toolRegistration.call() == null) {
return Mono.error(new McpError("Tool call handler must not be null"));
}
if (this.serverCapabilities.tools() == null) {
return Mono.error(new McpError("Server must be configured with tool capabilities"));
}
return Mono.defer(() -> {
// Check for duplicate tool names
if (this.tools.stream().anyMatch(th -> th.tool().name().equals(toolRegistration.tool().name()))) {
return Mono
.error(new McpError("Tool with name '" + toolRegistration.tool().name() + "' already exists"));
}
this.tools.add(toolRegistration.toSpecification());
logger.debug("Added tool handler: {}", toolRegistration.tool().name());
if (this.serverCapabilities.tools().listChanged()) {
return notifyToolsListChanged();
}
return Mono.empty();
});
}
/**
* Remove a tool handler at runtime.
* @param toolName The name of the tool handler to remove
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> removeTool(String toolName) {
if (toolName == null) {
return Mono.error(new McpError("Tool name must not be null"));
}
if (this.serverCapabilities.tools() == null) {
return Mono.error(new McpError("Server must be configured with tool capabilities"));
}
return Mono.defer(() -> {
boolean removed = this.tools
.removeIf(toolRegistration -> toolRegistration.tool().name().equals(toolName));
if (removed) {
logger.debug("Removed tool handler: {}", toolName);
if (this.serverCapabilities.tools().listChanged()) {
return notifyToolsListChanged();
}
return Mono.empty();
}
return Mono.error(new McpError("Tool with name '" + toolName + "' not found"));
});
}
/**
* Notifies clients that the list of available tools has changed.
* @return A Mono that completes when all clients have been notified
*/
public Mono<Void> notifyToolsListChanged() {
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_TOOLS_LIST_CHANGED, null);
}
private McpClientSession.RequestHandler<McpSchema.ListToolsResult> toolsListRequestHandler() {
return params -> {
List<Tool> tools = this.tools.stream().map(McpServerFeatures.AsyncToolSpecification::tool).toList();
return Mono.just(new McpSchema.ListToolsResult(tools, null));
};
}
private McpClientSession.RequestHandler<CallToolResult> toolsCallRequestHandler() {
return params -> {
McpSchema.CallToolRequest callToolRequest = transport.unmarshalFrom(params,
new TypeReference<McpSchema.CallToolRequest>() {
});
Optional<McpServerFeatures.AsyncToolSpecification> toolRegistration = this.tools.stream()
.filter(tr -> callToolRequest.name().equals(tr.tool().name()))
.findAny();
if (toolRegistration.isEmpty()) {
return Mono.error(new McpError("Tool not found: " + callToolRequest.name()));
}
return toolRegistration.map(tool -> tool.call().apply(null, callToolRequest.arguments()))
.orElse(Mono.error(new McpError("Tool not found: " + callToolRequest.name())));
};
}
// ---------------------------------------
// Resource Management
// ---------------------------------------
/**
* Add a new resource handler at runtime.
* @param resourceHandler The resource handler to add
* @return Mono that completes when clients have been notified of the change
*/
@Override
public Mono<Void> addResource(McpServerFeatures.AsyncResourceRegistration resourceHandler) {
if (resourceHandler == null || resourceHandler.resource() == null) {
return Mono.error(new McpError("Resource must not be null"));
}
if (this.serverCapabilities.resources() == null) {
return Mono.error(new McpError("Server must be configured with resource capabilities"));
}
return Mono.defer(() -> {
if (this.resources.putIfAbsent(resourceHandler.resource().uri(),
resourceHandler.toSpecification()) != null) {
return Mono.error(new McpError(
"Resource with URI '" + resourceHandler.resource().uri() + "' already exists"));
}
logger.debug("Added resource handler: {}", resourceHandler.resource().uri());
if (this.serverCapabilities.resources().listChanged()) {
return notifyResourcesListChanged();
}
return Mono.empty();
});
}
/**
* Remove a resource handler at runtime.
* @param resourceUri The URI of the resource handler to remove
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> removeResource(String resourceUri) {
if (resourceUri == null) {
return Mono.error(new McpError("Resource URI must not be null"));
}
if (this.serverCapabilities.resources() == null) {
return Mono.error(new McpError("Server must be configured with resource capabilities"));
}
return Mono.defer(() -> {
McpServerFeatures.AsyncResourceSpecification removed = this.resources.remove(resourceUri);
if (removed != null) {
logger.debug("Removed resource handler: {}", resourceUri);
if (this.serverCapabilities.resources().listChanged()) {
return notifyResourcesListChanged();
}
return Mono.empty();
}
return Mono.error(new McpError("Resource with URI '" + resourceUri + "' not found"));
});
}
/**
* Notifies clients that the list of available resources has changed.
* @return A Mono that completes when all clients have been notified
*/
public Mono<Void> notifyResourcesListChanged() {
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_RESOURCES_LIST_CHANGED, null);
}
private McpClientSession.RequestHandler<McpSchema.ListResourcesResult> resourcesListRequestHandler() {
return params -> {
var resourceList = this.resources.values()
.stream()
.map(McpServerFeatures.AsyncResourceSpecification::resource)
.toList();
return Mono.just(new McpSchema.ListResourcesResult(resourceList, null));
};
}
private McpClientSession.RequestHandler<McpSchema.ListResourceTemplatesResult> resourceTemplateListRequestHandler() {
return params -> Mono.just(new McpSchema.ListResourceTemplatesResult(this.resourceTemplates, null));
}
private McpClientSession.RequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
return params -> {
McpSchema.ReadResourceRequest resourceRequest = transport.unmarshalFrom(params,
new TypeReference<McpSchema.ReadResourceRequest>() {
});
var resourceUri = resourceRequest.uri();
McpServerFeatures.AsyncResourceSpecification registration = this.resources.get(resourceUri);
if (registration != null) {
return registration.readHandler().apply(null, resourceRequest);
}
return Mono.error(new McpError("Resource not found: " + resourceUri));
};
}
// ---------------------------------------
// Prompt Management
// ---------------------------------------
/**
* Add a new prompt handler at runtime.
* @param promptRegistration The prompt handler to add
* @return Mono that completes when clients have been notified of the change
*/
@Override
public Mono<Void> addPrompt(McpServerFeatures.AsyncPromptRegistration promptRegistration) {
if (promptRegistration == null) {
return Mono.error(new McpError("Prompt registration must not be null"));
}
if (this.serverCapabilities.prompts() == null) {
return Mono.error(new McpError("Server must be configured with prompt capabilities"));
}
return Mono.defer(() -> {
McpServerFeatures.AsyncPromptSpecification registration = this.prompts
.putIfAbsent(promptRegistration.prompt().name(), promptRegistration.toSpecification());
if (registration != null) {
return Mono.error(new McpError(
"Prompt with name '" + promptRegistration.prompt().name() + "' already exists"));
}
logger.debug("Added prompt handler: {}", promptRegistration.prompt().name());
// Servers that declared the listChanged capability SHOULD send a
// notification,
// when the list of available prompts changes
if (this.serverCapabilities.prompts().listChanged()) {
return notifyPromptsListChanged();
}
return Mono.empty();
});
}
/**
* Remove a prompt handler at runtime.
* @param promptName The name of the prompt handler to remove
* @return Mono that completes when clients have been notified of the change
*/
public Mono<Void> removePrompt(String promptName) {
if (promptName == null) {
return Mono.error(new McpError("Prompt name must not be null"));
}
if (this.serverCapabilities.prompts() == null) {
return Mono.error(new McpError("Server must be configured with prompt capabilities"));
}
return Mono.defer(() -> {
McpServerFeatures.AsyncPromptSpecification removed = this.prompts.remove(promptName);
if (removed != null) {
logger.debug("Removed prompt handler: {}", promptName);
// Servers that declared the listChanged capability SHOULD send a
// notification, when the list of available prompts changes
if (this.serverCapabilities.prompts().listChanged()) {
return this.notifyPromptsListChanged();
}
return Mono.empty();
}
return Mono.error(new McpError("Prompt with name '" + promptName + "' not found"));
});
}
/**
* Notifies clients that the list of available prompts has changed.
* @return A Mono that completes when all clients have been notified
*/
public Mono<Void> notifyPromptsListChanged() {
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_PROMPTS_LIST_CHANGED, null);
}
private McpClientSession.RequestHandler<McpSchema.ListPromptsResult> promptsListRequestHandler() {
return params -> {
// TODO: Implement pagination
// McpSchema.PaginatedRequest request = transport.unmarshalFrom(params,
// new TypeReference<McpSchema.PaginatedRequest>() {
// });
var promptList = this.prompts.values()
.stream()
.map(McpServerFeatures.AsyncPromptSpecification::prompt)
.toList();
return Mono.just(new McpSchema.ListPromptsResult(promptList, null));
};
}
private McpClientSession.RequestHandler<McpSchema.GetPromptResult> promptsGetRequestHandler() {
return params -> {
McpSchema.GetPromptRequest promptRequest = transport.unmarshalFrom(params,
new TypeReference<McpSchema.GetPromptRequest>() {
});
// Implement prompt retrieval logic here
McpServerFeatures.AsyncPromptSpecification registration = this.prompts.get(promptRequest.name());
if (registration == null) {
return Mono.error(new McpError("Prompt not found: " + promptRequest.name()));
}
return registration.promptHandler().apply(null, promptRequest);
};
}
// ---------------------------------------
// Logging Management
// ---------------------------------------
/**
* Send a logging message notification to all connected clients. Messages below
* the current minimum logging level will be filtered out.
* @param loggingMessageNotification The logging message to send
* @return A Mono that completes when the notification has been sent
*/
public Mono<Void> loggingNotification(LoggingMessageNotification loggingMessageNotification) {
if (loggingMessageNotification == null) {
return Mono.error(new McpError("Logging message must not be null"));
}
Map<String, Object> params = this.transport.unmarshalFrom(loggingMessageNotification,
new TypeReference<Map<String, Object>>() {
});
if (loggingMessageNotification.level().level() < minLoggingLevel.level()) {
return Mono.empty();
}
return this.mcpSession.sendNotification(McpSchema.METHOD_NOTIFICATION_MESSAGE, params);
}
/**
* Handles requests to set the minimum logging level. Messages below this level
* will not be sent.
* @return A handler that processes logging level change requests
*/
private McpClientSession.RequestHandler<Void> setLoggerRequestHandler() {
return params -> {
this.minLoggingLevel = transport.unmarshalFrom(params, new TypeReference<LoggingLevel>() {
});
return Mono.empty();
};
}
// ---------------------------------------
// Sampling
// ---------------------------------------
private static final TypeReference<McpSchema.CreateMessageResult> CREATE_MESSAGE_RESULT_TYPE_REF = new TypeReference<>() {
};
/**
* Create a new message using the sampling capabilities of the client. The Model
* Context Protocol (MCP) provides a standardized way for servers to request LLM
* sampling (“completions” or “generations”) from language models via clients.
* This flow allows clients to maintain control over model access, selection, and
* permissions while enabling servers to leverage AI capabilities—with no server
* API keys necessary. Servers can request text or image-based interactions and
* optionally include context from MCP servers in their prompts.
* @param createMessageRequest The request to create a new message
* @return A Mono that completes when the message has been created
* @throws McpError if the client has not been initialized or does not support
* sampling capabilities
* @throws McpError if the client does not support the createMessage method
* @see McpSchema.CreateMessageRequest
* @see McpSchema.CreateMessageResult
* @see <a href=
* "https://spec.modelcontextprotocol.io/specification/client/sampling/">Sampling
* Specification</a>
*/
public Mono<McpSchema.CreateMessageResult> createMessage(McpSchema.CreateMessageRequest createMessageRequest) {
if (this.clientCapabilities == null) {
return Mono.error(new McpError("Client must be initialized. Call the initialize method first!"));
}
if (this.clientCapabilities.sampling() == null) {
return Mono.error(new McpError("Client must be configured with sampling capabilities"));
}
return this.mcpSession.sendRequest(McpSchema.METHOD_SAMPLING_CREATE_MESSAGE, createMessageRequest,
CREATE_MESSAGE_RESULT_TYPE_REF);
}
/**
* This method is package-private and used for test only. Should not be called by
* user code.
* @param protocolVersions the Client supported protocol versions.
*/
void setProtocolVersions(List<String> protocolVersions) {
this.protocolVersions = protocolVersions;
}

View file

@ -11,16 +11,12 @@ import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.spec.ServerMcpTransport;
import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
import io.modelcontextprotocol.spec.McpSchema.ResourceTemplate;
import io.modelcontextprotocol.spec.McpServerTransportProvider;
import io.modelcontextprotocol.util.Assert;
import reactor.core.publisher.Mono;
@ -136,21 +132,6 @@ public interface McpServer {
return new SyncSpecification(transportProvider);
}
/**
* Starts building a synchronous MCP server that provides blocking operations.
* Synchronous servers block the current Thread's execution upon each request before
* giving the control back to the caller, making them simpler to implement but
* potentially less scalable for concurrent operations.
* @param transport The transport layer implementation for MCP communication
* @return A new instance of {@link SyncSpec} for configuring the server.
* @deprecated This method will be removed in 0.9.0. Use
* {@link #sync(McpServerTransportProvider)} instead.
*/
@Deprecated
static SyncSpec sync(ServerMcpTransport transport) {
return new SyncSpec(transport);
}
/**
* Starts building an asynchronous MCP server that provides non-blocking operations.
* Asynchronous servers can handle multiple requests concurrently on a single Thread
@ -163,21 +144,6 @@ public interface McpServer {
return new AsyncSpecification(transportProvider);
}
/**
* Starts building an asynchronous MCP server that provides non-blocking operations.
* Asynchronous servers can handle multiple requests concurrently on a single Thread
* using a functional paradigm with non-blocking server transports, making them more
* scalable for high-concurrency scenarios but more complex to implement.
* @param transport The transport layer implementation for MCP communication
* @return A new instance of {@link AsyncSpec} for configuring the server.
* @deprecated This method will be removed in 0.9.0. Use
* {@link #async(McpServerTransportProvider)} instead.
*/
@Deprecated
static AsyncSpec async(ServerMcpTransport transport) {
return new AsyncSpec(transport);
}
/**
* Asynchronous server specification.
*/
@ -1004,819 +970,4 @@ public interface McpServer {
}
/**
* Asynchronous server specification.
*
* @deprecated
*/
@Deprecated
class AsyncSpec {
private static final McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("mcp-server",
"1.0.0");
private final ServerMcpTransport transport;
private ObjectMapper objectMapper;
private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
private McpSchema.ServerCapabilities serverCapabilities;
/**
* The Model Context Protocol (MCP) allows servers to expose tools that can be
* invoked by language models. Tools enable models to interact with external
* systems, such as querying databases, calling APIs, or performing computations.
* Each tool is uniquely identified by a name and includes metadata describing its
* schema.
*/
private final List<McpServerFeatures.AsyncToolRegistration> tools = new ArrayList<>();
/**
* The Model Context Protocol (MCP) provides a standardized way for servers to
* expose resources to clients. Resources allow servers to share data that
* provides context to language models, such as files, database schemas, or
* application-specific information. Each resource is uniquely identified by a
* URI.
*/
private final Map<String, McpServerFeatures.AsyncResourceRegistration> resources = new HashMap<>();
private final List<ResourceTemplate> resourceTemplates = new ArrayList<>();
/**
* The Model Context Protocol (MCP) provides a standardized way for servers to
* expose prompt templates to clients. Prompts allow servers to provide structured
* messages and instructions for interacting with language models. Clients can
* discover available prompts, retrieve their contents, and provide arguments to
* customize them.
*/
private final Map<String, McpServerFeatures.AsyncPromptRegistration> prompts = new HashMap<>();
private final List<Function<List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers = new ArrayList<>();
private AsyncSpec(ServerMcpTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
}
/**
* Sets the server implementation information that will be shared with clients
* during connection initialization. This helps with version compatibility,
* debugging, and server identification.
* @param serverInfo The server implementation details including name and version.
* Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if serverInfo is null
*/
public AsyncSpec serverInfo(McpSchema.Implementation serverInfo) {
Assert.notNull(serverInfo, "Server info must not be null");
this.serverInfo = serverInfo;
return this;
}
/**
* Sets the server implementation information using name and version strings. This
* is a convenience method alternative to
* {@link #serverInfo(McpSchema.Implementation)}.
* @param name The server name. Must not be null or empty.
* @param version The server version. Must not be null or empty.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if name or version is null or empty
* @see #serverInfo(McpSchema.Implementation)
*/
public AsyncSpec serverInfo(String name, String version) {
Assert.hasText(name, "Name must not be null or empty");
Assert.hasText(version, "Version must not be null or empty");
this.serverInfo = new McpSchema.Implementation(name, version);
return this;
}
/**
* Sets the server capabilities that will be advertised to clients during
* connection initialization. Capabilities define what features the server
* supports, such as:
* <ul>
* <li>Tool execution
* <li>Resource access
* <li>Prompt handling
* <li>Streaming responses
* <li>Batch operations
* </ul>
* @param serverCapabilities The server capabilities configuration. Must not be
* null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if serverCapabilities is null
*/
public AsyncSpec capabilities(McpSchema.ServerCapabilities serverCapabilities) {
this.serverCapabilities = serverCapabilities;
return this;
}
/**
* Adds a single tool with its implementation handler to the server. This is a
* convenience method for registering individual tools without creating a
* {@link McpServerFeatures.AsyncToolRegistration} explicitly.
*
* <p>
* Example usage: <pre>{@code
* .tool(
* new Tool("calculator", "Performs calculations", schema),
* args -> Mono.just(new CallToolResult("Result: " + calculate(args)))
* )
* }</pre>
* @param tool The tool definition including name, description, and schema. Must
* not be null.
* @param handler The function that implements the tool's logic. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if tool or handler is null
*/
public AsyncSpec tool(McpSchema.Tool tool, Function<Map<String, Object>, Mono<CallToolResult>> handler) {
Assert.notNull(tool, "Tool must not be null");
Assert.notNull(handler, "Handler must not be null");
this.tools.add(new McpServerFeatures.AsyncToolRegistration(tool, handler));
return this;
}
/**
* Adds multiple tools with their handlers to the server using a List. This method
* is useful when tools are dynamically generated or loaded from a configuration
* source.
* @param toolRegistrations The list of tool registrations to add. Must not be
* null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolRegistrations is null
* @see #tools(McpServerFeatures.AsyncToolRegistration...)
*/
public AsyncSpec tools(List<McpServerFeatures.AsyncToolRegistration> toolRegistrations) {
Assert.notNull(toolRegistrations, "Tool handlers list must not be null");
this.tools.addAll(toolRegistrations);
return this;
}
/**
* Adds multiple tools with their handlers to the server using varargs. This
* method provides a convenient way to register multiple tools inline.
*
* <p>
* Example usage: <pre>{@code
* .tools(
* new McpServerFeatures.AsyncToolRegistration(calculatorTool, calculatorHandler),
* new McpServerFeatures.AsyncToolRegistration(weatherTool, weatherHandler),
* new McpServerFeatures.AsyncToolRegistration(fileManagerTool, fileManagerHandler)
* )
* }</pre>
* @param toolRegistrations The tool registrations to add. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolRegistrations is null
* @see #tools(List)
*/
public AsyncSpec tools(McpServerFeatures.AsyncToolRegistration... toolRegistrations) {
for (McpServerFeatures.AsyncToolRegistration tool : toolRegistrations) {
this.tools.add(tool);
}
return this;
}
/**
* Registers multiple resources with their handlers using a Map. This method is
* useful when resources are dynamically generated or loaded from a configuration
* source.
* @param resourceRegsitrations Map of resource name to registration. Must not be
* null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if resourceRegsitrations is null
* @see #resources(McpServerFeatures.AsyncResourceRegistration...)
*/
public AsyncSpec resources(Map<String, McpServerFeatures.AsyncResourceRegistration> resourceRegsitrations) {
Assert.notNull(resourceRegsitrations, "Resource handlers map must not be null");
this.resources.putAll(resourceRegsitrations);
return this;
}
/**
* Registers multiple resources with their handlers using a List. This method is
* useful when resources need to be added in bulk from a collection.
* @param resourceRegsitrations List of resource registrations. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if resourceRegsitrations is null
* @see #resources(McpServerFeatures.AsyncResourceRegistration...)
*/
public AsyncSpec resources(List<McpServerFeatures.AsyncResourceRegistration> resourceRegsitrations) {
Assert.notNull(resourceRegsitrations, "Resource handlers list must not be null");
for (McpServerFeatures.AsyncResourceRegistration resource : resourceRegsitrations) {
this.resources.put(resource.resource().uri(), resource);
}
return this;
}
/**
* Registers multiple resources with their handlers using varargs. This method
* provides a convenient way to register multiple resources inline.
*
* <p>
* Example usage: <pre>{@code
* .resources(
* new McpServerFeatures.AsyncResourceRegistration(fileResource, fileHandler),
* new McpServerFeatures.AsyncResourceRegistration(dbResource, dbHandler),
* new McpServerFeatures.AsyncResourceRegistration(apiResource, apiHandler)
* )
* }</pre>
* @param resourceRegistrations The resource registrations to add. Must not be
* null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if resourceRegistrations is null
*/
public AsyncSpec resources(McpServerFeatures.AsyncResourceRegistration... resourceRegistrations) {
Assert.notNull(resourceRegistrations, "Resource handlers list must not be null");
for (McpServerFeatures.AsyncResourceRegistration resource : resourceRegistrations) {
this.resources.put(resource.resource().uri(), resource);
}
return this;
}
/**
* Sets the resource templates that define patterns for dynamic resource access.
* Templates use URI patterns with placeholders that can be filled at runtime.
*
* <p>
* Example usage: <pre>{@code
* .resourceTemplates(
* new ResourceTemplate("file://{path}", "Access files by path"),
* new ResourceTemplate("db://{table}/{id}", "Access database records")
* )
* }</pre>
* @param resourceTemplates List of resource templates. If null, clears existing
* templates.
* @return This builder instance for method chaining
* @see #resourceTemplates(ResourceTemplate...)
*/
public AsyncSpec resourceTemplates(List<ResourceTemplate> resourceTemplates) {
this.resourceTemplates.addAll(resourceTemplates);
return this;
}
/**
* Sets the resource templates using varargs for convenience. This is an
* alternative to {@link #resourceTemplates(List)}.
* @param resourceTemplates The resource templates to set.
* @return This builder instance for method chaining
* @see #resourceTemplates(List)
*/
public AsyncSpec resourceTemplates(ResourceTemplate... resourceTemplates) {
for (ResourceTemplate resourceTemplate : resourceTemplates) {
this.resourceTemplates.add(resourceTemplate);
}
return this;
}
/**
* Registers multiple prompts with their handlers using a Map. This method is
* useful when prompts are dynamically generated or loaded from a configuration
* source.
*
* <p>
* Example usage: <pre>{@code
* .prompts(Map.of("analysis", new McpServerFeatures.AsyncPromptRegistration(
* new Prompt("analysis", "Code analysis template"),
* request -> Mono.just(new GetPromptResult(generateAnalysisPrompt(request)))
* )));
* }</pre>
* @param prompts Map of prompt name to registration. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if prompts is null
*/
public AsyncSpec prompts(Map<String, McpServerFeatures.AsyncPromptRegistration> prompts) {
this.prompts.putAll(prompts);
return this;
}
/**
* Registers multiple prompts with their handlers using a List. This method is
* useful when prompts need to be added in bulk from a collection.
* @param prompts List of prompt registrations. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if prompts is null
* @see #prompts(McpServerFeatures.AsyncPromptRegistration...)
*/
public AsyncSpec prompts(List<McpServerFeatures.AsyncPromptRegistration> prompts) {
for (McpServerFeatures.AsyncPromptRegistration prompt : prompts) {
this.prompts.put(prompt.prompt().name(), prompt);
}
return this;
}
/**
* Registers multiple prompts with their handlers using varargs. This method
* provides a convenient way to register multiple prompts inline.
*
* <p>
* Example usage: <pre>{@code
* .prompts(
* new McpServerFeatures.AsyncPromptRegistration(analysisPrompt, analysisHandler),
* new McpServerFeatures.AsyncPromptRegistration(summaryPrompt, summaryHandler),
* new McpServerFeatures.AsyncPromptRegistration(reviewPrompt, reviewHandler)
* )
* }</pre>
* @param prompts The prompt registrations to add. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if prompts is null
*/
public AsyncSpec prompts(McpServerFeatures.AsyncPromptRegistration... prompts) {
for (McpServerFeatures.AsyncPromptRegistration prompt : prompts) {
this.prompts.put(prompt.prompt().name(), prompt);
}
return this;
}
/**
* Registers a consumer that will be notified when the list of roots changes. This
* is useful for updating resource availability dynamically, such as when new
* files are added or removed.
* @param consumer The consumer to register. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if consumer is null
*/
public AsyncSpec rootsChangeConsumer(Function<List<McpSchema.Root>, Mono<Void>> consumer) {
Assert.notNull(consumer, "Consumer must not be null");
this.rootsChangeConsumers.add(consumer);
return this;
}
/**
* Registers multiple consumers that will be notified when the list of roots
* changes. This method is useful when multiple consumers need to be registered at
* once.
* @param consumers The list of consumers to register. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if consumers is null
*/
public AsyncSpec rootsChangeConsumers(List<Function<List<McpSchema.Root>, Mono<Void>>> consumers) {
Assert.notNull(consumers, "Consumers list must not be null");
this.rootsChangeConsumers.addAll(consumers);
return this;
}
/**
* Registers multiple consumers that will be notified when the list of roots
* changes using varargs. This method provides a convenient way to register
* multiple consumers inline.
* @param consumers The consumers to register. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if consumers is null
*/
public AsyncSpec rootsChangeConsumers(
@SuppressWarnings("unchecked") Function<List<McpSchema.Root>, Mono<Void>>... consumers) {
for (Function<List<McpSchema.Root>, Mono<Void>> consumer : consumers) {
this.rootsChangeConsumers.add(consumer);
}
return this;
}
/**
* Builds an asynchronous MCP server that provides non-blocking operations.
* @return A new instance of {@link McpAsyncServer} configured with this builder's
* settings
*/
public McpAsyncServer build() {
var tools = this.tools.stream().map(McpServerFeatures.AsyncToolRegistration::toSpecification).toList();
var resources = this.resources.entrySet()
.stream()
.map(entry -> Map.entry(entry.getKey(), entry.getValue().toSpecification()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
var prompts = this.prompts.entrySet()
.stream()
.map(entry -> Map.entry(entry.getKey(), entry.getValue().toSpecification()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
var rootsChangeHandlers = this.rootsChangeConsumers.stream()
.map(consumer -> (BiFunction<McpAsyncServerExchange, List<McpSchema.Root>, Mono<Void>>) (exchange,
roots) -> consumer.apply(roots))
.toList();
var features = new McpServerFeatures.Async(this.serverInfo, this.serverCapabilities, tools, resources,
this.resourceTemplates, prompts, rootsChangeHandlers);
return new McpAsyncServer(this.transport, features);
}
}
/**
* Synchronous server specification.
*
* @deprecated
*/
@Deprecated
class SyncSpec {
private static final McpSchema.Implementation DEFAULT_SERVER_INFO = new McpSchema.Implementation("mcp-server",
"1.0.0");
private final ServerMcpTransport transport;
private final McpServerTransportProvider transportProvider;
private ObjectMapper objectMapper;
private McpSchema.Implementation serverInfo = DEFAULT_SERVER_INFO;
private McpSchema.ServerCapabilities serverCapabilities;
/**
* The Model Context Protocol (MCP) allows servers to expose tools that can be
* invoked by language models. Tools enable models to interact with external
* systems, such as querying databases, calling APIs, or performing computations.
* Each tool is uniquely identified by a name and includes metadata describing its
* schema.
*/
private final List<McpServerFeatures.SyncToolRegistration> tools = new ArrayList<>();
/**
* The Model Context Protocol (MCP) provides a standardized way for servers to
* expose resources to clients. Resources allow servers to share data that
* provides context to language models, such as files, database schemas, or
* application-specific information. Each resource is uniquely identified by a
* URI.
*/
private final Map<String, McpServerFeatures.SyncResourceRegistration> resources = new HashMap<>();
private final List<ResourceTemplate> resourceTemplates = new ArrayList<>();
/**
* The Model Context Protocol (MCP) provides a standardized way for servers to
* expose prompt templates to clients. Prompts allow servers to provide structured
* messages and instructions for interacting with language models. Clients can
* discover available prompts, retrieve their contents, and provide arguments to
* customize them.
*/
private final Map<String, McpServerFeatures.SyncPromptRegistration> prompts = new HashMap<>();
private final List<Consumer<List<McpSchema.Root>>> rootsChangeConsumers = new ArrayList<>();
private SyncSpec(McpServerTransportProvider transportProvider) {
Assert.notNull(transportProvider, "Transport provider must not be null");
this.transportProvider = transportProvider;
this.transport = null;
}
private SyncSpec(ServerMcpTransport transport) {
Assert.notNull(transport, "Transport must not be null");
this.transport = transport;
this.transportProvider = null;
}
/**
* Sets the server implementation information that will be shared with clients
* during connection initialization. This helps with version compatibility,
* debugging, and server identification.
* @param serverInfo The server implementation details including name and version.
* Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if serverInfo is null
*/
public SyncSpec serverInfo(McpSchema.Implementation serverInfo) {
Assert.notNull(serverInfo, "Server info must not be null");
this.serverInfo = serverInfo;
return this;
}
/**
* Sets the server implementation information using name and version strings. This
* is a convenience method alternative to
* {@link #serverInfo(McpSchema.Implementation)}.
* @param name The server name. Must not be null or empty.
* @param version The server version. Must not be null or empty.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if name or version is null or empty
* @see #serverInfo(McpSchema.Implementation)
*/
public SyncSpec serverInfo(String name, String version) {
Assert.hasText(name, "Name must not be null or empty");
Assert.hasText(version, "Version must not be null or empty");
this.serverInfo = new McpSchema.Implementation(name, version);
return this;
}
/**
* Sets the server capabilities that will be advertised to clients during
* connection initialization. Capabilities define what features the server
* supports, such as:
* <ul>
* <li>Tool execution
* <li>Resource access
* <li>Prompt handling
* <li>Streaming responses
* <li>Batch operations
* </ul>
* @param serverCapabilities The server capabilities configuration. Must not be
* null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if serverCapabilities is null
*/
public SyncSpec capabilities(McpSchema.ServerCapabilities serverCapabilities) {
this.serverCapabilities = serverCapabilities;
return this;
}
/**
* Adds a single tool with its implementation handler to the server. This is a
* convenience method for registering individual tools without creating a
* {@link McpServerFeatures.SyncToolRegistration} explicitly.
*
* <p>
* Example usage: <pre>{@code
* .tool(
* new Tool("calculator", "Performs calculations", schema),
* args -> new CallToolResult("Result: " + calculate(args))
* )
* }</pre>
* @param tool The tool definition including name, description, and schema. Must
* not be null.
* @param handler The function that implements the tool's logic. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if tool or handler is null
*/
public SyncSpec tool(McpSchema.Tool tool, Function<Map<String, Object>, McpSchema.CallToolResult> handler) {
Assert.notNull(tool, "Tool must not be null");
Assert.notNull(handler, "Handler must not be null");
this.tools.add(new McpServerFeatures.SyncToolRegistration(tool, handler));
return this;
}
/**
* Adds multiple tools with their handlers to the server using a List. This method
* is useful when tools are dynamically generated or loaded from a configuration
* source.
* @param toolRegistrations The list of tool registrations to add. Must not be
* null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolRegistrations is null
* @see #tools(McpServerFeatures.SyncToolRegistration...)
*/
public SyncSpec tools(List<McpServerFeatures.SyncToolRegistration> toolRegistrations) {
Assert.notNull(toolRegistrations, "Tool handlers list must not be null");
this.tools.addAll(toolRegistrations);
return this;
}
/**
* Adds multiple tools with their handlers to the server using varargs. This
* method provides a convenient way to register multiple tools inline.
*
* <p>
* Example usage: <pre>{@code
* .tools(
* new ToolRegistration(calculatorTool, calculatorHandler),
* new ToolRegistration(weatherTool, weatherHandler),
* new ToolRegistration(fileManagerTool, fileManagerHandler)
* )
* }</pre>
* @param toolRegistrations The tool registrations to add. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if toolRegistrations is null
* @see #tools(List)
*/
public SyncSpec tools(McpServerFeatures.SyncToolRegistration... toolRegistrations) {
for (McpServerFeatures.SyncToolRegistration tool : toolRegistrations) {
this.tools.add(tool);
}
return this;
}
/**
* Registers multiple resources with their handlers using a Map. This method is
* useful when resources are dynamically generated or loaded from a configuration
* source.
* @param resourceRegsitrations Map of resource name to registration. Must not be
* null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if resourceRegsitrations is null
* @see #resources(McpServerFeatures.SyncResourceRegistration...)
*/
public SyncSpec resources(Map<String, McpServerFeatures.SyncResourceRegistration> resourceRegsitrations) {
Assert.notNull(resourceRegsitrations, "Resource handlers map must not be null");
this.resources.putAll(resourceRegsitrations);
return this;
}
/**
* Registers multiple resources with their handlers using a List. This method is
* useful when resources need to be added in bulk from a collection.
* @param resourceRegsitrations List of resource registrations. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if resourceRegsitrations is null
* @see #resources(McpServerFeatures.SyncResourceRegistration...)
*/
public SyncSpec resources(List<McpServerFeatures.SyncResourceRegistration> resourceRegsitrations) {
Assert.notNull(resourceRegsitrations, "Resource handlers list must not be null");
for (McpServerFeatures.SyncResourceRegistration resource : resourceRegsitrations) {
this.resources.put(resource.resource().uri(), resource);
}
return this;
}
/**
* Registers multiple resources with their handlers using varargs. This method
* provides a convenient way to register multiple resources inline.
*
* <p>
* Example usage: <pre>{@code
* .resources(
* new ResourceRegistration(fileResource, fileHandler),
* new ResourceRegistration(dbResource, dbHandler),
* new ResourceRegistration(apiResource, apiHandler)
* )
* }</pre>
* @param resourceRegistrations The resource registrations to add. Must not be
* null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if resourceRegistrations is null
*/
public SyncSpec resources(McpServerFeatures.SyncResourceRegistration... resourceRegistrations) {
Assert.notNull(resourceRegistrations, "Resource handlers list must not be null");
for (McpServerFeatures.SyncResourceRegistration resource : resourceRegistrations) {
this.resources.put(resource.resource().uri(), resource);
}
return this;
}
/**
* Sets the resource templates that define patterns for dynamic resource access.
* Templates use URI patterns with placeholders that can be filled at runtime.
*
* <p>
* Example usage: <pre>{@code
* .resourceTemplates(
* new ResourceTemplate("file://{path}", "Access files by path"),
* new ResourceTemplate("db://{table}/{id}", "Access database records")
* )
* }</pre>
* @param resourceTemplates List of resource templates. If null, clears existing
* templates.
* @return This builder instance for method chaining
* @see #resourceTemplates(ResourceTemplate...)
*/
public SyncSpec resourceTemplates(List<ResourceTemplate> resourceTemplates) {
this.resourceTemplates.addAll(resourceTemplates);
return this;
}
/**
* Sets the resource templates using varargs for convenience. This is an
* alternative to {@link #resourceTemplates(List)}.
* @param resourceTemplates The resource templates to set.
* @return This builder instance for method chaining
* @see #resourceTemplates(List)
*/
public SyncSpec resourceTemplates(ResourceTemplate... resourceTemplates) {
for (ResourceTemplate resourceTemplate : resourceTemplates) {
this.resourceTemplates.add(resourceTemplate);
}
return this;
}
/**
* Registers multiple prompts with their handlers using a Map. This method is
* useful when prompts are dynamically generated or loaded from a configuration
* source.
*
* <p>
* Example usage: <pre>{@code
* Map<String, PromptRegistration> prompts = new HashMap<>();
* prompts.put("analysis", new PromptRegistration(
* new Prompt("analysis", "Code analysis template"),
* request -> new GetPromptResult(generateAnalysisPrompt(request))
* ));
* .prompts(prompts)
* }</pre>
* @param prompts Map of prompt name to registration. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if prompts is null
*/
public SyncSpec prompts(Map<String, McpServerFeatures.SyncPromptRegistration> prompts) {
this.prompts.putAll(prompts);
return this;
}
/**
* Registers multiple prompts with their handlers using a List. This method is
* useful when prompts need to be added in bulk from a collection.
* @param prompts List of prompt registrations. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if prompts is null
* @see #prompts(McpServerFeatures.SyncPromptRegistration...)
*/
public SyncSpec prompts(List<McpServerFeatures.SyncPromptRegistration> prompts) {
for (McpServerFeatures.SyncPromptRegistration prompt : prompts) {
this.prompts.put(prompt.prompt().name(), prompt);
}
return this;
}
/**
* Registers multiple prompts with their handlers using varargs. This method
* provides a convenient way to register multiple prompts inline.
*
* <p>
* Example usage: <pre>{@code
* .prompts(
* new PromptRegistration(analysisPrompt, analysisHandler),
* new PromptRegistration(summaryPrompt, summaryHandler),
* new PromptRegistration(reviewPrompt, reviewHandler)
* )
* }</pre>
* @param prompts The prompt registrations to add. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if prompts is null
*/
public SyncSpec prompts(McpServerFeatures.SyncPromptRegistration... prompts) {
for (McpServerFeatures.SyncPromptRegistration prompt : prompts) {
this.prompts.put(prompt.prompt().name(), prompt);
}
return this;
}
/**
* Registers a consumer that will be notified when the list of roots changes. This
* is useful for updating resource availability dynamically, such as when new
* files are added or removed.
* @param consumer The consumer to register. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if consumer is null
*/
public SyncSpec rootsChangeConsumer(Consumer<List<McpSchema.Root>> consumer) {
Assert.notNull(consumer, "Consumer must not be null");
this.rootsChangeConsumers.add(consumer);
return this;
}
/**
* Registers multiple consumers that will be notified when the list of roots
* changes. This method is useful when multiple consumers need to be registered at
* once.
* @param consumers The list of consumers to register. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if consumers is null
*/
public SyncSpec rootsChangeConsumers(List<Consumer<List<McpSchema.Root>>> consumers) {
Assert.notNull(consumers, "Consumers list must not be null");
this.rootsChangeConsumers.addAll(consumers);
return this;
}
/**
* Registers multiple consumers that will be notified when the list of roots
* changes using varargs. This method provides a convenient way to register
* multiple consumers inline.
* @param consumers The consumers to register. Must not be null.
* @return This builder instance for method chaining
* @throws IllegalArgumentException if consumers is null
*/
public SyncSpec rootsChangeConsumers(Consumer<List<McpSchema.Root>>... consumers) {
for (Consumer<List<McpSchema.Root>> consumer : consumers) {
this.rootsChangeConsumers.add(consumer);
}
return this;
}
/**
* Builds a synchronous MCP server that provides blocking operations.
* @return A new instance of {@link McpSyncServer} configured with this builder's
* settings
*/
public McpSyncServer build() {
var tools = this.tools.stream().map(McpServerFeatures.SyncToolRegistration::toSpecification).toList();
var resources = this.resources.entrySet()
.stream()
.map(entry -> Map.entry(entry.getKey(), entry.getValue().toSpecification()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
var prompts = this.prompts.entrySet()
.stream()
.map(entry -> Map.entry(entry.getKey(), entry.getValue().toSpecification()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
var rootsChangeHandlers = this.rootsChangeConsumers.stream()
.map(consumer -> (BiConsumer<McpSyncServerExchange, List<McpSchema.Root>>) (exchange, roots) -> consumer
.accept(roots))
.toList();
McpServerFeatures.Sync syncFeatures = new McpServerFeatures.Sync(this.serverInfo, this.serverCapabilities,
tools, resources, this.resourceTemplates, prompts, rootsChangeHandlers);
McpServerFeatures.Async asyncFeatures = McpServerFeatures.Async.fromSync(syncFeatures);
var asyncServer = new McpAsyncServer(this.transport, asyncFeatures);
return new McpSyncServer(asyncServer);
}
}
}

View file

@ -10,7 +10,6 @@ import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Function;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.util.Assert;
@ -423,272 +422,4 @@ public class McpServerFeatures {
BiFunction<McpSyncServerExchange, McpSchema.GetPromptRequest, McpSchema.GetPromptResult> promptHandler) {
}
// ---------------------------------------
// Deprecated registrations
// ---------------------------------------
/**
* Registration of a tool with its asynchronous handler function. Tools are the
* primary way for MCP servers to expose functionality to AI models. Each tool
* represents a specific capability, such as:
* <ul>
* <li>Performing calculations
* <li>Accessing external APIs
* <li>Querying databases
* <li>Manipulating files
* <li>Executing system commands
* </ul>
*
* <p>
* Example tool registration: <pre>{@code
* new McpServerFeatures.AsyncToolRegistration(
* new Tool(
* "calculator",
* "Performs mathematical calculations",
* new JsonSchemaObject()
* .required("expression")
* .property("expression", JsonSchemaType.STRING)
* ),
* args -> {
* String expr = (String) args.get("expression");
* return Mono.just(new CallToolResult("Result: " + evaluate(expr)));
* }
* )
* }</pre>
*
* @param tool The tool definition including name, description, and parameter schema
* @param call The function that implements the tool's logic, receiving arguments and
* returning results
* @deprecated This class is deprecated and will be removed in 0.9.0. Use
* {@link AsyncToolSpecification}.
*/
@Deprecated
public record AsyncToolRegistration(McpSchema.Tool tool,
Function<Map<String, Object>, Mono<McpSchema.CallToolResult>> call) {
static AsyncToolRegistration fromSync(SyncToolRegistration tool) {
// FIXME: This is temporary, proper validation should be implemented
if (tool == null) {
return null;
}
return new AsyncToolRegistration(tool.tool(),
map -> Mono.fromCallable(() -> tool.call().apply(map)).subscribeOn(Schedulers.boundedElastic()));
}
public AsyncToolSpecification toSpecification() {
return new AsyncToolSpecification(tool(), (exchange, map) -> call.apply(map));
}
}
/**
* Registration of a resource with its asynchronous handler function. Resources
* provide context to AI models by exposing data such as:
* <ul>
* <li>File contents
* <li>Database records
* <li>API responses
* <li>System information
* <li>Application state
* </ul>
*
* <p>
* Example resource registration: <pre>{@code
* new McpServerFeatures.AsyncResourceRegistration(
* new Resource("docs", "Documentation files", "text/markdown"),
* request -> {
* String content = readFile(request.getPath());
* return Mono.just(new ReadResourceResult(content));
* }
* )
* }</pre>
*
* @param resource The resource definition including name, description, and MIME type
* @param readHandler The function that handles resource read requests
* @deprecated This class is deprecated and will be removed in 0.9.0. Use
* {@link AsyncResourceSpecification}.
*/
@Deprecated
public record AsyncResourceRegistration(McpSchema.Resource resource,
Function<McpSchema.ReadResourceRequest, Mono<McpSchema.ReadResourceResult>> readHandler) {
static AsyncResourceRegistration fromSync(SyncResourceRegistration resource) {
// FIXME: This is temporary, proper validation should be implemented
if (resource == null) {
return null;
}
return new AsyncResourceRegistration(resource.resource(),
req -> Mono.fromCallable(() -> resource.readHandler().apply(req))
.subscribeOn(Schedulers.boundedElastic()));
}
public AsyncResourceSpecification toSpecification() {
return new AsyncResourceSpecification(resource(), (exchange, request) -> readHandler.apply(request));
}
}
/**
* Registration of a prompt template with its asynchronous handler function. Prompts
* provide structured templates for AI model interactions, supporting:
* <ul>
* <li>Consistent message formatting
* <li>Parameter substitution
* <li>Context injection
* <li>Response formatting
* <li>Instruction templating
* </ul>
*
* <p>
* Example prompt registration: <pre>{@code
* new McpServerFeatures.AsyncPromptRegistration(
* new Prompt("analyze", "Code analysis template"),
* request -> {
* String code = request.getArguments().get("code");
* return Mono.just(new GetPromptResult(
* "Analyze this code:\n\n" + code + "\n\nProvide feedback on:"
* ));
* }
* )
* }</pre>
*
* @param prompt The prompt definition including name and description
* @param promptHandler The function that processes prompt requests and returns
* formatted templates
* @deprecated This class is deprecated and will be removed in 0.9.0. Use
* {@link AsyncPromptSpecification}.
*/
@Deprecated
public record AsyncPromptRegistration(McpSchema.Prompt prompt,
Function<McpSchema.GetPromptRequest, Mono<McpSchema.GetPromptResult>> promptHandler) {
static AsyncPromptRegistration fromSync(SyncPromptRegistration prompt) {
// FIXME: This is temporary, proper validation should be implemented
if (prompt == null) {
return null;
}
return new AsyncPromptRegistration(prompt.prompt(),
req -> Mono.fromCallable(() -> prompt.promptHandler().apply(req))
.subscribeOn(Schedulers.boundedElastic()));
}
public AsyncPromptSpecification toSpecification() {
return new AsyncPromptSpecification(prompt(), (exchange, request) -> promptHandler.apply(request));
}
}
/**
* Registration of a tool with its synchronous handler function. Tools are the primary
* way for MCP servers to expose functionality to AI models. Each tool represents a
* specific capability, such as:
* <ul>
* <li>Performing calculations
* <li>Accessing external APIs
* <li>Querying databases
* <li>Manipulating files
* <li>Executing system commands
* </ul>
*
* <p>
* Example tool registration: <pre>{@code
* new McpServerFeatures.SyncToolRegistration(
* new Tool(
* "calculator",
* "Performs mathematical calculations",
* new JsonSchemaObject()
* .required("expression")
* .property("expression", JsonSchemaType.STRING)
* ),
* args -> {
* String expr = (String) args.get("expression");
* return new CallToolResult("Result: " + evaluate(expr));
* }
* )
* }</pre>
*
* @param tool The tool definition including name, description, and parameter schema
* @param call The function that implements the tool's logic, receiving arguments and
* returning results
* @deprecated This class is deprecated and will be removed in 0.9.0. Use
* {@link SyncToolSpecification}.
*/
@Deprecated
public record SyncToolRegistration(McpSchema.Tool tool,
Function<Map<String, Object>, McpSchema.CallToolResult> call) {
public SyncToolSpecification toSpecification() {
return new SyncToolSpecification(tool, (exchange, map) -> call.apply(map));
}
}
/**
* Registration of a resource with its synchronous handler function. Resources provide
* context to AI models by exposing data such as:
* <ul>
* <li>File contents
* <li>Database records
* <li>API responses
* <li>System information
* <li>Application state
* </ul>
*
* <p>
* Example resource registration: <pre>{@code
* new McpServerFeatures.SyncResourceRegistration(
* new Resource("docs", "Documentation files", "text/markdown"),
* request -> {
* String content = readFile(request.getPath());
* return new ReadResourceResult(content);
* }
* )
* }</pre>
*
* @param resource The resource definition including name, description, and MIME type
* @param readHandler The function that handles resource read requests
* @deprecated This class is deprecated and will be removed in 0.9.0. Use
* {@link SyncResourceSpecification}.
*/
@Deprecated
public record SyncResourceRegistration(McpSchema.Resource resource,
Function<McpSchema.ReadResourceRequest, McpSchema.ReadResourceResult> readHandler) {
public SyncResourceSpecification toSpecification() {
return new SyncResourceSpecification(resource, (exchange, request) -> readHandler.apply(request));
}
}
/**
* Registration of a prompt template with its synchronous handler function. Prompts
* provide structured templates for AI model interactions, supporting:
* <ul>
* <li>Consistent message formatting
* <li>Parameter substitution
* <li>Context injection
* <li>Response formatting
* <li>Instruction templating
* </ul>
*
* <p>
* Example prompt registration: <pre>{@code
* new McpServerFeatures.SyncPromptRegistration(
* new Prompt("analyze", "Code analysis template"),
* request -> {
* String code = request.getArguments().get("code");
* return new GetPromptResult(
* "Analyze this code:\n\n" + code + "\n\nProvide feedback on:"
* );
* }
* )
* }</pre>
*
* @param prompt The prompt definition including name and description
* @param promptHandler The function that processes prompt requests and returns
* formatted templates
* @deprecated This class is deprecated and will be removed in 0.9.0. Use
* {@link SyncPromptSpecification}.
*/
@Deprecated
public record SyncPromptRegistration(McpSchema.Prompt prompt,
Function<McpSchema.GetPromptRequest, McpSchema.GetPromptResult> promptHandler) {
public SyncPromptSpecification toSpecification() {
return new SyncPromptSpecification(prompt, (exchange, request) -> promptHandler.apply(request));
}
}
}

View file

@ -65,40 +65,6 @@ public class McpSyncServer {
this.asyncServer = asyncServer;
}
/**
* Retrieves the list of all roots provided by the client.
* @return The list of roots
* @deprecated This method will be removed in 0.9.0. Use
* {@link McpSyncServerExchange#listRoots()}.
*/
@Deprecated
public McpSchema.ListRootsResult listRoots() {
return this.listRoots(null);
}
/**
* Retrieves a paginated list of roots provided by the server.
* @param cursor Optional pagination cursor from a previous list request
* @return The list of roots
* @deprecated This method will be removed in 0.9.0. Use
* {@link McpSyncServerExchange#listRoots(String)}.
*/
@Deprecated
public McpSchema.ListRootsResult listRoots(String cursor) {
return this.asyncServer.listRoots(cursor).block();
}
/**
* Add a new tool handler.
* @param toolHandler The tool handler to add
* @deprecated This method will be removed in 0.9.0. Use
* {@link #addTool(McpServerFeatures.SyncToolSpecification)}.
*/
@Deprecated
public void addTool(McpServerFeatures.SyncToolRegistration toolHandler) {
this.asyncServer.addTool(McpServerFeatures.AsyncToolRegistration.fromSync(toolHandler)).block();
}
/**
* Add a new tool handler.
* @param toolHandler The tool handler to add
@ -115,17 +81,6 @@ public class McpSyncServer {
this.asyncServer.removeTool(toolName).block();
}
/**
* Add a new resource handler.
* @param resourceHandler The resource handler to add
* @deprecated This method will be removed in 0.9.0. Use
* {@link #addResource(McpServerFeatures.SyncResourceSpecification)}.
*/
@Deprecated
public void addResource(McpServerFeatures.SyncResourceRegistration resourceHandler) {
this.asyncServer.addResource(McpServerFeatures.AsyncResourceRegistration.fromSync(resourceHandler)).block();
}
/**
* Add a new resource handler.
* @param resourceHandler The resource handler to add
@ -142,17 +97,6 @@ public class McpSyncServer {
this.asyncServer.removeResource(resourceUri).block();
}
/**
* Add a new prompt handler.
* @param promptRegistration The prompt registration to add
* @deprecated This method will be removed in 0.9.0. Use
* {@link #addPrompt(McpServerFeatures.SyncPromptSpecification)}.
*/
@Deprecated
public void addPrompt(McpServerFeatures.SyncPromptRegistration promptRegistration) {
this.asyncServer.addPrompt(McpServerFeatures.AsyncPromptRegistration.fromSync(promptRegistration)).block();
}
/**
* Add a new prompt handler.
* @param promptSpecification The prompt specification to add
@ -192,28 +136,6 @@ public class McpSyncServer {
return this.asyncServer.getServerInfo();
}
/**
* Get the client capabilities that define the supported features and functionality.
* @return The client capabilities
* @deprecated This method will be removed in 0.9.0. Use
* {@link McpSyncServerExchange#getClientCapabilities()}.
*/
@Deprecated
public ClientCapabilities getClientCapabilities() {
return this.asyncServer.getClientCapabilities();
}
/**
* Get the client implementation information.
* @return The client implementation details
* @deprecated This method will be removed in 0.9.0. Use
* {@link McpSyncServerExchange#getClientInfo()}.
*/
@Deprecated
public McpSchema.Implementation getClientInfo() {
return this.asyncServer.getClientInfo();
}
/**
* Notify clients that the list of available resources has changed.
*/
@ -258,36 +180,4 @@ public class McpSyncServer {
return this.asyncServer;
}
/**
* Create a new message using the sampling capabilities of the client. The Model
* Context Protocol (MCP) provides a standardized way for servers to request LLM
* sampling ("completions" or "generations") from language models via clients.
*
* <p>
* This flow allows clients to maintain control over model access, selection, and
* permissions while enabling servers to leverage AI capabilities—with no server API
* keys necessary. Servers can request text or image-based interactions and optionally
* include context from MCP servers in their prompts.
*
* <p>
* Unlike its async counterpart, this method blocks until the message creation is
* complete, making it easier to use in synchronous code paths.
* @param createMessageRequest The request to create a new message
* @return The result of the message creation
* @throws McpError if the client has not been initialized or does not support
* sampling capabilities
* @throws McpError if the client does not support the createMessage method
* @see McpSchema.CreateMessageRequest
* @see McpSchema.CreateMessageResult
* @see <a href=
* "https://spec.modelcontextprotocol.io/specification/client/sampling/">Sampling
* Specification</a>
* @deprecated This method will be removed in 0.9.0. Use
* {@link McpSyncServerExchange#createMessage(McpSchema.CreateMessageRequest)}.
*/
@Deprecated
public McpSchema.CreateMessageResult createMessage(McpSchema.CreateMessageRequest createMessageRequest) {
return this.asyncServer.createMessage(createMessageRequest).block();
}
}

View file

@ -1,419 +0,0 @@
/*
* Copyright 2024 - 2024 the original author or authors.
*/
package io.modelcontextprotocol.server.transport;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.modelcontextprotocol.spec.McpError;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.ServerMcpTransport;
import jakarta.servlet.AsyncContext;
import jakarta.servlet.ServletException;
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
/**
* A Servlet-based implementation of the MCP HTTP with Server-Sent Events (SSE) transport
* specification. This implementation provides similar functionality to
* WebFluxSseServerTransport but uses the traditional Servlet API instead of WebFlux.
*
* @deprecated This class will be removed in 0.9.0. Use
* {@link HttpServletSseServerTransportProvider}.
*
* <p>
* The transport handles two types of endpoints:
* <ul>
* <li>SSE endpoint (/sse) - Establishes a long-lived connection for server-to-client
* events</li>
* <li>Message endpoint (configurable) - Handles client-to-server message requests</li>
* </ul>
*
* <p>
* Features:
* <ul>
* <li>Asynchronous message handling using Servlet 6.0 async support</li>
* <li>Session management for multiple client connections</li>
* <li>Graceful shutdown support</li>
* <li>Error handling and response formatting</li>
* </ul>
* @author Christian Tzolov
* @author Alexandros Pappas
* @see ServerMcpTransport
* @see HttpServlet
*/
@WebServlet(asyncSupported = true)
@Deprecated
public class HttpServletSseServerTransport extends HttpServlet implements ServerMcpTransport {
/** Logger for this class */
private static final Logger logger = LoggerFactory.getLogger(HttpServletSseServerTransport.class);
public static final String UTF_8 = "UTF-8";
public static final String APPLICATION_JSON = "application/json";
public static final String FAILED_TO_SEND_ERROR_RESPONSE = "Failed to send error response: {}";
/** Default endpoint path for SSE connections */
public static final String DEFAULT_SSE_ENDPOINT = "/sse";
/** Event type for regular messages */
public static final String MESSAGE_EVENT_TYPE = "message";
/** Event type for endpoint information */
public static final String ENDPOINT_EVENT_TYPE = "endpoint";
/** JSON object mapper for serialization/deserialization */
private final ObjectMapper objectMapper;
/** The endpoint path for handling client messages */
private final String messageEndpoint;
/** The endpoint path for handling SSE connections */
private final String sseEndpoint;
/** Map of active client sessions, keyed by session ID */
private final Map<String, ClientSession> sessions = new ConcurrentHashMap<>();
/** Flag indicating if the transport is in the process of shutting down */
private final AtomicBoolean isClosing = new AtomicBoolean(false);
/** Handler for processing incoming messages */
private Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> connectHandler;
/**
* Creates a new HttpServletSseServerTransport instance with a custom SSE endpoint.
* @param objectMapper The JSON object mapper to use for message
* serialization/deserialization
* @param messageEndpoint The endpoint path where clients will send their messages
* @param sseEndpoint The endpoint path where clients will establish SSE connections
*/
public HttpServletSseServerTransport(ObjectMapper objectMapper, String messageEndpoint, String sseEndpoint) {
this.objectMapper = objectMapper;
this.messageEndpoint = messageEndpoint;
this.sseEndpoint = sseEndpoint;
}
/**
* Creates a new HttpServletSseServerTransport instance with the default SSE endpoint.
* @param objectMapper The JSON object mapper to use for message
* serialization/deserialization
* @param messageEndpoint The endpoint path where clients will send their messages
*/
public HttpServletSseServerTransport(ObjectMapper objectMapper, String messageEndpoint) {
this(objectMapper, messageEndpoint, DEFAULT_SSE_ENDPOINT);
}
/**
* Handles GET requests to establish SSE connections.
* <p>
* This method sets up a new SSE connection when a client connects to the SSE
* endpoint. It configures the response headers for SSE, creates a new session, and
* sends the initial endpoint information to the client.
* @param request The HTTP servlet request
* @param response The HTTP servlet response
* @throws ServletException If a servlet-specific error occurs
* @throws IOException If an I/O error occurs
*/
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String pathInfo = request.getPathInfo();
if (!sseEndpoint.equals(pathInfo)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
if (isClosing.get()) {
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Server is shutting down");
return;
}
response.setContentType("text/event-stream");
response.setCharacterEncoding(UTF_8);
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Connection", "keep-alive");
response.setHeader("Access-Control-Allow-Origin", "*");
String sessionId = UUID.randomUUID().toString();
AsyncContext asyncContext = request.startAsync();
asyncContext.setTimeout(0);
PrintWriter writer = response.getWriter();
ClientSession session = new ClientSession(sessionId, asyncContext, writer);
this.sessions.put(sessionId, session);
// Send initial endpoint event
this.sendEvent(writer, ENDPOINT_EVENT_TYPE, messageEndpoint);
}
/**
* Handles POST requests for client messages.
* <p>
* This method processes incoming messages from clients, routes them through the
* connect handler if configured, and sends back the appropriate response. It handles
* error cases and formats error responses according to the MCP specification.
* @param request The HTTP servlet request
* @param response The HTTP servlet response
* @throws ServletException If a servlet-specific error occurs
* @throws IOException If an I/O error occurs
*/
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if (isClosing.get()) {
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "Server is shutting down");
return;
}
String pathInfo = request.getPathInfo();
if (!messageEndpoint.equals(pathInfo)) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return;
}
try {
BufferedReader reader = request.getReader();
StringBuilder body = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
body.append(line);
}
McpSchema.JSONRPCMessage message = McpSchema.deserializeJsonRpcMessage(objectMapper, body.toString());
if (connectHandler != null) {
connectHandler.apply(Mono.just(message)).subscribe(responseMessage -> {
try {
response.setContentType(APPLICATION_JSON);
response.setCharacterEncoding(UTF_8);
String jsonResponse = objectMapper.writeValueAsString(responseMessage);
PrintWriter writer = response.getWriter();
writer.write(jsonResponse);
writer.flush();
}
catch (Exception e) {
logger.error("Error sending response: {}", e.getMessage());
try {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Error processing response: " + e.getMessage());
}
catch (IOException ex) {
logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage());
}
}
}, error -> {
try {
logger.error("Error processing message: {}", error.getMessage());
McpError mcpError = new McpError(error.getMessage());
response.setContentType(APPLICATION_JSON);
response.setCharacterEncoding(UTF_8);
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
String jsonError = objectMapper.writeValueAsString(mcpError);
PrintWriter writer = response.getWriter();
writer.write(jsonError);
writer.flush();
}
catch (IOException e) {
logger.error(FAILED_TO_SEND_ERROR_RESPONSE, e.getMessage());
try {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Error sending error response: " + e.getMessage());
}
catch (IOException ex) {
logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage());
}
}
});
}
else {
response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE, "No message handler configured");
}
}
catch (Exception e) {
logger.error("Invalid message format: {}", e.getMessage());
try {
McpError mcpError = new McpError("Invalid message format: " + e.getMessage());
response.setContentType(APPLICATION_JSON);
response.setCharacterEncoding(UTF_8);
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
String jsonError = objectMapper.writeValueAsString(mcpError);
PrintWriter writer = response.getWriter();
writer.write(jsonError);
writer.flush();
}
catch (IOException ex) {
logger.error(FAILED_TO_SEND_ERROR_RESPONSE, ex.getMessage());
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid message format");
}
}
}
/**
* Sets up the message handler for processing client requests.
* @param handler The function to process incoming messages and produce responses
* @return A Mono that completes when the handler is set up
*/
@Override
public Mono<Void> connect(Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler) {
this.connectHandler = handler;
return Mono.empty();
}
/**
* Broadcasts a message to all connected clients.
* <p>
* This method serializes the message and sends it to all active client sessions. If a
* client is disconnected, its session is removed.
* @param message The message to broadcast
* @return A Mono that completes when the message has been sent to all clients
*/
@Override
public Mono<Void> sendMessage(McpSchema.JSONRPCMessage message) {
if (sessions.isEmpty()) {
logger.debug("No active sessions to broadcast message to");
return Mono.empty();
}
return Mono.create(sink -> {
try {
String jsonText = objectMapper.writeValueAsString(message);
sessions.values().forEach(session -> {
try {
this.sendEvent(session.writer, MESSAGE_EVENT_TYPE, jsonText);
}
catch (IOException e) {
logger.error("Failed to send message to session {}: {}", session.id, e.getMessage());
removeSession(session);
}
});
sink.success();
}
catch (Exception e) {
logger.error("Failed to process message: {}", e.getMessage());
sink.error(new McpError("Failed to process message: " + e.getMessage()));
}
});
}
/**
* Closes the transport.
* <p>
* This implementation delegates to the super class's close method.
*/
@Override
public void close() {
ServerMcpTransport.super.close();
}
/**
* Unmarshals data from one type to another using the object mapper.
* @param <T> The target type
* @param data The source data
* @param typeRef The type reference for the target type
* @return The unmarshaled data
*/
@Override
public <T> T unmarshalFrom(Object data, TypeReference<T> typeRef) {
return objectMapper.convertValue(data, typeRef);
}
/**
* Initiates a graceful shutdown of the transport.
* <p>
* This method marks the transport as closing and closes all active client sessions.
* New connection attempts will be rejected during shutdown.
* @return A Mono that completes when all sessions have been closed
*/
@Override
public Mono<Void> closeGracefully() {
isClosing.set(true);
logger.debug("Initiating graceful shutdown with {} active sessions", sessions.size());
return Mono.create(sink -> {
sessions.values().forEach(this::removeSession);
sink.success();
});
}
/**
* Sends an SSE event to a client.
* @param writer The writer to send the event through
* @param eventType The type of event (message or endpoint)
* @param data The event data
* @throws IOException If an error occurs while writing the event
*/
private void sendEvent(PrintWriter writer, String eventType, String data) throws IOException {
writer.write("event: " + eventType + "\n");
writer.write("data: " + data + "\n\n");
writer.flush();
if (writer.checkError()) {
throw new IOException("Client disconnected");
}
}
/**
* Removes a client session and completes its async context.
* @param session The session to remove
*/
private void removeSession(ClientSession session) {
sessions.remove(session.id);
session.asyncContext.complete();
}
/**
* Represents a client connection session.
* <p>
* This class holds the necessary information about a client's SSE connection,
* including its ID, async context, and output writer.
*/
private static class ClientSession {
private final String id;
private final AsyncContext asyncContext;
private final PrintWriter writer;
ClientSession(String id, AsyncContext asyncContext, PrintWriter writer) {
this.id = id;
this.asyncContext = asyncContext;
this.writer = writer;
}
}
/**
* Cleans up resources when the servlet is being destroyed.
* <p>
* This method ensures a graceful shutdown by closing all client connections before
* calling the parent's destroy method.
*/
@Override
public void destroy() {
closeGracefully().block();
super.destroy();
}
}

View file

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

View file

@ -1,15 +0,0 @@
/*
* Copyright 2024 - 2024 the original author or authors.
*/
package io.modelcontextprotocol.spec;
/**
* Marker interface for the client-side MCP transport.
*
* @author Christian Tzolov
* @deprecated This class will be removed in 0.9.0. Use {@link McpClientTransport}.
*/
@Deprecated
public interface ClientMcpTransport extends McpTransport {
}

View file

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

View file

@ -44,7 +44,7 @@ public class McpClientSession implements McpSession {
private final Duration requestTimeout;
/** Transport layer implementation for message exchange */
private final McpTransport transport;
private final McpClientTransport transport;
/** Map of pending responses keyed by request ID */
private final ConcurrentHashMap<Object, MonoSink<McpSchema.JSONRPCResponse>> pendingResponses = new ConcurrentHashMap<>();
@ -104,7 +104,7 @@ public class McpClientSession implements McpSession {
* @param requestHandlers Map of method names to request handlers
* @param notificationHandlers Map of method names to notification handlers
*/
public McpClientSession(Duration requestTimeout, McpTransport transport,
public McpClientSession(Duration requestTimeout, McpClientTransport transport,
Map<String, RequestHandler<?>> requestHandlers, Map<String, NotificationHandler> notificationHandlers) {
Assert.notNull(requestTimeout, "The requstTimeout can not be null");

View file

@ -13,9 +13,8 @@ import reactor.core.publisher.Mono;
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
*/
public interface McpClientTransport extends ClientMcpTransport {
public interface McpClientTransport extends McpTransport {
@Override
Mono<Void> connect(Function<Mono<McpSchema.JSONRPCMessage>, Mono<McpSchema.JSONRPCMessage>> handler);
}

View file

@ -4,8 +4,6 @@
package io.modelcontextprotocol.spec;
import java.util.function.Function;
import com.fasterxml.jackson.core.type.TypeReference;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCMessage;
import reactor.core.publisher.Mono;
@ -39,21 +37,6 @@ import reactor.core.publisher.Mono;
*/
public interface McpTransport {
/**
* Initializes and starts the transport connection.
*
* <p>
* This method should be called before any message exchange can occur. It sets up the
* necessary resources and establishes the connection to the server.
* </p>
* @deprecated This is only relevant for client-side transports and will be removed
* from this interface in 0.9.0.
*/
@Deprecated
default Mono<Void> connect(Function<Mono<JSONRPCMessage>, Mono<JSONRPCMessage>> handler) {
return Mono.empty();
}
/**
* Closes the transport connection and releases any associated resources.
*

View file

@ -1,15 +0,0 @@
/*
* Copyright 2024 - 2024 the original author or authors.
*/
package io.modelcontextprotocol.spec;
/**
* Marker interface for the server-side MCP transport.
*
* @author Christian Tzolov
* @deprecated This class will be removed in 0.9.0. Use {@link McpServerTransport}.
*/
@Deprecated
public interface ServerMcpTransport extends McpTransport {
}