From c96beb2508610df38c27361026d354b13ecb4a59 Mon Sep 17 00:00:00 2001 From: Koichi ITO Date: Sat, 21 Mar 2026 03:54:27 +0900 Subject: [PATCH] Add Ruby MCP client example ## Motivation and Context Add an LLM-powered chatbot MCP client example using the MCP Ruby SDK and the Anthropic Ruby SDK, following the same pattern as other language examples. https://github.com/modelcontextprotocol/ruby-sdk The Gemfile specifies mcp version 0.9.0 or higher, which supports the stdio client. https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.9.0 ## Additional context Once https://github.com/anthropics/anthropic-sdk-ruby/pull/161 is merged and released, `gem "base64"` will no longer be needed in the Gemfile. --- mcp-client-ruby/.env.example | 1 + mcp-client-ruby/Gemfile | 8 ++ mcp-client-ruby/README.md | 3 + mcp-client-ruby/client.rb | 169 +++++++++++++++++++++++++++++++++++ 4 files changed, 181 insertions(+) create mode 100644 mcp-client-ruby/.env.example create mode 100644 mcp-client-ruby/Gemfile create mode 100644 mcp-client-ruby/README.md create mode 100644 mcp-client-ruby/client.rb diff --git a/mcp-client-ruby/.env.example b/mcp-client-ruby/.env.example new file mode 100644 index 00000000..8b3e1258 --- /dev/null +++ b/mcp-client-ruby/.env.example @@ -0,0 +1 @@ +ANTHROPIC_API_KEY= diff --git a/mcp-client-ruby/Gemfile b/mcp-client-ruby/Gemfile new file mode 100644 index 00000000..286ffd28 --- /dev/null +++ b/mcp-client-ruby/Gemfile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "anthropic" +gem "base64" +gem "dotenv" +gem "mcp", '>= 0.9.0' diff --git a/mcp-client-ruby/README.md b/mcp-client-ruby/README.md new file mode 100644 index 00000000..e8f26cc0 --- /dev/null +++ b/mcp-client-ruby/README.md @@ -0,0 +1,3 @@ +# An LLM-Powered Chatbot MCP Client written in Ruby + +See the [Building MCP clients](https://modelcontextprotocol.io/tutorials/building-a-client) tutorial for more information. diff --git a/mcp-client-ruby/client.rb b/mcp-client-ruby/client.rb new file mode 100644 index 00000000..c4aba72e --- /dev/null +++ b/mcp-client-ruby/client.rb @@ -0,0 +1,169 @@ +# frozen_string_literal: true + +require "anthropic" +require "dotenv/load" +require "json" +require "mcp" + +class MCPClient + ANTHROPIC_MODEL = "claude-sonnet-4-5" + + def initialize + @mcp_client = nil + @transport = nil + @anthropic_client = nil + end + + def connect_to_server(server_script_path) + command = case File.extname(server_script_path) + when ".rb" + "ruby" + when ".py" + "python3" + when ".js" + "node" + else + raise ArgumentError, "Server script must be a .rb, .py, or .js file." + end + + @transport = MCP::Client::Stdio.new(command: command, args: [server_script_path]) + @mcp_client = MCP::Client.new(transport: @transport) + + @tools = @mcp_client.tools + tool_names = @tools.map(&:name) + + puts "\nConnected to server with tools: #{tool_names}" + end + + def chat_loop + puts <<~MESSAGE + MCP Client Started! + Type your queries or 'quit' to exit. + MESSAGE + + loop do + print "\nQuery: " + line = $stdin.gets + break if line.nil? + + query = line.chomp.strip + break if query.downcase == "quit" + next if query.empty? + + begin + response = process_query(query) + puts "\n#{response}" + rescue => e + puts "\nError: #{e.message}" + end + end + end + + def cleanup + @transport&.close + end + + private + + def process_query(query) + messages = [{ role: "user", content: query }] + + available_tools = @tools.map do |tool| + { name: tool.name, description: tool.description, input_schema: tool.input_schema } + end + + # Initial Claude API call. + response = chat(messages, tools: available_tools) + + # Process response and handle tool calls. + if response.content.any?(Anthropic::Models::ToolUseBlock) + assistant_content = response.content.filter_map do |content_block| + case content_block + when Anthropic::Models::TextBlock + { type: "text", text: content_block.text } + when Anthropic::Models::ToolUseBlock + { type: "tool_use", id: content_block.id, name: content_block.name, input: content_block.input } + end + end + messages << { role: "assistant", content: assistant_content } + end + + response.content.each_with_object([]) do |content, response_parts| + case content + when Anthropic::Models::TextBlock + response_parts << content.text + when Anthropic::Models::ToolUseBlock + tool_name = content.name + tool_args = content.input + + # Find the matching MCP tool object. + tool = @tools.find { |tool| tool.name == tool_name } + + # Execute tool call via MCP. + result = @mcp_client.call_tool(tool: tool, arguments: tool_args) + response_parts << "[Calling tool #{tool_name} with args #{tool_args.to_json}]" + + tool_result_content = result.dig("result", "content") + result_text = if tool_result_content.is_a?(Array) + tool_result_content.filter_map { |content_item| content_item["text"] }.join("\n") + else + tool_result_content.to_s + end + + messages << { + role: "user", + content: [{ + type: "tool_result", + tool_use_id: content.id, + content: result_text + }] + } + + # Get next response from Claude. + response = chat(messages) + + response.content.each do |content_block| + response_parts << content_block.text if content_block.is_a?(Anthropic::Models::TextBlock) + end + end + end.join("\n") + end + + def chat(messages, tools: nil) + params = { model: ANTHROPIC_MODEL, max_tokens: 1000, messages: messages } + params[:tools] = tools if tools + + anthropic_client.messages.create(**params) + end + + def anthropic_client + @anthropic_client ||= Anthropic::Client.new(api_key: ENV["ANTHROPIC_API_KEY"]) + end +end + +if ARGV.empty? + puts "Usage: ruby client.rb " + exit 1 +end + +client = MCPClient.new + +begin + client.connect_to_server(ARGV[0]) + + api_key = ENV["ANTHROPIC_API_KEY"] + if api_key.nil? || api_key.empty? + puts <<~MESSAGE + No ANTHROPIC_API_KEY found. To query these tools with Claude, set your API key: + export ANTHROPIC_API_KEY=your-api-key-here + MESSAGE + exit + end + + client.chat_loop +rescue => e + puts "Error: #{e.message}" + exit 1 +ensure + client.cleanup +end