From 2cdb6f59c0b8b30f2243bfbc121801bbaadfe637 Mon Sep 17 00:00:00 2001 From: Pratham Goyal Date: Sat, 24 Oct 2026 07:56:40 +0000 Subject: [PATCH] fix: answer completion/complete without ref or argument with -32602 CompleteRequest requires ref and argument, so a request missing either failed to deserialize before the handler could check it. McpAsyncServer answered it with -32603 Internal error, carrying the Jackson exception message, and McpStatelessAsyncServer threw from handleRequest instead of returning a JSON-RPC error. Answer both with -32602 Invalid params, as CONTRIBUTING.md requires for a missing dispatch identifier. --- .../server/McpAsyncServer.java | 14 ++- .../server/McpStatelessAsyncServer.java | 14 ++- .../CompletionRequestValidationTests.java | 94 +++++++++++++++++++ 3 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 mcp-test/src/test/java/io/modelcontextprotocol/server/CompletionRequestValidationTests.java diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java index 32e76264e..837288d79 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java @@ -1028,8 +1028,18 @@ private McpRequestHandler setLoggerRequestHandler() { private McpRequestHandler completionCompleteRequestHandler() { return (exchange, params) -> { - McpSchema.CompleteRequest request = jsonMapper.convertValue(params, new TypeRef<>() { - }); + McpSchema.CompleteRequest request; + try { + request = jsonMapper.convertValue(params, new TypeRef<>() { + }); + } + catch (IllegalArgumentException e) { + // ref and argument are required and have no default to fall back on + logger.debug("Malformed completion request", e); + return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS) + .message("Invalid completion request: ref and argument are required") + .build()); + } if (request.ref() == null) { return Mono.error( 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 95b694e7f..952700603 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/McpStatelessAsyncServer.java @@ -764,8 +764,18 @@ private McpStatelessRequestHandler promptsGetRequestH private McpStatelessRequestHandler completionCompleteRequestHandler() { return (ctx, params) -> { - McpSchema.CompleteRequest request = jsonMapper.convertValue(params, new TypeRef<>() { - }); + McpSchema.CompleteRequest request; + try { + request = jsonMapper.convertValue(params, new TypeRef<>() { + }); + } + catch (IllegalArgumentException e) { + // ref and argument are required and have no default to fall back on + logger.debug("Malformed completion request", e); + return Mono.error(McpError.builder(ErrorCodes.INVALID_PARAMS) + .message("Invalid completion request: ref and argument are required") + .build()); + } if (request.ref() == null) { return Mono.error( diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/CompletionRequestValidationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/CompletionRequestValidationTests.java new file mode 100644 index 000000000..8486ba033 --- /dev/null +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/CompletionRequestValidationTests.java @@ -0,0 +1,94 @@ +/* + * Copyright 2026-2026 the original author or authors. + */ + +package io.modelcontextprotocol.server; + +import java.time.Duration; +import java.util.Map; +import java.util.concurrent.atomic.AtomicReference; + +import io.modelcontextprotocol.MockMcpServerTransport; +import io.modelcontextprotocol.MockMcpServerTransportProvider; +import io.modelcontextprotocol.common.McpTransportContext; +import io.modelcontextprotocol.spec.McpSchema; +import io.modelcontextprotocol.spec.McpSchema.ErrorCodes; +import io.modelcontextprotocol.spec.McpSchema.ServerCapabilities; +import io.modelcontextprotocol.spec.McpStatelessServerTransport; +import io.modelcontextprotocol.spec.ProtocolVersions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import reactor.core.publisher.Mono; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * A {@code completion/complete} request without its spec-required {@code ref} or + * {@code argument} is answered with Invalid params. + */ +class CompletionRequestValidationTests { + + static Map[] incompleteParams() { + @SuppressWarnings("unchecked") + Map[] params = new Map[] { Map.of("argument", Map.of("name", "a", "value", "b")), + Map.of("ref", Map.of("type", "ref/prompt", "name", "p")) }; + return params; + } + + @ParameterizedTest + @MethodSource("incompleteParams") + void incompleteRequestIsInvalidParams(Map params) { + MockMcpServerTransport transport = new MockMcpServerTransport(); + MockMcpServerTransportProvider transportProvider = new MockMcpServerTransportProvider(transport); + McpAsyncServer server = McpServer.async(transportProvider) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().completions().build()) + .build(); + transportProvider.simulateIncomingMessage(new McpSchema.JSONRPCRequest(McpSchema.METHOD_INITIALIZE, "init", + McpSchema.InitializeRequest + .builder(ProtocolVersions.MCP_2025_11_25, McpSchema.ClientCapabilities.builder().build(), + McpSchema.Implementation.builder("test-client", "1.0.0").build()) + .build())); + transportProvider + .simulateIncomingMessage(new McpSchema.JSONRPCNotification(McpSchema.METHOD_NOTIFICATION_INITIALIZED)); + + transportProvider.simulateIncomingMessage( + new McpSchema.JSONRPCRequest(McpSchema.METHOD_COMPLETION_COMPLETE, "complete", params)); + + McpSchema.JSONRPCResponse response = (McpSchema.JSONRPCResponse) transport.getLastSentMessage(); + assertThat(response.error()).isNotNull(); + assertThat(response.error().code()).isEqualTo(ErrorCodes.INVALID_PARAMS); + server.closeGracefully().block(Duration.ofSeconds(10)); + } + + @ParameterizedTest + @MethodSource("incompleteParams") + void incompleteStatelessRequestIsInvalidParams(Map params) { + AtomicReference handler = new AtomicReference<>(); + McpStatelessServerTransport transport = new McpStatelessServerTransport() { + @Override + public void setMcpHandler(McpStatelessServerHandler mcpHandler) { + handler.set(mcpHandler); + } + + @Override + public Mono closeGracefully() { + return Mono.empty(); + } + }; + McpStatelessAsyncServer server = McpServer.async(transport) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().completions().build()) + .build(); + + McpSchema.JSONRPCResponse response = handler.get() + .handleRequest(McpTransportContext.EMPTY, + new McpSchema.JSONRPCRequest(McpSchema.METHOD_COMPLETION_COMPLETE, "complete", params)) + .block(Duration.ofSeconds(10)); + + assertThat(response.error()).isNotNull(); + assertThat(response.error().code()).isEqualTo(ErrorCodes.INVALID_PARAMS); + server.closeGracefully().block(Duration.ofSeconds(10)); + } + +}