Skip to content

Commit 740e6df

Browse files
committed
fix: validate resource URIs and cap subscriptions per session
1 parent 8ee8ccb commit 740e6df

2 files changed

Lines changed: 118 additions & 0 deletions

File tree

mcp-core/src/main/java/io/modelcontextprotocol/server/McpAsyncServer.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ public class McpAsyncServer {
9292

9393
private static final Logger logger = LoggerFactory.getLogger(McpAsyncServer.class);
9494

95+
/**
96+
* Maximum number of resource URIs a single session may subscribe to. Prevents a
97+
* client from growing the resource subscriptions map without bound.
98+
*/
99+
private static final int MAX_RESOURCE_SUBSCRIPTIONS_PER_SESSION = 1024;
100+
95101
private final McpServerTransportProviderBase mcpTransportProvider;
96102

97103
private final McpJsonMapper jsonMapper;
@@ -758,6 +764,25 @@ private McpRequestHandler<Object> resourcesSubscribeRequestHandler() {
758764
});
759765
String uri = subscribeRequest.uri();
760766
String sessionId = exchange.sessionId();
767+
768+
// Validate the URI against the registered resources and resource templates
769+
// before accepting the subscription.
770+
if (this.findResourceSpecification(uri).isEmpty() && this.findResourceTemplateSpecification(uri).isEmpty()) {
771+
return Mono.error(RESOURCE_NOT_FOUND.apply(uri));
772+
}
773+
774+
// Cap the number of resource subscriptions per session to prevent a client
775+
// from growing the subscriptions map without bound.
776+
long sessionSubscriptions = this.resourceSubscriptions.values()
777+
.stream()
778+
.filter(subscribedSessions -> subscribedSessions.contains(sessionId))
779+
.count();
780+
if (sessionSubscriptions >= MAX_RESOURCE_SUBSCRIPTIONS_PER_SESSION) {
781+
return Mono.error(McpError.builder(McpSchema.ErrorCodes.INVALID_PARAMS)
782+
.message("Resource subscription limit exceeded for session: " + sessionId)
783+
.build());
784+
}
785+
761786
this.resourceSubscriptions.computeIfAbsent(uri, k -> Collections.newSetFromMap(new ConcurrentHashMap<>()))
762787
.add(sessionId);
763788
logger.debug("Session {} subscribed to resource URI: {}", sessionId, uri);

mcp-test/src/test/java/io/modelcontextprotocol/server/ResourceSubscriptionTests.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44

55
package io.modelcontextprotocol.server;
66

7+
import java.util.List;
8+
import java.util.Map;
79
import java.util.UUID;
810

911
import io.modelcontextprotocol.MockMcpServerTransport;
1012
import io.modelcontextprotocol.MockMcpServerTransportProvider;
1113
import io.modelcontextprotocol.spec.McpSchema;
1214
import io.modelcontextprotocol.spec.ProtocolVersions;
1315
import org.junit.jupiter.api.Test;
16+
import reactor.core.publisher.Mono;
1417
import reactor.test.StepVerifier;
1518

1619
import static org.assertj.core.api.Assertions.assertThat;
@@ -59,6 +62,31 @@ private static McpSchema.JSONRPCRequest unsubscribeRequest(String uri) {
5962
McpSchema.UnsubscribeRequest.builder(uri).build());
6063
}
6164

65+
private static McpAsyncServer buildServerWithResource(MockMcpServerTransportProvider transportProvider) {
66+
McpSchema.Resource resource = McpSchema.Resource.builder(RESOURCE_URI, "Test Resource")
67+
.mimeType("text/plain")
68+
.build();
69+
return McpServer.async(transportProvider)
70+
.serverInfo(SERVER_INFO)
71+
.capabilities(McpSchema.ServerCapabilities.builder().resources(true, false).build())
72+
.resources(new McpServerFeatures.AsyncResourceSpecification(resource,
73+
(exchange, request) -> Mono.just(new McpSchema.ReadResourceResult(List.of()))))
74+
.build();
75+
}
76+
77+
private static McpAsyncServer buildServerWithResourceTemplate(MockMcpServerTransportProvider transportProvider) {
78+
McpSchema.ResourceTemplate resourceTemplate = McpSchema.ResourceTemplate
79+
.builder("test://resource/{id}", "Test Resource Template")
80+
.mimeType("text/plain")
81+
.build();
82+
return McpServer.async(transportProvider)
83+
.serverInfo(SERVER_INFO)
84+
.capabilities(McpSchema.ServerCapabilities.builder().resources(true, false).build())
85+
.resourceTemplates(new McpServerFeatures.AsyncResourceTemplateSpecification(resourceTemplate,
86+
(exchange, request) -> Mono.just(new McpSchema.ReadResourceResult(List.of()))))
87+
.build();
88+
}
89+
6290
@Test
6391
void notifyResourcesUpdated_noSubscribers_completesEmpty() {
6492
MockMcpServerTransport transport = new MockMcpServerTransport();
@@ -166,4 +194,69 @@ void notifyResourcesUpdated_afterSessionClose_doesNotNotifySession() {
166194
server.closeGracefully().block();
167195
}
168196

197+
@Test
198+
void subscribeToRegisteredUri_succeeds() {
199+
MockMcpServerTransport transport = new MockMcpServerTransport();
200+
MockMcpServerTransportProvider transportProvider = new MockMcpServerTransportProvider(transport);
201+
McpAsyncServer server = buildServerWithResource(transportProvider);
202+
203+
transportProvider.simulateIncomingMessage(initRequest());
204+
transportProvider.simulateIncomingMessage(initializedNotification());
205+
transport.clearSentMessages();
206+
transportProvider.simulateIncomingMessage(subscribeRequest(RESOURCE_URI));
207+
208+
McpSchema.JSONRPCMessage sent = transport.getLastSentMessage();
209+
assertThat(sent).isInstanceOf(McpSchema.JSONRPCResponse.class);
210+
McpSchema.JSONRPCResponse response = (McpSchema.JSONRPCResponse) sent;
211+
assertThat(response.error()).as("subscribing to a registered resource must succeed").isNull();
212+
assertThat(response.result()).isEqualTo(Map.of());
213+
214+
server.closeGracefully().block();
215+
}
216+
217+
@Test
218+
void subscribeToUnregisteredUri_returnsResourceNotFound() {
219+
MockMcpServerTransport transport = new MockMcpServerTransport();
220+
MockMcpServerTransportProvider transportProvider = new MockMcpServerTransportProvider(transport);
221+
McpAsyncServer server = buildServerWithResource(transportProvider);
222+
223+
transportProvider.simulateIncomingMessage(initRequest());
224+
transportProvider.simulateIncomingMessage(initializedNotification());
225+
transport.clearSentMessages();
226+
transportProvider.simulateIncomingMessage(subscribeRequest("test://unknown/uri"));
227+
228+
McpSchema.JSONRPCMessage sent = transport.getLastSentMessage();
229+
assertThat(sent).isInstanceOf(McpSchema.JSONRPCResponse.class);
230+
McpSchema.JSONRPCResponse response = (McpSchema.JSONRPCResponse) sent;
231+
assertThat(response.error()).isNotNull();
232+
assertThat(response.error().code()).isEqualTo(McpSchema.ErrorCodes.RESOURCE_NOT_FOUND);
233+
234+
server.closeGracefully().block();
235+
}
236+
237+
@Test
238+
void subscribeBeyondPerSessionLimit_isRejected() {
239+
MockMcpServerTransport transport = new MockMcpServerTransport();
240+
MockMcpServerTransportProvider transportProvider = new MockMcpServerTransportProvider(transport);
241+
McpAsyncServer server = buildServerWithResourceTemplate(transportProvider);
242+
243+
transportProvider.simulateIncomingMessage(initRequest());
244+
transportProvider.simulateIncomingMessage(initializedNotification());
245+
transport.clearSentMessages();
246+
247+
for (int i = 0; i < 1024; i++) {
248+
transportProvider.simulateIncomingMessage(subscribeRequest("test://resource/" + i));
249+
}
250+
251+
transportProvider.simulateIncomingMessage(subscribeRequest("test://resource/1024"));
252+
253+
McpSchema.JSONRPCMessage sent = transport.getLastSentMessage();
254+
assertThat(sent).isInstanceOf(McpSchema.JSONRPCResponse.class);
255+
McpSchema.JSONRPCResponse response = (McpSchema.JSONRPCResponse) sent;
256+
assertThat(response.error()).isNotNull();
257+
assertThat(response.error().code()).isEqualTo(McpSchema.ErrorCodes.INVALID_PARAMS);
258+
259+
server.closeGracefully().block();
260+
}
261+
169262
}

0 commit comments

Comments
 (0)