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:
parent
7827cdc113
commit
25f3bad68d
15 changed files with 203 additions and 7660 deletions
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue