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.
- checksums.yaml +4 -4
- data/README.md +230 -14
- data/lib/json_rpc_handler.rb +1 -1
- data/lib/mcp/client.rb +62 -48
- data/lib/mcp/progress.rb +6 -3
- data/lib/mcp/server/transports/stdio_transport.rb +40 -3
- data/lib/mcp/server/transports/streamable_http_transport.rb +356 -60
- data/lib/mcp/server.rb +216 -62
- data/lib/mcp/server_context.rb +33 -1
- data/lib/mcp/server_session.rb +107 -0
- data/lib/mcp/tool/schema.rb +1 -14
- data/lib/mcp/transport.rb +15 -2
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +1 -0
- metadata +6 -3
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
283
|
+
if tools && !capabilities.dig(:sampling, :tools)
|
|
284
|
+
raise "Client does not support sampling with tools."
|
|
285
|
+
end
|
|
221
286
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
287
|
+
if tool_choice && !capabilities.dig(:sampling, :tools)
|
|
288
|
+
raise "Client does not support sampling with tool_choice."
|
|
289
|
+
end
|
|
225
290
|
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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(
|
|
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
|
data/lib/mcp/server_context.rb
CHANGED
|
@@ -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
|
data/lib/mcp/tool/schema.rb
CHANGED
|
@@ -8,7 +8,7 @@ module MCP
|
|
|
8
8
|
attr_reader :schema
|
|
9
9
|
|
|
10
10
|
def initialize(schema = {})
|
|
11
|
-
@schema =
|
|
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