Move the Documentation From README.md to the Documentation Site

Move the documentation that had grown to 3000+ lines in README.md onto https://ruby.sdk.modelcontextprotocol.io
and slim the README down to a quick start and feature overview, following the Python SDK layout:

- Add 21 server pages as the docs/_server/ collection (overview plus transports, discovery, tools, prompts, resources,
  roots, sampling, elicitation, multi round-trip results, notifications, notification subscriptions, cancellation, progress,
  ping, completions, logging, pagination, server context, configuration, and custom methods), 8 client pages as
  the docs/_client/ collection (overview, transports, lifecycle, multi round-trip results, cancellation, ping, pagination,
  and authorization), and 3 extension pages as the docs/_extensions/ collection (overview, capability extensions, and MCP Apps,
  following the Extensions Overview recommendation that SDK documentation list the supported extensions) -
  one page per topic, ordered to match the sidebar of the 2026-07-28 specification, with the client-side APIs
  (pinging, cancelling, and paginating from MCP::Client) documented under Building Clients
- Add top-level Examples and Protocol Versions pages after Installation, linking the runnable examples in examples/
  and summarizing the supported protocol versions, the era model, and client negotiation
- Render the three sections as just-the-docs collections, which list every page beneath a plain category heading in
  the sidebar with no folding, and serve every page at an extensionless URL under /server/, /client/, and /extensions/,
  matching the sibling SDK documentation sites
- Replace docs/building-servers.md and docs/building-clients.md with redirects to the new section overview pages via
  jekyll-redirect-from, redirect the previously published /installation.html to its extensionless URL, fold their content
  that was missing from README.md into the new pages, and update the Tool argument keys reference comment in lib/mcp/server.rb to
  the relocated Tools page
- Reduce README.md to the badges, a compact feature overview in the Python SDK style, installation instructions, a stdio
  server and client quick start, and a License section, using absolute URLs for the remaining repository links
  so they resolve on rubygems.org
- Adapt formatting where GitHub rendering habits break on the site: convert the numbered "three ways to define" lists into headings,
  since kramdown restarts numbering at 1 when code blocks split list items, convert GitHub-style alerts into just-the-docs callouts
  with the SEP-2260 server-to-client association note raised to a red warning, merge the duplicated Exception Reporting
  and Configuration Block Data sections into the Configuration page, and relocate the Streamable HTTP settings that were nested under
  the Logging section into the Transports page
- Refresh the migrated content against the current implementation: correct stale claims and broken samples the README carried,
  note on each session-era feature how it relates to the modern lifecycle of MCP 2026-07-28, and point spec links at the latest revision,
  keeping deliberate 2025-11-25 pins for pages the modern revision removed
- Style the site after the Rails API documentation palette (red links and accents on neutral surfaces) with matching
  light and dark color schemes, center the sidebar and content as one block, add a Previous/Next footer pager following
  the sidebar order, open external links in a new tab, and serve the MCP logo as the favicon

Every code block and heading from the previous README was verified to have a home in the new docs pages or the slimmed README before
the reduction; a few samples were corrected rather than copied, as noted above.
This commit is contained in:
Koichi ITO 2026-08-21 03:03:27 +09:00
commit 88831b46f6
21 changed files with 4253 additions and 3516 deletions

View file

@ -0,0 +1,137 @@
---
layout: default
title: Cancellation
nav_order: 13
---
# Cancellation
The MCP Ruby SDK supports server-side handling of
the [MCP `notifications/cancelled` utility](https://modelcontextprotocol.io/specification/latest/basic/patterns/cancellation).
When a client sends `notifications/cancelled` for an in-flight request, the server stops
processing cooperatively and suppresses the JSON-RPC response for that request.
Cancellation is cooperative: the SDK does not forcibly terminate tool code. Instead,
a `MCP::Cancellation` token is threaded through [`server_context`](/server/server-context/), and long-running tools
poll it to exit early. When a tool returns after cancellation has been observed,
the server suppresses the JSON-RPC response, matching the spec. The `initialize` request
is never cancellable per the spec.
{: .note }
> Cancellation by notification belongs to the handshake lifecycle, where the session correlates
> `notifications/cancelled` with the in-flight request it targets. Requests of the
> [modern lifecycle](/server/discovery/#the-stateless-modern-lifecycle) are sessionless single POST exchanges, so a separately
> POSTed cancel notification cannot reach them; a modern client abandons a request by closing
> the connection instead.
## Handlers that Check for Cancellation
Any handler that opts in to `server_context:` - tools (`Tool.call`), prompt templates,
`resources_read_handler`, `resources_list_handler`, `completion_handler`, `resources_subscribe_handler`,
`resources_unsubscribe_handler`, and [`define_custom_method`](/server/custom-methods/) blocks - receives
an `MCP::ServerContext` wired to the in-flight request's cancellation token.
Handlers check `cancelled?` in their work loop, or call `raise_if_cancelled!` to raise
`MCP::CancelledError` at a safe point:
```ruby
class LongRunningTool < MCP::Tool
description "A tool that supports cancellation"
input_schema(properties: { count: { type: "integer" } }, required: ["count"])
def self.call(count:, server_context:)
count.times do |i|
# Exit early if the client has sent `notifications/cancelled`.
break if server_context.cancelled?
do_work(i)
end
MCP::Tool::Response.new([{ type: "text", text: "Done" }])
end
end
```
Alternatively, raise at the next safe point with `raise_if_cancelled!`:
```ruby
def self.call(count:, server_context:)
count.times do |i|
server_context.raise_if_cancelled!
do_work(i)
end
MCP::Tool::Response.new([{ type: "text", text: "Done" }])
end
```
When a handler observes cancellation (either by returning early with `cancelled?` or
by raising `MCP::CancelledError` via `raise_if_cancelled!`), the server drops the response and
no JSON-RPC result is sent to the client.
The same pattern works for other handler types:
```ruby
# resources/read
server.resources_read_handler do |params, server_context:|
server_context.raise_if_cancelled!
# read the resource
end
# completion/complete
server.completion_handler do |params, server_context:|
server_context.raise_if_cancelled!
# compute completions
end
# custom method
server.define_custom_method(method_name: "custom/slow") do |params, server_context:|
server_context.raise_if_cancelled!
# do work
end
# prompts (via Prompt subclass)
class SlowPrompt < MCP::Prompt
prompt_name "slow_prompt"
def self.template(args, server_context:)
server_context.raise_if_cancelled!
MCP::Prompt::Result.new(messages: [])
end
end
```
Handlers that do not declare a `server_context:` keyword continue to work unchanged -
the opt-in detection only wraps the context when the block signature asks for it.
## Nested Server-to-Client Requests Are Cancelled Automatically
When a tool handler is waiting on a nested server-to-client request
(`server_context.create_sampling_message`, `create_form_elicitation`, or
`create_url_elicitation`), cancelling the parent tool call automatically raises
`MCP::CancelledError` from the nested call, so the tool does not need to wrap it
in its own `cancelled?` checks:
```ruby
def self.call(server_context:)
result = server_context.create_sampling_message(messages: messages, max_tokens: 100)
# If the parent tools/call is cancelled while waiting above, MCP::CancelledError
# is raised here and the tool can let it propagate or clean up as needed.
MCP::Tool::Response.new([{ type: "text", text: result[:content][:text] }])
rescue MCP::CancelledError
# Optional: run cleanup. Re-raising (or letting it propagate) is fine; the server
# will still suppress the JSON-RPC response per the MCP spec.
raise
end
```
Nested cancellation propagation is supported on `StreamableHTTPTransport` only.
`StdioTransport` is single-threaded and blocks on `$stdin.gets`, so a nested
`server_context.create_sampling_message` inside a tool runs to completion even if
the parent `tools/call` is cancelled. The parent tool itself still observes cancellation
via `server_context.cancelled?` between nested calls.
## Client Side
Cancelling a request the client has issued (the `cancellation:` keyword, wire-order guarantees,
and custom transport requirements) is documented on the client [Cancellation](/client/cancellation/) page.

View file

@ -0,0 +1,52 @@
---
layout: default
title: Completions
nav_order: 16
---
# Completions
MCP spec includes [Completions](https://modelcontextprotocol.io/specification/latest/server/utilities/completion),
which enable servers to provide autocompletion suggestions for prompt arguments and resource URIs.
To enable completions, declare the `completions` capability and register a handler:
```ruby
server = MCP::Server.new(
name: "my_server",
prompts: [CodeReviewPrompt],
resource_templates: [FileTemplate],
capabilities: { completions: {} },
)
server.completion_handler do |params|
ref = params[:ref]
argument = params[:argument]
value = argument[:value]
case ref[:type]
when "ref/prompt"
values = case argument[:name]
when "language"
["python", "pytorch", "pyside"].select { |v| v.start_with?(value) }
else
[]
end
{ completion: { values: values, hasMore: false } }
when "ref/resource"
{ completion: { values: [], hasMore: false } }
end
end
```
The handler receives a `params` hash with:
- `ref` - The reference (`{ type: "ref/prompt", name: "..." }` or `{ type: "ref/resource", uri: "..." }`)
- `argument` - The argument being completed (`{ name: "...", value: "..." }`)
- `context` (optional) - Previously resolved arguments (`{ arguments: { ... } }`)
The handler must return a hash with a `completion` key containing `values` (array of strings), and optionally `total` and `hasMore`.
The SDK automatically enforces the 100-item limit per the MCP specification.
The server validates that the referenced prompt, resource, or resource template is registered before calling the handler.
Requests for unknown references return an error.

View file

@ -0,0 +1,206 @@
---
layout: default
title: Configuration
nav_order: 20
---
# Configuration
The gem can be configured using the `MCP.configure` block:
```ruby
MCP.configure do |config|
config.exception_reporter = ->(exception, server_context) {
# Your exception reporting logic here
# For example with Bugsnag:
Bugsnag.notify(exception) do |report|
report.add_metadata(:model_context_protocol, server_context)
end
}
config.around_request = ->(data, &request_handler) {
logger.info("Start: #{data[:method]}")
request_handler.call
logger.info("Done: #{data[:method]}, tool: #{data[:tool_name]}")
}
end
```
or by creating an explicit configuration and passing it into the server.
This is useful for systems where an application hosts more than one MCP server but
they might require different configurations.
```ruby
configuration = MCP::Configuration.new
configuration.exception_reporter = ->(exception, server_context) {
# Your exception reporting logic here
# For example with Bugsnag:
Bugsnag.notify(exception) do |report|
report.add_metadata(:model_context_protocol, server_context)
end
}
configuration.around_request = ->(data, &request_handler) {
logger.info("Start: #{data[:method]}")
request_handler.call
logger.info("Done: #{data[:method]}, tool: #{data[:tool_name]}")
}
server = MCP::Server.new(
# ... all other options
configuration:,
)
```
## Exception Reporter
The exception reporter receives two arguments:
- `exception`: The Ruby exception object that was raised
- `server_context`: A hash containing contextual information about where the error occurred.
This is not the user-defined [`server_context`](/server/server-context/) passed to `Server.new`.
The `server_context` hash includes:
- For request handling failures: `{ request: { ... } }` (the raw JSON-RPC request hash)
- For notification delivery failures: `{ notification: "tools_list_changed" }` (or the relevant notification name)
**Signature:**
```ruby
exception_reporter = ->(exception, server_context) { ... }
```
When an exception occurs:
1. The exception is reported via the configured reporter
2. The client receives a generic JSON-RPC error response (for example, "Internal error calling tool <name>"
for a tool call); the exception's own message is deliberately withheld from clients
If no exception reporter is configured, a default no-op reporter is used that silently ignores exceptions.
## Around Request
The `around_request` hook wraps request handling, allowing you to execute code before and after each request.
This is useful for Application Performance Monitoring (APM) tracing, logging, or other observability needs.
The hook receives a `data` hash and a `request_handler` block. You must call `request_handler.call` to execute the request:
**Signature:**
```ruby
around_request = ->(data, &request_handler) { request_handler.call }
```
**`data` availability by timing:**
- Before `request_handler.call`: `method`, and `server_context` when `instrument_server_context` is enabled
- After `request_handler.call`: `tool_name`, `tool_arguments`, `prompt_name`, `resource_uri`, `error`, `client`
- Not available inside `around_request`: `duration` (added after `around_request` returns)
{: .note }
> `tool_name`, `prompt_name` and `resource_uri` may only be populated for the corresponding request methods
> (`tools/call`, `prompts/get`, `resources/read`), and may not be set depending on how the request is handled
> (for example, `prompt_name` is not recorded when the prompt is not found).
> `duration` is added after `around_request` returns, so it is not visible from within the hook.
**Example:**
```ruby
MCP.configure do |config|
config.around_request = ->(data, &request_handler) {
logger.info("Start: #{data[:method]}")
request_handler.call
logger.info("Done: #{data[:method]}, tool: #{data[:tool_name]}")
}
end
```
### Exposing the User-Defined `server_context`
`data` omits the user-defined `server_context` by default, because that hash is
application-supplied and may hold values a tracing backend should not receive.
Enable it when you need to tag spans with the request's subject:
```ruby
MCP.configure do |config|
config.instrument_server_context = true
config.around_request = ->(data, &request_handler) {
Sentry.set_user(id: data.dig(:server_context, :user_id))
request_handler.call
}
end
```
`data[:server_context]` is the hash passed to `Server.new` - `nil` when the host
set none. It is not the exception reporter's context argument, which describes
where a failure occurred rather than who made the request.
## Instrumentation Callback (soft-deprecated)
{: .note }
> `instrumentation_callback` is soft-deprecated. Use `around_request` instead.
>
> To migrate, wrap the call in `begin/ensure` so the callback still runs when the request fails:
>
> ```ruby
> # Before
> config.instrumentation_callback = ->(data) { log(data) }
>
> # After
> config.around_request = ->(data, &request_handler) do
> request_handler.call
> ensure
> log(data)
> end
> ```
>
> Note that `data[:duration]` is not available inside `around_request`.
> If you need it, measure elapsed time yourself within the hook, or keep using `instrumentation_callback`.
The instrumentation callback is called after each request finishes, whether successfully or with an error.
It receives a hash with the following possible keys:
- `method`: (String) The protocol method called (e.g., "ping", "tools/list")
- `tool_name`: (String, optional) The name of the tool called
- `tool_arguments`: (Hash, optional) The arguments passed to the tool
- `prompt_name`: (String, optional) The name of the prompt called
- `resource_uri`: (String, optional) The URI of the resource called
- `error`: (String, optional) Error code if a lookup failed
- `duration`: (Float) Duration of the call in seconds
- `client`: (Hash, optional) Client information with `name` and `version` keys, from the initialize request
- `server_context`: (Any, optional) The user-defined hash passed to `Server.new`, present only when
`instrument_server_context` is enabled
**Signature:**
```ruby
instrumentation_callback = ->(data) { ... }
```
## Server Protocol Version
The server's protocol version can be overridden using the `protocol_version` keyword argument:
```ruby
configuration = MCP::Configuration.new(protocol_version: "2024-11-05")
MCP::Server.new(name: "test_server", configuration: configuration)
```
If no protocol version is specified, the latest handshake version (`2025-11-25`) is applied by default.
This will make all new server instances use the specified protocol version instead of the default version. The protocol version can be reset to the default by setting it to `nil`:
```ruby
MCP::Configuration.new(protocol_version: nil)
```
If an invalid `protocol_version` value is set, an `ArgumentError` is raised.
The pin scopes the `initialize` handshake, so it accepts handshake versions (`2025-11-25` and earlier) only. Per the SEP-2575 era model,
`2026-07-28` carries its version on every request and has no handshake at all, so there is nothing for a pin to configure there and setting it raises `ArgumentError`;
a client asking `initialize` for a modern version is counter-offered the pinned version (or the latest handshake version), matching the TypeScript and Python SDKs.
Clients reach `2026-07-28` through [`server/discover`](/server/discovery/) and the per-request `_meta` envelope, which the bundled transports serve alongside the handshake with no configuration needed.
Be sure to check the [MCP spec](https://modelcontextprotocol.io/specification/versioning) for the protocol version to understand the supported features for the version being set.

View file

@ -0,0 +1,62 @@
---
layout: default
title: Custom Methods
nav_order: 21
---
# Custom Methods
The server allows you to define custom JSON-RPC methods beyond the standard MCP protocol methods using the `define_custom_method` method:
```ruby
server = MCP::Server.new(name: "my_server")
# Define a custom method that returns a result
server.define_custom_method(method_name: "add") do |params|
params[:a] + params[:b]
end
# Define a custom notification method (returns nil)
server.define_custom_method(method_name: "notify") do |params|
# Process notification
nil
end
```
**Key Features:**
- Accepts any method name as a string
- Block receives the request parameters as a hash
- Can handle both regular methods (with responses) and notifications
- Prevents overriding existing MCP protocol methods
- Supports instrumentation callbacks for monitoring
- Blocks may opt in to a [`server_context:`](/server/server-context/) keyword like the built-in handlers
(see [Cancellation](/server/cancellation/) for an example)
**Usage Example:**
The wire exchange for the custom `add` method defined above. The client sends:
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "add",
"params": { "a": 5, "b": 3 }
}
```
The server responds:
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": 8
}
```
**Error Handling:**
- Raises `MCP::Server::MethodAlreadyDefinedError` if trying to override an existing method
- Supports the same [exception reporting and instrumentation](/server/configuration/) as standard methods

61
docs/_server/discovery.md Normal file
View file

@ -0,0 +1,61 @@
---
layout: default
title: Discovery
nav_order: 3
---
# Discovery
`server/discover` is the sessionless capability discovery method of MCP 2026-07-28 (SEP-2575).
It responds before `initialize` and without an `Mcp-Session-Id`, so a client learns what a server
offers in a single exchange, without creating a session.
## The Discovery Result
The result carries the modern `supportedVersions`, `capabilities`, and `instructions`, together with
the required `ttlMs`/`cacheScope` cache hints, and the server identity as the optional
`io.modelcontextprotocol/serverInfo` stamp in the result `_meta`.
The request takes no parameters:
```json
{ "jsonrpc": "2.0", "id": 1, "method": "server/discover" }
```
The server answers with the full discovery result:
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"supportedVersions": ["2026-07-28"],
"capabilities": { "tools": { "listChanged": true } },
"instructions": "Use the tools of this server as a last resort",
"_meta": { "io.modelcontextprotocol/serverInfo": { "name": "my_server", "version": "1.0.0" } },
"ttlMs": 0,
"cacheScope": "private",
"resultType": "complete"
}
}
```
## The Stateless Modern Lifecycle
Beyond answering `server/discover`, the server serves the full stateless modern lifecycle: requests carrying the SEP-2575 `_meta` envelope
(`io.modelcontextprotocol/protocolVersion`, `clientInfo`, and `clientCapabilities`) are validated per request,
and the Streamable HTTP transport serves them on a sessionless single-exchange path.
Calling `server/discover` first is not required: the envelope alone selects the modern lifecycle for a request,
while a client performing the classic `initialize` handshake is served on the handshake lifecycle -
the lifecycle is a per-request property, not a server-wide mode.
The bundled transports serve `server/discover` and the per-request `_meta` envelope alongside the `initialize` handshake
with no configuration needed. The `protocol_version` pin scopes the handshake only
and does not affect the modern lifecycle; see [Configuration](/server/configuration/) for details.
## Client Side
On the client, `MCP::Client#connect` negotiates the lifecycle automatically by default
(probe `server/discover`, fall back to the `initialize` handshake), `connect(mode: :modern)` skips
the handshake entirely, `connect(mode: :legacy)` forces the classic handshake, and `MCP::Client#discover`
exposes the raw discovery result. See the client-side [Lifecycle](/client/lifecycle/) page for details.

231
docs/_server/elicitation.md Normal file
View file

@ -0,0 +1,231 @@
---
layout: default
title: Elicitation
nav_order: 9
---
# Elicitation
The MCP Ruby SDK supports [elicitation](https://modelcontextprotocol.io/specification/latest/client/elicitation),
which allows servers to request additional information from users through the client during tool execution.
Elicitation is a **server-to-client request**. The server sends a request and blocks until the user responds via the client.
{: .note }
> Unlike [roots](/server/roots/) and [sampling](/server/sampling/), elicitation carries no SEP-2577 deprecation
> and remains fully available. On the [modern lifecycle](/server/discovery/#the-stateless-modern-lifecycle) (MCP 2026-07-28), which forbids server-initiated requests,
> an `elicitation/create` request is embedded in an `input_required` result instead;
> see [Multi-Round-Trip Results](/server/multi-round-trip-results/).
{: .important }
> Per SEP-2260, server-to-client requests (`roots/list`, `sampling/createMessage`, `elicitation/create`) must be associated with
> an originating client request (`ping` is exempt). Use the `server_context` passed to your handler, which stamps the association
> automatically and routes the request onto the originating POST stream on the Streamable HTTP transport. Calling the corresponding
> `ServerSession` methods without `related_request_id:` still works but emits a deprecation warning.
## Capabilities
Clients must declare the `elicitation` capability during initialization. The server checks this before sending any elicitation request
and raises a `RuntimeError` if the client does not support it.
For URL mode support, the client must also declare `elicitation.url` capability.
## Using Elicitation in Tools
Tools that accept a [`server_context:`](/server/server-context/) parameter can call `create_form_elicitation` on it:
```ruby
server.define_tool(name: "collect_info", description: "Collect user info") do |server_context:|
result = server_context.create_form_elicitation(
message: "Please provide your name",
requested_schema: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
},
)
MCP::Tool::Response.new([{ type: "text", text: "Hello, #{result[:content][:name]}" }])
end
```
## Form Mode
Form mode collects structured data from the user directly through the MCP client:
```ruby
server.define_tool(name: "collect_contact", description: "Collect contact info") do |server_context:|
result = server_context.create_form_elicitation(
message: "Please provide your contact information",
requested_schema: {
type: "object",
properties: {
name: { type: "string", description: "Your full name" },
email: { type: "string", format: "email", description: "Your email address" },
},
required: ["name", "email"],
},
)
text = case result[:action]
when "accept"
"Hello, #{result[:content][:name]} (#{result[:content][:email]})"
when "decline"
"User declined"
when "cancel"
"User cancelled"
end
MCP::Tool::Response.new([{ type: "text", text: text }])
end
```
The `requested_schema` must be a flat object schema: a top-level `type: "object"` whose `properties` are limited to
primitive types (`string`, `number`, `integer`, `boolean`). Nested objects and arrays are not allowed, which keeps
the schema simple enough for clients to render as a form. Per the MCP specification, the client validates
the user's input against this schema before returning it, so the `content` of an `accept` response matches the requested shape.
## Default Values and Enums
Properties may declare a `default` value (SEP-1034), which clients use to pre-fill the form.
String properties may declare `enum` values, optionally with human-readable `enumNames` (SEP-1330), which clients render as a choice list:
```ruby
server.define_tool(name: "configure_deploy", description: "Configure a deployment") do |server_context:|
result = server_context.create_form_elicitation(
message: "Configure the deployment",
requested_schema: {
type: "object",
properties: {
replicas: { type: "integer", default: 3 },
verbose: { type: "boolean", default: false },
environment: {
type: "string",
enum: ["dev", "staging", "prod"],
enumNames: ["Development", "Staging", "Production"],
default: "dev",
},
},
required: ["environment"],
},
)
MCP::Tool::Response.new([{ type: "text", text: "Deploying to #{result[:content][:environment]}" }])
end
```
## Enum Schemas
For enumerated choices, use `MCP::Elicitation::EnumSchema` to construct the canonical schema shapes per
[SEP-1330](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1330) instead of building
the underlying Hash by hand. The five class methods cover titled and untitled, single-select and multi-select,
plus the legacy `enumNames` form retained for backward compatibility:
```ruby
size_schema = MCP::Elicitation::EnumSchema.titled_single_select(
options: [
{ value: "s", title: "Small" },
{ value: "m", title: "Medium" },
{ value: "l", title: "Large" },
],
default: "m",
)
tags_schema = MCP::Elicitation::EnumSchema.untitled_multi_select(
values: ["urgent", "billing", "feedback"],
)
result = server_context.create_form_elicitation(
message: "Tell us about your order",
requested_schema: {
type: "object",
properties: {
size: size_schema.to_h,
tags: tags_schema.to_h,
},
required: ["size"],
},
)
```
The available builders are `untitled_single_select`, `titled_single_select`, `untitled_multi_select`, `titled_multi_select`,
and `legacy_titled`. Each accepts optional `default:`, `title:`, and `description:`.
The same builders produce the `requestedSchema` of an `elicitation/create` request embedded in a SEP-2322 `input_required` result,
which is how elicitation reaches clients on the stateless 2026-07-28 lifecycle:
```ruby
MCP::Server::InputRequiredResult.new(
input_requests: {
"size" => {
method: "elicitation/create",
params: {
message: "Pick a size",
requestedSchema: {
type: "object",
properties: { size: size_schema.to_h },
required: ["size"],
},
},
},
},
)
```
## URL Mode
URL mode directs the user to an external URL for out-of-band interactions such as OAuth flows:
```ruby
server.define_tool(name: "authorize_github", description: "Authorize GitHub") do |server_context:|
elicitation_id = SecureRandom.uuid
result = server_context.create_url_elicitation(
message: "Please authorize access to your GitHub account",
url: "https://example.com/oauth/authorize?elicitation_id=#{elicitation_id}",
elicitation_id: elicitation_id,
)
server_context.notify_elicitation_complete(elicitation_id: elicitation_id)
MCP::Tool::Response.new([{ type: "text", text: "Authorization complete" }])
end
```
## URLElicitationRequiredError
When a tool cannot proceed until an out-of-band elicitation is completed, raise `MCP::Server::URLElicitationRequiredError`.
This returns a JSON-RPC error with code `-32042` to the client:
```ruby
server.define_tool(name: "access_github", description: "Access GitHub") do |server_context:|
raise MCP::Server::URLElicitationRequiredError.new([
{
mode: "url",
elicitationId: SecureRandom.uuid,
url: "https://example.com/oauth/authorize",
message: "GitHub authorization is required.",
},
])
end
```
## Timeouts
Every server-to-client request is bounded, so a client that never answers cannot park the handler's thread indefinitely.
`MCP::Server::Transports::StreamableHTTPTransport` waits `server_to_client_request_timeout:` seconds (600 by default), then tells
the client the request was abandoned and raises `MCP::Server::RequestTimeoutError`. Individual calls override the deadline with `timeout:`,
which is the knob to reach for when a prompt legitimately waits on a person:
```ruby
server_context.create_form_elicitation(
message: "Approve this deployment?",
requested_schema: { type: "object", properties: { approved: { type: "boolean" } } },
timeout: 3600, # This one waits up to an hour.
)
```
`StdioTransport` is not bounded and ignores `timeout:`: it owns the client process, so a client that stops answering
surfaces as end-of-file rather than as a wait that never ends.
The same timeout applies to [roots](/server/roots/) and [sampling](/server/sampling/) requests.

53
docs/_server/index.md Normal file
View file

@ -0,0 +1,53 @@
---
layout: default
title: Overview
nav_order: 1
permalink: /server/
redirect_from:
- /building-servers.html
- /building-servers/
---
# Building an MCP Server
The `MCP::Server` class is the core component that handles JSON-RPC requests and responses.
It implements the Model Context Protocol specification, handling model context requests and responses.
## Key Features
- Implements JSON-RPC 2.0 message handling
- Supports protocol initialization and capability negotiation
- Manages tool registration and invocation
- Supports prompt registration and execution
- Supports resource registration and retrieval
- Supports stdio and Streamable HTTP (including SSE) transports
- Supports notifications for list changes (tools, prompts, resources)
- Supports roots (server-to-client filesystem boundary queries)
- Supports sampling (server-to-client LLM completion requests)
- Supports cursor-based pagination for list operations
- Supports cancellation of in-flight requests on both server and client (notifications/cancelled)
## Supported Methods
- `initialize` - Initializes the protocol and returns server capabilities
- `server/discover` - Sessionless capability discovery (MCP 2026-07-28, SEP-2575): returns the server's capabilities
before `initialize` and without an `Mcp-Session-Id`, and anchors the stateless modern lifecycle; see [Discovery](/server/discovery/)
- `subscriptions/listen` - Long-lived notification subscription stream (MCP 2026-07-28, SEP-2575), replacing the legacy HTTP GET
listening stream; see [Notification Subscriptions](/server/notification-subscriptions/)
- Multi round-trip `input_required` results (MCP 2026-07-28, SEP-2322): handlers return `MCP::Server::InputRequiredResult` to ask
the client for additional input instead of performing a server-initiated request; see [Multi-Round-Trip Results](/server/multi-round-trip-results/)
- `ping` - Simple health check
- `logging/setLevel` - Configures the minimum log level for the server
- `tools/list` - Lists all registered tools and their schemas
- `tools/call` - Invokes a specific tool with provided arguments
- `prompts/list` - Lists all registered prompts and their schemas
- `prompts/get` - Retrieves a specific prompt by name
- `resources/list` - Lists all registered resources and their schemas
- `resources/read` - Retrieves a specific resource by name
- `resources/templates/list` - Lists all registered resource templates and their schemas
- `resources/subscribe` - Subscribes to updates for a specific resource
- `resources/unsubscribe` - Unsubscribes from updates for a specific resource
- `completion/complete` - Returns autocompletion suggestions for prompt arguments and resource URIs
- `roots/list` - Requests filesystem roots from the client (server-to-client)
- `sampling/createMessage` - Requests LLM completion from the client (server-to-client)
- `elicitation/create` - Requests user input from the client (server-to-client)

90
docs/_server/logging.md Normal file
View file

@ -0,0 +1,90 @@
---
layout: default
title: Logging
nav_order: 17
---
# Logging
The MCP Ruby SDK supports structured logging through the `notify_log_message` method, following the [MCP Logging specification](https://modelcontextprotocol.io/specification/latest/server/utilities/logging).
The `notifications/message` notification is used for structured logging between client and server.
{: .warning }
> MCP Logging (`logging/setLevel` and `notifications/message`) is deprecated as of protocol version `2026-07-28` (SEP-2577),
> while remaining fully supported under `2025-11-25`. Use stderr or OpenTelemetry for new servers.
{: .note }
> On the [modern lifecycle](/server/discovery/#the-stateless-modern-lifecycle), where `logging/setLevel` does not exist, the level
> comes per request from the `io.modelcontextprotocol/logLevel` `_meta` member: it authorizes
> `notifications/message` for that request only, delivered on the request's own response stream.
> A request without the member (or with an unrecognized level) receives no log messages.
## Log Levels
The SDK supports 8 log levels with increasing severity:
- `debug` - Detailed debugging information
- `info` - General informational messages
- `notice` - Normal but significant events
- `warning` - Warning conditions
- `error` - Error conditions
- `critical` - Critical conditions
- `alert` - Action must be taken immediately
- `emergency` - System is unusable
## How Logging Works
1. **Client Configuration**: The client sends a `logging/setLevel` request to configure the minimum log level
2. **Server Filtering**: The server only sends log messages at the configured level or higher severity
3. **Notification Delivery**: Log messages are sent as `notifications/message` to the client
For example, if the client sets the level to `"error"` (severity 4), the server will send messages with levels: `error`, `critical`, `alert`, and `emergency`.
For more details, see the [MCP Logging specification](https://modelcontextprotocol.io/specification/latest/server/utilities/logging).
**Usage Example:**
The client first configures the level with a `logging/setLevel` request:
```json
{ "jsonrpc": "2.0", "id": 1, "method": "logging/setLevel", "params": { "level": "info" } }
```
The server then emits messages at each severity; only those at or above the configured level are delivered:
```ruby
server = MCP::Server.new(name: "my_server")
transport = MCP::Server::Transports::StdioTransport.new(server)
server.notify_log_message(
data: { message: "Application started successfully" },
level: "info"
)
server.notify_log_message(
data: { message: "Configuration file not found, using defaults" },
level: "warning"
)
server.notify_log_message(
data: {
error: "Database connection failed",
details: { host: "localhost", port: 5432 }
},
level: "error",
logger: "DatabaseLogger" # Optional logger name
)
```
**Key Features:**
- Server has capability `logging` to send log messages
- Messages are only sent if a transport is configured
- Messages are filtered based on the client's configured log level
- If the log level hasn't been set by the client, no messages will be sent
## Transport Support
- **stdio**: Notifications are sent as JSON-RPC 2.0 messages to stdout
- **Streamable HTTP**: Notifications are sent as JSON-RPC 2.0 messages over HTTP with streaming (chunked transfer or SSE)

View file

@ -0,0 +1,85 @@
---
layout: default
title: Multi-Round-Trip Results
nav_order: 10
---
# Multi-Round-Trip Results
The [modern lifecycle](/server/discovery/#the-stateless-modern-lifecycle) (MCP 2026-07-28) forbids server-initiated requests. Instead, per SEP-2322, a `tools/call`, `prompts/get`, or `resources/read` handler that
opts in to [`server_context:`](/server/server-context/) may return `MCP::Server::InputRequiredResult.new(input_requests:, request_state:)` to ask the client for
additional input (`elicitation/create`, `sampling/createMessage`, or `roots/list` shapes):
```ruby
class GreetingTool < MCP::Tool
description "Greets the user by name"
def self.call(server_context:, **_args)
response = server_context.input_response("user_name")
unless response
return MCP::Server::InputRequiredResult.new(
input_requests: {
user_name: {
method: "elicitation/create",
params: {
message: "What is your name?",
requestedSchema: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
},
},
},
},
)
end
MCP::Tool::Response.new([{ type: "text", text: "Hello, #{response.dig(:content, :name)}!" }])
end
end
```
## Deterministic Replay
On the retried request the handler re-runs from the start and reads the answers via
`server_context.input_responses` / `server_context.input_response(key)` and the echoed opaque `server_context.request_state`
(deterministic replay; the server holds no memory between rounds). The server returns `-32021`
when an embedded request needs a client capability the request did not declare.
## Securing `requestState`
The echoed `requestState` arrives as client-controlled input: pass `MCP::Server::RequestStateSecurity.new(key:)` (a 32-byte key) via
`Server.new(request_state_security:)` to have it sealed with AES-256-GCM and bound to a TTL plus the originating method, target, and arguments,
all transparently to handlers. Multi-process deployments must share the key across workers; without `request_state_security:` the state crosses
the wire exactly as the handler wrote it and protecting it is the handler author's responsibility.
```ruby
server = MCP::Server.new(
name: "my_server",
tools: [GreetingTool],
request_state_security: MCP::Server::RequestStateSecurity.new(key: ENV.fetch("MCP_REQUEST_STATE_KEY")),
)
```
## `resultType` Stamping
SEP-2322 also makes `resultType` a required member of every result a 2026-07-28 server returns. The server stamps `resultType: "complete"`
on all results of requests carrying the modern `_meta` envelope (and on `server/discover` results), while results that already carry
a discriminator (`"input_required"`, the tasks extension's `"task"`) keep it. Legacy results stay unstamped, and clients treat an absent
`resultType` as `"complete"` per the spec.
## Legacy Clients
Handlers written in the 2026 style serve pre-2026 clients too: when a `tools/call`, `prompts/get`, or `resources/read` handler returns
an `InputRequiredResult` on the legacy wire, the server fulfills it in place of the client's driver. Each `inputRequests` entry is sent
as the equivalent real server-to-client request (`elicitation/create`, `sampling/createMessage`, `roots/list`), associated with
the originating request per SEP-2260; the answers are collected under the same keys, and the handler re-runs with
`server_context.input_responses` populated and the raw `requestState` echoed, the same deterministic replay contract
the modern client driver follows. The shim is on by default (matching the TypeScript SDK) and capped at 8 rounds;
`MCP::Server.new(input_required_legacy_shim: false)` restores the strict rejection of `input_required` results on legacy requests.
## Client Side
`call_tool`, `get_prompt`, and `read_resource` drive `input_required` results automatically once the matching handlers are registered;
see the client [Multi-Round-Trip Results](/client/multi-round-trip-results/) page.

View file

@ -0,0 +1,71 @@
---
layout: default
title: Notification Subscriptions
nav_order: 12
---
# Notification Subscriptions
`subscriptions/listen` is the long-lived notification subscription stream of MCP 2026-07-28 (SEP-2575),
replacing the legacy HTTP GET listening stream.
## Subscribing
The client opts in via the `notifications` filter (`toolsListChanged` / `promptsListChanged` / `resourcesListChanged` / `resourceSubscriptions`),
the server acknowledges the honored subset with `notifications/subscriptions/acknowledged` as the first stream message,
and every delivered notification carries the correlating `io.modelcontextprotocol/subscriptionId` in `_meta`.
The honored subset follows the capabilities the server declares (`listChanged` and `subscribe` flags).
The client opens the stream with a `subscriptions/listen` request:
```json
{
"jsonrpc": "2.0",
"id": "listen-1",
"method": "subscriptions/listen",
"params": {
"notifications": { "toolsListChanged": true, "resourceSubscriptions": ["file:///a.txt"] },
"_meta": {
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
"io.modelcontextprotocol/clientInfo": { "name": "my_client", "version": "1.0.0" },
"io.modelcontextprotocol/clientCapabilities": {}
}
}
}
```
The first SSE event on the stream is the acknowledgement:
```json
{
"jsonrpc": "2.0",
"method": "notifications/subscriptions/acknowledged",
"params": {
"notifications": { "toolsListChanged": true, "resourceSubscriptions": ["file:///a.txt"] },
"_meta": { "io.modelcontextprotocol/subscriptionId": "listen-1" }
}
}
```
## Transport Support
The stream is served on the Streamable HTTP modern path; stdio answers `-32601`.
## Limits and Keepalives
Concurrent streams are capped by `max_listen_subscriptions:` (default 1000; pass `nil` to remove the cap), and each stream receives an SSE keepalive
comment frame every `listen_keepalive_interval:` seconds (default 15) so a dropped connection frees its slot; pass `listen_keepalive_interval: nil`
when an upstream proxy already keeps the stream alive.
```ruby
transport = MCP::Server::Transports::StreamableHTTPTransport.new(
server,
max_listen_subscriptions: 500,
listen_keepalive_interval: 30,
)
```
A stream stays open until the client closes the connection; on graceful shutdown via `transport.close`,
each open stream receives its `subscriptions/listen` response before closing.
See [Notifications](/server/notifications/) for the notification types themselves and their session scoping.

View file

@ -0,0 +1,68 @@
---
layout: default
title: Notifications
nav_order: 11
---
# Notifications
The server supports sending notifications to clients when lists of tools, prompts, or resources change. This enables real-time updates without polling.
## Notification Methods
The server provides the following notification methods:
- `notify_tools_list_changed` - Send a notification when the tools list changes
- `notify_prompts_list_changed` - Send a notification when the prompts list changes
- `notify_resources_list_changed` - Send a notification when the resources list changes
- `notify_log_message` - Send a structured logging notification message (see [Logging](/server/logging/))
## Session Scoping
When using Streamable HTTP transport with multiple clients, each client connection gets its own session. Notifications are scoped as follows:
- **`report_progress`** and **`notify_log_message`** called via [`server_context`](/server/server-context/) inside a tool handler are automatically sent only to the requesting client.
No extra configuration is needed.
- **`notify_tools_list_changed`**, **`notify_prompts_list_changed`**, and **`notify_resources_list_changed`** are always broadcast to all connected clients,
as they represent server-wide state changes. These should be called on the `server` instance directly.
On the [modern lifecycle](/server/discovery/#the-stateless-modern-lifecycle) (MCP 2026-07-28), clients receive these broadcasts through the `subscriptions/listen` stream;
see [Notification Subscriptions](/server/notification-subscriptions/).
## Notification Format
Notifications follow the JSON-RPC 2.0 specification and use these method names:
- `notifications/tools/list_changed`
- `notifications/prompts/list_changed`
- `notifications/resources/list_changed`
- `notifications/cancelled` (see [Cancellation](/server/cancellation/))
- `notifications/progress` (see [Progress](/server/progress/))
- `notifications/message` (see [Logging](/server/logging/))
## Broadcasting a List Change
Call the matching `notify_*` method after changing a server-wide list:
```ruby
server = MCP::Server.new(name: "my_server")
# Default Streamable HTTP - session oriented
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
# When tools change, notify clients
server.define_tool(name: "new_tool") { |**args| MCP::Tool::Response.new([{ type: "text", text: "ok" }]) }
server.notify_tools_list_changed
# When prompts change, notify clients
server.define_prompt(name: "new_prompt") do |args, server_context:|
MCP::Prompt::Result.new(messages: [])
end
server.notify_prompts_list_changed
# When resources change, notify clients
server.define_resource(uri: "resource://new", name: "new_resource", mime_type: "text/plain") do
[MCP::Resource::TextContents.new(uri: "resource://new", mime_type: "text/plain", text: "contents")]
end
server.notify_resources_list_changed
```

View file

@ -0,0 +1,78 @@
---
layout: default
title: Pagination
nav_order: 18
---
# Pagination
The MCP Ruby SDK supports [pagination](https://modelcontextprotocol.io/specification/latest/server/utilities/pagination)
for list operations that may return large result sets. Pagination uses string cursor tokens carrying a zero-based offset,
treated as opaque by clients: the server decides page size, and the client follows `nextCursor` until the server omits it.
Pagination applies to `tools/list`, `prompts/list`, `resources/list`, and `resources/templates/list`,
including lists produced by a custom `resources_list_handler`.
## Enabling Pagination
Pass `page_size:` to `MCP::Server.new` to split list responses into pages. When `page_size` is omitted (the default),
list responses contain all items in a single response, preserving the pre-pagination behavior.
```ruby
server = MCP::Server.new(
name: "my_server",
tools: tools,
page_size: 50,
)
```
When `page_size` is set, list responses include a `nextCursor` field whenever more pages are available:
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"tools": [
{ "name": "example_tool" }
],
"nextCursor": "50"
}
}
```
Invalid cursors (e.g. non-numeric, negative, or out-of-range) are rejected with JSON-RPC error code `-32602 (Invalid params)` per the MCP specification.
## List Result Caching
Per SEP-2549, list and read results can carry cache hints telling clients how long a result stays fresh (`ttlMs`, max-age semantics in milliseconds;
`0` means do not cache) and whether shared intermediaries may cache it (`cacheScope`: `"public"` or `"private"`).
Emission is opt-in: pass `ttl_ms:` and/or `cache_scope:` to `MCP::Server.new` and both fields are added to `tools/list`, `prompts/list`, `resources/list`,
`resources/templates/list`, and `resources/read` results (a missing field is filled with the defaults `ttlMs: 0` / `cacheScope: "private"`,
the scope that keeps a potentially user-dependent result out of shared caches).
When neither is set, responses are serialized exactly as before.
The 2026-07-28 revision makes both hints required on these results, so on requests carrying the modern `_meta` envelope
the server always emits them, filling unset values with the same defaults; legacy protocol versions keep the opt-in behavior.
```ruby
server = MCP::Server.new(
name: "my_server",
tools: tools,
ttl_ms: 60_000, # results stay fresh for one minute
cache_scope: "private", # only the requesting client may cache them
)
```
A `resources_read_handler` can override the hints per result by returning a full result hash instead of bare contents:
```ruby
server.resources_read_handler do |params|
{ contents: [{ uri: params[:uri], mimeType: "text/plain", text: "..." }], ttlMs: 5_000 }
end
```
## Client Side
Iterating pages, fetching whole collections with the `max_pages` guard, and reading the cache hints
on the result structs are documented on the client [Pagination](/client/pagination/) page.

50
docs/_server/ping.md Normal file
View file

@ -0,0 +1,50 @@
---
layout: default
title: Ping
nav_order: 15
---
# Ping
The MCP Ruby SDK supports
the [MCP `ping` utility](https://modelcontextprotocol.io/specification/2025-11-25/basic/utilities/ping),
which allows either side of the connection to verify that the peer is still responsive.
A `ping` request has no parameters, and the receiver MUST respond promptly with an empty result.
{: .note }
> `ping` belongs to the handshake lifecycle: MCP 2026-07-28 removes the method altogether (SEP-2575),
> since requests of the [modern lifecycle](/server/discovery/#the-stateless-modern-lifecycle) are single POST exchanges whose connection
> itself signals liveness, leaving nothing to probe between requests. The server answers `ping` on
> the handshake lifecycle only - a modern request naming it is rejected with `-32601` Method not found -
> and calling `ping` on a `server_context` while serving a modern request raises an error. The long-lived
> [`subscriptions/listen`](/server/notification-subscriptions/) stream is kept alive by SSE keepalive
> frames instead.
Servers respond to incoming `ping` requests automatically - no setup is required.
Any `MCP::Server` instance replies with an empty result.
Servers can also send `ping` requests to the client via `ServerSession#ping`.
`ping` is exempt from the SEP-2260 association requirement, so it may also be sent outside a handler.
Inside a tool handler that receives [`server_context:`](/server/server-context/), call `ping` on it:
```ruby
class HealthCheckTool < MCP::Tool
description "Verifies the client is still responsive"
def self.call(server_context:)
server_context.ping # => {} on success
MCP::Tool::Response.new([{ type: "text", text: "client is alive" }])
end
end
```
`#ping` raises `MCP::Server::ValidationError` when the client returns a `result`
that is not a Hash. Transport-level errors (e.g., the client returning a JSON-RPC error)
propagate as exceptions raised by the transport layer.
Server-to-client requests are bounded by a timeout on the Streamable HTTP transport; see [Timeouts](/server/elicitation/#timeouts).
## Client Side
Pinging the server with `MCP::Client#ping` is documented on the client [Ping](/client/ping/) page.

64
docs/_server/progress.md Normal file
View file

@ -0,0 +1,64 @@
---
layout: default
title: Progress
nav_order: 14
---
# Progress
The MCP Ruby SDK supports progress tracking for long-running tool operations,
following the [MCP Progress specification](https://modelcontextprotocol.io/specification/latest/server/utilities/progress).
## How Progress Works
1. **Client Request**: The client sends a `progressToken` in the `_meta` field when calling a tool
2. **Server Notification**: The server sends `notifications/progress` messages back to the client during tool execution
3. **Tool Integration**: Tools call `server_context.report_progress` to report incremental progress
## Reporting Progress from Tools
Tools that accept a [`server_context:`](/server/server-context/) parameter can call `report_progress` on it.
The server automatically wraps the context in an `MCP::ServerContext` instance that provides this method:
```ruby
class LongRunningTool < MCP::Tool
description "A tool that reports progress during execution"
input_schema(
properties: {
count: { type: "integer" },
},
required: ["count"]
)
def self.call(count:, server_context:)
count.times do |i|
# Do work here.
server_context.report_progress(i + 1, total: count, message: "Processing item #{i + 1}")
end
MCP::Tool::Response.new([{ type: "text", text: "Done" }])
end
end
```
The `server_context.report_progress` method accepts:
- `progress` (required) - current progress value (numeric)
- `total:` (optional) - total expected value, so clients can display a percentage
- `message:` (optional) - human-readable status message
`report_progress` is a no-op when the request carried no `progressToken`, and both numeric and
string tokens are supported.
{: .note }
> On the [modern lifecycle](/server/discovery/#the-stateless-modern-lifecycle), progress notifications emitted during a request ride
> the request's own SSE response stream. The bundled transport buffers them and flushes after
> the handler returns, so they preserve order but arrive together with the final response rather
> than in real time.
## Client Side
Requesting progress is the client's side of the contract: pass `progress_token:` to `MCP::Client#call_tool`
and the token is sent as `_meta.progressToken`, as shown on the client [Transports](/client/transports/) page.
The bundled client transports do not currently expose a callback for observing the incoming `notifications/progress` messages;
the token's effect is visible on the server side only.

194
docs/_server/prompts.md Normal file
View file

@ -0,0 +1,194 @@
---
layout: default
title: Prompts
nav_order: 5
---
# Prompts
MCP spec includes [Prompts](https://modelcontextprotocol.io/specification/latest/server/prompts), which enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs.
## Defining Prompts
The `MCP::Prompt` class provides three ways to create prompts.
### 1. As a class definition with metadata
Subclass `MCP::Prompt` and declare the metadata with class-level helpers; `template` builds the result:
```ruby
class MyPrompt < MCP::Prompt
prompt_name "my_prompt" # Optional - defaults to underscored class name
title "My Prompt"
description "This prompt performs specific functionality..."
arguments [
MCP::Prompt::Argument.new(
name: "message",
title: "Message Title",
description: "Input message",
required: true
)
]
meta({ version: "1.0", category: "example" })
class << self
def template(args, server_context:)
MCP::Prompt::Result.new(
description: "Response description",
messages: [
MCP::Prompt::Message.new(
role: "user",
content: MCP::Content::Text.new("User message")
),
MCP::Prompt::Message.new(
role: "assistant",
content: MCP::Content::Text.new(args["message"])
)
]
)
end
end
end
prompt = MyPrompt
```
### 2. Using the `MCP::Prompt.define` method
`MCP::Prompt.define` builds a prompt from keyword arguments, with the block as its template:
```ruby
prompt = MCP::Prompt.define(
name: "my_prompt",
title: "My Prompt",
description: "This prompt performs specific functionality...",
arguments: [
MCP::Prompt::Argument.new(
name: "message",
title: "Message Title",
description: "Input message",
required: true
)
],
meta: { version: "1.0", category: "example" }
) do |args, server_context:|
MCP::Prompt::Result.new(
description: "Response description",
messages: [
MCP::Prompt::Message.new(
role: "user",
content: MCP::Content::Text.new("User message")
),
MCP::Prompt::Message.new(
role: "assistant",
content: MCP::Content::Text.new(args["message"])
)
]
)
end
```
### 3. Using the `MCP::Server#define_prompt` method
`MCP::Server#define_prompt` registers the prompt directly on a server instance:
```ruby
server = MCP::Server.new
server.define_prompt(
name: "my_prompt",
description: "This prompt performs specific functionality...",
arguments: [
Prompt::Argument.new(
name: "message",
title: "Message Title",
description: "Input message",
required: true
)
],
meta: { version: "1.0", category: "example" }
) do |args, server_context:|
Prompt::Result.new(
description: "Response description",
messages: [
Prompt::Message.new(
role: "user",
content: Content::Text.new("User message")
),
Prompt::Message.new(
role: "assistant",
content: Content::Text.new(args["message"])
)
]
)
end
```
The [`server_context`](/server/server-context/) parameter is the `server_context` passed into the server and can be used to pass per request information,
e.g. around authentication state or user preferences.
## Key Components
- `MCP::Prompt::Argument` - Defines input parameters for the prompt template with name, title, description, and required flag
- `MCP::Prompt::Message` - Represents a message in the conversation with a role and content
- `MCP::Prompt::Result` - The output of a prompt template containing description and messages
- `MCP::Content::Text` - Text content for messages
## Registering Prompts
Register prompts with the MCP server:
```ruby
server = MCP::Server.new(
name: "my_server",
prompts: [MyPrompt],
server_context: { user_id: current_user.id },
)
```
The server will handle prompt listing and execution through the MCP protocol methods:
- `prompts/list` - Lists all registered prompts and their schemas
- `prompts/get` - Retrieves and executes a specific prompt with arguments
## Prompts with Image and Embedded Resource Content
Prompt messages are not limited to text. The same `MCP::Content` types used in [tool responses](/server/tools/) can be used as message content,
letting a prompt template include images or inline resource contents. Unlike tool responses, the content object is passed directly rather than as a hash;
`MCP::Prompt::Message` serializes it when the prompt result is returned:
```ruby
class CodeReviewPrompt < MCP::Prompt
prompt_name "code_review"
description "Review a source file with an accompanying diagram"
arguments [
MCP::Prompt::Argument.new(name: "file_uri", description: "URI of the file to review", required: true),
]
class << self
def template(args, server_context:)
MCP::Prompt::Result.new(
messages: [
MCP::Prompt::Message.new(
role: "user",
content: MCP::Content::EmbeddedResource.new(
MCP::Resource::TextContents.new(
uri: args["file_uri"],
mime_type: "text/x-ruby",
text: read_source(args["file_uri"]),
),
),
),
MCP::Prompt::Message.new(
role: "user",
content: MCP::Content::Image.new(architecture_diagram_base64, "image/png"),
),
MCP::Prompt::Message.new(
role: "user",
content: MCP::Content::Text.new("Please review the code above, using the diagram for context."),
),
],
)
end
end
end
```

319
docs/_server/resources.md Normal file
View file

@ -0,0 +1,319 @@
---
layout: default
title: Resources
nav_order: 6
---
# Resources
MCP spec includes [Resources](https://modelcontextprotocol.io/specification/latest/server/resources).
## Defining Resources
Like [tools](/server/tools/) and [prompts](/server/prompts/), resources can be defined in three ways.
### 1. As a class definition
Define a class that inherits from `MCP::Resource`, implementing `contents` to serve the resource body:
```ruby
class MyResource < MCP::Resource
uri "https://example.com/my_resource"
resource_name "my-resource"
title "My Resource"
description "Lorem ipsum dolor sit amet"
mime_type "text/html"
class << self
def contents
[MCP::Resource::TextContents.new(
uri: uri,
mime_type: mime_type,
text: "Hello from example resource!"
)]
end
end
end
server = MCP::Server.new(
name: "my_server",
resources: [MyResource],
)
```
`resources/read` requests are routed automatically: when the requested URI matches a registered
class-based resource, its `contents` method is called. `contents` may return an array of
`MCP::Resource::TextContents` / `MCP::Resource::BlobContents` objects (or plain hashes), or a single one.
Like tools, `contents` can opt in to a [`server_context:`](/server/server-context/) keyword argument to receive per-request context.
When class-based resources or resource templates are registered and a `resources/read` request
does not match any of them, the server responds with the standard JSON-RPC Invalid Params error
(`-32602`) carrying the requested URI in the error `data` member, per SEP-2164.
### 2. Using the `MCP::Resource.define` method
The block implements `contents`:
```ruby
resource = MCP::Resource.define(
uri: "https://example.com/my_resource",
name: "my-resource",
mime_type: "text/html",
) do
[MCP::Resource::TextContents.new(uri: uri, mime_type: mime_type, text: "Hello!")]
end
```
### 3. Using the `MCP::Server#define_resource` method
`MCP::Server#define_resource` registers the resource directly on a server instance:
```ruby
server = MCP::Server.new(name: "my_server")
server.define_resource(
uri: "https://example.com/my_resource",
name: "my-resource",
mime_type: "text/html",
) do
[MCP::Resource::TextContents.new(uri: "https://example.com/my_resource", mime_type: "text/html", text: "Hello!")]
end
```
Alternatively, resources can be registered as plain data objects with `MCP::Resource.new`,
in which case the server only lists them:
```ruby
resource = MCP::Resource.new(
uri: "https://example.com/my_resource",
name: "my-resource",
title: "My Resource",
description: "Lorem ipsum dolor sit amet",
mime_type: "text/html",
)
server = MCP::Server.new(
name: "my_server",
resources: [resource],
)
```
With plain data resources, the server must register a handler for the `resources/read` method to
retrieve a resource dynamically.
```ruby
server.resources_read_handler do |params|
[{
uri: params[:uri],
mimeType: "text/plain",
text: "Hello from example resource! URI: #{params[:uri]}"
}]
end
```
otherwise `resources/read` requests will be a no-op. Note that a `resources_read_handler` fully replaces
the default `resources/read` handling, including the automatic routing to class-based resources described above.
To make the resource *list* depend on the request, register a `resources_list_handler`. The block returns the resource collection to serve,
so the visible resources can vary by the authenticated principal or the granted scope. The framework paginates the returned array
and stamps the same cache hints it applies to the constructor-provided resources, so the block returns only the array.
A block that declares `server_context:` receives it:
```ruby
server.resources_list_handler do |params, server_context:|
server_context[:authenticated] ? real_resources : demo_resources
end
```
The block is invoked once per page, so it must return a stable ordering across the pages of one query; the cursor is a positional offset
into the returned collection. When no handler is set, the resources passed to `MCP::Server.new` are served unchanged.
For unknown URIs, raise `MCP::Server::ResourceNotFoundError` from the handler.
Per SEP-2164, the server then responds with the standard JSON-RPC Invalid Params error (`-32602`)
carrying the requested URI in the error `data` member:
```ruby
server.resources_read_handler do |params|
resource = lookup(params[:uri])
raise MCP::Server::ResourceNotFoundError.new(params[:uri], params) unless resource
[{ uri: params[:uri], mimeType: resource.mime_type, text: resource.body }]
end
```
## Reading Binary Resources
For binary resources, respond with a base64-encoded `blob` field instead of `text`.
The `MCP::Resource::TextContents` and `MCP::Resource::BlobContents` classes build the two contents shapes defined by the spec:
```ruby
server.resources_read_handler do |params|
case params[:uri]
when "file:///logo.png"
[
MCP::Resource::BlobContents.new(
uri: params[:uri],
mime_type: "image/png",
data: Base64.strict_encode64(File.binread("logo.png")),
).to_h,
]
else
[
MCP::Resource::TextContents.new(
uri: params[:uri],
mime_type: "text/plain",
text: "Hello from example resource!",
).to_h,
]
end
end
```
## Resource Templates
Resource templates follow the same pattern. Class-based templates declare a `uri_template` and
receive the variables extracted from the requested URI as keyword arguments to `contents`:
```ruby
class UserProfileTemplate < MCP::ResourceTemplate
uri_template "users://{user_id}/profile"
resource_template_name "user-profile"
title "User Profile"
description "Profile data for a user"
mime_type "application/json"
class << self
def contents(user_id:)
[MCP::Resource::TextContents.new(
uri: "users://#{user_id}/profile",
mime_type: mime_type,
text: { id: user_id }.to_json
)]
end
end
end
server = MCP::Server.new(
name: "my_server",
resource_templates: [UserProfileTemplate],
)
```
A `resources/read` request for `users://42/profile` calls `UserProfileTemplate.contents(user_id: "42")`.
An exact match against a registered resource takes precedence over template matching.
`contents` can also opt in to a `server_context:` keyword argument.
URI template matching supports simple [RFC 6570](https://www.rfc-editor.org/rfc/rfc6570) level 1 `{variable}` expressions only:
- Operator expressions such as `{+path}`, `{#fragment}`, or `{?query}` are treated as literal text and never match an expanded URI.
- A variable matches one or more characters excluding `/`.
- Extracted values are not percent-decoded.
The `MCP::ResourceTemplate.define` and `MCP::Server#define_resource_template` methods are also available,
mirroring the resource variants:
```ruby
server.define_resource_template(
uri_template: "users://{user_id}/profile",
name: "user-profile",
mime_type: "application/json",
) do |user_id:|
[MCP::Resource::TextContents.new(
uri: "users://#{user_id}/profile",
mime_type: "application/json",
text: { id: user_id }.to_json
)]
end
```
Resource templates can also be registered as plain data objects with `MCP::ResourceTemplate.new`,
in which case reads must be served by a `resources_read_handler`:
```ruby
resource_template = MCP::ResourceTemplate.new(
uri_template: "https://example.com/my_resource_template",
name: "my-resource-template",
title: "My Resource Template",
description: "Lorem ipsum dolor sit amet",
mime_type: "text/html",
)
server = MCP::Server.new(
name: "my_server",
resource_templates: [resource_template],
)
```
Registered templates are listed through the `resources/templates/list` protocol method.
To serve reads for URIs that match a template, extract the variable parts of the URI in your `resources_read_handler`:
```ruby
resource_template = MCP::ResourceTemplate.new(
uri_template: "file:///items/{item_id}",
name: "item",
mime_type: "application/json",
)
server = MCP::Server.new(name: "my_server", resource_templates: [resource_template])
server.resources_read_handler do |params|
if (match = params[:uri].match(%r{\Afile:///items/(?<item_id>[^/]+)\z}))
[{
uri: params[:uri],
mimeType: "application/json",
text: { id: match[:item_id] }.to_json,
}]
else
raise MCP::Server::ResourceNotFoundError.new(params[:uri], params)
end
end
```
## Resource Subscriptions
Resource subscriptions allow clients to monitor specific resources for changes.
When a subscribed resource is updated, the server sends a notification to the client.
The SDK does not track subscription state internally.
Server developers register handlers and manage their own subscription state.
Three methods are provided:
- `Server#resources_subscribe_handler` - registers a handler for `resources/subscribe` requests
- `Server#resources_unsubscribe_handler` - registers a handler for `resources/unsubscribe` requests
- `ServerContext#notify_resources_updated` - sends a `notifications/resources/updated` notification to the subscribing client
```ruby
subscribed_uris = Set.new
server = MCP::Server.new(
name: "my_server",
resources: [my_resource],
capabilities: { resources: { subscribe: true } },
)
server.resources_subscribe_handler do |params|
subscribed_uris.add(params[:uri].to_s)
end
server.resources_unsubscribe_handler do |params|
subscribed_uris.delete(params[:uri].to_s)
end
server.define_tool(name: "update_resource") do |server_context:, **args|
if subscribed_uris.include?("test://my-resource")
server_context.notify_resources_updated(uri: "test://my-resource")
end
MCP::Tool::Response.new([MCP::Content::Text.new("Resource updated").to_h])
end
```
The `resources/subscribe` and `resources/unsubscribe` responses are empty results. The one field the spec allows
alongside is `_meta`, so a handler that returns `{ _meta: { ... } }` has it passed through; any other field it
returns is dropped. To convey a subscription identifier or other advisory data to the client, nest it under `_meta`
rather than returning it at the top level, which interoperating clients reject:
```ruby
server.resources_subscribe_handler do |params|
id = subscriptions.create(params[:uri].to_s)
{ _meta: { "myapp.example/subscriptionId" => id } }
end
```

82
docs/_server/roots.md Normal file
View file

@ -0,0 +1,82 @@
---
layout: default
title: Roots
nav_order: 7
---
# Roots
The Model Context Protocol allows servers to request filesystem roots from clients through the `roots/list` method.
Roots define the boundaries of where a server can operate, providing a list of directories and files the client has made available.
{: .warning }
> MCP Roots (`roots/list` and `notifications/roots/list_changed`) is deprecated as of protocol version `2026-07-28` (SEP-2577),
> while remaining fully supported under `2025-11-25`. Prefer tool parameters, resource URIs, server configuration, or environment
> variables for new servers. A client declaring the `roots` capability on a modern connection emits a deprecation warning.
{: .important }
> Per SEP-2260, server-to-client requests (`roots/list`, `sampling/createMessage`, `elicitation/create`) must be associated with
> an originating client request (`ping` is exempt). Use the `server_context` passed to your handler, which stamps the association
> automatically and routes the request onto the originating POST stream on the Streamable HTTP transport. Calling the corresponding
> `ServerSession` methods without `related_request_id:` still works but emits a deprecation warning.
Server-to-client requests are bounded by a timeout on the Streamable HTTP transport; see [Timeouts](/server/elicitation/#timeouts).
## Key Concepts
- **Server-to-Client Request**: Like [sampling](/server/sampling/), roots listing is initiated by the server
- **Client Capability**: Clients must declare `roots` capability during initialization
- **Change Notifications**: Clients that support `roots.listChanged` send `notifications/roots/list_changed` when roots change
## Using Roots in Tools
Tools that accept a [`server_context:`](/server/server-context/) parameter can call `list_roots` on it.
The request is automatically routed to the correct client session:
```ruby
class FileSearchTool < MCP::Tool
description "Search files within the client's project roots"
input_schema(
properties: {
query: { type: "string" }
},
required: ["query"]
)
def self.call(query:, server_context:)
roots = server_context.list_roots
root_uris = roots[:roots].map { |root| root[:uri] }
MCP::Tool::Response.new([{
type: "text",
text: "Searching in roots: #{root_uris.join(", ")}"
}])
end
end
```
Result contains an array of root objects:
```ruby
{
roots: [
{ uri: "file:///home/user/projects/myproject", name: "My Project" },
{ uri: "file:///home/user/repos/backend", name: "Backend Repository" }
]
}
```
## Handling Root Changes
Register a callback to be notified when the client's roots change:
```ruby
server.roots_list_changed_handler do
puts "Client's roots have changed, tools will see updated roots on next call."
end
```
## Error Handling
- Raises `RuntimeError` if client does not support `roots` capability
- Raises `StandardError` if client returns an error response

87
docs/_server/sampling.md Normal file
View file

@ -0,0 +1,87 @@
---
layout: default
title: Sampling
nav_order: 8
---
# Sampling
The Model Context Protocol allows servers to request LLM completions from clients through the `sampling/createMessage` method.
This enables servers to leverage the client's LLM capabilities without needing direct access to AI models.
{: .warning }
> MCP Sampling (`sampling/createMessage`) is deprecated as of protocol version `2026-07-28` (SEP-2577),
> while remaining fully supported under `2025-11-25`. New servers should call LLM provider APIs directly.
> A client declaring the `sampling` capability on a modern connection emits a deprecation warning.
{: .important }
> Per SEP-2260, server-to-client requests (`roots/list`, `sampling/createMessage`, `elicitation/create`) must be associated with
> an originating client request (`ping` is exempt). Use the `server_context` passed to your handler, which stamps the association
> automatically and routes the request onto the originating POST stream on the Streamable HTTP transport. Calling the corresponding
> `ServerSession` methods without `related_request_id:` still works but emits a deprecation warning.
Server-to-client requests are bounded by a timeout on the Streamable HTTP transport; see [Timeouts](/server/elicitation/#timeouts).
## Key Concepts
- **Server-to-Client Request**: Unlike typical MCP methods (client to server), sampling is initiated by the server
- **Client Capability**: Clients must declare `sampling` capability during initialization
- **Tool Support**: When using tools in sampling requests, clients must declare `sampling.tools` capability
- **Human-in-the-Loop**: Clients can implement user approval before forwarding requests to LLMs
## Using Sampling in Tools
Tools that accept a [`server_context:`](/server/server-context/) parameter can call `create_sampling_message` on it.
The request is automatically routed to the correct client session:
```ruby
class SummarizeTool < MCP::Tool
description "Summarize text using LLM"
input_schema(
properties: {
text: { type: "string" }
},
required: ["text"]
)
def self.call(text:, server_context:)
result = server_context.create_sampling_message(
messages: [
{ role: "user", content: { type: "text", text: "Please summarize: #{text}" } }
],
max_tokens: 500
)
MCP::Tool::Response.new([{
type: "text",
text: result[:content][:text]
}])
end
end
server = MCP::Server.new(name: "my_server", tools: [SummarizeTool])
```
## Parameters
Required:
- `messages:` (Array) - Array of message objects with `role` and `content`
- `max_tokens:` (Integer) - Maximum tokens in the response
Optional:
- `system_prompt:` (String) - System prompt for the LLM
- `model_preferences:` (Hash) - Model selection preferences (e.g., `{ intelligencePriority: 0.8 }`)
- `include_context:` (String) - Context inclusion: `"none"`, `"thisServer"`, or `"allServers"` (soft-deprecated)
- `temperature:` (Float) - Sampling temperature
- `stop_sequences:` (Array) - Sequences that stop generation
- `metadata:` (Hash) - Additional metadata
- `tools:` (Array) - Tools available to the LLM (requires `sampling.tools` capability)
- `tool_choice:` (Hash) - Tool selection mode (e.g., `{ mode: "auto" }`)
## Error Handling
- Raises `RuntimeError` if client does not support `sampling` capability
- Raises `RuntimeError` if `tools` are used but client lacks `sampling.tools` capability
- Raises `StandardError` if client returns an error response

View file

@ -0,0 +1,111 @@
---
layout: default
title: Server Context
nav_order: 19
---
# Server Context
The `server_context` is a user-defined hash that is passed into the server instance and made available to [tool](/server/tools/) and [prompt](/server/prompts/) calls.
It can be used to provide contextual information such as authentication state, user IDs, or request-specific data.
**Type:**
```ruby
server_context: { [String, Symbol] => Any }
```
**Example:**
```ruby
server = MCP::Server.new(
name: "my_server",
server_context: { user_id: current_user.id, request_id: request.uuid }
)
```
This hash is then passed as the `server_context` keyword argument to tool and prompt calls.
Note that the exception reporter does not receive this user-defined hash, and instrumentation
callbacks omit it unless you opt in with `instrument_server_context`.
See the [Configuration](/server/configuration/) page for the arguments they receive.
## Request-specific `_meta` Parameter
The MCP protocol supports a special [`_meta` parameter](https://modelcontextprotocol.io/specification/latest/basic#general-fields) in requests that allows clients to pass request-specific metadata. The server automatically extracts this parameter and makes it available to tools and prompts as a nested field within the `server_context`.
{: .note }
> `_meta` is only merged when `server_context` is a `Hash` (or `nil`, in which case a new `{ _meta: ... }` hash is synthesized).
> If you assign a non-`Hash` value to `server_context`, `_meta` is not merged and tools will not see it
> under `server_context[:_meta]`. Keep `server_context` as a `Hash` if your tools need access to `_meta`.
**Access Pattern:**
When a client includes `_meta` in the request params, it becomes available as `server_context[:_meta]`:
```ruby
class MyTool < MCP::Tool
def self.call(message:, server_context:)
# Access provider-specific metadata
session_id = server_context.dig(:_meta, :session_id)
request_id = server_context.dig(:_meta, :request_id)
# Access server's original context
user_id = server_context.dig(:user_id)
MCP::Tool::Response.new([{
type: "text",
text: "Processing for user #{user_id} in session #{session_id}"
}])
end
end
```
**Client Request Example:**
A `tools/call` request carrying `_meta`, as read by the tool above:
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "my_tool",
"arguments": { "message": "Hello" },
"_meta": {
"session_id": "abc123",
"request_id": "req_456"
}
}
}
```
### Distributed Tracing
Per SEP-414, the keys `traceparent`, `tracestate`, and `baggage` are reserved un-prefixed `_meta` keys for propagating
[W3C Trace Context](https://www.w3.org/TR/trace-context/) across MCP requests. The SDK guarantees these keys pass through
incoming request `_meta` untouched, and exposes their names as constants on `MCP::TraceContext` (`TRACEPARENT_META_KEY`,
`TRACESTATE_META_KEY`, `BAGGAGE_META_KEY`, and `META_KEYS`). The SDK does not depend on OpenTelemetry; bridge the values
to your tracing system yourself:
```ruby
class TracedTool < MCP::Tool
def self.call(message:, server_context:)
traceparent = server_context.dig(:_meta, :traceparent)
# Hand traceparent/tracestate/baggage to your tracing library
# (e.g. the opentelemetry-ruby gems) to continue the caller's trace.
MCP::Tool::Response.new([{ type: "text", text: "ok" }])
end
end
```
On the client side, every request method (`call_tool`, `read_resource`, `get_prompt`, `complete`, `ping`, and the `list_*` methods)
accepts a `meta:` keyword to inject these keys into the outgoing request, so trace context can flow on every request:
```ruby
meta = { MCP::TraceContext::TRACEPARENT_META_KEY => "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01" }
client.call_tool(tool: tool, arguments: { message: "Hello" }, meta: meta)
client.read_resource(uri: "file:///report.txt", meta: meta)
```

419
docs/_server/tools.md Normal file
View file

@ -0,0 +1,419 @@
---
layout: default
title: Tools
nav_order: 4
---
# Tools
MCP spec includes [Tools](https://modelcontextprotocol.io/specification/latest/server/tools) which provide functionality to LLM apps.
## Defining Tools
This gem provides a `MCP::Tool` class that can be used to create tools in three ways.
### 1. As a class definition
Subclass `MCP::Tool` and declare the metadata with class-level helpers; `call` implements the tool:
```ruby
class MyTool < MCP::Tool
title "My Tool"
description "This tool performs specific functionality..."
input_schema(
properties: {
message: { type: "string" },
},
required: ["message"]
)
output_schema(
properties: {
result: { type: "string" },
success: { type: "boolean" },
timestamp: { type: "string", format: "date-time" }
},
required: ["result", "success", "timestamp"]
)
annotations(
read_only_hint: true,
destructive_hint: false,
idempotent_hint: true,
open_world_hint: false,
title: "My Tool"
)
def self.call(message:, server_context:)
MCP::Tool::Response.new([{ type: "text", text: "OK" }])
end
end
tool = MyTool
```
### 2. Using the `MCP::Tool.define` method
`MCP::Tool.define` builds a tool from keyword arguments, with the block as its implementation:
```ruby
tool = MCP::Tool.define(
name: "my_tool",
title: "My Tool",
description: "This tool performs specific functionality...",
annotations: {
read_only_hint: true,
title: "My Tool"
}
) do |args, server_context:|
MCP::Tool::Response.new([{ type: "text", text: "OK" }])
end
```
### 3. Using the `MCP::Server#define_tool` method
`MCP::Server#define_tool` registers the tool directly on a server instance:
```ruby
server = MCP::Server.new
server.define_tool(
name: "my_tool",
description: "This tool performs specific functionality...",
annotations: {
title: "My Tool",
read_only_hint: true
}
) do |args, server_context:|
Tool::Response.new([{ type: "text", text: "OK" }])
end
```
The [`server_context`](/server/server-context/) parameter is the `server_context` passed into the server and can be used to pass per request information,
e.g. around authentication state.
## Tool argument keys
Tool arguments are delivered as a `Hash` whose keys are Ruby symbols at every nesting level, including nested objects
and objects inside arrays. The transports parse incoming JSON with `JSON.parse(..., symbolize_names: true)`,
so by the time a tool runs, a wire payload such as `{"payload": {"subject": "greet"}}` arrives as `{ payload: { subject: "greet" } }`.
This means top-level values are bound through keyword arguments (`def call(message:, payload: nil, server_context:)`),
and nested objects must be read with symbol keys:
```ruby
class ExampleTool < MCP::Tool
description "Echoes a nested argument"
input_schema(
properties: {
message: { type: "string" },
payload: {
type: "object",
properties: {
subject: { type: "string" },
}
}
},
required: ["message"]
)
def self.call(message:, payload: nil, server_context:)
subject = payload && payload[:subject] # symbol key, not payload["subject"]
MCP::Tool::Response.new([{
type: "text",
text: "Message: #{message}; subject: #{subject}"
}])
end
end
```
Reading a nested value with a string key (`payload["subject"]`) returns `nil`. This is a Ruby-specific contract:
Top-level keyword arguments require symbol keys, and parsing JSON with `symbolize_names: true` symbolizes nested objects too.
Calling a tool directly in a test with `MyTool.call(payload: { "subject" => "greet" }, server_context: nil)` passes string keys
that a transport never delivers, so string-key access can pass tests yet fail against a real client.
Exercise a tool under the delivered shape by round-tripping the arguments through JSON the same way a transport does:
```ruby
delivered = JSON.parse(JSON.generate(arguments), symbolize_names: true)
MyTool.call(**delivered, server_context: nil)
```
## Tool Annotations
Tools can include annotations that provide additional metadata about their behavior. The following annotations are supported:
- `destructive_hint`: Indicates if the tool performs destructive operations. Defaults to true
- `idempotent_hint`: Indicates if the tool's operations are idempotent. Defaults to false
- `open_world_hint`: Indicates if the tool operates in an open world context. Defaults to true
- `read_only_hint`: Indicates if the tool only reads data (doesn't modify state). Defaults to false
- `title`: A human-readable title for the tool
Annotations can be set either through the class definition using the `annotations` class method or when defining a tool using the `define` method.
{: .note }
> This **Tool Annotations** feature is supported starting from `protocol_version: '2025-03-26'`.
## Tool Output Schemas
Tools can optionally define an `output_schema` to specify the expected structure of their results. This works similarly to how `input_schema` is defined and can be used in three ways.
### 1. Class definition with `output_schema`
Declare the schema with the `output_schema` class helper, alongside `input_schema`:
```ruby
class WeatherTool < MCP::Tool
tool_name "get_weather"
description "Get current weather for a location"
input_schema(
properties: {
location: { type: "string" },
units: { type: "string", enum: ["celsius", "fahrenheit"] }
},
required: ["location"]
)
output_schema(
properties: {
temperature: { type: "number" },
condition: { type: "string" },
humidity: { type: "integer" }
},
required: ["temperature", "condition", "humidity"]
)
def self.call(location:, units: "celsius", server_context:)
# Call weather API and structure the response
api_response = WeatherAPI.fetch(location, units)
weather_data = {
temperature: api_response.temp,
condition: api_response.description,
humidity: api_response.humidity_percent
}
output_schema.validate_result(weather_data)
MCP::Tool::Response.new([{
type: "text",
text: weather_data.to_json
}])
end
end
```
### 2. Using `Tool.define` with `output_schema`
Pass the schema as the `output_schema:` keyword argument:
```ruby
tool = MCP::Tool.define(
name: "calculate_stats",
description: "Calculate statistics for a dataset",
input_schema: {
properties: {
numbers: { type: "array", items: { type: "number" } }
},
required: ["numbers"]
},
output_schema: {
properties: {
mean: { type: "number" },
median: { type: "number" },
count: { type: "integer" }
},
required: ["mean", "median", "count"]
}
) do |args, server_context:|
# Calculate statistics and validate against schema
MCP::Tool::Response.new([{ type: "text", text: "Statistics calculated" }])
end
```
### 3. Using `OutputSchema` objects
Construct an `MCP::Tool::OutputSchema` object explicitly:
```ruby
class DataTool < MCP::Tool
output_schema MCP::Tool::OutputSchema.new(
properties: {
success: { type: "boolean" },
data: { type: "object" }
},
required: ["success"]
)
end
```
Output schema may also describe an array of objects:
```ruby
class WeatherTool < MCP::Tool
output_schema(
type: "array",
items: {
properties: {
temperature: { type: "number" },
condition: { type: "string" },
humidity: { type: "integer" }
},
required: ["temperature", "condition", "humidity"]
}
)
end
```
Please note: in this case, you must provide `type: "array"`. The default type for output schemas is `object`,
applied only when the schema declares no root keyword (`type`, `$ref`, `oneOf`, `anyOf`, `allOf`, `not`, `if`, `const`, `enum`).
Per SEP-2106, an output schema may be any valid JSON Schema 2020-12 document, including a primitive root
(`{ type: "string" }`) or a root-level composition:
```ruby
class FlexibleTool < MCP::Tool
output_schema(
oneOf: [
{ type: "string" },
{ type: "array", items: { type: "number" } }
]
)
end
```
Input schemas keep `type: "object"` at the root but accept the full 2020-12 vocabulary below it
(`$defs`/`$ref`, `oneOf`/`anyOf`/`allOf`/`not`, `if`/`then`/`else`). Two resource bounds apply to
all tool schemas: only same-document `$ref`s (starting with `#`) are accepted, and documents are
capped at `MCP::Tool::Schema::MAX_SCHEMA_DEPTH` nesting levels and `MCP::Tool::Schema::MAX_SUBSCHEMA_COUNT` subschema objects;
violations raise `ArgumentError` at construction time.
MCP spec for the [Output Schema](https://modelcontextprotocol.io/specification/latest/server/tools#output-schema) specifies that:
- **Server Validation**: Servers MUST provide structured results that conform to the output schema
- **Client Validation**: Clients SHOULD validate structured results against the output schema
- **Better Integration**: Enables strict schema validation, type information, and improved developer experience
- **Backward Compatibility**: Tools returning structured content SHOULD also include serialized JSON in a TextContent block
The output schema follows standard JSON Schema format and helps ensure consistent data exchange between MCP servers and clients.
By default, server-side validation of tool results against `output_schema` is disabled for backwards compatibility.
To validate successful tool responses, enable `validate_tool_call_results` on the server [configuration](/server/configuration/):
```ruby
configuration = MCP::Configuration.new(validate_tool_call_results: true)
server = MCP::Server.new(
name: "example_server",
tools: [WeatherTool],
configuration: configuration
)
```
When enabled, successful tool responses for tools with an `output_schema` must include `structured_content` that conforms to the schema.
Error responses are not validated against the output schema.
## Tool Responses with Structured Content
Tools can return structured data alongside text content using the `structured_content` parameter.
The structured content will be included in the JSON-RPC response as the `structuredContent` field.
Per SEP-2106, `structured_content` may be any JSON value, not only an object. When a tool returns a non-object value (e.g. an array)
without providing any content blocks, the server automatically mirrors it into `content` as serialized JSON text so older clients
that only read `content` still receive the data.
```ruby
class WeatherTool < MCP::Tool
description "Get current weather and return structured data"
def self.call(location:, units: "celsius", server_context:)
# Call weather API and structure the response
api_response = WeatherAPI.fetch(location, units)
weather_data = {
temperature: api_response.temp,
condition: api_response.description,
humidity: api_response.humidity_percent
}
output_schema.validate_result(weather_data)
MCP::Tool::Response.new(
[{
type: "text",
text: weather_data.to_json
}],
structured_content: weather_data
)
end
end
```
## Tool Responses with Errors
Tools can return error information alongside text content using the `error` parameter.
The error will be included in the JSON-RPC response as the `isError` field.
```ruby
class WeatherTool < MCP::Tool
description "Get current weather and return structured data"
def self.call(server_context:)
# Do something here
content = {}
MCP::Tool::Response.new(
[{
type: "text",
text: content.to_json
}],
structured_content: content,
error: true
)
end
end
```
## Tool Responses with Image, Audio, and Embedded Resources
Tool responses are not limited to text. The `MCP::Content` module provides `Image`, `Audio`, and `EmbeddedResource` content types,
which serialize to the `image`, `audio`, and `resource` content blocks defined by the MCP spec. Image and audio data is passed as
a base64-encoded string together with its MIME type:
```ruby
class ChartTool < MCP::Tool
description "Render a chart as a PNG image"
def self.call(server_context:)
MCP::Tool::Response.new([
MCP::Content::Text.new("Here is the rendered chart:").to_h,
MCP::Content::Image.new(Base64.strict_encode64(render_chart_png), "image/png").to_h,
])
end
end
class SpeechTool < MCP::Tool
description "Synthesize speech audio"
def self.call(server_context:)
MCP::Tool::Response.new([
MCP::Content::Audio.new(Base64.strict_encode64(synthesize_wav), "audio/wav").to_h,
])
end
end
```
An [embedded resource](/server/resources/) wraps `MCP::Resource::TextContents` or `MCP::Resource::BlobContents`, allowing a tool to return resource contents inline:
```ruby
class ReportTool < MCP::Tool
description "Return a report as an embedded resource"
def self.call(server_context:)
contents = MCP::Resource::TextContents.new(
uri: "report://monthly",
mime_type: "application/json",
text: { total: 42 }.to_json,
)
MCP::Tool::Response.new([MCP::Content::EmbeddedResource.new(contents).to_h])
end
end
```

Some files were not shown because too many files have changed in this diff Show more