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>
77 lines
2.6 KiB
Ruby
77 lines
2.6 KiB
Ruby
# Tool MCP: busca semântica em FAQs/documentação aprovada pelas gerentes.
|
|
#
|
|
# Caso de uso típico: cliente pergunta algo que NÃO está na skill estruturada
|
|
# do Hermes (ex: aceita pet, formas de pagamento alternativo, política de
|
|
# alguma situação específica). Em vez de inventar, Hermes chama esta tool
|
|
# e responde com base no FAQ atualizado em tempo real pelo Captain UI.
|
|
#
|
|
# Reaproveita Captain::Tools::SearchReplyDocumentationService — exatamente
|
|
# o mesmo serviço que o orquestrador interno do Captain usava antes,
|
|
# garantindo que Hermes vê os mesmos FAQs que o caminho legado veria.
|
|
class Captain::Mcp::Tools::FaqLookupTool < Captain::Mcp::Tools::BaseTool
|
|
class << self
|
|
def name
|
|
'faq_lookup'
|
|
end
|
|
|
|
def description
|
|
'Busca semântica em FAQs/documentação aprovada pelas gerentes do hotel. ' \
|
|
'Use quando o cliente perguntar algo que NÃO está na sua skill ' \
|
|
'(ex: política de pets, horários especiais, convênios, regras pontuais). ' \
|
|
'Retorna até 5 perguntas/respostas mais próximas semanticamente da query. ' \
|
|
'Se não encontrar nada relevante, prefira transferir pro humano em vez ' \
|
|
'de inventar.'
|
|
end
|
|
|
|
def input_schema
|
|
{
|
|
type: 'object',
|
|
properties: {
|
|
query: {
|
|
type: 'string',
|
|
description: 'Pergunta ou tema a buscar em linguagem natural ' \
|
|
'(ex: aceitam pets, estacionamento coberto, ' \
|
|
'forma de pagamento sem ser Pix).'
|
|
}
|
|
},
|
|
required: ['query']
|
|
}
|
|
end
|
|
end
|
|
|
|
def call(args, context:)
|
|
query = args['query'].to_s.strip
|
|
return error_response('Argumento "query" é obrigatório.') if query.blank?
|
|
|
|
account = resolve_account(context)
|
|
return error_response('Account não encontrada no contexto MCP.') if account.blank?
|
|
|
|
assistant = resolve_assistant(context, account)
|
|
result = ::Captain::Tools::SearchReplyDocumentationService.new(
|
|
account: account,
|
|
assistant: assistant
|
|
).execute(query: query)
|
|
|
|
text_response(result.presence || 'Nenhum FAQ relevante encontrado pra essa pergunta.')
|
|
rescue StandardError => e
|
|
Rails.logger.error("[Captain::Mcp::FaqLookupTool] error: #{e.class}: #{e.message}")
|
|
error_response("Falha na busca de FAQ: #{e.message}")
|
|
end
|
|
|
|
private
|
|
|
|
def resolve_account(context)
|
|
account_id = context[:account_id]
|
|
return nil if account_id.blank?
|
|
|
|
Account.find_by(id: account_id)
|
|
end
|
|
|
|
def resolve_assistant(context, account)
|
|
assistant_id = context[:assistant_id]
|
|
return nil if assistant_id.blank?
|
|
|
|
account.captain_assistants.find_by(id: assistant_id)
|
|
end
|
|
end
|