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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27b97462ec0c99d98299df120726f90944f495aaddf198272d72725544240ec0
4
- data.tar.gz: b58b8a6ba53f698abe1cb3576fba8030d66cb0bd7eafa2e39e28943e7ef1337f
3
+ metadata.gz: 7e0949102c5c22625bef68e2f5b4a0e2aecdfc636495ecba52d16f663c35f2fc
4
+ data.tar.gz: fbeb7484c1458775b88aa074422913e9d09d8a37b23aac38c7edf82e2111b9cb
5
5
  SHA512:
6
- metadata.gz: 50d5791642a74f521133a057d924d3daf14e657ec617e73ad8f95510b4d97f3d52742c5a467c54d75dc02030a087f29e63ee346429271e8c80eb71cb96e38f94
7
- data.tar.gz: 5f92d87a5d176e3271a11e9bbea7eafc6720107c6ae795601250e088cde69e6d6340911eecd6ea8ab32776d97d9bfb0b4d3740087509eb704cb400bf4e23e5aa
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** - Complete resources implementation for accessing files and data with URI-based identification
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
- resources = client.list_resources
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
- resources = client.list_resources
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
- readme_content = client.read_resource('file:///sample/README.md')
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
@@ -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
- # @return [Array<MCPClient::Resource>] list of available resources
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
- return @resource_cache.values if cache && !@resource_cache.empty?
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.each do |resource|
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
- resources
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
- 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 }
179
+ result = list_resources
180
+ resources = result['resources'] || []
177
181
 
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
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
- 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
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
- clear_cache
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
- # @return [Array<MCPClient::Resource>] list of available resources
52
- def list_resources
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 [Object] the resource contents
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
- # @return [Array<MCPClient::Resource>] list of available resources
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 @resources if @resources
171
+ return @resources_result if @resources_result && !cursor
171
172
  end
172
173
 
173
174
  begin
174
175
  ensure_initialized
175
176
 
176
- resources_data = request_resources_list
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
- @resources = resources_data.map do |resource_data|
179
- MCPClient::Resource.from_json(resource_data, server: self)
180
- end
188
+ @resources_result = resources_result unless cursor
181
189
  end
182
190
 
183
- @mutex.synchronize { @resources }
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 [Object] the resource contents
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
- uri: uri
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
- # @return [Array<MCPClient::Resource>] list of available resources
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
- req = { 'jsonrpc' => '2.0', 'id' => req_id, 'method' => 'resources/list', 'params' => {} }
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
- (res.dig('result', 'resources') || []).map { |td| MCPClient::Resource.from_json(td, server: self) }
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 [Object] the resource contents
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
- # @return [Array<MCPClient::Resource>] list of available resources
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 @resources if @resources
270
+ return @resources_result if @resources_result && !cursor
270
271
  end
271
272
 
272
273
  begin
273
274
  ensure_connected
274
275
 
275
- resources_data = request_resources_list
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
- @resources = resources_data.map do |resource_data|
278
- MCPClient::Resource.from_json(resource_data, server: self)
279
- end
287
+ @resources_result = resources_result unless cursor
280
288
  end
281
289
 
282
- @mutex.synchronize { @resources }
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 [Object] the resource contents
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
@@ -2,7 +2,7 @@
2
2
 
3
3
  module MCPClient
4
4
  # Current version of the MCP client gem
5
- VERSION = '0.8.0'
5
+ VERSION = '0.8.1'
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
@@ -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.0
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-16 00:00:00.000000000 Z
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