diff --git a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/SubscriptionsFeature.java b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/SubscriptionsFeature.java index 3e815da23..cc7965002 100644 --- a/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/SubscriptionsFeature.java +++ b/mcp-core/src/main/java/io/modelcontextprotocol/modern/server/SubscriptionsFeature.java @@ -71,15 +71,18 @@ public Set methods() { @Override public Mono> handle(McpRequestContext ctx, Object params) { + // The JSON creator fills a missing filter in, so check the raw params. + if (!(params instanceof Map map) || map.get("notifications") == null) { + return Mono.error(McpException.invalidParams("params.notifications is required")); + } SubscriptionsListenRequest request; try { - request = params == null ? null : this.jsonMapper.convertValue(params, SubscriptionsListenRequest.class); + request = this.jsonMapper.convertValue(params, SubscriptionsListenRequest.class); } catch (RuntimeException ex) { return Mono.error(McpException.invalidParams("Malformed SubscriptionsListenRequest")); } - SubscriptionFilter requested = request == null || request.notifications() == null ? SubscriptionFilter.EMPTY - : request.notifications(); + SubscriptionFilter requested = request.notifications(); return Mono.just(McpAsyncResponse.streaming(notifier -> listen(ctx, intersect(requested), notifier))); } diff --git a/mcp-core/src/test/java/io/modelcontextprotocol/modern/server/SubscriptionsFeatureTests.java b/mcp-core/src/test/java/io/modelcontextprotocol/modern/server/SubscriptionsFeatureTests.java index 21cd7786d..5b450bf02 100644 --- a/mcp-core/src/test/java/io/modelcontextprotocol/modern/server/SubscriptionsFeatureTests.java +++ b/mcp-core/src/test/java/io/modelcontextprotocol/modern/server/SubscriptionsFeatureTests.java @@ -211,6 +211,41 @@ void resourceSubscribeIsNotAdvertisedWithoutSubscriptions() { assertThat(resourcesCapability(server)).containsEntry("subscribe", false); } + @Test + void listenWithoutNotificationsIsRejectedAsInvalidParams() { + McpServer server = toolsServer(new McpChangeFeed()); + JSONRPCRequest request = new JSONRPCRequest("subscriptions/listen", 1, Map.of("_meta", meta())); + + assertThat(server.handle(McpTransportContext.EMPTY, request).block()).isInstanceOfSatisfying( + McpTransportResponse.Error.class, + error -> assertThat(error.response().error().code()).isEqualTo(McpSchema.ErrorCodes.INVALID_PARAMS)); + } + + @Test + void listenWithNullNotificationsIsRejectedAsInvalidParams() { + McpServer server = toolsServer(new McpChangeFeed()); + Map params = new HashMap<>(); + params.put("_meta", meta()); + params.put("notifications", null); + JSONRPCRequest request = new JSONRPCRequest("subscriptions/listen", 1, params); + + assertThat(server.handle(McpTransportContext.EMPTY, request).block()).isInstanceOfSatisfying( + McpTransportResponse.Error.class, + error -> assertThat(error.response().error().code()).isEqualTo(McpSchema.ErrorCodes.INVALID_PARAMS)); + } + + @Test + void listenWithEmptyNotificationsIsAccepted() { + McpServer server = toolsServer(new McpChangeFeed()); + Map params = new HashMap<>(); + params.put("_meta", meta()); + params.put("notifications", Map.of()); + JSONRPCRequest request = new JSONRPCRequest("subscriptions/listen", 1, params); + + assertThat(server.handle(McpTransportContext.EMPTY, request).block()) + .isInstanceOf(McpTransportResponse.Streaming.class); + } + private static McpServer resourcesServer(boolean supportsSubscribe, McpChangePublisher publisher) { McpAsyncResourceRepository repository = new McpAsyncResourceRepository() { @Override diff --git a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java index 4dc8e6990..620a8d8c3 100644 --- a/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java +++ b/mcp-test/src/test/java/io/modelcontextprotocol/modern/server/transport/HttpServletMcpTransportIntegrationTests.java @@ -6,6 +6,7 @@ import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.net.Socket; @@ -260,6 +261,19 @@ void unknownToolIsInvalidParams() throws Exception { assertThat(errorCode(response)).isEqualTo(ErrorCodes.INVALID_PARAMS); } + @Test + void listenWithoutNotificationsIsInvalidParams() throws Exception { + HttpRequest request = post("subscriptions/listen", Map.of("_meta", meta())).build(); + HttpResponse response = HttpClient.newHttpClient() + .send(request, HttpResponse.BodyHandlers.ofInputStream()); + + try (InputStream body = response.body()) { + assertThat(response.statusCode()).isEqualTo(400); + assertThat(new String(body.readAllBytes(), StandardCharsets.UTF_8)) + .contains("\"code\":" + ErrorCodes.INVALID_PARAMS); + } + } + @Test void streamingBodyExceptionIsAnsweredInStream() throws Exception { HttpResponse response = callTool("fails-streaming");