Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions mcp-client-ruby/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ANTHROPIC_API_KEY=
8 changes: 8 additions & 0 deletions mcp-client-ruby/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# frozen_string_literal: true

source "https://rubygems.org"

gem "anthropic"
gem "base64"
gem "dotenv"
gem "mcp", '>= 0.9.0'
3 changes: 3 additions & 0 deletions mcp-client-ruby/README.md
Original file line number Diff line number Diff line change
@@ -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.
169 changes: 169 additions & 0 deletions mcp-client-ruby/client.rb
Original file line number Diff line number Diff line change
@@ -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 <path_to_server_script>"
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
Loading