mcp-x

Fornece à IA a API v2 do X (Twitter) como 42 ferramentas para posts, busca, usuários, listas e upload de mídia. Go, contexto de usuário OAuth 1.0a, stdio e HTTP transmissível. Construído em torno do faturamento pago por uso do X: leituras são cobradas por recurso retornado, então as ferramentas solicitam a menor página que responda à pergunta.

Documentação

mcp-x

Um servidor MCP que dá a um LLM a API do X (Twitter): ler e pesquisar posts, gerenciar usuários e listas, enviar mídia e publicar — como a conta cujas chaves ele usa.

License MIT Go MCP Transport X API v2 OAuth 1.0a Paid API

Site · Custos · Credenciais · Ferramentas · Início rápido · Configuração · Arquitetura · Contribuição


O que é

mcp-x é um servidor Model Context Protocol escrito em Go. Ele expõe a API X v2 a qualquer cliente compatível com MCP (Claude Desktop, agentes de IDE, aplicativos LLM personalizados) como 42 ferramentas que cobrem posts, usuários, listas e mídia.

Ele autentica com contexto de usuário OAuth 1.0a, o que significa que cada chamada age como uma conta X real — aquela à qual as quatro chaves pertencem. x_post_create publica publicamente. x_user_follow realmente segue. x_post_delete é irreversível. Isto não é um sandbox, e não é gratuito: veja A API X custa dinheiro antes de conectá-lo a um agente.

Ambos os transportes suportados pelo SDK MCP estão disponíveis e expõem o mesmo conjunto de ferramentas:

  • stdio — o cliente inicia o binário e conversa via stdin/stdout (o padrão, ideal para clientes desktop).
  • http — um servidor HTTP de streaming de longa duração (útil para implantações remotas/compartilhadas).

A API X custa dinheiro

[!WARNING] Não existe mais camada gratuita. O X aposentou os níveis de assinatura Free/Basic/Pro para novos desenvolvedores e migrou para créditos de pagamento por uso: você compra créditos antecipadamente no Developer Console e cada requisição deduz do saldo em tempo real. As assinaturas legadas Basic ($200/mês) e Pro ($5.000/mês) sobrevivem apenas para contas que já as possuíam; Enterprise começa em cerca de $42.000/mês. Uma conta de desenvolvedor nova hoje recebe pagamento por uso e nada mais.

Tarifas no momento da escrita (preços oficiais — sempre verifique novamente o Console, eles mudaram várias vezes em 2026):

OperaçãoPreço
Leitura de post$0,005 por post retornado
Leitura própria (seus posts, favoritos, seguidores, curtidas, listas)$0,001 por recurso
Leitura de usuário$0,010 por usuário retornado
Leitura de curtidas / mutes / bloqueios$0,001 por recurso
Leitura de seguidores / seguindo$0,010 por recurso
Publicar um post$0,015 por requisição
Publicar um post contendo uma URL$0,200 por requisição
Curtir / repostar e outras interações$0,015 por requisição
Escritas em listas e favoritos$0,005–$0,010 por requisição

Duas coisas decorrem disso, e ambas estão incorporadas ao servidor:

  • Leituras são cobradas por recurso retornado, não por requisição. max_results: 100 em x_posts_search custa vinte vezes o que max_results: 5 custa para a mesma consulta. A descrição de cada ferramenta de leitura diz ao modelo para pedir o menor max_results que responda à pergunta, e as ferramentas de lote (x_posts_lookup, x_users_lookup) dizem a ele para agrupar em lote em vez de fazer loop.
  • x_posts_count não consome o orçamento de leitura de posts. Ele retorna contagens de correspondências agrupadas por minuto/hora/dia para a mesma sintaxe de consulta. Dimensione um tópico com x_posts_count primeiro, depois pague por x_posts_search.

O X também deduplica: o mesmo recurso buscado duas vezes dentro de uma janela UTC de 24 horas é cobrado uma única vez. E o pagamento por uso é limitado a 3 milhões de leituras de posts por ciclo de faturamento — além disso, apenas Enterprise.

Quando o dinheiro acaba, a API responde com um erro distinto, e o servidor o mapeia para uma mensagem que diz explicitamente ao modelo para não tentar novamente — veja Erros.


Credenciais

O servidor precisa de quatro valores OAuth 1.0a, todos de um único app X:

X_API_KEY
X_API_KEY_SECRET
X_ACCESS_TOKEN
X_ACCESS_TOKEN_SECRET

O primeiro par identifica o app; o segundo par identifica a conta que age através dele. mcp-x usa AuthenMethodOAuth1UserContext, não autenticação bearer somente de app, porque todo endpoint de escrita e todo endpoint "eu" (x_users_me, x_posts_home, x_bookmarks_list, menções) exige contexto de usuário. Não existe modo de token bearer.

Obtendo-os — e o passo que todo mundo erra

  1. Vá para developer.x.com → seu projeto → seu app.
  2. Abra Configurações de autenticação de usuário e defina Permissões do app como Leitura e Escrita. Faça isso primeiro.
  3. Somente então vá para Chaves e tokens e gere o Access Token e Secret.

[!IMPORTANT] Um access token mantém permanentemente as permissões que o app tinha no momento em que foi gerado. Se você criou o token enquanto o app estava somente leitura e depois mudou o app para Leitura e Escrita, o token ainda é somente leitura. Nada na página de configurações do app lhe dirá isso. Toda escrita falhará com o tipo de problema oauth1-permissions do X, para sempre, até você voltar a Chaves e tokens e regenerar o Access Token e Secret.

Esta é a falha de configuração mais comum com a API X, por isso o servidor a verifica na inicialização e se recusa a iniciar com:

o access token é somente leitura: defina as permissões do app como Leitura e Escrita
no X Developer Console, depois regenere o Access Token e Secret

Regenerar a API Key/Secret não é a solução. Regenere o Access Token e Secret.

Verificação na inicialização

Antes de registrar uma única ferramenta, o servidor chama GET /2/users/me uma vez (client.Bootstrap) com um prazo de 15 segundos. Isso faz três trabalhos:

  • prova que as quatro chaves são válidas — chaves ruins falham o processo, não a primeira chamada de ferramenta;
  • detecta a armadilha do token somente leitura acima;
  • armazena em cache o id numérico do usuário dono das chaves, porque todo endpoint de escrita é POST /2/users/:id/... e buscar o id a cada escrita seria outra requisição cobrada.

Uma falha aqui é fatal por design. Um servidor que inicia e depois falha em toda chamada é pior do que um que não inicia.

O custo dessa escolha vale ser dito claramente: não há como testar este servidor sem uma conta de desenvolvedor X financiada. Sem credenciais, sem inicialização, o que significa sem lista de ferramentas — /mcp e claude mcp list mostrarão uma conexão falha e nada mais. Para confirmar uma instalação sem isso, execute o binário com -version e leia a seção Ferramentas para saber o que ele teria exposto.

Chaves são segredos

Os quatro valores são credenciais de uma conta ativa com acesso de escrita. Mantenha-os em um arquivo que só você possa ler, passe-o com -env e nunca o envie para o repositório — .env está no gitignore, .env.example é o modelo. Ao rodar sob um cliente MCP, o bloco env do cliente também funciona; ele tem precedência sobre o arquivo .env.


Ferramentas

42 ferramentas em quatro grupos. Cada ferramenta carrega anotações MCP: readOnlyHint em leituras, destructiveHint em qualquer coisa irreversível (x_post_delete, x_list_delete, descurtir, desrepostar, deixar de seguir, remoção de membro). Cada uma retorna um payload JSON estruturado correspondente ao seu esquema de saída; o SDK espelha o mesmo JSON no bloco de conteúdo de texto para clientes que não leem structuredContent.

Cada ferramenta aceita um timeout_ms opcional, limitado à janela [MIN, MAX] do grupo (veja Limites).

Posts — leitura

FerramentaDescrição
x_posts_searchPesquisa posts recentes para várias consultas em paralelo. A pesquisa recente alcança apenas 7 dias.
x_posts_countConta correspondências por consulta agrupadas por minuto/hora/dia. Não gasta leituras de posts — use-a para dimensionar um tópico antes de pesquisar.
x_posts_lookupBusca até 100 posts por id em uma única chamada.
x_posts_by_userPosts recentes para vários nomes de usuário, buscados em paralelo.
x_posts_mentionsPosts mencionando o dono das chaves.
x_posts_homeA timeline inicial do dono das chaves.
x_posts_quotesPosts citando um post dado.
x_posts_likedPosts que o dono das chaves curtiu.
x_bookmarks_listOs favoritos do dono das chaves.
x_post_liked_byUsuários que curtiram um post dado.
x_post_reposted_byUsuários que repostaram um post dado.

x_posts_search

ParâmetroTipoPadrãoNotas
queries[]stringObrigatório. Executado em paralelo, limitado a POSTS_MAX_QUERIES (5). ≤ 512 caracteres cada.
max_resultsint10Posts por consulta, 1..100. Cada um é cobrado.
sort_orderstringrecency (mais recentes primeiro) ou relevancy (melhor correspondência).
daysint7Até quanto para trás, 1..7. A API não pode ir mais longe.
include_retweetsboolfalseQuando falso, o servidor anexa -is:retweet a cada consulta.
timeout_msint6415000Timeout de toda a chamada, limitado a [2000, 60000].

Operadores de consulta vão dentro da string de consulta:

OperadorSignificado
espaçoE
OROU explícito
-termNÃO
( )agrupamento
from:user / to:userpor autor / por destinatário
@user / #tagmenções / hashtags
"exact phrase"frase exata
lang:enidioma
is:retweet is:reply is:quote is:verifiedtipo de post
has:media has:images has:videos has:linksanexos
url:example.comdomínio vinculado
conversation_id:123um único tópico

x_posts_count

ParâmetroTipoPadrãoNotas
queries[]stringObrigatório. Mesmos operadores que x_posts_search.
granularitystringhourminute, hour ou day.
daysint71..7, mesma janela.
timeout_msint6415000Limitado a [2000, 60000].

x_posts_lookup

ParâmetroTipoPadrãoNotas
ids[]stringObrigatório. 1..100 ids de posts. Agrupe-os em lote; não chame uma vez por id.

x_posts_by_user

ParâmetroTipoPadrãoNotas
usernames[]stringObrigatório. Sem o @. Limitado a POSTS_MAX_USERNAMES (5), buscados em paralelo.
max_resultsint10Posts por usuário, 1..100.
exclude[]stringreplies e/ou retweets.
daysint71..7.
timeout_msint6415000Limitado a [2000, 60000].

Ferramentas de timeline

x_posts_mentions, x_posts_home, x_posts_liked e x_bookmarks_list compartilham uma forma: max_results (1..100), pagination_token, timeout_ms. x_posts_quotes, x_post_liked_by e x_post_reposted_by adicionam um id obrigatório.

A paginação é explícita e manual: uma resposta carrega next_token, e você a passa de volta como pagination_token para a próxima página. O servidor nunca percorre páginas por conta própria — cada página é cobrada, então essa decisão permanece com o chamador.

Posts — escrita

FerramentaAnotaçãoDescrição
x_post_createwritePublica um post. Público e cobrado ($0,015, ou $0,20 com um link).
x_post_deletedestructiveExclui um dos seus posts. Irreversível.
x_post_like / x_post_unlikewrite / destructiveCurtir é público.
x_post_repost / x_post_unrepostwrite / destructiveRepost é público.

x_post_create

ParâmetroTipoObservações
textstring≤ 280 caracteres. Obrigatório, a menos que media_ids esteja definido.
reply_to_idstringResponder a este post.
quote_idstringCitar este post.
media_ids[]string1..4 ids de x_media_upload.
pollobjectoptions (2..4) + duration_minutes (5..10080). Não pode ser combinado com media_ids.
reply_settingsstringfollowing, mentionedUsers, subscribers ou verified.

Publicar o mesmo texto duas vezes seguidas é rejeitado pelo X como duplicado — o servidor apresenta isso como uma mensagem distinta, em vez de uma falha genérica.

Usuários

FerramentaAnotaçãoDescrição
x_users_lookupreadAté 100 nomes de usuário ou 100 ids em uma única chamada — um ou outro, não ambos. Nomes não resolvidos retornam em not_found.
x_users_mereadO perfil por trás das chaves.
x_user_followers / x_user_followingreadUma página por vez; max_results até 1000.
x_user_follow / x_user_unfollowwrite / destructivePúblico.
x_user_mute / x_user_unmutewriteUm mute é silencioso: a outra conta não é notificada e ainda pode ver você.
x_users_muted / x_users_blockedreadContas com mute / bloqueadas pelo dono das chaves.

Bloquear e desbloquear são deliberadamente não expostos. Ler a lista de bloqueios é.

Listas

FerramentaAnotaçãoDescrição
x_list_createwriteNome ≤ 25 caracteres, descrição ≤ 100. Listas privadas são visíveis apenas para o dono.
x_list_updatewriteEnvie apenas os campos que deseja alterar.
x_list_deletedestructiveIrreversível: lista, associações e seguidores desaparecem.
x_list_getreadUma lista com contagens de membros e seguidores.
x_lists_ownedreadListas pertencentes a um nome de usuário, ou ao dono das chaves quando omitido.
x_list_postsreadPosts recentes dos membros da lista. Cobrado por post.
x_list_membersreadContas na lista, paginado.
x_list_member_add / x_list_member_removewrite / destructivePor nome de usuário.
x_list_follow / x_list_unfollowwrite / destructiveApenas listas públicas.
x_list_pin / x_list_unpin / x_lists_pinnedwrite / write / readListas fixadas do dono das chaves.

Mídia

x_media_upload

Envia uma imagem, GIF ou vídeo com o fluxo fragmentado INIT → APPEND → FINALIZE e retorna um media_id para x_post_create.

ParâmetroTipoObservações
pathstringArquivo local. Deve estar dentro de MEDIA_ROOT.
base64stringConteúdo do arquivo em vez de um caminho; requer mime.
mimestringex.: image/jpeg, video/mp4. Obrigatório com base64.
categorystringObrigatório. tweet_image (≤ 5 MB), tweet_gif (≤ 15 MB), tweet_video (≤ 512 MB).
timeout_msint64Padrão 120000, limitado a [5000, 600000] — cobre o upload e a transcodificação do X.

Duas coisas para saber:

  • MEDIA_ROOT é um limite rígido. É uma configuração obrigatória sem padrão, e o servidor recusa qualquer caminho que resolva fora dele, incluindo symlinks. Sem ele, um LLM com esta ferramenta poderia ler qualquer arquivo no host e publicá-lo. Escolha um diretório dedicado.
  • O vídeo é transcodificado de forma assíncrona pelo X. O servidor consulta o status FINALIZE a cada MEDIA_POLL_INTERVAL_MS, mas uma codificação lenta ainda pode retornar status: "pending". O media_id é válido, mas ainda não pode ser postado — tente x_post_create novamente em breve. Um media_id expira após um tempo e é destinado a um único post.

Resultados parciais

As ferramentas de fan-out — x_posts_search, x_posts_count, x_posts_by_user — executam suas entradas em paralelo e retornam uma entrada por consulta/nome de usuário, cada uma com seu próprio status, para que uma entrada ruim não perca os resultados que funcionaram. x_posts_lookup faz o mesmo por id (not found fica ao lado dos posts que foram resolvidos), e x_users_lookup coleta nomes não resolvidos em not_found.

Uma chamada falha completamente apenas quando a entrada é rejeitada antes de qualquer trabalho começar, ou quando todos os itens nela falham.

Erros

As falhas retornam como um resultado de ferramenta com isError: true e uma mensagem em texto simples, não como um erro JSON-RPC — o modelo lê a mensagem e pode corrigir a chamada por conta própria. As mensagens são escritas para um modelo, não para um leitor de logs, então aquelas que não devem ser repetidas dizem isso explicitamente.

MensagemSignificado
the access token is read-only: …regenerate the Access Token and SecretA armadilha do token somente leitura.
the monthly usage cap for this project is reached; …do not retryLimite de uso atingido. Nada funciona até que seja redefinido.
the project has no X API credits left; …do not retryCréditos esgotados. Eles não são redefinidos — recarregue no Console.
rate limit exceeded; the window resets in about 15 minutesRecue; não insista.
X rejected the credentials; check the four keysChaves inválidas ou revogadas.
not authorized for this resourceProtegido, excluído ou de outra pessoa.
X rejected this as a duplicate of a recent postAltere o texto.
user not found / post not found / list not foundAutoexplicativo.
X failed to process the uploaded media; re-encode the fileFalha na transcodificação.
media exceeds the size limit for its category5 MB / 15 MB / 512 MB.
the path is outside the directory this server is allowed to readViolação de MEDIA_ROOT.
post text exceeds 280 characters
a post needs text unless it carries media
a poll needs 2 to 4 options and a duration between 5 and 10080 minutes
a post carries at most 4 media items
too many ids in one call; the limit is 100
invalid username: 1 to 15 letters, digits or underscoresValidado localmente, antes de gastar uma requisição.
every query failed; the X API may be unreachableTodos os itens falharam.
the call timed out; retry with a larger timeout_ms or fewer inputs
the X API is unavailable5xx ou falha upstream não classificada.

A classificação acontece em adapter/x/apierr, e é deliberadamente defensiva. O X responde a erros com Content-Type: application/problem+json, que o cliente gotwi subjacente não reconhece como JSON — então todo o corpo do problema vai literalmente para um campo de mensagem, em vez de ser decodificado em campos tipados. O mapeador, portanto, concatena todas as fontes que a resposta pode carregar e compara os tipos de problema (oauth1-permissions, usage-capped, credits-depleted, rate-limit-exceeded, …) contra esse texto, recorrendo aos códigos de status HTTP quando nada corresponde.

Limitações conhecidas

  • Apenas busca recente. 7 dias para trás, ponto final. A busca histórica/arquivada é um produto diferente (Enterprise) e não está conectada.
  • Bloqueio não é exposto. x_users_blocked lê a lista; não há ferramenta de bloquear/desbloquear.
  • Sem endpoints de streaming. O stream filtrado/amostrado não está implementado.
  • Sem DMs.
  • Uma conta por processo. As chaves são por processo; o dono das chaves é resolvido uma vez na inicialização. Atender várias contas significa vários processos.
  • Paginação é manual. As ferramentas retornam next_token; nada percorre páginas automaticamente, porque cada página é cobrada.

Início rápido

Instalação

Escolha o que preferir — todos fornecem o mesmo servidor.

Contêiner (sem necessidade de toolchain Go):

docker pull ghcr.io/role1776/mcp-x:0.1.1     # or :latest to track the newest release

Fixei uma versão explícita em qualquer coisa em que você confie. :latest avança a cada release estável, e um release pode adicionar ou alterar o comportamento da ferramenta; server.json fixa a mesma versão que o MCP Registry anuncia.

Binário pré-compilado — pegue o arquivo para sua plataforma no último release, descompacte-o e coloque mcp-x no seu PATH.

MCP Bundle — para clientes que instalam arquivos .mcpb, baixe mcp-x_<version>_<os>_<arch>.mcpb do último release e abra-o com seu cliente. O bundle carrega o binário compilado, então não precisa de Docker nem Go, e o cliente solicita os cinco valores obrigatórios na instalação. Escolha o arquivo que corresponde ao seu SO e à arquitetura da CPU: um bundle contém um único binário nativo.

[!NOTE] Bundles construídos antes de v0.1.1 não declaravam campos de configuração, então o cliente nunca pedia as credenciais e o servidor saía na inicialização toda vez. Use v0.1.1 ou mais recente.

A partir do código-fonte (requer Go 1.26+):

git clone https://github.com/Role1776/mcp-x
cd mcp-x
make build          # -> bin/mcp-x

go install também funciona, com uma ressalva que vale saber antes de digitar:

go install github.com/Role1776/mcp-x/app/cmd/mcp-x@latest

O módulo Go vive em app/, então as tags de release (v0.1.0) não o nomeiam — @v0.1.0 falha completamente e @latest resolve para uma pseudo-versão do commit mais recente em main. Uma compilação go install também relata sua versão como dev, porque a versão é carimbada pelo pipeline de release e não pelo módulo. Para uma compilação que corresponda exatamente a um release, use o binário pré-compilado, o contêiner ou make build de uma tag verificada.

Configuração

De um clone, ou de um arquivo de release descompactado (ambos incluem o modelo):

cp .env.example .env
$EDITOR .env        # fill in the four X_* keys and MEDIA_ROOT

Instalar a partir do contêiner ou de um bundle .mcpb não dá a você um checkout para copiar — pegue o modelo de .env.example, ou pule o arquivo completamente e passe os cinco valores no bloco env do seu cliente (abaixo).

Cinco valores são obrigatórios; todo o resto tem um padrão. MEDIA_ROOT deve ser um caminho absoluto para um diretório que já existe — o servidor verifica isso na inicialização e se recusa a iniciar caso contrário, em vez de falhar no primeiro upload.

Execução

./bin/mcp-x -env /absolute/path/to/.env
FlagSignificado
-versionImprime a versão que o binário relata aos clientes MCP e sai. Pode ser respondido sem credenciais, ao contrário do handshake.
-envCaminho para um arquivo .env. Não há busca implícita — sob stdio, o diretório de trabalho é escolhido pelo cliente MCP, então um padrão relativo seria imprevisível. Se a flag for omitida, ou o arquivo não existir, o servidor recorre ao ambiente e informa isso em stderr; os cinco valores obrigatórios ainda devem estar definidos em algum lugar ou a inicialização falha.

Uma inicialização bem-sucedida registra a conta autenticada:

INFO authenticated with the X API op=app.Run user_id=1234567890
INFO MCP server has started op=app.Run transport=stdio

Conectando um cliente MCP (stdio)

Claude Code — um comando, credenciais inline (-- separa o comando do próprio servidor das flags acima):

claude mcp add mcp-x \
  -e X_API_KEY=... \
  -e X_API_KEY_SECRET=... \
  -e X_ACCESS_TOKEN=... \
  -e X_ACCESS_TOKEN_SECRET=... \
  -e MEDIA_ROOT=/absolute/path/to/media \
  -- /absolute/path/to/mcp-x

Ou aponte para um arquivo .env: -- /absolute/path/to/mcp-x -env /absolute/path/to/.env. Verifique o resultado com claude mcp list, ou /mcp dentro de uma sessão.

Claude Desktop e outros clientes que aceitam uma configuração JSON — aponte-os para o binário compilado:

{
  "mcpServers": {
    "x": {
      "command": "/absolute/path/to/mcp-x",
      "args": ["-env", "/absolute/path/to/.env"]
    }
  }
}

Ou pule o arquivo e passe as credenciais no bloco env — variáveis já no ambiente vencem o arquivo .env, então o bloco env de um cliente sempre tem efeito:

{
  "mcpServers": {
    "x": {
      "command": "/absolute/path/to/mcp-x",
      "env": {
        "X_API_KEY": "...",
        "X_API_KEY_SECRET": "...",
        "X_ACCESS_TOKEN": "...",
        "X_ACCESS_TOKEN_SECRET": "...",
        "MEDIA_ROOT": "/absolute/path/to/media"
      }
    }
  }
}

Conectando um cliente MCP (contêiner)

Execute a imagem em stdio. O Docker precisa de cada variável nomeada na linha de comando com -e para que ela alcance o processo, e MEDIA_ROOT só faz sentido se o diretório estiver montado:

{
  "mcpServers": {
    "x": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "X_API_KEY",
        "-e", "X_API_KEY_SECRET",
        "-e", "X_ACCESS_TOKEN",
        "-e", "X_ACCESS_TOKEN_SECRET",
        "-e", "MEDIA_ROOT",
        "-v", "/host/media:/media:ro",
        "ghcr.io/role1776/mcp-x:0.1.1"
      ],
      "env": {
        "X_API_KEY": "...",
        "X_API_KEY_SECRET": "...",
        "X_ACCESS_TOKEN": "...",
        "X_ACCESS_TOKEN_SECRET": "...",
        "MEDIA_ROOT": "/media"
      }
    }
  }
}

-i é obrigatório — sem ele, o contêiner não recebe stdin e o cliente vê o servidor morrer imediatamente. Clientes que instalam a partir do MCP Registry em si constroem essa invocação por conta própria e solicitam as variáveis declaradas em server.json — isso é uma propriedade do cliente, não de todo cliente que possa falar com este servidor; o CLI do Claude Code, por exemplo, não instala a partir do registry, então use claude mcp add acima.

Quando o cliente diz que a conexão foi encerrada

Sob stdio, o stderr do servidor pertence ao cliente, e a maioria dos clientes o descarta. Então, um problema de configuração que o servidor explica perfeitamente em uma linha chega até você como nada além de:

✘ Failed to connect — -32000: Connection closed

Execute o binário manualmente com o mesmo ambiente para ver o motivo real:

/absolute/path/to/mcp-x -env /absolute/path/to/.env

Ele imprime exatamente o que está errado — quais variáveis estão faltando e onde obtê-las, ou que as chaves foram rejeitadas, ou que o token de acesso é somente leitura — e sai. Quase toda instalação falha é uma dessas três.

Executando via HTTP

Defina MCP_TRANSPORT=http e o servidor escuta em SERVER_PORT em MCP_PATH (padrão http://localhost:8080/mcp).

[!CAUTION] O transporte HTTP não tem autenticação própria. Qualquer pessoa que alcance o endpoint pode publicar, excluir e seguir como sua conta, usando seus créditos. Vincule-o a localhost ou coloque-o atrás de um proxy reverso autenticado — nunca o exponha à internet aberta.


Configuração

Tudo é configurado por meio de variáveis de ambiente, e cada valor é validado antes da inicialização: uma chave obrigatória ausente, ou um número não numérico ou não positivo, é um erro de inicialização que nomeia a variável que você realmente definiu (X_API_KEY, não APIKey). Relações entre limites não são verificadas na inicialização — veja Limites. Variáveis já presentes no ambiente vencem um arquivo .env.

Veja .env.example para a lista completa com seus valores padrão, pronta para copiar para .env.

Obrigatórias

EnvNotas
X_API_KEYChave de consumidor OAuth 1.0a.
X_API_KEY_SECRETSegredo de consumidor OAuth 1.0a.
X_ACCESS_TOKENToken de acesso do usuário — gere-o depois de definir Leitura e Escrita.
X_ACCESS_TOKEN_SECRETSegredo do token de acesso do usuário.
MEDIA_ROOTCaminho absoluto para o único diretório do qual x_media_upload pode ler. Deve existir e ser um diretório; ambos são verificados na inicialização. Sem padrão, de propósito.

A ausência de qualquer uma delas aborta a inicialização com a lista do que está faltando mais um ponteiro para o Developer Console.

Servidor MCP

EnvPadrãoNotas
MCP_TRANSPORTstdiostdio ou http.
MCP_NAMEmcp-xNome do servidor anunciado aos clientes.
MCP_PATH/mcpRota HTTP (somente transporte http).

A versão anunciada aos clientes não é configurável: ela é gravada no binário no momento da compilação a partir da tag git.

Servidor HTTP (somente transporte http)

EnvPadrão
SERVER_PORT8080
SERVER_READ_TIMEOUT60s
SERVER_WRITE_TIMEOUT60s

Cliente HTTP

EnvPadrãoNotas
MAX_IDLE_CONNS_PER_HOST100Pooling de conexões para a API do X.
X_DEBUGfalseRegistra requisições gotwi brutas. Verboso; útil quando um erro do X não faz sentido.

Registro de logs

EnvPadrãoNotas
LOG_MODElocallocal → manipulador de texto no nível de depuração; prod → manipulador JSON no nível de informação. Os logs vão para stderr (eles devem: stdout carrega o protocolo MCP).

Limites

Cada grupo tem seus próprios limites, então um upload de mídia lento não pode ser limitado por um timeout de busca.

Posts

EnvPadrãoNotas
POSTS_MAX_QUERIES5Consultas por chamada de x_posts_search / x_posts_count.
POSTS_MAX_USERNAMES5Nomes de usuário por chamada de x_posts_by_user.
POSTS_MAX_IDS100Ids por x_posts_lookup; o teto da própria API.
POSTS_DEFAULT_RESULTS10
POSTS_MAX_RESULTS100
POSTS_DEFAULT_DAYS7
POSTS_MAX_DAYS7A busca recente não pode olhar mais para trás.
POSTS_DEFAULT_TIMEOUT_MS15000
POSTS_MIN_TIMEOUT_MS2000
POSTS_MAX_TIMEOUT_MS60000

Usuários

EnvPadrão
USERS_MAX_USERNAMES100
USERS_MAX_IDS100
USERS_DEFAULT_RESULTS100
USERS_MAX_RESULTS1000
USERS_DEFAULT_TIMEOUT_MS15000
USERS_MIN_TIMEOUT_MS2000
USERS_MAX_TIMEOUT_MS60000

Listas

EnvPadrão
LISTS_DEFAULT_RESULTS25
LISTS_MAX_RESULTS100
LISTS_DEFAULT_TIMEOUT_MS15000
LISTS_MIN_TIMEOUT_MS2000
LISTS_MAX_TIMEOUT_MS60000

Mídia

EnvPadrãoNotas
MEDIA_CHUNK_BYTES41943044 MB. O endpoint APPEND limita um segmento a 5 MB.
MEDIA_POLL_INTERVAL_MS2000Com que frequência o status FINALIZE é consultado enquanto o X transcodifica.
MEDIA_DEFAULT_TIMEOUT_MS120000
MEDIA_MIN_TIMEOUT_MS5000
MEDIA_MAX_TIMEOUT_MS60000010 minutos, para vídeo grande.

Cada valor é verificado individualmente — deve ser maior que zero, e aqueles que a própria API limita (*_MAX_IDS, POSTS_MAX_RESULTS, LISTS_MAX_RESULTS, POSTS_MAX_DAYS) são adicionalmente limitados ao teto da API para que um erro de digitação não possa produzir uma requisição que o X rejeitará. Os trios DEFAULT_*, MIN_* e MAX_* não são verificados entre si na inicialização. Um conjunto inconsistente não impede o servidor; ele é reconciliado por requisição:

  • um valor que o chamador omite, ou passa como zero ou negativo, recai no DEFAULT_* correspondente;
  • o resultado é então limitado a [MIN_*, MAX_*], então um DEFAULT_* maior que seu MAX_* simplesmente produz MAX_*;
  • se MIN_* exceder MAX_*, o máximo vence.

O limite efetivo está, portanto, sempre dentro do máximo configurado, e uma configuração incorreta degrada para um servidor funcional em vez de uma falha na inicialização. A compensação é que ela degrada silenciosamente: POSTS_MAX_RESULTS=1 em vez de 10 não produz nenhum aviso, apenas respostas menores e silenciosas. Vale a pena verificar esses valores novamente quando os resultados parecerem truncados.


Arquitetura

O projeto segue uma estrutura limpa e em camadas. As dependências apontam para dentro em direção ao domínio, e cada camada fala com a próxima por meio de interfaces.

app/                       the Go module: sources plus its build files
                           (Dockerfile, .dockerignore, .goreleaser.yaml)

cmd/mcp-x/main.go          entry point: parse flags, load config, run app

internal/
  app/                     wiring + lifecycle (X client, bootstrap, run, graceful shutdown)
  config/                  config loading (.env → env vars → validate → env-name error messages)
  domain/x/                core types and errors
    posts/ users/ lists/ media/   value objects: PostID, UserID, Username, Draft, Query, Upload…
    format.go              shared snowflake-id and username validation
    errors.go              the sentinel error set the whole app maps onto
  dto/x/                   request/response shapes for the MCP tools, with jsonschema + validate tags
  transport/mcp/           MCP layer
    router/                registers every tool group on the MCP server
    x/posts|users|lists|media/   tool definitions, handlers and error → tool-result mapping
  usecase/x/               business logic: validation, parallelism, timeouts, limit resolution
  adapter/x/               X API wiring
    client/                gotwi client + startup Bootstrap (key check, key-owner id)
    apierr/                X problem responses → domain errors
    posts|users|lists|media/     endpoint calls and mappers to DTOs
  pkg/                     reusable building blocks (mcpserver, server, logger, validator, parallel)

Fluxo de requisição para uma chamada de ferramenta:

MCP client → transport/mcp/x/... (handler) → usecase/x/... → adapter/x/... → gotwi → X API
                    ↑ maps errors                 ↑ validates, resolves limits,
                      to isError text               fans out, applies timeouts

Duas fronteiras merecem destaque:

  • A camada de domínio se recusa a construir valores inválidos. NewPostID, NewUserID, NewUsername, NewDraft e amigos validam na construção, então um id malformado ou um post de 300 caracteres é rejeitado localmente — antes de custar uma requisição. Ids são verificados contra o formato snowflake e nomes de usuário contra a regra de 1–15 [A-Za-z0-9_] do X em um único pacote format compartilhado.
  • Cada camada fala o mesmo vocabulário de erros. Adaptadores convertem as respostas de problema do X nos sentinelas em domain/x/errors.go; a camada de transporte é o único lugar que transforma um sentinela em texto humano. É por isso que a mesma falha é lida de forma idêntica, não importa qual das 42 ferramentas a produziu.

Desenvolvimento

Todo o código Go vive em app/, então use o makefile da raiz do repositório ou passe -C app para a toolchain:

make build          # compile the binary -> bin/mcp-x
make test           # go test -v ./...
make cover          # total coverage percentage
make cover-html     # coverage report in the browser
make version        # the version that would be stamped into the binary

go -C app build ./...
go -C app test ./...
go -C app vet ./...

Mocks são gerados com mockgen a partir das diretivas //go:generate ao lado de cada interface usecase:

go -C app generate ./...

Espera-se que código novo venha com testes. Veja CONTRIBUTING.md para as diretrizes completas de pull request.

Licença

Lançado sob a Licença MIT.