Last active
May 25, 2025 18:32
-
-
Save maxim/e6d024892031ed526eb270c7baebe6c1 to your computer and use it in GitHub Desktop.
A quick one file stdio MCP server in Ruby
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require 'json' | |
require 'shellwords' | |
module Mcp | |
Prop = Data.define(:name, :type, :desc, :req) do | |
def to_h = { type: type, description: desc } | |
end | |
class Definition | |
attr_accessor :name, :desc, :props | |
def initialize = @props = [] | |
def to_h | |
{ | |
name:, | |
description: desc, | |
inputSchema: { | |
type: 'object', | |
properties: props.map { [_1.name, _1.to_h] }.to_h, | |
required: props.select(&:req).map(&:name) | |
} | |
} | |
end | |
end | |
class Tool | |
class << self | |
attr_accessor :mcp | |
def inherited(base) = base.mcp = Definition.new | |
def name(string) = mcp.name = string | |
def desc(string) = mcp.desc = string | |
def arg(*args) = mcp.props << Prop[*args, true] | |
def opt(*args) = mcp.props << Prop[*args, false] | |
end | |
def run = raise 'Override in subclass' | |
end | |
class Server | |
ERROR_TYPES = { | |
invalid_json: [-32700, 'Invalid JSON'], | |
invalid_request: [-32600, 'Invalid request'], | |
method_not_found: [-32601, 'Method not found'], | |
invalid_params: [-32602, 'Invalid params'], | |
internal: [-32603, 'Internal error'] | |
}.freeze | |
def initialize(*tools) | |
@tools = tools.map { [_1.mcp.name, _1.mcp.to_h] }.to_h | |
@objs = tools.map(&:new) | |
end | |
def run | |
loop do | |
input = STDIN.gets | |
break if input.nil? | |
request = | |
begin | |
JSON.parse(input.strip) | |
rescue | |
puts error_for({'id' => nil}, :invalid_json) | |
STDOUT.flush | |
next | |
end | |
response = handle_request(request) | |
puts JSON.generate(response) | |
STDOUT.flush | |
end | |
end | |
private | |
def handle_request(request) | |
case request['method'] | |
when 'initialize' | |
response_for request, | |
protocolVersion: '2024-11-05', | |
capabilities: { tools: {} }, | |
serverInfo: { name: 'ruby-mcp-server', version: '1.0.0' } | |
when 'tools/list' | |
response_for request, tools: @tools.values | |
when 'tools/call' | |
handle_tool_call request | |
else | |
error_for(request, :method_not_found) | |
end | |
end | |
def handle_tool_call(request) | |
name = request.dig('params', 'name') | |
tool = @objs.find { _1.class.mcp.name == name } | |
return error_for(request, :invalid_params, "Unknown_tool: #{name}") if !tool | |
args = request.dig('params', 'arguments')&.transform_keys(&:to_sym) | |
begin | |
result = tool.run(**args) | |
# TODO: Support other content types (ask claude code which ones). | |
response_for(request, content: [{ type: 'text', text: result.to_s }]) | |
rescue => e | |
error_for(request, :internal, e.full_message(highlight: false)) | |
end | |
end | |
def error_for(request, type, message = ERROR_TYPES[type][1]) | |
code = ERROR_TYPES[type][0] | |
{ jsonrpc: '2.0', id: request['id'], error: { code:, message: } } | |
end | |
def response_for(request, **hash) | |
{ jsonrpc: '2.0', id: request['id'], result: hash } | |
end | |
end | |
def self.serve(...) = Server.new(...).run | |
end | |
# IMPLEMENT YOUR TOOLS HERE: | |
# ========================== | |
class Tool1 < Mcp::Tool | |
name 'my_tool' | |
desc 'explaination for LLM what it does' | |
# Use arg for required and opt for optional. | |
arg :some_required_arg, :string, 'tell llm what this arg is (add example)' | |
opt :some_optional_arg, :string, 'tell llm what this opt is (add example)' | |
# Implement this method | |
def run(some_required_arg:, some_optional_arg: nil) | |
# 1. do something | |
# 2. whatever you return will be to_json'ed | |
# 3. let errors just raise, it's handled by server | |
# 4. assume args are always strings | |
end | |
end | |
class Tool2 < Mcp::Tool | |
name 'other_tool' | |
desc 'explanation' | |
arg :some_required_arg, :string, 'description' | |
def run(some_required_arg:) = puts 'something' | |
end | |
Mcp.serve(Tool1, Tool2) if __FILE__ == $0 | |
# Place this file somewhere like: `bin/mcp` | |
# Make it executable: `chmod +x bin/mcp` | |
# Then add to Claude with: `claude mcp add some-mcp-name bin/mcp` |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment