java-sdk/mcp/src/main/java/io/modelcontextprotocol/server/McpSyncServer.java
Daniel Garnier-Moiroux b5a1e3b8f4 Add option for immediate execution in McpSyncServer
- The McpSyncServer wraps an async server. By default, reactive
  operations are scheduled on a bounded-elastic scheduler, to offload
  blocking work and prevent accidental blocking of non-blocking
  operations.
- With the default behavior, there will be thead ops, even in a blocking
  context, which means thread-locals from the request thread will be
  lost. This is inconenvient for frameworks that store state in
  thread-locals.
- This commit adds the ability to avoid offloading, when the user is
  sure they are executing code in a blocking environment. Work happens
  in the calling thread, and thread-locals are available throughout the
  execution.
2025-07-08 15:50:36 +02:00

217 lines
6.6 KiB
Java

/*
* Copyright 2024-2024 the original author or authors.
*/
package io.modelcontextprotocol.server;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.spec.McpSchema.LoggingMessageNotification;
import io.modelcontextprotocol.util.Assert;
/**
* A synchronous implementation of the Model Context Protocol (MCP) server that wraps
* {@link McpAsyncServer} to provide blocking operations. This class delegates all
* operations to an underlying async server instance while providing a simpler,
* synchronous API for scenarios where reactive programming is not required.
*
* <p>
* The MCP server enables AI models to expose tools, resources, and prompts through a
* standardized interface. Key features available through this synchronous API include:
* <ul>
* <li>Tool registration and management for extending AI model capabilities
* <li>Resource handling with URI-based addressing for providing context
* <li>Prompt template management for standardized interactions
* <li>Real-time client notifications for state changes
* <li>Structured logging with configurable severity levels
* <li>Support for client-side AI model sampling
* </ul>
*
* <p>
* While {@link McpAsyncServer} uses Project Reactor's Mono and Flux types for
* non-blocking operations, this class converts those into blocking calls, making it more
* suitable for:
* <ul>
* <li>Traditional synchronous applications
* <li>Simple scripting scenarios
* <li>Testing and debugging
* <li>Cases where reactive programming adds unnecessary complexity
* </ul>
*
* <p>
* The server supports runtime modification of its capabilities through methods like
* {@link #addTool}, {@link #addResource}, and {@link #addPrompt}, automatically notifying
* connected clients of changes when configured to do so.
*
* @author Christian Tzolov
* @author Dariusz Jędrzejczyk
* @see McpAsyncServer
* @see McpSchema
*/
public class McpSyncServer {
/**
* The async server to wrap.
*/
private final McpAsyncServer asyncServer;
private final boolean immediateExecution;
/**
* Creates a new synchronous server that wraps the provided async server.
* @param asyncServer The async server to wrap
*/
public McpSyncServer(McpAsyncServer asyncServer) {
this(asyncServer, false);
}
/**
* Creates a new synchronous server that wraps the provided async server.
* @param asyncServer The async server to wrap
* @param immediateExecution Tools, prompts, and resources handlers execute work
* without blocking code offloading. Do NOT set to true if the {@code asyncServer}'s
* transport is non-blocking.
*/
public McpSyncServer(McpAsyncServer asyncServer, boolean immediateExecution) {
Assert.notNull(asyncServer, "Async server must not be null");
this.asyncServer = asyncServer;
this.immediateExecution = immediateExecution;
}
/**
* Add a new tool handler.
* @param toolHandler The tool handler to add
*/
public void addTool(McpServerFeatures.SyncToolSpecification toolHandler) {
this.asyncServer
.addTool(McpServerFeatures.AsyncToolSpecification.fromSync(toolHandler, this.immediateExecution))
.block();
}
/**
* Remove a tool handler.
* @param toolName The name of the tool handler to remove
*/
public void removeTool(String toolName) {
this.asyncServer.removeTool(toolName).block();
}
/**
* Add a new resource handler.
* @param resourceHandler The resource handler to add
*/
public void addResource(McpServerFeatures.SyncResourceSpecification resourceHandler) {
this.asyncServer
.addResource(
McpServerFeatures.AsyncResourceSpecification.fromSync(resourceHandler, this.immediateExecution))
.block();
}
/**
* Remove a resource handler.
* @param resourceUri The URI of the resource handler to remove
*/
public void removeResource(String resourceUri) {
this.asyncServer.removeResource(resourceUri).block();
}
/**
* Add a new prompt handler.
* @param promptSpecification The prompt specification to add
*/
public void addPrompt(McpServerFeatures.SyncPromptSpecification promptSpecification) {
this.asyncServer
.addPrompt(
McpServerFeatures.AsyncPromptSpecification.fromSync(promptSpecification, this.immediateExecution))
.block();
}
/**
* Remove a prompt handler.
* @param promptName The name of the prompt handler to remove
*/
public void removePrompt(String promptName) {
this.asyncServer.removePrompt(promptName).block();
}
/**
* Notify clients that the list of available tools has changed.
*/
public void notifyToolsListChanged() {
this.asyncServer.notifyToolsListChanged().block();
}
/**
* Get the server capabilities that define the supported features and functionality.
* @return The server capabilities
*/
public McpSchema.ServerCapabilities getServerCapabilities() {
return this.asyncServer.getServerCapabilities();
}
/**
* Get the server implementation information.
* @return The server implementation details
*/
public McpSchema.Implementation getServerInfo() {
return this.asyncServer.getServerInfo();
}
/**
* Notify clients that the list of available resources has changed.
*/
public void notifyResourcesListChanged() {
this.asyncServer.notifyResourcesListChanged().block();
}
/**
* Notify clients that the resources have updated.
*/
public void notifyResourcesUpdated(McpSchema.ResourcesUpdatedNotification resourcesUpdatedNotification) {
this.asyncServer.notifyResourcesUpdated(resourcesUpdatedNotification).block();
}
/**
* Notify clients that the list of available prompts has changed.
*/
public void notifyPromptsListChanged() {
this.asyncServer.notifyPromptsListChanged().block();
}
/**
* This implementation would, incorrectly, broadcast the logging message to all
* connected clients, using a single minLoggingLevel for all of them. Similar to the
* sampling and roots, the logging level should be set per client session and use the
* ServerExchange to send the logging message to the right client.
* @param loggingMessageNotification The logging message to send
* @deprecated Use
* {@link McpSyncServerExchange#loggingNotification(LoggingMessageNotification)}
* instead.
*/
@Deprecated
public void loggingNotification(LoggingMessageNotification loggingMessageNotification) {
this.asyncServer.loggingNotification(loggingMessageNotification).block();
}
/**
* Close the server gracefully.
*/
public void closeGracefully() {
this.asyncServer.closeGracefully().block();
}
/**
* Close the server immediately.
*/
public void close() {
this.asyncServer.close();
}
/**
* Get the underlying async server instance.
* @return The wrapped async server
*/
public McpAsyncServer getAsyncServer() {
return this.asyncServer;
}
}