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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fd6b4269c644ed783b7d574e5a60b26ef0c749c3fe0a58fc86b4d7d5d8786a0d
4
- data.tar.gz: 58eb4935ec413db8478dc29983f5baa68d7c28627968ffb5189d1b9754090a51
3
+ metadata.gz: 27b97462ec0c99d98299df120726f90944f495aaddf198272d72725544240ec0
4
+ data.tar.gz: b58b8a6ba53f698abe1cb3576fba8030d66cb0bd7eafa2e39e28943e7ef1337f
5
5
  SHA512:
6
- metadata.gz: ee86e5f1e58fb98df9a9a7e715049eb8d7ae5f182430060633c9f97d7b2268d1a02859403ace69f6a400344cffad80dd9238169a638e27491a6bc99ce70fb462
7
- data.tar.gz: 27e82788b84bc0887c273ab3c1277180e85f4930f413500fab4dbf2a7474dcb151bf510ec2509caf7a95bf53311dee587e65d8826b226e7ef164d9acd640582a
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
@@ -333,15 +367,21 @@ puts "Ping successful: #{ping_result.inspect}"
333
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
 
@@ -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
@@ -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
- attr_reader :base_url, :tools
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]
@@ -2,7 +2,7 @@
2
2
 
3
3
  module MCPClient
4
4
  # Current version of the MCP client gem
5
- VERSION = '0.7.3'
5
+ VERSION = '0.8.0'
6
6
 
7
7
  # MCP protocol version (date-based) - unified across all transports
8
8
  PROTOCOL_VERSION = '2025-03-26'
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.7.3
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-01 00:00:00.000000000 Z
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