RFC 0006 — Consultar o Prometheus pelo prometheus-mcp
| Campo | Valor |
|---|---|
| Status | Proposed |
| Escrita | 16/10/2026 |
| Última revisão | 16/10/2026, no mesmo dia da escrita: requisito de exposição, port-forward e tailnet entraram depois da primeira versão. Ver Mapa da proposta |
| Objetivo | Dar ao agente e ao operador um caminho de consulta ao Prometheus que não passe por ssh + docker exec + wget, sem abrir porta nova nem gastar nada |
| Requisito inegociável | Nada público na VPS, em nenhuma fase. Ver Requisito de exposição |
| Afeta | contabo-monitoring-server (docker-compose.yml, Makefile, .mcp.json, CLAUDE.md) |
| Depende de | — |
| Relacionada a | RFC 0003 — Fadiga de alertas, RFC 0004 — Plataforma vs aplicação |
Referência
- prometheus/prometheus-mcp, servidor MCP oficial do projeto Prometheus
- TSDB Admin APIs
- Web configuration file do exporter-toolkit
Contexto
O Prometheus do contabo-monitoring-server escuta só na rede interna do Docker. Nenhuma das portas de 9090, 9093 e 3000 é publicada no host, e o cloudflared é todo o ingresso. A consequência é que não existe curl do Mac para o Prometheus, e toda consulta hoje toma esta forma:
ssh monitoring-server 'docker exec prometheus wget -qO- \
--post-data="query=count(up)" http://127.0.0.1:9090/api/v1/query'
São 198 regras carregadas, 102 delas de alerta, e as últimas duas semanas de execução da RFC 0003 e da RFC 0004 foram feitas quase inteiras por esse comando: medir label presente, conferir se uma recording rule retorna série, comparar contagem antes e depois de um deploy.
O projeto Prometheus publica um servidor MCP oficial que fala com a API HTTP e expõe query, range_query, series, label_names, label_values, metric_metadata, list_rules, list_alerts, list_targets e tsdb_stats como ferramentas, além da documentação oficial embutida no binário. É o mesmo conjunto de endpoints que já usamos na mão.
Problema
O caminho atual tem três custos, e os três já foram pagos.
A camada de citação erra em silêncio. O corpo do POST vai como form-urlencoded, onde + vale espaço: =~".+" chega ao Prometheus como =~". ", casa nada e responde status: success com result: []. Em 16/10/2026 isso me fez medir sete labels como ausentes quando os sete estavam presentes. Não existe erro, existe resposta vazia, que é indistinguível do caso real de série ausente.
O laço quebra. ssh dentro de while read consome o stdin e o laço para na primeira iteração, então consulta em lote precisa de < /dev/null ou de iteração por índice. Mais uma regra para lembrar na hora de medir, não na hora de escrever a regra.
A resposta vem crua. /api/v1/rules com 198 regras devolve um JSON de alguns milhares de linhas, e hoje ele é filtrado por um python3 -c escrito na hora, diferente a cada consulta. O reload-prometheus do Makefile é a única versão dessa filtragem que sobreviveu em arquivo.
Nada disso é intransponível, e é justamente esse o problema: é atrito constante e pequeno, na operação que esta stack mais executa.
Requisito de exposição
Este requisito vem antes de qualquer item da proposta e vale para as duas fases. Nada desta RFC pode ficar público na VPS. A forma operacional da regra é esta: nenhuma fase pode criar um socket de escuta novo no host, e nenhum caminho de acesso pode existir sem autenticação antes dele. Um endpoint MCP é leitura do TSDB inteiro, porque quem alcança o endpoint consulta tudo que o Prometheus guarda com as credenciais do cliente default do servidor, e o servidor não valida credencial nenhuma por conta própria.
Cinco regras:
- Nenhum socket novo no host.
expose:sempre,ports:nunca. O conjunto de portas em escuta na VPS depois desta RFC tem que ser idêntico ao de antes dela, conferido porss -ltnp. - Todo caminho de acesso é autenticado antes de chegar ao servidor MCP. Na fase 1 a autenticação é a chave SSH, e o servidor nem chega a escutar em lugar nenhum: fala
stdiodentro da sessão. Na fase 2, sem autenticação na frente não há fase 2. - Nenhuma regra nova de firewall e nenhum hostname novo publicado sem política de acesso criada antes.
- Exposição se confere de fora, não na intenção. É o D8 da RFC 0003: a configuração que pretendia restringir é válida mesmo quando não restringe nada. O critério de aceitação 5 é tentativa de alcance do IP público, não leitura de arquivo de configuração.
- Nada de exceção temporária. Publicar
8080:8080"só por enquanto" ou "só para testar" deixa o TSDB inteiro aberto no IP público da VPS enquanto durar o teste, e o teste dura até alguém lembrar. Depuração se faz de dentro da rede do Docker, comdocker exec, como já se faz com o Prometheus.
Estado medido em 16/10/2026
A regra acima trata do que esta RFC acrescenta. O que já existe foi medido antes de escrevê-la, e o resultado muda a leitura do resto do documento:
ss -ltnp 0.0.0.0:22 sshd
0.0.0.0:4318 docker-proxy ← otelcol, em todas as interfaces
ufw status Status: inactive
iptables -S INPUT -P INPUT ACCEPT
iptables -S DOCKER-USER (vazia)
A 4318 está pública na internet hoje, e não restrita ao IP do cluster k3s como o CLAUDE.md do contabo-monitoring-server prescreve. O que a protege é só o mTLS com RequireAndVerifyClientCert, que é defesa real, mas é uma camada onde a intenção declarada eram duas. Não existe firewall na VPS: o ufw está inativo, a política de INPUT é ACCEPT e a DOCKER-USER está vazia, que é o motivo de o ports: do compose aparecer direto na internet.
Isso é anterior a esta RFC e não é criado por ela, mas fica registrado aqui porque o requisito do título não é verdadeiro para a VPS hoje. A correção é uma regra em DOCKER-USER limitando a 4318 ao IP do cluster, custo recorrente R$ 0, e ela é pré-requisito da fase 2: não faz sentido exigir cinco condições de um endpoint novo enquanto o que já existe está aberto. A fase 1 não depende dela, porque a fase 1 não escuta em porta nenhuma.
Vale notar que a VPS já está num tailnet (contabo, 100.118.125.5), e o Mac também (macbook-air-de-yslan). O nó da Hostinger não está, então mover a 4318 para o tailnet exigiria enrolar o cluster primeiro. Para a fase 2, porém, o tailnet basta sozinho, e é o que o item 8 passa a propor.
Princípio de design
- Nenhuma porta nova no host. É o item 1 do requisito de exposição, repetido aqui porque ele decide o desenho inteiro: é ele que escolhe
stdiosobre SSH em vez de um serviço HTTP. - Capacidade é allowlist, não default. O servidor registra todas as ferramentas por padrão, e este Prometheus roda com
--web.enable-lifecycle. - Custo recorrente zero. Nada de VPS, serviço gerenciado ou túnel pago.
- Versão fixada. O
docker-compose.ymlfixalatestem todas as imagens, o que a CLAUDE.md do repo já registra como peso morto. A entrada nova não aumenta esse passivo.
Proposta
Mapa da proposta
São dez itens, e nem todos estão no mesmo plano. O que está em fase 1 é o que esta RFC propõe fazer; o que está em fase 2 depende de decisão e de condições; o que está em achado é anterior à RFC, foi medido ao escrevê-la e não é criado por ela.
| Item | Plano | O que é |
|---|---|---|
| 1 | Fase 1 | Transporte stdio sobre SSH, sem porta e sem container residente |
| 2 | Fase 1 | .mcp.json versionado no repositório |
| 3 | Fase 1 | Allowlist de ferramentas, sem quit e sem reload |
| 4 | Fase 1 | Pin em v0.18.0 e a armadilha do rename na v0.19.0 |
| 5 | Fase 1 | Teto de resposta, para o contexto não encher |
| 6 | Fase 1 | make mcp-check, que verifica o que foi carregado |
| 7 | Fase 1 | Registro no CLAUDE.md, com a fronteira entre os caminhos |
| 9 | Fase 1 | make port-forward, o equivalente ao kubectl port-forward |
| 8 | Fase 2 | Transporte HTTP, com as condições de cada forma de alcance |
| 10.2 | Fase 2 | As três formas de fase 2 pelo tailnet, comparadas |
| Estado medido | Achado | A 4318 pública e a ausência de firewall na VPS |
| 10.1 | Achado | O alias de SSH pelo IP público, com o tailnet já instalado nas duas pontas |
Os dois achados não bloqueiam a fase 1, que não escuta em porta nenhuma, e são pré-requisito da fase 2.
1. Transporte stdio sobre SSH, sem serviço residente
O servidor fala MCP por stdio por padrão, e stdio atravessa SSH sem adaptação: o cliente MCP roda ssh, o ssh roda docker run -i na VPS, e o container conversa com o Prometheus pela rede interna do Docker. O acesso continua sendo autenticado pela chave SSH, que é o controle que já existe.
A rede do compose se chama monitoring_monitoring, com o prefixo do projeto (~/monitoring no remoto), e não monitoring. Verificado em 16/10/2026 com docker network ls. É a diferença entre o container resolver prometheus e falhar na conexão.
ssh monitoring-server docker run --rm -i --network monitoring_monitoring \
ghcr.io/tjhop/prometheus-mcp-server:v0.18.0 \
--prometheus.url=http://prometheus:9090
O container é efêmero: sobe quando o cliente MCP abre a sessão e morre quando ela fecha, porque o --rm -i termina junto com o stdin. Nada roda quando ninguém está consultando.
2. .mcp.json versionado no contabo-monitoring-server
A configuração entra no repositório, em escopo de projeto, e não no ~/.claude.json de uma máquina. Quem clonar o repo e tiver o alias monitoring-server no ~/.ssh/config tem a ferramenta funcionando sem passo nenhum, que é o objetivo desta RFC.
{
"mcpServers": {
"prometheus": {
"command": "ssh",
"args": [
"monitoring-server",
"docker", "run", "--rm", "-i",
"--network", "monitoring_monitoring",
"ghcr.io/tjhop/prometheus-mcp-server:v0.18.0",
"--prometheus.url=http://prometheus:9090",
"--prometheus.truncation-limit=300",
"--log.level=warn",
"--mcp.tools=list_alerts",
"--mcp.tools=list_rules",
"--mcp.tools=list_targets",
"--mcp.tools=targets_metadata",
"--mcp.tools=alertmanagers",
"--mcp.tools=tsdb_stats",
"--mcp.tools=wal_replay_status",
"--mcp.tools=config",
"--mcp.tools=flags",
"--mcp.tools=runtime_info",
"--mcp.tools=build_info",
"--mcp.tools=healthy",
"--mcp.tools=ready"
]
}
}
}
O .mcp.json não carrega segredo: o endereço é um alias de SSH e a URL é interna à rede do Docker. Pode entrar em commit.
3. Allowlist de ferramentas, porque o lifecycle está ligado
O default do --mcp.tools é all, e all inclui quit e reload, que chamam /-/quit e /-/reload. O docker-compose.yml desta stack sobe o Prometheus com --web.enable-lifecycle, então os dois endpoints respondem: com a configuração default, um LLM pode derrubar o Prometheus desta VPS por uma chamada de ferramenta.
A allowlist do item 2 deixa os dois de fora. Ficam de fora também as três ferramentas de TSDB Admin (snapshot, delete_series, clean_tombstones), que exigem a flag --dangerous.enable-tsdb-admin-tools e que esta RFC não propõe ligar em momento nenhum.
O reload continua existindo onde sempre esteve, no make reload-prometheus, que é o único caminho de deploy e o que confere a contagem de regras depois.
As ferramentas core são carregadas sempre e não precisam ser listadas: query, range_query, series, label_names, label_values, metric_metadata e os três docs_*. São elas que cobrem o trabalho de medição do dia a dia.
4. Pin em v0.18.0, com a armadilha do rename anotada
A última release é a v0.18.0, de 25/04/2026, e a imagem correspondente é ghcr.io/tjhop/prometheus-mcp-server:v0.18.0. O repositório migrou para a organização prometheus depois dela, e as duas consequências pegam quem seguir o README:
- O README da branch
maindocumenta uma versão que ainda não saiu. Ele descreve o binário comoprometheus-mcpe o prefixo de variável de ambiente comoPROMETHEUS_MCP_, ambos válidos só a partir dav0.19.0. Nav0.18.0o binário éprometheus-mcp-servere o prefixo éPROMETHEUS_MCP_SERVER_. As variáveis antigas passam a ser ignoradas sem erro no upgrade, que é o mesmo formato do defeito D8 da RFC 0003: configuração válida escrita num lugar que nada lê. A proposta usa flags em vez de variáveis de ambiente justamente por isso. - A imagem na
quay.io/prometheus/prometheus-mcpsó tem a tagmain, construída a partir da branch. Em 16/10/2026 não havia tag de versão ali. Quando av0.19.0sair, o pin migra para a imagem da organização e esta seção vira o registro do que mudou.
Também fica desligado o --docs.auto-update, que é o default: a documentação embutida no binário basta, e o auto-update abriria tráfego de saída periódico para buscar o que já veio na imagem.
5. Teto de resposta em 300 linhas
--prometheus.truncation-limit=300 limita o que cada chamada devolve ao modelo, e 0 desliga o limite. A escolha do número é arbitrária e serve para o caso que mais incomoda, que é list_rules sobre 198 regras. O modelo pode pedir mais por chamada quando precisar, então o limite não bloqueia consulta, só evita despejar o inventário inteiro em cada pergunta.
A flag --mcp.enable-toon-output troca JSON por um formato mais compacto. Fica fora por enquanto: ela muda o formato de toda resposta e o ganho depende do padrão de uso, que ainda não temos medido. Entra depois, com medição, se o consumo de contexto incomodar.
6. Alvo make mcp-check no Makefile
Um alvo que puxa a imagem e verifica que o servidor fala com o Prometheus, na mesma linha do check-remote, para que a falha apareça no terminal e não como ferramenta silenciosamente ausente no cliente MCP.
MCP_IMAGE ?= ghcr.io/tjhop/prometheus-mcp-server:v0.18.0
mcp-check:
ssh $(SERVER) "docker pull $(MCP_IMAGE)"
@printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"make","version":"0"}}}' \
'{"jsonrpc":"2.0","method":"notifications/initialized"}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| ssh $(SERVER) "docker run --rm -i --network monitoring_monitoring $(MCP_IMAGE) \
--prometheus.url=http://prometheus:9090 --log.level=error" \
| python3 -c "import json,sys; \
r=[json.loads(l) for l in sys.stdin if l.strip()]; \
t=[x for x in r if x.get('id')==2][0]['result']['tools']; \
n=sorted(i['name'] for i in t); \
print(f'prometheus-mcp: {len(n)} ferramentas'); \
print('PERIGO, carregadas: '+', '.join(set(n)&{'quit','reload','delete_series','snapshot','clean_tombstones'})) if set(n)&{'quit','reload','delete_series','snapshot','clean_tombstones'} else None"
O alvo roda o servidor sem a allowlist de propósito. Serve para enxergar o conjunto default e confirmar que quit e reload estão mesmo lá, que é a premissa do item 3.
7. Registro no CLAUDE.md do repositório
A seção "Consultar Prometheus e Alertmanager" passa a ter dois caminhos, com a fronteira explícita:
| Para | Caminho |
|---|---|
| Consulta de métrica, label, série, regra, alvo | Ferramentas MCP do prometheus |
| Alertmanager: alertas agrupados, silences, roteamento | docker exec alertmanager wget, como hoje |
| Reload, deploy, validação de regra | make, como hoje |
A armadilha do + em form-urlencoded continua valendo para o caminho wget e deixa de existir no caminho MCP, que passa a query como argumento estruturado. A armadilha do lookback de 5 minutos continua valendo nos dois: é do Prometheus, não do transporte.
8. Fase 2, condicional: transporte HTTP, e por onde ele é alcançado
Só se houver motivo, e só com as condições abaixo atendidas antes de subir. O servidor também fala MCP por HTTP (--mcp.transport=http, servido em /mcp no endereço de escuta), o que permitiria uma entrada no docker-compose.yml com expose: 8080. O que muda entre as três formas desta fase não é o serviço, é por onde ele é alcançado: por port-forward (item 9, a forma preferida), pelo túnel do Cloudflare com Access, ou pelo tailnet. As condições abaixo valem para a forma com Cloudflare; a do item 9 não precisa de nenhuma delas além da quinta e da sexta, porque não publica hostname nenhum.
O que ela traz de bom: o servidor expõe métricas próprias em /metrics (prom_mcp_tool_calls_failed_total, prom_mcp_api_call_duration_seconds, entre outras), que o Prometheus desta VPS pode raspar pela rede interna, e o repositório do projeto traz um dashboard pronto. O que ela traz de ruim é exatamente o que o requisito de exposição proíbe: um endpoint que fala com o Prometheus e que, sem autenticação na frente, é leitura do TSDB inteiro para quem o alcançar.
As cinco condições, todas obrigatórias e nenhuma delas suficiente sozinha:
expose: 8080, nuncaports:. O serviço só é alcançável de dentro da redemonitoring_monitoring, de onde o cloudflared fala com ele. O túnel é uma conexão de saída, então o host continua sem socket novo em escuta e a regra 1 do requisito de exposição segue satisfeita. Nenhuma mudança de firewall.- Política de Cloudflare Access no hostname, criada antes do primeiro deploy. Hostname publicado sem política é endpoint aberto durante a janela entre uma coisa e outra, por menor que ela seja.
- Service token para o cliente MCP, e não login interativo: o cliente manda
CF-Access-Client-IdeCF-Access-Client-Secret, que saem do.env, que é gitignored. O.mcp.jsonda fase 1 não carrega segredo nenhum e o da fase 2 carregaria, o que muda o que pode entrar em commit. --web.config.filecom TLS ebasic_auth_userscomo segunda camada, para que o endpoint não fique aberto dentro da rede do Docker caso o Access falhe. Com uma ressalva registrada no README do projeto: sobre o transporte HTTP, o servidor repassa o headerAuthorizationde cada requisição ao Prometheus, então basic auth no servidor MCP conflita com esse repasse. Neste ambiente o Prometheus não exige credencial, então o repasse não dá acesso a mais nada, mas a interação precisa ser medida antes de ir para produção, não deduzida.- A allowlist do item 3 continua valendo, com
quitereloadde fora. Autenticação na frente não é motivo para ampliar a capacidade atrás.
E uma sexta, que vem do estado medido: a regra de DOCKER-USER restringindo a 4318 ao IP do cluster entra antes. Enquanto a porta que já existe estiver aberta na internet, exigir cinco condições de uma porta nova é teatro.
Faltando qualquer uma das seis, a fase 2 não sobe. A fase 1 não depende de nenhuma delas e não cria nada disso.
Alternativa por tailnet
A VPS e o Mac já estão no mesmo tailnet, o que torna possível uma fase 2 sem Cloudflare Access, sem service token e sem segredo no .mcp.json. As três formas de fazer isso, e o que cada uma custa em exceção à regra 1, estão no item 10.
9. Port-forward por SSH, como ferramenta e como forma da fase 2
O equivalente ao kubectl port-forward nesta stack é ssh -L, com uma diferença que decide se funciona ou não: o alvo não pode ser localhost, porque no host da VPS as portas 9090, 9093 e 3000 não existem. O alvo é o IP do container na bridge do Docker, que o host alcança e o sshd conecta.
ssh -N -L 127.0.0.1:9090:172.18.0.3:9090 monitoring-server
Medido em 16/10/2026: com o túnel aberto, curl -sG http://127.0.0.1:9090/api/v1/query --data-urlencode 'query=count(up{job=~".+"})' respondeu 82, e o =~".+" que pelo wget --post-data casa zero voltou a funcionar, porque a codificação deixa de ser escrita na mão. No mesmo teste, a contagem de sockets em escuta na VPS ficou em 8 antes e depois: o túnel não cria nada do lado remoto, o socket novo é no Mac e em loopback.
O IP do container não é estável entre recriações, então ele se resolve a cada invocação em vez de ficar escrito em algum lugar:
# make port-forward → prometheus em 127.0.0.1:9090
# make port-forward SVC=grafana PORT=3000
# make port-forward SVC=alertmanager PORT=9093
SVC ?= prometheus
PORT ?= 9090
LOCAL_PORT ?= $(PORT)
port-forward:
@ip=$$(ssh $(SERVER) "docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' $(SVC)" 2>/dev/null); \
test -n "$$ip" || { echo "ERRO: container $(SVC) nao encontrado na VPS"; exit 1; }; \
echo "$(SVC) $$ip:$(PORT) -> http://127.0.0.1:$(LOCAL_PORT) (ctrl-c encerra)"; \
ssh -N -L 127.0.0.1:$(LOCAL_PORT):$$ip:$(PORT) $(SERVER)
O bind em 127.0.0.1 é explícito de propósito. O ssh já escuta só em loopback por padrão, mas um GatewayPorts yes no ssh_config transformaria o túnel em escuta na rede local sem nenhum sinal.
Como forma de alcançar o MCP
Com o transporte HTTP do item 8 e expose: 8080, o servidor MCP é alcançável no IP do container, e o mesmo túnel entrega o endpoint ao Mac:
make port-forward SVC=prometheus-mcp PORT=8080
# cliente MCP aponta para http://127.0.0.1:8080/mcp
Esta é a forma preferida da fase 2, e por um motivo só: ela não publica nada. Sem hostname no Cloudflare, sem política de Access para configurar certo, sem service token no .mcp.json, sem socket novo na VPS e sem a exceção à regra 1 que o tailnet exigiria. A autenticação continua sendo a chave SSH, que é o controle que já existe, e o endpoint MCP só existe enquanto o túnel estiver aberto.
A forma indireta, e por que ela não substitui o item 1
Com o túnel do Prometheus aberto, dá para rodar o prometheus-mcp local no Mac apontando para http://127.0.0.1:9090, sem servidor nenhum na VPS. Funciona, e mesmo assim o item 1 continua sendo o default: a ferramenta MCP passaria a depender de um túnel que alguém precisa lembrar de levantar antes, e cairia em silêncio quando ele caísse. O stdio sobre SSH sobe junto com a sessão e não tem esse estado externo.
O que o port-forward não resolve
Grafana por 127.0.0.1:3000 entra como admin, porque GF_AUTH_ANONYMOUS_ORG_ROLE=Admin vale também pelo túnel. Não é brecha nova e não muda o modelo de acesso, mas o túnel pula o Cloudflare Zero Trust, que é o controle que existe no caminho normal. Vale lembrar antes de deixar um make port-forward SVC=grafana aberto numa máquina compartilhada.
10. Tailnet, o caminho que já existe e não está sendo usado
Medido em 16/10/2026:
| Dispositivo | Endereço | Versão |
|---|---|---|
| VPS de monitoração | 100.118.125.5, contabo.tail278232.ts.net | 1.102.2 |
| Mac | 100.99.67.84, macbook-air-de-yslan | 1.104.1 |
Os dois estão online e no mesmo tailnet. Não há serve configurado na VPS (tailscale serve status responde No serve config) e o tailnet não tem certificados HTTPS habilitados (CertDomains vazio), o que importa para uma das formas abaixo. O nó da Hostinger não está no tailnet, então nada disto alcança o cluster k3s.
10.1. O alias de SSH ainda vai pelo IP público
O ~/.ssh/config aponta contabo e monitoring-server para o IP público. Como o item 1 e o item 9 são os dois por SSH, todo o tráfego desta RFC atravessa a internet até a porta 22, que está aberta para o mundo pelo que o estado medido mostrou.
Trocar o HostName do alias por contabo.tail278232.ts.net move os dois caminhos para dentro do tailnet sem mudar uma linha do .mcp.json nem do Makefile, porque os dois falam com o alias e não com o endereço. E é isso que destrava fechar a 22 no firewall mais tarde, que é o item que falta para o requisito do título ser verdadeiro.
Com uma ordem que não pode ser invertida, porque o erro aqui é perda de acesso: primeiro trocar o alias e usar por alguns dias, depois restringir a 22, e só então considerar fechá-la. A Contabo tem console no painel, que é a saída se o tailscaled cair com a 22 já fechada. Enquanto o alias apontar para o IP público, nada disso é urgente, e enquanto a 22 estiver aberta, mover o alias é ganho sem risco.
10.2. Fase 2 pelo tailnet, três formas
| Forma | Socket no host | Fica no Git | Observação |
|---|---|---|---|
Sidecar tailscale no compose | nenhum | sim, no docker-compose.yml | O container entra no tailnet como dispositivo próprio e proxia para o prometheus-mcp pela rede interna. Exige TS_AUTHKEY no .env |
| Bind no IP do tailnet | um, em 100.118.125.5 | sim, no docker-compose.yml | ports: "100.118.125.5:8080:8080". Mais simples, e é exceção à regra 1 |
tailscale serve no host | nenhum publicado | não | Estado do daemon, fora do repositório. É drift pelo D5, e a forma com TLS exige habilitar certificados no tailnet, hoje desligados |
A primeira é a que respeita as duas regras ao mesmo tempo, a do requisito de exposição e a do repositório como única fonte. Custa um container e uma chave de autenticação no .env, que já guarda três segredos e é gitignored.
A segunda é a mais simples de escrever e cria um socket em escuta, ainda que numa interface não roteável pela internet. Não é exposição pública, mas é exceção à regra 1 como ela está escrita, então entra por revisão explícita desta RFC, com a regra reescrita para distinguir interface pública de interface de tailnet e com ss -ltnp conferindo que o bind é no 100.118.125.5 e não no 0.0.0.0. O que não acontece é a exceção entrar em silêncio no docker-compose.yml.
A terceira é a que parece mais limpa e é a única que some do Git. Fica registrada como recusada por isso.
10.3. O que o tailnet não resolve
Todos os dispositivos do tailnet são da mesma conta e, sem ACL escrita, qualquer um deles alcança qualquer serviço que for publicado ali. São oito dispositivos na lista, cinco deles offline há mais de dois meses. Um notebook perdido com Tailscale instalado é acesso ao que estiver no tailnet, e é por isso que a allowlist do item 3 continua valendo mesmo nas formas com autenticação na frente.
Custo de tudo isto: R$ 0. O plano gratuito do Tailscale cobre bem mais dispositivos do que os oito em uso, e os dois que importam já estão instalados.
Decisões abertas
- A fase 2 vale a superfície que cria? Ela só se paga se houver uso real fora do Mac, e só entra com as cinco condições do item 8 atendidas. Até lá, a decisão registrada é não subir.
--prometheus.truncation-limit=300é chute. Rever depois de duas semanas de uso, com o número de truncamentos observados.- Quando a
v0.19.0sair, migrar o pin para a imagem da organizaçãoprometheusou continuar emghcr.io/tjhop? - Se a fase 2 acontecer pela forma do item 9, o Cloudflare Access e o tailnet saem do documento ou ficam registrados como alternativas recusadas?
- Mover o
HostNamedo alias de SSH para o tailnet agora, como no item 10.1? É mudança de uma linha no~/.ssh/config, fora de qualquer repositório, e é pré-requisito de fechar a 22 depois.
Critério de aceitação
make mcp-checkimprime a lista de ferramentas da VPS e confirma, na saída, quequitereloadaparecem no conjunto default.- Uma sessão com o
.mcp.jsondo repo lista as ferramentas MCP doprometheussemquit, semreloade sem as três de TSDB Admin. - Uma consulta que hoje erra por causa do
+emform-urlencoded, comocount(count by (job) ({job!=""})), devolve o mesmo valor pelos dois caminhos. - O
CLAUDE.mddocontabo-monitoring-servertem a tabela do item 7, e a seção de consulta deixa de descrever owgetcomo caminho único. - Nada público novo, conferido de fora e não no arquivo de configuração. O
ss -ltnpna VPS devolve exatamente o mesmo conjunto de antes da RFC (22, 4318, os resolvers locais e otailscaled),docker compose pscontinua mostrando só a 4318 publicada, e uma tentativa de alcance do Mac ao IP público da VPS em qualquer porta que não a 22 e a 4318 falha. É o item 4 do requisito de exposição. - O
.mcp.jsonque entra em commit não contém segredo: o alias de SSH e uma URL da rede interna do Docker, e nada além disso. make port-forwardresolve o IP do container na hora, abre o túnel, ess -ltnpna VPS devolve o mesmo conjunto de antes com o túnel aberto. Medido em 16/10/2026: oito sockets antes e oito depois.
Riscos
| Risco | Mitigação |
|---|---|
Um LLM chama quit e derruba o Prometheus, que roda com --web.enable-lifecycle | Allowlist do item 3, sem quit e sem reload, verificada pelo critério 2 |
| Operação destrutiva de TSDB | As três ferramentas exigem --dangerous.enable-tsdb-admin-tools, que esta RFC não liga |
| Resposta grande consome contexto e esconde o que importa | --prometheus.truncation-limit=300, com o modelo podendo pedir mais por chamada |
Upgrade para v0.19.0 renomeia binário e prefixo de env, e as variáveis antigas passam a ser ignoradas sem erro | Pin explícito em v0.18.0 e uso de flags em vez de variáveis de ambiente |
docker pull na VPS passa a ser dependência de uma sessão de consulta | O make mcp-check puxa a imagem antes; ela fica em cache no disco, com 56 GB livres em 16/10/2026 |
| O nome da rede muda se o diretório remoto for renomeado | O nome está em um lugar só, o .mcp.json, e o make mcp-check falha na hora se divergir |
| O IP do container muda e o port-forward do item 9 aponta para o lugar errado, ou para outro container | O IP é resolvido por docker inspect a cada invocação do alvo, nunca escrito em arquivo, e o alvo falha quando o container não existe |
GatewayPorts yes no ssh_config transforma o port-forward em escuta na rede local do Mac | Bind explícito em 127.0.0.1 no alvo, que vence o default do cliente |
O alias de SSH é movido para o tailnet e a 22 é fechada no mesmo dia, e um tailscaled fora do ar deixa a VPS inacessível | Ordem fixada no item 10.1: alias primeiro, uso por alguns dias, restrição depois. O console da Contabo é a saída |
| Um dispositivo do tailnet é perdido ou comprometido e alcança o que for publicado ali | ACL no tailnet, e a allowlist do item 3, que limita o que a ferramenta faz mesmo para quem chega autenticado |
| A fase 2 sobe com o hostname publicado antes da política de Access, e o endpoint fica aberto na janela entre as duas | Condição 2 do item 8: política criada antes do primeiro deploy, e critério de aceitação 5 conferido de fora |
Alguém publica 8080:8080 para depurar a fase 2 e esquece | Recusado em texto na regra 5 do requisito de exposição. Depuração se faz de dentro da rede do Docker, com docker exec, como já se faz com o Prometheus |
A VPS não tem firewall, então qualquer ports: futuro, desta RFC ou de outra mudança, nasce público | Medido e registrado em Estado medido. A regra de DOCKER-USER é pré-requisito da fase 2, e o critério 5 pega o caso na fase 1 |
Custo
| Item | Custo recorrente |
|---|---|
| Imagem na VPS, em disco | R$ 0. Dezenas de MB, com 56 GB livres |
| Container efêmero durante a consulta | R$ 0. Sobe e morre com a sessão, em VPS com 7,9 GB de RAM e 6,0 GB livres em 16/10/2026 |
| Fase 2, container residente | R$ 0 na conta. Dezenas de MB de RAM na VPS que já está paga |
| Tráfego pelo túnel do Cloudflare, se a fase 2 entrar | R$ 0 no plano atual |
| Port-forward do item 9 | R$ 0. Uma conexão SSH, enquanto estiver aberta |
| Tailnet, nas formas do item 10 | R$ 0. Plano gratuito, com oito dispositivos de um limite muito maior, e os dois que importam já instalados |
Regra de DOCKER-USER para a 4318, do achado do estado medido | R$ 0 |
A RFC inteira não adiciona nada à conta. O que ela adiciona é superfície, e por isso a fase 1 foi desenhada para não criar nenhuma e a fase 2 fica separada, condicional e fora de decisão.
Fora do escopo
- Alertmanager. O servidor fala só com a API do Prometheus.
list_alertsdevolve os alertas na visão do Prometheus, não os grupos, silences e inibições do Alertmanager. Silence e roteamento continuam nodocker exec alertmanager wget. - Grafana. Dashboard continua sendo arquivo no repo, implantado por
make deploy-grafana. Omake port-forward SVC=grafanado item 9 serve para olhar, não para editar: alteração feita pela interface não volta para o Git. - O Prometheus do cluster k3s. Não existe: o k3s manda métrica por OTLP para esta VPS, e é este Prometheus que as guarda.
- Escrita de regra pelo agente. Regra se escreve no repositório e se valida com
make check-remote. O caminho MCP é de leitura.
Consequências
Medir passa a custar uma chamada de ferramenta em vez de um comando com três camadas de citação, e a classe de erro que fez sete labels presentes parecerem ausentes desaparece desse caminho. As duas RFCs em execução, a 0003 e a 0004, são as primeiras beneficiadas: as duas avançam por medição repetida sobre este mesmo Prometheus.
Em troca, a stack passa a ter uma entrada de configuração que não é o docker-compose.yml, e um pin de versão que precisa ser revisado quando a v0.19.0 sair. É pouco, e é visível no repositório, que é o que diferencia disto do drift que o D5 descreve.