# Otto MCP — guia do agente

Este documento é para o agente, não para a pessoa. Leia antes da primeira
chamada de cada sessão.

O Otto escreve posts para X, LinkedIn e Threads **na voz do usuário**, usando o perfil de
voz, as memórias e os vetos que ele ensinou. O agente conduz a conversa e chama
as tools; quem redige o texto é o Otto.

## As cinco regras que mais causam falha

1. **`idea` é briefing, nunca um post pronto.** Passe assunto, ângulo, fatos e
   público. Texto já redigido pelo agente contamina a voz do usuário e é o erro
   de uso mais comum.
2. **Não reescreva o que a tool devolveu.** O retorno de `create_post` e
   `edit_post` é final: mostre como veio. Para mudar algo, chame `edit_post`
   com a instrução do que mudar ("mais curto", "troque o gancho") — não com o
   texto novo.
3. **Agendar é `publish_post` com `at`.** `edit_post` só reescreve texto e
   nunca agenda; pedir agendamento por ele devolve `EDIT_IS_SCHEDULING`.
4. **Post agendado não tem link público.** A URL pública só existe depois que a
   rede confirma a publicação. Nunca monte um link com o `postId` interno — ele
   é o id do Otto, não o id da rede. Para "me mande o link do rascunho", use a
   `url` que `create_post` devolve: ela abre o rascunho no Otto (exige login
   na conta).
5. **Imagem vai na mesma chamada de `create_post`.** Envie o arquivo original
   em `imageUrl` (preferido) ou `imageBase64`, e só confirme a mídia ao
   usuário quando o retorno trouxer `imageAttached: true`.

Use `network: "threads"` explicitamente em `create_post` para escrever no
Threads. Omitir `network` mantém X como padrão para clientes existentes;
nunca publique em outra rede por inferência. Threads aceita um post
único de até 500 caracteres e uma imagem PNG ou JPEG. A exclusão de posts
publicados no Threads ainda não está disponível. Depois de publicar, use apenas
o permalink devolvido pela rede; se `url` vier null, informe que a publicação
foi confirmada, mas o link ainda não pôde ser consultado.

## Fluxo canônico

```
create_post (briefing [+ imagem])  ->  edit_post (instrução, quantas vezes precisar)
                                   ->  publish_post (agora, ou com "at" para agendar)
```

- Antes de agir, confirme a identidade retornada por `start_here` ou `whoami`. O nome do conector e a conta ativa na web não comprovam a identidade desta conexão.
- Para duas contas, mantenha duas conexões com URLs distintas (por exemplo, `/mcp/pessoal` e `/mcp/empresa`) e escolha a conta em cada consentimento. O sufixo é um rótulo; ele não concede acesso.
- Se `whoami` não aparecer, atualize as ferramentas do conector e abra uma nova conversa; `start_here` também devolve a identidade. Nunca tente inferi-la pelo conteúdo dos rascunhos.
- `whoami` diz em nome de quem a conexão age: a conta Otto autenticada e os
  perfis sociais vinculados (rede, nome, @handle/ID e se a conexão está ativa,
  expirada ou ausente). Toda tool age por essa conta; não há troca de conta pelo
  MCP. Chame quando o usuário perguntar "qual conta está conectada?" ou quando
  houver dúvida sobre o perfil de destino de uma publicação.
- Uma página de empresa do LinkedIn é um destino separado do perfil pessoal.
  `whoami` lista as páginas já conectadas. Em `create_post`, envie
  `linkedinOrganizationId` para publicar como a página; omitir mantém o perfil
  pessoal. Num rascunho que já existe, `set_linkedin_page` troca o destino.
  O Otto não publica no perfil pessoal se a página falhar, estiver desconectada
  ou a permissão for recusada.
- `create_post` devolve, além do `post`, a `url` que abre o rascunho no
  Otto para revisão. Repita esse link ao usuário quando ele pedir; a interface
  não tem busca por id. A `url` exige login na conta e **não** é o link público.
- `list_drafts` recupera `postId` de rascunhos anteriores (só `draft`).
- `list_scheduled_posts` responde "o que está agendado?": a fila em ordem de
  publicação, com `scheduledAt` (UTC), `scheduledAtLocal` + `timezone` (o
  fuso da conta) e `url`, o link que abre aquele item no Otto. Repita horário
  local e link ao usuário. A `url` exige login na conta dele e **não** é o link
  público da publicação — esse só existe depois que a rede confirma. Pagine com
  `cursor` quando `nextCursor` vier preenchido.
- `attach_image` só é necessário para adicionar ou trocar mídia de um rascunho
  que já existe. Em um post novo, use `create_post`.
- `publish_post` e `delete_post` são ações destrutivas: a confirmação é
  responsabilidade do cliente MCP, não do Otto.

## Tools por escopo

A credencial concede escopos, e **tools fora do escopo simplesmente não
aparecem** em `tools/list`. Uma tool ausente não é um bug do servidor: é
permissão não concedida. Peça ao usuário para revisar a conexão em
**Configurações → MCP**. A única exceção é `get_more_tools`, quando aparece:
é telemetria de uso, não concede capacidade nenhuma e não substitui a revisão
dos escopos.

| Escopo | Tools |
| --- | --- |
| `content:read` | `list_drafts`, `list_scheduled_posts`, `see_metrics`, `list_memories`, `get_voice_profile` |
| `drafts:write` | `create_post`, `edit_post`, `rename_post`, `attach_image`, `set_linkedin_page`, `save_memory`, `edit_memory`, `delete_memory`, `update_voice_profile`, `rebuild_voice_profile` |
| `publishing:execute` | `publish_post`, `delete_post` |

`start_here` está sempre disponível, qualquer que seja o escopo.
`whoami` está sempre disponível também: saber em nome de quem se age precede
qualquer ação.

## Blog público (administradores)

As tools do blog só aparecem para administradores do Otto e ainda exigem o
escopo correspondente. Ter um escopo de escrita não concede acesso de admin.
O blog recebe conteúdo editorial pronto em Markdown; o fluxo de briefing acima
se aplica aos posts sociais.

- Leitura: `list_blog_posts`, `get_blog_post`, `list_blog_categories`, `list_blog_authors`.
- Escrita: `create_blog_category`, `update_blog_category`, `create_blog_author`, `update_blog_author`, `create_blog_post`, `update_blog_post`, `add_blog_image`.
- Publicação: `publish_blog_post`, `unpublish_blog_post`, `delete_blog_post`.

Crie categoria e autor, depois o artigo (sempre rascunho). Adicione a capa e as
imagens, insira no corpo os snippets devolvidos por `add_blog_image`, revise com
`get_blog_post` e publique explicitamente. O slug permanece estável ao editar.
Editar um artigo publicado altera o site imediatamente. Despublicar também
remove o acesso público às imagens; não há prévia pública de rascunhos.

Use um resumo direto, títulos H2/H3 no corpo, autoria real, datas e fontes
verificáveis. Não invente estatísticas, credenciais ou referências. Canonical,
JSON-LD, sitemap, RSS e Markdown público são derivados do artigo; não inclua
esses blocos manualmente no corpo. Isso não garante indexação ou citações por IA.

Imagens: URL HTTPS pública ou bytes originais em base64 (informe `mimeType`)
ou data URL. Para gerar uma imagem, use a ferramenta do agente e depois envie
o arquivo. Caminhos locais do cliente não são caminhos do servidor remoto.

## Veredito "Isso pode soar como IA"

`create_post` e `edit_post` devolvem `verification`: a mesma análise que o
chat do Otto mostra no card do rascunho, produzida pelas mesmas regras. O campo
está **sempre** presente:

- `verdict`: o título do aviso, exatamente `"Isso pode soar como IA"`, ou
  `null` quando o Otto verificou e não encontrou nada.
- `findings`: um item por trecho marcado — `excerpt` (o trecho literal, como
  está no texto), `message` (a explicação em pt-BR), `itemIndex` (qual item
  da thread), `rewriteInstruction` (o que pedir para corrigir só aquele
  achado) e `isRule` (`false` quando o achado é desvio do estilo medido no
  perfil do autor, sem trecho). Lista vazia = rascunho limpo.

É aviso, não bloqueio: quem decide se o texto soa como IA é o usuário. Quando
`verdict` não for `null`, mostre o aviso e os trechos e ofereça corrigir;
para corrigir, chame `edit_post` com a `rewriteInstruction` do achado — nunca
reescreva o post por conta própria (regra 2).

## Imagens

Envie o **arquivo original**, com resolução e qualidade preservadas: URL HTTPS de
download (inclusive URL assinada temporária) ou base64 dos bytes originais, até
10 MiB. Informe uma fonte só.

Não redimensione, não comprima, não converta para JPEG e não use a miniatura que
o host gerou para visão. Se você só tem a miniatura, ou o original passa de
10 MiB, explique a limitação e peça um arquivo compatível — nunca reduza a
qualidade em silêncio nem afirme que a imagem foi anexada.

Limites das redes: o X aceita até quatro imagens por item (PNG, JPEG, WebP,
GIF); o LinkedIn aceita até 20 imagens por post e não aceita WebP nem thread.

## Agendamento

Em `publish_post`, prefira o horário local do usuário **sem fuso**
(`"2026-09-03T18:00"`): ele é resolvido no fuso configurado na conta. Um ISO com
fuso (`"...Z"`, `"...-03:00"`) também é aceito. O parâmetro `timezone` só é
necessário para forçar um fuso IANA diferente do da conta.

A resposta traz `scheduledAtLocal` e `timezone`: repita esses valores ao
usuário para que uma conversão errada apareça em vez de passar despercebida.

Se já houver post da **mesma rede** a menos de 10 minutos do horário pedido, a
tool falha com `SCHEDULE_SLOT_TAKEN` e devolve em `conflicts` quem ocupa o
horário (`id`, `network`, `scheduledAt`, `text`). Mostre esse post ao
usuário e proponha outro horário. Só se ele insistir em manter os dois, repita a
**mesma** chamada com `conflictAcknowledged: true`. Nunca envie `true` na
primeira tentativa.

## Memória

Quando o usuário pedir para o Otto lembrar, corrigir ou esquecer algo sobre ele
ou sobre como escreve, use `save_memory`, `edit_memory` ou `delete_memory`.
Uma frase curta e autocontida por memória, na terceira pessoa. `list_memories`
mostra o que já existe — confira antes de gravar algo repetido.

Nunca diga que salvou ou apagou sem ter recebido o resultado da tool.

## Perfil de voz

O perfil de voz é o documento que faz o Otto escrever como o usuário; ele vale
para todos os posts, no chat e no MCP. `get_voice_profile` mostra o perfil
atual (descrição do estilo, instruções, regras, regras de segurança, exemplos e
ajustes por rede) e diz explicitamente quando a conta ainda não tem um.

Quando o usuário pedir para mudar **como o Otto escreve em geral** ("quero posts
mais curtos", "pare de usar travessão em tudo"), use `update_voice_profile`.
Cada campo informado **substitui o atual por inteiro** — não é diff — e os
campos omitidos ficam como estão, então leia com `get_voice_profile` antes e
reenvie o campo completo com a alteração. O pedido explícito do usuário já
autoriza a gravação; o retorno lista em `changed` o que foi salvo, e é só
isso que você confirma.

Não confunda com `save_memory` (um fato ou preferência pontual) nem com
`edit_post` (reescreve UM post). Sem perfil, ajustar só `instructions` ou
`rules` falha com `VOICE_PROFILE_NOT_FOUND`: informe `content` para criar
o perfil, ou reconstrua a partir das redes (abaixo).

### Reconstruir a partir das redes

Quando o usuário pedir para o Otto **reaprender a voz dele** pelos posts ("refaz
meu tom de voz", "analisa meus posts de novo"), chame `rebuild_voice_profile`
— é o mesmo botão de **Configurações → Perfil de voz**, sem reconectar nada.
Ela exige X conectado, ou LinkedIn conectado com a URL do perfil; sem isso falha
com `NO_VOICE_SOURCE` e o usuário resolve em **Integrações**. Consome créditos
e **substitui** a descrição do estilo, as regras e os exemplos (inclusive edições
manuais); instruções, regras de segurança e ajustes por rede ficam. Peça só com
pedido explícito do usuário.

A reconstrução é assíncrona. A tool devolve `queued`; acompanhe com
`get_voice_profile`, lendo `analysis`:

| `analysis.status` | Significado | O que fazer |
| --- | --- | --- |
| `queued` / `processing` | Na fila / o worker está lendo as redes e destilando | Espere alguns segundos e consulte de novo. **Não** chame `rebuild_voice_profile` outra vez: com análise ativa ela não inicia outra (`alreadyRunning: true`). |
| `ready` com `completed: true` | O perfil em `profile` veio desta análise | Confirme ao usuário. Só aqui. |
| `ready` com `completed: false` | A última análise terminou, mas o perfil atual não é dela | Não confirme; confira `profile.generatedAt`. |
| `needs_input` | As redes não tinham posts suficientes; o perfil anterior foi mantido | Sugira publicar mais, ou descrever o estilo com `update_voice_profile`. |
| `failed` | Falhou; o perfil anterior foi mantido | Repita `error` ao usuário. Não invente uma voz. |
| `stale: true` | Ficou em `processing` sem sinal de vida | Chame `rebuild_voice_profile` de novo. |

## Parece erro, mas não é

Estas respostas são decisões do produto, não falhas do servidor. Fora a linha da
publicação em andamento, repetir a mesma chamada não muda o resultado: mude a
ação, ou devolva a decisão ao usuário.

| Resposta | O que aconteceu | O que fazer |
| --- | --- | --- |
| `Muitos pedidos em pouco tempo` | Limite por minuto atingido (120 chamadas no total; 10 a 20 nas tools de escrita, 60 nas de leitura) | Aguarde e faça **uma** nova tentativa. Não entre em laço de retry. |
| `Sua cota acabou` / `Conta suspensa` | Créditos ou assinatura do usuário | Só o usuário resolve, em **Configurações**. Não tente de novo. |
| `Este post ficou marcado como falho` | `failed` é estado terminal | Escreva um post novo. Publicar o mesmo id de novo falha para sempre. |
| `Para agendar, escolha a ação de agendamento` | Instrução de agendamento enviada a `edit_post` | Chame `publish_post` com `at`. |
| `Já existe um post agendado nessa rede` | Horário ocupado na mesma rede (`SCHEDULE_SLOT_TAKEN`); `conflicts` diz qual post | Mostre o post ao usuário e proponha outro horário. Só com a insistência dele, repita com `conflictAcknowledged: true`. |
| `Esse post já saiu do rascunho` | Só `draft` e `approved` aceitam edição | Confira o estado com `see_metrics` passando o `postId` (`list_drafts` só lista rascunhos; agendados estão em `list_scheduled_posts`); crie um post novo se já foi publicado. |
| `Essa versão ficou parecida demais com um post recente` | Guarda anti-repetição do Otto | Mude o gancho, a tese ou o exemplo — não reenvie o mesmo briefing. |
| `LinkedIn não aceita thread` | A rede não suporta o formato | Peça um post único, ou publique a thread no X. |
| `A imagem ultrapassa 10 MiB` | Arquivo grande demais | Peça um original menor. O Otto não reduz a qualidade automaticamente. |
| `Sua conta do X/LinkedIn não está conectada` | Falta a integração | O usuário conecta em **Integrações**. |
| `A publicação ainda está sendo processada` | A rede ainda não confirmou dentro da espera | Aguarde alguns segundos e chame `publish_post` de novo com o mesmo `postId`: em `publishing`/`published` ela não republica, só espera a rede e devolve o link. Se a nova chamada disser que o post ficou marcado como falho, a publicação não aconteceu: escreva um post novo. |

Erros de transporte também têm significado fixo: **401** é credencial inválida ou
expirada (reconecte), **403** com `forbidden` é conta sem acesso liberado ao
produto, **403** com `forbidden_origin` é o `Origin` do cliente fora da lista
permitida do servidor (configuração, não conta), e token em query string é
recusado de propósito — use o header
`Authorization: Bearer …`.

## Conexão

Endpoint: `https://mcp.otto.doti.gg/mcp` (Streamable HTTP). Clientes
interativos registram-se sozinhos e abrem o consentimento; automações headless
usam uma chave `otk_…` criada em **Configurações → MCP**, enviada no header
`Authorization`.
