Skip to content

Commit 79d89e0

Browse files
authored
Merge pull request #523 from koic/restructure_readme_into_docs_site
Move the Documentation From README.md to the Documentation Site
2 parents 5124cb4 + 88831b4 commit 79d89e0

51 files changed

Lines changed: 4253 additions & 3516 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎AGENTS.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This is the official Ruby SDK for the Model Context Protocol (MCP), implementing
1616
- `rake test` - Run all tests
1717
- `rake rubocop` - Run linter
1818
- `rake` - Run tests and linting (default task)
19+
- `bundle exec rake conformance` - Run the MCP conformance suite (see conformance/README.md)
1920
- `ruby -I lib -I test test/path/to/specific_test.rb` - Run single test file
2021
- `gem build mcp.gemspec` - Build the gem
2122

@@ -34,6 +35,14 @@ This is the official Ruby SDK for the Model Context Protocol (MCP), implementing
3435
- Keep dependencies minimal
3536
- Use lowercase HTTP response header names (e.g. `mcp-session-id`); the Rack 3 SPEC requires this, and the MCP spec's `Mcp-Session-Id` casing is prose convention only
3637

38+
## Documentation
39+
40+
- User-facing documentation lives in `docs/`, one page per topic, published at https://ruby.sdk.modelcontextprotocol.io (deploys only when a release is published)
41+
- Pages live in the `docs/_server/`, `docs/_client/`, and `docs/_extensions/` collections
42+
- Keep README.md slim: quick start and pointers only; document features on the relevant docs page
43+
- Internal links are absolute and extensionless (e.g. `/server/tools/`); front matter is followed by a blank line before the h1
44+
- Callout tiers: `.note` (supplementary), `.important` (spec constraints), `.warning` (deprecated features)
45+
3746
## Commit message conventions
3847

3948
- Use conventional commit format when possible

‎README.md‎

Lines changed: 50 additions & 3000 deletions
Large diffs are not rendered by default.

‎docs/_client/authorization.md‎

Lines changed: 260 additions & 0 deletions
Large diffs are not rendered by default.

‎docs/_client/cancellation.md‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
---
2+
layout: default
3+
title: Cancellation
4+
nav_order: 5
5+
---
6+
7+
# Cancellation
8+
9+
`MCP::Client` lets the caller cancel a request it has already issued,
10+
per the [MCP `notifications/cancelled` utility](https://modelcontextprotocol.io/specification/latest/basic/patterns/cancellation).
11+
The recommended pattern is to pass
12+
an `MCP::Cancellation` token into the request method, run the request on a worker thread, and call
13+
`cancellation.cancel(reason:)` from another thread. The cancelling thread sends `notifications/cancelled` to
14+
the server, and the calling thread is woken up with `MCP::CancelledError`:
15+
16+
```ruby
17+
client = MCP::Client.new(transport: transport)
18+
cancellation = MCP::Cancellation.new
19+
20+
Thread.new do
21+
client.call_tool(name: "slow_tool", arguments: {}, cancellation: cancellation)
22+
rescue MCP::CancelledError
23+
# cleanup
24+
end
25+
26+
# Later, from another thread:
27+
cancellation.cancel(reason: "user pressed cancel")
28+
```
29+
30+
All request methods (`tools`, `list_tools`, `resources`, `list_resources`, `resource_templates`, `list_resource_templates`,
31+
`prompts`, `list_prompts`, `call_tool`, `read_resource`, `get_prompt`, `complete`, `discover`, `ping`) accept the `cancellation:` keyword.
32+
Request ids are managed internally, so the token is the only thing a caller needs to cancel a request.
33+
34+
{: .note }
35+
> When a cancel wins the race, the SDK's worker thread that is blocked on the underlying I/O is *not* force-killed;
36+
> it stays blocked until the transport actually returns (or the user closes the transport). This matches the server-side
37+
> `StreamableHTTPTransport#send_request` trade-off. For `Client::HTTP`
38+
> the leak resolves as soon as the server sends any response; for `Client::Stdio` you may need to call `client.transport.close`
39+
> to free the thread if the server stops responding entirely. The cancel-dispatch thread waits for the worker's send-boundary signal
40+
> (`&on_sent` from `send_request`) before issuing `notifications/cancelled`, so the cancel is held until the worker has at
41+
> least committed to writing the request; while the worker is wedged the cancel notification is deferred along with it.
42+
43+
{: .note }
44+
> On a [modern](/client/lifecycle/) connection the cancel notification cannot reach the in-flight
45+
> request: correlating the two is a session mechanic of the handshake lifecycle, and modern requests
46+
> are sessionless single POST exchanges. The local effect is unchanged - the calling thread still
47+
> raises `MCP::CancelledError` - but the server runs the request to completion.
48+
49+
## Wire-order guarantees
50+
51+
`Client::Stdio` serializes the request write and any subsequent `notifications/cancelled` write through a single `@write_mutex`,
52+
so the server is guaranteed to read the request line before the cancel line.
53+
54+
`Client::HTTP` cannot offer the same wire-arrival guarantee. Faraday's synchronous `post` does not expose a post-write / pre-response hook,
55+
so the SDK yields just before the request POST is dispatched. After the yield, the cancel-dispatch thread issues a separate `notifications/cancelled` POST
56+
on its own connection, and the two POSTs may overlap on the network. The spec is satisfied either way: the sender has already issued the request and
57+
still believes it to be in-progress when issuing the cancel ([MCP cancellation spec](https://modelcontextprotocol.io/specification/latest/basic/patterns/cancellation)),
58+
and on the receiver side, "receivers MAY ignore a cancellation notification whose `requestId` is unknown" covers the case where the cancel POST
59+
happens to arrive first. The calling thread raises `MCP::CancelledError` regardless of network ordering.
60+
61+
## Custom transports
62+
63+
Custom transports that want to support `cancellation:` must implement `send_notification(notification:)` so `notifications/cancelled` can be delivered.
64+
They should also accept the optional block passed to `send_request(request:, &on_sent)` and call it once the request bytes have been handed off to the wire
65+
(under a write-side mutex for stdio-style transports, immediately before the synchronous round-trip for HTTP-style transports).
66+
The cancel-dispatch thread waits on this signal before sending `notifications/cancelled`. Transports that do not invoke the block fall back to waiting for
67+
the worker thread to terminate, which preserves wire-order at the cost of delaying the cancel notification until the request has fully completed.
68+
69+
## Server Side
70+
71+
How servers observe cancellation in their handlers is documented on the server [Cancellation](/server/cancellation/) page.

‎docs/_client/index.md‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
layout: default
3+
title: Overview
4+
nav_order: 1
5+
permalink: /client/
6+
redirect_from:
7+
- /building-clients.html
8+
- /building-clients/
9+
---
10+
11+
# Building an MCP Client
12+
13+
The `MCP::Client` class provides an interface for interacting with MCP servers.
14+
15+
This class supports:
16+
17+
- Lifecycle negotiation and connection via `MCP::Client#connect`, adopting the modern lifecycle
18+
when the server serves it; see [Lifecycle](/client/lifecycle/)
19+
- Server discovery via the `server/discover` method (`MCP::Client#discover`); see [Explicit Discovery](/client/lifecycle/#explicit-discovery)
20+
- Liveness check via the `ping` method (`MCP::Client#ping`)
21+
- Tool listing via the `tools/list` method (`MCP::Client#tools`)
22+
- Tool invocation via the `tools/call` method (`MCP::Client#call_tool`)
23+
- Resource listing via the `resources/list` method (`MCP::Client#resources`)
24+
- Resource template listing via the `resources/templates/list` method (`MCP::Client#resource_templates`)
25+
- Resource reading via the `resources/read` method (`MCP::Client#read_resource`)
26+
- Prompt listing via the `prompts/list` method (`MCP::Client#prompts`)
27+
- Prompt retrieval via the `prompts/get` method (`MCP::Client#get_prompt`)
28+
- Completion requests via the `completion/complete` method (`MCP::Client#complete`); see [Completions](/server/completions/)
29+
- Automatic driving of multi round-trip `input_required` results once `on_elicitation`, `on_sampling`,
30+
or `on_roots` handlers are registered; see [Multi-Round-Trip Results](/client/multi-round-trip-results/)
31+
- Cancellation of in-flight requests via the `cancellation:` keyword; see [Cancellation](/client/cancellation/)
32+
- Cursor-based page iteration on the `list_*` methods and whole-collection fetching with
33+
the `max_pages` guard; see [Pagination](/client/pagination/)
34+
- Automatic JSON-RPC 2.0 message formatting
35+
- UUID request ID generation
36+
37+
Clients are initialized with a [transport layer](/client/transports/) instance that handles the low-level communication mechanics.
38+
Authorization is handled by the transport layer; see [Authorization](/client/authorization/).
39+
40+
## Tool Objects
41+
42+
The client provides a wrapper class for tools returned by the server:
43+
44+
- `MCP::Client::Tool` - Represents a single tool with its metadata
45+
46+
This class provides easy access to tool properties like name, description, input schema, and output schema.

‎docs/_client/lifecycle.md‎

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
---
2+
layout: default
3+
title: Lifecycle
4+
nav_order: 3
5+
---
6+
7+
# Lifecycle
8+
9+
Before sending requests, a client establishes its lifecycle with the server: the classic `initialize` handshake
10+
on legacy protocol versions, or the sessionless modern lifecycle of MCP 2026-07-28.
11+
This page covers `MCP::Client#connect` and how it negotiates between the two.
12+
13+
## Handshake
14+
15+
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):
16+
17+
```ruby
18+
client.connect
19+
# => { "protocolVersion" => "2025-11-25", "capabilities" => {...}, "serverInfo" => {...} }
20+
21+
client.connected? # => true
22+
client.server_info # => cached InitializeResult
23+
```
24+
25+
`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.
26+
27+
This applies to both the Stdio and HTTP transports described on the [Transports](/client/transports/) page.
28+
29+
By default `connect` [negotiates the lifecycle](#lifecycle-negotiation) first and performs this handshake
30+
only when the server does not serve the modern lifecycle, or when `mode: :legacy` or a legacy `protocol_version:` forces it.
31+
32+
## Lifecycle Negotiation
33+
34+
`MCP::Client#connect` selects the protocol lifecycle automatically by default: on the bundled
35+
`MCP::Client::HTTP` and `MCP::Client::Stdio` transports it probes [`server/discover`](/server/discovery/) first and adopts
36+
the stateless modern lifecycle (MCP 2026-07-28, SEP-2575) when the server serves it, falling back to
37+
the classic `initialize` handshake otherwise. Custom transports whose `connect` does not declare
38+
a `mode:` keyword always receive the classic call shape, unchanged.
39+
40+
```ruby
41+
client.connect # negotiate automatically (default)
42+
client.connect(mode: :modern) # require the modern lifecycle; fails on legacy-only servers
43+
client.connect(mode: :legacy) # force the classic initialize handshake
44+
client.connect(protocol_version: "2025-11-25") # an explicit legacy version pins the handshake, no probe
45+
```
46+
47+
Prefer `mode: :legacy` for spawn-per-invocation CLI tools (the probe adds a round trip per process)
48+
and when using server-initiated requests (`on_elicitation` / `on_sampling`), which exist only on
49+
the legacy lifecycle.
50+
51+
Because the raw `connect` return value and `MCP::Client#server_info` mirror the wire result,
52+
their shape depends on the negotiated lifecycle: `InitializeResult` (`protocolVersion`,
53+
top-level `serverInfo`) on legacy, `DiscoverResult` (`supportedVersions`, `ttlMs`/`cacheScope`)
54+
on modern. Code that should work against both lifecycles can use the era-independent readers instead:
55+
56+
```ruby
57+
client.protocol_version # negotiated or adopted version, either lifecycle
58+
client.server_capabilities # capabilities Hash, either lifecycle
59+
client.instructions # instructions text, either lifecycle
60+
client.server_implementation # server name/version; nil when a modern server does not identify itself
61+
```
62+
63+
Troubleshooting: if `server_info["protocolVersion"]` starts returning `nil` after a server you connect to was upgraded,
64+
the server now serves the modern lifecycle and the automatic negotiation adopted it.
65+
Pass `mode: :legacy` for an immediate return to the previous behavior, or switch to the readers above for a permanent fix.
66+
67+
## Explicit Discovery
68+
69+
`MCP::Client#discover` sends `server/discover` directly: sessionless capability discovery
70+
that works before (or instead of) `connect`. It returns an `MCP::Client::DiscoverResult` struct
71+
exposing `supported_versions`, `capabilities`, `server_info`, `instructions`, and
72+
the `ttl_ms` / `cache_scope` cache hints; see the server [Discovery](/server/discovery/) page
73+
for the wire shapes.
74+
75+
```ruby
76+
result = client.discover
77+
result.supported_versions # => ["2026-07-28"]
78+
```
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
---
2+
layout: default
3+
title: Multi-Round-Trip Results
4+
nav_order: 4
5+
---
6+
7+
# Multi-Round-Trip Results
8+
9+
MCP 2026-07-28 replaces in-flight server-to-client requests with Multi Round-Trip Requests (SEP-2322): instead of issuing `sampling/createMessage`, `roots/list`,
10+
or `elicitation/create` while a request is being processed, a server may answer with a result whose `resultType` is `"input_required"`, carrying an `inputRequests` map
11+
and an opaque `requestState`; the client fulfills the requests and re-issues the original request with `inputResponses` and the echoed `requestState`.
12+
13+
## Automatic Driving
14+
15+
The Ruby client drives such results automatically: once a handler is registered through `on_elicitation`, `on_sampling`, or `on_roots`, the `call_tool`, `get_prompt`,
16+
and `read_resource` methods fulfill the embedded requests and re-issue the original request with `inputResponses` plus the echoed `requestState`, capped at `input_required_max_rounds`
17+
(10 by default, matching the TypeScript and Python SDKs).
18+
19+
```ruby
20+
client = MCP::Client.new(transport: transport)
21+
client.connect(capabilities: { elicitation: { form: {} } })
22+
23+
client.on_elicitation do |params|
24+
{ action: "accept", content: { name: "Alice" } }
25+
end
26+
27+
# The input_required round trips are driven automatically; this returns the final result.
28+
response = client.call_tool(name: "collect_name", arguments: {})
29+
```
30+
31+
Declare the capabilities matching the registered handlers on `connect`: a server embeds only the request kinds
32+
the client declared.
33+
34+
## Manual Driving
35+
36+
Without a matching handler, `MCP::Client::InputRequiredError` is raised instead of returning the result as if it were final;
37+
the error exposes `input_requests`, `request_state`, and the raw `result` for manual driving via the `input_responses:` and `request_state:` keywords:
38+
39+
```ruby
40+
begin
41+
client.call_tool(name: "collect_name", arguments: {})
42+
rescue MCP::Client::InputRequiredError => error
43+
answers = error.input_requests.transform_values { |request| answer_for(request) }
44+
45+
client.call_tool(
46+
name: "collect_name",
47+
arguments: {},
48+
input_responses: answers,
49+
request_state: error.request_state,
50+
)
51+
end
52+
```
53+
54+
`MCP::ResultType::COMPLETE` and `MCP::ResultType::INPUT_REQUIRED` are provided for forward compatibility.
55+
Servers on legacy protocol versions never send `resultType`, so existing behavior is unchanged.
56+
57+
## Server Side
58+
59+
Authoring `input_required` results with `InputRequiredResult`, securing `requestState`, `resultType` stamping, and the legacy fulfillment
60+
shim that serves pre-2026 clients are documented on the server [Multi-Round-Trip Results](/server/multi-round-trip-results/) page.

‎docs/_client/pagination.md‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
---
2+
layout: default
3+
title: Pagination
4+
nav_order: 7
5+
---
6+
7+
# Pagination
8+
9+
Servers may paginate `tools/list`, `prompts/list`, `resources/list`, and `resources/templates/list` responses
10+
per the [MCP pagination utility](https://modelcontextprotocol.io/specification/latest/server/utilities/pagination).
11+
Cursor tokens are opaque to clients: the server decides page size, and the client follows `nextCursor` until the server omits it.
12+
13+
## Iterating Pages
14+
15+
`MCP::Client` exposes `list_tools`, `list_prompts`, `list_resources`, and `list_resource_templates`.
16+
**Each call issues exactly one `*/list` JSON-RPC request and returns exactly one page** - not the full collection.
17+
The returned result object (`MCP::Client::ListToolsResult` etc.) exposes the page items and the next cursor
18+
as method accessors; a `meta` accessor also mirrors the response's `_meta` field:
19+
20+
```ruby
21+
client = MCP::Client.new(transport: transport)
22+
23+
cursor = nil
24+
loop do
25+
page = client.list_tools(cursor: cursor)
26+
page.tools.each { |tool| process(tool) }
27+
cursor = page.next_cursor
28+
break unless cursor
29+
end
30+
```
31+
32+
The same pattern applies to `list_prompts` (`page.prompts`), `list_resources` (`page.resources`), and
33+
`list_resource_templates` (`page.resource_templates`). `next_cursor` is `nil` on the final page.
34+
35+
Because a single call returns a single page, how many items come back depends on the server's `page_size` configuration:
36+
37+
| Server `page_size` | `client.list_tools(cursor: nil)` |
38+
|--------------------|---------------------------------------------------------------------|
39+
| Not set (default) | Returns every item in one response. `next_cursor` is `nil`. |
40+
| Set to `N` | Returns the first `N` items. `next_cursor` is set for continuation. |
41+
42+
If your application needs the complete collection regardless of how the server is configured, either loop on
43+
`next_cursor` as shown above, or use the whole-collection methods described below.
44+
45+
## Fetching the Complete Collection
46+
47+
`client.tools`, `client.resources`, `client.resource_templates`, and `client.prompts` auto-iterate
48+
through all pages and return a plain array of items, guaranteeing the full collection regardless
49+
of the server's `page_size` setting. When a server paginates, they issue multiple JSON-RPC round
50+
trips per call. Two guards keep that loop finite: it stops when the server returns a `nextCursor`
51+
it has already sent, and it stops after `max_pages` pages.
52+
53+
```ruby
54+
tools = client.tools # => Array<MCP::Client::Tool> of every tool on the server.
55+
```
56+
57+
`MCP::Client.new` accepts an optional `max_pages:` keyword that caps how many pages these methods
58+
will walk. It defaults to `1_000`; a server that keeps offering a fresh `nextCursor` past that
59+
point raises `MCP::Client::PaginationLimitError` rather than being followed indefinitely. Raise it
60+
if you legitimately expect more pages than that.
61+
62+
Use these when you want the complete list; use `list_tools(cursor:)` etc. when you need
63+
fine-grained iteration (e.g. to stream-process pages without loading everything into memory).
64+
65+
## Cache Hints
66+
67+
Per SEP-2549, list and read results can carry cache hints telling clients how long a result stays fresh (`ttlMs`)
68+
and whether shared intermediaries may cache it (`cacheScope`); see
69+
[List Result Caching](/server/pagination/#list-result-caching) on the server page for how they are emitted.
70+
On the client, the values are surfaced on the paginated result structs as `ttl_ms` and `cache_scope`:
71+
72+
```ruby
73+
page = client.list_tools
74+
page.ttl_ms # => 60000 (nil when the server sent no hint)
75+
page.cache_scope # => "private"
76+
```
77+
78+
## Server Side
79+
80+
Enabling pagination with `page_size:` is documented on the server [Pagination](/server/pagination/) page.

0 commit comments

Comments
 (0)