Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public final class McpService {

private static final InternalLogger LOG = InternalLogger.getLogger(McpService.class);
private static final Context.Key<Boolean> 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()
Expand Down Expand Up @@ -131,8 +132,8 @@ public final class McpService {
* <li><b>Synchronous (return value):</b> For most requests, the response is returned directly.</li>
* <li><b>Asynchronous (callback):</b> For proxy tool calls, returns {@code null} and the callback
* is invoked when the proxy responds.</li>
* <li><b>Neither:</b> For notifications and unknown methods, returns {@code null} and the callback
* is never invoked.</li>
* <li><b>Neither:</b> For notifications, returns {@code null} and the callback is never
* invoked. Requests with unknown methods receive a -32601 (Method not found) error.</li>
* </ul>
*
* @param req The JSON-RPC request to handle
Expand Down Expand Up @@ -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);
};
}
};
Expand Down Expand Up @@ -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);
};
}
};
Expand Down Expand Up @@ -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<String, Tool> createTools(Map<String, Service> services) {
var tools = new ConcurrentHashMap<String, Tool>();
for (var entry : services.entrySet()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down