Menu

AI

How to Build an MCP Server in Ruby (Official mcp Gem, 2026)

Build an MCP server in Ruby with the official mcp gem. Define tools, return structured output, test with MCP Inspector and mount it in Rails over HTTP.

By: Irina ShvayaNovember 2, 2026

How to Build an MCP Server in Ruby (Official mcp Gem, 2026)

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 on json_schemer.
  • A tool is a class: description, input_schema, optional output_schema and annotations, and a self.call method.
  • 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.
  • StreamableHTTPTransport is a Rack app; mount it in Rails routes or call it from a controller with stateless: 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.

ItemValue (checked 2 November 2026)
Gemmcp on RubyGems
Latest release1.8.2, 27 October 2026
Ruby requirement2.7.0 or later (gemspec)
Runtime dependencyjson_schemer 2.4 or later
LicenseApache 2.0 for new contributions, existing code MIT
Official tierTier 1
Protocol versions2024-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::Tool is the base class. The README's sample calls show the tool name derived from the class as example_tool.
  • description is what the model reads to decide when to call the tool. Write it for a model, not a human skimming code.
  • input_schema is plain JSON Schema as a Ruby hash. The root stays type: "object".
  • call gets each top-level argument as a keyword, plus server_context, which carries per-request data such as auth state.
  • MCP::Tool::Response.new wraps a list of content blocks.
  • StdioTransport reads 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 on Mcp-Session-Id.
  • Stateless options. stateless: true drops sessions for handshake clients. Modern 2026-07-28 requests are sessionless anyway, with caveats for subscriptions/listen streams. Our stateless MCP explainer covers the spec change.
  • Host and Origin checks. The transport validates Host and Origin by default to block DNS rebinding. Behind a proxy or on a public host, set allowed_hosts: and allowed_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"].

You Might Also like to Read

Get a FREE Website Audit

Enter your website and get a 20–30 page SEO, GEO, AEO & CRO audit within 24 hours. Completely free.

Don't have a site yet? Get in touch →

Ready to speak with a marketing expert?
Give us a call! (916) 866-7893

Websites Launched
1407
Websites Launched
Marketing Experts
29
Marketing Experts
Hours of Dedicated Work
204533
Hours of Dedicated Work
Irina S.Benjamin G.Jeanette M.David G.