ask-mcp
Model Context Protocol (MCP) client and server for Ruby. Connect to MCP servers via stdio, SSE, or Streamable HTTP transports. Run as an MCP server to expose your own tools to any MCP client. No framework lock-in — just implement a couple of duck-typed methods and you're done.
MCP is the industry standard for LLM tool discovery — the same protocol used by Claude Code, Codex, Cursor, and GitHub Copilot.
Installation
gem "ask-mcp"
Or add to your Gemfile:
gem "ask-mcp", "~> 0.1.0"
Quick Start — Client
Connect to any MCP server and call its tools:
require "ask/mcp"
client = Ask::MCP.from_stdio("npx", ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])
client.start
client.tools.each { |name, tool| puts "#{name}: #{tool.description}" }
result = client.call_tool("read_file", path: "/tmp/test.txt")
puts result
client.stop
Quick Start — Server
Run as a standalone MCP server exposing your own tools to any MCP client
(Codex, Claude Code, Cursor, etc.). Any object that responds to name,
description, params_schema, and call(args) will work:
require "ask/mcp"
# Define your tools — no base class needed, just duck typing
class Greeter
def name; "greet" end
def description; "Greets someone by name" end
def params_schema
{ type: "object", properties: { "name" => { "type" => "string" } }, required: ["name"] }
end
def call(args = {})
"Hello, #{args['name']}!"
end
end
# Start the server (blocking — runs until stdin closes)
Ask::MCP::Server.start_stdio(
name: "my-server",
tools: [Greeter.new]
)
Configure your MCP client:
{
"mcpServers": {
"my-server": {
"command": "ruby",
"args": ["/path/to/your/server.rb"]
}
}
}
Tools can return any value the client can use. The server automatically wraps
the result into MCP's content array format.
What about complex tools?
The result object from call(args) should respond to ok? (or ok) and
output / error_message. Use OpenStruct for simple cases:
require "ostruct"
class BashTool
def name; "bash" end
def description; "Run a shell command" end
def params_schema
{ type: "object", properties: { "command" => { "type" => "string" } }, required: ["command"] }
end
def call(args = {})
output = `#{args['command']} 2>&1`
OpenStruct.new(ok?: true, output: output)
rescue => e
OpenStruct.new(ok?: false, error_message: e.)
end
end
For a production example with shell tools, file ops, and web search, see llm-proxy.
Using with ask-tools
If you use the ask-rb ecosystem, you can expose Ask::Tool subclasses directly:
require "ask/mcp"
require "ask-tools-shell"
require "ask-web-search"
tools = Ask::Tools::Shell::TOOLS.map(&:new) + [Ask::Tools::WebSearch.new]
Ask::MCP::Server.start_stdio(
name: "my-server",
tools: tools,
capabilities: { tools: {} }
)
Transports
# stdio — local processes
Ask::MCP.from_stdio("npx", ["-y", "@modelcontextprotocol/server-github"])
# SSE — remote servers with Server-Sent Events
Ask::MCP.from_sse("https://mcp.example.com/sse")
# Streamable HTTP — remote servers
Ask::MCP.from_http("https://mcp.example.com/mcp")
API
Client Lifecycle
# Create a client with any transport
transport = Ask::MCP::Transport::Stdio.new("ruby", ["server.rb"])
client = Ask::MCP::Client.new(transport, timeout: 30)
# Start the session (sends initialize + receives capabilities)
client.start
# Use the client
client.tools # => { "tool_name" => #<Ask::MCP::Tool> }
client.resources # => { "resource_uri" => #<Ask::MCP::Resource> }
client.prompts # => { "prompt_name" => #<Ask::MCP::Prompt> }
client.call_tool("tool_name", arg1: "value")
client.read_resource("file:///path")
client.get_prompt("prompt_name", arg1: "value")
# Stop the session
client.stop
Tool, Resource, Prompt Objects
# Tool
tool = Ask::MCP::Tool.new(
name: "read_file",
description: "Read a file from disk",
input_schema: {
type: "object",
properties: { path: { type: "string" } },
required: ["path"]
}
)
tool.name # => "read_file"
tool.description # => "Read a file from disk"
tool.input_schema # => { type: "object", ... }
# Resource
resource = Ask::MCP::Resource.new(
uri: "file:///tmp/test.txt",
name: "Test File",
mime_type: "text/plain"
)
# Prompt
prompt = Ask::MCP::Prompt.new(
name: "greet",
description: "Generate a greeting",
arguments: [{ name: "name", description: "Name to greet", required: true }]
)
Authentication
# Token-based auth
token = Ask::MCP::Auth::Token.new("my-api-token")
headers = token.apply({}) # => { "Authorization" => "Bearer my-api-token" }
# OAuth 2.1
oauth = Ask::MCP::Auth::OAuth.new(
client_id: "my-client",
client_secret: "my-secret",
token_url: "https://auth.example.com/token",
scopes: ["mcp"]
)
oauth.authenticate!
headers = oauth.apply({})
With ask-agent
require "ask/mcp"
client = Ask::MCP.from_stdio("npx", ["-y", "@modelcontextprotocol/server-github"])
client.start
# Convert MCP tools to Ask::Tool instances for use with Ask::Agent
client.tools.each do |name, mcp_tool|
agent.register_tool(mcp_tool.to_ask_tool)
end
# Or use the adapter directly
wrapped = Ask::MCP::Adapters::AskTool.wrap(client.tools)
wrapped.each { |name, adapter| agent.register_tool(adapter.to_ask_tool) }
Architecture
ask-mcp/
├── lib/ask/mcp.rb # Entry point, factory methods
├── lib/ask/mcp/client.rb # MCP client (connect, call_tool, etc.)
├── lib/ask/mcp/server.rb # MCP server representation + Server.start_stdio entry point
├── lib/ask/mcp/server/stdio.rb # MCP server stdio runtime (run as server)
├── lib/ask/mcp/tool.rb # MCP tool representation
├── lib/ask/mcp/resource.rb # MCP resource representation
├── lib/ask/mcp/prompt.rb # MCP prompt representation
├── lib/ask/mcp/native/messages.rb # JSON-RPC message layer
├── lib/ask/mcp/transport/
│ ├── stdio.rb # stdio transport (client direction)
│ ├── sse.rb # Server-Sent Events transport
│ └── streamable_http.rb # Streamable HTTP transport
├── lib/ask/mcp/auth/
│ ├── oauth.rb # OAuth 2.1 for MCP
│ └── token.rb # Token-based auth
└── lib/ask/mcp/adapters/
├── ask_tool.rb # MCP::Tool → Ask::Tool adapter
└── tool_server.rb # Duck-typed tools → MCP server adapter
Development
# Run tests
bundle exec rake test
# Run specific tests
bundle exec ruby -Itest test/messages_test.rb
bundle exec ruby -Itest test/stdio_integration_test.rb
License
MIT
Authentication
See the Auth Setup Guide for detailed documentation on token-based and OAuth 2.1 authentication, including ask-auth integration.