mcp 0.18.0 → 0.20.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 +92 -3
- data/lib/json_rpc_handler.rb +7 -2
- data/lib/mcp/client/http.rb +68 -7
- data/lib/mcp/client/oauth/client_credentials_provider.rb +89 -0
- data/lib/mcp/client/oauth/discovery.rb +55 -1
- data/lib/mcp/client/oauth/flow.rb +138 -9
- data/lib/mcp/client/oauth/provider.rb +42 -27
- data/lib/mcp/client/oauth/storage_backed_provider.rb +43 -0
- data/lib/mcp/client/oauth.rb +3 -1
- data/lib/mcp/client.rb +71 -79
- data/lib/mcp/configuration.rb +1 -0
- data/lib/mcp/resource.rb +6 -2
- data/lib/mcp/resource_template.rb +4 -2
- data/lib/mcp/server/transports/streamable_http_transport.rb +51 -31
- data/lib/mcp/server.rb +27 -1
- data/lib/mcp/tool/input_schema.rb +2 -2
- data/lib/mcp/tool/schema.rb +38 -19
- data/lib/mcp/trace_context.rb +23 -0
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +1 -0
- metadata +8 -5
|
@@ -3,9 +3,11 @@
|
|
|
3
3
|
module MCP
|
|
4
4
|
class Client
|
|
5
5
|
module OAuth
|
|
6
|
-
# Pluggable OAuth client configuration
|
|
7
|
-
# the `oauth:` keyword.
|
|
8
|
-
#
|
|
6
|
+
# Pluggable OAuth client configuration for the OAuth 2.1 Authorization Code + PKCE flow,
|
|
7
|
+
# handed to `MCP::Client::HTTP` via the `oauth:` keyword.
|
|
8
|
+
# Inspired by the OAuthClientProvider in the TypeScript SDK and the httpx.Auth-based provider
|
|
9
|
+
# in the Python SDK. For the non-interactive machine-to-machine `client_credentials` grant,
|
|
10
|
+
# use `ClientCredentialsProvider` instead.
|
|
9
11
|
#
|
|
10
12
|
# Required keyword arguments:
|
|
11
13
|
# - `client_metadata` - Hash sent to the authorization server's Dynamic Client
|
|
@@ -25,7 +27,19 @@ module MCP
|
|
|
25
27
|
# - `storage` - Object responding to `tokens`, `save_tokens(tokens)`,
|
|
26
28
|
# `client_information`, and `save_client_information(info)`. Defaults to
|
|
27
29
|
# an `InMemoryStorage`.
|
|
30
|
+
# - `client_id_metadata_document_url` - URL where the client publishes its Client ID Metadata Document
|
|
31
|
+
# (`draft-ietf-oauth-client-id-metadata-document-00` and the MCP authorization specification).
|
|
32
|
+
# When the authorization server advertises `client_id_metadata_document_supported: true`,
|
|
33
|
+
# the SDK uses this URL as the OAuth `client_id` and skips Dynamic Client Registration.
|
|
34
|
+
# Spec-required: `https://` scheme, a non-root path, and no fragment, userinfo, or `.`/`..` segments.
|
|
35
|
+
# The SDK additionally refuses to send query strings (the draft marks them only SHOULD NOT include,
|
|
36
|
+
# but different encodings of the same query would yield different `client_id` strings for the same document).
|
|
37
|
+
# The document served at the URL is a separate JSON artifact from the `client_metadata` keyword:
|
|
38
|
+
# DCR `client_metadata` MUST NOT include `client_id`, while the CIMD document MUST include `client_id` set
|
|
39
|
+
# to the URL, `client_name`, and `redirect_uris` covering `redirect_uri`.
|
|
28
40
|
class Provider
|
|
41
|
+
include StorageBackedProvider
|
|
42
|
+
|
|
29
43
|
# Raised when `Provider#initialize` is called with a `redirect_uri` that
|
|
30
44
|
# is neither HTTPS nor a loopback `http://` URL, per the MCP
|
|
31
45
|
# authorization spec's Communication Security requirement.
|
|
@@ -38,12 +52,21 @@ module MCP
|
|
|
38
52
|
# runtime; failing at construction surfaces the bug earlier.
|
|
39
53
|
class UnregisteredRedirectURIError < ArgumentError; end
|
|
40
54
|
|
|
55
|
+
# Raised when `client_id_metadata_document_url` is provided but does not meet
|
|
56
|
+
# the structural requirements for a Client ID Metadata Document URL:
|
|
57
|
+
# HTTPS, non-root path, and no fragment, query, userinfo, or `.`/`..` segments.
|
|
58
|
+
# The CIMD URL is sent to the authorization server as the OAuth `client_id`,
|
|
59
|
+
# so the same Communication Security guarantee that protects the redirect URI
|
|
60
|
+
# applies and the value must unambiguously identify the document.
|
|
61
|
+
class InvalidClientIDMetadataDocumentURLError < ArgumentError; end
|
|
62
|
+
|
|
41
63
|
attr_reader :client_metadata,
|
|
42
64
|
:redirect_uri,
|
|
43
65
|
:scope,
|
|
44
66
|
:storage,
|
|
45
67
|
:redirect_handler,
|
|
46
|
-
:callback_handler
|
|
68
|
+
:callback_handler,
|
|
69
|
+
:client_id_metadata_document_url
|
|
47
70
|
|
|
48
71
|
def initialize(
|
|
49
72
|
client_metadata:,
|
|
@@ -51,7 +74,8 @@ module MCP
|
|
|
51
74
|
redirect_handler:,
|
|
52
75
|
callback_handler:,
|
|
53
76
|
scope: nil,
|
|
54
|
-
storage: nil
|
|
77
|
+
storage: nil,
|
|
78
|
+
client_id_metadata_document_url: nil
|
|
55
79
|
)
|
|
56
80
|
unless Discovery.secure_url?(redirect_uri)
|
|
57
81
|
raise InsecureRedirectURIError,
|
|
@@ -66,36 +90,27 @@ module MCP
|
|
|
66
90
|
"(got #{registered.inspect}); otherwise the authorization server will reject the authorization request."
|
|
67
91
|
end
|
|
68
92
|
|
|
93
|
+
if client_id_metadata_document_url && !Discovery.client_id_metadata_document_url?(client_id_metadata_document_url)
|
|
94
|
+
raise InvalidClientIDMetadataDocumentURLError,
|
|
95
|
+
"client_id_metadata_document_url #{client_id_metadata_document_url.inspect} must be an https URL " \
|
|
96
|
+
"with a non-root path and no fragment, query, userinfo, or `.`/`..` segments, " \
|
|
97
|
+
"per the MCP authorization specification and `draft-ietf-oauth-client-id-metadata-document`."
|
|
98
|
+
end
|
|
99
|
+
|
|
69
100
|
@client_metadata = client_metadata
|
|
70
101
|
@redirect_uri = redirect_uri
|
|
71
102
|
@redirect_handler = redirect_handler
|
|
72
103
|
@callback_handler = callback_handler
|
|
73
104
|
@scope = scope
|
|
74
105
|
@storage = storage || InMemoryStorage.new
|
|
106
|
+
@client_id_metadata_document_url = client_id_metadata_document_url
|
|
75
107
|
end
|
|
76
108
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
@storage.tokens
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def save_tokens(tokens)
|
|
86
|
-
@storage.save_tokens(tokens)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def client_information
|
|
90
|
-
@storage.client_information
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def save_client_information(info)
|
|
94
|
-
@storage.save_client_information(info)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def clear_tokens!
|
|
98
|
-
@storage.save_tokens(nil)
|
|
109
|
+
# Identifies the OAuth flow this provider drives.
|
|
110
|
+
# `Flow` dispatches on this rather than inspecting `client_metadata[:grant_types]`,
|
|
111
|
+
# which is protocol metadata for the authorization server, not an SDK control signal.
|
|
112
|
+
def authorization_flow
|
|
113
|
+
:authorization_code
|
|
99
114
|
end
|
|
100
115
|
end
|
|
101
116
|
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
class Client
|
|
5
|
+
module OAuth
|
|
6
|
+
# Shared token/credential persistence for the OAuth provider classes
|
|
7
|
+
# (`Provider` for the authorization-code flow and `ClientCredentialsProvider`
|
|
8
|
+
# for the client_credentials flow). The two grants differ in how they authenticate,
|
|
9
|
+
# but both read and write the same two pieces of state through a `storage` object:
|
|
10
|
+
# the token response and the client information. This module supplies that delegation
|
|
11
|
+
# so the `Flow` orchestrator can treat any provider uniformly.
|
|
12
|
+
#
|
|
13
|
+
# Including classes must set `@storage` to an object responding to `tokens`,
|
|
14
|
+
# `save_tokens(tokens)`, `client_information`, and `save_client_information(info)`
|
|
15
|
+
# (see `InMemoryStorage`).
|
|
16
|
+
module StorageBackedProvider
|
|
17
|
+
def access_token
|
|
18
|
+
tokens&.dig("access_token") || tokens&.dig(:access_token)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def tokens
|
|
22
|
+
@storage.tokens
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def save_tokens(tokens)
|
|
26
|
+
@storage.save_tokens(tokens)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def client_information
|
|
30
|
+
@storage.client_information
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def save_client_information(info)
|
|
34
|
+
@storage.save_client_information(info)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def clear_tokens!
|
|
38
|
+
@storage.save_tokens(nil)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/mcp/client/oauth.rb
CHANGED
|
@@ -4,13 +4,15 @@ require_relative "oauth/discovery"
|
|
|
4
4
|
require_relative "oauth/flow"
|
|
5
5
|
require_relative "oauth/in_memory_storage"
|
|
6
6
|
require_relative "oauth/pkce"
|
|
7
|
+
require_relative "oauth/storage_backed_provider"
|
|
7
8
|
require_relative "oauth/provider"
|
|
9
|
+
require_relative "oauth/client_credentials_provider"
|
|
8
10
|
|
|
9
11
|
module MCP
|
|
10
12
|
class Client
|
|
11
13
|
# OAuth client support for the MCP Authorization spec (PRM discovery,
|
|
12
14
|
# Authorization Server metadata discovery, Dynamic Client Registration,
|
|
13
|
-
# OAuth 2.1 Authorization Code + PKCE).
|
|
15
|
+
# OAuth 2.1 Authorization Code + PKCE, and the client_credentials grant).
|
|
14
16
|
# https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization
|
|
15
17
|
module OAuth
|
|
16
18
|
end
|
data/lib/mcp/client.rb
CHANGED
|
@@ -103,6 +103,8 @@ module MCP
|
|
|
103
103
|
# Returns a single page of tools from the server.
|
|
104
104
|
#
|
|
105
105
|
# @param cursor [String, nil] Cursor from a previous page response.
|
|
106
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
107
|
+
# e.g. SEP-414 trace context (see {MCP::TraceContext}).
|
|
106
108
|
# @return [MCP::Client::ListToolsResult] Result with `tools` (Array<MCP::Client::Tool>)
|
|
107
109
|
# and `next_cursor` (String or nil).
|
|
108
110
|
#
|
|
@@ -114,9 +116,9 @@ module MCP
|
|
|
114
116
|
# cursor = page.next_cursor
|
|
115
117
|
# break unless cursor
|
|
116
118
|
# end
|
|
117
|
-
def list_tools(cursor: nil)
|
|
119
|
+
def list_tools(cursor: nil, meta: nil)
|
|
118
120
|
params = cursor ? { cursor: cursor } : nil
|
|
119
|
-
response = request(method: "tools/list", params: params)
|
|
121
|
+
response = request(method: "tools/list", params: params, meta: meta)
|
|
120
122
|
result = response["result"] || {}
|
|
121
123
|
|
|
122
124
|
tools = (result["tools"] || []).map do |tool|
|
|
@@ -146,31 +148,19 @@ module MCP
|
|
|
146
148
|
# end
|
|
147
149
|
def tools
|
|
148
150
|
# TODO: consider renaming to `list_all_tools`.
|
|
149
|
-
|
|
150
|
-
seen = Set.new
|
|
151
|
-
cursor = nil
|
|
152
|
-
|
|
153
|
-
loop do
|
|
154
|
-
page = list_tools(cursor: cursor)
|
|
155
|
-
all_tools.concat(page.tools)
|
|
156
|
-
next_cursor = page.next_cursor
|
|
157
|
-
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
158
|
-
|
|
159
|
-
seen << next_cursor
|
|
160
|
-
cursor = next_cursor
|
|
161
|
-
end
|
|
162
|
-
|
|
163
|
-
all_tools
|
|
151
|
+
fetch_all_pages { |cursor| list_tools(cursor: cursor) }.flat_map(&:tools)
|
|
164
152
|
end
|
|
165
153
|
|
|
166
154
|
# Returns a single page of resources from the server.
|
|
167
155
|
#
|
|
168
156
|
# @param cursor [String, nil] Cursor from a previous page response.
|
|
157
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
158
|
+
# e.g. SEP-414 trace context (see {MCP::TraceContext}).
|
|
169
159
|
# @return [MCP::Client::ListResourcesResult] Result with `resources` (Array<Hash>)
|
|
170
160
|
# and `next_cursor` (String or nil).
|
|
171
|
-
def list_resources(cursor: nil)
|
|
161
|
+
def list_resources(cursor: nil, meta: nil)
|
|
172
162
|
params = cursor ? { cursor: cursor } : nil
|
|
173
|
-
response = request(method: "resources/list", params: params)
|
|
163
|
+
response = request(method: "resources/list", params: params, meta: meta)
|
|
174
164
|
result = response["result"] || {}
|
|
175
165
|
|
|
176
166
|
ListResourcesResult.new(
|
|
@@ -189,31 +179,19 @@ module MCP
|
|
|
189
179
|
# @return [Array<Hash>] An array of available resources.
|
|
190
180
|
def resources
|
|
191
181
|
# TODO: consider renaming to `list_all_resources`.
|
|
192
|
-
|
|
193
|
-
seen = Set.new
|
|
194
|
-
cursor = nil
|
|
195
|
-
|
|
196
|
-
loop do
|
|
197
|
-
page = list_resources(cursor: cursor)
|
|
198
|
-
all_resources.concat(page.resources)
|
|
199
|
-
next_cursor = page.next_cursor
|
|
200
|
-
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
201
|
-
|
|
202
|
-
seen << next_cursor
|
|
203
|
-
cursor = next_cursor
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
all_resources
|
|
182
|
+
fetch_all_pages { |cursor| list_resources(cursor: cursor) }.flat_map(&:resources)
|
|
207
183
|
end
|
|
208
184
|
|
|
209
185
|
# Returns a single page of resource templates from the server.
|
|
210
186
|
#
|
|
211
187
|
# @param cursor [String, nil] Cursor from a previous page response.
|
|
188
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
189
|
+
# e.g. SEP-414 trace context (see {MCP::TraceContext}).
|
|
212
190
|
# @return [MCP::Client::ListResourceTemplatesResult] Result with `resource_templates`
|
|
213
191
|
# (Array<Hash>) and `next_cursor` (String or nil).
|
|
214
|
-
def list_resource_templates(cursor: nil)
|
|
192
|
+
def list_resource_templates(cursor: nil, meta: nil)
|
|
215
193
|
params = cursor ? { cursor: cursor } : nil
|
|
216
|
-
response = request(method: "resources/templates/list", params: params)
|
|
194
|
+
response = request(method: "resources/templates/list", params: params, meta: meta)
|
|
217
195
|
result = response["result"] || {}
|
|
218
196
|
|
|
219
197
|
ListResourceTemplatesResult.new(
|
|
@@ -232,31 +210,19 @@ module MCP
|
|
|
232
210
|
# @return [Array<Hash>] An array of available resource templates.
|
|
233
211
|
def resource_templates
|
|
234
212
|
# TODO: consider renaming to `list_all_resource_templates`.
|
|
235
|
-
|
|
236
|
-
seen = Set.new
|
|
237
|
-
cursor = nil
|
|
238
|
-
|
|
239
|
-
loop do
|
|
240
|
-
page = list_resource_templates(cursor: cursor)
|
|
241
|
-
all_templates.concat(page.resource_templates)
|
|
242
|
-
next_cursor = page.next_cursor
|
|
243
|
-
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
244
|
-
|
|
245
|
-
seen << next_cursor
|
|
246
|
-
cursor = next_cursor
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
all_templates
|
|
213
|
+
fetch_all_pages { |cursor| list_resource_templates(cursor: cursor) }.flat_map(&:resource_templates)
|
|
250
214
|
end
|
|
251
215
|
|
|
252
216
|
# Returns a single page of prompts from the server.
|
|
253
217
|
#
|
|
254
218
|
# @param cursor [String, nil] Cursor from a previous page response.
|
|
219
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
220
|
+
# e.g. SEP-414 trace context (see {MCP::TraceContext}).
|
|
255
221
|
# @return [MCP::Client::ListPromptsResult] Result with `prompts` (Array<Hash>)
|
|
256
222
|
# and `next_cursor` (String or nil).
|
|
257
|
-
def list_prompts(cursor: nil)
|
|
223
|
+
def list_prompts(cursor: nil, meta: nil)
|
|
258
224
|
params = cursor ? { cursor: cursor } : nil
|
|
259
|
-
response = request(method: "prompts/list", params: params)
|
|
225
|
+
response = request(method: "prompts/list", params: params, meta: meta)
|
|
260
226
|
result = response["result"] || {}
|
|
261
227
|
|
|
262
228
|
ListPromptsResult.new(
|
|
@@ -275,21 +241,7 @@ module MCP
|
|
|
275
241
|
# @return [Array<Hash>] An array of available prompts.
|
|
276
242
|
def prompts
|
|
277
243
|
# TODO: consider renaming to `list_all_prompts`.
|
|
278
|
-
|
|
279
|
-
seen = Set.new
|
|
280
|
-
cursor = nil
|
|
281
|
-
|
|
282
|
-
loop do
|
|
283
|
-
page = list_prompts(cursor: cursor)
|
|
284
|
-
all_prompts.concat(page.prompts)
|
|
285
|
-
next_cursor = page.next_cursor
|
|
286
|
-
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
287
|
-
|
|
288
|
-
seen << next_cursor
|
|
289
|
-
cursor = next_cursor
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
all_prompts
|
|
244
|
+
fetch_all_pages { |cursor| list_prompts(cursor: cursor) }.flat_map(&:prompts)
|
|
293
245
|
end
|
|
294
246
|
|
|
295
247
|
# Calls a tool via the transport layer and returns the full response from the server.
|
|
@@ -298,6 +250,10 @@ module MCP
|
|
|
298
250
|
# @param tool [MCP::Client::Tool] The tool to be called.
|
|
299
251
|
# @param arguments [Object, nil] The arguments to pass to the tool.
|
|
300
252
|
# @param progress_token [String, Integer, nil] A token to request progress notifications from the server during tool execution.
|
|
253
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
254
|
+
# e.g. the W3C Trace Context keys reserved by SEP-414
|
|
255
|
+
# (`MCP::TraceContext::TRACEPARENT_META_KEY`, `tracestate`, `baggage`).
|
|
256
|
+
# `progress_token` takes precedence over a `progressToken` entry in `meta`.
|
|
301
257
|
# @return [Hash] The full JSON-RPC response from the transport.
|
|
302
258
|
#
|
|
303
259
|
# @example Call by name
|
|
@@ -312,14 +268,17 @@ module MCP
|
|
|
312
268
|
# @note
|
|
313
269
|
# The exact requirements for `arguments` are determined by the transport layer in use.
|
|
314
270
|
# Consult the documentation for your transport (e.g., MCP::Client::HTTP) for details.
|
|
315
|
-
def call_tool(name: nil, tool: nil, arguments: nil, progress_token: nil)
|
|
271
|
+
def call_tool(name: nil, tool: nil, arguments: nil, progress_token: nil, meta: nil)
|
|
316
272
|
tool_name = name || tool&.name
|
|
317
273
|
raise ArgumentError, "Either `name:` or `tool:` must be provided." unless tool_name
|
|
318
274
|
|
|
319
275
|
params = { name: tool_name, arguments: arguments }
|
|
276
|
+
meta_entries = meta ? meta.dup : {}
|
|
320
277
|
if progress_token
|
|
321
|
-
|
|
278
|
+
meta_entries.delete("progressToken")
|
|
279
|
+
meta_entries[:progressToken] = progress_token
|
|
322
280
|
end
|
|
281
|
+
params[:_meta] = meta_entries unless meta_entries.empty?
|
|
323
282
|
|
|
324
283
|
request(method: "tools/call", params: params)
|
|
325
284
|
end
|
|
@@ -327,9 +286,11 @@ module MCP
|
|
|
327
286
|
# Reads a resource from the server by URI and returns the contents.
|
|
328
287
|
#
|
|
329
288
|
# @param uri [String] The URI of the resource to read.
|
|
289
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
290
|
+
# e.g. SEP-414 trace context (see {MCP::TraceContext}).
|
|
330
291
|
# @return [Array<Hash>] An array of resource contents (text or blob).
|
|
331
|
-
def read_resource(uri:)
|
|
332
|
-
response = request(method: "resources/read", params: { uri: uri })
|
|
292
|
+
def read_resource(uri:, meta: nil)
|
|
293
|
+
response = request(method: "resources/read", params: { uri: uri }, meta: meta)
|
|
333
294
|
|
|
334
295
|
response.dig("result", "contents") || []
|
|
335
296
|
end
|
|
@@ -337,9 +298,11 @@ module MCP
|
|
|
337
298
|
# Gets a prompt from the server by name and returns its details.
|
|
338
299
|
#
|
|
339
300
|
# @param name [String] The name of the prompt to get.
|
|
301
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
302
|
+
# e.g. SEP-414 trace context (see {MCP::TraceContext}).
|
|
340
303
|
# @return [Hash] A hash containing the prompt details.
|
|
341
|
-
def get_prompt(name:)
|
|
342
|
-
response = request(method: "prompts/get", params: { name: name })
|
|
304
|
+
def get_prompt(name:, meta: nil)
|
|
305
|
+
response = request(method: "prompts/get", params: { name: name }, meta: meta)
|
|
343
306
|
|
|
344
307
|
response.fetch("result", {})
|
|
345
308
|
end
|
|
@@ -350,12 +313,14 @@ module MCP
|
|
|
350
313
|
# or `{ type: "ref/resource", uri: "file:///{path}" }`.
|
|
351
314
|
# @param argument [Hash] The argument being completed, e.g. `{ name: "language", value: "py" }`.
|
|
352
315
|
# @param context [Hash, nil] Optional context with previously resolved arguments.
|
|
316
|
+
# @param meta [Hash, nil] Additional `_meta` entries to send with the request,
|
|
317
|
+
# e.g. SEP-414 trace context (see {MCP::TraceContext}).
|
|
353
318
|
# @return [Hash] The completion result with `"values"`, `"hasMore"`, and optionally `"total"`.
|
|
354
|
-
def complete(ref:, argument:, context: nil)
|
|
319
|
+
def complete(ref:, argument:, context: nil, meta: nil)
|
|
355
320
|
params = { ref: ref, argument: argument }
|
|
356
321
|
params[:context] = context if context
|
|
357
322
|
|
|
358
|
-
response = request(method: "completion/complete", params: params)
|
|
323
|
+
response = request(method: "completion/complete", params: params, meta: meta)
|
|
359
324
|
|
|
360
325
|
response.dig("result", "completion") || { "values" => [], "hasMore" => false }
|
|
361
326
|
end
|
|
@@ -371,8 +336,8 @@ module MCP
|
|
|
371
336
|
# client.ping # => {}
|
|
372
337
|
#
|
|
373
338
|
# @see https://modelcontextprotocol.io/specification/latest/basic/utilities/ping
|
|
374
|
-
def ping
|
|
375
|
-
result = request(method: Methods::PING)["result"]
|
|
339
|
+
def ping(meta: nil)
|
|
340
|
+
result = request(method: Methods::PING, meta: meta)["result"]
|
|
376
341
|
raise ValidationError, "Response validation failed: missing or invalid `result`" unless result.is_a?(Hash)
|
|
377
342
|
|
|
378
343
|
result
|
|
@@ -380,7 +345,34 @@ module MCP
|
|
|
380
345
|
|
|
381
346
|
private
|
|
382
347
|
|
|
383
|
-
|
|
348
|
+
# Walks every page of a list endpoint, following `next_cursor`, and returns
|
|
349
|
+
# the page results. The `seen` set guards against a server that repeats or
|
|
350
|
+
# cycles cursors, so the loop always terminates.
|
|
351
|
+
def fetch_all_pages
|
|
352
|
+
pages = []
|
|
353
|
+
seen = Set.new
|
|
354
|
+
cursor = nil
|
|
355
|
+
|
|
356
|
+
loop do
|
|
357
|
+
page = yield(cursor)
|
|
358
|
+
pages << page
|
|
359
|
+
next_cursor = page.next_cursor
|
|
360
|
+
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
361
|
+
|
|
362
|
+
seen << next_cursor
|
|
363
|
+
cursor = next_cursor
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
pages
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
# Merges caller-supplied `meta` entries into the request params as `_meta`,
|
|
370
|
+
# without mutating the caller's hashes. Per SEP-414, `_meta` carries
|
|
371
|
+
# request-specific metadata such as W3C trace context (`traceparent`,
|
|
372
|
+
# `tracestate`, `baggage`); see {MCP::TraceContext}.
|
|
373
|
+
def request(method:, params: nil, meta: nil)
|
|
374
|
+
params = (params || {}).merge(_meta: meta) if meta && !meta.empty?
|
|
375
|
+
|
|
384
376
|
request_body = {
|
|
385
377
|
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
386
378
|
id: request_id,
|
data/lib/mcp/configuration.rb
CHANGED
data/lib/mcp/resource.rb
CHANGED
|
@@ -5,15 +5,17 @@ require_relative "resource/embedded"
|
|
|
5
5
|
|
|
6
6
|
module MCP
|
|
7
7
|
class Resource
|
|
8
|
-
attr_reader :uri, :name, :title, :description, :icons, :mime_type, :meta
|
|
8
|
+
attr_reader :uri, :name, :title, :description, :icons, :mime_type, :annotations, :size, :meta
|
|
9
9
|
|
|
10
|
-
def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil)
|
|
10
|
+
def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil, annotations: nil, size: 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
|
+
@annotations = annotations
|
|
18
|
+
@size = size
|
|
17
19
|
@meta = meta
|
|
18
20
|
end
|
|
19
21
|
|
|
@@ -25,6 +27,8 @@ module MCP
|
|
|
25
27
|
description: description,
|
|
26
28
|
icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
|
|
27
29
|
mimeType: mime_type,
|
|
30
|
+
annotations: annotations&.to_h,
|
|
31
|
+
size: size,
|
|
28
32
|
_meta: meta,
|
|
29
33
|
}.compact
|
|
30
34
|
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, :meta
|
|
5
|
+
attr_reader :uri_template, :name, :title, :description, :icons, :mime_type, :annotations, :meta
|
|
6
6
|
|
|
7
|
-
def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil)
|
|
7
|
+
def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil, annotations: 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
|
+
@annotations = annotations
|
|
14
15
|
@meta = meta
|
|
15
16
|
end
|
|
16
17
|
|
|
@@ -22,6 +23,7 @@ module MCP
|
|
|
22
23
|
description: description,
|
|
23
24
|
icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
|
|
24
25
|
mimeType: mime_type,
|
|
26
|
+
annotations: annotations&.to_h,
|
|
25
27
|
_meta: meta,
|
|
26
28
|
}.compact
|
|
27
29
|
end
|
|
@@ -389,7 +389,11 @@ module MCP
|
|
|
389
389
|
end
|
|
390
390
|
rescue StandardError => e
|
|
391
391
|
MCP.configuration.exception_reporter.call(e, { request: body_string })
|
|
392
|
-
|
|
392
|
+
json_rpc_error_response(
|
|
393
|
+
status: 500,
|
|
394
|
+
code: JsonRpcHandler::ErrorCode::INTERNAL_ERROR,
|
|
395
|
+
message: "Internal server error",
|
|
396
|
+
)
|
|
393
397
|
end
|
|
394
398
|
|
|
395
399
|
def handle_get(request)
|
|
@@ -513,19 +517,19 @@ module MCP
|
|
|
513
517
|
media_type = content_type&.split(";")&.first&.strip&.downcase
|
|
514
518
|
return if media_type == "application/json"
|
|
515
519
|
|
|
516
|
-
|
|
517
|
-
415,
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
520
|
+
json_rpc_error_response(
|
|
521
|
+
status: 415,
|
|
522
|
+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
|
523
|
+
message: "Unsupported Media Type: Content-Type must be application/json",
|
|
524
|
+
)
|
|
521
525
|
end
|
|
522
526
|
|
|
523
527
|
def not_acceptable_response(required_types)
|
|
524
|
-
|
|
525
|
-
406,
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
528
|
+
json_rpc_error_response(
|
|
529
|
+
status: 406,
|
|
530
|
+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
|
531
|
+
message: "Not Acceptable: Accept header must include #{required_types.join(" and ")}",
|
|
532
|
+
)
|
|
529
533
|
end
|
|
530
534
|
|
|
531
535
|
def parse_request_body(body_string)
|
|
@@ -535,7 +539,11 @@ module MCP
|
|
|
535
539
|
end
|
|
536
540
|
|
|
537
541
|
def invalid_json_response
|
|
538
|
-
|
|
542
|
+
json_rpc_error_response(
|
|
543
|
+
status: 400,
|
|
544
|
+
code: JsonRpcHandler::ErrorCode::PARSE_ERROR,
|
|
545
|
+
message: "Parse error: Invalid JSON",
|
|
546
|
+
)
|
|
539
547
|
end
|
|
540
548
|
|
|
541
549
|
def initialize_request?(body)
|
|
@@ -543,20 +551,20 @@ module MCP
|
|
|
543
551
|
end
|
|
544
552
|
|
|
545
553
|
def validate_protocol_version_header(request)
|
|
546
|
-
header_value = request.env["HTTP_MCP_PROTOCOL_VERSION"]
|
|
547
|
-
return if header_value.nil?
|
|
554
|
+
header_value = request.env["HTTP_MCP_PROTOCOL_VERSION"] || MCP::Configuration::DEFAULT_NEGOTIATED_PROTOCOL_VERSION
|
|
548
555
|
return if MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(header_value)
|
|
549
556
|
|
|
550
557
|
supported = MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.join(", ")
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
558
|
+
json_rpc_error_response(
|
|
559
|
+
status: 400,
|
|
560
|
+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
|
561
|
+
message: "Bad Request: Unsupported protocol version: #{header_value}. Supported versions: #{supported}",
|
|
562
|
+
)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def json_rpc_error_response(status:, code:, message:)
|
|
566
|
+
body = { jsonrpc: "2.0", id: nil, error: { code: code, message: message } }
|
|
567
|
+
[status, { "Content-Type" => "application/json" }, [body.to_json]]
|
|
560
568
|
end
|
|
561
569
|
|
|
562
570
|
def notification?(body)
|
|
@@ -793,15 +801,27 @@ module MCP
|
|
|
793
801
|
end
|
|
794
802
|
|
|
795
803
|
def method_not_allowed_response
|
|
796
|
-
|
|
804
|
+
json_rpc_error_response(
|
|
805
|
+
status: 405,
|
|
806
|
+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
|
807
|
+
message: "Method not allowed",
|
|
808
|
+
)
|
|
797
809
|
end
|
|
798
810
|
|
|
799
811
|
def missing_session_id_response
|
|
800
|
-
|
|
812
|
+
json_rpc_error_response(
|
|
813
|
+
status: 400,
|
|
814
|
+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
|
815
|
+
message: "Missing session ID",
|
|
816
|
+
)
|
|
801
817
|
end
|
|
802
818
|
|
|
803
819
|
def session_not_found_response
|
|
804
|
-
|
|
820
|
+
json_rpc_error_response(
|
|
821
|
+
status: 404,
|
|
822
|
+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
|
823
|
+
message: "Session not found",
|
|
824
|
+
)
|
|
805
825
|
end
|
|
806
826
|
|
|
807
827
|
def already_initialized_response(request_id)
|
|
@@ -821,11 +841,11 @@ module MCP
|
|
|
821
841
|
end
|
|
822
842
|
|
|
823
843
|
def session_already_connected_response
|
|
824
|
-
|
|
825
|
-
409,
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
844
|
+
json_rpc_error_response(
|
|
845
|
+
status: 409,
|
|
846
|
+
code: JsonRpcHandler::ErrorCode::INVALID_REQUEST,
|
|
847
|
+
message: "Conflict: Only one SSE stream is allowed per session",
|
|
848
|
+
)
|
|
829
849
|
end
|
|
830
850
|
|
|
831
851
|
def setup_sse_stream(session_id)
|