Merge pull request #438 from koic/server_discover

Add `server/discover` and stateless lifecycle error codes per SEP-2575
This commit is contained in:
Koichi ITO 2026-07-10 00:26:49 +09:00 • committed by GitHub
commit 61233ed43b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 149 additions and 2 deletions

View file

@ -0,0 +1,13 @@
# frozen_string_literal: true
require "test_helper"
module MCP
class ErrorCodesTest < ActiveSupport::TestCase
test "exposes the SEP-2575 stateless lifecycle error codes" do
# The exact values are wire vocabulary shared with other SDKs.
assert_equal(-32021, ErrorCodes::MISSING_REQUIRED_CLIENT_CAPABILITY)
assert_equal(-32022, ErrorCodes::UNSUPPORTED_PROTOCOL_VERSION)
end
end
end

View file

@ -24,6 +24,8 @@ module MCP
# Server methods and notifications
ensure_capability_does_not_raise_for Methods::INITIALIZE
# `server/discover` (SEP-2575) must respond regardless of declared capabilities.
ensure_capability_does_not_raise_for Methods::SERVER_DISCOVER
ensure_capability_raises_error_for Methods::PROMPTS_LIST, required_capability_name: "prompts"
ensure_capability_raises_error_for Methods::PROMPTS_GET, required_capability_name: "prompts"

View file

@ -551,6 +551,41 @@ module MCP
assert_equal 200, response[0]
end
test "allows server/discover POST without session ID in stateful mode" do
# Per SEP-2575, `server/discover` is sessionless capability discovery and is exempt
# from the session requirement, like `initialize`.
request = create_rack_request(
"POST",
"/",
{ "CONTENT_TYPE" => "application/json" },
{ jsonrpc: "2.0", method: "server/discover", id: "1" }.to_json,
)
response = @transport.handle_request(request)
assert_equal 200, response[0]
body = JSON.parse(response[2][0])
assert_equal Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS, body.dig("result", "supportedVersions")
refute response[1].key?("Mcp-Session-Id")
end
test "allows server/discover POST without session ID in stateless mode" do
stateless_transport = StreamableHTTPTransport.new(@server, stateless: true)
request = create_rack_request(
"POST",
"/",
{ "CONTENT_TYPE" => "application/json" },
{ jsonrpc: "2.0", method: "server/discover", id: "1" }.to_json,
)
response = stateless_transport.handle_request(request)
assert_equal 200, response[0]
body = JSON.parse(response[2][0])
assert_equal Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS, body.dig("result", "supportedVersions")
end
test "rejects duplicate SSE connection with 409" do
# Create a session
init_request = create_rack_request(

View file

@ -123,6 +123,54 @@ module MCP
assert_instrumentation_data({ method: "ping" })
end
test "#handle server/discover returns supported versions, capabilities, server info, and instructions" do
response = @server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 })
result = response[:result]
assert_equal Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS, result[:supportedVersions]
assert_equal @server.capabilities, result[:capabilities]
assert_equal @server_name, result.dig(:serverInfo, :name)
assert_equal "1.2.3", result.dig(:serverInfo, :version)
assert_equal "Optional instructions for the client", result[:instructions]
end
test "#handle server/discover responds before initialize and regardless of capabilities" do
# Per SEP-2575, discovery is sessionless: no prior `initialize`, no capability gate.
server = Server.new(name: "discover_test", capabilities: {})
response = server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 })
assert_equal Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS, response.dig(:result, :supportedVersions)
end
test "#handle server/discover does not mark the session initialized" do
session = ServerSession.new(server: @server, transport: mock)
@server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 }, session: session)
refute_predicate session, :initialized?
# `initialize` must still succeed afterwards.
response = @server.handle(
{ jsonrpc: "2.0", method: "initialize", id: 2, params: { clientInfo: { name: "c", version: "1" } } },
session: session,
)
refute_nil response[:result]
end
test "#handle server/discover omits instructions when the server has none" do
server = Server.new(name: "discover_test")
response = server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 })
refute response[:result].key?(:instructions)
end
test "#define_custom_method raises for server/discover" do
assert_raises(Server::MethodAlreadyDefinedError) do
@server.define_custom_method(method_name: "server/discover") { {} }
end
end
test "#handle initialize request returns protocol info, server info, and capabilities" do
request = {
jsonrpc: "2.0",