Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
29 changes: 29 additions & 0 deletions lib/mcp/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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<Hash>] 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.
Expand Down Expand Up @@ -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
Expand Down
130 changes: 130 additions & 0 deletions test/mcp/client_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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