Merge pull request #489 from koic/enforce_modern_request_admission
Enforce the modern lifecycle admission rules per SEP-2575
This commit is contained in:
commit
b98b783a5f
3 changed files with 144 additions and 39 deletions
|
|
@ -555,7 +555,7 @@ module MCP
|
|||
test "locks the legacy era on a successful initialize and rejects a later modern envelope" do
|
||||
responses = run_transport_session([
|
||||
initialize_request(id: 1),
|
||||
modern_ping_request(id: 2),
|
||||
modern_tools_list_request(id: 2),
|
||||
])
|
||||
|
||||
refute responses[0].key?(:error)
|
||||
|
|
@ -568,7 +568,7 @@ module MCP
|
|||
# the legacy handshake locks `:legacy`, so a later modern envelope is still rejected.
|
||||
responses = run_transport_session([
|
||||
initialize_request(id: 1, protocol_version: "2026-07-28"),
|
||||
modern_ping_request(id: 2),
|
||||
modern_tools_list_request(id: 2),
|
||||
])
|
||||
|
||||
assert_equal "2026-07-28", responses[0].dig(:result, :protocolVersion)
|
||||
|
|
@ -576,22 +576,23 @@ module MCP
|
|||
assert_equal JsonRpcHandler::ErrorCode::INVALID_REQUEST, responses[1].dig(:error, :code)
|
||||
end
|
||||
|
||||
test "locks the modern era on a successful server/discover and rejects a later initialize with -32022" do
|
||||
test "locks the modern era on a successful server/discover and rejects a later initialize with -32601" do
|
||||
responses = run_transport_session([
|
||||
{ jsonrpc: "2.0", method: "server/discover", id: 1 },
|
||||
initialize_request(id: 2),
|
||||
modern_ping_request(id: 3),
|
||||
modern_tools_list_request(id: 3),
|
||||
])
|
||||
|
||||
assert_equal Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS, responses[0].dig(:result, :supportedVersions)
|
||||
assert_equal :modern, session_era
|
||||
assert_equal ErrorCodes::UNSUPPORTED_PROTOCOL_VERSION, responses[1].dig(:error, :code)
|
||||
assert_equal Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS, responses[1].dig(:error, :data, :supported)
|
||||
# SEP-2575 removes `initialize` from the modern lifecycle, so a modern-locked connection
|
||||
# answers it with Method not found.
|
||||
assert_equal JsonRpcHandler::ErrorCode::METHOD_NOT_FOUND, responses[1].dig(:error, :code)
|
||||
refute responses[2].key?(:error)
|
||||
end
|
||||
|
||||
test "locks the modern era on a successful request carrying the modern envelope" do
|
||||
responses = run_transport_session([modern_ping_request(id: 1)])
|
||||
responses = run_transport_session([modern_tools_list_request(id: 1)])
|
||||
|
||||
refute responses[0].key?(:error)
|
||||
assert_equal :modern, session_era
|
||||
|
|
@ -601,7 +602,7 @@ module MCP
|
|||
# An unsupported envelope version fails with -32022, so the connection stays unlocked
|
||||
# and a legacy initialize can still succeed afterwards.
|
||||
responses = run_transport_session([
|
||||
modern_ping_request(id: 1, version: "2027-01-01"),
|
||||
modern_tools_list_request(id: 1, version: "2027-01-01"),
|
||||
initialize_request(id: 2),
|
||||
])
|
||||
|
||||
|
|
@ -612,16 +613,29 @@ module MCP
|
|||
|
||||
test "requires the modern envelope after a modern era lock" do
|
||||
responses = run_transport_session([
|
||||
modern_ping_request(id: 1),
|
||||
{ jsonrpc: "2.0", method: "ping", id: 2 },
|
||||
modern_tools_list_request(id: 1),
|
||||
{ jsonrpc: "2.0", method: "tools/list", id: 2 },
|
||||
])
|
||||
|
||||
refute responses[0].key?(:error)
|
||||
assert_equal JsonRpcHandler::ErrorCode::INVALID_PARAMS, responses[1].dig(:error, :code)
|
||||
end
|
||||
|
||||
test "answers removed lifecycle methods with -32601 after a modern era lock" do
|
||||
removed_requests = Methods::MODERN_REMOVED_METHODS.each_with_index.map do |method, index|
|
||||
{ jsonrpc: "2.0", method: method, id: index + 2 }
|
||||
end
|
||||
responses = run_transport_session([modern_tools_list_request(id: 1)] + removed_requests)
|
||||
|
||||
refute responses[0].key?(:error)
|
||||
responses[1..].each_with_index do |response, index|
|
||||
method = Methods::MODERN_REMOVED_METHODS[index]
|
||||
assert_equal JsonRpcHandler::ErrorCode::METHOD_NOT_FOUND, response.dig(:error, :code), "expected -32601 for #{method}"
|
||||
end
|
||||
end
|
||||
|
||||
test "#send_request raises on a modern-locked session" do
|
||||
run_transport_session([modern_ping_request(id: 1)])
|
||||
run_transport_session([modern_tools_list_request(id: 1)])
|
||||
|
||||
error = assert_raises(RuntimeError) do
|
||||
@transport.send_request("roots/list")
|
||||
|
|
@ -643,10 +657,12 @@ module MCP
|
|||
}
|
||||
end
|
||||
|
||||
def modern_ping_request(id:, version: "2026-07-28")
|
||||
# `tools/list` stands in as the era-distinctive modern request: `ping` cannot,
|
||||
# because SEP-2575 removes it from the modern lifecycle (-32601 there).
|
||||
def modern_tools_list_request(id:, version: "2026-07-28")
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
method: "ping",
|
||||
method: "tools/list",
|
||||
id: id,
|
||||
params: {
|
||||
_meta: {
|
||||
|
|
|
|||
|
|
@ -5421,13 +5421,57 @@ module MCP
|
|||
|
||||
test "modern POST requires the _meta envelope" do
|
||||
response = @transport.handle_request(modern_rack_request(
|
||||
{ jsonrpc: "2.0", method: "ping", id: 1 }.to_json,
|
||||
{ jsonrpc: "2.0", method: "tools/list", id: 1 }.to_json,
|
||||
))
|
||||
|
||||
assert_equal 400, response[0]
|
||||
assert_equal(-32602, JSON.parse(response[2][0]).dig("error", "code"))
|
||||
end
|
||||
|
||||
test "modern POST rejects an envelope missing clientCapabilities with 400 and -32602" do
|
||||
body = {
|
||||
jsonrpc: "2.0",
|
||||
method: "tools/list",
|
||||
id: 1,
|
||||
params: { _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" } },
|
||||
}.to_json
|
||||
|
||||
response = @transport.handle_request(modern_rack_request(body))
|
||||
|
||||
assert_equal 400, response[0]
|
||||
assert_equal(-32602, JSON.parse(response[2][0]).dig("error", "code"))
|
||||
end
|
||||
|
||||
test "modern POST serves an envelope without the optional clientInfo" do
|
||||
body = {
|
||||
jsonrpc: "2.0",
|
||||
method: "tools/list",
|
||||
id: 1,
|
||||
params: {
|
||||
_meta: {
|
||||
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
|
||||
"io.modelcontextprotocol/clientCapabilities": {},
|
||||
},
|
||||
},
|
||||
}.to_json
|
||||
|
||||
response = @transport.handle_request(modern_rack_request(body))
|
||||
|
||||
assert_equal 200, response[0]
|
||||
assert JSON.parse(response[2][0]).key?("result")
|
||||
end
|
||||
|
||||
test "modern POST answers removed lifecycle methods with 404 and -32601" do
|
||||
MCP::Methods::MODERN_REMOVED_METHODS.each do |method|
|
||||
response = @transport.handle_request(modern_rack_request(modern_body(method, {})))
|
||||
|
||||
assert_equal 404, response[0], "expected 404 for #{method}"
|
||||
body = JSON.parse(response[2][0])
|
||||
assert_equal(-32601, body.dig("error", "code"), "expected -32601 for #{method}")
|
||||
assert_equal 1, body["id"], "expected the request id to be preserved for #{method}"
|
||||
end
|
||||
end
|
||||
|
||||
test "modern POST maps a missing client capability to 400 with -32021" do
|
||||
@server.define_tool(name: "guarded_tool") do |server_context:|
|
||||
server_context.require_client_capability!(:elicitation, :form)
|
||||
|
|
@ -5607,10 +5651,8 @@ module MCP
|
|||
assert_empty reported
|
||||
end
|
||||
|
||||
test "modern POST serves server/discover without an envelope" do
|
||||
response = @transport.handle_request(modern_rack_request(
|
||||
{ jsonrpc: "2.0", method: "server/discover", id: 1 }.to_json,
|
||||
))
|
||||
test "modern POST serves server/discover carrying the envelope" do
|
||||
response = @transport.handle_request(modern_rack_request(modern_body("server/discover", {})))
|
||||
|
||||
assert_equal 200, response[0]
|
||||
refute response[1].key?("mcp-session-id")
|
||||
|
|
@ -5618,6 +5660,17 @@ module MCP
|
|||
assert_equal Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS, body.dig("result", "supportedVersions")
|
||||
end
|
||||
|
||||
test "modern POST rejects an envelope-less server/discover with 400 and -32602" do
|
||||
# The 2026-07-28 conformance requirements validate the `_meta` envelope on `server/discover` like
|
||||
# on every other modern request (SEP-2575).
|
||||
response = @transport.handle_request(modern_rack_request(
|
||||
{ jsonrpc: "2.0", method: "server/discover", id: 1 }.to_json,
|
||||
))
|
||||
|
||||
assert_equal 400, response[0]
|
||||
assert_equal(-32602, JSON.parse(response[2][0]).dig("error", "code"))
|
||||
end
|
||||
|
||||
test "legacy POST rejects a modern envelope body without the modern header as -32020" do
|
||||
# Without header-primary routing evidence, the body-level triple would silently fall through
|
||||
# the legacy path via the header default.
|
||||
|
|
|
|||
|
|
@ -360,28 +360,33 @@ module MCP
|
|||
assert_includes response.dig(:error, :message), "io.modelcontextprotocol/protocolVersion"
|
||||
end
|
||||
|
||||
test "#handle rejects initialize on a modern-locked session with -32022" do
|
||||
test "#handle rejects removed lifecycle methods on a modern-locked session with -32601" do
|
||||
session = ServerSession.new(server: @server, transport: mock, era: :modern)
|
||||
|
||||
response = @server.handle(
|
||||
{
|
||||
jsonrpc: "2.0",
|
||||
method: "initialize",
|
||||
id: 1,
|
||||
params: { protocolVersion: "2025-11-25", clientInfo: { name: "c", version: "1" } },
|
||||
},
|
||||
session: session,
|
||||
)
|
||||
Methods::MODERN_REMOVED_METHODS.each_with_index do |method, index|
|
||||
response = @server.handle(
|
||||
{ jsonrpc: "2.0", method: method, id: index + 1 },
|
||||
session: session,
|
||||
)
|
||||
|
||||
assert_equal ErrorCodes::UNSUPPORTED_PROTOCOL_VERSION, response.dig(:error, :code)
|
||||
assert_equal "2025-11-25", response.dig(:error, :data, :requested)
|
||||
assert_equal JsonRpcHandler::ErrorCode::METHOD_NOT_FOUND, response.dig(:error, :code), "expected -32601 for #{method}"
|
||||
assert_equal index + 1, response[:id], "expected the request id to be preserved for #{method}"
|
||||
end
|
||||
end
|
||||
|
||||
test "#handle server/discover succeeds without an envelope on a modern-locked session" do
|
||||
test "#handle server/discover requires the envelope on a modern-locked session" do
|
||||
# The 2026-07-28 conformance requirements reject an envelope-less `server/discover`
|
||||
# on the modern era with -32602 (SEP-2575).
|
||||
session = ServerSession.new(server: @server, transport: mock, era: :modern)
|
||||
|
||||
response = @server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 }, session: session)
|
||||
|
||||
assert_equal JsonRpcHandler::ErrorCode::INVALID_PARAMS, response.dig(:error, :code)
|
||||
end
|
||||
|
||||
test "#handle server/discover succeeds without an envelope outside the modern era" do
|
||||
response = @server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 })
|
||||
|
||||
refute_nil response[:result]
|
||||
end
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue