fix: handle resource not found according to spec (#567)

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
This commit is contained in:
Sergio del Amo 2025-09-22 16:43:44 +02:00 • committed by Christian Tzolov
commit e5bcb49e68
5 changed files with 68 additions and 30 deletions

View file

@ -48,6 +48,8 @@ import org.slf4j.LoggerFactory;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import static io.modelcontextprotocol.spec.McpError.RESOURCE_NOT_FOUND;
/**
* The Model Context Protocol (MCP) server implementation that provides asynchronous
* communication using Project Reactor's Mono and Flux types.
@ -638,24 +640,23 @@ public class McpAsyncServer {
}
private McpRequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
return (exchange, params) -> {
McpSchema.ReadResourceRequest resourceRequest = jsonMapper.convertValue(params,
new TypeRef<McpSchema.ReadResourceRequest>() {
});
return (ex, params) -> {
McpSchema.ReadResourceRequest resourceRequest = jsonMapper.convertValue(params, new TypeRef<>() {
});
var resourceUri = resourceRequest.uri();
McpServerFeatures.AsyncResourceSpecification specification = this.resources.values()
.stream()
.filter(resourceSpecification -> this.uriTemplateManagerFactory
.create(resourceSpecification.resource().uri())
.matches(resourceUri))
.findFirst()
.orElseThrow(() -> new McpError("Resource not found: " + resourceUri));
return Mono.defer(() -> specification.readHandler().apply(exchange, resourceRequest));
return asyncResourceSpecification(resourceUri)
.map(spec -> Mono.defer(() -> spec.readHandler().apply(ex, resourceRequest)))
.orElseGet(() -> Mono.error(RESOURCE_NOT_FOUND.apply(resourceUri)));
};
}
private Optional<McpServerFeatures.AsyncResourceSpecification> asyncResourceSpecification(String uri) {
return resources.values()
.stream()
.filter(spec -> uriTemplateManagerFactory.create(spec.resource().uri()).matches(uri))
.findFirst();
}
// ---------------------------------------
// Prompt Management
// ---------------------------------------
@ -846,7 +847,7 @@ public class McpAsyncServer {
if (type.equals("ref/resource") && request.ref() instanceof McpSchema.ResourceReference resourceReference) {
McpServerFeatures.AsyncResourceSpecification resourceSpec = this.resources.get(resourceReference.uri());
if (resourceSpec == null) {
return Mono.error(new McpError("Resource not found: " + resourceReference.uri()));
return Mono.error(RESOURCE_NOT_FOUND.apply(resourceReference.uri()));
}
if (!uriTemplateManagerFactory.create(resourceSpec.resource().uri())
.getVariableNames()

View file

@ -33,6 +33,8 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction;
import static io.modelcontextprotocol.spec.McpError.RESOURCE_NOT_FOUND;
/**
* A stateless MCP server implementation for use with Streamable HTTP transport types. It
* allows simple horizontal scalability since it does not maintain a session and does not
@ -478,23 +480,21 @@ public class McpStatelessAsyncServer {
private McpStatelessRequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
return (ctx, params) -> {
McpSchema.ReadResourceRequest resourceRequest = jsonMapper.convertValue(params,
new TypeRef<McpSchema.ReadResourceRequest>() {
});
McpSchema.ReadResourceRequest resourceRequest = jsonMapper.convertValue(params, new TypeRef<>() {
});
var resourceUri = resourceRequest.uri();
McpStatelessServerFeatures.AsyncResourceSpecification specification = this.resources.values()
.stream()
.filter(resourceSpecification -> this.uriTemplateManagerFactory
.create(resourceSpecification.resource().uri())
.matches(resourceUri))
.findFirst()
.orElseThrow(() -> new McpError("Resource not found: " + resourceUri));
return specification.readHandler().apply(ctx, resourceRequest);
return asyncResourceSpecification(resourceUri).map(spec -> spec.readHandler().apply(ctx, resourceRequest))
.orElseGet(() -> Mono.error(RESOURCE_NOT_FOUND.apply(resourceUri)));
};
}
private Optional<McpStatelessServerFeatures.AsyncResourceSpecification> asyncResourceSpecification(String uri) {
return resources.values()
.stream()
.filter(spec -> uriTemplateManagerFactory.create(spec.resource().uri()).matches(uri))
.findFirst();
}
// ---------------------------------------
// Prompt Management
// ---------------------------------------
@ -612,10 +612,10 @@ public class McpStatelessAsyncServer {
}
if (type.equals("ref/resource") && request.ref() instanceof McpSchema.ResourceReference resourceReference) {
McpStatelessServerFeatures.AsyncResourceSpecification resourceSpec = this.resources
McpStatelessServerFeatures.AsyncResourceSpecification resourceSpec = resources
.get(resourceReference.uri());
if (resourceSpec == null) {
return Mono.error(new McpError("Resource not found: " + resourceReference.uri()));
return Mono.error(RESOURCE_NOT_FOUND.apply(resourceReference.uri()));
}
if (!uriTemplateManagerFactory.create(resourceSpec.resource().uri())
.getVariableNames()

View file

@ -7,8 +7,19 @@ package io.modelcontextprotocol.spec;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse.JSONRPCError;
import io.modelcontextprotocol.util.Assert;
import java.util.Map;
import java.util.function.Function;
public class McpError extends RuntimeException {
/**
* <a href=
* "https://modelcontextprotocol.io/specification/2025-06-18/server/resources#error-handling">Resource
* Error Handling</a>
*/
public static final Function<String, McpError> RESOURCE_NOT_FOUND = resourceUri -> new McpError(new JSONRPCError(
McpSchema.ErrorCodes.RESOURCE_NOT_FOUND, "Resource not found", Map.of("uri", resourceUri)));
private JSONRPCError jsonRpcError;
public McpError(JSONRPCError jsonRpcError) {

View file

@ -143,6 +143,11 @@ public final class McpSchema {
*/
public static final int INTERNAL_ERROR = -32603;
/**
* Resource not found.
*/
public static final int RESOURCE_NOT_FOUND = -32002;
}
public sealed interface Request

View file

@ -0,0 +1,21 @@
package io.modelcontextprotocol.spec;
import org.junit.jupiter.api.Test;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.*;
class McpErrorTest {
@Test
void testNotFound() {
String uri = "file:///nonexistent.txt";
McpError mcpError = McpError.RESOURCE_NOT_FOUND.apply(uri);
assertNotNull(mcpError.getJsonRpcError());
assertEquals(-32002, mcpError.getJsonRpcError().code());
assertEquals("Resource not found", mcpError.getJsonRpcError().message());
assertEquals(Map.of("uri", uri), mcpError.getJsonRpcError().data());
}
}