Ruby MCP Client
Um cliente Ruby para o Model Context Protocol (MCP), permitindo integração com ferramentas e serviços externos por meio de um protocolo padronizado.
Documentação
ruby-mcp-client
Um cliente Ruby para o Model Context Protocol (MCP), permitindo integração com ferramentas e serviços externos por meio de um protocolo padronizado.
Instalação
# Gemfile
gem 'ruby-mcp-client'
bundle install
# or
gem install ruby-mcp-client
Visão geral
O MCP permite que assistentes de IA descubram e invoquem ferramentas externas por meio de diferentes mecanismos de transporte:
- stdio - Processos locais que implementam o protocolo MCP
- SSE - Server-Sent Events com suporte a streaming
- HTTP - Requisição/resposta simples (sem streaming)
- Streamable HTTP - HTTP POST com respostas formatadas em SSE
Conversões de API integradas: to_openai_tools(), to_anthropic_tools(), to_google_tools()
Suporte ao Protocolo MCP
Implementa a especificação MCP 2025-11-25. O cliente negocia a
versão do protocolo durante initialize e desconecta se o servidor responder
com uma revisão que ele não consegue falar (suportadas: 2025-11-25, 2025-06-18,
2025-03-26, 2024-11-05):
- Ferramentas: listar, chamar, streaming, anotações (estilo dica), saídas estruturadas, título
- Prompts: listar, obter com parâmetros
- Recursos: listar, ler, modelos, assinaturas, paginação, conteúdo ResourceLink
- Elicitação: Interações iniciadas pelo servidor (stdio, SSE, Streamable HTTP)
- Raízes: Limites de escopo do sistema de arquivos com notificações de alteração
- Amostragem: Completions de LLM solicitadas pelo servidor com modelPreferences
- Conclusão: Autocompletar para prompts/recursos com contexto
- Registro: Mensagens de log do servidor com filtro por nível
- Tarefas:
tools/callaumentado por tarefa — criar com umttl, consultartasks/get, recuperar viatasks/result, além detasks/listetasks/cancel - Áudio: Suporte a tipo de conteúdo de áudio
- Progresso e Cancelamento:
progressTokencom callbacks por chamada;notifications/cancelledautomático para requisições abandonadas - Metadados:
icons,titlee_metaanalisados em ferramentas, prompts e recursos - OAuth 2.1: PKCE (S256 obrigatório), descoberta RFC 8414/9728, registro dinâmico, Documentos de Metadados de ID do Cliente, desafios de aumento de escopo
Os transportes tratam o servidor como entrada não confiável — veja Tratando o Servidor como Não Confiável para os limites aplicados a dados controlados por pares.
API de Conexão Rápida (Recomendado)
A maneira mais simples de conectar a um servidor MCP:
require 'mcp_client'
# Auto-detect transport from URL
client = MCPClient.connect('http://localhost:8000/sse') # SSE
client = MCPClient.connect('http://localhost:8931/mcp') # Streamable HTTP
client = MCPClient.connect('npx -y @modelcontextprotocol/server-filesystem /home') # stdio
# With options
client = MCPClient.connect('http://api.example.com/mcp',
headers: { 'Authorization' => 'Bearer TOKEN' },
read_timeout: 60,
retries: 3,
logger: Logger.new($stdout)
)
# Multiple servers
client = MCPClient.connect(['http://server1/mcp', 'http://server2/sse'])
# Force specific transport
client = MCPClient.connect('http://custom.com/api', transport: :streamable_http)
# Use the client
tools = client.list_tools
result = client.call_tool('example_tool', { param: 'value' })
client.cleanup
Detecção de Transporte:
| Padrão de URL | Transporte |
|---|---|
Termina com /sse | SSE |
Termina com /mcp | Streamable HTTP |
stdio://command ou Array | stdio |
npx, node, python, etc. | stdio |
| Outras URLs HTTP | Detecção automática (Streamable HTTP → SSE → HTTP) |
Trabalhando com Ferramentas, Prompts e Recursos
# Tools
tools = client.list_tools
result = client.call_tool('tool_name', { param: 'value' })
result = client.call_tool('tool_name', { param: 'value' }, server: 'server_name')
# Batch tool calls
results = client.call_tools([
{ name: 'tool1', parameters: { key: 'value' } },
{ name: 'tool2', parameters: { key: 'value' }, server: 'specific_server' }
])
# Streaming (SSE/Streamable HTTP)
client.call_tool_streaming('tool', { param: 'value' }).each do |chunk|
puts chunk
end
# Prompts
prompts = client.list_prompts
result = client.get_prompt('greeting', { name: 'Alice' })
# Pagination: list_tools and list_prompts automatically follow the server's
# nextCursor and return the COMPLETE set across all pages (with a per-call
# safety bound and an identical-cursor loop guard). No manual cursor handling
# is required.
# Resources
result = client.list_resources
contents = client.read_resource('file:///example.txt')
contents.each do |content|
puts content.text if content.text?
data = Base64.decode64(content.blob) if content.binary?
end
Recursos do MCP 2025-11-25
Anotações de Ferramenta
tool = client.find_tool('delete_user')
# Hint-style annotations (MCP 2025-11-25)
# Defaults follow the MCP ToolAnnotations schema: when a hint is absent the
# client assumes the less-safe value, so an un-annotated tool is treated as
# writable, potentially destructive, and open-world.
tool.read_only_hint? # Defaults to false; tool may modify its environment
tool.destructive_hint? # Defaults to true; tool may perform destructive updates
tool.idempotent_hint? # Defaults to false; repeated calls may have additional effects
tool.open_world_hint? # Defaults to true; tool may interact with external entities
# Legacy annotations
tool.read_only? # Safe to execute?
tool.destructive? # Warning: destructive operation
tool.requires_confirmation? # Needs user confirmation
Saídas Estruturadas
tool = client.find_tool('get_weather')
tool.structured_output? # Has output schema?
tool.output_schema # JSON Schema for output
result = client.call_tool('get_weather', { location: 'SF' })
data = result['structuredContent'] # Type-safe structured data
# Per MCP 2025-11-25, clients SHOULD validate structured results against the
# tool's output schema, and a tool that declares an outputSchema must return
# structuredContent in successful results. call_tool checks both automatically
# for the common JSON Schema keywords (type, properties, required, items, enum,
# numeric/string bounds). The full 2020-12 vocabulary ($ref/$dynamicRef/$defs,
# allOf/anyOf/oneOf/not, if/then/else, additionalProperties, patternProperties,
# propertyNames, prefixItems, contains/minContains/maxContains, uniqueItems,
# multipleOf, format, dependentRequired/dependentSchemas, minProperties/
# maxProperties, unevaluated*) is NOT evaluated: when a schema uses any of
# those keywords, call_tool logs a "validation is partial" warning naming them
# (in both modes), since data may pass this check that a full validator would
# reject. By default a violation (mismatch, or missing structuredContent on a
# successful result) logs a warning; opt in to strict mode to raise instead:
client = MCPClient::Client.new(
mcp_server_configs: [...],
validate_structured_content: :strict # raises MCPClient::Errors::ValidationError on violation
)
# Task-delivered results (get_task_result) are not validated yet.
Raízes
# Set filesystem scope boundaries
client.roots = [
{ uri: 'file:///home/user/project', name: 'Project' },
{ uri: 'file:///var/log', name: 'Logs' }
]
# Access current roots
client.roots
Amostragem (Completions de LLM solicitadas pelo servidor)
# Configure handler when creating client
client = MCPClient.connect('http://server/mcp',
sampling_handler: ->(messages, model_prefs, system_prompt, max_tokens) {
# Process server's LLM request
{
'model' => 'gpt-4',
'stopReason' => 'endTurn',
'role' => 'assistant',
'content' => { 'type' => 'text', 'text' => 'Response here' }
}
}
)
A chamada de ferramenta com amostragem (SEP-1577) é opcional: passe sampling_supports_tools: true
para declarar a capacidade sampling.tools. O manipulador então recebe os
parâmetros completos da requisição (incluindo tools/toolChoice) como um quinto argumento opcional;
sem a adesão, requisições de amostragem habilitadas para ferramentas são rejeitadas com -32602
como a especificação exige:
client = MCPClient::Client.new(
mcp_server_configs: [...],
sampling_supports_tools: true,
sampling_handler: ->(messages, prefs, system_prompt, max_tokens, params = nil) {
tools = params && params['tools'] # ToolUseContent may be returned in content
# ...
}
)
Acompanhamento de Progresso
Anexe um callback de progresso por chamada — o cliente gera um
progressToken único, o coloca no _meta da requisição e roteia
notifications/progress correspondentes para o seu bloco enquanto a requisição está ativa (tokens
obsoletos após a conclusão são descartados):
client.call_tool('long_running', args, progress: ->(progress, total, message) {
puts "#{message}: #{progress}/#{total}"
})
Um _meta em nível de requisição (por exemplo, um progressToken escolhido manualmente) também pode ser passado
dentro dos argumentos sob a chave '_meta' em todos os transportes — ele é elevado
para o nível de parâmetros JSON-RPC no fio, nunca enviado como argumento de ferramenta.
Timeouts e Cancelamento
Timeouts são configuráveis por requisição, além do read_timeout por servidor.
Uma requisição com timeout levanta
MCPClient::Errors::RequestTimeoutError (uma subclasse de TransportError), é
nunca reenviada silenciosamente pela camada de repetição, e um
notifications/cancelled de melhor esforço é enviado para a requisição abandonada (nunca para
initialize, e chamadas aumentadas por tarefa usam tasks/cancel em vez disso):
client.send_rpc('tools/call', params: { name: 'slow', arguments: {} }, timeout: 300)
server.rpc_request('tools/list', {}, timeout: 5)
Identidade do Cliente e Instruções do Servidor
Os hosts podem apresentar suas próprias informações de Implementation (enviadas como clientInfo durante
a inicialização; name e version obrigatórios — title, description,
websiteUrl, icons opcionais) e ler a dica de instructions do servidor
após conectar:
client = MCPClient::Client.new(
mcp_server_configs: [...],
client_info: { 'name' => 'my-ide', 'version' => '2.0.0', 'description' => 'An MCP-powered IDE' }
)
client.servers.first.connect
puts client.servers.first.instructions # e.g. "Use the search tool before answering."
Controle de Capacidades
Recursos opcionais do servidor (logging/setLevel, resources/subscribe,
completion/complete, tasks/list, tasks/cancel) são enviados apenas para servidores
que negociaram a capacidade correspondente; caso contrário,
MCPClient::Errors::CapabilityError é levantado (o ciclo de vida proíbe usar
capacidades que não foram negociadas). Client#log_level= pula
servidores sem registro em vez de falhar. As capacidades do cliente declaradas são
derivadas do que o host realmente registrou (manipuladores, raízes), nunca
codificadas.
Conclusão (Autocompletar)
result = client.complete(
ref: { type: 'ref/prompt', name: 'greeting' },
argument: { name: 'name', value: 'A' }
)
# => { 'values' => ['Alice', 'Alex'], 'total' => 100, 'hasMore' => true }
Registro
# Set log level
client.log_level = 'debug' # debug/info/notice/warning/error/critical
# Handle log notifications
client.on_notification do |server, method, params|
if method == 'notifications/message'
puts "[#{params['level']}] #{params['logger']}: #{params['data']}"
end
end
Tarefas (Ferramentas de longa duração aumentadas por tarefa)
Um servidor com capacidade de tarefa (um que anuncia tasks.requests.tools.call) pode executar uma ferramenta
cujo execution.taskSupport é optional ou required como uma tarefa em segundo plano:
a chamada retorna imediatamente com um identificador de tarefa, e o resultado é buscado depois.
Experimente localmente: python3 examples/echo_server_streamable.py & e depois
./examples/tasks_example.rb executa o ciclo de vida completo contra um servidor de demonstração
com capacidade de tarefa.
tool = client.find_tool('long_job')
tool.supports_task? # execution.taskSupport is optional/required?
# Create the task (returns immediately); ttl is the requested lifetime in ms
task = client.call_tool_as_task('long_job', { input: 'data' }, ttl: 60_000)
# Poll until the task reaches a terminal (or input-required) status,
# honoring the server's suggested poll interval
until task.terminal? || task.input_required?
sleep((task.poll_interval || 1000) / 1000.0)
task = client.get_task(task) # tasks/get, routed to the task's own server
end
# Retrieve the underlying result (e.g. a CallToolResult) via tasks/result
result = client.get_task_result(task)
# List and cancel tasks
page = client.list_tasks # { tasks: [...], next_cursor: ... }
client.cancel_task(task) # tasks/cancel
Os IDs de tarefa são únicos apenas dentro do servidor que os emitiu, então passe o Task
retornado por call_tool_as_task — ele carrega seu próprio servidor. Um ID de tarefa simples também
funciona quando o cliente tem um único servidor; com vários servidores configurados, ele
levanta ArgumentError em vez de adivinhar, então nomeie o servidor explicitamente:
client.get_task('task-123', server: 'my-server')
# React to server-pushed status updates
client.on_notification do |server, method, params|
puts "Task #{params['taskId']} -> #{params['status']}" if method == 'notifications/tasks/status'
end
Elicitação (Interações iniciadas pelo servidor)
client = MCPClient::Client.new(
mcp_server_configs: [MCPClient.stdio_config(command: 'python server.py')],
elicitation_handler: ->(message, schema) {
puts "Server asks: #{message}"
# Return: { 'action' => 'accept', 'content' => { 'field' => 'value' } }
# Or: { 'action' => 'decline' } or { 'action' => 'cancel' }
}
)
Configuração Avançada
Para mais controle, use create_client com configurações explícitas:
client = MCPClient.create_client(
mcp_server_configs: [
MCPClient.stdio_config(command: 'npx server', name: 'local'),
MCPClient.sse_config(
base_url: 'https://api.example.com/sse',
headers: { 'Authorization' => 'Bearer TOKEN' },
read_timeout: 30, ping: 10, retries: 3
),
MCPClient.http_config(
base_url: 'https://api.example.com',
endpoint: '/rpc',
headers: { 'Authorization' => 'Bearer TOKEN' }
),
MCPClient.streamable_http_config(
base_url: 'https://api.example.com/mcp',
read_timeout: 60, retries: 3
)
],
logger: Logger.new($stdout)
)
# Or load from JSON file
client = MCPClient.create_client(server_definition_file: 'servers.json')
Repetições
A opção retries: controla a repetição automática com backoff exponencial. Apenas
falhas onde a requisição provavelmente não foi concluída no servidor são
repetidas: erros de transporte/rede e respostas HTTP 5xx. Falhas em nível de aplicação
— uma resposta de erro JSON-RPC ou um HTTP 4xx — nunca são repetidas,
porque o servidor já processou ou rejeitou a requisição. Falhas de servidor repetíveis
levantam MCPClient::Errors::TransientServerError, uma subclasse de
MCPClient::Errors::ServerError, então manipuladores rescue ServerError existentes são
afetados.
tools/call nunca é repetido automaticamente. Mesmo uma falha "transitória" pode chegar
após o servidor ter executado a requisição, e JSON-RPC não tem chave de idempotência
que torne uma repetição segura — então uma repetição poderia executar um efeito colateral duas vezes.
Repita uma chamada de ferramenta explicitamente se sua aplicação souber que é seguro repetir, e
trate o erro levantado como resultado desconhecido em vez de não executado:
begin
client.call_tool('send_invoice', { customer: 'acme' })
rescue MCPClient::Errors::TransportError => e
# The server may or may not have sent the invoice. Check before retrying.
end
O mesmo raciocínio exclui RequestTimeoutError e ResponseTooLargeError
das repetições, e se aplica à recuperação de sessão: se um tools/call retornar com
um 404 de sessão expirada, o cliente inicia uma nova sessão, mas não reenvia
a chamada — ele levanta para que você decida. Requisições idempotentes são reenviadas contra
a nova sessão como antes.
Limites de Tamanho de Resposta (Streamable HTTP)
Uma resposta codificada em gzip é descomprimida incrementalmente e abandonada quando
expande além de max_decompressed_body_bytes (padrão 64 MiB), então um corpo pequeno
altamente comprimido não pode esgotar a memória. Exceder isso levanta
MCPClient::Errors::ResponseTooLargeError.
Aumente o limite se você legitimamente trocar cargas muito grandes — blobs de recursos em base64 ou áudio — para que a aceitação de uma resposta não dependa da escolha do servidor de comprimi-la:
MCPClient.streamable_http_config(
base_url: 'https://api.example.com/mcp',
max_decompressed_body_bytes: 256 * 1024 * 1024
)
Personalização do Faraday
MCPClient.http_config(base_url: 'https://internal.company.com') do |faraday|
faraday.ssl.cert_store = custom_cert_store
faraday.ssl.verify = true
end
JSON de Definição do Servidor
{
"mcpServers": {
"filesystem": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem", "/home"]
},
"api": {
"type": "streamable_http",
"url": "https://api.example.com/mcp",
"headers": { "Authorization": "Bearer TOKEN" }
}
}
}
Exemplos de Integração com IA
OpenAI
require 'mcp_client'
require 'openai'
mcp = MCPClient.connect('npx -y @modelcontextprotocol/server-filesystem .')
tools = mcp.to_openai_tools
client = OpenAI::Client.new(api_key: ENV['OPENAI_API_KEY'])
response = client.chat.completions.create(
model: 'gpt-4',
messages: [{ role: 'user', content: 'List files' }],
tools: tools
)
Anthropic
require 'mcp_client'
require 'anthropic'
mcp = MCPClient.connect('npx -y @modelcontextprotocol/server-filesystem .')
tools = mcp.to_anthropic_tools
client = Anthropic::Client.new(access_token: ENV['ANTHROPIC_API_KEY'])
# Use tools with Claude API
RubyLLM
require 'mcp_client'
require 'ruby_llm'
RubyLLM.configure { |c| c.openai_api_key = ENV['OPENAI_API_KEY'] }
mcp = MCPClient.connect('http://localhost:8931/mcp') # Playwright MCP
# Wrap each MCP tool as a RubyLLM tool
tools = mcp.list_tools.map do |t|
tool_name = t.name
Class.new(RubyLLM::Tool) do
description t.description
params t.schema
define_method(:name) { tool_name }
define_method(:execute) { |**args| mcp.call_tool(tool_name, args) }
end.new
end
chat = RubyLLM.chat(model: 'gpt-4o-mini')
tools.each { |tool| chat.with_tool(tool) }
response = chat.ask('Navigate to google.com and tell me the page title')
Veja examples/ para implementações completas:
ruby_openai_mcp.rb,openai_ruby_mcp.rb- Integração OpenAIruby_anthropic_mcp.rb- Integração Anthropicgemini_ai_mcp.rb- Integração Google Vertex AIruby_llm_mcp.rb- Integração RubyLLM (provedor OpenAI)
Executando os Exemplos
O harness examples/run_all_examples.sh executa todos os exemplos que podem rodar na máquina atual — servidores stdio autocontidos, os servidores de eco e elicitação Python/Flask/FastMCP, servidores MCP baseados em npx e (opcionalmente) as integrações pagas de LLM. Ele inicia e encerra cada servidor automaticamente e imprime um resumo PASS/FAIL/SKIP. tasks_example.rb é sempre pulado (precisa de um servidor remoto com capacidade de tarefa); oauth_browser_auth.rb é interativo e só roda quando você opta por ele com RUN_OAUTH=1.
Pré-requisitos
Execute bundle install primeiro. O script verifica previamente o seguinte e imprime um aviso (ele não aborta) para qualquer item ausente; os exemplos afetados são então pulados ou falham:
ruby,bundle,curl,lsof- emPATHpython3(ou$PYTHON) mais um bináriopythonseparado - emPATH- Pacotes Python
flask,fastmcp,mcp- importáveis por$PYTHON npx(Node) - necessário pelo exemplo baseado emnpx(json_input) e por todos os exemplos de LLM, que iniciam servidoresnpxfilesystem/Playwright
Uso
examples/run_all_examples.sh # run everything runnable on this machine
RUN_AI=0 examples/run_all_examples.sh # skip the paid-LLM examples
RUN_NPX=0 examples/run_all_examples.sh # skip the npx-based example (json_input)
LOG_DIR=/path examples/run_all_examples.sh # write logs to a chosen dir
PYTHON=python3.12 TIMEOUT=180 examples/run_all_examples.sh # override interpreter and per-example timeout
Variáveis de Ambiente
| Variável | Padrão | Efeito |
|---|---|---|
RUN_AI | 1 | Defina como 0 para pular as integrações de LLM, que fazem chamadas de API reais e pagas. |
RUN_NPX | 1 | Defina como 0 (ou deixe npx desligado em PATH) para pular o exemplo baseado em npx (json_input). Os exemplos de LLM também iniciam servidores npx, mas são controlados por RUN_AI e suas chaves de API. |
PYTHON | python3 | Interpretador usado para iniciar os servidores Python/Flask/FastMCP e executar as verificações de importação. |
TIMEOUT | 120 | Timeout de relógio de parede por exemplo em segundos; um timeout é relatado como um FAIL. |
LOG_DIR | diretório mktemp novo | Diretório para logs por exemplo e por servidor; o caminho é impresso após a verificação prévia e no resumo. |
Segredos e Chaves de API
Segredos reais vivem em examples/secrets.env, que é gitignored e carregado automaticamente (cada linha KEY=value é exportada) quando presente. Copie o modelo rastreado para começar:
cp examples/secrets.env.example examples/secrets.env
# then set ZAPIER_MCP_TOKEN=... to enable the Zapier streamable-HTTP example
Defina ZAPIER_MCP_TOKEN (da página de configuração do Zapier MCP, "Opção 1: cabeçalho de autorização") para executar streamable_http_example.rb e oauth_example.rb contra o Zapier; substitua ZAPIER_MCP_URL se sua URL de conexão for diferente. Para executar o oauth_browser_auth.rb interativo, defina MCP_SERVER_URL (por exemplo, um túnel ngrok para seu servidor MCP protegido por OAuth) em secrets.env e passe RUN_OAUTH=1. Os exemplos de LLM precisam cada um de suas próprias credenciais no ambiente e são pulados sem elas:
ruby_anthropic_mcp.rb-ANTHROPIC_API_KEY(+npx)openai_ruby_mcp.rb-OPENAI_API_KEY(+npx)ruby_openai_mcp.rb,ruby_llm_mcp.rb-OPENAI_API_KEY(+npx, além de um servidor MCP Playwright em:8931)gemini_ai_mcp.rb- um JSON de conta de serviço Vertex emVERTEX_CREDENTIALS_FILE(padrãoexamples/google-credentials.json, +npx)
Como o Pass/Fail é Julgado
A maioria dos exemplos imprime suas próprias marcas de sucesso/falha, mas sai com código 0 independentemente, então o harness combina o código de saída com uma varredura da saída em vez de confiar apenas no status de saída. Um exemplo FAIL quando sai com código diferente de zero, expira (código de saída 124), imprime uma assinatura de erro grave (um traceback de Ruby/Python, Connection refused, uninitialized constant e similares), imprime uma marca ❌, ou não tem sua marca de sucesso esperada; caso contrário, ele PASS. (A verificação de ❌ é suprimida com IGNORE_XMARK=1 para as demonstrações interativas de elicitação, onde ❌ pode ser uma saída legítima de "recusado".) O script sai com código 0 apenas se zero exemplos falharem — SKIP não afetam o status de saída.
Para tutoriais mais aprofundados por tópico, veja examples/README.md, examples/README_ECHO_SERVER.md, examples/STREAMABLE_HTTP_TESTING.md e examples/elicitation/README.md.
Autenticação OAuth 2.1
require 'mcp_client'
require 'mcp_client/auth/browser_oauth'
oauth = MCPClient::Auth::OAuthProvider.new(
server_url: 'https://api.example.com/mcp',
redirect_uri: 'http://localhost:8080/callback',
scope: 'mcp:read mcp:write'
)
browser_oauth = MCPClient::Auth::BrowserOAuth.new(oauth)
token = browser_oauth.authenticate # Opens browser, handles callback
client = MCPClient::Client.new(
mcp_server_configs: [{
type: 'streamable_http',
base_url: 'https://api.example.com/mcp',
oauth_provider: oauth
}]
)
Recursos: PKCE, descoberta de servidor (.well-known), registro dinâmico, renovação de token.
Veja OAUTH.md para documentação completa.
Extras de OAuth (2025-11-25)
- Documentos de metadados de ID do cliente (SEP-991) — passa
client_id_metadata_url: 'https://myapp.example/oauth-client.json'(uma URL HTTPS com um caminho, que também serve comoclient_id); quando o servidor de autorização anunciaclient_id_metadata_document_supported, o registro dinâmico de cliente é totalmente ignorado. - Desafios de escopo (SEP-835) — um desafio
insufficient_scopede HTTP 403 levantaMCPClient::Errors::InsufficientScopeError(uma subclasse deConnectionError) expondo#scopee#error_description; os escopos desafiados são tratados como autoritativos para o próximo fluxo de autorização. - PKCE — a autorização se recusa a prosseguir quando o servidor de autorização
não anuncia
code_challenge_methods_supportedincluindoS256.
Notificações do Servidor
client.on_notification do |server, method, params|
case method
when 'notifications/tools/list_changed'
client.clear_cache # Auto-handled
when 'notifications/message'
puts "Log: #{params['data']}"
when 'notifications/roots/list_changed'
puts "Roots changed"
end
end
Gerenciamento de Sessão
Tanto os transportes HTTP quanto Streamable HTTP lidam automaticamente com servidores baseados em sessão:
- Captura de sessão: Extrai
Mcp-Session-Idda resposta de inicialização - Persistência de sessão: Inclui o cabeçalho de sessão em solicitações subsequentes
- Encerramento de sessão: Envia solicitação DELETE durante a limpeza
- Retomabilidade (Streamable HTTP, SEP-1699): rastreia IDs de eventos SSE e, quando um
fluxo de resposta é interrompido, retoma via GET com
Last-Event-IDpara que o servidor possa reproduzir mensagens perdidas — honrando a diretivaretry:do servidor
Nenhuma configuração necessária — funciona automaticamente.
Compatibilidade do Servidor
Funciona com qualquer servidor compatível com MCP:
- @modelcontextprotocol/server-filesystem
- @playwright/mcp
- FastMCP
- Servidores personalizados que implementam o protocolo MCP
Exemplo FastMCP
# Start server
python examples/echo_server_streamable.py
# Connect and use
client = MCPClient.connect('http://localhost:8931/mcp')
tools = client.list_tools
result = client.call_tool('echo', { message: 'Hello!' })
Tratando o Servidor como Não Confiável
Um servidor MCP conectado controla tudo o que envia para você, e os transportes são escritos com essa premissa. Você não precisa configurar nada disso — é o comportamento padrão — mas vale a pena saber o que o cliente recusará:
| Entrada controlada pelo peer | O que o cliente faz |
|---|---|
| Corpos de resposta compactados (somente Streamable HTTP — o único transporte que solicita gzip) | Descompactados incrementalmente, abandonados após max_decompressed_body_bytes (64 MiB padrão) |
| Fluxos SSE | Limite de buffer por conexão; eventos varridos incrementalmente, então um evento não terminado custa memória limitada e CPU |
Diretivas retry: | Honradas, mas com piso, para que retry: 0 não possa gerar um loop de reconexão |
| IDs de eventos SSE | Comprimento limitado, ASCII imprimível apenas (são ecoados em Last-Event-ID) |
Eventos SSE legados endpoint | Devem permanecer na origem da conexão; redirecionamentos fora da origem são recusados, então cabeçalhos de credenciais configurados nunca alcançam outro host |
| URLs de descoberta OAuth de um peer | Devem ser HTTPS e são rejeitadas quando o host é um endereço literal de loopback/privado/link-local (a menos que o servidor configurado seja ele próprio local); um desafio recusado falha de forma fechada. Nomes de host não são resolvidos, então um nome público apontando para um endereço privado não é detectado — veja a nota abaixo |
| Respostas JSON-RPC não solicitadas | Descartadas — apenas IDs com uma solicitação pendente são aceitos |
| Solicitações iniciadas pelo servidor | Respostas são limitadas por um orçamento de concorrência em vez de gerar threads ilimitadas |
Valores de esquema pattern | Correspondidos sob um orçamento de tempo de operação inteiro; um timeout falha na validação em vez de passar silenciosamente |
Mensagens de log (notifications/message) | Caracteres de controle escapados e comprimento limitado, então um servidor não pode forjar linhas de log |
Limite conhecido: a verificação de OAuth é textual. Um peer ainda pode anunciar um nome de host público cujo registro DNS aponta para dentro da sua rede; detectar isso exige filtragem no momento da resolução na camada HTTP, o que esta gem não faz. Se você executa em um ambiente onde isso importa, restrinja a saída na camada de rede.
Dois padrões relacionados que merecem destaque porque afetam seus dados em vez dos dados do peer:
- Payloads nunca são gravados em logs. Em DEBUG, o cliente registra um resumo de método/id e uma contagem de bytes, não parâmetros de solicitação, corpos de resposta ou chunks SSE brutos. Configurações de servidor são registradas com chaves que contêm credenciais mascaradas.
- Exceções de host não são refletidas para o servidor. Um handler de elicitação, amostragem ou raízes que levanta exceção produz uma mensagem de erro JSON-RPC constante; o detalhe permanece no seu log local.
Requisitos
- Ruby >= 3.2.0
- Dependências de runtime:
faraday(~> 2.0) comfaraday-follow_redirectsefaraday-retry, além debase64— todas puxadas automaticamente pela gem
O desenvolvimento usa Ruby 4.0.6 (veja .ruby-version). O CI executa a suíte em 4.0.6
mais o piso suportado, 3.2 e 3.3.
Licença
Disponível como código aberto sob a Licença MIT.
Contribuindo
Relatórios de bugs e pull requests são bem-vindos em https://github.com/simonx1/ruby-mcp-client.