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/call aumentado por tarefa — criar com um ttl, consultar tasks/get, recuperar via tasks/result, além de tasks/list e tasks/cancel
  • Áudio: Suporte a tipo de conteúdo de áudio
  • Progresso e Cancelamento: progressToken com callbacks por chamada; notifications/cancelled automático para requisições abandonadas
  • Metadados: icons, title e _meta analisados 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 URLTransporte
Termina com /sseSSE
Termina com /mcpStreamable HTTP
stdio://command ou Arraystdio
npx, node, python, etc.stdio
Outras URLs HTTPDetecçã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 4xxnunca 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 OpenAI
  • ruby_anthropic_mcp.rb - Integração Anthropic
  • gemini_ai_mcp.rb - Integração Google Vertex AI
  • ruby_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 - em PATH
  • python3 (ou $PYTHON) mais um binário python separado - em PATH
  • Pacotes Python flask, fastmcp, mcp - importáveis por $PYTHON
  • npx (Node) - necessário pelo exemplo baseado em npx (json_input) e por todos os exemplos de LLM, que iniciam servidores npx filesystem/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ávelPadrãoEfeito
RUN_AI1Defina como 0 para pular as integrações de LLM, que fazem chamadas de API reais e pagas.
RUN_NPX1Defina 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.
PYTHONpython3Interpretador usado para iniciar os servidores Python/Flask/FastMCP e executar as verificações de importação.
TIMEOUT120Timeout de relógio de parede por exemplo em segundos; um timeout é relatado como um FAIL.
LOG_DIRdiretório mktemp novoDiretó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 em VERTEX_CREDENTIALS_FILE (padrão examples/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 como client_id); quando o servidor de autorização anuncia client_id_metadata_document_supported, o registro dinâmico de cliente é totalmente ignorado.
  • Desafios de escopo (SEP-835) — um desafio insufficient_scope de HTTP 403 levanta MCPClient::Errors::InsufficientScopeError (uma subclasse de ConnectionError) expondo #scope e #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_supported incluindo S256.

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-Id da 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-ID para que o servidor possa reproduzir mensagens perdidas — honrando a diretiva retry: do servidor

Nenhuma configuração necessária — funciona automaticamente.

Compatibilidade do Servidor

Funciona com qualquer servidor compatível com 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 peerO 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 SSELimite 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 SSEComprimento limitado, ASCII imprimível apenas (são ecoados em Last-Event-ID)
Eventos SSE legados endpointDevem 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 peerDevem 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 solicitadasDescartadas — apenas IDs com uma solicitação pendente são aceitos
Solicitações iniciadas pelo servidorRespostas são limitadas por um orçamento de concorrência em vez de gerar threads ilimitadas
Valores de esquema patternCorrespondidos 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) com faraday-follow_redirects e faraday-retry, além de base64 — 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.