mcp 1.9.0 → 1.10.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 +3 -0
- data/lib/json_rpc_handler.rb +1 -1
- data/lib/mcp/client/http.rb +29 -5
- data/lib/mcp/client/oauth/client_credentials_provider.rb +16 -0
- data/lib/mcp/client/oauth/cross_app_access_provider.rb +19 -2
- data/lib/mcp/client/oauth/flow.rb +11 -3
- data/lib/mcp/client/oauth/jwt_client_assertion.rb +3 -2
- data/lib/mcp/client/oauth/pkce.rb +3 -2
- data/lib/mcp/client/stdio.rb +15 -3
- data/lib/mcp/client.rb +5 -1
- data/lib/mcp/instrumentation.rb +12 -7
- data/lib/mcp/mcp_param_headers.rb +332 -0
- data/lib/mcp/server/oauth/jwt_verifier.rb +34 -5
- data/lib/mcp/server/transports/stdio_transport.rb +12 -0
- data/lib/mcp/server/transports/streamable_http_transport.rb +168 -19
- data/lib/mcp/server.rb +60 -35
- data/lib/mcp/server_session.rb +23 -1
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +2 -0
- metadata +3 -3
- data/lib/mcp/client/mcp_param_headers.rb +0 -242
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
# The custom-header half of SEP-2243 (MCP 2026-07-28), shared by the client and the server transport: scanning a tool's `inputSchema`
|
|
5
|
+
# for `x-mcp-header` declarations, encoding `tools/call` argument values into `Mcp-Param-{Name}` HTTP headers with
|
|
6
|
+
# the `=?base64?...?=` sentinel for values that cannot ride as plain ASCII field values, decoding such headers, and checking that
|
|
7
|
+
# the headers a request carries agree with the arguments in its body. Mirrors the TypeScript SDK's `mcpParamHeaders` codec;
|
|
8
|
+
# the standard-header half (`Mcp-Method`, `Mcp-Name`) lives with the transports.
|
|
9
|
+
#
|
|
10
|
+
# https://modelcontextprotocol.io/specification/draft/basic/transports/streamable-http#custom-headers-from-tool-parameters
|
|
11
|
+
module McpParamHeaders
|
|
12
|
+
# The fixed prefix every custom-parameter header carries.
|
|
13
|
+
HEADER_PREFIX = "Mcp-Param-"
|
|
14
|
+
|
|
15
|
+
# The schema-extension property name a tool's `inputSchema` carries.
|
|
16
|
+
X_MCP_HEADER_KEY = "x-mcp-header"
|
|
17
|
+
|
|
18
|
+
# RFC 9110 Section 5.1 `token` syntax (`1*tchar`): rejects empty names, spaces,
|
|
19
|
+
# control characters (including CR/LF), and the HTTP delimiters.
|
|
20
|
+
RFC9110_TOKEN = /\A[!#$%&'*+\-.^_`|~0-9A-Za-z]+\z/.freeze
|
|
21
|
+
|
|
22
|
+
# The spec text admits `string`, `integer`, and `boolean`. `number` is also accepted because
|
|
23
|
+
# the published conformance referee annotates `type: "number"` parameters and expects them mirrored;
|
|
24
|
+
# the TypeScript SDK makes the same accommodation.
|
|
25
|
+
PERMITTED_TYPES = ["string", "integer", "boolean", "number"].freeze
|
|
26
|
+
|
|
27
|
+
# JSON Schema keywords the SEP-2243 static-reachability constraint excludes from the `properties`-only chain.
|
|
28
|
+
# An `x-mcp-header` under any of these invalidates the tool definition rather than being silently ignored.
|
|
29
|
+
NON_REACHABLE_SUBSCHEMA_KEYWORDS = [
|
|
30
|
+
"items",
|
|
31
|
+
"prefixItems",
|
|
32
|
+
"contains",
|
|
33
|
+
"additionalProperties",
|
|
34
|
+
"unevaluatedProperties",
|
|
35
|
+
"unevaluatedItems",
|
|
36
|
+
"propertyNames",
|
|
37
|
+
"patternProperties",
|
|
38
|
+
"dependentSchemas",
|
|
39
|
+
"oneOf",
|
|
40
|
+
"anyOf",
|
|
41
|
+
"allOf",
|
|
42
|
+
"not",
|
|
43
|
+
"if",
|
|
44
|
+
"then",
|
|
45
|
+
"else",
|
|
46
|
+
"contentSchema",
|
|
47
|
+
"$defs",
|
|
48
|
+
"definitions",
|
|
49
|
+
].freeze
|
|
50
|
+
|
|
51
|
+
# Keywords whose value maps names to subschemas rather than being one subschema or a list of them.
|
|
52
|
+
OBJECT_VALUED_SUBSCHEMA_KEYWORDS = ["patternProperties", "dependentSchemas", "$defs", "definitions"].freeze
|
|
53
|
+
|
|
54
|
+
# Integers beyond 2**53 - 1 lose precision in JSON number interchange, so they are not mirrored;
|
|
55
|
+
# the TypeScript SDK refuses unsafe integers the same way.
|
|
56
|
+
MAX_SAFE_INTEGER = (2**53) - 1
|
|
57
|
+
|
|
58
|
+
BASE64_SENTINEL_PREFIX = "=?base64?"
|
|
59
|
+
BASE64_SENTINEL_SUFFIX = "?="
|
|
60
|
+
|
|
61
|
+
# The decimal numerals the numeric comparison of an integer or number declaration accepts from a header:
|
|
62
|
+
# the rendering of an integer (`42`, `-7`), or one with a fraction, which the spec's SHOULD equates to the integer (`42.0`).
|
|
63
|
+
# Anything else (`0x2a`, `1e3`, `042`, ` 42`) is compared as a string and so never agrees by accident.
|
|
64
|
+
CANONICAL_DECIMAL = /\A-?(?:0|[1-9]\d*)(?:\.\d+)?\z/.freeze
|
|
65
|
+
|
|
66
|
+
NUMERIC_TYPES = ["integer", "number"].freeze
|
|
67
|
+
|
|
68
|
+
# The longest numeral the numeric comparison reads, which bounds the arbitrary-precision parsing a header can ask for.
|
|
69
|
+
# A longer numeral is judged by byte equality alone: that still accepts the rendering a client mirrors,
|
|
70
|
+
# however long (an integral double renders as its digits), and refuses every other spelling of the number.
|
|
71
|
+
MAX_NUMERAL_BYTES = 64
|
|
72
|
+
|
|
73
|
+
class << self
|
|
74
|
+
# Scans a tool's `inputSchema` for `x-mcp-header` declarations and validates every constraint the spec places on them:
|
|
75
|
+
# RFC 9110 token names, case-insensitive uniqueness, primitive-typed declaring properties, and static reachability through
|
|
76
|
+
# a chain of `properties` keys only. Returns `{ valid: true, declarations: [...] }` with each declaration `{ path:, header_name:, type: }`,
|
|
77
|
+
# or `{ valid: false, reason: "..." }` on the first violation.
|
|
78
|
+
def scan(input_schema)
|
|
79
|
+
declarations = []
|
|
80
|
+
fault = visit(input_schema, [], true, declarations, {})
|
|
81
|
+
|
|
82
|
+
fault ? { valid: false, reason: fault } : { valid: true, declarations: declarations }
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Builds the `Mcp-Param-{Name}` headers for one `tools/call` from the scanned declarations and the call's `arguments`.
|
|
86
|
+
# A `null` or absent value omits its header (the spec's MUST-omit rows); a non-primitive or non-representable value is
|
|
87
|
+
# omitted rather than emitted malformed.
|
|
88
|
+
def build(declarations, arguments)
|
|
89
|
+
declarations.each_with_object({}) do |declaration, headers|
|
|
90
|
+
value = value_at_path(arguments, declaration[:path])
|
|
91
|
+
next if value.nil?
|
|
92
|
+
|
|
93
|
+
string_value = primitive_to_string(value)
|
|
94
|
+
next unless string_value
|
|
95
|
+
|
|
96
|
+
encoded = begin
|
|
97
|
+
encode_value(string_value)
|
|
98
|
+
rescue EncodingError
|
|
99
|
+
# A string that cannot be represented as UTF-8 (e.g. binary data) has no header representation;
|
|
100
|
+
# omit it like the other non-representable values.
|
|
101
|
+
next
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
headers["#{HEADER_PREFIX}#{declaration[:header_name]}"] = encoded
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Converts a primitive argument to its header string per the spec's type-conversion rules: strings pass through,
|
|
109
|
+
# booleans become lowercase `"true"` / `"false"`, and numbers become their decimal string.
|
|
110
|
+
# `nil` means "not representable: do not emit a header".
|
|
111
|
+
def primitive_to_string(value)
|
|
112
|
+
case value
|
|
113
|
+
when String
|
|
114
|
+
value
|
|
115
|
+
when true
|
|
116
|
+
"true"
|
|
117
|
+
when false
|
|
118
|
+
"false"
|
|
119
|
+
when Integer
|
|
120
|
+
value.abs <= MAX_SAFE_INTEGER ? value.to_s : nil
|
|
121
|
+
when Float
|
|
122
|
+
return unless value.finite?
|
|
123
|
+
|
|
124
|
+
# JSON has one number type: an integral float serializes without the fractional part,
|
|
125
|
+
# matching the `String(42.0)` the JavaScript reference emits.
|
|
126
|
+
value == value.truncate ? value.truncate.to_s : value.to_s
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Encodes a header value per the spec's value-encoding rules: a safe plain-ASCII field value
|
|
131
|
+
# passes through unchanged, everything else is wrapped as `=?base64?{base64-of-UTF-8}?=`.
|
|
132
|
+
def encode_value(value)
|
|
133
|
+
return value unless needs_base64?(value)
|
|
134
|
+
|
|
135
|
+
"#{BASE64_SENTINEL_PREFIX}#{[value.encode(Encoding::UTF_8)].pack("m0")}#{BASE64_SENTINEL_SUFFIX}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Decodes an `Mcp-Param-{Name}` header value: one carrying the sentinel yields its payload decoded as UTF-8,
|
|
139
|
+
# any other value is taken literally, as the spec's examples show. `nil` marks a value the server has to refuse rather
|
|
140
|
+
# than compare: one holding a byte outside the field-value set (the spec's invalid characters; a conforming client
|
|
141
|
+
# wraps such a value), or a sentinel whose payload is not canonical Base64 (padding, alphabet, trailing bits) or
|
|
142
|
+
# not valid UTF-8, since a lenient decoder would let two different header values stand for one body value.
|
|
143
|
+
def decode_value(value)
|
|
144
|
+
return unless field_value?(value)
|
|
145
|
+
return value unless sentinel_wrapped?(value)
|
|
146
|
+
|
|
147
|
+
payload_size = value.bytesize - BASE64_SENTINEL_PREFIX.bytesize - BASE64_SENTINEL_SUFFIX.bytesize
|
|
148
|
+
decoded = value.byteslice(BASE64_SENTINEL_PREFIX.bytesize, payload_size).unpack1("m0").force_encoding(Encoding::UTF_8)
|
|
149
|
+
decoded.valid_encoding? ? decoded : nil
|
|
150
|
+
rescue ArgumentError
|
|
151
|
+
nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The server side of the spec's table for custom headers: for each declaration, the `Mcp-Param-{Name}` header and
|
|
155
|
+
# the body value at the declared path are present together and agree once the header is decoded, or are absent together.
|
|
156
|
+
# The block is called with each declared header name and returns the raw field value, or `nil` when the request
|
|
157
|
+
# does not carry it. Returns a message naming the first disagreement by header and argument path, never by value,
|
|
158
|
+
# or `nil` when every declaration agrees.
|
|
159
|
+
def mismatch(declarations, arguments)
|
|
160
|
+
declarations.each do |declaration|
|
|
161
|
+
header_name = "#{HEADER_PREFIX}#{declaration[:header_name]}"
|
|
162
|
+
raw = yield(header_name)
|
|
163
|
+
value = value_at_path(arguments, declaration[:path])
|
|
164
|
+
argument = declaration[:path].join(".")
|
|
165
|
+
|
|
166
|
+
if value.nil?
|
|
167
|
+
# The spec has the server expect no header for a null or absent value. One sent anyway claims a value
|
|
168
|
+
# the body does not carry, which is exactly what an intermediary acting on the header would act on.
|
|
169
|
+
return "#{header_name} header is present but the request body carries no `#{argument}` argument" if raw
|
|
170
|
+
|
|
171
|
+
next
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
rendered = primitive_to_string(value)
|
|
175
|
+
if rendered.nil?
|
|
176
|
+
# A value without a header rendering (an object, an array, an integer past the safe range) is one
|
|
177
|
+
# a conforming client omits the header for, so a header claiming it can never agree with it.
|
|
178
|
+
return "#{header_name} header does not match the `#{argument}` argument" if raw
|
|
179
|
+
|
|
180
|
+
next
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
return "#{header_name} header is missing but the request body carries a `#{argument}` argument" if raw.nil?
|
|
184
|
+
return "#{header_name} header holds a character that is not allowed in a field value" unless field_value?(raw)
|
|
185
|
+
|
|
186
|
+
decoded = decode_value(raw)
|
|
187
|
+
return "#{header_name} header carries a malformed Base64 value" if decoded.nil?
|
|
188
|
+
|
|
189
|
+
unless agrees?(declaration[:type], value, rendered, decoded)
|
|
190
|
+
return "#{header_name} header does not match the `#{argument}` argument"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
nil
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
# A decoded header agrees with a body value when it equals the value's header rendering byte for byte, or, for
|
|
200
|
+
# an integer or number declaration with a numeric body value, when it is a canonical decimal of the same number
|
|
201
|
+
# within `MAX_NUMERAL_BYTES`: the spec's SHOULD that `42.0` equals `42`. An integer compares exactly, so a value
|
|
202
|
+
# past the double range still compares; a float compares as the double the header denotes.
|
|
203
|
+
def agrees?(type, value, rendered, decoded)
|
|
204
|
+
return true if decoded.b == rendered.b
|
|
205
|
+
return false unless NUMERIC_TYPES.include?(type) && value.is_a?(Numeric)
|
|
206
|
+
return false unless decoded.bytesize <= MAX_NUMERAL_BYTES && CANONICAL_DECIMAL.match?(decoded)
|
|
207
|
+
|
|
208
|
+
value.is_a?(Integer) ? Rational(decoded) == value : Float(decoded) == value
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def visit(node, path, reachable, declarations, seen_lower)
|
|
212
|
+
return unless node.is_a?(Hash)
|
|
213
|
+
|
|
214
|
+
if key?(node, X_MCP_HEADER_KEY)
|
|
215
|
+
fault = validate_declaration(node, path, reachable, declarations, seen_lower)
|
|
216
|
+
|
|
217
|
+
return fault if fault
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
properties = read(node, "properties")
|
|
221
|
+
if properties.is_a?(Hash)
|
|
222
|
+
properties.each do |key, child|
|
|
223
|
+
fault = visit(child, path + [key.to_s], reachable, declarations, seen_lower)
|
|
224
|
+
|
|
225
|
+
return fault if fault
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# Static-reachability sweep: descend the keywords the `properties` chain MUST NOT pass through with `reachable: false`,
|
|
230
|
+
# so an annotation under any of them is reported. `$defs` covers `$ref`-within-`$defs`; chasing arbitrary `$ref` URIs
|
|
231
|
+
# is out of scope.
|
|
232
|
+
NON_REACHABLE_SUBSCHEMA_KEYWORDS.each do |keyword|
|
|
233
|
+
next unless (sub = read(node, keyword))
|
|
234
|
+
|
|
235
|
+
branches = if sub.is_a?(Array)
|
|
236
|
+
sub
|
|
237
|
+
elsif sub.is_a?(Hash) && OBJECT_VALUED_SUBSCHEMA_KEYWORDS.include?(keyword)
|
|
238
|
+
sub.values
|
|
239
|
+
else
|
|
240
|
+
[sub]
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
branches.each do |branch|
|
|
244
|
+
fault = visit(branch, path + ["<#{keyword}>"], false, declarations, seen_lower)
|
|
245
|
+
|
|
246
|
+
return fault if fault
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
nil
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def validate_declaration(node, path, reachable, declarations, seen_lower)
|
|
254
|
+
if !reachable || path.empty?
|
|
255
|
+
return "#{path_name(path)}: x-mcp-header is only permitted on properties statically reachable via a chain of `properties` keys"
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
annotation = read(node, X_MCP_HEADER_KEY)
|
|
259
|
+
|
|
260
|
+
unless annotation.is_a?(String) && !annotation.empty?
|
|
261
|
+
return "#{path_name(path)}: x-mcp-header MUST be a non-empty string"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
unless RFC9110_TOKEN.match?(annotation)
|
|
265
|
+
return "#{path_name(path)}: x-mcp-header `#{annotation}` is not a valid RFC 9110 token"
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
type = read(node, "type")
|
|
269
|
+
unless type.is_a?(String) && PERMITTED_TYPES.include?(type)
|
|
270
|
+
return "#{path_name(path)}: x-mcp-header is only permitted on primitive-typed properties (got `#{type.inspect}`)"
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
lower = annotation.downcase
|
|
274
|
+
prior = seen_lower[lower]
|
|
275
|
+
if prior
|
|
276
|
+
return "x-mcp-header `#{annotation}` is not case-insensitively unique (also declared as `#{prior}`)"
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
seen_lower[lower] = annotation
|
|
280
|
+
declarations << { path: path, header_name: annotation, type: type }
|
|
281
|
+
nil
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
# A value cannot ride as a plain ASCII field value when it is empty, already shaped like the Base64 sentinel
|
|
285
|
+
# (the spec's ambiguity rule), carries edge whitespace that field parsing would strip, or contains
|
|
286
|
+
# a byte outside visible ASCII plus interior tab.
|
|
287
|
+
def needs_base64?(value)
|
|
288
|
+
return true if value.empty?
|
|
289
|
+
return true if value.start_with?(BASE64_SENTINEL_PREFIX) && value.end_with?(BASE64_SENTINEL_SUFFIX)
|
|
290
|
+
return true if value != value.strip
|
|
291
|
+
|
|
292
|
+
!field_value?(value)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def sentinel_wrapped?(value)
|
|
296
|
+
return false if value.bytesize < BASE64_SENTINEL_PREFIX.bytesize + BASE64_SENTINEL_SUFFIX.bytesize
|
|
297
|
+
|
|
298
|
+
value.start_with?(BASE64_SENTINEL_PREFIX) && value.end_with?(BASE64_SENTINEL_SUFFIX)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# The bytes RFC 9110 lets a field value carry, which the spec's value encoding keeps a plain header within:
|
|
302
|
+
# visible ASCII, space, and horizontal tab. Checked byte by byte, so a value of any encoding is judged the same.
|
|
303
|
+
def field_value?(value)
|
|
304
|
+
value.each_byte.all? { |byte| byte == 0x09 || byte.between?(0x20, 0x7e) }
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def value_at_path(root, path)
|
|
308
|
+
path.reduce(root) do |node, key|
|
|
309
|
+
break unless node.is_a?(Hash)
|
|
310
|
+
|
|
311
|
+
read(node, key)
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Schemas and arguments arrive with string keys off the wire but may carry symbol keys when constructed in Ruby;
|
|
316
|
+
# read both forms like the SDK's other readers.
|
|
317
|
+
def read(hash, key)
|
|
318
|
+
value = hash[key]
|
|
319
|
+
|
|
320
|
+
value.nil? ? hash[key.to_sym] : value
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def key?(hash, key)
|
|
324
|
+
hash.key?(key) || hash.key?(key.to_sym)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def path_name(path)
|
|
328
|
+
path.empty? ? "<root>" : path.join(".")
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
@@ -39,6 +39,15 @@ module MCP
|
|
|
39
39
|
|
|
40
40
|
DEFAULT_ALGORITHMS = ["RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512", "EdDSA"].freeze
|
|
41
41
|
|
|
42
|
+
EC_CURVE_ALGORITHMS = {
|
|
43
|
+
"prime256v1" => "ES256",
|
|
44
|
+
"secp256r1" => "ES256",
|
|
45
|
+
"secp384r1" => "ES384",
|
|
46
|
+
"secp521r1" => "ES512",
|
|
47
|
+
"secp256k1" => "ES256K",
|
|
48
|
+
}.freeze
|
|
49
|
+
private_constant :EC_CURVE_ALGORITHMS
|
|
50
|
+
|
|
42
51
|
HMAC_ALGORITHM_PATTERN = /\AHS\d+\z/i.freeze
|
|
43
52
|
private_constant :HMAC_ALGORITHM_PATTERN
|
|
44
53
|
|
|
@@ -126,12 +135,32 @@ module MCP
|
|
|
126
135
|
leeway: @leeway,
|
|
127
136
|
}
|
|
128
137
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
138
|
+
JWT.decode(token, nil, true, options) do |header|
|
|
139
|
+
raise InvalidTokenError, "Invalid token" unless @key || header["kid"]
|
|
140
|
+
|
|
141
|
+
key = @key || JWT::JWK::KeyFinder.new(jwks: @static_jwks || jwk_loader).key_for(header["kid"])
|
|
142
|
+
keys = Array(key).select { |candidate| algorithm_matches_key?(header["alg"], candidate) }
|
|
143
|
+
raise InvalidTokenError, "Invalid token" if keys.empty?
|
|
144
|
+
|
|
145
|
+
keys
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def algorithm_matches_key?(algorithm, key)
|
|
150
|
+
case key
|
|
151
|
+
when OpenSSL::PKey::RSA
|
|
152
|
+
algorithm.match?(/\A(?:RS|PS)(?:256|384|512)\z/i)
|
|
153
|
+
when OpenSSL::PKey::EC, OpenSSL::PKey::EC::Point
|
|
154
|
+
curve_algorithm = EC_CURVE_ALGORITHMS[key.group.curve_name]
|
|
155
|
+
curve_algorithm && curve_algorithm.casecmp?(algorithm)
|
|
156
|
+
when String
|
|
157
|
+
algorithm.match?(/\AHS(?:256|384|512)\z/i)
|
|
158
|
+
when JWT::JWK::KeyBase
|
|
159
|
+
algorithm_matches_key?(algorithm, key.verify_key)
|
|
133
160
|
else
|
|
134
|
-
|
|
161
|
+
ed25519_key = (defined?(Ed25519::VerifyKey) && key.is_a?(Ed25519::VerifyKey)) ||
|
|
162
|
+
(defined?(RbNaCl::Signatures::Ed25519::VerifyKey) && key.is_a?(RbNaCl::Signatures::Ed25519::VerifyKey))
|
|
163
|
+
ed25519_key && ["EdDSA", "Ed25519"].any? { |name| name.casecmp?(algorithm) }
|
|
135
164
|
end
|
|
136
165
|
end
|
|
137
166
|
|
|
@@ -96,6 +96,18 @@ module MCP
|
|
|
96
96
|
response
|
|
97
97
|
end
|
|
98
98
|
|
|
99
|
+
# Sessions a server-wide log broadcast is offered to, each applying its own level or the server default.
|
|
100
|
+
# Public only so that `Server#notify_log_message` can call it. Custom transports may implement this hook;
|
|
101
|
+
# returning `nil` keeps the server-wide filter.
|
|
102
|
+
#
|
|
103
|
+
# @api private
|
|
104
|
+
# @return [Array<ServerSession>, nil]
|
|
105
|
+
def logging_sessions
|
|
106
|
+
return unless @session
|
|
107
|
+
|
|
108
|
+
@session.era == :legacy ? [@session] : []
|
|
109
|
+
end
|
|
110
|
+
|
|
99
111
|
def send_notification(method, params = nil)
|
|
100
112
|
notification = {
|
|
101
113
|
jsonrpc: "2.0",
|