Skip to content

Commit 90e1af1

Browse files
committed
fix: handle resource not found according to spec
see: https://modelcontextprotocol.io/specification/2025-06-18/server/resources#error-handling Resource not found should send a JSON RPC response such as: ```json { "jsonrpc": "2.0", "id": 5, "error": { "code": -32002, "message": "Resource not found", "data": { "uri": "file:///nonexistent.txt" } } } ``` This PR also changes some instances where a `McpError` was thrown instead of being passed in the reactive chain with `Mono.error`
1 parent a0afdcd commit 90e1af1

File tree

5 files changed

+71
-25
lines changed

5 files changed

+71
-25
lines changed

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

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@
4848
import reactor.core.publisher.Flux;
4949
import reactor.core.publisher.Mono;
5050

51+
import static io.modelcontextprotocol.spec.McpError.RESOURCE_NOT_FOUND;
52+
5153
/**
5254
* The Model Context Protocol (MCP) server implementation that provides asynchronous
5355
* communication using Project Reactor's Mono and Flux types.
@@ -644,18 +646,23 @@ private McpRequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHand
644646
});
645647
var resourceUri = resourceRequest.uri();
646648

647-
McpServerFeatures.AsyncResourceSpecification specification = this.resources.values()
648-
.stream()
649-
.filter(resourceSpecification -> this.uriTemplateManagerFactory
650-
.create(resourceSpecification.resource().uri())
651-
.matches(resourceUri))
652-
.findFirst()
653-
.orElseThrow(() -> new McpError("Resource not found: " + resourceUri));
654-
655-
return Mono.defer(() -> specification.readHandler().apply(exchange, resourceRequest));
649+
Optional<McpServerFeatures.AsyncResourceSpecification> specification = asyncResourceSpecification(
650+
resourceUri);
651+
if (specification.isPresent()) {
652+
return Mono.error(RESOURCE_NOT_FOUND.apply(resourceUri));
653+
}
654+
return Mono.defer(() -> specification.get().readHandler().apply(exchange, resourceRequest));
656655
};
657656
}
658657

658+
private Optional<McpServerFeatures.AsyncResourceSpecification> asyncResourceSpecification(String resourceUri) {
659+
return resources.values()
660+
.stream()
661+
.filter(resourceSpecification -> uriTemplateManagerFactory.create(resourceSpecification.resource().uri())
662+
.matches(resourceUri))
663+
.findFirst();
664+
}
665+
659666
// ---------------------------------------
660667
// Prompt Management
661668
// ---------------------------------------
@@ -846,7 +853,7 @@ private McpRequestHandler<McpSchema.CompleteResult> completionCompleteRequestHan
846853
if (type.equals("ref/resource") && request.ref() instanceof McpSchema.ResourceReference resourceReference) {
847854
McpServerFeatures.AsyncResourceSpecification resourceSpec = this.resources.get(resourceReference.uri());
848855
if (resourceSpec == null) {
849-
return Mono.error(new McpError("Resource not found: " + resourceReference.uri()));
856+
return Mono.error(RESOURCE_NOT_FOUND.apply(resourceReference.uri()));
850857
}
851858
if (!uriTemplateManagerFactory.create(resourceSpec.resource().uri())
852859
.getVariableNames()

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

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
import java.util.concurrent.CopyOnWriteArrayList;
3434
import java.util.function.BiFunction;
3535

36+
import static io.modelcontextprotocol.spec.McpError.RESOURCE_NOT_FOUND;
37+
3638
/**
3739
* A stateless MCP server implementation for use with Streamable HTTP transport types. It
3840
* allows simple horizontal scalability since it does not maintain a session and does not
@@ -478,23 +480,23 @@ private List<ResourceTemplate> getResourceTemplates() {
478480

479481
private McpStatelessRequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
480482
return (ctx, params) -> {
481-
McpSchema.ReadResourceRequest resourceRequest = jsonMapper.convertValue(params,
482-
new TypeRef<McpSchema.ReadResourceRequest>() {
483-
});
483+
McpSchema.ReadResourceRequest resourceRequest = jsonMapper.convertValue(params, new TypeRef<>() {
484+
});
484485
var resourceUri = resourceRequest.uri();
485-
486-
McpStatelessServerFeatures.AsyncResourceSpecification specification = this.resources.values()
487-
.stream()
488-
.filter(resourceSpecification -> this.uriTemplateManagerFactory
489-
.create(resourceSpecification.resource().uri())
490-
.matches(resourceUri))
491-
.findFirst()
492-
.orElseThrow(() -> new McpError("Resource not found: " + resourceUri));
493-
494-
return specification.readHandler().apply(ctx, resourceRequest);
486+
return asyncResourceSpecification(resourceUri).map(spec -> spec.readHandler().apply(ctx, resourceRequest))
487+
.orElseGet(() -> Mono.error(RESOURCE_NOT_FOUND.apply(resourceUri)));
495488
};
496489
}
497490

491+
private Optional<McpStatelessServerFeatures.AsyncResourceSpecification> asyncResourceSpecification(String resourceUri) {
492+
return resources.values()
493+
.stream()
494+
.filter(resourceSpecification -> this.uriTemplateManagerFactory
495+
.create(resourceSpecification.resource().uri())
496+
.matches(resourceUri))
497+
.findFirst();
498+
}
499+
498500
// ---------------------------------------
499501
// Prompt Management
500502
// ---------------------------------------
@@ -612,10 +614,10 @@ private McpStatelessRequestHandler<McpSchema.CompleteResult> completionCompleteR
612614
}
613615

614616
if (type.equals("ref/resource") && request.ref() instanceof McpSchema.ResourceReference resourceReference) {
615-
McpStatelessServerFeatures.AsyncResourceSpecification resourceSpec = this.resources
617+
McpStatelessServerFeatures.AsyncResourceSpecification resourceSpec = resources
616618
.get(resourceReference.uri());
617619
if (resourceSpec == null) {
618-
return Mono.error(new McpError("Resource not found: " + resourceReference.uri()));
620+
return Mono.error(RESOURCE_NOT_FOUND.apply(resourceReference.uri()));
619621
}
620622
if (!uriTemplateManagerFactory.create(resourceSpec.resource().uri())
621623
.getVariableNames()

mcp-core/src/main/java/io/modelcontextprotocol/spec/McpError.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,19 @@
77
import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse.JSONRPCError;
88
import io.modelcontextprotocol.util.Assert;
99

10+
import java.util.Map;
11+
import java.util.function.Function;
12+
1013
public class McpError extends RuntimeException {
1114

15+
/**
16+
* <a href=
17+
* "https://modelcontextprotocol.io/specification/2025-06-18/server/resources#error-handling">Resource
18+
* Error Handling</a>
19+
*/
20+
public static final Function<String, McpError> RESOURCE_NOT_FOUND = resourceUri -> new McpError(new JSONRPCError(
21+
McpSchema.ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found", Map.of("uri", resourceUri)));
22+
1223
private JSONRPCError jsonRpcError;
1324

1425
public McpError(JSONRPCError jsonRpcError) {

mcp-core/src/main/java/io/modelcontextprotocol/spec/McpSchema.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,11 @@ public static final class ErrorCodes {
143143
*/
144144
public static final int INTERNAL_ERROR = -32603;
145145

146+
/**
147+
* Resource not found.
148+
*/
149+
public static final int RESOURCE_NOT_FOUND = -32002;
150+
146151
}
147152

148153
public sealed interface Request
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package io.modelcontextprotocol.spec;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.Map;
6+
7+
import static org.junit.jupiter.api.Assertions.*;
8+
9+
class McpErrorTest {
10+
11+
@Test
12+
void testNotFound() {
13+
String uri = "file:///nonexistent.txt";
14+
McpError mcpError = McpError.RESOURCE_NOT_FOUND.apply(uri);
15+
assertNotNull(mcpError.getJsonRpcError());
16+
assertEquals(-32002, mcpError.getJsonRpcError().code());
17+
assertEquals("Resource not found", mcpError.getJsonRpcError().message());
18+
assertEquals(Map.of("uri", uri), mcpError.getJsonRpcError().data());
19+
}
20+
21+
}

0 commit comments

Comments
 (0)