From 8751ad453360d330bdaa1809a0c72a682c69d432 Mon Sep 17 00:00:00 2001 From: Param Parikh Date: Thu, 27 Aug 2026 23:06:03 +0000 Subject: [PATCH 1/2] fix(mcp-server): Return -32601 for unknown JSON-RPC methods Per JSON-RPC 2.0, a request carrying an id must receive a response; the server previously returned null for unknown methods, which surfaced as an HTTP 200 with an empty body through HTTP transports. Notifications (requests without an id) are still silently dropped. This also makes MCP 2026-07-28 clients' auto mode fall back to the legacy handshake via a clean, deterministic -32601 on the server/discover probe instead of relying on a synthesized parse error from the empty body. https://issues.amazon.com/issues/APPDEV-2256 --- .../smithy/java/mcp/server/McpService.java | 28 ++++++-- .../smithy/java/mcp/server/McpServerTest.java | 71 +++++++++++++++++++ 2 files changed, 95 insertions(+), 4 deletions(-) diff --git a/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpService.java b/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpService.java index d22343b2d7..04e15fb914 100644 --- a/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpService.java +++ b/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpService.java @@ -77,6 +77,7 @@ public final class McpService { private static final InternalLogger LOG = InternalLogger.getLogger(McpService.class); private static final Context.Key ASYNC_DISPATCH = Context.key("mcp.asyncDispatch"); + private static final int METHOD_NOT_FOUND_ERROR_CODE = -32601; private static final JsonCodec CODEC = JsonCodec.builder() .settings(JsonSettings.builder() @@ -131,8 +132,8 @@ public final class McpService { *
  • Synchronous (return value): For most requests, the response is returned directly.
  • *
  • Asynchronous (callback): For proxy tool calls, returns {@code null} and the callback * is invoked when the proxy responds.
  • - *
  • Neither: For notifications and unknown methods, returns {@code null} and the callback - * is never invoked.
  • + *
  • Neither: For notifications, returns {@code null} and the callback is never + * invoked. Requests with unknown methods receive a -32601 (Method not found) error.
  • * * * @param req The JSON-RPC request to handle @@ -172,7 +173,7 @@ yield switch (method) { case "tools/list" -> handleToolsList(currentReq, protocolVersion); case "tools/call" -> handleToolsCall(currentReq, asyncResponseCallback, protocolVersion, hook); - default -> null; + default -> methodNotFound(currentReq); }; } }; @@ -211,7 +212,7 @@ yield switch (method) { case "tools/list" -> handleToolsList(req, protocolVersion); case "tools/call" -> handleToolsCallDirect(req, asyncResponseCallback, protocolVersion); - default -> null; // Notifications or unknown methods + default -> methodNotFound(req); }; } }; @@ -798,6 +799,25 @@ private JsonRpcResponse createErrorResponse(JsonRpcRequest req, String s) { .build(); } + /** + * Per JSON-RPC 2.0, a request with an unknown method must receive a -32601 (Method not found) + * error, while notifications (requests without an id) must never receive a response. + */ + private static JsonRpcResponse methodNotFound(JsonRpcRequest req) { + if (req.getId() == null) { + return null; + } + var error = JsonRpcErrorResponse.builder() + .code(METHOD_NOT_FOUND_ERROR_CODE) + .message("Method not found: " + req.getMethod()) + .build(); + return JsonRpcResponse.builder() + .id(req.getId()) + .error(error) + .jsonrpc("2.0") + .build(); + } + private Map createTools(Map services) { var tools = new ConcurrentHashMap(); for (var entry : services.entrySet()) { diff --git a/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/McpServerTest.java b/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/McpServerTest.java index 326fef7d86..88c7abfe6f 100644 --- a/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/McpServerTest.java +++ b/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/McpServerTest.java @@ -754,6 +754,77 @@ void testNotificationsDoNotRequireRequestId() { assertNotNull(response.getResult()); } + @Test + void testUnknownMethodReturnsMethodNotFound() { + server = McpServer.builder() + .name("smithy-mcp-server") + .input(input) + .output(output) + .addService("test-mcp", + ProxyService.builder() + .service(ShapeId.from("smithy.test#TestService")) + .proxyEndpoint("http://localhost") + .model(MODEL) + .build()) + .build(); + + server.start(); + + write("nonexistent/method", Document.of(Map.of()), Document.of(42)); + var response = read(); + assertEquals("2.0", response.getJsonrpc()); + assertEquals(42, response.getId().asNumber().intValue()); + assertNull(response.getResult()); + assertNotNull(response.getError()); + assertEquals(-32601, response.getError().getCode()); + assertTrue(response.getError().getMessage().contains("nonexistent/method")); + + // String ids must be echoed back with their original type + write("server/discover", Document.of(Map.of()), Document.of("discover-1")); + response = read(); + assertEquals("discover-1", response.getId().asString()); + assertNull(response.getResult()); + assertEquals(-32601, response.getError().getCode()); + + // Known methods are unaffected + write("ping", Document.of(Map.of()), Document.of(43)); + response = read(); + assertEquals(43, response.getId().asNumber().intValue()); + assertNull(response.getError()); + assertNotNull(response.getResult()); + } + + @Test + void testUnknownNotificationIsSilentlyDropped() { + server = McpServer.builder() + .name("smithy-mcp-server") + .input(input) + .output(output) + .addService("test-mcp", + ProxyService.builder() + .service(ShapeId.from("smithy.test#TestService")) + .proxyEndpoint("http://localhost") + .model(MODEL) + .build()) + .build(); + + server.start(); + + // Unknown notifications (no id) must not receive a Method not found error + writeNotification("notifications/does-not-exist", Document.of(Map.of())); + output.assertNoOutput(); + + // Known notifications remain silently dropped + writeNotification("notifications/initialized", Document.of(Map.of())); + output.assertNoOutput(); + + // The next response on the wire belongs to the follow-up request, not a late error + write("tools/list", Document.of(Map.of()), Document.of(7)); + var response = read(); + assertEquals(7, response.getId().asNumber().intValue()); + assertNotNull(response.getResult()); + } + @Test void testPromptsList() { server = McpServer.builder() From af73ee0fbca876085a393dccee4a724553e86f82 Mon Sep 17 00:00:00 2001 From: Param Parikh Date: Fri, 28 Aug 2026 00:06:22 +0000 Subject: [PATCH 2/2] fix(mcp-server): Answer latest supported version for unknown protocol versions Per the MCP spec, a server that does not support the requested protocol version must respond with the latest version it supports. Previously handleInitialize left protocolVersion unset for unknown versions, so the Smithy model default (2024-11-05, the oldest version) leaked into InitializeResult. Add ProtocolVersion.latestVersion(), derived from the supported-version registry so it cannot drift as versions are added. The registry moves into an initialization-on-demand holder: computing the latest during class init of the sealed base class NPEs when a subclass INSTANCE is the first member of the hierarchy touched (base clinit reads a still-null INSTANCE; caught by McpServerIntegrationTest). Known versions are still echoed as-is; a missing protocolVersion param keeps its existing behavior. https://issues.amazon.com/issues/APPDEV-2257 --- .../smithy/java/mcp/server/McpService.java | 6 ++- .../java/mcp/server/ProtocolVersion.java | 41 +++++++++++++++---- .../smithy/java/mcp/server/McpServerTest.java | 23 +++++++++++ .../java/mcp/server/ProtocolVersionTest.java | 6 +++ 4 files changed, 67 insertions(+), 9 deletions(-) diff --git a/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpService.java b/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpService.java index 04e15fb914..6d7fd679f7 100644 --- a/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpService.java +++ b/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/McpService.java @@ -296,7 +296,11 @@ private JsonRpcResponse handleInitialize(JsonRpcRequest req) { String pv = null; if (maybeVersion != null) { var protocolVersion = ProtocolVersion.version(maybeVersion.asString()); - if (!(protocolVersion instanceof ProtocolVersion.UnknownVersion)) { + if (protocolVersion instanceof ProtocolVersion.UnknownVersion) { + // Per the MCP spec, a server that does not support the requested protocol + // version must respond with the latest version it supports. + pv = ProtocolVersion.latestVersion().identifier(); + } else { pv = protocolVersion.identifier(); } } diff --git a/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/ProtocolVersion.java b/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/ProtocolVersion.java index 555559a3b8..f5b8a62543 100644 --- a/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/ProtocolVersion.java +++ b/mcp/mcp-server/src/main/java/software/amazon/smithy/java/mcp/server/ProtocolVersion.java @@ -5,6 +5,8 @@ package software.amazon.smithy.java.mcp.server; +import java.util.Collections; +import java.util.List; import software.amazon.smithy.utils.SmithyUnstableApi; @SmithyUnstableApi @@ -49,6 +51,20 @@ private UnknownVersion(String identifier) { } } + /** + * Holder defers initialization until first use so the version subclasses are fully loaded + * first — a plain static field on this class would read a still-null INSTANCE whenever a + * subclass is the first member of the hierarchy to be initialized. + */ + private static final class SupportedVersions { + private static final List ALL = List.of( + v2024_11_05.INSTANCE, + v2025_03_26.INSTANCE, + v2025_06_18.INSTANCE, + v2025_11_25.INSTANCE); + private static final ProtocolVersion LATEST = Collections.max(ALL); + } + private final String identifier; private ProtocolVersion(String identifier) { @@ -72,17 +88,26 @@ public final int compareTo(ProtocolVersion o) { } public static ProtocolVersion version(String identifier) { - return switch (identifier) { - case null -> v2025_03_26.INSTANCE; - case "2024-11-05" -> v2024_11_05.INSTANCE; - case "2025-03-26" -> v2025_03_26.INSTANCE; - case "2025-06-18" -> v2025_06_18.INSTANCE; - case "2025-11-25" -> v2025_11_25.INSTANCE; - default -> new UnknownVersion(identifier); - }; + if (identifier == null) { + return defaultVersion(); + } + for (var version : SupportedVersions.ALL) { + if (version.identifier.equals(identifier)) { + return version; + } + } + return new UnknownVersion(identifier); } public static ProtocolVersion defaultVersion() { return v2025_03_26.INSTANCE; } + + /** + * The most recent protocol version this server supports, derived from the supported-version + * registry. + */ + public static ProtocolVersion latestVersion() { + return SupportedVersions.LATEST; + } } diff --git a/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/McpServerTest.java b/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/McpServerTest.java index 88c7abfe6f..37ee6a2f81 100644 --- a/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/McpServerTest.java +++ b/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/McpServerTest.java @@ -154,6 +154,29 @@ public void initializeWithV2025_11_25ProtocolVersion() { assertTrue(outputSchema.get("properties").asStringMap().containsKey("outputStr")); } + @Test + public void initializeWithUnknownProtocolVersionAnswersLatestSupported() { + server = McpServer.builder() + .name("smithy-mcp-server") + .input(input) + .output(output) + .addService("test-mcp", + ProxyService.builder() + .service(ShapeId.from("smithy.test#TestService")) + .proxyEndpoint("http://localhost") + .model(MODEL) + .build()) + .build(); + + server.start(); + + // Per the MCP spec, a server answers a request for a version it does not support with + // the latest version it supports — not the oldest. + write("initialize", Document.of(Map.of("protocolVersion", Document.of("2026-07-28")))); + var pv = read().getResult().getMember("protocolVersion").asString(); + assertEquals("2025-11-25", pv); + } + @Test public void noOutputSchemaWithUnsupportedProtocolVersion() { server = McpServer.builder() diff --git a/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/ProtocolVersionTest.java b/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/ProtocolVersionTest.java index d95017e472..5d08c0596d 100644 --- a/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/ProtocolVersionTest.java +++ b/mcp/mcp-server/src/test/java/software/amazon/smithy/java/mcp/server/ProtocolVersionTest.java @@ -39,6 +39,12 @@ void defaultVersionIs2025_03_26() { assertEquals("2025-03-26", ProtocolVersion.defaultVersion().identifier()); } + @Test + void latestVersionIs2025_11_25() { + assertInstanceOf(ProtocolVersion.v2025_11_25.class, ProtocolVersion.latestVersion()); + assertEquals("2025-11-25", ProtocolVersion.latestVersion().identifier()); + } + @Test void compareToOrdersChronologically() { assertTrue(ProtocolVersion.v2024_11_05.INSTANCE.compareTo(ProtocolVersion.v2025_03_26.INSTANCE) < 0);