mcp 0.10.0 → 0.15.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 +1384 -691
- data/lib/json_rpc_handler.rb +23 -10
- data/lib/mcp/cancellation.rb +72 -0
- data/lib/mcp/cancelled_error.rb +13 -0
- data/lib/mcp/client/http.rb +234 -15
- data/lib/mcp/client/paginated_result.rb +13 -0
- data/lib/mcp/client/stdio.rb +100 -49
- data/lib/mcp/client.rb +293 -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 +42 -0
- data/lib/mcp/server/transports/streamable_http_transport.rb +383 -88
- data/lib/mcp/server.rb +361 -50
- data/lib/mcp/server_context.rb +81 -2
- data/lib/mcp/server_session.rb +170 -12
- 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
- data/lib/mcp.rb +2 -0
- metadata +11 -6
- data/lib/mcp/transports/stdio.rb +0 -15
data/lib/mcp/client.rb
CHANGED
|
@@ -2,10 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "client/stdio"
|
|
4
4
|
require_relative "client/http"
|
|
5
|
+
require_relative "client/paginated_result"
|
|
5
6
|
require_relative "client/tool"
|
|
6
7
|
|
|
7
8
|
module MCP
|
|
8
9
|
class Client
|
|
10
|
+
class ServerError < StandardError
|
|
11
|
+
attr_reader :code, :data
|
|
12
|
+
|
|
13
|
+
def initialize(message, code:, data: nil)
|
|
14
|
+
super(message)
|
|
15
|
+
@code = code
|
|
16
|
+
@data = data
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class RequestHandlerError < StandardError
|
|
21
|
+
attr_reader :error_type, :original_error, :request
|
|
22
|
+
|
|
23
|
+
def initialize(message, request, error_type: :internal_error, original_error: nil)
|
|
24
|
+
super(message)
|
|
25
|
+
@request = request
|
|
26
|
+
@error_type = error_type
|
|
27
|
+
@original_error = original_error
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Raised when a server response fails client-side validation, e.g., a success response
|
|
32
|
+
# whose `result` field is missing or has the wrong type. This is distinct from a
|
|
33
|
+
# server-returned JSON-RPC error, which is raised as `ServerError`.
|
|
34
|
+
class ValidationError < StandardError; end
|
|
35
|
+
|
|
36
|
+
# Raised when the server responds 404 to a request containing a session ID,
|
|
37
|
+
# indicating the session has expired. Inherits from `RequestHandlerError` for
|
|
38
|
+
# backward compatibility with callers that rescue the generic error. Per spec,
|
|
39
|
+
# clients MUST start a new session with a fresh `initialize` request in response.
|
|
40
|
+
class SessionExpiredError < RequestHandlerError
|
|
41
|
+
def initialize(message, request, original_error: nil)
|
|
42
|
+
super(message, request, error_type: :not_found, original_error: original_error)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
9
46
|
# Initializes a new MCP::Client instance.
|
|
10
47
|
#
|
|
11
48
|
# @param transport [Object] The transport object to use for communication with the server.
|
|
@@ -22,8 +59,81 @@ module MCP
|
|
|
22
59
|
# So keeping it public
|
|
23
60
|
attr_reader :transport
|
|
24
61
|
|
|
25
|
-
#
|
|
26
|
-
#
|
|
62
|
+
# The server's `InitializeResult` (protocol version, capabilities, server info,
|
|
63
|
+
# instructions), as reported by the transport after a successful `connect`.
|
|
64
|
+
# Returns `nil` before `connect`, after `close`, or when the transport does
|
|
65
|
+
# not expose a cached handshake result.
|
|
66
|
+
def server_info
|
|
67
|
+
transport.server_info if transport.respond_to?(:server_info)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Performs the MCP `initialize` handshake by delegating to the transport
|
|
71
|
+
# (e.g. `MCP::Client::HTTP`, `MCP::Client::Stdio`). Returns the server's
|
|
72
|
+
# `InitializeResult`.
|
|
73
|
+
#
|
|
74
|
+
# When the transport does not respond to `:connect`, this is a no-op and
|
|
75
|
+
# returns `nil`.
|
|
76
|
+
#
|
|
77
|
+
# @param client_info [Hash, nil] `{ name:, version: }` identifying the client.
|
|
78
|
+
# @param protocol_version [String, nil] Protocol version to offer.
|
|
79
|
+
# @param capabilities [Hash] Capabilities advertised by the client.
|
|
80
|
+
# @return [Hash, nil] The server's `InitializeResult`, or `nil` when the transport
|
|
81
|
+
# does not expose an explicit handshake.
|
|
82
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization
|
|
83
|
+
def connect(client_info: nil, protocol_version: nil, capabilities: {})
|
|
84
|
+
return unless transport.respond_to?(:connect)
|
|
85
|
+
|
|
86
|
+
transport.connect(
|
|
87
|
+
client_info: client_info,
|
|
88
|
+
protocol_version: protocol_version,
|
|
89
|
+
capabilities: capabilities,
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns true once `connect` has completed the handshake on the underlying
|
|
94
|
+
# transport. Transports that do not expose connection state are assumed
|
|
95
|
+
# connected and return `true`.
|
|
96
|
+
def connected?
|
|
97
|
+
return transport.connected? if transport.respond_to?(:connected?)
|
|
98
|
+
|
|
99
|
+
true
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Returns a single page of tools from the server.
|
|
103
|
+
#
|
|
104
|
+
# @param cursor [String, nil] Cursor from a previous page response.
|
|
105
|
+
# @return [MCP::Client::ListToolsResult] Result with `tools` (Array<MCP::Client::Tool>)
|
|
106
|
+
# and `next_cursor` (String or nil).
|
|
107
|
+
#
|
|
108
|
+
# @example Iterate all pages
|
|
109
|
+
# cursor = nil
|
|
110
|
+
# loop do
|
|
111
|
+
# page = client.list_tools(cursor: cursor)
|
|
112
|
+
# page.tools.each { |tool| puts tool.name }
|
|
113
|
+
# cursor = page.next_cursor
|
|
114
|
+
# break unless cursor
|
|
115
|
+
# end
|
|
116
|
+
def list_tools(cursor: nil)
|
|
117
|
+
params = cursor ? { cursor: cursor } : nil
|
|
118
|
+
response = request(method: "tools/list", params: params)
|
|
119
|
+
result = response["result"] || {}
|
|
120
|
+
|
|
121
|
+
tools = (result["tools"] || []).map do |tool|
|
|
122
|
+
Tool.new(
|
|
123
|
+
name: tool["name"],
|
|
124
|
+
description: tool["description"],
|
|
125
|
+
input_schema: tool["inputSchema"],
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
ListToolsResult.new(tools: tools, next_cursor: result["nextCursor"], meta: result["_meta"])
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Returns every tool available on the server. Iterates through all pages automatically
|
|
133
|
+
# when the server paginates, so the full collection is returned regardless of the server's `page_size` setting.
|
|
134
|
+
# Use {#list_tools} when you need fine-grained cursor control.
|
|
135
|
+
#
|
|
136
|
+
# Each call will make a new request - the result is not cached.
|
|
27
137
|
#
|
|
28
138
|
# @return [Array<MCP::Client::Tool>] An array of available tools.
|
|
29
139
|
#
|
|
@@ -33,61 +143,151 @@ module MCP
|
|
|
33
143
|
# puts tool.name
|
|
34
144
|
# end
|
|
35
145
|
def tools
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
})
|
|
146
|
+
# TODO: consider renaming to `list_all_tools`.
|
|
147
|
+
all_tools = []
|
|
148
|
+
seen = Set.new
|
|
149
|
+
cursor = nil
|
|
41
150
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
151
|
+
loop do
|
|
152
|
+
page = list_tools(cursor: cursor)
|
|
153
|
+
all_tools.concat(page.tools)
|
|
154
|
+
next_cursor = page.next_cursor
|
|
155
|
+
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
156
|
+
|
|
157
|
+
seen << next_cursor
|
|
158
|
+
cursor = next_cursor
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
all_tools
|
|
49
162
|
end
|
|
50
163
|
|
|
51
|
-
# Returns
|
|
52
|
-
#
|
|
164
|
+
# Returns a single page of resources from the server.
|
|
165
|
+
#
|
|
166
|
+
# @param cursor [String, nil] Cursor from a previous page response.
|
|
167
|
+
# @return [MCP::Client::ListResourcesResult] Result with `resources` (Array<Hash>)
|
|
168
|
+
# and `next_cursor` (String or nil).
|
|
169
|
+
def list_resources(cursor: nil)
|
|
170
|
+
params = cursor ? { cursor: cursor } : nil
|
|
171
|
+
response = request(method: "resources/list", params: params)
|
|
172
|
+
result = response["result"] || {}
|
|
173
|
+
|
|
174
|
+
ListResourcesResult.new(
|
|
175
|
+
resources: result["resources"] || [],
|
|
176
|
+
next_cursor: result["nextCursor"],
|
|
177
|
+
meta: result["_meta"],
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Returns every resource available on the server. Iterates through all pages automatically
|
|
182
|
+
# when the server paginates, so the full collection is returned regardless of the server's `page_size` setting.
|
|
183
|
+
# Use {#list_resources} when you need fine-grained cursor control.
|
|
184
|
+
#
|
|
185
|
+
# Each call will make a new request - the result is not cached.
|
|
53
186
|
#
|
|
54
187
|
# @return [Array<Hash>] An array of available resources.
|
|
55
188
|
def resources
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
189
|
+
# TODO: consider renaming to `list_all_resources`.
|
|
190
|
+
all_resources = []
|
|
191
|
+
seen = Set.new
|
|
192
|
+
cursor = nil
|
|
193
|
+
|
|
194
|
+
loop do
|
|
195
|
+
page = list_resources(cursor: cursor)
|
|
196
|
+
all_resources.concat(page.resources)
|
|
197
|
+
next_cursor = page.next_cursor
|
|
198
|
+
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
199
|
+
|
|
200
|
+
seen << next_cursor
|
|
201
|
+
cursor = next_cursor
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
all_resources
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Returns a single page of resource templates from the server.
|
|
208
|
+
#
|
|
209
|
+
# @param cursor [String, nil] Cursor from a previous page response.
|
|
210
|
+
# @return [MCP::Client::ListResourceTemplatesResult] Result with `resource_templates`
|
|
211
|
+
# (Array<Hash>) and `next_cursor` (String or nil).
|
|
212
|
+
def list_resource_templates(cursor: nil)
|
|
213
|
+
params = cursor ? { cursor: cursor } : nil
|
|
214
|
+
response = request(method: "resources/templates/list", params: params)
|
|
215
|
+
result = response["result"] || {}
|
|
61
216
|
|
|
62
|
-
|
|
217
|
+
ListResourceTemplatesResult.new(
|
|
218
|
+
resource_templates: result["resourceTemplates"] || [],
|
|
219
|
+
next_cursor: result["nextCursor"],
|
|
220
|
+
meta: result["_meta"],
|
|
221
|
+
)
|
|
63
222
|
end
|
|
64
223
|
|
|
65
|
-
# Returns
|
|
66
|
-
#
|
|
224
|
+
# Returns every resource template available on the server. Iterates through all pages automatically
|
|
225
|
+
# when the server paginates, so the full collection is returned regardless of the server's `page_size` setting.
|
|
226
|
+
# Use {#list_resource_templates} when you need fine-grained cursor control.
|
|
227
|
+
#
|
|
228
|
+
# Each call will make a new request - the result is not cached.
|
|
67
229
|
#
|
|
68
230
|
# @return [Array<Hash>] An array of available resource templates.
|
|
69
231
|
def resource_templates
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
232
|
+
# TODO: consider renaming to `list_all_resource_templates`.
|
|
233
|
+
all_templates = []
|
|
234
|
+
seen = Set.new
|
|
235
|
+
cursor = nil
|
|
236
|
+
|
|
237
|
+
loop do
|
|
238
|
+
page = list_resource_templates(cursor: cursor)
|
|
239
|
+
all_templates.concat(page.resource_templates)
|
|
240
|
+
next_cursor = page.next_cursor
|
|
241
|
+
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
242
|
+
|
|
243
|
+
seen << next_cursor
|
|
244
|
+
cursor = next_cursor
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
all_templates
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# Returns a single page of prompts from the server.
|
|
251
|
+
#
|
|
252
|
+
# @param cursor [String, nil] Cursor from a previous page response.
|
|
253
|
+
# @return [MCP::Client::ListPromptsResult] Result with `prompts` (Array<Hash>)
|
|
254
|
+
# and `next_cursor` (String or nil).
|
|
255
|
+
def list_prompts(cursor: nil)
|
|
256
|
+
params = cursor ? { cursor: cursor } : nil
|
|
257
|
+
response = request(method: "prompts/list", params: params)
|
|
258
|
+
result = response["result"] || {}
|
|
75
259
|
|
|
76
|
-
|
|
260
|
+
ListPromptsResult.new(
|
|
261
|
+
prompts: result["prompts"] || [],
|
|
262
|
+
next_cursor: result["nextCursor"],
|
|
263
|
+
meta: result["_meta"],
|
|
264
|
+
)
|
|
77
265
|
end
|
|
78
266
|
|
|
79
|
-
# Returns
|
|
80
|
-
#
|
|
267
|
+
# Returns every prompt available on the server. Iterates through all pages automatically
|
|
268
|
+
# when the server paginates, so the full collection is returned regardless of the server's `page_size` setting.
|
|
269
|
+
# Use {#list_prompts} when you need fine-grained cursor control.
|
|
270
|
+
#
|
|
271
|
+
# Each call will make a new request - the result is not cached.
|
|
81
272
|
#
|
|
82
273
|
# @return [Array<Hash>] An array of available prompts.
|
|
83
274
|
def prompts
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
})
|
|
275
|
+
# TODO: consider renaming to `list_all_prompts`.
|
|
276
|
+
all_prompts = []
|
|
277
|
+
seen = Set.new
|
|
278
|
+
cursor = nil
|
|
89
279
|
|
|
90
|
-
|
|
280
|
+
loop do
|
|
281
|
+
page = list_prompts(cursor: cursor)
|
|
282
|
+
all_prompts.concat(page.prompts)
|
|
283
|
+
next_cursor = page.next_cursor
|
|
284
|
+
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
285
|
+
|
|
286
|
+
seen << next_cursor
|
|
287
|
+
cursor = next_cursor
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
all_prompts
|
|
91
291
|
end
|
|
92
292
|
|
|
93
293
|
# Calls a tool via the transport layer and returns the full response from the server.
|
|
@@ -119,12 +319,7 @@ module MCP
|
|
|
119
319
|
params[:_meta] = { progressToken: progress_token }
|
|
120
320
|
end
|
|
121
321
|
|
|
122
|
-
|
|
123
|
-
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
124
|
-
id: request_id,
|
|
125
|
-
method: "tools/call",
|
|
126
|
-
params: params,
|
|
127
|
-
})
|
|
322
|
+
request(method: "tools/call", params: params)
|
|
128
323
|
end
|
|
129
324
|
|
|
130
325
|
# Reads a resource from the server by URI and returns the contents.
|
|
@@ -132,12 +327,7 @@ module MCP
|
|
|
132
327
|
# @param uri [String] The URI of the resource to read.
|
|
133
328
|
# @return [Array<Hash>] An array of resource contents (text or blob).
|
|
134
329
|
def read_resource(uri:)
|
|
135
|
-
response =
|
|
136
|
-
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
137
|
-
id: request_id,
|
|
138
|
-
method: "resources/read",
|
|
139
|
-
params: { uri: uri },
|
|
140
|
-
})
|
|
330
|
+
response = request(method: "resources/read", params: { uri: uri })
|
|
141
331
|
|
|
142
332
|
response.dig("result", "contents") || []
|
|
143
333
|
end
|
|
@@ -147,31 +337,68 @@ module MCP
|
|
|
147
337
|
# @param name [String] The name of the prompt to get.
|
|
148
338
|
# @return [Hash] A hash containing the prompt details.
|
|
149
339
|
def get_prompt(name:)
|
|
150
|
-
response =
|
|
151
|
-
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
152
|
-
id: request_id,
|
|
153
|
-
method: "prompts/get",
|
|
154
|
-
params: { name: name },
|
|
155
|
-
})
|
|
340
|
+
response = request(method: "prompts/get", params: { name: name })
|
|
156
341
|
|
|
157
342
|
response.fetch("result", {})
|
|
158
343
|
end
|
|
159
344
|
|
|
160
|
-
|
|
345
|
+
# Requests completion suggestions from the server for a prompt argument or resource template URI.
|
|
346
|
+
#
|
|
347
|
+
# @param ref [Hash] The reference, e.g. `{ type: "ref/prompt", name: "my_prompt" }`
|
|
348
|
+
# or `{ type: "ref/resource", uri: "file:///{path}" }`.
|
|
349
|
+
# @param argument [Hash] The argument being completed, e.g. `{ name: "language", value: "py" }`.
|
|
350
|
+
# @param context [Hash, nil] Optional context with previously resolved arguments.
|
|
351
|
+
# @return [Hash] The completion result with `"values"`, `"hasMore"`, and optionally `"total"`.
|
|
352
|
+
def complete(ref:, argument:, context: nil)
|
|
353
|
+
params = { ref: ref, argument: argument }
|
|
354
|
+
params[:context] = context if context
|
|
161
355
|
|
|
162
|
-
|
|
163
|
-
|
|
356
|
+
response = request(method: "completion/complete", params: params)
|
|
357
|
+
|
|
358
|
+
response.dig("result", "completion") || { "values" => [], "hasMore" => false }
|
|
164
359
|
end
|
|
165
360
|
|
|
166
|
-
|
|
167
|
-
|
|
361
|
+
# Sends a `ping` request to the server to verify the connection is alive.
|
|
362
|
+
# Per the MCP spec, the server responds with an empty result.
|
|
363
|
+
#
|
|
364
|
+
# @return [Hash] An empty hash on success.
|
|
365
|
+
# @raise [ServerError] If the server returns a JSON-RPC error.
|
|
366
|
+
# @raise [ValidationError] If the response `result` is missing or not a Hash.
|
|
367
|
+
#
|
|
368
|
+
# @example
|
|
369
|
+
# client.ping # => {}
|
|
370
|
+
#
|
|
371
|
+
# @see https://modelcontextprotocol.io/specification/latest/basic/utilities/ping
|
|
372
|
+
def ping
|
|
373
|
+
result = request(method: Methods::PING)["result"]
|
|
374
|
+
raise ValidationError, "Response validation failed: missing or invalid `result`" unless result.is_a?(Hash)
|
|
168
375
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
376
|
+
result
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
private
|
|
380
|
+
|
|
381
|
+
def request(method:, params: nil)
|
|
382
|
+
request_body = {
|
|
383
|
+
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
384
|
+
id: request_id,
|
|
385
|
+
method: method,
|
|
386
|
+
}
|
|
387
|
+
request_body[:params] = params if params
|
|
388
|
+
|
|
389
|
+
response = transport.send_request(request: request_body)
|
|
390
|
+
|
|
391
|
+
# Guard with `is_a?(Hash)` because custom transports may return non-Hash values.
|
|
392
|
+
if response.is_a?(Hash) && response.key?("error")
|
|
393
|
+
error = response["error"]
|
|
394
|
+
raise ServerError.new(error["message"], code: error["code"], data: error["data"])
|
|
174
395
|
end
|
|
396
|
+
|
|
397
|
+
response
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def request_id
|
|
401
|
+
SecureRandom.uuid
|
|
175
402
|
end
|
|
176
403
|
end
|
|
177
404
|
end
|
data/lib/mcp/configuration.rb
CHANGED
|
@@ -7,11 +7,18 @@ module MCP
|
|
|
7
7
|
LATEST_STABLE_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05",
|
|
8
8
|
]
|
|
9
9
|
|
|
10
|
-
attr_writer :exception_reporter, :
|
|
10
|
+
attr_writer :exception_reporter, :around_request
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
# @deprecated Use {#around_request=} instead. `instrumentation_callback`
|
|
13
|
+
# fires only after a request completes and cannot wrap execution in a
|
|
14
|
+
# surrounding block (e.g. for Application Performance Monitoring (APM) spans).
|
|
15
|
+
# @see #around_request=
|
|
16
|
+
attr_writer :instrumentation_callback
|
|
17
|
+
|
|
18
|
+
def initialize(exception_reporter: nil, around_request: nil, instrumentation_callback: nil, protocol_version: nil,
|
|
13
19
|
validate_tool_call_arguments: true)
|
|
14
20
|
@exception_reporter = exception_reporter
|
|
21
|
+
@around_request = around_request
|
|
15
22
|
@instrumentation_callback = instrumentation_callback
|
|
16
23
|
@protocol_version = protocol_version
|
|
17
24
|
if protocol_version
|
|
@@ -50,10 +57,24 @@ module MCP
|
|
|
50
57
|
!@exception_reporter.nil?
|
|
51
58
|
end
|
|
52
59
|
|
|
60
|
+
def around_request
|
|
61
|
+
@around_request || default_around_request
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def around_request?
|
|
65
|
+
!@around_request.nil?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @deprecated Use {#around_request} instead. `instrumentation_callback`
|
|
69
|
+
# fires only after a request completes and cannot wrap execution in a
|
|
70
|
+
# surrounding block (e.g. for Application Performance Monitoring (APM) spans).
|
|
71
|
+
# @see #around_request
|
|
53
72
|
def instrumentation_callback
|
|
54
73
|
@instrumentation_callback || default_instrumentation_callback
|
|
55
74
|
end
|
|
56
75
|
|
|
76
|
+
# @deprecated Use {#around_request?} instead.
|
|
77
|
+
# @see #around_request?
|
|
57
78
|
def instrumentation_callback?
|
|
58
79
|
!@instrumentation_callback.nil?
|
|
59
80
|
end
|
|
@@ -72,20 +93,30 @@ module MCP
|
|
|
72
93
|
else
|
|
73
94
|
@exception_reporter
|
|
74
95
|
end
|
|
96
|
+
|
|
97
|
+
around_request = if other.around_request?
|
|
98
|
+
other.around_request
|
|
99
|
+
else
|
|
100
|
+
@around_request
|
|
101
|
+
end
|
|
102
|
+
|
|
75
103
|
instrumentation_callback = if other.instrumentation_callback?
|
|
76
104
|
other.instrumentation_callback
|
|
77
105
|
else
|
|
78
106
|
@instrumentation_callback
|
|
79
107
|
end
|
|
108
|
+
|
|
80
109
|
protocol_version = if other.protocol_version?
|
|
81
110
|
other.protocol_version
|
|
82
111
|
else
|
|
83
112
|
@protocol_version
|
|
84
113
|
end
|
|
114
|
+
|
|
85
115
|
validate_tool_call_arguments = other.validate_tool_call_arguments
|
|
86
116
|
|
|
87
117
|
Configuration.new(
|
|
88
118
|
exception_reporter: exception_reporter,
|
|
119
|
+
around_request: around_request,
|
|
89
120
|
instrumentation_callback: instrumentation_callback,
|
|
90
121
|
protocol_version: protocol_version,
|
|
91
122
|
validate_tool_call_arguments: validate_tool_call_arguments,
|
|
@@ -111,6 +142,11 @@ module MCP
|
|
|
111
142
|
@default_exception_reporter ||= ->(exception, server_context) {}
|
|
112
143
|
end
|
|
113
144
|
|
|
145
|
+
def default_around_request
|
|
146
|
+
@default_around_request ||= ->(_data, &request_handler) { request_handler.call }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @deprecated Use {#default_around_request} instead.
|
|
114
150
|
def default_instrumentation_callback
|
|
115
151
|
@default_instrumentation_callback ||= ->(data) {}
|
|
116
152
|
end
|
data/lib/mcp/content.rb
CHANGED
|
@@ -3,56 +3,60 @@
|
|
|
3
3
|
module MCP
|
|
4
4
|
module Content
|
|
5
5
|
class Text
|
|
6
|
-
attr_reader :text, :annotations
|
|
6
|
+
attr_reader :text, :annotations, :meta
|
|
7
7
|
|
|
8
|
-
def initialize(text, annotations: nil)
|
|
8
|
+
def initialize(text, annotations: nil, meta: nil)
|
|
9
9
|
@text = text
|
|
10
10
|
@annotations = annotations
|
|
11
|
+
@meta = meta
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def to_h
|
|
14
|
-
{ text: text, annotations: annotations, type: "text" }.compact
|
|
15
|
+
{ text: text, annotations: annotations, _meta: meta, type: "text" }.compact
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
class Image
|
|
19
|
-
attr_reader :data, :mime_type, :annotations
|
|
20
|
+
attr_reader :data, :mime_type, :annotations, :meta
|
|
20
21
|
|
|
21
|
-
def initialize(data, mime_type, annotations: nil)
|
|
22
|
+
def initialize(data, mime_type, annotations: nil, meta: nil)
|
|
22
23
|
@data = data
|
|
23
24
|
@mime_type = mime_type
|
|
24
25
|
@annotations = annotations
|
|
26
|
+
@meta = meta
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def to_h
|
|
28
|
-
{ data: data, mimeType: mime_type, annotations: annotations, type: "image" }.compact
|
|
30
|
+
{ data: data, mimeType: mime_type, annotations: annotations, _meta: meta, type: "image" }.compact
|
|
29
31
|
end
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
class Audio
|
|
33
|
-
attr_reader :data, :mime_type, :annotations
|
|
35
|
+
attr_reader :data, :mime_type, :annotations, :meta
|
|
34
36
|
|
|
35
|
-
def initialize(data, mime_type, annotations: nil)
|
|
37
|
+
def initialize(data, mime_type, annotations: nil, meta: nil)
|
|
36
38
|
@data = data
|
|
37
39
|
@mime_type = mime_type
|
|
38
40
|
@annotations = annotations
|
|
41
|
+
@meta = meta
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
def to_h
|
|
42
|
-
{ data: data, mimeType: mime_type, annotations: annotations, type: "audio" }.compact
|
|
45
|
+
{ data: data, mimeType: mime_type, annotations: annotations, _meta: meta, type: "audio" }.compact
|
|
43
46
|
end
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
class EmbeddedResource
|
|
47
|
-
attr_reader :resource, :annotations
|
|
50
|
+
attr_reader :resource, :annotations, :meta
|
|
48
51
|
|
|
49
|
-
def initialize(resource, annotations: nil)
|
|
52
|
+
def initialize(resource, annotations: nil, meta: nil)
|
|
50
53
|
@resource = resource
|
|
51
54
|
@annotations = annotations
|
|
55
|
+
@meta = meta
|
|
52
56
|
end
|
|
53
57
|
|
|
54
58
|
def to_h
|
|
55
|
-
{ resource: resource.to_h, annotations: annotations, type: "resource" }.compact
|
|
59
|
+
{ resource: resource.to_h, annotations: annotations, _meta: meta, type: "resource" }.compact
|
|
56
60
|
end
|
|
57
61
|
end
|
|
58
62
|
end
|
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/progress.rb
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module MCP
|
|
4
4
|
class Progress
|
|
5
|
-
def initialize(notification_target:, progress_token:)
|
|
5
|
+
def initialize(notification_target:, progress_token:, related_request_id: nil)
|
|
6
6
|
@notification_target = notification_target
|
|
7
7
|
@progress_token = progress_token
|
|
8
|
+
@related_request_id = related_request_id
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def report(progress, total: nil, message: nil)
|
|
@@ -16,6 +17,7 @@ module MCP
|
|
|
16
17
|
progress: progress,
|
|
17
18
|
total: total,
|
|
18
19
|
message: message,
|
|
20
|
+
related_request_id: @related_request_id,
|
|
19
21
|
)
|
|
20
22
|
end
|
|
21
23
|
end
|