@expertcustom/funilaria-mcp
README.md
# @expertcustom/funilaria-mcp
Servidor MCP (Model Context Protocol) com as tools tipadas que a IA do Aurora usa para escrever e ler no portal **Funilaria & Pintura**.
Ele substitui o `mcp-fetch` montando requisição HTTP à mão com o segredo escrito no system prompt: aqui cada operação é uma tool com schema, descrição e erro em português.
```
IA do Aurora ──stdio──> npx @expertcustom/funilaria-mcp ──HTTPS──> backend NestJS
```
Pela ADR-001, este pacote é **adaptador**: nenhuma regra de negócio mora aqui. Toda tool chama um endpoint que já existe, e o serviço do backend continua sendo o dono da decisão.
## Tools — quem define é o backend
Este pacote **não sabe quais tools existem**. Na subida ele busca `GET /mcp/catalogo` e publica o que vier; a execução vai por `POST /mcp/executar`, e o backend resolve o nome para o serviço.
```
subida ──> GET /mcp/catalogo → lista publicada em tools/list
chamada ──> POST /mcp/executar → { name, args }
```
O motivo é operacional: antes, tool nova custava editar este pacote, buildar, commitar, publicar e reinstalar o MCP no Aurora — cinco passos para expor um endpoint que já existia no backend. Agora é commit de um módulo só, em `backend/src/mcp`.
A lista corrente sai de `GET /mcp/catalogo`. Não há cópia dela aqui de propósito: cópia envelhece e passa a mentir.
**Quando o catálogo não responde**, o servidor publica uma única tool, `funilaria_catalogo_indisponivel`, cuja descrição explica o que verificar. Sem ela o sintoma seria "sumiram as tools" e o diagnóstico começaria pelo lugar errado.
O catálogo é buscado na subida e **relido a cada 5 minutos** (`FUNILARIA_CATALOGO_INTERVALO`, em segundos; `0` desliga). Quando a lista muda — nome, descrição, schema ou annotation —, o servidor manda `notifications/tools/list_changed` e o Aurora troca as tools da IA sem reinstalar. Releitura que falha não mexe em nada: a última lista boa continua valendo. Se a subida caiu na `funilaria_catalogo_indisponivel`, a primeira releitura boa a substitui pela lista real.
### Leitura ou escrita (annotations)
Cada tool sai com `readOnlyHint`, e as de escrita com `destructiveHint: true` — com a política "pedir confirmação" do Aurora, é isso que faz a escrita esperar o usuário aprovar. O critério, em ordem (`src/server/catalog/anotacoes.ts`):
1. `annotations` que o backend mandar na tool — hoje ele **não manda**; quando mandar, vence;
2. a descrição: "Somente leitura" ou a rota citada (`(GET /...)` é leitura, `(POST /...)` é escrita);
3. o nome: `consultar_`, `buscar_`, `listar_`, `identificar_`, `ver_` são leitura;
4. sem sinal nenhum, vale como escrita destrutiva.
O jeito certo é o backend mandar `annotations: { readOnlyHint, destructiveHint }` em cada item de `GET /mcp/catalogo` — ele é quem sabe se `responder_busca_peca`, por exemplo, só acrescenta (`destructiveHint: false`).
### Instruções
No `initialize` o servidor manda `instructions` curtas — consultar antes de gravar, não inventar id nem valor, confirmar com a pessoa quando faltar dado — que o Aurora anexa ao prompt da IA. O detalhe de cada tool continua na descrição dela.
## Autenticação
**Credencial de serviço com `shopId` explícito** é o caminho principal, tanto para escrever quanto para ler. Header `x-aurora-secret`, o mesmo valor de `AURORA_WEBHOOK_SECRET` no backend; ele não representa pessoa nenhuma, representa o serviço.
Uma IA que atende várias oficinas não tem sessão, então a oficina é **parâmetro**, nunca contexto implícito. Do lado do backend isso é o `@AllowService()` nas rotas de leitura de estoque: o `JwtAuthGuard` aceita o segredo no lugar do JWT e o `ShopContextGuard` passa a exigir o `shopId` — id inexistente responde `404 Oficina não encontrada`, e não uma lista vazia que se confundiria com "oficina sem estoque".
**Sessão de usuário** (JWT de `POST /auth/entrar`) continua existindo para os comandos de CLI, mas **não vale mais para as tools**: `POST /mcp/executar` é sempre chamada de serviço, e a oficina é sempre parâmetro explícito. Uma IA que atende várias oficinas nunca teve sessão; manter os dois modos só criava um caminho em que `shopId` omitido significava coisas diferentes.
### Configuração — env é o caminho principal
Em produção quem sobe este processo é o runtime do Aurora, que injeta as variáveis: **não há terminal, e nenhum comando de login é executado**. O servidor funciona com o disco totalmente vazio.
| Env | Apelido aceito | Para quê |
|---|---|---|
| `FUNILARIA_API_URL` | `PUBLIC_API_URL` | Base da API |
| `FUNILARIA_SERVICE_SECRET` | `AURORA_WEBHOOK_SECRET` | Segredo de serviço (`x-aurora-secret`) |
| `FUNILARIA_SIGNING_SECRET` | `AURORA_WEBHOOK_SIGNING_SECRET` | Segredo da assinatura HMAC (opcional) |
| `FUNILARIA_SHOP_ID` | — | Oficina padrão de `consultar_estoque` |
| `FUNILARIA_PERFIL` | — | Qual IA do conjunto este servidor atende: `pecas`, `estoque`. Sem definir, o catálogo vem inteiro |
| `FUNILARIA_TOKEN` | — | JWT de usuário, se houver (opcional) |
| `FUNILARIA_CATALOGO_INTERVALO` | — | Segundos entre releituras do catálogo (padrão 300; `0` desliga) |
O `FUNILARIA_PERFIL` existe porque o Aurora liga **servidor inteiro** a uma IA —
não há filtro de tool por IA no painel dele. Então o escopo sai do catálogo: o
backend devolve só as tools daquele perfil, do mesmo jeito que o
`FUNILARIA_SHOP_ID` já limita a oficina. Assim a IA de peças não recebe
`lancar_consumo`, e a de estoque não recebe `responder_busca_peca`.
Perfil desconhecido vale como ausente e devolve tudo: atender demais é
recuperável, atender de menos deixa a IA sem ferramenta e o sintoma aparece
longe da causa. Por isso o `status` imprime o perfil que ele leu.
Os apelidos existem para o erro clássico de copiar o `.env` do backend e o segredo "sumir" por causa do prefixo diferente — `AURORA_WEBHOOK_SECRET` é exatamente o mesmo valor dos dois lados.
Segredo nunca é hardcoded nem lido de prompt. O arquivo `~/.config/funilaria-mcp/credentials.json` (modo `0600`) é conveniência de **desenvolvimento local**; a env sempre vence e nunca é gravada em disco.
No boot, o servidor escreve em **stderr** (stdout é do protocolo MCP) uma linha dizendo o que está configurado e **de qual env veio cada coisa** — nunca o valor. É o que aparece no log do Aurora quando alguém erra o nome da variável:
```
[funilaria-mcp] API: https://api.exemplo.com (FUNILARIA_API_URL) · Credencial de serviço: configurada via AURORA_WEBHOOK_SECRET · ...
[funilaria-mcp] Sem credencial de serviço: as tools de escrita vão recusar toda chamada. Defina FUNILARIA_SERVICE_SECRET no ambiente deste processo.
```
### Assinatura HMAC
Quando `FUNILARIA_SIGNING_SECRET` existe, toda escrita leva também:
```
x-timestamp: <epoch em segundos>
x-signature: sha256=<HMAC-SHA256(`${timestamp}.${corpo}`)>
```
É a melhoria mapeada na ADR-001 (fecha replay e vazamento por log). O backend **ainda não verifica** — header desconhecido é ignorado, então dá para ligar o lado do servidor sem quebrar quem já está rodando.
## Instalação
### Na IA do Aurora (produção)
Registre o servidor com as variáveis no próprio cadastro do MCP — nada de login, nada de segredo no system prompt:
```json
{
"command": "npx",
"args": ["-y", "@expertcustom/funilaria-mcp"],
"env": {
"FUNILARIA_API_URL": "https://<api-do-portal>",
"FUNILARIA_SERVICE_SECRET": "<mesmo valor de AURORA_WEBHOOK_SECRET>"
}
}
```
### Local, para desenvolver
```bash
# opção A — env no shell (igual à produção)
FUNILARIA_API_URL=http://localhost:3334 FUNILARIA_SERVICE_SECRET=... npx @expertcustom/funilaria-mcp
# opção B — guardar em ~/.config para não exportar em todo shell
npx @expertcustom/funilaria-mcp login-servico
# sessão de usuário: só é necessária para consultar_estoque sem shopId
npx @expertcustom/funilaria-mcp login
# conferir o que está valendo e de onde veio (nunca imprime segredo)
npx @expertcustom/funilaria-mcp status
# registrar no Claude Code
claude mcp add funilaria --env FUNILARIA_API_URL=http://localhost:3334 -- npx -y @expertcustom/funilaria-mcp
```
## Pendências no backend
As quatro pendências originais (webhook de estoque inalcançável, leitura sem credencial de serviço, segredo checado depois da validação, distância como código morto) foram **corrigidas no backend** e revalidadas contra `localhost:3334`. O que sobrou:
1. **A IA não tem como descobrir o `shopId`.** É o único dado que ela precisa saber de cor, e hoje só chega por `FUNILARIA_SHOP_ID` — o que amarra um servidor a uma oficina e derruba o caso multi-oficina que motivou o desenho de serviço.
O ponto mais barato de resolver é o `lancar_consumo`: o backend **já identificou** funcionário e oficina pelo número do WhatsApp, mas devolve só o texto de confirmação. Se `IntakeResult` incluísse `shopId` e `memberId`, a conversa fluiria — "usei 100ml de verniz" → "quanto gastei esse mês?" seria `consultar_balancete` com os dois ids em mãos. Sem isso, a segunda pergunta não tem resposta possível.
2. **`GET /estoque/movimentos` ficou fora do `@AllowService()`.** O `shopId` está declarado no `ListMovementsDto`, mas a rota não aceita credencial de serviço — o parâmetro não tem como ser usado. Ou marca a rota, ou tira o campo do DTO para não sugerir capacidade que não existe.
3. **Assinatura HMAC ainda não é verificada.** O cliente já envia `x-timestamp` e `x-signature` quando há segredo de assinatura (ver acima). Falta o lado do servidor para fechar replay e vazamento por log, como prevê a ADR-001.
## Desenvolvimento
```bash
npm install
npm run build # tsc estrito, gera dist/
npm start # sobe o servidor MCP em stdio
```
TDQS
A4.1/5.0
Scored across 1 tool
Disambiguation5/5
With only one tool, there is no possibility of tool confusion. The tool's purpose—to report that the catalog is unavailable—is uniquely clear.
Naming Consistency5/5
The single tool name is descriptive and domain-specific, following a clear pattern. With no other tools, there is no inconsistency to penalize.
Tool Count1/5
A single tool that serves only as an error placeholder is far below the expected scope for a catalog server. This is an extreme mismatch with the apparent purpose.
Completeness1/5
The server provides no actual catalog operations; the only tool explicitly states that all functional tools are unavailable. The surface is severely incomplete and unusable.
Maintenance
ActivityMaintained
ResponsivenessNo issues