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.
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ção | Preç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: 100emx_posts_searchcusta vinte vezes o quemax_results: 5custa para a mesma consulta. A descrição de cada ferramenta de leitura diz ao modelo para pedir o menormax_resultsque 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_countnã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 comx_posts_countprimeiro, depois pague porx_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
- Vá para developer.x.com → seu projeto → seu app.
- Abra Configurações de autenticação de usuário e defina Permissões do app como Leitura e Escrita. Faça isso primeiro.
- 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-permissionsdo 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 SecretRegenerar 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
| Ferramenta | Descrição |
|---|---|
x_posts_search | Pesquisa posts recentes para várias consultas em paralelo. A pesquisa recente alcança apenas 7 dias. |
x_posts_count | Conta 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_lookup | Busca até 100 posts por id em uma única chamada. |
x_posts_by_user | Posts recentes para vários nomes de usuário, buscados em paralelo. |
x_posts_mentions | Posts mencionando o dono das chaves. |
x_posts_home | A timeline inicial do dono das chaves. |
x_posts_quotes | Posts citando um post dado. |
x_posts_liked | Posts que o dono das chaves curtiu. |
x_bookmarks_list | Os favoritos do dono das chaves. |
x_post_liked_by | Usuários que curtiram um post dado. |
x_post_reposted_by | Usuários que repostaram um post dado. |
x_posts_search
| Parâmetro | Tipo | Padrão | Notas |
|---|---|---|---|
queries | []string | — | Obrigatório. Executado em paralelo, limitado a POSTS_MAX_QUERIES (5). ≤ 512 caracteres cada. |
max_results | int | 10 | Posts por consulta, 1..100. Cada um é cobrado. |
sort_order | string | — | recency (mais recentes primeiro) ou relevancy (melhor correspondência). |
days | int | 7 | Até quanto para trás, 1..7. A API não pode ir mais longe. |
include_retweets | bool | false | Quando falso, o servidor anexa -is:retweet a cada consulta. |
timeout_ms | int64 | 15000 | Timeout de toda a chamada, limitado a [2000, 60000]. |
Operadores de consulta vão dentro da string de consulta:
| Operador | Significado |
|---|---|
| espaço | E |
OR | OU explícito |
-term | NÃO |
( ) | agrupamento |
from:user / to:user | por autor / por destinatário |
@user / #tag | menções / hashtags |
"exact phrase" | frase exata |
lang:en | idioma |
is:retweet is:reply is:quote is:verified | tipo de post |
has:media has:images has:videos has:links | anexos |
url:example.com | domínio vinculado |
conversation_id:123 | um único tópico |
x_posts_count
| Parâmetro | Tipo | Padrão | Notas |
|---|---|---|---|
queries | []string | — | Obrigatório. Mesmos operadores que x_posts_search. |
granularity | string | hour | minute, hour ou day. |
days | int | 7 | 1..7, mesma janela. |
timeout_ms | int64 | 15000 | Limitado a [2000, 60000]. |
x_posts_lookup
| Parâmetro | Tipo | Padrão | Notas |
|---|---|---|---|
ids | []string | — | Obrigatório. 1..100 ids de posts. Agrupe-os em lote; não chame uma vez por id. |
x_posts_by_user
| Parâmetro | Tipo | Padrão | Notas |
|---|---|---|---|
usernames | []string | — | Obrigatório. Sem o @. Limitado a POSTS_MAX_USERNAMES (5), buscados em paralelo. |
max_results | int | 10 | Posts por usuário, 1..100. |
exclude | []string | — | replies e/ou retweets. |
days | int | 7 | 1..7. |
timeout_ms | int64 | 15000 | Limitado 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
| Ferramenta | Anotação | Descrição |
|---|---|---|
x_post_create | write | Publica um post. Público e cobrado ($0,015, ou $0,20 com um link). |
x_post_delete | destructive | Exclui um dos seus posts. Irreversível. |
x_post_like / x_post_unlike | write / destructive | Curtir é público. |
x_post_repost / x_post_unrepost | write / destructive | Repost é público. |
x_post_create
| Parâmetro | Tipo | Observações |
|---|---|---|
text | string | ≤ 280 caracteres. Obrigatório, a menos que media_ids esteja definido. |
reply_to_id | string | Responder a este post. |
quote_id | string | Citar este post. |
media_ids | []string | 1..4 ids de x_media_upload. |
poll | object | options (2..4) + duration_minutes (5..10080). Não pode ser combinado com media_ids. |
reply_settings | string | following, 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
| Ferramenta | Anotação | Descrição |
|---|---|---|
x_users_lookup | read | Até 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_me | read | O perfil por trás das chaves. |
x_user_followers / x_user_following | read | Uma página por vez; max_results até 1000. |
x_user_follow / x_user_unfollow | write / destructive | Público. |
x_user_mute / x_user_unmute | write | Um mute é silencioso: a outra conta não é notificada e ainda pode ver você. |
x_users_muted / x_users_blocked | read | Contas com mute / bloqueadas pelo dono das chaves. |
Bloquear e desbloquear são deliberadamente não expostos. Ler a lista de bloqueios é.
Listas
| Ferramenta | Anotação | Descrição |
|---|---|---|
x_list_create | write | Nome ≤ 25 caracteres, descrição ≤ 100. Listas privadas são visíveis apenas para o dono. |
x_list_update | write | Envie apenas os campos que deseja alterar. |
x_list_delete | destructive | Irreversível: lista, associações e seguidores desaparecem. |
x_list_get | read | Uma lista com contagens de membros e seguidores. |
x_lists_owned | read | Listas pertencentes a um nome de usuário, ou ao dono das chaves quando omitido. |
x_list_posts | read | Posts recentes dos membros da lista. Cobrado por post. |
x_list_members | read | Contas na lista, paginado. |
x_list_member_add / x_list_member_remove | write / destructive | Por nome de usuário. |
x_list_follow / x_list_unfollow | write / destructive | Apenas listas públicas. |
x_list_pin / x_list_unpin / x_lists_pinned | write / write / read | Listas 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âmetro | Tipo | Observações |
|---|---|---|
path | string | Arquivo local. Deve estar dentro de MEDIA_ROOT. |
base64 | string | Conteúdo do arquivo em vez de um caminho; requer mime. |
mime | string | ex.: image/jpeg, video/mp4. Obrigatório com base64. |
category | string | Obrigatório. tweet_image (≤ 5 MB), tweet_gif (≤ 15 MB), tweet_video (≤ 512 MB). |
timeout_ms | int64 | Padrã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 retornarstatus: "pending". Omedia_idé válido, mas ainda não pode ser postado — tentex_post_createnovamente em breve. Ummedia_idexpira 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.
| Mensagem | Significado |
|---|---|
the access token is read-only: …regenerate the Access Token and Secret | A armadilha do token somente leitura. |
the monthly usage cap for this project is reached; …do not retry | Limite de uso atingido. Nada funciona até que seja redefinido. |
the project has no X API credits left; …do not retry | Créditos esgotados. Eles não são redefinidos — recarregue no Console. |
rate limit exceeded; the window resets in about 15 minutes | Recue; não insista. |
X rejected the credentials; check the four keys | Chaves inválidas ou revogadas. |
not authorized for this resource | Protegido, excluído ou de outra pessoa. |
X rejected this as a duplicate of a recent post | Altere o texto. |
user not found / post not found / list not found | Autoexplicativo. |
X failed to process the uploaded media; re-encode the file | Falha na transcodificação. |
media exceeds the size limit for its category | 5 MB / 15 MB / 512 MB. |
the path is outside the directory this server is allowed to read | Violaçã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 underscores | Validado localmente, antes de gastar uma requisição. |
every query failed; the X API may be unreachable | Todos os itens falharam. |
the call timed out; retry with a larger timeout_ms or fewer inputs | — |
the X API is unavailable | 5xx 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_blockedlê 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
| Flag | Significado |
|---|---|
-version | Imprime a versão que o binário relata aos clientes MCP e sai. Pode ser respondido sem credenciais, ao contrário do handshake. |
-env | Caminho 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
| Env | Notas |
|---|---|
X_API_KEY | Chave de consumidor OAuth 1.0a. |
X_API_KEY_SECRET | Segredo de consumidor OAuth 1.0a. |
X_ACCESS_TOKEN | Token de acesso do usuário — gere-o depois de definir Leitura e Escrita. |
X_ACCESS_TOKEN_SECRET | Segredo do token de acesso do usuário. |
MEDIA_ROOT | Caminho 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
| Env | Padrão | Notas |
|---|---|---|
MCP_TRANSPORT | stdio | stdio ou http. |
MCP_NAME | mcp-x | Nome do servidor anunciado aos clientes. |
MCP_PATH | /mcp | Rota 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)
| Env | Padrão |
|---|---|
SERVER_PORT | 8080 |
SERVER_READ_TIMEOUT | 60s |
SERVER_WRITE_TIMEOUT | 60s |
Cliente HTTP
| Env | Padrão | Notas |
|---|---|---|
MAX_IDLE_CONNS_PER_HOST | 100 | Pooling de conexões para a API do X. |
X_DEBUG | false | Registra requisições gotwi brutas. Verboso; útil quando um erro do X não faz sentido. |
Registro de logs
| Env | Padrão | Notas |
|---|---|---|
LOG_MODE | local | local → 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
| Env | Padrão | Notas |
|---|---|---|
POSTS_MAX_QUERIES | 5 | Consultas por chamada de x_posts_search / x_posts_count. |
POSTS_MAX_USERNAMES | 5 | Nomes de usuário por chamada de x_posts_by_user. |
POSTS_MAX_IDS | 100 | Ids por x_posts_lookup; o teto da própria API. |
POSTS_DEFAULT_RESULTS | 10 | |
POSTS_MAX_RESULTS | 100 | |
POSTS_DEFAULT_DAYS | 7 | |
POSTS_MAX_DAYS | 7 | A busca recente não pode olhar mais para trás. |
POSTS_DEFAULT_TIMEOUT_MS | 15000 | |
POSTS_MIN_TIMEOUT_MS | 2000 | |
POSTS_MAX_TIMEOUT_MS | 60000 |
Usuários
| Env | Padrão |
|---|---|
USERS_MAX_USERNAMES | 100 |
USERS_MAX_IDS | 100 |
USERS_DEFAULT_RESULTS | 100 |
USERS_MAX_RESULTS | 1000 |
USERS_DEFAULT_TIMEOUT_MS | 15000 |
USERS_MIN_TIMEOUT_MS | 2000 |
USERS_MAX_TIMEOUT_MS | 60000 |
Listas
| Env | Padrão |
|---|---|
LISTS_DEFAULT_RESULTS | 25 |
LISTS_MAX_RESULTS | 100 |
LISTS_DEFAULT_TIMEOUT_MS | 15000 |
LISTS_MIN_TIMEOUT_MS | 2000 |
LISTS_MAX_TIMEOUT_MS | 60000 |
Mídia
| Env | Padrão | Notas |
|---|---|---|
MEDIA_CHUNK_BYTES | 4194304 | 4 MB. O endpoint APPEND limita um segmento a 5 MB. |
MEDIA_POLL_INTERVAL_MS | 2000 | Com que frequência o status FINALIZE é consultado enquanto o X transcodifica. |
MEDIA_DEFAULT_TIMEOUT_MS | 120000 | |
MEDIA_MIN_TIMEOUT_MS | 5000 | |
MEDIA_MAX_TIMEOUT_MS | 600000 | 10 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 umDEFAULT_*maior que seuMAX_*simplesmente produzMAX_*; - se
MIN_*excederMAX_*, 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,NewDrafte 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 pacoteformatcompartilhado. - 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.