ruby-mcp-client 0.8.0 → 0.8.1
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 +150 -6
- data/lib/mcp_client/client.rb +86 -44
- data/lib/mcp_client/resource_content.rb +80 -0
- data/lib/mcp_client/resource_template.rb +57 -0
- data/lib/mcp_client/server_base.rb +31 -3
- data/lib/mcp_client/server_http.rb +142 -0
- data/lib/mcp_client/server_sse.rb +72 -12
- data/lib/mcp_client/server_stdio/json_rpc_transport.rb +5 -0
- data/lib/mcp_client/server_stdio.rb +97 -6
- data/lib/mcp_client/server_streamable_http.rb +66 -10
- 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: 7e0949102c5c22625bef68e2f5b4a0e2aecdfc636495ecba52d16f663c35f2fc
|
4
|
+
data.tar.gz: fbeb7484c1458775b88aa074422913e9d09d8a37b23aac38c7edf82e2111b9cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2e75071c0fceb5061037b1537d9e71fd8f03dc5c94aa6c2bbc2a63ce6f24ae16c1c2c2cdb560f48e12ca4115224561f89043dde2145116d1e4e397c9cb7e409
|
7
|
+
data.tar.gz: d3900ae79aba5d896a1496bc28af3e9ec8f0b7066f30eb0723cd1664845769dd09afd130faa2db0b4b1b67d948f15e3532cfb5abcd3b076d0e7547e7e89671fb
|
data/README.md
CHANGED
@@ -50,7 +50,7 @@ This Ruby MCP Client implements key features from the latest MCP specification (
|
|
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
52
|
- **Prompts Support** - Full implementation of MCP prompts for dynamic content generation
|
53
|
-
- **Resources Support** -
|
53
|
+
- **Resources Support** - Full MCP resources specification compliance including templates, subscriptions, pagination, and annotations
|
54
54
|
|
55
55
|
## Usage
|
56
56
|
|
@@ -165,11 +165,30 @@ result = client.get_prompt('greeting', { name: 'Ruby Developer' })
|
|
165
165
|
result = client.get_prompt('greeting', { name: 'Ruby Developer' }, server: 'filesystem')
|
166
166
|
|
167
167
|
# === Working with Resources ===
|
168
|
-
# List available resources from all servers
|
169
|
-
|
168
|
+
# List available resources from all servers (returns hash with 'resources' array and optional 'nextCursor')
|
169
|
+
result = client.list_resources
|
170
|
+
resources = result['resources'] # Array of Resource objects from all servers
|
171
|
+
next_cursor = result['nextCursor'] # nil when aggregating multiple servers
|
172
|
+
|
173
|
+
# Get resources from a specific server with pagination support
|
174
|
+
result = client.servers.first.list_resources
|
175
|
+
resources = result['resources'] # Array of Resource objects
|
176
|
+
next_cursor = result['nextCursor'] # For pagination
|
177
|
+
|
178
|
+
# List resources with pagination (only works with single server or client.list_resources with cursor)
|
179
|
+
result = client.list_resources(cursor: next_cursor) # Uses first server when cursor provided
|
170
180
|
|
171
|
-
# Read a specific resource by URI
|
181
|
+
# Read a specific resource by URI (returns array of ResourceContent objects)
|
172
182
|
contents = client.read_resource('file:///example.txt')
|
183
|
+
contents.each do |content|
|
184
|
+
if content.text?
|
185
|
+
puts content.text # Text content
|
186
|
+
elsif content.binary?
|
187
|
+
data = Base64.decode64(content.blob) # Binary content
|
188
|
+
end
|
189
|
+
puts content.mime_type if content.mime_type
|
190
|
+
puts content.annotations if content.annotations # Optional metadata
|
191
|
+
end
|
173
192
|
|
174
193
|
# Read a resource from a specific server
|
175
194
|
contents = client.read_resource('file:///example.txt', server: 'filesystem')
|
@@ -413,11 +432,16 @@ prompts.each { |prompt| puts "- #{prompt.name}: #{prompt.description}" }
|
|
413
432
|
greeting = client.get_prompt('greeting', { name: 'Ruby Developer' })
|
414
433
|
|
415
434
|
# List and read resources
|
416
|
-
|
435
|
+
result = client.list_resources
|
436
|
+
resources = result['resources']
|
417
437
|
puts "Found #{resources.length} resources:"
|
418
438
|
resources.each { |resource| puts "- #{resource.name} (#{resource.uri})" }
|
419
439
|
|
420
|
-
|
440
|
+
# Read resource (returns array of ResourceContent objects)
|
441
|
+
contents = client.read_resource('file:///sample/README.md')
|
442
|
+
contents.each do |content|
|
443
|
+
puts content.text if content.text?
|
444
|
+
end
|
421
445
|
```
|
422
446
|
|
423
447
|
#### Streamable HTTP Example
|
@@ -764,6 +788,126 @@ token = oauth_provider.complete_authorization_flow(code, state)
|
|
764
788
|
|
765
789
|
For complete OAuth documentation, see [OAUTH.md](OAUTH.md).
|
766
790
|
|
791
|
+
## Resources
|
792
|
+
|
793
|
+
The Ruby MCP Client provides full support for the MCP resources specification, enabling access to files, data, and other content with URI-based identification.
|
794
|
+
|
795
|
+
### Resource Features
|
796
|
+
|
797
|
+
- **Resource listing** with cursor-based pagination
|
798
|
+
- **Resource templates** with URI patterns (RFC 6570)
|
799
|
+
- **Resource subscriptions** for real-time updates
|
800
|
+
- **Binary content support** with base64 encoding
|
801
|
+
- **Resource annotations** for metadata (audience, priority, lastModified)
|
802
|
+
- **ResourceContent objects** for structured content access
|
803
|
+
|
804
|
+
### Resource API
|
805
|
+
|
806
|
+
```ruby
|
807
|
+
# Get a server instance
|
808
|
+
server = client.servers.first # or client.find_server('name')
|
809
|
+
|
810
|
+
# List resources with pagination
|
811
|
+
result = server.list_resources
|
812
|
+
resources = result['resources'] # Array of Resource objects
|
813
|
+
next_cursor = result['nextCursor'] # String cursor for next page (if any)
|
814
|
+
|
815
|
+
# Get next page of resources
|
816
|
+
if next_cursor
|
817
|
+
next_result = server.list_resources(cursor: next_cursor)
|
818
|
+
end
|
819
|
+
|
820
|
+
# Access Resource properties
|
821
|
+
resource = resources.first
|
822
|
+
resource.uri # "file:///example.txt"
|
823
|
+
resource.name # "example.txt"
|
824
|
+
resource.title # Optional human-readable title
|
825
|
+
resource.description # Optional description
|
826
|
+
resource.mime_type # "text/plain"
|
827
|
+
resource.size # Optional file size in bytes
|
828
|
+
resource.annotations # Optional metadata hash
|
829
|
+
|
830
|
+
# Read resource contents (returns array of ResourceContent objects)
|
831
|
+
contents = server.read_resource(resource.uri)
|
832
|
+
|
833
|
+
contents.each do |content|
|
834
|
+
# Check content type
|
835
|
+
if content.text?
|
836
|
+
# Text content
|
837
|
+
text = content.text
|
838
|
+
mime = content.mime_type # e.g., "text/plain"
|
839
|
+
elsif content.binary?
|
840
|
+
# Binary content (base64 encoded)
|
841
|
+
blob = content.blob # Base64 string
|
842
|
+
data = Base64.decode64(blob) # Decoded binary data
|
843
|
+
end
|
844
|
+
|
845
|
+
# Access optional annotations
|
846
|
+
if content.annotations
|
847
|
+
audience = content.annotations['audience'] # e.g., ["user", "assistant"]
|
848
|
+
priority = content.annotations['priority'] # e.g., 0.5
|
849
|
+
modified = content.annotations['lastModified'] # ISO 8601 timestamp
|
850
|
+
end
|
851
|
+
end
|
852
|
+
|
853
|
+
# List resource templates
|
854
|
+
templates_result = server.list_resource_templates
|
855
|
+
templates = templates_result['resourceTemplates'] # Array of ResourceTemplate objects
|
856
|
+
|
857
|
+
template = templates.first
|
858
|
+
template.uri_template # "file:///{path}" (RFC 6570 URI template)
|
859
|
+
template.name # Template name
|
860
|
+
template.title # Optional title
|
861
|
+
template.description # Optional description
|
862
|
+
template.mime_type # Optional MIME type hint
|
863
|
+
|
864
|
+
# Subscribe to resource updates
|
865
|
+
server.subscribe_resource('file:///watched.txt') # Returns true on success
|
866
|
+
|
867
|
+
# Unsubscribe from resource updates
|
868
|
+
server.unsubscribe_resource('file:///watched.txt') # Returns true on success
|
869
|
+
|
870
|
+
# Check server capabilities for resources
|
871
|
+
capabilities = server.capabilities
|
872
|
+
if capabilities['resources']
|
873
|
+
can_subscribe = capabilities['resources']['subscribe'] # true/false
|
874
|
+
list_changed = capabilities['resources']['listChanged'] # true/false
|
875
|
+
end
|
876
|
+
```
|
877
|
+
|
878
|
+
### Working with ResourceContent
|
879
|
+
|
880
|
+
The `ResourceContent` class provides a structured way to handle both text and binary content:
|
881
|
+
|
882
|
+
```ruby
|
883
|
+
# ResourceContent for text
|
884
|
+
content = MCPClient::ResourceContent.new(
|
885
|
+
uri: 'file:///example.txt',
|
886
|
+
name: 'example.txt',
|
887
|
+
mime_type: 'text/plain',
|
888
|
+
text: 'File contents here',
|
889
|
+
annotations: {
|
890
|
+
'audience' => ['user'],
|
891
|
+
'priority' => 1.0
|
892
|
+
}
|
893
|
+
)
|
894
|
+
|
895
|
+
# ResourceContent for binary data
|
896
|
+
binary_content = MCPClient::ResourceContent.new(
|
897
|
+
uri: 'file:///image.png',
|
898
|
+
name: 'image.png',
|
899
|
+
mime_type: 'image/png',
|
900
|
+
blob: Base64.strict_encode64(binary_data)
|
901
|
+
)
|
902
|
+
|
903
|
+
# Access content
|
904
|
+
if content.text?
|
905
|
+
puts content.text
|
906
|
+
elsif content.binary?
|
907
|
+
data = Base64.decode64(content.blob)
|
908
|
+
end
|
909
|
+
```
|
910
|
+
|
767
911
|
## Key Features
|
768
912
|
|
769
913
|
### Client Features
|
data/lib/mcp_client/client.rb
CHANGED
@@ -126,17 +126,32 @@ module MCPClient
|
|
126
126
|
|
127
127
|
# Lists all available resources from all connected MCP servers
|
128
128
|
# @param cache [Boolean] whether to use cached resources or fetch fresh
|
129
|
-
# @
|
129
|
+
# @param cursor [String, nil] optional cursor for pagination (only works with single server)
|
130
|
+
# @return [Hash] result containing 'resources' array and optional 'nextCursor'
|
130
131
|
# @raise [MCPClient::Errors::ConnectionError] on authorization failures
|
131
132
|
# @raise [MCPClient::Errors::ResourceReadError] if no resources could be retrieved from any server
|
132
|
-
def list_resources(cache: true)
|
133
|
-
|
133
|
+
def list_resources(cache: true, cursor: nil)
|
134
|
+
# If cursor is provided, we can only query one server (the one that provided the cursor)
|
135
|
+
# This is a limitation of aggregating multiple servers
|
136
|
+
if cursor
|
137
|
+
# For now, just use the first server when cursor is provided
|
138
|
+
# In a real implementation, you'd need to track which server the cursor came from
|
139
|
+
return servers.first.list_resources(cursor: cursor) if servers.any?
|
140
|
+
|
141
|
+
return { 'resources' => [], 'nextCursor' => nil }
|
142
|
+
end
|
143
|
+
|
144
|
+
# Use cache if available and no cursor
|
145
|
+
return { 'resources' => @resource_cache.values, 'nextCursor' => nil } if cache && !@resource_cache.empty?
|
134
146
|
|
135
147
|
resources = []
|
136
148
|
connection_errors = []
|
137
149
|
|
138
150
|
servers.each do |server|
|
139
|
-
server.list_resources
|
151
|
+
result = server.list_resources
|
152
|
+
resource_list = result['resources'] || []
|
153
|
+
|
154
|
+
resource_list.each do |resource|
|
140
155
|
cache_key = cache_key_for(server, resource.uri)
|
141
156
|
@resource_cache[cache_key] = resource
|
142
157
|
resources << resource
|
@@ -152,7 +167,8 @@ module MCPClient
|
|
152
167
|
@logger.error("Server error: #{e.message}")
|
153
168
|
end
|
154
169
|
|
155
|
-
|
170
|
+
# Return hash format consistent with server methods
|
171
|
+
{ 'resources' => resources, 'nextCursor' => nil }
|
156
172
|
end
|
157
173
|
|
158
174
|
# Reads a specific resource by URI
|
@@ -160,46 +176,16 @@ module MCPClient
|
|
160
176
|
# @param server [String, Symbol, Integer, MCPClient::ServerBase, nil] optional server to use
|
161
177
|
# @return [Object] the resource contents
|
162
178
|
def read_resource(uri, server: nil)
|
163
|
-
|
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 }
|
179
|
+
result = list_resources
|
180
|
+
resources = result['resources'] || []
|
177
181
|
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
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
|
182
|
+
resource = if server
|
183
|
+
find_resource_on_server(uri, resources, server)
|
184
|
+
else
|
185
|
+
find_resource_across_servers(uri, resources)
|
186
|
+
end
|
187
187
|
|
188
|
-
|
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
|
188
|
+
execute_resource_read(resource, uri)
|
203
189
|
end
|
204
190
|
|
205
191
|
# Lists all available tools from all connected MCP servers
|
@@ -478,7 +464,7 @@ module MCPClient
|
|
478
464
|
case method
|
479
465
|
when 'notifications/tools/list_changed'
|
480
466
|
logger.warn("[#{server_id}] Tool list has changed, clearing tool cache")
|
481
|
-
|
467
|
+
@tool_cache.clear
|
482
468
|
when 'notifications/resources/updated'
|
483
469
|
logger.warn("[#{server_id}] Resource #{params['uri']} updated")
|
484
470
|
when 'notifications/prompts/list_changed'
|
@@ -556,5 +542,61 @@ module MCPClient
|
|
556
542
|
server_id = server.object_id.to_s
|
557
543
|
"#{server_id}:#{item_id}"
|
558
544
|
end
|
545
|
+
|
546
|
+
# Find a resource on a specific server
|
547
|
+
# @param uri [String] the URI of the resource
|
548
|
+
# @param resources [Array<Resource>] available resources
|
549
|
+
# @param server [String, Symbol, Integer, MCPClient::ServerBase] server selector
|
550
|
+
# @return [Resource] the found resource
|
551
|
+
# @raise [MCPClient::Errors::ResourceNotFound] if resource not found
|
552
|
+
def find_resource_on_server(uri, resources, server)
|
553
|
+
srv = select_server(server)
|
554
|
+
resource = resources.find { |r| r.uri == uri && r.server == srv }
|
555
|
+
unless resource
|
556
|
+
raise MCPClient::Errors::ResourceNotFound,
|
557
|
+
"Resource '#{uri}' not found on server '#{srv.name || srv.class.name}'"
|
558
|
+
end
|
559
|
+
resource
|
560
|
+
end
|
561
|
+
|
562
|
+
# Find a resource across all servers
|
563
|
+
# @param uri [String] the URI of the resource
|
564
|
+
# @param resources [Array<Resource>] available resources
|
565
|
+
# @return [Resource] the found resource
|
566
|
+
# @raise [MCPClient::Errors::ResourceNotFound] if resource not found
|
567
|
+
# @raise [MCPClient::Errors::AmbiguousResourceURI] if multiple resources found
|
568
|
+
def find_resource_across_servers(uri, resources)
|
569
|
+
matching_resources = resources.select { |r| r.uri == uri }
|
570
|
+
|
571
|
+
if matching_resources.empty?
|
572
|
+
raise MCPClient::Errors::ResourceNotFound, "Resource '#{uri}' not found"
|
573
|
+
elsif matching_resources.size > 1
|
574
|
+
server_names = matching_resources.map { |r| r.server&.name || 'unnamed' }
|
575
|
+
raise MCPClient::Errors::AmbiguousResourceURI,
|
576
|
+
"Multiple resources with URI '#{uri}' found across servers (#{server_names.join(', ')}). " \
|
577
|
+
"Please specify a server using the 'server' parameter."
|
578
|
+
end
|
579
|
+
|
580
|
+
matching_resources.first
|
581
|
+
end
|
582
|
+
|
583
|
+
# Execute the resource read operation
|
584
|
+
# @param resource [Resource] the resource to read
|
585
|
+
# @param uri [String] the URI of the resource
|
586
|
+
# @return [Object] the resource contents
|
587
|
+
# @raise [MCPClient::Errors::ServerNotFound] if no server found
|
588
|
+
# @raise [MCPClient::Errors::ResourceReadError] on read errors
|
589
|
+
def execute_resource_read(resource, uri)
|
590
|
+
server = resource.server
|
591
|
+
raise MCPClient::Errors::ServerNotFound, "No server found for resource '#{uri}'" unless server
|
592
|
+
|
593
|
+
begin
|
594
|
+
server.read_resource(uri)
|
595
|
+
rescue MCPClient::Errors::ConnectionError => e
|
596
|
+
server_id = server.name ? "#{server.class}[#{server.name}]" : server.class.name
|
597
|
+
raise MCPClient::Errors::ResourceReadError,
|
598
|
+
"Error reading resource '#{uri}': #{e.message} (Server: #{server_id})"
|
599
|
+
end
|
600
|
+
end
|
559
601
|
end
|
560
602
|
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MCPClient
|
4
|
+
# Representation of MCP resource content
|
5
|
+
# Resources can contain either text or binary data
|
6
|
+
class ResourceContent
|
7
|
+
# @!attribute [r] uri
|
8
|
+
# @return [String] unique identifier for the resource
|
9
|
+
# @!attribute [r] name
|
10
|
+
# @return [String] the name of the resource
|
11
|
+
# @!attribute [r] title
|
12
|
+
# @return [String, nil] optional human-readable name for display purposes
|
13
|
+
# @!attribute [r] mime_type
|
14
|
+
# @return [String, nil] optional MIME type
|
15
|
+
# @!attribute [r] text
|
16
|
+
# @return [String, nil] text content (mutually exclusive with blob)
|
17
|
+
# @!attribute [r] blob
|
18
|
+
# @return [String, nil] base64-encoded binary content (mutually exclusive with text)
|
19
|
+
# @!attribute [r] annotations
|
20
|
+
# @return [Hash, nil] optional annotations that provide hints to clients
|
21
|
+
attr_reader :uri, :name, :title, :mime_type, :text, :blob, :annotations
|
22
|
+
|
23
|
+
# Initialize resource content
|
24
|
+
# @param uri [String] unique identifier for the resource
|
25
|
+
# @param name [String] the name of the resource
|
26
|
+
# @param title [String, nil] optional human-readable name for display purposes
|
27
|
+
# @param mime_type [String, nil] optional MIME type
|
28
|
+
# @param text [String, nil] text content (mutually exclusive with blob)
|
29
|
+
# @param blob [String, nil] base64-encoded binary content (mutually exclusive with text)
|
30
|
+
# @param annotations [Hash, nil] optional annotations that provide hints to clients
|
31
|
+
def initialize(uri:, name:, title: nil, mime_type: nil, text: nil, blob: nil, annotations: nil)
|
32
|
+
raise ArgumentError, 'ResourceContent cannot have both text and blob' if text && blob
|
33
|
+
raise ArgumentError, 'ResourceContent must have either text or blob' if !text && !blob
|
34
|
+
|
35
|
+
@uri = uri
|
36
|
+
@name = name
|
37
|
+
@title = title
|
38
|
+
@mime_type = mime_type
|
39
|
+
@text = text
|
40
|
+
@blob = blob
|
41
|
+
@annotations = annotations
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create a ResourceContent instance from JSON data
|
45
|
+
# @param data [Hash] JSON data from MCP server
|
46
|
+
# @return [MCPClient::ResourceContent] resource content instance
|
47
|
+
def self.from_json(data)
|
48
|
+
new(
|
49
|
+
uri: data['uri'],
|
50
|
+
name: data['name'],
|
51
|
+
title: data['title'],
|
52
|
+
mime_type: data['mimeType'],
|
53
|
+
text: data['text'],
|
54
|
+
blob: data['blob'],
|
55
|
+
annotations: data['annotations']
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Check if content is text
|
60
|
+
# @return [Boolean] true if content is text
|
61
|
+
def text?
|
62
|
+
!@text.nil?
|
63
|
+
end
|
64
|
+
|
65
|
+
# Check if content is binary
|
66
|
+
# @return [Boolean] true if content is binary
|
67
|
+
def binary?
|
68
|
+
!@blob.nil?
|
69
|
+
end
|
70
|
+
|
71
|
+
# Get the content (text or decoded blob)
|
72
|
+
# @return [String] the content
|
73
|
+
def content
|
74
|
+
return @text if text?
|
75
|
+
|
76
|
+
require 'base64'
|
77
|
+
Base64.decode64(@blob)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module MCPClient
|
4
|
+
# Representation of an MCP resource template
|
5
|
+
# Resource templates allow servers to expose parameterized resources using URI templates
|
6
|
+
class ResourceTemplate
|
7
|
+
# @!attribute [r] uri_template
|
8
|
+
# @return [String] URI template following RFC 6570
|
9
|
+
# @!attribute [r] name
|
10
|
+
# @return [String] the name of the resource template
|
11
|
+
# @!attribute [r] title
|
12
|
+
# @return [String, nil] optional human-readable name for display purposes
|
13
|
+
# @!attribute [r] description
|
14
|
+
# @return [String, nil] optional description
|
15
|
+
# @!attribute [r] mime_type
|
16
|
+
# @return [String, nil] optional MIME type for resources created from this template
|
17
|
+
# @!attribute [r] annotations
|
18
|
+
# @return [Hash, nil] optional annotations that provide hints to clients
|
19
|
+
# @!attribute [r] server
|
20
|
+
# @return [MCPClient::ServerBase, nil] the server this resource template belongs to
|
21
|
+
attr_reader :uri_template, :name, :title, :description, :mime_type, :annotations, :server
|
22
|
+
|
23
|
+
# Initialize a new resource template
|
24
|
+
# @param uri_template [String] URI template following RFC 6570
|
25
|
+
# @param name [String] the name of the resource template
|
26
|
+
# @param title [String, nil] optional human-readable name for display purposes
|
27
|
+
# @param description [String, nil] optional description
|
28
|
+
# @param mime_type [String, nil] optional MIME type
|
29
|
+
# @param annotations [Hash, nil] optional annotations that provide hints to clients
|
30
|
+
# @param server [MCPClient::ServerBase, nil] the server this resource template belongs to
|
31
|
+
def initialize(uri_template:, name:, title: nil, description: nil, mime_type: nil, annotations: nil, server: nil)
|
32
|
+
@uri_template = uri_template
|
33
|
+
@name = name
|
34
|
+
@title = title
|
35
|
+
@description = description
|
36
|
+
@mime_type = mime_type
|
37
|
+
@annotations = annotations
|
38
|
+
@server = server
|
39
|
+
end
|
40
|
+
|
41
|
+
# Create a ResourceTemplate instance from JSON data
|
42
|
+
# @param data [Hash] JSON data from MCP server
|
43
|
+
# @param server [MCPClient::ServerBase, nil] the server this resource template belongs to
|
44
|
+
# @return [MCPClient::ResourceTemplate] resource template instance
|
45
|
+
def self.from_json(data, server: nil)
|
46
|
+
new(
|
47
|
+
uri_template: data['uriTemplate'],
|
48
|
+
name: data['name'],
|
49
|
+
title: data['title'],
|
50
|
+
description: data['description'],
|
51
|
+
mime_type: data['mimeType'],
|
52
|
+
annotations: data['annotations'],
|
53
|
+
server: server
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -48,18 +48,46 @@ module MCPClient
|
|
48
48
|
end
|
49
49
|
|
50
50
|
# List all resources available from the MCP server
|
51
|
-
# @
|
52
|
-
|
51
|
+
# @param cursor [String, nil] optional cursor for pagination
|
52
|
+
# @return [Hash] result containing resources array and optional nextCursor
|
53
|
+
def list_resources(cursor: nil)
|
53
54
|
raise NotImplementedError, 'Subclasses must implement list_resources'
|
54
55
|
end
|
55
56
|
|
56
57
|
# Read a resource by its URI
|
57
58
|
# @param uri [String] the URI of the resource to read
|
58
|
-
# @return [
|
59
|
+
# @return [Array<MCPClient::ResourceContent>] array of resource contents
|
59
60
|
def read_resource(uri)
|
60
61
|
raise NotImplementedError, 'Subclasses must implement read_resource'
|
61
62
|
end
|
62
63
|
|
64
|
+
# List all resource templates available from the MCP server
|
65
|
+
# @param cursor [String, nil] optional cursor for pagination
|
66
|
+
# @return [Hash] result containing resourceTemplates array and optional nextCursor
|
67
|
+
def list_resource_templates(cursor: nil)
|
68
|
+
raise NotImplementedError, 'Subclasses must implement list_resource_templates'
|
69
|
+
end
|
70
|
+
|
71
|
+
# Subscribe to resource updates
|
72
|
+
# @param uri [String] the URI of the resource to subscribe to
|
73
|
+
# @return [Boolean] true if subscription successful
|
74
|
+
def subscribe_resource(uri)
|
75
|
+
raise NotImplementedError, 'Subclasses must implement subscribe_resource'
|
76
|
+
end
|
77
|
+
|
78
|
+
# Unsubscribe from resource updates
|
79
|
+
# @param uri [String] the URI of the resource to unsubscribe from
|
80
|
+
# @return [Boolean] true if unsubscription successful
|
81
|
+
def unsubscribe_resource(uri)
|
82
|
+
raise NotImplementedError, 'Subclasses must implement unsubscribe_resource'
|
83
|
+
end
|
84
|
+
|
85
|
+
# Get server capabilities
|
86
|
+
# @return [Hash, nil] server capabilities
|
87
|
+
def capabilities
|
88
|
+
raise NotImplementedError, 'Subclasses must implement capabilities'
|
89
|
+
end
|
90
|
+
|
63
91
|
# Clean up the server connection
|
64
92
|
def cleanup
|
65
93
|
raise NotImplementedError, 'Subclasses must implement cleanup'
|
@@ -216,6 +216,148 @@ module MCPClient
|
|
216
216
|
end
|
217
217
|
end
|
218
218
|
|
219
|
+
# List all prompts available from the MCP server
|
220
|
+
# @return [Array<MCPClient::Prompt>] list of available prompts
|
221
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
222
|
+
# @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
|
223
|
+
# @raise [MCPClient::Errors::PromptGetError] for other errors during prompt listing
|
224
|
+
def list_prompts
|
225
|
+
@mutex.synchronize do
|
226
|
+
return @prompts if @prompts
|
227
|
+
end
|
228
|
+
|
229
|
+
begin
|
230
|
+
ensure_connected
|
231
|
+
|
232
|
+
prompts_data = rpc_request('prompts/list')
|
233
|
+
prompts = prompts_data['prompts'] || []
|
234
|
+
|
235
|
+
@mutex.synchronize do
|
236
|
+
@prompts = prompts.map do |prompt_data|
|
237
|
+
MCPClient::Prompt.from_json(prompt_data, server: self)
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
@mutex.synchronize { @prompts }
|
242
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
243
|
+
raise
|
244
|
+
rescue StandardError => e
|
245
|
+
raise MCPClient::Errors::PromptGetError, "Error listing prompts: #{e.message}"
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# Get a prompt with the given parameters
|
250
|
+
# @param prompt_name [String] the name of the prompt to get
|
251
|
+
# @param parameters [Hash] the parameters to pass to the prompt
|
252
|
+
# @return [Object] the result of the prompt interpolation
|
253
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
254
|
+
# @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
|
255
|
+
# @raise [MCPClient::Errors::PromptGetError] for other errors during prompt interpolation
|
256
|
+
def get_prompt(prompt_name, parameters)
|
257
|
+
rpc_request('prompts/get', {
|
258
|
+
name: prompt_name,
|
259
|
+
arguments: parameters
|
260
|
+
})
|
261
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
262
|
+
raise
|
263
|
+
rescue StandardError => e
|
264
|
+
raise MCPClient::Errors::PromptGetError, "Error getting prompt '#{prompt_name}': #{e.message}"
|
265
|
+
end
|
266
|
+
|
267
|
+
# List all resources available from the MCP server
|
268
|
+
# @param cursor [String, nil] optional cursor for pagination
|
269
|
+
# @return [Hash] result containing resources array and optional nextCursor
|
270
|
+
# @raise [MCPClient::Errors::ResourceReadError] if resources list retrieval fails
|
271
|
+
def list_resources(cursor: nil)
|
272
|
+
@mutex.synchronize do
|
273
|
+
return @resources_result if @resources_result && !cursor
|
274
|
+
end
|
275
|
+
|
276
|
+
begin
|
277
|
+
ensure_connected
|
278
|
+
|
279
|
+
params = {}
|
280
|
+
params['cursor'] = cursor if cursor
|
281
|
+
result = rpc_request('resources/list', params)
|
282
|
+
|
283
|
+
resources = (result['resources'] || []).map do |resource_data|
|
284
|
+
MCPClient::Resource.from_json(resource_data, server: self)
|
285
|
+
end
|
286
|
+
|
287
|
+
resources_result = { 'resources' => resources, 'nextCursor' => result['nextCursor'] }
|
288
|
+
|
289
|
+
@mutex.synchronize do
|
290
|
+
@resources_result = resources_result unless cursor
|
291
|
+
end
|
292
|
+
|
293
|
+
resources_result
|
294
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
295
|
+
raise
|
296
|
+
rescue StandardError => e
|
297
|
+
raise MCPClient::Errors::ResourceReadError, "Error listing resources: #{e.message}"
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
# Read a resource by its URI
|
302
|
+
# @param uri [String] the URI of the resource to read
|
303
|
+
# @return [Array<MCPClient::ResourceContent>] array of resource contents
|
304
|
+
# @raise [MCPClient::Errors::ResourceReadError] if resource reading fails
|
305
|
+
def read_resource(uri)
|
306
|
+
result = rpc_request('resources/read', { uri: uri })
|
307
|
+
contents = result['contents'] || []
|
308
|
+
contents.map { |content| MCPClient::ResourceContent.from_json(content) }
|
309
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
310
|
+
raise
|
311
|
+
rescue StandardError => e
|
312
|
+
raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
|
313
|
+
end
|
314
|
+
|
315
|
+
# List all resource templates available from the MCP server
|
316
|
+
# @param cursor [String, nil] optional cursor for pagination
|
317
|
+
# @return [Hash] result containing resourceTemplates array and optional nextCursor
|
318
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during resource template listing
|
319
|
+
def list_resource_templates(cursor: nil)
|
320
|
+
params = {}
|
321
|
+
params['cursor'] = cursor if cursor
|
322
|
+
result = rpc_request('resources/templates/list', params)
|
323
|
+
|
324
|
+
templates = (result['resourceTemplates'] || []).map do |template_data|
|
325
|
+
MCPClient::ResourceTemplate.from_json(template_data, server: self)
|
326
|
+
end
|
327
|
+
|
328
|
+
{ 'resourceTemplates' => templates, 'nextCursor' => result['nextCursor'] }
|
329
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
330
|
+
raise
|
331
|
+
rescue StandardError => e
|
332
|
+
raise MCPClient::Errors::ResourceReadError, "Error listing resource templates: #{e.message}"
|
333
|
+
end
|
334
|
+
|
335
|
+
# Subscribe to resource updates
|
336
|
+
# @param uri [String] the URI of the resource to subscribe to
|
337
|
+
# @return [Boolean] true if subscription successful
|
338
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during subscription
|
339
|
+
def subscribe_resource(uri)
|
340
|
+
rpc_request('resources/subscribe', { uri: uri })
|
341
|
+
true
|
342
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
343
|
+
raise
|
344
|
+
rescue StandardError => e
|
345
|
+
raise MCPClient::Errors::ResourceReadError, "Error subscribing to resource '#{uri}': #{e.message}"
|
346
|
+
end
|
347
|
+
|
348
|
+
# Unsubscribe from resource updates
|
349
|
+
# @param uri [String] the URI of the resource to unsubscribe from
|
350
|
+
# @return [Boolean] true if unsubscription successful
|
351
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during unsubscription
|
352
|
+
def unsubscribe_resource(uri)
|
353
|
+
rpc_request('resources/unsubscribe', { uri: uri })
|
354
|
+
true
|
355
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
356
|
+
raise
|
357
|
+
rescue StandardError => e
|
358
|
+
raise MCPClient::Errors::ResourceReadError, "Error unsubscribing from resource '#{uri}': #{e.message}"
|
359
|
+
end
|
360
|
+
|
219
361
|
# Stream tool call (default implementation returns single-value stream)
|
220
362
|
# @param tool_name [String] the name of the tool to call
|
221
363
|
# @param parameters [Hash] the parameters to pass to the tool
|
@@ -161,26 +161,34 @@ module MCPClient
|
|
161
161
|
end
|
162
162
|
|
163
163
|
# List all resources available from the MCP server
|
164
|
-
# @
|
164
|
+
# @param cursor [String, nil] optional cursor for pagination
|
165
|
+
# @return [Hash] result containing resources array and optional nextCursor
|
165
166
|
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
166
167
|
# @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
|
167
168
|
# @raise [MCPClient::Errors::ResourceReadError] for other errors during resource listing
|
168
|
-
def list_resources
|
169
|
+
def list_resources(cursor: nil)
|
169
170
|
@mutex.synchronize do
|
170
|
-
return @
|
171
|
+
return @resources_result if @resources_result && !cursor
|
171
172
|
end
|
172
173
|
|
173
174
|
begin
|
174
175
|
ensure_initialized
|
175
176
|
|
176
|
-
|
177
|
+
params = {}
|
178
|
+
params['cursor'] = cursor if cursor
|
179
|
+
result = rpc_request('resources/list', params)
|
180
|
+
|
181
|
+
resources = (result['resources'] || []).map do |resource_data|
|
182
|
+
MCPClient::Resource.from_json(resource_data, server: self)
|
183
|
+
end
|
184
|
+
|
185
|
+
resources_result = { 'resources' => resources, 'nextCursor' => result['nextCursor'] }
|
186
|
+
|
177
187
|
@mutex.synchronize do
|
178
|
-
@
|
179
|
-
MCPClient::Resource.from_json(resource_data, server: self)
|
180
|
-
end
|
188
|
+
@resources_result = resources_result unless cursor
|
181
189
|
end
|
182
190
|
|
183
|
-
|
191
|
+
resources_result
|
184
192
|
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
185
193
|
# Re-raise these errors directly
|
186
194
|
raise
|
@@ -191,15 +199,15 @@ module MCPClient
|
|
191
199
|
|
192
200
|
# Read a resource by its URI
|
193
201
|
# @param uri [String] the URI of the resource to read
|
194
|
-
# @return [
|
202
|
+
# @return [Array<MCPClient::ResourceContent>] array of resource contents
|
195
203
|
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
196
204
|
# @raise [MCPClient::Errors::TransportError] if response isn't valid JSON
|
197
205
|
# @raise [MCPClient::Errors::ResourceReadError] for other errors during resource reading
|
198
206
|
# @raise [MCPClient::Errors::ConnectionError] if server is disconnected
|
199
207
|
def read_resource(uri)
|
200
|
-
rpc_request('resources/read', {
|
201
|
-
|
202
|
-
|
208
|
+
result = rpc_request('resources/read', { uri: uri })
|
209
|
+
contents = result['contents'] || []
|
210
|
+
contents.map { |content| MCPClient::ResourceContent.from_json(content) }
|
203
211
|
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
204
212
|
# Re-raise connection/transport errors directly to match test expectations
|
205
213
|
raise
|
@@ -208,6 +216,58 @@ module MCPClient
|
|
208
216
|
raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
|
209
217
|
end
|
210
218
|
|
219
|
+
# List all resource templates available from the MCP server
|
220
|
+
# @param cursor [String, nil] optional cursor for pagination
|
221
|
+
# @return [Hash] result containing resourceTemplates array and optional nextCursor
|
222
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
223
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during resource template listing
|
224
|
+
def list_resource_templates(cursor: nil)
|
225
|
+
ensure_initialized
|
226
|
+
params = {}
|
227
|
+
params['cursor'] = cursor if cursor
|
228
|
+
result = rpc_request('resources/templates/list', params)
|
229
|
+
|
230
|
+
templates = (result['resourceTemplates'] || []).map do |template_data|
|
231
|
+
MCPClient::ResourceTemplate.from_json(template_data, server: self)
|
232
|
+
end
|
233
|
+
|
234
|
+
{ 'resourceTemplates' => templates, 'nextCursor' => result['nextCursor'] }
|
235
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
236
|
+
raise
|
237
|
+
rescue StandardError => e
|
238
|
+
raise MCPClient::Errors::ResourceReadError, "Error listing resource templates: #{e.message}"
|
239
|
+
end
|
240
|
+
|
241
|
+
# Subscribe to resource updates
|
242
|
+
# @param uri [String] the URI of the resource to subscribe to
|
243
|
+
# @return [Boolean] true if subscription successful
|
244
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
245
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during subscription
|
246
|
+
def subscribe_resource(uri)
|
247
|
+
ensure_initialized
|
248
|
+
rpc_request('resources/subscribe', { uri: uri })
|
249
|
+
true
|
250
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
251
|
+
raise
|
252
|
+
rescue StandardError => e
|
253
|
+
raise MCPClient::Errors::ResourceReadError, "Error subscribing to resource '#{uri}': #{e.message}"
|
254
|
+
end
|
255
|
+
|
256
|
+
# Unsubscribe from resource updates
|
257
|
+
# @param uri [String] the URI of the resource to unsubscribe from
|
258
|
+
# @return [Boolean] true if unsubscription successful
|
259
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
260
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during unsubscription
|
261
|
+
def unsubscribe_resource(uri)
|
262
|
+
ensure_initialized
|
263
|
+
rpc_request('resources/unsubscribe', { uri: uri })
|
264
|
+
true
|
265
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
266
|
+
raise
|
267
|
+
rescue StandardError => e
|
268
|
+
raise MCPClient::Errors::ResourceReadError, "Error unsubscribing from resource '#{uri}': #{e.message}"
|
269
|
+
end
|
270
|
+
|
211
271
|
# List all tools available from the MCP server
|
212
272
|
# @return [Array<MCPClient::Tool>] list of available tools
|
213
273
|
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
@@ -34,6 +34,11 @@ module MCPClient
|
|
34
34
|
raise MCPClient::Errors::ConnectionError, "Initialize failed: #{err['message']}"
|
35
35
|
end
|
36
36
|
|
37
|
+
# Store server info and capabilities
|
38
|
+
result = res['result'] || {}
|
39
|
+
@server_info = result['serverInfo']
|
40
|
+
@capabilities = result['capabilities']
|
41
|
+
|
37
42
|
# Send initialized notification
|
38
43
|
notif = build_jsonrpc_notification('notifications/initialized', {})
|
39
44
|
@stdin.puts(notif.to_json)
|
@@ -39,6 +39,8 @@ module MCPClient
|
|
39
39
|
@next_id = 1
|
40
40
|
@pending = {}
|
41
41
|
@initialized = false
|
42
|
+
@server_info = nil
|
43
|
+
@capabilities = nil
|
42
44
|
initialize_logger(logger)
|
43
45
|
@max_retries = retries
|
44
46
|
@retry_backoff = retry_backoff
|
@@ -46,6 +48,14 @@ module MCPClient
|
|
46
48
|
@env = env || {}
|
47
49
|
end
|
48
50
|
|
51
|
+
# Server info from the initialize response
|
52
|
+
# @return [Hash, nil] Server information
|
53
|
+
attr_reader :server_info
|
54
|
+
|
55
|
+
# Server capabilities from the initialize response
|
56
|
+
# @return [Hash, nil] Server capabilities
|
57
|
+
attr_reader :capabilities
|
58
|
+
|
49
59
|
# Connect to the MCP server by launching the command process via stdin/stdout
|
50
60
|
# @return [Boolean] true if connection was successful
|
51
61
|
# @raise [MCPClient::Errors::ConnectionError] if connection fails
|
@@ -149,27 +159,32 @@ module MCPClient
|
|
149
159
|
end
|
150
160
|
|
151
161
|
# List all resources available from the MCP server
|
152
|
-
# @
|
162
|
+
# @param cursor [String, nil] optional cursor for pagination
|
163
|
+
# @return [Hash] result containing resources array and optional nextCursor
|
153
164
|
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
154
165
|
# @raise [MCPClient::Errors::ResourceReadError] for other errors during resource listing
|
155
|
-
def list_resources
|
166
|
+
def list_resources(cursor: nil)
|
156
167
|
ensure_initialized
|
157
168
|
req_id = next_id
|
158
|
-
|
169
|
+
params = {}
|
170
|
+
params['cursor'] = cursor if cursor
|
171
|
+
req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'resources/list', 'params' => params }
|
159
172
|
send_request(req)
|
160
173
|
res = wait_response(req_id)
|
161
174
|
if (err = res['error'])
|
162
175
|
raise MCPClient::Errors::ServerError, err['message']
|
163
176
|
end
|
164
177
|
|
165
|
-
|
178
|
+
result = res['result'] || {}
|
179
|
+
resources = (result['resources'] || []).map { |td| MCPClient::Resource.from_json(td, server: self) }
|
180
|
+
{ 'resources' => resources, 'nextCursor' => result['nextCursor'] }
|
166
181
|
rescue StandardError => e
|
167
182
|
raise MCPClient::Errors::ResourceReadError, "Error listing resources: #{e.message}"
|
168
183
|
end
|
169
184
|
|
170
185
|
# Read a resource by its URI
|
171
186
|
# @param uri [String] the URI of the resource to read
|
172
|
-
# @return [
|
187
|
+
# @return [Array<MCPClient::ResourceContent>] array of resource contents
|
173
188
|
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
174
189
|
# @raise [MCPClient::Errors::ResourceReadError] for other errors during resource reading
|
175
190
|
def read_resource(uri)
|
@@ -188,11 +203,87 @@ module MCPClient
|
|
188
203
|
raise MCPClient::Errors::ServerError, err['message']
|
189
204
|
end
|
190
205
|
|
191
|
-
res['result']
|
206
|
+
result = res['result'] || {}
|
207
|
+
contents = result['contents'] || []
|
208
|
+
contents.map { |content| MCPClient::ResourceContent.from_json(content) }
|
192
209
|
rescue StandardError => e
|
193
210
|
raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
|
194
211
|
end
|
195
212
|
|
213
|
+
# List all resource templates available from the MCP server
|
214
|
+
# @param cursor [String, nil] optional cursor for pagination
|
215
|
+
# @return [Hash] result containing resourceTemplates array and optional nextCursor
|
216
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
217
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during resource template listing
|
218
|
+
def list_resource_templates(cursor: nil)
|
219
|
+
ensure_initialized
|
220
|
+
req_id = next_id
|
221
|
+
params = {}
|
222
|
+
params['cursor'] = cursor if cursor
|
223
|
+
req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'resources/templates/list', 'params' => params }
|
224
|
+
send_request(req)
|
225
|
+
res = wait_response(req_id)
|
226
|
+
if (err = res['error'])
|
227
|
+
raise MCPClient::Errors::ServerError, err['message']
|
228
|
+
end
|
229
|
+
|
230
|
+
result = res['result'] || {}
|
231
|
+
templates = (result['resourceTemplates'] || []).map { |td| MCPClient::ResourceTemplate.from_json(td, server: self) }
|
232
|
+
{ 'resourceTemplates' => templates, 'nextCursor' => result['nextCursor'] }
|
233
|
+
rescue StandardError => e
|
234
|
+
raise MCPClient::Errors::ResourceReadError, "Error listing resource templates: #{e.message}"
|
235
|
+
end
|
236
|
+
|
237
|
+
# Subscribe to resource updates
|
238
|
+
# @param uri [String] the URI of the resource to subscribe to
|
239
|
+
# @return [Boolean] true if subscription successful
|
240
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
241
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during subscription
|
242
|
+
def subscribe_resource(uri)
|
243
|
+
ensure_initialized
|
244
|
+
req_id = next_id
|
245
|
+
req = {
|
246
|
+
'jsonrpc' => '2.0',
|
247
|
+
'id' => req_id,
|
248
|
+
'method' => 'resources/subscribe',
|
249
|
+
'params' => { 'uri' => uri }
|
250
|
+
}
|
251
|
+
send_request(req)
|
252
|
+
res = wait_response(req_id)
|
253
|
+
if (err = res['error'])
|
254
|
+
raise MCPClient::Errors::ServerError, err['message']
|
255
|
+
end
|
256
|
+
|
257
|
+
true
|
258
|
+
rescue StandardError => e
|
259
|
+
raise MCPClient::Errors::ResourceReadError, "Error subscribing to resource '#{uri}': #{e.message}"
|
260
|
+
end
|
261
|
+
|
262
|
+
# Unsubscribe from resource updates
|
263
|
+
# @param uri [String] the URI of the resource to unsubscribe from
|
264
|
+
# @return [Boolean] true if unsubscription successful
|
265
|
+
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
266
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during unsubscription
|
267
|
+
def unsubscribe_resource(uri)
|
268
|
+
ensure_initialized
|
269
|
+
req_id = next_id
|
270
|
+
req = {
|
271
|
+
'jsonrpc' => '2.0',
|
272
|
+
'id' => req_id,
|
273
|
+
'method' => 'resources/unsubscribe',
|
274
|
+
'params' => { 'uri' => uri }
|
275
|
+
}
|
276
|
+
send_request(req)
|
277
|
+
res = wait_response(req_id)
|
278
|
+
if (err = res['error'])
|
279
|
+
raise MCPClient::Errors::ServerError, err['message']
|
280
|
+
end
|
281
|
+
|
282
|
+
true
|
283
|
+
rescue StandardError => e
|
284
|
+
raise MCPClient::Errors::ResourceReadError, "Error unsubscribing from resource '#{uri}': #{e.message}"
|
285
|
+
end
|
286
|
+
|
196
287
|
# List all tools available from the MCP server
|
197
288
|
# @return [Array<MCPClient::Tool>] list of available tools
|
198
289
|
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
@@ -262,24 +262,32 @@ module MCPClient
|
|
262
262
|
end
|
263
263
|
|
264
264
|
# List all resources available from the MCP server
|
265
|
-
# @
|
265
|
+
# @param cursor [String, nil] optional cursor for pagination
|
266
|
+
# @return [Hash] result containing resources array and optional nextCursor
|
266
267
|
# @raise [MCPClient::Errors::ResourceReadError] if resources list retrieval fails
|
267
|
-
def list_resources
|
268
|
+
def list_resources(cursor: nil)
|
268
269
|
@mutex.synchronize do
|
269
|
-
return @
|
270
|
+
return @resources_result if @resources_result && !cursor
|
270
271
|
end
|
271
272
|
|
272
273
|
begin
|
273
274
|
ensure_connected
|
274
275
|
|
275
|
-
|
276
|
+
params = {}
|
277
|
+
params['cursor'] = cursor if cursor
|
278
|
+
result = rpc_request('resources/list', params)
|
279
|
+
|
280
|
+
resources = (result['resources'] || []).map do |resource_data|
|
281
|
+
MCPClient::Resource.from_json(resource_data, server: self)
|
282
|
+
end
|
283
|
+
|
284
|
+
resources_result = { 'resources' => resources, 'nextCursor' => result['nextCursor'] }
|
285
|
+
|
276
286
|
@mutex.synchronize do
|
277
|
-
@
|
278
|
-
MCPClient::Resource.from_json(resource_data, server: self)
|
279
|
-
end
|
287
|
+
@resources_result = resources_result unless cursor
|
280
288
|
end
|
281
289
|
|
282
|
-
|
290
|
+
resources_result
|
283
291
|
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
284
292
|
# Re-raise these errors directly
|
285
293
|
raise
|
@@ -290,10 +298,12 @@ module MCPClient
|
|
290
298
|
|
291
299
|
# Read a resource by its URI
|
292
300
|
# @param uri [String] the URI of the resource to read
|
293
|
-
# @return [
|
301
|
+
# @return [Array<MCPClient::ResourceContent>] array of resource contents
|
294
302
|
# @raise [MCPClient::Errors::ResourceReadError] if resource reading fails
|
295
303
|
def read_resource(uri)
|
296
|
-
rpc_request('resources/read', { uri: uri })
|
304
|
+
result = rpc_request('resources/read', { uri: uri })
|
305
|
+
contents = result['contents'] || []
|
306
|
+
contents.map { |content| MCPClient::ResourceContent.from_json(content) }
|
297
307
|
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
298
308
|
# Re-raise connection/transport errors directly
|
299
309
|
raise
|
@@ -302,6 +312,52 @@ module MCPClient
|
|
302
312
|
raise MCPClient::Errors::ResourceReadError, "Error reading resource '#{uri}': #{e.message}"
|
303
313
|
end
|
304
314
|
|
315
|
+
# List all resource templates available from the MCP server
|
316
|
+
# @param cursor [String, nil] optional cursor for pagination
|
317
|
+
# @return [Hash] result containing resourceTemplates array and optional nextCursor
|
318
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during resource template listing
|
319
|
+
def list_resource_templates(cursor: nil)
|
320
|
+
params = {}
|
321
|
+
params['cursor'] = cursor if cursor
|
322
|
+
result = rpc_request('resources/templates/list', params)
|
323
|
+
|
324
|
+
templates = (result['resourceTemplates'] || []).map do |template_data|
|
325
|
+
MCPClient::ResourceTemplate.from_json(template_data, server: self)
|
326
|
+
end
|
327
|
+
|
328
|
+
{ 'resourceTemplates' => templates, 'nextCursor' => result['nextCursor'] }
|
329
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
330
|
+
raise
|
331
|
+
rescue StandardError => e
|
332
|
+
raise MCPClient::Errors::ResourceReadError, "Error listing resource templates: #{e.message}"
|
333
|
+
end
|
334
|
+
|
335
|
+
# Subscribe to resource updates
|
336
|
+
# @param uri [String] the URI of the resource to subscribe to
|
337
|
+
# @return [Boolean] true if subscription successful
|
338
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during subscription
|
339
|
+
def subscribe_resource(uri)
|
340
|
+
rpc_request('resources/subscribe', { uri: uri })
|
341
|
+
true
|
342
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
343
|
+
raise
|
344
|
+
rescue StandardError => e
|
345
|
+
raise MCPClient::Errors::ResourceReadError, "Error subscribing to resource '#{uri}': #{e.message}"
|
346
|
+
end
|
347
|
+
|
348
|
+
# Unsubscribe from resource updates
|
349
|
+
# @param uri [String] the URI of the resource to unsubscribe from
|
350
|
+
# @return [Boolean] true if unsubscription successful
|
351
|
+
# @raise [MCPClient::Errors::ResourceReadError] for other errors during unsubscription
|
352
|
+
def unsubscribe_resource(uri)
|
353
|
+
rpc_request('resources/unsubscribe', { uri: uri })
|
354
|
+
true
|
355
|
+
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError, MCPClient::Errors::ServerError
|
356
|
+
raise
|
357
|
+
rescue StandardError => e
|
358
|
+
raise MCPClient::Errors::ResourceReadError, "Error unsubscribing from resource '#{uri}': #{e.message}"
|
359
|
+
end
|
360
|
+
|
305
361
|
# Override apply_request_headers to add session and SSE headers for MCP protocol
|
306
362
|
def apply_request_headers(req, request)
|
307
363
|
super
|
data/lib/mcp_client/version.rb
CHANGED
data/lib/mcp_client.rb
CHANGED
@@ -5,6 +5,8 @@ require_relative 'mcp_client/errors'
|
|
5
5
|
require_relative 'mcp_client/tool'
|
6
6
|
require_relative 'mcp_client/prompt'
|
7
7
|
require_relative 'mcp_client/resource'
|
8
|
+
require_relative 'mcp_client/resource_template'
|
9
|
+
require_relative 'mcp_client/resource_content'
|
8
10
|
require_relative 'mcp_client/server_base'
|
9
11
|
require_relative 'mcp_client/server_stdio'
|
10
12
|
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.8.
|
4
|
+
version: 0.8.1
|
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-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: faraday
|
@@ -129,6 +129,8 @@ files:
|
|
129
129
|
- lib/mcp_client/oauth_client.rb
|
130
130
|
- lib/mcp_client/prompt.rb
|
131
131
|
- lib/mcp_client/resource.rb
|
132
|
+
- lib/mcp_client/resource_content.rb
|
133
|
+
- lib/mcp_client/resource_template.rb
|
132
134
|
- lib/mcp_client/server_base.rb
|
133
135
|
- lib/mcp_client/server_factory.rb
|
134
136
|
- lib/mcp_client/server_http.rb
|