Bringing AI Agent Capabilities to Ruby with MCP

Kazuyuki Hashimoto - Feb 15 - - Dev Community

MCP is a protocol for building AI agents.

https://modelcontextprotocol.io/introduction

During the LLM boom that started with ChatGPT, LLMs were integrated as features in tools like GitHub Copilot. In the recent AI Agent trend, LLMs are expected and implemented to operate external resources and tools. However, unfortunately, the methods varied depending on the LLM. MCP aims to abstract these differences and enable similar operations across multiple LLMs.

Reference implementations were published in TypeScript and Python:

https://github.com/modelcontextprotocol/typescript-sdk
https://github.com/modelcontextprotocol/python-sdk

As a Ruby enthusiast, I created an SDK for Rubyists to use.

https://github.com/funwarioisii/mcp-rb

This article introduces mcp-rb.

Introduction to MCP Concepts

The expectation for AI Agents is to read external information and use it to manipulate the external environment. To achieve this, MCP defines two concepts: Tool and Resource. For web service developers, it might be easier to think of these as GET and POST.

mcp-rb provides a DSL to make implementing these concepts easier.

How to Write

I adopted a DSL influenced by Sinatra and graphql-ruby.

Let's review a bit.

In Sinatra, writing the following code returns the string 'Put this in your pipe & smoke it!' when accessing /frank-says. This not only defines the get method but also starts a web server at runtime.

require 'sinatra'
get '/frank-says' do
  'Put this in your pipe & smoke it!'
end
Enter fullscreen mode Exit fullscreen mode

Also, in graphql-ruby, mutations arguments were defined like this. The following code is partially modified from https://graphql-ruby.org/mutations/mutation_classes:

class Mutations::CreateComment < Mutations::BaseMutation
  argument :body, String, required: true
  argument :post_id, ID, required: true

  def resolve(body:, post_id:)
    ...
  end
end
Enter fullscreen mode Exit fullscreen mode

Both are very Ruby-like, simple, and easy to understand. I deeply respect these ideas. In mcp-rb, we provide a DSL inspired by these two to define MCP's Tools and Resources.

require "mcp"

name "demo"

tool "greet" do
  description "Greet someone by name"
  argument :name, String, required: true, description: "Name to greet"
  call { |args| "Hello, #{args[:name]}!" }
end

resource "hello://world" do
  name "Hello World"
  description "A simple hello world message"
  mime_type "text/plain"
  call { "Hello, World!" }
end
Enter fullscreen mode Exit fullscreen mode

What do you think?

You can start using it immediately with:

bundle add mcp-rb
Enter fullscreen mode Exit fullscreen mode

The implementation is publicly available at:

https://github.com/funwarioisii/mcp-rb

Which do you like?

Let's compare it with the provided TypeScript implementation.
You can see that the Ruby DSL allows for more concise writing.

TypeScript

import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";

// Create an MCP server
const server = new McpServer({
  name: "Demo",
});

// Add an addition tool
server.tool("add",
  { a: z.number(), b: z.number() },
  async ({ a, b }) => ({
    content: [{ type: "text", text: String(a + b) }]
  })
);

// Add a dynamic greeting resource
server.resource(
  "greeting",
  new ResourceTemplate("greeting://{name}", { list: undefined }),
  async (uri, { name }) => ({
    contents: [{
      uri: uri.href,
      text: `Hello, ${name}!`
    }]
  })
);

// Start receiving messages on stdin and sending messages on stdout
const transport = new StdioServerTransport();
await server.connect(transport);
Enter fullscreen mode Exit fullscreen mode

Ruby (I propose this)

require "mcp"

name "demo"

tool "add" do
  description "Add two numbers"
  argument :a, Integer, required: true
  argument :b, Integer, required: true
  call { |args| args[:a] + args[:b] }
end

# Dynamic resource definition is not yet implemented
resource "greeting://hello" do
  description "A greeting resource"
  call { |args| "Hello, #{args[:name]}!" }
end
Enter fullscreen mode Exit fullscreen mode

What's Missing

Of course, we haven't achieved a type system as powerful as TypeScript's. Also, several features provided in the official SDK are not yet implemented.

However, the sample code targeting Cosense, a Wiki service made in Japan, is already working with Claude. I'm already using it at work and finding it very comfortable.
https://github.com/funwarioisii/mcp-rb/blob/a5b84026bd7228d6b9f425b52ea0bc49781eff5f/examples/cosense_example.rb

It's published on GitHub under the MIT license. If you're interested in improving the user experience, please send a Pull Request.

Summary

mcp-rb provides a DSL for defining MCP Server Tools and Resources in Ruby. Ruby has various technical assets. Even in new fields like AI Agents, Ruby can leverage these technical assets. Please give it a try.

https://github.com/funwarioisii/mcp-rb

.