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

View file

@ -33,6 +33,8 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.BiFunction; 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 * 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 * 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() { private McpStatelessRequestHandler<McpSchema.ReadResourceResult> resourcesReadRequestHandler() {
return (ctx, params) -> { return (ctx, params) -> {
McpSchema.ReadResourceRequest resourceRequest = jsonMapper.convertValue(params, McpSchema.ReadResourceRequest resourceRequest = jsonMapper.convertValue(params, new TypeRef<>() {
new TypeRef<McpSchema.ReadResourceRequest>() { });
});
var resourceUri = resourceRequest.uri(); var resourceUri = resourceRequest.uri();
return asyncResourceSpecification(resourceUri).map(spec -> spec.readHandler().apply(ctx, resourceRequest))
McpStatelessServerFeatures.AsyncResourceSpecification specification = this.resources.values() .orElseGet(() -> Mono.error(RESOURCE_NOT_FOUND.apply(resourceUri)));
.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);
}; };
} }
private Optional<McpStatelessServerFeatures.AsyncResourceSpecification> asyncResourceSpecification(String uri) {
return resources.values()
.stream()
.filter(spec -> uriTemplateManagerFactory.create(spec.resource().uri()).matches(uri))
.findFirst();
}
// --------------------------------------- // ---------------------------------------
// Prompt Management // Prompt Management
// --------------------------------------- // ---------------------------------------
@ -612,10 +612,10 @@ public class McpStatelessAsyncServer {
} }
if (type.equals("ref/resource") && request.ref() instanceof McpSchema.ResourceReference resourceReference) { if (type.equals("ref/resource") && request.ref() instanceof McpSchema.ResourceReference resourceReference) {
McpStatelessServerFeatures.AsyncResourceSpecification resourceSpec = this.resources McpStatelessServerFeatures.AsyncResourceSpecification resourceSpec = resources
.get(resourceReference.uri()); .get(resourceReference.uri());
if (resourceSpec == null) { 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()) if (!uriTemplateManagerFactory.create(resourceSpec.resource().uri())
.getVariableNames() .getVariableNames()

View file

@ -7,8 +7,19 @@ package io.modelcontextprotocol.spec;
import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse.JSONRPCError; import io.modelcontextprotocol.spec.McpSchema.JSONRPCResponse.JSONRPCError;
import io.modelcontextprotocol.util.Assert; import io.modelcontextprotocol.util.Assert;
import java.util.Map;
import java.util.function.Function;
public class McpError extends RuntimeException { 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; private JSONRPCError jsonRpcError;
public McpError(JSONRPCError jsonRpcError) { public McpError(JSONRPCError jsonRpcError) {

View file

@ -143,6 +143,11 @@ public final class McpSchema {
*/ */
public static final int INTERNAL_ERROR = -32603; public static final int INTERNAL_ERROR = -32603;
/**
* Resource not found.
*/
public static final int RESOURCE_NOT_FOUND = -32002;
} }
public sealed interface Request 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());
}
}