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/list` - Lists all registered resources and their schemas
|
||||||
- `resources/read` - Retrieves a specific resource by name
|
- `resources/read` - Retrieves a specific resource by name
|
||||||
- `resources/templates/list` - Lists all registered resource templates and their schemas
|
- `resources/templates/list` - Lists all registered resource templates and their schemas
|
||||||
|
- `completion/complete` - Returns autocompletion suggestions for prompt arguments and resource URIs
|
||||||
|
|
||||||
### Custom Methods
|
### 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
|
- `report_progress` is a no-op when no `progressToken` was provided by the client
|
||||||
- Supports both numeric and string progress tokens
|
- 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
|
### 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).
|
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)
|
### Unsupported Features (to be implemented in future versions)
|
||||||
|
|
||||||
- Resource subscriptions
|
- Resource subscriptions
|
||||||
- Completions
|
|
||||||
- Elicitation
|
- Elicitation
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
@ -1056,6 +1103,7 @@ This class supports:
|
||||||
- Resource reading via the `resources/read` method (`MCP::Client#read_resources`)
|
- Resource reading via the `resources/read` method (`MCP::Client#read_resources`)
|
||||||
- Prompt listing via the `prompts/list` method (`MCP::Client#prompts`)
|
- Prompt listing via the `prompts/list` method (`MCP::Client#prompts`)
|
||||||
- Prompt retrieval via the `prompts/get` method (`MCP::Client#get_prompt`)
|
- 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
|
- Automatic JSON-RPC 2.0 message formatting
|
||||||
- UUID request ID generation
|
- UUID request ID generation
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -488,6 +488,7 @@ module Conformance
|
||||||
server.server_context = server
|
server.server_context = server
|
||||||
|
|
||||||
configure_resources_read_handler(server)
|
configure_resources_read_handler(server)
|
||||||
|
configure_completion_handler(server)
|
||||||
end
|
end
|
||||||
|
|
||||||
def configure_resources_read_handler(server)
|
def configure_resources_read_handler(server)
|
||||||
|
|
@ -528,6 +529,35 @@ module Conformance
|
||||||
end
|
end
|
||||||
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)
|
def build_rack_app(transport)
|
||||||
mcp_app = proc do |env|
|
mcp_app = proc do |env|
|
||||||
request = Rack::Request.new(env)
|
request = Rack::Request.new(env)
|
||||||
|
|
|
||||||
|
|
@ -147,6 +147,22 @@ module MCP
|
||||||
response.fetch("result", {})
|
response.fetch("result", {})
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def request(method:, params: nil)
|
def request(method:, params: nil)
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@ module MCP
|
||||||
UNSUPPORTED_PROPERTIES_UNTIL_2025_06_18 = [:description, :icons].freeze
|
UNSUPPORTED_PROPERTIES_UNTIL_2025_06_18 = [:description, :icons].freeze
|
||||||
UNSUPPORTED_PROPERTIES_UNTIL_2025_03_26 = [:title, :websiteUrl].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
|
class RequestHandlerError < StandardError
|
||||||
attr_reader :error_type
|
attr_reader :error_type
|
||||||
attr_reader :original_error
|
attr_reader :original_error
|
||||||
|
|
@ -100,12 +106,12 @@ module MCP
|
||||||
Methods::PING => ->(_) { {} },
|
Methods::PING => ->(_) { {} },
|
||||||
Methods::NOTIFICATIONS_INITIALIZED => ->(_) {},
|
Methods::NOTIFICATIONS_INITIALIZED => ->(_) {},
|
||||||
Methods::NOTIFICATIONS_PROGRESS => ->(_) {},
|
Methods::NOTIFICATIONS_PROGRESS => ->(_) {},
|
||||||
|
Methods::COMPLETION_COMPLETE => ->(_) { DEFAULT_COMPLETION_RESULT },
|
||||||
Methods::LOGGING_SET_LEVEL => method(:configure_logging_level),
|
Methods::LOGGING_SET_LEVEL => method(:configure_logging_level),
|
||||||
|
|
||||||
# No op handlers for currently unsupported methods
|
# No op handlers for currently unsupported methods
|
||||||
Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
|
Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
|
||||||
Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
|
Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
|
||||||
Methods::COMPLETION_COMPLETE => ->(_) { { completion: { values: [], hasMore: false } } },
|
|
||||||
Methods::ELICITATION_CREATE => ->(_) {},
|
Methods::ELICITATION_CREATE => ->(_) {},
|
||||||
}
|
}
|
||||||
@transport = transport
|
@transport = transport
|
||||||
|
|
@ -208,6 +214,15 @@ module MCP
|
||||||
@handlers[Methods::RESOURCES_READ] = block
|
@handlers[Methods::RESOURCES_READ] = block
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def validate!
|
def validate!
|
||||||
|
|
@ -307,6 +322,8 @@ module MCP
|
||||||
{ resourceTemplates: @handlers[Methods::RESOURCES_TEMPLATES_LIST].call(params) }
|
{ resourceTemplates: @handlers[Methods::RESOURCES_TEMPLATES_LIST].call(params) }
|
||||||
when Methods::TOOLS_CALL
|
when Methods::TOOLS_CALL
|
||||||
call_tool(params, session: session)
|
call_tool(params, session: session)
|
||||||
|
when Methods::COMPLETION_COMPLETE
|
||||||
|
complete(params)
|
||||||
when Methods::LOGGING_SET_LEVEL
|
when Methods::LOGGING_SET_LEVEL
|
||||||
configure_logging_level(params, session: session)
|
configure_logging_level(params, session: session)
|
||||||
else
|
else
|
||||||
|
|
@ -481,6 +498,14 @@ module MCP
|
||||||
@resource_templates.map(&:to_h)
|
@resource_templates.map(&:to_h)
|
||||||
end
|
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 = {})
|
def report_exception(exception, server_context = {})
|
||||||
configuration.exception_reporter.call(exception, server_context)
|
configuration.exception_reporter.call(exception, server_context)
|
||||||
end
|
end
|
||||||
|
|
@ -539,5 +564,56 @@ module MCP
|
||||||
server_context
|
server_context
|
||||||
end
|
end
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -456,5 +456,86 @@ module MCP
|
||||||
error = assert_raises(Client::ServerError) { client.tools }
|
error = assert_raises(Client::ServerError) { client.tools }
|
||||||
assert_equal("extra details", error.data)
|
assert_equal("extra details", error.data)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1468,8 +1468,10 @@ module MCP
|
||||||
end
|
end
|
||||||
|
|
||||||
test "#handle completion/complete returns default completion result" do
|
test "#handle completion/complete returns default completion result" do
|
||||||
|
prompt = Prompt.define(name: "test") {}
|
||||||
server = Server.new(
|
server = Server.new(
|
||||||
name: "test_server",
|
name: "test_server",
|
||||||
|
prompts: [prompt],
|
||||||
capabilities: { completions: {} },
|
capabilities: { completions: {} },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -1496,6 +1498,432 @@ module MCP
|
||||||
)
|
)
|
||||||
end
|
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
|
test "#handle resources/subscribe returns empty result" do
|
||||||
server = Server.new(
|
server = Server.new(
|
||||||
name: "test_server",
|
name: "test_server",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue