Skip to main content
Glama
MLAN1O

equatorial-mcp

by MLAN1O
README.md
# equatorial-mcp

Servidor MCP comunitário e não oficial para consultar e baixar faturas da Equatorial.

> Projeto comunitário, não oficial e sem afiliação com a Equatorial Energia.

Licença MIT. Veja `LICENSE`.

## Escopo do v0.1.0

- Suporte somente a Goiás (`EQUATORIAL_UF=GO`). Outras UFs (por exemplo PA e MA) falham com `UF_UNSUPPORTED` antes de abrir o navegador.
- Três tools orientadas à intenção: `listar_ucs`, `listar_faturas` e `baixar_fatura`.
- O PDF da segunda via é exposto como Resource MCP (`equatorial://faturas/{artifact_id}/pdf`); capturas de diagnóstico usam `equatorial://diagnosticos/{error_id}/screenshot`.
- Operação local e determinística, sem LLM, sem OCR em nuvem, sem mensageria e sem agendamento.
- Uma instância representa uma única conta configurada. Não use como serviço multi-tenant.
- Nenhuma operação de pagamento é executada. O servidor apenas lista unidades consumidoras, lista faturas e baixa a segunda via em PDF com extração local.

## Requisitos

- Node.js `>=20`.
- Chromium compatível via Puppeteer (dependência fixada no lockfile).

## Instalação

A partir do tarball publicado:

```bash
npm install -g equatorial-mcp
equatorial-mcp
```

Uso local sem instalar:

```bash
npx -y equatorial-mcp
```

A partir do repositório:

```bash
npm ci
npm run build
npm pack --dry-run
```

## Configuração do servidor no cliente

Os arquivos em `examples/` configuram o transporte MCP e não contêm segredos: o servidor herda o ambiente privado do processo. Exemplos:

- Claude Code e Cursor usam o formato `mcpServers` — veja `examples/mcp.json` (`command` `npx`, `args` `["-y", "equatorial-mcp"]`, `env` vazio).
- OpenCode v2 usa o aninhamento `mcp.servers` com `type: "local"` e comando em array — veja `examples/opencode.jsonc`.

Prefira o escopo de usuário/local do cliente (nunca commite credenciais):

- Claude Code: registre o servidor no escopo de usuário e informe as cinco variáveis no armazenamento privado de ambiente do cliente ou no shell que o inicia.
- Cursor: coloque a entrada de `examples/mcp.json` no seu `mcp.json` de usuário e defina as variáveis fora do repositório.
- OpenCode: mescle a entrada de `examples/opencode.jsonc` sob `mcp.servers` no seu `opencode.json` de usuário ou do projeto, sem valores secretos.

## Instalação da Skill

A Skill canônica é publicada em `skills/equatorial-faturas/SKILL.md` (dentro do repositório e do tarball NPM). A instalação é por cópia — nenhum `postinstall` escreve no seu home. Copie o diretório inteiro `equatorial-faturas`, para que futuras referências e anexos continuem junto do `SKILL.md`:

- Claude Code (projeto): `.claude/skills/equatorial-faturas/SKILL.md`
- Claude Code (usuário): `~/.claude/skills/equatorial-faturas/SKILL.md`
- OpenCode (projeto): `.opencode/skills/equatorial-faturas/SKILL.md`
- OpenCode (usuário): `~/.config/opencode/skills/equatorial-faturas/SKILL.md`
- Cursor (projeto): `.cursor/skills/equatorial-faturas/SKILL.md` ou o local compartilhado documentado `.agents/skills/equatorial-faturas/SKILL.md`
- Cursor (usuário): `~/.cursor/skills/equatorial-faturas/SKILL.md`

Exemplo (projeto Claude Code, a partir da raiz do repositório):

```bash
mkdir -p .claude/skills
cp -r skills/equatorial-faturas .claude/skills/
```

Em atualizações, copie novamente por cima para manter a Skill sincronizada com o servidor.

## Configuração

O binário não carrega `.env` automaticamente e não depende de `dotenv`: `.env.example` é apenas documentação. Exporte as variáveis no ambiente do processo ou na configuração privada do cliente MCP. Nunca cole CPF, nascimento, cookies ou códigos de pagamento em chamadas de tool.

```bash
export EQUATORIAL_UF=GO
export EQUATORIAL_UC='SUA_UC'
export EQUATORIAL_CPF='SEU_CPF'
export EQUATORIAL_NASCIMENTO='DD/MM/AAAA'
export HEADLESS='true'
```

Variáveis normativas:

| Variável | Obrigatória | Padrão |
|---|---|---|
| `EQUATORIAL_UF` | não | `GO` |
| `EQUATORIAL_UC` | sim | — |
| `EQUATORIAL_CPF` | sim | — |
| `EQUATORIAL_NASCIMENTO` | sim | — |
| `HEADLESS` | não | `true` |

Não há aliases: variáveis com outros nomes são ignoradas e segredos não têm flags CLI. `EQUATORIAL_CPF` exige 11 dígitos com dígitos verificadores válidos; `EQUATORIAL_NASCIMENTO` exige data real em `DD/MM/AAAA`; `HEADLESS` aceita somente `true`/`false`. `EQUATORIAL_CPF` aceita entrada formatada com pontuação (somente os dígitos são validados) e `HEADLESS` é insensível a maiúsculas/minúsculas com espaços aparados, com vazio/ausente assumindo `true`.

## Transporte

- Padrão: stdio (`stdin`/`stdout` exclusivos do protocolo, diagnósticos em `stderr`).
- Opt-in local: `--http` serve `POST /mcp` e `GET /healthz` (responde `{"status":"ok"}`) somente em loopback. `--host` aceita apenas `127.0.0.1`, `::1` ou `localhost` (padrão `127.0.0.1`); `--port` define a porta (padrão `3000`). `--host` e `--port` exigem `--http`, e binds não-loopback são recusados porque o v0.1.0 não tem autenticação HTTP multiusuário.
- Flags com aparência de segredo são recusadas: segredos entram somente pelo ambiente do processo.

```bash
npx -y equatorial-mcp --http --port 3000
```

## Tools

| Tool | Intenção | Observações |
|---|---|---|
| `listar_ucs` | Lista as Unidades Consumidoras disponíveis na conta Equatorial configurada. Não baixa faturas. | Somente leitura; aceita `refresh` para reler o portal. |
| `listar_faturas` | Lista faturas disponíveis para uma UC autorizada e devolve fatura_id opaca para download inequívoco. | Somente leitura; sem `uc` usa `EQUATORIAL_UC`. Lista vazia é sucesso com array vazio. |
| `baixar_fatura` | Baixa uma segunda via em PDF e extrai dados locais. Use preferencialmente a fatura_id retornada por listar_faturas. | Cria arquivo local (`readOnlyHint: false`); idempotente — reutiliza artefato já validado, salvo com `force_download: true`. Também aceita o par `uc` + `conta_mes` quando a correspondência for única. |

Chamadas do mesmo perfil são serializadas: faça chamadas em série e não baixe todas as UCs por padrão.

## Resultado estruturado e Resources

Toda tool devolve o valor canônico em `structuredContent` no envelope `{ "ok", "data", "error" }`. Em sucesso, `content` traz um resumo curto e `baixar_fatura` acrescenta um bloco `resource_link`. Em falha de domínio/portal, o resultado usa `isError: true` e repete o envelope seguro com `code`, `message`, `retryable`, `evidence_uri` e `details` (somente campos da allowlist).

Campos extraídos de `baixar_fatura` (`uc`, `conta_mes`, `vencimento`, `total`, `consumo_kwh`, `codigo_pix`, `linha_digitavel`, `saldos_scee`) usam `null` para “não extraído/não presente” — nunca zero ou string inventada. `parse_status` resume a extração:

- `complete`: campos principais obtidos e códigos de pagamento validados (seção SCEE ausente é legítima e usa `saldos_scee.status: not_present`);
- `partial`: há texto útil, mas algo está em `missing_fields` — informe os ausentes e ofereça o PDF, sem inferir valores;
- `unreadable`: PDF válido sem texto aproveitável — entregue/vincule o PDF e explique a conferência manual.

Regras de leitura: a `uc` principal é a consumidora (nunca a de injeção/geração SCEE); `codigo_pix` confiável começa com `000201` com validadores aprovados; `linha_digitavel` confiável começa com `341`, com comprimento e dígitos verificadores aceitos; `saldos_scee` nulo não é zero.

O PDF nunca entra inline em base64 no JSON/texto inicial: a referência canônica é `arquivo.resource_uri` (formato `equatorial://faturas/{artifact_id}/pdf`) mais o bloco `resource_link`. O `resources/read` entrega o binário somente quando o host solicita. `arquivo.local_path` é auxiliar e pode apontar para dentro de um container — trate `resource_uri` como referência canônica. PIX e linha digitável completos fazem parte do resultado estruturado, mas o resumo textual os omite; repita-os na conversa somente com pedido explícito do usuário.

## Estado local

Raiz padrão em sistemas Unix: `~/.equatorial-mcp/` (no Windows, sob o diretório de dados local da aplicação). Conteúdo:

```text
~/.equatorial-mcp/
├── profile-key                 # segredo local aleatório, 0600
├── profiles/<uf>/<profile-id>/ # userDataDir do Chromium (cookies/sessão isolados por perfil)
├── downloads/<uf>/             # PDFs da segunda via, 0600
├── diagnostics/                # PNGs de diagnóstico temporários, 0600
├── manifests/
│   ├── artifacts.json          # IDs opacos -> caminhos/metadados
│   └── invoices.json           # fatura_id -> fingerprint seguro
└── locks/                      # lock interprocesso do perfil
```

Diretórios usam permissão `0700` e arquivos sensíveis `0600` em POSIX; no Windows a permissão é aplicada como melhor esforço da plataforma (o `chmod` ali só alterna o atributo de leitura). O `profile-id` deriva de HMAC-SHA-256 sobre UF + UC de login + CPF normalizado — o CPF nunca aparece em caminhos ou manifestos. Não commite esse diretório.

PDFs ficam sob seu controle e não são apagados silenciosamente. Capturas de diagnóstico expiram: na inicialização, o servidor remove capturas com mais de sete dias e nunca toca nos PDFs nessa limpeza.

## Remoção precisa de estado

A remoção nunca é recursiva ampla: pare o servidor, faça backup do que for manter e remova somente o que você identificou.

1. Pare o servidor (`SIGINT`/`SIGTERM` fecha navegador e sessão sem corromper perfil ou manifesto).
2. Inspecione localmente os manifestos (`manifests/artifacts.json` e `manifests/invoices.json`) para mapear IDs opacos a arquivos — os PDFs físicos chamam-se `downloads/<uf>/Fatura_<uc>_<mes>.pdf` com a UC exata mais o mês `YYYY-MM` ou o prefixo de 8 caracteres da `fatura_id` (sem CPF e sem códigos de pagamento), e os manifestos registam a UC em texto claro ao lado dos IDs opacos (sem CPF/nascimento/pagamento).
3. Para limpar cookies/sessão de um perfil, remova somente o diretório `profiles/<uf>/<profile-id>` desejado.
4. Para descartar documentos, remova somente os arquivos listados no manifesto que você escolheu, junto dos registros correspondentes. Edite `artifacts.json`/`invoices.json` somente com o servidor parado e mantendo JSON/schema válidos — edições malformadas quebram a resolução e exigem restaurar o backup da etapa 1.
5. Nunca apague um caminho amplo de home ou raiz. PDFs não são limpos automaticamente; capturas com mais de sete dias, sim.

## Erros

| Código | Quando ocorre | Conduta |
|---|---|---|
| `CONFIG_MISSING` / `CONFIG_INVALID` | variável ausente ou malformada | informe somente o nome da variável, sem ecoar valores |
| `UF_UNSUPPORTED` | UF registrada mas não implementada (PA/MA no v0.1.0) | informe que só GO é suportado; falha antes de abrir o navegador |
| `AUTH_FAILED` | login rejeitado | revise credenciais no ambiente privado, sem repeti-las |
| `SESSION_EXPIRED` | relogin único não recuperou a sessão | reinicie a operação manualmente |
| `CAPTCHA_REQUIRED` | desafio humano detectado | rode com `HEADLESS=false`, resolva manualmente e repita; nunca contorne |
| `WAF_BLOCKED` | 403/429 ou página de bloqueio | pare as tentativas e aguarde antes de tentar manualmente |
| `NAVIGATION_TIMEOUT` | navegação excedeu o limite após a repetição permitida | verifique rede/site |
| `SITE_CHANGED` | seletor conhecido ausente | use o screenshot privado e abra issue sem anexar dados pessoais |
| `UC_NOT_FOUND` | UC fora das opções visíveis | chame `listar_ucs` |
| `NO_INVOICES` | fluxo concluído sem links | sucesso com lista vazia em `listar_faturas`; erro apenas em download direto inexistente |
| `INVOICE_NOT_FOUND` | referência/mês não existe mais | atualize com `listar_faturas` |
| `INVOICE_AMBIGUOUS` | mais de uma linha para o mês | escolha pela `fatura_id` |
| `PDF_TIMEOUT` / `PDF_INVALID` | captura excedeu o tempo ou resposta não é PDF | repetição manual única, com screenshot de apoio |
| `PDF_TOO_LARGE` | corpo acima de 20 MiB | download não é exposto; reporte o limite |
| `RESOURCE_NOT_FOUND` | URI opaca inexistente/expirada | baixe novamente |
| `LOCK_TIMEOUT` | outra operação retém o perfil | aguarde a operação anterior |
| `IO_ERROR` | storage/permissão/espaço | revise volume, permissão e espaço |
| `INTERNAL_ERROR` | falha não classificada | use o `error_id`; stack só existe no `stderr` redigido |

Repetições automáticas são limitadas a uma única repetição controlada por operação e nunca disparam em rajada; CAPTCHA, credencial inválida, WAF e estrutura alterada nunca entram em retry automático.

## Container

A imagem OCI é construída a partir da imagem oficial do navegador fixada por
versão e digest no `Dockerfile` (nunca `latest`), em build multi-stage que
compila sem segredos e publica somente dependências de produção, `package.json`,
`dist`, Skill, exemplos, README, licença e exemplo de ambiente. O processo roda
como usuário não-root da imagem, com stdio como transporte padrão e sandbox do
navegador preservado.

```bash
docker build -t equatorial-mcp:0.1.0 .
docker inspect equatorial-mcp:0.1.0 --format '{{.Config.User}} {{json .Config.Entrypoint}}'
```

O diretório de estado permanece em `/home/pptruser/.equatorial-mcp` e deve ser
persistido em volume — sem ele, sessão e artefatos se perdem. Segredos entram
somente em runtime via arquivo de ambiente do orquestrador; nenhum segredo vai
em build. O exemplo `examples/docker-compose.yml` usa `init: true`,
`stdin_open: true`, volume nomeado para o caminho de estado, `env_file` sem
valores e a capacidade documentada exigida pela imagem oficial. Quando
`local_path` apontar para dentro do container, monte o volume correspondente ou
use a `resource_uri`, que continua válida na sessão MCP.

Antes de criar qualquer tag de release, estes gates precisam de decisão do
mantenedor e não podem ser adivinhados: disponibilidade/autorização do nome no
registro público (ou decisão de fallback com escopo), remoto público canônico
para metadados de repositório, digest imutável da imagem base do navegador,
SHAs imutáveis das actions fixadas, chave pública de assinatura no segredo de
ambiente `RELEASE_SIGNING_PUBKEY` com fingerprint fixado no workflow
(`2D413C35D5BAF50B95DAA3DE91257A029C418CF8`), ordem de publicação (a imagem só
publica após o tarball; em falha, repita o job de imagem no mesmo commit e
nunca republicar o npm — depreciar, nunca sobrescrever), errata de recursos
ainda aberta, confirmação de performance do parser na máquina de release,
revisão vigente de termos e autorização, e validação nos três hosts. Nenhuma
tag é criada com gate pendente.

## Privacidade e base legal

- O tratamento ocorre para atender ao pedido explícito do titular/operador autenticado. Não acesse conta de terceiros, não compartilhe credenciais e não explore endpoints não públicos.
- Revise os termos de uso do portal antes de automatizar: se o portal proibir automação ou exigir consentimento adicional, o adaptador deve falhar de forma explícita e o canal oficial deve ser usado.
- Sem telemetria, sem analytics e sem upload de documentos. O parser trabalha em memória e não persiste texto bruto extraído.
- Logs usam allowlist de campos e mascaram a UC (no máximo os quatro últimos dígitos). CPF, nascimento, cookies, cabeçalhos de autenticação, corpo do PDF, texto integral da fatura e códigos de pagamento completos nunca entram em logs.
- Reporte vulnerabilidades de forma privada conforme `SECURITY.md`, sem anexar documentos reais, capturas com dados pessoais, cookies, credenciais, endereços ou códigos de pagamento.

## O que o software NÃO faz

- Pagamento, confirmação de pagamento, geração de PIX, alteração cadastral ou qualquer operação mutável.
- Envio por WhatsApp, e-mail, SMS ou qualquer canal de disseminação do documento.
- Agendamento, monitoramento de novas contas ou operação como daemon de cobrança.
- Bypass de CAPTCHA/WAF, proxy rotativo ou falsificação de fingerprint.
- API HTTP pública, multi-tenancy ou armazenamento remoto.
- OCR em nuvem, chave de IA ou processamento remoto de PDF.

## Compatibilidade de hosts (2026-09-07)

| Host | Arquivo de exemplo | Chaves de schema | Estado |
|---|---|---|---|
| Claude Code | `examples/mcp.json` | `mcpServers.equatorial` (`command` `npx`, `args` `["-y", "equatorial-mcp"]`, `env` vazio) | sintaxe validada localmente (JSON + formato); execução no host pendente de validação de release |
| Cursor | `examples/mcp.json` | `mcpServers.equatorial` (mesmo formato stdio acima) | sintaxe validada localmente (JSON + formato); execução no host pendente de validação de release |
| OpenCode | `examples/opencode.jsonc` | `mcp.servers.equatorial` (`type: "local"`, `command` em array) | sintaxe validada localmente (JSONC + formato); execução no host pendente de validação de release |

Somente esses três hosts têm exemplos neste repositório; nenhum outro host é declarado suportado. Os formatos evoluem fora deste projeto — cada release revalida os exemplos contra a documentação oficial e as versões instaladas antes de declarar execução confirmada.

## Validação de release

- Evals da Skill em `evals/` (casos sintéticos + sequências de tools esperadas), verificados pelo contrato `npx vitest run tests/contract/skill-evals.test.ts`: nenhuma chamada real, nenhum modelo, nenhuma rede.
- Candidato offline antes de qualquer validação manual: `npm ci`, `npm run lint`, `npm run typecheck`, `npm run test:coverage`, `npm run build`, `npm pack --dry-run --json`, probe de performance do parser com `RUN_PARSER_PERF=1 npx vitest run tests/unit/fatura-parser.performance.test.ts` e build da imagem candidata com `docker build --pull=false -t equatorial-mcp:0.1.0-rc .`.
- Evidência de produção: nenhuma. O mapa de aliases de produção permanece vazio (a conveniência UC/mês segue baseada em nulos), o parser SCEE segue validado apenas por fixtures sintéticas e nenhuma amostra real foi registrada.
- A tag `v0.1.0` está BLOQUEADA até a execução humana autorizada do runbook em `evals/README.md` (portões de entrada, orçamento de requisições, evidência privada, condições de parada e rollback) e a quitação dos demais portões pré-tag: errata do código de Resource, autorização de nome/escopo no registro, digests de imagem e actions, e revisão de termos e autorização.

## Limitações

- Somente Goiás no v0.1.0; PA/MA falham explicitamente com `UF_UNSUPPORTED`.
- Sem pagamento, sem alteração cadastral, sem envio por WhatsApp ou e-mail.
- Sem bypass de CAPTCHA ou WAF. Em bloqueio, pare e tente manualmente depois.
- PDF escaneado/sem texto continua disponível como Resource, mas os campos voltam `null` com `parse_status: unreadable` — sem OCR no MVP.
- Clientes MCP sem bom suporte a Resources ainda recebem `local_path` quando acessível; nunca base64 inline na resposta inicial.

TDQS

A4/5.0

Scored across 3 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: listing consumer units, listing invoices for a specific unit, and downloading an invoice. There is no overlap, and the descriptions reinforce the boundaries (e.g., listar_ucs explicitly says it does not download invoices). An agent can easily select the correct tool for each step.

Naming Consistency5/5

All tool names follow a consistent verb_noun pattern in Portuguese: listar_ucs, listar_faturas, baixar_fatura. The verbs are action-oriented (list, download) and the nouns are the resources (UCs, invoices). The pattern is uniform and predictable.

Tool Count5/5

With exactly 3 tools, the server is well-scoped for its purpose of retrieving energy invoices. Each tool serves a necessary step in the workflow, and there is no redundancy or unnecessary bloat. This is an ideal size for a focused utility integration.

Completeness5/5

The tool set covers the complete lifecycle for the stated purpose: listing consumer units to identify the target, listing invoices to select one, and downloading the invoice PDF. There are no dead ends—the opaque fatura_id returned by listar_faturas is specifically designed to be used by baixar_fatura, closing the loop.

Maintenance

ActivityMaintained
ResponsivenessNo issues