About Manceps
Manceps is an MCP server in the AI category: ruby client for the Model Context Protocol (MCP). Latin: one who takes in hand.
Install
git clone https://github.com/zarpay/mancepsThis server has no ConduID identity, so agent calls to it are not receipted. Pin the version you install and review the source before granting it credentials.
Ask AI
Ask AI about Manceps
Powered by Claude · Grounded in docs
Security checks
- ✓README presentThe repository ships documentation.
5 checks have not been run yet.
README
Manceps
A Ruby client for the Model Context Protocol (MCP).
From Latin manceps -- one who takes in hand (contractor, acquirer). From manus (hand) + capere (to take).
Installation
# Gemfile
gem "manceps"
Or install directly:
gem install manceps
Requires Ruby >= 3.4.0.
Quick Start
require "manceps"
# HTTP server with bearer auth
Manceps::Client.open("https://mcp.example.com/mcp", auth: Manceps::Auth::Bearer.new(ENV["MCP_TOKEN"])) do |client|
client.tools.each { |t| puts "#{t.name}: #{t.description}" }
result = client.call_tool("search_documents", query: "quarterly report")
puts result.text
end
# stdio server (local process)
Manceps::Client.open("npx", args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]) do |client|
contents = client.read_resource("file:///tmp/hello.txt")
puts contents.text
end
The block form connects, yields the client, and disconnects on exit -- even if an exception is raised.
Transports
Streamable HTTP
The primary MCP transport. Uses httpx for persistent connections -- MCP servers bind sessions to TCP connections, so connection reuse is required.
client = Manceps::Client.new("https://mcp.example.com/mcp", auth: auth)
stdio
Spawns a local subprocess and communicates via newline-delimited JSON over stdin/stdout.
client = Manceps::Client.new("npx", args: ["-y", "@modelcontextprotocol/server-memory"])
# With environment variables
client = Manceps::Client.new("mm-mcp", env: { "MM_TOKEN" => "...", "MM_URL" => "..." })
The transport auto-detects: HTTP(S) URLs use Streamable HTTP, everything else uses stdio.
Authentication
Bearer Token
auth = Manceps::Auth::Bearer.new("your-token")
API Key Header
auth = Manceps::Auth::ApiKeyHeader.new("x-api-key", "your-key")
OAuth 2.1 (Experimental)
RFC 8414 discovery, RFC 7591 dynamic registration, PKCE, and automatic token refresh. Works but not yet tested against a wide range of authorization servers.
# If you already have tokens
auth = Manceps::Auth::OAuth.new(
access_token: "...",
refresh_token: "...",
token_url: "https://auth.example.com/token",
client_id: "...",
expires_at: Time.now + 3600,
on_token_refresh: ->(tokens) { save_tokens(tokens) }
)
# Full discovery + authorization flow
discovery = Manceps::Auth::OAuth.discover("https://mcp.example.com", redirect_uri: "http://localhost:3000/callback")
pkce = Manceps::Auth::OAuth.generate_pkce
url = Manceps::Auth::OAuth.authorize_url(
authorization_url: discovery.authorization_url,
client_id: discovery.client_id,
redirect_uri: "http://localhost:3000/callback",
state: SecureRandom.hex(16),
scopes: discovery.scopes,
code_challenge: pkce[:challenge]
)
# Redirect user to `url`, then exchange the code:
tokens = Manceps::Auth::OAuth.exchange_code(
token_url: discovery.token_url,
client_id: discovery.client_id,
client_secret: discovery.client_secret,
code: params[:code],
redirect_uri: "http://localhost:3000/callback",
code_verifier: pkce[:verifier]
)
Token refresh happens automatically when a token is within 5 minutes of expiry. The on_token_refresh callback fires after each refresh so you can persist the new tokens.
No Auth
The default. Useful for local servers:
client = Manceps::Client.new("http://localhost:3000/mcp")
Tools
# List available tools
tools = client.tools
tools.each do |tool|
puts "#{tool.title || tool.name}: #{tool.description}"
puts " Input: #{tool.input_schema}"
puts " Output: #{tool.output_schema}" if tool.output_schema # structured output (2025-06-18+)
end
# Call a tool
result = client.call_tool("get_weather", location: "New York")
result.text # joined text content
result.content # Array<Content>
result.error? # true if server flagged an error
result.structured_content # parsed structured output (when tool declares outputSchema)
result.structured? # true if structured content present
# Stream a long-running tool call
client.call_tool_streaming("analyze_data", dataset: "large.csv") do |event|
puts "Progress: #{event}"
end
Resources
# List resources
resources = client.resources
resources.each { |r| puts "#{r.uri}: #{r.title || r.name}" }
# List resource templates
templates = client.resource_templates
templates.each { |t| puts "#{t.uri_template}: #{t.title || t.name}" }
# Read a resource
contents = client.read_resource("file:///project/src/main.rs")
puts contents.text
Prompts
# List prompts
prompts = client.prompts
prompts.each do |p|
puts "#{p.name}: #{p.description}"
p.arguments.each { |a| puts " #{a.name} (required: #{a.required?})" }
end
# Get a prompt
result = client.get_prompt("code_review", code: "def hello; end")
result.messages.each { |m| puts "#{m.role}: #{m.text}" }
Configuration
Manceps.configure do |c|
c.client_name = "MyApp" # default: "Manceps"
c.client_version = "1.0.0" # default: Manceps::VERSION
c.protocol_version = "2025-11-25" # default: "2025-11-25"
c.request_timeout = 60 # default: 30 (seconds)
c.connect_timeout = 15 # default: 10 (seconds)
c.client_description = "My app" # optional, sent in clientInfo
c.supported_versions = ["2025-11-25", "2025-06-18", "2025-03-26"] # for negotiation
end
Error Handling
All errors inherit from Manceps::Error:
Manceps::Error
Manceps::ConnectionError # transport-level failures
Manceps::TimeoutError # request or connect timeout
Manceps::ProtocolError # JSON-RPC error (has #code, #data)
Manceps::AuthenticationError # 401, failed OAuth flows
Manceps::SessionExpiredError # server invalidated the session (404)
Manceps::ToolError # tool invocation failed (has #result)
begin
result = client.call_tool("risky_operation", id: 42)
rescue Manceps::SessionExpiredError
client.connect # re-establish session
retry
rescue Manceps::ProtocolError => e
puts "RPC error #{e.code}: #{e.message}"
end
Why Manceps?
Persistent connections. MCP servers bind sessions to TCP connections. Manceps uses httpx to keep connections alive across requests, which most HTTP libraries don't do by default.
Auth-first. Bearer, API key, and OAuth 2.1 (experimental) are built in, not bolted on.
No LLM coupling. Pure protocol client. No to_openai_tools() or framework integrations -- use it with anything.
Extracted from production. Built and tested under real MCP load, not just spec examples.
Full 2025-11-25 spec. Protocol version negotiation, elicitation, tasks, structured tool output, MCP-Protocol-Version header -- not just the basics.
Notifications
Register handlers for server-initiated messages:
client.on("notifications/tools/list_changed") { puts "Tools changed!" }
client.on("notifications/resources/updated") { |params| puts "Updated: #{params['uri']}" }
# Subscribe to resource updates
client.subscribe_resource("file:///project/config.yml")
# Cancel a long-running request
client.cancel_request(request_id, reason: "User cancelled")
# Listen for notifications (blocking)
client.listen # dispatches to registered handlers
Elicitation
Handle server requests for additional user input during tool calls:
client.on_elicitation do |elicitation|
puts "Server asks: #{elicitation.message}"
puts "Schema: #{elicitation.requested_schema}"
# Respond with user input
Manceps::Elicitation.accept({ "name" => "Alice", "confirm" => true })
# Or decline/cancel:
# Manceps::Elicitation.decline
# Manceps::Elicitation.cancel
end
Elicitation capability is automatically declared during initialization when a handler is registered.
Tasks (Experimental)
Track long-running operations:
# List tasks
client.tasks.each { |t| puts "#{t.id}: #{t.status}" }
# Get a specific task
task = client.get_task("task-123")
task.completed? # => false
task.running? # => true
# Poll until done
task = client.await_task("task-123", interval: 2, timeout: 60)
puts task.result
# Cancel a task
client.cancel_task("task-123")
Resilience
Automatic retry with exponential backoff on connection failures:
# Connect retries up to max_retries (default: 3)
client = Manceps::Client.new("https://mcp.example.com/mcp", auth: auth, max_retries: 5)
# Requests auto-retry once on session expiry (re-initializes session)
client.call_tool("search", query: "test") # retries transparently on 404
# Check connection health
client.ping # => true/false
# Manual reconnect
client.reconnect!
Protocol Version
Manceps targets MCP protocol 2025-11-25 by default. It negotiates with the server during initialization and supports fallback to 2025-06-18 and 2025-03-26.
After initialization, the MCP-Protocol-Version header is included on all HTTP requests per the spec.
License
MIT. See LICENSE for details.
Author: Obie Fernandez
README copy captured when this server was indexed; it may lag the repository. The original is authoritative.