# MCP remoto e OAuth

Status: disponível em produção em `https://api.tdfacil.com/mcp`.

## Transporte e descoberta

O recurso MCP é stateless e usa Streamable HTTP:

- `POST /mcp` processa mensagens JSON-RPC/MCP;
- `GET /mcp` e `DELETE /mcp` exigem autenticação e retornam `405`;
- não existe sessão persistida nem header `Mcp-Session-Id`;
- cada `POST` cria um servidor de protocolo isolado, evitando estado ou credenciais cruzadas entre tenants;
- `GET /.well-known/oauth-protected-resource/mcp` publica `resource`, authorization server, bearer method e scopes aceitos.

Um cliente sem bearer válido recebe `401` e:

```http
WWW-Authenticate: Bearer resource_metadata="https://<host>/.well-known/oauth-protected-resource/mcp", scope="openid email profile"
```

O `Host` deve coincidir exatamente com `MCP_ALLOWED_HOSTS`. Se um cliente enviar
`Origin`, ele também precisa estar em `MCP_ALLOWED_ORIGINS`; clientes não-browser
podem omitir `Origin`.

## Validação OAuth efetiva

O MCP valida o access token pelo JWKS configurado e exige:

1. assinatura `RS256`, `ES256` ou `EdDSA`;
2. `iss` igual a `MCP_OAUTH_ISSUER`;
3. `aud` igual a `MCP_OAUTH_AUDIENCE`;
4. `exp` presente e vigente, com tolerância de relógio de 30 segundos;
5. `sub` e `client_id` ou `azp`;
6. grant persistido e ativo para o par `sub` + `client_id`;
7. membership, credencial interna e limites ainda válidos no banco.

Os scopes efetivos vêm da interseção persistida retornada por
`private.resolve_mcp_oauth_grant`; claims de scope do token não concedem acesso
sozinhas. O valor `MCP_RESOURCE_URL` é publicado na metadata e associado ao
contexto MCP. A implementação atual faz a validação vinculante pelo `aud`; ela
não valida um claim JWT adicional chamado `resource`.

O authorization server e o registro de clientes pertencem ao Supabase. A
plataforma não implementa endpoints OAuth de autorização/token próprios. O
Dynamic Client Registration, quando habilitado no Supabase Auth, é usado por
clientes MCP compatíveis.

O discovery OAuth do Supabase também anuncia `offline_access`. Clientes MCP
podem solicitá-lo para receber/usar refresh tokens e renovar a sessão sem abrir
um novo login a cada expiração. A tela de consentimento deve reconhecer esse
scope como duração de sessão; ele não é convertido em permissão TDFÁCIL e não
concede acesso a leads, créditos ou operações. A metadata do recurso MCP
continua anunciando somente `openid email profile`, que são os scopes mínimos
necessários ao recurso.

A migration instala `private.tdfacil_custom_access_token_hook`, que substitui
`aud` pelo resource MCP somente quando existe grant interno ativo. Instalar a
função não ativa o hook: cada projeto Supabase local/staging deve configurá-la
explicitamente como Custom Access Token Hook e provar que grant ausente,
pendente, expirado ou revogado mantém a audience padrão e é rejeitado pelo MCP.

## Persistência do consentimento

O backend do dashboard expõe duas etapas de consentimento e uma operação de
revogação, todas para admin real da empresa, com reautenticação e
`Idempotency-Key`:

1. `POST /control/v1/developers/oauth-grants` cria um grant `pending` por dez minutos;
2. `POST /control/v1/developers/oauth-grants/{id}/activate` ativa o grant e revoga o grant anterior do mesmo usuário/cliente.
3. `DELETE /control/v1/developers/oauth-grants/{id}` revoga o grant e a credencial interna associada.

A expiração pedida usa 30 dias por padrão e nunca pode ultrapassar 90 dias.
O body de preparação aceita somente grants TDFácil da tabela abaixo; eles não
são os scopes OIDC padrão que o Supabase usa no protocolo OAuth.

## Scopes

Scopes OAuth/OIDC (`openid`, `email`, `profile`, `phone` e
`offline_access`) identificam a conta e mantêm a sessão. Eles são separados dos
grants TDFÁCIL abaixo, que controlam efetivamente as ferramentas disponíveis.

| Scope | Ferramentas |
|---|---|
| `balance:read` | `tdfacil_get_balance` |
| `usage:read` | `tdfacil_get_usage` |
| `references:read` | `tdfacil_search_reference` |
| `leads:read` | preview, listagem/consulta de listas e leads |
| `leads:write` | `tdfacil_create_lead_list` |
| `enrichments:write` | `tdfacil_request_enrichment` |
| `operations:read` | consulta e cancelamento de operação |
| `webhooks:manage` | publicado como scope comum, mas sem tool MCP nesta versão |

## Catálogo implementado

Campos `limit` usam padrão `50` e máximo `200`, salvo indicação diferente.
IDs são UUIDs. `idempotency_key` aceita 8 a 128 caracteres de
`A-Z a-z 0-9 . _ : -`.

| Tool | Entrada |
|---|---|
| `tdfacil_get_balance` | nenhuma |
| `tdfacil_get_usage` | `limit?`, `cursor?` |
| `tdfacil_search_reference` | `catalog: cnaes\|municipios`, `search?`, `limit?`, `cursor?` |
| `tdfacil_preview_lead_search` | `filters`, `limit?` (padrão 100, máximo 10.000), `idempotency_key` |
| `tdfacil_create_lead_list` | `title`, `description?`, `filters`, `limit` (1..1.000), `priority?` (-100..100), `idempotency_key` |
| `tdfacil_list_lead_lists` | `status?`, `limit?`, `cursor?` |
| `tdfacil_get_lead_list` | `id` |
| `tdfacil_get_leads` | `list_id`, `limit?`, `cursor?` |
| `tdfacil_request_enrichment` | `lead_id`, `kind?=company_people`, `idempotency_key` |
| `tdfacil_get_operation` | `id` |
| `tdfacil_cancel_operation` | `id`, `idempotency_key` |

## Resultados, erros e idempotência

Ferramentas retornam o mesmo dado de domínio usado pela API REST em
`structuredContent` e uma representação JSON em `content[0].text`. Páginas MCP
usam `{ "data": [...], "next_cursor": "..." }`, sem o wrapper `meta` do REST.

Erros de domínio usam:

```json
{
  "error": {
    "code": "insufficient_credits",
    "status": 402,
    "title": "Payment Required",
    "detail": "..."
  }
}
```

Todas as ferramentas que alteram estado e o preview exigem
`idempotency_key`. A chave é isolada por tenant, credencial e nome da ferramenta;
mesma chave e mesma entrada reproduzem a resposta. Entrada diferente retorna
`409 idempotency_key_reused`.

Credenciais OAuth são vinculadas a uma credencial interna `live`; portanto não
existe MCP de fixture `test` nesta implementação. Billing mutável não é exposto
como ferramenta MCP. Gerenciamento de webhooks também permanece na API REST e
no control plane, não no catálogo MCP atual.

## Limites e limitações atuais

- leitura: 120/min; preview: 10/min; mutação: 10/min, sempre reduzidos pelo limite da credencial;
- autenticação inválida é limitada a 30/min por IP e preview a 100/dia por tenant;
- operações pagas admitem no máximo 20 grupos pendentes por credencial e 100 por tenant;
- o scheduler executa no máximo 2 jobs da mesma credencial e 10 do mesmo tenant simultaneamente;
- budgets por identidade, IP, tenant e janela diária são consumidos de forma atômica no Postgres;
- MCP usa grants `live`; seu preview usa o gateway CNPJa quando
  `LEAD_PREVIEW_ENABLED=true` e retorna `503 lead_search_preview_unavailable`
  quando o gateway está desabilitado ou indisponível;
- o endpoint de enriquecimento atual aceita um `lead_id` por chamada; não há
  schema de lote MCP nesta versão;
- `GET`/`DELETE` não são transportes MCP nesta versão;
- recurso MCP: `https://api.tdfacil.com/mcp`;
- metadata do recurso:
  `https://api.tdfacil.com/.well-known/oauth-protected-resource/mcp`;
- authorization server:
  `https://izaprggcgnhycfkffdks.supabase.co/auth/v1`.
