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:
parent
6669601b5d
commit
88e1f9a840
6 changed files with 681 additions and 2 deletions
50
README.md
50
README.md
|
|
@ -50,6 +50,7 @@ It implements the Model Context Protocol specification, handling model context r
|
|||
- `resources/list` - Lists all registered resources and their schemas
|
||||
- `resources/read` - Retrieves a specific resource by name
|
||||
- `resources/templates/list` - Lists all registered resource templates and their schemas
|
||||
- `completion/complete` - Returns autocompletion suggestions for prompt arguments and resource URIs
|
||||
|
||||
### Custom Methods
|
||||
|
||||
|
|
@ -183,6 +184,53 @@ The `server_context.report_progress` method accepts:
|
|||
- `report_progress` is a no-op when no `progressToken` was provided by the client
|
||||
- Supports both numeric and string progress tokens
|
||||
|
||||
### Completions
|
||||
|
||||
MCP spec includes [Completions](https://modelcontextprotocol.io/specification/latest/server/utilities/completion),
|
||||
which enable servers to provide autocompletion suggestions for prompt arguments and resource URIs.
|
||||
|
||||
To enable completions, declare the `completions` capability and register a handler:
|
||||
|
||||
```ruby
|
||||
server = MCP::Server.new(
|
||||
name: "my_server",
|
||||
prompts: [CodeReviewPrompt],
|
||||
resource_templates: [FileTemplate],
|
||||
capabilities: { completions: {} },
|
||||
)
|
||||
|
||||
server.completion_handler do |params|
|
||||
ref = params[:ref]
|
||||
argument = params[:argument]
|
||||
value = argument[:value]
|
||||
|
||||
case ref[:type]
|
||||
when "ref/prompt"
|
||||
values = case argument[:name]
|
||||
when "language"
|
||||
["python", "pytorch", "pyside"].select { |v| v.start_with?(value) }
|
||||
else
|
||||
[]
|
||||
end
|
||||
{ completion: { values: values, hasMore: false } }
|
||||
when "ref/resource"
|
||||
{ completion: { values: [], hasMore: false } }
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The handler receives a `params` hash with:
|
||||
|
||||
- `ref` - The reference (`{ type: "ref/prompt", name: "..." }` or `{ type: "ref/resource", uri: "..." }`)
|
||||
- `argument` - The argument being completed (`{ name: "...", value: "..." }`)
|
||||
- `context` (optional) - Previously resolved arguments (`{ arguments: { ... } }`)
|
||||
|
||||
The handler must return a hash with a `completion` key containing `values` (array of strings), and optionally `total` and `hasMore`.
|
||||
The SDK automatically enforces the 100-item limit per the MCP specification.
|
||||
|
||||
The server validates that the referenced prompt, resource, or resource template is registered before calling the handler.
|
||||
Requests for unknown references return an error.
|
||||
|
||||
### Logging
|
||||
|
||||
The MCP Ruby SDK supports structured logging through the `notify_log_message` method, following the [MCP Logging specification](https://modelcontextprotocol.io/specification/latest/server/utilities/logging).
|
||||
|
|
@ -298,7 +346,6 @@ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, session
|
|||
### Unsupported Features (to be implemented in future versions)
|
||||
|
||||
- Resource subscriptions
|
||||
- Completions
|
||||
- Elicitation
|
||||
|
||||
### Usage
|
||||
|
|
@ -1056,6 +1103,7 @@ This class supports:
|
|||
- Resource reading via the `resources/read` method (`MCP::Client#read_resources`)
|
||||
- Prompt listing via the `prompts/list` method (`MCP::Client#prompts`)
|
||||
- Prompt retrieval via the `prompts/get` method (`MCP::Client#get_prompt`)
|
||||
- Completion requests via the `completion/complete` method (`MCP::Client#complete`)
|
||||
- Automatic JSON-RPC 2.0 message formatting
|
||||
- UUID request ID generation
|
||||
|
||||
|
|
|
|||
|
|
@ -488,6 +488,7 @@ module Conformance
|
|||
server.server_context = server
|
||||
|
||||
configure_resources_read_handler(server)
|
||||
configure_completion_handler(server)
|
||||
end
|
||||
|
||||
def configure_resources_read_handler(server)
|
||||
|
|
@ -528,6 +529,35 @@ module Conformance
|
|||
end
|
||||
end
|
||||
|
||||
def configure_completion_handler(server)
|
||||
server.completion_handler do |params|
|
||||
ref = params[:ref]
|
||||
argument = params[:argument]
|
||||
value = argument[:value].to_s
|
||||
|
||||
case ref[:type]
|
||||
when "ref/prompt"
|
||||
case ref[:name]
|
||||
when "test_prompt_with_arguments"
|
||||
candidates = case argument[:name]
|
||||
when "arg1"
|
||||
["value1", "value2", "value3"]
|
||||
when "arg2"
|
||||
["optionA", "optionB", "optionC"]
|
||||
else
|
||||
[]
|
||||
end
|
||||
values = candidates.select { |v| v.start_with?(value) }
|
||||
{ completion: { values: values, hasMore: false } }
|
||||
else
|
||||
{ completion: { values: [], hasMore: false } }
|
||||
end
|
||||
else
|
||||
{ completion: { values: [], hasMore: false } }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def build_rack_app(transport)
|
||||
mcp_app = proc do |env|
|
||||
request = Rack::Request.new(env)
|
||||
|
|
|
|||
|
|
@ -147,6 +147,22 @@ module MCP
|
|||
response.fetch("result", {})
|
||||
end
|
||||
|
||||
# Requests completion suggestions from the server for a prompt argument or resource template URI.
|
||||
#
|
||||
# @param ref [Hash] The reference, e.g. `{ type: "ref/prompt", name: "my_prompt" }`
|
||||
# or `{ type: "ref/resource", uri: "file:///{path}" }`.
|
||||
# @param argument [Hash] The argument being completed, e.g. `{ name: "language", value: "py" }`.
|
||||
# @param context [Hash, nil] Optional context with previously resolved arguments.
|
||||
# @return [Hash] The completion result with `"values"`, `"hasMore"`, and optionally `"total"`.
|
||||
def complete(ref:, argument:, context: nil)
|
||||
params = { ref: ref, argument: argument }
|
||||
params[:context] = context if context
|
||||
|
||||
response = request(method: "completion/complete", params: params)
|
||||
|
||||
response.dig("result", "completion") || { "values" => [], "hasMore" => false }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def request(method:, params: nil)
|
||||
|
|
|
|||
|
|
@ -24,6 +24,12 @@ module MCP
|
|||
UNSUPPORTED_PROPERTIES_UNTIL_2025_06_18 = [:description, :icons].freeze
|
||||
UNSUPPORTED_PROPERTIES_UNTIL_2025_03_26 = [:title, :websiteUrl].freeze
|
||||
|
||||
DEFAULT_COMPLETION_RESULT = { completion: { values: [], hasMore: false } }.freeze
|
||||
|
||||
# Servers return an array of completion values ranked by relevance, with maximum 100 items per response.
|
||||
# https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion#completion-results
|
||||
MAX_COMPLETION_VALUES = 100
|
||||
|
||||
class RequestHandlerError < StandardError
|
||||
attr_reader :error_type
|
||||
attr_reader :original_error
|
||||
|
|
@ -100,12 +106,12 @@ module MCP
|
|||
Methods::PING => ->(_) { {} },
|
||||
Methods::NOTIFICATIONS_INITIALIZED => ->(_) {},
|
||||
Methods::NOTIFICATIONS_PROGRESS => ->(_) {},
|
||||
Methods::COMPLETION_COMPLETE => ->(_) { DEFAULT_COMPLETION_RESULT },
|
||||
Methods::LOGGING_SET_LEVEL => method(:configure_logging_level),
|
||||
|
||||
# No op handlers for currently unsupported methods
|
||||
Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
|
||||
Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
|
||||
Methods::COMPLETION_COMPLETE => ->(_) { { completion: { values: [], hasMore: false } } },
|
||||
Methods::ELICITATION_CREATE => ->(_) {},
|
||||
}
|
||||
@transport = transport
|
||||
|
|
@ -208,6 +214,15 @@ module MCP
|
|||
@handlers[Methods::RESOURCES_READ] = block
|
||||
end
|
||||
|
||||
# Sets a custom handler for `completion/complete` requests.
|
||||
# The block receives the parsed request params and should return completion values.
|
||||
#
|
||||
# @yield [params] The request params containing `:ref`, `:argument`, and optionally `:context`.
|
||||
# @yieldreturn [Hash] A hash with `:completion` key containing `:values`, optional `:total`, and `:hasMore`.
|
||||
def completion_handler(&block)
|
||||
@handlers[Methods::COMPLETION_COMPLETE] = block
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate!
|
||||
|
|
@ -307,6 +322,8 @@ module MCP
|
|||
{ resourceTemplates: @handlers[Methods::RESOURCES_TEMPLATES_LIST].call(params) }
|
||||
when Methods::TOOLS_CALL
|
||||
call_tool(params, session: session)
|
||||
when Methods::COMPLETION_COMPLETE
|
||||
complete(params)
|
||||
when Methods::LOGGING_SET_LEVEL
|
||||
configure_logging_level(params, session: session)
|
||||
else
|
||||
|
|
@ -481,6 +498,14 @@ module MCP
|
|||
@resource_templates.map(&:to_h)
|
||||
end
|
||||
|
||||
def complete(params)
|
||||
validate_completion_params!(params)
|
||||
|
||||
result = @handlers[Methods::COMPLETION_COMPLETE].call(params)
|
||||
|
||||
normalize_completion_result(result)
|
||||
end
|
||||
|
||||
def report_exception(exception, server_context = {})
|
||||
configuration.exception_reporter.call(exception, server_context)
|
||||
end
|
||||
|
|
@ -539,5 +564,56 @@ module MCP
|
|||
server_context
|
||||
end
|
||||
end
|
||||
|
||||
def validate_completion_params!(params)
|
||||
unless params.is_a?(Hash)
|
||||
raise RequestHandlerError.new("Invalid params", params, error_type: :invalid_params)
|
||||
end
|
||||
|
||||
ref = params[:ref]
|
||||
if ref.nil? || ref[:type].nil?
|
||||
raise RequestHandlerError.new("Missing or invalid ref", params, error_type: :invalid_params)
|
||||
end
|
||||
|
||||
argument = params[:argument]
|
||||
if argument.nil? || argument[:name].nil? || !argument.key?(:value)
|
||||
raise RequestHandlerError.new("Missing argument name or value", params, error_type: :invalid_params)
|
||||
end
|
||||
|
||||
case ref[:type]
|
||||
when "ref/prompt"
|
||||
unless @prompts[ref[:name]]
|
||||
raise RequestHandlerError.new("Prompt not found: #{ref[:name]}", params, error_type: :invalid_params)
|
||||
end
|
||||
when "ref/resource"
|
||||
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)
|
||||
end
|
||||
else
|
||||
raise RequestHandlerError.new("Invalid ref type: #{ref[:type]}", params, error_type: :invalid_params)
|
||||
end
|
||||
end
|
||||
|
||||
def normalize_completion_result(result)
|
||||
return DEFAULT_COMPLETION_RESULT unless result.is_a?(Hash)
|
||||
|
||||
completion = result[:completion] || result["completion"]
|
||||
return DEFAULT_COMPLETION_RESULT unless completion.is_a?(Hash)
|
||||
|
||||
values = completion[:values] || completion["values"] || []
|
||||
total = completion[:total] || completion["total"]
|
||||
has_more = completion[:hasMore] || completion["hasMore"] || false
|
||||
|
||||
count = values.length
|
||||
if count > MAX_COMPLETION_VALUES
|
||||
has_more = true
|
||||
total ||= count
|
||||
values = values.first(MAX_COMPLETION_VALUES)
|
||||
end
|
||||
|
||||
{ completion: { values: values, total: total, hasMore: has_more }.compact }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1468,8 +1468,10 @@ module MCP
|
|||
end
|
||||
|
||||
test "#handle completion/complete returns default completion result" do
|
||||
prompt = Prompt.define(name: "test") {}
|
||||
server = Server.new(
|
||||
name: "test_server",
|
||||
prompts: [prompt],
|
||||
capabilities: { completions: {} },
|
||||
)
|
||||
|
||||
|
|
@ -1496,6 +1498,432 @@ module MCP
|
|||
)
|
||||
end
|
||||
|
||||
test "#handle completion/complete with custom handler for ref/prompt" do
|
||||
prompt = Prompt.define(
|
||||
name: "code_review",
|
||||
arguments: [Prompt::Argument.new(name: "language", required: true)],
|
||||
) {}
|
||||
server = Server.new(
|
||||
name: "test_server",
|
||||
prompts: [prompt],
|
||||
capabilities: { completions: {} },
|
||||
)
|
||||
|
||||
server.completion_handler do |_params|
|
||||
{ completion: { values: ["python", "pytorch", "pyside"], total: 10, hasMore: true } }
|
||||
end
|
||||
|
||||
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: "code_review" },
|
||||
argument: { name: "language", value: "py" },
|
||||
},
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: 2,
|
||||
result: { completion: { values: ["python", "pytorch", "pyside"], total: 10, hasMore: true } },
|
||||
},
|
||||
response,
|
||||
)
|
||||
end
|
||||
|
||||
test "#handle completion/complete with custom handler for ref/resource" do
|
||||
template = ResourceTemplate.new(
|
||||
uri_template: "file:///{path}",
|
||||
name: "file",
|
||||
)
|
||||
server = Server.new(
|
||||
name: "test_server",
|
||||
resource_templates: [template],
|
||||
capabilities: { completions: {} },
|
||||
)
|
||||
|
||||
server.completion_handler do |_params|
|
||||
{ completion: { values: ["file:///src", "file:///spec"], hasMore: false } }
|
||||
end
|
||||
|
||||
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: "file:///{path}" },
|
||||
argument: { name: "path", value: "s" },
|
||||
},
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: 2,
|
||||
result: { completion: { values: ["file:///src", "file:///spec"], hasMore: false } },
|
||||
},
|
||||
response,
|
||||
)
|
||||
end
|
||||
|
||||
test "#handle completion/complete passes context arguments to handler" do
|
||||
prompt = Prompt.define(
|
||||
name: "code_review",
|
||||
arguments: [
|
||||
Prompt::Argument.new(name: "language", required: true),
|
||||
Prompt::Argument.new(name: "framework", required: false),
|
||||
],
|
||||
) {}
|
||||
server = Server.new(
|
||||
name: "test_server",
|
||||
prompts: [prompt],
|
||||
capabilities: { completions: {} },
|
||||
)
|
||||
|
||||
received_params = nil
|
||||
server.completion_handler do |params|
|
||||
received_params = params
|
||||
{ completion: { values: ["flask"], hasMore: false } }
|
||||
end
|
||||
|
||||
server.handle({ jsonrpc: "2.0", method: "initialize", id: 1 })
|
||||
server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })
|
||||
|
||||
server.handle({
|
||||
jsonrpc: "2.0",
|
||||
id: 2,
|
||||
method: "completion/complete",
|
||||
params: {
|
||||
ref: { type: "ref/prompt", name: "code_review" },
|
||||
argument: { name: "framework", value: "fla" },
|
||||
context: { arguments: { language: "python" } },
|
||||
},
|
||||
})
|
||||
|
||||
assert_equal({ language: "python" }, received_params.dig(:context, :arguments))
|
||||
end
|
||||
|
||||
test "#handle completion/complete truncates values exceeding 100 items" do
|
||||
prompt = Prompt.define(name: "test") {}
|
||||
server = Server.new(
|
||||
name: "test_server",
|
||||
prompts: [prompt],
|
||||
capabilities: { completions: {} },
|
||||
)
|
||||
|
||||
server.completion_handler do |_params|
|
||||
{ completion: { values: (1..150).map(&:to_s), hasMore: false } }
|
||||
end
|
||||
|
||||
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: "" },
|
||||
},
|
||||
})
|
||||
|
||||
completion = response[:result][:completion]
|
||||
assert_equal 100, completion[:values].length
|
||||
assert_equal "1", completion[:values].first
|
||||
assert_equal "100", completion[:values].last
|
||||
assert(completion[:hasMore])
|
||||
assert_equal 150, completion[:total]
|
||||
end
|
||||
|
||||
test "#handle completion/complete returns error for nonexistent prompt" 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: "nonexistent" },
|
||||
argument: { name: "arg", value: "val" },
|
||||
},
|
||||
})
|
||||
|
||||
assert_equal(-32_602, response[:error][:code])
|
||||
end
|
||||
|
||||
test "#handle completion/complete returns error for nonexistent resource template" 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(-32_602, response[:error][:code])
|
||||
end
|
||||
|
||||
test "#handle completion/complete returns error for invalid ref type" 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/invalid" },
|
||||
argument: { name: "arg", value: "val" },
|
||||
},
|
||||
})
|
||||
|
||||
assert_equal(-32_602, response[:error][:code])
|
||||
end
|
||||
|
||||
test "#handle completion/complete returns error for missing ref" 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: {},
|
||||
argument: { name: "arg", value: "val" },
|
||||
},
|
||||
})
|
||||
|
||||
assert_equal(-32_602, response[:error][:code])
|
||||
end
|
||||
|
||||
test "#handle completion/complete with custom handler for ref/resource with resource URI" do
|
||||
resource = Resource.new(
|
||||
uri: "file:///README.md",
|
||||
name: "readme",
|
||||
)
|
||||
server = Server.new(
|
||||
name: "test_server",
|
||||
resources: [resource],
|
||||
capabilities: { completions: {} },
|
||||
)
|
||||
|
||||
server.completion_handler do |_params|
|
||||
{ completion: { values: ["file:///README.md"], hasMore: false } }
|
||||
end
|
||||
|
||||
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: "file:///README.md" },
|
||||
argument: { name: "path", value: "R" },
|
||||
},
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: 2,
|
||||
result: { completion: { values: ["file:///README.md"], hasMore: false } },
|
||||
},
|
||||
response,
|
||||
)
|
||||
end
|
||||
|
||||
test "#handle completion/complete returns error for missing argument" do
|
||||
prompt = Prompt.define(name: "test") {}
|
||||
server = Server.new(
|
||||
name: "test_server",
|
||||
prompts: [prompt],
|
||||
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" },
|
||||
},
|
||||
})
|
||||
|
||||
assert_equal(-32_602, response[:error][:code])
|
||||
end
|
||||
|
||||
test "#handle completion/complete returns error for missing argument value" do
|
||||
prompt = Prompt.define(name: "test") {}
|
||||
server = Server.new(
|
||||
name: "test_server",
|
||||
prompts: [prompt],
|
||||
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" },
|
||||
},
|
||||
})
|
||||
|
||||
assert_equal(-32_602, response[:error][:code])
|
||||
end
|
||||
|
||||
test "#handle completion/complete returns default when handler returns nil" do
|
||||
prompt = Prompt.define(name: "test") {}
|
||||
server = Server.new(
|
||||
name: "test_server",
|
||||
prompts: [prompt],
|
||||
capabilities: { completions: {} },
|
||||
)
|
||||
|
||||
server.completion_handler do |_params|
|
||||
nil
|
||||
end
|
||||
|
||||
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: "" },
|
||||
},
|
||||
})
|
||||
|
||||
assert_equal(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
id: 2,
|
||||
result: { completion: { values: [], hasMore: false } },
|
||||
},
|
||||
response,
|
||||
)
|
||||
end
|
||||
|
||||
test "#handle completion/complete with string-keyed handler result" do
|
||||
prompt = Prompt.define(name: "test") {}
|
||||
server = Server.new(
|
||||
name: "test_server",
|
||||
prompts: [prompt],
|
||||
capabilities: { completions: {} },
|
||||
)
|
||||
|
||||
server.completion_handler do |_params|
|
||||
{ "completion" => { "values" => ["alpha", "beta"], "hasMore" => true } }
|
||||
end
|
||||
|
||||
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: "" },
|
||||
},
|
||||
})
|
||||
|
||||
assert_equal ["alpha", "beta"], response[:result][:completion][:values]
|
||||
assert response[:result][:completion][:hasMore]
|
||||
end
|
||||
|
||||
test "#handle completion/complete returns invalid params for non-Hash params" do
|
||||
server = Server.new(
|
||||
name: "test_server",
|
||||
prompts: [],
|
||||
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: "invalid",
|
||||
})
|
||||
|
||||
assert_equal(-32602, response[:error][:code])
|
||||
end
|
||||
|
||||
test "#handle completion/complete returns error when completions capability is not declared" do
|
||||
server = Server.new(
|
||||
name: "test_server",
|
||||
prompts: [],
|
||||
)
|
||||
|
||||
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: "" },
|
||||
},
|
||||
})
|
||||
|
||||
assert response[:error]
|
||||
assert_includes response[:error][:data], "completions"
|
||||
end
|
||||
|
||||
test "#handle resources/subscribe returns empty result" do
|
||||
server = Server.new(
|
||||
name: "test_server",
|
||||
|
|
|
|||
Loading…
Reference in a new issue