Skip to main content
Glama
ronnyarruda20

mcp-email-instagram

README.md
# mcp-email-instagram

![mcp-email-instagram: servidor MCP em Python para e-mail e Instagram](docs/banner.jpg)

[![CI](https://github.com/ronnyarruda20/mcp-email-instagram/actions/workflows/ci.yml/badge.svg)](https://github.com/ronnyarruda20/mcp-email-instagram/actions/workflows/ci.yml)
![Python 3.11+](https://img.shields.io/badge/Python-3.11%2B-3776ab)
![MCP](https://img.shields.io/badge/MCP-SDK%201.x-6b4fbb)
![Licença MIT](https://img.shields.io/badge/licen%C3%A7a-MIT-green)

Servidor **MCP (Model Context Protocol)** em Python que dá a assistentes de IA acesso a **caixas de e-mail** (qualquer provedor IMAP/SMTP, uma ou várias) e a uma **conta profissional do Instagram**, pela API oficial da Meta.

Com ele conectado, você pede ao Claude (ou a qualquer cliente MCP) coisas como:

> "Tem e-mail de cliente sem resposta desde segunda? Rascunha uma resposta para cada um."
>
> "Quantas pessoas visitaram o perfil do Instagram esta semana? E qual post teve mais alcance?"
>
> "Responde o comentário do último post agradecendo, e oculta o que é spam."

e o modelo chama as ferramentas certas em vez de pedir para você copiar e colar.

**Em resumo:**
- **20 ferramentas**: 9 de e-mail e 11 de Instagram.
- **Travas no servidor**, não no prompt: a DM só sai dentro de 24h, o token nunca aparece em URL e toda escrita fica registrada.
- **Segredos só na sua máquina**: o modelo nunca vê senha nem token.
- **27 testes sem rede**, rodando no CI em Python 3.11, 3.12 e 3.13.

## Como funciona

![Como funciona: o assistente pede pelo MCP, o servidor confere as travas e só então age no e-mail e no Instagram; os segredos ficam num .env local que o modelo nunca vê](docs/como-funciona.png)

## Na prática

Um pedido vira uma sequência de chamadas. As de leitura correm soltas, as de escrita ficam registradas, e o que a regra não permite é recusado antes de chegar à API:

![Exemplo de conversa: o assistente busca e lê dois e-mails, grava dois rascunhos no mesmo fio, responde uma DM e tem a segunda recusada por estar fora da janela de 24h](docs/exemplo-conversa.png)

<sub>As duas imagens são geradas de HTML: [`docs/como-funciona.html`](docs/como-funciona.html) e [`docs/exemplo-conversa.html`](docs/exemplo-conversa.html).</sub>

## Ferramentas

| Ferramenta | O que faz | Tipo |
|---|---|---|
| `email_pastas` | Lista as pastas, com os marcadores especiais (Enviados, Lixeira, Rascunhos) | leitura |
| `email_buscar` | Busca por remetente, assunto, texto, período, não lidas; no Gmail, aceita a sintaxe de busca dele | leitura |
| `email_ler` | Cabeçalhos, corpo em texto (HTML convertido) e lista de anexos, sem marcar como lido | leitura |
| `email_baixar_anexo` | Salva um anexo localmente | escrita |
| `email_enviar` | Envia, inclusive **respondendo no mesmo fio** (`In-Reply-To`/`References`), com anexos | escrita |
| `email_rascunho` | Grava rascunho sem enviar | escrita |
| `email_mover` · `email_marcar` | Move entre pastas; marca lida/não lida, sinalizada | escrita |
| `email_apagar` | Manda para a Lixeira; apaga em definitivo só se pedido | **destrutiva** |
| `ig_perfil` · `ig_posts` | Perfil, seguidores e posts com curtidas e comentários | leitura |
| `ig_metricas_conta` · `ig_metricas_post` | Alcance, visualizações, visitas ao perfil, interações | leitura |
| `ig_comentarios` · `ig_conversas` · `ig_mensagens` | Comentários de um post e o Direct | leitura |
| `ig_responder_comentario` · `ig_ocultar_comentario` | Responde ou oculta comentário | escrita |
| `ig_enviar_dm` | Responde no Direct, **só dentro da janela de 24h** | escrita |
| `ig_publicar` | Publica imagem, carrossel (2 a 10) ou Reels | escrita |

Cada ferramenta declara ao cliente se é somente leitura, destrutiva ou idempotente (`ToolAnnotations`). O cliente usa isso para decidir quando pedir confirmação.

## Decisões de implementação

- **Token no cabeçalho, nunca na URL.** O `httpx` registra a URL completa em nível INFO, e servidores MCP costumam ligar o log nesse nível. Com o token na query string, ele vai parar no log do cliente. Descobri isso num teste real. Agora o token vai em `Authorization: Bearer`, os loggers `httpx`/`httpcore` ficam em WARNING, e há teste para as duas coisas. Veja [`instagram.graph`](src/mcp_email_instagram/instagram.py).
- **Instagram sem Facebook.** O caminho padrão é a *API do Instagram com login do Instagram* (graph.instagram.com), que não exige conta no Facebook nem Página vinculada. O token vale 60 dias, e o servidor **renova sozinho** depois de 20 (a Meta só renova token com mais de 24h), regravando o `.env`. O caminho antigo, por token de Página, continua disponível com `INSTAGRAM_LOGIN=facebook`.
- **Sem conversa fria no Direct.** A Meta só permite responder até 24h depois da última mensagem da pessoa. `ig_enviar_dm` confere isso **antes** de chamar a API: o modelo não consegue iniciar conversa, mesmo que alguém peça. Automação de navegador ficou de fora de propósito, porque viola os termos e dispara o "confirme que você é humano" na conta.
- **Resposta no mesmo fio.** Ao responder, o destinatário vem do `Reply-To` (ou `From`), o assunto ganha `Re:` uma vez só, e `In-Reply-To`/`References` são montados a partir do original. Sem isso, cada resposta abre uma conversa nova no cliente de e-mail de quem recebe.
- **IMAP que funciona em servidor real.**
  - Tudo é feito por **UID**, que não muda quando outras mensagens são apagadas.
  - As pastas especiais são achadas pelo marcador SPECIAL-USE (RFC 6154), com plano B pelo nome, porque o Gmail chama a de enviados de `[Gmail]/E-mails enviados` e a Hostinger de `INBOX.Sent`.
  - Busca com acento vai como literal UTF-8 no fim do comando.
  - A data do `SINCE` não usa `strftime('%b')`, que segue o locale da máquina: em português sairia `set` e o servidor recusa.
  - Com `UIDPLUS`, o expurgo é `UID EXPUNGE`. Sem ele, o `EXPUNGE` comum levaria junto qualquer mensagem já marcada para exclusão na pasta.
- **Destinatários sem cabeçalho vazio.** Desde a correção do CVE-2023-27043 (Python 3.12+ e também as versões de segurança do 3.11), `getaddresses(['a@x.com', '', ''])` devolve `[('', '')]`: um Cc ou Cco vazio na lista zera **todos** os destinatários, e o SMTP recusa com um `SMTPRecipientsRefused({})` que aparece só como `{}`. Só os cabeçalhos preenchidos entram na conta. O primeiro envio real pegou isso, e há teste de regressão para os três casos.
- **Cópia em Enviados sem duplicar.** O SMTP não guarda cópia. O servidor grava em Enviados por IMAP, exceto no Gmail, que já faz isso sozinho e ficaria com duas cópias.
- **Trilha de auditoria.** Toda ação que escreve (enviar, apagar, mover, publicar, responder, renovar token) vira uma linha em `auditoria.jsonl`. Leitura não é registrada.
- **Segredo não passa pelo modelo.** Senhas e tokens são digitados no terminal pelo assistente de configuração (`getpass`), testados contra o servidor e só então gravados. Nenhuma ferramenta recebe credencial como parâmetro.

## Como rodar

Requisitos: [uv](https://docs.astral.sh/uv/) e Python 3.11+.

```bash
git clone https://github.com/ronnyarruda20/mcp-email-instagram
cd mcp-email-instagram
uv run mcp-email-instagram-configurar email       # uma vez por caixa
uv run mcp-email-instagram-configurar instagram   # opcional
```

O assistente grava tudo em `~/.mcp-email-instagram/.env`, fora do repositório.

**Gmail:** com a verificação em duas etapas ligada, crie uma *senha de app* em [myaccount.google.com/apppasswords](https://myaccount.google.com/apppasswords). A senha normal é recusada por IMAP.

**Instagram:** a conta precisa ser profissional (Empresa ou Criador).
1. Crie um app do tipo Empresa em [developers.facebook.com](https://developers.facebook.com/apps).
2. Adicione o caso de uso *Gerenciar mensagens e conteúdo no Instagram* e, em *Permissões e recursos*, as permissões `instagram_business_*` que for usar.
3. Em *Configuração da API com login do Instagram*, clique em *Adicionar conta*. A conta entra como *Testador do Instagram*; aceite o convite em instagram.com → Configurações → Apps e sites.
4. Clique em *Gerar token*.

Como dono do app e da conta, você usa o acesso padrão, sem App Review. Para o Direct, ligue *Permitir acesso às mensagens* nas configurações de mensagens do Instagram.

## Conectando a um assistente

**Claude Code:**

```bash
claude mcp add email-instagram -- uv run --project /caminho/para/mcp-email-instagram mcp-email-instagram
```

**Claude Desktop** (`claude_desktop_config.json`):

```json
{
  "mcpServers": {
    "email-instagram": {
      "command": "uv",
      "args": ["run", "--project", "/caminho/para/mcp-email-instagram", "mcp-email-instagram"]
    }
  }
}
```

## Configuração

Tudo por variável de ambiente ou pelo `.env` da pasta de dados. O ambiente vence o arquivo. Modelo completo em [`.env.example`](.env.example).

| Variável | Para quê |
|---|---|
| `CAIXAS` | Nomes das caixas, separados por vírgula; a primeira é a padrão |
| `CAIXA_<NOME>_ENDERECO` · `_SENHA` · `_IMAP_HOST` · `_SMTP_HOST` | Credenciais e servidores de cada caixa (portas padrão 993 e 465) |
| `CAIXA_<NOME>_SMTP_GUARDA_ENVIADO` | `auto` (padrão: true no Gmail), `true` ou `false` |
| `INSTAGRAM_LOGIN` | `instagram` (padrão) ou `facebook` |
| `INSTAGRAM_TOKEN` · `INSTAGRAM_USUARIO_ID` · `INSTAGRAM_TOKEN_EMITIDO_EM` | Gravados pelo assistente de configuração |
| `FACEBOOK_PAGINA_ID` | Só no modo `facebook` |
| `ORIENTACOES_EXTRAS` | Texto somado às instruções que o modelo recebe (tom de voz, o que não prometer) |
| `MCP_EMAIL_INSTAGRAM_HOME` | Pasta de dados (padrão `~/.mcp-email-instagram`) |

## Estrutura

```
src/mcp_email_instagram/
├── servidor.py      # as 20 ferramentas MCP: nome, descrição, parâmetros e anotações
├── email_imap.py    # IMAP/SMTP: busca, leitura, envio, resposta no fio, pastas especiais
├── instagram.py     # Graph API: token no cabeçalho, renovação, trava de 24h, publicação
├── config.py        # .env, caixas de e-mail, pasta de dados
├── auditoria.py     # auditoria.jsonl de toda ação que escreve
└── configurar.py    # assistente de terminal que testa e grava as credenciais
tests/               # IMAP, SMTP e Meta falsos: nada sai para a rede
docs/                # imagens do README e o HTML de onde saem
```

## Testes

```bash
uv run --group dev pytest -v
```

Os testes não usam rede: IMAP, SMTP e Graph API são substituídos por versões falsas que registram cada comando. Eles cobrem:

- o encadeamento da resposta;
- a ordem dos critérios de busca com acento;
- a escolha de gravar ou não a cópia enviada;
- destinatários preservados com Cc e Cco vazios (regressão do `getaddresses` estrito);
- a trava de 24h do Direct (conversa fria, fora da janela, dentro dela);
- o token fora da URL e o log do `httpx` calado;
- a renovação do token aos 20 dias;
- a publicação de carrossel;
- as anotações de cada ferramenta.

O CI roda em Python 3.11, 3.12 e 3.13.

## Limitações

- **SDK MCP 1.x.** A versão 2 renomeou `FastMCP` para `MCPServer` e mudou a API. O projeto fixa `mcp<2` até a migração.
- **Gmail por senha de app, não OAuth.** É o caminho mais simples para uso pessoal. Para distribuir a terceiros, o certo seria OAuth com escopo restrito.
- **Métricas do Instagram mudam de nome** entre versões da Graph API. Se uma for recusada, o erro diz qual, e basta tirá-la da lista.
- **Publicação exige URL pública** da mídia: a Meta baixa o arquivo de lá. Upload de arquivo local não é suportado.
- **Token vazado não se revoga individualmente.** Remover o app da conta só suspende o token; reautorizar o app ressuscita os antigos.

## Stack

Python 3.11+ · [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) (FastMCP) · httpx · `imaplib`/`smtplib` da biblioteca padrão · pytest · uv · GitHub Actions

## Licença

[MIT](LICENSE)