Merge pull request #480 from koic/discover_finalize_and_client
Finalize `server/discover` and Add Client Modern Lifecycle Support per SEP-2575
This commit is contained in:
commit
4ed461c72f
6 changed files with 1203 additions and 151 deletions
|
|
@ -1844,6 +1844,109 @@ module MCP
|
|||
refute_predicate(client, :connected?)
|
||||
end
|
||||
|
||||
def test_connect_modern_probes_discover_and_adopts_the_modern_lifecycle
|
||||
discover_stub = stub_discover
|
||||
|
||||
result = client.connect(mode: :modern)
|
||||
|
||||
assert_requested(discover_stub)
|
||||
assert_predicate(client, :connected?)
|
||||
assert_predicate(client, :modern?)
|
||||
assert_equal(["2026-07-28"], result["supportedVersions"])
|
||||
assert_equal(result, client.server_info)
|
||||
assert_equal("2026-07-28", client.protocol_version)
|
||||
end
|
||||
|
||||
def test_modern_requests_carry_the_envelope_and_matching_header
|
||||
stub_discover
|
||||
client.connect(
|
||||
mode: :modern,
|
||||
client_info: { name: "my-app", version: "9.9" },
|
||||
capabilities: { elicitation: {} },
|
||||
)
|
||||
|
||||
request_stub = stub_request(:post, url).with do |req|
|
||||
body = JSON.parse(req.body)
|
||||
meta = body.dig("params", "_meta")
|
||||
body["method"] == "tools/list" &&
|
||||
req.headers["Mcp-Protocol-Version"] == "2026-07-28" &&
|
||||
meta == {
|
||||
"io.modelcontextprotocol/protocolVersion" => "2026-07-28",
|
||||
"io.modelcontextprotocol/clientInfo" => { "name" => "my-app", "version" => "9.9" },
|
||||
"io.modelcontextprotocol/clientCapabilities" => { "elicitation" => {} },
|
||||
}
|
||||
end.to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json" },
|
||||
body: { result: { tools: [] } }.to_json,
|
||||
)
|
||||
|
||||
client.send_request(request: { jsonrpc: "2.0", id: "1", method: "tools/list" })
|
||||
|
||||
assert_requested(request_stub)
|
||||
end
|
||||
|
||||
def test_connect_modern_fails_without_a_mutual_modern_version
|
||||
stub_request(:post, url).with do |req|
|
||||
JSON.parse(req.body)["method"] == "server/discover"
|
||||
end.to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json" },
|
||||
body: { result: { supportedVersions: ["2025-11-25"] } }.to_json,
|
||||
)
|
||||
|
||||
error = assert_raises(RequestHandlerError) { client.connect(mode: :modern) }
|
||||
|
||||
assert_includes(error.message, "no mutually supported modern protocol version")
|
||||
refute_predicate(client, :connected?)
|
||||
refute_predicate(client, :modern?)
|
||||
end
|
||||
|
||||
def test_connect_auto_falls_back_to_the_legacy_handshake_on_discovery_errors
|
||||
stub_request(:post, url).with do |req|
|
||||
JSON.parse(req.body)["method"] == "server/discover"
|
||||
end.to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json" },
|
||||
body: { error: { code: -32601, message: "Method not found" } }.to_json,
|
||||
)
|
||||
init_stub = stub_initialize
|
||||
notification_stub = stub_notification
|
||||
|
||||
result = client.connect(mode: :auto)
|
||||
|
||||
assert_requested(init_stub)
|
||||
assert_requested(notification_stub)
|
||||
refute_predicate(client, :modern?)
|
||||
assert_predicate(client, :connected?)
|
||||
assert_equal("2025-11-25", result["protocolVersion"])
|
||||
end
|
||||
|
||||
def test_connect_auto_falls_back_when_discovery_lacks_a_mutual_modern_version
|
||||
# Rollout tolerance: a server may answer discovery while only serving legacy versions;
|
||||
# auto mode falls back to the legacy handshake.
|
||||
stub_request(:post, url).with do |req|
|
||||
JSON.parse(req.body)["method"] == "server/discover"
|
||||
end.to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json" },
|
||||
body: { result: { supportedVersions: ["2025-11-25"] } }.to_json,
|
||||
)
|
||||
init_stub = stub_initialize
|
||||
stub_notification
|
||||
|
||||
client.connect(mode: :auto)
|
||||
|
||||
assert_requested(init_stub)
|
||||
refute_predicate(client, :modern?)
|
||||
assert_predicate(client, :connected?)
|
||||
end
|
||||
|
||||
def test_connect_rejects_unknown_modes_and_legacy_versions_for_modern_mode
|
||||
assert_raises(ArgumentError) { client.connect(mode: :bogus) }
|
||||
assert_raises(ArgumentError) { client.connect(mode: :modern, protocol_version: "2025-11-25") }
|
||||
end
|
||||
|
||||
def test_reconnect_after_close
|
||||
stub_initialize
|
||||
stub_notification
|
||||
|
|
@ -1949,6 +2052,24 @@ module MCP
|
|||
.to_return(status: 202, body: "")
|
||||
end
|
||||
|
||||
def stub_discover
|
||||
stub_request(:post, url).with do |req|
|
||||
JSON.parse(req.body)["method"] == "server/discover"
|
||||
end.to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json" },
|
||||
body: {
|
||||
result: {
|
||||
supportedVersions: ["2026-07-28"],
|
||||
capabilities: { tools: {} },
|
||||
_meta: { "io.modelcontextprotocol/serverInfo": { name: "test-server", version: "1.0" } },
|
||||
ttlMs: 0,
|
||||
cacheScope: "private",
|
||||
},
|
||||
}.to_json,
|
||||
)
|
||||
end
|
||||
|
||||
def stub_request(method, url)
|
||||
WebMock.stub_request(method, url)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1414,6 +1414,198 @@ module MCP
|
|||
[transport, server_thread, [stdin_read, stdin_write, stdout_read, stdout_write]]
|
||||
end
|
||||
|
||||
def test_send_request_allows_server_discover_before_connect
|
||||
stdin_read, stdin_write = IO.pipe
|
||||
stdout_read, stdout_write = IO.pipe
|
||||
stderr_read, _ = IO.pipe
|
||||
|
||||
Open3.stubs(:popen3).returns([stdin_write, stdout_read, stderr_read, mock_wait_thread])
|
||||
transport = Stdio.new(command: "ruby", args: ["server.rb"])
|
||||
|
||||
server_thread = Thread.new do
|
||||
discover_request = JSON.parse(stdin_read.gets)
|
||||
response = {
|
||||
jsonrpc: "2.0",
|
||||
id: discover_request["id"],
|
||||
result: { supportedVersions: ["2026-07-28"], ttlMs: 0, cacheScope: "private" },
|
||||
}
|
||||
stdout_write.puts(JSON.generate(response))
|
||||
stdout_write.flush
|
||||
end
|
||||
|
||||
# No prior `connect`: `server/discover` (SEP-2575) is sessionless discovery.
|
||||
response = transport.send_request(request: { jsonrpc: "2.0", id: "d1", method: "server/discover" })
|
||||
|
||||
assert_equal(["2026-07-28"], response.dig("result", "supportedVersions"))
|
||||
refute_predicate(transport, :connected?)
|
||||
ensure
|
||||
server_thread&.join
|
||||
[stdin_read, stdin_write, stdout_read, stdout_write, stderr_read].each do |io|
|
||||
io.close unless io.closed?
|
||||
end
|
||||
end
|
||||
|
||||
def test_connect_modern_probes_discover_and_stamps_requests
|
||||
stdin_read, stdin_write = IO.pipe
|
||||
stdout_read, stdout_write = IO.pipe
|
||||
stderr_read, _ = IO.pipe
|
||||
|
||||
Open3.stubs(:popen3).returns([stdin_write, stdout_read, stderr_read, mock_wait_thread])
|
||||
transport = Stdio.new(command: "ruby", args: ["server.rb"])
|
||||
|
||||
received = []
|
||||
server_thread = Thread.new do
|
||||
discover_request = JSON.parse(stdin_read.gets)
|
||||
received << discover_request
|
||||
stdout_write.puts(JSON.generate({
|
||||
jsonrpc: "2.0",
|
||||
id: discover_request["id"],
|
||||
result: {
|
||||
supportedVersions: ["2026-07-28"],
|
||||
capabilities: {},
|
||||
serverInfo: { name: "test-server", version: "1.0" },
|
||||
ttlMs: 0,
|
||||
cacheScope: "private",
|
||||
},
|
||||
}))
|
||||
stdout_write.flush
|
||||
|
||||
tools_request = JSON.parse(stdin_read.gets)
|
||||
received << tools_request
|
||||
stdout_write.puts(JSON.generate({ jsonrpc: "2.0", id: tools_request["id"], result: { tools: [] } }))
|
||||
stdout_write.flush
|
||||
end
|
||||
|
||||
result = transport.connect(mode: :modern, client_info: { name: "my-app", version: "9.9" })
|
||||
response = transport.send_request(request: { jsonrpc: "2.0", id: "t1", method: "tools/list" })
|
||||
|
||||
assert_equal(["2026-07-28"], result["supportedVersions"])
|
||||
assert_predicate(transport, :modern?)
|
||||
assert_predicate(transport, :connected?)
|
||||
assert_equal("2026-07-28", transport.protocol_version)
|
||||
assert_equal("t1", response["id"])
|
||||
|
||||
# No `initialize` was sent; both frames carry the SEP-2575 envelope.
|
||||
assert_equal(["server/discover", "tools/list"], received.map { |frame| frame["method"] })
|
||||
triple = received[1].dig("params", "_meta")
|
||||
assert_equal("2026-07-28", triple["io.modelcontextprotocol/protocolVersion"])
|
||||
assert_equal({ "name" => "my-app", "version" => "9.9" }, triple["io.modelcontextprotocol/clientInfo"])
|
||||
assert_equal({}, triple["io.modelcontextprotocol/clientCapabilities"])
|
||||
ensure
|
||||
server_thread&.join
|
||||
[stdin_read, stdin_write, stdout_read, stdout_write, stderr_read].each do |io|
|
||||
io.close unless io.closed?
|
||||
end
|
||||
end
|
||||
|
||||
def test_connect_auto_falls_back_to_the_legacy_handshake
|
||||
stdin_read, stdin_write = IO.pipe
|
||||
stdout_read, stdout_write = IO.pipe
|
||||
stderr_read, _ = IO.pipe
|
||||
|
||||
Open3.stubs(:popen3).returns([stdin_write, stdout_read, stderr_read, mock_wait_thread])
|
||||
transport = Stdio.new(command: "ruby", args: ["server.rb"])
|
||||
|
||||
server_thread = Thread.new do
|
||||
discover_request = JSON.parse(stdin_read.gets)
|
||||
stdout_write.puts(JSON.generate({
|
||||
jsonrpc: "2.0",
|
||||
id: discover_request["id"],
|
||||
error: { code: -32601, message: "Method not found" },
|
||||
}))
|
||||
stdout_write.flush
|
||||
|
||||
init_request = JSON.parse(stdin_read.gets)
|
||||
stdout_write.puts(JSON.generate({
|
||||
jsonrpc: "2.0",
|
||||
id: init_request["id"],
|
||||
result: {
|
||||
protocolVersion: "2025-11-25",
|
||||
capabilities: {},
|
||||
serverInfo: { name: "test-server", version: "1.0" },
|
||||
},
|
||||
}))
|
||||
stdout_write.flush
|
||||
|
||||
# `notifications/initialized`
|
||||
stdin_read.gets
|
||||
end
|
||||
|
||||
result = transport.connect(mode: :auto)
|
||||
|
||||
assert_equal("2025-11-25", result["protocolVersion"])
|
||||
assert_equal("2025-11-25", transport.protocol_version)
|
||||
refute_predicate(transport, :modern?)
|
||||
assert_predicate(transport, :connected?)
|
||||
ensure
|
||||
server_thread&.join
|
||||
[stdin_read, stdin_write, stdout_read, stdout_write, stderr_read].each do |io|
|
||||
io.close unless io.closed?
|
||||
end
|
||||
end
|
||||
|
||||
def test_connect_auto_bounds_the_discover_probe_when_no_read_timeout_is_configured
|
||||
transport = Stdio.new(command: "ruby", args: ["server.rb"])
|
||||
transport.stubs(:start)
|
||||
|
||||
observed_timeout = nil
|
||||
transport.define_singleton_method(:probe_discover) do
|
||||
observed_timeout = @read_timeout
|
||||
raise RequestHandlerError.new("probe timed out", {}, error_type: :internal_error)
|
||||
end
|
||||
transport.define_singleton_method(:connect_legacy) do |**|
|
||||
{ "protocolVersion" => "2025-11-25" }
|
||||
end
|
||||
|
||||
result = transport.connect(mode: :auto)
|
||||
|
||||
assert_equal(Stdio::DEFAULT_DISCOVER_PROBE_TIMEOUT, observed_timeout)
|
||||
assert_nil(transport.instance_variable_get(:@read_timeout))
|
||||
assert_equal("2025-11-25", result["protocolVersion"])
|
||||
end
|
||||
|
||||
def test_connect_auto_falls_back_when_the_server_never_answers_the_probe
|
||||
stdin_read, stdin_write = IO.pipe
|
||||
stdout_read, stdout_write = IO.pipe
|
||||
stderr_read, _ = IO.pipe
|
||||
|
||||
Open3.stubs(:popen3).returns([stdin_write, stdout_read, stderr_read, mock_wait_thread])
|
||||
# A configured `read_timeout` also bounds the probe; the server below reads
|
||||
# the `server/discover` request and never responds, which must count as
|
||||
# legacy evidence instead of blocking `connect` forever.
|
||||
transport = Stdio.new(command: "ruby", args: ["server.rb"], read_timeout: 0.05)
|
||||
|
||||
server_thread = Thread.new do
|
||||
# Silently swallow the probe.
|
||||
stdin_read.gets
|
||||
|
||||
init_request = JSON.parse(stdin_read.gets)
|
||||
stdout_write.puts(JSON.generate({
|
||||
jsonrpc: "2.0",
|
||||
id: init_request["id"],
|
||||
result: {
|
||||
protocolVersion: "2025-11-25",
|
||||
capabilities: {},
|
||||
serverInfo: { name: "test-server", version: "1.0" },
|
||||
},
|
||||
}))
|
||||
stdout_write.flush
|
||||
|
||||
# `notifications/initialized`
|
||||
stdin_read.gets
|
||||
end
|
||||
|
||||
result = transport.connect(mode: :auto)
|
||||
|
||||
assert_equal("2025-11-25", result["protocolVersion"])
|
||||
refute_predicate(transport, :modern?)
|
||||
ensure
|
||||
server_thread&.join
|
||||
[stdin_read, stdin_write, stdout_read, stdout_write, stderr_read].each do |io|
|
||||
io.close unless io.closed?
|
||||
end
|
||||
end
|
||||
|
||||
def mock_wait_thread
|
||||
thread = mock("wait_thread")
|
||||
thread.stubs(:alive?).returns(true)
|
||||
|
|
|
|||
|
|
@ -85,6 +85,218 @@ module MCP
|
|||
assert_includes(error.message, "does not support server-to-client requests")
|
||||
end
|
||||
|
||||
# A transport that declares `mode:` on `connect`, recording what it received.
|
||||
# The `:not_forwarded` sentinel distinguishes "Client did not forward mode" from
|
||||
# "Client forwarded mode: :legacy".
|
||||
class ModeAwareTransport
|
||||
attr_reader :received
|
||||
|
||||
def connect(client_info: nil, protocol_version: nil, capabilities: {}, mode: :not_forwarded)
|
||||
@received = {
|
||||
client_info: client_info,
|
||||
protocol_version: protocol_version,
|
||||
capabilities: capabilities,
|
||||
mode: mode,
|
||||
}
|
||||
{ "supportedVersions" => ["2026-07-28"] }
|
||||
end
|
||||
end
|
||||
|
||||
def test_connect_defaults_to_auto_on_transports_declaring_mode
|
||||
transport = ModeAwareTransport.new
|
||||
|
||||
Client.new(transport: transport).connect
|
||||
|
||||
assert_equal(:auto, transport.received[:mode])
|
||||
end
|
||||
|
||||
def test_connect_keeps_the_legacy_call_shape_for_transports_without_mode
|
||||
# Custom transports whose `connect` does not declare `mode:` keep receiving
|
||||
# the historical call shape, so the `:auto` default cannot break them.
|
||||
transport = mock
|
||||
transport.expects(:connect).with(
|
||||
client_info: nil, protocol_version: nil, capabilities: {},
|
||||
).returns({ "protocolVersion" => "2025-11-25" }).once
|
||||
|
||||
Client.new(transport: transport).connect
|
||||
end
|
||||
|
||||
def test_connect_routes_explicit_legacy_through_the_legacy_call_shape
|
||||
transport = ModeAwareTransport.new
|
||||
|
||||
Client.new(transport: transport).connect(mode: :legacy)
|
||||
|
||||
assert_equal(:not_forwarded, transport.received[:mode])
|
||||
end
|
||||
|
||||
def test_connect_pins_legacy_for_an_explicit_stable_protocol_version
|
||||
# An explicitly requested legacy-generation version must never be overridden
|
||||
# by the default negotiation adopting the modern lifecycle.
|
||||
transport = ModeAwareTransport.new
|
||||
|
||||
Client.new(transport: transport).connect(protocol_version: "2025-11-25")
|
||||
|
||||
assert_equal(:not_forwarded, transport.received[:mode])
|
||||
assert_equal("2025-11-25", transport.received[:protocol_version])
|
||||
end
|
||||
|
||||
def test_connect_negotiates_automatically_for_an_explicit_modern_protocol_version
|
||||
transport = ModeAwareTransport.new
|
||||
|
||||
Client.new(transport: transport).connect(protocol_version: "2026-07-28")
|
||||
|
||||
assert_equal(:auto, transport.received[:mode])
|
||||
end
|
||||
|
||||
def test_connect_raises_for_explicit_non_legacy_mode_on_a_transport_without_mode
|
||||
transport = mock
|
||||
transport.expects(:connect).never
|
||||
|
||||
error = assert_raises(ArgumentError) do
|
||||
Client.new(transport: transport).connect(mode: :modern)
|
||||
end
|
||||
|
||||
assert_includes(error.message, "does not support mode:")
|
||||
end
|
||||
|
||||
def test_connect_rejects_unknown_modes
|
||||
transport = ModeAwareTransport.new
|
||||
|
||||
assert_raises(ArgumentError) do
|
||||
Client.new(transport: transport).connect(mode: :bogus)
|
||||
end
|
||||
end
|
||||
|
||||
def test_discover_returns_a_discover_result
|
||||
transport = mock
|
||||
transport.expects(:send_request).with do |args|
|
||||
args[:request][:method] == "server/discover"
|
||||
end.returns({
|
||||
"result" => {
|
||||
"supportedVersions" => ["2026-07-28"],
|
||||
"capabilities" => { "tools" => {} },
|
||||
"_meta" => { "io.modelcontextprotocol/serverInfo" => { "name" => "test-server", "version" => "1.0" } },
|
||||
"instructions" => "Optional instructions",
|
||||
"ttlMs" => 3_600_000,
|
||||
"cacheScope" => "public",
|
||||
},
|
||||
}).once
|
||||
|
||||
result = Client.new(transport: transport).discover
|
||||
|
||||
assert_equal(["2026-07-28"], result.supported_versions)
|
||||
assert_equal({ "tools" => {} }, result.capabilities)
|
||||
assert_equal({ "name" => "test-server", "version" => "1.0" }, result.server_info)
|
||||
assert_equal("Optional instructions", result.instructions)
|
||||
assert_equal(3_600_000, result.ttl_ms)
|
||||
assert_equal("public", result.cache_scope)
|
||||
end
|
||||
|
||||
def test_discover_maps_a_top_level_server_info_as_a_fallback
|
||||
# Servers built against the frozen SEP text still stamp `serverInfo` at the top level;
|
||||
# the finalized spec moved it into the result `_meta`.
|
||||
transport = mock
|
||||
transport.expects(:send_request).returns({
|
||||
"result" => {
|
||||
"supportedVersions" => ["2026-07-28"],
|
||||
"serverInfo" => { "name" => "sep-era-server", "version" => "1.0" },
|
||||
},
|
||||
}).once
|
||||
|
||||
result = Client.new(transport: transport).discover
|
||||
|
||||
assert_equal({ "name" => "sep-era-server", "version" => "1.0" }, result.server_info)
|
||||
end
|
||||
|
||||
def test_discover_raises_validation_error_on_missing_result
|
||||
transport = mock
|
||||
transport.expects(:send_request).returns({}).once
|
||||
|
||||
assert_raises(Client::ValidationError) do
|
||||
Client.new(transport: transport).discover
|
||||
end
|
||||
end
|
||||
|
||||
def test_discover_raises_server_error_on_jsonrpc_error
|
||||
transport = mock
|
||||
transport.expects(:send_request).returns({
|
||||
"error" => { "code" => -32601, "message" => "Method not found" },
|
||||
}).once
|
||||
|
||||
error = assert_raises(Client::ServerError) do
|
||||
Client.new(transport: transport).discover
|
||||
end
|
||||
|
||||
assert_equal(-32601, error.code)
|
||||
end
|
||||
|
||||
def test_era_independent_readers_on_a_legacy_result
|
||||
transport = mock
|
||||
transport.stubs(:server_info).returns({
|
||||
"protocolVersion" => "2025-11-25",
|
||||
"capabilities" => { "tools" => {} },
|
||||
"serverInfo" => { "name" => "legacy-server", "version" => "1.0" },
|
||||
"instructions" => "Be helpful",
|
||||
})
|
||||
|
||||
client = Client.new(transport: transport)
|
||||
|
||||
assert_equal("2025-11-25", client.protocol_version)
|
||||
assert_equal({ "tools" => {} }, client.server_capabilities)
|
||||
assert_equal("Be helpful", client.instructions)
|
||||
assert_equal({ "name" => "legacy-server", "version" => "1.0" }, client.server_implementation)
|
||||
end
|
||||
|
||||
def test_era_independent_readers_on_a_modern_result
|
||||
# A modern `DiscoverResult` has no `protocolVersion`; the adopted version comes
|
||||
# from the transport. `serverInfo` sits in the result `_meta` per the final spec.
|
||||
transport = mock
|
||||
transport.stubs(:protocol_version).returns("2026-07-28")
|
||||
transport.stubs(:server_info).returns({
|
||||
"supportedVersions" => ["2026-07-28"],
|
||||
"capabilities" => { "tools" => {} },
|
||||
"instructions" => "Be helpful",
|
||||
"_meta" => { "io.modelcontextprotocol/serverInfo" => { "name" => "modern-server", "version" => "2.0" } },
|
||||
"ttlMs" => 0,
|
||||
"cacheScope" => "private",
|
||||
})
|
||||
|
||||
client = Client.new(transport: transport)
|
||||
|
||||
assert_equal("2026-07-28", client.protocol_version)
|
||||
assert_equal({ "tools" => {} }, client.server_capabilities)
|
||||
assert_equal("Be helpful", client.instructions)
|
||||
assert_equal({ "name" => "modern-server", "version" => "2.0" }, client.server_implementation)
|
||||
end
|
||||
|
||||
def test_server_implementation_falls_back_to_the_top_level_and_tolerates_absence
|
||||
transport = mock
|
||||
transport.stubs(:server_info).returns({
|
||||
"supportedVersions" => ["2026-07-28"],
|
||||
"serverInfo" => { "name" => "top-level", "version" => "1.0" },
|
||||
})
|
||||
|
||||
assert_equal({ "name" => "top-level", "version" => "1.0" }, Client.new(transport: transport).server_implementation)
|
||||
|
||||
anonymous = mock
|
||||
anonymous.stubs(:server_info).returns({ "supportedVersions" => ["2026-07-28"] })
|
||||
|
||||
assert_nil(Client.new(transport: anonymous).server_implementation)
|
||||
end
|
||||
|
||||
def test_era_independent_readers_return_nil_before_connect
|
||||
transport = mock
|
||||
transport.stubs(:respond_to?).with(:server_info).returns(false)
|
||||
transport.stubs(:respond_to?).with(:protocol_version).returns(false)
|
||||
|
||||
client = Client.new(transport: transport)
|
||||
|
||||
assert_nil(client.protocol_version)
|
||||
assert_nil(client.server_capabilities)
|
||||
assert_nil(client.instructions)
|
||||
assert_nil(client.server_implementation)
|
||||
end
|
||||
|
||||
def test_connected_delegates_to_transport_when_supported
|
||||
transport = mock
|
||||
transport.expects(:connected?).returns(true)
|
||||
|
|
|
|||
|
|
@ -583,7 +583,7 @@ module MCP
|
|||
modern_ping_request(id: 3),
|
||||
])
|
||||
|
||||
assert_equal Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS, responses[0].dig(:result, :supportedVersions)
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -660,7 +660,7 @@ module MCP
|
|||
|
||||
assert_equal 200, response[0]
|
||||
body = JSON.parse(response[2][0])
|
||||
assert_equal Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS, body.dig("result", "supportedVersions")
|
||||
assert_equal Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS, body.dig("result", "supportedVersions")
|
||||
refute response[1].key?("mcp-session-id")
|
||||
end
|
||||
|
||||
|
|
@ -678,7 +678,7 @@ module MCP
|
|||
|
||||
assert_equal 200, response[0]
|
||||
body = JSON.parse(response[2][0])
|
||||
assert_equal Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS, body.dig("result", "supportedVersions")
|
||||
assert_equal Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS, body.dig("result", "supportedVersions")
|
||||
end
|
||||
|
||||
test "rejects duplicate SSE connection with 409" do
|
||||
|
|
@ -5426,7 +5426,7 @@ module MCP
|
|||
assert_equal 200, response[0]
|
||||
refute response[1].key?("mcp-session-id")
|
||||
body = JSON.parse(response[2][0])
|
||||
assert_equal Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS, body.dig("result", "supportedVersions")
|
||||
assert_equal Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS, body.dig("result", "supportedVersions")
|
||||
end
|
||||
|
||||
test "legacy POST rejects a modern envelope body without the modern header as -32020" do
|
||||
|
|
|
|||
|
|
@ -129,10 +129,13 @@ module MCP
|
|||
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 Configuration::SUPPORTED_MODERN_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)
|
||||
# Per the finalized spec (PR #3002), the server identity is the optional `_meta` stamp,
|
||||
# not a top-level `serverInfo` field.
|
||||
assert_equal @server_name, result.dig(:_meta, RequestEnvelope::SERVER_INFO_META_KEY, :name)
|
||||
assert_equal "1.2.3", result.dig(:_meta, RequestEnvelope::SERVER_INFO_META_KEY, :version)
|
||||
refute result.key?(:serverInfo)
|
||||
assert_equal "Optional instructions for the client", result[:instructions]
|
||||
end
|
||||
|
||||
|
|
@ -142,7 +145,7 @@ module MCP
|
|||
|
||||
response = server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 })
|
||||
|
||||
assert_equal Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS, response.dig(:result, :supportedVersions)
|
||||
assert_equal Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS, response.dig(:result, :supportedVersions)
|
||||
end
|
||||
|
||||
test "#handle server/discover does not mark the session initialized" do
|
||||
|
|
@ -167,6 +170,26 @@ module MCP
|
|||
refute response[:result].key?(:instructions)
|
||||
end
|
||||
|
||||
test "#handle server/discover always carries the required cache hints with spec defaults" do
|
||||
# Unlike the opt-in SEP-2549 hints on list/read results, `ttlMs`/`cacheScope`
|
||||
# are REQUIRED on `DiscoverResult`.
|
||||
server = Server.new(name: "discover_test")
|
||||
|
||||
result = server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 })[:result]
|
||||
|
||||
assert_equal 0, result[:ttlMs]
|
||||
assert_equal "private", result[:cacheScope]
|
||||
end
|
||||
|
||||
test "#handle server/discover reuses the configured SEP-2549 cache hints" do
|
||||
server = Server.new(name: "discover_test", ttl_ms: 3_600_000, cache_scope: "public")
|
||||
|
||||
result = server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 })[:result]
|
||||
|
||||
assert_equal 3_600_000, result[:ttlMs]
|
||||
assert_equal "public", result[:cacheScope]
|
||||
end
|
||||
|
||||
test "#define_custom_method raises for server/discover" do
|
||||
assert_raises(Server::MethodAlreadyDefinedError) do
|
||||
@server.define_custom_method(method_name: "server/discover") { {} }
|
||||
|
|
|
|||
Loading…
Reference in a new issue