## Motivation and Context The MCP specification recommends expiring session IDs to reduce session hijacking risks: https://modelcontextprotocol.io/specification/latest/basic/security_best_practices#session-hijacking Several other SDKs (Go, C#, PHP, Python) already implement session expiry controls, but the Ruby SDK did not. Sessions persisted indefinitely unless explicitly deleted or a stream error occurred, leaving abandoned sessions to accumulate in memory. This adds a `session_idle_timeout:` option to `StreamableHTTPTransport#initialize`. When set, sessions that receive no HTTP requests for the specified duration (in seconds) are automatically expired. Expired sessions return 404 on subsequent requests (GET and POST), matching the MCP specification's behavior for terminated sessions. Each request resets the idle timer, so actively used sessions are not interrupted. A background reaper thread periodically cleans up expired sessions to handle orphaned sessions that receive no further requests. The reaper only starts when `session_idle_timeout` is configured. The default is `nil` (no expiry) for backward compatibility, consistent with the Python SDK's approach. The Python SDK recommends 1800 seconds (30 minutes) for most deployments: https://github.com/modelcontextprotocol/python-sdk/pull/2022 Resolves #265. ## How Has This Been Tested? Added tests for session expiry, idle timeout reset, reaper thread lifecycle, input validation, and default behavior in `streamable_http_transport_test.rb`. All existing tests continue to pass. ## Breaking Change None. The default value of `session_idle_timeout` is `nil`, which preserves the existing behavior of sessions never expiring. The new `last_active_at` field in the internal session hash is not part of the public API. Existing code that instantiates `StreamableHTTPTransport.new(server)` or `StreamableHTTPTransport.new(server, stateless: true)` continues to work without changes.
1226 lines
37 KiB
Markdown
1226 lines
37 KiB
Markdown
# MCP Ruby SDK [](https://rubygems.org/gems/mcp) [](https://github.com/modelcontextprotocol/ruby-sdk/blob/main/LICENSE) [](https://github.com/modelcontextprotocol/ruby-sdk/actions/workflows/ci.yml)
|
|
|
|
The official Ruby SDK for Model Context Protocol servers and clients.
|
|
|
|
## Installation
|
|
|
|
Add this line to your application's Gemfile:
|
|
|
|
```ruby
|
|
gem 'mcp'
|
|
```
|
|
|
|
And then execute:
|
|
|
|
```console
|
|
$ bundle install
|
|
```
|
|
|
|
Or install it yourself as:
|
|
|
|
```console
|
|
$ gem install mcp
|
|
```
|
|
|
|
You may need to add additional dependencies depending on which features you wish to access.
|
|
|
|
## 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 & Streamable HTTP (including SSE) transports
|
|
- Supports notifications for list changes (tools, prompts, resources)
|
|
|
|
### Supported Methods
|
|
|
|
- `initialize` - Initializes the protocol and returns server capabilities
|
|
- `ping` - Simple health check
|
|
- `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
|
|
|
|
### 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
|
|
|
|
**Usage Example:**
|
|
|
|
```ruby
|
|
# Client request
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "add",
|
|
"params": { "a": 5, "b": 3 }
|
|
}
|
|
|
|
# Server response
|
|
{
|
|
"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 as standard methods
|
|
|
|
### 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_progress` - Send a progress notification for long-running operations
|
|
- `notify_log_message` - Send a structured logging notification message
|
|
|
|
#### 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` 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.
|
|
|
|
#### 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/progress`
|
|
- `notifications/message`
|
|
|
|
### 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
|
|
|
|
#### Server-Side: Tool with Progress
|
|
|
|
Tools that accept a `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
|
|
|
|
#### Server-Side: Direct `notify_progress` Usage
|
|
|
|
You can also call `notify_progress` directly on the server instance:
|
|
|
|
```ruby
|
|
server.notify_progress(
|
|
progress_token: "token-123",
|
|
progress: 50,
|
|
total: 100, # optional
|
|
message: "halfway" # optional
|
|
)
|
|
```
|
|
|
|
**Key Features:**
|
|
|
|
- Tools report progress via `server_context.report_progress`
|
|
- `report_progress` is a no-op when no `progressToken` was provided by the client
|
|
- `notify_progress` is a no-op when no transport is configured
|
|
- Supports both numeric and string progress tokens
|
|
|
|
### 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.
|
|
|
|
#### 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:**
|
|
|
|
```ruby
|
|
server = MCP::Server.new(name: "my_server")
|
|
transport = MCP::Server::Transports::StdioTransport.new(server)
|
|
server.transport = transport
|
|
|
|
# The client first configures the logging level (on the client side):
|
|
transport.send_request(
|
|
request: {
|
|
jsonrpc: "2.0",
|
|
method: "logging/setLevel",
|
|
params: { level: "info" },
|
|
id: session_id # Unique request ID within the session
|
|
}
|
|
)
|
|
|
|
# Send log messages at different severity levels
|
|
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:**
|
|
|
|
- Supports 8 log levels (debug, info, notice, warning, error, critical, alert, emergency) based on https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging#log-levels
|
|
- 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)
|
|
|
|
#### Usage Example
|
|
|
|
```ruby
|
|
server = MCP::Server.new(name: "my_server")
|
|
|
|
# Default Streamable HTTP - session oriented
|
|
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
|
|
|
|
server.transport = transport
|
|
|
|
# When tools change, notify clients
|
|
server.define_tool(name: "new_tool") { |**args| { result: "ok" } }
|
|
server.notify_tools_list_changed
|
|
```
|
|
|
|
You can use Stateless Streamable HTTP, where notifications are not supported and all calls are request/response interactions.
|
|
This mode allows for easy multi-node deployment.
|
|
Set `stateless: true` in `MCP::Server::Transports::StreamableHTTPTransport.new` (`stateless` defaults to `false`):
|
|
|
|
```ruby
|
|
# Stateless Streamable HTTP - session-less
|
|
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
|
|
```
|
|
|
|
By default, sessions do not expire. To mitigate session hijacking risks, you can set a `session_idle_timeout` (in seconds).
|
|
When configured, sessions that receive no HTTP requests for this duration are automatically expired and cleaned up:
|
|
|
|
```ruby
|
|
# Session timeout of 30 minutes
|
|
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, session_idle_timeout: 1800)
|
|
```
|
|
|
|
### Unsupported Features (to be implemented in future versions)
|
|
|
|
- Resource subscriptions
|
|
- Completions
|
|
- Elicitation
|
|
|
|
### Usage
|
|
|
|
#### Rails Controller
|
|
|
|
When added to a Rails controller on a route that handles POST requests, your server will be compliant with non-streaming
|
|
[Streamable HTTP](https://modelcontextprotocol.io/specification/latest/basic/transports#streamable-http) transport
|
|
requests.
|
|
|
|
You can use `StreamableHTTPTransport#handle_request` to handle requests with proper HTTP
|
|
status codes (e.g., 202 Accepted for notifications).
|
|
|
|
```ruby
|
|
class McpController < ActionController::Base
|
|
def create
|
|
server = MCP::Server.new(
|
|
name: "my_server",
|
|
title: "Example Server Display Name",
|
|
version: "1.0.0",
|
|
instructions: "Use the tools of this server as a last resort",
|
|
tools: [SomeTool, AnotherTool],
|
|
prompts: [MyPrompt],
|
|
server_context: { user_id: current_user.id },
|
|
)
|
|
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
|
|
server.transport = transport
|
|
status, headers, body = transport.handle_request(request)
|
|
|
|
render(json: body.first, status: status, headers: headers)
|
|
end
|
|
end
|
|
```
|
|
|
|
#### Stdio Transport
|
|
|
|
If you want to build a local command-line application, you can use the stdio transport:
|
|
|
|
```ruby
|
|
require "mcp"
|
|
|
|
# Create a simple tool
|
|
class ExampleTool < MCP::Tool
|
|
description "A simple example tool that echoes back its arguments"
|
|
input_schema(
|
|
properties: {
|
|
message: { type: "string" },
|
|
},
|
|
required: ["message"]
|
|
)
|
|
|
|
class << self
|
|
def call(message:, server_context:)
|
|
MCP::Tool::Response.new([{
|
|
type: "text",
|
|
text: "Hello from example tool! Message: #{message}",
|
|
}])
|
|
end
|
|
end
|
|
end
|
|
|
|
# Set up the server
|
|
server = MCP::Server.new(
|
|
name: "example_server",
|
|
tools: [ExampleTool],
|
|
)
|
|
|
|
# Create and start the transport
|
|
transport = MCP::Server::Transports::StdioTransport.new(server)
|
|
transport.open
|
|
```
|
|
|
|
You can run this script and then type in requests to the server at the command line.
|
|
|
|
```console
|
|
$ ruby examples/stdio_server.rb
|
|
{"jsonrpc":"2.0","id":"1","method":"ping"}
|
|
{"jsonrpc":"2.0","id":"2","method":"tools/list"}
|
|
{"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"example_tool","arguments":{"message":"Hello"}}}
|
|
```
|
|
|
|
### 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.instrumentation_callback = ->(data) {
|
|
puts "Got instrumentation data #{data.inspect}"
|
|
}
|
|
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 instrumentation callbacks.
|
|
|
|
```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.instrumentation_callback = ->(data) {
|
|
puts "Got instrumentation data #{data.inspect}"
|
|
}
|
|
|
|
server = MCP::Server.new(
|
|
# ... all other options
|
|
configuration:,
|
|
)
|
|
```
|
|
|
|
### Server Context and Configuration Block Data
|
|
|
|
#### `server_context`
|
|
|
|
The `server_context` is a user-defined hash that is passed into the server instance and made available to tools, prompts, and exception/instrumentation callbacks. 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` argument to tool and prompt calls, and is included in exception and instrumentation callbacks.
|
|
|
|
#### Request-specific `_meta` Parameter
|
|
|
|
The MCP protocol supports a special [`_meta` parameter](https://modelcontextprotocol.io/specification/2025-06-18/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`.
|
|
|
|
**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:**
|
|
|
|
```json
|
|
{
|
|
"jsonrpc": "2.0",
|
|
"id": 1,
|
|
"method": "tools/call",
|
|
"params": {
|
|
"name": "my_tool",
|
|
"arguments": { "message": "Hello" },
|
|
"_meta": {
|
|
"session_id": "abc123",
|
|
"request_id": "req_456"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Configuration Block Data
|
|
|
|
##### Exception Reporter
|
|
|
|
The exception reporter receives:
|
|
|
|
- `exception`: The Ruby exception object that was raised
|
|
- `server_context`: The context hash provided to the server
|
|
|
|
**Signature:**
|
|
|
|
```ruby
|
|
exception_reporter = ->(exception, server_context) { ... }
|
|
```
|
|
|
|
##### Instrumentation Callback
|
|
|
|
The instrumentation callback 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
|
|
|
|
> [!NOTE]
|
|
> `tool_name`, `prompt_name` and `resource_uri` are only populated if a matching handler is registered.
|
|
> This is to avoid potential issues with metric cardinality.
|
|
|
|
**Type:**
|
|
|
|
```ruby
|
|
instrumentation_callback = ->(data) { ... }
|
|
# where data is a Hash with keys as described above
|
|
```
|
|
|
|
**Example:**
|
|
|
|
```ruby
|
|
MCP.configure do |config|
|
|
config.instrumentation_callback = ->(data) {
|
|
puts "Instrumentation: #{data.inspect}"
|
|
}
|
|
end
|
|
```
|
|
|
|
### 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 stable version will be applied by default.
|
|
The latest stable version includes new features from the [draft version](https://modelcontextprotocol.io/specification/draft).
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
### Exception Reporting
|
|
|
|
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
|
|
|
|
The server_context hash includes:
|
|
|
|
- For tool calls: `{ tool_name: "name", arguments: { ... } }`
|
|
- For general request handling: `{ request: { ... } }`
|
|
|
|
When an exception occurs:
|
|
|
|
1. The exception is reported via the configured reporter
|
|
2. For tool calls, a generic error response is returned to the client: `{ error: "Internal error occurred", isError: true }`
|
|
3. For other requests, the exception is re-raised after reporting
|
|
|
|
If no exception reporter is configured, a default no-op reporter is used that silently ignores exceptions.
|
|
|
|
### Tools
|
|
|
|
MCP spec includes [Tools](https://modelcontextprotocol.io/specification/latest/server/tools) which provide functionality to LLM apps.
|
|
|
|
This gem provides a `MCP::Tool` class that can be used to create tools in three ways:
|
|
|
|
1. As a class definition:
|
|
|
|
```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. By using the `MCP::Tool.define` method with a block:
|
|
|
|
```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. By using the `MCP::Server#define_tool` method with a block:
|
|
|
|
```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 parameter is the server_context passed into the server and can be used to pass per request information,
|
|
e.g. around authentication state.
|
|
|
|
### 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:**
|
|
|
|
```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:**
|
|
|
|
```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:**
|
|
|
|
```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`.
|
|
|
|
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.
|
|
|
|
### 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.
|
|
|
|
```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
|
|
```
|
|
|
|
### 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.
|
|
|
|
The `MCP::Prompt` class provides three ways to create prompts:
|
|
|
|
1. As a class definition with metadata:
|
|
|
|
```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:
|
|
|
|
```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:
|
|
|
|
```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 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
|
|
|
|
### Usage
|
|
|
|
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
|
|
|
|
### Resources
|
|
|
|
MCP spec includes [Resources](https://modelcontextprotocol.io/specification/latest/server/resources).
|
|
|
|
### Reading Resources
|
|
|
|
The `MCP::Resource` class provides a way to register resources with the server.
|
|
|
|
```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],
|
|
)
|
|
```
|
|
|
|
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.
|
|
|
|
### Resource Templates
|
|
|
|
The `MCP::ResourceTemplate` class provides a way to register resource templates with the server.
|
|
|
|
```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],
|
|
)
|
|
```
|
|
|
|
## Building an MCP Client
|
|
|
|
The `MCP::Client` class provides an interface for interacting with MCP servers.
|
|
|
|
This class supports:
|
|
|
|
- Tool listing via the `tools/list` method (`MCP::Client#tools`)
|
|
- Tool invocation via the `tools/call` method (`MCP::Client#call_tools`)
|
|
- Resource listing via the `resources/list` method (`MCP::Client#resources`)
|
|
- Resource template listing via the `resources/templates/list` method (`MCP::Client#resource_templates`)
|
|
- Resource reading via the `resources/read` method (`MCP::Client#read_resources`)
|
|
- Prompt listing via the `prompts/list` method (`MCP::Client#prompts`)
|
|
- Prompt retrieval via the `prompts/get` method (`MCP::Client#get_prompt`)
|
|
- Automatic JSON-RPC 2.0 message formatting
|
|
- UUID request ID generation
|
|
|
|
Clients are initialized with a transport layer instance that handles the low-level communication mechanics.
|
|
Authorization is handled by the transport layer.
|
|
|
|
## Transport Layer Interface
|
|
|
|
If the transport layer you need is not included in the gem, you can build and pass your own instances so long as they conform to the following interface:
|
|
|
|
```ruby
|
|
class CustomTransport
|
|
# Sends a JSON-RPC request to the server and returns the raw response.
|
|
#
|
|
# @param request [Hash] A complete JSON-RPC request object.
|
|
# https://www.jsonrpc.org/specification#request_object
|
|
# @return [Hash] A hash modeling a JSON-RPC response object.
|
|
# https://www.jsonrpc.org/specification#response_object
|
|
def send_request(request:)
|
|
# Your transport-specific logic here
|
|
# - HTTP: POST to endpoint with JSON body
|
|
# - WebSocket: Send message over WebSocket
|
|
# - stdio: Write to stdout, read from stdin
|
|
# - etc.
|
|
end
|
|
end
|
|
```
|
|
|
|
### Stdio Transport Layer
|
|
|
|
Use the `MCP::Client::Stdio` transport to interact with MCP servers running as subprocesses over standard input/output.
|
|
|
|
`MCP::Client::Stdio.new` accepts the following keyword arguments:
|
|
|
|
| Parameter | Required | Description |
|
|
|---|---|---|
|
|
| `command:` | Yes | The command to spawn the server process (e.g., `"ruby"`, `"bundle"`, `"npx"`). |
|
|
| `args:` | No | An array of arguments passed to the command. Defaults to `[]`. |
|
|
| `env:` | No | A hash of environment variables to set for the server process. Defaults to `nil`. |
|
|
| `read_timeout:` | No | Timeout in seconds for waiting for a server response. Defaults to `nil` (no timeout). |
|
|
|
|
Example usage:
|
|
|
|
```ruby
|
|
stdio_transport = MCP::Client::Stdio.new(
|
|
command: "bundle",
|
|
args: ["exec", "ruby", "path/to/server.rb"],
|
|
env: { "API_KEY" => "my_secret_key" },
|
|
read_timeout: 30
|
|
)
|
|
client = MCP::Client.new(transport: stdio_transport)
|
|
|
|
# List available tools.
|
|
tools = client.tools
|
|
tools.each do |tool|
|
|
puts "Tool: #{tool.name} - #{tool.description}"
|
|
end
|
|
|
|
# Call a specific tool.
|
|
response = client.call_tool(
|
|
tool: tools.first,
|
|
arguments: { message: "Hello, world!" }
|
|
)
|
|
|
|
# Close the transport when done.
|
|
stdio_transport.close
|
|
```
|
|
|
|
The stdio transport automatically handles:
|
|
|
|
- Spawning the server process with `Open3.popen3`
|
|
- MCP protocol initialization handshake (`initialize` request + `notifications/initialized`)
|
|
- JSON-RPC 2.0 message framing over newline-delimited JSON
|
|
|
|
### HTTP Transport Layer
|
|
|
|
Use the `MCP::Client::HTTP` transport to interact with MCP servers using simple HTTP requests.
|
|
|
|
You'll need to add `faraday` as a dependency in order to use the HTTP transport layer:
|
|
|
|
```ruby
|
|
gem 'mcp'
|
|
gem 'faraday', '>= 2.0'
|
|
```
|
|
|
|
Example usage:
|
|
|
|
```ruby
|
|
http_transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp")
|
|
client = MCP::Client.new(transport: http_transport)
|
|
|
|
# List available tools
|
|
tools = client.tools
|
|
tools.each do |tool|
|
|
puts <<~TOOL_INFORMATION
|
|
Tool: #{tool.name}
|
|
Description: #{tool.description}
|
|
Input Schema: #{tool.input_schema}
|
|
TOOL_INFORMATION
|
|
end
|
|
|
|
# Call a specific tool
|
|
response = client.call_tool(
|
|
tool: tools.first,
|
|
arguments: { message: "Hello, world!" }
|
|
)
|
|
|
|
# Call a tool with progress tracking.
|
|
response = client.call_tool(
|
|
tool: tools.first,
|
|
arguments: { count: 10 },
|
|
progress_token: "my-progress-token"
|
|
)
|
|
```
|
|
|
|
The server will send `notifications/progress` back to the client during execution.
|
|
|
|
#### HTTP Authorization
|
|
|
|
By default, the HTTP transport layer provides no authentication to the server, but you can provide custom headers if you need authentication. For example, to use Bearer token authentication:
|
|
|
|
```ruby
|
|
http_transport = MCP::Client::HTTP.new(
|
|
url: "https://api.example.com/mcp",
|
|
headers: {
|
|
"Authorization" => "Bearer my_token"
|
|
}
|
|
)
|
|
|
|
client = MCP::Client.new(transport: http_transport)
|
|
client.tools # will make the call using Bearer auth
|
|
```
|
|
|
|
You can add any custom headers needed for your authentication scheme, or for any other purpose. The client will include these headers on every request.
|
|
|
|
### Tool Objects
|
|
|
|
The client provides a wrapper class for tools returned by the server:
|
|
|
|
- `MCP::Client::Tool` - Represents a single tool with its metadata
|
|
|
|
This class provides easy access to tool properties like name, description, input schema, and output schema.
|
|
|
|
## Conformance Testing
|
|
|
|
The `conformance/` directory contains a test server and runner that validate the SDK against the MCP specification using [`@modelcontextprotocol/conformance`](https://github.com/modelcontextprotocol/conformance).
|
|
|
|
See [conformance/README.md](conformance/README.md) for usage instructions.
|
|
|
|
## Documentation
|
|
|
|
- [SDK API documentation](https://rubydoc.info/gems/mcp)
|
|
- [Model Context Protocol documentation](https://modelcontextprotocol.io)
|