diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java index 54f0ac030..20ffb7e3a 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/server/transport/HttpServletStatelessServerTransport.java @@ -195,7 +195,12 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) response.setContentType(APPLICATION_JSON); response.setCharacterEncoding(UTF_8); - response.setStatus(HttpServletResponse.SC_OK); + + int httpStatus = HttpServletResponse.SC_OK; + if (jsonrpcResponse.error() != null) { + httpStatus = mapJsonRpcErrorToHttpStatus(jsonrpcResponse.error().code()); + } + response.setStatus(httpStatus); String jsonResponseText = jsonMapper.writeValueAsString(jsonrpcResponse); PrintWriter writer = response.getWriter(); @@ -249,6 +254,21 @@ else if (message instanceof McpSchema.JSONRPCNotification jsonrpcNotification) { } } + /** + * Maps a JSON-RPC error code to an appropriate HTTP status code per the MCP + * Streamable HTTP specification (2026-07-28). Only METHOD_NOT_FOUND is mapped to a + * non-200 status (HTTP 404) as specified by the protocol. All other JSON-RPC errors + * are returned with HTTP 200 per standard JSON-RPC conventions. + * @param jsonRpcErrorCode The JSON-RPC error code + * @return The corresponding HTTP status code + */ + private static int mapJsonRpcErrorToHttpStatus(int jsonRpcErrorCode) { + if (jsonRpcErrorCode == McpSchema.ErrorCodes.METHOD_NOT_FOUND) { + return HttpServletResponse.SC_NOT_FOUND; + } + return HttpServletResponse.SC_OK; + } + /** * Sends an error response to the client. * @param response The HTTP servlet response diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java index 611175780..cd81ab951 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/server/HttpServletStatelessIntegrationTests.java @@ -915,6 +915,77 @@ public void cancel() { assertThat(response.statusCode()).isEqualTo(HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE); } + @Test + void testMissingHandlerReturnsHttp404WithMethodNotFoundError() throws Exception { + var mcpServer = McpServer.sync(mcpStatelessServerTransport) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().build()) + .build(); + + // Use MockHttpServletRequest/Response to directly verify the HTTP 404 status + // and JSON-RPC METHOD_NOT_FOUND error, since a real HTTP client treats 404 + // as a transport-level error per the spec. + McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest("foo/bar", "test-request-123"); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", CUSTOM_MESSAGE_ENDPOINT); + MockHttpServletResponse response = new MockHttpServletResponse(); + + byte[] content = JSON_MAPPER.writeValueAsBytes(jsonrpcRequest); + request.setContent(content); + request.addHeader("Content-Type", APPLICATION_JSON); + request.addHeader("Content-Length", Integer.toString(content.length)); + request.addHeader("Accept", APPLICATION_JSON + ", " + TEXT_EVENT_STREAM); + request.addHeader(HttpHeaders.PROTOCOL_VERSION, ProtocolVersions.MCP_2025_03_26); + + mcpStatelessServerTransport.service(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NOT_FOUND); + + McpSchema.JSONRPCResponse jsonrpcResponse = JSON_MAPPER.readValue(response.getContentAsByteArray(), + McpSchema.JSONRPCResponse.class); + + assertThat(jsonrpcResponse).isNotNull(); + assertThat(jsonrpcResponse.error()).isNotNull(); + assertThat(jsonrpcResponse.error().code()).isEqualTo(ErrorCodes.METHOD_NOT_FOUND); + assertThat(jsonrpcResponse.error().message()).isEqualTo("Method not found: foo/bar"); + + mcpServer.closeGracefully(); + } + + @Test + void testUnknownMethodReturnsHttp404WithMethodNotFoundError() throws Exception { + var mcpServer = McpServer.sync(mcpStatelessServerTransport) + .serverInfo("test-server", "1.0.0") + .capabilities(ServerCapabilities.builder().build()) + .build(); + + McpSchema.JSONRPCRequest jsonrpcRequest = new McpSchema.JSONRPCRequest("server/discover", "discover-1"); + + MockHttpServletRequest request = new MockHttpServletRequest("POST", CUSTOM_MESSAGE_ENDPOINT); + MockHttpServletResponse response = new MockHttpServletResponse(); + + byte[] content = JSON_MAPPER.writeValueAsBytes(jsonrpcRequest); + request.setContent(content); + request.addHeader("Content-Type", APPLICATION_JSON); + request.addHeader("Content-Length", Integer.toString(content.length)); + request.addHeader("Accept", APPLICATION_JSON + ", " + TEXT_EVENT_STREAM); + request.addHeader(HttpHeaders.PROTOCOL_VERSION, ProtocolVersions.MCP_2025_03_26); + + mcpStatelessServerTransport.service(request, response); + + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NOT_FOUND); + + McpSchema.JSONRPCResponse jsonrpcResponse = JSON_MAPPER.readValue(response.getContentAsByteArray(), + McpSchema.JSONRPCResponse.class); + + assertThat(jsonrpcResponse).isNotNull(); + assertThat(jsonrpcResponse.error()).isNotNull(); + assertThat(jsonrpcResponse.error().code()).isEqualTo(ErrorCodes.METHOD_NOT_FOUND); + assertThat(jsonrpcResponse.error().message()).isEqualTo("Method not found: server/discover"); + + mcpServer.close(); + } + private double evaluateExpression(String expression) { // Simple expression evaluator for testing return switch (expression) {