
This tutorial is part of Build an MCP Server, within The MCP Guide. Want it built for you? See MCP development.
You build an MCP server in Ruby with the official mcp gem. You subclass MCP::Tool, declare a description and an input schema, implement self.call, and hand the tool to MCP::Server. A stdio transport serves it to desktop apps, and a Rack-based HTTP transport mounts it inside Rails.
The Ruby SDK is one of six Tier 1 SDKs on the official list as of November 2026, the top support level. It serves both the older initialize handshake and the stateless 2026-07-28 spec revision with no extra configuration.
This guide was checked against mcp 1.8.2 (released 27 October 2026) and the official Ruby SDK README and SDK guides on 2 November 2026. The code follows the SDK's own examples; it was not run for this guide.
Key Takeaways
- Install with
gem "mcp"in your Gemfile; the gem needs Ruby 2.7 or later and depends onjson_schemer. - A tool is a class:
description,input_schema, optionaloutput_schemaandannotations, and aself.callmethod. - Arguments arrive with symbol keys at every level, so read nested values with
payload[:subject], not strings. - Return
MCP::Tool::Response.new(content, structured_content: ..., error: true)to send structured data or a tool error. StreamableHTTPTransportis a Rack app; mount it in Rails routes or call it from a controller withstateless: true.- On the handshake era, the HTTP transport keeps session state in memory, so run one process or use sticky sessions.
Where the Ruby SDK stands
The official SDK page lists Ruby as Tier 1, alongside TypeScript, Python, C#, Go and Rust. Tier 1 means full conformance and fast issue response; our official MCP SDKs overview explains each tier.
| Item | Value (checked 2 November 2026) |
|---|---|
| Gem | mcp on RubyGems |
| Latest release | 1.8.2, 27 October 2026 |
| Ruby requirement | 2.7.0 or later (gemspec) |
| Runtime dependency | json_schemer 2.4 or later |
| License | Apache 2.0 for new contributions, existing code MIT |
| Official tier | Tier 1 |
| Protocol versions | 2024-11-05 through 2026-07-28 |
The SDK's protocol versions page explains the two eras. Handshake versions (2025-11-25 and earlier) start with initialize. The modern 2026-07-28 version has no handshake: clients call server/discover, and every request carries its version in _meta. The bundled transports serve both side by side.
Prerequisites
- Ruby 2.7 or later. Use a current Ruby if you can; check your version with
ruby -v. - Bundler.
- Node.js with
npx, only for the MCP Inspector step. - An MCP host such as Claude Desktop, Cursor or Claude Code for a real-app test.
New to hosts, clients and servers? The MCP course starts there.
Install the gem
Add the gem to your Gemfile and install:
gem "mcp"
bundle install
Or install it directly with gem install mcp.
A minimal working server
This is the README's quick-start server. Save it as server.rb:
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
What each part does:
MCP::Toolis the base class. The README's sample calls show the tool name derived from the class asexample_tool.descriptionis what the model reads to decide when to call the tool. Write it for a model, not a human skimming code.input_schemais plain JSON Schema as a Ruby hash. The root staystype: "object".callgets each top-level argument as a keyword, plusserver_context, which carries per-request data such as auth state.MCP::Tool::Response.newwraps a list of content blocks.StdioTransportreads JSON-RPC from stdin and writes to stdout.
You can drive it by hand. Run ruby server.rb and paste a request line, for example {"jsonrpc":"2.0","id":"2","method":"tools/list"}. That trick is in the README and is handy for quick checks.
Because stdout carries the protocol, keep puts and p out of tool code. Write debug output to $stderr.
How to add a tool
The Tools guide shows three ways to define a tool: a class, MCP::Tool.define with a block, or server.define_tool on an existing server. The class form is easiest to test and read. Here is a fuller one with a title, annotations and an output schema:
class WeatherTool < MCP::Tool
tool_name "get_weather"
title "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"]
)
annotations(
read_only_hint: true,
destructive_hint: false,
idempotent_hint: true,
open_world_hint: true
)
def self.call(location:, units: "celsius", server_context:)
weather_data = { temperature: 21.5, condition: "cloudy", humidity: 60 }
output_schema.validate_result(weather_data)
MCP::Tool::Response.new(
[{ type: "text", text: weather_data.to_json }],
structured_content: weather_data
)
end
end
Replace the hard-coded hash with your real data source. Then register the class: MCP::Server.new(name: "weather", tools: [WeatherTool]).
Annotations and their defaults
The Tools guide lists the Ruby defaults, which match the defaults in the 2026-07-28 schema reference: destructive_hint true, idempotent_hint false, open_world_hint true and read_only_hint false. So a tool with no annotations is treated as possibly destructive. Set them honestly. Hosts use them to decide what needs approval, and our page on MCP tool annotations explains why clients must still treat them as hints.
Structured output
Pass structured_content: to send a machine-readable value next to the text. The guide says that per SEP-2106 it may be any JSON value, not only an object. It also says the server mirrors a non-object value into content as JSON text when you send no content blocks, so older clients still see it.
Server-side checking against output_schema is off by default. Turn it on with MCP::Configuration.new(validate_tool_call_results: true) and pass that as configuration: to MCP::Server.new. The tools spec says a tool that returns structured content should also return the serialized JSON as text.
Tool errors
Return error: true on the response and the result goes out with isError: true. The model reads your text and can retry, so say what went wrong and what valid input looks like.
The symbol-key trap
The guide calls this out as Ruby-specific. Transports parse JSON with symbolize_names: true, so nested objects arrive with symbol keys. payload["subject"] returns nil; use payload[:subject]. A unit test that passes string keys can pass while a real client fails. The guide's fix is to round-trip test arguments through JSON first:
delivered = JSON.parse(JSON.generate(arguments), symbolize_names: true)
MyTool.call(**delivered, server_context: nil)
Test with MCP Inspector
The MCP Inspector launches any stdio server from its command line. It needs Node 22.19.0 or newer:
npx @modelcontextprotocol/inspector ruby server.rb
Open the URL it prints, list the tools and call example_tool with a message. For scripted checks, add --cli and --method tools/list. Our MCP Inspector lesson walks through both modes.
In a host config, use the absolute path to Ruby and to server.rb, or bundle exec with the right working directory. The config generator builds the JSON block for common hosts.
Serve it over HTTP and Rails
The transports guide says MCP::Server::Transports::StreamableHTTPTransport is a standard Rack app. The simplest Rails setup mounts it in routes:
# config/routes.rb
server = MCP::Server.new(
name: "my_server",
title: "Example Server Display Name",
version: "1.0.0",
tools: [WeatherTool],
)
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
Rails.application.routes.draw do
mount transport => "/mcp"
end
The guide also shows a controller pattern that builds a server per request, which lets you pass server_context: { user_id: current_user.id }. That pattern sets stateless: true and serve_subscriptions_listen: false, because render buffers the body and cannot hold open a streaming response.
A runnable Rails app is in the repository's examples/rails folder.
Deploy notes
- Processes. On the handshake era, the HTTP transport stores session and SSE state in memory. The guide says to run one process (for example Puma with
workers 0) or use sticky sessions keyed onMcp-Session-Id. - Stateless options.
stateless: truedrops sessions for handshake clients. Modern 2026-07-28 requests are sessionless anyway, with caveats forsubscriptions/listenstreams. Our stateless MCP explainer covers the spec change. - Host and Origin checks. The transport validates
HostandOriginby default to block DNS rebinding. Behind a proxy or on a public host, setallowed_hosts:andallowed_origins:. - Auth. The SDK supports OAuth 2.1 resource-server protection. Any public endpoint needs it.
Common pitfalls
- String keys in nested arguments. Use symbols, and test with JSON round-tripped arguments.
- Printing to stdout. It breaks stdio. Log to stderr.
- Multi-worker servers on the handshake era. Sessions break across forked workers.
- Stale examples. The 2026-07-28 revision deprecates roots, sampling and logging, per the protocol versions page. They still work for handshake clients, but avoid building new features on them.
- Missing Host allow list. A deployed server returns 403 until you add your domain.
Next step
For design advice that applies in any language, read how to build your first MCP server. If you want a Ruby or Rails MCP server built, secured and shipped for your team, our MCP development service can help.
More in the MCP Guide
Free tools: server.json Validator · MCP Error Code Lookup
Latest research: State of MCP: October 2026
Frequently asked questions
Is there an official MCP SDK for Ruby?
Yes. The official gem is mcp, maintained in github.com/modelcontextprotocol/ruby-sdk. As of November 2026 the official SDK page lists it as Tier 1.
What Ruby version does the mcp gem need?
The gemspec sets required_ruby_version to 2.7.0 or later. Version 1.8.2 depends on json_schemer 2.4 or later.
Can I add an MCP server to an existing Rails app?
Yes. Mount StreamableHTTPTransport at a route such as /mcp, or call it from a controller. The transports guide documents both, and the repository ships a full Rails example.
Does the Ruby SDK support the 2026-07-28 spec?
Yes. Its protocol versions page lists 2026-07-28 as the modern, handshake-free version and says the bundled transports serve it next to older handshake versions.
Why is my nested argument nil?
Arguments arrive with symbol keys at every level. Read payload[:subject], not payload["subject"].











