Support completion/complete per MCP specification

## Motivation and Context

The MCP specification defines `completion/complete` for providing autocompletion
suggestions for prompt arguments and resource template URIs.
The Ruby SDK previously had only a no-op handler that returned empty results,
with no way for users to define custom completion logic.

This aligns the Ruby SDK with the Python and TypeScript SDKs,
both of which support user-defined completion handlers.

## How Has This Been Tested?

Server tests cover: default handler, custom handler for `ref/prompt` and `ref/resource`,
context argument passing, 100-item truncation, and error responses for nonexistent prompts,
nonexistent resource templates, and invalid ref types.

Client tests cover: request structure, context parameter inclusion,
and fallback when result is missing.

## Breaking Changes

None. The existing default no-op handler behavior is preserved.
The `completion_handler` method is purely additive. The only behavioral change
is that `completion/complete` requests now validate that the referenced prompt
or resource template exists before calling the handler,
returning an `invalid_params` error for unknown references.
This commit is contained in:
Koichi ITO 2026-04-01 01:03:23 +09:00
commit 88e1f9a840

View file

@ -456,5 +456,86 @@ module MCP
error = assert_raises(Client::ServerError) { client.tools }
assert_equal("extra details", error.data)
end
def test_complete_raises_server_error_on_error_response
transport = mock
mock_response = { "error" => { "code" => -32_602, "message" => "Invalid params" } }
transport.expects(:send_request).returns(mock_response).once
client = Client.new(transport: transport)
error = assert_raises(Client::ServerError) { client.complete(ref: { type: "ref/prompt", name: "missing" }, argument: { name: "arg", value: "" }) }
assert_equal(-32_602, error.code)
end
def test_complete_sends_request_and_returns_completion_result
transport = mock
mock_response = {
"result" => {
"completion" => {
"values" => ["python", "pytorch"],
"hasMore" => false,
},
},
}
transport.expects(:send_request).with do |args|
args.dig(:request, :method) == "completion/complete" &&
args.dig(:request, :jsonrpc) == "2.0" &&
args.dig(:request, :params, :ref) == { type: "ref/prompt", name: "code_review" } &&
args.dig(:request, :params, :argument) == { name: "language", value: "py" } &&
!args.dig(:request, :params).key?(:context)
end.returns(mock_response).once
client = Client.new(transport: transport)
result = client.complete(
ref: { type: "ref/prompt", name: "code_review" },
argument: { name: "language", value: "py" },
)
assert_equal(["python", "pytorch"], result["values"])
refute(result["hasMore"])
end
def test_complete_includes_context_when_provided
transport = mock
mock_response = {
"result" => {
"completion" => {
"values" => ["flask"],
"hasMore" => false,
},
},
}
transport.expects(:send_request).with do |args|
args.dig(:request, :params, :context) == { arguments: { language: "python" } }
end.returns(mock_response).once
client = Client.new(transport: transport)
result = client.complete(
ref: { type: "ref/prompt", name: "code_review" },
argument: { name: "framework", value: "fla" },
context: { arguments: { language: "python" } },
)
assert_equal(["flask"], result["values"])
end
def test_complete_returns_default_when_result_is_missing
transport = mock
mock_response = { "result" => {} }
transport.expects(:send_request).returns(mock_response).once
client = Client.new(transport: transport)
result = client.complete(
ref: { type: "ref/prompt", name: "test" },
argument: { name: "arg", value: "" },
)
assert_equal([], result["values"])
refute(result["hasMore"])
end
end
end