From 75b5306dfb3877876caf2df49c7813e051a04bc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Je=CC=A8drzejczyk?= Date: Mon, 26 Oct 2026 17:11:22 +0100 Subject: [PATCH 1/2] Return a handler from find in modern tool and prompt repositories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tool and prompt repositories had separate find and call/get methods, so every tools/call and prompts/get looked the primitive up twice. That means two round trips for repositories backed by a database or remote definitions. find now returns McpAsyncTool/McpSyncTool or McpAsyncPrompt/McpSyncPrompt, which carry the definition and its handler. The call/get methods are removed from the repositories. ToolsFeature and PromptsFeature validate against the definition and invoke the handler they found. Closes #1268 Signed-off-by: Dariusz Jędrzejczyk --- .../modern/ModernConformanceServlet.java | 48 ++++------------ .../modern/server/feature/McpAsyncPrompt.java | 54 ++++++++++++++++++ .../feature/McpAsyncPromptRepository.java | 13 +---- .../modern/server/feature/McpAsyncTool.java | 57 +++++++++++++++++++ .../feature/McpAsyncToolRepository.java | 14 +---- .../modern/server/feature/McpSyncPrompt.java | 52 +++++++++++++++++ .../feature/McpSyncPromptRepository.java | 8 +-- .../modern/server/feature/McpSyncTool.java | 52 +++++++++++++++++ .../server/feature/McpSyncToolRepository.java | 8 +-- .../modern/server/feature/PromptsFeature.java | 25 ++++---- .../modern/server/feature/ToolsFeature.java | 21 +++---- .../modern/server/ModernTestFixtures.java | 19 ++----- .../server/feature/PromptsFeatureTests.java | 15 ++--- .../server/feature/ToolsFeatureTests.java | 45 ++++----------- ...tpServletMcpTransportIntegrationTests.java | 10 ++-- ...cpTransportSlowClientIntegrationTests.java | 13 +---- ...pTransportThreadLocalIntegrationTests.java | 7 ++- 17 files changed, 289 insertions(+), 172 deletions(-) create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncPrompt.java create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncTool.java create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncPrompt.java create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncTool.java diff --git a/conformance-tests/server-servlet-modern/src/main/java/io/modelcontextprotocol/conformance/server/modern/ModernConformanceServlet.java b/conformance-tests/server-servlet-modern/src/main/java/io/modelcontextprotocol/conformance/server/modern/ModernConformanceServlet.java index b5997834e..1b4390c87 100644 --- a/conformance-tests/server-servlet-modern/src/main/java/io/modelcontextprotocol/conformance/server/modern/ModernConformanceServlet.java +++ b/conformance-tests/server-servlet-modern/src/main/java/io/modelcontextprotocol/conformance/server/modern/ModernConformanceServlet.java @@ -50,8 +50,10 @@ import io.modelcontextprotocol.modern.server.feature.CompletionsFeature; import io.modelcontextprotocol.modern.server.feature.McpChangeBroadcaster; import io.modelcontextprotocol.modern.server.feature.McpChangeFeed; +import io.modelcontextprotocol.modern.server.feature.McpSyncPrompt; import io.modelcontextprotocol.modern.server.feature.McpSyncPromptRepository; import io.modelcontextprotocol.modern.server.feature.McpSyncResourceRepository; +import io.modelcontextprotocol.modern.server.feature.McpSyncTool; import io.modelcontextprotocol.modern.server.feature.McpSyncToolRepository; import io.modelcontextprotocol.modern.server.feature.PromptsFeature; import io.modelcontextprotocol.modern.server.feature.PromptsPage; @@ -254,12 +256,9 @@ private static Handler stream(StreamingTool to return (ctx, request) -> McpSyncResponse.streaming(notifier -> tool.run(ctx, request, notifier)); } - private record ToolEntry(Tool tool, Handler handler) { - } - private static final class ConformanceTools implements McpSyncToolRepository { - private final Map tools = new LinkedHashMap<>(); + private final Map tools = new LinkedHashMap<>(); ConformanceTools(McpChangeBroadcaster changes) { add("test_simple_text", "Returns simple text content for testing", @@ -465,27 +464,17 @@ private void add(String name, String description, Handler handler) { - this.tools.put(tool.name(), new ToolEntry(tool, handler)); + this.tools.put(tool.name(), McpSyncTool.of(tool, handler::handle)); } @Override public ToolsPage list(McpRequestContext ctx, String cursor) { - return ToolsPage.of(this.tools.values().stream().map(ToolEntry::tool).toList()); - } - - @Override - public Tool find(McpRequestContext ctx, String name) { - ToolEntry entry = this.tools.get(name); - return entry != null ? entry.tool() : null; + return ToolsPage.of(this.tools.values().stream().map(McpSyncTool::definition).toList()); } @Override - public McpSyncResponse call(McpRequestContext ctx, CallToolRequest request) { - ToolEntry entry = this.tools.get(request.name()); - if (entry == null) { - throw McpException.invalidParams("Unknown tool: " + request.name()); - } - return entry.handler().handle(ctx, request); + public McpSyncTool find(McpRequestContext ctx, String name) { + return this.tools.get(name); } private static void sleep(long millis) { @@ -571,12 +560,9 @@ private static ReadResourceResult templateResource(String uri) { } - private record PromptEntry(Prompt prompt, Handler handler) { - } - private static final class ConformancePrompts implements McpSyncPromptRepository { - private final Map prompts = new LinkedHashMap<>(); + private final Map prompts = new LinkedHashMap<>(); ConformancePrompts() { add(Prompt.builder("test_simple_prompt").description("A simple prompt for testing").build(), @@ -637,7 +623,7 @@ private static final class ConformancePrompts implements McpSyncPromptRepository } private void add(Prompt prompt, Handler handler) { - this.prompts.put(prompt.name(), new PromptEntry(prompt, handler)); + this.prompts.put(prompt.name(), McpSyncPrompt.of(prompt, handler::handle)); } private static GetPromptResult messages(PromptMessage... messages) { @@ -646,22 +632,12 @@ private static GetPromptResult messages(PromptMessage... messages) { @Override public PromptsPage list(McpRequestContext ctx, String cursor) { - return PromptsPage.of(this.prompts.values().stream().map(PromptEntry::prompt).toList()); - } - - @Override - public Prompt find(McpRequestContext ctx, String name) { - PromptEntry entry = this.prompts.get(name); - return entry != null ? entry.prompt() : null; + return PromptsPage.of(this.prompts.values().stream().map(McpSyncPrompt::definition).toList()); } @Override - public McpSyncResponse get(McpRequestContext ctx, GetPromptRequest request) { - PromptEntry entry = this.prompts.get(request.name()); - if (entry == null) { - throw McpException.invalidParams("Unknown prompt: " + request.name()); - } - return entry.handler().handle(ctx, request); + public McpSyncPrompt find(McpRequestContext ctx, String name) { + return this.prompts.get(name); } } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncPrompt.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncPrompt.java new file mode 100644 index 000000000..d83815eb3 --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncPrompt.java @@ -0,0 +1,54 @@ +/* + * Copyright 2026-2026 the original author or authors. + */ + +package io.modelcontextprotocol.modern.server.feature; + +import io.modelcontextprotocol.modern.McpSchema.GetPromptOutcome; +import io.modelcontextprotocol.modern.McpSchema.GetPromptRequest; +import io.modelcontextprotocol.modern.McpSchema.Prompt; +import io.modelcontextprotocol.modern.server.McpAsyncResponse; +import io.modelcontextprotocol.modern.server.McpRequestContext; +import io.modelcontextprotocol.util.Assert; +import reactor.core.publisher.Mono; + +/** + * A prompt's definition together with the handler that renders it. + * + * @author Dariusz Jędrzejczyk + */ +public final class McpAsyncPrompt { + + private final Prompt definition; + + private final Handler handler; + + private McpAsyncPrompt(Prompt definition, Handler handler) { + Assert.notNull(definition, "definition must not be null"); + Assert.notNull(handler, "handler must not be null"); + this.definition = definition; + this.handler = handler; + } + + public static McpAsyncPrompt of(Prompt definition, Handler handler) { + return new McpAsyncPrompt(definition, handler); + } + + public Prompt definition() { + return this.definition; + } + + public Handler handler() { + return this.handler; + } + + /** Answers a {@code prompts/get} for this prompt. */ + @FunctionalInterface + public interface Handler { + + /** The arguments include every required argument of the definition. */ + Mono> get(McpRequestContext ctx, GetPromptRequest request); + + } + +} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncPromptRepository.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncPromptRepository.java index 7522fde18..bd371befe 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncPromptRepository.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncPromptRepository.java @@ -5,11 +5,7 @@ package io.modelcontextprotocol.modern.server.feature; import io.modelcontextprotocol.modern.McpException; -import io.modelcontextprotocol.modern.McpSchema.GetPromptOutcome; -import io.modelcontextprotocol.modern.McpSchema.GetPromptRequest; -import io.modelcontextprotocol.modern.McpSchema.Prompt; import io.modelcontextprotocol.modern.server.McpRequestContext; -import io.modelcontextprotocol.modern.server.McpAsyncResponse; import reactor.core.publisher.Mono; /** @@ -26,13 +22,6 @@ public interface McpAsyncPromptRepository { Mono list(McpRequestContext ctx, String cursor); /** The prompt with this name, or empty if there is none. */ - Mono find(McpRequestContext ctx, String name); - - /** - * Answer a {@code prompts/get}. An unknown prompt is an - * {@link McpException#invalidParams(String) invalid-params error}. The arguments - * include every required argument of the prompt {@link #find} returned. - */ - Mono> get(McpRequestContext ctx, GetPromptRequest request); + Mono find(McpRequestContext ctx, String name); } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncTool.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncTool.java new file mode 100644 index 000000000..76616da5f --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncTool.java @@ -0,0 +1,57 @@ +/* + * Copyright 2026-2026 the original author or authors. + */ + +package io.modelcontextprotocol.modern.server.feature; + +import io.modelcontextprotocol.modern.McpSchema.CallToolOutcome; +import io.modelcontextprotocol.modern.McpSchema.CallToolRequest; +import io.modelcontextprotocol.modern.McpSchema.Tool; +import io.modelcontextprotocol.modern.server.McpAsyncResponse; +import io.modelcontextprotocol.modern.server.McpRequestContext; +import io.modelcontextprotocol.util.Assert; +import reactor.core.publisher.Mono; + +/** + * A tool's definition together with the handler that answers its calls. + * + * @author Dariusz Jędrzejczyk + */ +public final class McpAsyncTool { + + private final Tool definition; + + private final Handler handler; + + private McpAsyncTool(Tool definition, Handler handler) { + Assert.notNull(definition, "definition must not be null"); + Assert.notNull(handler, "handler must not be null"); + this.definition = definition; + this.handler = handler; + } + + public static McpAsyncTool of(Tool definition, Handler handler) { + return new McpAsyncTool(definition, handler); + } + + public Tool definition() { + return this.definition; + } + + public Handler handler() { + return this.handler; + } + + /** Answers a {@code tools/call} for this tool. */ + @FunctionalInterface + public interface Handler { + + /** + * The arguments conform to the definition's {@code inputSchema}, and a result's + * {@code structuredContent} must conform to its {@code outputSchema}. + */ + Mono> call(McpRequestContext ctx, CallToolRequest request); + + } + +} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncToolRepository.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncToolRepository.java index ea7f32b86..0412186f0 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncToolRepository.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncToolRepository.java @@ -5,11 +5,7 @@ package io.modelcontextprotocol.modern.server.feature; import io.modelcontextprotocol.modern.McpException; -import io.modelcontextprotocol.modern.McpSchema.CallToolOutcome; -import io.modelcontextprotocol.modern.McpSchema.CallToolRequest; -import io.modelcontextprotocol.modern.McpSchema.Tool; import io.modelcontextprotocol.modern.server.McpRequestContext; -import io.modelcontextprotocol.modern.server.McpAsyncResponse; import reactor.core.publisher.Mono; /** @@ -27,14 +23,6 @@ public interface McpAsyncToolRepository { Mono list(McpRequestContext ctx, String cursor); /** The tool with this name, or empty if there is none. */ - Mono find(McpRequestContext ctx, String name); - - /** - * Answer a {@code tools/call}. An unknown tool is an - * {@link McpException#invalidParams(String) invalid-params error}. The arguments - * conform to the {@code inputSchema} of the tool {@link #find} returned, and a - * result's {@code structuredContent} must conform to its {@code outputSchema}. - */ - Mono> call(McpRequestContext ctx, CallToolRequest request); + Mono find(McpRequestContext ctx, String name); } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncPrompt.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncPrompt.java new file mode 100644 index 000000000..3f69f8797 --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncPrompt.java @@ -0,0 +1,52 @@ +/* + * Copyright 2026-2026 the original author or authors. + */ + +package io.modelcontextprotocol.modern.server.feature; + +import io.modelcontextprotocol.modern.McpSchema.GetPromptOutcome; +import io.modelcontextprotocol.modern.McpSchema.GetPromptRequest; +import io.modelcontextprotocol.modern.McpSchema.Prompt; +import io.modelcontextprotocol.modern.server.McpRequestContext; +import io.modelcontextprotocol.modern.server.McpSyncResponse; +import io.modelcontextprotocol.util.Assert; + +/** + * The blocking counterpart of {@link McpAsyncPrompt}. + * + * @author Dariusz Jędrzejczyk + */ +public final class McpSyncPrompt { + + private final Prompt definition; + + private final Handler handler; + + private McpSyncPrompt(Prompt definition, Handler handler) { + Assert.notNull(definition, "definition must not be null"); + Assert.notNull(handler, "handler must not be null"); + this.definition = definition; + this.handler = handler; + } + + public static McpSyncPrompt of(Prompt definition, Handler handler) { + return new McpSyncPrompt(definition, handler); + } + + public Prompt definition() { + return this.definition; + } + + public Handler handler() { + return this.handler; + } + + /** See {@link McpAsyncPrompt.Handler}. */ + @FunctionalInterface + public interface Handler { + + McpSyncResponse get(McpRequestContext ctx, GetPromptRequest request); + + } + +} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncPromptRepository.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncPromptRepository.java index 794e0a28d..38dbdd405 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncPromptRepository.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncPromptRepository.java @@ -4,11 +4,7 @@ package io.modelcontextprotocol.modern.server.feature; -import io.modelcontextprotocol.modern.McpSchema.GetPromptOutcome; -import io.modelcontextprotocol.modern.McpSchema.GetPromptRequest; -import io.modelcontextprotocol.modern.McpSchema.Prompt; import io.modelcontextprotocol.modern.server.McpRequestContext; -import io.modelcontextprotocol.modern.server.McpSyncResponse; /** * The blocking counterpart of {@link McpAsyncPromptRepository}. @@ -20,8 +16,6 @@ public interface McpSyncPromptRepository { PromptsPage list(McpRequestContext ctx, String cursor); /** The prompt with this name, or {@code null} if there is none. */ - Prompt find(McpRequestContext ctx, String name); - - McpSyncResponse get(McpRequestContext ctx, GetPromptRequest request); + McpSyncPrompt find(McpRequestContext ctx, String name); } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncTool.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncTool.java new file mode 100644 index 000000000..90016dcdc --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncTool.java @@ -0,0 +1,52 @@ +/* + * Copyright 2026-2026 the original author or authors. + */ + +package io.modelcontextprotocol.modern.server.feature; + +import io.modelcontextprotocol.modern.McpSchema.CallToolOutcome; +import io.modelcontextprotocol.modern.McpSchema.CallToolRequest; +import io.modelcontextprotocol.modern.McpSchema.Tool; +import io.modelcontextprotocol.modern.server.McpRequestContext; +import io.modelcontextprotocol.modern.server.McpSyncResponse; +import io.modelcontextprotocol.util.Assert; + +/** + * The blocking counterpart of {@link McpAsyncTool}. + * + * @author Dariusz Jędrzejczyk + */ +public final class McpSyncTool { + + private final Tool definition; + + private final Handler handler; + + private McpSyncTool(Tool definition, Handler handler) { + Assert.notNull(definition, "definition must not be null"); + Assert.notNull(handler, "handler must not be null"); + this.definition = definition; + this.handler = handler; + } + + public static McpSyncTool of(Tool definition, Handler handler) { + return new McpSyncTool(definition, handler); + } + + public Tool definition() { + return this.definition; + } + + public Handler handler() { + return this.handler; + } + + /** See {@link McpAsyncTool.Handler}. */ + @FunctionalInterface + public interface Handler { + + McpSyncResponse call(McpRequestContext ctx, CallToolRequest request); + + } + +} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncToolRepository.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncToolRepository.java index 66c74e6bf..64dafa5f3 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncToolRepository.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncToolRepository.java @@ -4,11 +4,7 @@ package io.modelcontextprotocol.modern.server.feature; -import io.modelcontextprotocol.modern.McpSchema.CallToolOutcome; -import io.modelcontextprotocol.modern.McpSchema.CallToolRequest; -import io.modelcontextprotocol.modern.McpSchema.Tool; import io.modelcontextprotocol.modern.server.McpRequestContext; -import io.modelcontextprotocol.modern.server.McpSyncResponse; /** * The blocking counterpart of {@link McpAsyncToolRepository}. @@ -20,8 +16,6 @@ public interface McpSyncToolRepository { ToolsPage list(McpRequestContext ctx, String cursor); /** The tool with this name, or {@code null} if there is none. */ - Tool find(McpRequestContext ctx, String name); - - McpSyncResponse call(McpRequestContext ctx, CallToolRequest request); + McpSyncTool find(McpRequestContext ctx, String name); } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/PromptsFeature.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/PromptsFeature.java index bbb198a53..557defa11 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/PromptsFeature.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/PromptsFeature.java @@ -4,6 +4,7 @@ package io.modelcontextprotocol.modern.server.feature; +import java.util.List; import java.util.Map; import java.util.Set; @@ -16,7 +17,6 @@ import io.modelcontextprotocol.modern.McpSchema.GetPromptRequest; import io.modelcontextprotocol.modern.McpSchema.ListPromptsResult; import io.modelcontextprotocol.modern.McpSchema.PaginatedRequest; -import io.modelcontextprotocol.modern.McpSchema.Prompt; import io.modelcontextprotocol.modern.McpSchema.PromptArgument; import io.modelcontextprotocol.modern.McpSchema.Result; import io.modelcontextprotocol.modern.McpSchema.ServerCapabilities; @@ -91,17 +91,18 @@ public Mono> handle(McpRequestConte .flatMap(prompt -> get(ctx, prompt, request))); } - private Mono> get(McpRequestContext ctx, Prompt prompt, + private Mono> get(McpRequestContext ctx, McpAsyncPrompt prompt, GetPromptRequest request) { - if (prompt.arguments() != null) { + List declared = prompt.definition().arguments(); + if (declared != null) { Map arguments = request.arguments() != null ? request.arguments() : Map.of(); - for (PromptArgument argument : prompt.arguments()) { + for (PromptArgument argument : declared) { if (Boolean.TRUE.equals(argument.required()) && arguments.get(argument.name()) == null) { return Mono.error(McpException.invalidParams("Missing required argument: " + argument.name())); } } } - return this.repository.get(ctx, request); + return prompt.handler().get(ctx, request); } @Override @@ -130,15 +131,15 @@ public Mono list(McpRequestContext ctx, String cursor) { } @Override - public Mono find(McpRequestContext ctx, String name) { - return SyncAdapters.call(ctx, () -> repository.find(ctx, name)); - } - - @Override - public Mono> get(McpRequestContext ctx, GetPromptRequest request) { - return SyncAdapters.respond(ctx, () -> repository.get(ctx, request)); + public Mono find(McpRequestContext ctx, String name) { + return SyncAdapters.call(ctx, () -> repository.find(ctx, name)).map(PromptsFeature::adapt); } }; } + private static McpAsyncPrompt adapt(McpSyncPrompt prompt) { + return McpAsyncPrompt.of(prompt.definition(), + (ctx, request) -> SyncAdapters.respond(ctx, () -> prompt.handler().get(ctx, request))); + } + } diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/ToolsFeature.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/ToolsFeature.java index 6af2d33a9..ea71eb5bf 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/ToolsFeature.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/ToolsFeature.java @@ -103,9 +103,10 @@ public Mono> handle(McpRequestConte .flatMap(tool -> call(ctx, tool, request))); } - private Mono> call(McpRequestContext ctx, Tool tool, CallToolRequest request) { + private Mono> call(McpRequestContext ctx, McpAsyncTool tool, + CallToolRequest request) { Map arguments = request.arguments() != null ? request.arguments() : Map.of(); - ValidationResponse input = this.jsonSchemaValidator.validate(tool.inputSchema(), arguments); + ValidationResponse input = this.jsonSchemaValidator.validate(tool.definition().inputSchema(), arguments); if (!input.valid()) { // A tool execution error, so the model can fix the call. return Mono.just(McpAsyncResponse.result(CallToolResult.builder() @@ -113,7 +114,7 @@ private Mono> call(McpRequestContext ctx, Tool .isError(true) .build())); } - return this.repository.call(ctx, request).map(response -> checkOutput(tool, response)); + return tool.handler().call(ctx, request).map(response -> checkOutput(tool.definition(), response)); } private McpAsyncResponse checkOutput(Tool tool, McpAsyncResponse response) { @@ -182,15 +183,15 @@ public Mono list(McpRequestContext ctx, String cursor) { } @Override - public Mono find(McpRequestContext ctx, String name) { - return SyncAdapters.call(ctx, () -> repository.find(ctx, name)); - } - - @Override - public Mono> call(McpRequestContext ctx, CallToolRequest request) { - return SyncAdapters.respond(ctx, () -> repository.call(ctx, request)); + public Mono find(McpRequestContext ctx, String name) { + return SyncAdapters.call(ctx, () -> repository.find(ctx, name)).map(ToolsFeature::adapt); } }; } + private static McpAsyncTool adapt(McpSyncTool tool) { + return McpAsyncTool.of(tool.definition(), + (ctx, request) -> SyncAdapters.respond(ctx, () -> tool.handler().call(ctx, request))); + } + } diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/ModernTestFixtures.java b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/ModernTestFixtures.java index d4d8547ec..7b15a2ccf 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/ModernTestFixtures.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/ModernTestFixtures.java @@ -14,7 +14,6 @@ import io.modelcontextprotocol.json.schema.JsonSchemaValidator.ValidationResponse; import io.modelcontextprotocol.modern.JsonRpc.JSONRPCRequest; import io.modelcontextprotocol.modern.JsonRpc.JSONRPCResponse; -import io.modelcontextprotocol.modern.McpException; import io.modelcontextprotocol.modern.McpSchema; import io.modelcontextprotocol.modern.McpSchema.CallToolOutcome; import io.modelcontextprotocol.modern.McpSchema.CallToolRequest; @@ -23,6 +22,7 @@ import io.modelcontextprotocol.modern.McpSchema.Tool; import io.modelcontextprotocol.modern.server.feature.McpAsyncToolRepository; import io.modelcontextprotocol.modern.server.feature.ToolsPage; +import io.modelcontextprotocol.modern.server.feature.McpAsyncTool; import io.modelcontextprotocol.util.ToolsUtils; import reactor.core.publisher.Mono; @@ -82,13 +82,9 @@ public Mono list(McpRequestContext ctx, String cursor) { } @Override - public Mono find(McpRequestContext ctx, String name) { - return Mono.just(Tool.builder(name, ToolsUtils.EMPTY_JSON_SCHEMA).build()); - } - - @Override - public Mono> call(McpRequestContext ctx, CallToolRequest request) { - return call.apply(ctx, request); + public Mono find(McpRequestContext ctx, String name) { + return Mono + .just(McpAsyncTool.of(Tool.builder(name, ToolsUtils.EMPTY_JSON_SCHEMA).build(), call::apply)); } }; } @@ -102,14 +98,9 @@ public Mono list(McpRequestContext ctx, String cursor) { } @Override - public Mono find(McpRequestContext ctx, String name) { + public Mono find(McpRequestContext ctx, String name) { return Mono.empty(); } - - @Override - public Mono> call(McpRequestContext ctx, CallToolRequest request) { - return Mono.error(McpException.invalidParams("Unknown tool: " + request.name())); - } }; } diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/feature/PromptsFeatureTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/feature/PromptsFeatureTests.java index 0e3f2a80e..e8ed91844 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/feature/PromptsFeatureTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/feature/PromptsFeatureTests.java @@ -12,8 +12,6 @@ import io.modelcontextprotocol.modern.JsonRpc.JSONRPCResponse; import io.modelcontextprotocol.modern.McpSchema.CacheScope; import io.modelcontextprotocol.modern.McpSchema.ErrorCodes; -import io.modelcontextprotocol.modern.McpSchema.GetPromptOutcome; -import io.modelcontextprotocol.modern.McpSchema.GetPromptRequest; import io.modelcontextprotocol.modern.McpSchema.GetPromptResult; import io.modelcontextprotocol.modern.McpSchema.Prompt; import io.modelcontextprotocol.modern.McpSchema.PromptArgument; @@ -44,13 +42,12 @@ public Mono list(McpRequestContext ctx, String cursor) { } @Override - public Mono find(McpRequestContext ctx, String name) { - return GREETING.name().equals(name) ? Mono.just(GREETING) : Mono.empty(); - } - - @Override - public Mono> get(McpRequestContext ctx, GetPromptRequest request) { - return Mono.just(McpAsyncResponse.result(GetPromptResult.builder(List.of()).build())); + public Mono find(McpRequestContext ctx, String name) { + return GREETING.name().equals(name) + ? Mono.just(McpAsyncPrompt.of(GREETING, + (c, request) -> Mono + .just(McpAsyncResponse.result(GetPromptResult.builder(List.of()).build())))) + : Mono.empty(); } }, McpJsonDefaults.getMapper(), 0L, CacheScope.PRIVATE)) .build(); diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/feature/ToolsFeatureTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/feature/ToolsFeatureTests.java index d4a06ba66..cba1299e9 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/feature/ToolsFeatureTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/feature/ToolsFeatureTests.java @@ -21,11 +21,8 @@ import io.modelcontextprotocol.modern.JsonRpc.JSONRPCNotification; import io.modelcontextprotocol.modern.JsonRpc.JSONRPCRequest; import io.modelcontextprotocol.modern.JsonRpc.JSONRPCResponse; -import io.modelcontextprotocol.modern.McpException; -import io.modelcontextprotocol.modern.McpSchema; import io.modelcontextprotocol.modern.McpSchema.CacheScope; import io.modelcontextprotocol.modern.McpSchema.CallToolOutcome; -import io.modelcontextprotocol.modern.McpSchema.CallToolRequest; import io.modelcontextprotocol.modern.McpSchema.CallToolResult; import io.modelcontextprotocol.modern.McpSchema.ErrorCodes; import io.modelcontextprotocol.modern.McpSchema.MetaKeys; @@ -116,13 +113,8 @@ public Mono list(McpRequestContext ctx, String cursor) { } @Override - public Mono find(McpRequestContext ctx, String name) { - return Mono.just(STRUCTURED_TOOL); - } - - @Override - public Mono> call(McpRequestContext ctx, CallToolRequest request) { - return Mono.just(response); + public Mono find(McpRequestContext ctx, String name) { + return Mono.just(McpAsyncTool.of(STRUCTURED_TOOL, (c, request) -> Mono.just(response))); } }; return baseBuilder() @@ -230,14 +222,11 @@ public ToolsPage list(McpRequestContext ctx, String cursor) { } @Override - public Tool find(McpRequestContext ctx, String name) { - return ECHO_TOOL; - } - - @Override - public McpSyncResponse call(McpRequestContext ctx, CallToolRequest request) { - handlerThread.set(Thread.currentThread().getName()); - return McpSyncResponse.result(greeting(principal.apply(ctx))); + public McpSyncTool find(McpRequestContext ctx, String name) { + return McpSyncTool.of(ECHO_TOOL, (c, request) -> { + handlerThread.set(Thread.currentThread().getName()); + return McpSyncResponse.result(greeting(principal.apply(c))); + }); } }; } @@ -254,18 +243,13 @@ public ToolsPage list(McpRequestContext ctx, String cursor) { } @Override - public Tool find(McpRequestContext ctx, String name) { - return ECHO_TOOL; - } - - @Override - public McpSyncResponse call(McpRequestContext ctx, CallToolRequest request) { - return McpSyncResponse.streaming(notifier -> { + public McpSyncTool find(McpRequestContext ctx, String name) { + return McpSyncTool.of(ECHO_TOOL, (c, request) -> McpSyncResponse.streaming(notifier -> { handlerThread.set(Thread.currentThread().getName()); notifier.progress(1.0, 1.0, "greeting"); deliveredWhenProgressReturned.set(delivered.size()); - return greeting(principal.apply(ctx)); - }); + return greeting(principal.apply(c)); + })); } }; } @@ -335,14 +319,9 @@ public ToolsPage list(McpRequestContext ctx, String cursor) { } @Override - public Tool find(McpRequestContext ctx, String name) { + public McpSyncTool find(McpRequestContext ctx, String name) { return null; } - - @Override - public McpSyncResponse call(McpRequestContext ctx, CallToolRequest request) { - throw McpException.invalidParams("Unknown tool: " + request.name()); - } }; McpServer server = baseBuilder().feature(tools(repo)).build(); JSONRPCRequest listTools = new JSONRPCRequest("tools/list", 1, Map.of("_meta", meta())); diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java index 5d92df2a8..e63354160 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java @@ -42,6 +42,7 @@ import io.modelcontextprotocol.modern.server.feature.ServerChange; import io.modelcontextprotocol.modern.server.feature.ToolsFeature; import io.modelcontextprotocol.modern.server.feature.ToolsPage; +import io.modelcontextprotocol.modern.server.feature.McpAsyncTool; import io.modelcontextprotocol.server.transport.DefaultServerTransportSecurityValidator; import io.modelcontextprotocol.server.transport.TomcatTestUtil; import io.modelcontextprotocol.util.ToolsUtils; @@ -87,13 +88,12 @@ public Mono list(McpRequestContext ctx, String cursor) { } @Override - public Mono find(McpRequestContext ctx, String name) { - return "does-not-exist".equals(name) ? Mono.empty() - : Mono.just(Tool.builder(name, ToolsUtils.EMPTY_JSON_SCHEMA).build()); + public Mono find(McpRequestContext ctx, String name) { + return "does-not-exist".equals(name) ? Mono.empty() : Mono + .just(McpAsyncTool.of(Tool.builder(name, ToolsUtils.EMPTY_JSON_SCHEMA).build(), this::call)); } - @Override - public Mono> call(McpRequestContext ctx, CallToolRequest request) { + private Mono> call(McpRequestContext ctx, CallToolRequest request) { McpAsyncResponse response = switch (request.name()) { case "echo" -> McpAsyncResponse.result(text("echo:" + request.name())); case "streamer" -> McpAsyncResponse diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportSlowClientIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportSlowClientIntegrationTests.java index 1e894cfe9..2887cb271 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportSlowClientIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportSlowClientIntegrationTests.java @@ -19,14 +19,9 @@ import io.modelcontextprotocol.json.McpJsonDefaults; import io.modelcontextprotocol.json.McpJsonMapper; -import io.modelcontextprotocol.modern.McpException; import io.modelcontextprotocol.modern.McpSchema; -import io.modelcontextprotocol.modern.McpSchema.CallToolOutcome; -import io.modelcontextprotocol.modern.McpSchema.CallToolRequest; import io.modelcontextprotocol.modern.McpSchema.Implementation; import io.modelcontextprotocol.modern.McpSchema.MetaKeys; -import io.modelcontextprotocol.modern.McpSchema.Tool; -import io.modelcontextprotocol.modern.server.McpAsyncResponse; import io.modelcontextprotocol.modern.server.McpRequestContext; import io.modelcontextprotocol.modern.server.McpServer; import io.modelcontextprotocol.modern.server.feature.McpAsyncToolRepository; @@ -35,6 +30,7 @@ import io.modelcontextprotocol.modern.server.feature.ServerChange; import io.modelcontextprotocol.modern.server.feature.ToolsFeature; import io.modelcontextprotocol.modern.server.feature.ToolsPage; +import io.modelcontextprotocol.modern.server.feature.McpAsyncTool; import io.modelcontextprotocol.server.transport.TomcatTestUtil; import org.apache.catalina.LifecycleException; import org.apache.catalina.startup.Tomcat; @@ -76,14 +72,9 @@ public Mono list(McpRequestContext ctx, String cursor) { } @Override - public Mono find(McpRequestContext ctx, String name) { + public Mono find(McpRequestContext ctx, String name) { return Mono.empty(); } - - @Override - public Mono> call(McpRequestContext ctx, CallToolRequest request) { - return Mono.error(McpException.invalidParams("Unknown tool: " + request.name())); - } })) .subscriptions(publisher) .build(); diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportThreadLocalIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportThreadLocalIntegrationTests.java index ab835d315..c40163083 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportThreadLocalIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportThreadLocalIntegrationTests.java @@ -35,6 +35,7 @@ import io.modelcontextprotocol.modern.server.feature.McpSyncToolRepository; import io.modelcontextprotocol.modern.server.feature.ToolsFeature; import io.modelcontextprotocol.modern.server.feature.ToolsPage; +import io.modelcontextprotocol.modern.server.feature.McpSyncTool; import io.modelcontextprotocol.server.transport.TomcatTestUtil; import io.modelcontextprotocol.util.ToolsUtils; import jakarta.servlet.Filter; @@ -116,16 +117,16 @@ public ToolsPage list(McpRequestContext ctx, String cursor) { } @Override - public Tool find(McpRequestContext ctx, String name) { + public McpSyncTool find(McpRequestContext ctx, String name) { return list(ctx, null).tools() .stream() .filter(tool -> tool.name().equals(name)) .findFirst() + .map(tool -> McpSyncTool.of(tool, this::call)) .orElse(null); } - @Override - public McpSyncResponse call(McpRequestContext ctx, CallToolRequest request) { + private McpSyncResponse call(McpRequestContext ctx, CallToolRequest request) { return switch (request.name()) { case "whoami" -> McpSyncResponse.result(text(whoami())); case "whoami-streaming" -> McpSyncResponse.streaming(notifier -> { From e9f252d8ac3915313524131407ee054884e57427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dariusz=20Je=CC=A8drzejczyk?= Date: Mon, 26 Oct 2026 17:40:32 +0100 Subject: [PATCH 2/2] Return a handler from find in modern resource repositories MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align the resource repositories with the tool and prompt ones: find(ctx, uri) returns McpAsyncResource/McpSyncResource, carrying the read handler, in place of read(ctx, request). ResourcesFeature now answers an unknown URI with the -32602 error carrying {"uri": ...} itself, so repositories no longer build it. See #1268 Signed-off-by: Dariusz Jędrzejczyk --- .../modern/ModernConformanceServlet.java | 17 ++++--- .../server/feature/McpAsyncResource.java | 44 +++++++++++++++++++ .../feature/McpAsyncResourceRepository.java | 10 ++--- .../server/feature/McpSyncResource.java | 43 ++++++++++++++++++ .../feature/McpSyncResourceRepository.java | 6 +-- .../server/feature/ResourcesFeature.java | 17 +++++-- .../modern/server/McpServerMrtrTests.java | 13 +++++- .../server/SubscriptionsFeatureTests.java | 8 ++-- .../server/feature/ResourcesFeatureTests.java | 31 +++++++++---- 9 files changed, 150 insertions(+), 39 deletions(-) create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncResource.java create mode 100644 mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncResource.java diff --git a/conformance-tests/server-servlet-modern/src/main/java/io/modelcontextprotocol/conformance/server/modern/ModernConformanceServlet.java b/conformance-tests/server-servlet-modern/src/main/java/io/modelcontextprotocol/conformance/server/modern/ModernConformanceServlet.java index 1b4390c87..fbc593e82 100644 --- a/conformance-tests/server-servlet-modern/src/main/java/io/modelcontextprotocol/conformance/server/modern/ModernConformanceServlet.java +++ b/conformance-tests/server-servlet-modern/src/main/java/io/modelcontextprotocol/conformance/server/modern/ModernConformanceServlet.java @@ -31,8 +31,6 @@ import io.modelcontextprotocol.modern.McpSchema.Prompt; import io.modelcontextprotocol.modern.McpSchema.PromptArgument; import io.modelcontextprotocol.modern.McpSchema.PromptMessage; -import io.modelcontextprotocol.modern.McpSchema.ReadResourceOutcome; -import io.modelcontextprotocol.modern.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.modern.McpSchema.ReadResourceResult; import io.modelcontextprotocol.modern.McpSchema.Resource; import io.modelcontextprotocol.modern.McpSchema.Result; @@ -52,6 +50,7 @@ import io.modelcontextprotocol.modern.server.feature.McpChangeFeed; import io.modelcontextprotocol.modern.server.feature.McpSyncPrompt; import io.modelcontextprotocol.modern.server.feature.McpSyncPromptRepository; +import io.modelcontextprotocol.modern.server.feature.McpSyncResource; import io.modelcontextprotocol.modern.server.feature.McpSyncResourceRepository; import io.modelcontextprotocol.modern.server.feature.McpSyncTool; import io.modelcontextprotocol.modern.server.feature.McpSyncToolRepository; @@ -519,9 +518,13 @@ public ResourceTemplatesPage listTemplates(McpRequestContext ctx, String cursor) } @Override - public McpSyncResponse read(McpRequestContext ctx, ReadResourceRequest request) { - String uri = request.uri(); - ReadResourceResult result = switch (uri) { + public McpSyncResource find(McpRequestContext ctx, String uri) { + ReadResourceResult result = read(uri); + return result != null ? McpSyncResource.of((c, request) -> McpSyncResponse.result(result)) : null; + } + + private static ReadResourceResult read(String uri) { + return switch (uri) { case "test://static-text" -> textResource(uri, "This is the content of the static text resource."); case "test://watched-resource" -> textResource(uri, "This is a watched resource content."); case "test://static-binary" -> ReadResourceResult @@ -529,10 +532,6 @@ public McpSyncResponse read(McpRequestContext ctx, ReadReso .build(); default -> templateResource(uri); }; - if (result == null) { - throw McpException.invalidParams("Unknown resource: " + uri, Map.of("uri", uri)); - } - return McpSyncResponse.result(result); } @Override diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncResource.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncResource.java new file mode 100644 index 000000000..4ca10f637 --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncResource.java @@ -0,0 +1,44 @@ +/* + * Copyright 2026-2026 the original author or authors. + */ + +package io.modelcontextprotocol.modern.server.feature; + +import io.modelcontextprotocol.modern.McpSchema.ReadResourceOutcome; +import io.modelcontextprotocol.modern.McpSchema.ReadResourceRequest; +import io.modelcontextprotocol.modern.server.McpAsyncResponse; +import io.modelcontextprotocol.modern.server.McpRequestContext; +import io.modelcontextprotocol.util.Assert; +import reactor.core.publisher.Mono; + +/** + * A readable resource: the handler that answers {@code resources/read} for it. + * + * @author Dariusz Jędrzejczyk + */ +public final class McpAsyncResource { + + private final Handler handler; + + private McpAsyncResource(Handler handler) { + Assert.notNull(handler, "handler must not be null"); + this.handler = handler; + } + + public static McpAsyncResource of(Handler handler) { + return new McpAsyncResource(handler); + } + + public Handler handler() { + return this.handler; + } + + /** Answers a {@code resources/read} for this resource. */ + @FunctionalInterface + public interface Handler { + + Mono> read(McpRequestContext ctx, ReadResourceRequest request); + + } + +} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncResourceRepository.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncResourceRepository.java index 48883ffc8..a853c2a4e 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncResourceRepository.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncResourceRepository.java @@ -7,10 +7,7 @@ import java.util.List; import io.modelcontextprotocol.modern.McpException; -import io.modelcontextprotocol.modern.McpSchema.ReadResourceOutcome; -import io.modelcontextprotocol.modern.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.modern.server.McpRequestContext; -import io.modelcontextprotocol.modern.server.McpAsyncResponse; import reactor.core.publisher.Mono; /** @@ -32,11 +29,10 @@ default Mono listTemplates(McpRequestContext ctx, String } /** - * Answer a {@code resources/read}. An unknown resource is an - * {@link McpException#invalidParams(String, Object) invalid-params error} carrying - * {@code {"uri": ...}} as data. + * The resource at this URI, whether listed or matching a template, or empty if there + * is none. */ - Mono> read(McpRequestContext ctx, ReadResourceRequest request); + Mono find(McpRequestContext ctx, String uri); /** * Whether {@code resources/updated} is reported for individual resources, advertised diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncResource.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncResource.java new file mode 100644 index 000000000..35de165e0 --- /dev/null +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncResource.java @@ -0,0 +1,43 @@ +/* + * Copyright 2026-2026 the original author or authors. + */ + +package io.modelcontextprotocol.modern.server.feature; + +import io.modelcontextprotocol.modern.McpSchema.ReadResourceOutcome; +import io.modelcontextprotocol.modern.McpSchema.ReadResourceRequest; +import io.modelcontextprotocol.modern.server.McpRequestContext; +import io.modelcontextprotocol.modern.server.McpSyncResponse; +import io.modelcontextprotocol.util.Assert; + +/** + * The blocking counterpart of {@link McpAsyncResource}. + * + * @author Dariusz Jędrzejczyk + */ +public final class McpSyncResource { + + private final Handler handler; + + private McpSyncResource(Handler handler) { + Assert.notNull(handler, "handler must not be null"); + this.handler = handler; + } + + public static McpSyncResource of(Handler handler) { + return new McpSyncResource(handler); + } + + public Handler handler() { + return this.handler; + } + + /** See {@link McpAsyncResource.Handler}. */ + @FunctionalInterface + public interface Handler { + + McpSyncResponse read(McpRequestContext ctx, ReadResourceRequest request); + + } + +} diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncResourceRepository.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncResourceRepository.java index 68a830b3f..5e4392943 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncResourceRepository.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncResourceRepository.java @@ -6,10 +6,7 @@ import java.util.List; -import io.modelcontextprotocol.modern.McpSchema.ReadResourceOutcome; -import io.modelcontextprotocol.modern.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.modern.server.McpRequestContext; -import io.modelcontextprotocol.modern.server.McpSyncResponse; /** * The blocking counterpart of {@link McpAsyncResourceRepository}. @@ -24,7 +21,8 @@ default ResourceTemplatesPage listTemplates(McpRequestContext ctx, String cursor return ResourceTemplatesPage.of(List.of()); } - McpSyncResponse read(McpRequestContext ctx, ReadResourceRequest request); + /** See {@link McpAsyncResourceRepository#find}. */ + McpSyncResource find(McpRequestContext ctx, String uri); /** See {@link McpAsyncResourceRepository#supportsSubscribe()}. */ default boolean supportsSubscribe() { diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/ResourcesFeature.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/ResourcesFeature.java index d90a3a0bd..99c8b7819 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/ResourcesFeature.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/ResourcesFeature.java @@ -4,10 +4,12 @@ package io.modelcontextprotocol.modern.server.feature; +import java.util.Map; import java.util.Set; import io.modelcontextprotocol.json.McpJsonDefaults; import io.modelcontextprotocol.json.McpJsonMapper; +import io.modelcontextprotocol.modern.McpException; import io.modelcontextprotocol.modern.McpSchema; import io.modelcontextprotocol.modern.McpSchema.CacheScope; import io.modelcontextprotocol.modern.McpSchema.ListResourceTemplatesResult; @@ -81,7 +83,10 @@ public Set methods() { public Mono> handle(McpRequestContext ctx, Object params) { if (McpSchema.METHOD_RESOURCES_READ.equals(ctx.method())) { return Params.decode(this.jsonMapper, params, ReadResourceRequest.class) - .flatMap(request -> this.repository.read(ctx, request)); + .flatMap(request -> this.repository.find(ctx, request.uri()) + .switchIfEmpty(Mono.error(() -> McpException.invalidParams("Unknown resource: " + request.uri(), + Map.of("uri", request.uri())))) + .flatMap(resource -> resource.handler().read(ctx, request))); } Mono request = Params.decode(this.jsonMapper, params, PaginatedRequest.class); if (McpSchema.METHOD_RESOURCES_LIST.equals(ctx.method())) { @@ -131,9 +136,8 @@ public Mono listTemplates(McpRequestContext ctx, String c } @Override - public Mono> read(McpRequestContext ctx, - ReadResourceRequest request) { - return SyncAdapters.respond(ctx, () -> repository.read(ctx, request)); + public Mono find(McpRequestContext ctx, String uri) { + return SyncAdapters.call(ctx, () -> repository.find(ctx, uri)).map(ResourcesFeature::adapt); } @Override @@ -143,4 +147,9 @@ public boolean supportsSubscribe() { }; } + private static McpAsyncResource adapt(McpSyncResource resource) { + return McpAsyncResource + .of((ctx, request) -> SyncAdapters.respond(ctx, () -> resource.handler().read(ctx, request))); + } + } diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/McpServerMrtrTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/McpServerMrtrTests.java index 425d86f19..2c5379fdb 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/McpServerMrtrTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/McpServerMrtrTests.java @@ -26,6 +26,7 @@ import io.modelcontextprotocol.modern.McpSchema.Result; import io.modelcontextprotocol.modern.McpSchema.TextContent; import io.modelcontextprotocol.modern.McpSchema.TextResourceContents; +import io.modelcontextprotocol.modern.server.feature.McpAsyncResource; import io.modelcontextprotocol.modern.server.feature.McpAsyncResourceRepository; import io.modelcontextprotocol.modern.server.feature.ResourcesFeature; import io.modelcontextprotocol.modern.server.feature.ResourcesPage; @@ -179,7 +180,11 @@ public Mono list(McpRequestContext ctx, String cursor) { } @Override - public Mono> read(McpRequestContext ctx, + public Mono find(McpRequestContext ctx, String uri) { + return Mono.just(McpAsyncResource.of(this::read)); + } + + private Mono> read(McpRequestContext ctx, McpSchema.ReadResourceRequest request) { return Mono.just(McpAsyncResponse.result(ReadResourceResult .builder(List.of(new TextResourceContents(request.uri(), "text/plain", "content", null))) @@ -215,7 +220,11 @@ public Mono list(McpRequestContext ctx, String cursor) { } @Override - public Mono> read(McpRequestContext ctx, + public Mono find(McpRequestContext ctx, String uri) { + return Mono.just(McpAsyncResource.of(this::read)); + } + + private Mono> read(McpRequestContext ctx, McpSchema.ReadResourceRequest request) { if (request.requestState() != null) { seenRequestState.set(request.requestState()); diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/SubscriptionsFeatureTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/SubscriptionsFeatureTests.java index b9d72ff78..b5172d1a4 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/SubscriptionsFeatureTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/SubscriptionsFeatureTests.java @@ -13,13 +13,12 @@ import io.modelcontextprotocol.modern.JsonRpc.JSONRPCNotification; import io.modelcontextprotocol.modern.JsonRpc.JSONRPCRequest; import io.modelcontextprotocol.modern.JsonRpc.JSONRPCResponse; -import io.modelcontextprotocol.modern.McpSchema; import io.modelcontextprotocol.modern.McpSchema.CacheScope; import io.modelcontextprotocol.modern.McpSchema.MetaKeys; -import io.modelcontextprotocol.modern.McpSchema.ReadResourceOutcome; -import io.modelcontextprotocol.modern.McpSchema.ReadResourceRequest; import io.modelcontextprotocol.modern.McpSchema.SubscriptionFilter; import io.modelcontextprotocol.modern.McpSchema.SubscriptionsAcknowledgedParams; +import io.modelcontextprotocol.modern.McpSchema; +import io.modelcontextprotocol.modern.server.feature.McpAsyncResource; import io.modelcontextprotocol.modern.server.feature.McpAsyncResourceRepository; import io.modelcontextprotocol.modern.server.feature.McpChangeFeed; import io.modelcontextprotocol.modern.server.feature.McpChangePublisher; @@ -213,8 +212,7 @@ public Mono list(McpRequestContext ctx, String cursor) { } @Override - public Mono> read(McpRequestContext ctx, - ReadResourceRequest request) { + public Mono find(McpRequestContext ctx, String uri) { return Mono.empty(); } diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/feature/ResourcesFeatureTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/feature/ResourcesFeatureTests.java index c41642473..a9a00b2ef 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/feature/ResourcesFeatureTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/feature/ResourcesFeatureTests.java @@ -9,15 +9,15 @@ import io.modelcontextprotocol.json.McpJsonDefaults; import io.modelcontextprotocol.modern.JsonRpc.JSONRPCRequest; +import io.modelcontextprotocol.modern.JsonRpc.JSONRPCResponse; import io.modelcontextprotocol.modern.McpException; -import io.modelcontextprotocol.modern.McpSchema; import io.modelcontextprotocol.modern.McpSchema.ErrorCodes; import io.modelcontextprotocol.modern.McpSchema.ReadResourceOutcome; -import io.modelcontextprotocol.modern.McpSchema.ReadResourceRequest; -import io.modelcontextprotocol.modern.server.McpTransportResponse; -import io.modelcontextprotocol.modern.server.McpRequestContext; +import io.modelcontextprotocol.modern.McpSchema; import io.modelcontextprotocol.modern.server.McpAsyncResponse; +import io.modelcontextprotocol.modern.server.McpRequestContext; import io.modelcontextprotocol.modern.server.McpServer; +import io.modelcontextprotocol.modern.server.McpTransportResponse; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -38,6 +38,10 @@ private static McpServer server(McpAsyncResourceRepository repo) { } private static McpAsyncResourceRepository reading(Mono> response) { + return finding(Mono.just(McpAsyncResource.of((ctx, request) -> response))); + } + + private static McpAsyncResourceRepository finding(Mono resource) { return new McpAsyncResourceRepository() { @Override public Mono list(McpRequestContext ctx, String cursor) { @@ -45,17 +49,28 @@ public Mono list(McpRequestContext ctx, String cursor) { } @Override - public Mono> read(McpRequestContext ctx, - ReadResourceRequest request) { - return response; + public Mono find(McpRequestContext ctx, String uri) { + return resource; } }; } + @Test + void unknownResourceIsInvalidParamsCarryingItsUri() { + McpServer server = server(finding(Mono.empty())); + JSONRPCRequest request = new JSONRPCRequest(McpSchema.METHOD_RESOURCES_READ, 1, + Map.of("_meta", meta(), "uri", "test://missing")); + + JSONRPCResponse response = respond(server, request).block(); + + assertThat(response.error().code()).isEqualTo(ErrorCodes.INVALID_PARAMS); + assertThat(response.error().data()).isEqualTo(Map.of("uri", "test://missing")); + } + @Test void readFailureIsAnsweredWithItsError() { McpServer server = server( - reading(Mono.error(McpException.invalidParams("Unknown resource", Map.of("uri", "test://missing"))))); + reading(Mono.error(McpException.invalidParams("Bad resource", Map.of("uri", "test://missing"))))); JSONRPCRequest request = new JSONRPCRequest(McpSchema.METHOD_RESOURCES_READ, 1, Map.of("_meta", meta(), "uri", "test://missing"));