Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -921,6 +921,19 @@ end

otherwise `resources/read` requests will be a no-op.

For unknown URIs, raise `MCP::Server::ResourceNotFoundError` from the handler.
Per SEP-2164, the server then responds with the standard JSON-RPC Invalid Params error (`-32602`)
carrying the requested URI in the error `data` member:

```ruby
server.resources_read_handler do |params|
resource = lookup(params[:uri])
raise MCP::Server::ResourceNotFoundError.new(params[:uri], params) unless resource

[{ uri: params[:uri], mimeType: resource.mime_type, text: resource.body }]
end
```

### Resource Templates

The `MCP::ResourceTemplate` class provides a way to register resource templates with the server.
Expand Down
28 changes: 27 additions & 1 deletion lib/mcp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,32 @@ def initialize(elicitations)
end
end

# Raised when a requested resource URI does not exist. Per SEP-2164,
# resource-not-found errors use the standard JSON-RPC Invalid Params code (-32602)
# with the requested URI in the error `data` member. Raise this from
# a `resources_read_handler` block for unknown URIs:
#
# server.resources_read_handler do |params|
# raise MCP::Server::ResourceNotFoundError.new(params[:uri], params) unless known?(params[:uri])
# do_something(params[:uri])
# end
#
# https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2164
class ResourceNotFoundError < RequestHandlerError
def initialize(uri, request = nil)
# The explicit `error_code` keeps the descriptive message in the JSON-RPC
# error response; `error_type: :invalid_params` alone would replace it
# with the generic "Invalid params" string.
super(
"Resource not found: #{uri}",
request,
error_type: :invalid_params,
error_code: JsonRpcHandler::ErrorCode::INVALID_PARAMS,
error_data: { uri: uri },
)
end
end

class MethodAlreadyDefinedError < StandardError
attr_reader :method_name

Expand Down Expand Up @@ -846,7 +872,7 @@ def validate_completion_params!(params)
uri = ref[:uri]
found = @resource_index.key?(uri) || @resource_templates.any? { |t| t.uri_template == uri }
unless found
raise RequestHandlerError.new("Resource not found: #{uri}", params, error_type: :invalid_params)
raise ResourceNotFoundError.new(uri, params)
end
else
raise RequestHandlerError.new("Invalid ref type: #{ref[:type]}", params, error_type: :invalid_params)
Expand Down
21 changes: 21 additions & 0 deletions test/mcp/client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -610,6 +610,27 @@ def test_read_resource_raises_server_error_on_error_response
assert_raises(Client::ServerError) { client.read_resource(uri: "file:///missing") }
end

def test_read_resource_surfaces_resource_not_found_code_and_data
# Per SEP-2164, servers report unknown resource URIs with -32602 and
# the URI in the error `data`; the client must expose both unmodified.
transport = mock
mock_response = {
"error" => {
"code" => -32_602,
"message" => "Resource not found: file:///missing.txt",
"data" => { "uri" => "file:///missing.txt" },
},
}

transport.expects(:send_request).returns(mock_response).once

client = Client.new(transport: transport)
error = assert_raises(Client::ServerError) { client.read_resource(uri: "file:///missing.txt") }

assert_equal(-32_602, error.code)
assert_equal({ "uri" => "file:///missing.txt" }, error.data)
end

def test_get_prompt_raises_server_error_on_error_response
transport = mock
mock_response = { "error" => { "code" => -32_602, "message" => "Prompt not found" } }
Expand Down
43 changes: 43 additions & 0 deletions test/mcp/server_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -724,6 +724,25 @@ class Example < Tool
)
end

test "#handle resources/read returns -32602 with the uri in error data when the handler raises ResourceNotFoundError" do
# Per SEP-2164, resource-not-found errors use the standard JSON-RPC Invalid Params code (-32602)
# and carry the requested URI in `data`.
@server.resources_read_handler do |request|
raise Server::ResourceNotFoundError.new(request[:uri], request)
end

response = @server.handle({
jsonrpc: "2.0",
method: "resources/read",
id: 1,
params: { uri: "file:///missing.txt" },
})

assert_equal(-32602, response[:error][:code])
assert_equal("Resource not found: file:///missing.txt", response[:error][:message])
assert_equal({ uri: "file:///missing.txt" }, response[:error][:data])
end

test "#handle resources/templates/list returns a list of resource templates" do
request = {
jsonrpc: "2.0",
Expand Down Expand Up @@ -2206,6 +2225,30 @@ class Example < Tool
assert_equal(-32_602, response[:error][:code])
end

test "#handle completion/complete resource-not-found error carries the uri in error data" do
server = Server.new(
name: "test_server",
capabilities: { completions: {} },
)

server.handle({ jsonrpc: "2.0", method: "initialize", id: 1 })
server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

response = server.handle({
jsonrpc: "2.0",
id: 2,
method: "completion/complete",
params: {
ref: { type: "ref/resource", uri: "unknown://template" },
argument: { name: "arg", value: "val" },
},
})

assert_equal(-32602, response[:error][:code])
assert_equal("Resource not found: unknown://template", response[:error][:message])
assert_equal({ uri: "unknown://template" }, response[:error][:data])
end

test "#handle completion/complete returns error for invalid ref type" do
server = Server.new(
name: "test_server",
Expand Down