wrmmax-criativo-mcp
OfficialREADME.md
# wrmax-criativo
Pipeline de geração e edição de imagem da WRMax.
**Claude Code é o cérebro; este repo é a mão.**
O código não sabe nada sobre marketing — ele recebe parâmetros e devolve arquivo.
Quem decide formato, ângulo e prompt é o Claude, que depois **olha a peça gerada**
e decide se aceita ou refaz. É esse loop fechado que caracteriza a orquestração.
> O código, os comentários e as mensagens estão em inglês. A documentação e a
> conversa com o time seguem em português.
---
## Setup (5 minutos)
```bash
npm install
export OPENAI_API_KEY="sua-chave" # https://platform.openai.com/api-keys
```
> **Importante:** assinatura do ChatGPT Pro ou do app Gemini **não** dá acesso à
> API. São cobranças separadas. Precisa de chave de API com billing ativo.
Motor B, via `IMAGE_PROVIDER=gemini` (padrão é `openai`):
```bash
export IMAGE_PROVIDER=gemini
export GEMINI_API_KEY="sua-chave" # https://aistudio.google.com/apikey
```
---
## Estrutura
Cada pasta tem uma responsabilidade, e nenhum arquivo acumula duas.
```
bin/ entradas executáveis
cli.js CLI
mcp-server.js servidor MCP (só escolhe o transporte)
src/
bootstrap/ carga do .env e resolução de caminhos
config/ ÚNICO ponto que lê process.env; tabelas de modelo,
formato e qualidade
brands/ brand kit, compliance e montagem do prompt
media/ entrada, redução e saída de imagem (Drive, download,
arquivo local, preview, upload)
providers/ motores de imagem, por registro
core/ regra de negócio: artwork-service, artifact-store,
delivery
mcp/ servidor MCP, tools e transportes
http/ app Express, middleware, rotas e views
auth/ OAuth com Google
cli/ args, ajuda e orquestração do CLI
test/ node --test, sem chave e sem custo
scripts/ smoke — gasta crédito ou precisa de rede viva
brand/ um JSON por cliente
out/ saída local (só com PERSIST_OUTPUT=true)
```
O desenho central: **`src/core/artwork-service.js` não sabe o que é MCP nem o
que é CLI.** Ele recebe um pedido simples e devolve um resultado simples. Quem
formata content block é `src/mcp/tool-result.js`; quem escreve JSON no stdout é
`src/cli/run.js`. Por isso os dois frontends compartilham um caminho só.
Toda dependência (config, artifact store, diretório de brand) é **injetada**,
não importada como singleton — é o que permite testar a rota, a tool e o
serviço sem tocar no ambiente.
---
## Uso
**Gerar do zero:**
```bash
node bin/cli.js --brand forno-paulista --format feed \
--prompt "Studio product shot of a rustic pizza on a wooden board, steam rising"
```
**Editar foto real do cliente** (troca de fundo preservando o produto):
```bash
node bin/cli.js --brand forno-paulista --format square \
--ref fotos/produto.jpg \
--prompt "Change only the background to a clean warm studio gradient. Keep the product, its label and the lighting on it exactly unchanged."
```
**Rascunho barato antes de gastar no final:**
```bash
node bin/cli.js --quality draft --prompt "..."
```
Sempre rascunho antes do final. Custa uma fração e evita refazer caro.
---
## Servidor MCP
```bash
npm run mcp # stdio — é o que o Claude Code fala
npm run mcp:http # Streamable HTTP em :8787/mcp — é o que conector remoto exige
```
Ferramentas expostas: `list_brands`, `generate_image`, `edit_image`.
Não existe ferramenta que liste, busque ou navegue imagens no servidor, e isso
é proposital: quem escolhe o arquivo é o usuário. Uma ferramenta de busca
transformaria um prompt injetado numa foto de cliente em varredura do ambiente.
Detalhes de transporte, autenticação e destino da resolução cheia estão em
`CLAUDE.md`.
---
## Como o Claude Code usa o CLI
O comando imprime **JSON no stdout** e log no stderr. Isso é proposital: o
Claude roda, lê o JSON, abre o PNG, avalia e encadeia a próxima chamada. Sem
humano no meio de cada iteração.
```json
{"ok":true,"file":"out/1755777.png","seconds":6.2,"aspectRatio":"4:5"}
```
Códigos de saída: `0` sucesso · `1` falha técnica · `2` bloqueado por
compliance — o `2` existe para um hook distinguir os dois casos.
---
## Compliance
`brand/*.json` tem um array `forbidden_terms`. O `assertPromptAllowed()` roda
**antes** da chamada e bloqueia — economiza crédito e, mais importante, não
depende do modelo obedecer instrução.
```json
{
"name": "Forno Paulista",
"visual": {
"style": "appetizing food photography, rustic warmth, artisanal",
"colors": ["wood brown", "tomato red", "warm cream"],
"lighting": "warm golden light, natural window light",
"avoid": ["cold blue tones", "plastic-looking food"]
},
"forbidden_terms": [],
"compliance_reason": ""
}
```
| Marca | Bloqueio |
|---|---|
| `cliente-medico` | paciente, antes/depois, corpo, resultado de procedimento — CFM 2.336/2023 |
Teste rápido do guarda-corpo, sem chave e sem custo:
```bash
node bin/cli.js --brand cliente-medico --prompt "before and after of a patient"
# x BLOCKED by compliance rules for "Cliente médico (template CFM)"
```
---
## Testes
```bash
npm test # 110 testes, sem chave de API, sem rede externa, sem custo
```
Cobre: compliance, brand kit, config, artifact store, conversão de link do
Drive, todos os modos de falha de download, redução, upload, tabelas de tamanho,
o fluxo OAuth inteiro (com um Google de mentira), a descoberta que o claude.ai
faz, e os dois transportes MCP de ponta a ponta.
Os testes que **gastam crédito** ou dependem de rede viva ficam fora da suíte,
em `scripts/`:
```bash
npm run probe # ~US$ 0,005 — separa "chave ruim" de "pipeline ruim" (openai)
npm run probe:gemini # idem, para o provider gemini
npm run smoke:drive # ~US$ 0,01 — link do Drive de ponta a ponta
npm run smoke:edit # ~US$ 0,02 — o modelo edita ou só regenera?
npm run smoke:stateless # ~US$ 0,01 — não deixa um byte para trás
```
---
## Variáveis de ambiente
| Variável | Padrão | Para quê |
|---|---|---|
| `OPENAI_API_KEY` | — | Obrigatória com o provider `openai` |
| `GEMINI_API_KEY` | — | Obrigatória com o provider `gemini` |
| `IMAGE_PROVIDER` | `openai` | Troca o motor de imagem: `openai` ou `gemini` |
| `MCP_TRANSPORT` | `stdio` | `stdio` ou `http` |
| `PORT` | `8787` | Porta do modo HTTP |
| `MCP_PATH` | `/mcp` | Caminho do endpoint MCP |
| `MCP_TOKEN` | — | Bearer fixo (script e teste; o claude.ai não aceita) |
| `MCP_BASE_URL` | — | Obrigatória com OAuth: é o issuer, e precisa ser fixa |
| `GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET` | — | Ligam o OAuth |
| `MCP_EMAILS` | — | Quem pode autorizar. Conta Google válida não é permissão |
| `PERSIST_OUTPUT` | `false` | Salva a resolução cheia em `out/` (só dev local) |
| `ARTIFACT_TTL_MS` | `900000` | Validade do link de download |
| `ARTIFACT_MAX_BYTES` | `134217728` | Teto de memória do depósito de peças |
---
## Hospedagem (EasyPanel, ou qualquer host de container)
O servidor guarda estado em memória de propósito — clientes OAuth, tokens e o
depósito de peças são `Map()`. Isso exige **um processo vivo e único**, e é o
que descarta plataforma serverless: lá o `POST /register` cairia numa instância
e o `GET /authorize` em outra, que não conhece o cliente. O login falharia de
forma intermitente, com sintoma que não parece com a causa.
Por isso o deploy é container, e a regra vale para qualquer host: **uma réplica
só.** Para escalar além disso, troque os três armazenamentos em memória por
Redis primeiro.
O `Dockerfile` na raiz serve a qualquer plataforma de container. Os passos
abaixo são do EasyPanel; em outro host muda a interface, não o conteúdo.
### O domínio vem antes
O Google **não aceita endereço IP** como redirect de OAuth, e exige HTTPS. Ou
seja, um domínio é pré-requisito, não acabamento.
Aponte um registro A do subdomínio para o IP do servidor. Quem não tiver
domínio pode usar DNS curinga — `mcp.<ip-com-hifens>.sslip.io` resolve sozinho
para o IP embutido no nome, e o Let's Encrypt emite normalmente desde que a
porta 80 esteja aberta.
### Serviço
1. **Criar serviço → App**, com source neste repositório e branch `main`.
2. **Build: Dockerfile**, na raiz.
3. **Environment:**
| Variável | Valor |
|---|---|
| `PORT` | `8787` |
| `MCP_BASE_URL` | `https://<seu-dominio>` — sem barra no fim |
| `OPENAI_API_KEY` | a chave da OpenAI |
| `GOOGLE_CLIENT_ID` | do cliente OAuth (Aplicativo da Web) |
| `GOOGLE_CLIENT_SECRET` | do **mesmo** cliente |
| `MCP_EMAILS` | quem pode autorizar, separado por vírgula |
`MCP_TRANSPORT=http` já vem do `Dockerfile` — não defina.
4. **Domains:** o subdomínio apontando para a porta `8787`, com HTTPS ligado.
5. **Deploy.**
6. **Google Cloud Console → Credenciais → seu cliente OAuth**, adicione o
redirect autorizado, exatamente:
```
https://<seu-dominio>/oauth/google/callback
```
7. **claude.ai → conectores:** `https://<seu-dominio>/mcp`.
O `MCP_BASE_URL` vira o issuer do OAuth e é comparado caractere por caractere
com o que o cliente descobre. Domínio diferente do configurado, ou barra
sobrando, faz o vínculo falhar sem mensagem útil.
### Conferindo
```bash
curl https://<seu-dominio>/health
```
O campo que importa é `"auth":"oauth"`. Se vier `"none"`, alguma variável do
Google não chegou — e aí o servidor subiu **aberto**, aceitando qualquer
chamada e gastando a chave de quem hospeda.
---
## Notas de API que economizam debug
- O `K` de `image_size` é **maiúsculo**. `2k` é rejeitado.
- `gpt-image-2` aceita qualquer WxH divisível por 16; os menores só aceitam três
tamanhos fixos. Story/reels final precisa de `gpt-image-2`.
- Na edição, a imagem vem **antes** do texto no array de input.
- Não existe refação encadeada no provider `openai`: `previous_interaction_id` é
da Interactions API do Gemini. Para ajustar, reenvie a imagem como referência.
- Entrada por URL manda `User-Agent` próprio: várias origens (Wikimedia entre
elas) devolvem 400/403 para requisição sem UA identificável.
- Peça com texto: defina a copy primeiro, depois peça a imagem com aquela copy.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing