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:
parent
1158c13278
commit
e5bcb49e68
5 changed files with 68 additions and 30 deletions
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in a new issue