mcp 0.10.0 → 0.14.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 +1267 -709
- data/lib/json_rpc_handler.rb +17 -10
- data/lib/mcp/client/http.rb +137 -15
- data/lib/mcp/client/paginated_result.rb +13 -0
- data/lib/mcp/client.rb +253 -66
- data/lib/mcp/configuration.rb +38 -2
- data/lib/mcp/content.rb +16 -12
- data/lib/mcp/instrumentation.rb +23 -2
- data/lib/mcp/methods.rb +4 -5
- data/lib/mcp/progress.rb +3 -1
- data/lib/mcp/prompt/result.rb +4 -3
- data/lib/mcp/resource/contents.rb +8 -7
- data/lib/mcp/resource.rb +4 -2
- data/lib/mcp/resource_template.rb +4 -2
- data/lib/mcp/server/pagination.rb +42 -0
- data/lib/mcp/server/transports/stdio_transport.rb +35 -0
- data/lib/mcp/server/transports/streamable_http_transport.rb +321 -88
- data/lib/mcp/server.rb +226 -46
- data/lib/mcp/server_context.rb +70 -2
- data/lib/mcp/server_session.rb +80 -7
- data/lib/mcp/tool/response.rb +4 -3
- data/lib/mcp/tool/schema.rb +1 -14
- data/lib/mcp/transport.rb +14 -0
- data/lib/mcp/version.rb +1 -1
- metadata +9 -5
data/lib/mcp/server.rb
CHANGED
|
@@ -6,6 +6,7 @@ require_relative "methods"
|
|
|
6
6
|
require_relative "logging_message_notification"
|
|
7
7
|
require_relative "progress"
|
|
8
8
|
require_relative "server_context"
|
|
9
|
+
require_relative "server/pagination"
|
|
9
10
|
require_relative "server/transports"
|
|
10
11
|
|
|
11
12
|
module MCP
|
|
@@ -24,15 +25,34 @@ module MCP
|
|
|
24
25
|
UNSUPPORTED_PROPERTIES_UNTIL_2025_06_18 = [:description, :icons].freeze
|
|
25
26
|
UNSUPPORTED_PROPERTIES_UNTIL_2025_03_26 = [:title, :websiteUrl].freeze
|
|
26
27
|
|
|
28
|
+
DEFAULT_COMPLETION_RESULT = { completion: { values: [], hasMore: false } }.freeze
|
|
29
|
+
|
|
30
|
+
# Servers return an array of completion values ranked by relevance, with maximum 100 items per response.
|
|
31
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/server/utilities/completion#completion-results
|
|
32
|
+
MAX_COMPLETION_VALUES = 100
|
|
33
|
+
|
|
27
34
|
class RequestHandlerError < StandardError
|
|
28
|
-
attr_reader :error_type
|
|
29
|
-
attr_reader :original_error
|
|
35
|
+
attr_reader :error_type, :original_error, :error_code, :error_data
|
|
30
36
|
|
|
31
|
-
def initialize(message, request, error_type: :internal_error, original_error: nil)
|
|
37
|
+
def initialize(message, request, error_type: :internal_error, original_error: nil, error_code: nil, error_data: nil)
|
|
32
38
|
super(message)
|
|
33
39
|
@request = request
|
|
34
40
|
@error_type = error_type
|
|
35
41
|
@original_error = original_error
|
|
42
|
+
@error_code = error_code
|
|
43
|
+
@error_data = error_data
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
class URLElicitationRequiredError < RequestHandlerError
|
|
48
|
+
def initialize(elicitations)
|
|
49
|
+
super(
|
|
50
|
+
"URL elicitation required",
|
|
51
|
+
nil,
|
|
52
|
+
error_type: :url_elicitation_required,
|
|
53
|
+
error_code: -32042,
|
|
54
|
+
error_data: { elicitations: elicitations },
|
|
55
|
+
)
|
|
36
56
|
end
|
|
37
57
|
end
|
|
38
58
|
|
|
@@ -46,8 +66,10 @@ module MCP
|
|
|
46
66
|
end
|
|
47
67
|
|
|
48
68
|
include Instrumentation
|
|
69
|
+
include Pagination
|
|
49
70
|
|
|
50
71
|
attr_accessor :description, :icons, :name, :title, :version, :website_url, :instructions, :tools, :prompts, :resources, :server_context, :configuration, :capabilities, :transport, :logging_message_notification
|
|
72
|
+
attr_reader :page_size, :client_capabilities
|
|
51
73
|
|
|
52
74
|
def initialize(
|
|
53
75
|
description: nil,
|
|
@@ -64,6 +86,7 @@ module MCP
|
|
|
64
86
|
server_context: nil,
|
|
65
87
|
configuration: nil,
|
|
66
88
|
capabilities: nil,
|
|
89
|
+
page_size: nil,
|
|
67
90
|
transport: nil
|
|
68
91
|
)
|
|
69
92
|
@description = description
|
|
@@ -80,18 +103,22 @@ module MCP
|
|
|
80
103
|
@resource_templates = resource_templates
|
|
81
104
|
@resource_index = index_resources_by_uri(resources)
|
|
82
105
|
@server_context = server_context
|
|
106
|
+
self.page_size = page_size
|
|
83
107
|
@configuration = MCP.configuration.merge(configuration)
|
|
84
108
|
@client = nil
|
|
85
109
|
|
|
86
110
|
validate!
|
|
87
111
|
|
|
88
112
|
@capabilities = capabilities || default_capabilities
|
|
113
|
+
@client_capabilities = nil
|
|
89
114
|
@logging_message_notification = nil
|
|
90
115
|
|
|
91
116
|
@handlers = {
|
|
92
117
|
Methods::RESOURCES_LIST => method(:list_resources),
|
|
93
118
|
Methods::RESOURCES_READ => method(:read_resource_no_content),
|
|
94
119
|
Methods::RESOURCES_TEMPLATES_LIST => method(:list_resource_templates),
|
|
120
|
+
Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
|
|
121
|
+
Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
|
|
95
122
|
Methods::TOOLS_LIST => method(:list_tools),
|
|
96
123
|
Methods::TOOLS_CALL => method(:call_tool),
|
|
97
124
|
Methods::PROMPTS_LIST => method(:list_prompts),
|
|
@@ -100,13 +127,9 @@ module MCP
|
|
|
100
127
|
Methods::PING => ->(_) { {} },
|
|
101
128
|
Methods::NOTIFICATIONS_INITIALIZED => ->(_) {},
|
|
102
129
|
Methods::NOTIFICATIONS_PROGRESS => ->(_) {},
|
|
130
|
+
Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED => ->(_) {},
|
|
131
|
+
Methods::COMPLETION_COMPLETE => ->(_) { DEFAULT_COMPLETION_RESULT },
|
|
103
132
|
Methods::LOGGING_SET_LEVEL => method(:configure_logging_level),
|
|
104
|
-
|
|
105
|
-
# No op handlers for currently unsupported methods
|
|
106
|
-
Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
|
|
107
|
-
Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
|
|
108
|
-
Methods::COMPLETION_COMPLETE => ->(_) { { completion: { values: [], hasMore: false } } },
|
|
109
|
-
Methods::ELICITATION_CREATE => ->(_) {},
|
|
110
133
|
}
|
|
111
134
|
@transport = transport
|
|
112
135
|
end
|
|
@@ -119,8 +142,8 @@ module MCP
|
|
|
119
142
|
# When `nil`, progress and logging notifications from tool handlers are silently skipped.
|
|
120
143
|
# @return [Hash, nil] The JSON-RPC response, or `nil` for notifications.
|
|
121
144
|
def handle(request, session: nil)
|
|
122
|
-
JsonRpcHandler.handle(request) do |method|
|
|
123
|
-
handle_request(request, method, session: session)
|
|
145
|
+
JsonRpcHandler.handle(request) do |method, request_id|
|
|
146
|
+
handle_request(request, method, session: session, related_request_id: request_id)
|
|
124
147
|
end
|
|
125
148
|
end
|
|
126
149
|
|
|
@@ -132,8 +155,8 @@ module MCP
|
|
|
132
155
|
# When `nil`, progress and logging notifications from tool handlers are silently skipped.
|
|
133
156
|
# @return [String, nil] The JSON-RPC response as JSON, or `nil` for notifications.
|
|
134
157
|
def handle_json(request, session: nil)
|
|
135
|
-
JsonRpcHandler.handle_json(request) do |method|
|
|
136
|
-
handle_request(request, method, session: session)
|
|
158
|
+
JsonRpcHandler.handle_json(request) do |method, request_id|
|
|
159
|
+
handle_request(request, method, session: session, related_request_id: request_id)
|
|
137
160
|
end
|
|
138
161
|
end
|
|
139
162
|
|
|
@@ -162,6 +185,14 @@ module MCP
|
|
|
162
185
|
@handlers[method_name] = block
|
|
163
186
|
end
|
|
164
187
|
|
|
188
|
+
def page_size=(page_size)
|
|
189
|
+
unless page_size.nil? || (page_size.is_a?(Integer) && page_size > 0)
|
|
190
|
+
raise ArgumentError, "page_size must be nil or a positive integer"
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
@page_size = page_size
|
|
194
|
+
end
|
|
195
|
+
|
|
165
196
|
def notify_tools_list_changed
|
|
166
197
|
return unless @transport
|
|
167
198
|
|
|
@@ -198,6 +229,14 @@ module MCP
|
|
|
198
229
|
report_exception(e, { notification: "log_message" })
|
|
199
230
|
end
|
|
200
231
|
|
|
232
|
+
# Sets a handler for `notifications/roots/list_changed` notifications.
|
|
233
|
+
# Called when a client notifies the server that its filesystem roots have changed.
|
|
234
|
+
#
|
|
235
|
+
# @yield [params] The notification params (typically `nil`).
|
|
236
|
+
def roots_list_changed_handler(&block)
|
|
237
|
+
@handlers[Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED] = block
|
|
238
|
+
end
|
|
239
|
+
|
|
201
240
|
# Sets a custom handler for `resources/read` requests.
|
|
202
241
|
# The block receives the parsed request params and should return resource
|
|
203
242
|
# contents. The return value is set as the `contents` field of the response.
|
|
@@ -208,6 +247,72 @@ module MCP
|
|
|
208
247
|
@handlers[Methods::RESOURCES_READ] = block
|
|
209
248
|
end
|
|
210
249
|
|
|
250
|
+
# Sets a custom handler for `completion/complete` requests.
|
|
251
|
+
# The block receives the parsed request params and should return completion values.
|
|
252
|
+
#
|
|
253
|
+
# @yield [params] The request params containing `:ref`, `:argument`, and optionally `:context`.
|
|
254
|
+
# @yieldreturn [Hash] A hash with `:completion` key containing `:values`, optional `:total`, and `:hasMore`.
|
|
255
|
+
def completion_handler(&block)
|
|
256
|
+
@handlers[Methods::COMPLETION_COMPLETE] = block
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Sets a custom handler for `resources/subscribe` requests.
|
|
260
|
+
# The block receives the parsed request params. The return value is
|
|
261
|
+
# ignored; the response is always an empty result `{}` per the MCP specification.
|
|
262
|
+
#
|
|
263
|
+
# @yield [params] The request params containing `:uri`.
|
|
264
|
+
def resources_subscribe_handler(&block)
|
|
265
|
+
@handlers[Methods::RESOURCES_SUBSCRIBE] = block
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Sets a custom handler for `resources/unsubscribe` requests.
|
|
269
|
+
# The block receives the parsed request params. The return value is
|
|
270
|
+
# ignored; the response is always an empty result `{}` per the MCP specification.
|
|
271
|
+
#
|
|
272
|
+
# @yield [params] The request params containing `:uri`.
|
|
273
|
+
def resources_unsubscribe_handler(&block)
|
|
274
|
+
@handlers[Methods::RESOURCES_UNSUBSCRIBE] = block
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def build_sampling_params(
|
|
278
|
+
capabilities,
|
|
279
|
+
messages:,
|
|
280
|
+
max_tokens:,
|
|
281
|
+
system_prompt: nil,
|
|
282
|
+
model_preferences: nil,
|
|
283
|
+
include_context: nil,
|
|
284
|
+
temperature: nil,
|
|
285
|
+
stop_sequences: nil,
|
|
286
|
+
metadata: nil,
|
|
287
|
+
tools: nil,
|
|
288
|
+
tool_choice: nil
|
|
289
|
+
)
|
|
290
|
+
unless capabilities&.dig(:sampling)
|
|
291
|
+
raise "Client does not support sampling."
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
if tools && !capabilities.dig(:sampling, :tools)
|
|
295
|
+
raise "Client does not support sampling with tools."
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
if tool_choice && !capabilities.dig(:sampling, :tools)
|
|
299
|
+
raise "Client does not support sampling with tool_choice."
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
{
|
|
303
|
+
messages: messages,
|
|
304
|
+
maxTokens: max_tokens,
|
|
305
|
+
systemPrompt: system_prompt,
|
|
306
|
+
modelPreferences: model_preferences,
|
|
307
|
+
includeContext: include_context,
|
|
308
|
+
temperature: temperature,
|
|
309
|
+
stopSequences: stop_sequences,
|
|
310
|
+
metadata: metadata,
|
|
311
|
+
tools: tools,
|
|
312
|
+
toolChoice: tool_choice,
|
|
313
|
+
}.compact
|
|
314
|
+
end
|
|
315
|
+
|
|
211
316
|
private
|
|
212
317
|
|
|
213
318
|
def validate!
|
|
@@ -278,10 +383,10 @@ module MCP
|
|
|
278
383
|
end
|
|
279
384
|
end
|
|
280
385
|
|
|
281
|
-
def handle_request(request, method, session: nil)
|
|
386
|
+
def handle_request(request, method, session: nil, related_request_id: nil)
|
|
282
387
|
handler = @handlers[method]
|
|
283
388
|
unless handler
|
|
284
|
-
instrument_call("unsupported_method") do
|
|
389
|
+
instrument_call("unsupported_method", server_context: { request: request }) do
|
|
285
390
|
client = session&.client || @client
|
|
286
391
|
add_instrumentation_data(client: client) if client
|
|
287
392
|
end
|
|
@@ -291,22 +396,24 @@ module MCP
|
|
|
291
396
|
Methods.ensure_capability!(method, capabilities)
|
|
292
397
|
|
|
293
398
|
->(params) {
|
|
294
|
-
|
|
399
|
+
reported_exception = nil
|
|
400
|
+
instrument_call(
|
|
401
|
+
method,
|
|
402
|
+
server_context: { request: request },
|
|
403
|
+
exception_already_reported: ->(e) { reported_exception.equal?(e) },
|
|
404
|
+
) do
|
|
295
405
|
result = case method
|
|
296
406
|
when Methods::INITIALIZE
|
|
297
407
|
init(params, session: session)
|
|
298
|
-
when Methods::TOOLS_LIST
|
|
299
|
-
{ tools: @handlers[Methods::TOOLS_LIST].call(params) }
|
|
300
|
-
when Methods::PROMPTS_LIST
|
|
301
|
-
{ prompts: @handlers[Methods::PROMPTS_LIST].call(params) }
|
|
302
|
-
when Methods::RESOURCES_LIST
|
|
303
|
-
{ resources: @handlers[Methods::RESOURCES_LIST].call(params) }
|
|
304
408
|
when Methods::RESOURCES_READ
|
|
305
409
|
{ contents: @handlers[Methods::RESOURCES_READ].call(params) }
|
|
306
|
-
when Methods::
|
|
307
|
-
|
|
410
|
+
when Methods::RESOURCES_SUBSCRIBE, Methods::RESOURCES_UNSUBSCRIBE
|
|
411
|
+
@handlers[method].call(params)
|
|
412
|
+
{}
|
|
308
413
|
when Methods::TOOLS_CALL
|
|
309
|
-
call_tool(params, session: session)
|
|
414
|
+
call_tool(params, session: session, related_request_id: related_request_id)
|
|
415
|
+
when Methods::COMPLETION_COMPLETE
|
|
416
|
+
complete(params)
|
|
310
417
|
when Methods::LOGGING_SET_LEVEL
|
|
311
418
|
configure_logging_level(params, session: session)
|
|
312
419
|
else
|
|
@@ -316,15 +423,17 @@ module MCP
|
|
|
316
423
|
add_instrumentation_data(client: client) if client
|
|
317
424
|
|
|
318
425
|
result
|
|
426
|
+
rescue RequestHandlerError => e
|
|
427
|
+
report_exception(e.original_error || e, { request: request })
|
|
428
|
+
add_instrumentation_data(error: e.error_type)
|
|
429
|
+
reported_exception = e
|
|
430
|
+
raise e
|
|
319
431
|
rescue => e
|
|
320
432
|
report_exception(e, { request: request })
|
|
321
|
-
if e.is_a?(RequestHandlerError)
|
|
322
|
-
add_instrumentation_data(error: e.error_type)
|
|
323
|
-
raise e
|
|
324
|
-
end
|
|
325
|
-
|
|
326
433
|
add_instrumentation_data(error: :internal_error)
|
|
327
|
-
|
|
434
|
+
wrapped = RequestHandlerError.new("Internal error handling #{method} request", request, original_error: e)
|
|
435
|
+
reported_exception = wrapped
|
|
436
|
+
raise wrapped
|
|
328
437
|
end
|
|
329
438
|
}
|
|
330
439
|
end
|
|
@@ -355,10 +464,11 @@ module MCP
|
|
|
355
464
|
session.store_client_info(client: params[:clientInfo], capabilities: params[:capabilities])
|
|
356
465
|
else
|
|
357
466
|
@client = params[:clientInfo]
|
|
467
|
+
@client_capabilities = params[:capabilities]
|
|
358
468
|
end
|
|
469
|
+
protocol_version = params[:protocolVersion]
|
|
359
470
|
end
|
|
360
471
|
|
|
361
|
-
protocol_version = params[:protocolVersion] if params
|
|
362
472
|
negotiated_version = if Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(protocol_version)
|
|
363
473
|
protocol_version
|
|
364
474
|
else
|
|
@@ -401,10 +511,12 @@ module MCP
|
|
|
401
511
|
end
|
|
402
512
|
|
|
403
513
|
def list_tools(request)
|
|
404
|
-
@tools.values
|
|
514
|
+
page = paginate(@tools.values, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
|
|
515
|
+
|
|
516
|
+
{ tools: page[:items], nextCursor: page[:next_cursor] }.compact
|
|
405
517
|
end
|
|
406
518
|
|
|
407
|
-
def call_tool(request, session: nil)
|
|
519
|
+
def call_tool(request, session: nil, related_request_id: nil)
|
|
408
520
|
tool_name = request[:name]
|
|
409
521
|
|
|
410
522
|
tool = tools[tool_name]
|
|
@@ -421,7 +533,7 @@ module MCP
|
|
|
421
533
|
add_instrumentation_data(error: :missing_required_arguments)
|
|
422
534
|
|
|
423
535
|
missing = tool.input_schema.missing_required_arguments(arguments).join(", ")
|
|
424
|
-
|
|
536
|
+
raise RequestHandlerError.new("Missing required arguments: #{missing}", request, error_type: :invalid_params)
|
|
425
537
|
end
|
|
426
538
|
|
|
427
539
|
if configuration.validate_tool_call_arguments && tool.input_schema
|
|
@@ -430,23 +542,28 @@ module MCP
|
|
|
430
542
|
rescue Tool::InputSchema::ValidationError => e
|
|
431
543
|
add_instrumentation_data(error: :invalid_schema)
|
|
432
544
|
|
|
433
|
-
|
|
545
|
+
raise RequestHandlerError.new(e.message, request, error_type: :invalid_params)
|
|
434
546
|
end
|
|
435
547
|
end
|
|
436
548
|
|
|
437
549
|
progress_token = request.dig(:_meta, :progressToken)
|
|
438
550
|
|
|
439
|
-
call_tool_with_args(tool, arguments, server_context_with_meta(request), progress_token: progress_token, session: session)
|
|
551
|
+
call_tool_with_args(tool, arguments, server_context_with_meta(request), progress_token: progress_token, session: session, related_request_id: related_request_id)
|
|
440
552
|
rescue RequestHandlerError
|
|
441
553
|
raise
|
|
442
554
|
rescue => e
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
555
|
+
raise RequestHandlerError.new(
|
|
556
|
+
"Internal error calling tool #{tool_name}: #{e.message}",
|
|
557
|
+
request,
|
|
558
|
+
error_type: :internal_error,
|
|
559
|
+
original_error: e,
|
|
560
|
+
)
|
|
446
561
|
end
|
|
447
562
|
|
|
448
563
|
def list_prompts(request)
|
|
449
|
-
@prompts.values
|
|
564
|
+
page = paginate(@prompts.values, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
|
|
565
|
+
|
|
566
|
+
{ prompts: page[:items], nextCursor: page[:next_cursor] }.compact
|
|
450
567
|
end
|
|
451
568
|
|
|
452
569
|
def get_prompt(request)
|
|
@@ -466,7 +583,9 @@ module MCP
|
|
|
466
583
|
end
|
|
467
584
|
|
|
468
585
|
def list_resources(request)
|
|
469
|
-
@resources
|
|
586
|
+
page = paginate(@resources, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
|
|
587
|
+
|
|
588
|
+
{ resources: page[:items], nextCursor: page[:next_cursor] }.compact
|
|
470
589
|
end
|
|
471
590
|
|
|
472
591
|
# Server implementation should set `resources_read_handler` to override no-op default
|
|
@@ -476,7 +595,17 @@ module MCP
|
|
|
476
595
|
end
|
|
477
596
|
|
|
478
597
|
def list_resource_templates(request)
|
|
479
|
-
@resource_templates
|
|
598
|
+
page = paginate(@resource_templates, cursor: cursor_from(request), page_size: @page_size, request: request, &:to_h)
|
|
599
|
+
|
|
600
|
+
{ resourceTemplates: page[:items], nextCursor: page[:next_cursor] }.compact
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def complete(params)
|
|
604
|
+
validate_completion_params!(params)
|
|
605
|
+
|
|
606
|
+
result = @handlers[Methods::COMPLETION_COMPLETE].call(params)
|
|
607
|
+
|
|
608
|
+
normalize_completion_result(result)
|
|
480
609
|
end
|
|
481
610
|
|
|
482
611
|
def report_exception(exception, server_context = {})
|
|
@@ -505,12 +634,12 @@ module MCP
|
|
|
505
634
|
parameters.any? { |type, name| type == :keyrest || name == :server_context }
|
|
506
635
|
end
|
|
507
636
|
|
|
508
|
-
def call_tool_with_args(tool, arguments, context, progress_token: nil, session: nil)
|
|
637
|
+
def call_tool_with_args(tool, arguments, context, progress_token: nil, session: nil, related_request_id: nil)
|
|
509
638
|
args = arguments&.transform_keys(&:to_sym) || {}
|
|
510
639
|
|
|
511
640
|
if accepts_server_context?(tool.method(:call))
|
|
512
|
-
progress = Progress.new(notification_target: session, progress_token: progress_token)
|
|
513
|
-
server_context = ServerContext.new(context, progress: progress, notification_target: session)
|
|
641
|
+
progress = Progress.new(notification_target: session, progress_token: progress_token, related_request_id: related_request_id)
|
|
642
|
+
server_context = ServerContext.new(context, progress: progress, notification_target: session, related_request_id: related_request_id)
|
|
514
643
|
tool.call(**args, server_context: server_context).to_h
|
|
515
644
|
else
|
|
516
645
|
tool.call(**args).to_h
|
|
@@ -537,5 +666,56 @@ module MCP
|
|
|
537
666
|
server_context
|
|
538
667
|
end
|
|
539
668
|
end
|
|
669
|
+
|
|
670
|
+
def validate_completion_params!(params)
|
|
671
|
+
unless params.is_a?(Hash)
|
|
672
|
+
raise RequestHandlerError.new("Invalid params", params, error_type: :invalid_params)
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
ref = params[:ref]
|
|
676
|
+
if ref.nil? || ref[:type].nil?
|
|
677
|
+
raise RequestHandlerError.new("Missing or invalid ref", params, error_type: :invalid_params)
|
|
678
|
+
end
|
|
679
|
+
|
|
680
|
+
argument = params[:argument]
|
|
681
|
+
if argument.nil? || argument[:name].nil? || !argument.key?(:value)
|
|
682
|
+
raise RequestHandlerError.new("Missing argument name or value", params, error_type: :invalid_params)
|
|
683
|
+
end
|
|
684
|
+
|
|
685
|
+
case ref[:type]
|
|
686
|
+
when "ref/prompt"
|
|
687
|
+
unless @prompts[ref[:name]]
|
|
688
|
+
raise RequestHandlerError.new("Prompt not found: #{ref[:name]}", params, error_type: :invalid_params)
|
|
689
|
+
end
|
|
690
|
+
when "ref/resource"
|
|
691
|
+
uri = ref[:uri]
|
|
692
|
+
found = @resource_index.key?(uri) || @resource_templates.any? { |t| t.uri_template == uri }
|
|
693
|
+
unless found
|
|
694
|
+
raise RequestHandlerError.new("Resource not found: #{uri}", params, error_type: :invalid_params)
|
|
695
|
+
end
|
|
696
|
+
else
|
|
697
|
+
raise RequestHandlerError.new("Invalid ref type: #{ref[:type]}", params, error_type: :invalid_params)
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def normalize_completion_result(result)
|
|
702
|
+
return DEFAULT_COMPLETION_RESULT unless result.is_a?(Hash)
|
|
703
|
+
|
|
704
|
+
completion = result[:completion] || result["completion"]
|
|
705
|
+
return DEFAULT_COMPLETION_RESULT unless completion.is_a?(Hash)
|
|
706
|
+
|
|
707
|
+
values = completion[:values] || completion["values"] || []
|
|
708
|
+
total = completion[:total] || completion["total"]
|
|
709
|
+
has_more = completion[:hasMore] || completion["hasMore"] || false
|
|
710
|
+
|
|
711
|
+
count = values.length
|
|
712
|
+
if count > MAX_COMPLETION_VALUES
|
|
713
|
+
has_more = true
|
|
714
|
+
total ||= count
|
|
715
|
+
values = values.first(MAX_COMPLETION_VALUES)
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
{ completion: { values: values, total: total, hasMore: has_more }.compact }
|
|
719
|
+
end
|
|
540
720
|
end
|
|
541
721
|
end
|
data/lib/mcp/server_context.rb
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
module MCP
|
|
4
4
|
class ServerContext
|
|
5
|
-
def initialize(context, progress:, notification_target:)
|
|
5
|
+
def initialize(context, progress:, notification_target:, related_request_id: nil)
|
|
6
6
|
@context = context
|
|
7
7
|
@progress = progress
|
|
8
8
|
@notification_target = notification_target
|
|
9
|
+
@related_request_id = related_request_id
|
|
9
10
|
end
|
|
10
11
|
|
|
11
12
|
# Reports progress for the current tool operation.
|
|
@@ -26,7 +27,74 @@ module MCP
|
|
|
26
27
|
def notify_log_message(data:, level:, logger: nil)
|
|
27
28
|
return unless @notification_target
|
|
28
29
|
|
|
29
|
-
@notification_target.notify_log_message(data: data, level: level, logger: logger)
|
|
30
|
+
@notification_target.notify_log_message(data: data, level: level, logger: logger, related_request_id: @related_request_id)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Sends a resource updated notification scoped to the originating session.
|
|
34
|
+
#
|
|
35
|
+
# @param uri [String] The URI of the updated resource.
|
|
36
|
+
def notify_resources_updated(uri:)
|
|
37
|
+
return unless @notification_target
|
|
38
|
+
|
|
39
|
+
@notification_target.notify_resources_updated(uri: uri)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Delegates to the session so the request is scoped to the originating client.
|
|
43
|
+
def list_roots
|
|
44
|
+
if @notification_target.respond_to?(:list_roots)
|
|
45
|
+
@notification_target.list_roots(related_request_id: @related_request_id)
|
|
46
|
+
else
|
|
47
|
+
raise NoMethodError, "undefined method 'list_roots' for #{self}"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Delegates to the session so the request is scoped to the originating client.
|
|
52
|
+
# Falls back to `@context` (via `method_missing`) when `@notification_target`
|
|
53
|
+
# does not support sampling.
|
|
54
|
+
def create_sampling_message(**kwargs)
|
|
55
|
+
if @notification_target.respond_to?(:create_sampling_message)
|
|
56
|
+
@notification_target.create_sampling_message(**kwargs, related_request_id: @related_request_id)
|
|
57
|
+
elsif @context.respond_to?(:create_sampling_message)
|
|
58
|
+
@context.create_sampling_message(**kwargs, related_request_id: @related_request_id)
|
|
59
|
+
else
|
|
60
|
+
raise NoMethodError, "undefined method 'create_sampling_message' for #{self}"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Delegates to the session so the request is scoped to the originating client.
|
|
65
|
+
# Falls back to `@context` (via `method_missing`) when `@notification_target`
|
|
66
|
+
# does not support elicitation.
|
|
67
|
+
def create_form_elicitation(**kwargs)
|
|
68
|
+
if @notification_target.respond_to?(:create_form_elicitation)
|
|
69
|
+
@notification_target.create_form_elicitation(**kwargs, related_request_id: @related_request_id)
|
|
70
|
+
elsif @context.respond_to?(:create_form_elicitation)
|
|
71
|
+
@context.create_form_elicitation(**kwargs, related_request_id: @related_request_id)
|
|
72
|
+
else
|
|
73
|
+
raise NoMethodError, "undefined method 'create_form_elicitation' for #{self}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Delegates to the session so the request is scoped to the originating client.
|
|
78
|
+
# Falls back to `@context` when `@notification_target` does not support URL mode elicitation.
|
|
79
|
+
def create_url_elicitation(**kwargs)
|
|
80
|
+
if @notification_target.respond_to?(:create_url_elicitation)
|
|
81
|
+
@notification_target.create_url_elicitation(**kwargs, related_request_id: @related_request_id)
|
|
82
|
+
elsif @context.respond_to?(:create_url_elicitation)
|
|
83
|
+
@context.create_url_elicitation(**kwargs, related_request_id: @related_request_id)
|
|
84
|
+
else
|
|
85
|
+
raise NoMethodError, "undefined method 'create_url_elicitation' for #{self}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Delegates to the session so the notification is scoped to the originating client.
|
|
90
|
+
def notify_elicitation_complete(**kwargs)
|
|
91
|
+
if @notification_target.respond_to?(:notify_elicitation_complete)
|
|
92
|
+
@notification_target.notify_elicitation_complete(**kwargs)
|
|
93
|
+
elsif @context.respond_to?(:notify_elicitation_complete)
|
|
94
|
+
@context.notify_elicitation_complete(**kwargs)
|
|
95
|
+
else
|
|
96
|
+
raise NoMethodError, "undefined method 'notify_elicitation_complete' for #{self}"
|
|
97
|
+
end
|
|
30
98
|
end
|
|
31
99
|
|
|
32
100
|
def method_missing(name, ...)
|
data/lib/mcp/server_session.rb
CHANGED
|
@@ -13,7 +13,7 @@ module MCP
|
|
|
13
13
|
@transport = transport
|
|
14
14
|
@session_id = session_id
|
|
15
15
|
@client = nil
|
|
16
|
-
@client_capabilities = nil
|
|
16
|
+
@client_capabilities = nil
|
|
17
17
|
@logging_message_notification = nil
|
|
18
18
|
end
|
|
19
19
|
|
|
@@ -36,8 +36,64 @@ module MCP
|
|
|
36
36
|
@logging_message_notification = logging_message_notification
|
|
37
37
|
end
|
|
38
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 `roots/list` request scoped to this session.
|
|
45
|
+
def list_roots(related_request_id: nil)
|
|
46
|
+
unless client_capabilities&.dig(:roots)
|
|
47
|
+
raise "Client does not support roots."
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
send_to_transport_request(Methods::ROOTS_LIST, nil, related_request_id: related_request_id)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Sends a `sampling/createMessage` request scoped to this session.
|
|
54
|
+
def create_sampling_message(related_request_id: nil, **kwargs)
|
|
55
|
+
params = @server.build_sampling_params(client_capabilities, **kwargs)
|
|
56
|
+
send_to_transport_request(Methods::SAMPLING_CREATE_MESSAGE, params, related_request_id: related_request_id)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Sends an `elicitation/create` request (form mode) scoped to this session.
|
|
60
|
+
def create_form_elicitation(message:, requested_schema:, related_request_id: nil)
|
|
61
|
+
unless client_capabilities&.dig(:elicitation)
|
|
62
|
+
raise "Client does not support elicitation. " \
|
|
63
|
+
"The client must declare the `elicitation` capability during initialization."
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
params = { mode: "form", message: message, requestedSchema: requested_schema }
|
|
67
|
+
send_to_transport_request(Methods::ELICITATION_CREATE, params, related_request_id: related_request_id)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Sends an `elicitation/create` request (URL mode) scoped to this session.
|
|
71
|
+
def create_url_elicitation(message:, url:, elicitation_id:, related_request_id: nil)
|
|
72
|
+
unless client_capabilities&.dig(:elicitation, :url)
|
|
73
|
+
raise "Client does not support URL mode elicitation. " \
|
|
74
|
+
"The client must declare the `elicitation.url` capability during initialization."
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
params = { mode: "url", message: message, url: url, elicitationId: elicitation_id }
|
|
78
|
+
send_to_transport_request(Methods::ELICITATION_CREATE, params, related_request_id: related_request_id)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Sends an elicitation complete notification scoped to this session.
|
|
82
|
+
def notify_elicitation_complete(elicitation_id:)
|
|
83
|
+
send_to_transport(Methods::NOTIFICATIONS_ELICITATION_COMPLETE, { elicitationId: elicitation_id })
|
|
84
|
+
rescue => e
|
|
85
|
+
@server.report_exception(e, notification: "elicitation_complete")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Sends a resource updated notification to this session only.
|
|
89
|
+
def notify_resources_updated(uri:)
|
|
90
|
+
send_to_transport(Methods::NOTIFICATIONS_RESOURCES_UPDATED, { "uri" => uri })
|
|
91
|
+
rescue => e
|
|
92
|
+
@server.report_exception(e, notification: "resources_updated")
|
|
93
|
+
end
|
|
94
|
+
|
|
39
95
|
# Sends a progress notification to this session only.
|
|
40
|
-
def notify_progress(progress_token:, progress:, total: nil, message: nil)
|
|
96
|
+
def notify_progress(progress_token:, progress:, total: nil, message: nil, related_request_id: nil)
|
|
41
97
|
params = {
|
|
42
98
|
"progressToken" => progress_token,
|
|
43
99
|
"progress" => progress,
|
|
@@ -45,35 +101,52 @@ module MCP
|
|
|
45
101
|
"message" => message,
|
|
46
102
|
}.compact
|
|
47
103
|
|
|
48
|
-
send_to_transport(Methods::NOTIFICATIONS_PROGRESS, params)
|
|
104
|
+
send_to_transport(Methods::NOTIFICATIONS_PROGRESS, params, related_request_id: related_request_id)
|
|
49
105
|
rescue => e
|
|
50
106
|
@server.report_exception(e, notification: "progress")
|
|
51
107
|
end
|
|
52
108
|
|
|
53
109
|
# Sends a log message notification to this session only.
|
|
54
|
-
def notify_log_message(data:, level:, logger: nil)
|
|
110
|
+
def notify_log_message(data:, level:, logger: nil, related_request_id: nil)
|
|
55
111
|
effective_logging = @logging_message_notification || @server.logging_message_notification
|
|
56
112
|
return unless effective_logging&.should_notify?(level)
|
|
57
113
|
|
|
58
114
|
params = { "data" => data, "level" => level }
|
|
59
115
|
params["logger"] = logger if logger
|
|
60
116
|
|
|
61
|
-
send_to_transport(Methods::NOTIFICATIONS_MESSAGE, params)
|
|
117
|
+
send_to_transport(Methods::NOTIFICATIONS_MESSAGE, params, related_request_id: related_request_id)
|
|
62
118
|
rescue => e
|
|
63
119
|
@server.report_exception(e, { notification: "log_message" })
|
|
64
120
|
end
|
|
65
121
|
|
|
66
122
|
private
|
|
67
123
|
|
|
124
|
+
# Branches on `@session_id` because `StdioTransport` creates a `ServerSession` without
|
|
125
|
+
# a `session_id` (`session_id: nil`), while `StreamableHTTPTransport` always provides one.
|
|
126
|
+
#
|
|
68
127
|
# TODO: When Ruby 2.7 support is dropped, replace with a direct call:
|
|
69
128
|
# `@transport.send_notification(method, params, session_id: @session_id)` and
|
|
70
129
|
# add `**` to `Transport#send_notification` and `StdioTransport#send_notification`.
|
|
71
|
-
def send_to_transport(method, params)
|
|
130
|
+
def send_to_transport(method, params, related_request_id: nil)
|
|
72
131
|
if @session_id
|
|
73
|
-
@transport.send_notification(method, params, session_id: @session_id)
|
|
132
|
+
@transport.send_notification(method, params, session_id: @session_id, related_request_id: related_request_id)
|
|
74
133
|
else
|
|
75
134
|
@transport.send_notification(method, params)
|
|
76
135
|
end
|
|
77
136
|
end
|
|
137
|
+
|
|
138
|
+
# Branches on `@session_id` because `StdioTransport` creates a `ServerSession` without
|
|
139
|
+
# a `session_id` (`session_id: nil`), while `StreamableHTTPTransport` always provides one.
|
|
140
|
+
#
|
|
141
|
+
# TODO: When Ruby 2.7 support is dropped, replace with a direct call:
|
|
142
|
+
# `@transport.send_request(method, params, session_id: @session_id)` and
|
|
143
|
+
# add `**` to `Transport#send_request` and `StdioTransport#send_request`.
|
|
144
|
+
def send_to_transport_request(method, params, related_request_id: nil)
|
|
145
|
+
if @session_id
|
|
146
|
+
@transport.send_request(method, params, session_id: @session_id, related_request_id: related_request_id)
|
|
147
|
+
else
|
|
148
|
+
@transport.send_request(method, params)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
78
151
|
end
|
|
79
152
|
end
|