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.
@@ -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
@@ -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-06-18: Support for server-initiated user interactions
67
- 'roots' => { 'listChanged' => true }, # MCP 2025-06-18: Support for roots
68
- 'sampling' => {} # MCP 2025-06-18: Support for server-initiated LLM sampling
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
  {
@@ -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 headers for MCP protocol
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 header if we have one (for non-initialize requests)
203
- return unless @session_id && request['method'] != 'initialize'
202
+ # Add session and protocol version headers for non-initialize requests
203
+ return unless request['method'] != 'initialize'
204
204
 
205
- req.headers['Mcp-Session-Id'] = @session_id
206
- @logger.debug("Adding session header: Mcp-Session-Id: #{@session_id}")
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
- result = rpc_request('completion/complete', { ref: ref, argument: argument })
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-06-18
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
- result = rpc_request('completion/complete', { ref: ref, argument: argument })
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-06-18)
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-06-18)
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-06-18)
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-06-18
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' => { 'ref' => ref, 'argument' => argument }
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-06-18)
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-06-18)
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-06-18)
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-06-18
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
- result = rpc_request('completion/complete', { ref: ref, argument: argument })
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 header if we have one (for non-initialize requests)
397
- if @session_id && request['method'] != 'initialize'
398
- req.headers['Mcp-Session-Id'] = @session_id
399
- @logger.debug("Adding session header: Mcp-Session-Id: #{@session_id}")
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-06-18)
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-06-18)
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-06-18)
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