mcp-searxng-relay
Pesquisa web MCP reforçada através do seu próprio SearXNG — autenticação por bearer, logs de auditoria por identidade, busca protegida contra SSRF, builds de contêiner reproduzíveis.
Documentação
mcp-searxng-relay
Um servidor Model Context Protocol (MCP) que fornece a agentes de IA busca na web e recuperação de URLs através da sua própria instância SearXNG auto-hospedada — construído para ambientes onde a busca deve permanecer em infraestrutura aprovada e cada consulta deve ser auditável. Sem APIs de busca de terceiros, sem intermediários de dados externos; consultas nunca saem da infraestrutura que você controla.
Para quem é isso. Equipes que executam agentes de IA em ambientes corporativos ou governamentais onde a busca de saída é restrita, monitorada, ou ambos — e onde "usamos uma API de busca hospedada" não é uma resposta aceitável. O projeto prioriza uma postura de segurança defensável e uma trilha de auditoria limpa em vez de amplitude de recursos.
O que é distinto. A maioria das ferramentas de busca para agentes para em "aqui estão alguns resultados." Este relay também informa o que o agente realmente fez com eles.
searxng_session_sources retorna as URLs que este relay genuinamente buscou para um determinado chamador — exatas em bytes, mais recentes primeiro, cada uma marcada com quanto foi realmente lido: o texto completo, uma janela de um documento mais longo, apenas metadados, ou uma busca que falhou. Agentes transcrevem URLs incorretamente quando compõem uma resposta final milhares de tokens após a chamada de ferramenta que a produziu, e as fabricam completamente quando nunca buscaram uma. Uma instrução como "não cite fontes que você não leu" é inexequível contra a memória de um modelo; contra esta lista é uma consulta. A distinção de profundidade de leitura é a parte que importa — "buscado" e "lido por completo" não são a mesma afirmação, e é precisamente a que os modelos perdem.
Uma API de busca hospedada estruturalmente não pode oferecer isso: ela vê uma consulta por vez e não mantém um registro por chamador. O mesmo raciocínio percorre o resto do projeto — cada busca e recuperação é atribuída a uma identidade e uma sessão, a política de SSRF é documentada e seu alcance é declarado na configuração em vez de inferido, e nada que ampliaria um limite de segurança é permitido acontecer silenciosamente. Se você precisa ser capaz de dizer o que seus agentes buscaram, o que leram e quanto disso, é para isso que isto serve.
Projeto complementar. Este relay é projetado para ser implantado junto com searxng-helm, um chart Helm endurecido para SearXNG em Kubernetes (sem root, rootfs somente leitura, NetworkPolicies com negação por padrão, assinado com cosign). O chart implanta tanto SearXNG quanto este relay como um par; veja seu README para a história completa de segurança da infraestrutura. O relay também inclui manifestos K8s mínimos e independentes para testes rápidos — veja Kubernetes abaixo.
Este servidor MCP suporta tanto o transporte stdio (para uso local com Claude Desktop e clientes similares) quanto o transporte Streamable HTTP (para implantações em rede ou containerizadas).
Conteúdo
- Recursos
- Requisitos
- Início rápido
- Configuração
- Ferramentas MCP
- Usando com Claude Desktop (modo stdio)
- Usando com Claude Desktop (modo HTTP)
- Escopando um relay para mecanismos específicos
- Notas de segurança
- Limitação de taxa
- Limites de sessão
- Operações
- Construindo a imagem Docker
- Registro de logs
- Métricas
Recursos
- Lista de fontes verificável —
searxng_session_sourcesretorna as URLs que o relay realmente buscou para um chamador, exatas em bytes e mais recentes primeiro, cada uma marcada com quanto foi lido: texto completo, uma janela de um documento mais longo, apenas metadados, ou uma busca falha. Agentes transcrevem URLs incorretamente ao compor uma resposta final longe da chamada de ferramenta que as produziu, e as fabricam completamente quando nunca buscaram uma; isso dá ao modelo a verdade fundamental para copiar em vez de recordar, e dá a você um registro do que ele realmente leu. Entregue em um cercado codificado em CDATA para que as URLs sobrevivam à viagem de ida e volta sem escape. - Busca na web via SearXNG com controle total sobre idioma, categoria, intervalo de tempo, nível de busca segura e contagem de resultados
- Recuperação de URLs com saída Markdown estruturada — cabeçalhos, listas, tabelas, blocos de código e ênfase inline todos preservados
- Triagem de metadados de URL —
searxng_url_metadataretorna apenas título, autor, data de publicação, idioma, nome do site, descrição, imagem, categorias e tags como JSON, a um custo de tokens aproximadamente uma ordem de magnitude menor do que buscar o corpo completo. Útil para escolher qual de várias URLs candidatas ler por completo. O cache é compartilhado comsearxng_read_url, então uma busca de metadados seguida por uma busca de conteúdo (ou vice-versa) custa uma requisição HTTP upstream, não duas. - Extração de texto de PDF de URLs buscadas
- Extração de documentos Office — DOCX, XLSX, PPTX além de DOC, XLS, PPT legados. Documentos são renderizados em Markdown em vez de texto simples para que cabeçalhos, tabelas e estrutura de listas sobrevivam ao contexto do modelo (planilhas em particular se beneficiam — uma tabela Markdown é muito mais útil do que células achatadas em CSV)
- Paginação para documentos longos — respostas são janeladas em 100 mil caracteres, e uma resposta truncada termina com um aviso nomeando o tamanho total e o
start_indexexato para a próxima chamada. O texto extraído completo (atéMAX_EXTRACTED_CHARS) é armazenado em cache, então paginar por um PDF grande custa uma busca upstream, não uma por página - Respostas de imagem — URLs JPEG, PNG, GIF e WebP retornam como blocos MCP
ImageContentpara consumo por modelos de visão (o SDK codifica em base64 os bytes brutos no fio). SVG é intencionalmente excluído — mais útil ao modelo como texto do que como um blob binário. O tamanho bruto é limitado porMAX_IMAGE_BYTES, separadamente deMAX_BODY_BYTES, para que limites de imagem e texto possam ser ajustados independentemente. - Detecção automática de charset — páginas não-UTF-8 (Shift-JIS, windows-1252, ISO-8859-1, …) são decodificadas corretamente antes da análise
- Extração de conteúdo estilo readability — barras de navegação, rodapés, barras laterais e banners de cookies são removidos automaticamente
- Visibilidade de busca degradada — SearXNG responde com HTTP 200 mesmo quando alguns de seus backends falharam, então uma busca silenciosamente retorna resultados mais escassos e o primeiro sintoma visível geralmente é alguém concluindo que o modelo regrediu. O relay lê o campo
unresponsive_enginesque o SearXNG reporta e registra umWARNnomeando os mecanismos e por que falharam, para que um backend quebrado seja diagnosticado pelos próprios logs do relay em vez de ser confundido com um bug do relay ou do modelo - Atribuição de mecanismo nos resultados de busca — cada resultado inclui a lista de mecanismos backend SearXNG que o retornaram. Uma URL surgida de três mecanismos é um sinal diferente de uma surgida de um, e o agente pode ponderar isso sem que o servidor imponha uma classificação por cima. O parâmetro de busca
enginesfecha o ciclo: um agente pode reconsultar o backend específico que trouxe um resultado promissor. - Métricas de busca por domínio —
/metricsexpõemcp_fetches_by_domain_total{domain="…",outcome="success|error"}para que um operador possa ver quais hosts de destino estão saudáveis e quais não estão. Cardinalidade limitada: no máximo 512 domínios distintos rastreados, com o restante agrupado sobdomain="__overflow__". - Cache de respostas com TTL configurável e bypass de cache por requisição
- Proteção SSRF — endereços não roteáveis globalmente são bloqueados no momento da conexão TCP (loopback, link-local, privados, multicast, broadcast, não especificados, além de uma blocklist codificada cobrindo CGNAT, TEST-NET-{1,2,3}, benchmark, atribuições de protocolo IETF, NAT64, Teredo, 6to4, documentação IPv6, ORCHID, o prefixo de descarte, 240/4 reservado para futuro, e outras faixas reservadas que os predicados da stdlib não detectam). Cadeias de redirecionamento são revalidadas a cada salto para fechar a janela de rebinding de DNS. Operadores podem optar por alcançar recursos internos (Confluence, Jira, wikis) via
FETCH_ALLOWED_HOSTS/FETCH_ALLOWED_CIDRS; ambos exigem uma porta explícita, então permitir uma wiki nunca expõe também o listener Redis ou kubelet ao lado dela. - Autenticação por token Bearer com tabelas de múltiplos tokens (
MCP_AUTH_TOKEN,MCP_AUTH_TOKENS, ouMCP_AUTH_TOKEN_FILE) e registro de auditoria por identidade - Limitação de taxa por chamador — throttle de balde de tokens chaveado por identidade quando autenticado e por IP de origem caso contrário. RPS e rajada configuráveis, padrão 5 rps / rajada 10. Exposto em
mcp_rate_limit_rejections_total. - Cercamento de prompt — cada resposta de ferramenta é envolvida em um elemento
<sec:fence>assinado com um nonce aleatório por resposta, implementando arXiv:2511.19727. Chave pública exposta em/fence/public-keypara compatibilidade futura com clientes verificadores. A chave de assinatura é por processo por padrão, ou fornecida pelo operador viaFENCE_SIGNING_KEY/FENCE_SIGNING_KEY_FILEquando um verificador precisa de uma impressão digital estável para fixar. - Builds de container reproduzíveis — bit por bit. Dado o mesmo commit de origem e
SOURCE_DATE_EPOCH, o build produz uma imagem byte-idêntica, verificável viadocker save <image> | sha256sum. Toolchain fixado por digest,go.sumcongelado, sem caminhos embutidos, estado VCS ou IDs de build. Detalhes emsupply-chain.md. - Banner de inicialização estruturado com todos os valores de configuração impressos no stderr na inicialização (segredos redigidos)
Requisitos
- Uma instância SearXNG em execução com o formato de saída JSON habilitado
- Go 1.26+ (para compilar a partir do código-fonte) ou Docker
Habilitando o formato JSON no SearXNG
Adicione o seguinte ao seu settings.yml do SearXNG:
search:
formats:
- html
- json
Início rápido
Docker (recomendado)
docker run -d \
-e SEARXNG_URL=https://your-searxng-instance.example.com \
-e MCP_PORT=8080 \
-e MCP_AUTH_TOKEN=$(openssl rand -hex 32) \
-p 8080:8080 \
ghcr.io/littleoffice/mcp-searxng-relay:latest
Docker Compose
services:
mcp-searxng:
image: ghcr.io/littleoffice/mcp-searxng-relay:latest
restart: unless-stopped
environment:
SEARXNG_URL: https://your-searxng-instance.example.com
MCP_PORT: "8080"
MCP_AUTH_TOKEN: your-strong-random-token
ports:
- "8080:8080"
Construindo a imagem do container
Calcule as duas entradas de reprodutibilidade uma vez, então escolha sua ferramenta de build:
SOURCE_DATE_EPOCH="$(git log -1 --pretty=%ct HEAD)"
SERVER_VERSION="$(git describe --tags --always)"
Docker (com BuildKit / buildx):
docker buildx build \
--build-arg SERVER_VERSION="${SERVER_VERSION}" \
--build-arg SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH}" \
--output type=docker,rewrite-timestamp=true \
-t mcp-searxng-relay:"${SERVER_VERSION}" .
Podman:
podman build \
--build-arg SERVER_VERSION="${SERVER_VERSION}" \
--build-arg SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH}" \
--timestamp "${SOURCE_DATE_EPOCH}" \
-t mcp-searxng-relay:"${SERVER_VERSION}" .
O build multi-estágio compila o binário em um builder golang:1.26.6-trixie fixado por digest e copia apenas o binário estático e os certificados CA para uma imagem de runtime scratch.
Reprodutibilidade. Dado o mesmo commit de origem e SOURCE_DATE_EPOCH (canonicamente o timestamp do próprio commit), qualquer invocação produz uma imagem byte-idêntica — verificável via docker save <image> | sha256sum ou podman save <image> | sha256sum. O toolchain é fixado por digest de conteúdo, o grafo de módulos é congelado por go.sum, e o build define -trimpath, -buildvcs=false, -buildid= e -Wl,--build-id=none para que nem caminhos, estado VCS, nem IDs de build em tempo de link vazem para o binário. O rewrite-timestamp do BuildKit e o --timestamp do Podman ambos fixam todos os timestamps de arquivos de camada ao mesmo valor para que o envelope da imagem seja reproduzível, não apenas o binário dentro. Veja supply-chain.md para a declaração completa de proveniência e etapas de verificação.
Observe que Docker e Podman usam codificações de manifesto em disco ligeiramente diferentes, então imagens construídas com um e salvas através do outro não terão SHA-256 correspondentes mesmo quando funcionalmente idênticas. Escolha uma ferramenta de build e mantenha-a para verificações de reprodutibilidade entre máquinas.
Kubernetes
Para produção, use searxng-helm. O chart implanta SearXNG e este relay juntos com um contexto de segurança restrito, NetworkPolicies com negação por padrão, credenciais gerenciadas por Secret e releases assinados por cosign. É a implantação de referência para o modelo de ameaça para o qual este relay foi construído. Consulte o README para a história completa de segurança da infraestrutura, o bloco de valores mcpRelay e as notas de integração com GitOps / external-secret-store.
Manifestos standalone mínimos estão incluídos em deploy/kubernetes/ para testes rápidos no cluster sem um release Helm: deployment.yaml com um securityContext restrito, service.yaml, kustomization.yaml e secret.example.yaml como modelo para MCP_AUTH_TOKEN_FILE. Eles são intencionalmente mínimos — réplica única, sem Ingress, sem NetworkPolicy — e são um ponto de partida, não uma implantação endurecida. Aplique com kubectl apply -k deploy/kubernetes/ após criar um Secret real fora de banda a partir de secret.example.yaml (copie para secret.yaml, preencha os tokens, aplique uma vez; ele é deliberadamente não listado em kustomization.yaml para que uma reaplicação não possa reverter um Secret real para os valores de placeholder). As orientações completas sobre formato de implantação, rotação de tokens e external-secret-store estão em deploy/kubernetes/README.md.
Configuração
Toda a configuração é feita por meio de variáveis de ambiente. O servidor se recusará a iniciar se SEARXNG_URL não estiver definido. Pelo menos um de MCP_AUTH_TOKEN / MCP_AUTH_TOKENS / MCP_AUTH_TOKEN_FILE é obrigatório quando MCP_PORT está definido.
| Variável | Obrigatória | Padrão | Descrição |
|---|---|---|---|
SEARXNG_URL | sim | — | URL base da sua instância SearXNG (a barra final é removida automaticamente) |
MCP_PORT | não | — | Porta para escutar no modo HTTP. Se não definida, o servidor usa stdio |
MCP_AUTH_TOKEN | modo HTTP¹ | — | Token bearer único; a identidade é registrada como "default". Compatível com implantações de locatário único |
MCP_AUTH_TOKENS | modo HTTP¹ | — | Pares identity:token separados por vírgula para pequenos grupos estáticos, ex.: alice:abc...,bob:def... |
MCP_AUTH_TOKEN_FILE | modo HTTP¹ | — | Caminho para um arquivo com um identity:token por linha; comentários # e linhas em branco são ignorados |
MCP_HEALTH_TOKEN | não | — | Token bearer opcional que protege GET /health. Um segredo separado dos tokens MCP acima — não reutilize um valor. Não definido (o padrão) deixa /health aberto. Mesmo mínimo de 32 caracteres. Se você definir, todo prober deve enviá-lo (veja Endpoint de saúde) |
MCP_METRICS_TOKEN | para scraping | — | Token bearer que protege GET /metrics. Um segredo separado dos tokens MCP acima — não reutilize um valor. Não definido, /metrics retorna 401 para todos, incluindo chamadores com um token MCP válido. Mesmo mínimo de 32 caracteres. Obrigatório se você fizer scraping de métricas (veja Métricas) |
MCP_TLS_CERT | não | — | Caminho para um certificado PEM. Com MCP_TLS_KEY, o relay serve HTTPS diretamente em vez de HTTP simples. O par é recarregado a quente na mudança do arquivo, então uma renovação é captada sem reinício. Mutuamente exclusivo com as variáveis MCP_TLS_ACME_*. Veja TLS |
MCP_TLS_KEY | não | — | Caminho para a chave privada PEM de MCP_TLS_CERT. Ambos são obrigatórios juntos; apenas um falha na inicialização |
MCP_TLS_ACME_DOMAINS | para ACME | — | Nomes de host separados por vírgula que o certificado pode cobrir (a lista de permissão de host ACME). Definir isso (ou qualquer variável MCP_TLS_ACME_*) ativa ACME — não há um sinalizador separado de ligar/desligar — e este é então obrigatório. Os certificados são obtidos automaticamente, com desafios servidos via TLS-ALPN-01 na mesma porta (sem necessidade de segunda porta). Mutuamente exclusivo com MCP_TLS_CERT. Veja TLS |
MCP_TLS_ACME_EMAIL | não | — | Endereço de contato da conta ACME. Opcional; se definido, deve ser um endereço simples válido (ex.: admin@example.com), ou a inicialização falha — uma CA pública rejeita um contato malformado no registro. Deixe não definido para registrar sem contato |
MCP_TLS_ACME_DIRECTORY | não | Let's Encrypt | URL do diretório ACME. Aponte para uma CA privada (ex.: step-ca) para usar uma em vez de Let's Encrypt |
MCP_TLS_ACME_CACHE_DIR | não | /var/cache/mcp-acme | Diretório onde os certificados emitidos são armazenados em cache para sobreviver a reinícios. Padrão é o caminho mostrado; monte um volume, bind mount ou PVC lá para torná-lo persistente (sem persistência, reinícios re-solicitam e podem atingir limites de taxa da CA). A inicialização falha se o caminho não for gravável |
MCP_TLS_ACME_CA_ROOTS | não | — | Pacote PEM opcional que o cliente ACME deve confiar para um diretório ACME privado. Por padrão, a CA privada é confiada através do armazenamento de confiança do processo (monte sua raiz lá, ou defina SSL_CERT_FILE); esta substituição em vez disso confina essa confiança ao cliente ACME, mantendo-a fora da ferramenta de busca e dos caminhos SearXNG |
MCP_TLS_HEALTHCHECK_INSECURE | não | false | Quando a sonda --healthcheck fala HTTPS, pule a verificação de certificado. Padrão é false (verificar). Principalmente para TLS de certificado manual cujo certificado não é válido para o endereço de sonda loopback; no modo ACME, a sonda apresenta o primeiro domínio como SNI e verifica normalmente, então isso não é necessário. Afeta apenas a autoprobe, não o endpoint servido. Veja TLS |
MCP_STATELESS | não | false | Se true, o SDK não emite IDs de sessão e trata cada solicitação como uma nova sessão temporária; o relay lê Mcp-Session-Id ele mesmo para correlação. Veja "Modos de sessão" abaixo |
MCP_SESSION_MAX_AGE | não | 168h | Somente modo stateful. Quanto tempo uma sessão pode viver antes que o zelador a feche. Sintaxe de duração Go (30m, 12h, 168h — sem d ou w) |
MCP_SESSION_JANITOR_INTERVAL | não | 15m | Somente modo stateful. Com que frequência o zelador varre sessões expiradas. Mesma sintaxe de duração |
MCP_RATE_LIMIT_RPS | não | 5 | Taxa de solicitações sustentada por chamador (solicitações/segundo). Defina como 0 para desativar. Valores fracionários suportados (ex.: 0.5 = uma solicitação a cada dois segundos) |
MCP_RATE_LIMIT_BURST | não | 2 × RPS, min 1 | Capacidade de rajada do token bucket — o número de solicitações que um chamador pode disparar em sequência antes que a taxa sustentada entre em ação |
MCP_RATE_LIMIT_EXEMPT | não | — | Nomes de identidade separados por vírgula que ignoram o limitador de taxa completamente (ex.: ci,uptime-monitor). Útil para chamadores internos confiáveis e identidades de monitoramento |
AUTH_USERNAME | não | — | Nome de usuário HTTP Basic Auth para SearXNG (se sua instância exigir) |
AUTH_PASSWORD | não | — | Senha HTTP Basic Auth para SearXNG |
SEARXNG_TOKENS | não | — | Tokens de mecanismo privado separados por vírgula enviados como o parâmetro de busca tokens em cada consulta. Mecanismos que carregam uma lista tokens: no settings.yml do SearXNG são invisíveis e inutilizáveis sem um. Escopa este relay para um subconjunto dos mecanismos em uma instância SearXNG compartilhada. Veja Escopando um relay para mecanismos específicos |
USER_AGENT | não | mcp-searxng-relay/<version> | Cabeçalho User-Agent enviado com todas as solicitações de saída |
CACHE_TTL_SECONDS | não | 300 | Quanto tempo o conteúdo de URL buscado é armazenado em cache (segundos) |
CACHE_MAX_ENTRIES | não | 1000 | Número máximo de URLs mantidos no cache em memória. Entradas mais antigas são removidas automaticamente quando o limite é atingido |
MAX_BODY_BYTES | não | 500000 | Tamanho máximo do corpo de resposta lido de URLs buscadas (bytes) |
MAX_PDF_BYTES | não | 50000000 | Tamanho máximo do corpo de resposta para URLs de PDF (bytes). PDFs recebem um limite separado e maior, pois um documento de várias centenas de páginas pode facilmente ter 50 MB |
MAX_OFFICE_BYTES | não | 50000000 | Tamanho máximo do corpo de resposta para URLs de documentos Office (DOCX, XLSX, PPTX + DOC, XLS, PPT legados) (bytes). Arquivos OOXML modernos são arquivos ZIP que rotineiramente incorporam imagens, fontes e dados de gráficos, então recebem seu próprio limite separado de MAX_BODY_BYTES |
MAX_IMAGE_BYTES | não | 7500000 | Tamanho bruto máximo para respostas de imagem (bytes). A forma na transmissão é ~33% maior após codificação base64 |
MCP_HISTORY_ENTRIES | não | 50 | Quantas fontes distintas searxng_session_sources retém por chamador. Slots seguram fontes, não buscas, então isso conta coisas que um agente pode citar. A restrição para aumentá-lo é contexto, não memória — a lista é lida no contexto do modelo em cada chamada, a aproximadamente 40–80 tokens por entrada. Observe mcp_session_sources_elided_total para descobrir se seus agentes precisam de mais |
MAX_EXTRACTED_CHARS | não | 1000000 | Limite no texto extraído mantido (e armazenado em cache) por URL, distinto dos limites MAX_*_BYTES no corpo de resposta bruto. É isso que a paginação searxng_read_url percorre; cada resposta retorna no máximo 100 mil caracteres disso. Nota de memória: no pior caso, o cache mantém CACHE_MAX_ENTRIES × MAX_EXTRACTED_CHARS bytes de conteúdo (~1 GB nos padrões, embora páginas reais raramente se aproximem do limite) — reduza qualquer valor em orçamentos de memória apertados, aumente este para paginar mais fundo em documentos muito grandes |
EXTRACT_LINKS | não | true | Se os alvos de hiperlink de HTML buscado são expostos ao modelo. Quando habilitado, âncoras renderizam como links Markdown ([label](https://resolved-target)) tanto em prosa quanto em células de tabela, correspondendo ao que documentos Office já produzem. hrefs relativos são resolvidos contra a URL da página; apenas alvos http/https são emitidos (javascript:, data: e amigos são descartados). Defina como false para restaurar o comportamento anterior de emitir apenas texto de âncora. Não afeta documentos Office, cujos links passam pelo conversor office_oxide de qualquer forma |
PRUNE_SELECTOR | não | [class*="related"], [id*="related"] | Seletor CSS cujas correspondências são removidas antes de o trafilatura decidir qual subárvore é o artigo. Sem ele, sites que envolvem boilerplate em um contêiner de aparência atraente podem ter esse contêiner selecionado em vez da história — silenciosamente, com texto plausível e sem erro. O padrão é o seletor mais estreito medido para corrigir um caso real (um artigo da Register onde a barra lateral mais popular foi extraída no lugar do corpo) sem mudança em um artigo da heise. Defina como uma string vazia para desativar a poda. Um seletor malformado falha na inicialização em vez de ser silenciosamente ignorado. Note que header e footer são deliberadamente não incluídos: <article><header><h1> é HTML5 comum e podá-lo decapita artigos |
FETCH_ALLOWED_HOSTS | não | — | Entradas host:port separadas por vírgula cujas buscas ignoram a verificação SSRF de IP público, para que a ferramenta de busca possa alcançar recursos internos nomeados (ex.: confluence.corp:443,wiki.internal:8443). A porta é obrigatória — um hostname simples falha na inicialização. Correspondido exatamente no hostname da solicitação (insensível a maiúsculas e ponto final; sem curingas de subdomínio) e re-verificado em cada salto de redirecionamento. Veja Proteção SSRF |
FETCH_ALLOWED_CIDRS | não | — | Entradas range/prefix:port separadas por vírgula tratadas como alcançáveis mesmo que a política padrão as bloqueie (ex.: 10.1.2.0/24:443,192.168.5.0/24:8443). A porta é obrigatória; uma rota padrão (0.0.0.0/0, ::/0) é recusada. Verificado contra o IP resolvido no momento da conexão e em cada redirecionamento, então permanece robusto contra rebinding de DNS. O tamanho de cada intervalo é registrado na inicialização. Veja Proteção SSRF |
FETCH_PROXY | não | — | Proxy de saída para a ferramenta de busca (http, https, socks5, socks5h), ex.: http://proxy.corp:3128. Sozinho, aplica-se apenas a hosts em FETCH_ALLOWED_HOSTS. Deliberadamente não lido de HTTP_PROXY/HTTPS_PROXY. Uma URL malformada ou esquema não suportado falha na inicialização. Veja Proteção SSRF |
FETCH_PROXY_ALL | não | false | Roteie toda busca através de FETCH_PROXY, não apenas hosts na lista de permissão. Para redes sem saída direta. Delega a política SSRF por IP ao proxy: FETCH_ALLOWED_CIDRS e a verificação de IP público param de se aplicar. Definir sem FETCH_PROXY falha na inicialização. Veja Proteção SSRF |
FENCE_SIGNING_KEY | não | — | Chave privada Ed25519 usada para assinar elementos <sec:fence>, fornecida inline. Aceita PEM PKCS#8, DER PKCS#8 base64, uma semente de 32 bytes base64 ou uma chave privada de 64 bytes base64 — a codificação é auto-detectada, e base64 com quebras de linha é aceitável. Quando não definida (o padrão), uma nova chave é gerada a cada início de processo. Mutuamente exclusiva com FENCE_SIGNING_KEY_FILE: definir ambas falha na inicialização, assim como uma chave malformada. Veja Chave de assinatura de cerca |
FENCE_SIGNING_KEY_FILE | não | — | Caminho para um arquivo contendo o mesmo material de chave, para montagens Secret e podman secret. Mesmas codificações e mesma validação que FENCE_SIGNING_KEY. Um arquivo legível além de seu proprietário registra um aviso, mas não falha na inicialização, já que montagens somente leitura rotineiramente caem em 0444. Veja Chave de assinatura de cerca |
LOG_LEVEL | não | info | Verbosidade de log: debug, info, warn, error, off |
LOG_FORMAT | não | text | Formato de log: text ou json |
| ¹ O modo HTTP exige pelo menos uma das três variáveis de token de autenticação. Elas também podem ser combinadas: fontes posteriores sobrescrevem as anteriores se o mesmo digest aparecer em mais de uma. Todos os tokens são validados de forma independente contra um mínimo de 32 caracteres. |
Gere um token forte:
openssl rand -hex 32
Formato do arquivo de token
Ao usar MCP_AUTH_TOKEN_FILE, cada linha que não seja comentário é identity:token. A divisão ocorre no primeiro :, portanto os tokens podem conter dois-pontos; identidades não podem. Identidades são strings arbitrárias usadas apenas para correlação de logs — normalmente um nome de usuário, nome de agente ou rótulo de conta de serviço.
# This is a comment.
alice:7f3a8c2e9b1d4f6a0c8e2b9d4f6a0c8e2b9d4f6a0c8e2b9d4f6a0c8e2b9d4f6a
bob:0e1d2c3b4a596877665544332211ffeedccbbaa998877665544332211ffeedc
service-ci:9876543210fedcba9876543210fedcba9876543210fedcba9876543210fedcba
# Identity rotation: both lines below are accepted for "alice" until
# the old one is removed. Useful for zero-downtime token rotation.
alice:newtokenvaluefor32charsminimum0123456789abcdef0123456789abcdef
Defina o modo do arquivo para 0600 e coloque-o em tmpfs (ou um segredo Docker / volume projetado do Kubernetes) se o seu modelo de ameaças incluir outros usuários no host.
Modos de sessão
O transporte MCP Streamable HTTP é stateful por padrão: o SDK atribui um ID de sessão em initialize, o cliente o ecoa em cada requisição subsequente, e o SDK o procura em um mapa em memória. Quando o servidor reinicia, esse mapa é reconstruído vazio — o ID de sessão antigo do cliente retorna 404, e muitos clientes MCP falham ao reinicializar automaticamente, apesar da especificação exigir isso. O resultado é "eu reimplantei e meu agente está travado até eu reiniciá-lo."
| Modo | MCP_STATELESS | Quando usar | Trade-off |
|---|---|---|---|
| Stateful (padrão) | false | Implantação multi-tenant onde session_id deve ser emitido pelo servidor e à prova de falsificação | O agente deve refazer o handshake após cada reinício do servidor |
| Stateless | true | Implantação que deve sobreviver a reinícios do servidor sem reconexão do cliente | session_id torna-se afirmado pelo cliente (não validado pelo servidor); GET/DELETE retornam 405; notificações iniciadas pelo servidor não podem alcançar o cliente |
Para correlação de auditoria no modo stateful, cada linha de log de chamada de ferramenta carrega tanto identity (qual token autenticou a requisição) quanto session_id (a qual handshake de inicialização a requisição pertence). O session_id conecta as chamadas de ferramenta de volta à linha de log do "session initialized" para a mesma sessão — é onde a identidade do cliente é registrada no momento do handshake. Sessões ociosas são removidas após MCP_SESSION_MAX_AGE por um zelador em segundo plano (padrão: 7 dias); sessões fechadas limpa pelo cliente (DELETE) também são rastreadas e liberadas imediatamente.
No modo stateless, o campo session_id ainda está presente e é estável entre requisições de um cliente, mas vem de outro lugar e significa algo mais fraco. A partir do go-sdk v1.7.0, um servidor stateless não lê nem define Mcp-Session-Id — o próprio req.Session.ID() do SDK fica vazio para cada requisição, e ServerOptions.GetSessionID não é consultado. (Antes do v1.7.0, o SDK ecoava o valor do cliente; a mudança segue a direção sem sessão da especificação MCP, SEP-2567.)
Portanto, no modo stateless, este relay lê o cabeçalho ele mesmo, em um middleware, e apenas nesse modo. O valor é validado quanto à forma — no máximo 128 bytes de ASCII imprimível, sem espaços, rejeitado por completo em vez de truncado — e então usado para exatamente duas coisas: o campo session_id nos logs de auditoria, e a metade de conversa da chave por chamador por trás de searxng_session_sources. Sem ele, dois agentes compartilhando um token compartilhariam um ledger de fonte e expulsariam as entradas um do outro.
O que esse valor é não mudou: um cliente autenticado pode afirmar qualquer session_id que quiser, então é um identificador de correlação e nunca uma afirmação sobre quem é o chamador. O que mudou é quem o lê — agora é a escolha deliberada do relay, em vez de um comportamento do SDK herdado por acidente. Um cliente que não envia cabeçalho simplesmente recebe um session_id vazio, o que é uma degradação limpa em vez de uma falha. identity permanece validado pelo servidor em ambos os modos, e é a chave de junção canônica quando a resistência à falsificação importa — no modo stateless, é a única coisa que mantém os tenants separados, já que a metade de conversa é inteiramente fornecida pelo cliente.
Se você não quiser IDs de sessão nos seus logs, há dois casos. No modo stateful, defina mcp.ServerOptions.GetSessionID para func() string { return "" } em server.go:buildMCPServer: o SDK então omite o cabeçalho de resposta Mcp-Session-Id e req.Session.ID() retorna vazio para cada requisição — modo verdadeiramente "sem sessão". A leitura de cabeçalho do próprio relay deliberadamente não está conectada a esse caminho, então ele não pode devolver os IDs fornecidos pelo cliente que você acabou de pedir para parar de registrar. No modo stateless, remova o middleware trackClientSession da cadeia em main.go; GetSessionID não é consultado lá e não teria efeito. Nenhum dos dois é exposto como variável de ambiente porque o caso de uso é restrito.
Ajustando o zelador de sessão
As duas alavancas do zelador servem a propósitos diferentes e valem a pena entender antes de mudar os padrões:
-
MCP_SESSION_MAX_AGEé uma configuração de política. Limita por quanto tempo qualquer sessão pode viver. Reduza (ex.:24h) quando seu ambiente rotaciona tokens de autenticação diariamente — sessões mais antigas que o período de rotação estão usando um token que não existe mais na tabela, então removê-las força um handshake limpo com o atual. Reduza ainda mais para frameworks de conformidade que exigem reautenticação periódica. Aumente (ex.:720h/ 30d) para agentes em lote ou agendados que legitimamente ficam ociosos por longos períodos. -
MCP_SESSION_JANITOR_INTERVALé uma configuração de mecanismo. Controla com que frequência a passagem de limpeza roda. Intervalos mais curtos capturam sessões expiradas mais cedo ao custo de um pouco de contenção de mutex; intervalos mais longos são mais baratos, mas permitem mais ultrapassagem além deMCP_SESSION_MAX_AGE. O padrão de15msignifica que uma sessão pode viver até 15 minutos além da idade máxima antes de ser fechada — ok para a política "aproximadamente uma semana", mas vale reduzir se sua idade máxima for curta.
Se você não vê o limite de sessão (mcp_active_sessions em /metrics) subindo sob carga, os padrões estão funcionando e não há nada para ajustar.
Ferramentas MCP
searxng_web_search
Executa uma busca na web e retorna títulos, URLs e trechos.
| Parâmetro | Tipo | Obrigatório | Padrão | Descrição |
|---|---|---|---|---|
query | string | sim | — | A consulta de busca |
num_results | number | não | 10 | Número de resultados a retornar (máx. 20) |
pageno | number | não | 1 | Número da página de resultados (máx. 100) |
categories | string | não | general | Categorias SearXNG separadas por vírgula: news, science, files, images, etc. |
language | string | não | all | Código de idioma, ex.: en, de, fr |
time_range | string | não | — | Filtrar por recência: day, month ou year |
safesearch | number | não | 0 | Nível de busca segura: 0 = desligado, 1 = moderado, 2 = estrito |
engines | string | não | padrão da instância | Nomes de mecanismos SearXNG separados por vírgula para consultar, ex.: wikipedia,github. Os nomes correspondem à atribuição de mecanismo em resultados anteriores, então um agente pode reconsultar o backend que exibiu um resultado promissor. A entrada é convertida para minúsculas e espaços são removidos; nomes que a instância não executa são silenciosamente ignorados pelo SearXNG (uma consulta nomeando apenas mecanismos desconhecidos retorna nenhum resultado em vez de um erro) |
Exemplo — notícias recentes em inglês:
{
"query": "fusion energy breakthrough",
"categories": "news",
"language": "en",
"time_range": "month",
"num_results": 5
}
Formato de saída. Cada resultado é renderizado como um bloco de texto da forma:
Title: Example article title
URL: https://example.com/article
Snippet: First sentence or two of the page…
Engines: google, bing, duckduckgo
A linha Engines é omitida quando o SearXNG não retornou o campo (versões mais antigas do SearXNG, ou resultados de uma configuração de mecanismo único). A lista reflete os mecanismos que retornaram esta URL, na ordem que o SearXNG os fornece. Nenhuma pontuação é calculada por cima — o agente é livre para ler a contagem de mecanismos como um sinal de corroboração ou ignorá-la.
searxng_read_url
Busca uma URL e retorna seu conteúdo. Lida com HTML (convertido para Markdown estruturado), PDF (texto extraído via pdf_oxide), documentos do Office (DOCX, XLSX, PPTX, além de DOC, XLS, PPT legados — convertidos para Markdown via office_oxide), texto simples (decodificado por charset) e imagens (JPEG, PNG, GIF, WebP retornadas como blocos MCP ImageContent para consumo por modelos de visão — SVG é intencionalmente excluído, pois é mais útil ao modelo como texto do que como um blob binário codificado em base64). Armazena resultados em cache por padrão; respostas de imagem ignoram o cache de texto.
Documentos longos são paginados. Cada resposta retorna uma janela de no máximo 100.000 caracteres do texto extraído; quando há mais, a resposta termina com um aviso como [content truncated — showing chars 0-100000 of 348211; call searxng_read_url again with start_index=100000 to continue]. O texto extraído completo (até MAX_EXTRACTED_CHARS) é armazenado em cache na primeira busca, então páginas subsequentes são acertos de cache e não custam requisição upstream. Os offsets no aviso são exatos — o agente os ecoa de volta literalmente; o servidor ajusta qualquer offset que dividiria um caractere multibyte e garante que cada página avança, então seguir as dicas de continuação sempre termina.
O texto de PDF é delimitado por linhas de marcador --- [PDF page N of M] ---, uma por página, para que agentes possam responder "o que está na página 47", citar números de página e se orientar dentro de qualquer janela de paginação. Os marcadores são consultivos: eles ficam dentro da cerca de conteúdo não confiável, e um PDF malicioso pode incorporar texto semelhante (veja SECURITY.md). Documentos do Office não recebem marcadores de página — DOCX não tem páginas intrínsecas (a paginação é calculada no momento da renderização, não armazenada no arquivo), então os cabeçalhos Markdown preservados pelo conversor são as âncoras de navegação lá; slides PPTX e planilhas XLSX aparecem como quebras de cabeçalho.
| Parâmetro | Tipo | Obrigatório | Padrão | Descrição |
|---|---|---|---|---|
url | string | sim | — | A URL a buscar (apenas http/https) |
force_refresh | boolean | não | false | Ignorar o cache e buscar uma cópia nova |
start_index | integer | não | 0 | Offset no texto extraído para começar. Use o valor do aviso de truncamento de uma resposta anterior |
max_chars | integer | não | 100000 | Caracteres do texto extraído a retornar nesta resposta (teto: 100000) |
Ambos os parâmetros de paginação são ignorados para URLs de imagem, que são retornadas inteiras como blocos de conteúdo de imagem.
Exemplo — forçar uma busca nova:
{
"url": "https://example.com/article",
"force_refresh": true
}
Exemplo — continuar lendo um documento longo de onde a última resposta parou:
{
"url": "https://example.com/big-report.pdf",
"start_index": 100000
}
searxng_url_metadata
Busca apenas os metadados estruturados de uma URL — título, autor, data de publicação, idioma, nome do site, descrição, imagem, categorias e tags — sem retornar o corpo da página. Para PDFs, page_count também é retornado, para que um agente possa avaliar se um candidato é um memorando de 3 páginas ou um relatório de 400 páginas antes de se comprometer com uma leitura completa (deliberadamente ausente para documentos do Office: DOCX não tem contagem de páginas intrínseca, pois a paginação é calculada no momento da renderização). Aproximadamente uma ordem de magnitude mais barato em tokens que searxng_read_url, e destinado como uma etapa de triagem antes de se comprometer a ler uma URL candidata por completo. Os resultados são armazenados em cache e o cache é compartilhado com searxng_read_url: uma busca de metadados seguida de uma busca de conteúdo (ou vice-versa) custa uma requisição HTTP upstream, não duas.
| Parâmetro | Tipo | Obrigatório | Padrão | Descrição |
|---|---|---|---|---|
url | string | sim | — | A URL para buscar metadados (apenas http/https) |
force_refresh | boolean | não | false | Ignorar o cache e buscar uma cópia nova |
Exemplo — triar três candidatos antes de ler um por completo:
{ "url": "https://example.com/article-a" }
{ "url": "https://example.com/article-b" }
{ "url": "https://example.com/article-c" }
Formato de saída. Um objeto JSON com os campos de metadados selecionados. Campos que o extrator não conseguiu preencher são omitidos em vez de renderizados como strings vazias ou null, então a resposta tem formato variável; no mínimo, url está sempre presente:
{
"url": "https://example.com/article",
"title": "Example article title",
"author": "Jane Doe",
"description": "First paragraph or meta-description.",
"site_name": "Example.com",
"date": "2026-03-12T14:23:00Z",
"language": "en",
"image": "https://example.com/article/cover.jpg",
"categories": ["technology"],
"tags": ["distributed-systems", "go"]
}
Quando usar isto em vez de searxng_read_url. Use searxng_url_metadata para triar qual de várias URLs candidatas vale a pena ler por completo, para construção de citações e para verificação de data/autor/site quando o corpo em si não é necessário. Use searxng_read_url depois de se comprometer a ler uma URL específica. As duas ferramentas compartilham um cache, então triar com metadados primeiro e depois ler as URLs escolhidas por completo não dobra a carga upstream.
searxng_session_sources
Retorna as URLs que este relay buscou para a identidade chamadora, da mais recente para a mais antiga, byte-exatas.
O problema que ela resolve não é recuperação — é transcrição. Um modelo compondo uma resposta final contendo dez URLs está reproduzindo-as a partir de um contexto que passou milhares de tokens antes, token por token, sem nada para verificar. Esse passo acontece após a última chamada de ferramenta, em uma mensagem que nenhum servidor MCP jamais vê, então nada no fio pode validá-lo. Esta ferramenta move os bytes corretos de volta para a posição imediatamente antes de a resposta ser escrita, que é o único lugar onde um servidor pode ajudar. A mesma lista responde à segunda falha — uma URL de aparência plausível para uma página que nunca foi buscada — porque uma URL ausente da lista não foi buscada.
| Parâmetro | Tipo | Obrigatório | Padrão | Descrição |
|---|---|---|---|---|
since_seq | inteiro | não | 0 | Retorna apenas entradas com número de sequência acima deste. Passe o maior seq de uma chamada anterior para ver apenas o que foi buscado desde então |
Formato de saída. Um objeto JSON, uma linha por URL distinta em vez de por busca:
{
"note": "URLs below are byte-exact as fetched by this relay …",
"total_fetches": 12,
"returned": 9,
"elided": 0,
"sources": [
{
"url": "https://www.example.com/psu/flex-atx-350w",
"requested_url": "https://example.com/psu/flex-atx-350w",
"title": "FlexATX 350W review",
"read": "full",
"outcome": "ok",
"chars_read": 18422,
"total_chars": 18422,
"fetched_at": "2026-08-18T09:14:02Z",
"tool": "searxng_read_url",
"seq": 12,
"fetches": 2
}
]
}
read é o campo que distingue uma fonte que um agente pode alegar ter lido daquela que ele apenas olhou: full (todo o texto extraído), partial (uma janela de paginação de um documento mais longo), metadata (searxng_url_metadata apenas — o corpo nunca foi retornado), image ou none (a busca falhou). Buscas com falha aparecem com outcome: "error" e o texto do erro; omiti-las tornaria um 404 indistinguível de uma URL nunca tentada. requested_url aparece apenas quando um redirecionamento moveu a URL, e o url pós-redirecionamento é o que deve ser citado — é a única URL na troca que o modelo nunca viu e, portanto, não consegue reconstruir de forma alguma.
Buscas repetidas se dobram em uma linha. Uma URL buscada mais de uma vez — triada com metadados e depois lida, paginada janela por janela, ou relida após o cache expirar — mantém uma única linha, e fetches conta as chamadas por trás dela. A linha então reporta a leitura mais profunda já alcançada para aquela URL e a marca d'água máxima de chars_read / total_chars: uma chamada posterior apenas de metadados não "deslê" uma página já lida por completo, e uma nova busca que falha não apaga a cópia que foi retornada. Os campos de recência (seq, fetched_at, tool, from_cache) sempre descrevem a busca mais recente, que é o que since_seq e a ordenação da mais recente para a mais antiga estão perguntando.
Escopo do histórico. Por chamador (identidade + ID de sessão), em memória, as 50 fontes mais recentes (MCP_HISTORY_ENTRIES). Os slots guardam fontes em vez de buscas — uma leitura de seis janelas de um documento longo custa um slot, não seis — então o limite é uma contagem de coisas que você pode citar, não de chamadas de ferramenta. Fontes descartadas para abrir espaço são reportadas como uma contagem de elided; total_fetches continua contando chamadas, então legitimamente excede o número de linhas. Chavear por identidade além da sessão importa: sob MCP_STATELESS=true a metade da conversa é afirmada pelo cliente (e vazia para um cliente que não envia Mcp-Session-Id), e na configuração documentada sem sessão ela é vazia para todos — chavear apenas nela permitiria que um chamador lesse as URLs buscadas de outro. No modo sem estado, um cliente que rotaciona esse cabeçalho também cunha chaves de cache livremente e pode empurrar os registros de outros chamadores para fora do cache de 1.000 entradas; isso é uma degradação em vez de um vazamento, e mcp_history_callers_evicted_total é o que torna isso visível. O histórico não sobrevive a um reinício e não cruza réplicas — ele só precisa sobreviver à conversa, e armazenamento compartilhado o ampliaria para "tudo que esta identidade já buscou", o que piora a lista para seu propósito em vez de melhorar. Para implantações com múltiplas réplicas, configure afinidade de sessão no ingress.
Codificação de cerca. Esta resposta é envolvida em um <sec:fence> carregando encoding="cdata". A cerca escapada comum transforma cada & em &, o que para um payload cujo propósito inteiro são URLs byte-exatas é um canal de corrupção autoinfligido — e URLs densas em query string atingem isso em quase toda entrada. A assinatura ainda cobre os bytes pré-codificação exatamente como no caminho escapado; encoding está dentro da forma assinada canônica, então um verificador pode saber como recuperá-los e um atacante não pode alterá-lo. A classificação permanece untrusted: o relay é o autor da afirmação ("busquei X em T"), mas não dos valores — títulos vêm de páginas buscadas — e marcá-la como confiável permitiria que qualquer site lavasse texto em uma cerca confiável sendo buscado uma vez.
Usando com Claude Desktop (modo stdio)
Adicione o seguinte ao seu claude_desktop_config.json:
{
"mcpServers": {
"searxng": {
"command": "/path/to/mcp-searxng-relay",
"env": {
"SEARXNG_URL": "https://your-searxng-instance.example.com"
}
}
}
}
Nenhum MCP_PORT ou MCP_AUTH_TOKEN é necessário no modo stdio — o processo se comunica via stdin/stdout e não é acessível pela rede.
Usando com Claude Desktop (modo HTTP)
Se você preferir executar o servidor como um processo de fundo persistente em vez de gerá-lo por sessão:
{
"mcpServers": {
"searxng": {
"type": "http",
"url": "http://localhost:8080",
"headers": {
"Authorization": "Bearer your-strong-random-token"
}
}
}
}
Nota: Em qualquer implantação não local, o endpoint MCP deve ser alcançado via TLS — seus tokens de portador viajam no que o envolve. Ou coloque um proxy reverso com terminação TLS na frente (nginx, Caddy, Traefik) ou um Ingress, ou faça o relay servir HTTPS ele mesmo com
MCP_TLS_CERT/MCP_TLS_KEYouMCP_TLS_ACME_DOMAINS(veja TLS). Sem nenhum desses, o relay serve HTTP simples e registra um aviso na inicialização.
Escopando um relay para mecanismos específicos
SEARXNG_TOKENS permite que vários relays compartilhem uma única instância SearXNG enquanto cada um alcança apenas seus próprios mecanismos — útil quando equipes separadas têm backends de busca internos separados e não devem ler os uns dos outros.
Marque o mecanismo como privado no settings.yml do SearXNG. tokens: controla quem pode selecionar o mecanismo; a credencial própria do mecanismo (api_key ou equivalente) é o que limita o que ele pode ver:
engines:
- name: teama-confluence
engine: json_engine
base_url: https://confluence-a.corp/rest/api/search
api_key: "<team A service account token>"
shortcut: cfa
categories: [general]
disabled: true
tokens: ['ENGINE-TOKEN-A']
Então dê a cada relay apenas seu próprio token:
docker run -d \
-e SEARXNG_URL=https://searxng.corp \
-e SEARXNG_TOKENS=ENGINE-TOKEN-A \
-e MCP_AUTH_TOKEN=$(openssl rand -hex 32) \
-e MCP_PORT=8080 -p 8080:8080 \
ghcr.io/littleoffice/mcp-searxng-relay:latest
Notas:
disabled: truenão é redundante. Sem ele, o mecanismo fica em sua categoria e dispara em toda busca web comum, adicionando latência e colocando resultados internos diante de consultas não relacionadas. Nomear um mecanismo explicitamente através do parâmetro de buscaenginesconstrói a referência do mecanismo diretamente e não é afetado pelo estado desabilitado por padrão, então o mecanismo ainda roda quando realmente solicitado.- O limite é imposto pelo SearXNG, não por este relay. O SearXNG resolve a lista completa de referências de mecanismos — categorias, o parâmetro
enginese a sintaxe!bangdentro da string de consulta igualmente — e só então descarta mecanismos cujostokens:não são satisfeitos. Um filtro neste processo sobre o parâmetroenginesperderia o caminho do bang; apresentar o token errado não pode ser contornado do lado do agente. - Tokens são por processo, não por chamador. Toda identidade na tabela de tokens os compartilha. Onde dois grupos de chamadores devem ser separados, execute um relay por grupo. As identidades em
MCP_AUTH_TOKEN_FILEsão rótulos de auditoria, não um limite de autorização. tokenscomo parâmetro de consulta é não documentado upstream. A documentação da API de Busca do SearXNG descreve tokens de mecanismo apenas como uma configuração da página de Preferências. Que eles também sejam aceitos como parâmetro de requisição decorre dewebapp.pre_requestmesclandorequest.argsnas preferências que ele analisa. É um comportamento de longa data, mas fixe sua imagem SearXNG por digest e mantenha um teste afirmando o caso negativo — uma busca nomeando o mecanismo de outra equipe sem seu token não retorna resultados.- Apenas busca.
searxng_read_urlnão usa esses tokens. Se o relay deve ser mantido longe dos hosts internos de outra equipe, isso éFETCH_ALLOWED_HOSTS/FETCH_ALLOWED_CIDRS, definidos por relay.
Notas de segurança
Injeção de prompt. Ambas as ferramentas retornam conteúdo originado da web aberta — títulos, trechos e corpos de páginas escritos por terceiros. Um site malicioso pode embutir instruções nesse conteúdo (incluindo em elementos invisíveis ou ocultos) na tentativa de sequestrar o comportamento do agente, causar chamadas de ferramenta inesperadas ou exfiltrar contexto da conversa. Este é o principal risco em tempo de execução ao usar este servidor com um agente LLM.
Este servidor implementa a especificação de cerca de prompt de Peh, S. (2025), "Prompt Fencing: A Cryptographic Approach to Establishing Security Boundaries in Large Language Model Prompts" (arXiv:2511.19727). Toda resposta de ferramenta é envolvida em um elemento <sec:fence> com metadados estruturados, precedido por um preâmbulo curto de conscientização que diz ao modelo consumidor como interpretar o limite:
<sec:fence xmlns:sec="http://promptfence.org/security/1.0"
signature="MEYCIQDx5w2l7..."
kid="3f9a1c7e2b4d8056"
nonce="a9f7e2c14b8d6f31..."
rating="untrusted"
source="https://example.com/article"
timestamp="2026-05-07T14:23:00Z"
type="content"
version="1.0">
<extracted content>
</sec:fence>
O que isso fornece hoje:
- Identificação de chave e versionamento de formato. Toda cerca carrega
kid— a mesma impressão digital reportada por/fence/public-key— eversion.kidpermite que um verificador com várias chaves selecione uma em vez de testar a verificação contra todas, que é o que torna a rotação de chaves viável: cercas assinadas por uma chave de saída permanecem na janela de contexto e continuam chegando enquanto a nova chave é implementada, e semkid"assinado por uma chave que desde então aposentei" e "forjado" ambos se apresentam como "nada no meu conjunto verifica isto". Ambos os atributos estão dentro da forma assinada canônica, então um atacante não pode reescreverkidpara nomear uma chave que controla, ou rebaixarversionpara alcançar um caminho de verificação mais antigo, sem invalidar a assinatura. - Proteção contra escape de limite. Cada cerca carrega um
noncealeatório de 128 bits (decrypto/rand). Um atacante que controla conteúdo buscado não pode adivinhar o nonce, então não pode forjar uma tag de fechamento que encerra a cerca prematuramente ou abrir uma nova cerca "confiável" dentro dela. O preâmbulo de conscientização diz ao modelo consumidor para honrar apenas o limite identificado pelo nonce por resposta. - Assinaturas compatíveis com o futuro. Cada cerca carrega uma assinatura Ed25519 para que um futuro cliente verificador de cercas (ou um gateway verificador externo) possa autenticar que o conteúdo cercado foi emitido por este processo de servidor específico. Os bytes assinados são uma serialização separada por domínio e prefixada por comprimento —
"PromptFence/v1.0" || 0x00 || uint64_be(len(content)) || content || canonical_metadata— alimentada ao PureEd25519 conforme RFC 8032 §5.1 (a operação de assinatura faz hash da mensagem internamente com SHA-512; não pré-hash). Isso é um desvio deliberado da construção literalEd25519(SHA-256(C || M))da seção §4.3 do artigo, que silenciosamente muda o argumento de segurança ao alimentar um digest de 32 bytes em um esquema de assinatura que já faz hash de sua entrada. A tag de domínio previne confusão de assinatura entre protocolos; o prefixo de comprimento remove a ambiguidade de limite que uma concatenaçãocontent || canonical_metadatanua deixaria. O conteúdo é assinado em sua forma pré-XML-escape, então um verificador faz xml-unescape do corpo do elemento analisado antes de verificar. O formato exato do fio é documentado nos blocos de comentáriofence.gocomputeFenceSignatureebuildFenceSigningInput. Nenhum cliente MCP atualmente verifica essas assinaturas; elas estão presentes para compatibilidade futura.
Limitações, declaradas honestamente:
- Sem um verificador, as assinaturas não oferecem nenhuma garantia criptográfica. A proteção contra escape de limites vem inteiramente do nonce por resposta.
- O artigo sobre Prompt Fencing mediu 100% de prevenção de injeção direta em seu ambiente experimental (n=300 tentativas em dois modelos de fronteira), mas esse resultado depende da conformidade do modelo com o preâmbulo de conscientização. Modelos menores ou especializados podem se comportar de forma diferente.
- Ataques semânticos — em que conteúdo não confiável tenta persuadir em vez de personificar — não são abordados por nenhum esquema de fencing.
Chave pública. A chave pública Ed25519 do servidor em execução é exposta em GET /fence/public-key (modo HTTP, sem autenticação — uma chave pública por definição não é um segredo). O banner de inicialização imprime a impressão digital da mesma chave, para que as duas possam ser verificadas cruzadamente. Esse campo fingerprint também é o valor que cada fence carrega como seu kid, para que um verificador possa basear diretamente seu conjunto de chaves confiáveis nele; o campo é deliberadamente não renomeado para kid na resposta do endpoint, já que qualquer coisa que já o analisa espera fingerprint.
Chave de assinatura do fence. Por padrão, a chave de assinatura é gerada do zero a cada início de processo, então a impressão digital muda entre os ciclos de vida do processo. Esse padrão é deliberado: sem uma âncora de confiança externa (uma CA, um conjunto JWK publicado, um KMS), persistir uma chave implicaria uma propriedade de continuidade que este servidor não pode garantir sozinho.
Também é inútil para um verificador. Qualquer coisa que realmente verifique essas assinaturas — um cliente que verifica fences, ou o gateway de segurança externo da seção 4.5 do artigo — precisa de uma chave que possa fixar. Contra uma chave que gira a cada reinicialização, suas únicas opções são buscar novamente /fence/public-key no momento da verificação, o que reduz a verificação a "assinado por quem respondeu", ou fixar manualmente uma impressão digital após cada implantação.
Operadores que executam tal verificador podem, portanto, fornecer sua própria chave, o que coloca a âncora de confiança em seu KMS ou cofre de segredos, em vez de neste processo:
# PKCS#8 PEM — the usual choice for a mounted Secret
openssl genpkey -algorithm ed25519 -out fence.pem
# or a bare 32-byte seed, if an inline env var is easier to manage
# (any 32 bytes is a valid Ed25519 seed)
openssl rand -base64 32
FENCE_SIGNING_KEY_FILE=/etc/mcp-auth/fence-key # mounted file
FENCE_SIGNING_KEY="$(openssl rand -base64 32)" # or inline
O banner informa qual modo está ativo, para que uma configuração incorreta seja visível de relance, em vez de apenas quando um verificador começa a rejeitar fences:
fence key 3f9a1c7e2b4d8056 (persistent, from FENCE_SIGNING_KEY_FILE (PKCS#8 PEM))
fence key a17c04e9b3f2d158 (ephemeral, rotates on restart)
O modo persistente também emite uma linha warn na inicialização, pela mesma razão que ampliar a política de SSRF: ele reverte um padrão deliberado e estende o raio de impacto de um vazamento de chave de um ciclo de vida do processo para "até o operador girar". Gire esta chave na mesma cadência que você gira seu outro material de assinatura — não há expiração automática.
Uma implantação com múltiplas réplicas obtém um segundo benefício. Cada réplica, caso contrário, gera sua própria chave, então um verificador diante de um Service com balanceamento de carga teria que confiar na chave de cada pod e reaprendê-las a cada rollout. Uma chave compartilhada de um único Secret significa que todas as réplicas assinam de forma idêntica.
Duas coisas que uma chave persistente não faz, ditas claramente:
- Ela não fornece verificação. Ela torna a verificação possível ao dar ao verificador algo estável para fixar. Nada neste servidor verifica assinaturas, e uma assinatura que ninguém verifica não oferece garantia, independentemente de como a chave é gerenciada.
- Ela não resolve a distribuição de chaves, que é a metade mais difícil. Um gateway que busca o que
/fence/public-keyretorna atualmente está confiando exatamente no endpoint que tenta autenticar: um atacante que consegue se passar pelo relay também serve sua própria chave. Fixe a impressão digital fora de banda, ou busque-a uma vez por um canal autenticado e alerte sobre mudanças. Observe também que o modo stdio não expõe nenhum endpoint HTTP, então nesse modo a chave pública tem que vir do banner ou ser derivada da chave privada que você já possui.
Para implantações de alto risco, considere restringir as ferramentas a uma lista de permissões conhecida de domínios, executar o agente com um escopo mínimo de permissões e auditar sequências de chamadas de ferramentas em sua camada de aplicação.
Proteção SSRF. A ferramenta de busca de URL (searxng_read_url) resolve nomes de host no momento da conexão TCP e rejeita qualquer endereço que não seja um IP unicast globalmente roteável. Duas camadas são executadas em cada conexão e em cada salto de redirecionamento:
- Predicados da stdlib:
IsLoopback,IsLinkLocalUnicast,IsLinkLocalMulticast,IsPrivate(RFC 1918 + RFC 4193 ULA),IsUnspecified,IsMulticaste!IsGlobalUnicast(que captura broadcast dirigido IPv4). - Uma lista codificada de CIDRs reservados que os predicados da stdlib não capturam, cada um anotado com a RFC que o reserva:
0.0.0.0/8(RFC 1122),100.64.0.0/10CGNAT (RFC 6598),192.0.0.0/24atribuições de protocolo IETF (RFC 6890),192.0.2.0/24/198.51.100.0/24/203.0.113.0/24TEST-NET-1/2/3 (RFC 5737),192.88.99.0/24anycast 6to4 obsoleto (RFC 7526),198.18.0.0/15benchmark (RFC 2544),240.0.0.0/4reservado para o futuro incluindo255.255.255.255(RFC 1112),64:ff9b::/96e64:ff9b:1::/48NAT64 (RFC 6052/8215),100::/64prefixo de descarte (RFC 6666),2001::/32Teredo (RFC 4380),2001:2::/48benchmark IPv6 (RFC 5180),2001:10::/28e2001:20::/28ORCHID/ORCHIDv2 (RFC 4843/7343),2001:db8::/32documentação (RFC 3849),2002::/166to4 (RFC 3056).
Ambas as verificações são executadas antes que qualquer byte chegue à rede, e a cadeia de redirecionamento é revalidada em cada salto, então um atacante que controla o DNS de um host de aparência pública não pode fazer rebinding para um endereço interno entre a verificação e a conexão.
Acessando recursos internos (opt-in). O padrão acima bloqueia todos os endereços não públicos, que é a postura correta para uma ferramenta que busca URLs influenciadas por atacantes. Operadores que executam o relay dentro de uma rede confiável e desejam que ele leia recursos internos — um Confluence auto-hospedado, Jira, GitLab ou wiki — podem ampliar a política com duas listas de permissões, ambas vazias por padrão (então o comportamento padrão permanece inalterado):
-
FETCH_ALLOWED_HOSTS— entradas exatas dehost:portque ignoram a verificação de IP público. A correspondência é no hostname da solicitação, não em um IP resolvido, então você pode nomear um host interno sem fixar seu endereço; o chamador não pode forjá-lo (o hostname vem da URL que um chamador autenticado solicitou) e você controla o DNS para seus próprios nomes, então isso não reabre o buraco de rebinding. A correspondência é insensível a maiúsculas/minúsculas e ponto final, e exata —confluence.corp:443não corresponde asub.confluence.corp:443.A porta é obrigatória. Escreva
host:port; um hostname simples é um erro de inicialização. Isso ocorre porque permitir apenas um hostname entregaria à ferramenta de busca qualquer outra coisa que essa máquina esteja escutando — Redis na 6379, etcd na 2379, um kubelet na 10250. Um chamador autenticado só precisa pedirhttp://confluence.corp:6379/, e um redirecionamento do serviço permitido alcança os mesmos lugares. Nomear a porta faz você declarar o alcance que realmente pretende:FETCH_ALLOWED_HOSTS=confluence.corp:443,wiki.internal:8443Você não precisa escrever a porta nas URLs. A entrada é comparada com a porta efetiva da solicitação, com o padrão do esquema preenchido primeiro, então
confluence.corp:443corresponde a umhttps://confluence.corp/pagesimples ewiki.internal:80corresponde ahttp://wiki.internal/page. Um host que atende ambos os esquemas precisa de ambas as entradas (wiki.internal:80,wiki.internal:443); portas se acumulam por host em vez de sobrescrever.Literais IPv6 usam a forma de URL entre colchetes:
[fd00::1]:8443.Uma entrada inválida — sem porta, porta vazia ou fora do intervalo, ou um valor que na verdade é uma URL — falha na inicialização com uma mensagem nomeando a entrada e a forma esperada. Nunca é descartada silenciosamente: uma linha de lista de permissões que analisa mas nunca pode corresponder é o pior resultado possível aqui, porque você acreditaria que o acesso foi concedido e a busca falharia longe da configuração que a causou.
-
FETCH_ALLOWED_CIDRS— intervalos de IP tratados como alcançáveis, escritosrange/prefix:port. Verificados contra o IP resolvido no momento da conexão e em cada salto de redirecionamento, então permanece seguro contra rebinding: um atacante que faz rebinding de um nome de aparência pública para um IP privado ainda é bloqueado, a menos que esse IP exato caia dentro de um intervalo que você listou, em uma porta que você listou.A porta também é obrigatória aqui, e importa mais do que para hostnames. Um hostname nomeia uma máquina, então a porta era toda a sua exposição. Um intervalo já cobre muitas máquinas, e deixar a porta aberta multiplica isso por 65535:
Entrada Endereços Pares endereço:porta alcançáveis 10.1.2.0/24:443256 256 10.1.2.0/24(rejeitada)256 16.776.960 10.0.0.0/8:44316.777.216 16.777.216 10.0.0.0/8(rejeitada)16.777.216 1.099.494.850.560 IPv6 funciona da mesma forma —
fd00:1234::/64:8443. Os dois-pontos não são ambíguos: um CIDR sempre termina em/<prefixlen>, e um comprimento de prefixo é apenas dígitos, então a porta é o que segue o último dois-pontos após a barra.Uma rota padrão é recusada, não apenas alertada.
0.0.0.0/0ou::/0não amplia a política de endereços, ela a remove — loopback, link-local e o endpoint de metadados da nuvem se tornam todos alcançáveis, e a ferramenta de busca fica sem nenhuma restrição de endereço. Se a aplicação genuinamente pertence a outro lugar, diga isso comFETCH_PROXY_ALL, que é explícito sobre a delegação.A largura é relatada, não limitada. Cada intervalo permitido é registrado na inicialização com o número de endereços que cobre, e separadamente se ele varre um endereço sensível:
WARN fetch allow-list covers an IP range cidr=10.0.0.0/8 addresses=16777216 ports=443 WARN fetch allow-list covers a sensitive address cidr=169.254.0.0/16 address=169.254.169.254 what="cloud metadata endpoint (IMDS) — hands out instance credentials"Um
/8interno plano é incomum, mas real, e recusá-lo empurraria operadores paraFETCH_PROXY_ALL— o que impede o relay de resolver destinos completamente. Um intervalo amplo, mas visível, é o melhor resultado. A distinção que o aviso faz é deliberado versus varrido:127.0.0.1/32:8080é alguém que quis dizer isso; um/8que por acaso contém link-local é alguém que não olhou.Prefira
FETCH_ALLOWED_HOSTSonde puder. Um hostname nomeia o único serviço que você quis dizer. Recorra a um intervalo apenas quando você genuinamente não puder fixar os nomes.
Os dois são independentes (semântica OU): uma busca é permitida se seu host e porta estão na lista de permissões, ou seu IP resolvido é público, ou seu IP resolvido e porta estão dentro de uma entrada CIDR permitida. Ambos são reavaliados em cada salto de redirecionamento, então um redirecionamento aberto em um host permitido ainda não pode pivotar para um endereço interno bloqueado — nem, quando a entrada é limitada por porta, para uma porta diferente no próprio host permitido.
Duas cautelas ao usar estes:
- Um CIDR permitido substitui todos os bloqueios padrão para os endereços que cobre, incluindo loopback e link-local. Listar um intervalo é uma declaração explícita de que é seguro alcançá-lo. Mantenha intervalos restritos — em particular, não liste
169.254.0.0/16a menos que você realmente pretenda expor o endpoint de metadados da nuvem em169.254.169.254. - Um CIDR malformado falha na inicialização com um erro claro em vez de ser descartado silenciosamente — um erro de digitação em um controle de segurança deve parar o servidor, não ampliar ou estreitar silenciosamente.
Quando qualquer lista não está vazia, o banner de inicialização reflete a política ampliada (uma linha fetch policy mais os exatos allowed hosts / allowed cidrs que você configurou), e uma linha de auditoria de nível warn é emitida, então é óbvio pelos logs que a ferramenta de busca agora pode alcançar alvos internos e precisamente quais.
Acessando recursos através de um proxy de saída (opt-in). Algumas redes não dão ao relay rota para um segmento interno, ou nenhuma rota direta para fora; a única maneira é um proxy encaminhador. Mais duas variáveis optam por isso, ambas não definidas por padrão:
FETCH_PROXY— a URL do proxy. Sozinho, ele se aplica apenas a hosts emFETCH_ALLOWED_HOSTS. Isso não custa nada em termos de aplicação: hosts na lista de permissões já ignoram a verificação por IP por design, então roteá-los por um proxy não abre mão de nada que ainda estivesse em execução. Qualquer outra busca continua sendo feita diretamente, com a política completa de IP público em vigor.FETCH_PROXY_ALL— roteia todas as buscas pelo proxy. Isso é para implantações sem saída direta, onde a alternativa não é uma postura mais rígida, mas uma ferramenta que não funciona. Entenda isso como uma delegação: o proxy realiza a conexão e, portanto, a resolução de DNS, então o relay nunca descobre o IP de destino, eassertPublicIPeFETCH_ALLOWED_CIDRSdeixam de participar. Seu proxy de saída se torna o ponto de aplicação. Redirecionamentos também ficam a cargo do proxy (o limite de saltos ainda se aplica), porque uma consulta local não poderia restringi-los e essas redes frequentemente não dão ao relay nenhum resolvedor.
Nenhuma das variáveis é lida do ambiente HTTP_PROXY / HTTPS_PROXY. Elas são definidas por imagens base, sistemas de CI e controladores de admissão de cluster por motivos não relacionados, e honrá-las aqui permitiria que uma variável que ninguém definiu para esse propósito alterasse silenciosamente um controle de segurança; elas também carregam o padrão oposto (proxy para tudo, exceto NO_PROXY) em relação à postura de negação por padrão deste subsistema. O cliente SearXNG ainda as honra, já que seu único destino é controlado pelo operador e não apresenta exposição a SSRF.
Duas observações:
- Conexões ao proxy configurado ignoram a verificação por IP — nomeá-lo em
FETCH_PROXYjá é a declaração de que é seguro alcançá-lo, e exigir10.0.0.0/8emFETCH_ALLOWED_CIDRSapenas para alcançar um proxy seria um trade-off muito pior. Uma busca cuja URL alvo nomeia o proxy é recusada, então essa isenção não pode ser alcançada a partir de uma URL fornecida pelo chamador. - Um proxy restrito a hosts na lista de permissões registra logs em
info.FETCH_PROXY_ALLemite uma linha de auditoria de nívelwarn, e ambas aparecem como uma linhafetch proxyno banner de inicialização, com qualquer senha na URL redigida.
Autenticação. Cabeçalhos Authorization recebidos passam por SHA-256 uma vez e são consultados em uma tabela em memória indexada pelo SHA-256 de cada "Bearer <token>" configurado. A consulta opera em chaves de comprimento fixo de 32 bytes, então não pode vazar o comprimento do token por diferenças de tempo de resposta (uma verificação de igualdade byte a byte faria um curto-circuito no primeiro byte diferente). Apenas digests permanecem na memória do processo após a inicialização — os tokens brutos são lidos apenas do env / arquivo de token durante o parsing. Tokens nunca aparecem em logs; o banner de inicialização mostra apenas a contagem de tokens configurados e identidades distintas. Em caso de correspondência bem-sucedida, a identidade associada ao token é anexada ao contexto da requisição e registrada em cada linha de log de chamada de ferramenta (identity=<name>) para correlação de auditoria.
Proteção entre origens. O transporte HTTP Streamable é envolvido pelo net/http.CrossOriginProtection do Go pelo go-sdk (v1.4.1+, aplicado como correção para CVE-2026-33252 — "Cross-Site Tool Execution for HTTP Servers without Authorization"). POSTs originados de navegadores cujos cabeçalhos Sec-Fetch-Site ou Origin indicam uma requisição entre origens são rejeitados, assim como POSTs sem Content-Type: application/json. Clientes que não são navegadores — curl, Go http.Client, tráfego de agentes de IA — não enviam nem Sec-Fetch-Site nem Origin e passam sem impacto, então o uso legítimo por agentes remotos não é afetado. Isso é adicional à autenticação por bearer token, não um substituto: a verificação entre origens ocorre antes do processamento da requisição, mas qualquer requisição que sobreviva a ela ainda precisa apresentar um token válido para alcançar o handler do MCP.
Endurecimento do contêiner. A imagem Docker executa como usuário não-root (UID 1001) em uma base mínima scratch — a imagem de runtime contém apenas o binário estaticamente linkado e certificados CA, sem shell, gerenciador de pacotes ou userland do SO.
Segurança de PDF e Office. A extração de PDF usa pdf_oxide e a extração de Office (DOCX/XLSX/PPTX + DOC/XLS/PPT legados) usa office_oxide, ambos núcleos Rust que garantem zero panics e zero timeouts em todas as entradas. Um documento malformado ou criado de forma adversa retornará um erro, não derrubará o processo do servidor.
Relato e proveniência. Problemas de segurança devem ser relatados de forma privada — veja SECURITY.md para o processo de divulgação e escopo. O código é principalmente gerado por IA e revisado, construído e testado por um único mantenedor humano antes do lançamento; supply-chain.md é a declaração completa de dependências, proveniência de build e processo de desenvolvimento, escrita para revisores que avaliam o projeto para um ambiente controlado.
Limitação de taxa
No modo HTTP, o servidor aplica um limite de taxa token-bucket por chamador a requisições sob /. Os padrões são 5 requisições por segundo sustentadas com uma rajada de 10 — confortável para um único agente raciocinando com as ferramentas (padrão típico é 1–3 chamadas de ferramenta por turno do agente, com segundos de tempo de pensamento do modelo entre elas), enquanto ainda limita o dano que um agente descontrolado ou token vazado pode causar. Defina MCP_RATE_LIMIT_RPS=0 para desativar.
Os buckets são indexados por identidade quando a requisição carrega um bearer token reconhecido, e por IP remoto caso contrário. O fallback é intencional: um atacante não autenticado tentando forçar tokens a partir de um único host compartilha um bucket indexado por IP, independentemente de qual palpite de token apresente, então o limitador reduz o ataque na borda da rede, em vez de na verificação de autenticação. Chamadores autenticados são cobrados contra sua identidade — múltiplos agentes usando o mesmo token compartilham um bucket, que é a semântica correta para "orçamento de uso desta credencial."
Rejeições retornam HTTP 429 Too Many Requests com um cabeçalho Retry-After contendo uma contagem inteira de segundos. Cada rejeição emite uma linha de log WARN estruturada com identity (quando conhecido), remote, method, path e retry_after para que a trilha de auditoria registre tráfego negado da mesma forma que registra tráfego não autorizado. O contador Prometheus mcp_rate_limit_rejections_total agrega rejeições para dashboards e alertas (sem rótulo por identidade por design — eventos de rejeição já estão no log estruturado quando a forense precisar deles).
O armazenamento de buckets é um LRU limitado a 10.000 entradas. Identidades são limitadas pela tabela de tokens de autenticação configurada, então todas cabem confortavelmente; o limite restringe a memória sob um ataque de rotação de IP, ao custo de que buckets despejados resetam para cheios no próximo contato (o que não afeta materialmente a limitação para atacantes distintos).
O que isso não cobre. /health nunca é limitado por taxa, para que um balanceador de carga em polling não seja marcado como abusivo. /metrics também é isento — um scraper que faz polling em intervalo fixo não deve produzir lacunas no Prometheus que pareçam indisponibilidades, e um scraper abusivo é melhor contido rotacionando MCP_METRICS_TOKEN do que dando 429 no endpoint de métricas. Como esse token é separado dos tokens MCP, revogá-lo custa ao scraper apenas seu próprio acesso. /fence/public-key não é autenticado e não é limitado (é uma chave pública, pública). O modo Stdio não tem middleware HTTP e, portanto, não tem limite de taxa, mas também é um único processo confiável, sem superfície de ataque remota.
Lista de isenção. MCP_RATE_LIMIT_EXEMPT=ci,uptime-monitor pula o limitador inteiramente para essas identidades. Use para agentes de monitoramento interno que acessam o endpoint raiz do MCP (em vez de /metrics) e para pipelines de CI que executam testes funcionais de alta taxa contra o serviço ao vivo. Tokens para identidades isentas ainda devem vir de uma fonte forte — isenção é sobre volume, não confiança.
Notas de ajuste.
- Agente único. Os padrões são suficientes. Um agente de raciocínio faz chamadas de ferramenta de dígito único por turno, bem abaixo de 5 rps.
- Muitos agentes concorrentes sob uma identidade. Se você colocar vários agentes atrás de um token, calcule
(agents × peak-burst-per-agent)e definaMCP_RATE_LIMIT_BURSTpara cobri-lo, deixandoMCP_RATE_LIMIT_RPSno orçamento sustentado por identidade que você realmente deseja. Ou divida em uma identidade por agente e deixe os limites se acumularem naturalmente. - Implantações multi-réplica. Buckets são por processo. Sob roteamento round-robin, a taxa efetiva por chamador é
(replicas × RPS); sob roteamento com sessão fixa, éRPS. Se você precisar de um orçamento globalmente aplicado, termine no Ingress e definaMCP_RATE_LIMIT_RPS=0nos pods. - Público/exposto à internet. Aperte o RPS para uma taxa amigável ao upstream do SearXNG e mantenha
MCP_RATE_LIMIT_BURSTpróximo disso — a rajada é o que um atacante exploraria primeiro.
Limites de sessão
No modo HTTP, o servidor limita sessões concorrentes a 1.000. Requisições para inicializar além desse limite recebem uma resposta 503 Service Unavailable. Sessões são removidas quando o cliente envia uma requisição DELETE.
Operações
Notas para executar o servidor em produção. A maior parte disso vive no código e nos comentários, mas é o tipo de detalhe que um operador precisa antes do primeiro incidente, não depois.
Endpoint de saúde
GET /health é uma sonda de liveness + readiness não autenticada. Ela retorna:
| Status | Corpo | Significado |
|---|---|---|
200 OK | {"status":"ok","searxng":"reachable"} | O servidor está em execução e a instância SearXNG upstream respondeu com HTTP < 500. |
503 Service Unavailable | {"status":"degraded","searxng":"unreachable"} | O servidor está em execução, mas a sonda SearXNG upstream falhou. |
O resultado de alcance do upstream é armazenado em cache por 10 segundos para que um balanceador de carga em polling não sobrecarregue o SearXNG. O endpoint é aberto por padrão (sondas não precisam enviar um bearer token) e intencionalmente não limitado por taxa (um poller de LB de alta frequência nunca deve receber 429 de /health).
Opcionalmente exigindo um token. Defina MCP_HEALTH_TOKEN para proteger /health atrás de um bearer token — útil quando o endpoint é alcançável além do host local, já que um /health aberto tanto divulga o status de alcance do upstream quanto (uma vez por janela de cache de 10 s) dispara uma sonda contra o SearXNG. O token é um segredo separado dos bearer tokens do MCP: a sonda e o endpoint do MCP são domínios de confiança diferentes, então não devem compartilhar uma credencial. Ele é validado contra o mesmo mínimo de 32 caracteres, e um valor curto demais falha na inicialização.
⚠️ Se você definir
MCP_HEALTH_TOKEN, todo prober deve enviá-lo. O token é aplicado para todos os chamadores de/health, então qualquer verificador de saúde que não apresenteAuthorization: Bearer <token>começará a receber401e marcará o serviço como não saudável. Isso inclui balanceadores de carga externos, monitores de uptime e sondas KuberneteshttpGet. O único prober conectado automaticamente é a auto-sonda do contêiner--healthcheck, que lê a mesma variável de ambiente (veja abaixo). Para uma sonda KuberneteshttpGet, adicione o cabeçalho explicitamente:readinessProbe: httpGet: path: /health port: http httpHeaders: - name: Authorization value: Bearer <your-health-token>
O deployment.yaml incluído usa /health apenas como sonda de readiness; liveness é uma sonda TCP simples. Isso é deliberado: uma indisponibilidade transitória do SearXNG não deve cascatear para o kubelet matar o pod, apenas para o tráfego ser roteado para longe até o SearXNG se recuperar. (Uma sonda de liveness TCP não precisa de cabeçalho Authorization mesmo quando MCP_HEALTH_TOKEN está definido, já que ela nunca acessa /health.)
Flag CLI --healthcheck
A diretiva HEALTHCHECK do contêiner no Dockerfile invoca mcp-searxng-relay --healthcheck, que é uma autoverificação: o binário faz uma única GET para http://127.0.0.1:$MCP_PORT/health com um tempo limite de 5 segundos, sai com 0 se a resposta for 200 e sai com 1 caso contrário. A flag existe porque a imagem de runtime scratch não tem shell, curl ou wget para escrever uma verificação convencional — o binário precisa ser sua própria verificação. Quando MCP_HEALTH_TOKEN está definido, a autoverificação lê essa mesma variável de ambiente e envia o cabeçalho Authorization: Bearer automaticamente, então uma única entrada de ambiente cobre tanto o servidor quanto sua própria verificação.
Isso é para implantações simples com docker run / Compose. O Kubernetes usa as sondas HTTP em deployment.yaml e ignora a diretiva HEALTHCHECK.
Desligamento gracioso
Ao receber SIGTERM ou SIGINT, o servidor para de aceitar novas conexões e dá às solicitações em andamento até 30 segundos para concluir antes de sair. O janitor de sessões (modo stateful) é interrompido ao mesmo tempo. Se a janela de drenagem expirar com solicitações ainda em andamento, o processo sai com código não zero.
Dois controles de implantação interagem com isso:
- Kubernetes
terminationGracePeriodSeconds. O padrão é 30s na maioria dos clusters, o que corresponde exatamente ao tempo limite de drenagem — deixando margem zero para o kubelet entregar o SIGTERM, o servidor recebê-lo e a resposta ser liberada. DefinaterminationGracePeriodSeconds: 45(ou mais) na especificação do Pod para que a drenagem tenha uma chance real de terminar. - Compose
stop_grace_period. O padrão é 10s, que é mais curto que o tempo limite de drenagem do servidor. Definastop_grace_period: 45sno serviço para que o SIGKILL não chegue no meio da drenagem.
Para implantações com múltiplas réplicas atrás de um Ingress ou balanceador de carga, o LB precisa remover o Pod do registro antes que o SIGTERM chegue — caso contrário, o tráfego continua chegando durante a janela de drenagem. O Kubernetes lida com isso automaticamente assim que as sondas de prontidão começam a falhar, que é uma das razões pelas quais /health é a sonda de prontidão e não a de vivacidade.
Timeouts do servidor HTTP
O http.Server da stdlib do servidor é configurado com três valores deliberados:
| Configuração | Valor | Motivo |
|---|---|---|
ReadTimeout | 30s | Limita por quanto tempo um cliente lento pode segurar a linha de solicitação, cabeçalhos e leitura do corpo. Longo o suficiente para corpos JSON-RPC típicos; curto o suficiente para desencorajar ataques estilo slowloris. |
WriteTimeout | desabilitado (0) | O go-sdk gerencia prazos por stream para respostas SSE. Um prazo de escrita no nível do servidor fecharia prematuramente streams de eventos de longa duração durante chamadas de ferramenta que levam mais de alguns segundos. |
IdleTimeout | 120s | Janela ociosa de keepalive. Acima do tempo típico de pensamento do cliente entre chamadas de ferramenta; abaixo do ponto em que conexões mortas se acumulam. |
Ao colocar um proxy reverso na frente do servidor (recomendado para qualquer implantação não local — veja Notas de segurança), os timeouts do próprio proxy devem acomodar respostas de streaming:
- nginx. Defina
proxy_read_timeouteproxy_send_timeoutpara pelo menos o maior tempo de parede de chamada de ferramenta que você espera — um agente de raciocínio sobre um PDF grande ou documento do Office pode levar 30+ segundos. Desabiliteproxy_bufferingpara a rota MCP para que os chunks SSE cheguem ao cliente imediatamente. - Caddy. O
Caddyfileincluído defineflush_interval -1na diretiva reverse_proxy do MCP, que é o que desabilita o buffer de resposta do Caddy para streaming. - Traefik. Use o campo
forwardingTimeouts.responseHeaderTimeoute garanta que o entrypoint não esteja configurado com um timeout ocioso agressivo.
Se você vir chamadas de ferramenta falhando com streams SSE truncados em uma implantação com proxy reverso, o timeout de leitura/escrita do proxy é quase sempre a causa, não o relay.
TLS
Por padrão, o relay fala HTTP simples e o TLS é terminado por qualquer coisa que esteja na frente dele — o serviço Caddy na stack podman, um Ingress no Kubernetes. Esse continua sendo o formato recomendado onde quer que tal terminador já exista. Para uma implantação sem proxy — o relay rodando sozinho — ele também pode servir HTTPS diretamente, em um de dois modos (mutuamente exclusivos; configurar ambos falha na inicialização):
Certificado manual. Aponte MCP_TLS_CERT e MCP_TLS_KEY para um certificado PEM e chave:
docker run -e MCP_PORT=8443 -e MCP_TLS_CERT=/tls/tls.crt -e MCP_TLS_KEY=/tls/tls.key ...
O par é carregado uma vez na inicialização (um caminho ruim ou certificado/chave incompatíveis falham na inicialização, não no primeiro handshake) e relido no próximo handshake sempre que os arquivos mudarem — então uma renovação in-place (cert-manager reescrevendo um Secret montado, um hook de deploy do certbot) é captada sem reinicialização.
Certificados automáticos (ACME). Não há flag de ligar/desligar — nomear o(s) hostname(s) para certificar com MCP_TLS_ACME_DOMAINS ativa o ACME:
docker run -e MCP_PORT=443 \
-e MCP_TLS_ACME_DOMAINS=relay.example.com \
-e MCP_TLS_ACME_EMAIL=admin@example.com \
-v mcp-acme:/var/cache/mcp-acme ...
Definir qualquer variável MCP_TLS_ACME_* seleciona o modo ACME, e MCP_TLS_ACME_DOMAINS é então obrigatório — então uma configuração ACME pela metade (uma variável perdida ou com erro de digitação) falha na inicialização de forma ruidosa em vez de cair silenciosamente para HTTP simples. Os certificados são armazenados em cache em MCP_TLS_ACME_CACHE_DIR (padrão /var/cache/mcp-acme); monte um volume, bind mount ou PVC lá para que sobrevivam a reinicializações — sem persistência, reinicializações re-solicitam e podem atingir limites de taxa da CA, e a inicialização falha se o caminho não for gravável. Os desafios são respondidos via TLS-ALPN-01 no mesmo listener, então apenas a porta TLS precisa estar acessível — sem responder :80.
-
Emissão e registro na inicialização. O relay contata a CA na inicialização, solicitando um certificado para cada host
MCP_TLS_ACME_DOMAINSassim que o listener está ativo (em vez de preguiçosamente no primeiro handshake do cliente), então uma configuração incorreta aparece imediatamente. Observe o log para isso — eminfovocê obtémacme: enabled(o diretório, hosts, diretório de cache e como a CA é confiada), depoisacme: requesting certificate/acme: certificate ready(ouacme: certificate request failedcom o erro) por host; emLOG_LEVEL=debugcada handshake — incluindo o desafio TLS-ALPN-01 da própria CA — é registrado. Erros de handshake TLS de clientes reais também são registrados. Se você não vir nenhuma linhaacme:, o ACME não ativou — verifique seMCP_TLS_ACME_DOMAINSestá definido e escrito corretamente, e se você está executando um build que inclui isso (não há mais uma flag de ligar/desligarMCP_TLS_ACME). A CA deve conseguir alcançar a porta TLS do relay para completar o desafio; se seu servidor ACME não registrar nenhuma solicitação recebida, essa acessibilidade (DNS/firewall/roteamento para o listener) é a primeira coisa a verificar. -
E-mail de contato.
MCP_TLS_ACME_EMAILé opcional. Deixe sem definir para registrar a conta ACME sem contato; se você definir, forneça um endereço válido simples — uma CA pública (Let's Encrypt) rejeita um contato malformado no registro, e o relay verifica a forma do endereço na inicialização para que essa falha apareça imediatamente em vez de na primeira emissão. -
Acessibilidade do desafio (
could not connect to validation target). A CA valida via TLS-ALPN-01 conectando-se ao host na porta tcp/443 — a porta 443 é fixada pela RFC 8737, qualquer que seja a porta em que o relay escuta. Então<host>:443(para cada nome emMCP_TLS_ACME_DOMAINS) deve resolver, a partir da rede da CA, para este relay e ser alcançável através de qualquer firewall/NAT. Se o relay escuta em uma porta não-443, publique-a para que o:443do domínio ainda roteie (por exemplo,-p 443:8443). Umacme:error:connection/ "não foi possível conectar ao alvo de validação" no log significa que esse caminho está quebrado, não o relay — verifique a partir do host da CA comopenssl s_client -connect <host>:443 -alpn acme-tls/1 -servername <host>. -
CA privada (ex.: step-ca).
MCP_TLS_ACME_DIRECTORYseleciona o diretório ACME (padrão: produção Let's Encrypt). Se o certificado do endpoint do diretório dessa CA não for publicamente confiável, o caminho mais simples é adicionar sua raiz ao trust store do contêiner (grave na imagem ou definaSSL_CERT_FILE) — então deixeMCP_TLS_ACME_CA_ROOTSsem definir e o ACME usa o trust store do sistema. DefinaMCP_TLS_ACME_CA_ROOTS=/path/to/ca-roots.pemapenas quando quiser que essa confiança seja confinada ao cliente ACME para que a CA privada não seja também confiada pela ferramenta de busca e pelo cliente SearXNG. Isso é o equivalente in-process da configuração ACME que oCaddyfileincluído já usa. -
Verificação de saúde. A autoverificação
--healthcheck(usada peloHEALTHCHECKdo Docker) segue o servidor para HTTPS quando o TLS está ativo. No modo ACME, ela apresenta o primeiro hostMCP_TLS_ACME_DOMAINScomo SNI TLS enquanto ainda disca para127.0.0.1, para que o servidor possa servir seu certificado real (um SNI de IP de loopback é recusado pela política de host ACME) e a verificação valide contra esse hostname — nenhuma configuração extra é necessária uma vez que o certificado foi emitido. No modo manual, a verificação disca para127.0.0.1diretamente, então o certificado servido deve ser válido para o endereço de loopback (adicione127.0.0.1/localhostcomo SANs) para que a verificação passe;MCP_TLS_HEALTHCHECK_INSECURE=truepula a verificação quando não pode. De qualquer forma, isso afeta apenas a autoverificação de loopback, nunca o endpoint servido.
No Kubernetes, prefira terminar TLS em um Ingress com cert-manager (ingress.example.yaml); o caminho MCP_TLS_* dentro do pod existe para rodar o relay sozinho em um namespace sem Ingress (veja o README do Kubernetes).
Construindo a imagem Docker
docker build -t mcp-searxng-relay .
O build multi-estágio compila o binário no builder golang:1.26.6-trixie com digest fixado e copia apenas o binário estático e os certificados CA para uma imagem de runtime scratch.
Registro de logs
Toda a saída de log vai para stderr. Defina LOG_FORMAT=json para registro estruturado compatível com agregadores de log.
Na inicialização, o servidor imprime um banner de configuração no stderr independentemente do nível de log. O banner lista todas as configurações ativas com segredos mascarados. AUTH_USERNAME só é mostrado quando está definido.
######################################################################################################################
mcp-searxng-relay v1.0.0
######################################################################################################################
mode streamable-http
address :3000
searxng http://searxng:8080
password [not set]
user-agent Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
cache ttl 5m0s
cache entries 1000 max
body limit 500000 bytes
pdf limit 50000000 bytes
office limit 50000000 bytes
image limit 7500000 bytes
log level info
log format text
session mode stateless
auth tokens 3 configured (3 identities)
rate limit 5 rps, burst 10
fence key 3e21267250e41cbb
######################################################################################################################
Uma vez que o servidor está rodando, linhas de log típicas se parecem com isto (modo stateful, LOG_FORMAT=text):
time=2026-05-24T07:41:10.301Z level=INFO msg="url fetched" url=https://github.com/asgeirtj/system_prompts_leaks content_type="text/html; charset=utf-8" bytes_raw=372821 chars_extracted=5469
time=2026-05-24T07:41:10.302Z level=INFO msg="fetch completed" url=https://github.com/asgeirtj/system_prompts_leaks kind=text identity=zed session_id=O3GD67SQIYXDYN57XCVQMZYKDI
time=2026-05-24T07:43:39.212Z level=INFO msg="search completed" query="site:github.com/asgeirtj/system_prompts_leaks \"Claude Code\" system prompt" page=1 results=10 categories="" identity=zed session_id=O3GD67SQIYXDYN57XCVQMZYKDI
time=2026-05-24T07:43:52.249Z level=INFO msg="url fetched" url=https://github.com/asgeirtj/system_prompts_leaks/blob/main/Anthropic/claude-code.md content_type="text/html; charset=utf-8" bytes_raw=500000 chars_extracted=185
time=2026-05-24T07:43:52.253Z level=INFO msg="fetch completed" url=https://github.com/asgeirtj/system_prompts_leaks/blob/main/Anthropic/claude-code.md kind=text identity=zed session_id=O3GD67SQIYXDYN57XCVQMZYKDI
time=2026-05-24T07:44:07.656Z level=INFO msg="url fetched" url=https://raw.githubusercontent.com/asgeirtj/system_prompts_leaks/main/Anthropic/claude-code.md content_type="text/plain; charset=utf-8" bytes_raw=58874 chars_extracted=58873
time=2026-05-24T07:44:07.657Z level=INFO msg="fetch completed" url=https://raw.githubusercontent.com/asgeirtj/system_prompts_leaks/main/Anthropic/claude-code.md kind=text identity=zed session_id=O3GD67SQIYXDYN57XCVQMZYKDI
Uma busca onde alguns backends SearXNG falharam não é um erro — o upstream responde 200 com o que os mecanismos sobreviventes produziram — mas é uma resposta degradada, e é registrada como tal:
level=WARN msg="searxng search was degraded: some engines did not respond"
unresponsive_engines=google,bing unresponsive_count=2
detail="google: Suspended: Access denied; bing: timeout"
query="..." results=7
hint="results are incomplete; check the named engines in your SearXNG instance
before treating thin results as a relay or model problem"
Mecanismos quebram — mudanças no markup upstream, uma API descontinuada, um muro de captcha — e o SearXNG os suspende e segue em frente. Sem essa linha, a degradação é invisível em toda a pilha: menos resultados chegam ao agente, suas respostas pioram e nada em lugar nenhum diz o porquê. Implantações com muitos mecanismos configurados verão entradas intermitentes conforme os mecanismos passam por suspensão; isso é ruído que vale a pena ter, porque a alternativa é o silêncio.
O mesmo evento incrementa mcp_searches_degraded_total e mcp_searxng_engine_errors_total{engine="…"} (veja Métricas): a linha de log é como você diagnostica um incidente, os contadores são como você descobre que há um — um WARN que ninguém grep não é monitoramento.
O campo session_id une cada chamada de ferramenta de volta à linha "session initialized" onde o identity do cliente foi registrado pela primeira vez; combinados, eles formam a trilha de auditoria. A linha "unauthorized request" mostra como é uma tentativa de token bearer falha — o valor Authorization rejeitado nunca é registrado, apenas o endereço remoto. Em LOG_FORMAT=json, os mesmos campos aparecem como um objeto JSON plano por linha, que é o que a maioria dos agregadores de log espera.
Métricas
No modo HTTP, GET /metrics retorna contadores em formato texto Prometheus, controlados por MCP_METRICS_TOKEN.
⚠️
MCP_METRICS_TOKENé necessário para fazer scraping. Sem ele definido,/metricsretorna401para todo chamador — incluindo alguém que possui um token MCP válido. Defina-o com um valor deopenssl rand -hex 32e forneça esse valor ao seu scraper:MCP_METRICS_TOKEN=<openssl rand -hex 32>Ele não deve ser um dos seus tokens MCP.
mcp_fetches_by_domain_totallista até 512 nomes de host de destino que este relay buscou, em todos os chamadores. Servido na tabela de tokens MCP, isso permite que cada locatário leia quais hosts todos os outros locatários estão lendo — e em um relay comFETCH_ALLOWED_HOSTSconfigurado, esses são seus nomes de host internos. Um scraper não é um locatário e um locatário não é um scraper; a credencial os separa em ambas as direções.
O endpoint é fechado em vez de aberto por padrão porque uma fronteira que só existe após ser configurada não é uma fronteira — ela falharia silenciosamente em todas as implantações que ainda não leram este parágrafo.
/healthadota o padrão oposto (aberto, a menos queMCP_HEALTH_TOKENesteja definido) porque divulga dois campos fixos, não o perfil de saída da frota.
A linha
metrics authdo banner de inicialização reportaCLOSEDquando nenhum token está definido, e o servidor registra uma linha de aviso na inicialização informando isso, para que um painel em branco seja diagnosticável deste lado, em vez do lado do scraper.
As séries expostas são:
| Série | Rótulos | Notas |
|---|---|---|
mcp_searches_total | — | Todas as chamadas para searxng_web_search |
mcp_search_errors_total | — | Subconjunto das acima que retornou um erro |
mcp_metadata_total | — | Todas as chamadas para searxng_url_metadata |
mcp_metadata_errors_total | — | Subconjunto das acima que retornou um erro |
mcp_session_sources_total | — | Todas as chamadas para searxng_session_sources. Leia como uma proporção em relação a mcp_fetches_total: indica com que frequência os agentes verificam suas URLs antes de responder |
mcp_session_sources_elided_total | — | Chamadas que retornaram uma lista incompleta, ou seja, um agente pode ter respondido com base em um registro que não continha mais tudo o que leu. Persistentemente diferente de zero é o sinal para aumentar MCP_HISTORY_ENTRIES |
mcp_history_callers_evicted_total | — | Chamadores cujo histórico inteiro foi descartado do cache de 1.000 entradas. Aumentar com uma contagem estável de chamadores significa que alguém está gerando chaves de cache — no modo sem estado, a metade da conversa da chave é declarada pelo cliente, então um cliente que rotaciona Mcp-Session-Id pode expulsar todos os outros |
mcp_history_evictions_total | — | Fontes descartadas do histórico de um chamador para abrir espaço. Indica o quanto os chamadores ultrapassam o limite; por si só, pode ser um chamador ocupado que nunca lê sua lista de volta, então ajuste com base no contador omitido acima |
mcp_fetches_total | — | Todas as chamadas para searxng_read_url |
mcp_fetch_errors_total | — | Subconjunto que retornou um erro |
mcp_fetches_by_type_total | type=html|pdf|office|plain|image | Buscas bem-sucedidas por extrator utilizado |
mcp_fetches_by_domain_total | domain=<host>, outcome=success|error | Contadores de sucesso/falha por domínio |
mcp_cache_hits_total | — | Requisições searxng_read_url atendidas pelo cache |
mcp_cache_misses_total | — | Requisições que caíram em uma busca de rede |
mcp_cache_force_refresh_total | — | Requisições com force_refresh=true |
mcp_rate_limit_rejections_total | — | Requisições HTTP rejeitadas pelo limitador de taxa por chamador (respostas 429). Detalhes da rejeição — identidade, remoto, nova tentativa — estão no log WARN estruturado; nenhum rótulo por identidade aqui por design |
mcp_ssrf_blocked_total | reason=loopback|link_local|private|unspecified|multicast|non_global_unicast|reserved | Conexões de busca/redirecionamento recusadas porque o destino resolveu para um endereço não público, por classe. Esta é a fronteira de saída tornada visível; um pico é um agente (ou atacante) sondando endereços internos/de metadados de nuvem. O CIDR reservado correspondente e o IP ofensor permanecem no log de depuração, nunca neste rótulo ou em qualquer resposta ao chamador |
mcp_auth_failures_total | endpoint=mcp|metrics|health | Requisições HTTP rejeitadas com 401 em cada superfície protegida. Um pico é sondagem de credenciais ou um scraper/probe mal configurado (por exemplo, um scraper ainda recebendo 401 porque MCP_METRICS_TOKEN não está definido — o caso do endpoint fechado conta sob endpoint="metrics"). O remoto ofensor está no log WARN; nenhum rótulo por remoto aqui |
mcp_searches_degraded_total | — | Chamadas searxng_web_search que retornaram HTTP 200, mas nomearam mecanismos sem resposta. Leia como uma proporção em relação a mcp_searches_total — o número único que indica se a instabilidade do backend é ruído de fundo ou a coisa que está piorando as respostas dos seus agentes. Não é um erro, então mcp_search_errors_total deliberadamente não as vê |
mcp_searxng_engine_errors_total | engine=<name> | Falhas por backend SearXNG, do campo unresponsive_engines upstream. Responde qual mecanismo, uma vez que a proporção acima indica que há um problema. Limitado a 256 nomes distintos; o restante é agregado sob engine="__overflow__" |
mcp_active_sessions | — | Medidor: sessões MCP ativas atuais (somente modo com estado) |
mcp_search_duration_seconds | le | Histograma: latência de ida e volta da busca SearXNG. Baldes de 50ms a 30s |
mcp_fetch_duration_seconds | le | Histograma: latência do pipeline de busca de URL (conexão até extração), observada para ambos searxng_read_url e searxng_url_metadata. Inclui acertos de cache, que caem no balde mais baixo — alerte nos quantis superiores (por exemplo, histogram_quantile(0.99, ...)) e leia o p50 junto com mcp_cache_hits_total. O balde superior corresponde ao tempo limite de 30s do cliente de busca, então observações de +Inf são requisições próximas ao tempo limite |
Cardinalidade por domínio
mcp_fetches_by_domain_total é limitado a 512 domínios distintos. Uma vez atingido esse limite, destinos únicos adicionais são agregados sob o valor de rótulo sintético domain="__overflow__" em vez de expandir ainda mais o conjunto de rótulos. O limite é uma escolha de design deliberada: um agente buscando muitos hosts únicos não deve poder crescer a memória do processo ou o índice do Prometheus sem limite.
Se o contador de estouro for diferente de zero no seu ambiente, ou sua frota de agentes legitima toca mais de 512 domínios (nesse caso, aumente maxTrackedDomains em metrics.go e reconstrua) ou algo está errado com as consultas que você está passando para a ferramenta (nesse caso, o estouro está fazendo seu trabalho ao sinalizar isso). Operadores que desejam uma auditoria completa de cada URL buscada devem confiar nas linhas de log de busca estruturadas (url=…) em vez do contador de métricas; a métrica é observabilidade, não proveniência.
O que a métrica por domínio não é
Não é uma entrada de lista de bloqueio que o servidor lê de volta. O projeto não bloqueia automaticamente domínios com base em taxas de falha — essa decisão pertence ao operador. O fluxo de trabalho pretendido é: o operador revisa as contagens de falha por domínio em sua configuração Prometheus / Grafana, decide quais (se houver) hosts descartar e atualiza sua configuração estática de acordo. Em comparação com um sistema que muta seu próprio comportamento, isso mantém o comportamento do servidor em qualquer momento como uma função apenas de sua configuração, que é o que o torna auditável.