Skip to content

Commit e9f252d

Browse files
committed
Return a handler from find in modern resource repositories
Align the resource repositories with the tool and prompt ones: find(ctx, uri) returns McpAsyncResource/McpSyncResource, carrying the read handler, in place of read(ctx, request). ResourcesFeature now answers an unknown URI with the -32602 error carrying {"uri": ...} itself, so repositories no longer build it. See #1268 Signed-off-by: Dariusz Jędrzejczyk <[email protected]>
1 parent 75b5306 commit e9f252d

9 files changed

Lines changed: 150 additions & 39 deletions

File tree

‎conformance-tests/server-servlet-modern/src/main/java/io/modelcontextprotocol/conformance/server/modern/ModernConformanceServlet.java‎

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@
3131
import io.modelcontextprotocol.modern.McpSchema.Prompt;
3232
import io.modelcontextprotocol.modern.McpSchema.PromptArgument;
3333
import io.modelcontextprotocol.modern.McpSchema.PromptMessage;
34-
import io.modelcontextprotocol.modern.McpSchema.ReadResourceOutcome;
35-
import io.modelcontextprotocol.modern.McpSchema.ReadResourceRequest;
3634
import io.modelcontextprotocol.modern.McpSchema.ReadResourceResult;
3735
import io.modelcontextprotocol.modern.McpSchema.Resource;
3836
import io.modelcontextprotocol.modern.McpSchema.Result;
@@ -52,6 +50,7 @@
5250
import io.modelcontextprotocol.modern.server.feature.McpChangeFeed;
5351
import io.modelcontextprotocol.modern.server.feature.McpSyncPrompt;
5452
import io.modelcontextprotocol.modern.server.feature.McpSyncPromptRepository;
53+
import io.modelcontextprotocol.modern.server.feature.McpSyncResource;
5554
import io.modelcontextprotocol.modern.server.feature.McpSyncResourceRepository;
5655
import io.modelcontextprotocol.modern.server.feature.McpSyncTool;
5756
import io.modelcontextprotocol.modern.server.feature.McpSyncToolRepository;
@@ -519,20 +518,20 @@ public ResourceTemplatesPage listTemplates(McpRequestContext ctx, String cursor)
519518
}
520519

521520
@Override
522-
public McpSyncResponse<ReadResourceOutcome> read(McpRequestContext ctx, ReadResourceRequest request) {
523-
String uri = request.uri();
524-
ReadResourceResult result = switch (uri) {
521+
public McpSyncResource find(McpRequestContext ctx, String uri) {
522+
ReadResourceResult result = read(uri);
523+
return result != null ? McpSyncResource.of((c, request) -> McpSyncResponse.result(result)) : null;
524+
}
525+
526+
private static ReadResourceResult read(String uri) {
527+
return switch (uri) {
525528
case "test://static-text" -> textResource(uri, "This is the content of the static text resource.");
526529
case "test://watched-resource" -> textResource(uri, "This is a watched resource content.");
527530
case "test://static-binary" -> ReadResourceResult
528531
.builder(List.of(BlobResourceContents.builder(uri, RED_PIXEL_PNG).mimeType("image/png").build()))
529532
.build();
530533
default -> templateResource(uri);
531534
};
532-
if (result == null) {
533-
throw McpException.invalidParams("Unknown resource: " + uri, Map.of("uri", uri));
534-
}
535-
return McpSyncResponse.result(result);
536535
}
537536

538537
@Override
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.modern.server.feature;
6+
7+
import io.modelcontextprotocol.modern.McpSchema.ReadResourceOutcome;
8+
import io.modelcontextprotocol.modern.McpSchema.ReadResourceRequest;
9+
import io.modelcontextprotocol.modern.server.McpAsyncResponse;
10+
import io.modelcontextprotocol.modern.server.McpRequestContext;
11+
import io.modelcontextprotocol.util.Assert;
12+
import reactor.core.publisher.Mono;
13+
14+
/**
15+
* A readable resource: the handler that answers {@code resources/read} for it.
16+
*
17+
* @author Dariusz Jędrzejczyk
18+
*/
19+
public final class McpAsyncResource {
20+
21+
private final Handler handler;
22+
23+
private McpAsyncResource(Handler handler) {
24+
Assert.notNull(handler, "handler must not be null");
25+
this.handler = handler;
26+
}
27+
28+
public static McpAsyncResource of(Handler handler) {
29+
return new McpAsyncResource(handler);
30+
}
31+
32+
public Handler handler() {
33+
return this.handler;
34+
}
35+
36+
/** Answers a {@code resources/read} for this resource. */
37+
@FunctionalInterface
38+
public interface Handler {
39+
40+
Mono<McpAsyncResponse<ReadResourceOutcome>> read(McpRequestContext ctx, ReadResourceRequest request);
41+
42+
}
43+
44+
}

‎mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpAsyncResourceRepository.java‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,7 @@
77
import java.util.List;
88

99
import io.modelcontextprotocol.modern.McpException;
10-
import io.modelcontextprotocol.modern.McpSchema.ReadResourceOutcome;
11-
import io.modelcontextprotocol.modern.McpSchema.ReadResourceRequest;
1210
import io.modelcontextprotocol.modern.server.McpRequestContext;
13-
import io.modelcontextprotocol.modern.server.McpAsyncResponse;
1411
import reactor.core.publisher.Mono;
1512

1613
/**
@@ -32,11 +29,10 @@ default Mono<ResourceTemplatesPage> listTemplates(McpRequestContext ctx, String
3229
}
3330

3431
/**
35-
* Answer a {@code resources/read}. An unknown resource is an
36-
* {@link McpException#invalidParams(String, Object) invalid-params error} carrying
37-
* {@code {"uri": ...}} as data.
32+
* The resource at this URI, whether listed or matching a template, or empty if there
33+
* is none.
3834
*/
39-
Mono<McpAsyncResponse<ReadResourceOutcome>> read(McpRequestContext ctx, ReadResourceRequest request);
35+
Mono<McpAsyncResource> find(McpRequestContext ctx, String uri);
4036

4137
/**
4238
* Whether {@code resources/updated} is reported for individual resources, advertised
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2026-2026 the original author or authors.
3+
*/
4+
5+
package io.modelcontextprotocol.modern.server.feature;
6+
7+
import io.modelcontextprotocol.modern.McpSchema.ReadResourceOutcome;
8+
import io.modelcontextprotocol.modern.McpSchema.ReadResourceRequest;
9+
import io.modelcontextprotocol.modern.server.McpRequestContext;
10+
import io.modelcontextprotocol.modern.server.McpSyncResponse;
11+
import io.modelcontextprotocol.util.Assert;
12+
13+
/**
14+
* The blocking counterpart of {@link McpAsyncResource}.
15+
*
16+
* @author Dariusz Jędrzejczyk
17+
*/
18+
public final class McpSyncResource {
19+
20+
private final Handler handler;
21+
22+
private McpSyncResource(Handler handler) {
23+
Assert.notNull(handler, "handler must not be null");
24+
this.handler = handler;
25+
}
26+
27+
public static McpSyncResource of(Handler handler) {
28+
return new McpSyncResource(handler);
29+
}
30+
31+
public Handler handler() {
32+
return this.handler;
33+
}
34+
35+
/** See {@link McpAsyncResource.Handler}. */
36+
@FunctionalInterface
37+
public interface Handler {
38+
39+
McpSyncResponse<ReadResourceOutcome> read(McpRequestContext ctx, ReadResourceRequest request);
40+
41+
}
42+
43+
}

‎mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/McpSyncResourceRepository.java‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66

77
import java.util.List;
88

9-
import io.modelcontextprotocol.modern.McpSchema.ReadResourceOutcome;
10-
import io.modelcontextprotocol.modern.McpSchema.ReadResourceRequest;
119
import io.modelcontextprotocol.modern.server.McpRequestContext;
12-
import io.modelcontextprotocol.modern.server.McpSyncResponse;
1310

1411
/**
1512
* The blocking counterpart of {@link McpAsyncResourceRepository}.
@@ -24,7 +21,8 @@ default ResourceTemplatesPage listTemplates(McpRequestContext ctx, String cursor
2421
return ResourceTemplatesPage.of(List.of());
2522
}
2623

27-
McpSyncResponse<ReadResourceOutcome> read(McpRequestContext ctx, ReadResourceRequest request);
24+
/** See {@link McpAsyncResourceRepository#find}. */
25+
McpSyncResource find(McpRequestContext ctx, String uri);
2826

2927
/** See {@link McpAsyncResourceRepository#supportsSubscribe()}. */
3028
default boolean supportsSubscribe() {

‎mcp-core/src/main/java/io/modelcontextprotocol/modern/server/feature/ResourcesFeature.java‎

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44

55
package io.modelcontextprotocol.modern.server.feature;
66

7+
import java.util.Map;
78
import java.util.Set;
89

910
import io.modelcontextprotocol.json.McpJsonDefaults;
1011
import io.modelcontextprotocol.json.McpJsonMapper;
12+
import io.modelcontextprotocol.modern.McpException;
1113
import io.modelcontextprotocol.modern.McpSchema;
1214
import io.modelcontextprotocol.modern.McpSchema.CacheScope;
1315
import io.modelcontextprotocol.modern.McpSchema.ListResourceTemplatesResult;
@@ -81,7 +83,10 @@ public Set<String> methods() {
8183
public Mono<? extends McpAsyncResponse<? extends Result>> handle(McpRequestContext ctx, Object params) {
8284
if (McpSchema.METHOD_RESOURCES_READ.equals(ctx.method())) {
8385
return Params.decode(this.jsonMapper, params, ReadResourceRequest.class)
84-
.flatMap(request -> this.repository.read(ctx, request));
86+
.flatMap(request -> this.repository.find(ctx, request.uri())
87+
.switchIfEmpty(Mono.error(() -> McpException.invalidParams("Unknown resource: " + request.uri(),
88+
Map.of("uri", request.uri()))))
89+
.flatMap(resource -> resource.handler().read(ctx, request)));
8590
}
8691
Mono<PaginatedRequest> request = Params.decode(this.jsonMapper, params, PaginatedRequest.class);
8792
if (McpSchema.METHOD_RESOURCES_LIST.equals(ctx.method())) {
@@ -131,9 +136,8 @@ public Mono<ResourceTemplatesPage> listTemplates(McpRequestContext ctx, String c
131136
}
132137

133138
@Override
134-
public Mono<McpAsyncResponse<ReadResourceOutcome>> read(McpRequestContext ctx,
135-
ReadResourceRequest request) {
136-
return SyncAdapters.respond(ctx, () -> repository.read(ctx, request));
139+
public Mono<McpAsyncResource> find(McpRequestContext ctx, String uri) {
140+
return SyncAdapters.call(ctx, () -> repository.find(ctx, uri)).map(ResourcesFeature::adapt);
137141
}
138142

139143
@Override
@@ -143,4 +147,9 @@ public boolean supportsSubscribe() {
143147
};
144148
}
145149

150+
private static McpAsyncResource adapt(McpSyncResource resource) {
151+
return McpAsyncResource
152+
.of((ctx, request) -> SyncAdapters.respond(ctx, () -> resource.handler().read(ctx, request)));
153+
}
154+
146155
}

‎mcp-test/src/test/java/io/modelcontextprotocol/modern/server/McpServerMrtrTests.java‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import io.modelcontextprotocol.modern.McpSchema.Result;
2727
import io.modelcontextprotocol.modern.McpSchema.TextContent;
2828
import io.modelcontextprotocol.modern.McpSchema.TextResourceContents;
29+
import io.modelcontextprotocol.modern.server.feature.McpAsyncResource;
2930
import io.modelcontextprotocol.modern.server.feature.McpAsyncResourceRepository;
3031
import io.modelcontextprotocol.modern.server.feature.ResourcesFeature;
3132
import io.modelcontextprotocol.modern.server.feature.ResourcesPage;
@@ -179,7 +180,11 @@ public Mono<ResourcesPage> list(McpRequestContext ctx, String cursor) {
179180
}
180181

181182
@Override
182-
public Mono<McpAsyncResponse<McpSchema.ReadResourceOutcome>> read(McpRequestContext ctx,
183+
public Mono<McpAsyncResource> find(McpRequestContext ctx, String uri) {
184+
return Mono.just(McpAsyncResource.of(this::read));
185+
}
186+
187+
private Mono<McpAsyncResponse<McpSchema.ReadResourceOutcome>> read(McpRequestContext ctx,
183188
McpSchema.ReadResourceRequest request) {
184189
return Mono.just(McpAsyncResponse.result(ReadResourceResult
185190
.builder(List.of(new TextResourceContents(request.uri(), "text/plain", "content", null)))
@@ -215,7 +220,11 @@ public Mono<ResourcesPage> list(McpRequestContext ctx, String cursor) {
215220
}
216221

217222
@Override
218-
public Mono<McpAsyncResponse<McpSchema.ReadResourceOutcome>> read(McpRequestContext ctx,
223+
public Mono<McpAsyncResource> find(McpRequestContext ctx, String uri) {
224+
return Mono.just(McpAsyncResource.of(this::read));
225+
}
226+
227+
private Mono<McpAsyncResponse<McpSchema.ReadResourceOutcome>> read(McpRequestContext ctx,
219228
McpSchema.ReadResourceRequest request) {
220229
if (request.requestState() != null) {
221230
seenRequestState.set(request.requestState());

‎mcp-test/src/test/java/io/modelcontextprotocol/modern/server/SubscriptionsFeatureTests.java‎

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@
1313
import io.modelcontextprotocol.modern.JsonRpc.JSONRPCNotification;
1414
import io.modelcontextprotocol.modern.JsonRpc.JSONRPCRequest;
1515
import io.modelcontextprotocol.modern.JsonRpc.JSONRPCResponse;
16-
import io.modelcontextprotocol.modern.McpSchema;
1716
import io.modelcontextprotocol.modern.McpSchema.CacheScope;
1817
import io.modelcontextprotocol.modern.McpSchema.MetaKeys;
19-
import io.modelcontextprotocol.modern.McpSchema.ReadResourceOutcome;
20-
import io.modelcontextprotocol.modern.McpSchema.ReadResourceRequest;
2118
import io.modelcontextprotocol.modern.McpSchema.SubscriptionFilter;
2219
import io.modelcontextprotocol.modern.McpSchema.SubscriptionsAcknowledgedParams;
20+
import io.modelcontextprotocol.modern.McpSchema;
21+
import io.modelcontextprotocol.modern.server.feature.McpAsyncResource;
2322
import io.modelcontextprotocol.modern.server.feature.McpAsyncResourceRepository;
2423
import io.modelcontextprotocol.modern.server.feature.McpChangeFeed;
2524
import io.modelcontextprotocol.modern.server.feature.McpChangePublisher;
@@ -213,8 +212,7 @@ public Mono<ResourcesPage> list(McpRequestContext ctx, String cursor) {
213212
}
214213

215214
@Override
216-
public Mono<McpAsyncResponse<ReadResourceOutcome>> read(McpRequestContext ctx,
217-
ReadResourceRequest request) {
215+
public Mono<McpAsyncResource> find(McpRequestContext ctx, String uri) {
218216
return Mono.empty();
219217
}
220218

‎mcp-test/src/test/java/io/modelcontextprotocol/modern/server/feature/ResourcesFeatureTests.java‎

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99

1010
import io.modelcontextprotocol.json.McpJsonDefaults;
1111
import io.modelcontextprotocol.modern.JsonRpc.JSONRPCRequest;
12+
import io.modelcontextprotocol.modern.JsonRpc.JSONRPCResponse;
1213
import io.modelcontextprotocol.modern.McpException;
13-
import io.modelcontextprotocol.modern.McpSchema;
1414
import io.modelcontextprotocol.modern.McpSchema.ErrorCodes;
1515
import io.modelcontextprotocol.modern.McpSchema.ReadResourceOutcome;
16-
import io.modelcontextprotocol.modern.McpSchema.ReadResourceRequest;
17-
import io.modelcontextprotocol.modern.server.McpTransportResponse;
18-
import io.modelcontextprotocol.modern.server.McpRequestContext;
16+
import io.modelcontextprotocol.modern.McpSchema;
1917
import io.modelcontextprotocol.modern.server.McpAsyncResponse;
18+
import io.modelcontextprotocol.modern.server.McpRequestContext;
2019
import io.modelcontextprotocol.modern.server.McpServer;
20+
import io.modelcontextprotocol.modern.server.McpTransportResponse;
2121
import org.junit.jupiter.api.Test;
2222
import reactor.core.publisher.Mono;
2323
import reactor.test.StepVerifier;
@@ -38,24 +38,39 @@ private static McpServer server(McpAsyncResourceRepository repo) {
3838
}
3939

4040
private static McpAsyncResourceRepository reading(Mono<McpAsyncResponse<ReadResourceOutcome>> response) {
41+
return finding(Mono.just(McpAsyncResource.of((ctx, request) -> response)));
42+
}
43+
44+
private static McpAsyncResourceRepository finding(Mono<McpAsyncResource> resource) {
4145
return new McpAsyncResourceRepository() {
4246
@Override
4347
public Mono<ResourcesPage> list(McpRequestContext ctx, String cursor) {
4448
return Mono.just(ResourcesPage.of(List.of()));
4549
}
4650

4751
@Override
48-
public Mono<McpAsyncResponse<ReadResourceOutcome>> read(McpRequestContext ctx,
49-
ReadResourceRequest request) {
50-
return response;
52+
public Mono<McpAsyncResource> find(McpRequestContext ctx, String uri) {
53+
return resource;
5154
}
5255
};
5356
}
5457

58+
@Test
59+
void unknownResourceIsInvalidParamsCarryingItsUri() {
60+
McpServer server = server(finding(Mono.empty()));
61+
JSONRPCRequest request = new JSONRPCRequest(McpSchema.METHOD_RESOURCES_READ, 1,
62+
Map.of("_meta", meta(), "uri", "test://missing"));
63+
64+
JSONRPCResponse response = respond(server, request).block();
65+
66+
assertThat(response.error().code()).isEqualTo(ErrorCodes.INVALID_PARAMS);
67+
assertThat(response.error().data()).isEqualTo(Map.of("uri", "test://missing"));
68+
}
69+
5570
@Test
5671
void readFailureIsAnsweredWithItsError() {
5772
McpServer server = server(
58-
reading(Mono.error(McpException.invalidParams("Unknown resource", Map.of("uri", "test://missing")))));
73+
reading(Mono.error(McpException.invalidParams("Bad resource", Map.of("uri", "test://missing")))));
5974
JSONRPCRequest request = new JSONRPCRequest(McpSchema.METHOD_RESOURCES_READ, 1,
6075
Map.of("_meta", meta(), "uri", "test://missing"));
6176

0 commit comments

Comments
 (0)