Merge pull request #289 from koic/feature_completion_complete
Support `completion/complete` per MCP specification
This commit is contained in:
commit
c563036f88
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