Add client-level connect handshake to stdio transport
## Motivation and Context This commit implements Phase 1 of 3 for #334. The MCP specification requires an `initialize` request followed by a `notifications/initialized` notification before any other interaction. PR #327 added `MCP::Client#connect` and `MCP::Client::HTTP#connect` to expose this handshake explicitly for the HTTP transport, but stdio remained on its private `initialize_session` lazy-init path, inconsistent with the Python SDK (`ClientSession.initialize()`) and TypeScript SDK (`Client.connect(transport)`), both of which require an explicit handshake call regardless of transport. The implicit init path on the first `send_request` is preserved as a non-breaking compatibility shim. Public API added on `Stdio`: - `connect(client_info:, protocol_version:, capabilities:)` performs the handshake, validates the negotiated `protocolVersion` against `MCP::Configuration::SUPPORTED_STABLE_PROTOCOL_VERSIONS`, caches the `InitializeResult`, and is idempotent. - `connected?` reports handshake completion. - `server_info` exposes the cached `InitializeResult`. Documentation and examples are updated so new users learn the explicit pattern from the start. ## How Has This Been Tested? Fifteen regression tests cover: explicit handshake, `server_info` caching, idempotence, custom keyword arguments, JSON-RPC error response, missing or non-Hash `result`, unsupported protocol version with state rollback, state rollback when the `notifications/initialized` write fails, retry after a failed handshake, and the `connected?` / `server_info` lifecycle. The pre-existing stdio tests pass without modification, verifying that the implicit-init compatibility path still works. ## Breaking Changes None for the typical "implicit init via `client.tools` / `client.call_tool`" usage. Existing stdio clients keep working unchanged. Three observable behavior changes for code that inspects transport state directly: - `Client#connected?` for a never-initialized stdio client returns `false` (previously fell through to a `true` default via the `respond_to?` fallback). - `Client#server_info` for stdio returns the cached `InitializeResult` after the handshake (previously always `nil` via the fallback). - The implicit handshake path now validates the server's negotiated protocol version, raising `RequestHandlerError` for unsupported versions. Production servers that return one of the supported versions are unaffected; this closes a pre-existing spec-conformance gap on stdio.
This commit is contained in:
parent
3d9437736b
commit
46b738e945
1 changed files with 581 additions and 75 deletions
|
|
@ -61,18 +61,17 @@ module MCP
|
|||
|
||||
# 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).
|
||||
# Returns `nil` before `connect`, after `close`, or when the transport does
|
||||
# not expose a cached handshake result.
|
||||
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`.
|
||||
# Performs the MCP `initialize` handshake by delegating to the transport
|
||||
# (e.g. `MCP::Client::HTTP`, `MCP::Client::Stdio`). 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
|
||||
# When the transport does not respond to `:connect`, this is a no-op and
|
||||
# returns `nil`.
|
||||
#
|
||||
# @param client_info [Hash, nil] `{ name:, version: }` identifying the client.
|
||||
|
|
@ -91,10 +90,9 @@ module MCP
|
|||
)
|
||||
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.
|
||||
# Returns true once `connect` has completed the handshake on the underlying
|
||||
# transport. Transports that do not expose connection state are assumed
|
||||
# connected and return `true`.
|
||||
def connected?
|
||||
return transport.connected? if transport.respond_to?(:connected?)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue