mcp 0.12.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 +467 -131
- data/lib/json_rpc_handler.rb +16 -9
- data/lib/mcp/client/http.rb +133 -14
- data/lib/mcp/client/paginated_result.rb +13 -0
- data/lib/mcp/client.rb +195 -22
- 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/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/streamable_http_transport.rb +35 -8
- data/lib/mcp/server.rb +82 -60
- data/lib/mcp/server_context.rb +54 -0
- data/lib/mcp/server_session.rb +45 -0
- data/lib/mcp/tool/response.rb +4 -3
- data/lib/mcp/version.rb +1 -1
- metadata +6 -4
data/lib/mcp/instrumentation.rb
CHANGED
|
@@ -2,19 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
module MCP
|
|
4
4
|
module Instrumentation
|
|
5
|
-
def instrument_call(method, &block)
|
|
5
|
+
def instrument_call(method, server_context: {}, exception_already_reported: nil, &block)
|
|
6
6
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
7
7
|
begin
|
|
8
8
|
@instrumentation_data = {}
|
|
9
9
|
add_instrumentation_data(method: method)
|
|
10
10
|
|
|
11
|
-
result =
|
|
11
|
+
result = configuration.around_request.call(@instrumentation_data, &block)
|
|
12
12
|
|
|
13
13
|
result
|
|
14
|
+
rescue => e
|
|
15
|
+
already_reported = begin
|
|
16
|
+
!!exception_already_reported&.call(e)
|
|
17
|
+
# rubocop:disable Lint/RescueException
|
|
18
|
+
rescue Exception
|
|
19
|
+
# rubocop:enable Lint/RescueException
|
|
20
|
+
# The predicate is expected to be side-effect-free and return a boolean.
|
|
21
|
+
# Any exception raised from it (including non-StandardError such as SystemExit)
|
|
22
|
+
# must not shadow the original exception.
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
unless already_reported
|
|
27
|
+
add_instrumentation_data(error: :internal_error) unless @instrumentation_data.key?(:error)
|
|
28
|
+
configuration.exception_reporter.call(e, server_context)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
raise
|
|
14
32
|
ensure
|
|
15
33
|
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
16
34
|
add_instrumentation_data(duration: end_time - start_time)
|
|
17
35
|
|
|
36
|
+
# Backward compatibility: `instrumentation_callback` is soft-deprecated
|
|
37
|
+
# in favor of `around_request`, but existing callers still expect it
|
|
38
|
+
# to fire after every request until it is removed in a future version.
|
|
18
39
|
configuration.instrumentation_callback.call(@instrumentation_data)
|
|
19
40
|
end
|
|
20
41
|
end
|
data/lib/mcp/methods.rb
CHANGED
|
@@ -33,6 +33,7 @@ module MCP
|
|
|
33
33
|
NOTIFICATIONS_MESSAGE = "notifications/message"
|
|
34
34
|
NOTIFICATIONS_PROGRESS = "notifications/progress"
|
|
35
35
|
NOTIFICATIONS_CANCELLED = "notifications/cancelled"
|
|
36
|
+
NOTIFICATIONS_ELICITATION_COMPLETE = "notifications/elicitation/complete"
|
|
36
37
|
|
|
37
38
|
class MissingRequiredCapabilityError < StandardError
|
|
38
39
|
attr_reader :method
|
|
@@ -72,15 +73,13 @@ module MCP
|
|
|
72
73
|
require_capability!(method, capabilities, :completions)
|
|
73
74
|
when ROOTS_LIST
|
|
74
75
|
require_capability!(method, capabilities, :roots)
|
|
75
|
-
when NOTIFICATIONS_ROOTS_LIST_CHANGED
|
|
76
|
-
require_capability!(method, capabilities, :roots)
|
|
77
|
-
require_capability!(method, capabilities, :roots, :listChanged)
|
|
78
76
|
when SAMPLING_CREATE_MESSAGE
|
|
79
77
|
require_capability!(method, capabilities, :sampling)
|
|
80
78
|
when ELICITATION_CREATE
|
|
81
79
|
require_capability!(method, capabilities, :elicitation)
|
|
82
|
-
when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED,
|
|
83
|
-
|
|
80
|
+
when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_ROOTS_LIST_CHANGED,
|
|
81
|
+
NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED, NOTIFICATIONS_ELICITATION_COMPLETE
|
|
82
|
+
# No specific capability required.
|
|
84
83
|
end
|
|
85
84
|
end
|
|
86
85
|
|
data/lib/mcp/prompt/result.rb
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
module MCP
|
|
4
4
|
class Prompt
|
|
5
5
|
class Result
|
|
6
|
-
attr_reader :description, :messages
|
|
6
|
+
attr_reader :description, :messages, :meta
|
|
7
7
|
|
|
8
|
-
def initialize(description: nil, messages: [])
|
|
8
|
+
def initialize(description: nil, messages: [], meta: nil)
|
|
9
9
|
@description = description
|
|
10
10
|
@messages = messages
|
|
11
|
+
@meta = meta
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def to_h
|
|
14
|
-
{ description: description, messages: messages.map(&:to_h) }.compact
|
|
15
|
+
{ description: description, messages: messages.map(&:to_h), _meta: meta }.compact
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
end
|
|
@@ -3,23 +3,24 @@
|
|
|
3
3
|
module MCP
|
|
4
4
|
class Resource
|
|
5
5
|
class Contents
|
|
6
|
-
attr_reader :uri, :mime_type
|
|
6
|
+
attr_reader :uri, :mime_type, :meta
|
|
7
7
|
|
|
8
|
-
def initialize(uri:, mime_type: nil)
|
|
8
|
+
def initialize(uri:, mime_type: nil, meta: nil)
|
|
9
9
|
@uri = uri
|
|
10
10
|
@mime_type = mime_type
|
|
11
|
+
@meta = meta
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def to_h
|
|
14
|
-
{ uri: uri, mimeType: mime_type }.compact
|
|
15
|
+
{ uri: uri, mimeType: mime_type, _meta: meta }.compact
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
class TextContents < Contents
|
|
19
20
|
attr_reader :text
|
|
20
21
|
|
|
21
|
-
def initialize(text:, uri:, mime_type:)
|
|
22
|
-
super(uri: uri, mime_type: mime_type)
|
|
22
|
+
def initialize(text:, uri:, mime_type:, meta: nil)
|
|
23
|
+
super(uri: uri, mime_type: mime_type, meta: meta)
|
|
23
24
|
@text = text
|
|
24
25
|
end
|
|
25
26
|
|
|
@@ -31,8 +32,8 @@ module MCP
|
|
|
31
32
|
class BlobContents < Contents
|
|
32
33
|
attr_reader :data
|
|
33
34
|
|
|
34
|
-
def initialize(data:, uri:, mime_type:)
|
|
35
|
-
super(uri: uri, mime_type: mime_type)
|
|
35
|
+
def initialize(data:, uri:, mime_type:, meta: nil)
|
|
36
|
+
super(uri: uri, mime_type: mime_type, meta: meta)
|
|
36
37
|
@data = data
|
|
37
38
|
end
|
|
38
39
|
|
data/lib/mcp/resource.rb
CHANGED
|
@@ -5,15 +5,16 @@ require_relative "resource/embedded"
|
|
|
5
5
|
|
|
6
6
|
module MCP
|
|
7
7
|
class Resource
|
|
8
|
-
attr_reader :uri, :name, :title, :description, :icons, :mime_type
|
|
8
|
+
attr_reader :uri, :name, :title, :description, :icons, :mime_type, :meta
|
|
9
9
|
|
|
10
|
-
def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil)
|
|
10
|
+
def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil)
|
|
11
11
|
@uri = uri
|
|
12
12
|
@name = name
|
|
13
13
|
@title = title
|
|
14
14
|
@description = description
|
|
15
15
|
@icons = icons
|
|
16
16
|
@mime_type = mime_type
|
|
17
|
+
@meta = meta
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def to_h
|
|
@@ -24,6 +25,7 @@ module MCP
|
|
|
24
25
|
description: description,
|
|
25
26
|
icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
|
|
26
27
|
mimeType: mime_type,
|
|
28
|
+
_meta: meta,
|
|
27
29
|
}.compact
|
|
28
30
|
end
|
|
29
31
|
end
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module MCP
|
|
4
4
|
class ResourceTemplate
|
|
5
|
-
attr_reader :uri_template, :name, :title, :description, :icons, :mime_type
|
|
5
|
+
attr_reader :uri_template, :name, :title, :description, :icons, :mime_type, :meta
|
|
6
6
|
|
|
7
|
-
def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil)
|
|
7
|
+
def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil)
|
|
8
8
|
@uri_template = uri_template
|
|
9
9
|
@name = name
|
|
10
10
|
@title = title
|
|
11
11
|
@description = description
|
|
12
12
|
@icons = icons
|
|
13
13
|
@mime_type = mime_type
|
|
14
|
+
@meta = meta
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def to_h
|
|
@@ -21,6 +22,7 @@ module MCP
|
|
|
21
22
|
description: description,
|
|
22
23
|
icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
|
|
23
24
|
mimeType: mime_type,
|
|
25
|
+
_meta: meta,
|
|
24
26
|
}.compact
|
|
25
27
|
end
|
|
26
28
|
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
class Server
|
|
5
|
+
module Pagination
|
|
6
|
+
private
|
|
7
|
+
|
|
8
|
+
def cursor_from(request)
|
|
9
|
+
return if request.nil?
|
|
10
|
+
|
|
11
|
+
unless request.is_a?(Hash)
|
|
12
|
+
raise RequestHandlerError.new("Invalid params", request, error_type: :invalid_params)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
request[:cursor]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def paginate(items, cursor:, page_size:, request:, &block)
|
|
19
|
+
start_index = 0
|
|
20
|
+
|
|
21
|
+
if cursor
|
|
22
|
+
unless cursor.is_a?(String)
|
|
23
|
+
raise RequestHandlerError.new("Invalid cursor", request, error_type: :invalid_params)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
start_index = Integer(cursor, exception: false)
|
|
27
|
+
if start_index.nil? || start_index < 0 || start_index >= items.size
|
|
28
|
+
raise RequestHandlerError.new("Invalid cursor", request, error_type: :invalid_params)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
end_index = page_size ? start_index + page_size : items.size
|
|
33
|
+
page = items[start_index...end_index]
|
|
34
|
+
page = page.map(&block) if block
|
|
35
|
+
|
|
36
|
+
result = { items: page }
|
|
37
|
+
result[:next_cursor] = end_index.to_s if end_index < items.size
|
|
38
|
+
result
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "../../transport"
|
|
5
5
|
|
|
6
|
+
# This file is autoloaded only when `StreamableHTTPTransport` is referenced,
|
|
7
|
+
# so the `rack` dependency does not affect `StdioTransport` users.
|
|
8
|
+
begin
|
|
9
|
+
require "rack"
|
|
10
|
+
rescue LoadError
|
|
11
|
+
raise LoadError, "The 'rack' gem is required to use the StreamableHTTPTransport. " \
|
|
12
|
+
"Add it to your Gemfile: gem 'rack'"
|
|
13
|
+
end
|
|
14
|
+
|
|
6
15
|
module MCP
|
|
7
16
|
class Server
|
|
8
17
|
module Transports
|
|
@@ -13,13 +22,14 @@ module MCP
|
|
|
13
22
|
"Connection" => "keep-alive",
|
|
14
23
|
}.freeze
|
|
15
24
|
|
|
16
|
-
def initialize(server, stateless: false, session_idle_timeout: nil)
|
|
25
|
+
def initialize(server, stateless: false, enable_json_response: false, session_idle_timeout: nil)
|
|
17
26
|
super(server)
|
|
18
27
|
# Maps `session_id` to `{ get_sse_stream: stream_object, server_session: ServerSession, last_active_at: float_from_monotonic_clock }`.
|
|
19
28
|
@sessions = {}
|
|
20
29
|
@mutex = Mutex.new
|
|
21
30
|
|
|
22
31
|
@stateless = stateless
|
|
32
|
+
@enable_json_response = enable_json_response
|
|
23
33
|
@session_idle_timeout = session_idle_timeout
|
|
24
34
|
@pending_responses = {}
|
|
25
35
|
|
|
@@ -34,11 +44,17 @@ module MCP
|
|
|
34
44
|
start_reaper_thread if @session_idle_timeout
|
|
35
45
|
end
|
|
36
46
|
|
|
37
|
-
|
|
47
|
+
REQUIRED_POST_ACCEPT_TYPES_SSE = ["application/json", "text/event-stream"].freeze
|
|
48
|
+
REQUIRED_POST_ACCEPT_TYPES_JSON = ["application/json"].freeze
|
|
38
49
|
REQUIRED_GET_ACCEPT_TYPES = ["text/event-stream"].freeze
|
|
39
50
|
STREAM_WRITE_ERRORS = [IOError, Errno::EPIPE, Errno::ECONNRESET].freeze
|
|
40
51
|
SESSION_REAP_INTERVAL = 60
|
|
41
52
|
|
|
53
|
+
# Rack app interface. This transport can be mounted as a Rack app.
|
|
54
|
+
def call(env)
|
|
55
|
+
handle_request(Rack::Request.new(env))
|
|
56
|
+
end
|
|
57
|
+
|
|
42
58
|
def handle_request(request)
|
|
43
59
|
case request.env["REQUEST_METHOD"]
|
|
44
60
|
when "POST"
|
|
@@ -80,6 +96,12 @@ module MCP
|
|
|
80
96
|
|
|
81
97
|
result = @mutex.synchronize do
|
|
82
98
|
if session_id
|
|
99
|
+
# JSON response mode returns a single JSON object as the POST response,
|
|
100
|
+
# so request-scoped notifications (e.g. progress, log) cannot be delivered
|
|
101
|
+
# alongside it. Session-scoped standalone notifications
|
|
102
|
+
# (e.g. `resources/updated`, `elicitation/complete`) still flow via GET SSE.
|
|
103
|
+
next false if @enable_json_response && related_request_id
|
|
104
|
+
|
|
83
105
|
# Send to specific session
|
|
84
106
|
if (session = @sessions[session_id])
|
|
85
107
|
stream = active_stream(session, related_request_id: related_request_id)
|
|
@@ -158,6 +180,10 @@ module MCP
|
|
|
158
180
|
raise "Stateless mode does not support server-to-client requests."
|
|
159
181
|
end
|
|
160
182
|
|
|
183
|
+
if @enable_json_response
|
|
184
|
+
raise "JSON response mode does not support server-to-client requests."
|
|
185
|
+
end
|
|
186
|
+
|
|
161
187
|
unless session_id
|
|
162
188
|
raise "session_id is required for server-to-client requests."
|
|
163
189
|
end
|
|
@@ -255,16 +281,17 @@ module MCP
|
|
|
255
281
|
def send_to_stream(stream, data)
|
|
256
282
|
message = data.is_a?(String) ? data : data.to_json
|
|
257
283
|
stream.write("data: #{message}\n\n")
|
|
258
|
-
stream.flush
|
|
284
|
+
stream.flush
|
|
259
285
|
end
|
|
260
286
|
|
|
261
287
|
def send_ping_to_stream(stream)
|
|
262
288
|
stream.write(": ping #{Time.now.iso8601}\n\n")
|
|
263
|
-
stream.flush
|
|
289
|
+
stream.flush
|
|
264
290
|
end
|
|
265
291
|
|
|
266
292
|
def handle_post(request)
|
|
267
|
-
|
|
293
|
+
required_types = @enable_json_response ? REQUIRED_POST_ACCEPT_TYPES_JSON : REQUIRED_POST_ACCEPT_TYPES_SSE
|
|
294
|
+
accept_error = validate_accept_header(request, required_types)
|
|
268
295
|
return accept_error if accept_error
|
|
269
296
|
|
|
270
297
|
content_type_error = validate_content_type(request)
|
|
@@ -505,7 +532,7 @@ module MCP
|
|
|
505
532
|
end
|
|
506
533
|
end
|
|
507
534
|
|
|
508
|
-
if session_id && !@stateless
|
|
535
|
+
if session_id && !@stateless && !@enable_json_response
|
|
509
536
|
handle_request_with_sse_response(body_string, session_id, server_session, related_request_id: related_request_id)
|
|
510
537
|
else
|
|
511
538
|
response = dispatch_handle_json(body_string, server_session)
|
|
@@ -546,7 +573,7 @@ module MCP
|
|
|
546
573
|
end
|
|
547
574
|
end
|
|
548
575
|
|
|
549
|
-
[200, SSE_HEADERS, body]
|
|
576
|
+
[200, SSE_HEADERS.dup, body]
|
|
550
577
|
end
|
|
551
578
|
|
|
552
579
|
# Returns the SSE stream available for server-to-client messages.
|
|
@@ -628,7 +655,7 @@ module MCP
|
|
|
628
655
|
def setup_sse_stream(session_id)
|
|
629
656
|
body = create_sse_body(session_id)
|
|
630
657
|
|
|
631
|
-
[200, SSE_HEADERS, body]
|
|
658
|
+
[200, SSE_HEADERS.dup, body]
|
|
632
659
|
end
|
|
633
660
|
|
|
634
661
|
def create_sse_body(session_id)
|
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
|
|
@@ -31,14 +32,27 @@ module MCP
|
|
|
31
32
|
MAX_COMPLETION_VALUES = 100
|
|
32
33
|
|
|
33
34
|
class RequestHandlerError < StandardError
|
|
34
|
-
attr_reader :error_type
|
|
35
|
-
attr_reader :original_error
|
|
35
|
+
attr_reader :error_type, :original_error, :error_code, :error_data
|
|
36
36
|
|
|
37
|
-
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)
|
|
38
38
|
super(message)
|
|
39
39
|
@request = request
|
|
40
40
|
@error_type = error_type
|
|
41
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
|
+
)
|
|
42
56
|
end
|
|
43
57
|
end
|
|
44
58
|
|
|
@@ -52,9 +66,10 @@ module MCP
|
|
|
52
66
|
end
|
|
53
67
|
|
|
54
68
|
include Instrumentation
|
|
69
|
+
include Pagination
|
|
55
70
|
|
|
56
71
|
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
|
|
72
|
+
attr_reader :page_size, :client_capabilities
|
|
58
73
|
|
|
59
74
|
def initialize(
|
|
60
75
|
description: nil,
|
|
@@ -71,6 +86,7 @@ module MCP
|
|
|
71
86
|
server_context: nil,
|
|
72
87
|
configuration: nil,
|
|
73
88
|
capabilities: nil,
|
|
89
|
+
page_size: nil,
|
|
74
90
|
transport: nil
|
|
75
91
|
)
|
|
76
92
|
@description = description
|
|
@@ -87,6 +103,7 @@ module MCP
|
|
|
87
103
|
@resource_templates = resource_templates
|
|
88
104
|
@resource_index = index_resources_by_uri(resources)
|
|
89
105
|
@server_context = server_context
|
|
106
|
+
self.page_size = page_size
|
|
90
107
|
@configuration = MCP.configuration.merge(configuration)
|
|
91
108
|
@client = nil
|
|
92
109
|
|
|
@@ -100,6 +117,8 @@ module MCP
|
|
|
100
117
|
Methods::RESOURCES_LIST => method(:list_resources),
|
|
101
118
|
Methods::RESOURCES_READ => method(:read_resource_no_content),
|
|
102
119
|
Methods::RESOURCES_TEMPLATES_LIST => method(:list_resource_templates),
|
|
120
|
+
Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
|
|
121
|
+
Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
|
|
103
122
|
Methods::TOOLS_LIST => method(:list_tools),
|
|
104
123
|
Methods::TOOLS_CALL => method(:call_tool),
|
|
105
124
|
Methods::PROMPTS_LIST => method(:list_prompts),
|
|
@@ -108,13 +127,9 @@ module MCP
|
|
|
108
127
|
Methods::PING => ->(_) { {} },
|
|
109
128
|
Methods::NOTIFICATIONS_INITIALIZED => ->(_) {},
|
|
110
129
|
Methods::NOTIFICATIONS_PROGRESS => ->(_) {},
|
|
130
|
+
Methods::NOTIFICATIONS_ROOTS_LIST_CHANGED => ->(_) {},
|
|
111
131
|
Methods::COMPLETION_COMPLETE => ->(_) { DEFAULT_COMPLETION_RESULT },
|
|
112
132
|
Methods::LOGGING_SET_LEVEL => method(:configure_logging_level),
|
|
113
|
-
|
|
114
|
-
# No op handlers for currently unsupported methods
|
|
115
|
-
Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
|
|
116
|
-
Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
|
|
117
|
-
Methods::ELICITATION_CREATE => ->(_) {},
|
|
118
133
|
}
|
|
119
134
|
@transport = transport
|
|
120
135
|
end
|
|
@@ -170,6 +185,14 @@ module MCP
|
|
|
170
185
|
@handlers[method_name] = block
|
|
171
186
|
end
|
|
172
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
|
+
|
|
173
196
|
def notify_tools_list_changed
|
|
174
197
|
return unless @transport
|
|
175
198
|
|
|
@@ -206,42 +229,12 @@ module MCP
|
|
|
206
229
|
report_exception(e, { notification: "log_message" })
|
|
207
230
|
end
|
|
208
231
|
|
|
209
|
-
#
|
|
210
|
-
#
|
|
211
|
-
#
|
|
212
|
-
#
|
|
213
|
-
def
|
|
214
|
-
|
|
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
|
|
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)
|
|
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
|
|
245
238
|
end
|
|
246
239
|
|
|
247
240
|
# Sets a custom handler for `resources/read` requests.
|
|
@@ -263,6 +256,24 @@ module MCP
|
|
|
263
256
|
@handlers[Methods::COMPLETION_COMPLETE] = block
|
|
264
257
|
end
|
|
265
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
|
+
|
|
266
277
|
def build_sampling_params(
|
|
267
278
|
capabilities,
|
|
268
279
|
messages:,
|
|
@@ -375,7 +386,7 @@ module MCP
|
|
|
375
386
|
def handle_request(request, method, session: nil, related_request_id: nil)
|
|
376
387
|
handler = @handlers[method]
|
|
377
388
|
unless handler
|
|
378
|
-
instrument_call("unsupported_method") do
|
|
389
|
+
instrument_call("unsupported_method", server_context: { request: request }) do
|
|
379
390
|
client = session&.client || @client
|
|
380
391
|
add_instrumentation_data(client: client) if client
|
|
381
392
|
end
|
|
@@ -385,20 +396,20 @@ module MCP
|
|
|
385
396
|
Methods.ensure_capability!(method, capabilities)
|
|
386
397
|
|
|
387
398
|
->(params) {
|
|
388
|
-
|
|
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
|
|
389
405
|
result = case method
|
|
390
406
|
when Methods::INITIALIZE
|
|
391
407
|
init(params, session: session)
|
|
392
|
-
when Methods::TOOLS_LIST
|
|
393
|
-
{ tools: @handlers[Methods::TOOLS_LIST].call(params) }
|
|
394
|
-
when Methods::PROMPTS_LIST
|
|
395
|
-
{ prompts: @handlers[Methods::PROMPTS_LIST].call(params) }
|
|
396
|
-
when Methods::RESOURCES_LIST
|
|
397
|
-
{ resources: @handlers[Methods::RESOURCES_LIST].call(params) }
|
|
398
408
|
when Methods::RESOURCES_READ
|
|
399
409
|
{ contents: @handlers[Methods::RESOURCES_READ].call(params) }
|
|
400
|
-
when Methods::
|
|
401
|
-
|
|
410
|
+
when Methods::RESOURCES_SUBSCRIBE, Methods::RESOURCES_UNSUBSCRIBE
|
|
411
|
+
@handlers[method].call(params)
|
|
412
|
+
{}
|
|
402
413
|
when Methods::TOOLS_CALL
|
|
403
414
|
call_tool(params, session: session, related_request_id: related_request_id)
|
|
404
415
|
when Methods::COMPLETION_COMPLETE
|
|
@@ -415,11 +426,14 @@ module MCP
|
|
|
415
426
|
rescue RequestHandlerError => e
|
|
416
427
|
report_exception(e.original_error || e, { request: request })
|
|
417
428
|
add_instrumentation_data(error: e.error_type)
|
|
429
|
+
reported_exception = e
|
|
418
430
|
raise e
|
|
419
431
|
rescue => e
|
|
420
432
|
report_exception(e, { request: request })
|
|
421
433
|
add_instrumentation_data(error: :internal_error)
|
|
422
|
-
|
|
434
|
+
wrapped = RequestHandlerError.new("Internal error handling #{method} request", request, original_error: e)
|
|
435
|
+
reported_exception = wrapped
|
|
436
|
+
raise wrapped
|
|
423
437
|
end
|
|
424
438
|
}
|
|
425
439
|
end
|
|
@@ -497,7 +511,9 @@ module MCP
|
|
|
497
511
|
end
|
|
498
512
|
|
|
499
513
|
def list_tools(request)
|
|
500
|
-
@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
|
|
501
517
|
end
|
|
502
518
|
|
|
503
519
|
def call_tool(request, session: nil, related_request_id: nil)
|
|
@@ -545,7 +561,9 @@ module MCP
|
|
|
545
561
|
end
|
|
546
562
|
|
|
547
563
|
def list_prompts(request)
|
|
548
|
-
@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
|
|
549
567
|
end
|
|
550
568
|
|
|
551
569
|
def get_prompt(request)
|
|
@@ -565,7 +583,9 @@ module MCP
|
|
|
565
583
|
end
|
|
566
584
|
|
|
567
585
|
def list_resources(request)
|
|
568
|
-
@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
|
|
569
589
|
end
|
|
570
590
|
|
|
571
591
|
# Server implementation should set `resources_read_handler` to override no-op default
|
|
@@ -575,7 +595,9 @@ module MCP
|
|
|
575
595
|
end
|
|
576
596
|
|
|
577
597
|
def list_resource_templates(request)
|
|
578
|
-
@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
|
|
579
601
|
end
|
|
580
602
|
|
|
581
603
|
def complete(params)
|