fg-tracker-mcp 0.10.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +19 -7
  3. package/dist/cli.js +23 -8
  4. package/dist/cli.js.map +1 -1
  5. package/dist/client.d.ts +82 -5
  6. package/dist/client.js +182 -20
  7. package/dist/client.js.map +1 -1
  8. package/dist/confirm.d.ts +15 -0
  9. package/dist/confirm.js +32 -0
  10. package/dist/confirm.js.map +1 -0
  11. package/dist/diagnostics.d.ts +18 -0
  12. package/dist/diagnostics.js +467 -0
  13. package/dist/diagnostics.js.map +1 -0
  14. package/dist/format.d.ts +25 -2
  15. package/dist/format.js +35 -8
  16. package/dist/format.js.map +1 -1
  17. package/dist/guide.js +10 -6
  18. package/dist/guide.js.map +1 -1
  19. package/dist/infra/tools.js +68 -27
  20. package/dist/infra/tools.js.map +1 -1
  21. package/dist/inputs.d.ts +40 -0
  22. package/dist/inputs.js +30 -0
  23. package/dist/inputs.js.map +1 -0
  24. package/dist/journeys.d.ts +71 -0
  25. package/dist/journeys.js +130 -0
  26. package/dist/journeys.js.map +1 -0
  27. package/dist/links.d.ts +87 -0
  28. package/dist/links.js +124 -0
  29. package/dist/links.js.map +1 -0
  30. package/dist/loadtest/scenario.d.ts +4 -4
  31. package/dist/loadtest/tools.d.ts +1 -1
  32. package/dist/loadtest/tools.js +78 -27
  33. package/dist/loadtest/tools.js.map +1 -1
  34. package/dist/permissions.d.ts +85 -0
  35. package/dist/permissions.js +216 -0
  36. package/dist/permissions.js.map +1 -0
  37. package/dist/schemas.d.ts +680 -0
  38. package/dist/schemas.js +308 -0
  39. package/dist/schemas.js.map +1 -0
  40. package/dist/server.d.ts +20 -2
  41. package/dist/server.js +495 -119
  42. package/dist/server.js.map +1 -1
  43. package/dist/writes.d.ts +35 -0
  44. package/dist/writes.js +409 -0
  45. package/dist/writes.js.map +1 -0
  46. package/package.json +2 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog — fg-tracker-mcp
2
2
 
3
+ ## 0.12.0
4
+
5
+ Tools por permissão do token pessoal e escrita pelo MCP (PR B da proposta de tokens com permissões; a API e o painel vieram no PR #35). Requer a API com permissões por recurso (`credential.scopes` em `GET /v1/auth/me`, `API_KEY_PROJECT_MISMATCH`); nenhuma rota nova na API.
6
+
7
+ - **Tools por permissão.** Com token `fgt_…`, o MCP lê `credential.scopes` ao iniciar (até 5 s) e só registra as tools que o token cobre (mapa em `src/permissions.ts`); token restrito a projetos não registra `tracker_create_project`. Sessão registra todas. Se a leitura falhar (API fora do ar, token revogado, API anterior às permissões), só as de leitura são registradas: falha de introspecção nunca libera escrita. `FG_TRACKER_READ_ONLY=1` continua valendo por cima. A API segue decidindo cada chamada.
8
+ - **`tracker_status`**: diz qual credencial governa a escrita (sem valores), as permissões lidas no início × as de agora, as tools ocultas e o motivo, e pede para reiniciar o cliente MCP quando as permissões ou os projetos do token mudaram (a lista de tools só muda no reinício). `credential.scopes`, `permissions` e `writeCredential` no `structuredContent`.
9
+ - **Sem fallback para a chave.** `loadtest_create_scenario` e `infra_discover` deixam de repetir a escrita com `FG_TRACKER_API_KEY` quando o token é recusado (401, 403 e, no discover, 404): com `FG_TRACKER_TOKEN`, a recusa é final. A chave só vale sem token, só nessas duas tools e só no projeto dela (o `projectId` vai no corpo; 400 `API_KEY_PROJECT_MISMATCH` vira "a chave é de outro projeto, nada foi gravado"; uma API anterior que grave no projeto da chave é detectada pelo `projectId` da resposta do roteiro). Cada requisição leva uma credencial só: a chave não viaja mais nas chamadas do Bearer.
10
+ - **403 claros** pelo `code` da API: permissão que falta (`falta a permissão "Monitores: excluir" neste token`), operação só do painel, papel (`seu papel neste projeto é viewer`), projeto fora do token, token restrito criando projeto e falta de associação. Mensagens de erro mascaram `stk_…`/`fgt_…`.
11
+ - **Tools novas** (sem alias `track_*`):
12
+ - issues: `tracker_update_issue` (resolver, "resolver no próximo release", ignorar até data ou até escalar, reabrir, atribuir com o membro conferido antes, priorizar) e `tracker_list_members` (sem telefone);
13
+ - monitores: `tracker_create_monitor`, `tracker_update_monitor` (pausar, agenda, ambiente do monitor antigo antes do 1º check-in), `tracker_run_monitor`, `tracker_delete_monitor`;
14
+ - alertas: `tracker_list_alert_rules` (com a severidade mínima por canal), `tracker_create_alert_rule`, `tracker_update_alert_rule` (config mesclada com a gravada), `tracker_delete_alert_rule`, `tracker_ack_alert_event`, `tracker_update_alert_delivery`;
15
+ - projeto: `tracker_update_project` (limiares, repositório, regras de dono com `memberId` conferido — `addOwnershipRules`/`removeOwnershipRules` mesclam com a lista completa da API, `ownershipRules` substitui —, autoatribuição; `tracker_update_allowed_origins` continua) e `tracker_delete_project`;
16
+ - `tracker_delete_sourcemaps`; `loadtest_update_scenario`, `loadtest_delete_scenario`, `loadtest_stop_run`; `infra_update_finding`, `infra_delete_connection`.
17
+ - **Exclusões**: `destructiveHint: true` e `confirm` obrigatório com o nome ou slug do recurso (texto, nunca booleano), conferido contra o recurso lido na API antes do DELETE; a descrição manda perguntar ao usuário antes. O cliente HTTP recusa caminhos com segmento `.` ou `..` (o `fetch` os normalizaria para outra rota: `…/sourcemaps/..` viraria a exclusão do projeto), e `tracker_delete_sourcemaps` recusa essas releases e apaga também releases fora das 100 que a API lista.
18
+ - `infra_validate` passa a mostrar o id de cada achado aberto (texto e `findings`) e `infra_get_diagram` o de cada conexão (texto e `connections`), para `infra_update_finding` e `infra_delete_connection`.
19
+ - `tracker_list_alert_rules` respeita `FG_TRACKER_MAX_ITEMS` (`total`, `returned`, `truncated`).
20
+ - `tracker_list_environments` passa a declarar também `releases:read` (lê `/release-environments`).
21
+ - Documentação: tokens `fgt_` com as permissões necessárias, modelos do painel, precedência de credenciais, edição e reinício; a instalação recomendada deixa de pôr `FG_TRACKER_READ_ONLY=1` por padrão (continua como opção de só leitura).
22
+ - `tools/list` (sessão): 87 tools e ~168 KB no padrão, 66 e ~139 KB sem aliases; token Agente MCP sem aliases, 47 e ~108 KB; Somente leitura sem aliases, 34 e ~84 KB.
23
+ - Testes: registro por permissão, sessão, `FG_TRACKER_READ_ONLY`, introspecção que falha, suficiência de cada tool contra o mapa de rotas da API, permissão reduzida depois de iniciar, papel viewer, projetos restritos, exclusões com `confirm` errado e certo, nenhuma rota da chave com token recusado (401, 403, 404) e modo só com a chave; E2E com a API real em `apps/api/test/mcp-permissions.cases.cjs`.
24
+
25
+ ## 0.11.0
26
+
27
+ Diagnóstico sem painel (revisão de 15/10/2026, T3). Todas as ferramentas novas usam rotas de leitura com `@ReadScope()` (funcionam com token pessoal `fgt_…`). As de janela absoluta, ordenação e comparação dependem da API desta mesma entrega (CONTRATO §4 "Janelas absolutas" e "Comparação", §18); com uma API anterior elas avisam (`windowIgnored`, `filtersApplied: false`, "API ignorou sort") ou explicam o caminho alternativo, e as demais continuam funcionando.
28
+
29
+ - **Saída estruturada:** toda ferramenta de leitura declara `outputSchema` e devolve `structuredContent` validado contra ele; o texto continua o de antes (resumo + JSON). Onde o JSON era um array (`tracker_list_endpoints`, `tracker_list_releases`), o `structuredContent` é `{ items, … }`. Respostas com janela trazem `window: { from, to }`.
30
+ - **Evidência e links:** as ferramentas de análise devolvem `evidence[]` (`traceId`, `spanId?`, `metric`, `value`, `window`, `note?`) só com traces que a API devolveu — no detalhe do trace, a raiz, os buracos sem span, os spans de I/O mais lentos, os repetidos e os que falharam (com o serviço). `links.dashboard` e blocos `resource_link` para o painel (`FG_TRACKER_WEB_URL` ou derivado da API) e para os novos recursos `fg-tracker://trace/{traceId}` e `fg-tracker://issue/{issueId}` (`?projectId=` opcional).
31
+ - **Ferramentas novas** (sem alias `track_*`): `tracker_compare` (abaixo), `tracker_get_endpoint` (`/endpoints/detail`), `tracker_list_queries` (`/database`, com `env`/`service`, busca no SQL e um trace + span real da execução mais lenta de cada query), `tracker_log_stats` (`/logs/histogram` + `/logs/facets`), `tracker_list_alert_events` (`/alerts/events`, filtros por tipo, severidade, estado e janela; destinatários mascarados) e `tracker_list_environments` (`/release-environments` + ambientes e serviços do overview).
32
+ - `tracker_search_logs` aceita `from`/`to` ISO 8601 (até 31 dias, validado antes da chamada).
33
+ - **Janelas absolutas:** `tracker_get_overview`, `tracker_list_endpoints`, `tracker_get_endpoint`, `tracker_list_traces`, `tracker_list_queries` e `tracker_list_providers` aceitam `from`/`to` e conferem a janela que a API aplicou (cabeçalho `x-fg-tracker-window`).
34
+ - **`tracker_compare`** (`GET …/compare`): duas janelas ou duas releases — latência total e por rota, decomposição do tempo por execução por tipo de span e spans que mais cresceram, com `evidence`. Cada lado declara `coverage` de spans brutos (retenção); sem eles a latência vem dos rollups e `decomposition` é `null`.
35
+ - `tracker_list_traces`: `sort: "duration"`, `rootKind: "browser"` e `hasSpan: { kind?, service?, status?, name? }` (traces com algum span — inclusive filho ou de outro serviço — que case).
36
+ - `tracker_get_monitor`: `from`/`to`, horário esperado e atraso (`delayMs`) de cada execução de job e `schedule.overdueMs`.
37
+ - **M01:** `tracker_endpoint_history` não esconde mais o período recente: `points` é amostrado em 48 pontos com o primeiro e o último (antes, os 25 mais antigos), `markers` traz os 25 deploys mais recentes; aceita `service`/`env`. Todas as séries passam a incluir o último ponto na amostragem.
38
+ - **M02:** `tracker_verify_ingest` procura qualquer raiz (`rootKind=all`): projeto só de jobs ou de navegador não recebe mais "NENHUM trace".
39
+ - **M03:** dicas de erro por tipo de credencial: 401 com token pessoal (crie outro), 401 com sessão (prefira um token de leitura), 403 `TOKEN_SCOPE_INSUFFICIENT` (o token é somente leitura) distinto de 403 por falta de acesso ao projeto.
40
+ - **M04:** `apiKey` vazia (token de leitura) é tratada como ausente: o guia usa o placeholder e explica onde copiar a chave, e `tracker_list_projects` diz que a chave não foi devolvida.
41
+ - **M05:** `infra_plan_handover` deixa de se declarar somente leitura (faz POST numa rota de sessão).
42
+ - **Segurança:** `FG_TRACKER_READ_ONLY=1` registra só as ferramentas com `readOnlyHint: true`; `tracker_status` mostra o tipo de credencial (token pessoal ou sessão), a validade, o modo somente leitura e avisa quando é sessão. A documentação recomenda `fgt_` + `FG_TRACKER_READ_ONLY=1`.
43
+ - **Prompts das cinco jornadas:** `investigar-regressao-p95`, `investigar-query-lenta`, `investigar-job-atrasado`, `investigar-erro-distribuido` (API → serviço) e `confirmar-correcao`, com a sequência de ferramentas (≤ 6 chamadas), o critério de parada e a regra de citar a evidência.
44
+ - Aliases `track_*`: continuam para os 21 nomes da 0.10, com o mesmo resultado (inclusive `structuredContent`), mas sem repetir o `outputSchema` no `tools/list`.
45
+ - `tools/list`: 67 ferramentas (~130 KB) no padrão; 32 (~79 KB) com `FG_TRACKER_READ_ONLY=1` e `FG_TRACKER_LEGACY_TOOL_NAMES=0` (recomendado).
46
+ - Testes pelo transporte MCP (cliente em memória, sem `_registeredTools`); `npm test` compila antes. E2E das jornadas contra a API real (`apps/api/test/mcp-journeys.cases.cjs`).
47
+
3
48
  ## 0.10.0
4
49
 
5
50
  Reúne o que entrou na `main` desde a 0.9.1 (PRs #11, #12, #13, #15) e a renomeação para FG Tracker. Requer a API com filtros de issues por cursor (PR #12), raízes `custom` (PR #15) e ambientes de monitores e releases (PR #14); com uma API anterior, os parâmetros novos são ignorados ou geram aviso, e as ferramentas antigas continuam funcionando.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # fg-tracker-mcp
2
2
 
3
- Servidor [MCP](https://modelcontextprotocol.io) (stdio) do FG Tracker para agentes de código (Claude Code, Cursor, Windsurf, Claude Desktop): projetos, métricas, traces, issues com commits suspeitos, releases, monitores, provedores externos, logs e guia de instrumentação. Documentação completa: `docs/mcp.md` no repositório.
3
+ Servidor [MCP](https://modelcontextprotocol.io) (stdio) do FG Tracker para agentes de código (Claude Code, Cursor, Windsurf, Claude Desktop): projetos, métricas, traces, issues com commits suspeitos, releases, monitores, provedores externos, logs, slow queries, eventos de alerta e guia de instrumentação, com respostas estruturadas (`structuredContent`) e evidência verificável para diagnosticar sem o painel — e, com as permissões do token, triagem de issues, monitores, regras de alerta e configurações do projeto. Documentação completa: `docs/mcp.md` no repositório.
4
4
 
5
5
  ## Instalação
6
6
 
@@ -9,7 +9,8 @@ Claude Code:
9
9
  ```bash
10
10
  claude mcp add fg-tracker \
11
11
  -e FG_TRACKER_URL=http://localhost:4050 \
12
- -e FG_TRACKER_TOKEN=fgt_<seu_token_de_leitura> \
12
+ -e FG_TRACKER_TOKEN=fgt_<seu_token> \
13
+ -e FG_TRACKER_LEGACY_TOOL_NAMES=0 \
13
14
  -- npx -y fg-tracker-mcp
14
15
  ```
15
16
 
@@ -23,14 +24,15 @@ Cursor / Windsurf / Claude Desktop (JSON):
23
24
  "args": ["-y", "fg-tracker-mcp"],
24
25
  "env": {
25
26
  "FG_TRACKER_URL": "http://localhost:4050",
26
- "FG_TRACKER_TOKEN": "fgt_<seu_token_de_leitura>"
27
+ "FG_TRACKER_TOKEN": "fgt_<seu_token>",
28
+ "FG_TRACKER_LEGACY_TOOL_NAMES": "0"
27
29
  }
28
30
  }
29
31
  }
30
32
  }
31
33
  ```
32
34
 
33
- Gere o token em **Conectar → Usar MCP → Gerar token de leitura para o MCP** (ou em **Conta → Tokens de acesso**). Ele só lê; para o agente criar projetos, registrar releases, mexer em infraestrutura ou iniciar testes de carga, use a configuração com a sessão. Detalhes em `docs/mcp.md` → Token de leitura.
35
+ Gere o token em **Conectar → Usar MCP → Gerar token para o MCP** (ou em **Conta → Tokens de acesso**) e marque só o que o agente precisa: o modelo **Agente MCP** lê tudo e cria e edita projetos, issues, releases, monitores e alertas, sem excluir; **Somente leitura** serve para diagnóstico; exclusões, infraestrutura AWS e testes de carga entram pela matriz (ou **Acesso total**). O MCP lê as permissões do token ao iniciar (`GET /v1/auth/me`) e só registra as tools que elas cobrem; a API confere cada chamada, dentro do seu papel em cada projeto. **Editou as permissões? Reinicie o cliente MCP** para a lista de tools mudar (`tracker_status` avisa). Para nunca expor escrita, acrescente `FG_TRACKER_READ_ONLY=1`. Detalhes em `docs/mcp.md` → Token pessoal.
34
36
 
35
37
  Em produção, `FG_TRACKER_URL` é a URL da API mostrada no painel em **Conectar**. O endereço de produção da API é `https://tracker-api.fortground.com.br` (o antigo `track-api.fortground.com.br` continua respondendo).
36
38
 
@@ -39,15 +41,22 @@ Em produção, `FG_TRACKER_URL` é a URL da API mostrada no painel em **Conectar
39
41
  | variável | default | uso |
40
42
  |---|---|---|
41
43
  | `FG_TRACKER_URL` | `http://localhost:4050` | URL base da API |
42
- | `FG_TRACKER_TOKEN` | — | `Authorization: Bearer`: token pessoal de leitura `fgt_…` (painel → Conta → Tokens de acesso; não cai com logout) ou token de sessão, necessário só para as tools que escrevem — projetos, métricas, traces, issues, logs |
43
- | `FG_TRACKER_API_KEY` | — | chave de projeto (`x-fg-tracker-key`) — opcional, só ingestão |
44
+ | `FG_TRACKER_TOKEN` | — | `Authorization: Bearer`: token pessoal `fgt_…` com as permissões necessárias (painel → Conta → Tokens de acesso; não cai com logout) ou token de sessão — projetos, métricas, traces, issues, logs e toda escrita |
45
+ | `FG_TRACKER_API_KEY` | — | chave de projeto (`x-fg-tracker-key`) — opcional; só sem `FG_TRACKER_TOKEN`, `loadtest_create_scenario` e `infra_discover` gravam por ela, no projeto dela. Com token, nunca é usada |
44
46
  | `FG_TRACKER_TIMEOUT_MS` | `15000` | timeout das chamadas HTTP |
45
47
  | `FG_TRACKER_MAX_ITEMS` | `200` | teto de itens da lista principal de cada tool (1 a 1000) |
48
+ | `FG_TRACKER_READ_ONLY` | desligado | `1`: só as tools de leitura (`readOnlyHint: true`) são registradas, mesmo com um token que escreve |
49
+ | `FG_TRACKER_WEB_URL` | derivada da API | URL do painel nos links das respostas (`tracker-api.*` → `tracker.*`) |
50
+ | `FG_TRACKER_LEGACY_TOOL_NAMES` | ligado | `0` desliga os aliases obsoletos `track_*` |
46
51
 
47
52
  Os nomes antigos `STREAD_TRACK_*` continuam lidos quando o nome novo está ausente.
48
53
 
49
54
  ## Tools, prompt e resource
50
55
 
56
+ Escrita (0.12): `tracker_update_issue` e `tracker_list_members` (triagem: resolver, "resolver no próximo release", ignorar, atribuir, priorizar), `tracker_create_monitor`, `tracker_update_monitor`, `tracker_run_monitor`, `tracker_delete_monitor`, `tracker_list_alert_rules`, `tracker_create_alert_rule`, `tracker_update_alert_rule`, `tracker_delete_alert_rule`, `tracker_ack_alert_event`, `tracker_update_alert_delivery`, `tracker_update_project`, `tracker_delete_project`, `tracker_delete_sourcemaps`, `loadtest_update_scenario`, `loadtest_delete_scenario`, `loadtest_stop_run`, `infra_update_finding` e `infra_delete_connection`. Toda exclusão tem `destructiveHint: true` e exige `confirm` com o nome ou slug do recurso, conferido antes do DELETE. Com `FG_TRACKER_TOKEN`, a recusa do token é final (sem nova tentativa pela chave `stk_`). Detalhes em `docs/mcp.md` → Escrita e exclusões.
57
+
58
+ Diagnóstico (0.11): toda tool de leitura tem `outputSchema` e devolve `structuredContent` (o texto continua igual); as de análise trazem `evidence[]` (`traceId`, `spanId?`, métrica, valor e janela) apontando para traces existentes, mais links para o painel e para os recursos `fg-tracker://trace/{traceId}` e `fg-tracker://issue/{issueId}`. Tools novas: `tracker_compare` (duas janelas ou releases: onde o tempo cresceu, com cobertura de spans brutos), `tracker_get_endpoint` (detalhe e traces mais lentos de uma rota), `tracker_list_queries` (slow queries com o trace e o span da execução mais lenta), `tracker_log_stats` (histograma e facetas de logs), `tracker_list_alert_events` (histórico de alertas, destinatários mascarados) e `tracker_list_environments`; métricas, traces e logs aceitam `from`/`to` ISO; `tracker_list_traces` ordena por duração e filtra por span filho (`hasSpan`); `tracker_get_monitor` mostra o atraso de cada execução. Prompts das jornadas: `investigar-regressao-p95`, `investigar-query-lenta`, `investigar-job-atrasado`, `investigar-erro-distribuido` e `confirmar-correcao`. Detalhes em `docs/mcp.md` → Saída estruturada e Jornadas de diagnóstico.
59
+
51
60
  Tools `tracker_*` (até a 0.9, `track_*`), prompt `instrument-project` e resource `fg-tracker://docs/quickstart`. Os nomes `track_*` continuam registrados como aliases obsoletos (a versão seguinte os desliga por padrão e a 1.0 os remove); `FG_TRACKER_LEGACY_TOOL_NAMES=0` deixa só os nomes novos.
52
61
 
53
62
  Infraestrutura AWS (0.6.0): tools `infra_*` e prompt `connect-infrastructure` — detectar contas/ambientes/regiões nas credenciais locais, conectar cada ambiente, descobrir recursos com credenciais locais (somente leitura), analisar o IaC, instrumentar, validar e gerar o PR de transferência de controle. Nenhuma tool altera recursos na AWS. Detalhes em `docs/mcp.md` → Infraestrutura.
@@ -61,11 +70,14 @@ Simulação de ambiente (0.8.0): prompt `create-load-test` e tools `loadtest_*`
61
70
  | tool | entrada | o que faz |
62
71
  |---|---|---|
63
72
  | `loadtest_scan_routes` | `{ path?, format? }` | local, somente leitura: rotas NestJS/Express/Next.js (método, caminho, arquivo:linha, controller, DTO, protegida?) e pistas de autenticação |
64
- | `loadtest_create_scenario` | `{ projectId, scenario, description? }` | valida o roteiro (§28.2, erros com caminho do campo) e grava (`POST /v1/projects/:id/loadtest/scenarios`, ou `POST /v1/loadtest/scenarios` só com `FG_TRACKER_API_KEY`); resumo com passos, variáveis extraídas, checks e avisos de efeitos externos |
73
+ | `loadtest_create_scenario` | `{ projectId, scenario, description? }` | valida o roteiro (§28.2, erros com caminho do campo) e grava (`POST /v1/projects/:id/loadtest/scenarios` com o token, cuja recusa é final; `POST /v1/loadtest/scenarios` só quando não há `FG_TRACKER_TOKEN`, no projeto da chave); resumo com passos, variáveis extraídas, checks e avisos de efeitos externos |
74
+ | `loadtest_update_scenario` | `{ projectId, scenarioId, name?, description?, scenario? }` | atualiza o roteiro (documento validado antes do `PUT`) |
75
+ | `loadtest_delete_scenario` | `{ projectId, scenarioId, confirm }` | exclui o roteiro; `confirm` = nome |
65
76
  | `loadtest_list_scenarios` | `{ projectId }` | roteiros do projeto |
66
77
  | `loadtest_suggest_plan` | `{ preset, vus?, targetRps?, duration? }` | puro: plano k6 (§28.3) para smoke/load/stress/spike/soak/breakpoint |
67
78
  | `loadtest_get_run` | `{ projectId, runId, generateAdvice? }` | resumo do run: latências, erros, checks, thresholds, por passo e sugestões da IA |
68
79
  | `loadtest_start_run` | `{ projectId, scenarioId, baseUrl, connectionId, plan, credentialId?, environment?, confirm }` | gera carga real: recusa sem `confirm: true` e devolve o texto de confirmação (ambiente, baseUrl, pico de VUs/RPS, duração); prefira iniciar pela UI |
80
+ | `loadtest_stop_run` | `{ projectId, runId }` | para a simulação (na fila, aborta; rodando, os executores param no próximo relatório) |
69
81
 
70
82
  ## Migração de stread-track-mcp
71
83
 
package/dist/cli.js CHANGED
@@ -1,12 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
3
  * Entrada do binário `fg-tracker-mcp` (alias obsoleto: `stread-track-mcp`): sobe o servidor MCP em stdio.
4
- * Env: FG_TRACKER_URL (default http://localhost:4050), FG_TRACKER_TOKEN, FG_TRACKER_API_KEY,
5
- * FG_TRACKER_TIMEOUT_MS — os nomes legados STREAD_TRACK_* continuam valendo como fallback.
4
+ * Env: FG_TRACKER_URL (default http://localhost:4050), FG_TRACKER_TOKEN (fgt_… com permissões, ou sessão), FG_TRACKER_API_KEY,
5
+ * FG_TRACKER_TIMEOUT_MS, FG_TRACKER_READ_ONLY, FG_TRACKER_WEB_URL — os nomes legados STREAD_TRACK_*
6
+ * continuam valendo como fallback.
6
7
  */
7
8
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
8
9
  import { configFromEnv, TrackClient } from './client.js';
9
- import { createServer, SERVER_NAME, SERVER_VERSION } from './server.js';
10
+ import { resolveToolAccess } from './permissions.js';
11
+ import { createServer, readOnlyFromEnv, SERVER_NAME, SERVER_VERSION } from './server.js';
10
12
  const argv = process.argv.slice(2);
11
13
  if (argv.includes('--help') || argv.includes('-h')) {
12
14
  process.stdout.write([
@@ -17,14 +19,18 @@ if (argv.includes('--help') || argv.includes('-h')) {
17
19
  '',
18
20
  'Variáveis de ambiente:',
19
21
  ' FG_TRACKER_URL URL da API (default http://localhost:4050)',
20
- ' FG_TRACKER_TOKEN token de sessão (Authorization: Bearer) — projetos, métricas, logs',
21
- ' FG_TRACKER_API_KEY chave de projeto (x-fg-tracker-key) — opcional, só ingestão',
22
+ ' FG_TRACKER_TOKEN token pessoal fgt_… (painel → Conta → Tokens de acesso) com as permissões necessárias, ou sessão (Authorization: Bearer).',
23
+ ' Com token pessoal, só as ferramentas que as permissões dele cobrem são registradas (lidas ao iniciar; reinicie ao editar o token)',
24
+ ' FG_TRACKER_API_KEY chave de projeto (x-fg-tracker-key) — opcional; sem FG_TRACKER_TOKEN, grava roteiros e snapshots só no projeto dela',
22
25
  ' FG_TRACKER_TIMEOUT_MS timeout das chamadas HTTP (default 15000)',
26
+ ' FG_TRACKER_READ_ONLY=1 só registra ferramentas de leitura (esconde toda ferramenta que cria, edita ou exclui), mesmo com permissões de escrita',
27
+ ' FG_TRACKER_WEB_URL URL do painel para os links (default: derivada de FG_TRACKER_URL, ex. tracker-api.* → tracker.*)',
23
28
  ' FG_TRACKER_LEGACY_TOOL_NAMES=0 não registra os aliases obsoletos track_* das ferramentas tracker_*',
24
29
  ' (os nomes antigos STREAD_TRACK_* ainda são lidos como fallback; serão removidos na 1.0)',
25
30
  '',
26
31
  'Instalação no Claude Code:',
27
- ' claude mcp add fg-tracker -e FG_TRACKER_URL=http://localhost:4050 -e FG_TRACKER_TOKEN=... -- npx -y fg-tracker-mcp',
32
+ ' claude mcp add fg-tracker -e FG_TRACKER_URL=http://localhost:4050 -e FG_TRACKER_TOKEN=fgt_... -- npx -y fg-tracker-mcp',
33
+ ' (só diagnóstico, sem nenhuma escrita: acrescente -e FG_TRACKER_READ_ONLY=1)',
28
34
  '',
29
35
  ].join('\n'));
30
36
  process.exit(0);
@@ -35,13 +41,22 @@ if (argv.includes('--version') || argv.includes('-v')) {
35
41
  }
36
42
  const config = configFromEnv();
37
43
  const client = new TrackClient(config);
38
- const server = createServer({ client });
44
+ // Permissões do token antes de registrar as ferramentas (GET /v1/auth/me, até 5 s). Falha =
45
+ // só as de leitura; a API continua decidindo cada chamada.
46
+ const access = await resolveToolAccess(client);
47
+ const server = createServer({ client, access });
39
48
  // stdout é o canal JSON-RPC: qualquer diagnóstico vai para stderr.
40
49
  const invoked = process.argv[1]?.split(/[\\/]/).pop() ?? '';
41
50
  if (invoked.startsWith('stread-track-')) {
42
51
  console.error(`[fg-tracker-mcp] "${invoked}" está obsoleto: use "fg-tracker-mcp" (o alias será removido na 1.0).`);
43
52
  }
44
- console.error(`[fg-tracker-mcp] API ${config.baseUrl} · token ${config.token ? 'ok' : 'ausente'} · apiKey ${config.apiKey ? 'ok' : 'ausente'}`);
53
+ const credential = {
54
+ token: `pessoal (${access.scopes?.length ?? 0} permissões)`,
55
+ 'token-unverified': `pessoal (permissões não lidas: só leitura${access.error ? `; ${access.error}` : ''})`,
56
+ session: 'de sessão (faz tudo o que você faz no painel; prefira um fgt_ com só o necessário)',
57
+ none: 'ausente',
58
+ }[access.mode];
59
+ console.error(`[fg-tracker-mcp] API ${config.baseUrl} · token ${credential} · apiKey ${config.apiKey ? (client.hasToken ? 'definida (não usada: o token decide)' : 'ok') : 'ausente'}${readOnlyFromEnv() ? ' · somente leitura' : ''}`);
45
60
  const transport = new StdioServerTransport();
46
61
  await server.connect(transport);
47
62
  const shutdown = () => {
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;GAIG;AACH,OAAO,EAAE,oBAAoB,EAAE,MAAM,2CAA2C,CAAC;AAEjF,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAExE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;AACnC,IAAI,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IACnD,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB;QACE,GAAG,WAAW,QAAQ,cAAc,uCAAuC;QAC3E,EAAE;QACF,uFAAuF;QACvF,+BAA+B;QAC/B,EAAE;QACF,wBAAwB;QACxB,qEAAqE;QACrE,6FAA6F;QAC7F,sFAAsF;QACtF,oEAAoE;QACpE,uGAAuG;QACvG,2FAA2F;QAC3F,EAAE;QACF,4BAA4B;QAC5B,sHAAsH;QACtH,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CACb,CAAC;IACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AACD,IAAI,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;IACtD,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,cAAc,IAAI,CAAC,CAAC;IAC5C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,MAAM,MAAM,GAAG,aAAa,EAAE,CAAC;AAC/B,MAAM,MAAM,GAAG,IAAI,WAAW,CAAC,MAAM,CAAC,CAAC;AACvC,MAAM,MAAM,GAAG,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;AAExC,mEAAmE;AACnE,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,IAAI,EAAE,CAAC;AAC5D,IAAI,OAAO,CAAC,UAAU,CAAC,eAAe,CAAC,EAAE,CAAC;IACxC,OAAO,CAAC,KAAK,CAAC,qBAAqB,OAAO,uEAAuE,CAAC,CAAC;AACrH,CAAC;AACD,OAAO,CAAC,KAAK,CAAC,wBAAwB,MAAM,CAAC,OAAO,YAAY,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,aAAa,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC;AAEhJ,MAAM,SAAS,GAAG,IAAI,oBAAoB,EAAE,CAAC;AAC7C,MAAM,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;AAEhC,MAAM,QAAQ,GAAG,GAAS,EAAE;IAC1B,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACrD,CAAC,CAAC;AACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;AAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
package/dist/client.d.ts CHANGED
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * Cliente HTTP mínimo para a API do FG Tracker (docs/CONTRATO.md §4, §8, §11).
3
- * Lê `FG_TRACKER_URL`, `FG_TRACKER_TOKEN` (Bearer de sessão), `FG_TRACKER_API_KEY`
4
- * (chave de projeto, header `x-fg-tracker-key`) e `FG_TRACKER_TIMEOUT_MS` — com
3
+ * Lê `FG_TRACKER_URL`, `FG_TRACKER_TOKEN` (Bearer: token pessoal `fgt_…` com as permissões
4
+ * marcadas no painel, ou token de sessão), `FG_TRACKER_API_KEY` (chave de projeto, header
5
+ * `x-fg-tracker-key`), `FG_TRACKER_TIMEOUT_MS` e `FG_TRACKER_WEB_URL` — com
5
6
  * fallback para os nomes legados `STREAD_TRACK_*` (`stread-track-mcp` ≤ 0.3).
7
+ *
8
+ * Cada requisição leva uma credencial só: o Bearer nas rotas de usuário (o padrão) e a chave
9
+ * apenas nas rotas da chave (`auth: 'key'`), que as ferramentas só usam sem FG_TRACKER_TOKEN.
6
10
  */
7
11
  /** API local do monorepo. Em produção a API é `https://tracker-api.fortground.com.br`. */
8
12
  export declare const DEFAULT_URL = "http://localhost:4050";
@@ -18,33 +22,106 @@ export interface TrackClientConfig {
18
22
  token?: string;
19
23
  apiKey?: string;
20
24
  timeoutMs?: number;
25
+ /** URL do painel, para links (`FG_TRACKER_WEB_URL`; default derivado de `baseUrl`). */
26
+ webUrl?: string;
21
27
  }
22
28
  export type QueryValue = string | number | boolean | undefined | null;
29
+ /** Campos de uma recusa 403 da API (CONTRATO.md §8.1, "Erros"). */
30
+ export interface ApiErrorDetails {
31
+ requiredScope?: string | null;
32
+ requiredPermission?: string | null;
33
+ sessionOnly?: boolean;
34
+ role?: string;
35
+ requiredRoles?: string[];
36
+ }
23
37
  export declare class TrackApiError extends Error {
24
38
  readonly status: number | undefined;
25
39
  readonly path: string;
26
40
  readonly hint?: string | undefined;
27
- constructor(message: string, status: number | undefined, path: string, hint?: string | undefined);
41
+ /** `code` do corpo de erro da API (ex.: TOKEN_SCOPE_INSUFFICIENT), quando houver. */
42
+ readonly code?: string | undefined;
43
+ readonly details: ApiErrorDetails;
44
+ constructor(message: string, status: number | undefined, path: string, hint?: string | undefined,
45
+ /** `code` do corpo de erro da API (ex.: TOKEN_SCOPE_INSUFFICIENT), quando houver. */
46
+ code?: string | undefined, details?: ApiErrorDetails);
28
47
  /** Mensagem amigável pronta para devolver ao agente. */
29
48
  describe(): string;
30
49
  }
31
50
  export declare function configFromEnv(env?: NodeJS.ProcessEnv): TrackClientConfig;
51
+ /**
52
+ * Painel que acompanha a API: `tracker-api.<domínio>` → `tracker.<domínio>` (o alias antigo
53
+ * `track-api` também), `localhost:4050` → `localhost:3050`. Sem regra conhecida, `null`:
54
+ * as ferramentas deixam de mandar links do painel, nunca inventam um.
55
+ */
56
+ export declare function webUrlFor(baseUrl: string): string | null;
57
+ /** Cabeçalho com a janela absoluta (`from`/`to`) que a API aplicou (CONTRATO.md §4). */
58
+ export declare const WINDOW_HEADER = "x-fg-tracker-window";
59
+ /** Cabeçalho com os parâmetros extras que a rota aplicou (ex.: `traces-sort,traces-has-span`). */
60
+ export declare const CAPABILITIES_HEADER = "x-fg-tracker-capabilities";
61
+ /** Prefixo dos tokens pessoais (CONTRATO.md §8.1). */
62
+ export declare const ACCESS_TOKEN_PREFIX = "fgt_";
63
+ /**
64
+ * Esconde valores de credencial num texto que vai para o agente: chave de projeto `stk_…` e
65
+ * token pessoal `fgt_…`. As mensagens de erro da API não ecoam segredos; isto é a segunda linha.
66
+ */
67
+ export declare function redactSecrets(value: string): string;
68
+ /** Tipo da credencial de `FG_TRACKER_TOKEN`, pelo prefixo (sem chamar a API). */
69
+ export type CredentialKind = 'access_token' | 'session' | 'none';
70
+ /**
71
+ * Segmento "." ou ".." (também `%2e`) num caminho: o `fetch` (WHATWG URL) o normaliza e a
72
+ * requisição vai para outra rota — `DELETE …/sourcemaps/..` viraria `DELETE /v1/projects/:id`.
73
+ */
74
+ export declare function hasDotSegment(path: string): boolean;
75
+ /** Credencial de uma requisição: Bearer (rotas de usuário) ou a chave de projeto (rotas da chave). */
76
+ export type RequestAuth = 'bearer' | 'key';
32
77
  export declare class TrackClient {
33
78
  readonly config: TrackClientConfig;
34
79
  constructor(config: TrackClientConfig);
35
80
  get baseUrl(): string;
36
81
  get hasToken(): boolean;
82
+ /** `FG_TRACKER_TOKEN` é um token pessoal `fgt_…` (não uma sessão). */
83
+ get hasAccessToken(): boolean;
84
+ get credentialKind(): CredentialKind;
85
+ /** Painel para links (`FG_TRACKER_WEB_URL` ou derivado da API); `null` se desconhecido. */
86
+ get webUrl(): string | null;
37
87
  get hasApiKey(): boolean;
38
- /** Headers de autenticação: Bearer (sessão) e/ou chave de projeto. */
88
+ /**
89
+ * Headers de autenticação de uma requisição: o Bearer (token pessoal ou sessão) nas rotas de
90
+ * usuário, ou só a chave de projeto nas rotas da chave. Nunca os dois: a chave não viaja nas
91
+ * chamadas que o Bearer decide.
92
+ */
39
93
  private authHeaders;
40
- request<T = unknown>(method: 'GET' | 'POST' | 'PATCH' | 'DELETE', path: string, options?: {
94
+ request<T = unknown>(method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE', path: string, options?: {
41
95
  query?: Record<string, QueryValue>;
42
96
  body?: unknown;
43
97
  headers?: Record<string, string>;
98
+ onResponse?: (res: Response) => void;
99
+ auth?: RequestAuth;
100
+ timeoutMs?: number;
44
101
  }): Promise<T>;
102
+ private unauthorizedHint;
103
+ /** Dica de uma recusa 403 conforme o `code` e a permissão exigida que a API devolve. */
104
+ private forbiddenHint;
45
105
  get<T = unknown>(path: string, query?: Record<string, QueryValue>): Promise<T>;
106
+ /**
107
+ * GET de uma leitura com janela: devolve também a janela absoluta que a API confirmou no
108
+ * cabeçalho `x-fg-tracker-window` (`null` = a API usou `range`, ou é anterior a from/to e os
109
+ * ignorou).
110
+ */
111
+ getWindowed<T = unknown>(path: string, query?: Record<string, QueryValue>): Promise<{
112
+ body: T;
113
+ window: {
114
+ from: string;
115
+ to: string;
116
+ } | null;
117
+ capabilities: Set<string>;
118
+ }>;
46
119
  post<T = unknown>(path: string, body?: unknown): Promise<T>;
47
120
  patch<T = unknown>(path: string, body?: unknown): Promise<T>;
121
+ put<T = unknown>(path: string, body?: unknown): Promise<T>;
122
+ delete<T = unknown>(path: string): Promise<T>;
123
+ /** POST numa rota da chave de projeto (`x-fg-tracker-key`), sem o Bearer. */
124
+ postWithKey<T = unknown>(path: string, body?: unknown): Promise<T>;
48
125
  }
49
126
  export type RangeKey = '15m' | '1h' | '6h' | '24h' | '7d' | '30d';
50
127
  export declare const RANGE_KEYS: readonly RangeKey[];
package/dist/client.js CHANGED
@@ -1,8 +1,12 @@
1
1
  /**
2
2
  * Cliente HTTP mínimo para a API do FG Tracker (docs/CONTRATO.md §4, §8, §11).
3
- * Lê `FG_TRACKER_URL`, `FG_TRACKER_TOKEN` (Bearer de sessão), `FG_TRACKER_API_KEY`
4
- * (chave de projeto, header `x-fg-tracker-key`) e `FG_TRACKER_TIMEOUT_MS` — com
3
+ * Lê `FG_TRACKER_URL`, `FG_TRACKER_TOKEN` (Bearer: token pessoal `fgt_…` com as permissões
4
+ * marcadas no painel, ou token de sessão), `FG_TRACKER_API_KEY` (chave de projeto, header
5
+ * `x-fg-tracker-key`), `FG_TRACKER_TIMEOUT_MS` e `FG_TRACKER_WEB_URL` — com
5
6
  * fallback para os nomes legados `STREAD_TRACK_*` (`stread-track-mcp` ≤ 0.3).
7
+ *
8
+ * Cada requisição leva uma credencial só: o Bearer nas rotas de usuário (o padrão) e a chave
9
+ * apenas nas rotas da chave (`auth: 'key'`), que as ferramentas só usam sem FG_TRACKER_TOKEN.
6
10
  */
7
11
  /** API local do monorepo. Em produção a API é `https://tracker-api.fortground.com.br`. */
8
12
  export const DEFAULT_URL = 'http://localhost:4050';
@@ -19,11 +23,17 @@ export class TrackApiError extends Error {
19
23
  status;
20
24
  path;
21
25
  hint;
22
- constructor(message, status, path, hint) {
26
+ code;
27
+ details;
28
+ constructor(message, status, path, hint,
29
+ /** `code` do corpo de erro da API (ex.: TOKEN_SCOPE_INSUFFICIENT), quando houver. */
30
+ code, details = {}) {
23
31
  super(message);
24
32
  this.status = status;
25
33
  this.path = path;
26
34
  this.hint = hint;
35
+ this.code = code;
36
+ this.details = details;
27
37
  this.name = 'TrackApiError';
28
38
  }
29
39
  /** Mensagem amigável pronta para devolver ao agente. */
@@ -37,7 +47,27 @@ export function configFromEnv(env = process.env) {
37
47
  const apiKey = readEnv('API_KEY', env);
38
48
  const timeoutRaw = Number(readEnv('TIMEOUT_MS', env));
39
49
  const timeoutMs = Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? timeoutRaw : 15_000;
40
- return { baseUrl, token, apiKey, timeoutMs };
50
+ const webUrl = readEnv('WEB_URL', env)?.replace(/\/+$/, '');
51
+ return { baseUrl, token, apiKey, timeoutMs, ...(webUrl ? { webUrl } : {}) };
52
+ }
53
+ /**
54
+ * Painel que acompanha a API: `tracker-api.<domínio>` → `tracker.<domínio>` (o alias antigo
55
+ * `track-api` também), `localhost:4050` → `localhost:3050`. Sem regra conhecida, `null`:
56
+ * as ferramentas deixam de mandar links do painel, nunca inventam um.
57
+ */
58
+ export function webUrlFor(baseUrl) {
59
+ let url;
60
+ try {
61
+ url = new URL(baseUrl);
62
+ }
63
+ catch {
64
+ return null;
65
+ }
66
+ if (/^track(?:er)?-api\./.test(url.hostname))
67
+ return `${url.protocol}//${url.hostname.replace(/^track(?:er)?-api\./, 'tracker.')}`;
68
+ if ((url.hostname === 'localhost' || url.hostname === '127.0.0.1') && url.port === '4050')
69
+ return `${url.protocol}//${url.hostname}:3050`;
70
+ return null;
41
71
  }
42
72
  function buildQuery(query) {
43
73
  if (!query)
@@ -59,6 +89,47 @@ function isConnectionError(err) {
59
89
  const msg = err instanceof Error ? err.message : String(err);
60
90
  return /fetch failed|ECONNREFUSED|network/i.test(msg);
61
91
  }
92
+ /** Cabeçalho com a janela absoluta (`from`/`to`) que a API aplicou (CONTRATO.md §4). */
93
+ export const WINDOW_HEADER = 'x-fg-tracker-window';
94
+ /** Cabeçalho com os parâmetros extras que a rota aplicou (ex.: `traces-sort,traces-has-span`). */
95
+ export const CAPABILITIES_HEADER = 'x-fg-tracker-capabilities';
96
+ /** Prefixo dos tokens pessoais (CONTRATO.md §8.1). */
97
+ export const ACCESS_TOKEN_PREFIX = 'fgt_';
98
+ /**
99
+ * Esconde valores de credencial num texto que vai para o agente: chave de projeto `stk_…` e
100
+ * token pessoal `fgt_…`. As mensagens de erro da API não ecoam segredos; isto é a segunda linha.
101
+ */
102
+ export function redactSecrets(value) {
103
+ return value.replace(/\b(stk|fgt)_[A-Za-z0-9_-]{6,}/g, '$1_…');
104
+ }
105
+ function extractCode(body) {
106
+ const code = body && typeof body === 'object' ? body.code : undefined;
107
+ return typeof code === 'string' ? code : undefined;
108
+ }
109
+ function extractDetails(body) {
110
+ if (!body || typeof body !== 'object')
111
+ return {};
112
+ const b = body;
113
+ const details = {};
114
+ if (typeof b.requiredScope === 'string' || b.requiredScope === null)
115
+ details.requiredScope = b.requiredScope;
116
+ if (typeof b.requiredPermission === 'string' || b.requiredPermission === null)
117
+ details.requiredPermission = b.requiredPermission;
118
+ if (typeof b.sessionOnly === 'boolean')
119
+ details.sessionOnly = b.sessionOnly;
120
+ if (typeof b.role === 'string')
121
+ details.role = b.role;
122
+ if (Array.isArray(b.requiredRoles))
123
+ details.requiredRoles = b.requiredRoles.map(String);
124
+ return details;
125
+ }
126
+ /**
127
+ * Segmento "." ou ".." (também `%2e`) num caminho: o `fetch` (WHATWG URL) o normaliza e a
128
+ * requisição vai para outra rota — `DELETE …/sourcemaps/..` viraria `DELETE /v1/projects/:id`.
129
+ */
130
+ export function hasDotSegment(path) {
131
+ return path.split('?')[0].split('/').some((segment) => /^(?:\.|%2e){1,2}$/i.test(segment));
132
+ }
62
133
  function extractMessage(body, fallback) {
63
134
  if (body && typeof body === 'object') {
64
135
  const m = body.message;
@@ -82,22 +153,44 @@ export class TrackClient {
82
153
  get hasToken() {
83
154
  return Boolean(this.config.token);
84
155
  }
156
+ /** `FG_TRACKER_TOKEN` é um token pessoal `fgt_…` (não uma sessão). */
157
+ get hasAccessToken() {
158
+ return Boolean(this.config.token?.startsWith(ACCESS_TOKEN_PREFIX));
159
+ }
160
+ get credentialKind() {
161
+ return this.hasAccessToken ? 'access_token' : this.config.token ? 'session' : 'none';
162
+ }
163
+ /** Painel para links (`FG_TRACKER_WEB_URL` ou derivado da API); `null` se desconhecido. */
164
+ get webUrl() {
165
+ return this.config.webUrl ?? webUrlFor(this.config.baseUrl);
166
+ }
85
167
  get hasApiKey() {
86
168
  return Boolean(this.config.apiKey);
87
169
  }
88
- /** Headers de autenticação: Bearer (sessão) e/ou chave de projeto. */
89
- authHeaders(extra) {
170
+ /**
171
+ * Headers de autenticação de uma requisição: o Bearer (token pessoal ou sessão) nas rotas de
172
+ * usuário, ou só a chave de projeto nas rotas da chave. Nunca os dois: a chave não viaja nas
173
+ * chamadas que o Bearer decide.
174
+ */
175
+ authHeaders(auth, extra) {
90
176
  const headers = { accept: 'application/json', ...extra };
91
- if (this.config.token)
177
+ if (auth === 'key') {
178
+ if (this.config.apiKey)
179
+ headers[API_KEY_HEADER] = this.config.apiKey;
180
+ }
181
+ else if (this.config.token) {
92
182
  headers.authorization = `Bearer ${this.config.token}`;
93
- if (this.config.apiKey)
94
- headers[API_KEY_HEADER] = this.config.apiKey;
183
+ }
95
184
  return headers;
96
185
  }
97
186
  async request(method, path, options = {}) {
187
+ if (hasDotSegment(path)) {
188
+ throw new TrackApiError(`Recusado: ${method} ${path} tem um segmento "." ou "..", que mudaria a rota chamada. Nada foi enviado.`, undefined, path);
189
+ }
98
190
  const url = `${this.config.baseUrl}${path}${buildQuery(options.query)}`;
99
- const headers = this.authHeaders(options.headers);
100
- let init = { method, headers, signal: AbortSignal.timeout(this.config.timeoutMs ?? 15_000) };
191
+ const auth = options.auth ?? 'bearer';
192
+ const headers = this.authHeaders(auth, options.headers);
193
+ let init = { method, headers, signal: AbortSignal.timeout(options.timeoutMs ?? this.config.timeoutMs ?? 15_000) };
101
194
  if (options.body !== undefined) {
102
195
  headers['content-type'] = 'application/json';
103
196
  init = { ...init, body: JSON.stringify(options.body) };
@@ -126,33 +219,102 @@ export class TrackClient {
126
219
  body = text;
127
220
  }
128
221
  }
129
- if (res.ok)
222
+ if (res.ok) {
223
+ options.onResponse?.(res);
130
224
  return body;
131
- const message = extractMessage(body, `${res.status} ${res.statusText}`);
225
+ }
226
+ const message = redactSecrets(extractMessage(body, `${res.status} ${res.statusText}`));
227
+ const code = extractCode(body);
228
+ const details = extractDetails(body);
229
+ const fail = (summary, hint) => {
230
+ throw new TrackApiError(summary, res.status, path, hint, code, details);
231
+ };
132
232
  switch (res.status) {
133
233
  case 401:
134
- throw new TrackApiError(`Não autenticado (401) em ${method} ${path}: ${message}`, 401, path, this.config.token
135
- ? 'o FG_TRACKER_TOKEN expirou ou é inválido — faça login de novo (POST /v1/auth/login) e defina FG_TRACKER_TOKEN com o novo token.'
136
- : 'defina FG_TRACKER_TOKEN com um token de sessão do FG Tracker (POST /v1/auth/login → `token`). Para ingestão use FG_TRACKER_API_KEY.');
234
+ return fail(`Não autenticado (401) em ${method} ${path}: ${message}`, auth === 'key' ? 'a FG_TRACKER_API_KEY não é a chave de um projeto (stk_…): copie FG_TRACKER_KEY no painel, em Conectar.' : this.unauthorizedHint());
137
235
  case 403:
138
- throw new TrackApiError(`Sem permissão (403) em ${method} ${path}: ${message}`, 403, path, 'o usuário do FG_TRACKER_TOKEN não é membro deste projeto. Peça a um owner/admin para convidá-lo em /members.');
236
+ return fail(`Sem permissão (403) em ${method} ${path}: ${message}`, this.forbiddenHint(method, path, code, details));
139
237
  case 404:
140
- throw new TrackApiError(`Não encontrado (404) em ${method} ${path}: ${message}`, 404, path);
238
+ return fail(`Não encontrado (404) em ${method} ${path}: ${message}`);
141
239
  case 400:
142
- throw new TrackApiError(`Requisição inválida (400) em ${method} ${path}: ${message}`, 400, path);
240
+ if (code === 'API_KEY_PROJECT_MISMATCH') {
241
+ return fail(`Recusado (400) em ${method} ${path}: a FG_TRACKER_API_KEY é de outro projeto, não do projectId pedido. Nada foi gravado.`, 'sem FG_TRACKER_TOKEN, as ferramentas que gravam pela chave só valem no projeto dela: use o projectId da chave, troque FG_TRACKER_API_KEY pela chave do projeto certo ou configure FG_TRACKER_TOKEN.');
242
+ }
243
+ return fail(`Requisição inválida (400) em ${method} ${path}: ${message}`);
143
244
  default:
144
- throw new TrackApiError(`Erro ${res.status} em ${method} ${path}: ${message}`, res.status, path);
245
+ return fail(`Erro ${res.status} em ${method} ${path}: ${message}`);
246
+ }
247
+ }
248
+ unauthorizedHint() {
249
+ if (this.hasAccessToken)
250
+ return 'o token pessoal do FG_TRACKER_TOKEN expirou, foi revogado ou a conta está inativa — crie outro no painel (Conta → Tokens de acesso), com as permissões de que o agente precisa, e atualize FG_TRACKER_TOKEN. A recusa é final: o MCP não tenta de novo com outra credencial.';
251
+ if (this.config.token)
252
+ return 'a sessão do FG_TRACKER_TOKEN expirou ou foi encerrada — crie um token pessoal no painel (Conta → Tokens de acesso), que não cai com logout, e defina FG_TRACKER_TOKEN com ele.';
253
+ return 'defina FG_TRACKER_TOKEN com um token pessoal do FG Tracker (painel → Conta → Tokens de acesso, `fgt_…`). Para ingestão use FG_TRACKER_API_KEY.';
254
+ }
255
+ /** Dica de uma recusa 403 conforme o `code` e a permissão exigida que a API devolve. */
256
+ forbiddenHint(method, path, code, details) {
257
+ switch (code) {
258
+ case 'TOKEN_SCOPE_INSUFFICIENT':
259
+ if (details.sessionOnly)
260
+ return 'esta operação só pode ser feita com a sessão do painel: tokens pessoais não a fazem com nenhuma permissão. Peça ao usuário para fazê-la no painel.';
261
+ return details.requiredPermission
262
+ ? `falta a permissão "${details.requiredPermission}" neste token. O usuário pode marcá-la em Conta → Tokens de acesso → editar; a lista de ferramentas do MCP só muda depois de reiniciar o cliente MCP.`
263
+ : 'o token não tem a permissão que esta operação exige (Conta → Tokens de acesso → editar; reinicie o cliente MCP depois).';
264
+ case 'PROJECT_ROLE_INSUFFICIENT':
265
+ return `seu papel neste projeto é ${details.role ?? 'outro'}${details.requiredRoles?.length ? `, e a ação exige ${details.requiredRoles.join(' ou ')}` : ''}. Nenhum token faz mais do que o papel: peça a um owner ou admin do projeto.`;
266
+ case 'TOKEN_PROJECT_SCOPE':
267
+ return method === 'POST' && path === '/v1/projects'
268
+ ? 'um token restrito a projetos não cria projeto. Use um token válido para todos os projetos (com "Projetos: criar e editar") ou a sessão do painel.'
269
+ : 'o token não vale para este projeto: ele foi restrito a outros projetos (Conta → Tokens de acesso → projetos do token).';
270
+ case 'PROJECT_ACCESS_DENIED':
271
+ return 'o usuário da credencial não é membro ativo deste projeto. Peça a um owner/admin para convidá-lo.';
272
+ case 'EMAIL_VERIFICATION_REQUIRED':
273
+ return 'confirme o e-mail da conta no painel antes de continuar.';
274
+ default:
275
+ return this.hasAccessToken
276
+ ? 'o usuário do token não tem acesso a este projeto ou o token não cobre esta operação (Conta → Tokens de acesso).'
277
+ : 'o usuário do FG_TRACKER_TOKEN não tem acesso a este projeto ou a esta operação. Peça a um owner/admin.';
145
278
  }
146
279
  }
147
280
  get(path, query) {
148
281
  return this.request('GET', path, { query });
149
282
  }
283
+ /**
284
+ * GET de uma leitura com janela: devolve também a janela absoluta que a API confirmou no
285
+ * cabeçalho `x-fg-tracker-window` (`null` = a API usou `range`, ou é anterior a from/to e os
286
+ * ignorou).
287
+ */
288
+ async getWindowed(path, query) {
289
+ let window = null;
290
+ let capabilities = new Set();
291
+ const body = await this.request('GET', path, {
292
+ query,
293
+ onResponse: (res) => {
294
+ const [from, to] = (res.headers.get(WINDOW_HEADER) ?? '').split('/');
295
+ if (from && to)
296
+ window = { from, to };
297
+ capabilities = new Set((res.headers.get(CAPABILITIES_HEADER) ?? '').split(',').map((item) => item.trim()).filter(Boolean));
298
+ },
299
+ });
300
+ return { body, window, capabilities };
301
+ }
150
302
  post(path, body) {
151
303
  return this.request('POST', path, { body });
152
304
  }
153
305
  patch(path, body) {
154
306
  return this.request('PATCH', path, { body });
155
307
  }
308
+ put(path, body) {
309
+ return this.request('PUT', path, { body });
310
+ }
311
+ delete(path) {
312
+ return this.request('DELETE', path);
313
+ }
314
+ /** POST numa rota da chave de projeto (`x-fg-tracker-key`), sem o Bearer. */
315
+ postWithKey(path, body) {
316
+ return this.request('POST', path, { body, auth: 'key' });
317
+ }
156
318
  }
157
319
  export const RANGE_KEYS = ['15m', '1h', '6h', '24h', '7d', '30d'];
158
320
  export const RANGE_SECONDS = {