diff --git a/README.md b/README.md index ba08cac..572e361 100644 --- a/README.md +++ b/README.md @@ -830,6 +830,8 @@ This class supports: - Tool invocation via the `tools/call` method (`MCP::Client#call_tools`) - Resource listing via the `resources/list` method (`MCP::Client#resources`) - 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`) - Automatic JSON-RPC 2.0 message formatting - UUID request ID generation diff --git a/lib/mcp/client.rb b/lib/mcp/client.rb index 90095d3..7ecf39a 100644 --- a/lib/mcp/client.rb +++ b/lib/mcp/client.rb @@ -58,6 +58,20 @@ def resources response.dig("result", "resources") || [] end + # Returns the list of prompts available from the server. + # Each call will make a new request – the result is not cached. + # + # @return [Array] An array of available prompts. + def prompts + response = transport.send_request(request: { + jsonrpc: JsonRpcHandler::Version::V2_0, + id: request_id, + method: "prompts/list", + }) + + response.dig("result", "prompts") || [] + end + # Calls a tool via the transport layer and returns the full response from the server. # # @param tool [MCP::Client::Tool] The tool to be called. @@ -96,6 +110,21 @@ def read_resource(uri:) response.dig("result", "contents") || [] end + # Gets a prompt from the server by name and returns its details. + # + # @param name [String] The name of the prompt to get. + # @return [Hash] A hash containing the prompt details. + def get_prompt(name:) + response = transport.send_request(request: { + jsonrpc: JsonRpcHandler::Version::V2_0, + id: request_id, + method: "prompts/get", + params: { name: name }, + }) + + response.fetch("result", {}) + end + private def request_id diff --git a/test/mcp/client_test.rb b/test/mcp/client_test.rb index a98d258..7b55448 100644 --- a/test/mcp/client_test.rb +++ b/test/mcp/client_test.rb @@ -141,5 +141,135 @@ def test_read_resource_returns_empty_array_when_no_contents assert_empty(contents) end + + def test_prompts_sends_request_to_transport_and_returns_prompts_array + transport = mock + mock_response = { + "result" => { + "prompts" => [ + { + "name" => "prompt_1", + "description" => "First prompt", + "arguments" => [ + { + "name" => "code_1", + "description" => "The code_1 to review", + "required" => true, + }, + ], + }, + { + "name" => "prompt_2", + "description" => "Second prompt", + "arguments" => [ + { + "name" => "code_2", + "description" => "The code_2 to review", + "required" => true, + }, + ], + }, + ], + }, + } + + # Only checking for the essential parts of the request + transport.expects(:send_request).with do |args| + args in { request: { method: "prompts/list", jsonrpc: "2.0" } } + end.returns(mock_response).once + + client = Client.new(transport: transport) + prompts = client.prompts + + assert_equal(2, prompts.size) + assert_equal("prompt_1", prompts.first["name"]) + assert_equal("First prompt", prompts.first["description"]) + assert_equal("code_1", prompts.first["arguments"].first["name"]) + assert_equal("The code_1 to review", prompts.first["arguments"].first["description"]) + assert(prompts.first["arguments"].first["required"]) + + assert_equal("prompt_2", prompts.last["name"]) + assert_equal("Second prompt", prompts.last["description"]) + assert_equal("code_2", prompts.last["arguments"].first["name"]) + assert_equal("The code_2 to review", prompts.last["arguments"].first["description"]) + assert(prompts.last["arguments"].first["required"]) + end + + def test_prompts_returns_empty_array_when_no_prompts + transport = mock + mock_response = { "result" => { "prompts" => [] } } + + transport.expects(:send_request).returns(mock_response).once + + client = Client.new(transport: transport) + prompts = client.prompts + + assert_empty(prompts) + end + + def test_get_prompt_sends_request_to_transport_and_returns_contents + transport = mock + name = "first_prompt" + mock_response = { + "result" => { + "description" => "First prompt", + "messages" => [ + { + "role" => "user", + "content" => { + "text" => "First prompt content", + "type" => "text", + }, + }, + ], + }, + } + + # Only checking for the essential parts of the request + transport.expects(:send_request).with do |args| + args in { + request: { + method: "prompts/get", + jsonrpc: "2.0", + params: { + name: name, + }, + }, + } + end.returns(mock_response).once + + client = Client.new(transport: transport) + contents = client.get_prompt(name: name) + + assert_equal("First prompt", contents["description"]) + assert_equal("user", contents["messages"].first["role"]) + assert_equal("First prompt content", contents["messages"].first["content"]["text"]) + end + + def test_get_prompt_returns_empty_hash_when_no_contents + transport = mock + name = "nonexistent_prompt" + mock_response = { "result" => {} } + + transport.expects(:send_request).returns(mock_response).once + + client = Client.new(transport: transport) + contents = client.get_prompt(name: name) + + assert_empty(contents) + end + + def test_get_prompt_returns_empty_hash + transport = mock + name = "nonexistent_prompt" + mock_response = {} + + transport.expects(:send_request).returns(mock_response).once + + client = Client.new(transport: transport) + contents = client.get_prompt(name: name) + + assert_empty(contents) + end end end