GitHub - modelcontextprotocol/ruby-sdk: The official Ruby SDK for the Model Context Protocol. Maintained in collaboration with Shopify. (original) (raw)

The official Ruby SDK for Model Context Protocol servers and clients.

Installation

Add this line to your application's Gemfile:

And then execute:

Or install it yourself as:

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

Supported Methods

Custom Methods

The server allows you to define custom JSON-RPC methods beyond the standard MCP protocol methods using the define_custom_method method:

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:

Usage Example:

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:

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 three notification methods:

Notification Format

Notifications follow the JSON-RPC 2.0 specification and use these method names:

Transport Support

Usage Example

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):

Stateless Streamable HTTP - session-less

transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)

Unsupported Features (to be implemented in future versions)

Usage

Rails Controller

When added to a Rails controller on a route that handles POST requests, your server will be compliant with non-streamingStreamable HTTP transport requests.

You can use the Server#handle_json method to handle requests.

class ApplicationController < ActionController::Base def index server = MCP::Server.new( name: "my_server", title: "Example Server Display Name", # WARNING: This is a Draft and is not supported in the Version 2025-06-18 (latest) specification. 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 }, ) render(json: server.handle_json(request.body.read)) end end

Stdio Transport

If you want to build a local command-line application, you can use the stdio transport:

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.

$ 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:

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.

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:

server_context: { [String, Symbol] => Any }

Example:

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.

Configuration Block Data

Exception Reporter

The exception reporter receives:

Signature:

exception_reporter = ->(exception, server_context) { ... }

Instrumentation Callback

The instrumentation callback receives a hash with the following possible keys:

Type:

instrumentation_callback = ->(data) { ... }

where data is a Hash with keys as described above

Example:

config.instrumentation_callback = ->(data) { puts "Instrumentation: #{data.inspect}" }

Server Protocol Version

The server's protocol version can be overridden using the protocol_version keyword argument:

configuration = MCP::Configuration.new(protocol_version: "2024-11-05") MCP::Server.new(name: "test_server", configuration: configuration)

If no protocol version is specified, the Draft version will be 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:

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 for the protocol version to understand the supported features for the version being set.

Exception Reporting

The exception reporter receives two arguments:

The server_context hash includes:

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 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:

class MyTool < MCP::Tool title "My Tool" # WARNING: This is a Draft and is not supported in the Version 2025-06-18 (latest) specification. 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

  1. By using the MCP::Tool.define method with a block:

tool = MCP::Tool.define( name: "my_tool", title: "My Tool", # WARNING: This is a Draft and is not supported in the Version 2025-06-18 (latest) specification. 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

  1. By using the MCP::Server#define_tool method with a block:

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:

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:

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

  1. Using Tool.define with output_schema:

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

  1. Using OutputSchema objects:

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:

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 specifies that:

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.

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.

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, 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:

class MyPrompt < MCP::Prompt prompt_name "my_prompt" # Optional - defaults to underscored class name title "My Prompt" # WARNING: This is a Draft and is not supported in the Version 2025-06-18 (latest) specification. 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

  1. Using the MCP::Prompt.define method:

prompt = MCP::Prompt.define( name: "my_prompt", title: "My Prompt", # WARNING: This is a Draft and is not supported in the Version 2025-06-18 (latest) specification. 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

  1. Using the MCP::Server#define_prompt method:

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

Usage

Register prompts with the MCP server:

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:

Instrumentation

The server allows registering a callback to receive information about instrumentation. To register a handler pass a proc/lambda to as instrumentation_callback into the server constructor.

MCP.configure do |config| config.instrumentation_callback = ->(data) { puts "Got instrumentation data #{data.inspect}" } end

The data contains the following keys:

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

Resources

MCP spec includes Resources.

Reading Resources

The MCP::Resource class provides a way to register resources with the server.

resource = MCP::Resource.new( uri: "https://example.com/my_resource", name: "my-resource", title: "My Resource", # WARNING: This is a Draft and is not supported in the Version 2025-06-18 (latest) specification. 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.

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.

resource_template = MCP::ResourceTemplate.new( uri_template: "https://example.com/my_resource_template", name: "my-resource-template", title: "My Resource Template", # WARNING: This is a Draft and is not supported in the Version 2025-06-18 (latest) specification. 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:

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:

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

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:

gem 'mcp' gem 'faraday', '>= 2.0'

Example usage:

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!" } )

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:

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:

This class provides easy access to tool properties like name, description, input schema, and output schema.

Documentation