ruby-mcp-client 0.9.1 → 1.0.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 +42 -6
- data/lib/mcp_client/audio_content.rb +46 -0
- data/lib/mcp_client/auth/oauth_provider.rb +29 -6
- data/lib/mcp_client/auth.rb +68 -8
- data/lib/mcp_client/client.rb +265 -54
- data/lib/mcp_client/elicitation_validator.rb +262 -0
- data/lib/mcp_client/errors.rb +6 -0
- data/lib/mcp_client/http_transport_base.rb +2 -0
- data/lib/mcp_client/json_rpc_common.rb +3 -3
- data/lib/mcp_client/resource.rb +8 -0
- data/lib/mcp_client/resource_link.rb +63 -0
- data/lib/mcp_client/server_http.rb +17 -7
- data/lib/mcp_client/server_sse.rb +9 -6
- data/lib/mcp_client/server_stdio.rb +9 -6
- data/lib/mcp_client/server_streamable_http.rb +23 -10
- data/lib/mcp_client/task.rb +127 -0
- data/lib/mcp_client/tool.rb +73 -9
- data/lib/mcp_client/version.rb +2 -2
- data/lib/mcp_client.rb +27 -0
- metadata +8 -4
|
@@ -58,6 +58,7 @@ module MCPClient
|
|
|
58
58
|
# Apply base headers but prioritize session termination headers
|
|
59
59
|
@headers.each { |k, v| req.headers[k] = v }
|
|
60
60
|
req.headers['Mcp-Session-Id'] = @session_id
|
|
61
|
+
req.headers['Mcp-Protocol-Version'] = @protocol_version if @protocol_version
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
if response.success?
|
|
@@ -138,6 +139,7 @@ module MCPClient
|
|
|
138
139
|
|
|
139
140
|
@server_info = result['serverInfo']
|
|
140
141
|
@capabilities = result['capabilities']
|
|
142
|
+
@protocol_version = result['protocolVersion']
|
|
141
143
|
end
|
|
142
144
|
|
|
143
145
|
# Send a JSON-RPC request to the server and wait for result
|
|
@@ -63,9 +63,9 @@ module MCPClient
|
|
|
63
63
|
# @return [Hash] the initialization parameters
|
|
64
64
|
def initialization_params
|
|
65
65
|
capabilities = {
|
|
66
|
-
'elicitation' => {}, # MCP 2025-
|
|
67
|
-
'roots' => { 'listChanged' => true }, # MCP 2025-
|
|
68
|
-
'sampling' => {} # MCP 2025-
|
|
66
|
+
'elicitation' => {}, # MCP 2025-11-25: Support for server-initiated user interactions
|
|
67
|
+
'roots' => { 'listChanged' => true }, # MCP 2025-11-25: Support for roots
|
|
68
|
+
'sampling' => {} # MCP 2025-11-25: Support for server-initiated LLM sampling
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
{
|
data/lib/mcp_client/resource.rb
CHANGED
|
@@ -41,6 +41,14 @@ module MCPClient
|
|
|
41
41
|
@server = server
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
# Return the lastModified annotation value (ISO 8601 timestamp string)
|
|
45
|
+
# @return [String, nil] the lastModified timestamp, or nil if not set
|
|
46
|
+
def last_modified
|
|
47
|
+
return nil unless @annotations.is_a?(Hash)
|
|
48
|
+
|
|
49
|
+
@annotations['lastModified'] || @annotations[:lastModified]
|
|
50
|
+
end
|
|
51
|
+
|
|
44
52
|
# Create a Resource instance from JSON data
|
|
45
53
|
# @param data [Hash] JSON data from MCP server
|
|
46
54
|
# @param server [MCPClient::ServerBase, nil] the server this resource belongs to
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCPClient
|
|
4
|
+
# Representation of an MCP resource link in tool result content
|
|
5
|
+
# A resource link references a server resource that can be read separately.
|
|
6
|
+
# Used in tool results to point clients to available resources (MCP 2025-11-25).
|
|
7
|
+
class ResourceLink
|
|
8
|
+
# @!attribute [r] uri
|
|
9
|
+
# @return [String] URI of the linked resource
|
|
10
|
+
# @!attribute [r] name
|
|
11
|
+
# @return [String] the name of the linked resource
|
|
12
|
+
# @!attribute [r] description
|
|
13
|
+
# @return [String, nil] optional human-readable description
|
|
14
|
+
# @!attribute [r] mime_type
|
|
15
|
+
# @return [String, nil] optional MIME type of the resource
|
|
16
|
+
# @!attribute [r] annotations
|
|
17
|
+
# @return [Hash, nil] optional annotations that provide hints to clients
|
|
18
|
+
# @!attribute [r] title
|
|
19
|
+
# @return [String, nil] optional display title for the resource
|
|
20
|
+
# @!attribute [r] size
|
|
21
|
+
# @return [Integer, nil] optional size of the resource in bytes
|
|
22
|
+
attr_reader :uri, :name, :description, :mime_type, :annotations, :title, :size
|
|
23
|
+
|
|
24
|
+
# Initialize a resource link
|
|
25
|
+
# @param uri [String] URI of the linked resource
|
|
26
|
+
# @param name [String] the name of the linked resource
|
|
27
|
+
# @param description [String, nil] optional human-readable description
|
|
28
|
+
# @param mime_type [String, nil] optional MIME type of the resource
|
|
29
|
+
# @param annotations [Hash, nil] optional annotations that provide hints to clients
|
|
30
|
+
# @param title [String, nil] optional display title for the resource
|
|
31
|
+
# @param size [Integer, nil] optional size of the resource in bytes
|
|
32
|
+
def initialize(uri:, name:, description: nil, mime_type: nil, annotations: nil, title: nil, size: nil)
|
|
33
|
+
@uri = uri
|
|
34
|
+
@name = name
|
|
35
|
+
@description = description
|
|
36
|
+
@mime_type = mime_type
|
|
37
|
+
@annotations = annotations
|
|
38
|
+
@title = title
|
|
39
|
+
@size = size
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Create a ResourceLink instance from JSON data
|
|
43
|
+
# @param data [Hash] JSON data from MCP server (content item with type 'resource_link')
|
|
44
|
+
# @return [MCPClient::ResourceLink] resource link instance
|
|
45
|
+
def self.from_json(data)
|
|
46
|
+
new(
|
|
47
|
+
uri: data['uri'],
|
|
48
|
+
name: data['name'],
|
|
49
|
+
description: data['description'],
|
|
50
|
+
mime_type: data['mimeType'],
|
|
51
|
+
annotations: data['annotations'],
|
|
52
|
+
title: data['title'],
|
|
53
|
+
size: data['size']
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# The content type identifier for this content type
|
|
58
|
+
# @return [String] 'resource_link'
|
|
59
|
+
def type
|
|
60
|
+
'resource_link'
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -195,15 +195,22 @@ module MCPClient
|
|
|
195
195
|
raise MCPClient::Errors::ToolCallError, "Error calling tool '#{tool_name}': #{e.message}"
|
|
196
196
|
end
|
|
197
197
|
|
|
198
|
-
# Override apply_request_headers to add session
|
|
198
|
+
# Override apply_request_headers to add session and protocol version headers
|
|
199
199
|
def apply_request_headers(req, request)
|
|
200
200
|
super
|
|
201
201
|
|
|
202
|
-
# Add session
|
|
203
|
-
return unless
|
|
202
|
+
# Add session and protocol version headers for non-initialize requests
|
|
203
|
+
return unless request['method'] != 'initialize'
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
205
|
+
if @session_id
|
|
206
|
+
req.headers['Mcp-Session-Id'] = @session_id
|
|
207
|
+
@logger.debug("Adding session header: Mcp-Session-Id: #{@session_id}")
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
return unless @protocol_version
|
|
211
|
+
|
|
212
|
+
req.headers['Mcp-Protocol-Version'] = @protocol_version
|
|
213
|
+
@logger.debug("Adding protocol version header: Mcp-Protocol-Version: #{@protocol_version}")
|
|
207
214
|
end
|
|
208
215
|
|
|
209
216
|
# Override handle_successful_response to capture session ID
|
|
@@ -325,10 +332,13 @@ module MCPClient
|
|
|
325
332
|
# Request completion suggestions from the server (MCP 2025-06-18)
|
|
326
333
|
# @param ref [Hash] reference to complete (prompt or resource)
|
|
327
334
|
# @param argument [Hash] the argument being completed (e.g., { 'name' => 'arg_name', 'value' => 'partial' })
|
|
335
|
+
# @param context [Hash, nil] optional context for the completion (MCP 2025-11-25)
|
|
328
336
|
# @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
|
|
329
337
|
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
330
|
-
def complete(ref:, argument:)
|
|
331
|
-
|
|
338
|
+
def complete(ref:, argument:, context: nil)
|
|
339
|
+
params = { ref: ref, argument: argument }
|
|
340
|
+
params[:context] = context if context
|
|
341
|
+
result = rpc_request('completion/complete', params)
|
|
332
342
|
result['completion'] || { 'values' => [] }
|
|
333
343
|
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
|
334
344
|
raise
|
|
@@ -106,7 +106,7 @@ module MCPClient
|
|
|
106
106
|
@activity_timer_thread = nil
|
|
107
107
|
@elicitation_request_callback = nil # MCP 2025-06-18
|
|
108
108
|
@roots_list_request_callback = nil # MCP 2025-06-18
|
|
109
|
-
@sampling_request_callback = nil # MCP 2025-
|
|
109
|
+
@sampling_request_callback = nil # MCP 2025-11-25
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
# Stream tool call fallback for SSE transport (yields single result)
|
|
@@ -330,10 +330,13 @@ module MCPClient
|
|
|
330
330
|
# Request completion suggestions from the server (MCP 2025-06-18)
|
|
331
331
|
# @param ref [Hash] reference object (e.g., { 'type' => 'ref/prompt', 'name' => 'prompt_name' })
|
|
332
332
|
# @param argument [Hash] the argument being completed (e.g., { 'name' => 'arg_name', 'value' => 'partial' })
|
|
333
|
+
# @param context [Hash, nil] optional context for the completion (MCP 2025-11-25)
|
|
333
334
|
# @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
|
|
334
335
|
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
335
|
-
def complete(ref:, argument:)
|
|
336
|
-
|
|
336
|
+
def complete(ref:, argument:, context: nil)
|
|
337
|
+
params = { ref: ref, argument: argument }
|
|
338
|
+
params[:context] = context if context
|
|
339
|
+
result = rpc_request('completion/complete', params)
|
|
337
340
|
result['completion'] || { 'values' => [] }
|
|
338
341
|
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
|
339
342
|
raise
|
|
@@ -455,7 +458,7 @@ module MCPClient
|
|
|
455
458
|
@roots_list_request_callback = block
|
|
456
459
|
end
|
|
457
460
|
|
|
458
|
-
# Register a callback for sampling requests (MCP 2025-
|
|
461
|
+
# Register a callback for sampling requests (MCP 2025-11-25)
|
|
459
462
|
# @param block [Proc] callback that receives (request_id, params) and returns response hash
|
|
460
463
|
# @return [void]
|
|
461
464
|
def on_sampling_request(&block)
|
|
@@ -545,7 +548,7 @@ module MCPClient
|
|
|
545
548
|
@logger.error("Error sending roots/list response: #{e.message}")
|
|
546
549
|
end
|
|
547
550
|
|
|
548
|
-
# Handle sampling/createMessage request from server (MCP 2025-
|
|
551
|
+
# Handle sampling/createMessage request from server (MCP 2025-11-25)
|
|
549
552
|
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
550
553
|
# @param params [Hash] the sampling parameters
|
|
551
554
|
# @return [void]
|
|
@@ -564,7 +567,7 @@ module MCPClient
|
|
|
564
567
|
send_sampling_response(request_id, result)
|
|
565
568
|
end
|
|
566
569
|
|
|
567
|
-
# Send sampling response back to server via HTTP POST (MCP 2025-
|
|
570
|
+
# Send sampling response back to server via HTTP POST (MCP 2025-11-25)
|
|
568
571
|
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
569
572
|
# @param result [Hash] the sampling result (role, content, model, stopReason)
|
|
570
573
|
# @return [void]
|
|
@@ -48,7 +48,7 @@ module MCPClient
|
|
|
48
48
|
@env = env || {}
|
|
49
49
|
@elicitation_request_callback = nil # MCP 2025-06-18
|
|
50
50
|
@roots_list_request_callback = nil # MCP 2025-06-18
|
|
51
|
-
@sampling_request_callback = nil # MCP 2025-
|
|
51
|
+
@sampling_request_callback = nil # MCP 2025-11-25
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# Server info from the initialize response
|
|
@@ -345,16 +345,19 @@ module MCPClient
|
|
|
345
345
|
# Request completion suggestions from the server (MCP 2025-06-18)
|
|
346
346
|
# @param ref [Hash] reference object (e.g., { 'type' => 'ref/prompt', 'name' => 'prompt_name' })
|
|
347
347
|
# @param argument [Hash] the argument being completed (e.g., { 'name' => 'arg_name', 'value' => 'partial' })
|
|
348
|
+
# @param context [Hash, nil] optional context for the completion (MCP 2025-11-25)
|
|
348
349
|
# @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
|
|
349
350
|
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
350
|
-
def complete(ref:, argument:)
|
|
351
|
+
def complete(ref:, argument:, context: nil)
|
|
351
352
|
ensure_initialized
|
|
352
353
|
req_id = next_id
|
|
354
|
+
params = { 'ref' => ref, 'argument' => argument }
|
|
355
|
+
params['context'] = context if context
|
|
353
356
|
req = {
|
|
354
357
|
'jsonrpc' => '2.0',
|
|
355
358
|
'id' => req_id,
|
|
356
359
|
'method' => 'completion/complete',
|
|
357
|
-
'params' =>
|
|
360
|
+
'params' => params
|
|
358
361
|
}
|
|
359
362
|
send_request(req)
|
|
360
363
|
res = wait_response(req_id)
|
|
@@ -406,7 +409,7 @@ module MCPClient
|
|
|
406
409
|
@roots_list_request_callback = block
|
|
407
410
|
end
|
|
408
411
|
|
|
409
|
-
# Register a callback for sampling requests (MCP 2025-
|
|
412
|
+
# Register a callback for sampling requests (MCP 2025-11-25)
|
|
410
413
|
# @param block [Proc] callback that receives (request_id, params) and returns response hash
|
|
411
414
|
# @return [void]
|
|
412
415
|
def on_sampling_request(&block)
|
|
@@ -477,7 +480,7 @@ module MCPClient
|
|
|
477
480
|
send_roots_list_response(request_id, result)
|
|
478
481
|
end
|
|
479
482
|
|
|
480
|
-
# Handle sampling/createMessage request from server (MCP 2025-
|
|
483
|
+
# Handle sampling/createMessage request from server (MCP 2025-11-25)
|
|
481
484
|
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
482
485
|
# @param params [Hash] the sampling parameters
|
|
483
486
|
# @return [void]
|
|
@@ -509,7 +512,7 @@ module MCPClient
|
|
|
509
512
|
send_message(response)
|
|
510
513
|
end
|
|
511
514
|
|
|
512
|
-
# Send sampling response back to server (MCP 2025-
|
|
515
|
+
# Send sampling response back to server (MCP 2025-11-25)
|
|
513
516
|
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
514
517
|
# @param result [Hash] the sampling result (role, content, model, stopReason)
|
|
515
518
|
# @return [void]
|
|
@@ -119,7 +119,7 @@ module MCPClient
|
|
|
119
119
|
@buffer = '' # Buffer for partial SSE event data
|
|
120
120
|
@elicitation_request_callback = nil # MCP 2025-06-18
|
|
121
121
|
@roots_list_request_callback = nil # MCP 2025-06-18
|
|
122
|
-
@sampling_request_callback = nil # MCP 2025-
|
|
122
|
+
@sampling_request_callback = nil # MCP 2025-11-25
|
|
123
123
|
end
|
|
124
124
|
|
|
125
125
|
# Connect to the MCP server over Streamable HTTP
|
|
@@ -222,10 +222,13 @@ module MCPClient
|
|
|
222
222
|
# Request completion suggestions from the server (MCP 2025-06-18)
|
|
223
223
|
# @param ref [Hash] reference object (e.g., { 'type' => 'ref/prompt', 'name' => 'prompt_name' })
|
|
224
224
|
# @param argument [Hash] the argument being completed (e.g., { 'name' => 'arg_name', 'value' => 'partial' })
|
|
225
|
+
# @param context [Hash, nil] optional context for the completion (MCP 2025-11-25)
|
|
225
226
|
# @return [Hash] completion result with 'values', optional 'total', and 'hasMore' fields
|
|
226
227
|
# @raise [MCPClient::Errors::ServerError] if server returns an error
|
|
227
|
-
def complete(ref:, argument:)
|
|
228
|
-
|
|
228
|
+
def complete(ref:, argument:, context: nil)
|
|
229
|
+
params = { ref: ref, argument: argument }
|
|
230
|
+
params[:context] = context if context
|
|
231
|
+
result = rpc_request('completion/complete', params)
|
|
229
232
|
result['completion'] || { 'values' => [] }
|
|
230
233
|
rescue MCPClient::Errors::ConnectionError, MCPClient::Errors::TransportError
|
|
231
234
|
raise
|
|
@@ -393,10 +396,17 @@ module MCPClient
|
|
|
393
396
|
def apply_request_headers(req, request)
|
|
394
397
|
super
|
|
395
398
|
|
|
396
|
-
# Add session
|
|
397
|
-
if
|
|
398
|
-
|
|
399
|
-
|
|
399
|
+
# Add session and protocol version headers for non-initialize requests
|
|
400
|
+
if request['method'] != 'initialize'
|
|
401
|
+
if @session_id
|
|
402
|
+
req.headers['Mcp-Session-Id'] = @session_id
|
|
403
|
+
@logger.debug("Adding session header: Mcp-Session-Id: #{@session_id}")
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
if @protocol_version
|
|
407
|
+
req.headers['Mcp-Protocol-Version'] = @protocol_version
|
|
408
|
+
@logger.debug("Adding protocol version header: Mcp-Protocol-Version: #{@protocol_version}")
|
|
409
|
+
end
|
|
400
410
|
end
|
|
401
411
|
|
|
402
412
|
# Add Last-Event-ID header for resumability (if available)
|
|
@@ -496,7 +506,7 @@ module MCPClient
|
|
|
496
506
|
@roots_list_request_callback = block
|
|
497
507
|
end
|
|
498
508
|
|
|
499
|
-
# Register a callback for sampling requests (MCP 2025-
|
|
509
|
+
# Register a callback for sampling requests (MCP 2025-11-25)
|
|
500
510
|
# @param block [Proc] callback that receives (request_id, params) and returns response hash
|
|
501
511
|
# @return [void]
|
|
502
512
|
def on_sampling_request(&block)
|
|
@@ -730,6 +740,7 @@ module MCPClient
|
|
|
730
740
|
def apply_events_headers(req)
|
|
731
741
|
@headers.each { |k, v| req.headers[k] = v }
|
|
732
742
|
req.headers['Mcp-Session-Id'] = @session_id if @session_id
|
|
743
|
+
req.headers['Mcp-Protocol-Version'] = @protocol_version if @protocol_version
|
|
733
744
|
end
|
|
734
745
|
|
|
735
746
|
# Process event chunks from the server
|
|
@@ -840,6 +851,7 @@ module MCPClient
|
|
|
840
851
|
response = conn.post(@endpoint) do |req|
|
|
841
852
|
@headers.each { |k, v| req.headers[k] = v }
|
|
842
853
|
req.headers['Mcp-Session-Id'] = @session_id if @session_id
|
|
854
|
+
req.headers['Mcp-Protocol-Version'] = @protocol_version if @protocol_version
|
|
843
855
|
req.body = pong_response.to_json
|
|
844
856
|
end
|
|
845
857
|
|
|
@@ -920,7 +932,7 @@ module MCPClient
|
|
|
920
932
|
send_roots_list_response(request_id, result)
|
|
921
933
|
end
|
|
922
934
|
|
|
923
|
-
# Handle sampling/createMessage request from server (MCP 2025-
|
|
935
|
+
# Handle sampling/createMessage request from server (MCP 2025-11-25)
|
|
924
936
|
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
925
937
|
# @param params [Hash] the sampling parameters
|
|
926
938
|
# @return [void]
|
|
@@ -956,7 +968,7 @@ module MCPClient
|
|
|
956
968
|
@logger.error("Error sending roots/list response: #{e.message}")
|
|
957
969
|
end
|
|
958
970
|
|
|
959
|
-
# Send sampling response back to server via HTTP POST (MCP 2025-
|
|
971
|
+
# Send sampling response back to server via HTTP POST (MCP 2025-11-25)
|
|
960
972
|
# @param request_id [String, Integer] the JSON-RPC request ID
|
|
961
973
|
# @param result [Hash] the sampling result (role, content, model, stopReason)
|
|
962
974
|
# @return [void]
|
|
@@ -1040,6 +1052,7 @@ module MCPClient
|
|
|
1040
1052
|
resp = conn.post(@endpoint) do |req|
|
|
1041
1053
|
@headers.each { |k, v| req.headers[k] = v }
|
|
1042
1054
|
req.headers['Mcp-Session-Id'] = @session_id if @session_id
|
|
1055
|
+
req.headers['Mcp-Protocol-Version'] = @protocol_version if @protocol_version
|
|
1043
1056
|
req.body = json_body
|
|
1044
1057
|
end
|
|
1045
1058
|
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCPClient
|
|
4
|
+
# Represents an MCP Task for long-running operations with progress tracking
|
|
5
|
+
# Tasks follow the MCP 2025-11-25 specification for structured task management
|
|
6
|
+
#
|
|
7
|
+
# Task states: pending, running, completed, failed, cancelled
|
|
8
|
+
class Task
|
|
9
|
+
# Valid task states
|
|
10
|
+
VALID_STATES = %w[pending running completed failed cancelled].freeze
|
|
11
|
+
|
|
12
|
+
attr_reader :id, :state, :progress_token, :progress, :total, :message, :result, :server
|
|
13
|
+
|
|
14
|
+
# Create a new Task
|
|
15
|
+
# @param id [String] unique task identifier
|
|
16
|
+
# @param state [String] task state (pending, running, completed, failed, cancelled)
|
|
17
|
+
# @param progress_token [String, nil] optional token for tracking progress
|
|
18
|
+
# @param progress [Integer, nil] current progress value
|
|
19
|
+
# @param total [Integer, nil] total progress value
|
|
20
|
+
# @param message [String, nil] human-readable status message
|
|
21
|
+
# @param result [Object, nil] task result (when completed)
|
|
22
|
+
# @param server [MCPClient::ServerBase, nil] the server this task belongs to
|
|
23
|
+
def initialize(id:, state: 'pending', progress_token: nil, progress: nil, total: nil,
|
|
24
|
+
message: nil, result: nil, server: nil)
|
|
25
|
+
validate_state!(state)
|
|
26
|
+
@id = id
|
|
27
|
+
@state = state
|
|
28
|
+
@progress_token = progress_token
|
|
29
|
+
@progress = progress
|
|
30
|
+
@total = total
|
|
31
|
+
@message = message
|
|
32
|
+
@result = result
|
|
33
|
+
@server = server
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Create a Task from a JSON hash
|
|
37
|
+
# @param json [Hash] the JSON hash with task fields
|
|
38
|
+
# @param server [MCPClient::ServerBase, nil] optional server reference
|
|
39
|
+
# @return [Task]
|
|
40
|
+
def self.from_json(json, server: nil)
|
|
41
|
+
new(
|
|
42
|
+
id: json['id'] || json[:id],
|
|
43
|
+
state: json['state'] || json[:state] || 'pending',
|
|
44
|
+
progress_token: json['progressToken'] || json[:progressToken] || json[:progress_token],
|
|
45
|
+
progress: json['progress'] || json[:progress],
|
|
46
|
+
total: json['total'] || json[:total],
|
|
47
|
+
message: json['message'] || json[:message],
|
|
48
|
+
result: json.key?('result') ? json['result'] : json[:result],
|
|
49
|
+
server: server
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Convert to JSON-serializable hash
|
|
54
|
+
# @return [Hash]
|
|
55
|
+
def to_h
|
|
56
|
+
result = { 'id' => @id, 'state' => @state }
|
|
57
|
+
result['progressToken'] = @progress_token if @progress_token
|
|
58
|
+
result['progress'] = @progress if @progress
|
|
59
|
+
result['total'] = @total if @total
|
|
60
|
+
result['message'] = @message if @message
|
|
61
|
+
result['result'] = @result unless @result.nil?
|
|
62
|
+
result
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Convert to JSON string
|
|
66
|
+
# @return [String]
|
|
67
|
+
def to_json(*)
|
|
68
|
+
to_h.to_json(*)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Check if task is in a terminal state
|
|
72
|
+
# @return [Boolean]
|
|
73
|
+
def terminal?
|
|
74
|
+
%w[completed failed cancelled].include?(@state)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Check if task is still active (pending or running)
|
|
78
|
+
# @return [Boolean]
|
|
79
|
+
def active?
|
|
80
|
+
%w[pending running].include?(@state)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Calculate progress percentage
|
|
84
|
+
# @return [Float, nil] percentage (0.0-100.0) or nil if progress info unavailable
|
|
85
|
+
def progress_percentage
|
|
86
|
+
return nil unless @progress && @total&.positive?
|
|
87
|
+
|
|
88
|
+
(@progress.to_f / @total * 100).round(2)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Check equality
|
|
92
|
+
def ==(other)
|
|
93
|
+
return false unless other.is_a?(Task)
|
|
94
|
+
|
|
95
|
+
id == other.id && state == other.state
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
alias eql? ==
|
|
99
|
+
|
|
100
|
+
def hash
|
|
101
|
+
[id, state].hash
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# String representation
|
|
105
|
+
def to_s
|
|
106
|
+
parts = ["Task[#{@id}]: #{@state}"]
|
|
107
|
+
parts << "(#{@progress}/#{@total})" if @progress && @total
|
|
108
|
+
parts << "- #{@message}" if @message
|
|
109
|
+
parts.join(' ')
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def inspect
|
|
113
|
+
"#<MCPClient::Task id=#{@id.inspect} state=#{@state.inspect}>"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
# Validate task state
|
|
119
|
+
# @param state [String] the state to validate
|
|
120
|
+
# @raise [ArgumentError] if the state is not valid
|
|
121
|
+
def validate_state!(state)
|
|
122
|
+
return if VALID_STATES.include?(state)
|
|
123
|
+
|
|
124
|
+
raise ArgumentError, "Invalid task state: #{state.inspect}. Must be one of: #{VALID_STATES.join(', ')}"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
data/lib/mcp_client/tool.rb
CHANGED
|
@@ -5,6 +5,8 @@ module MCPClient
|
|
|
5
5
|
class Tool
|
|
6
6
|
# @!attribute [r] name
|
|
7
7
|
# @return [String] the name of the tool
|
|
8
|
+
# @!attribute [r] title
|
|
9
|
+
# @return [String, nil] optional human-readable name of the tool for display purposes
|
|
8
10
|
# @!attribute [r] description
|
|
9
11
|
# @return [String] the description of the tool
|
|
10
12
|
# @!attribute [r] schema
|
|
@@ -15,17 +17,19 @@ module MCPClient
|
|
|
15
17
|
# @return [Hash, nil] optional annotations describing tool behavior (e.g., readOnly, destructive)
|
|
16
18
|
# @!attribute [r] server
|
|
17
19
|
# @return [MCPClient::ServerBase, nil] the server this tool belongs to
|
|
18
|
-
attr_reader :name, :description, :schema, :output_schema, :annotations, :server
|
|
20
|
+
attr_reader :name, :title, :description, :schema, :output_schema, :annotations, :server
|
|
19
21
|
|
|
20
22
|
# Initialize a new Tool
|
|
21
23
|
# @param name [String] the name of the tool
|
|
22
24
|
# @param description [String] the description of the tool
|
|
23
25
|
# @param schema [Hash] the JSON schema for the tool inputs
|
|
26
|
+
# @param title [String, nil] optional human-readable name of the tool for display purposes
|
|
24
27
|
# @param output_schema [Hash, nil] optional JSON schema for structured tool outputs (MCP 2025-06-18)
|
|
25
28
|
# @param annotations [Hash, nil] optional annotations describing tool behavior
|
|
26
29
|
# @param server [MCPClient::ServerBase, nil] the server this tool belongs to
|
|
27
|
-
def initialize(name:, description:, schema:, output_schema: nil, annotations: nil, server: nil)
|
|
30
|
+
def initialize(name:, description:, schema:, title: nil, output_schema: nil, annotations: nil, server: nil)
|
|
28
31
|
@name = name
|
|
32
|
+
@title = title
|
|
29
33
|
@description = description
|
|
30
34
|
@schema = schema
|
|
31
35
|
@output_schema = output_schema
|
|
@@ -43,10 +47,12 @@ module MCPClient
|
|
|
43
47
|
schema = data['inputSchema'] || data[:inputSchema] || data['schema'] || data[:schema]
|
|
44
48
|
output_schema = data['outputSchema'] || data[:outputSchema]
|
|
45
49
|
annotations = data['annotations'] || data[:annotations]
|
|
50
|
+
title = data['title'] || data[:title]
|
|
46
51
|
new(
|
|
47
52
|
name: data['name'] || data[:name],
|
|
48
53
|
description: data['description'] || data[:description],
|
|
49
54
|
schema: schema,
|
|
55
|
+
title: title,
|
|
50
56
|
output_schema: output_schema,
|
|
51
57
|
annotations: annotations,
|
|
52
58
|
server: server
|
|
@@ -67,12 +73,12 @@ module MCPClient
|
|
|
67
73
|
end
|
|
68
74
|
|
|
69
75
|
# Convert tool to Anthropic Claude tool specification format
|
|
70
|
-
# @return [Hash] Anthropic Claude tool specification
|
|
76
|
+
# @return [Hash] Anthropic Claude tool specification with cleaned schema
|
|
71
77
|
def to_anthropic_tool
|
|
72
78
|
{
|
|
73
79
|
name: @name,
|
|
74
80
|
description: @description,
|
|
75
|
-
input_schema: @schema
|
|
81
|
+
input_schema: cleaned_schema(@schema)
|
|
76
82
|
}
|
|
77
83
|
end
|
|
78
84
|
|
|
@@ -86,22 +92,62 @@ module MCPClient
|
|
|
86
92
|
}
|
|
87
93
|
end
|
|
88
94
|
|
|
89
|
-
# Check if the tool is marked as read-only
|
|
95
|
+
# Check if the tool is marked as read-only (legacy annotation field)
|
|
90
96
|
# @return [Boolean] true if the tool is read-only
|
|
97
|
+
# @see #read_only_hint? for MCP 2025-11-25 annotation
|
|
91
98
|
def read_only?
|
|
92
|
-
@annotations && @annotations['readOnly'] == true
|
|
99
|
+
!!(@annotations && @annotations['readOnly'] == true)
|
|
93
100
|
end
|
|
94
101
|
|
|
95
|
-
# Check if the tool is marked as destructive
|
|
102
|
+
# Check if the tool is marked as destructive (legacy annotation field)
|
|
96
103
|
# @return [Boolean] true if the tool is destructive
|
|
104
|
+
# @see #destructive_hint? for MCP 2025-11-25 annotation
|
|
97
105
|
def destructive?
|
|
98
|
-
@annotations && @annotations['destructive'] == true
|
|
106
|
+
!!(@annotations && @annotations['destructive'] == true)
|
|
99
107
|
end
|
|
100
108
|
|
|
101
109
|
# Check if the tool requires confirmation before execution
|
|
102
110
|
# @return [Boolean] true if the tool requires confirmation
|
|
103
111
|
def requires_confirmation?
|
|
104
|
-
@annotations && @annotations['requiresConfirmation'] == true
|
|
112
|
+
!!(@annotations && @annotations['requiresConfirmation'] == true)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Check the readOnlyHint annotation (MCP 2025-11-25)
|
|
116
|
+
# When true, the tool does not modify its environment.
|
|
117
|
+
# @return [Boolean] defaults to true when not specified
|
|
118
|
+
def read_only_hint?
|
|
119
|
+
return true unless @annotations
|
|
120
|
+
|
|
121
|
+
fetch_annotation_hint('readOnlyHint', :readOnlyHint, true)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Check the destructiveHint annotation (MCP 2025-11-25)
|
|
125
|
+
# When true, the tool may perform destructive updates.
|
|
126
|
+
# Only meaningful when readOnlyHint is false.
|
|
127
|
+
# @return [Boolean] defaults to false when not specified
|
|
128
|
+
def destructive_hint?
|
|
129
|
+
return false unless @annotations
|
|
130
|
+
|
|
131
|
+
fetch_annotation_hint('destructiveHint', :destructiveHint, false)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Check the idempotentHint annotation (MCP 2025-11-25)
|
|
135
|
+
# When true, calling the tool repeatedly with the same arguments has no additional effect.
|
|
136
|
+
# Only meaningful when readOnlyHint is false.
|
|
137
|
+
# @return [Boolean] defaults to false when not specified
|
|
138
|
+
def idempotent_hint?
|
|
139
|
+
return false unless @annotations
|
|
140
|
+
|
|
141
|
+
fetch_annotation_hint('idempotentHint', :idempotentHint, false)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Check the openWorldHint annotation (MCP 2025-11-25)
|
|
145
|
+
# When true, the tool may interact with the "open world" (external entities).
|
|
146
|
+
# @return [Boolean] defaults to true when not specified
|
|
147
|
+
def open_world_hint?
|
|
148
|
+
return true unless @annotations
|
|
149
|
+
|
|
150
|
+
fetch_annotation_hint('openWorldHint', :openWorldHint, true)
|
|
105
151
|
end
|
|
106
152
|
|
|
107
153
|
# Check if the tool supports structured outputs (MCP 2025-06-18)
|
|
@@ -112,6 +158,24 @@ module MCPClient
|
|
|
112
158
|
|
|
113
159
|
private
|
|
114
160
|
|
|
161
|
+
# Fetch a boolean annotation hint, checking both string and symbol keys.
|
|
162
|
+
# Uses Hash#key? to correctly handle false values.
|
|
163
|
+
# @param str_key [String] the string key to check
|
|
164
|
+
# @param sym_key [Symbol] the symbol key to check
|
|
165
|
+
# @param default [Boolean] the default value when the key is not present
|
|
166
|
+
# @return [Boolean] the annotation value, or the default
|
|
167
|
+
def fetch_annotation_hint(str_key, sym_key, default)
|
|
168
|
+
return default unless @annotations.is_a?(Hash)
|
|
169
|
+
|
|
170
|
+
if @annotations.key?(str_key)
|
|
171
|
+
@annotations[str_key]
|
|
172
|
+
elsif @annotations.key?(sym_key)
|
|
173
|
+
@annotations[sym_key]
|
|
174
|
+
else
|
|
175
|
+
default
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
115
179
|
# Recursively remove "$schema" keys that are not accepted by Vertex AI
|
|
116
180
|
# @param obj [Object] schema element (Hash/Array/other)
|
|
117
181
|
# @return [Object] cleaned schema without "$schema" keys
|
data/lib/mcp_client/version.rb
CHANGED