Add client-level connect for initialize handshake#327
Open
atesgoral wants to merge 1 commit intomodelcontextprotocol:mainfrom
Open
Add client-level connect for initialize handshake#327atesgoral wants to merge 1 commit intomodelcontextprotocol:mainfrom
atesgoral wants to merge 1 commit intomodelcontextprotocol:mainfrom
Conversation
4ebea00 to
aee9398
Compare
Per MCP spec, clients must send an `initialize` request followed by a `notifications/initialized` notification before issuing any other requests. The server's `InitializeResult` (protocol version, capabilities, server info, instructions) negotiates the session for the lifetime of the connection. `MCP::Client#connect` performs the handshake, returns the server's `InitializeResult`, and exposes `connected?` and `server_info` readers. It delegates to `transport.connect(...)` when the transport exposes an explicit handshake (e.g. `MCP::Client::HTTP`) and is a no-op otherwise (e.g. `MCP::Client::Stdio`, which manages the handshake implicitly on the first request). `initialize` is a protocol method, so the public API lives on the client. This matches the Python SDK (`ClientSession.initialize()`) and the TypeScript SDK (`Client.connect(transport)`). Transports retain their transport-specific concerns: HTTP captures the `Mcp-Session-Id` header and negotiated protocol version via the existing `capture_session_info` hook that fires when an `initialize` request passes through `send_request`. https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization
aee9398 to
31892a5
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
MCP::Client#connectto perform the MCP initialization handshake: sends aninitializerequest through the transport, followed by the requirednotifications/initializednotification, and returns the server'sInitializeResult(protocol version, capabilities, server info, instructions).client.connect(client_info:, protocol_version:, capabilities:)— idempotent. Delegates totransport.connect(...)when the transport exposes an explicit handshake (e.g.MCP::Client::HTTP); no-op when it doesn't (e.g.MCP::Client::Stdio, which still initializes implicitly on the firstsend_request).client.connected?— reports handshake completion; delegates to the transport when supported, otherwise returnstrue.client.server_info— returns the transport's cachedInitializeResult, ornilwhen the transport doesn't expose it.Why on the client
initializeis a protocol method — same level asping,list_tools,call_tool— and the client already owns those. Python (ClientSession.initialize()) and TypeScript (Client.connect(transport)) both put the handshake on the client. Aligning Ruby with those SDKs gives users a unified API regardless of transport and keeps the transport layer focused on byte transport plus transport-specific state (e.g. HTTP session headers).HTTP transport retains
connect,connected?,server_info,session_id, andprotocol_version— the client delegates through. The existingcapture_session_infohook still fires during the handshake becauseinitializegoes throughtransport.send_request, so session headers and negotiated protocol version continue to get captured automatically.Stdio
Stdio currently initializes implicitly on the first
send_requestvia a privateinitialize_session— unchanged in this PR.client.connectis a no-op for stdio, and existing stdio clients continue working without modification.Open question for SDK contributors: should we fully unify by removing stdio's implicit init and requiring
client.connectuniversally? It would tighten the contract (one explicit handshake path) at the cost of a breaking change for existing stdio users. Happy to do that in a follow-up if there's consensus.Example
Test plan
:connect,connected?delegation and default,server_infodelegation and defaultresult,connected?lifecycle, reconnect aftercloserake test— 782 runs, 1938 assertions, 0 failuresrubocopclean on touched filesexamples/streamable_http_server.rb:client.connect→ tools → call_tool →transport.close→ reconnect viaclient.connect(new session id issued);client.server_infocorrectly clears oncloseexamples/stdio_client.rbstill runs end-to-end without an explicitclient.connectcall (implicit-init path unchanged)