Ruby Meets Claude: Building Your First MCP Server in 5 Minutes
You don't need Python to build AI agents. Ruby's elegant block syntax makes defining AI tools cleaner and more readable than Python decorators. In this quick-start guide, you'll go from zero to a working MCP server that Claude can call directly.
Table of Contents
- Why Ruby for MCP?
- The Setup
- The Server Code
- The "Ah-ha" Moment: Your First Tool
- The Wiring: Connecting to Claude Desktop
- The Payoff: Testing Your Server
- Next Steps
Why Ruby for MCP?
Ruby developers often feel left out of the AI tooling ecosystem, which tends to favor Python. But the Model Context Protocol changes that. Here's why Ruby is actually a fantastic choice:
- Block syntax: Defining tools with Ruby blocks is more intuitive than Python decorators
- Readability: Ruby's expressiveness makes your tool definitions self-documenting
- Ecosystem: Leverage the rich Ruby ecosystem including Rails, Sidekiq, and more
- Familiarity: If you already know Ruby, why learn another language just for AI integration?
The Setup
Getting started takes just two commands:
# Install the official Ruby MCP SDK
gem install mcp-sdk
# Create your project directory
mkdir my-first-mcp-server
cd my-first-mcp-server
That's it. No virtual environments, no complex dependency management. Just pure Ruby simplicity.
Project Structure
Your minimal project structure looks like this:
my-first-mcp-server/
βββ server.rb # Your MCP server code
βββ Gemfile # Optional, for dependency management
If you prefer using Bundler:
# Gemfile
source 'https://rubygems.org'
gem 'mcp-sdk'
Then run bundle install.
The Server Code
Let's create the most basic MCP server possible. Create a file called server.rb:
require "mcp"
# Instantiate your server with a descriptive name
server = MCP::Server.new(name: "ruby-quickstart")
# Your server is now ready to register tools!
puts "Ruby MCP Server initialized"
This creates an MCP server instance. The name parameter identifies your server to Claude Desktop and appears in logs and debugging output.
The "Ah-ha" Moment: Your First Tool
Now for the magic. Let's add a tool that returns the current time. This is where Ruby's block syntax shines:
require "mcp"
# The most basic implementation using the Official SDK
server = MCP::Server.new(name: "ruby-quickstart")
server.register_tool(
name: "get_current_time",
description: "Returns the current time in a specific timezone",
input_schema: {
type: "object",
properties: { timezone: { type: "string" } }
}
) do |input|
# Pure Ruby logic here
require 'time'
timezone = input[:timezone] || "UTC"
time = Time.now.getlocal(timezone_offset(timezone))
{ time: time.strftime("%Y-%m-%d %H:%M:%S %Z") }
end
# Helper method for timezone conversion
def timezone_offset(tz)
offsets = {
"UTC" => "+00:00",
"EST" => "-05:00",
"PST" => "-08:00",
"GMT" => "+00:00",
"CET" => "+01:00",
"JST" => "+09:00"
}
offsets[tz.upcase] || "+00:00"
end
# Use Stdio for local desktop integration
server.run
Understanding the Code
Let's break down what's happening:
register_tool: This method defines a new tool that Claude can callname: A unique identifier for your tool (snake_case recommended)description: Tells Claude what this tool does and when to use itinput_schema: JSON Schema defining what parameters the tool accepts- The block: Contains your actual Ruby logic that executes when called
Key Insight: The
Stdiotransport (used by default withserver.run) means Claude Desktop actually spawns and runs your Ruby script directly. No HTTP servers, no ports, no networking complexity.
The Wiring: Connecting to Claude Desktop
To make Claude aware of your Ruby server, you need to update the Claude Desktop configuration file.
Finding the Config File
macOS:
~/Library/Application Support/Claude/claude_desktop_config.json
Windows:
%APPDATA%\Claude\claude_desktop_config.json
Linux:
~/.config/Claude/claude_desktop_config.json
Adding Your Server
Edit the configuration file to include your Ruby server:
{
"mcpServers": {
"ruby-quickstart": {
"command": "ruby",
"args": ["/absolute/path/to/your/server.rb"]
}
}
}
Important Notes:
- Use absolute paths, not relative paths
- On Windows, use forward slashes or escaped backslashes:
C:/Users/you/server.rborC:\\Users\\you\\server.rb - Make sure Ruby is in your system PATH, or use the full path to the Ruby executable
Verify Ruby is Accessible
Before restarting Claude, verify Ruby is accessible:
# Check Ruby version
ruby --version
# Verify you can run your script
ruby /path/to/server.rb
If the script runs without errors and waits for input (Stdio mode), you're ready!
The Payoff: Testing Your Server
- Restart Claude Desktop completely (quit and reopen)
- Open a new conversation
- Ask Claude: "What time is it in PST?"
You should see Claude call your get_current_time tool and return the result from your Ruby code!
Example Interaction
You: What time is it right now in Tokyo?
Claude: Let me check the current time for you. [Calls get_current_time with timezone: "JST"]
The current time in Tokyo (JST) is 2025-12-05 03:45:12 JST.
Debugging Tips
If things don't work:
- Check Claude's logs: Look in the Claude Desktop developer console for error messages
- Test standalone: Run
ruby server.rbin terminal and ensure no errors - Verify JSON: Ensure your config file is valid JSON (no trailing commas!)
- Restart completely: Claude caches server configurations; a full restart is required
Adding More Tools
Once you have the basics working, adding more tools is simple:
# A calculator tool
server.register_tool(
name: "calculate",
description: "Performs basic mathematical calculations",
input_schema: {
type: "object",
properties: {
expression: {
type: "string",
description: "A mathematical expression like '2 + 2' or '10 * 5'"
}
},
required: ["expression"]
}
) do |input|
# SAFETY: Only allow basic math operations
expression = input[:expression]
if expression.match?(/\A[\d\s\+\-\*\/\(\)\.]+\z/)
result = eval(expression)
{ result: result, expression: expression }
else
{ error: "Invalid expression. Only numbers and +, -, *, / are allowed." }
end
end
# A greeting tool
server.register_tool(
name: "greet",
description: "Generates a personalized greeting",
input_schema: {
type: "object",
properties: {
name: { type: "string" },
language: {
type: "string",
enum: ["english", "spanish", "french", "japanese"]
}
},
required: ["name"]
}
) do |input|
greetings = {
"english" => "Hello",
"spanish" => "Hola",
"french" => "Bonjour",
"japanese" => "Konnichiwa"
}
language = input[:language] || "english"
greeting = greetings[language] || greetings["english"]
{ message: "#{greeting}, #{input[:name]}!" }
end
Complete Working Example
Here's a complete server with multiple tools:
#!/usr/bin/env ruby
require "mcp"
server = MCP::Server.new(name: "ruby-quickstart")
# Tool 1: Current Time
server.register_tool(
name: "get_current_time",
description: "Returns the current time. Optionally specify a timezone.",
input_schema: {
type: "object",
properties: {
timezone: {
type: "string",
description: "Timezone like UTC, EST, PST, GMT, CET, JST"
}
}
}
) do |input|
require 'time'
offsets = { "UTC" => 0, "EST" => -5, "PST" => -8, "GMT" => 0, "CET" => 1, "JST" => 9 }
tz = (input[:timezone] || "UTC").upcase
offset = offsets[tz] || 0
time = Time.now.utc + (offset * 3600)
{ time: time.strftime("%Y-%m-%d %H:%M:%S"), timezone: tz }
end
# Tool 2: Word Counter
server.register_tool(
name: "count_words",
description: "Counts the number of words in a given text",
input_schema: {
type: "object",
properties: {
text: { type: "string", description: "The text to count words in" }
},
required: ["text"]
}
) do |input|
words = input[:text].split(/\s+/).reject(&:empty?)
{
word_count: words.length,
character_count: input[:text].length,
character_count_no_spaces: input[:text].gsub(/\s/, '').length
}
end
# Tool 3: Random Number Generator
server.register_tool(
name: "random_number",
description: "Generates a random number between min and max (inclusive)",
input_schema: {
type: "object",
properties: {
min: { type: "integer", description: "Minimum value (default: 1)" },
max: { type: "integer", description: "Maximum value (default: 100)" }
}
}
) do |input|
min = input[:min] || 1
max = input[:max] || 100
{ random_number: rand(min..max), range: "#{min}-#{max}" }
end
# Start the server
server.run
Next Steps
Now that you have a working Ruby MCP server, here's where to go next:
- Exposing ActiveRecord Models to AI Agents: Connect your Rails database to Claude
- Understanding MCP: Tools vs. Resources: Learn when to use each primitive
- Testing Your AI Tools with RSpec: Write proper tests for your MCP tools
- Stdio vs. SSE Transport: Deploy to production with SSE
Troubleshooting
"Command not found: ruby"
Ensure Ruby is installed and in your PATH:
# macOS with Homebrew
brew install ruby
# Ubuntu/Debian
sudo apt install ruby-full
# Windows
# Download from https://rubyinstaller.org/
"Cannot load such file -- mcp"
Install the MCP SDK gem:
gem install mcp-sdk
"Server not appearing in Claude"
- Verify your
claude_desktop_config.jsonis valid JSON - Use absolute paths in the configuration
- Restart Claude Desktop completely
- Check Claude's developer logs for errors
"Tool returns error"
Add debugging to your Ruby script:
server.register_tool(...) do |input|
STDERR.puts "Received input: #{input.inspect}"
# Your code here
end
Check Claude Desktop logs for STDERR output.
Summary
In just 5 minutes, you've:
- Installed the Ruby MCP SDK
- Created a server with a working tool
- Connected it to Claude Desktop
- Successfully called Ruby code from an AI
Ruby's elegant syntax makes MCP development a joy. The block-based tool registration is intuitive, readable, and very Ruby-like. Welcome to the world of AI-powered Ruby applications!