ruby-mcp-client 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +113 -13
- data/lib/mcp_client/client.rb +182 -5
- data/lib/mcp_client/errors.rb +18 -0
- data/lib/mcp_client/prompt.rb +41 -0
- data/lib/mcp_client/resource.rb +61 -0
- data/lib/mcp_client/server_base.rb +27 -0
- data/lib/mcp_client/server_sse/json_rpc_transport.rb +1 -0
- data/lib/mcp_client/server_sse.rb +157 -1
- data/lib/mcp_client/server_stdio/json_rpc_transport.rb +1 -0
- data/lib/mcp_client/server_stdio.rb +91 -0
- data/lib/mcp_client/server_streamable_http.rb +145 -0
- data/lib/mcp_client/version.rb +1 -1
- data/lib/mcp_client.rb +2 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 27b97462ec0c99d98299df120726f90944f495aaddf198272d72725544240ec0
|
4
|
+
data.tar.gz: b58b8a6ba53f698abe1cb3576fba8030d66cb0bd7eafa2e39e28943e7ef1337f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 50d5791642a74f521133a057d924d3daf14e657ec617e73ad8f95510b4d97f3d52742c5a467c54d75dc02030a087f29e63ee346429271e8c80eb71cb96e38f94
|
7
|
+
data.tar.gz: 5f92d87a5d176e3271a11e9bbea7eafc6720107c6ae795601250e088cde69e6d6340911eecd6ea8ab32776d97d9bfb0b4d3740087509eb704cb400bf4e23e5aa
|
data/README.md
CHANGED
@@ -49,6 +49,8 @@ This Ruby MCP Client implements key features from the latest MCP specification (
|
|
49
49
|
- **Streamable HTTP Transport** - Enhanced transport with Server-Sent Event formatted responses and session management
|
50
50
|
- **HTTP Redirect Support** - Automatic redirect handling for both SSE and HTTP transports with configurable limits
|
51
51
|
- **FastMCP Compatibility** - Full compatibility with FastMCP servers including proper line ending handling
|
52
|
+
- **Prompts Support** - Full implementation of MCP prompts for dynamic content generation
|
53
|
+
- **Resources Support** - Complete resources implementation for accessing files and data with URI-based identification
|
52
54
|
|
53
55
|
## Usage
|
54
56
|
|
@@ -87,6 +89,17 @@ client = MCPClient.create_client(
|
|
87
89
|
retries: 3, # Optional number of retry attempts (default: 3)
|
88
90
|
retry_backoff: 1, # Optional backoff delay in seconds (default: 1)
|
89
91
|
logger: Logger.new($stdout, level: Logger::INFO) # Optional logger for this server
|
92
|
+
),
|
93
|
+
# Streamable HTTP server (HTTP POST with SSE responses and session management)
|
94
|
+
MCPClient.streamable_http_config(
|
95
|
+
base_url: 'https://api.example.com/mcp',
|
96
|
+
endpoint: '/rpc', # Optional JSON-RPC endpoint path (default: '/rpc')
|
97
|
+
headers: { 'Authorization' => 'Bearer YOUR_TOKEN' },
|
98
|
+
name: 'streamable_api', # Optional name for this server
|
99
|
+
read_timeout: 60, # Optional timeout in seconds (default: 30)
|
100
|
+
retries: 3, # Optional number of retry attempts (default: 3)
|
101
|
+
retry_backoff: 1, # Optional backoff delay in seconds (default: 1)
|
102
|
+
logger: Logger.new($stdout, level: Logger::INFO) # Optional logger for this server
|
90
103
|
)
|
91
104
|
],
|
92
105
|
# Optional logger for the client and all servers without explicit loggers
|
@@ -100,13 +113,14 @@ client = MCPClient.create_client(
|
|
100
113
|
)
|
101
114
|
|
102
115
|
# MCP server configuration JSON format can be:
|
103
|
-
# 1. A single server object:
|
116
|
+
# 1. A single server object:
|
104
117
|
# { "type": "sse", "url": "http://example.com/sse" }
|
105
118
|
# { "type": "http", "url": "http://example.com", "endpoint": "/rpc" }
|
106
|
-
#
|
107
|
-
#
|
119
|
+
# { "type": "streamable_http", "url": "http://example.com/mcp", "endpoint": "/rpc" }
|
120
|
+
# 2. An array of server objects:
|
121
|
+
# [{ "type": "stdio", "command": "npx server" }, { "type": "sse", "url": "http://..." }, { "type": "streamable_http", "url": "http://..." }]
|
108
122
|
# 3. An object with "mcpServers" key containing named servers:
|
109
|
-
# { "mcpServers": { "server1": { "type": "sse", "url": "http://..." }, "server2": { "type": "
|
123
|
+
# { "mcpServers": { "server1": { "type": "sse", "url": "http://..." }, "server2": { "type": "streamable_http", "url": "http://..." } } }
|
110
124
|
# Note: When using this format, server1/server2 will be accessible by name
|
111
125
|
|
112
126
|
# List available tools
|
@@ -140,6 +154,26 @@ client.call_tool_streaming('streaming_tool', { param: 'value' }, server: 'api').
|
|
140
154
|
puts chunk
|
141
155
|
end
|
142
156
|
|
157
|
+
# === Working with Prompts ===
|
158
|
+
# List available prompts from all servers
|
159
|
+
prompts = client.list_prompts
|
160
|
+
|
161
|
+
# Get a specific prompt with parameters
|
162
|
+
result = client.get_prompt('greeting', { name: 'Ruby Developer' })
|
163
|
+
|
164
|
+
# Get a prompt from a specific server
|
165
|
+
result = client.get_prompt('greeting', { name: 'Ruby Developer' }, server: 'filesystem')
|
166
|
+
|
167
|
+
# === Working with Resources ===
|
168
|
+
# List available resources from all servers
|
169
|
+
resources = client.list_resources
|
170
|
+
|
171
|
+
# Read a specific resource by URI
|
172
|
+
contents = client.read_resource('file:///example.txt')
|
173
|
+
|
174
|
+
# Read a resource from a specific server
|
175
|
+
contents = client.read_resource('file:///example.txt', server: 'filesystem')
|
176
|
+
|
143
177
|
# Format tools for specific AI services
|
144
178
|
openai_tools = client.to_openai_tools
|
145
179
|
anthropic_tools = client.to_anthropic_tools
|
@@ -333,15 +367,21 @@ puts "Ping successful: #{ping_result.inspect}"
|
|
333
367
|
mcp_client.cleanup
|
334
368
|
```
|
335
369
|
|
336
|
-
See `examples/
|
370
|
+
See `examples/streamable_http_example.rb` for the full Playwright SSE example.
|
337
371
|
|
338
372
|
### FastMCP Example
|
339
373
|
|
340
|
-
The repository includes
|
374
|
+
The repository includes complete FastMCP server examples that demonstrate the Ruby MCP client working with Python FastMCP servers, including full MCP protocol support with tools, prompts, and resources:
|
375
|
+
|
376
|
+
#### Basic FastMCP Example
|
377
|
+
|
378
|
+
**For FastMCP server with SSE transport (includes tools, prompts, and resources):**
|
379
|
+
```bash
|
380
|
+
# From the ruby-mcp-client directory
|
381
|
+
python examples/echo_server.py
|
382
|
+
```
|
341
383
|
|
342
384
|
```ruby
|
343
|
-
# Start the FastMCP server
|
344
|
-
# python examples/echo_server.py
|
345
385
|
|
346
386
|
# Run the Ruby client
|
347
387
|
# bundle exec ruby examples/echo_server_client.rb
|
@@ -352,17 +392,72 @@ require 'mcp_client'
|
|
352
392
|
client = MCPClient.create_client(
|
353
393
|
mcp_server_configs: [
|
354
394
|
MCPClient.sse_config(
|
355
|
-
base_url: 'http://127.0.0.1:8000/sse
|
395
|
+
base_url: 'http://127.0.0.1:8000/sse',
|
356
396
|
read_timeout: 30
|
357
397
|
)
|
358
398
|
]
|
359
399
|
)
|
360
400
|
|
361
|
-
# List
|
401
|
+
# List and use tools
|
362
402
|
tools = client.list_tools
|
363
403
|
puts "Found #{tools.length} tools:"
|
364
404
|
tools.each { |tool| puts "- #{tool.name}: #{tool.description}" }
|
365
405
|
|
406
|
+
result = client.call_tool('echo', { message: 'Hello FastMCP!' })
|
407
|
+
|
408
|
+
# List and use prompts
|
409
|
+
prompts = client.list_prompts
|
410
|
+
puts "Found #{prompts.length} prompts:"
|
411
|
+
prompts.each { |prompt| puts "- #{prompt.name}: #{prompt.description}" }
|
412
|
+
|
413
|
+
greeting = client.get_prompt('greeting', { name: 'Ruby Developer' })
|
414
|
+
|
415
|
+
# List and read resources
|
416
|
+
resources = client.list_resources
|
417
|
+
puts "Found #{resources.length} resources:"
|
418
|
+
resources.each { |resource| puts "- #{resource.name} (#{resource.uri})" }
|
419
|
+
|
420
|
+
readme_content = client.read_resource('file:///sample/README.md')
|
421
|
+
```
|
422
|
+
|
423
|
+
#### Streamable HTTP Example
|
424
|
+
|
425
|
+
**For FastMCP server with Streamable HTTP transport (includes tools, prompts, and resources):**
|
426
|
+
```bash
|
427
|
+
# From the ruby-mcp-client directory
|
428
|
+
python examples/echo_server_streamable.py
|
429
|
+
```
|
430
|
+
|
431
|
+
```ruby
|
432
|
+
|
433
|
+
# Run the streamable HTTP client
|
434
|
+
# bundle exec ruby examples/echo_server_streamable_client.rb
|
435
|
+
|
436
|
+
require 'mcp_client'
|
437
|
+
|
438
|
+
# Connect to streamable HTTP server with full MCP protocol support
|
439
|
+
client = MCPClient.create_client(
|
440
|
+
mcp_server_configs: [
|
441
|
+
MCPClient.streamable_http_config(
|
442
|
+
base_url: 'http://localhost:8931/mcp',
|
443
|
+
read_timeout: 60
|
444
|
+
)
|
445
|
+
]
|
446
|
+
)
|
447
|
+
|
448
|
+
# Full protocol support including real-time notifications
|
449
|
+
client.on_notification do |method, params|
|
450
|
+
puts "Server notification: #{method} - #{params}"
|
451
|
+
end
|
452
|
+
|
453
|
+
# Use all MCP features: tools, prompts, resources
|
454
|
+
tools = client.list_tools
|
455
|
+
prompts = client.list_prompts
|
456
|
+
resources = client.list_resources
|
457
|
+
|
458
|
+
# Real-time progress notifications for long-running tasks
|
459
|
+
result = client.call_tool('long_task', { duration: 5, steps: 5 })
|
460
|
+
|
366
461
|
# Use the tools
|
367
462
|
result = client.call_tool('echo', { message: 'Hello from Ruby!' })
|
368
463
|
result = client.call_tool('reverse', { text: 'FastMCP rocks!' })
|
@@ -371,8 +466,10 @@ client.cleanup
|
|
371
466
|
```
|
372
467
|
|
373
468
|
The FastMCP example includes:
|
374
|
-
- **`echo_server.py`** - A Python FastMCP server with
|
375
|
-
- **`echo_server_client.rb`** - Ruby client demonstrating all features
|
469
|
+
- **`echo_server.py`** - A Python FastMCP server with tools, prompts, and resources
|
470
|
+
- **`echo_server_client.rb`** - Ruby client demonstrating all features including prompts and resources
|
471
|
+
- **`echo_server_streamable.py`** - Enhanced streamable HTTP server with tools, prompts, and resources
|
472
|
+
- **`echo_server_streamable_client.rb`** - Ruby client demonstrating streamable HTTP transport
|
376
473
|
- **`README_ECHO_SERVER.md`** - Complete setup and usage instructions
|
377
474
|
|
378
475
|
This example showcases redirect support, proper line ending handling, and seamless integration between Ruby and Python MCP implementations.
|
@@ -464,8 +561,9 @@ Complete examples can be found in the `examples/` directory:
|
|
464
561
|
- `openai_ruby_mcp.rb` - Integration with official openai/openai-ruby gem
|
465
562
|
- `ruby_anthropic_mcp.rb` - Integration with alexrudall/ruby-anthropic gem
|
466
563
|
- `gemini_ai_mcp.rb` - Integration with Google Vertex AI and Gemini models
|
467
|
-
- `
|
564
|
+
- `streamable_http_example.rb` - Streamable HTTP transport with Playwright MCP
|
468
565
|
- `echo_server.py` & `echo_server_client.rb` - FastMCP server example with full setup
|
566
|
+
- `echo_server_streamable.py` & `echo_server_streamable_client.rb` - Enhanced streamable HTTP server example
|
469
567
|
|
470
568
|
## MCP Server Compatibility
|
471
569
|
|
@@ -679,6 +777,8 @@ For complete OAuth documentation, see [OAUTH.md](OAUTH.md).
|
|
679
777
|
- **Server disambiguation** - Specify which server to use when tools with same name exist in multiple servers
|
680
778
|
- **Atomic tool calls** - Simple API for invoking tools with parameters
|
681
779
|
- **Batch support** - Call multiple tools in a single operation
|
780
|
+
- **Prompts support** - List and get prompts with parameters from MCP servers
|
781
|
+
- **Resources support** - List and read resources by URI from MCP servers
|
682
782
|
- **API conversions** - Built-in format conversion for OpenAI, Anthropic, and Google Vertex AI APIs
|
683
783
|
- **Thread safety** - Synchronized access for thread-safe operation
|
684
784
|
- **Server notifications** - Support for JSON-RPC notifications
|
data/lib/mcp_client/client.rb
CHANGED
@@ -9,10 +9,14 @@ module MCPClient
|
|
9
9
|
# @!attribute [r] servers
|
10
10
|
# @return [Array<MCPClient::ServerBase>] list of servers
|
11
11
|
# @!attribute [r] tool_cache
|
12
|
-
# @return [Hash<String, MCPClient::Tool>] cache of tools by name
|
12
|
+
# @return [Hash<String, MCPClient::Tool>] cache of tools by composite key (server_id:name)
|
13
|
+
# @!attribute [r] prompt_cache
|
14
|
+
# @return [Hash<String, MCPClient::Prompt>] cache of prompts by composite key (server_id:name)
|
15
|
+
# @!attribute [r] resource_cache
|
16
|
+
# @return [Hash<String, MCPClient::Resource>] cache of resources by composite key (server_id:uri)
|
13
17
|
# @!attribute [r] logger
|
14
18
|
# @return [Logger] logger for client operations
|
15
|
-
attr_reader :servers, :tool_cache, :logger
|
19
|
+
attr_reader :servers, :tool_cache, :prompt_cache, :resource_cache, :logger
|
16
20
|
|
17
21
|
# Initialize a new MCPClient::Client
|
18
22
|
# @param mcp_server_configs [Array<Hash>] configurations for MCP servers
|
@@ -26,6 +30,8 @@ module MCPClient
|
|
26
30
|
MCPClient::ServerFactory.create(config, logger: @logger)
|
27
31
|
end
|
28
32
|
@tool_cache = {}
|
33
|
+
@prompt_cache = {}
|
34
|
+
@resource_cache = {}
|
29
35
|
# JSON-RPC notification listeners
|
30
36
|
@notification_listeners = []
|
31
37
|
# Register default and user-defined notification handlers on each server
|
@@ -39,6 +45,163 @@ module MCPClient
|
|
39
45
|
end
|
40
46
|
end
|
41
47
|
|
48
|
+
# Lists all available prompts from all connected MCP servers
|
49
|
+
# @param cache [Boolean] whether to use cached prompts or fetch fresh
|
50
|
+
# @return [Array<MCPClient::Prompt>] list of available prompts
|
51
|
+
# @raise [MCPClient::Errors::ConnectionError] on authorization failures
|
52
|
+
# @raise [MCPClient::Errors::PromptGetError] if no prompts could be retrieved from any server
|
53
|
+
def list_prompts(cache: true)
|
54
|
+
return @prompt_cache.values if cache && !@prompt_cache.empty?
|
55
|
+
|
56
|
+
prompts = []
|
57
|
+
connection_errors = []
|
58
|
+
|
59
|
+
servers.each do |server|
|
60
|
+
server.list_prompts.each do |prompt|
|
61
|
+
cache_key = cache_key_for(server, prompt.name)
|
62
|
+
@prompt_cache[cache_key] = prompt
|
63
|
+
prompts << prompt
|
64
|
+
end
|
65
|
+
rescue MCPClient::Errors::ConnectionError => e
|
66
|
+
# Fast-fail on authorization errors for better user experience
|
67
|
+
# If this is the first server or we haven't collected any prompts yet,
|
68
|
+
# raise the auth error directly to avoid cascading error messages
|
69
|
+
raise e if e.message.include?('Authorization failed') && prompts.empty?
|
70
|
+
|
71
|
+
# Store the error and try other servers
|
72
|
+
connection_errors << e
|
73
|
+
@logger.error("Server error: #{e.message}")
|
74
|
+
end
|
75
|
+
|
76
|
+
prompts
|
77
|
+
end
|
78
|
+
|
79
|
+
# Gets a specific prompt by name with the given parameters
|
80
|
+
# @param prompt_name [String] the name of the prompt to get
|
81
|
+
# @param parameters [Hash] the parameters to pass to the prompt
|
82
|
+
# @param server [String, Symbol, Integer, MCPClient::ServerBase, nil] optional server to use
|
83
|
+
# @return [Object] the final prompt
|
84
|
+
def get_prompt(prompt_name, parameters, server: nil)
|
85
|
+
prompts = list_prompts
|
86
|
+
|
87
|
+
if server
|
88
|
+
# Use the specified server
|
89
|
+
srv = select_server(server)
|
90
|
+
# Find the prompt on this specific server
|
91
|
+
prompt = prompts.find { |t| t.name == prompt_name && t.server == srv }
|
92
|
+
unless prompt
|
93
|
+
raise MCPClient::Errors::PromptNotFound,
|
94
|
+
"Prompt '#{prompt_name}' not found on server '#{srv.name || srv.class.name}'"
|
95
|
+
end
|
96
|
+
else
|
97
|
+
# Find the prompt across all servers
|
98
|
+
matching_prompts = prompts.select { |t| t.name == prompt_name }
|
99
|
+
|
100
|
+
if matching_prompts.empty?
|
101
|
+
raise MCPClient::Errors::PromptNotFound, "Prompt '#{prompt_name}' not found"
|
102
|
+
elsif matching_prompts.size > 1
|
103
|
+
# If multiple matches, disambiguate with server names
|
104
|
+
server_names = matching_prompts.map { |t| t.server&.name || 'unnamed' }
|
105
|
+
raise MCPClient::Errors::AmbiguousPromptName,
|
106
|
+
"Multiple prompts named '#{prompt_name}' found across servers (#{server_names.join(', ')}). " \
|
107
|
+
"Please specify a server using the 'server' parameter."
|
108
|
+
end
|
109
|
+
|
110
|
+
prompt = matching_prompts.first
|
111
|
+
end
|
112
|
+
|
113
|
+
# Use the prompt's associated server
|
114
|
+
server = prompt.server
|
115
|
+
raise MCPClient::Errors::ServerNotFound, "No server found for prompt '#{prompt_name}'" unless server
|
116
|
+
|
117
|
+
begin
|
118
|
+
server.get_prompt(prompt_name, parameters)
|
119
|
+
rescue MCPClient::Errors::ConnectionError => e
|
120
|
+
# Add server identity information to the error for better context
|
121
|
+
server_id = server.name ? "#{server.class}[#{server.name}]" : server.class.name
|
122
|
+
raise MCPClient::Errors::PromptGetError,
|
123
|
+
"Error getting prompt '#{prompt_name}': #{e.message} (Server: #{server_id})"
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# Lists all available resources from all connected MCP servers
|
128
|
+
# @param cache [Boolean] whether to use cached resources or fetch fresh
|
129
|
+
# @return [Array<MCPClient::Resource>] list of available resources
|
130
|
+
# @raise [MCPClient::Errors::ConnectionError] on authorization failures
|
131
|
+
# @raise [MCPClient::Errors::ResourceReadError] if no resources could be retrieved from any server
|
132
|
+
def list_resources(cache: true)
|
133
|
+
return @resource_cache.values if cache && !@resource_cache.empty?
|
134
|
+
|
135
|
+
resources = []
|
136
|
+
connection_errors = []
|
137
|
+
|
138
|
+
servers.each do |server|
|
139
|
+
server.list_resources.each do |resource|
|
140
|
+
cache_key = cache_key_for(server, resource.uri)
|
141
|
+
@resource_cache[cache_key] = resource
|
142
|
+
resources << resource
|
143
|
+
end
|
144
|
+
rescue MCPClient::Errors::ConnectionError => e
|
145
|
+
# Fast-fail on authorization errors for better user experience
|
146
|
+
# If this is the first server or we haven't collected any resources yet,
|
147
|
+
# raise the auth error directly to avoid cascading error messages
|
148
|
+
raise e if e.message.include?('Authorization failed') && resources.empty?
|
149
|
+
|
150
|
+
# Store the error and try other servers
|
151
|
+
connection_errors << e
|
152
|
+
@logger.error("Server error: #{e.message}")
|
153
|
+
end
|
154
|
+
|
155
|
+
resources
|
156
|
+
end
|
157
|
+
|
158
|
+
# Reads a specific resource by URI
|
159
|
+
# @param uri [String] the URI of the resource to read
|
160
|
+
# @param server [String, Symbol, Integer, MCPClient::ServerBase, nil] optional server to use
|
161
|
+
# @return [Object] the resource contents
|
162
|
+
def read_resource(uri, server: nil)
|
163
|
+
resources = list_resources
|
164
|
+
|
165
|
+
if server
|
166
|
+
# Use the specified server
|
167
|
+
srv = select_server(server)
|
168
|
+
# Find the resource on this specific server
|
169
|
+
resource = resources.find { |r| r.uri == uri && r.server == srv }
|
170
|
+
unless resource
|
171
|
+
raise MCPClient::Errors::ResourceNotFound,
|
172
|
+
"Resource '#{uri}' not found on server '#{srv.name || srv.class.name}'"
|
173
|
+
end
|
174
|
+
else
|
175
|
+
# Find the resource across all servers
|
176
|
+
matching_resources = resources.select { |r| r.uri == uri }
|
177
|
+
|
178
|
+
if matching_resources.empty?
|
179
|
+
raise MCPClient::Errors::ResourceNotFound, "Resource '#{uri}' not found"
|
180
|
+
elsif matching_resources.size > 1
|
181
|
+
# If multiple matches, disambiguate with server names
|
182
|
+
server_names = matching_resources.map { |r| r.server&.name || 'unnamed' }
|
183
|
+
raise MCPClient::Errors::AmbiguousResourceURI,
|
184
|
+
"Multiple resources with URI '#{uri}' found across servers (#{server_names.join(', ')}). " \
|
185
|
+
"Please specify a server using the 'server' parameter."
|
186
|
+
end
|
187
|
+
|
188
|
+
resource = matching_resources.first
|
189
|
+
end
|
190
|
+
|
191
|
+
# Use the resource's associated server
|
192
|
+
server = resource.server
|
193
|
+
raise MCPClient::Errors::ServerNotFound, "No server found for resource '#{uri}'" unless server
|
194
|
+
|
195
|
+
begin
|
196
|
+
server.read_resource(uri)
|
197
|
+
rescue MCPClient::Errors::ConnectionError => e
|
198
|
+
# Add server identity information to the error for better context
|
199
|
+
server_id = server.name ? "#{server.class}[#{server.name}]" : server.class.name
|
200
|
+
raise MCPClient::Errors::ResourceReadError,
|
201
|
+
"Error reading resource '#{uri}': #{e.message} (Server: #{server_id})"
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
42
205
|
# Lists all available tools from all connected MCP servers
|
43
206
|
# @param cache [Boolean] whether to use cached tools or fetch fresh
|
44
207
|
# @return [Array<MCPClient::Tool>] list of available tools
|
@@ -52,7 +215,8 @@ module MCPClient
|
|
52
215
|
|
53
216
|
servers.each do |server|
|
54
217
|
server.list_tools.each do |tool|
|
55
|
-
|
218
|
+
cache_key = cache_key_for(server, tool.name)
|
219
|
+
@tool_cache[cache_key] = tool
|
56
220
|
tools << tool
|
57
221
|
end
|
58
222
|
rescue MCPClient::Errors::ConnectionError => e
|
@@ -162,6 +326,8 @@ module MCPClient
|
|
162
326
|
# @return [void]
|
163
327
|
def clear_cache
|
164
328
|
@tool_cache.clear
|
329
|
+
@prompt_cache.clear
|
330
|
+
@resource_cache.clear
|
165
331
|
end
|
166
332
|
|
167
333
|
# Register a callback for JSON-RPC notifications from servers
|
@@ -316,9 +482,11 @@ module MCPClient
|
|
316
482
|
when 'notifications/resources/updated'
|
317
483
|
logger.warn("[#{server_id}] Resource #{params['uri']} updated")
|
318
484
|
when 'notifications/prompts/list_changed'
|
319
|
-
logger.warn("[#{server_id}] Prompt list has changed")
|
485
|
+
logger.warn("[#{server_id}] Prompt list has changed, clearing prompt cache")
|
486
|
+
@prompt_cache.clear
|
320
487
|
when 'notifications/resources/list_changed'
|
321
|
-
logger.warn("[#{server_id}] Resource list has changed")
|
488
|
+
logger.warn("[#{server_id}] Resource list has changed, clearing resource cache")
|
489
|
+
@resource_cache.clear
|
322
490
|
else
|
323
491
|
# Log unknown notification types for debugging purposes
|
324
492
|
logger.debug("[#{server_id}] Received unknown notification: #{method} - #{params}")
|
@@ -379,5 +547,14 @@ module MCPClient
|
|
379
547
|
server.list_tools.any? { |t| t.name == tool.name }
|
380
548
|
end
|
381
549
|
end
|
550
|
+
|
551
|
+
# Generate a cache key for server-specific items
|
552
|
+
# @param server [MCPClient::ServerBase] the server
|
553
|
+
# @param item_id [String] the item identifier (name or URI)
|
554
|
+
# @return [String] composite cache key
|
555
|
+
def cache_key_for(server, item_id)
|
556
|
+
server_id = server.object_id.to_s
|
557
|
+
"#{server_id}:#{item_id}"
|
558
|
+
end
|
382
559
|
end
|
383
560
|
end
|
data/lib/mcp_client/errors.rb
CHANGED
@@ -9,12 +9,24 @@ module MCPClient
|
|
9
9
|
# Raised when a tool is not found
|
10
10
|
class ToolNotFound < MCPError; end
|
11
11
|
|
12
|
+
# Raised when a prompt is not found
|
13
|
+
class PromptNotFound < MCPError; end
|
14
|
+
|
15
|
+
# Raised when a resource is not found
|
16
|
+
class ResourceNotFound < MCPError; end
|
17
|
+
|
12
18
|
# Raised when a server is not found
|
13
19
|
class ServerNotFound < MCPError; end
|
14
20
|
|
15
21
|
# Raised when there's an error calling a tool
|
16
22
|
class ToolCallError < MCPError; end
|
17
23
|
|
24
|
+
# Raised when there's an error getting a prompt
|
25
|
+
class PromptGetError < MCPError; end
|
26
|
+
|
27
|
+
# Raised when there's an error reading a resource
|
28
|
+
class ResourceReadError < MCPError; end
|
29
|
+
|
18
30
|
# Raised when there's a connection error with an MCP server
|
19
31
|
class ConnectionError < MCPError; end
|
20
32
|
|
@@ -29,5 +41,11 @@ module MCPClient
|
|
29
41
|
|
30
42
|
# Raised when multiple tools with the same name exist across different servers
|
31
43
|
class AmbiguousToolName < MCPError; end
|
44
|
+
|
45
|
+
# Raised when multiple prompts with the same name exist across different servers
|
46
|
+
class AmbiguousPromptName < MCPError; end
|
47
|
+
|
48
|
+
# Raised when multiple resources with the same URI exist across different servers
|
49
|
+
class AmbiguousResourceURI < MCPError; end
|
32
50
|
end
|
33
51
|
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MCPClient
|
4
|
+
# Representation of an MCP prompt
|
5
|
+
class Prompt
|
6
|
+
# @!attribute [r] name
|
7
|
+
# @return [String] the name of the prompt
|
8
|
+
# @!attribute [r] description
|
9
|
+
# @return [String] the description of the prompt
|
10
|
+
# @!attribute [r] arguments
|
11
|
+
# @return [Hash] the JSON arguments for the prompt
|
12
|
+
# @!attribute [r] server
|
13
|
+
# @return [MCPClient::ServerBase, nil] the server this prompt belongs to
|
14
|
+
attr_reader :name, :description, :arguments, :server
|
15
|
+
|
16
|
+
# Initialize a new prompt
|
17
|
+
# @param name [String] the name of the prompt
|
18
|
+
# @param description [String] the description of the prompt
|
19
|
+
# @param arguments [Hash] the JSON arguments for the prompt
|
20
|
+
# @param server [MCPClient::ServerBase, nil] the server this prompt belongs to
|
21
|
+
def initialize(name:, description:, arguments: {}, server: nil)
|
22
|
+
@name = name
|
23
|
+
@description = description
|
24
|
+
@arguments = arguments
|
25
|
+
@server = server
|
26
|
+
end
|
27
|
+
|
28
|
+
# Create a Prompt instance from JSON data
|
29
|
+
# @param data [Hash] JSON data from MCP server
|
30
|
+
# @param server [MCPClient::ServerBase, nil] the server this prompt belongs to
|
31
|
+
# @return [MCPClient::Prompt] prompt instance
|
32
|
+
def self.from_json(data, server: nil)
|
33
|
+
new(
|
34
|
+
name: data['name'],
|
35
|
+
description: data['description'],
|
36
|
+
arguments: data['arguments'] || {},
|
37
|
+
server: server
|
38
|
+
)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MCPClient
|
4
|
+
# Representation of an MCP resource
|
5
|
+
class Resource
|
6
|
+
# @!attribute [r] uri
|
7
|
+
# @return [String] unique identifier for the resource
|
8
|
+
# @!attribute [r] name
|
9
|
+
# @return [String] the name of the resource
|
10
|
+
# @!attribute [r] title
|
11
|
+
# @return [String, nil] optional human-readable name of the resource for display purposes
|
12
|
+
# @!attribute [r] description
|
13
|
+
# @return [String, nil] optional description
|
14
|
+
# @!attribute [r] mime_type
|
15
|
+
# @return [String, nil] optional MIME type
|
16
|
+
# @!attribute [r] size
|
17
|
+
# @return [Integer, nil] optional size in bytes
|
18
|
+
# @!attribute [r] annotations
|
19
|
+
# @return [Hash, nil] optional annotations that provide hints to clients
|
20
|
+
# @!attribute [r] server
|
21
|
+
# @return [MCPClient::ServerBase, nil] the server this resource belongs to
|
22
|
+
attr_reader :uri, :name, :title, :description, :mime_type, :size, :annotations, :server
|
23
|
+
|
24
|
+
# Initialize a new resource
|
25
|
+
# @param uri [String] unique identifier for the resource
|
26
|
+
# @param name [String] the name of the resource
|
27
|
+
# @param title [String, nil] optional human-readable name of the resource for display purposes
|
28
|
+
# @param description [String, nil] optional description
|
29
|
+
# @param mime_type [String, nil] optional MIME type
|
30
|
+
# @param size [Integer, nil] optional size in bytes
|
31
|
+
# @param annotations [Hash, nil] optional annotations that provide hints to clients
|
32
|
+
# @param server [MCPClient::ServerBase, nil] the server this resource belongs to
|
33
|
+
def initialize(uri:, name:, title: nil, description: nil, mime_type: nil, size: nil, annotations: nil, server: nil)
|
34
|
+
@uri = uri
|
35
|
+
@name = name
|
36
|
+
@title = title
|
37
|
+
@description = description
|
38
|
+
@mime_type = mime_type
|
39
|
+
@size = size
|
40
|
+
@annotations = annotations
|
41
|
+
@server = server
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create a Resource instance from JSON data
|
45
|
+
# @param data [Hash] JSON data from MCP server
|
46
|
+
# @param server [MCPClient::ServerBase, nil] the server this resource belongs to
|
47
|
+
# @return [MCPClient::Resource] resource instance
|
48
|
+
def self.from_json(data, server: nil)
|
49
|
+
new(
|
50
|
+
uri: data['uri'],
|
51
|
+
name: data['name'],
|
52
|
+
title: data['title'],
|
53
|
+
description: data['description'],
|
54
|
+
mime_type: data['mimeType'],
|
55
|
+
size: data['size'],
|
56
|
+
annotations: data['annotations'],
|
57
|
+
server: server
|
58
|
+
)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -33,6 +33,33 @@ module MCPClient
|
|
33
33
|
raise NotImplementedError, 'Subclasses must implement call_tool'
|
34
34
|
end
|
35
35
|
|
36
|
+
# List all prompts available from the MCP server
|
37
|
+
# @return [Array<MCPClient::Prompt>] list of available prompts
|
38
|
+
def list_prompts
|
39
|
+
raise NotImplementedError, 'Subclasses must implement list_prompts'
|
40
|
+
end
|
41
|
+
|
42
|
+
# Get a prompt with the given parameters
|
43
|
+
# @param prompt_name [String] the name of the prompt to get
|
44
|
+
# @param parameters [Hash] the parameters to pass to the prompt
|
45
|
+
# @return [Object] the result of the prompt interpolation
|
46
|
+
def get_prompt(prompt_name, parameters)
|
47
|
+
raise NotImplementedError, 'Subclasses must implement get_prompt'
|
48
|
+
end
|
49
|
+
|
50
|
+
# List all resources available from the MCP server
|
51
|
+
# @return [Array<MCPClient::Resource>] list of available resources
|
52
|
+
def list_resources
|
53
|
+
raise NotImplementedError, 'Subclasses must implement list_resources'
|
54
|
+
end
|
55
|
+
|
56
|
+
# Read a resource by its URI
|
57
|
+
# @param uri [String] the URI of the resource to read
|
58
|
+
# @return [Object] the resource contents
|
59
|
+
def read_resource(uri)
|
60
|
+
raise NotImplementedError, 'Subclasses must implement read_resource'
|
61
|
+
end
|
62
|
+
|
36
63
|
# Clean up the server connection
|
37
64
|
def cleanup
|
38
65
|
raise NotImplementedError, 'Subclasses must implement cleanup'
|
@@ -7,6 +7,7 @@ module MCPClient
|
|
7
7
|
# JSON-RPC request/notification plumbing for SSE transport
|
8
8
|
module JsonRpcTransport
|
9
9
|
include JsonRpcCommon
|
10
|
+
|
10
11
|
# Generic JSON-RPC request: send method with params and return result
|
11
12
|
# @param method [String] JSON-RPC method name
|
12
13
|
# @param params [Hash] parameters for the request
|
@@ -17,9 +17,11 @@ module MCPClient
|
|
17
17
|
|
18
18
|
include SseParser
|
19
19
|
include JsonRpcTransport
|
20
|
+
|
20
21
|
require_relative 'server_sse/reconnect_monitor'
|
21
22
|
|
22
23
|
include ReconnectMonitor
|
24
|
+
|
23
25
|
# Ratio of close_after timeout to ping interval
|
24
26
|
CLOSE_AFTER_PING_RATIO = 2.5
|
25
27
|
|
@@ -36,7 +38,11 @@ module MCPClient
|
|
36
38
|
# @return [String] The base URL of the MCP server
|
37
39
|
# @!attribute [r] tools
|
38
40
|
# @return [Array<MCPClient::Tool>, nil] List of available tools (nil if not fetched yet)
|
39
|
-
|
41
|
+
# @!attribute [r] prompts
|
42
|
+
# @return [Array<MCPClient::Prompt>, nil] List of available prompts (nil if not fetched yet)
|
43
|
+
# @!attribute [r] resources
|
44
|
+
# @return [Array<MCPClient::Resource>, nil] List of available resources (nil if not fetched yet)
|
45
|
+
attr_reader :base_url, :tools, :prompts, :resources
|
40
46
|
|
41
47
|
# Server information from initialize response
|
42
48
|
# @return [Hash, nil] Server information
|
@@ -104,6 +110,104 @@ module MCPClient
|
|
104
110
|
end
|
105
111
|
end
|
106
112
|
|
113
|
+
# List all prompts available from the MCP server
|
114
|
+
# @return [Array<MCPClient::Prompt>] list of available prompts
|
115
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
116
|
+
# @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
|
117
|
+
# @raise [MCPClient::Errors::PromptGetError] for other errors during prompt listing
|
118
|
+
def list_prompts
|
119
|
+
@mutex.synchronize do
|
120
|
+
return @prompts if @prompts
|
121
|
+
end
|
122
|
+
|
123
|
+
begin
|
124
|
+
ensure_initialized
|
125
|
+
|
126
|
+
prompts_data = request_prompts_list
|
127
|
+
@mutex.synchronize do
|
128
|
+
@prompts = prompts_data.map do |prompt_data|
|
129
|
+
MCPClient::Prompt.from_json(prompt_data, server: self)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
@mutex.synchronize { @prompts }
|
134
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
135
|
+
# Re-raise these errors directly
|
136
|
+
raise
|
137
|
+
rescue StandardError => e
|
138
|
+
raise MCPClient::Errors::PromptGetError, "Error listing prompts: #{e.message}"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Get a prompt with the given parameters
|
143
|
+
# @param prompt_name [String] the name of the prompt to get
|
144
|
+
# @param parameters [Hash] the parameters to pass to the prompt
|
145
|
+
# @return [Object] the result of the prompt interpolation
|
146
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
147
|
+
# @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
|
148
|
+
# @raise [MCPClient::Errors::PromptGetError] for other errors during prompt interpolation
|
149
|
+
# @raise [MCPClient::Errors::ConnectionError] if server is disconnected
|
150
|
+
def get_prompt(prompt_name, parameters)
|
151
|
+
rpc_request('prompts/get', {
|
152
|
+
name: prompt_name,
|
153
|
+
arguments: parameters
|
154
|
+
})
|
155
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
156
|
+
# Re-raise connection/transport errors directly to match test expectations
|
157
|
+
raise
|
158
|
+
rescue StandardError => e
|
159
|
+
# For all other errors, wrap in PromptGetError
|
160
|
+
raise MCPClient::Errors::PromptGetError, "Error get prompt '#{prompt_name}': #{e.message}"
|
161
|
+
end
|
162
|
+
|
163
|
+
# List all resources available from the MCP server
|
164
|
+
# @return [Array<MCPClient::Resource>] list of available resources
|
165
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
166
|
+
# @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
|
167
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during resource listing
|
168
|
+
def list_resources
|
169
|
+
@mutex.synchronize do
|
170
|
+
return @resources if @resources
|
171
|
+
end
|
172
|
+
|
173
|
+
begin
|
174
|
+
ensure_initialized
|
175
|
+
|
176
|
+
resources_data = request_resources_list
|
177
|
+
@mutex.synchronize do
|
178
|
+
@resources = resources_data.map do |resource_data|
|
179
|
+
MCPClient::Resource.from_json(resource_data, server: self)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
@mutex.synchronize { @resources }
|
184
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
185
|
+
# Re-raise these errors directly
|
186
|
+
raise
|
187
|
+
rescue StandardError => e
|
188
|
+
raise MCPClient::Errors::ResourceReadError, "Error listing resources: #{e.message}"
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# Read a resource by its URI
|
193
|
+
# @param uri [String] the URI of the resource to read
|
194
|
+
# @return [Object] the resource contents
|
195
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
196
|
+
# @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
|
197
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during resource reading
|
198
|
+
# @raise [MCPClient::Errors::ConnectionError] if server is disconnected
|
199
|
+
def read_resource(uri)
|
200
|
+
rpc_request('resources/read', {
|
201
|
+
uri: uri
|
202
|
+
})
|
203
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
204
|
+
# Re-raise connection/transport errors directly to match test expectations
|
205
|
+
raise
|
206
|
+
rescue StandardError => e
|
207
|
+
# For all other errors, wrap in ResourceReadError
|
208
|
+
raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
|
209
|
+
end
|
210
|
+
|
107
211
|
# List all tools available from the MCP server
|
108
212
|
# @return [Array<MCPClient::Tool>] list of available tools
|
109
213
|
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
@@ -465,6 +569,58 @@ module MCPClient
|
|
465
569
|
raise MCPClient::Errors::ConnectionError, "Authorization failed: #{error_message}"
|
466
570
|
end
|
467
571
|
|
572
|
+
# Request the prompts list using JSON-RPC
|
573
|
+
# @return [Array<Hash>] the prompts data
|
574
|
+
# @raise [MCPClient::Errors::PromptGetError] if prompts list retrieval fails
|
575
|
+
# @private
|
576
|
+
def request_prompts_list
|
577
|
+
@mutex.synchronize do
|
578
|
+
return @prompts_data if @prompts_data
|
579
|
+
end
|
580
|
+
|
581
|
+
result = rpc_request('prompts/list')
|
582
|
+
|
583
|
+
if result && result['prompts']
|
584
|
+
@mutex.synchronize do
|
585
|
+
@prompts_data = result['prompts']
|
586
|
+
end
|
587
|
+
return @mutex.synchronize { @prompts_data.dup }
|
588
|
+
elsif result
|
589
|
+
@mutex.synchronize do
|
590
|
+
@prompts_data = result
|
591
|
+
end
|
592
|
+
return @mutex.synchronize { @prompts_data.dup }
|
593
|
+
end
|
594
|
+
|
595
|
+
raise MCPClient::Errors::PromptGetError, 'Failed to get prompts list from JSON-RPC request'
|
596
|
+
end
|
597
|
+
|
598
|
+
# Request the resources list using JSON-RPC
|
599
|
+
# @return [Array<Hash>] the resources data
|
600
|
+
# @raise [MCPClient::Errors::ResourceReadError] if resources list retrieval fails
|
601
|
+
# @private
|
602
|
+
def request_resources_list
|
603
|
+
@mutex.synchronize do
|
604
|
+
return @resources_data if @resources_data
|
605
|
+
end
|
606
|
+
|
607
|
+
result = rpc_request('resources/list')
|
608
|
+
|
609
|
+
if result && result['resources']
|
610
|
+
@mutex.synchronize do
|
611
|
+
@resources_data = result['resources']
|
612
|
+
end
|
613
|
+
return @mutex.synchronize { @resources_data.dup }
|
614
|
+
elsif result
|
615
|
+
@mutex.synchronize do
|
616
|
+
@resources_data = result
|
617
|
+
end
|
618
|
+
return @mutex.synchronize { @resources_data.dup }
|
619
|
+
end
|
620
|
+
|
621
|
+
raise MCPClient::Errors::ResourceReadError, 'Failed to get resources list from JSON-RPC request'
|
622
|
+
end
|
623
|
+
|
468
624
|
# Request the tools list using JSON-RPC
|
469
625
|
# @return [Array<Hash>] the tools data
|
470
626
|
# @raise [MCPClient::Errors::ToolCallError] if tools list retrieval fails
|
@@ -7,6 +7,7 @@ module MCPClient
|
|
7
7
|
# JSON-RPC request/notification plumbing for stdio transport
|
8
8
|
module JsonRpcTransport
|
9
9
|
include JsonRpcCommon
|
10
|
+
|
10
11
|
# Ensure the server process is started and initialized (handshake)
|
11
12
|
# @return [void]
|
12
13
|
# @raise [MCPClient::Errors::ConnectionError] if initialization fails
|
@@ -102,6 +102,97 @@ module MCPClient
|
|
102
102
|
# Skip non-JSONRPC lines in the output stream
|
103
103
|
end
|
104
104
|
|
105
|
+
# List all prompts available from the MCP server
|
106
|
+
# @return [Array<MCPClient::Prompt>] list of available prompts
|
107
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
108
|
+
# @raise [MCPClient::Errors::PromptGetError] for other errors during prompt listing
|
109
|
+
def list_prompts
|
110
|
+
ensure_initialized
|
111
|
+
req_id = next_id
|
112
|
+
req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'prompts/list', 'params' => {} }
|
113
|
+
send_request(req)
|
114
|
+
res = wait_response(req_id)
|
115
|
+
if (err = res['error'])
|
116
|
+
raise MCPClient::Errors::ServerError, err['message']
|
117
|
+
end
|
118
|
+
|
119
|
+
(res.dig('result', 'prompts') || []).map { |td| MCPClient::Prompt.from_json(td, server: self) }
|
120
|
+
rescue StandardError => e
|
121
|
+
raise MCPClient::Errors::PromptGetError, "Error listing prompts: #{e.message}"
|
122
|
+
end
|
123
|
+
|
124
|
+
# Get a prompt with the given parameters
|
125
|
+
# @param prompt_name [String] the name of the prompt to get
|
126
|
+
# @param parameters [Hash] the parameters to pass to the prompt
|
127
|
+
# @return [Object] the result of the prompt interpolation
|
128
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
129
|
+
# @raise [MCPClient::Errors::PromptGetError] for other errors during prompt interpolation
|
130
|
+
def get_prompt(prompt_name, parameters)
|
131
|
+
ensure_initialized
|
132
|
+
req_id = next_id
|
133
|
+
# JSON-RPC method for getting a prompt
|
134
|
+
req = {
|
135
|
+
'jsonrpc' => '2.0',
|
136
|
+
'id' => req_id,
|
137
|
+
'method' => 'prompts/get',
|
138
|
+
'params' => { 'name' => prompt_name, 'arguments' => parameters }
|
139
|
+
}
|
140
|
+
send_request(req)
|
141
|
+
res = wait_response(req_id)
|
142
|
+
if (err = res['error'])
|
143
|
+
raise MCPClient::Errors::ServerError, err['message']
|
144
|
+
end
|
145
|
+
|
146
|
+
res['result']
|
147
|
+
rescue StandardError => e
|
148
|
+
raise MCPClient::Errors::PromptGetError, "Error calling prompt '#{prompt_name}': #{e.message}"
|
149
|
+
end
|
150
|
+
|
151
|
+
# List all resources available from the MCP server
|
152
|
+
# @return [Array<MCPClient::Resource>] list of available resources
|
153
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
154
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during resource listing
|
155
|
+
def list_resources
|
156
|
+
ensure_initialized
|
157
|
+
req_id = next_id
|
158
|
+
req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'resources/list', 'params' => {} }
|
159
|
+
send_request(req)
|
160
|
+
res = wait_response(req_id)
|
161
|
+
if (err = res['error'])
|
162
|
+
raise MCPClient::Errors::ServerError, err['message']
|
163
|
+
end
|
164
|
+
|
165
|
+
(res.dig('result', 'resources') || []).map { |td| MCPClient::Resource.from_json(td, server: self) }
|
166
|
+
rescue StandardError => e
|
167
|
+
raise MCPClient::Errors::ResourceReadError, "Error listing resources: #{e.message}"
|
168
|
+
end
|
169
|
+
|
170
|
+
# Read a resource by its URI
|
171
|
+
# @param uri [String] the URI of the resource to read
|
172
|
+
# @return [Object] the resource contents
|
173
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
174
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during resource reading
|
175
|
+
def read_resource(uri)
|
176
|
+
ensure_initialized
|
177
|
+
req_id = next_id
|
178
|
+
# JSON-RPC method for reading a resource
|
179
|
+
req = {
|
180
|
+
'jsonrpc' => '2.0',
|
181
|
+
'id' => req_id,
|
182
|
+
'method' => 'resources/read',
|
183
|
+
'params' => { 'uri' => uri }
|
184
|
+
}
|
185
|
+
send_request(req)
|
186
|
+
res = wait_response(req_id)
|
187
|
+
if (err = res['error'])
|
188
|
+
raise MCPClient::Errors::ServerError, err['message']
|
189
|
+
end
|
190
|
+
|
191
|
+
res['result']
|
192
|
+
rescue StandardError => e
|
193
|
+
raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
|
194
|
+
end
|
195
|
+
|
105
196
|
# List all tools available from the MCP server
|
106
197
|
# @return [Array<MCPClient::Tool>] list of available tools
|
107
198
|
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
@@ -99,6 +99,10 @@ module MCPClient
|
|
99
99
|
@read_timeout = opts[:read_timeout]
|
100
100
|
@tools = nil
|
101
101
|
@tools_data = nil
|
102
|
+
@prompts = nil
|
103
|
+
@prompts_data = nil
|
104
|
+
@resources = nil
|
105
|
+
@resources_data = nil
|
102
106
|
@request_id = 0
|
103
107
|
@mutex = Monitor.new
|
104
108
|
@connection_established = false
|
@@ -211,6 +215,93 @@ module MCPClient
|
|
211
215
|
end
|
212
216
|
end
|
213
217
|
|
218
|
+
# List all prompts available from the MCP server
|
219
|
+
# @return [Array<MCPClient::Prompt>] list of available prompts
|
220
|
+
# @raise [MCPClient::Errors::PromptGetError] if prompts list retrieval fails
|
221
|
+
def list_prompts
|
222
|
+
@mutex.synchronize do
|
223
|
+
return @prompts if @prompts
|
224
|
+
end
|
225
|
+
|
226
|
+
begin
|
227
|
+
ensure_connected
|
228
|
+
|
229
|
+
prompts_data = request_prompts_list
|
230
|
+
@mutex.synchronize do
|
231
|
+
@prompts = prompts_data.map do |prompt_data|
|
232
|
+
MCPClient::Prompt.from_json(prompt_data, server: self)
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
@mutex.synchronize { @prompts }
|
237
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
238
|
+
# Re-raise these errors directly
|
239
|
+
raise
|
240
|
+
rescue StandardError => e
|
241
|
+
raise MCPClient::Errors::PromptGetError, "Error listing prompts: #{e.message}"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
# Get a prompt with the given parameters
|
246
|
+
# @param prompt_name [String] the name of the prompt to get
|
247
|
+
# @param parameters [Hash] the parameters to pass to the prompt
|
248
|
+
# @return [Object] the result of the prompt (with string keys for backward compatibility)
|
249
|
+
# @raise [MCPClient::Errors::PromptGetError] if prompt retrieval fails
|
250
|
+
def get_prompt(prompt_name, parameters)
|
251
|
+
rpc_request('prompts/get', {
|
252
|
+
name: prompt_name,
|
253
|
+
arguments: parameters.except(:_meta),
|
254
|
+
**parameters.slice(:_meta)
|
255
|
+
})
|
256
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
257
|
+
# Re-raise connection/transport errors directly
|
258
|
+
raise
|
259
|
+
rescue StandardError => e
|
260
|
+
# For all other errors, wrap in PromptGetError
|
261
|
+
raise MCPClient::Errors::PromptGetError, "Error getting prompt '#{prompt_name}': #{e.message}"
|
262
|
+
end
|
263
|
+
|
264
|
+
# List all resources available from the MCP server
|
265
|
+
# @return [Array<MCPClient::Resource>] list of available resources
|
266
|
+
# @raise [MCPClient::Errors::ResourceReadError] if resources list retrieval fails
|
267
|
+
def list_resources
|
268
|
+
@mutex.synchronize do
|
269
|
+
return @resources if @resources
|
270
|
+
end
|
271
|
+
|
272
|
+
begin
|
273
|
+
ensure_connected
|
274
|
+
|
275
|
+
resources_data = request_resources_list
|
276
|
+
@mutex.synchronize do
|
277
|
+
@resources = resources_data.map do |resource_data|
|
278
|
+
MCPClient::Resource.from_json(resource_data, server: self)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
@mutex.synchronize { @resources }
|
283
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
284
|
+
# Re-raise these errors directly
|
285
|
+
raise
|
286
|
+
rescue StandardError => e
|
287
|
+
raise MCPClient::Errors::ResourceReadError, "Error listing resources: #{e.message}"
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
# Read a resource by its URI
|
292
|
+
# @param uri [String] the URI of the resource to read
|
293
|
+
# @return [Object] the resource contents
|
294
|
+
# @raise [MCPClient::Errors::ResourceReadError] if resource reading fails
|
295
|
+
def read_resource(uri)
|
296
|
+
rpc_request('resources/read', { uri: uri })
|
297
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
298
|
+
# Re-raise connection/transport errors directly
|
299
|
+
raise
|
300
|
+
rescue StandardError => e
|
301
|
+
# For all other errors, wrap in ResourceReadError
|
302
|
+
raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
|
303
|
+
end
|
304
|
+
|
214
305
|
# Override apply_request_headers to add session and SSE headers for MCP protocol
|
215
306
|
def apply_request_headers(req, request)
|
216
307
|
super
|
@@ -294,6 +385,10 @@ module MCPClient
|
|
294
385
|
# Clear cached data
|
295
386
|
@tools = nil
|
296
387
|
@tools_data = nil
|
388
|
+
@prompts = nil
|
389
|
+
@prompts_data = nil
|
390
|
+
@resources = nil
|
391
|
+
@resources_data = nil
|
297
392
|
@buffer = ''
|
298
393
|
|
299
394
|
@logger.info('Cleanup completed')
|
@@ -381,6 +476,56 @@ module MCPClient
|
|
381
476
|
raise MCPClient::Errors::ToolCallError, 'Failed to get tools list from JSON-RPC request'
|
382
477
|
end
|
383
478
|
|
479
|
+
# Request the prompts list using JSON-RPC
|
480
|
+
# @return [Array<Hash>] the prompts data
|
481
|
+
# @raise [MCPClient::Errors::PromptGetError] if prompts list retrieval fails
|
482
|
+
def request_prompts_list
|
483
|
+
@mutex.synchronize do
|
484
|
+
return @prompts_data if @prompts_data
|
485
|
+
end
|
486
|
+
|
487
|
+
result = rpc_request('prompts/list')
|
488
|
+
|
489
|
+
if result.is_a?(Hash) && result['prompts']
|
490
|
+
@mutex.synchronize do
|
491
|
+
@prompts_data = result['prompts']
|
492
|
+
end
|
493
|
+
return @mutex.synchronize { @prompts_data.dup }
|
494
|
+
elsif result.is_a?(Array) || result
|
495
|
+
@mutex.synchronize do
|
496
|
+
@prompts_data = result
|
497
|
+
end
|
498
|
+
return @mutex.synchronize { @prompts_data.dup }
|
499
|
+
end
|
500
|
+
|
501
|
+
raise MCPClient::Errors::PromptGetError, 'Failed to get prompts list from JSON-RPC request'
|
502
|
+
end
|
503
|
+
|
504
|
+
# Request the resources list using JSON-RPC
|
505
|
+
# @return [Array<Hash>] the resources data
|
506
|
+
# @raise [MCPClient::Errors::ResourceReadError] if resources list retrieval fails
|
507
|
+
def request_resources_list
|
508
|
+
@mutex.synchronize do
|
509
|
+
return @resources_data if @resources_data
|
510
|
+
end
|
511
|
+
|
512
|
+
result = rpc_request('resources/list')
|
513
|
+
|
514
|
+
if result.is_a?(Hash) && result['resources']
|
515
|
+
@mutex.synchronize do
|
516
|
+
@resources_data = result['resources']
|
517
|
+
end
|
518
|
+
return @mutex.synchronize { @resources_data.dup }
|
519
|
+
elsif result.is_a?(Array) || result
|
520
|
+
@mutex.synchronize do
|
521
|
+
@resources_data = result
|
522
|
+
end
|
523
|
+
return @mutex.synchronize { @resources_data.dup }
|
524
|
+
end
|
525
|
+
|
526
|
+
raise MCPClient::Errors::ResourceReadError, 'Failed to get resources list from JSON-RPC request'
|
527
|
+
end
|
528
|
+
|
384
529
|
# Start the long-lived GET connection for server events
|
385
530
|
# Creates a separate thread to maintain SSE connection for server notifications
|
386
531
|
# @return [void]
|
data/lib/mcp_client/version.rb
CHANGED
data/lib/mcp_client.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
# Load all MCPClient components
|
4
4
|
require_relative 'mcp_client/errors'
|
5
5
|
require_relative 'mcp_client/tool'
|
6
|
+
require_relative 'mcp_client/prompt'
|
7
|
+
require_relative 'mcp_client/resource'
|
6
8
|
require_relative 'mcp_client/server_base'
|
7
9
|
require_relative 'mcp_client/server_stdio'
|
8
10
|
require_relative 'mcp_client/server_sse'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ruby-mcp-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Szymon Kurcab
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-09-
|
11
|
+
date: 2025-09-16 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -127,6 +127,8 @@ files:
|
|
127
127
|
- lib/mcp_client/http_transport_base.rb
|
128
128
|
- lib/mcp_client/json_rpc_common.rb
|
129
129
|
- lib/mcp_client/oauth_client.rb
|
130
|
+
- lib/mcp_client/prompt.rb
|
131
|
+
- lib/mcp_client/resource.rb
|
130
132
|
- lib/mcp_client/server_base.rb
|
131
133
|
- lib/mcp_client/server_factory.rb
|
132
134
|
- lib/mcp_client/server_http.rb
|