java-sdk/mcp/src/main/java/io/modelcontextprotocol/server/McpServerFeatures.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

506 lines
22 KiB
Java

/*
* Copyright 2024-2025 the original author or authors.
*/
package io.modelcontextprotocol.server;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import io.modelcontextprotocol.spec.McpSchema;
import io.modelcontextprotocol.util.Assert;
import io.modelcontextprotocol.util.Utils;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
/**
* MCP server features specification that a particular server can choose to support.
*
* @author Dariusz Jędrzejczyk
* @author Jihoon Kim
*/
public class McpServerFeatures {
/**
* Asynchronous server features specification.
*
* @param serverInfo The server implementation details
* @param serverCapabilities The server capabilities
* @param tools The list of tool specifications
* @param resources The map of resource specifications
* @param resourceTemplates The list of resource templates
* @param prompts The map of prompt specifications
* @param rootsChangeConsumers The list of consumers that will be notified when the
* roots list changes
* @param instructions The server instructions text
*/
record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
List<McpServerFeatures.AsyncToolSpecification> tools, Map<String, AsyncResourceSpecification> resources,
List<McpSchema.ResourceTemplate> resourceTemplates,
Map<String, McpServerFeatures.AsyncPromptSpecification> prompts,
Map<McpSchema.CompleteReference, McpServerFeatures.AsyncCompletionSpecification> completions,
List<BiFunction<McpAsyncServerExchange, List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers,
String instructions) {
/**
* Create an instance and validate the arguments.
* @param serverInfo The server implementation details
* @param serverCapabilities The server capabilities
* @param tools The list of tool specifications
* @param resources The map of resource specifications
* @param resourceTemplates The list of resource templates
* @param prompts The map of prompt specifications
* @param rootsChangeConsumers The list of consumers that will be notified when
* the roots list changes
* @param instructions The server instructions text
*/
Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
List<McpServerFeatures.AsyncToolSpecification> tools, Map<String, AsyncResourceSpecification> resources,
List<McpSchema.ResourceTemplate> resourceTemplates,
Map<String, McpServerFeatures.AsyncPromptSpecification> prompts,
Map<McpSchema.CompleteReference, McpServerFeatures.AsyncCompletionSpecification> completions,
List<BiFunction<McpAsyncServerExchange, List<McpSchema.Root>, Mono<Void>>> rootsChangeConsumers,
String instructions) {
Assert.notNull(serverInfo, "Server info must not be null");
this.serverInfo = serverInfo;
this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities
: new McpSchema.ServerCapabilities(null, // completions
null, // experimental
new McpSchema.ServerCapabilities.LoggingCapabilities(), // Enable
// logging
// by
// default
!Utils.isEmpty(prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null,
!Utils.isEmpty(resources)
? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false) : null,
!Utils.isEmpty(tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
this.tools = (tools != null) ? tools : List.of();
this.resources = (resources != null) ? resources : Map.of();
this.resourceTemplates = (resourceTemplates != null) ? resourceTemplates : List.of();
this.prompts = (prompts != null) ? prompts : Map.of();
this.completions = (completions != null) ? completions : Map.of();
this.rootsChangeConsumers = (rootsChangeConsumers != null) ? rootsChangeConsumers : List.of();
this.instructions = instructions;
}
/**
* Convert a synchronous specification into an asynchronous one and provide
* blocking code offloading to prevent accidental blocking of the non-blocking
* transport.
* @param syncSpec a potentially blocking, synchronous specification.
* @param immediateExecution when true, do not offload. Do NOT set to true when
* using a non-blocking transport.
* @return a specification which is protected from blocking calls specified by the
* user.
*/
static Async fromSync(Sync syncSpec, boolean immediateExecution) {
List<McpServerFeatures.AsyncToolSpecification> tools = new ArrayList<>();
for (var tool : syncSpec.tools()) {
tools.add(AsyncToolSpecification.fromSync(tool, immediateExecution));
}
Map<String, AsyncResourceSpecification> resources = new HashMap<>();
syncSpec.resources().forEach((key, resource) -> {
resources.put(key, AsyncResourceSpecification.fromSync(resource, immediateExecution));
});
Map<String, AsyncPromptSpecification> prompts = new HashMap<>();
syncSpec.prompts().forEach((key, prompt) -> {
prompts.put(key, AsyncPromptSpecification.fromSync(prompt, immediateExecution));
});
Map<McpSchema.CompleteReference, McpServerFeatures.AsyncCompletionSpecification> completions = new HashMap<>();
syncSpec.completions().forEach((key, completion) -> {
completions.put(key, AsyncCompletionSpecification.fromSync(completion, immediateExecution));
});
List<BiFunction<McpAsyncServerExchange, List<McpSchema.Root>, Mono<Void>>> rootChangeConsumers = new ArrayList<>();
for (var rootChangeConsumer : syncSpec.rootsChangeConsumers()) {
rootChangeConsumers.add((exchange, list) -> Mono
.<Void>fromRunnable(() -> rootChangeConsumer.accept(new McpSyncServerExchange(exchange), list))
.subscribeOn(Schedulers.boundedElastic()));
}
return new Async(syncSpec.serverInfo(), syncSpec.serverCapabilities(), tools, resources,
syncSpec.resourceTemplates(), prompts, completions, rootChangeConsumers, syncSpec.instructions());
}
}
/**
* Synchronous server features specification.
*
* @param serverInfo The server implementation details
* @param serverCapabilities The server capabilities
* @param tools The list of tool specifications
* @param resources The map of resource specifications
* @param resourceTemplates The list of resource templates
* @param prompts The map of prompt specifications
* @param rootsChangeConsumers The list of consumers that will be notified when the
* roots list changes
* @param instructions The server instructions text
*/
record Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
List<McpServerFeatures.SyncToolSpecification> tools,
Map<String, McpServerFeatures.SyncResourceSpecification> resources,
List<McpSchema.ResourceTemplate> resourceTemplates,
Map<String, McpServerFeatures.SyncPromptSpecification> prompts,
Map<McpSchema.CompleteReference, McpServerFeatures.SyncCompletionSpecification> completions,
List<BiConsumer<McpSyncServerExchange, List<McpSchema.Root>>> rootsChangeConsumers, String instructions) {
/**
* Create an instance and validate the arguments.
* @param serverInfo The server implementation details
* @param serverCapabilities The server capabilities
* @param tools The list of tool specifications
* @param resources The map of resource specifications
* @param resourceTemplates The list of resource templates
* @param prompts The map of prompt specifications
* @param rootsChangeConsumers The list of consumers that will be notified when
* the roots list changes
* @param instructions The server instructions text
*/
Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
List<McpServerFeatures.SyncToolSpecification> tools,
Map<String, McpServerFeatures.SyncResourceSpecification> resources,
List<McpSchema.ResourceTemplate> resourceTemplates,
Map<String, McpServerFeatures.SyncPromptSpecification> prompts,
Map<McpSchema.CompleteReference, McpServerFeatures.SyncCompletionSpecification> completions,
List<BiConsumer<McpSyncServerExchange, List<McpSchema.Root>>> rootsChangeConsumers,
String instructions) {
Assert.notNull(serverInfo, "Server info must not be null");
this.serverInfo = serverInfo;
this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities
: new McpSchema.ServerCapabilities(null, // completions
null, // experimental
new McpSchema.ServerCapabilities.LoggingCapabilities(), // Enable
// logging
// by
// default
!Utils.isEmpty(prompts) ? new McpSchema.ServerCapabilities.PromptCapabilities(false) : null,
!Utils.isEmpty(resources)
? new McpSchema.ServerCapabilities.ResourceCapabilities(false, false) : null,
!Utils.isEmpty(tools) ? new McpSchema.ServerCapabilities.ToolCapabilities(false) : null);
this.tools = (tools != null) ? tools : new ArrayList<>();
this.resources = (resources != null) ? resources : new HashMap<>();
this.resourceTemplates = (resourceTemplates != null) ? resourceTemplates : new ArrayList<>();
this.prompts = (prompts != null) ? prompts : new HashMap<>();
this.completions = (completions != null) ? completions : new HashMap<>();
this.rootsChangeConsumers = (rootsChangeConsumers != null) ? rootsChangeConsumers : new ArrayList<>();
this.instructions = instructions;
}
}
/**
* Specification 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 specification: <pre>{@code
* new McpServerFeatures.AsyncToolSpecification(
* new Tool(
* "calculator",
* "Performs mathematical calculations",
* new JsonSchemaObject()
* .required("expression")
* .property("expression", JsonSchemaType.STRING)
* ),
* (exchange, args) -> {
* String expr = (String) args.get("expression");
* return Mono.fromSupplier(() -> evaluate(expr))
* .map(result -> new CallToolResult("Result: " + result));
* }
* )
* }</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. The function's first argument is an
* {@link McpAsyncServerExchange} upon which the server can interact with the
* connected client. The second arguments is a map of tool arguments.
*/
public record AsyncToolSpecification(McpSchema.Tool tool,
BiFunction<McpAsyncServerExchange, Map<String, Object>, Mono<McpSchema.CallToolResult>> call) {
static AsyncToolSpecification fromSync(SyncToolSpecification tool, boolean immediate) {
// FIXME: This is temporary, proper validation should be implemented
if (tool == null) {
return null;
}
return new AsyncToolSpecification(tool.tool(), (exchange, map) -> {
var toolResult = Mono.fromCallable(() -> tool.call().apply(new McpSyncServerExchange(exchange), map));
return immediate ? toolResult : toolResult.subscribeOn(Schedulers.boundedElastic());
});
}
}
/**
* Specification 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 specification: <pre>{@code
* new McpServerFeatures.AsyncResourceSpecification(
* new Resource("docs", "Documentation files", "text/markdown"),
* (exchange, request) ->
* Mono.fromSupplier(() -> readFile(request.getPath()))
* .map(ReadResourceResult::new)
* )
* }</pre>
*
* @param resource The resource definition including name, description, and MIME type
* @param readHandler The function that handles resource read requests. The function's
* first argument is an {@link McpAsyncServerExchange} upon which the server can
* interact with the connected client. The second arguments is a
* {@link io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest}.
*/
public record AsyncResourceSpecification(McpSchema.Resource resource,
BiFunction<McpAsyncServerExchange, McpSchema.ReadResourceRequest, Mono<McpSchema.ReadResourceResult>> readHandler) {
static AsyncResourceSpecification fromSync(SyncResourceSpecification resource, boolean immediateExecution) {
// FIXME: This is temporary, proper validation should be implemented
if (resource == null) {
return null;
}
return new AsyncResourceSpecification(resource.resource(), (exchange, req) -> {
var resourceResult = Mono
.fromCallable(() -> resource.readHandler().apply(new McpSyncServerExchange(exchange), req));
return immediateExecution ? resourceResult : resourceResult.subscribeOn(Schedulers.boundedElastic());
});
}
}
/**
* Specification 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 specification: <pre>{@code
* new McpServerFeatures.AsyncPromptSpecification(
* new Prompt("analyze", "Code analysis template"),
* (exchange, 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. The function's first argument is an
* {@link McpAsyncServerExchange} upon which the server can interact with the
* connected client. The second arguments is a
* {@link io.modelcontextprotocol.spec.McpSchema.GetPromptRequest}.
*/
public record AsyncPromptSpecification(McpSchema.Prompt prompt,
BiFunction<McpAsyncServerExchange, McpSchema.GetPromptRequest, Mono<McpSchema.GetPromptResult>> promptHandler) {
static AsyncPromptSpecification fromSync(SyncPromptSpecification prompt, boolean immediateExecution) {
// FIXME: This is temporary, proper validation should be implemented
if (prompt == null) {
return null;
}
return new AsyncPromptSpecification(prompt.prompt(), (exchange, req) -> {
var promptResult = Mono
.fromCallable(() -> prompt.promptHandler().apply(new McpSyncServerExchange(exchange), req));
return immediateExecution ? promptResult : promptResult.subscribeOn(Schedulers.boundedElastic());
});
}
}
/**
* Specification of a completion handler function with asynchronous execution support.
* Completions generate AI model outputs based on prompt or resource references and
* user-provided arguments. This abstraction enables:
* <ul>
* <li>Customizable response generation logic
* <li>Parameter-driven template expansion
* <li>Dynamic interaction with connected clients
* </ul>
*
* @param referenceKey The unique key representing the completion reference.
* @param completionHandler The asynchronous function that processes completion
* requests and returns results. The first argument is an
* {@link McpAsyncServerExchange} used to interact with the client. The second
* argument is a {@link io.modelcontextprotocol.spec.McpSchema.CompleteRequest}.
*/
public record AsyncCompletionSpecification(McpSchema.CompleteReference referenceKey,
BiFunction<McpAsyncServerExchange, McpSchema.CompleteRequest, Mono<McpSchema.CompleteResult>> completionHandler) {
/**
* Converts a synchronous {@link SyncCompletionSpecification} into an
* {@link AsyncCompletionSpecification} by wrapping the handler in a bounded
* elastic scheduler for safe non-blocking execution.
* @param completion the synchronous completion specification
* @return an asynchronous wrapper of the provided sync specification, or
* {@code null} if input is null
*/
static AsyncCompletionSpecification fromSync(SyncCompletionSpecification completion,
boolean immediateExecution) {
if (completion == null) {
return null;
}
return new AsyncCompletionSpecification(completion.referenceKey(), (exchange, request) -> {
var completionResult = Mono.fromCallable(
() -> completion.completionHandler().apply(new McpSyncServerExchange(exchange), request));
return immediateExecution ? completionResult
: completionResult.subscribeOn(Schedulers.boundedElastic());
});
}
}
/**
* Specification 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 specification: <pre>{@code
* new McpServerFeatures.SyncToolSpecification(
* new Tool(
* "calculator",
* "Performs mathematical calculations",
* new JsonSchemaObject()
* .required("expression")
* .property("expression", JsonSchemaType.STRING)
* ),
* (exchange, 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. The function's first argument is an
* {@link McpSyncServerExchange} upon which the server can interact with the connected
* client. The second arguments is a map of arguments passed to the tool.
*/
public record SyncToolSpecification(McpSchema.Tool tool,
BiFunction<McpSyncServerExchange, Map<String, Object>, McpSchema.CallToolResult> call) {
}
/**
* Specification 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 specification: <pre>{@code
* new McpServerFeatures.SyncResourceSpecification(
* new Resource("docs", "Documentation files", "text/markdown"),
* (exchange, 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. The function's
* first argument is an {@link McpSyncServerExchange} upon which the server can
* interact with the connected client. The second arguments is a
* {@link io.modelcontextprotocol.spec.McpSchema.ReadResourceRequest}.
*/
public record SyncResourceSpecification(McpSchema.Resource resource,
BiFunction<McpSyncServerExchange, McpSchema.ReadResourceRequest, McpSchema.ReadResourceResult> readHandler) {
}
/**
* Specification 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 specification: <pre>{@code
* new McpServerFeatures.SyncPromptSpecification(
* new Prompt("analyze", "Code analysis template"),
* (exchange, 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. The function's first argument is an
* {@link McpSyncServerExchange} upon which the server can interact with the connected
* client. The second arguments is a
* {@link io.modelcontextprotocol.spec.McpSchema.GetPromptRequest}.
*/
public record SyncPromptSpecification(McpSchema.Prompt prompt,
BiFunction<McpSyncServerExchange, McpSchema.GetPromptRequest, McpSchema.GetPromptResult> promptHandler) {
}
/**
* Specification of a completion handler function with synchronous execution support.
*
* @param referenceKey The unique key representing the completion reference.
* @param completionHandler The synchronous function that processes completion
* requests and returns results. The first argument is an
* {@link McpSyncServerExchange} used to interact with the client. The second argument
* is a {@link io.modelcontextprotocol.spec.McpSchema.CompleteRequest}.
*/
public record SyncCompletionSpecification(McpSchema.CompleteReference referenceKey,
BiFunction<McpSyncServerExchange, McpSchema.CompleteRequest, McpSchema.CompleteResult> completionHandler) {
}
}