Ruby MCP server giving AI agents tool access, dark navy and teal Tha-Shed branded graphic

Every MCP tutorial you’ll find is written in Python or TypeScript. That’s fine if your stack is Python or TypeScript. But a lot of DevOps and platform teams already have a decade of operational tooling written in Ruby, log parsers, cron managers, service watchdogs, the kind of scripts we’ve been covering in this series. Rather than rewrite all of that to hand it to an AI agent, you can wrap it in an MCP server and hand it over as is.

This post walks through building a working MCP server in plain Ruby, no framework required, that exposes real operational tools to an AI agent like Claude.

What MCP Actually Is

The Model Context Protocol is a standard way for an AI agent to discover what tools are available and call them, using JSON-RPC messages over a transport like standard input and output. An MCP server advertises a list of tools with names, descriptions, and input schemas. When the agent decides to use one, it sends a call, the server runs the actual code, and returns a result. The protocol is deliberately simple, which is exactly why it is easy to implement in a language with no official SDK yet.

Why Ruby Is an Underrated Choice Here

Ruby has a few things going for it that make it a good fit for this specific job. It has excellent standard library support for JSON, process management, and file I/O, everything an operational tool actually needs. It reads close to plain English, which matters when you are writing tool descriptions an LLM has to interpret correctly. And if your team already maintains Ruby scripts for system administration, an MCP wrapper is a thin layer on top of code that already works, not a rewrite.

Building a Minimal MCP Server in Ruby

The Three Things Every MCP Server Needs

Strip away the spec’s full surface area and a working server needs exactly three things: a way to list available tools, a way to handle a tool call and return a result, and a transport loop that reads JSON-RPC requests and writes JSON-RPC responses. Here is that loop:

require 'json'

TOOLS = {
  "check_disk_usage" => {
    description: "Report disk usage for a given mount point",
    input_schema: { type: "object", properties: { path: { type: "string" } }, required: ["path"] }
  }
}

def handle_request(req)
  case req["method"]
  when "tools/list"
    { tools: TOOLS.map { |name, t| { name: name, description: t[:description], inputSchema: t[:input_schema] } } }
  when "tools/call"
    name = req.dig("params", "name")
    args = req.dig("params", "arguments") || {}
    result = call_tool(name, args)
    { content: [{ type: "text", text: result }] }
  else
    { error: "unknown method #{req['method']}" }
  end
end

STDIN.each_line do |line|
  request = JSON.parse(line)
  response = handle_request(request)
  STDOUT.puts({ jsonrpc: "2.0", id: request["id"], result: response }.to_json)
  STDOUT.flush
end

A Real Tool: Disk Usage Checker

The call_tool method is where your existing Ruby operations code plugs in. This one shells out to df and formats the result as text the model can read directly:

def call_tool(name, args)
  case name
  when "check_disk_usage"
    path = args["path"] || "/"
    output = `df -h #{path}`
    "Disk usage for #{path}:\n#{output}"
  else
    "Unknown tool: #{name}"
  end
end

Swap that one case statement out for a call into any script from our disk usage reporting series or the TLS certificate expiry checker and the agent now has a real, tested tool instead of a toy example.

Wiring It Into an MCP Client

Most MCP clients, including Claude Desktop, read a small config file pointing at your server’s launch command:

{
  "mcpServers": {
    "ruby-ops": {
      "command": "ruby",
      "args": ["/path/to/ops_server.rb"]
    }
  }
}

Restart the client, and the tools you defined show up alongside whatever else the agent has access to.

Where This Gets Powerful: Multi-Tool Ops Servers

A single tool is a demo. A useful server exposes a handful of related, safe operations an agent can chain together during an actual investigation:

  • Disk and memory usage checks, read-only, no side effects
  • Log tail and grep, scoped to specific known-safe log directories
  • Certificate expiry lookups across a list of domains
  • Cron job listing and validation, using the patterns from our cron job manager post

Notice the pattern: everything on that list reads state. None of it changes anything. That is deliberate.

Security Considerations

An MCP server is a remote code execution surface by design, that’s the entire point, so treat it that way. Keep write and restart actions out of an agent-facing server unless you have a specific, reviewed reason to include them. Hardcode an allowlist of paths and hosts the tools can touch rather than accepting arbitrary input. Log every tool call with its arguments so you have an audit trail if something goes wrong. And run the server under a dedicated, minimally privileged system account, not your own login.

If you want the deeper security fundamentals behind that advice, our Linux System Administration course covers permission models and service isolation in more depth than fits in one blog post.

FAQ

Do I need the official MCP SDK to build a Ruby server?

No. The protocol is just JSON-RPC over a transport, so a plain Ruby script reading STDIN and writing STDOUT works fine, as shown above. An SDK adds convenience, not capability, once you understand the three core pieces: list tools, handle calls, run the loop.

Can an MCP server modify my systems, not just read from them?

It can if you write it that way, which is exactly why you generally shouldn’t. Keep agent-facing tools read-only wherever possible, and if you must expose a write action, gate it behind explicit confirmation and a narrow allowlist rather than open-ended shell access.

What’s the fastest way to test a new tool without a full MCP client?

Pipe a hand-written JSON-RPC request straight into your script from the command line, for example echo '{"id":1,"method":"tools/call","params":{"name":"check_disk_usage","arguments":{"path":"/"}}}' | ruby ops_server.rb, and read the response on STDOUT before ever connecting a real client.

Click to access the login or register cheese