|
13 | 13 | import io.modelcontextprotocol.modern.JsonRpc.JSONRPCNotification; |
14 | 14 | import io.modelcontextprotocol.modern.JsonRpc.JSONRPCRequest; |
15 | 15 | import io.modelcontextprotocol.modern.JsonRpc.JSONRPCResponse; |
| 16 | +import io.modelcontextprotocol.modern.JsonRpc.JSONRPCResponse.JSONRPCError; |
| 17 | +import io.modelcontextprotocol.modern.McpException; |
16 | 18 | import io.modelcontextprotocol.modern.McpSchema.CacheScope; |
| 19 | +import io.modelcontextprotocol.modern.McpSchema.ErrorCodes; |
17 | 20 | import io.modelcontextprotocol.modern.McpSchema.MetaKeys; |
18 | 21 | import io.modelcontextprotocol.modern.McpSchema.SubscriptionFilter; |
19 | 22 | import io.modelcontextprotocol.modern.McpSchema.SubscriptionsAcknowledgedParams; |
|
35 | 38 | import static io.modelcontextprotocol.modern.server.ModernTestFixtures.emptyTools; |
36 | 39 | import static io.modelcontextprotocol.modern.server.ModernTestFixtures.meta; |
37 | 40 | import static io.modelcontextprotocol.modern.server.ModernTestFixtures.respond; |
| 41 | +import static org.assertj.core.api.Assertions.as; |
38 | 42 | import static org.assertj.core.api.Assertions.assertThat; |
| 43 | +import static org.assertj.core.api.InstanceOfAssertFactories.STRING; |
| 44 | +import static org.assertj.core.api.InstanceOfAssertFactories.type; |
39 | 45 |
|
40 | 46 | class SubscriptionsFeatureTests { |
41 | 47 |
|
@@ -171,6 +177,66 @@ void listenAfterEarlierListenEndedReceivesChanges() { |
171 | 177 | .verifyComplete(); |
172 | 178 | } |
173 | 179 |
|
| 180 | + @Test |
| 181 | + void listenWithoutNotificationsIsRejected() { |
| 182 | + McpServer server = toolsServer(new McpChangeFeed()); |
| 183 | + |
| 184 | + Map<String, Object> params = new HashMap<>(); |
| 185 | + params.put("_meta", meta()); |
| 186 | + McpTransportResponse response = server |
| 187 | + .handle(McpTransportContext.EMPTY, new JSONRPCRequest("subscriptions/listen", 1, params)) |
| 188 | + .block(); |
| 189 | + |
| 190 | + assertNotificationsRequired(response); |
| 191 | + } |
| 192 | + |
| 193 | + @Test |
| 194 | + void listenWithNullNotificationsIsRejected() { |
| 195 | + McpServer server = toolsServer(new McpChangeFeed()); |
| 196 | + |
| 197 | + Map<String, Object> params = new HashMap<>(); |
| 198 | + params.put("_meta", meta()); |
| 199 | + params.put("notifications", null); |
| 200 | + McpTransportResponse response = server |
| 201 | + .handle(McpTransportContext.EMPTY, new JSONRPCRequest("subscriptions/listen", 1, params)) |
| 202 | + .block(); |
| 203 | + |
| 204 | + assertNotificationsRequired(response); |
| 205 | + } |
| 206 | + |
| 207 | + @Test |
| 208 | + void listenWithoutParamsIsRejected() { |
| 209 | + SubscriptionsFeature feature = new SubscriptionsFeature(new McpChangeFeed(), McpJsonDefaults.getMapper(), true, |
| 210 | + false, false, false); |
| 211 | + |
| 212 | + StepVerifier.create(feature.handle(null, null)).expectErrorSatisfies(error -> { |
| 213 | + JSONRPCError jsonRpcError = ((McpException) error).error(); |
| 214 | + assertThat(jsonRpcError.code()).isEqualTo(ErrorCodes.INVALID_PARAMS); |
| 215 | + assertThat(jsonRpcError.message()).contains("notifications").contains("required"); |
| 216 | + }).verify(); |
| 217 | + } |
| 218 | + |
| 219 | + @Test |
| 220 | + void emptyNotificationsIsAcknowledgedAndStaysOpen() { |
| 221 | + McpChangeFeed feed = new McpChangeFeed(); |
| 222 | + McpServer server = toolsServer(feed); |
| 223 | + |
| 224 | + Map<String, Object> params = new HashMap<>(); |
| 225 | + params.put("_meta", meta()); |
| 226 | + params.put("notifications", Map.of()); |
| 227 | + var invocation = (McpTransportResponse.Streaming) server |
| 228 | + .handle(McpTransportContext.EMPTY, new JSONRPCRequest("subscriptions/listen", 1, params)) |
| 229 | + .block(); |
| 230 | + |
| 231 | + StepVerifier.create(invocation.messages()) |
| 232 | + .assertNext(msg -> assertThat(acknowledgedNotifications((JSONRPCNotification) msg)) |
| 233 | + .isEqualTo(SubscriptionFilter.EMPTY)) |
| 234 | + .then(() -> feed.broadcast(new ServerChange.ToolsListChanged())) |
| 235 | + .then(server::closeGracefully) |
| 236 | + .expectNextMatches(msg -> msg instanceof JSONRPCResponse) |
| 237 | + .verifyComplete(); |
| 238 | + } |
| 239 | + |
174 | 240 | @Test |
175 | 241 | void resourceSubscribeIsAdvertisedAndHonouredWhenRepositorySupportsIt() { |
176 | 242 | McpServer server = resourcesServer(true, new McpChangeFeed()); |
@@ -268,6 +334,15 @@ private static McpTransportResponse.Streaming listen(McpServer server, int id) { |
268 | 334 | return (McpTransportResponse.Streaming) server.handle(McpTransportContext.EMPTY, request).block(); |
269 | 335 | } |
270 | 336 |
|
| 337 | + private static void assertNotificationsRequired(McpTransportResponse response) { |
| 338 | + // An error response, not a stream: no acknowledgement is ever sent. |
| 339 | + assertThat(response).asInstanceOf(type(McpTransportResponse.Error.class)) |
| 340 | + .extracting(error -> error.response().error()) |
| 341 | + .returns(ErrorCodes.INVALID_PARAMS, JSONRPCError::code) |
| 342 | + .extracting(JSONRPCError::message, as(STRING)) |
| 343 | + .contains("notifications", "required"); |
| 344 | + } |
| 345 | + |
271 | 346 | private static Object subscriptionIdOf(JSONRPCNotification notification) { |
272 | 347 | return ((SubscriptionsAcknowledgedParams) notification.params()).meta().get(MetaKeys.SUBSCRIPTION_ID); |
273 | 348 | } |
|
0 commit comments