## Motivation and Context Clients that require MCP 2026-07-28, such as ChatGPT, reject connections because the SDK does not advertise that version: `Server#init` answers a 2026-07-28 request with 2025-11-25, the Streamable HTTP transport 400s an `MCP-Protocol-Version: 2026-07-28` header, and `server/discover` omits the version from `supportedVersions`. Making 2026-07-28 the latest entry in `SUPPORTED_STABLE_PROTOCOL_VERSIONS` fixes all three at once, since they all read that list. The version now serves both lifecycles of the SEP-2575 dual-era model: negotiable through the legacy `initialize` handshake, and simultaneously the version of the per-request modern lifecycle. Parts of the 2026-07-28 feature set remain unimplemented, such as the stateless modern lifecycle over the transports, and are being built separately. This change deliberately limits itself to the version bump, focusing first on eliminating the connection errors that 2026-07-28 clients hit. Known follow-up: the in-progress modern HTTP path routes requests to the sessionless modern lifecycle when the header version is absent from `SUPPORTED_STABLE_PROTOCOL_VERSIONS`, so that routing predicate needs rework on top of this change. Closes #474 ## How Has This Been Tested? New regression tests: `initialize` with `protocolVersion: "2026-07-28"` echoes it back (`server_test.rb`), a follow-up POST with `MCP-Protocol-Version: 2026-07-28` passes header validation (`streamable_http_transport_test.rb`), and `Configuration.new(protocol_version: "2026-07-28")` is accepted (`configuration_test.rb`). `bundle exec rake test` passes (1454 runs, 0 failures). The conformance server suite passes 30/30. The client `initialize` scenario is added to the expected-failures baseline: the conformance framework pins the client's requested version to 2025-11-25 and does not accept 2026-07-28 yet; the entry carries a TODO to remove it when the framework catches up. ## Breaking Changes Servers that do not pin `configuration.protocol_version` now answer version negotiation fallbacks and default client requests with 2026-07-28 instead of 2025-11-25. 2025-11-25 remains fully supported and negotiable; clients that request it are unaffected.
153 lines
5.6 KiB
Markdown
153 lines
5.6 KiB
Markdown
---
|
|
layout: default
|
|
title: Building Clients
|
|
nav_order: 4
|
|
---
|
|
|
|
# Building an MCP Client
|
|
|
|
The `MCP::Client` class provides an interface for interacting with MCP servers.
|
|
|
|
**Supported operations:**
|
|
|
|
- Tool listing (`MCP::Client#tools`) and invocation (`MCP::Client#call_tool`)
|
|
- Resource listing (`MCP::Client#resources`) and reading (`MCP::Client#read_resource`)
|
|
- Resource template listing (`MCP::Client#resource_templates`)
|
|
- Prompt listing (`MCP::Client#prompts`) and retrieval (`MCP::Client#get_prompt`)
|
|
- Completion requests (`MCP::Client#complete`)
|
|
|
|
## Handshake
|
|
|
|
Call `MCP::Client#connect` to perform the MCP [initialization handshake](https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#initialization) before sending any other requests. 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" => "2026-07-28", "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.
|
|
|
|
This applies to both the Stdio and HTTP transports below.
|
|
|
|
## Stdio Transport
|
|
|
|
Use `MCP::Client::Stdio` to interact with MCP servers running as subprocesses:
|
|
|
|
```ruby
|
|
stdio_transport = MCP::Client::Stdio.new(
|
|
command: "bundle",
|
|
args: ["exec", "ruby", "path/to/server.rb"],
|
|
env: { "API_KEY" => "my_secret_key" },
|
|
read_timeout: 30
|
|
)
|
|
client = MCP::Client.new(transport: stdio_transport)
|
|
client.connect
|
|
|
|
tools = client.tools
|
|
tools.each do |tool|
|
|
puts "Tool: #{tool.name} - #{tool.description}"
|
|
end
|
|
|
|
response = client.call_tool(
|
|
tool: tools.first,
|
|
arguments: { message: "Hello, world!" }
|
|
)
|
|
|
|
stdio_transport.close
|
|
```
|
|
|
|
| Parameter | Required | Description |
|
|
|---|---|---|
|
|
| `command:` | Yes | The command to spawn the server process. |
|
|
| `args:` | No | An array of arguments passed to the command. Defaults to `[]`. |
|
|
| `env:` | No | A hash of environment variables for the server process. Defaults to `nil`. |
|
|
| `read_timeout:` | No | Timeout in seconds for waiting for a server response. Defaults to `nil`. |
|
|
| `max_line_bytes:` | No | Maximum byte length of a single newline-delimited response frame. A frame that reaches this limit without a newline is rejected as a transport error, preventing unbounded memory growth from a server that never emits a newline. Defaults to `4 * 1024 * 1024` (4 MiB). |
|
|
|
|
## HTTP Transport
|
|
|
|
Use `MCP::Client::HTTP` to interact with MCP servers over HTTP. Requires the `faraday` gem, plus `event_stream_parser` if the server uses SSE (`text/event-stream`) responses:
|
|
|
|
```ruby
|
|
gem 'mcp'
|
|
gem 'faraday', '>= 2.0'
|
|
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|
|
|
puts "Tool: #{tool.name} - #{tool.description}"
|
|
end
|
|
|
|
response = client.call_tool(
|
|
tool: tools.first,
|
|
arguments: { message: "Hello, world!" }
|
|
)
|
|
```
|
|
|
|
### Sessions
|
|
|
|
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 # => "2026-07-28"
|
|
```
|
|
|
|
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.
|
|
|
|
```ruby
|
|
http_transport.close
|
|
```
|
|
|
|
### Authorization
|
|
|
|
Provide custom headers for authentication:
|
|
|
|
```ruby
|
|
http_transport = MCP::Client::HTTP.new(
|
|
url: "https://api.example.com/mcp",
|
|
headers: {
|
|
"Authorization" => "Bearer my_token"
|
|
}
|
|
)
|
|
client = MCP::Client.new(transport: http_transport)
|
|
```
|
|
|
|
### Customizing the Faraday Connection
|
|
|
|
Pass a block to customize the underlying Faraday connection:
|
|
|
|
```ruby
|
|
http_transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp") do |faraday|
|
|
faraday.use MyApp::Middleware::HttpRecorder
|
|
faraday.adapter :typhoeus
|
|
end
|
|
```
|
|
|
|
## Custom Transport
|
|
|
|
If the built-in transports do not fit your needs, you can implement your own:
|
|
|
|
```ruby
|
|
class CustomTransport
|
|
def send_request(request:)
|
|
# Your transport-specific logic here.
|
|
# Returns a Hash modeling a JSON-RPC response object.
|
|
end
|
|
end
|
|
|
|
client = MCP::Client.new(transport: CustomTransport.new)
|
|
```
|
|
|
|
For more details, see the [full README](https://github.com/modelcontextprotocol/ruby-sdk/blob/main/README.md#building-an-mcp-client).
|