completions) {
Assert.notNull(completions, "Completions list must not be null");
+ assertNoRepository(this.completionsRepository, "completionsRepository", "Static completion registrations");
for (var completion : completions) {
this.completions.put(completion.referenceKey(), completion);
}
@@ -2374,12 +2438,27 @@ public StatelessSyncSpecification completions(
public StatelessSyncSpecification completions(
McpStatelessServerFeatures.SyncCompletionSpecification... completions) {
Assert.notNull(completions, "Completions list must not be null");
+ assertNoRepository(this.completionsRepository, "completionsRepository", "Static completion registrations");
for (var completion : completions) {
this.completions.put(completion.referenceKey(), completion);
}
return this;
}
+ /**
+ * Sets the repository used to complete stateless completion requests. Static
+ * completion registrations cannot be mixed with a completions repository.
+ * @param completionsRepository The completions repository. Must not be null.
+ * @return This builder instance for method chaining
+ */
+ public StatelessSyncSpecification completionsRepository(CompletionsRepository completionsRepository) {
+ Assert.notNull(completionsRepository, "Completions repository must not be null");
+ assertNoStaticRegistrations(!this.completions.isEmpty(), "completionsRepository",
+ "static completion registrations");
+ this.completionsRepository = completionsRepository;
+ return this;
+ }
+
/**
* Sets the JsonMapper to use for serializing and deserializing JSON messages.
* @param jsonMapper the mapper to use. Must not be null.
@@ -2407,7 +2486,7 @@ public StatelessSyncSpecification jsonSchemaValidator(JsonSchemaValidator jsonSc
}
/**
- * Enable on "immediate execution" of the operations on the underlying
+ * Enables immediate execution of operations on the underlying
* {@link McpStatelessAsyncServer}. Defaults to false, which does blocking code
* offloading to prevent accidental blocking of the non-blocking transport.
*
@@ -2424,7 +2503,8 @@ public StatelessSyncSpecification immediateExecution(boolean immediateExecution)
public McpStatelessSyncServer build() {
var syncFeatures = new McpStatelessServerFeatures.Sync(this.serverInfo, this.serverCapabilities, this.tools,
- this.resources, this.resourceTemplates, this.prompts, this.completions, this.instructions);
+ this.resources, this.resourceTemplates, this.prompts, this.completions, this.toolsRepository,
+ this.resourcesRepository, this.promptsRepository, this.completionsRepository, this.instructions);
var asyncFeatures = McpStatelessServerFeatures.Async.fromSync(syncFeatures, this.immediateExecution);
var jsonSchemaValidator = this.jsonSchemaValidator != null ? this.jsonSchemaValidator
: McpJsonDefaults.getSchemaValidator();
@@ -2434,11 +2514,26 @@ public McpStatelessSyncServer build() {
var asyncServer = new McpStatelessAsyncServer(transport,
jsonMapper == null ? McpJsonDefaults.getMapper() : jsonMapper, asyncFeatures, requestTimeout,
uriTemplateManagerFactory, jsonSchemaValidator, validateToolInputs);
- return new McpStatelessSyncServer(asyncServer, this.immediateExecution);
+ return new McpStatelessSyncServer(asyncServer);
}
}
+ private static void assertNoStaticRegistrations(boolean hasStaticRegistrations, String repositoryName,
+ String registrationsName) {
+ if (hasStaticRegistrations) {
+ throw new IllegalStateException(
+ repositoryName + " cannot be configured together with " + registrationsName);
+ }
+ }
+
+ private static void assertNoRepository(Object repository, String repositoryName, String registrationsName) {
+ if (repository != null) {
+ throw new IllegalStateException(
+ registrationsName + " cannot be configured together with " + repositoryName);
+ }
+ }
+
private static void validateAsyncToolSchemas(JsonSchemaValidator validator,
List tools) {
tools.forEach(spec -> validateToolSchema(validator, spec.tool()));
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java
index c6105267d..c43722ed3 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java
@@ -47,6 +47,7 @@
* knowledge and can serve the clients with the capabilities it supports.
*
* @author Dariusz Jędrzejczyk
+ * @author Taewoong Kim
*/
public class McpStatelessAsyncServer {
@@ -72,6 +73,18 @@ public class McpStatelessAsyncServer {
private final ConcurrentHashMap completions = new ConcurrentHashMap<>();
+ private final ToolsRepository toolsRepository;
+
+ private final ResourcesRepository resourcesRepository;
+
+ private final PromptsRepository promptsRepository;
+
+ private final CompletionsRepository completionsRepository;
+
+ private final boolean immediateExecution;
+
+ private final SyncRepositoryCallAdapter repositoryCallAdapter;
+
private List protocolVersions;
private McpUriTemplateManagerFactory uriTemplateManagerFactory = new DefaultMcpUriTemplateManagerFactory();
@@ -94,6 +107,12 @@ public class McpStatelessAsyncServer {
this.resourceTemplates.putAll(features.resourceTemplates());
this.prompts.putAll(features.prompts());
this.completions.putAll(features.completions());
+ this.toolsRepository = features.toolsRepository();
+ this.resourcesRepository = features.resourcesRepository();
+ this.promptsRepository = features.promptsRepository();
+ this.completionsRepository = features.completionsRepository();
+ this.immediateExecution = features.immediateExecution();
+ this.repositoryCallAdapter = new SyncRepositoryCallAdapter(this.immediateExecution);
this.uriTemplateManagerFactory = uriTemplateManagerFactory;
this.jsonSchemaValidator = jsonSchemaValidator;
this.validateToolInputs = validateToolInputs;
@@ -187,6 +206,14 @@ public McpSchema.Implementation getServerInfo() {
return this.serverInfo;
}
+ private McpSchema.PaginatedRequest paginatedRequest(Object params) {
+ if (params == null) {
+ return new McpSchema.PaginatedRequest();
+ }
+ return this.jsonMapper.convertValue(params, new TypeRef<>() {
+ });
+ }
+
/**
* Gracefully closes the server, allowing any in-progress operations to complete.
* @return A Mono that completes when the server has been closed
@@ -235,6 +262,94 @@ private static McpStatelessServerFeatures.AsyncToolSpecification withStructuredO
toolSpecification.callHandler()));
}
+ private void assertToolSpecification(McpStatelessServerFeatures.AsyncToolSpecification toolSpecification) {
+ if (toolSpecification == null) {
+ throw new IllegalArgumentException("Tool specification must not be null");
+ }
+ assertToolSpecification(toolSpecification.tool(), toolSpecification.callHandler());
+ }
+
+ private void assertToolSpecification(McpStatelessServerFeatures.SyncToolSpecification toolSpecification) {
+ if (toolSpecification == null) {
+ throw new IllegalArgumentException("Tool specification must not be null");
+ }
+ assertToolSpecification(toolSpecification.tool(), toolSpecification.callHandler());
+ }
+
+ private void assertToolSpecification(McpSchema.Tool tool, Object callHandler) {
+ if (tool == null) {
+ throw new IllegalArgumentException("Tool must not be null");
+ }
+ if (callHandler == null) {
+ throw new IllegalArgumentException("Tool call handler must not be null");
+ }
+ if (this.serverCapabilities.tools() == null) {
+ throw new IllegalStateException("Server must be configured with tool capabilities");
+ }
+ this.jsonSchemaValidator.assertConforms("Tool '" + tool.name() + "' inputSchema", tool.inputSchema());
+ this.jsonSchemaValidator.assertConforms("Tool '" + tool.name() + "' outputSchema", tool.outputSchema());
+ }
+
+ private static CallToolResult validateToolOutput(JsonSchemaValidator jsonSchemaValidator,
+ Map outputSchema, McpSchema.CallToolRequest request, CallToolResult result) {
+
+ if (Boolean.TRUE.equals(result.isError())) {
+ // If the tool call resulted in an error, skip further validation
+ return result;
+ }
+
+ if (outputSchema == null) {
+ if (result.structuredContent() != null) {
+ logger.warn(
+ "Tool call with no outputSchema is not expected to have a result with structured content, but got: {}",
+ result.structuredContent());
+ }
+ // Pass through. No validation is required if no output schema is
+ // provided.
+ return result;
+ }
+
+ // If an output schema is provided, servers MUST provide structured
+ // results that conform to this schema.
+ // https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema
+ if (result.structuredContent() == null) {
+ String content = "Response missing structured content which is expected when calling tool with non-empty outputSchema";
+ logger.warn(content);
+ return CallToolResult.builder()
+ .content(List.of(McpSchema.TextContent.builder(content).build()))
+ .isError(true)
+ .build();
+ }
+
+ // Validate the result against the output schema
+ var validation = jsonSchemaValidator.validate(outputSchema, result.structuredContent());
+
+ if (!validation.valid()) {
+ String message = "Tool (" + request.name() + ") output validation failed: " + validation.errorMessage();
+ logger.warn(message);
+ return CallToolResult.builder()
+ .content(List.of(McpSchema.TextContent.builder(message).build()))
+ .isError(true)
+ .build();
+ }
+
+ if (Utils.isEmpty(result.content())) {
+ // For backwards compatibility, a tool that returns structured
+ // content SHOULD also return functionally equivalent unstructured
+ // content. (For example, serialized JSON can be returned in a
+ // TextContent block.)
+ // https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
+
+ return CallToolResult.builder()
+ .content(List.of(McpSchema.TextContent.builder(validation.jsonStructuredOutput()).build()))
+ .isError(result.isError())
+ .structuredContent(result.structuredContent())
+ .build();
+ }
+
+ return result;
+ }
+
private static class StructuredOutputCallToolHandler
implements BiFunction> {
@@ -259,65 +374,8 @@ public StructuredOutputCallToolHandler(JsonSchemaValidator jsonSchemaValidator,
@Override
public Mono apply(McpTransportContext transportContext, McpSchema.CallToolRequest request) {
- return this.delegateHandler.apply(transportContext, request).map(result -> {
-
- if (Boolean.TRUE.equals(result.isError())) {
- // If the tool call resulted in an error, skip further validation
- return result;
- }
-
- if (outputSchema == null) {
- if (result.structuredContent() != null) {
- logger.warn(
- "Tool call with no outputSchema is not expected to have a result with structured content, but got: {}",
- result.structuredContent());
- }
- // Pass through. No validation is required if no output schema is
- // provided.
- return result;
- }
-
- // If an output schema is provided, servers MUST provide structured
- // results that conform to this schema.
- // https://modelcontextprotocol.io/specification/2025-06-18/server/tools#output-schema
- if (result.structuredContent() == null) {
- String content = "Response missing structured content which is expected when calling tool with non-empty outputSchema";
- logger.warn(content);
- return CallToolResult.builder()
- .content(List.of(McpSchema.TextContent.builder(content).build()))
- .isError(true)
- .build();
- }
-
- // Validate the result against the output schema
- var validation = this.jsonSchemaValidator.validate(outputSchema, result.structuredContent());
-
- if (!validation.valid()) {
- String message = "Tool (" + request.name() + ") output validation failed: "
- + validation.errorMessage();
- logger.warn(message);
- return CallToolResult.builder()
- .content(List.of(McpSchema.TextContent.builder(message).build()))
- .isError(true)
- .build();
- }
-
- if (Utils.isEmpty(result.content())) {
- // For backwards compatibility, a tool that returns structured
- // content SHOULD also return functionally equivalent unstructured
- // content. (For example, serialized JSON can be returned in a
- // TextContent block.)
- // https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content
-
- return CallToolResult.builder()
- .content(List.of(McpSchema.TextContent.builder(validation.jsonStructuredOutput()).build()))
- .isError(result.isError())
- .structuredContent(result.structuredContent())
- .build();
- }
-
- return result;
- });
+ return this.delegateHandler.apply(transportContext, request)
+ .map(result -> validateToolOutput(this.jsonSchemaValidator, this.outputSchema, request, result));
}
}
@@ -325,28 +383,17 @@ public Mono apply(McpTransportContext transportContext, McpSchem
/**
* Add a new tool specification at runtime.
* @param toolSpecification The tool specification to add
- * @return Mono that completes when clients have been notified of the change
+ * @return Mono that completes when the server has been updated
*/
public Mono addTool(McpStatelessServerFeatures.AsyncToolSpecification toolSpecification) {
- if (toolSpecification == null) {
- return Mono.error(new IllegalArgumentException("Tool specification must not be null"));
- }
- if (toolSpecification.tool() == null) {
- return Mono.error(new IllegalArgumentException("Tool must not be null"));
- }
- if (toolSpecification.callHandler() == null) {
- return Mono.error(new IllegalArgumentException("Tool call handler must not be null"));
- }
- if (this.serverCapabilities.tools() == null) {
- return Mono.error(new IllegalStateException("Server must be configured with tool capabilities"));
+ if (this.toolsRepository != null) {
+ return Mono.error(new IllegalStateException(
+ "Server is configured with a tools repository and cannot accept async tool registrations"));
}
-
try {
- var t = toolSpecification.tool();
- this.jsonSchemaValidator.assertConforms("Tool '" + t.name() + "' inputSchema", t.inputSchema());
- this.jsonSchemaValidator.assertConforms("Tool '" + t.name() + "' outputSchema", t.outputSchema());
+ assertToolSpecification(toolSpecification);
}
- catch (IllegalArgumentException e) {
+ catch (RuntimeException e) {
return Mono.error(e);
}
@@ -365,18 +412,39 @@ public Mono addTool(McpStatelessServerFeatures.AsyncToolSpecification tool
});
}
+ Mono addTool(McpStatelessServerFeatures.SyncToolSpecification toolSpecification) {
+ if (this.toolsRepository != null) {
+ try {
+ assertToolSpecification(toolSpecification);
+ }
+ catch (RuntimeException e) {
+ return Mono.error(e);
+ }
+ return this.repositoryCallAdapter.run(() -> this.toolsRepository.addTool(toolSpecification));
+ }
+ return addTool(
+ McpStatelessServerFeatures.AsyncToolSpecification.fromSync(toolSpecification, this.immediateExecution));
+ }
+
/**
- * List all registered tools.
- * @return A Flux stream of all registered tools
+ * List tools without a client request context.
+ * @return A Flux stream of context-free visible tools
*/
public Flux listTools() {
+ if (this.toolsRepository != null) {
+ return this.repositoryCallAdapter
+ .invoke(() -> this.toolsRepository
+ .listTools(new McpSchema.PaginatedRequest(), McpTransportContext.EMPTY)
+ .tools())
+ .flatMapIterable(tools -> tools);
+ }
return Flux.fromIterable(this.tools).map(McpStatelessServerFeatures.AsyncToolSpecification::tool);
}
/**
* 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
+ * @return Mono that completes when the server has been updated
*/
public Mono removeTool(String toolName) {
if (toolName == null) {
@@ -385,10 +453,12 @@ public Mono removeTool(String toolName) {
if (this.serverCapabilities.tools() == null) {
return Mono.error(new IllegalStateException("Server must be configured with tool capabilities"));
}
+ if (this.toolsRepository != null) {
+ return this.repositoryCallAdapter.invoke(() -> this.toolsRepository.removeTool(toolName)).then();
+ }
return Mono.defer(() -> {
if (this.tools.removeIf(toolSpecification -> toolSpecification.tool().name().equals(toolName))) {
-
logger.debug("Removed tool handler: {}", toolName);
}
else {
@@ -401,6 +471,10 @@ public Mono removeTool(String toolName) {
private McpStatelessRequestHandler toolsListRequestHandler() {
return (ctx, params) -> {
+ if (this.toolsRepository != null) {
+ McpSchema.PaginatedRequest request = paginatedRequest(params);
+ return this.repositoryCallAdapter.invoke(() -> this.toolsRepository.listTools(request, ctx));
+ }
List tools = this.tools.stream()
.map(McpStatelessServerFeatures.AsyncToolSpecification::tool)
.toList();
@@ -414,6 +488,16 @@ private McpStatelessRequestHandler toolsCallRequestHandler() {
new TypeRef() {
});
+ if (this.toolsRepository != null) {
+ return this.repositoryCallAdapter
+ .invoke(() -> this.toolsRepository.resolveTool(callToolRequest.name(), ctx))
+ .flatMap(tool -> tool.map(resolvedTool -> callRepositoryTool(ctx, callToolRequest, resolvedTool))
+ .orElseGet(() -> Mono.error(McpError.builder(McpSchema.ErrorCodes.INVALID_PARAMS)
+ .message("Unknown tool: invalid_tool_name")
+ .data("Tool not found: " + callToolRequest.name())
+ .build())));
+ }
+
Optional toolSpecification = this.tools.stream()
.filter(tr -> callToolRequest.name().equals(tr.tool().name()))
.findAny();
@@ -436,6 +520,19 @@ private McpStatelessRequestHandler toolsCallRequestHandler() {
};
}
+ private Mono callRepositoryTool(McpTransportContext transportContext,
+ McpSchema.CallToolRequest callToolRequest, McpSchema.Tool tool) {
+
+ CallToolResult validationError = ToolInputValidator.validate(tool, callToolRequest.arguments(),
+ this.validateToolInputs, this.jsonSchemaValidator);
+ if (validationError != null) {
+ return Mono.just(validationError);
+ }
+
+ return this.repositoryCallAdapter.invoke(() -> this.toolsRepository.callTool(callToolRequest, transportContext))
+ .map(result -> validateToolOutput(this.jsonSchemaValidator, tool.outputSchema(), callToolRequest, result));
+ }
+
// ---------------------------------------
// Resource Management
// ---------------------------------------
@@ -443,9 +540,13 @@ private McpStatelessRequestHandler toolsCallRequestHandler() {
/**
* Add a new resource handler at runtime.
* @param resourceSpecification The resource handler to add
- * @return Mono that completes when clients have been notified of the change
+ * @return Mono that completes when the server has been updated
*/
public Mono addResource(McpStatelessServerFeatures.AsyncResourceSpecification resourceSpecification) {
+ if (this.resourcesRepository != null) {
+ return Mono.error(new IllegalStateException(
+ "Server is configured with a resources repository and cannot accept async resource registrations"));
+ }
if (resourceSpecification == null || resourceSpecification.resource() == null) {
return Mono.error(new IllegalArgumentException("Resource must not be null"));
}
@@ -466,11 +567,32 @@ public Mono addResource(McpStatelessServerFeatures.AsyncResourceSpecificat
});
}
+ Mono addResource(McpStatelessServerFeatures.SyncResourceSpecification resourceSpecification) {
+ if (this.resourcesRepository != null) {
+ if (resourceSpecification == null || resourceSpecification.resource() == null) {
+ return Mono.error(new IllegalArgumentException("Resource must not be null"));
+ }
+ if (this.serverCapabilities.resources() == null) {
+ return Mono.error(new IllegalStateException("Server must be configured with resource capabilities"));
+ }
+ return this.repositoryCallAdapter.run(() -> this.resourcesRepository.addResource(resourceSpecification));
+ }
+ return addResource(McpStatelessServerFeatures.AsyncResourceSpecification.fromSync(resourceSpecification,
+ this.immediateExecution));
+ }
+
/**
- * List all registered resources.
- * @return A Flux stream of all registered resources
+ * List resources without a client request context.
+ * @return A Flux stream of context-free visible resources
*/
public Flux listResources() {
+ if (this.resourcesRepository != null) {
+ return this.repositoryCallAdapter
+ .invoke(() -> this.resourcesRepository
+ .listResources(new McpSchema.PaginatedRequest(), McpTransportContext.EMPTY)
+ .resources())
+ .flatMapIterable(resources -> resources);
+ }
return Flux.fromIterable(this.resources.values())
.map(McpStatelessServerFeatures.AsyncResourceSpecification::resource);
}
@@ -478,7 +600,7 @@ public Flux listResources() {
/**
* 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
+ * @return Mono that completes when the server has been updated
*/
public Mono removeResource(String resourceUri) {
if (resourceUri == null) {
@@ -487,6 +609,9 @@ public Mono removeResource(String resourceUri) {
if (this.serverCapabilities.resources() == null) {
return Mono.error(new IllegalStateException("Server must be configured with resource capabilities"));
}
+ if (this.resourcesRepository != null) {
+ return this.repositoryCallAdapter.invoke(() -> this.resourcesRepository.removeResource(resourceUri)).then();
+ }
return Mono.defer(() -> {
McpStatelessServerFeatures.AsyncResourceSpecification removed = this.resources.remove(resourceUri);
@@ -503,10 +628,14 @@ public Mono removeResource(String resourceUri) {
/**
* Add a new resource template at runtime.
* @param resourceTemplateSpecification The resource template to add
- * @return Mono that completes when clients have been notified of the change
+ * @return Mono that completes when the server has been updated
*/
public Mono addResourceTemplate(
McpStatelessServerFeatures.AsyncResourceTemplateSpecification resourceTemplateSpecification) {
+ if (this.resourcesRepository != null) {
+ return Mono.error(new IllegalStateException(
+ "Server is configured with a resources repository and cannot accept async resource template registrations"));
+ }
if (this.serverCapabilities.resources() == null) {
return Mono.error(new IllegalStateException(
@@ -528,11 +657,32 @@ public Mono addResourceTemplate(
});
}
+ Mono addResourceTemplate(
+ McpStatelessServerFeatures.SyncResourceTemplateSpecification resourceTemplateSpecification) {
+ if (this.resourcesRepository != null) {
+ if (this.serverCapabilities.resources() == null) {
+ return Mono.error(new IllegalStateException(
+ "Server must be configured with resource capabilities to allow adding resource templates"));
+ }
+ return this.repositoryCallAdapter
+ .run(() -> this.resourcesRepository.addResourceTemplate(resourceTemplateSpecification));
+ }
+ return addResourceTemplate(McpStatelessServerFeatures.AsyncResourceTemplateSpecification
+ .fromSync(resourceTemplateSpecification, this.immediateExecution));
+ }
+
/**
- * List all registered resource templates.
- * @return A Flux stream of all registered resource templates
+ * List resource templates without a client request context.
+ * @return A Flux stream of context-free visible resource templates
*/
public Flux listResourceTemplates() {
+ if (this.resourcesRepository != null) {
+ return this.repositoryCallAdapter
+ .invoke(() -> this.resourcesRepository
+ .listResourceTemplates(new McpSchema.PaginatedRequest(), McpTransportContext.EMPTY)
+ .resourceTemplates())
+ .flatMapIterable(resourceTemplates -> resourceTemplates);
+ }
return Flux.fromIterable(this.resourceTemplates.values())
.map(McpStatelessServerFeatures.AsyncResourceTemplateSpecification::resourceTemplate);
}
@@ -540,7 +690,7 @@ public Flux listResourceTemplates() {
/**
* Remove a resource template at runtime.
* @param uriTemplate The URI template of the resource template to remove
- * @return Mono that completes when clients have been notified of the change
+ * @return Mono that completes when the server has been updated
*/
public Mono removeResourceTemplate(String uriTemplate) {
@@ -548,6 +698,10 @@ public Mono removeResourceTemplate(String uriTemplate) {
return Mono.error(new IllegalStateException(
"Server must be configured with resource capabilities to allow removing resource templates"));
}
+ if (this.resourcesRepository != null) {
+ return this.repositoryCallAdapter.invoke(() -> this.resourcesRepository.removeResourceTemplate(uriTemplate))
+ .then();
+ }
return Mono.defer(() -> {
McpStatelessServerFeatures.AsyncResourceTemplateSpecification removed = this.resourceTemplates
@@ -564,6 +718,10 @@ public Mono removeResourceTemplate(String uriTemplate) {
private McpStatelessRequestHandler resourcesListRequestHandler() {
return (ctx, params) -> {
+ if (this.resourcesRepository != null) {
+ McpSchema.PaginatedRequest request = paginatedRequest(params);
+ return this.repositoryCallAdapter.invoke(() -> this.resourcesRepository.listResources(request, ctx));
+ }
var resourceList = this.resources.values()
.stream()
.map(McpStatelessServerFeatures.AsyncResourceSpecification::resource)
@@ -573,7 +731,12 @@ private McpStatelessRequestHandler resourcesListR
}
private McpStatelessRequestHandler resourceTemplateListRequestHandler() {
- return (exchange, params) -> {
+ return (ctx, params) -> {
+ if (this.resourcesRepository != null) {
+ McpSchema.PaginatedRequest request = paginatedRequest(params);
+ return this.repositoryCallAdapter
+ .invoke(() -> this.resourcesRepository.listResourceTemplates(request, ctx));
+ }
var resourceList = this.resourceTemplates.values()
.stream()
.map(AsyncResourceTemplateSpecification::resourceTemplate)
@@ -588,6 +751,21 @@ private McpStatelessRequestHandler resourcesReadRe
});
var resourceUri = resourceRequest.uri();
+ if (this.resourcesRepository != null) {
+ return this.repositoryCallAdapter
+ .invoke(() -> this.resourcesRepository.resolveResource(resourceUri, ctx))
+ .flatMap(resource -> {
+ if (resource.isPresent()) {
+ return readRepositoryResource(resourceRequest, ctx);
+ }
+ return this.repositoryCallAdapter
+ .invoke(() -> this.resourcesRepository.resolveResourceTemplate(resourceUri, ctx))
+ .flatMap(resourceTemplate -> resourceTemplate.isPresent()
+ ? readRepositoryResource(resourceRequest, ctx)
+ : Mono.error(RESOURCE_NOT_FOUND.apply(resourceUri)));
+ });
+ }
+
// First try to find a static resource specification
// Static resources have exact URIs
return this.findResourceSpecification(resourceUri)
@@ -603,6 +781,12 @@ private McpStatelessRequestHandler resourcesReadRe
};
}
+ private Mono readRepositoryResource(McpSchema.ReadResourceRequest resourceRequest,
+ McpTransportContext transportContext) {
+ return this.repositoryCallAdapter
+ .invoke(() -> this.resourcesRepository.readResource(resourceRequest, transportContext));
+ }
+
private Optional findResourceSpecification(String uri) {
var result = this.resources.values()
.stream()
@@ -626,9 +810,13 @@ private Optional
/**
* Add a new prompt handler at runtime.
* @param promptSpecification The prompt handler to add
- * @return Mono that completes when clients have been notified of the change
+ * @return Mono that completes when the server has been updated
*/
public Mono addPrompt(McpStatelessServerFeatures.AsyncPromptSpecification promptSpecification) {
+ if (this.promptsRepository != null) {
+ return Mono.error(new IllegalStateException(
+ "Server is configured with a prompts repository and cannot accept async prompt registrations"));
+ }
if (promptSpecification == null) {
return Mono.error(new IllegalArgumentException("Prompt specification must not be null"));
}
@@ -649,11 +837,32 @@ public Mono addPrompt(McpStatelessServerFeatures.AsyncPromptSpecification
});
}
+ Mono addPrompt(McpStatelessServerFeatures.SyncPromptSpecification promptSpecification) {
+ if (this.promptsRepository != null) {
+ if (promptSpecification == null) {
+ return Mono.error(new IllegalArgumentException("Prompt specification must not be null"));
+ }
+ if (this.serverCapabilities.prompts() == null) {
+ return Mono.error(new IllegalStateException("Server must be configured with prompt capabilities"));
+ }
+ return this.repositoryCallAdapter.run(() -> this.promptsRepository.addPrompt(promptSpecification));
+ }
+ return addPrompt(McpStatelessServerFeatures.AsyncPromptSpecification.fromSync(promptSpecification,
+ this.immediateExecution));
+ }
+
/**
- * List all registered prompts.
- * @return A Flux stream of all registered prompts
+ * List prompts without a client request context.
+ * @return A Flux stream of context-free visible prompts
*/
public Flux listPrompts() {
+ if (this.promptsRepository != null) {
+ return this.repositoryCallAdapter
+ .invoke(() -> this.promptsRepository
+ .listPrompts(new McpSchema.PaginatedRequest(), McpTransportContext.EMPTY)
+ .prompts())
+ .flatMapIterable(prompts -> prompts);
+ }
return Flux.fromIterable(this.prompts.values())
.map(McpStatelessServerFeatures.AsyncPromptSpecification::prompt);
}
@@ -661,7 +870,7 @@ public Flux listPrompts() {
/**
* 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
+ * @return Mono that completes when the server has been updated
*/
public Mono removePrompt(String promptName) {
if (promptName == null) {
@@ -670,6 +879,9 @@ public Mono removePrompt(String promptName) {
if (this.serverCapabilities.prompts() == null) {
return Mono.error(new IllegalStateException("Server must be configured with prompt capabilities"));
}
+ if (this.promptsRepository != null) {
+ return this.repositoryCallAdapter.invoke(() -> this.promptsRepository.removePrompt(promptName)).then();
+ }
return Mono.defer(() -> {
McpStatelessServerFeatures.AsyncPromptSpecification removed = this.prompts.remove(promptName);
@@ -688,6 +900,10 @@ public Mono removePrompt(String promptName) {
private McpStatelessRequestHandler promptsListRequestHandler() {
return (ctx, params) -> {
+ if (this.promptsRepository != null) {
+ McpSchema.PaginatedRequest request = paginatedRequest(params);
+ return this.repositoryCallAdapter.invoke(() -> this.promptsRepository.listPrompts(request, ctx));
+ }
// TODO: Implement pagination
// McpSchema.PaginatedRequest request = objectMapper.convertValue(params,
// new TypeReference() {
@@ -708,6 +924,18 @@ private McpStatelessRequestHandler promptsGetRequestH
new TypeRef() {
});
+ if (this.promptsRepository != null) {
+ return this.repositoryCallAdapter
+ .invoke(() -> this.promptsRepository.resolvePrompt(promptRequest.name(), ctx))
+ .flatMap(prompt -> prompt
+ .map(resolvedPrompt -> this.repositoryCallAdapter
+ .invoke(() -> this.promptsRepository.getPrompt(promptRequest, ctx)))
+ .orElseGet(() -> Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
+ .message("Invalid prompt name")
+ .data("Prompt not found: " + promptRequest.name())
+ .build())));
+ }
+
// Implement prompt retrieval logic here
McpStatelessServerFeatures.AsyncPromptSpecification specification = this.prompts.get(promptRequest.name());
if (specification == null) {
@@ -721,8 +949,10 @@ private McpStatelessRequestHandler promptsGetRequestH
};
}
- private static final Mono EMPTY_COMPLETION_RESULT = Mono
- .just(new McpSchema.CompleteResult(new CompleteCompletion(List.of(), 0, false)));
+ private static final McpSchema.CompleteResult EMPTY_COMPLETION = new McpSchema.CompleteResult(
+ new CompleteCompletion(List.of(), 0, false));
+
+ private static final Mono EMPTY_COMPLETION_RESULT = Mono.just(EMPTY_COMPLETION);
private McpStatelessRequestHandler completionCompleteRequestHandler() {
return (ctx, params) -> {
@@ -744,84 +974,130 @@ private McpStatelessRequestHandler completionCompleteR
String argumentName = request.argument().name();
- // Check if valid a Prompt exists for this completion request
- if (type.equals(PromptReference.TYPE)
- && request.ref() instanceof McpSchema.PromptReference promptReference) {
+ return validateCompletionReference(ctx, request, type, argumentName)
+ .flatMap(result -> result.map(Mono::just).orElseGet(() -> {
+ if (this.completionsRepository != null) {
+ return this.repositoryCallAdapter
+ .invoke(() -> this.completionsRepository.complete(request, ctx));
+ }
+ return resolveCompletionSpecification(request.ref())
+ .flatMap(specification -> specification.completionHandler().apply(ctx, request));
+ }));
+ };
+ }
+
+ private Mono> validateCompletionReference(McpTransportContext transportContext,
+ McpSchema.CompleteRequest request, String type, String argumentName) {
- McpStatelessServerFeatures.AsyncPromptSpecification promptSpec = this.prompts
- .get(promptReference.name());
- if (promptSpec == null) {
+ if (type.equals(PromptReference.TYPE) && request.ref() instanceof McpSchema.PromptReference promptReference) {
+ return resolvePrompt(promptReference.name(), transportContext).flatMap(prompt -> {
+ if (prompt.isEmpty()) {
return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
.message("Prompt not found: " + promptReference.name())
.build());
}
- List arguments = promptSpec.prompt().arguments();
- if (arguments == null
- || !arguments.stream().filter(arg -> arg.name().equals(argumentName)).findFirst().isPresent()) {
+ List arguments = prompt.get().arguments();
+ if (arguments == null || arguments.stream().noneMatch(arg -> arg.name().equals(argumentName))) {
logger.warn("Argument not found: {} in prompt: {}", argumentName, promptReference.name());
-
- return EMPTY_COMPLETION_RESULT;
+ return Mono.just(Optional.of(EMPTY_COMPLETION));
}
- }
- // Check if valid Resource or ResourceTemplate exists for this completion
- // request
- if (type.equals(ResourceReference.TYPE)
- && request.ref() instanceof McpSchema.ResourceReference resourceReference) {
+ return Mono.just(Optional.empty());
+ });
+ }
- var uriTemplateManager = uriTemplateManagerFactory.create(resourceReference.uri());
+ if (type.equals(ResourceReference.TYPE)
+ && request.ref() instanceof McpSchema.ResourceReference resourceReference) {
- if (!uriTemplateManager.isUriTemplate(resourceReference.uri())) {
- // Attempting to autocomplete a fixed resource URI is not an error in
- // the spec (but probably should be).
- return EMPTY_COMPLETION_RESULT;
- }
+ var uriTemplateManager = uriTemplateManagerFactory.create(resourceReference.uri());
- McpStatelessServerFeatures.AsyncResourceSpecification resourceSpec = this
- .findResourceSpecification(resourceReference.uri())
- .orElse(null);
+ if (!uriTemplateManager.isUriTemplate(resourceReference.uri())) {
+ // Attempting to autocomplete a fixed resource URI is not an error in
+ // the spec (but probably should be).
+ return Mono.just(Optional.of(EMPTY_COMPLETION));
+ }
- if (resourceSpec != null) {
- if (!uriTemplateManagerFactory.create(resourceSpec.resource().uri())
- .getVariableNames()
- .contains(argumentName)) {
+ return validateResourceCompletionReference(resourceReference, argumentName, transportContext);
+ }
- return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
- .message("Argument not found: " + argumentName + " in resource: " + resourceReference.uri())
- .build());
- }
- }
- else {
- var templateSpec = this.findResourceTemplateSpecification(resourceReference.uri()).orElse(null);
- if (templateSpec != null) {
-
- if (!uriTemplateManagerFactory.create(templateSpec.resourceTemplate().uriTemplate())
- .getVariableNames()
- .contains(argumentName)) {
-
- return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
- .message("Argument not found: " + argumentName + " in resource template: "
- + resourceReference.uri())
- .build());
- }
- }
- else {
- return Mono.error(RESOURCE_NOT_FOUND.apply(resourceReference.uri()));
- }
+ return Mono.just(Optional.empty());
+ }
+
+ private Mono> validateResourceCompletionReference(
+ McpSchema.ResourceReference resourceReference, String argumentName, McpTransportContext transportContext) {
+
+ return resolveResource(resourceReference.uri(), transportContext).flatMap(resource -> {
+ if (resource.isPresent()) {
+ if (!uriTemplateManagerFactory.create(resource.get().uri()).getVariableNames().contains(argumentName)) {
+ return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
+ .message("Argument not found: " + argumentName + " in resource: " + resourceReference.uri())
+ .build());
}
+ return Mono.just(Optional.empty());
}
- McpStatelessServerFeatures.AsyncCompletionSpecification specification = this.completions.get(request.ref());
+ return resolveResourceTemplate(resourceReference.uri(), transportContext).flatMap(resourceTemplate -> {
+ if (resourceTemplate.isEmpty()) {
+ return Mono.error(RESOURCE_NOT_FOUND.apply(resourceReference.uri()));
+ }
+ if (!uriTemplateManagerFactory.create(resourceTemplate.get().uriTemplate())
+ .getVariableNames()
+ .contains(argumentName)) {
+ return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
+ .message("Argument not found: " + argumentName + " in resource template: "
+ + resourceReference.uri())
+ .build());
+ }
+ return Mono.just(Optional.empty());
+ });
+ });
+ }
- if (specification == null) {
- return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
- .message("AsyncCompletionSpecification not found: " + request.ref())
- .build());
- }
+ private Mono> resolvePrompt(String name, McpTransportContext transportContext) {
- return specification.completionHandler().apply(ctx, request);
- };
+ if (this.promptsRepository != null) {
+ return this.repositoryCallAdapter
+ .invoke(() -> this.promptsRepository.resolvePrompt(name, transportContext));
+ }
+
+ return Mono.just(Optional.ofNullable(this.prompts.get(name))
+ .map(McpStatelessServerFeatures.AsyncPromptSpecification::prompt));
+ }
+
+ private Mono> resolveResource(String uri, McpTransportContext transportContext) {
+
+ if (this.resourcesRepository != null) {
+ return this.repositoryCallAdapter
+ .invoke(() -> this.resourcesRepository.resolveResource(uri, transportContext));
+ }
+
+ return Mono
+ .just(findResourceSpecification(uri).map(McpStatelessServerFeatures.AsyncResourceSpecification::resource));
+ }
+
+ private Mono> resolveResourceTemplate(String uri,
+ McpTransportContext transportContext) {
+
+ if (this.resourcesRepository != null) {
+ return this.repositoryCallAdapter
+ .invoke(() -> this.resourcesRepository.resolveResourceTemplate(uri, transportContext));
+ }
+
+ return Mono.just(findResourceTemplateSpecification(uri)
+ .map(McpStatelessServerFeatures.AsyncResourceTemplateSpecification::resourceTemplate));
+ }
+
+ private Mono resolveCompletionSpecification(
+ McpSchema.CompleteReference reference) {
+
+ McpStatelessServerFeatures.AsyncCompletionSpecification specification = this.completions.get(reference);
+ if (specification == null) {
+ return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS)
+ .message("AsyncCompletionSpecification not found: " + reference)
+ .build());
+ }
+ return Mono.just(specification);
}
/**
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessServerFeatures.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessServerFeatures.java
index 0c1fbfba7..002de8361 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessServerFeatures.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessServerFeatures.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024-2025 the original author or authors.
+ * Copyright 2024-2026 the original author or authors.
*/
package io.modelcontextprotocol.server;
@@ -24,6 +24,7 @@
*
* @author Dariusz Jędrzejczyk
* @author Christian Tzolov
+ * @author Taewoong Kim
*/
public class McpStatelessServerFeatures {
@@ -36,6 +37,17 @@ public class McpStatelessServerFeatures {
* @param resources The map of resource specifications
* @param resourceTemplates The map of resource templates
* @param prompts The map of prompt specifications
+ * @param completions The map of completion specifications
+ * @param toolsRepository The repository used to resolve tools from the request
+ * context
+ * @param resourcesRepository The repository used to resolve resources from the
+ * request context
+ * @param promptsRepository The repository used to resolve prompts from the request
+ * context
+ * @param completionsRepository The repository used to handle completion requests from
+ * the request context
+ * @param immediateExecution Whether repository and synchronous specification calls
+ * should execute immediately instead of being offloaded
* @param instructions The server instructions text
*/
record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
@@ -44,7 +56,9 @@ record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities s
Map resourceTemplates,
Map prompts,
Map completions,
- String instructions) {
+ ToolsRepository toolsRepository, ResourcesRepository resourcesRepository,
+ PromptsRepository promptsRepository, CompletionsRepository completionsRepository,
+ boolean immediateExecution, String instructions) {
/**
* Create an instance and validate the arguments.
@@ -54,6 +68,17 @@ record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities s
* @param resources The map of resource specifications
* @param resourceTemplates The map of resource templates
* @param prompts The map of prompt specifications
+ * @param completions The map of completion specifications
+ * @param toolsRepository The repository used to resolve tools from the request
+ * context
+ * @param resourcesRepository The repository used to resolve resources from the
+ * request context
+ * @param promptsRepository The repository used to resolve prompts from the
+ * request context
+ * @param completionsRepository The repository used to handle completion requests
+ * from the request context
+ * @param immediateExecution Whether repository and synchronous specification
+ * calls should execute immediately instead of being offloaded
* @param instructions The server instructions text
*/
Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
@@ -62,33 +87,42 @@ record Async(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities s
Map resourceTemplates,
Map prompts,
Map completions,
- String instructions) {
+ ToolsRepository toolsRepository, ResourcesRepository resourcesRepository,
+ PromptsRepository promptsRepository, CompletionsRepository completionsRepository,
+ boolean immediateExecution, 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
- null, // currently statless server doesn't support set logging
- !Utils.isEmpty(prompts) ? McpSchema.ServerCapabilities.PromptCapabilities.builder().build()
+ : new McpSchema.ServerCapabilities(
+ (completionsRepository != null) ? new McpSchema.ServerCapabilities.CompletionCapabilities()
: null,
- !Utils.isEmpty(resources)
+ null, // experimental
+ null, // currently stateless server does not support logging
+ (!Utils.isEmpty(prompts) || promptsRepository != null)
+ ? McpSchema.ServerCapabilities.PromptCapabilities.builder().build() : null,
+ (!Utils.isEmpty(resources) || resourcesRepository != null)
? McpSchema.ServerCapabilities.ResourceCapabilities.builder().build() : null,
- !Utils.isEmpty(tools) ? McpSchema.ServerCapabilities.ToolCapabilities.builder().build()
- : null);
+ (!Utils.isEmpty(tools) || toolsRepository != null)
+ ? McpSchema.ServerCapabilities.ToolCapabilities.builder().build() : null);
this.tools = (tools != null) ? tools : List.of();
this.resources = (resources != null) ? resources : Map.of();
this.resourceTemplates = (resourceTemplates != null) ? resourceTemplates : Map.of();
this.prompts = (prompts != null) ? prompts : Map.of();
this.completions = (completions != null) ? completions : Map.of();
+ this.toolsRepository = toolsRepository;
+ this.resourcesRepository = resourcesRepository;
+ this.promptsRepository = promptsRepository;
+ this.completionsRepository = completionsRepository;
+ this.immediateExecution = immediateExecution;
this.instructions = instructions;
}
/**
- * Convert a synchronous specification into an asynchronous one and provide
- * blocking code offloading to prevent accidental blocking of the non-blocking
+ * Convert a synchronous specification into an asynchronous one, optionally
+ * offloading blocking code 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
@@ -123,7 +157,9 @@ static Async fromSync(Sync syncSpec, boolean immediateExecution) {
});
return new Async(syncSpec.serverInfo(), syncSpec.serverCapabilities(), tools, resources, resourceTemplates,
- prompts, completions, syncSpec.instructions());
+ prompts, completions, syncSpec.toolsRepository(), syncSpec.resourcesRepository(),
+ syncSpec.promptsRepository(), syncSpec.completionsRepository(), immediateExecution,
+ syncSpec.instructions());
}
}
@@ -136,6 +172,15 @@ static Async fromSync(Sync syncSpec, boolean immediateExecution) {
* @param resources The map of resource specifications
* @param resourceTemplates The map of resource templates
* @param prompts The map of prompt specifications
+ * @param completions The map of completion specifications
+ * @param toolsRepository The repository used to resolve tools from the request
+ * context
+ * @param resourcesRepository The repository used to resolve resources from the
+ * request context
+ * @param promptsRepository The repository used to resolve prompts from the request
+ * context
+ * @param completionsRepository The repository used to handle completion requests from
+ * the request context
* @param instructions The server instructions text
*/
record Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
@@ -144,7 +189,8 @@ record Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities se
Map resourceTemplates,
Map prompts,
Map completions,
- String instructions) {
+ ToolsRepository toolsRepository, ResourcesRepository resourcesRepository,
+ PromptsRepository promptsRepository, CompletionsRepository completionsRepository, String instructions) {
/**
* Create an instance and validate the arguments.
@@ -154,6 +200,15 @@ record Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities se
* @param resources The map of resource specifications
* @param resourceTemplates The map of resource templates
* @param prompts The map of prompt specifications
+ * @param completions The map of completion specifications
+ * @param toolsRepository The repository used to resolve tools from the request
+ * context
+ * @param resourcesRepository The repository used to resolve resources from the
+ * request context
+ * @param promptsRepository The repository used to resolve prompts from the
+ * request context
+ * @param completionsRepository The repository used to handle completion requests
+ * from the request context
* @param instructions The server instructions text
*/
Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities serverCapabilities,
@@ -162,30 +217,37 @@ record Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities se
Map resourceTemplates,
Map prompts,
Map completions,
- String instructions) {
+ ToolsRepository toolsRepository, ResourcesRepository resourcesRepository,
+ PromptsRepository promptsRepository, CompletionsRepository completionsRepository, String instructions) {
Assert.notNull(serverInfo, "Server info must not be null");
this.serverInfo = serverInfo;
this.serverCapabilities = (serverCapabilities != null) ? serverCapabilities
- : new McpSchema.ServerCapabilities(null, // completions
+ : new McpSchema.ServerCapabilities(
+ (completionsRepository != null) ? new McpSchema.ServerCapabilities.CompletionCapabilities()
+ : null,
null, // experimental
new McpSchema.ServerCapabilities.LoggingCapabilities(), // Enable
// logging
// by
// default
- !Utils.isEmpty(prompts) ? McpSchema.ServerCapabilities.PromptCapabilities.builder().build()
- : null,
- !Utils.isEmpty(resources)
+ (!Utils.isEmpty(prompts) || promptsRepository != null)
+ ? McpSchema.ServerCapabilities.PromptCapabilities.builder().build() : null,
+ (!Utils.isEmpty(resources) || resourcesRepository != null)
? McpSchema.ServerCapabilities.ResourceCapabilities.builder().build() : null,
- !Utils.isEmpty(tools) ? McpSchema.ServerCapabilities.ToolCapabilities.builder().build()
- : null);
+ (!Utils.isEmpty(tools) || toolsRepository != null)
+ ? McpSchema.ServerCapabilities.ToolCapabilities.builder().build() : null);
this.tools = (tools != null) ? tools : new ArrayList<>();
this.resources = (resources != null) ? resources : new HashMap<>();
this.resourceTemplates = (resourceTemplates != null) ? resourceTemplates : Map.of();
this.prompts = (prompts != null) ? prompts : new HashMap<>();
this.completions = (completions != null) ? completions : new HashMap<>();
+ this.toolsRepository = toolsRepository;
+ this.resourcesRepository = resourcesRepository;
+ this.promptsRepository = promptsRepository;
+ this.completionsRepository = completionsRepository;
this.instructions = instructions;
}
@@ -197,8 +259,9 @@ record Sync(McpSchema.Implementation serverInfo, McpSchema.ServerCapabilities se
* represents a specific capability.
*
* @param tool The tool definition including name, description, and parameter schema
- * @param callHandler The function that implements the tool's logic, receiving a
- * {@link CallToolRequest} and returning the result.
+ * @param callHandler The function that implements the tool's logic. The first
+ * argument is the {@link McpTransportContext}; the second argument is the
+ * {@link CallToolRequest}.
*/
public record AsyncToolSpecification(McpSchema.Tool tool,
BiFunction> callHandler) {
@@ -290,7 +353,8 @@ public static Builder builder() {
*
* @param resource The resource definition including name, description, and MIME type
* @param readHandler The function that handles resource read requests. The function's
- * argument is a {@link McpSchema.ReadResourceRequest}.
+ * first argument is the {@link McpTransportContext}; the second argument is the
+ * {@link McpSchema.ReadResourceRequest}.
*/
public record AsyncResourceSpecification(McpSchema.Resource resource,
BiFunction> readHandler) {
@@ -308,7 +372,7 @@ static AsyncResourceSpecification fromSync(SyncResourceSpecification resource, b
}
/**
- * Specification of a resource template with its synchronous handler function.
+ * Specification of a resource template with its asynchronous handler function.
* Resource templates allow servers to expose parameterized resources using URI
* templates: URI
* templates.. Arguments may be auto-completed through > readHandler) {
@@ -360,8 +422,8 @@ static AsyncResourceTemplateSpecification fromSync(SyncResourceTemplateSpecifica
*
* @param prompt The prompt definition including name and description
* @param promptHandler The function that processes prompt requests and returns
- * formatted templates. The function's argument is a
- * {@link McpSchema.GetPromptRequest}.
+ * formatted templates. The first argument is the {@link McpTransportContext}; the
+ * second argument is the {@link McpSchema.GetPromptRequest}.
*/
public record AsyncPromptSpecification(McpSchema.Prompt prompt,
BiFunction> promptHandler) {
@@ -385,12 +447,13 @@ static AsyncPromptSpecification fromSync(SyncPromptSpecification prompt, boolean
*
* - Customizable response generation logic
*
- Parameter-driven template expansion
- *
- Dynamic interaction with connected clients
+ *
- Context-aware completion logic
*
*
* @param referenceKey The unique key representing the completion reference.
* @param completionHandler The asynchronous function that processes completion
- * requests and returns results. The function's argument is a
+ * requests and returns results. The first argument is the
+ * {@link McpTransportContext}; the second argument is the
* {@link McpSchema.CompleteRequest}.
*/
public record AsyncCompletionSpecification(McpSchema.CompleteReference referenceKey,
@@ -398,9 +461,10 @@ public record AsyncCompletionSpecification(McpSchema.CompleteReference reference
/**
* Converts a synchronous {@link SyncCompletionSpecification} into an
- * {@link AsyncCompletionSpecification} by wrapping the handler in a bounded
- * elastic scheduler for safe non-blocking execution.
+ * {@link AsyncCompletionSpecification}, optionally offloading the handler to a
+ * bounded elastic scheduler for safe non-blocking execution.
* @param completion the synchronous completion specification
+ * @param immediateExecution whether the handler should execute immediately
* @return an asynchronous wrapper of the provided sync specification, or
* {@code null} if input is null
*/
@@ -422,8 +486,9 @@ static AsyncCompletionSpecification fromSync(SyncCompletionSpecification complet
* primary way for MCP servers to expose functionality to AI models.
*
* @param tool The tool definition including name, description, and parameter schema
- * @param callHandler The function that implements the tool's logic, receiving a
- * {@link CallToolRequest} and returning results.
+ * @param callHandler The function that implements the tool's logic. The first
+ * argument is the {@link McpTransportContext}; the second argument is the
+ * {@link CallToolRequest}.
*/
public record SyncToolSpecification(McpSchema.Tool tool,
BiFunction callHandler) {
@@ -491,7 +556,8 @@ public SyncToolSpecification build() {
*
* @param resource The resource definition including name, description, and MIME type
* @param readHandler The function that handles resource read requests. The function's
- * argument is a {@link McpSchema.ReadResourceRequest}.
+ * first argument is the {@link McpTransportContext}; the second argument is the
+ * {@link McpSchema.ReadResourceRequest}.
*/
public record SyncResourceSpecification(McpSchema.Resource resource,
BiFunction readHandler) {
@@ -516,10 +582,8 @@ public record SyncResourceSpecification(McpSchema.Resource resource,
* @param resourceTemplate The resource template definition including name,
* description, and parameter schema
* @param readHandler The function that handles resource read requests. The function's
- * first argument is an {@link McpTransportContext} upon which the server can interact
- * with the connected client. The second arguments is a
- * {@link McpSchema.ReadResourceRequest}. {@link McpSchema.ResourceTemplate}
- * {@link McpSchema.ReadResourceResult}
+ * first argument is the {@link McpTransportContext}; the second argument is the
+ * {@link McpSchema.ReadResourceRequest}.
*/
public record SyncResourceTemplateSpecification(McpSchema.ResourceTemplate resourceTemplate,
BiFunction readHandler) {
@@ -538,8 +602,8 @@ public record SyncResourceTemplateSpecification(McpSchema.ResourceTemplate resou
*
* @param prompt The prompt definition including name and description
* @param promptHandler The function that processes prompt requests and returns
- * formatted templates. The function's argument is a
- * {@link McpSchema.GetPromptRequest}.
+ * formatted templates. The first argument is the {@link McpTransportContext}; the
+ * second argument is the {@link McpSchema.GetPromptRequest}.
*/
public record SyncPromptSpecification(McpSchema.Prompt prompt,
BiFunction promptHandler) {
@@ -550,7 +614,9 @@ public record SyncPromptSpecification(McpSchema.Prompt prompt,
*
* @param referenceKey The unique key representing the completion reference.
* @param completionHandler The synchronous function that processes completion
- * requests and returns results. The argument is a {@link McpSchema.CompleteRequest}.
+ * requests and returns results. The first argument is the
+ * {@link McpTransportContext}; the second argument is the
+ * {@link McpSchema.CompleteRequest}.
*/
public record SyncCompletionSpecification(McpSchema.CompleteReference referenceKey,
BiFunction completionHandler) {
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessSyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessSyncServer.java
index 475f88df8..9fd55a1d9 100644
--- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessSyncServer.java
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessSyncServer.java
@@ -1,15 +1,14 @@
/*
- * Copyright 2024-2024 the original author or authors.
+ * Copyright 2024-2026 the original author or authors.
*/
package io.modelcontextprotocol.server;
+import java.util.List;
+
import io.modelcontextprotocol.spec.McpSchema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import reactor.core.publisher.Mono;
-
-import java.util.List;
/**
* A stateless MCP server implementation for use with Streamable HTTP transport types. It
@@ -18,6 +17,7 @@
* knowledge and can serve the clients with the capabilities it supports.
*
* @author Dariusz Jędrzejczyk
+ * @author Taewoong Kim
*/
public class McpStatelessSyncServer {
@@ -25,11 +25,8 @@ public class McpStatelessSyncServer {
private final McpStatelessAsyncServer asyncServer;
- private final boolean immediateExecution;
-
- McpStatelessSyncServer(McpStatelessAsyncServer asyncServer, boolean immediateExecution) {
+ McpStatelessSyncServer(McpStatelessAsyncServer asyncServer) {
this.asyncServer = asyncServer;
- this.immediateExecution = immediateExecution;
}
/**
@@ -68,15 +65,12 @@ public void close() {
* @param toolSpecification The tool specification to add
*/
public void addTool(McpStatelessServerFeatures.SyncToolSpecification toolSpecification) {
- this.asyncServer
- .addTool(McpStatelessServerFeatures.AsyncToolSpecification.fromSync(toolSpecification,
- this.immediateExecution))
- .block();
+ this.asyncServer.addTool(toolSpecification).block();
}
/**
- * List all registered tools.
- * @return A list of all registered tools
+ * List tools without a client request context.
+ * @return A list of context-free visible tools
*/
public List listTools() {
return this.asyncServer.listTools().collectList().block();
@@ -95,15 +89,12 @@ public void removeTool(String toolName) {
* @param resourceSpecification The resource handler to add
*/
public void addResource(McpStatelessServerFeatures.SyncResourceSpecification resourceSpecification) {
- this.asyncServer
- .addResource(McpStatelessServerFeatures.AsyncResourceSpecification.fromSync(resourceSpecification,
- this.immediateExecution))
- .block();
+ this.asyncServer.addResource(resourceSpecification).block();
}
/**
- * List all registered resources.
- * @return A list of all registered resources
+ * List resources without a client request context.
+ * @return A list of context-free visible resources
*/
public List listResources() {
return this.asyncServer.listResources().collectList().block();
@@ -123,15 +114,12 @@ public void removeResource(String resourceUri) {
*/
public void addResourceTemplate(
McpStatelessServerFeatures.SyncResourceTemplateSpecification resourceTemplateSpecification) {
- this.asyncServer
- .addResourceTemplate(McpStatelessServerFeatures.AsyncResourceTemplateSpecification
- .fromSync(resourceTemplateSpecification, this.immediateExecution))
- .block();
+ this.asyncServer.addResourceTemplate(resourceTemplateSpecification).block();
}
/**
- * List all registered resource templates.
- * @return A list of all registered resource templates
+ * List resource templates without a client request context.
+ * @return A list of context-free visible resource templates
*/
public List listResourceTemplates() {
return this.asyncServer.listResourceTemplates().collectList().block();
@@ -150,15 +138,12 @@ public void removeResourceTemplate(String uriTemplate) {
* @param promptSpecification The prompt handler to add
*/
public void addPrompt(McpStatelessServerFeatures.SyncPromptSpecification promptSpecification) {
- this.asyncServer
- .addPrompt(McpStatelessServerFeatures.AsyncPromptSpecification.fromSync(promptSpecification,
- this.immediateExecution))
- .block();
+ this.asyncServer.addPrompt(promptSpecification).block();
}
/**
- * List all registered prompts.
- * @return A list of all registered prompts
+ * List prompts without a client request context.
+ * @return A list of context-free visible prompts
*/
public List listPrompts() {
return this.asyncServer.listPrompts().collectList().block();
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/PromptsRepository.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/PromptsRepository.java
new file mode 100644
index 000000000..92da526fb
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/PromptsRepository.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ */
+
+package io.modelcontextprotocol.server;
+
+import java.util.Optional;
+
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.spec.McpSchema;
+
+/**
+ * Repository contract for listing, resolving, and getting stateless prompts from the
+ * current MCP request context.
+ *
+ * Context-free server-side helper calls receive {@link McpTransportContext#EMPTY}.
+ *
+ * @author Taewoong Kim
+ */
+public interface PromptsRepository {
+
+ /**
+ * List prompts visible for the current request context.
+ * @param request the paginated list request
+ * @param transportContext the transport context for the current request, or
+ * {@link McpTransportContext#EMPTY} for a context-free server-side call
+ * @return the prompts visible for the current request
+ */
+ McpSchema.ListPromptsResult listPrompts(McpSchema.PaginatedRequest request, McpTransportContext transportContext);
+
+ /**
+ * Resolve prompt metadata for the current request context.
+ * @param name the prompt name
+ * @param transportContext the transport context for the current request
+ * @return the matching prompt metadata, if any
+ */
+ Optional resolvePrompt(String name, McpTransportContext transportContext);
+
+ /**
+ * Get a prompt for the current request context.
+ * @param request the prompt get request
+ * @param transportContext the transport context for the current request
+ * @return the prompt result
+ */
+ McpSchema.GetPromptResult getPrompt(McpSchema.GetPromptRequest request, McpTransportContext transportContext);
+
+ /**
+ * Add a prompt to repositories that support runtime mutation.
+ *
+ * The default implementation throws {@link UnsupportedOperationException}.
+ * @param promptSpecification the prompt specification to add
+ */
+ default void addPrompt(McpStatelessServerFeatures.SyncPromptSpecification promptSpecification) {
+ throw new UnsupportedOperationException("Prompts repository does not support adding prompts");
+ }
+
+ /**
+ * Remove a prompt from repositories that support runtime mutation.
+ *
+ * The default implementation throws {@link UnsupportedOperationException}.
+ * @param promptName the prompt name to remove
+ * @return {@code true} if a prompt was removed
+ */
+ default boolean removePrompt(String promptName) {
+ throw new UnsupportedOperationException("Prompts repository does not support removing prompts");
+ }
+
+}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/ResourcesRepository.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/ResourcesRepository.java
new file mode 100644
index 000000000..ae4a53ac9
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/ResourcesRepository.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ */
+
+package io.modelcontextprotocol.server;
+
+import java.util.Optional;
+
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.spec.McpSchema;
+
+/**
+ * Repository contract for listing, resolving, and reading stateless resources from the
+ * current MCP request context.
+ *
+ * Context-free server-side helper calls receive {@link McpTransportContext#EMPTY}.
+ *
+ * @author Taewoong Kim
+ */
+public interface ResourcesRepository {
+
+ /**
+ * List resources visible for the current request context.
+ * @param request the paginated list request
+ * @param transportContext the transport context for the current request, or
+ * {@link McpTransportContext#EMPTY} for a context-free server-side call
+ * @return the resources visible for the current request
+ */
+ McpSchema.ListResourcesResult listResources(McpSchema.PaginatedRequest request,
+ McpTransportContext transportContext);
+
+ /**
+ * List resource templates visible for the current request context.
+ * @param request the paginated list request
+ * @param transportContext the transport context for the current request, or
+ * {@link McpTransportContext#EMPTY} for a context-free server-side call
+ * @return the resource templates visible for the current request
+ */
+ McpSchema.ListResourceTemplatesResult listResourceTemplates(McpSchema.PaginatedRequest request,
+ McpTransportContext transportContext);
+
+ /**
+ * Resolve resource metadata for the current request context.
+ * @param uri the requested resource URI
+ * @param transportContext the transport context for the current request
+ * @return the matching resource metadata, if any
+ */
+ Optional resolveResource(String uri, McpTransportContext transportContext);
+
+ /**
+ * Resolve resource-template metadata for the current request context.
+ * @param uri the requested resource URI
+ * @param transportContext the transport context for the current request
+ * @return the matching resource-template metadata, if any
+ */
+ Optional resolveResourceTemplate(String uri, McpTransportContext transportContext);
+
+ /**
+ * Read a resource for the current request context.
+ * @param request the resource read request
+ * @param transportContext the transport context for the current request
+ * @return the resource read result
+ */
+ McpSchema.ReadResourceResult readResource(McpSchema.ReadResourceRequest request,
+ McpTransportContext transportContext);
+
+ /**
+ * Add a resource to repositories that support runtime mutation.
+ *
+ * The default implementation throws {@link UnsupportedOperationException}.
+ * @param resourceSpecification the resource specification to add
+ */
+ default void addResource(McpStatelessServerFeatures.SyncResourceSpecification resourceSpecification) {
+ throw new UnsupportedOperationException("Resources repository does not support adding resources");
+ }
+
+ /**
+ * Remove a resource from repositories that support runtime mutation.
+ *
+ * The default implementation throws {@link UnsupportedOperationException}.
+ * @param resourceUri the resource URI to remove
+ * @return {@code true} if a resource was removed
+ */
+ default boolean removeResource(String resourceUri) {
+ throw new UnsupportedOperationException("Resources repository does not support removing resources");
+ }
+
+ /**
+ * Add a resource template to repositories that support runtime mutation.
+ *
+ * The default implementation throws {@link UnsupportedOperationException}.
+ * @param resourceTemplateSpecification the resource template specification to add
+ */
+ default void addResourceTemplate(
+ McpStatelessServerFeatures.SyncResourceTemplateSpecification resourceTemplateSpecification) {
+ throw new UnsupportedOperationException("Resources repository does not support adding resource templates");
+ }
+
+ /**
+ * Remove a resource template from repositories that support runtime mutation.
+ *
+ * The default implementation throws {@link UnsupportedOperationException}.
+ * @param uriTemplate the resource template URI template to remove
+ * @return {@code true} if a resource template was removed
+ */
+ default boolean removeResourceTemplate(String uriTemplate) {
+ throw new UnsupportedOperationException("Resources repository does not support removing resource templates");
+ }
+
+}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/SyncRepositoryCallAdapter.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/SyncRepositoryCallAdapter.java
new file mode 100644
index 000000000..9c015be75
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/SyncRepositoryCallAdapter.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ */
+
+package io.modelcontextprotocol.server;
+
+import java.util.function.Supplier;
+
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+
+/**
+ * Adapts synchronous repository calls to the stateless asynchronous server core.
+ *
+ * @author Taewoong Kim
+ */
+final class SyncRepositoryCallAdapter {
+
+ private final boolean immediateExecution;
+
+ SyncRepositoryCallAdapter(boolean immediateExecution) {
+ this.immediateExecution = immediateExecution;
+ }
+
+ Mono invoke(Supplier supplier) {
+ Mono result = Mono.fromCallable(supplier::get);
+ return this.immediateExecution ? result : result.subscribeOn(Schedulers.boundedElastic());
+ }
+
+ Mono run(Runnable runnable) {
+ Mono result = Mono.fromRunnable(runnable);
+ return this.immediateExecution ? result : result.subscribeOn(Schedulers.boundedElastic());
+ }
+
+}
diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/ToolsRepository.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/ToolsRepository.java
new file mode 100644
index 000000000..7d0a83cdd
--- /dev/null
+++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/ToolsRepository.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ */
+
+package io.modelcontextprotocol.server;
+
+import java.util.Optional;
+
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.spec.McpSchema.CallToolResult;
+
+/**
+ * Repository contract for listing, resolving, and calling stateless tools from the
+ * current MCP request context.
+ *
+ * Context-free server-side helper calls receive {@link McpTransportContext#EMPTY}.
+ *
+ * @author Taewoong Kim
+ */
+public interface ToolsRepository {
+
+ /**
+ * List tools visible for the current request context.
+ * @param request the paginated list request
+ * @param transportContext the transport context for the current request, or
+ * {@link McpTransportContext#EMPTY} for a context-free server-side call
+ * @return the tools visible for the current request
+ */
+ McpSchema.ListToolsResult listTools(McpSchema.PaginatedRequest request, McpTransportContext transportContext);
+
+ /**
+ * Resolve tool metadata for the current request context.
+ * @param name the tool name
+ * @param transportContext the transport context for the current request
+ * @return the matching tool metadata, if any
+ */
+ Optional resolveTool(String name, McpTransportContext transportContext);
+
+ /**
+ * Call a tool for the current request context.
+ * @param request the tool call request
+ * @param transportContext the transport context for the current request
+ * @return the tool call result
+ */
+ CallToolResult callTool(McpSchema.CallToolRequest request, McpTransportContext transportContext);
+
+ /**
+ * Add a tool to repositories that support runtime mutation.
+ *
+ * The default implementation throws {@link UnsupportedOperationException}.
+ * @param toolSpecification the tool specification to add
+ */
+ default void addTool(McpStatelessServerFeatures.SyncToolSpecification toolSpecification) {
+ throw new UnsupportedOperationException("Tools repository does not support adding tools");
+ }
+
+ /**
+ * Remove a tool from repositories that support runtime mutation.
+ *
+ * The default implementation throws {@link UnsupportedOperationException}.
+ * @param toolName the tool name to remove
+ * @return {@code true} if a tool was removed
+ */
+ default boolean removeTool(String toolName) {
+ throw new UnsupportedOperationException("Tools repository does not support removing tools");
+ }
+
+}
diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/StatelessRepositoriesTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/StatelessRepositoriesTests.java
new file mode 100644
index 000000000..5bf756b63
--- /dev/null
+++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/StatelessRepositoriesTests.java
@@ -0,0 +1,746 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ */
+
+package io.modelcontextprotocol.server;
+
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+
+import io.modelcontextprotocol.common.McpTransportContext;
+import io.modelcontextprotocol.json.McpJsonMapper;
+import io.modelcontextprotocol.json.TypeRef;
+import io.modelcontextprotocol.json.schema.JsonSchemaValidator;
+import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse;
+import io.modelcontextprotocol.spec.McpSchema;
+import io.modelcontextprotocol.spec.McpStatelessServerTransport;
+import io.modelcontextprotocol.spec.json.gson.GsonMcpJsonMapper;
+import org.junit.jupiter.api.Test;
+import reactor.core.publisher.Mono;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * Tests for stateless primitive repositories.
+ *
+ * @author Taewoong Kim
+ */
+class StatelessRepositoriesTests {
+
+ private static final String USER_KEY = "X-User";
+
+ private static final JsonSchemaValidator VALID_SCHEMA_VALIDATOR = (schema, structuredContent) -> ValidationResponse
+ .asValid(null);
+
+ private static final McpJsonMapper JSON_MAPPER = new PassThroughMcpJsonMapper();
+
+ @Test
+ void statelessRepositoriesResolvePrimitivesFromTransportContext() {
+ var transport = new MockStatelessServerTransport();
+ var repository = new ContextAwareRepository();
+
+ McpServer.sync(transport)
+ .jsonMapper(JSON_MAPPER)
+ .jsonSchemaValidator(VALID_SCHEMA_VALIDATOR)
+ .toolsRepository(repository)
+ .resourcesRepository(repository)
+ .promptsRepository(repository)
+ .completionsRepository(repository)
+ .build();
+
+ McpSchema.ListToolsResult tools = result(transport, "alice", McpSchema.METHOD_TOOLS_LIST,
+ new McpSchema.PaginatedRequest());
+ assertThat(tools.tools()).extracting(McpSchema.Tool::name).containsExactly("alice-tool");
+
+ McpSchema.CallToolResult toolResult = result(transport, "alice", McpSchema.METHOD_TOOLS_CALL,
+ McpSchema.CallToolRequest.builder("alice-tool").build());
+ assertThat(text(toolResult.content().get(0))).isEqualTo("tool:alice");
+
+ McpSchema.ListResourcesResult resources = result(transport, "alice", McpSchema.METHOD_RESOURCES_LIST,
+ new McpSchema.PaginatedRequest());
+ assertThat(resources.resources()).extracting(McpSchema.Resource::uri).containsExactly("test://resources/alice");
+
+ McpSchema.ReadResourceResult resourceResult = result(transport, "alice", McpSchema.METHOD_RESOURCES_READ,
+ McpSchema.ReadResourceRequest.builder("test://resources/alice").build());
+ assertThat(((McpSchema.TextResourceContents) resourceResult.contents().get(0)).text())
+ .isEqualTo("resource:alice");
+
+ McpSchema.ListResourceTemplatesResult templates = result(transport, "alice",
+ McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, new McpSchema.PaginatedRequest());
+ assertThat(templates.resourceTemplates()).extracting(McpSchema.ResourceTemplate::uriTemplate)
+ .containsExactly("test://resource-templates/alice/{name}");
+
+ McpSchema.ReadResourceResult templateResult = result(transport, "alice", McpSchema.METHOD_RESOURCES_READ,
+ McpSchema.ReadResourceRequest.builder("test://resource-templates/alice/guide").build());
+ assertThat(((McpSchema.TextResourceContents) templateResult.contents().get(0)).text())
+ .isEqualTo("resource-template:alice");
+
+ McpSchema.ListPromptsResult prompts = result(transport, "alice", McpSchema.METHOD_PROMPT_LIST,
+ new McpSchema.PaginatedRequest());
+ assertThat(prompts.prompts()).extracting(McpSchema.Prompt::name).containsExactly("alice-prompt");
+
+ McpSchema.GetPromptResult promptResult = result(transport, "alice", McpSchema.METHOD_PROMPT_GET,
+ McpSchema.GetPromptRequest.builder("alice-prompt").build());
+ assertThat(text(promptResult.messages().get(0).content())).isEqualTo("prompt:alice");
+
+ McpSchema.CompleteResult completeResult = result(transport, "alice", McpSchema.METHOD_COMPLETION_COMPLETE,
+ McpSchema.CompleteRequest
+ .builder(new McpSchema.PromptReference("alice-prompt"),
+ new McpSchema.CompleteRequest.CompleteArgument("name", "a"))
+ .build());
+ assertThat(completeResult.completion().values()).containsExactly("completion:alice");
+
+ McpSchema.CompleteResult resourceCompleteResult = result(transport, "alice",
+ McpSchema.METHOD_COMPLETION_COMPLETE,
+ McpSchema.CompleteRequest
+ .builder(new McpSchema.ResourceReference("test://resource-templates/alice/{name}"),
+ new McpSchema.CompleteRequest.CompleteArgument("name", "g"))
+ .build());
+ assertThat(resourceCompleteResult.completion().values()).containsExactly("completion:alice");
+
+ McpSchema.ListToolsResult bobTools = result(transport, "bob", McpSchema.METHOD_TOOLS_LIST,
+ new McpSchema.PaginatedRequest());
+ assertThat(bobTools.tools()).extracting(McpSchema.Tool::name).containsExactly("bob-tool");
+
+ McpSchema.CallToolResult bobToolResult = result(transport, "bob", McpSchema.METHOD_TOOLS_CALL,
+ McpSchema.CallToolRequest.builder("bob-tool").build());
+ assertThat(text(bobToolResult.content().get(0))).isEqualTo("tool:bob");
+
+ McpSchema.JSONRPCResponse hiddenResource = response(transport, "bob", McpSchema.METHOD_RESOURCES_READ,
+ McpSchema.ReadResourceRequest.builder("test://resources/alice").build());
+ assertThat(hiddenResource.error()).isNotNull();
+ assertThat(hiddenResource.error().code()).isEqualTo(McpSchema.ErrorCodes.RESOURCE_NOT_FOUND);
+ }
+
+ @Test
+ void statelessRepositoriesReceivePaginatedListRequests() {
+ var transport = new MockStatelessServerTransport();
+ var repository = new ContextAwareRepository();
+
+ McpServer.sync(transport)
+ .jsonMapper(JSON_MAPPER)
+ .jsonSchemaValidator(VALID_SCHEMA_VALIDATOR)
+ .toolsRepository(repository)
+ .resourcesRepository(repository)
+ .promptsRepository(repository)
+ .build();
+
+ McpSchema.ListToolsResult tools = result(transport, "alice", McpSchema.METHOD_TOOLS_LIST,
+ new McpSchema.PaginatedRequest("tools-cursor"));
+ assertThat(tools.nextCursor()).isEqualTo("tools-cursor");
+
+ McpSchema.ListResourcesResult resources = result(transport, "alice", McpSchema.METHOD_RESOURCES_LIST,
+ new McpSchema.PaginatedRequest("resources-cursor"));
+ assertThat(resources.nextCursor()).isEqualTo("resources-cursor");
+
+ McpSchema.ListResourceTemplatesResult templates = result(transport, "alice",
+ McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, new McpSchema.PaginatedRequest("templates-cursor"));
+ assertThat(templates.nextCursor()).isEqualTo("templates-cursor");
+
+ McpSchema.ListPromptsResult prompts = result(transport, "alice", McpSchema.METHOD_PROMPT_LIST,
+ new McpSchema.PaginatedRequest("prompts-cursor"));
+ assertThat(prompts.nextCursor()).isEqualTo("prompts-cursor");
+ }
+
+ @Test
+ void statelessRepositorySupportsRuntimeToolMutation() {
+ var transport = new MockStatelessServerTransport();
+ var repository = new MutableRepository();
+
+ McpStatelessSyncServer server = McpServer.sync(transport)
+ .jsonMapper(JSON_MAPPER)
+ .jsonSchemaValidator(VALID_SCHEMA_VALIDATOR)
+ .toolsRepository(repository)
+ .build();
+
+ server.addTool(toolSpec("runtime-tool", "runtime"));
+
+ McpSchema.ListToolsResult tools = result(transport, "alice", McpSchema.METHOD_TOOLS_LIST,
+ new McpSchema.PaginatedRequest());
+ assertThat(tools.tools()).extracting(McpSchema.Tool::name).containsExactly("runtime-tool");
+
+ server.removeTool("runtime-tool");
+
+ McpSchema.ListToolsResult emptyTools = result(transport, "alice", McpSchema.METHOD_TOOLS_LIST,
+ new McpSchema.PaginatedRequest());
+ assertThat(emptyTools.tools()).isEmpty();
+ }
+
+ @Test
+ void statelessRepositoriesSupportRuntimeResourceAndPromptMutation() {
+ var transport = new MockStatelessServerTransport();
+ var repository = new MutableRepository();
+
+ McpStatelessSyncServer server = McpServer.sync(transport)
+ .jsonMapper(JSON_MAPPER)
+ .jsonSchemaValidator(VALID_SCHEMA_VALIDATOR)
+ .resourcesRepository(repository)
+ .promptsRepository(repository)
+ .build();
+
+ server.addResource(resourceSpec("test://resources/runtime", "runtime-resource", "resource-runtime"));
+ server.addResourceTemplate(resourceTemplateSpec("test://resource-templates/runtime/{name}",
+ "runtime-resource-template", "template-runtime"));
+ server.addPrompt(promptSpec("runtime-prompt", "prompt-runtime"));
+
+ McpSchema.ListResourcesResult resources = result(transport, "alice", McpSchema.METHOD_RESOURCES_LIST,
+ new McpSchema.PaginatedRequest());
+ assertThat(resources.resources()).extracting(McpSchema.Resource::uri)
+ .containsExactly("test://resources/runtime");
+ McpSchema.ListResourceTemplatesResult templates = result(transport, "alice",
+ McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, new McpSchema.PaginatedRequest());
+ assertThat(templates.resourceTemplates()).extracting(McpSchema.ResourceTemplate::uriTemplate)
+ .containsExactly("test://resource-templates/runtime/{name}");
+ McpSchema.ListPromptsResult prompts = result(transport, "alice", McpSchema.METHOD_PROMPT_LIST,
+ new McpSchema.PaginatedRequest());
+ assertThat(prompts.prompts()).extracting(McpSchema.Prompt::name).containsExactly("runtime-prompt");
+
+ McpSchema.ReadResourceResult resourceResult = result(transport, "alice", McpSchema.METHOD_RESOURCES_READ,
+ McpSchema.ReadResourceRequest.builder("test://resources/runtime").build());
+ assertThat(((McpSchema.TextResourceContents) resourceResult.contents().get(0)).text())
+ .isEqualTo("resource-runtime");
+ McpSchema.ReadResourceResult templateResult = result(transport, "alice", McpSchema.METHOD_RESOURCES_READ,
+ McpSchema.ReadResourceRequest.builder("test://resource-templates/runtime/guide").build());
+ assertThat(((McpSchema.TextResourceContents) templateResult.contents().get(0)).text())
+ .isEqualTo("template-runtime");
+ McpSchema.GetPromptResult promptResult = result(transport, "alice", McpSchema.METHOD_PROMPT_GET,
+ McpSchema.GetPromptRequest.builder("runtime-prompt").build());
+ assertThat(text(promptResult.messages().get(0).content())).isEqualTo("prompt-runtime");
+
+ server.removeResource("test://resources/runtime");
+ server.removeResourceTemplate("test://resource-templates/runtime/{name}");
+ server.removePrompt("runtime-prompt");
+
+ McpSchema.ListResourcesResult emptyResources = result(transport, "alice", McpSchema.METHOD_RESOURCES_LIST,
+ new McpSchema.PaginatedRequest());
+ assertThat(emptyResources.resources()).isEmpty();
+ McpSchema.ListResourceTemplatesResult emptyTemplates = result(transport, "alice",
+ McpSchema.METHOD_RESOURCES_TEMPLATES_LIST, new McpSchema.PaginatedRequest());
+ assertThat(emptyTemplates.resourceTemplates()).isEmpty();
+ McpSchema.ListPromptsResult emptyPrompts = result(transport, "alice", McpSchema.METHOD_PROMPT_LIST,
+ new McpSchema.PaginatedRequest());
+ assertThat(emptyPrompts.prompts()).isEmpty();
+ }
+
+ @Test
+ void statelessRuntimeMutationRequiresRepositoryMutationSupport() {
+ var transport = new MockStatelessServerTransport();
+ var repository = new ContextAwareRepository();
+
+ McpStatelessSyncServer server = McpServer.sync(transport)
+ .jsonMapper(JSON_MAPPER)
+ .jsonSchemaValidator(VALID_SCHEMA_VALIDATOR)
+ .toolsRepository(repository)
+ .resourcesRepository(repository)
+ .promptsRepository(repository)
+ .build();
+
+ assertThatThrownBy(() -> server.addTool(toolSpec("runtime-tool", "runtime")))
+ .isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageContaining("does not support adding tools");
+ assertThatThrownBy(() -> server.removeTool("runtime-tool")).isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageContaining("does not support removing tools");
+
+ assertThatThrownBy(() -> server
+ .addResource(resourceSpec("test://resources/runtime", "runtime-resource", "resource-runtime")))
+ .isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageContaining("does not support adding resources");
+ assertThatThrownBy(() -> server.removeResource("test://resources/runtime"))
+ .isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageContaining("does not support removing resources");
+
+ assertThatThrownBy(
+ () -> server.addResourceTemplate(resourceTemplateSpec("test://resource-templates/runtime/{name}",
+ "runtime-resource-template", "template-runtime")))
+ .isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageContaining("does not support adding resource templates");
+ assertThatThrownBy(() -> server.removeResourceTemplate("test://resource-templates/runtime/{name}"))
+ .isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageContaining("does not support removing resource templates");
+
+ assertThatThrownBy(() -> server.addPrompt(promptSpec("runtime-prompt", "prompt-runtime")))
+ .isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageContaining("does not support adding prompts");
+ assertThatThrownBy(() -> server.removePrompt("runtime-prompt"))
+ .isInstanceOf(UnsupportedOperationException.class)
+ .hasMessageContaining("does not support removing prompts");
+ }
+
+ @Test
+ void statelessBuilderRejectsRepositoriesMixedWithStaticRegistrations() {
+ var tool = tool("static-tool");
+ var prompt = McpSchema.Prompt.builder("static-prompt").build();
+ var resource = McpSchema.Resource.builder("test://resources/static", "static-resource").build();
+ var reference = new McpSchema.PromptReference("static-prompt");
+
+ assertThatThrownBy(() -> McpServer.sync(new MockStatelessServerTransport())
+ .jsonSchemaValidator(VALID_SCHEMA_VALIDATOR)
+ .toolsRepository(new MutableRepository())
+ .toolCall(tool, (context, request) -> McpSchema.CallToolResult.builder().build()))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("toolsRepository");
+ assertThatThrownBy(() -> McpServer.sync(new MockStatelessServerTransport())
+ .jsonSchemaValidator(VALID_SCHEMA_VALIDATOR)
+ .toolCall(tool, (context, request) -> McpSchema.CallToolResult.builder().build())
+ .toolsRepository(new MutableRepository())).isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("toolsRepository");
+
+ assertThatThrownBy(() -> McpServer.sync(new MockStatelessServerTransport())
+ .resourcesRepository(new MutableRepository())
+ .resources(new McpStatelessServerFeatures.SyncResourceSpecification(resource,
+ (context, request) -> McpSchema.ReadResourceResult.builder(List.of()).build())))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("resourcesRepository");
+ assertThatThrownBy(() -> McpServer.sync(new MockStatelessServerTransport())
+ .resources(new McpStatelessServerFeatures.SyncResourceSpecification(resource,
+ (context, request) -> McpSchema.ReadResourceResult.builder(List.of()).build()))
+ .resourcesRepository(new MutableRepository())).isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("resourcesRepository");
+
+ assertThatThrownBy(() -> McpServer.sync(new MockStatelessServerTransport())
+ .promptsRepository(new MutableRepository())
+ .prompts(new McpStatelessServerFeatures.SyncPromptSpecification(prompt,
+ (context, request) -> McpSchema.GetPromptResult.builder(List.of()).build())))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("promptsRepository");
+ assertThatThrownBy(() -> McpServer.sync(new MockStatelessServerTransport())
+ .prompts(new McpStatelessServerFeatures.SyncPromptSpecification(prompt,
+ (context, request) -> McpSchema.GetPromptResult.builder(List.of()).build()))
+ .promptsRepository(new MutableRepository())).isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("promptsRepository");
+
+ assertThatThrownBy(() -> McpServer.sync(new MockStatelessServerTransport())
+ .completionsRepository(new MutableRepository())
+ .completions(new McpStatelessServerFeatures.SyncCompletionSpecification(reference,
+ (context, request) -> new McpSchema.CompleteResult(
+ new McpSchema.CompleteResult.CompleteCompletion(List.of())))))
+ .isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("completionsRepository");
+ assertThatThrownBy(() -> McpServer.sync(new MockStatelessServerTransport())
+ .completions(new McpStatelessServerFeatures.SyncCompletionSpecification(reference,
+ (context, request) -> new McpSchema.CompleteResult(
+ new McpSchema.CompleteResult.CompleteCompletion(List.of()))))
+ .completionsRepository(new MutableRepository())).isInstanceOf(IllegalStateException.class)
+ .hasMessageContaining("completionsRepository");
+ }
+
+ @SuppressWarnings("unchecked")
+ private static T result(MockStatelessServerTransport transport, String user, String method, Object params) {
+ McpSchema.JSONRPCResponse response = response(transport, user, method, params);
+ assertThat(response).isNotNull();
+ assertThat(response.error()).isNull();
+ return (T) response.result();
+ }
+
+ private static McpSchema.JSONRPCResponse response(MockStatelessServerTransport transport, String user,
+ String method, Object params) {
+ return transport.handler
+ .handleRequest(McpTransportContext.create(Map.of(USER_KEY, user)),
+ new McpSchema.JSONRPCRequest(method, method + "-id", params))
+ .block();
+ }
+
+ private static McpStatelessServerFeatures.SyncToolSpecification toolSpec(String name, String responseText) {
+ return new McpStatelessServerFeatures.SyncToolSpecification(tool(name),
+ (context, request) -> McpSchema.CallToolResult.builder().addTextContent(responseText).build());
+ }
+
+ private static McpStatelessServerFeatures.SyncResourceSpecification resourceSpec(String uri, String name,
+ String responseText) {
+ var resource = McpSchema.Resource.builder(uri, name).build();
+ return new McpStatelessServerFeatures.SyncResourceSpecification(resource,
+ (context, request) -> McpSchema.ReadResourceResult
+ .builder(List.of(McpSchema.TextResourceContents.builder(request.uri(), responseText).build()))
+ .build());
+ }
+
+ private static McpStatelessServerFeatures.SyncResourceTemplateSpecification resourceTemplateSpec(String uriTemplate,
+ String name, String responseText) {
+ var resourceTemplate = McpSchema.ResourceTemplate.builder(uriTemplate, name).build();
+ return new McpStatelessServerFeatures.SyncResourceTemplateSpecification(resourceTemplate,
+ (context, request) -> McpSchema.ReadResourceResult
+ .builder(List.of(McpSchema.TextResourceContents.builder(request.uri(), responseText).build()))
+ .build());
+ }
+
+ private static McpStatelessServerFeatures.SyncPromptSpecification promptSpec(String name, String responseText) {
+ var prompt = McpSchema.Prompt.builder(name).build();
+ return new McpStatelessServerFeatures.SyncPromptSpecification(prompt,
+ (context,
+ request) -> McpSchema.GetPromptResult.builder(List.of(McpSchema.PromptMessage
+ .builder(McpSchema.Role.USER, McpSchema.TextContent.builder(responseText).build())
+ .build())).build());
+ }
+
+ private static McpSchema.Tool tool(String name) {
+ return McpSchema.Tool.builder(name, Map.of("type", "object")).build();
+ }
+
+ private static String text(McpSchema.Content content) {
+ return ((McpSchema.TextContent) content).text();
+ }
+
+ private static String user(McpTransportContext transportContext) {
+ Object user = transportContext.get(USER_KEY);
+ return (user != null) ? user.toString() : "anonymous";
+ }
+
+ private static final class PassThroughMcpJsonMapper implements McpJsonMapper {
+
+ private final GsonMcpJsonMapper delegate = new GsonMcpJsonMapper();
+
+ @Override
+ public T readValue(String content, Class type) throws IOException {
+ return this.delegate.readValue(content, type);
+ }
+
+ @Override
+ public T readValue(byte[] content, Class type) throws IOException {
+ return this.delegate.readValue(content, type);
+ }
+
+ @Override
+ public T readValue(String content, TypeRef type) throws IOException {
+ return this.delegate.readValue(content, type);
+ }
+
+ @Override
+ public T readValue(byte[] content, TypeRef type) throws IOException {
+ return this.delegate.readValue(content, type);
+ }
+
+ @Override
+ public T convertValue(Object fromValue, Class type) {
+ if (type.isInstance(fromValue)) {
+ return type.cast(fromValue);
+ }
+ return this.delegate.convertValue(fromValue, type);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public T convertValue(Object fromValue, TypeRef type) {
+ Type targetType = type.getType();
+ if (targetType instanceof Class> targetClass && targetClass.isInstance(fromValue)) {
+ return (T) fromValue;
+ }
+ return this.delegate.convertValue(fromValue, type);
+ }
+
+ @Override
+ public String writeValueAsString(Object value) throws IOException {
+ return this.delegate.writeValueAsString(value);
+ }
+
+ @Override
+ public byte[] writeValueAsBytes(Object value) throws IOException {
+ return this.delegate.writeValueAsBytes(value);
+ }
+
+ }
+
+ private static final class MockStatelessServerTransport implements McpStatelessServerTransport {
+
+ private McpStatelessServerHandler handler;
+
+ @Override
+ public void setMcpHandler(McpStatelessServerHandler mcpHandler) {
+ this.handler = mcpHandler;
+ }
+
+ @Override
+ public Mono closeGracefully() {
+ return Mono.empty();
+ }
+
+ }
+
+ private static final class ContextAwareRepository
+ implements ToolsRepository, ResourcesRepository, PromptsRepository, CompletionsRepository {
+
+ @Override
+ public McpSchema.ListToolsResult listTools(McpSchema.PaginatedRequest request,
+ McpTransportContext transportContext) {
+ return McpSchema.ListToolsResult.builder(List.of(tool(user(transportContext) + "-tool")))
+ .nextCursor(request.cursor())
+ .build();
+ }
+
+ @Override
+ public Optional resolveTool(String name, McpTransportContext transportContext) {
+ String user = user(transportContext);
+ if (!name.equals(user + "-tool")) {
+ return Optional.empty();
+ }
+ return Optional.of(tool(name));
+ }
+
+ @Override
+ public McpSchema.CallToolResult callTool(McpSchema.CallToolRequest request,
+ McpTransportContext transportContext) {
+ return McpSchema.CallToolResult.builder().addTextContent("tool:" + user(transportContext)).build();
+ }
+
+ @Override
+ public McpSchema.ListResourcesResult listResources(McpSchema.PaginatedRequest request,
+ McpTransportContext transportContext) {
+ String user = user(transportContext);
+ return McpSchema.ListResourcesResult
+ .builder(List.of(McpSchema.Resource.builder("test://resources/" + user, user + "-resource").build()))
+ .nextCursor(request.cursor())
+ .build();
+ }
+
+ @Override
+ public McpSchema.ListResourceTemplatesResult listResourceTemplates(McpSchema.PaginatedRequest request,
+ McpTransportContext transportContext) {
+ String user = user(transportContext);
+ return McpSchema.ListResourceTemplatesResult.builder(List.of(McpSchema.ResourceTemplate
+ .builder("test://resource-templates/" + user + "/{name}", user + "-resource-template")
+ .build())).nextCursor(request.cursor()).build();
+ }
+
+ @Override
+ public Optional resolveResource(String uri, McpTransportContext transportContext) {
+ String user = user(transportContext);
+ if (!uri.equals("test://resources/" + user)) {
+ return Optional.empty();
+ }
+ return Optional.of(McpSchema.Resource.builder(uri, user + "-resource").build());
+ }
+
+ @Override
+ public Optional resolveResourceTemplate(String uri,
+ McpTransportContext transportContext) {
+ String user = user(transportContext);
+ if (!uri.equals("test://resource-templates/" + user + "/{name}")
+ && !uri.equals("test://resource-templates/" + user + "/guide")) {
+ return Optional.empty();
+ }
+ return Optional.of(McpSchema.ResourceTemplate
+ .builder("test://resource-templates/" + user + "/{name}", user + "-resource-template")
+ .build());
+ }
+
+ @Override
+ public McpSchema.ReadResourceResult readResource(McpSchema.ReadResourceRequest request,
+ McpTransportContext transportContext) {
+ String user = user(transportContext);
+ String content = request.uri().equals("test://resources/" + user) ? "resource:" + user
+ : "resource-template:" + user;
+ return McpSchema.ReadResourceResult
+ .builder(List.of(McpSchema.TextResourceContents.builder(request.uri(), content).build()))
+ .build();
+ }
+
+ @Override
+ public McpSchema.ListPromptsResult listPrompts(McpSchema.PaginatedRequest request,
+ McpTransportContext transportContext) {
+ String user = user(transportContext);
+ return McpSchema.ListPromptsResult.builder(List.of(McpSchema.Prompt.builder(user + "-prompt")
+ .arguments(List.of(McpSchema.PromptArgument.builder("name").build()))
+ .build())).nextCursor(request.cursor()).build();
+ }
+
+ @Override
+ public Optional resolvePrompt(String name, McpTransportContext transportContext) {
+ String user = user(transportContext);
+ if (!name.equals(user + "-prompt")) {
+ return Optional.empty();
+ }
+ return Optional.of(McpSchema.Prompt.builder(name)
+ .arguments(List.of(McpSchema.PromptArgument.builder("name").build()))
+ .build());
+ }
+
+ @Override
+ public McpSchema.GetPromptResult getPrompt(McpSchema.GetPromptRequest request,
+ McpTransportContext transportContext) {
+ return McpSchema.GetPromptResult
+ .builder(
+ List.of(McpSchema.PromptMessage
+ .builder(McpSchema.Role.USER,
+ McpSchema.TextContent.builder("prompt:" + user(transportContext)).build())
+ .build()))
+ .build();
+ }
+
+ @Override
+ public McpSchema.CompleteResult complete(McpSchema.CompleteRequest request,
+ McpTransportContext transportContext) {
+ return new McpSchema.CompleteResult(
+ new McpSchema.CompleteResult.CompleteCompletion(List.of("completion:" + user(transportContext))));
+ }
+
+ }
+
+ private static final class MutableRepository
+ implements ToolsRepository, ResourcesRepository, PromptsRepository, CompletionsRepository {
+
+ private final Map tools = new LinkedHashMap<>();
+
+ private final Map resources = new LinkedHashMap<>();
+
+ private final Map resourceTemplates = new LinkedHashMap<>();
+
+ private final Map prompts = new LinkedHashMap<>();
+
+ @Override
+ public McpSchema.ListToolsResult listTools(McpSchema.PaginatedRequest request,
+ McpTransportContext transportContext) {
+ return McpSchema.ListToolsResult.builder(
+ this.tools.values().stream().map(McpStatelessServerFeatures.SyncToolSpecification::tool).toList())
+ .build();
+ }
+
+ @Override
+ public Optional resolveTool(String name, McpTransportContext transportContext) {
+ return Optional.ofNullable(this.tools.get(name))
+ .map(McpStatelessServerFeatures.SyncToolSpecification::tool);
+ }
+
+ @Override
+ public McpSchema.CallToolResult callTool(McpSchema.CallToolRequest request,
+ McpTransportContext transportContext) {
+ return this.tools.get(request.name()).callHandler().apply(transportContext, request);
+ }
+
+ @Override
+ public void addTool(McpStatelessServerFeatures.SyncToolSpecification toolSpecification) {
+ this.tools.put(toolSpecification.tool().name(), toolSpecification);
+ }
+
+ @Override
+ public boolean removeTool(String toolName) {
+ return this.tools.remove(toolName) != null;
+ }
+
+ @Override
+ public McpSchema.ListResourcesResult listResources(McpSchema.PaginatedRequest request,
+ McpTransportContext transportContext) {
+ return McpSchema.ListResourcesResult
+ .builder(this.resources.values()
+ .stream()
+ .map(McpStatelessServerFeatures.SyncResourceSpecification::resource)
+ .toList())
+ .build();
+ }
+
+ @Override
+ public McpSchema.ListResourceTemplatesResult listResourceTemplates(McpSchema.PaginatedRequest request,
+ McpTransportContext transportContext) {
+ return McpSchema.ListResourceTemplatesResult
+ .builder(this.resourceTemplates.values()
+ .stream()
+ .map(McpStatelessServerFeatures.SyncResourceTemplateSpecification::resourceTemplate)
+ .toList())
+ .build();
+ }
+
+ @Override
+ public Optional resolveResource(String uri, McpTransportContext transportContext) {
+ return Optional.ofNullable(this.resources.get(uri))
+ .map(McpStatelessServerFeatures.SyncResourceSpecification::resource);
+ }
+
+ @Override
+ public Optional resolveResourceTemplate(String uri,
+ McpTransportContext transportContext) {
+ return this.resourceTemplates.values()
+ .stream()
+ .filter(resourceTemplate -> matches(resourceTemplate.resourceTemplate().uriTemplate(), uri))
+ .map(McpStatelessServerFeatures.SyncResourceTemplateSpecification::resourceTemplate)
+ .findFirst();
+ }
+
+ @Override
+ public McpSchema.ReadResourceResult readResource(McpSchema.ReadResourceRequest request,
+ McpTransportContext transportContext) {
+ McpStatelessServerFeatures.SyncResourceSpecification resourceSpecification = this.resources
+ .get(request.uri());
+ if (resourceSpecification != null) {
+ return resourceSpecification.readHandler().apply(transportContext, request);
+ }
+ return this.resourceTemplates.values()
+ .stream()
+ .filter(resourceTemplate -> matches(resourceTemplate.resourceTemplate().uriTemplate(), request.uri()))
+ .findFirst()
+ .orElseThrow()
+ .readHandler()
+ .apply(transportContext, request);
+ }
+
+ @Override
+ public void addResource(McpStatelessServerFeatures.SyncResourceSpecification resourceSpecification) {
+ this.resources.put(resourceSpecification.resource().uri(), resourceSpecification);
+ }
+
+ @Override
+ public boolean removeResource(String resourceUri) {
+ return this.resources.remove(resourceUri) != null;
+ }
+
+ @Override
+ public void addResourceTemplate(
+ McpStatelessServerFeatures.SyncResourceTemplateSpecification resourceTemplateSpecification) {
+ this.resourceTemplates.put(resourceTemplateSpecification.resourceTemplate().uriTemplate(),
+ resourceTemplateSpecification);
+ }
+
+ @Override
+ public boolean removeResourceTemplate(String uriTemplate) {
+ return this.resourceTemplates.remove(uriTemplate) != null;
+ }
+
+ @Override
+ public McpSchema.ListPromptsResult listPrompts(McpSchema.PaginatedRequest request,
+ McpTransportContext transportContext) {
+ return McpSchema.ListPromptsResult
+ .builder(this.prompts.values()
+ .stream()
+ .map(McpStatelessServerFeatures.SyncPromptSpecification::prompt)
+ .toList())
+ .build();
+ }
+
+ @Override
+ public Optional resolvePrompt(String name, McpTransportContext transportContext) {
+ return Optional.ofNullable(this.prompts.get(name))
+ .map(McpStatelessServerFeatures.SyncPromptSpecification::prompt);
+ }
+
+ @Override
+ public McpSchema.GetPromptResult getPrompt(McpSchema.GetPromptRequest request,
+ McpTransportContext transportContext) {
+ return this.prompts.get(request.name()).promptHandler().apply(transportContext, request);
+ }
+
+ @Override
+ public void addPrompt(McpStatelessServerFeatures.SyncPromptSpecification promptSpecification) {
+ this.prompts.put(promptSpecification.prompt().name(), promptSpecification);
+ }
+
+ @Override
+ public boolean removePrompt(String promptName) {
+ return this.prompts.remove(promptName) != null;
+ }
+
+ @Override
+ public McpSchema.CompleteResult complete(McpSchema.CompleteRequest request,
+ McpTransportContext transportContext) {
+ return new McpSchema.CompleteResult(new McpSchema.CompleteResult.CompleteCompletion(List.of()));
+ }
+
+ private static boolean matches(String uriTemplate, String uri) {
+ String prefix = uriTemplate.replace("{name}", "");
+ return uriTemplate.equals(uri) || uri.startsWith(prefix);
+ }
+
+ }
+
+}