ruby-mcp-client 0.7.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e71d5420a77345079ef2da077240e6a578900c3acfd2c16c904b0a2bf84a8bb7
4
- data.tar.gz: ea4f41ebef3d5898d3dd0786d50d08dbbd19057bbbd5b7aa8cc046d5a1608d7c
3
+ metadata.gz: 27b97462ec0c99d98299df120726f90944f495aaddf198272d72725544240ec0
4
+ data.tar.gz: b58b8a6ba53f698abe1cb3576fba8030d66cb0bd7eafa2e39e28943e7ef1337f
5
5
  SHA512:
6
- metadata.gz: a837ba7337913a453e5ae2567c67fed453b5e1cab3712705689c74b2efff27fa5c257750c834b5ac5bfff04333f08f1999b6813982d0c548ee92f4992c4d6aad
7
- data.tar.gz: e10f2262b5dd10f0323dbbf870daa9f2d25d2d9f6a7f0c053870a8e0a5453ed9c5ed2c667211dfcf094c4fd56e7c12fbc1407c358e955fb14b4cbf4601312226
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
- # 2. An array of server objects:
107
- # [{ "type": "stdio", "command": "npx server" }, { "type": "sse", "url": "http://..." }, { "type": "http", "url": "http://..." }]
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": "http", "url": "http://..." } } }
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
@@ -288,12 +322,12 @@ require 'logger'
288
322
  logger = Logger.new($stdout)
289
323
  logger.level = Logger::INFO
290
324
 
291
- # Create an MCP client that connects to a Playwright MCP server via SSE
325
+ # Create an MCP client that connects to a Playwright MCP server via Streamable HTTP
292
326
  # First run: npx @playwright/mcp@latest --port 8931
293
- sse_client = MCPClient.create_client(
327
+ mcp_client = MCPClient.create_client(
294
328
  mcp_server_configs: [
295
- MCPClient.sse_config(
296
- base_url: 'http://localhost:8931/sse',
329
+ MCPClient.streamable_http_config(
330
+ base_url: 'http://localhost:8931/mcp',
297
331
  read_timeout: 30, # Timeout in seconds for request fulfillment
298
332
  ping: 10, # Send ping after 10 seconds of inactivity
299
333
  # Connection closes automatically after inactivity (2.5x ping interval)
@@ -304,44 +338,50 @@ sse_client = MCPClient.create_client(
304
338
  )
305
339
 
306
340
  # List available tools
307
- tools = sse_client.list_tools
341
+ tools = mcp_client.list_tools
308
342
 
309
343
  # Launch a browser
310
- result = sse_client.call_tool('browser_install', {})
311
- result = sse_client.call_tool('browser_navigate', { url: 'about:blank' })
344
+ result = mcp_client.call_tool('browser_install', {})
345
+ result = mcp_client.call_tool('browser_navigate', { url: 'about:blank' })
312
346
  # No browser ID needed with these tool names
313
347
 
314
348
  # Create a new page
315
- page_result = sse_client.call_tool('browser_tab_new', {})
349
+ page_result = mcp_client.call_tool('browser_tab', {action: 'create'})
316
350
  # No page ID needed with these tool names
317
351
 
318
352
  # Navigate to a website
319
- sse_client.call_tool('browser_navigate', { url: 'https://example.com' })
353
+ mcp_client.call_tool('browser_navigate', { url: 'https://example.com' })
320
354
 
321
355
  # Get page title
322
- title_result = sse_client.call_tool('browser_snapshot', {})
356
+ title_result = mcp_client.call_tool('browser_snapshot', {})
323
357
  puts "Page snapshot: #{title_result}"
324
358
 
325
359
  # Take a screenshot
326
- screenshot_result = sse_client.call_tool('browser_take_screenshot', {})
360
+ screenshot_result = mcp_client.call_tool('browser_take_screenshot', {})
327
361
 
328
362
  # Ping the server to verify connectivity
329
- ping_result = sse_client.ping
363
+ ping_result = mcp_client.ping
330
364
  puts "Ping successful: #{ping_result.inspect}"
331
365
 
332
366
  # Clean up
333
- sse_client.cleanup
367
+ mcp_client.cleanup
334
368
  ```
335
369
 
336
- See `examples/mcp_sse_server_example.rb` for the full Playwright SSE example.
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 a complete FastMCP server example that demonstrates the Ruby MCP client working with a Python FastMCP server:
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 available tools
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 4 interactive tools
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
- - `mcp_sse_server_example.rb` - SSE transport with Playwright MCP
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
 
@@ -485,7 +583,7 @@ You can define MCP server configurations in JSON files for easier management:
485
583
  "mcpServers": {
486
584
  "playwright": {
487
585
  "type": "sse",
488
- "url": "http://localhost:8931/sse",
586
+ "url": "http://localhost:8931/mcp",
489
587
  "headers": {
490
588
  "Authorization": "Bearer TOKEN"
491
589
  }
@@ -517,7 +615,7 @@ A simpler example used in the Playwright demo (found in `examples/sample_server_
517
615
  {
518
616
  "mcpServers": {
519
617
  "playwright": {
520
- "url": "http://localhost:8931/sse",
618
+ "url": "http://localhost:8931/mcp",
521
619
  "headers": {},
522
620
  "comment": "Local Playwright MCP Server running on port 8931"
523
621
  }
@@ -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
@@ -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
- @tool_cache[tool.name] = tool
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
@@ -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