Implementa servidor MCP (Model Context Protocol) HTTP no Captain pra o Hermes Agent invocar tools do Captain via `hermes mcp add`. Substrato pra integração de Nível 2 onde o agente consulta tools quando precisa executar ações reais (buscar FAQ, adicionar label, futuramente Pix etc). Arquivos: - app/controllers/webhooks/captain/mcp_controller.rb Endpoint POST /webhooks/captain/mcp. Valida HMAC (CAPTAIN_MCP_SECRET), parseia JSON-RPC, despacha pro Server. Extrai params._captain_context com multi-tenant ids (conversation_id, inbox_id, account_id, etc). - enterprise/app/services/captain/mcp/server.rb Subset MCP suficiente: initialize, tools/list, tools/call, ping, notifications/initialized. JSON-RPC síncrono (sem SSE). - enterprise/app/services/captain/mcp/tool_registry.rb Lista centralizada de tools. - enterprise/app/services/captain/mcp/tools/base_tool.rb Interface base pras tools (.name, .description, .input_schema, #call). - enterprise/app/services/captain/mcp/tools/add_label_tool.rb Tool 1: aplica label na conversation atual. - enterprise/app/services/captain/mcp/tools/faq_lookup_tool.rb Tool 2: busca semântica em FAQs (Captain::AssistantResponse via pgvector cosine). Reaproveita SearchReplyDocumentationService pra paridade com o caminho legado do Captain. - config/routes.rb Rota POST /webhooks/captain/mcp. Conexão pelo Hermes: hermes mcp add captain-tools --url http://CAPTAIN_HOST/webhooks/captain/mcp Auth: HMAC X-Hub-Signature-256 quando CAPTAIN_MCP_SECRET setado. TODO próxima sprint: generate_pix_tool, send_suite_images_tool. Handoff fica via automation hoje (UI Chatwoot). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
97 lines
2.8 KiB
Ruby
97 lines
2.8 KiB
Ruby
# Servidor MCP (Model Context Protocol) HTTP do Captain.
|
|
#
|
|
# Implementa subset suficiente da spec MCP pra Hermes Agent consumir como
|
|
# cliente via `hermes mcp add captain-tools --url <url>`. Métodos
|
|
# implementados:
|
|
# - initialize — handshake (cliente apresenta info, server responde
|
|
# capabilities + serverInfo)
|
|
# - tools/list — devolve a lista de tools registradas
|
|
# - tools/call — executa uma tool específica e devolve o resultado
|
|
# - notifications/initialized — no-op (cliente confirma que está pronto)
|
|
# - ping — no-op (health check)
|
|
#
|
|
# Não suporta SSE/streaming ainda — modo POST/JSON síncrono basta pro
|
|
# caso de uso atual (tools que retornam rápido como add_label, faq_lookup).
|
|
#
|
|
# Auth/segurança ficam no controller (HMAC), aqui só roteia.
|
|
class Captain::Mcp::Server
|
|
PROTOCOL_VERSION = '2024-11-05'.freeze
|
|
SERVER_NAME = 'captain-mcp'.freeze
|
|
SERVER_VERSION = '0.1.0'.freeze
|
|
|
|
class << self
|
|
def handle(request, context: {})
|
|
new(context: context).handle(request)
|
|
end
|
|
end
|
|
|
|
def initialize(context: {})
|
|
@context = context || {}
|
|
end
|
|
|
|
def handle(request)
|
|
rid = request['id']
|
|
method = request['method'].to_s
|
|
params = request['params'] || {}
|
|
|
|
dispatch(method, rid, params)
|
|
rescue StandardError => e
|
|
Rails.logger.error("[Captain::Mcp::Server] error handling #{method}: #{e.class}: #{e.message}")
|
|
error_response(rid, -32_603, "Internal error: #{e.message}")
|
|
end
|
|
|
|
private
|
|
|
|
def dispatch(method, rid, params)
|
|
case method
|
|
when 'initialize' then respond(rid, initialize_result(params))
|
|
when 'tools/list' then respond(rid, { tools: Captain::Mcp::ToolRegistry.descriptors })
|
|
when 'tools/call' then respond(rid, tools_call(params))
|
|
when 'ping' then respond(rid, {})
|
|
when 'notifications/initialized', 'notifications/cancelled' then nil
|
|
else
|
|
error_response(rid, -32_601, "Method not found: #{method}")
|
|
end
|
|
end
|
|
|
|
attr_reader :context
|
|
|
|
def initialize_result(_params)
|
|
{
|
|
protocolVersion: PROTOCOL_VERSION,
|
|
capabilities: {
|
|
tools: { listChanged: false }
|
|
},
|
|
serverInfo: {
|
|
name: SERVER_NAME,
|
|
version: SERVER_VERSION
|
|
}
|
|
}
|
|
end
|
|
|
|
def tools_call(params)
|
|
name = params['name'].to_s
|
|
args = params['arguments'] || {}
|
|
Captain::Mcp::ToolRegistry.call(name, args, context: context)
|
|
end
|
|
|
|
def respond(id, result)
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: id,
|
|
result: result
|
|
}
|
|
end
|
|
|
|
def error_response(id, code, message)
|
|
{
|
|
jsonrpc: '2.0',
|
|
id: id,
|
|
error: {
|
|
code: code,
|
|
message: message
|
|
}
|
|
}
|
|
end
|
|
end
|