ruby-sdk/docs/building-clients.md
Ates Goral ff3a42f3fb Add HTTP client close for explicit session termination
Per MCP spec, clients that no longer need a session SHOULD send an HTTP
DELETE to the MCP endpoint with the Mcp-Session-Id header to explicitly
terminate it. Servers MAY respond with 405 Method Not Allowed when they
don't support client-initiated termination.

`MCP::Client::HTTP#close` sends the DELETE with session headers, clears
local session state regardless of outcome, and is a no-op when no
session has been established.

https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
2026-04-20 13:58:41 -04:00

134 lines
4.3 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_resources`)
- Resource template listing (`MCP::Client#resource_templates`)
- Prompt listing (`MCP::Client#prompts`) and retrieval (`MCP::Client#get_prompt`)
- Completion requests (`MCP::Client#complete`)
## 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)
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`. |
## 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)
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 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:
```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.
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).