Fix default handler return values to comply with MCP spec

The no-op handlers for `completion/complete`, `resources/subscribe`,
and `resources/unsubscribe` returned `nil`.
When a client accesses the expected response structure (e.g., `result[:completion][:values]`),
a `nil` result raises `NoMethodError`.

The MCP spec requires specific response structures even for no-op handlers.
Returning spec-compliant defaults ensures that clients receive valid responses without
requiring servers to explicitly override each handler.

`completion/complete` now returns `{ completion: { values: [], hasMore: false } }`.
ref: https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion

`resources/subscribe` and `resources/unsubscribe` now return `{}` (empty result).
ref: https://modelcontextprotocol.io/specification/2025-11-25/server/resources#subscriptions
This commit is contained in:
Koichi ITO 2026-03-01 00:55:31 +09:00
commit 7b910bca23
2 changed files with 84 additions and 3 deletions

View file

@ -99,9 +99,9 @@ module MCP
Methods::LOGGING_SET_LEVEL => method(:configure_logging_level),
# No op handlers for currently unsupported methods
Methods::RESOURCES_SUBSCRIBE => ->(_) {},
Methods::RESOURCES_UNSUBSCRIBE => ->(_) {},
Methods::COMPLETION_COMPLETE => ->(_) {},
Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
Methods::COMPLETION_COMPLETE => ->(_) { { completion: { values: [], hasMore: false } } },
Methods::ELICITATION_CREATE => ->(_) {},
}
@transport = transport

View file

@ -1540,6 +1540,87 @@ module MCP
assert_includes response[:result][:content][0][:text], "Invalid arguments"
end
test "#handle completion/complete returns default completion result" 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/prompt", name: "test" },
argument: { name: "arg", value: "val" },
},
})
assert_equal(
{
jsonrpc: "2.0",
id: 2,
result: { completion: { values: [], hasMore: false } },
},
response,
)
end
test "#handle resources/subscribe returns empty result" do
server = Server.new(
name: "test_server",
capabilities: { resources: { subscribe: true } },
)
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: "resources/subscribe",
params: { uri: "https://example.com/resource" },
})
assert_equal(
{
jsonrpc: "2.0",
id: 2,
result: {},
},
response,
)
end
test "#handle resources/unsubscribe returns empty result" do
server = Server.new(
name: "test_server",
capabilities: { resources: { subscribe: true } },
)
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: "resources/unsubscribe",
params: { uri: "https://example.com/resource" },
})
assert_equal(
{
jsonrpc: "2.0",
id: 2,
result: {},
},
response,
)
end
test "tools/call with no args" do
server = Server.new(tools: [@tool_with_no_args])