From 88e1f9a8407ad3e4d8cf08617a2b011e0cf660bd Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Wed, 1 Apr 2026 01:03:23 +0900 Subject: [PATCH] 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. --- README.md | 50 ++++- conformance/server.rb | 30 +++ lib/mcp/client.rb | 16 ++ lib/mcp/server.rb | 78 +++++++- test/mcp/client_test.rb | 81 ++++++++ test/mcp/server_test.rb | 428 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 681 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 651c2ef8..36d67840 100644 --- a/README.md +++ b/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 diff --git a/conformance/server.rb b/conformance/server.rb index 9c99f2b2..9e5fd5ec 100644 --- a/conformance/server.rb +++ b/conformance/server.rb @@ -488,6 +488,7 @@ def configure_handlers(server) server.server_context = server configure_resources_read_handler(server) + configure_completion_handler(server) end def configure_resources_read_handler(server) @@ -528,6 +529,35 @@ def configure_resources_read_handler(server) 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) diff --git a/lib/mcp/client.rb b/lib/mcp/client.rb index 7c89bcb1..d37b24a8 100644 --- a/lib/mcp/client.rb +++ b/lib/mcp/client.rb @@ -147,6 +147,22 @@ def get_prompt(name:) 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) diff --git a/lib/mcp/server.rb b/lib/mcp/server.rb index 827ac903..1b026a13 100644 --- a/lib/mcp/server.rb +++ b/lib/mcp/server.rb @@ -24,6 +24,12 @@ class Server 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 @@ def initialize( 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 @@ def resources_read_handler(&block) @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 @@ def handle_request(request, method, session: nil) { 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 @@ def list_resource_templates(request) @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 @@ def server_context_with_meta(request) 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 diff --git a/test/mcp/client_test.rb b/test/mcp/client_test.rb index 5495b15f..0252e0e0 100644 --- a/test/mcp/client_test.rb +++ b/test/mcp/client_test.rb @@ -456,5 +456,86 @@ def test_server_error_includes_data_field 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 diff --git a/test/mcp/server_test.rb b/test/mcp/server_test.rb index ac86726b..785e16ee 100644 --- a/test/mcp/server_test.rb +++ b/test/mcp/server_test.rb @@ -1468,8 +1468,10 @@ class Example < Tool 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 @@ class Example < Tool ) 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",