Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prevent server errors if clients call the mcp server with unprovided …
…capabilities
  • Loading branch information
sorin-florea committed Oct 3, 2025
commit 20d52f81dad39d060dd1d52220e559dc91bca743
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -21,29 +23,41 @@ class DefaultMcpStatelessServerHandler implements McpStatelessServerHandler {

Map<String, McpStatelessNotificationHandler> notificationHandlers;

private final McpSchema.ServerCapabilities serverCapabilities;

public DefaultMcpStatelessServerHandler(Map<String, McpStatelessRequestHandler<?>> requestHandlers,
Map<String, McpStatelessNotificationHandler> notificationHandlers) {
Map<String, McpStatelessNotificationHandler> notificationHandlers,
McpSchema.ServerCapabilities serverCapabilities) {
this.requestHandlers = requestHandlers;
this.notificationHandlers = notificationHandlers;
this.serverCapabilities = serverCapabilities;
}

@Override
public Mono<McpSchema.JSONRPCResponse> handleRequest(McpTransportContext transportContext,
McpSchema.JSONRPCRequest request) {
public Mono<JSONRPCResponse> 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));
});
Expand All @@ -60,4 +74,32 @@ public Mono<Void> 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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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) {
Expand Down