# Servidor MCP do Praeko

> O servidor MCP remoto do Praeko em praeko.app/mcp: 18 ferramentas, chave de API no cabeçalho Bearer ou login OAuth, e confirmação em dois passos antes de publicar ou alterar posts.

Fonte: https://praeko.app/docs/mcp

Um endereço, `https://praeko.app/mcp`, e uma de duas credenciais: a mesma chave de API da REST ou o login OAuth do seu agente. O agente lê estas docs, consulta a sua conta e, com a sua aprovação, cria, agenda, edita e cancela posts. Toda escrita que publica ou muda conteúdo tem dois passos: nada acontece na primeira chamada.

18 ferramentas · Streamable HTTP, só POST · Chave Bearer ou OAuth · Escrita em dois passos

## O que é

O MCP (Model Context Protocol) é o jeito padrão de um agente de IA chamar ferramentas. O servidor do Praeko roda no mesmo endereço da API e não guarda sessão: cada chamada é um `POST` com JSON-RPC e a resposta é um objeto JSON. Por dentro, cada ferramenta chama a mesma rota REST que você chamaria, com o contrato em inglês (`X-Praeko-Contract: en`). Por isso a validação, o escopo, o isolamento por workspace e os erros são os da API, e os nomes de campo são os do [contrato](https://praeko.app/docs/api).

| Uso | Ferramentas |
|---|---|
| Ler as docs | `search_docs`, `read_page`: busca e leitura das rotas (corpo, exemplo, erros) e de guias curtos, na versão atual. |
| Consultar a conta | `list_channels`, `list_media`, `get_media`, `list_posts`, `get_calendar`, `get_insights`, `get_retention`, `get_usage`. |
| Agir, sempre em dois passos | `create_post`, `schedule_post`, `update_post`, `cancel_post`, `upload_media`, `create_idea`. |
| Enviar um arquivo local | `create_media_upload`, `complete_media_upload`: o servidor prepara o envio e o agente roda o `curl` no shell dele (um passo só: criar o espaço não publica nada). |

> **Fora do servidor, de propósito** Webhooks, chaves de API, conexão de canal (o login nas redes), publicar agora, comentários e respostas não existem como ferramenta: ficam no painel e na API. O arquivo nunca passa pela conversa com o agente: se ele tem shell, usa `create_media_upload` (veja "Arquivo local"); sem shell, `upload_media` com uma URL pública.

## Dois servidores MCP: o remoto publica, o local faz o vídeo

Um agente pode usar os dois ao mesmo tempo. Este, `praeko`, é o **remoto**: roda em `praeko.app/mcp` e é o único que cria, agenda, edita e cancela posts. O `praeko-studio` é o servidor **local** do [Praeko Studio](https://praeko.app/docs/studio/mcp) (`@praeko/studio-mcp`): roda na sua máquina por stdio, serve a um agente sem terminal e **nunca publica**. Precisa de Node, de Chrome/Remotion para renderizar e das suas chaves, tudo no seu computador.

O fluxo completo: criar e renderizar o vídeo no servidor local, subir o resultado com `studio_upload` (devolve só o `mediaId`, em duas etapas) e publicar com `create_post` neste servidor, com a sua aprovação. Detalhes e a lista das 14 ferramentas locais em [Studio no Claude Desktop: o servidor MCP local](https://praeko.app/docs/studio/mcp).

## Conectar

O endpoint aceita duas credenciais, sempre em `Authorization: Bearer …` e em toda chamada: uma **chave de API** (`praeko_…`), que você cria e cola na configuração do agente, ou um **token OAuth** (`mcpat_…`), que o agente obtém sozinho depois que você entra e aprova a conexão. Os dois dão as mesmas ferramentas, com o mesmo escopo e a mesma confirmação em dois passos.

**Dados de conexão**

```text
URL            https://praeko.app/mcp
Transporte     Streamable HTTP (só POST; sem sessão, sem stream)
Autenticação   Authorization: Bearer praeko_…   (chave de API, em TODA chamada; nunca na URL)
               Authorization: Bearer mcpat_…    (token OAuth, obtido pelo agente no login)
OAuth          https://praeko.app/.well-known/oauth-protected-resource/mcp
```

| Credencial | Quando usar | Como |
|---|---|---|
| Chave de API | Agentes de terminal e editor que aceitam cabeçalho (Claude Code, Codex, Cursor, Gemini CLI, opencode e outros), automações e o envio de arquivo local pelo shell. | Crie a chave e configure o cabeçalho (abaixo). |
| Login OAuth | Conectores que só falam OAuth (Claude.ai, Claude Desktop e mobile) e qualquer agente que prefira entrar sem colar chave, como o Claude Code. | Informe só a URL; o agente abre a tela de consentimento (abaixo). |

### Com chave de API

Crie a chave no painel (Desenvolvedores, Chaves de API) ou com `praeko key create` (que exige uma chave `admin`). Escopo `read` basta para ler; as ferramentas que escrevem pedem `write` ou `admin`. Guarde a chave em variável de ambiente, nunca em arquivo versionado. Cada agente registra o servidor de um jeito: escolha o seu em [Agentes de IA](https://praeko.app/docs/agents) e copie o bloco pronto.

### Com login (OAuth)

Adicione `https://praeko.app/mcp` no agente sem cabeçalho nenhum. A primeira chamada recebe `401` com `resource_metadata` no `WWW-Authenticate`; o agente descobre o servidor de autorização do Praeko e abre o navegador. Não há chave para copiar nem para vazar.

1. **Adicionar a URL**: No Claude.ai ou no Claude Desktop, um conector personalizado com `https://praeko.app/mcp`. No Claude Code, `claude mcp add --transport http praeko https://praeko.app/mcp` e depois `/mcp`.
2. **Entrar no Praeko**: O navegador abre o painel. Se você ainda não entrou, entra como sempre (Google, senha ou passkey, mais a verificação em duas etapas, se estiver ligada).
3. **Consentimento**: A tela "Conectar {agente} ao Praeko" mostra o nome do aplicativo, o endereço de retorno, o workspace e o acesso (Leitura ou Escrita). Você escolhe e clica em Permitir ou Negar.
4. **Conectado**: O agente volta com o token e passa a chamar as ferramentas. A conexão aparece em Conexões de agentes, no painel.

**Claude Code com login, sem chave**

```bash
# registra o servidor sem cabeçalho; o login acontece no navegador
claude mcp add --transport http praeko https://praeko.app/mcp
# dentro do Claude Code: /mcp, escolha praeko e entre
```

| Ponto | Como funciona |
|---|---|
| Acesso | **Leitura** = escopo `read` (as dez ferramentas de leitura e docs). **Escrita** = escopo `write` (as dezoito). `admin` nunca é concedido por OAuth: chaves de API, canais, membros e configurações ficam fora do alcance do agente. O cliente pede um teto e a tela deixa você escolher esse acesso ou menos; sem pedido, vale Leitura. |
| Escopo efetivo | O menor entre o que você concedeu e o que o seu papel no workspace permite hoje. Quem perde poder no workspace perde também no agente. |
| Tokens | Token de acesso `mcpat_…` vale 1 hora; o de renovação `mcprt_…`, 30 dias, e é trocado a cada uso. Reapresentar um token de renovação já usado revoga a conexão inteira (o agente precisa entrar de novo). O servidor guarda só o hash. |
| Onde o token vale | Só no `/mcp`. A REST e o painel recusam o token OAuth; para eles, use uma chave de API. |
| Gerenciar e revogar | No painel, em Desenvolvedores, Chaves de API, seção **Conexões de agentes**: aplicativo, acesso, pessoa e último uso, com o botão Revogar, que corta o acesso na hora. Cada pessoa vê e revoga as próprias conexões; o admin do workspace vê e revoga todas. Autorizar de novo o mesmo aplicativo substitui a conexão anterior. |
| Padrões | OAuth 2.1 só com cliente público: código de autorização com PKCE `S256` obrigatório e `resource` (RFC 8707). Descoberta por Protected Resource Metadata (`/.well-known/oauth-protected-resource/mcp`) e pelos metadados do servidor de autorização (`/.well-known/oauth-authorization-server`). Registro de cliente por Client ID Metadata Document (o `client_id` é uma URL `https`) ou por registro dinâmico (RFC 7591, `POST /oauth/register`). A resposta de autorização traz `iss` (RFC 9207). |
| Endereço de retorno | `https`, ou `http` só em `localhost`, `127.0.0.1` e `[::1]` (porta livre, como pede a RFC 8252). Esquemas próprios como `cursor://` e `vscode://` são recusados: nesses clientes, use a chave de API. |

> **Cabeçalho fixo ganha do login** No Claude Code, um `Authorization` configurado tem precedência sobre o OAuth: se a chave for recusada, ele não cai para o login. Para usar o login, registre o servidor sem `--header`. Com o ChatGPT, o login por OAuth ainda não foi conferido na documentação da OpenAI.

### Respostas de autenticação

| Situação | Resposta |
|---|---|
| Sem `Authorization` | `401` com `WWW-Authenticate: Bearer realm="Praeko MCP", scope="read write", resource_metadata="https://praeko.app/.well-known/oauth-protected-resource/mcp"`. |
| Chave ou token desconhecido, revogado ou vencido | `401` com `error="invalid_token"` e o mesmo `resource_metadata` no `WWW-Authenticate`. |
| Escopo insuficiente para a ferramenta | `403` com `WWW-Authenticate: Bearer error="insufficient_scope", scope="write"` (e o `resource_metadata`). |
| Cookie do painel | Não vale aqui: só chave de API ou token OAuth. |
| `GET`, `DELETE` ou outro método | `405` com `Allow: POST`. |
| `Origin` que não é do Praeko nem localhost | `403` (proteção contra DNS rebinding). Cliente de linha de comando não manda `Origin`. |
| Passou de 300 requisições por minuto | `429`. O limite é o mesmo da REST, contado por chave de API ou por conexão OAuth. |

## Ferramentas

Dezoito ferramentas. A lista que o agente recebe em `tools/list` varia com o escopo: `read` vê as dez de leitura e docs; `write` e `admin` veem as dezoito (o servidor não expõe nada a mais para `admin`: evite `admin` em agente). A coluna "O agente recebe" é a descrição literal, em inglês, que o servidor entrega; o texto de dois passos que fecha as seis ferramentas que publicam ou mudam conteúdo está logo depois da tabela. `create_media_upload` e `complete_media_upload` são as únicas escritas sem dois passos (veja "Arquivo local").

| Ferramenta | Escopo e anotações | O agente recebe | Argumentos |
|---|---|---|---|
| `search_docs` | (read) somente leitura | Search the Praeko API documentation (routes, fields, enums, errors, guides). Returns the best pages with a snippet; open one with read_page. Use it before building a create_post or update_post body. | `query` (obrigatório, até 200 caracteres), `limit` (1 a 20, padrão 8) |
| `read_page` | (read) somente leitura | Read one documentation page in full (a route with its body schema, example and errors, or a guide). Ids come from search_docs: `route:posts.create`, `guide:overview`, `guide:mcp-two-step`... | `id` (obrigatório) |
| `list_channels` | (read) somente leitura | List the connected channels (one item per account; `default` marks the one used when a post item has no `channel`), with state and slots count. Use the channel ids in create_post. | nenhum |
| `list_media` | (read) somente leitura | List the media library (last 100 items): id, type, name, state, size and duration. Use a ready media id in create_post. | `type` (`video`, `cover` ou `image`) |
| `get_media` | (read) somente leitura | One media item of the library: state (uploading or ready), type, size and, when ready, publicUrl, durationMs, width and height. Use it to check a media id (for example after complete_media_upload) before create_post or schedule_post. | `mediaId` (obrigatório) |
| `list_posts` | (read) somente leitura | List posts, newest first (queue and drafts nearest first). Filters: state, channel, tag. | `state` (`queued`, `draft`, `sent`, `error`, `canceled`), `channel`, `tag`, `limit` (1 a 200), `offset` |
| `get_calendar` | (read) somente leitura | Scheduled items in a period plus the free weekly slots of each channel (default: next 14 days; at most 100 days). | `from`, `to` (ISO-8601 com fuso), `channel`, `tag` |
| `get_insights` | (read) somente leitura | Cards, top posts, per-channel results and a summary for a period (views, likes, comments, followers), optionally compared with the previous period. | `from`, `to`, `channel`, `tag`, `type`, `compare`, `sort`, `direction`, `page`, `limit` |
| `get_retention` | (read) somente leitura | Audience retention, impressions and CTR per post of the period (YouTube) and reach/watch time (Instagram). A missing number is null, never 0; `channels[].state` says whether each account can deliver them. | `from`, `to`, `channel`, `limit` |
| `get_usage` | (read) somente leitura | API usage of this workspace over the last N days (total, errors, p95, peak per minute vs the limit, by day and by key). MCP calls appear with the route prefix `mcp:`. | `days` (1 a 30, padrão 7) |
| `create_post` | (write) escreve, rede externa | Create a post with one item per network. Items are drafts by default or take the next free slot of the channel (queue: true). For a fixed publication time use schedule_post. For a single-photo Instagram post send kind image with media.images (one JPEG image media, optional alt text). Never publishes immediately. TWO-STEP (parágrafo final comum às seis ferramentas de dois passos, logo abaixo da tabela) | `media`, `texts`, `items` (obrigatórios); `kind` (`video` ou `image`), `title`, `baseText`, `queue`, `draft`, `options`, `tags`, `confirm`. Com `kind: image`, `media.images` leva a foto (uma). **Recusa `scheduledFor`** |
| `schedule_post` | (write) escreve, rede externa | Create a post and publish it at a fixed time per item (scheduledFor, ISO-8601 with timezone: ALWAYS state the BRT time to the user as well; the panel runs on America/Sao_Paulo). The post goes out by itself at that time on the real account. For a single-photo Instagram post send kind image with media.images. TWO-STEP (parágrafo final comum às seis ferramentas de dois passos, logo abaixo da tabela) | os de `create_post`, mas todo item exige `scheduledFor`; sem `queue` nem `draft` |
| `update_post` | (write) escreve, destrutiva, idempotente, rede externa | Edit a post: title, texts, media, tags, items (mode, scheduledFor, queue, draft) or moveTo draft/queue. Send only what changes. WHAT CAN CHANGE, per network and when: (1) Any item still pending (draft or scheduled, not yet sent): time, mode, texts, queue/draft. (2) YouTube item ALREADY UPLOADED (the video sits in the channel as private with a future publish time, state "scheduled", or private without a time): it can still be RESCHEDULED (scheduledFor) and its title, description (texts.youtube), videoTags, categoryId and a not-yet-posted pinnedComment can change, until the video goes live. The server checks live on YouTube first; if the video is already public, the scheduled time already passed, the video was deleted, or Praeko did not upload it, nothing is written and the error names the reason and the manual alternative (YouTube Studio: https://studio.youtube.com/video/<videoId>/edit). The new time must be at least 5 minutes ahead; asking for the same time again changes nothing. A YouTube video never becomes public by this tool. (3) Instagram Reels in Buffer: text, time and draft only. (4) TikTok/Instagram items already sent or in the TikTok inbox cannot change. A request touching several items applies each one independently and the result lists `itemResults` (applied or not, and why): read it, nothing is skipped silently. TWO-STEP (parágrafo final comum às seis ferramentas de dois passos, logo abaixo da tabela) The preview lists, per item, "will_change" or "cannot_change" with the reason. | `postId` (obrigatório); `title`, `texts`, `media`, `tags`, `items` (`id` ou `network`, `mode`, `scheduledFor`, `queue`, `draft`, `videoTags`, `categoryId`), `moveTo` (`draft` ou `queue`), `confirm` |
| `cancel_post` | (write) escreve, destrutiva, idempotente, rede externa | Cancel the pending items of a post (they will not be published; Instagram items scheduled in Buffer are removed there). Items already sent are kept. IMPORTANT for YouTube: a video Praeko already uploaded and scheduled (private with a future publish time) is NOT cancelled by this tool: it stays and YouTube will still publish it on time (the result says so). To stop it, either reschedule it with update_post, or manage it in YouTube Studio (https://studio.youtube.com/video/<videoId>/edit: remove the schedule to keep it private, or delete it). TWO-STEP (parágrafo final comum às seis ferramentas de dois passos, logo abaixo da tabela) | `postId` (obrigatório), `confirm` |
| `upload_media` | (write) escreve, rede externa | Add a video, a cover or the photo of an image post (type image: JPEG only) to the media library by giving a PUBLIC https URL that the Praeko server downloads (video/mp4, video/quicktime, image/png, image/jpeg; up to 50 MB; private networks are refused). Files are never sent through MCP: for larger files use the REST/CLI upload and then use the media id. TWO-STEP (parágrafo final comum às seis ferramentas de dois passos, logo abaixo da tabela) | `url` e `type` (`video`, `cover` ou `image`) obrigatórios; `name`, `confirm` |
| `create_media_upload` | (write) escreve, rede externa | Use this to publish a video, cover or photo (type image, JPEG) that is a file on the user's machine (you need a shell; with no shell, use upload_media with a public URL). The file never goes through MCP: this tool only creates the media slot and returns the exact upload command (curl; a loop of parts above 50 MiB). Steps: 1) call this with the real size in bytes (wc -c < file), type (video, cover or image), mime (video/mp4, video/quicktime, image/png, image/jpeg; an image type takes only image/jpeg); 2) run upload.command in the shell after replacing /path/to/file; the API key is read from the shell environment (PRAEKO_KEY): NEVER ask for the key, paste it in the chat, print it or write it to a file; 3) call complete_media_upload with the mediaId; 4) use that media id in create_post or schedule_post (their two-step preview and confirmation still protect the publication). Creating the slot publishes nothing, so there is no confirmation code. Limits: video up to 2 GiB, cover and image up to 20 MiB (Instagram itself takes photos up to 8 MB: that is checked when the post is created), 20 pending uploads per workspace, 60 per hour; an upload not finished in 24 hours is discarded. | `type` (`video`, `cover` ou `image`), `name`, `mime` (`video/mp4`, `video/quicktime`, `image/png`, `image/jpeg`) e `size` em bytes, todos obrigatórios. **Sem `confirm`**: criar o espaço de envio não publica nada |
| `complete_media_upload` | (write) escreve, idempotente | Call this after running the upload command from create_media_upload. It finishes the upload (joins the parts of a large file; a single-PUT file is already complete), verifies it and returns status ready or uploading, with the media record: publicUrl, durationMs, width and height (read from the video header). Safe to call again. Then use the media id in create_post or schedule_post. | `mediaId` (obrigatório). **Sem `confirm`** |
| `create_idea` | (write) escreve | Add a card to the idea board (the "Create" screen). Nothing is published. TWO-STEP (parágrafo final comum às seis ferramentas de dois passos, logo abaixo da tabela) | `title` (obrigatório, até 200 caracteres); `note`, `group` (`idea`, `scripting`, `ready`, `posted`), `tags`, `confirm` |

**Parágrafo final das seis ferramentas com dois passos**

```text
TWO-STEP: the first call (without `confirm`) executes NOTHING: it returns a preview and a single-use `confirmation_code` (10 minutes). Show the preview (action, channels, time in BRT and UTC, media, text, warnings) to the user and WAIT for explicit approval. Only then call this tool again with the SAME arguments plus `confirm: "<code>"`. Changing any argument invalidates the code.
```

As anotações vão em `tools/list` (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`) e a coluna as escreve em palavras: "somente leitura" é `readOnlyHint`, "destrutiva" é `destructiveHint`, "idempotente" é `idempotentHint` e "rede externa" é `openWorldHint` (o que fala com uma rede social ou baixa da internet). O cliente decide se confia nelas: a especificação manda tratá-las como dicas.

### O que cada escrita faz

- `create_post`: um post com um item por rede. Sem `draft` nem `queue`, nasce como **rascunho**; com `queue: true`, pega a próxima vaga livre do canal. **Nunca publica na hora** e recusa `scheduledFor`.
- `schedule_post`: o mesmo post, mas `scheduledFor` (ISO-8601 com fuso) é obrigatório em todo item e vale o horário fixo. Sem `queue` nem `draft`. O post sai sozinho, na conta real.
- `update_post`: edita o que ainda não saiu (título, textos, mídia, etiquetas, itens, `moveTo`). Um vídeo do YouTube que o Praeko já subiu e que ainda está privado ou agendado também muda de horário, título, descrição, `videoTags`, `categoryId` e comentário fixo ainda não postado; modo, fila e rascunho não mudam nele. O servidor confere ao vivo no YouTube antes de escrever e, se o vídeo já está público, o horário passou ou o vídeo sumiu, não escreve e o erro traz o motivo e o link do YouTube Studio. O novo horário precisa estar pelo menos 5 minutos à frente, e a ferramenta nunca torna um vídeo público. A prévia lista por item `will_change` ou `cannot_change` (em `itemChanges`, com o motivo); a resposta traz `itemResults`, item a item. Mande só o que muda.
- `cancel_post`: cancela os itens pendentes; o que já saiu fica. **Não** impede a publicação de um vídeo do YouTube já subido e agendado (o YouTube o publica na hora marcada; a prévia e a resposta avisam): mude o horário com `update_post` ou resolva no YouTube Studio. A ferramenta não tem `dismiss` nem apaga vídeo; pela REST, `DELETE /v1/posts/{id}?dismiss=true` tira o horário (o vídeo fica privado) e `dismiss=true&delete_video=true` apaga o vídeo privado.
- `upload_media`: baixa uma URL pública `https` para a biblioteca (veja abaixo).
- `create_idea`: um cartão no quadro de ideias. Nada é publicado.

## Escrita em dois passos

A confirmação é do servidor, não do agente: mesmo que o agente esteja configurado para aprovar tudo, uma ferramenta de escrita nunca executa na primeira chamada. Vale igual para chave de API e para login OAuth.

1. **Chamada sem confirm**: O servidor valida e devolve a prévia. Nada é gravado, nada é enviado.
2. **A prévia**: Ação exata, canais com id e nome, horário em BRT e UTC, mídia encontrada ou não, textos e avisos, mais um `confirmation_code`.
3. **Você aprova**: O agente mostra a prévia e espera o seu "sim".
4. **Chamada com confirm**: Os mesmos argumentos mais `confirm: "<código>"`. Só agora executa.

**Passo 1: o agente chama (sem confirm)**

```json
{
  "name": "schedule_post",
  "arguments": {
    "title": "O teste que só falhava na segunda",
    "media": {
      "video": "01KA3VQ7M2T9XE4HB8N5RZ6C0D"
    },
    "texts": {
      "youtube": {
        "title": "O teste que só falhava na segunda",
        "text": "…"
      }
    },
    "items": [
      {
        "network": "youtube",
        "mode": "scheduled",
        "scheduledFor": "2026-11-13T20:30:00Z"
      }
    ]
  }
}
```

**Passo 1: a prévia (nada foi gravado)**

```json
{
  "preview": true,
  "executed": false,
  "tool": "schedule_post",
  "action": "Create a post and schedule it at fixed times",
  "title": "O teste que só falhava na segunda",
  "items": [
    {
      "network": "youtube",
      "channel": {
        "id": "…",
        "name": "Meu canal",
        "connected": true
      },
      "mode": "network default",
      "timing": "fixed_time",
      "scheduledFor": {
        "utc": "2026-11-13T20:30:00.000Z",
        "brt": "2026-11-13 17:30 (BRT, UTC-03:00)"
      }
    }
  ],
  "media": [
    {
      "id": "01KA3VQ7M2T9XE4HB8N5RZ6C0D",
      "found": true,
      "type": "video",
      "state": "ready"
    }
  ],
  "warnings": [
    "Confirming schedules a REAL publication on the connected account(s) at the time above. It can be edited or cancelled until then (update_post, cancel_post)."
  ],
  "confirmation_code": "K7MQ-2VXZ-9RT4",
  "expires_at": "2026-11-13T12:10:00.000Z",
  "expires_in_seconds": 600,
  "next_step": "NOTHING was done yet. Show this preview to the user and wait for approval. …"
}
```

**Passo 2: o agente repete com o código**

```json
{
  "name": "schedule_post",
  "arguments": {
    "title": "O teste que só falhava na segunda",
    "media": {
      "video": "01KA3VQ7M2T9XE4HB8N5RZ6C0D"
    },
    "texts": {
      "youtube": {
        "title": "O teste que só falhava na segunda",
        "text": "…"
      }
    },
    "items": [
      {
        "network": "youtube",
        "mode": "scheduled",
        "scheduledFor": "2026-11-13T20:30:00Z"
      }
    ],
    "confirm": "K7MQ-2VXZ-9RT4"
  }
}
```

**Passo 2: resultado**

```json
{
  "preview": false,
  "executed": true,
  "tool": "schedule_post",
  "result": {
    "id": "…",
    "items": [
      {
        "network": "youtube",
        "state": "scheduled"
      }
    ]
  }
}
```

### O código de confirmação

| Propriedade | Valor |
|---|---|
| Formato | `XXXX-XXXX-XXXX`: 12 caracteres aleatórios (cerca de 58 bits), por exemplo `K7MQ-2VXZ-9RT4`. |
| Validade | 10 minutos (`expires_in_seconds: 600`). |
| Uso | Único. Duas confirmações ao mesmo tempo: só uma executa. |
| Preso a | O workspace, a credencial que pediu a prévia (a chave de API ou a conexão OAuth: renovar o token não invalida o código), a ferramenta e os argumentos (SHA-256 do JSON canônico: a ordem das chaves não importa). |
| Guardado como | Só o hash, no banco. |
| Recusado pela validação | Se a rota recusar o pedido (4xx, nada foi gravado), o código volta a valer até vencer: o usuário corrige o que faltava (a mídia ainda subindo, por exemplo) e confirma de novo. |

Mudar qualquer argumento, repetir o código, usar um vencido ou de outra credencial, ferramenta ou workspace falha. A resposta é um erro de execução (`isError: true`) com `error.code` `confirmation_invalid` e o motivo em `error.reason` (a `message` vem em inglês):

| `reason` | Significa | O que o agente faz |
|---|---|---|
| `unknown_code` | O código não existe neste workspace. | Chamar sem `confirm` para uma prévia nova. |
| `already_used` | O código já foi usado. | Pedir uma prévia nova se for repetir a ação. |
| `expired` | Passaram os 10 minutos. | Pedir uma prévia nova e mostrar de novo ao usuário. |
| `wrong_key` | O código foi emitido para outra chave de API ou outra conexão OAuth. | Pedir uma prévia com a credencial atual. |
| `wrong_tool` | O código é de outra ferramenta. | Usar a ferramenta que gerou o código. |
| `arguments_changed` | Os argumentos diferem dos que foram vistos na prévia. | Pedir uma prévia nova com os argumentos finais. |

**Exemplo de erro**

```json
{
  "error": {
    "code": "confirmation_invalid",
    "message": "this confirmation code expired (10 minutes). Request a new preview and show it to the user again.",
    "reason": "expired"
  }
}
```

## `upload_media`: mídia por URL

A ferramenta recebe uma URL pública `https` e o servidor baixa o arquivo. O arquivo nunca passa pela conversa com o agente.

| Regra | Valor |
|---|---|
| Tipos | `video`: `video/mp4`, `video/quicktime`. `cover`: `image/png`, `image/jpeg`. `image` (a foto de um post de imagem): só `image/jpeg`. O tipo é conferido no download (`unsupported_mime`). |
| Tamanho | Até 50 MB, conferido no cabeçalho e durante o download. Acima disso, `file_too_large`: use o envio de arquivo local abaixo, ou o envio em partes da REST, do CLI ou do SDK. |
| Redirecionamentos | Até 3 saltos, e cada salto passa de novo pela mesma verificação. |
| Endereços recusados | Só `https`, sem usuário e senha, sem `localhost`, nome interno, IP privado, reservado ou de metadados de nuvem (a mesma barreira dos webhooks). |

O Worker não resolve o DNS antes de baixar: contra um nome público que resolva para IP privado, a proteção é a própria rede da Cloudflare, que não alcança a rede privada de ninguém.

## Arquivo local: `create_media_upload` e `complete_media_upload`

O caso mais comum de um agente com shell (Claude Code, Codex, Gemini CLI, opencode, Cursor, Kimi, Cline): o vídeo está no disco. O MCP não carrega arquivo grande, então o servidor só prepara o envio e quem envia é o `curl` do próprio agente, direto para a API, com a chave que já está no ambiente do usuário.

1. **create_media_upload**: O agente informa `type`, `name`, `mime` e o `size` exato em bytes (`wc -c < arquivo`). O servidor cria a mídia e devolve `mediaId`, o comando `curl` pronto e a especificação HTTP.
2. **Rodar o comando**: O agente roda `upload.command` no shell. A chave vem de `$PRAEKO_KEY` (exporte a partir de `PRAEKO_API_KEY` se for o nome que você usa) e nunca aparece na conversa.
3. **complete_media_upload**: Junta as partes (arquivo grande), confere e devolve `status: ready` com `publicUrl`, `durationMs`, `width` e `height`. Pode ser repetida; `get_media` consulta o estado.
4. **create_post ou schedule_post**: Com o `mediaId`. Estes continuam em dois passos: prévia, o seu "sim", código.

| Tamanho | Como o arquivo sobe |
|---|---|
| Até 50 MiB | Um `PUT` em `/v1/media/{id}/content`, com o corpo igual ao arquivo. |
| Acima de 50 MiB (vídeo até 2 GiB) | Partes de 64 MiB em `PUT /v1/media/{id}/parts/{n}` (todas iguais, a última menor; cada parte pode ser reenviada). O comando devolvido já é o laço com `dd` e `curl`; a resposta traz também a lista de partes com a URL de cada uma. |

**Resposta de create_media_upload (arquivo até 50 MiB)**

```json
{
  "executed": true,
  "tool": "create_media_upload",
  "upload": {
    "mediaId": "01KB7J3V9Q2W8E5R1T6Y4N0M3P",
    "state": "uploading",
    "expectedSize": 48213377,
    "mime": "video/mp4",
    "expiresAt": "2026-11-08T12:00:00.000Z",
    "keyHandling": "The API key is already in the user's shell environment as PRAEKO_KEY (…)",
    "method": "single_put",
    "http": {
      "method": "PUT",
      "url": "https://praeko.app/v1/media/01KB7J3V9Q2W8E5R1T6Y4N0M3P/content",
      "headers": {
        "Authorization": "Bearer $PRAEKO_KEY",
        "Content-Type": "video/mp4"
      },
      "body": "the raw bytes of the file (exactly 48213377 bytes)"
    },
    "command": "curl -sS --fail-with-body -X PUT -H \"Authorization: Bearer $PRAEKO_KEY\" -H \"Content-Type: video/mp4\" -H \"Expect:\" -T \"/path/to/file\" \"https://praeko.app/v1/media/01KB7J3V9Q2W8E5R1T6Y4N0M3P/content\""
  },
  "next_step": "Run `upload.command` in the shell (replace /path/to/file; the key comes from the environment, do not print it), then call complete_media_upload with this mediaId, then create_post or schedule_post with media.video = mediaId (those keep their two-step preview/confirmation)."
}
```

### Por que sem código de confirmação

Criar o espaço de envio não publica nada, não envia nada para uma rede e não altera post. O que protege a conta é o passo seguinte, `create_post` ou `schedule_post`, que segue em dois passos. Por isso estas duas ferramentas são de um passo só, com limites no lugar do código:

| Limite | Valor |
|---|---|
| Escopo | `write` (a lista de quem tem só `read` não mostra as duas). |
| Tipos | `video/mp4`, `video/quicktime` (vídeo), `image/png`, `image/jpeg` (capa) e `image/jpeg` (imagem); o tipo tem de combinar com `type`. |
| Tamanho | Vídeo até 2 GiB, capa e imagem até 20 MiB. O servidor recusa o envio cujo tamanho difira do declarado (`size_mismatch`). |
| Envios pendentes | No máximo 20 por workspace (`too_many_pending_uploads`, com os ids pendentes) e 60 criações por hora (`rate_limited`). |
| Validade | Um envio que não terminou em 24 horas é descartado (o arquivo parcial é apagado e a mídia some). |
| Isolamento | A mídia é do workspace da credencial; outro workspace recebe `media_not_found`. |
| A chave | O `curl` usa uma chave de API com escopo `write`, lida do ambiente do shell (`$PRAEKO_KEY`). As ferramentas nunca devolvem a chave, só o marcador `$PRAEKO_KEY`, e mandam o agente não pedir, não imprimir e não colar a chave na conversa. |
| Com login OAuth | O token OAuth só vale no `/mcp`, e o `curl` fala com a REST: mesmo conectado por login, o shell precisa de uma chave `write` em `$PRAEKO_KEY`. Sem ela, use `upload_media` com uma URL pública. |

> **O cliente pode pedir para aprovar o comando** O Claude Code pede aprovação para um comando de shell que usa uma variável como `$PRAEKO_KEY` e, sem interação (`claude -p`), recusa o que não estiver liberado nas permissões. Aprove o `curl` na sessão ou libere-o nas permissões. O comando devolvido usa só `$PRAEKO_KEY`, sem a forma `${VAR:-…}`, que o Claude Code bloqueia. Isso é regra do cliente, não do Praeko.

> **Agente sem shell** Conectores do Claude.ai, do Claude Desktop e do ChatGPT não têm shell nem acesso ao disco: use `upload_media` com uma URL pública `https` (até 50 MB). Sem URL, suba pelo CLI (`praeko post`), pelo SDK (`uploadFile`) ou pela REST e entregue o id da mídia.

## Protocolo

O endpoint fala duas eras do MCP no mesmo endereço, como a especificação prevê. Os clientes de hoje, na maioria, ainda usam a `2025-11-25`; a `2026-07-28` (a mais nova na data da conferência, 07/11/2026) não tem `initialize` nem sessão.

|  | Era moderna `2026-07-28` | Era antiga `2025-11-25`, `2025-06-18`, `2025-03-26` |
|---|---|---|
| Como é reconhecida | `_meta["io.modelcontextprotocol/protocolVersion"]` no corpo ou `MCP-Protocol-Version: 2026-07-28` | Qualquer outro pedido. Sem cabeçalho de versão vale `2025-03-26`. |
| Abertura | Nenhuma. `server/discover` é opcional. | `initialize`, resposta com a versão pedida (ou `2025-11-25`), `notifications/initialized` (`202`). |
| Cabeçalhos exigidos | `MCP-Protocol-Version` igual ao corpo, `Mcp-Method` igual ao `method`, `Mcp-Name` igual a `params.name` em `tools/call`. Divergência: `400` e JSON-RPC `-32020`. | `MCP-Protocol-Version`, se vier, tem de ser uma versão conhecida (senão `400`). |
| Versão desconhecida | `400` e `-32022`, com `data.supported` e `data.requested`. | Idem. |
| Método desconhecido | `404` e `-32601`. | `200` e `-32601`. |
| Resultado | Traz `"resultType": "complete"`. | Sem `resultType`. |
| Lote JSON-RPC | Recusado (`400`). | Aceito. |

- A resposta é sempre `application/json` (a especificação permite no lugar de SSE). O servidor não emite `Mcp-Session-Id` e não olha o `Accept`. Notificação e resposta do cliente: `202` sem corpo.
- `tools/call` devolve `content` (texto com o JSON) e `structuredContent` (o mesmo objeto). Não há `outputSchema`.
- Ferramenta desconhecida é erro de protocolo (`-32602`). Argumento inválido, regra de negócio e falha da API são erro de execução (`isError: true`, com `structuredContent.error = { code, message, status, details? }`), para o modelo se corrigir.

**Testar com curl (era antiga, sem sessão)**

```bash
curl -s https://praeko.app/mcp \
  -H "Authorization: Bearer $PRAEKO_KEY" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

## Segurança

| Cuidado | Como funciona |
|---|---|
| Confirmação no servidor | Nenhuma escrita que publica ou muda conteúdo executa sem o segundo passo. O código é de uso único, vale 10 minutos e está preso à credencial, à ferramenta e aos argumentos. |
| Menor escopo | Uma chave `read` (ou o acesso Leitura, no login) para o agente que só consulta; `write` só para quem agenda. O escopo é conferido no `/mcp` e de novo na rota REST. |
| Consentimento explícito | A tela de consentimento aparece em toda autorização, mesmo para um aplicativo já conhecido, e destaca quando ele pede mais acesso do que tinha. Ela mostra o endereço de retorno e avisa quando o aplicativo é desconhecido ou só tem endereço local. |
| Texto das ferramentas é dado | As instruções do servidor mandam o modelo tratar títulos, comentários e notas devolvidos como dado do usuário, nunca como ordem. |
| Envio de arquivo local | Só um espaço de envio com limites (20 pendentes, 60 por hora, 24 h de validade, 2 GiB); o arquivo vai do shell do agente direto para a API. Publicar continua em dois passos. |
| Credencial fora do código | Chave em variável de ambiente. Chave e token nunca aparecem em resposta, log ou banco (só o hash). Se uma chave vazou, revogue em Chaves de API; uma conexão, em Conexões de agentes. |
| Rastro | Cada chamada MCP vira uma linha em Uso e log, com a rota `mcp:tools/call create_post`, `mcp:tools/list` etc. e o status; as chamadas por login aparecem com a chave `mcp-oauth`. Filtre pela rota `mcp:` no painel ou com `GET /v1/usage/log?route=mcp%3A`. |

## Clientes conferidos

Testado com o Claude Code real: com chave de API (inicialização, `tools/list`, `list_channels` e a prévia de `create_idea`) e com login OAuth (descoberta, registro pelo documento de metadados do cliente, consentimento, conexão e revogação). Os blocos dos demais agentes em [Agentes de IA](https://praeko.app/docs/agents) vêm da documentação de cada fabricante. O login pelo ChatGPT não foi conferido em fonte primária.
