mcp 1.0.0 → 1.2.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 +428 -9
- data/lib/json_rpc_handler.rb +3 -2
- data/lib/mcp/client/http.rb +338 -63
- data/lib/mcp/client/mcp_param_headers.rb +242 -0
- data/lib/mcp/client/modern_envelope.rb +32 -0
- data/lib/mcp/client/oauth/discovery.rb +108 -14
- data/lib/mcp/client/oauth/flow.rb +95 -7
- data/lib/mcp/client/stdio.rb +243 -66
- data/lib/mcp/client/tool.rb +3 -2
- data/lib/mcp/client.rb +364 -41
- data/lib/mcp/configuration.rb +84 -7
- data/lib/mcp/elicitation/enum_schema.rb +121 -0
- data/lib/mcp/elicitation.rb +10 -0
- data/lib/mcp/error_codes.rb +14 -8
- data/lib/mcp/instrumentation.rb +6 -0
- data/lib/mcp/methods.rb +21 -0
- data/lib/mcp/prompt.rb +8 -1
- data/lib/mcp/protocol_deprecations.rb +61 -0
- data/lib/mcp/request_envelope.rb +117 -0
- data/lib/mcp/server/input_required_result.rb +163 -0
- data/lib/mcp/server/pending_response.rb +62 -0
- data/lib/mcp/server/request_state_security.rb +131 -0
- data/lib/mcp/server/transports/stdio_transport.rb +39 -2
- data/lib/mcp/server/transports/streamable_http_transport.rb +710 -20
- data/lib/mcp/server.rb +541 -43
- data/lib/mcp/server_context.rb +92 -5
- data/lib/mcp/server_session.rb +77 -15
- data/lib/mcp/tool/response.rb +8 -2
- data/lib/mcp/transport.rb +7 -0
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +3 -0
- metadata +11 -2
data/lib/mcp/configuration.rb
CHANGED
|
@@ -2,12 +2,49 @@
|
|
|
2
2
|
|
|
3
3
|
module MCP
|
|
4
4
|
class Configuration
|
|
5
|
-
LATEST_STABLE_PROTOCOL_VERSION = "
|
|
5
|
+
LATEST_STABLE_PROTOCOL_VERSION = "2026-07-28"
|
|
6
|
+
ROOTS_SAMPLING_LOGGING_DEPRECATED_PROTOCOL_VERSION = "2026-07-28"
|
|
6
7
|
SUPPORTED_STABLE_PROTOCOL_VERSIONS = [
|
|
7
|
-
LATEST_STABLE_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05",
|
|
8
|
+
LATEST_STABLE_PROTOCOL_VERSION, "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05",
|
|
8
9
|
].freeze
|
|
9
10
|
DEFAULT_NEGOTIATED_PROTOCOL_VERSION = "2025-03-26"
|
|
10
11
|
|
|
12
|
+
# Protocol versions of the stateless "modern" lifecycle introduced by the MCP 2026-07-28 spec release (SEP-2575),
|
|
13
|
+
# where each request carries its own version in `_meta` and is validated against this list independently,
|
|
14
|
+
# with no handshake. These are reachable only through `server/discover` and the per-request envelope.
|
|
15
|
+
# https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575
|
|
16
|
+
LATEST_MODERN_PROTOCOL_VERSION = "2026-07-28"
|
|
17
|
+
SUPPORTED_MODERN_PROTOCOL_VERSIONS = [LATEST_MODERN_PROTOCOL_VERSION].freeze
|
|
18
|
+
|
|
19
|
+
# Protocol versions reachable through the legacy `initialize` handshake, derived so the era partition
|
|
20
|
+
# (handshake = stable minus modern) cannot drift when a new revision lands.
|
|
21
|
+
# Per the SEP-2575 era model, an era is a property of the protocol version itself: legacy versions establish
|
|
22
|
+
# a session via `initialize` (2025-11-25 and earlier), and modern versions carry the version on every request
|
|
23
|
+
# in `_meta` with no handshake at all. The handshake therefore never negotiates a modern version:
|
|
24
|
+
# a client asking `initialize` for one is counter-offered
|
|
25
|
+
# `LATEST_HANDSHAKE_PROTOCOL_VERSION`, matching the TypeScript and Python SDKs.
|
|
26
|
+
SUPPORTED_HANDSHAKE_PROTOCOL_VERSIONS = (SUPPORTED_STABLE_PROTOCOL_VERSIONS - SUPPORTED_MODERN_PROTOCOL_VERSIONS).freeze
|
|
27
|
+
LATEST_HANDSHAKE_PROTOCOL_VERSION = SUPPORTED_HANDSHAKE_PROTOCOL_VERSIONS.first
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
def modern_protocol_version?(version)
|
|
31
|
+
SUPPORTED_MODERN_PROTOCOL_VERSIONS.include?(version)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def handshake_protocol_version?(version)
|
|
35
|
+
SUPPORTED_HANDSHAKE_PROTOCOL_VERSIONS.include?(version)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The one statement of the client-side handshake contract, shared by every transport:
|
|
39
|
+
# a modern version cannot ride the legacy `initialize` handshake.
|
|
40
|
+
def reject_modern_handshake_version!(version)
|
|
41
|
+
return unless version && modern_protocol_version?(version)
|
|
42
|
+
|
|
43
|
+
raise ArgumentError, "protocol version #{version.inspect} cannot be negotiated through the legacy " \
|
|
44
|
+
"`initialize` handshake; use `mode: :modern` (or `:auto`) instead"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
11
48
|
attr_writer :exception_reporter, :around_request
|
|
12
49
|
|
|
13
50
|
# @deprecated Use {#around_request=} instead. `instrumentation_callback`
|
|
@@ -17,7 +54,7 @@ module MCP
|
|
|
17
54
|
attr_writer :instrumentation_callback
|
|
18
55
|
|
|
19
56
|
def initialize(exception_reporter: nil, around_request: nil, instrumentation_callback: nil, protocol_version: nil,
|
|
20
|
-
validate_tool_call_arguments: true, validate_tool_call_results: false)
|
|
57
|
+
validate_tool_call_arguments: true, validate_tool_call_results: false, instrument_server_context: false)
|
|
21
58
|
@exception_reporter = exception_reporter
|
|
22
59
|
@around_request = around_request
|
|
23
60
|
@instrumentation_callback = instrumentation_callback
|
|
@@ -27,9 +64,11 @@ module MCP
|
|
|
27
64
|
end
|
|
28
65
|
validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
|
|
29
66
|
validate_value_of_validate_tool_call_results!(validate_tool_call_results)
|
|
67
|
+
validate_value_of_instrument_server_context!(instrument_server_context)
|
|
30
68
|
|
|
31
69
|
@validate_tool_call_arguments = validate_tool_call_arguments
|
|
32
70
|
@validate_tool_call_results = validate_tool_call_results
|
|
71
|
+
@instrument_server_context = instrument_server_context
|
|
33
72
|
end
|
|
34
73
|
|
|
35
74
|
def protocol_version=(protocol_version)
|
|
@@ -44,14 +83,28 @@ module MCP
|
|
|
44
83
|
@validate_tool_call_arguments = validate_tool_call_arguments
|
|
45
84
|
end
|
|
46
85
|
|
|
86
|
+
# Opt in to exposing the user-defined `server_context` in the
|
|
87
|
+
# `around_request` / `instrumentation_callback` data hash. Off by default:
|
|
88
|
+
# the hash is application-supplied and may hold values a tracing backend
|
|
89
|
+
# should not receive, so surfacing it has to be a deliberate choice.
|
|
90
|
+
def instrument_server_context=(instrument_server_context)
|
|
91
|
+
validate_value_of_instrument_server_context!(instrument_server_context)
|
|
92
|
+
|
|
93
|
+
@instrument_server_context = instrument_server_context
|
|
94
|
+
end
|
|
95
|
+
|
|
47
96
|
def validate_tool_call_results=(validate_tool_call_results)
|
|
48
97
|
validate_value_of_validate_tool_call_results!(validate_tool_call_results)
|
|
49
98
|
|
|
50
99
|
@validate_tool_call_results = validate_tool_call_results
|
|
51
100
|
end
|
|
52
101
|
|
|
102
|
+
# The pin scopes the `initialize` handshake, so an unset pin reads as the version that handshake
|
|
103
|
+
# settles on by default. Reading the newest version of any era here would hand back a value
|
|
104
|
+
# the writer rejects, and no caller wants the modern revision: a modern connection carries
|
|
105
|
+
# its version on every request instead of consulting configuration.
|
|
53
106
|
def protocol_version
|
|
54
|
-
@protocol_version ||
|
|
107
|
+
@protocol_version || LATEST_HANDSHAKE_PROTOCOL_VERSION
|
|
55
108
|
end
|
|
56
109
|
|
|
57
110
|
def protocol_version?
|
|
@@ -91,6 +144,10 @@ module MCP
|
|
|
91
144
|
attr_reader :validate_tool_call_arguments
|
|
92
145
|
attr_reader :validate_tool_call_results
|
|
93
146
|
|
|
147
|
+
def instrument_server_context?
|
|
148
|
+
!!@instrument_server_context
|
|
149
|
+
end
|
|
150
|
+
|
|
94
151
|
def validate_tool_call_arguments?
|
|
95
152
|
!!@validate_tool_call_arguments
|
|
96
153
|
end
|
|
@@ -136,16 +193,30 @@ module MCP
|
|
|
136
193
|
protocol_version: protocol_version,
|
|
137
194
|
validate_tool_call_arguments: validate_tool_call_arguments,
|
|
138
195
|
validate_tool_call_results: validate_tool_call_results,
|
|
196
|
+
instrument_server_context: other.instrument_server_context?,
|
|
139
197
|
)
|
|
140
198
|
end
|
|
141
199
|
|
|
142
200
|
private
|
|
143
201
|
|
|
202
|
+
# A pin scopes the `initialize` handshake, so only handshake versions are accepted:
|
|
203
|
+
# a modern version has no handshake to pin (its version rides every request in `_meta`),
|
|
204
|
+
# and accepting one here would configure nothing. Failing at construction beats
|
|
205
|
+
# a setting that silently does not apply.
|
|
144
206
|
def validate_protocol_version!(protocol_version)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
207
|
+
return if SUPPORTED_HANDSHAKE_PROTOCOL_VERSIONS.include?(protocol_version)
|
|
208
|
+
|
|
209
|
+
# A version this SDK serves, rejected only for where it was set, deserves the reason and
|
|
210
|
+
# the alternative rather than a list it is missing from: this is the error an upgrade from
|
|
211
|
+
# a release that accepted the value lands on, so it doubles as the migration note.
|
|
212
|
+
if self.class.modern_protocol_version?(protocol_version)
|
|
213
|
+
raise ArgumentError, "protocol_version #{protocol_version.inspect} is a modern protocol version and cannot be pinned here: " \
|
|
214
|
+
"the pin scopes the `initialize` handshake, which never negotiates a modern version. " \
|
|
215
|
+
"Modern clients carry their version on every request and need no pin; remove the setting."
|
|
148
216
|
end
|
|
217
|
+
|
|
218
|
+
raise ArgumentError, "protocol_version must be #{SUPPORTED_HANDSHAKE_PROTOCOL_VERSIONS[0...-1].join(", ")}, " \
|
|
219
|
+
"or #{SUPPORTED_HANDSHAKE_PROTOCOL_VERSIONS[-1]}"
|
|
149
220
|
end
|
|
150
221
|
|
|
151
222
|
def validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
|
|
@@ -160,6 +231,12 @@ module MCP
|
|
|
160
231
|
end
|
|
161
232
|
end
|
|
162
233
|
|
|
234
|
+
def validate_value_of_instrument_server_context!(instrument_server_context)
|
|
235
|
+
unless instrument_server_context.is_a?(TrueClass) || instrument_server_context.is_a?(FalseClass)
|
|
236
|
+
raise ArgumentError, "instrument_server_context must be a boolean"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
163
240
|
def default_exception_reporter
|
|
164
241
|
@default_exception_reporter ||= ->(exception, server_context) {}
|
|
165
242
|
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
module Elicitation
|
|
5
|
+
# Builds the four enum schema variants defined by MCP 2025-11-25 (SEP-1330) plus the legacy `enumNames` form
|
|
6
|
+
# retained for backward compatibility.
|
|
7
|
+
#
|
|
8
|
+
# Each class method returns an `EnumSchema` instance; call `to_h` to get the property-value Hash and
|
|
9
|
+
# pass it through `requested_schema` to `ServerSession#create_form_elicitation`.
|
|
10
|
+
class EnumSchema
|
|
11
|
+
class << self
|
|
12
|
+
# Single-select with plain string values: `{ type: "string", enum: [...] }`.
|
|
13
|
+
def untitled_single_select(values:, default: nil, title: nil, description: nil)
|
|
14
|
+
validate_values!(values)
|
|
15
|
+
|
|
16
|
+
new(
|
|
17
|
+
type: "string",
|
|
18
|
+
extras: { enum: values },
|
|
19
|
+
default: default,
|
|
20
|
+
title: title,
|
|
21
|
+
description: description,
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Single-select with display titles per option: `{ type: "string", oneOf: [{ const, title }, ...] }`.
|
|
26
|
+
# `options` is an Array of `{ value:, title: }` hashes.
|
|
27
|
+
def titled_single_select(options:, default: nil, title: nil, description: nil)
|
|
28
|
+
validate_titled_options!(options)
|
|
29
|
+
|
|
30
|
+
new(
|
|
31
|
+
type: "string",
|
|
32
|
+
extras: { oneOf: options.map { |o| { const: o[:value], title: o[:title] } } },
|
|
33
|
+
default: default,
|
|
34
|
+
title: title,
|
|
35
|
+
description: description,
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Multi-select with plain string values: `{ type: "array", items: { type: "string", enum: [...] } }`.
|
|
40
|
+
def untitled_multi_select(values:, default: nil, title: nil, description: nil)
|
|
41
|
+
validate_values!(values)
|
|
42
|
+
|
|
43
|
+
new(
|
|
44
|
+
type: "array",
|
|
45
|
+
extras: { items: { type: "string", enum: values } },
|
|
46
|
+
default: default,
|
|
47
|
+
title: title,
|
|
48
|
+
description: description,
|
|
49
|
+
)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Multi-select with display titles per option: `{ type: "array", items: { anyOf: [{ const, title }, ...] } }`.
|
|
53
|
+
def titled_multi_select(options:, default: nil, title: nil, description: nil)
|
|
54
|
+
validate_titled_options!(options)
|
|
55
|
+
|
|
56
|
+
items_anyof = options.map { |o| { const: o[:value], title: o[:title] } }
|
|
57
|
+
new(
|
|
58
|
+
type: "array",
|
|
59
|
+
extras: { items: { anyOf: items_anyof } },
|
|
60
|
+
default: default,
|
|
61
|
+
title: title,
|
|
62
|
+
description: description,
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Legacy single-select retained for backward compatibility with clients implementing
|
|
67
|
+
# the pre-SEP-1330 form: `{ enum, enumNames }`.
|
|
68
|
+
def legacy_titled(values:, value_titles:, default: nil, title: nil, description: nil)
|
|
69
|
+
validate_values!(values)
|
|
70
|
+
unless value_titles.is_a?(Array) && value_titles.length == values.length
|
|
71
|
+
raise ArgumentError, "value_titles must be an Array of the same length as values"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
new(
|
|
75
|
+
type: "string",
|
|
76
|
+
extras: { enum: values, enumNames: value_titles },
|
|
77
|
+
default: default,
|
|
78
|
+
title: title,
|
|
79
|
+
description: description,
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private
|
|
84
|
+
|
|
85
|
+
def validate_values!(values)
|
|
86
|
+
unless values.is_a?(Array) && !values.empty?
|
|
87
|
+
raise ArgumentError, "values must be a non-empty Array"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def validate_titled_options!(options)
|
|
92
|
+
unless options.is_a?(Array) && !options.empty?
|
|
93
|
+
raise ArgumentError, "options must be a non-empty Array of {value:, title:} hashes"
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
options.each do |option|
|
|
97
|
+
unless option.is_a?(Hash) && option.key?(:value) && option.key?(:title)
|
|
98
|
+
raise ArgumentError, "each option must be a Hash with :value and :title keys"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def initialize(type:, extras:, default: nil, title: nil, description: nil)
|
|
105
|
+
@type = type
|
|
106
|
+
@extras = extras
|
|
107
|
+
@default = default
|
|
108
|
+
@title = title
|
|
109
|
+
@description = description
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def to_h
|
|
113
|
+
hash = { type: @type }.merge(@extras)
|
|
114
|
+
hash[:title] = @title if @title
|
|
115
|
+
hash[:description] = @description if @description
|
|
116
|
+
hash[:default] = @default unless @default.nil?
|
|
117
|
+
hash
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
# Builders for elicitation `requestedSchema` definitions per MCP 2025-11-25.
|
|
5
|
+
# Each builder returns an instance whose `to_h` produces the JSON-Schema-shaped Hash
|
|
6
|
+
# a server passes as a property value in `create_form_elicitation(requested_schema:)`.
|
|
7
|
+
module Elicitation
|
|
8
|
+
autoload :EnumSchema, "mcp/elicitation/enum_schema"
|
|
9
|
+
end
|
|
10
|
+
end
|
data/lib/mcp/error_codes.rb
CHANGED
|
@@ -3,18 +3,24 @@
|
|
|
3
3
|
module MCP
|
|
4
4
|
# MCP-specific JSON-RPC error codes, complementing the generic codes in `JsonRpcHandler::ErrorCode`.
|
|
5
5
|
#
|
|
6
|
-
#
|
|
7
|
-
# `UNSUPPORTED_PROTOCOL_VERSION` rejects a request whose `_meta`-carried protocol version the server does not
|
|
8
|
-
# support (`error.data: { supported: [...], requested: "..." }`), and `MISSING_REQUIRED_CLIENT_CAPABILITY`
|
|
9
|
-
# rejects a request that requires a client capability the request did not declare
|
|
10
|
-
# (`error.data: { requiredCapabilities: {...} }`). The SDK exports the vocabulary; it does not raise
|
|
11
|
-
# these codes itself yet.
|
|
6
|
+
# All three constants below are introduced by the stateless lifecycle of the MCP 2026-07-28 draft (SEP-2575):
|
|
12
7
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
8
|
+
# - `HEADER_MISMATCH` rejects an HTTP request whose headers do not match the corresponding body values,
|
|
9
|
+
# or whose required headers are missing or malformed (no `error.data`). It is reserved for
|
|
10
|
+
# the Streamable HTTP transport, since headers do not exist on stdio.
|
|
11
|
+
# - `MISSING_REQUIRED_CLIENT_CAPABILITY` rejects a request that requires a client capability
|
|
12
|
+
# the request did not declare (`error.data: { requiredCapabilities: {...} }`).
|
|
13
|
+
# Raised via `Server::MissingRequiredClientCapabilityError`.
|
|
14
|
+
# - `UNSUPPORTED_PROTOCOL_VERSION` rejects a request whose `_meta`-carried protocol version the server
|
|
15
|
+
# does not support (`error.data: { supported: [...], requested: "..." }`). Raised via
|
|
16
|
+
# `Server::UnsupportedProtocolVersionError`.
|
|
17
|
+
#
|
|
18
|
+
# The values come from the spec's MCP-specific error code block, which is allocated sequentially from `-32020`
|
|
19
|
+
# toward `-32099`.
|
|
15
20
|
#
|
|
16
21
|
# https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575
|
|
17
22
|
module ErrorCodes
|
|
23
|
+
HEADER_MISMATCH = -32020
|
|
18
24
|
MISSING_REQUIRED_CLIENT_CAPABILITY = -32021
|
|
19
25
|
UNSUPPORTED_PROTOCOL_VERSION = -32022
|
|
20
26
|
end
|
data/lib/mcp/instrumentation.rb
CHANGED
|
@@ -7,6 +7,12 @@ module MCP
|
|
|
7
7
|
begin
|
|
8
8
|
@instrumentation_data = {}
|
|
9
9
|
add_instrumentation_data(method: method)
|
|
10
|
+
# `self.` is required: the `server_context:` keyword above shadows the
|
|
11
|
+
# reader, and the value we want here is the user-defined hash passed to
|
|
12
|
+
# `Server.new`, not the per-call reporter context.
|
|
13
|
+
if configuration.instrument_server_context? && respond_to?(:server_context)
|
|
14
|
+
add_instrumentation_data(server_context: self.server_context)
|
|
15
|
+
end
|
|
10
16
|
|
|
11
17
|
result = configuration.around_request.call(@instrumentation_data, &block)
|
|
12
18
|
|
data/lib/mcp/methods.rb
CHANGED
|
@@ -7,6 +7,10 @@ module MCP
|
|
|
7
7
|
LOGGING_SET_LEVEL = "logging/setLevel"
|
|
8
8
|
# Sessionless capability discovery (MCP 2026-07-28 draft, SEP-2575).
|
|
9
9
|
SERVER_DISCOVER = "server/discover"
|
|
10
|
+
# Long-lived notification subscription stream (MCP 2026-07-28, SEP-2575),
|
|
11
|
+
# replacing the legacy HTTP GET listening stream. Served at the transport layer
|
|
12
|
+
# (Streamable HTTP modern path); transports without streaming support answer `-32601`.
|
|
13
|
+
SUBSCRIPTIONS_LISTEN = "subscriptions/listen"
|
|
10
14
|
|
|
11
15
|
PROMPTS_GET = "prompts/get"
|
|
12
16
|
PROMPTS_LIST = "prompts/list"
|
|
@@ -21,6 +25,20 @@ module MCP
|
|
|
21
25
|
TOOLS_CALL = "tools/call"
|
|
22
26
|
TOOLS_LIST = "tools/list"
|
|
23
27
|
|
|
28
|
+
# RPC methods the stateless modern lifecycle removes (MCP 2026-07-28, SEP-2575):
|
|
29
|
+
# `initialize` is replaced by the per-request `_meta` envelope plus `server/discover`,
|
|
30
|
+
# `logging/setLevel` by the envelope's `logLevel` member, and `ping` and the resource
|
|
31
|
+
# subscription pair by the connectionless model, which leaves nothing to keep alive
|
|
32
|
+
# or subscribe on. A modern-era request naming one of these answers with `-32601`
|
|
33
|
+
# Method not found (HTTP 404 on Streamable HTTP).
|
|
34
|
+
MODERN_REMOVED_METHODS = [
|
|
35
|
+
INITIALIZE,
|
|
36
|
+
PING,
|
|
37
|
+
LOGGING_SET_LEVEL,
|
|
38
|
+
RESOURCES_SUBSCRIBE,
|
|
39
|
+
RESOURCES_UNSUBSCRIBE,
|
|
40
|
+
].freeze
|
|
41
|
+
|
|
24
42
|
ROOTS_LIST = "roots/list"
|
|
25
43
|
SAMPLING_CREATE_MESSAGE = "sampling/createMessage"
|
|
26
44
|
ELICITATION_CREATE = "elicitation/create"
|
|
@@ -36,6 +54,9 @@ module MCP
|
|
|
36
54
|
NOTIFICATIONS_PROGRESS = "notifications/progress"
|
|
37
55
|
NOTIFICATIONS_CANCELLED = "notifications/cancelled"
|
|
38
56
|
NOTIFICATIONS_ELICITATION_COMPLETE = "notifications/elicitation/complete"
|
|
57
|
+
# First message on a `subscriptions/listen` stream (SEP-2575): reports the subset
|
|
58
|
+
# of requested notification types the server agreed to honor.
|
|
59
|
+
NOTIFICATIONS_SUBSCRIPTIONS_ACKNOWLEDGED = "notifications/subscriptions/acknowledged"
|
|
39
60
|
|
|
40
61
|
class MissingRequiredCapabilityError < StandardError
|
|
41
62
|
attr_reader :method
|
data/lib/mcp/prompt.rb
CHANGED
|
@@ -111,8 +111,15 @@ module MCP
|
|
|
111
111
|
missing = required_args - args.keys
|
|
112
112
|
return if missing.empty?
|
|
113
113
|
|
|
114
|
+
# The explicit `error_code` maps a missing prompt argument to Invalid Params (-32602) rather
|
|
115
|
+
# than the default Internal Error (-32603); a missing required argument is client input, not a
|
|
116
|
+
# server fault. `error_type: :missing_required_arguments` keeps the descriptive message and
|
|
117
|
+
# instrumentation label. Mirrors `MCP::Server::ResourceNotFoundError`.
|
|
114
118
|
raise MCP::Server::RequestHandlerError.new(
|
|
115
|
-
"Missing required arguments: #{missing.join(", ")}",
|
|
119
|
+
"Missing required arguments: #{missing.join(", ")}",
|
|
120
|
+
nil,
|
|
121
|
+
error_type: :missing_required_arguments,
|
|
122
|
+
error_code: JsonRpcHandler::ErrorCode::INVALID_PARAMS,
|
|
116
123
|
)
|
|
117
124
|
end
|
|
118
125
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "configuration"
|
|
4
|
+
|
|
5
|
+
module MCP
|
|
6
|
+
# Warning texts for the features SEP-2577 deprecates at 2026-07-28.
|
|
7
|
+
#
|
|
8
|
+
# Only the client emits these, from the modern connect, where the capabilities it declares are
|
|
9
|
+
# the ones being deprecated. No server-side trigger remains: the `initialize` handshake never lands on
|
|
10
|
+
# a deprecating revision and `Configuration` rejects a modern pin, so nothing on that side can reach
|
|
11
|
+
# a version these apply to. `LOGGING_MESSAGE` in particular has no caller left
|
|
12
|
+
# (a modern client declares no logging capability, and `notify_log_message` on that wire is
|
|
13
|
+
# the SEP-2575 sanctioned delivery path rather than a deprecated call). It stays public and
|
|
14
|
+
# callable for embedders, and because deleting it would be a breaking change for no gain.
|
|
15
|
+
module ProtocolDeprecations
|
|
16
|
+
extend self
|
|
17
|
+
|
|
18
|
+
ROOTS_MESSAGE =
|
|
19
|
+
"MCP Roots (`roots/list` and `notifications/roots/list_changed`) is deprecated as of protocol version " \
|
|
20
|
+
"2026-07-28 (SEP-2577). Use tool parameters, resource URIs, server configuration, or environment " \
|
|
21
|
+
"variables instead."
|
|
22
|
+
SAMPLING_MESSAGE =
|
|
23
|
+
"MCP Sampling (`sampling/createMessage`) is deprecated as of protocol version 2026-07-28 (SEP-2577). " \
|
|
24
|
+
"Use direct LLM provider APIs instead."
|
|
25
|
+
LOGGING_MESSAGE =
|
|
26
|
+
"MCP Logging (`logging/setLevel` and `notifications/message`) is deprecated as of protocol version " \
|
|
27
|
+
"2026-07-28 (SEP-2577). Use stderr or OpenTelemetry instead."
|
|
28
|
+
|
|
29
|
+
MESSAGES = {
|
|
30
|
+
roots: ROOTS_MESSAGE,
|
|
31
|
+
sampling: SAMPLING_MESSAGE,
|
|
32
|
+
logging: LOGGING_MESSAGE,
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
def deprecated_roots_sampling_logging?(protocol_version)
|
|
36
|
+
return false unless protocol_version
|
|
37
|
+
|
|
38
|
+
protocol_version >= Configuration::ROOTS_SAMPLING_LOGGING_DEPRECATED_PROTOCOL_VERSION
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def warn_for(feature, protocol_version:, uplevel: 1)
|
|
42
|
+
return unless deprecated_roots_sampling_logging?(protocol_version)
|
|
43
|
+
|
|
44
|
+
Kernel.warn(MESSAGES.fetch(feature), uplevel: uplevel)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def warn_for_client_capabilities(capabilities, protocol_version:, uplevel: 1)
|
|
48
|
+
return unless deprecated_roots_sampling_logging?(protocol_version)
|
|
49
|
+
return unless capabilities
|
|
50
|
+
|
|
51
|
+
warn_for(:roots, protocol_version: protocol_version, uplevel: uplevel) if capability?(capabilities, :roots)
|
|
52
|
+
warn_for(:sampling, protocol_version: protocol_version, uplevel: uplevel) if capability?(capabilities, :sampling)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def capability?(capabilities, key)
|
|
58
|
+
capabilities.key?(key) || capabilities.key?(key.to_s)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
# The per-request `_meta` envelope of the stateless "modern" lifecycle (MCP 2026-07-28, SEP-2575).
|
|
5
|
+
# The modern lifecycle has no `initialize` handshake: every request identifies its protocol version
|
|
6
|
+
# and client capabilities through reserved `_meta` keys (plus an optional client identity),
|
|
7
|
+
# and the server validates each request independently. Servers MUST NOT infer capabilities from
|
|
8
|
+
# prior requests, which is why the envelope is a per-request value object rather than session state.
|
|
9
|
+
#
|
|
10
|
+
# https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575
|
|
11
|
+
class RequestEnvelope
|
|
12
|
+
PROTOCOL_VERSION_META_KEY = "io.modelcontextprotocol/protocolVersion"
|
|
13
|
+
CLIENT_INFO_META_KEY = "io.modelcontextprotocol/clientInfo"
|
|
14
|
+
CLIENT_CAPABILITIES_META_KEY = "io.modelcontextprotocol/clientCapabilities"
|
|
15
|
+
|
|
16
|
+
# Optional per-request log level, replacing the `logging/setLevel` RPC in the modern lifecycle.
|
|
17
|
+
# Deprecated as of 2026-07-28 (SEP-2577) but still part of the wire format.
|
|
18
|
+
LOG_LEVEL_META_KEY = "io.modelcontextprotocol/logLevel"
|
|
19
|
+
# Notification-side reserved key (SEP-2575): correlates a notification delivered on
|
|
20
|
+
# a `subscriptions/listen` stream (and the stream's closing result) with the JSON-RPC id of
|
|
21
|
+
# the `subscriptions/listen` request that opened it. Not part of the request envelope triple.
|
|
22
|
+
SUBSCRIPTION_ID_META_KEY = "io.modelcontextprotocol/subscriptionId"
|
|
23
|
+
|
|
24
|
+
# Result-side counterpart of the request envelope: the server's identity rides in
|
|
25
|
+
# the result's `_meta` as an optional stamp, not as a top-level field, since the SEP was
|
|
26
|
+
# finalized (spec PR modelcontextprotocol/modelcontextprotocol#3002). A server MAY omit it.
|
|
27
|
+
SERVER_INFO_META_KEY = "io.modelcontextprotocol/serverInfo"
|
|
28
|
+
|
|
29
|
+
# `clientInfo` is deliberately absent: it became optional after the SEP was finalized
|
|
30
|
+
# (spec PR modelcontextprotocol/modelcontextprotocol#3002), so servers MUST accept
|
|
31
|
+
# envelopes without it. The TypeScript and Python SDKs validate the same required pair.
|
|
32
|
+
REQUIRED_META_KEYS = [
|
|
33
|
+
PROTOCOL_VERSION_META_KEY,
|
|
34
|
+
CLIENT_CAPABILITIES_META_KEY,
|
|
35
|
+
].freeze
|
|
36
|
+
|
|
37
|
+
class << self
|
|
38
|
+
# A request claims the modern lifecycle when its `_meta` carries `io.modelcontextprotocol/protocolVersion`,
|
|
39
|
+
# matching the TypeScript SDK's envelope claim and the Python SDK's `_has_modern_envelope`.
|
|
40
|
+
# Classification is deliberately looser than validation: a claimed-but-malformed envelope is
|
|
41
|
+
# rejected by {parse!} with `-32602` instead of silently flowing through the legacy path,
|
|
42
|
+
# while `_meta` without the claim key (`progressToken`, trace context) stays legacy.
|
|
43
|
+
def modern?(params)
|
|
44
|
+
meta = extract_meta(params)
|
|
45
|
+
return false unless meta.is_a?(Hash)
|
|
46
|
+
|
|
47
|
+
!read(meta, PROTOCOL_VERSION_META_KEY).nil?
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Parses and validates the envelope: `protocolVersion` and `clientCapabilities` are required,
|
|
51
|
+
# `clientInfo` is optional. A missing or mistyped field is Invalid params (`-32602`) naming
|
|
52
|
+
# the offending keys, the code and shape the spec mandates and the reference SDKs emit.
|
|
53
|
+
# `request` is only used to enrich the raised error; callers dispatching notifications can omit it.
|
|
54
|
+
def parse!(params, request: nil)
|
|
55
|
+
meta = extract_meta(params)
|
|
56
|
+
meta = {} unless meta.is_a?(Hash)
|
|
57
|
+
|
|
58
|
+
protocol_version = read(meta, PROTOCOL_VERSION_META_KEY)
|
|
59
|
+
client_info = read(meta, CLIENT_INFO_META_KEY)
|
|
60
|
+
client_capabilities = read(meta, CLIENT_CAPABILITIES_META_KEY)
|
|
61
|
+
|
|
62
|
+
invalid_keys = []
|
|
63
|
+
invalid_keys << PROTOCOL_VERSION_META_KEY unless protocol_version.is_a?(String)
|
|
64
|
+
invalid_keys << CLIENT_CAPABILITIES_META_KEY unless client_capabilities.is_a?(Hash)
|
|
65
|
+
invalid_keys << CLIENT_INFO_META_KEY unless client_info.nil? || client_info.is_a?(Hash)
|
|
66
|
+
|
|
67
|
+
unless invalid_keys.empty?
|
|
68
|
+
raise Server::RequestHandlerError.new(
|
|
69
|
+
"Invalid params: missing or invalid `#{invalid_keys.join("`, `")}` in `_meta`",
|
|
70
|
+
request,
|
|
71
|
+
error_type: :invalid_params,
|
|
72
|
+
error_code: JsonRpcHandler::ErrorCode::INVALID_PARAMS,
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
unless Configuration.modern_protocol_version?(protocol_version)
|
|
77
|
+
raise Server::UnsupportedProtocolVersionError.new(protocol_version, request)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
new(
|
|
81
|
+
protocol_version: protocol_version,
|
|
82
|
+
client_info: client_info,
|
|
83
|
+
client_capabilities: client_capabilities,
|
|
84
|
+
log_level: read(meta, LOG_LEVEL_META_KEY),
|
|
85
|
+
)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
private
|
|
89
|
+
|
|
90
|
+
# `Server#handle` accepts hashes parsed with either symbol or string keys, so read both forms
|
|
91
|
+
# (the same tolerance as `Server#handle_cancelled_notification`).
|
|
92
|
+
def extract_meta(params)
|
|
93
|
+
return unless params.is_a?(Hash)
|
|
94
|
+
|
|
95
|
+
meta = params[:_meta]
|
|
96
|
+
meta.nil? ? params["_meta"] : meta
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def read(meta, key)
|
|
100
|
+
value = meta[key.to_sym]
|
|
101
|
+
value.nil? ? meta[key] : value
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# `client_info` is `nil` when the client chose not to identify itself, which is legal:
|
|
106
|
+
# it is self-reported data and MUST NOT drive behavior or security decisions anyway.
|
|
107
|
+
attr_reader :protocol_version, :client_info, :client_capabilities, :log_level
|
|
108
|
+
|
|
109
|
+
def initialize(protocol_version:, client_capabilities:, client_info: nil, log_level: nil)
|
|
110
|
+
@protocol_version = protocol_version
|
|
111
|
+
@client_info = client_info
|
|
112
|
+
@client_capabilities = client_capabilities
|
|
113
|
+
@log_level = log_level
|
|
114
|
+
freeze
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|