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.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()
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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