diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java index d1b55f594..018ee0fc0 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/DefaultMcpStatelessServerHandler.java @@ -7,6 +7,8 @@ import io.modelcontextprotocol.common.McpTransportContext; import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse; +import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse.JSONRPCError; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; @@ -21,29 +23,41 @@ class DefaultMcpStatelessServerHandler implements McpStatelessServerHandler { Map notificationHandlers; + private final McpSchema.ServerCapabilities serverCapabilities; + public DefaultMcpStatelessServerHandler(Map> requestHandlers, - Map notificationHandlers) { + Map notificationHandlers, + McpSchema.ServerCapabilities serverCapabilities) { this.requestHandlers = requestHandlers; this.notificationHandlers = notificationHandlers; + this.serverCapabilities = serverCapabilities; } @Override - public Mono handleRequest(McpTransportContext transportContext, - McpSchema.JSONRPCRequest request) { + public Mono handleRequest(McpTransportContext transportContext, McpSchema.JSONRPCRequest request) { McpStatelessRequestHandler requestHandler = this.requestHandlers.get(request.method()); if (requestHandler == null) { - return Mono.error(new McpError("Missing handler for request type: " + request.method())); + // Capability is not declared, but the client is trying to call the method – + // this is an invalid request. + if (!isCapabilityDeclaredForMethod(request.method())) { + JSONRPCError error = new JSONRPCError(McpSchema.ErrorCodes.METHOD_NOT_FOUND, + "Server does not provide " + request.method() + " capability", null); + return Mono.just(new JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null, error)); + } + // Capability is declared, but we failed to register a handler – this is a + // server error. + return Mono.error(new McpError(new JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR, + "Missing handler for request type: " + request.method(), null))); } return requestHandler.handle(transportContext, request.params()) .map(result -> new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), result, null)) .onErrorResume(t -> { - McpSchema.JSONRPCResponse.JSONRPCError error; + JSONRPCError error; if (t instanceof McpError mcpError && mcpError.getJsonRpcError() != null) { error = mcpError.getJsonRpcError(); } else { - error = new McpSchema.JSONRPCResponse.JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR, - t.getMessage(), null); + error = new JSONRPCError(McpSchema.ErrorCodes.INTERNAL_ERROR, t.getMessage(), null); } return Mono.just(new McpSchema.JSONRPCResponse(McpSchema.JSONRPC_VERSION, request.id(), null, error)); }); @@ -60,4 +74,32 @@ public Mono handleNotification(McpTransportContext transportContext, return notificationHandler.handle(transportContext, notification.params()); } + private boolean isCapabilityDeclaredForMethod(String method) { + if (this.serverCapabilities == null) { + return false; + } + + // Ping is always supported + if (McpSchema.METHOD_PING.equals(method) || McpSchema.METHOD_INITIALIZE.equals(method)) + return true; + + if (McpSchema.METHOD_TOOLS_LIST.equals(method) || McpSchema.METHOD_TOOLS_CALL.equals(method)) { + return this.serverCapabilities.tools() != null; + } + if (McpSchema.METHOD_RESOURCES_LIST.equals(method) || McpSchema.METHOD_RESOURCES_READ.equals(method) + || McpSchema.METHOD_RESOURCES_TEMPLATES_LIST.equals(method)) { + return this.serverCapabilities.resources() != null; + } + if (McpSchema.METHOD_PROMPT_LIST.equals(method) || McpSchema.METHOD_PROMPT_GET.equals(method)) { + return this.serverCapabilities.prompts() != null; + } + if (McpSchema.METHOD_LOGGING_SET_LEVEL.equals(method)) { + return this.serverCapabilities.logging() != null; + } + if (McpSchema.METHOD_COMPLETION_COMPLETE.equals(method)) { + return this.serverCapabilities.completions() != null; + } + return false; + } + } 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 823aca41d..e5e1386c9 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java @@ -14,7 +14,6 @@ import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.CompleteResult.CompleteCompletion; import io.modelcontextprotocol.spec.McpSchema.ErrorCodes; -import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse; import io.modelcontextprotocol.spec.McpSchema.PromptReference; import io.modelcontextprotocol.spec.McpSchema.ResourceReference; import io.modelcontextprotocol.spec.McpSchema.Tool; @@ -129,7 +128,8 @@ public class McpStatelessAsyncServer { this.protocolVersions = new ArrayList<>(mcpTransport.protocolVersions()); - McpStatelessServerHandler handler = new DefaultMcpStatelessServerHandler(requestHandlers, Map.of()); + McpStatelessServerHandler handler = new DefaultMcpStatelessServerHandler(requestHandlers, Map.of(), + this.serverCapabilities); mcpTransport.setMcpHandler(handler); } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java index de74bafc1..f06c88675 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java @@ -17,6 +17,7 @@ import io.modelcontextprotocol.server.transport.HttpServletStatelessServerTransport; import io.modelcontextprotocol.server.transport.TomcatTestUtil; import io.modelcontextprotocol.spec.HttpHeaders; +import io.modelcontextprotocol.spec.McpError; import io.modelcontextprotocol.spec.McpSchema; import io.modelcontextprotocol.spec.McpSchema.CallToolResult; import io.modelcontextprotocol.spec.McpSchema.CompleteRequest; @@ -53,6 +54,8 @@ import static net.javacrumbs.jsonunit.assertj.JsonAssertions.json; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; @Timeout(15) class HttpServletStatelessIntegrationTests { @@ -305,7 +308,7 @@ void testStructuredOutputOfObjectArrayValidationSuccess(String clientType) { "type", "object", "properties", Map.of( "name", Map.of("type", "string"), - "age", Map.of("type", "number")), + "age", Map.of("type", "number")), "required", List.of("name", "age"))); // @formatter:on Tool calculatorTool = Tool.builder() @@ -636,6 +639,25 @@ void testThrownMcpErrorAndJsonRpcError() throws Exception { mcpServer.close(); } + @ParameterizedTest(name = "{0} : {displayName} ") + @ValueSource(strings = { "httpclient" }) + void testClientAttemptsToCallUnsupportedCapabilityJsonRpcError(String clientType) { + var mcpServer = McpServer.sync(mcpStatelessServerTransport) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().build()) + .build(); + + var clientBuilder = clientBuilders.get(clientType); + try (var mcpClient = clientBuilder.build()) { + McpSchema.JSONRPCResponse.JSONRPCError promptsError = assertThrows(McpError.class, mcpClient::listPrompts) + .getJsonRpcError(); + assertThat(promptsError.code()).isEqualTo(ErrorCodes.METHOD_NOT_FOUND); + } + finally { + mcpServer.close(); + } + } + private double evaluateExpression(String expression) { // Simple expression evaluator for testing return switch (expression) {