Last active
May 25, 2025 18:32
-
-
Save maxim/e6d024892031ed526eb270c7baebe6c1 to your computer and use it in GitHub Desktop.
Revisions
-
maxim revised this gist
May 25, 2025 . No changes.There are no files selected for viewing
-
maxim created this gist
May 25, 2025 .There are no files selected for viewing
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 charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,153 @@ #!/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`