mcp 0.9.2 → 0.11.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.
data/lib/mcp/server.rb CHANGED
@@ -24,6 +24,12 @@ module MCP
24
24
  UNSUPPORTED_PROPERTIES_UNTIL_2025_06_18 = [:description, :icons].freeze
25
25
  UNSUPPORTED_PROPERTIES_UNTIL_2025_03_26 = [:title, :websiteUrl].freeze
26
26
 
27
+ DEFAULT_COMPLETION_RESULT = { completion: { values: [], hasMore: false } }.freeze
28
+
29
+ # Servers return an array of completion values ranked by relevance, with maximum 100 items per response.
30
+ # https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion#completion-results
31
+ MAX_COMPLETION_VALUES = 100
32
+
27
33
  class RequestHandlerError < StandardError
28
34
  attr_reader :error_type
29
35
  attr_reader :original_error
@@ -48,6 +54,7 @@ module MCP
48
54
  include Instrumentation
49
55
 
50
56
  attr_accessor :description, :icons, :name, :title, :version, :website_url, :instructions, :tools, :prompts, :resources, :server_context, :configuration, :capabilities, :transport, :logging_message_notification
57
+ attr_reader :client_capabilities
51
58
 
52
59
  def initialize(
53
60
  description: nil,
@@ -86,6 +93,7 @@ module MCP
86
93
  validate!
87
94
 
88
95
  @capabilities = capabilities || default_capabilities
96
+ @client_capabilities = nil
89
97
  @logging_message_notification = nil
90
98
 
91
99
  @handlers = {
@@ -100,26 +108,40 @@ module MCP
100
108
  Methods::PING => ->(_) { {} },
101
109
  Methods::NOTIFICATIONS_INITIALIZED => ->(_) {},
102
110
  Methods::NOTIFICATIONS_PROGRESS => ->(_) {},
111
+ Methods::COMPLETION_COMPLETE => ->(_) { DEFAULT_COMPLETION_RESULT },
103
112
  Methods::LOGGING_SET_LEVEL => method(:configure_logging_level),
104
113
 
105
114
  # No op handlers for currently unsupported methods
106
115
  Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
107
116
  Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
108
- Methods::COMPLETION_COMPLETE => ->(_) { { completion: { values: [], hasMore: false } } },
109
117
  Methods::ELICITATION_CREATE => ->(_) {},
110
118
  }
111
119
  @transport = transport
112
120
  end
113
121
 
114
- def handle(request)
115
- JsonRpcHandler.handle(request) do |method|
116
- handle_request(request, method)
122
+ # Processes a parsed JSON-RPC request and returns the response as a Hash.
123
+ #
124
+ # @param request [Hash] A parsed JSON-RPC request.
125
+ # @param session [ServerSession, nil] Per-connection session. Passed by
126
+ # `ServerSession#handle` for session-scoped notification delivery.
127
+ # When `nil`, progress and logging notifications from tool handlers are silently skipped.
128
+ # @return [Hash, nil] The JSON-RPC response, or `nil` for notifications.
129
+ def handle(request, session: nil)
130
+ JsonRpcHandler.handle(request) do |method, request_id|
131
+ handle_request(request, method, session: session, related_request_id: request_id)
117
132
  end
118
133
  end
119
134
 
120
- def handle_json(request)
121
- JsonRpcHandler.handle_json(request) do |method|
122
- handle_request(request, method)
135
+ # Processes a JSON-RPC request string and returns the response as a JSON string.
136
+ #
137
+ # @param request [String] A JSON-RPC request as a JSON string.
138
+ # @param session [ServerSession, nil] Per-connection session. Passed by
139
+ # `ServerSession#handle_json` for session-scoped notification delivery.
140
+ # When `nil`, progress and logging notifications from tool handlers are silently skipped.
141
+ # @return [String, nil] The JSON-RPC response as JSON, or `nil` for notifications.
142
+ def handle_json(request, session: nil)
143
+ JsonRpcHandler.handle_json(request) do |method, request_id|
144
+ handle_request(request, method, session: session, related_request_id: request_id)
123
145
  end
124
146
  end
125
147
 
@@ -172,21 +194,6 @@ module MCP
172
194
  report_exception(e, { notification: "resources_list_changed" })
173
195
  end
174
196
 
175
- def notify_progress(progress_token:, progress:, total: nil, message: nil)
176
- return unless @transport
177
-
178
- params = {
179
- "progressToken" => progress_token,
180
- "progress" => progress,
181
- "total" => total,
182
- "message" => message,
183
- }.compact
184
-
185
- @transport.send_notification(Methods::NOTIFICATIONS_PROGRESS, params)
186
- rescue => e
187
- report_exception(e, notification: "progress")
188
- end
189
-
190
197
  def notify_log_message(data:, level:, logger: nil)
191
198
  return unless @transport
192
199
  return unless logging_message_notification&.should_notify?(level)
@@ -199,32 +206,100 @@ module MCP
199
206
  report_exception(e, { notification: "log_message" })
200
207
  end
201
208
 
202
- def resources_list_handler(&block)
203
- @handlers[Methods::RESOURCES_LIST] = block
204
- end
209
+ # Sends a `sampling/createMessage` request to the client.
210
+ # For single-client transports (e.g., `StdioTransport`). For multi-client transports
211
+ # (e.g., `StreamableHTTPTransport`), use `ServerSession#create_sampling_message` instead
212
+ # to ensure the request is routed to the correct client.
213
+ def create_sampling_message(
214
+ messages:,
215
+ max_tokens:,
216
+ system_prompt: nil,
217
+ model_preferences: nil,
218
+ include_context: nil,
219
+ temperature: nil,
220
+ stop_sequences: nil,
221
+ metadata: nil,
222
+ tools: nil,
223
+ tool_choice: nil,
224
+ related_request_id: nil
225
+ )
226
+ unless @transport
227
+ raise "Cannot send sampling request without a transport."
228
+ end
205
229
 
230
+ params = build_sampling_params(
231
+ @client_capabilities,
232
+ messages: messages,
233
+ max_tokens: max_tokens,
234
+ system_prompt: system_prompt,
235
+ model_preferences: model_preferences,
236
+ include_context: include_context,
237
+ temperature: temperature,
238
+ stop_sequences: stop_sequences,
239
+ metadata: metadata,
240
+ tools: tools,
241
+ tool_choice: tool_choice,
242
+ )
243
+
244
+ @transport.send_request(Methods::SAMPLING_CREATE_MESSAGE, params)
245
+ end
246
+
247
+ # Sets a custom handler for `resources/read` requests.
248
+ # The block receives the parsed request params and should return resource
249
+ # contents. The return value is set as the `contents` field of the response.
250
+ #
251
+ # @yield [params] The request params containing `:uri`.
252
+ # @yieldreturn [Array<Hash>, Hash] Resource contents.
206
253
  def resources_read_handler(&block)
207
254
  @handlers[Methods::RESOURCES_READ] = block
208
255
  end
209
256
 
210
- def resources_templates_list_handler(&block)
211
- @handlers[Methods::RESOURCES_TEMPLATES_LIST] = block
212
- end
213
-
214
- def tools_list_handler(&block)
215
- @handlers[Methods::TOOLS_LIST] = block
216
- end
257
+ # Sets a custom handler for `completion/complete` requests.
258
+ # The block receives the parsed request params and should return completion values.
259
+ #
260
+ # @yield [params] The request params containing `:ref`, `:argument`, and optionally `:context`.
261
+ # @yieldreturn [Hash] A hash with `:completion` key containing `:values`, optional `:total`, and `:hasMore`.
262
+ def completion_handler(&block)
263
+ @handlers[Methods::COMPLETION_COMPLETE] = block
264
+ end
265
+
266
+ def build_sampling_params(
267
+ capabilities,
268
+ messages:,
269
+ max_tokens:,
270
+ system_prompt: nil,
271
+ model_preferences: nil,
272
+ include_context: nil,
273
+ temperature: nil,
274
+ stop_sequences: nil,
275
+ metadata: nil,
276
+ tools: nil,
277
+ tool_choice: nil
278
+ )
279
+ unless capabilities&.dig(:sampling)
280
+ raise "Client does not support sampling."
281
+ end
217
282
 
218
- def tools_call_handler(&block)
219
- @handlers[Methods::TOOLS_CALL] = block
220
- end
283
+ if tools && !capabilities.dig(:sampling, :tools)
284
+ raise "Client does not support sampling with tools."
285
+ end
221
286
 
222
- def prompts_list_handler(&block)
223
- @handlers[Methods::PROMPTS_LIST] = block
224
- end
287
+ if tool_choice && !capabilities.dig(:sampling, :tools)
288
+ raise "Client does not support sampling with tool_choice."
289
+ end
225
290
 
226
- def prompts_get_handler(&block)
227
- @handlers[Methods::PROMPTS_GET] = block
291
+ {
292
+ messages: messages,
293
+ maxTokens: max_tokens,
294
+ systemPrompt: system_prompt,
295
+ modelPreferences: model_preferences,
296
+ includeContext: include_context,
297
+ temperature: temperature,
298
+ stopSequences: stop_sequences,
299
+ metadata: metadata,
300
+ tools: tools,
301
+ toolChoice: tool_choice,
302
+ }.compact
228
303
  end
229
304
 
230
305
  private
@@ -297,11 +372,12 @@ module MCP
297
372
  end
298
373
  end
299
374
 
300
- def handle_request(request, method)
375
+ def handle_request(request, method, session: nil, related_request_id: nil)
301
376
  handler = @handlers[method]
302
377
  unless handler
303
378
  instrument_call("unsupported_method") do
304
- add_instrumentation_data(client: @client) if @client
379
+ client = session&.client || @client
380
+ add_instrumentation_data(client: client) if client
305
381
  end
306
382
  return
307
383
  end
@@ -311,6 +387,8 @@ module MCP
311
387
  ->(params) {
312
388
  instrument_call(method) do
313
389
  result = case method
390
+ when Methods::INITIALIZE
391
+ init(params, session: session)
314
392
  when Methods::TOOLS_LIST
315
393
  { tools: @handlers[Methods::TOOLS_LIST].call(params) }
316
394
  when Methods::PROMPTS_LIST
@@ -321,19 +399,25 @@ module MCP
321
399
  { contents: @handlers[Methods::RESOURCES_READ].call(params) }
322
400
  when Methods::RESOURCES_TEMPLATES_LIST
323
401
  { resourceTemplates: @handlers[Methods::RESOURCES_TEMPLATES_LIST].call(params) }
402
+ when Methods::TOOLS_CALL
403
+ call_tool(params, session: session, related_request_id: related_request_id)
404
+ when Methods::COMPLETION_COMPLETE
405
+ complete(params)
406
+ when Methods::LOGGING_SET_LEVEL
407
+ configure_logging_level(params, session: session)
324
408
  else
325
409
  @handlers[method].call(params)
326
410
  end
327
- add_instrumentation_data(client: @client) if @client
411
+ client = session&.client || @client
412
+ add_instrumentation_data(client: client) if client
328
413
 
329
414
  result
415
+ rescue RequestHandlerError => e
416
+ report_exception(e.original_error || e, { request: request })
417
+ add_instrumentation_data(error: e.error_type)
418
+ raise e
330
419
  rescue => e
331
420
  report_exception(e, { request: request })
332
- if e.is_a?(RequestHandlerError)
333
- add_instrumentation_data(error: e.error_type)
334
- raise e
335
- end
336
-
337
421
  add_instrumentation_data(error: :internal_error)
338
422
  raise RequestHandlerError.new("Internal error handling #{method} request", request, original_error: e)
339
423
  end
@@ -360,10 +444,17 @@ module MCP
360
444
  }.compact
361
445
  end
362
446
 
363
- def init(params)
364
- @client = params[:clientInfo] if params
447
+ def init(params, session: nil)
448
+ if params
449
+ if session
450
+ session.store_client_info(client: params[:clientInfo], capabilities: params[:capabilities])
451
+ else
452
+ @client = params[:clientInfo]
453
+ @client_capabilities = params[:capabilities]
454
+ end
455
+ protocol_version = params[:protocolVersion]
456
+ end
365
457
 
366
- protocol_version = params[:protocolVersion] if params
367
458
  negotiated_version = if Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(protocol_version)
368
459
  protocol_version
369
460
  else
@@ -389,7 +480,7 @@ module MCP
389
480
  }.compact
390
481
  end
391
482
 
392
- def configure_logging_level(request)
483
+ def configure_logging_level(request, session: nil)
393
484
  if capabilities[:logging].nil?
394
485
  raise RequestHandlerError.new("Server does not support logging", request, error_type: :internal_error)
395
486
  end
@@ -399,6 +490,7 @@ module MCP
399
490
  raise RequestHandlerError.new("Invalid log level #{request[:level]}", request, error_type: :invalid_params)
400
491
  end
401
492
 
493
+ session&.configure_logging(logging_message_notification)
402
494
  @logging_message_notification = logging_message_notification
403
495
 
404
496
  {}
@@ -408,7 +500,7 @@ module MCP
408
500
  @tools.values.map(&:to_h)
409
501
  end
410
502
 
411
- def call_tool(request)
503
+ def call_tool(request, session: nil, related_request_id: nil)
412
504
  tool_name = request[:name]
413
505
 
414
506
  tool = tools[tool_name]
@@ -425,7 +517,7 @@ module MCP
425
517
  add_instrumentation_data(error: :missing_required_arguments)
426
518
 
427
519
  missing = tool.input_schema.missing_required_arguments(arguments).join(", ")
428
- return error_tool_response("Missing required arguments: #{missing}")
520
+ raise RequestHandlerError.new("Missing required arguments: #{missing}", request, error_type: :invalid_params)
429
521
  end
430
522
 
431
523
  if configuration.validate_tool_call_arguments && tool.input_schema
@@ -434,19 +526,22 @@ module MCP
434
526
  rescue Tool::InputSchema::ValidationError => e
435
527
  add_instrumentation_data(error: :invalid_schema)
436
528
 
437
- return error_tool_response(e.message)
529
+ raise RequestHandlerError.new(e.message, request, error_type: :invalid_params)
438
530
  end
439
531
  end
440
532
 
441
533
  progress_token = request.dig(:_meta, :progressToken)
442
534
 
443
- call_tool_with_args(tool, arguments, server_context_with_meta(request), progress_token: progress_token)
535
+ call_tool_with_args(tool, arguments, server_context_with_meta(request), progress_token: progress_token, session: session, related_request_id: related_request_id)
444
536
  rescue RequestHandlerError
445
537
  raise
446
538
  rescue => e
447
- report_exception(e, request: request)
448
-
449
- error_tool_response("Internal error calling tool #{tool_name}: #{e.message}")
539
+ raise RequestHandlerError.new(
540
+ "Internal error calling tool #{tool_name}: #{e.message}",
541
+ request,
542
+ error_type: :internal_error,
543
+ original_error: e,
544
+ )
450
545
  end
451
546
 
452
547
  def list_prompts(request)
@@ -483,6 +578,14 @@ module MCP
483
578
  @resource_templates.map(&:to_h)
484
579
  end
485
580
 
581
+ def complete(params)
582
+ validate_completion_params!(params)
583
+
584
+ result = @handlers[Methods::COMPLETION_COMPLETE].call(params)
585
+
586
+ normalize_completion_result(result)
587
+ end
588
+
486
589
  def report_exception(exception, server_context = {})
487
590
  configuration.exception_reporter.call(exception, server_context)
488
591
  end
@@ -509,12 +612,12 @@ module MCP
509
612
  parameters.any? { |type, name| type == :keyrest || name == :server_context }
510
613
  end
511
614
 
512
- def call_tool_with_args(tool, arguments, context, progress_token: nil)
615
+ def call_tool_with_args(tool, arguments, context, progress_token: nil, session: nil, related_request_id: nil)
513
616
  args = arguments&.transform_keys(&:to_sym) || {}
514
617
 
515
618
  if accepts_server_context?(tool.method(:call))
516
- progress = Progress.new(server: self, progress_token: progress_token)
517
- server_context = ServerContext.new(context, progress: progress)
619
+ progress = Progress.new(notification_target: session, progress_token: progress_token, related_request_id: related_request_id)
620
+ server_context = ServerContext.new(context, progress: progress, notification_target: session, related_request_id: related_request_id)
518
621
  tool.call(**args, server_context: server_context).to_h
519
622
  else
520
623
  tool.call(**args).to_h
@@ -541,5 +644,56 @@ module MCP
541
644
  server_context
542
645
  end
543
646
  end
647
+
648
+ def validate_completion_params!(params)
649
+ unless params.is_a?(Hash)
650
+ raise RequestHandlerError.new("Invalid params", params, error_type: :invalid_params)
651
+ end
652
+
653
+ ref = params[:ref]
654
+ if ref.nil? || ref[:type].nil?
655
+ raise RequestHandlerError.new("Missing or invalid ref", params, error_type: :invalid_params)
656
+ end
657
+
658
+ argument = params[:argument]
659
+ if argument.nil? || argument[:name].nil? || !argument.key?(:value)
660
+ raise RequestHandlerError.new("Missing argument name or value", params, error_type: :invalid_params)
661
+ end
662
+
663
+ case ref[:type]
664
+ when "ref/prompt"
665
+ unless @prompts[ref[:name]]
666
+ raise RequestHandlerError.new("Prompt not found: #{ref[:name]}", params, error_type: :invalid_params)
667
+ end
668
+ when "ref/resource"
669
+ uri = ref[:uri]
670
+ found = @resource_index.key?(uri) || @resource_templates.any? { |t| t.uri_template == uri }
671
+ unless found
672
+ raise RequestHandlerError.new("Resource not found: #{uri}", params, error_type: :invalid_params)
673
+ end
674
+ else
675
+ raise RequestHandlerError.new("Invalid ref type: #{ref[:type]}", params, error_type: :invalid_params)
676
+ end
677
+ end
678
+
679
+ def normalize_completion_result(result)
680
+ return DEFAULT_COMPLETION_RESULT unless result.is_a?(Hash)
681
+
682
+ completion = result[:completion] || result["completion"]
683
+ return DEFAULT_COMPLETION_RESULT unless completion.is_a?(Hash)
684
+
685
+ values = completion[:values] || completion["values"] || []
686
+ total = completion[:total] || completion["total"]
687
+ has_more = completion[:hasMore] || completion["hasMore"] || false
688
+
689
+ count = values.length
690
+ if count > MAX_COMPLETION_VALUES
691
+ has_more = true
692
+ total ||= count
693
+ values = values.first(MAX_COMPLETION_VALUES)
694
+ end
695
+
696
+ { completion: { values: values, total: total, hasMore: has_more }.compact }
697
+ end
544
698
  end
545
699
  end
@@ -2,15 +2,47 @@
2
2
 
3
3
  module MCP
4
4
  class ServerContext
5
- def initialize(context, progress:)
5
+ def initialize(context, progress:, notification_target:, related_request_id: nil)
6
6
  @context = context
7
7
  @progress = progress
8
+ @notification_target = notification_target
9
+ @related_request_id = related_request_id
8
10
  end
9
11
 
12
+ # Reports progress for the current tool operation.
13
+ # The notification is automatically scoped to the originating session.
14
+ #
15
+ # @param progress [Numeric] Current progress value.
16
+ # @param total [Numeric, nil] Total expected value.
17
+ # @param message [String, nil] Human-readable status message.
10
18
  def report_progress(progress, total: nil, message: nil)
11
19
  @progress.report(progress, total: total, message: message)
12
20
  end
13
21
 
22
+ # Sends a log message notification scoped to the originating session.
23
+ #
24
+ # @param data [Object] The log data to send.
25
+ # @param level [String] Log level (e.g., `"debug"`, `"info"`, `"error"`).
26
+ # @param logger [String, nil] Logger name.
27
+ def notify_log_message(data:, level:, logger: nil)
28
+ return unless @notification_target
29
+
30
+ @notification_target.notify_log_message(data: data, level: level, logger: logger, related_request_id: @related_request_id)
31
+ end
32
+
33
+ # Delegates to the session so the request is scoped to the originating client.
34
+ # Falls back to `@context` (via `method_missing`) when `@notification_target`
35
+ # does not support sampling.
36
+ def create_sampling_message(**kwargs)
37
+ if @notification_target.respond_to?(:create_sampling_message)
38
+ @notification_target.create_sampling_message(**kwargs, related_request_id: @related_request_id)
39
+ elsif @context.respond_to?(:create_sampling_message)
40
+ @context.create_sampling_message(**kwargs, related_request_id: @related_request_id)
41
+ else
42
+ raise NoMethodError, "undefined method 'create_sampling_message' for #{self}"
43
+ end
44
+ end
45
+
14
46
  def method_missing(name, ...)
15
47
  if @context.respond_to?(name)
16
48
  @context.public_send(name, ...)
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "methods"
4
+
5
+ module MCP
6
+ # Holds per-connection state for a single client session.
7
+ # Created by the transport layer; delegates request handling to the shared `Server`.
8
+ class ServerSession
9
+ attr_reader :session_id, :client, :logging_message_notification
10
+
11
+ def initialize(server:, transport:, session_id: nil)
12
+ @server = server
13
+ @transport = transport
14
+ @session_id = session_id
15
+ @client = nil
16
+ @client_capabilities = nil
17
+ @logging_message_notification = nil
18
+ end
19
+
20
+ def handle(request)
21
+ @server.handle(request, session: self)
22
+ end
23
+
24
+ def handle_json(request_json)
25
+ @server.handle_json(request_json, session: self)
26
+ end
27
+
28
+ # Called by `Server#init` during the initialization handshake.
29
+ def store_client_info(client:, capabilities: nil)
30
+ @client = client
31
+ @client_capabilities = capabilities
32
+ end
33
+
34
+ # Called by `Server#configure_logging_level`.
35
+ def configure_logging(logging_message_notification)
36
+ @logging_message_notification = logging_message_notification
37
+ end
38
+
39
+ # Returns per-session client capabilities, falling back to global.
40
+ def client_capabilities
41
+ @client_capabilities || @server.client_capabilities
42
+ end
43
+
44
+ # Sends a `sampling/createMessage` request scoped to this session.
45
+ def create_sampling_message(related_request_id: nil, **kwargs)
46
+ params = @server.build_sampling_params(client_capabilities, **kwargs)
47
+ send_to_transport_request(Methods::SAMPLING_CREATE_MESSAGE, params, related_request_id: related_request_id)
48
+ end
49
+
50
+ # Sends a progress notification to this session only.
51
+ def notify_progress(progress_token:, progress:, total: nil, message: nil, related_request_id: nil)
52
+ params = {
53
+ "progressToken" => progress_token,
54
+ "progress" => progress,
55
+ "total" => total,
56
+ "message" => message,
57
+ }.compact
58
+
59
+ send_to_transport(Methods::NOTIFICATIONS_PROGRESS, params, related_request_id: related_request_id)
60
+ rescue => e
61
+ @server.report_exception(e, notification: "progress")
62
+ end
63
+
64
+ # Sends a log message notification to this session only.
65
+ def notify_log_message(data:, level:, logger: nil, related_request_id: nil)
66
+ effective_logging = @logging_message_notification || @server.logging_message_notification
67
+ return unless effective_logging&.should_notify?(level)
68
+
69
+ params = { "data" => data, "level" => level }
70
+ params["logger"] = logger if logger
71
+
72
+ send_to_transport(Methods::NOTIFICATIONS_MESSAGE, params, related_request_id: related_request_id)
73
+ rescue => e
74
+ @server.report_exception(e, { notification: "log_message" })
75
+ end
76
+
77
+ private
78
+
79
+ # Branches on `@session_id` because `StdioTransport` creates a `ServerSession` without
80
+ # a `session_id` (`session_id: nil`), while `StreamableHTTPTransport` always provides one.
81
+ #
82
+ # TODO: When Ruby 2.7 support is dropped, replace with a direct call:
83
+ # `@transport.send_notification(method, params, session_id: @session_id)` and
84
+ # add `**` to `Transport#send_notification` and `StdioTransport#send_notification`.
85
+ def send_to_transport(method, params, related_request_id: nil)
86
+ if @session_id
87
+ @transport.send_notification(method, params, session_id: @session_id, related_request_id: related_request_id)
88
+ else
89
+ @transport.send_notification(method, params)
90
+ end
91
+ end
92
+
93
+ # Branches on `@session_id` because `StdioTransport` creates a `ServerSession` without
94
+ # a `session_id` (`session_id: nil`), while `StreamableHTTPTransport` always provides one.
95
+ #
96
+ # TODO: When Ruby 2.7 support is dropped, replace with a direct call:
97
+ # `@transport.send_request(method, params, session_id: @session_id)` and
98
+ # add `**` to `Transport#send_request` and `StdioTransport#send_request`.
99
+ def send_to_transport_request(method, params, related_request_id: nil)
100
+ if @session_id
101
+ @transport.send_request(method, params, session_id: @session_id, related_request_id: related_request_id)
102
+ else
103
+ @transport.send_request(method, params)
104
+ end
105
+ end
106
+ end
107
+ end
@@ -8,7 +8,7 @@ module MCP
8
8
  attr_reader :schema
9
9
 
10
10
  def initialize(schema = {})
11
- @schema = deep_transform_keys(JSON.parse(JSON.dump(schema)), &:to_sym)
11
+ @schema = JSON.parse(JSON.dump(schema), symbolize_names: true)
12
12
  @schema[:type] ||= "object"
13
13
  validate_schema!
14
14
  end
@@ -27,19 +27,6 @@ module MCP
27
27
  JSON::Validator.fully_validate(to_h, data)
28
28
  end
29
29
 
30
- def deep_transform_keys(schema, &block)
31
- case schema
32
- when Hash
33
- schema.each_with_object({}) do |(key, value), result|
34
- result[yield(key)] = deep_transform_keys(value, &block)
35
- end
36
- when Array
37
- schema.map { |e| deep_transform_keys(e, &block) }
38
- else
39
- schema
40
- end
41
- end
42
-
43
30
  def validate_schema!
44
31
  schema = to_h
45
32
  gem_path = File.realpath(Gem.loaded_specs["json-schema"].full_gem_path)
data/lib/mcp/transport.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "securerandom"
4
+
3
5
  module MCP
4
6
  class Transport
5
7
  # Initialize the transport with the server instance
@@ -36,10 +38,21 @@ module MCP
36
38
  send_response(response) if response
37
39
  end
38
40
 
39
- # Send a notification to the client
40
- # Returns true if the notification was sent successfully
41
+ # Send a notification to the client.
42
+ # Returns true if the notification was sent successfully.
41
43
  def send_notification(method, params = nil)
42
44
  raise NotImplementedError, "Subclasses must implement send_notification"
43
45
  end
46
+
47
+ # Send a JSON-RPC request to the client and wait for a response.
48
+ def send_request(method, params = nil)
49
+ raise NotImplementedError, "Subclasses must implement send_request"
50
+ end
51
+
52
+ private
53
+
54
+ def generate_request_id
55
+ SecureRandom.uuid
56
+ end
44
57
  end
45
58
  end
data/lib/mcp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MCP
4
- VERSION = "0.9.2"
4
+ VERSION = "0.11.0"
5
5
  end
data/lib/mcp.rb CHANGED
@@ -15,6 +15,7 @@ module MCP
15
15
  autoload :Resource, "mcp/resource"
16
16
  autoload :ResourceTemplate, "mcp/resource_template"
17
17
  autoload :Server, "mcp/server"
18
+ autoload :ServerSession, "mcp/server_session"
18
19
  autoload :Tool, "mcp/tool"
19
20
 
20
21
  class << self