ruby-mcp-client 0.9.1 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +16 -6
- data/lib/mcp_client/audio_content.rb +46 -0
- 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
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCPClient
|
|
4
|
+
# Validates elicitation schemas and content per MCP 2025-11-25 spec.
|
|
5
|
+
# Schemas are restricted to flat objects with primitive property types:
|
|
6
|
+
# string (with optional enum, pattern, format, minLength, maxLength)
|
|
7
|
+
# number / integer (with optional minimum, maximum)
|
|
8
|
+
# boolean
|
|
9
|
+
# array (multi-select enum only, with items containing enum or anyOf)
|
|
10
|
+
module ElicitationValidator
|
|
11
|
+
# Allowed primitive types for schema properties
|
|
12
|
+
PRIMITIVE_TYPES = %w[string number integer boolean].freeze
|
|
13
|
+
|
|
14
|
+
# Allowed string formats per MCP spec
|
|
15
|
+
STRING_FORMATS = %w[email uri date date-time].freeze
|
|
16
|
+
|
|
17
|
+
# Validate that a requestedSchema conforms to MCP elicitation constraints.
|
|
18
|
+
# Returns an array of error messages (empty if valid).
|
|
19
|
+
# @param schema [Hash] the requestedSchema
|
|
20
|
+
# @return [Array<String>] validation errors
|
|
21
|
+
def self.validate_schema(schema)
|
|
22
|
+
errors = []
|
|
23
|
+
return errors unless schema.is_a?(Hash)
|
|
24
|
+
|
|
25
|
+
unless schema['type'] == 'object'
|
|
26
|
+
errors << "Schema type must be 'object', got '#{schema['type']}'"
|
|
27
|
+
return errors
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
properties = schema['properties']
|
|
31
|
+
return errors unless properties.is_a?(Hash)
|
|
32
|
+
|
|
33
|
+
properties.each do |name, prop|
|
|
34
|
+
errors.concat(validate_property(name, prop))
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
errors
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Validate a single property definition.
|
|
41
|
+
# @param name [String] property name
|
|
42
|
+
# @param prop [Hash] property schema
|
|
43
|
+
# @return [Array<String>] validation errors
|
|
44
|
+
def self.validate_property(name, prop)
|
|
45
|
+
errors = []
|
|
46
|
+
return errors unless prop.is_a?(Hash)
|
|
47
|
+
|
|
48
|
+
type = prop['type']
|
|
49
|
+
|
|
50
|
+
if type == 'array'
|
|
51
|
+
errors.concat(validate_array_property(name, prop))
|
|
52
|
+
elsif PRIMITIVE_TYPES.include?(type)
|
|
53
|
+
errors.concat(validate_primitive_property(name, prop))
|
|
54
|
+
else
|
|
55
|
+
errors << "Property '#{name}' has unsupported type '#{type}'"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
errors
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Validate a primitive property (string, number, integer, boolean).
|
|
62
|
+
# @param name [String] property name
|
|
63
|
+
# @param prop [Hash] property schema
|
|
64
|
+
# @return [Array<String>] validation errors
|
|
65
|
+
def self.validate_primitive_property(name, prop)
|
|
66
|
+
errors = []
|
|
67
|
+
type = prop['type']
|
|
68
|
+
|
|
69
|
+
case type
|
|
70
|
+
when 'string'
|
|
71
|
+
if prop['format'] && !STRING_FORMATS.include?(prop['format'])
|
|
72
|
+
errors << "Property '#{name}' has unsupported format '#{prop['format']}'"
|
|
73
|
+
end
|
|
74
|
+
errors << "Property '#{name}' enum must be an array" if prop['enum'] && !prop['enum'].is_a?(Array)
|
|
75
|
+
when 'number', 'integer'
|
|
76
|
+
if prop.key?('minimum') && !prop['minimum'].is_a?(Numeric)
|
|
77
|
+
errors << "Property '#{name}' minimum must be numeric"
|
|
78
|
+
end
|
|
79
|
+
if prop.key?('maximum') && !prop['maximum'].is_a?(Numeric)
|
|
80
|
+
errors << "Property '#{name}' maximum must be numeric"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
errors
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Validate an array property (multi-select enum only).
|
|
88
|
+
# @param name [String] property name
|
|
89
|
+
# @param prop [Hash] property schema
|
|
90
|
+
# @return [Array<String>] validation errors
|
|
91
|
+
def self.validate_array_property(name, prop)
|
|
92
|
+
errors = []
|
|
93
|
+
items = prop['items']
|
|
94
|
+
|
|
95
|
+
unless items.is_a?(Hash)
|
|
96
|
+
errors << "Property '#{name}' array type requires 'items' definition"
|
|
97
|
+
return errors
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
has_enum = items['enum'].is_a?(Array)
|
|
101
|
+
has_any_of = items['anyOf'].is_a?(Array)
|
|
102
|
+
|
|
103
|
+
errors << "Property '#{name}' array items must have 'enum' or 'anyOf'" unless has_enum || has_any_of
|
|
104
|
+
|
|
105
|
+
errors
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Validate content against a requestedSchema.
|
|
109
|
+
# Returns an array of error messages (empty if valid).
|
|
110
|
+
# @param content [Hash] the response content
|
|
111
|
+
# @param schema [Hash] the requestedSchema
|
|
112
|
+
# @return [Array<String>] validation errors
|
|
113
|
+
def self.validate_content(content, schema)
|
|
114
|
+
errors = []
|
|
115
|
+
return errors unless content.is_a?(Hash) && schema.is_a?(Hash)
|
|
116
|
+
|
|
117
|
+
properties = schema['properties'] || {}
|
|
118
|
+
required = Array(schema['required'])
|
|
119
|
+
|
|
120
|
+
# Check required fields
|
|
121
|
+
required.each do |field|
|
|
122
|
+
field_s = field.to_s
|
|
123
|
+
errors << "Missing required field '#{field_s}'" unless content.key?(field_s) || content.key?(field_s.to_sym)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Validate each provided field
|
|
127
|
+
content.each do |field, value|
|
|
128
|
+
prop = properties[field.to_s]
|
|
129
|
+
next unless prop.is_a?(Hash)
|
|
130
|
+
|
|
131
|
+
errors.concat(validate_value(field.to_s, value, prop))
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
errors
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Validate a single value against its property schema.
|
|
138
|
+
# @param field [String] field name
|
|
139
|
+
# @param value [Object] the value to validate
|
|
140
|
+
# @param prop [Hash] property schema
|
|
141
|
+
# @return [Array<String>] validation errors
|
|
142
|
+
def self.validate_value(field, value, prop)
|
|
143
|
+
errors = []
|
|
144
|
+
type = prop['type']
|
|
145
|
+
|
|
146
|
+
case type
|
|
147
|
+
when 'string'
|
|
148
|
+
errors.concat(validate_string_value(field, value, prop))
|
|
149
|
+
when 'number', 'integer'
|
|
150
|
+
errors.concat(validate_number_value(field, value, prop))
|
|
151
|
+
when 'boolean'
|
|
152
|
+
errors << "Field '#{field}' must be a boolean" unless [true, false].include?(value)
|
|
153
|
+
when 'array'
|
|
154
|
+
errors.concat(validate_array_value(field, value, prop))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
errors
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Validate a string value against its property schema.
|
|
161
|
+
# @param field [String] field name
|
|
162
|
+
# @param value [Object] the value
|
|
163
|
+
# @param prop [Hash] property schema
|
|
164
|
+
# @return [Array<String>] validation errors
|
|
165
|
+
def self.validate_string_value(field, value, prop)
|
|
166
|
+
errors = []
|
|
167
|
+
|
|
168
|
+
unless value.is_a?(String)
|
|
169
|
+
errors << "Field '#{field}' must be a string"
|
|
170
|
+
return errors
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
if prop['enum'].is_a?(Array) && !prop['enum'].include?(value)
|
|
174
|
+
errors << "Field '#{field}' must be one of: #{prop['enum'].join(', ')}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
if prop['oneOf'].is_a?(Array)
|
|
178
|
+
allowed = prop['oneOf'].map { |o| o['const'] }
|
|
179
|
+
errors << "Field '#{field}' must be one of: #{allowed.join(', ')}" unless allowed.include?(value)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
if prop['pattern']
|
|
183
|
+
begin
|
|
184
|
+
unless value.match?(Regexp.new(prop['pattern']))
|
|
185
|
+
errors << "Field '#{field}' must match pattern '#{prop['pattern']}'"
|
|
186
|
+
end
|
|
187
|
+
rescue RegexpError
|
|
188
|
+
# Skip pattern validation if the pattern is invalid
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
if prop['minLength'] && value.length < prop['minLength']
|
|
193
|
+
errors << "Field '#{field}' must be at least #{prop['minLength']} characters"
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
if prop['maxLength'] && value.length > prop['maxLength']
|
|
197
|
+
errors << "Field '#{field}' must be at most #{prop['maxLength']} characters"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
errors
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Validate a number value against its property schema.
|
|
204
|
+
# @param field [String] field name
|
|
205
|
+
# @param value [Object] the value
|
|
206
|
+
# @param prop [Hash] property schema
|
|
207
|
+
# @return [Array<String>] validation errors
|
|
208
|
+
def self.validate_number_value(field, value, prop)
|
|
209
|
+
errors = []
|
|
210
|
+
|
|
211
|
+
unless value.is_a?(Numeric)
|
|
212
|
+
errors << "Field '#{field}' must be a number"
|
|
213
|
+
return errors
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
errors << "Field '#{field}' must be an integer" if prop['type'] == 'integer' && !value.is_a?(Integer)
|
|
217
|
+
|
|
218
|
+
errors << "Field '#{field}' must be >= #{prop['minimum']}" if prop['minimum'] && value < prop['minimum']
|
|
219
|
+
|
|
220
|
+
errors << "Field '#{field}' must be <= #{prop['maximum']}" if prop['maximum'] && value > prop['maximum']
|
|
221
|
+
|
|
222
|
+
errors
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# Validate an array value against its property schema (multi-select enum).
|
|
226
|
+
# @param field [String] field name
|
|
227
|
+
# @param value [Object] the value
|
|
228
|
+
# @param prop [Hash] property schema
|
|
229
|
+
# @return [Array<String>] validation errors
|
|
230
|
+
def self.validate_array_value(field, value, prop)
|
|
231
|
+
errors = []
|
|
232
|
+
|
|
233
|
+
unless value.is_a?(Array)
|
|
234
|
+
errors << "Field '#{field}' must be an array"
|
|
235
|
+
return errors
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
items = prop['items'] || {}
|
|
239
|
+
allowed = if items['enum'].is_a?(Array)
|
|
240
|
+
items['enum']
|
|
241
|
+
elsif items['anyOf'].is_a?(Array)
|
|
242
|
+
items['anyOf'].map { |o| o['const'] }
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
if allowed
|
|
246
|
+
value.each do |v|
|
|
247
|
+
errors << "Field '#{field}' contains invalid value '#{v}'" unless allowed.include?(v)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
if prop['minItems'] && value.length < prop['minItems']
|
|
252
|
+
errors << "Field '#{field}' must have at least #{prop['minItems']} items"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
if prop['maxItems'] && value.length > prop['maxItems']
|
|
256
|
+
errors << "Field '#{field}' must have at most #{prop['maxItems']} items"
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
errors
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
data/lib/mcp_client/errors.rb
CHANGED
|
@@ -50,5 +50,11 @@ module MCPClient
|
|
|
50
50
|
|
|
51
51
|
# Raised when transport type cannot be determined from target URL/command
|
|
52
52
|
class TransportDetectionError < MCPError; end
|
|
53
|
+
|
|
54
|
+
# Raised when a task is not found
|
|
55
|
+
class TaskNotFound < MCPError; end
|
|
56
|
+
|
|
57
|
+
# Raised when there's an error creating or managing a task
|
|
58
|
+
class TaskError < MCPError; end
|
|
53
59
|
end
|
|
54
60
|
end
|
|
@@ -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
|
|