Add client-level connect for initialize handshake
This commit is contained in:
parent
a2706cc7ef
commit
ddfbfad81e
5 changed files with 471 additions and 4 deletions
|
|
@ -62,6 +62,7 @@ gem 'event_stream_parser', '>= 1.0' # optional, required only for SSE responses
|
|||
```ruby
|
||||
http_transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp")
|
||||
client = MCP::Client.new(transport: http_transport)
|
||||
client.connect
|
||||
|
||||
tools = client.tools
|
||||
tools.each do |tool|
|
||||
|
|
@ -74,16 +75,30 @@ response = client.call_tool(
|
|||
)
|
||||
```
|
||||
|
||||
### Handshake
|
||||
|
||||
Call `MCP::Client#connect` to perform the MCP [initialization handshake](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization): the client sends an `initialize` request through the transport, followed by the required `notifications/initialized` notification, and caches the server's `InitializeResult` (protocol version, capabilities, server info, instructions):
|
||||
|
||||
```ruby
|
||||
client.connect
|
||||
# => { "protocolVersion" => "2025-11-25", "capabilities" => {...}, "serverInfo" => {...} }
|
||||
|
||||
client.connected? # => true
|
||||
client.server_info # => cached InitializeResult
|
||||
```
|
||||
|
||||
`connect` accepts optional `client_info:`, `protocol_version:`, and `capabilities:` keyword arguments. It is idempotent — a second call returns the cached result without contacting the server. After `close`, state is cleared and `connect` will handshake again.
|
||||
|
||||
### Sessions
|
||||
|
||||
After a successful `initialize` request, the transport captures the `Mcp-Session-Id` header and `protocolVersion` from the response and includes the session ID on subsequent requests. Both are exposed on the transport:
|
||||
After `connect` succeeds, the HTTP transport captures the `Mcp-Session-Id` header and `protocolVersion` from the response and includes them on subsequent requests. Both are exposed on the transport as transport-specific state:
|
||||
|
||||
```ruby
|
||||
http_transport.session_id # => "abc123..."
|
||||
http_transport.protocol_version # => "2025-11-25"
|
||||
```
|
||||
|
||||
If the server terminates the session, subsequent requests return HTTP 404 and the transport raises `MCP::Client::SessionExpiredError` (a subclass of `RequestHandlerError`). Session state is cleared automatically; callers should start a new session by sending a fresh `initialize` request.
|
||||
If the server terminates the session, subsequent requests return HTTP 404 and the transport raises `MCP::Client::SessionExpiredError` (a subclass of `RequestHandlerError`). Session state is cleared automatically; callers should start a new session by calling `connect` again.
|
||||
|
||||
To explicitly terminate a session (e.g., when the client application is shutting down), call `close`. The transport sends an HTTP DELETE to the MCP endpoint with the session header and clears local session state. A `405 Method Not Allowed` response (server doesn't support client-initiated termination) or `404 Not Found` (session already terminated server-side) is treated as success. Other errors — 5xx, authentication failures, connection errors — propagate to the caller. Local session state is cleared either way. Calling `close` without an active session is a no-op.
|
||||
|
||||
|
|
|
|||
|
|
@ -59,6 +59,48 @@ module MCP
|
|||
# So keeping it public
|
||||
attr_reader :transport
|
||||
|
||||
# The server's `InitializeResult` (protocol version, capabilities, server info,
|
||||
# instructions), as reported by the transport after a successful `connect`.
|
||||
# Returns `nil` before `connect`, after `close`, or when the transport manages
|
||||
# the handshake implicitly and does not expose it (e.g. stdio).
|
||||
def server_info
|
||||
transport.server_info if transport.respond_to?(:server_info)
|
||||
end
|
||||
|
||||
# Performs the MCP `initialize` handshake by delegating to the transport when
|
||||
# it exposes a `connect` method (e.g. `MCP::Client::HTTP`). Returns the
|
||||
# server's `InitializeResult`.
|
||||
#
|
||||
# When the transport does not respond to `:connect` (e.g. `MCP::Client::Stdio`
|
||||
# manages the handshake implicitly on the first request), this is a no-op and
|
||||
# returns `nil`.
|
||||
#
|
||||
# @param client_info [Hash, nil] `{ name:, version: }` identifying the client.
|
||||
# @param protocol_version [String, nil] Protocol version to offer.
|
||||
# @param capabilities [Hash] Capabilities advertised by the client.
|
||||
# @return [Hash, nil] The server's `InitializeResult`, or `nil` when the transport
|
||||
# does not expose an explicit handshake.
|
||||
# https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization
|
||||
def connect(client_info: nil, protocol_version: nil, capabilities: {})
|
||||
return unless transport.respond_to?(:connect)
|
||||
|
||||
transport.connect(
|
||||
client_info: client_info,
|
||||
protocol_version: protocol_version,
|
||||
capabilities: capabilities,
|
||||
)
|
||||
end
|
||||
|
||||
# Returns true once `connect` has completed the handshake on transports that
|
||||
# expose connection state. Transports that manage the handshake implicitly
|
||||
# (e.g. stdio) always report `true`, since the first request will initialize
|
||||
# on demand.
|
||||
def connected?
|
||||
return transport.connected? if transport.respond_to?(:connected?)
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
# Returns a single page of tools from the server.
|
||||
#
|
||||
# @param cursor [String, nil] Cursor from a previous page response.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "securerandom"
|
||||
require_relative "../../json_rpc_handler"
|
||||
require_relative "../configuration"
|
||||
require_relative "../methods"
|
||||
require_relative "../version"
|
||||
|
||||
module MCP
|
||||
class Client
|
||||
|
|
@ -13,7 +17,7 @@ module MCP
|
|||
SESSION_ID_HEADER = "Mcp-Session-Id"
|
||||
PROTOCOL_VERSION_HEADER = "MCP-Protocol-Version"
|
||||
|
||||
attr_reader :url, :session_id, :protocol_version
|
||||
attr_reader :url, :session_id, :protocol_version, :server_info
|
||||
|
||||
def initialize(url:, headers: {}, &block)
|
||||
@url = url
|
||||
|
|
@ -21,6 +25,94 @@ module MCP
|
|||
@faraday_customizer = block
|
||||
@session_id = nil
|
||||
@protocol_version = nil
|
||||
@server_info = nil
|
||||
@connected = false
|
||||
end
|
||||
|
||||
# Performs the MCP `initialize` handshake: sends an `initialize` request
|
||||
# followed by the required `notifications/initialized` notification. The
|
||||
# server's `InitializeResult` (protocol version, capabilities, server
|
||||
# info, instructions) is cached on the transport and returned.
|
||||
#
|
||||
# Idempotent: a second call returns the cached `InitializeResult` without
|
||||
# contacting the server. After `close`, state is cleared and `connect`
|
||||
# will handshake again.
|
||||
#
|
||||
# @param client_info [Hash, nil] `{ name:, version: }` identifying the client.
|
||||
# Defaults to `{ name: "mcp-ruby-client", version: MCP::VERSION }`.
|
||||
# @param protocol_version [String, nil] Protocol version to offer. Defaults
|
||||
# to `MCP::Configuration::LATEST_STABLE_PROTOCOL_VERSION`.
|
||||
# @param capabilities [Hash] Capabilities advertised by the client. Defaults to `{}`.
|
||||
# @return [Hash] The server's `InitializeResult`.
|
||||
# @raise [RequestHandlerError] If the server responds with a JSON-RPC error
|
||||
# or a malformed result.
|
||||
# https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization
|
||||
def connect(client_info: nil, protocol_version: nil, capabilities: {})
|
||||
return @server_info if connected?
|
||||
|
||||
client_info ||= { name: "mcp-ruby-client", version: MCP::VERSION }
|
||||
protocol_version ||= MCP::Configuration::LATEST_STABLE_PROTOCOL_VERSION
|
||||
|
||||
response = send_request(request: {
|
||||
jsonrpc: JsonRpcHandler::Version::V2_0,
|
||||
id: SecureRandom.uuid,
|
||||
method: MCP::Methods::INITIALIZE,
|
||||
params: {
|
||||
protocolVersion: protocol_version,
|
||||
capabilities: capabilities,
|
||||
clientInfo: client_info,
|
||||
},
|
||||
})
|
||||
|
||||
if response.is_a?(Hash) && response.key?("error")
|
||||
clear_session
|
||||
error = response["error"]
|
||||
raise RequestHandlerError.new(
|
||||
"Server initialization failed: #{error["message"]}",
|
||||
{ method: MCP::Methods::INITIALIZE },
|
||||
error_type: :internal_error,
|
||||
)
|
||||
end
|
||||
|
||||
unless response.is_a?(Hash) && response["result"].is_a?(Hash)
|
||||
clear_session
|
||||
raise RequestHandlerError.new(
|
||||
"Server initialization failed: missing result in response",
|
||||
{ method: MCP::Methods::INITIALIZE },
|
||||
error_type: :internal_error,
|
||||
)
|
||||
end
|
||||
|
||||
@server_info = response["result"]
|
||||
negotiated_protocol_version = @server_info["protocolVersion"]
|
||||
unless MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(negotiated_protocol_version)
|
||||
clear_session
|
||||
raise RequestHandlerError.new(
|
||||
"Server initialization failed: unsupported protocol version #{negotiated_protocol_version.inspect}",
|
||||
{ method: MCP::Methods::INITIALIZE },
|
||||
error_type: :internal_error,
|
||||
)
|
||||
end
|
||||
|
||||
begin
|
||||
send_request(request: {
|
||||
jsonrpc: JsonRpcHandler::Version::V2_0,
|
||||
method: MCP::Methods::NOTIFICATIONS_INITIALIZED,
|
||||
})
|
||||
rescue StandardError
|
||||
clear_session
|
||||
raise
|
||||
end
|
||||
|
||||
@connected = true
|
||||
@server_info
|
||||
end
|
||||
|
||||
# Returns true once `connect` has completed the full handshake
|
||||
# (`initialize` response received and `notifications/initialized` sent).
|
||||
# Returns false before the first handshake and after `close`.
|
||||
def connected?
|
||||
@connected
|
||||
end
|
||||
|
||||
# Sends a JSON-RPC request and returns the parsed response body.
|
||||
|
|
@ -105,7 +197,10 @@ module MCP
|
|||
# session state is cleared either way.
|
||||
# https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
|
||||
def close
|
||||
return unless @session_id
|
||||
unless @session_id
|
||||
clear_session
|
||||
return
|
||||
end
|
||||
|
||||
begin
|
||||
client.delete("", nil, session_headers)
|
||||
|
|
@ -159,6 +254,8 @@ module MCP
|
|||
def clear_session
|
||||
@session_id = nil
|
||||
@protocol_version = nil
|
||||
@server_info = nil
|
||||
@connected = false
|
||||
end
|
||||
|
||||
def require_faraday!
|
||||
|
|
|
|||
|
|
@ -633,6 +633,25 @@ module MCP
|
|||
assert_nil(client.session_id)
|
||||
end
|
||||
|
||||
def test_close_clears_stateless_connection_state
|
||||
stub_request(:post, url)
|
||||
.with { |req| JSON.parse(req.body)["method"] == "initialize" }
|
||||
.to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json" },
|
||||
body: { result: { protocolVersion: "2025-11-25" } }.to_json,
|
||||
)
|
||||
stub_notification
|
||||
|
||||
client.connect
|
||||
client.close
|
||||
|
||||
assert_not_requested(:delete, url)
|
||||
refute_predicate(client, :connected?)
|
||||
assert_nil(client.protocol_version)
|
||||
assert_nil(client.server_info)
|
||||
end
|
||||
|
||||
def test_close_tolerates_405_response
|
||||
initialize_session
|
||||
stub_request(:delete, url).to_return(status: 405)
|
||||
|
|
@ -695,6 +714,213 @@ module MCP
|
|||
assert_requested(:delete, url, times: 1)
|
||||
end
|
||||
|
||||
def test_connect_performs_initialize_handshake
|
||||
init_stub = stub_request(:post, url)
|
||||
.with { |req| JSON.parse(req.body)["method"] == "initialize" }
|
||||
.to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json", "Mcp-Session-Id" => "s1" },
|
||||
body: {
|
||||
result: {
|
||||
protocolVersion: "2025-11-25",
|
||||
capabilities: { tools: {} },
|
||||
serverInfo: { name: "test-server", version: "1.0" },
|
||||
},
|
||||
}.to_json,
|
||||
)
|
||||
|
||||
notification_stub = stub_request(:post, url)
|
||||
.with { |req| JSON.parse(req.body)["method"] == "notifications/initialized" }
|
||||
.to_return(status: 202, body: "")
|
||||
|
||||
result = client.connect
|
||||
|
||||
assert_requested(init_stub)
|
||||
assert_requested(notification_stub)
|
||||
assert_equal("2025-11-25", result["protocolVersion"])
|
||||
assert_equal({ "tools" => {} }, result["capabilities"])
|
||||
assert_equal({ "name" => "test-server", "version" => "1.0" }, result["serverInfo"])
|
||||
end
|
||||
|
||||
def test_connect_caches_server_info
|
||||
stub_initialize
|
||||
stub_notification
|
||||
|
||||
client.connect
|
||||
|
||||
assert_equal("2025-11-25", client.server_info["protocolVersion"])
|
||||
end
|
||||
|
||||
def test_connect_uses_default_client_info_and_protocol_version
|
||||
notification_stub = stub_notification
|
||||
|
||||
init_stub = stub_request(:post, url)
|
||||
.with do |req|
|
||||
body = JSON.parse(req.body)
|
||||
body["method"] == "initialize" &&
|
||||
body["params"]["protocolVersion"] == MCP::Configuration::LATEST_STABLE_PROTOCOL_VERSION &&
|
||||
body["params"]["clientInfo"] == { "name" => "mcp-ruby-client", "version" => MCP::VERSION } &&
|
||||
body["params"]["capabilities"] == {}
|
||||
end
|
||||
.to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json" },
|
||||
body: { result: { protocolVersion: MCP::Configuration::LATEST_STABLE_PROTOCOL_VERSION } }.to_json,
|
||||
)
|
||||
|
||||
client.connect
|
||||
|
||||
assert_requested(init_stub)
|
||||
assert_requested(notification_stub)
|
||||
end
|
||||
|
||||
def test_connect_accepts_custom_parameters
|
||||
notification_stub = stub_notification
|
||||
|
||||
init_stub = stub_request(:post, url)
|
||||
.with do |req|
|
||||
body = JSON.parse(req.body)
|
||||
body["method"] == "initialize" &&
|
||||
body["params"]["protocolVersion"] == "2025-03-26" &&
|
||||
body["params"]["clientInfo"] == { "name" => "my-app", "version" => "9.9" } &&
|
||||
body["params"]["capabilities"] == { "roots" => { "listChanged" => true } }
|
||||
end
|
||||
.to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json" },
|
||||
body: { result: { protocolVersion: "2025-03-26" } }.to_json,
|
||||
)
|
||||
|
||||
client.connect(
|
||||
client_info: { name: "my-app", version: "9.9" },
|
||||
protocol_version: "2025-03-26",
|
||||
capabilities: { roots: { listChanged: true } },
|
||||
)
|
||||
|
||||
assert_requested(init_stub)
|
||||
assert_requested(notification_stub)
|
||||
end
|
||||
|
||||
def test_connect_is_idempotent
|
||||
init_stub = stub_initialize
|
||||
notification_stub = stub_notification
|
||||
|
||||
first_result = client.connect
|
||||
second_result = client.connect
|
||||
|
||||
assert_same(first_result, second_result)
|
||||
assert_requested(init_stub, times: 1)
|
||||
assert_requested(notification_stub, times: 1)
|
||||
end
|
||||
|
||||
def test_connect_raises_on_jsonrpc_error_response
|
||||
stub_request(:post, url).to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json", "Mcp-Session-Id" => "session-abc" },
|
||||
body: { error: { code: -32602, message: "Unsupported protocol version" } }.to_json,
|
||||
)
|
||||
|
||||
error = assert_raises(RequestHandlerError) do
|
||||
client.connect
|
||||
end
|
||||
|
||||
assert_includes(error.message, "Unsupported protocol version")
|
||||
refute_predicate(client, :connected?)
|
||||
assert_nil(client.session_id)
|
||||
assert_nil(client.protocol_version)
|
||||
assert_nil(client.server_info)
|
||||
end
|
||||
|
||||
def test_connect_raises_on_missing_result
|
||||
stub_request(:post, url).to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json", "Mcp-Session-Id" => "session-abc" },
|
||||
body: { jsonrpc: "2.0", id: "x" }.to_json,
|
||||
)
|
||||
|
||||
error = assert_raises(RequestHandlerError) do
|
||||
client.connect
|
||||
end
|
||||
|
||||
assert_includes(error.message, "missing result in response")
|
||||
refute_predicate(client, :connected?)
|
||||
assert_nil(client.session_id)
|
||||
assert_nil(client.protocol_version)
|
||||
assert_nil(client.server_info)
|
||||
end
|
||||
|
||||
def test_connect_raises_on_unsupported_negotiated_protocol_version
|
||||
stub_request(:post, url)
|
||||
.with { |req| JSON.parse(req.body)["method"] == "initialize" }
|
||||
.to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json", "Mcp-Session-Id" => "session-abc" },
|
||||
body: { result: { protocolVersion: "2099-01-01" } }.to_json,
|
||||
)
|
||||
|
||||
error = assert_raises(RequestHandlerError) do
|
||||
client.connect
|
||||
end
|
||||
|
||||
assert_includes(error.message, 'unsupported protocol version "2099-01-01"')
|
||||
refute_predicate(client, :connected?)
|
||||
assert_nil(client.session_id)
|
||||
assert_nil(client.protocol_version)
|
||||
assert_nil(client.server_info)
|
||||
end
|
||||
|
||||
def test_connect_clears_session_when_initialized_notification_fails
|
||||
stub_initialize
|
||||
stub_request(:post, url)
|
||||
.with { |req| JSON.parse(req.body)["method"] == "notifications/initialized" }
|
||||
.to_return(status: 500)
|
||||
|
||||
assert_raises(RequestHandlerError) do
|
||||
client.connect
|
||||
end
|
||||
|
||||
refute_predicate(client, :connected?)
|
||||
assert_nil(client.session_id)
|
||||
assert_nil(client.protocol_version)
|
||||
assert_nil(client.server_info)
|
||||
end
|
||||
|
||||
def test_connected_lifecycle
|
||||
refute_predicate(client, :connected?)
|
||||
|
||||
stub_initialize
|
||||
stub_notification
|
||||
client.connect
|
||||
|
||||
assert_predicate(client, :connected?)
|
||||
|
||||
stub_request(:delete, url).to_return(status: 200)
|
||||
client.close
|
||||
|
||||
refute_predicate(client, :connected?)
|
||||
end
|
||||
|
||||
def test_reconnect_after_close
|
||||
stub_initialize
|
||||
stub_notification
|
||||
client.connect
|
||||
stub_request(:delete, url).to_return(status: 200)
|
||||
client.close
|
||||
|
||||
stub_request(:post, url)
|
||||
.with { |req| JSON.parse(req.body)["method"] == "initialize" }
|
||||
.to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json", "Mcp-Session-Id" => "s2" },
|
||||
body: { result: { protocolVersion: "2025-11-25" } }.to_json,
|
||||
)
|
||||
|
||||
client.connect
|
||||
|
||||
assert_predicate(client, :connected?)
|
||||
assert_equal("s2", client.session_id)
|
||||
end
|
||||
|
||||
def test_close_allows_reinitializing_a_fresh_session
|
||||
initialize_session
|
||||
stub_request(:delete, url).to_return(status: 200)
|
||||
|
|
@ -732,6 +958,22 @@ module MCP
|
|||
client.send_request(request: { jsonrpc: "2.0", id: "1", method: "initialize" })
|
||||
end
|
||||
|
||||
def stub_initialize
|
||||
stub_request(:post, url)
|
||||
.with { |req| JSON.parse(req.body)["method"] == "initialize" }
|
||||
.to_return(
|
||||
status: 200,
|
||||
headers: { "Content-Type" => "application/json", "Mcp-Session-Id" => "session-abc" },
|
||||
body: { result: { protocolVersion: "2025-11-25" } }.to_json,
|
||||
)
|
||||
end
|
||||
|
||||
def stub_notification
|
||||
stub_request(:post, url)
|
||||
.with { |req| JSON.parse(req.body)["method"] == "notifications/initialized" }
|
||||
.to_return(status: 202, body: "")
|
||||
end
|
||||
|
||||
def stub_request(method, url)
|
||||
WebMock.stub_request(method, url)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,6 +5,77 @@ require "securerandom"
|
|||
|
||||
module MCP
|
||||
class ClientTest < Minitest::Test
|
||||
def test_connect_delegates_to_transport_and_forwards_keyword_args
|
||||
transport = mock
|
||||
init_result = {
|
||||
"protocolVersion" => "2025-11-25",
|
||||
"capabilities" => { "tools" => {} },
|
||||
"serverInfo" => { "name" => "test-server", "version" => "1.0" },
|
||||
}
|
||||
transport.expects(:connect).with(
|
||||
client_info: { name: "my-app", version: "1.0" },
|
||||
protocol_version: "2025-11-25",
|
||||
capabilities: { roots: {} },
|
||||
).returns(init_result).once
|
||||
|
||||
result = Client.new(transport: transport).connect(
|
||||
client_info: { name: "my-app", version: "1.0" },
|
||||
protocol_version: "2025-11-25",
|
||||
capabilities: { roots: {} },
|
||||
)
|
||||
|
||||
assert_equal(init_result, result)
|
||||
end
|
||||
|
||||
def test_connect_passes_nil_defaults_to_transport
|
||||
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_is_noop_when_transport_does_not_respond_to_connect
|
||||
transport = mock
|
||||
transport.stubs(:respond_to?).with(:connect).returns(false)
|
||||
transport.stubs(:respond_to?).with(:server_info).returns(false)
|
||||
|
||||
client = Client.new(transport: transport)
|
||||
|
||||
assert_nil(client.connect)
|
||||
assert_nil(client.server_info)
|
||||
end
|
||||
|
||||
def test_connected_delegates_to_transport_when_supported
|
||||
transport = mock
|
||||
transport.expects(:connected?).returns(true)
|
||||
|
||||
assert_predicate(Client.new(transport: transport), :connected?)
|
||||
end
|
||||
|
||||
def test_connected_returns_true_when_transport_does_not_respond_to_connected
|
||||
transport = mock
|
||||
transport.stubs(:respond_to?).with(:connected?).returns(false)
|
||||
|
||||
assert_predicate(Client.new(transport: transport), :connected?)
|
||||
end
|
||||
|
||||
def test_server_info_delegates_to_transport_when_supported
|
||||
transport = mock
|
||||
init_result = { "protocolVersion" => "2025-11-25" }
|
||||
transport.expects(:server_info).returns(init_result)
|
||||
|
||||
assert_equal(init_result, Client.new(transport: transport).server_info)
|
||||
end
|
||||
|
||||
def test_server_info_returns_nil_when_transport_does_not_expose_it
|
||||
transport = mock
|
||||
transport.stubs(:respond_to?).with(:server_info).returns(false)
|
||||
|
||||
assert_nil(Client.new(transport: transport).server_info)
|
||||
end
|
||||
|
||||
def test_tools_sends_request_to_transport_and_returns_tools_array
|
||||
transport = mock
|
||||
mock_response = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue