Skip to content

Commit 6f9beb2

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` functional style
1 parent a0afdcd commit 6f9beb2

File tree

5 files changed

+68
-30
lines changed

5 files changed

+68
-30
lines changed

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

Lines changed: 16 additions & 15 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.
@@ -638,24 +640,23 @@ private List<McpSchema.ResourceTemplate> getResourceTemplates() {
638640
}
639641

640642
private McpRequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
641-
return (exchange, params) -> {
642-
McpSchema.ReadResourceRequest resourceRequest = jsonMapper.convertValue(params,
643-
new TypeRef<McpSchema.ReadResourceRequest>() {
644-
});
643+
return (ex, params) -> {
644+
McpSchema.ReadResourceRequest resourceRequest = jsonMapper.convertValue(params, new TypeRef<>() {
645+
});
645646
var resourceUri = resourceRequest.uri();
646-
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));
647+
return asyncResourceSpecification(resourceUri)
648+
.map(spec -> Mono.defer(() -> spec.readHandler().apply(ex, resourceRequest)))
649+
.orElseGet(() -> Mono.error(RESOURCE_NOT_FOUND.apply(resourceUri)));
656650
};
657651
}
658652

653+
private Optional<McpServerFeatures.AsyncResourceSpecification> asyncResourceSpecification(String uri) {
654+
return resources.values()
655+
.stream()
656+
.filter(spec -> uriTemplateManagerFactory.create(spec.resource().uri()).matches(uri))
657+
.findFirst();
658+
}
659+
659660
// ---------------------------------------
660661
// Prompt Management
661662
// ---------------------------------------
@@ -846,7 +847,7 @@ private McpRequestHandler<McpSchema.CompleteResult> completionCompleteRequestHan
846847
if (type.equals("ref/resource") && request.ref() instanceof McpSchema.ResourceReference resourceReference) {
847848
McpServerFeatures.AsyncResourceSpecification resourceSpec = this.resources.get(resourceReference.uri());
848849
if (resourceSpec == null) {
849-
return Mono.error(new McpError("Resource not found: " + resourceReference.uri()));
850+
return Mono.error(RESOURCE_NOT_FOUND.apply(resourceReference.uri()));
850851
}
851852
if (!uriTemplateManagerFactory.create(resourceSpec.resource().uri())
852853
.getVariableNames()

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

Lines changed: 15 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,21 @@ 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 uri) {
492+
return resources.values()
493+
.stream()
494+
.filter(spec -> uriTemplateManagerFactory.create(spec.resource().uri()).matches(uri))
495+
.findFirst();
496+
}
497+
498498
// ---------------------------------------
499499
// Prompt Management
500500
// ---------------------------------------
@@ -612,10 +612,10 @@ private McpStatelessRequestHandler<McpSchema.CompleteResult> completionCompleteR
612612
}
613613

614614
if (type.equals("ref/resource") && request.ref() instanceof McpSchema.ResourceReference resourceReference) {
615-
McpStatelessServerFeatures.AsyncResourceSpecification resourceSpec = this.resources
615+
McpStatelessServerFeatures.AsyncResourceSpecification resourceSpec = resources
616616
.get(resourceReference.uri());
617617
if (resourceSpec == null) {
618-
return Mono.error(new McpError("Resource not found: " + resourceReference.uri()));
618+
return Mono.error(RESOURCE_NOT_FOUND.apply(resourceReference.uri()));
619619
}
620620
if (!uriTemplateManagerFactory.create(resourceSpec.resource().uri())
621621
.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)