Merge pull request #489 from koic/enforce_modern_request_admission

Enforce the modern lifecycle admission rules per SEP-2575
This commit is contained in:
Koichi ITO 2026-08-09 03:34:29 +09:00 • committed by GitHub
commit b98b783a5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 144 additions and 39 deletions

View file

@ -21,6 +21,20 @@ module MCP
TOOLS_CALL = "tools/call"
TOOLS_LIST = "tools/list"
# RPC methods the stateless modern lifecycle removes (MCP 2026-07-28, SEP-2575):
# `initialize` is replaced by the per-request `_meta` envelope plus `server/discover`,
# `logging/setLevel` by the envelope's `logLevel` member, and `ping` and the resource
# subscription pair by the connectionless model, which leaves nothing to keep alive
# or subscribe on. A modern-era request naming one of these answers with `-32601`
# Method not found (HTTP 404 on Streamable HTTP).
MODERN_REMOVED_METHODS = [
INITIALIZE,
PING,
LOGGING_SET_LEVEL,
RESOURCES_SUBSCRIBE,
RESOURCES_UNSUBSCRIBE,
].freeze
ROOTS_LIST = "roots/list"
SAMPLING_CREATE_MESSAGE = "sampling/createMessage"
ELICITATION_CREATE = "elicitation/create"

View file

@ -561,6 +561,18 @@ module MCP
return
end
# SEP-2575 removes these RPCs from the modern lifecycle, so they answer with Method not found
# there regardless of the envelope or the server's declared capabilities: in this era the method
# does not exist at all, which is why the check precedes `ensure_capability!`.
if session.respond_to?(:era) && session.era == :modern && Methods::MODERN_REMOVED_METHODS.include?(method)
raise RequestHandlerError.new(
"Method not found: #{method} is not part of the modern lifecycle (SEP-2575)",
request,
error_type: :method_not_found,
error_code: JsonRpcHandler::ErrorCode::METHOD_NOT_FOUND,
)
end
begin
Methods.ensure_capability!(method, capabilities)
rescue Methods::MissingRequiredCapabilityError => e
@ -669,14 +681,13 @@ module MCP
# an invalid request because a connection can never change eras.
def lift_request_envelope(params, method:, session:)
return if Methods.notification?(method)
return if method == Methods::SERVER_DISCOVER
era = session.respond_to?(:era) ? session.era : nil
if era == :modern && method == Methods::INITIALIZE
requested = params.is_a?(Hash) ? params[:protocolVersion] || params["protocolVersion"] : nil
raise UnsupportedProtocolVersionError.new(requested, params)
end
# Outside the modern era, `server/discover` stays envelope-exempt so legacy connections can probe capabilities
# before any negotiation. Under the modern era every request carries the envelope, discovery included:
# the conformance suite's 2026-07-28 requirements reject an envelope-less `server/discover` with `-32602` (SEP-2575).
return if method == Methods::SERVER_DISCOVER && era != :modern
if RequestEnvelope.modern?(params)
if era == :legacy

View file

@ -1041,10 +1041,16 @@ module MCP
!MCP::Configuration.modern_protocol_version?(version)
end
# Era sniff for a sessionless POST under a dual-era header version: only an `initialize` body is legacy-distinctive.
# Unparsable or non-object bodies go to the modern path, whose error responses cover them.
# Era sniff for a sessionless POST under a dual-era header version: only an `initialize` body
# WITHOUT the modern `_meta` envelope is legacy-distinctive. An `initialize` carrying
# the envelope comes from a modern client naming a method its lifecycle removed, so it stays
# on the modern path and answers with -32601/404 (SEP-2575).
# Unparsable or non-object bodies go to the modern path, whose error responses cover them.
def legacy_handshake_body?(body_string)
initialize_request?(parse_request_body(body_string))
body = parse_request_body(body_string)
return false unless initialize_request?(body)
!RequestEnvelope.modern?(body[:params])
rescue InvalidJsonError
false
end