mcp-x
Proporciona al modelo la API v2 de X (Twitter) como 42 herramientas en publicaciones, búsqueda, usuarios, listas y carga de medios. Go, contexto de usuario OAuth 1.0a, stdio y HTTP transmisible. Construido en torno a la facturación de pago por uso de X: las lecturas se cobran por recurso devuelto, por lo que las herramientas solicitan la página más pequeña que responda a la pregunta.
Documentación
mcp-x
Un servidor MCP que le da a un LLM la API de X (Twitter): leer y buscar publicaciones, gestionar usuarios y listas, subir medios y publicar — como la cuenta cuyas claves utiliza.
Sitio web · Costos · Credenciales · Herramientas · Inicio rápido · Configuración · Arquitectura · Contribuciones
Qué es
mcp-x es un servidor de Model Context Protocol escrito en Go. Expone la API de X v2 a cualquier cliente compatible con MCP (Claude Desktop, agentes de IDE, aplicaciones LLM personalizadas) como 42 herramientas que cubren publicaciones, usuarios, listas y medios.
Se autentica con contexto de usuario OAuth 1.0a, lo que significa que cada llamada actúa como una cuenta real de X — la que posee las cuatro claves. x_post_create publica públicamente. x_user_follow realmente sigue. x_post_delete es irreversible. Esto no es un entorno de pruebas y no es gratuito: consulta La API de X cuesta dinero antes de conectarla a un agente.
Ambos transportes que soporta el SDK de MCP están disponibles y exponen el mismo conjunto de herramientas:
- stdio — el cliente lanza el binario y se comunica por stdin/stdout (el predeterminado, ideal para clientes de escritorio).
- http — un servidor HTTP de transmisión de larga duración (útil para despliegues remotos/compartidos).
La API de X cuesta dinero
[!WARNING] Ya no existe un nivel gratuito. X retiró los niveles de suscripción Free/Basic/Pro para nuevos desarrolladores y pasó a créditos de pago por uso: compras créditos por adelantado en la Developer Console y cada solicitud descuenta del saldo en tiempo real. Las suscripciones heredadas Basic ($200/mes) y Pro ($5,000/mes) sobreviven solo para cuentas que ya las tenían; Enterprise comienza alrededor de $42,000/mes. Una cuenta de desarrollador nueva hoy obtiene pago por uso y nada más.
Tarifas al momento de escribir esto (precios oficiales — siempre vuelve a consultar la Console, han cambiado varias veces en 2026):
| Operación | Precio |
|---|---|
| Lectura de publicación | $0.005 por publicación devuelta |
| Lectura propia (tus publicaciones, marcadores, seguidores, me gusta, listas) | $0.001 por recurso |
| Lectura de usuario | $0.010 por usuario devuelto |
| Lectura de me gusta / silenciados / bloqueados | $0.001 por recurso |
| Lectura de seguidores / siguiendo | $0.010 por recurso |
| Publicar una publicación | $0.015 por solicitud |
| Publicar una publicación que contiene una URL | $0.200 por solicitud |
| Me gusta / republicar y otras interacciones | $0.015 por solicitud |
| Escrituras de listas y marcadores | $0.005–$0.010 por solicitud |
De esto se derivan dos cosas, y ambas están integradas en el servidor:
- Las lecturas se cobran por recurso devuelto, no por solicitud.
max_results: 100enx_posts_searchcuesta veinte veces lo quemax_results: 5cuesta por la misma consulta. La descripción de cada herramienta de lectura le indica al modelo que pida elmax_resultsmás pequeño que responda la pregunta, y las herramientas de agrupación (x_posts_lookup,x_users_lookup) le indican que agrupe en lugar de hacer bucles. x_posts_countno consume el presupuesto de lectura de publicaciones. Devuelve recuentos de coincidencias agrupados por minuto/hora/día para la misma sintaxis de consulta. Dimensiona un tema conx_posts_countprimero, luego paga porx_posts_search.
X también deduplica: el mismo recurso obtenido dos veces dentro de una ventana UTC de 24 horas se factura una sola vez. Y el pago por uso está limitado a 3 millones de lecturas de publicaciones por ciclo de facturación — más allá de eso, solo Enterprise.
Cuando el dinero se agota, la API responde con un error distinto, y el servidor lo mapea a un mensaje que le dice explícitamente al modelo que no reintente — consulta Errores.
Credenciales
El servidor necesita cuatro valores de OAuth 1.0a, todos de una sola app de X:
X_API_KEY
X_API_KEY_SECRET
X_ACCESS_TOKEN
X_ACCESS_TOKEN_SECRET
El primer par identifica la app; el segundo par identifica la cuenta que actúa a través de ella. mcp-x usa AuthenMethodOAuth1UserContext, no autenticación de portador solo para apps, porque cada endpoint de escritura y cada endpoint "yo" (x_users_me, x_posts_home, x_bookmarks_list, menciones) requiere un contexto de usuario. No existe un modo de token de portador.
Cómo obtenerlas — y el paso que todos hacen mal
- Ve a developer.x.com → tu proyecto → tu app.
- Abre User authentication settings y establece App permissions en Read and Write. Haz esto primero.
- Solo entonces ve a Keys and tokens y genera el Access Token and Secret.
[!IMPORTANT] Un token de acceso conserva permanentemente los permisos que la app tenía en el momento en que se generó. Si creaste el token mientras la app era de solo lectura y luego cambiaste la app a Read and Write, el token sigue siendo de solo lectura. Nada en la página de configuración de la app te lo dirá. Cada escritura fallará con el tipo de problema
oauth1-permissionsde X, para siempre, hasta que vuelvas a Keys and tokens y regeneres el Access Token and Secret.Este es el fallo de configuración más común con la API de X, por eso el servidor lo verifica al inicio y se niega a arrancar con:
the access token is read-only: set the app permissions to Read and Write in the X Developer Console, then regenerate the Access Token and SecretRegenerar la API Key/Secret no es la solución. Regenera el Access Token and Secret.
Verificación al inicio
Antes de registrar una sola herramienta, el servidor llama a GET /2/users/me una vez (client.Bootstrap) con un plazo de 15 segundos. Esto hace tres trabajos:
- demuestra que las cuatro claves son válidas — las claves malas fallan el proceso, no la primera llamada de herramienta;
- detecta la trampa del token de solo lectura mencionada arriba;
- almacena en caché el id numérico de usuario del propietario de las claves, porque cada endpoint de escritura es
POST /2/users/:id/...y buscar el id por cada escritura sería otra solicitud facturada.
Un fallo aquí es fatal por diseño. Un servidor que arranca y luego falla en cada llamada es peor que uno que no arranca.
El costo de esa elección vale la pena decirlo claramente: no hay forma de probar este servidor sin una cuenta de desarrollador de X financiada. Sin credenciales no hay arranque, lo que significa que no hay lista de herramientas — /mcp y claude mcp list mostrarán una conexión fallida y nada más. Para confirmar una instalación sin llegar a eso, ejecuta el binario con -version y lee la sección Herramientas para ver qué habría expuesto.
Las claves son secretos
Los cuatro valores son credenciales para una cuenta activa con acceso de escritura. Mantenlos en un archivo que solo tú puedas leer, pásalo con -env y nunca lo subas a un repositorio — .env está en gitignore, .env.example es la plantilla. Cuando se ejecuta bajo un cliente MCP, el bloque env del cliente también funciona; tiene prioridad sobre el archivo .env.
Herramientas
42 herramientas en cuatro grupos. Cada herramienta lleva anotaciones MCP: readOnlyHint en lecturas, destructiveHint en cualquier cosa irreversible (x_post_delete, x_list_delete, quitar me gusta, desrepublicar, dejar de seguir, eliminación de miembros). Cada una devuelve una carga JSON estructurada que coincide con su esquema de salida; el SDK refleja el mismo JSON en el bloque de contenido de texto para clientes que no leen structuredContent.
Cada herramienta acepta un timeout_ms opcional, limitado a la ventana [MIN, MAX] del grupo (consulta Límites).
Publicaciones — lectura
| Herramienta | Descripción |
|---|---|
x_posts_search | Busca publicaciones recientes para varias consultas en paralelo. La búsqueda reciente alcanza solo 7 días atrás. |
x_posts_count | Cuenta coincidencias por consulta agrupadas por minuto/hora/día. No gasta lecturas de publicaciones — úsala para dimensionar un tema antes de buscar. |
x_posts_lookup | Obtiene hasta 100 publicaciones por id en una sola llamada. |
x_posts_by_user | Publicaciones recientes para varios nombres de usuario, obtenidas en paralelo. |
x_posts_mentions | Publicaciones que mencionan al propietario de las claves. |
x_posts_home | La línea de tiempo principal del propietario de las claves. |
x_posts_quotes | Publicaciones que citan una publicación dada. |
x_posts_liked | Publicaciones a las que el propietario de las claves dio me gusta. |
x_bookmarks_list | Los marcadores del propietario de las claves. |
x_post_liked_by | Usuarios que dieron me gusta a una publicación dada. |
x_post_reposted_by | Usuarios que republicaron una publicación dada. |
x_posts_search
| Parámetro | Tipo | Predeterminado | Notas |
|---|---|---|---|
queries | []string | — | Requerido. Se ejecuta en paralelo, limitado a POSTS_MAX_QUERIES (5). ≤ 512 caracteres cada uno. |
max_results | int | 10 | Publicaciones por consulta, 1..100. Cada una se factura. |
sort_order | string | — | recency (más recientes primero) o relevancy (mejor coincidencia). |
days | int | 7 | Cuánto hacia atrás, 1..7. La API no puede ir más lejos. |
include_retweets | bool | false | Cuando es false, el servidor agrega -is:retweet a cada consulta. |
timeout_ms | int64 | 15000 | Tiempo de espera de toda la llamada, limitado a [2000, 60000]. |
Los operadores de consulta van dentro de la cadena de consulta:
| Operador | Significado |
|---|---|
| espacio | Y |
OR | O explícito |
-term | NO |
( ) | agrupación |
from:user / to:user | por autor / por destinatario |
@user / #tag | menciones / hashtags |
"exact phrase" | frase exacta |
lang:en | idioma |
is:retweet is:reply is:quote is:verified | tipo de publicación |
has:media has:images has:videos has:links | adjuntos |
url:example.com | dominio enlazado |
conversation_id:123 | un hilo |
x_posts_count
| Parámetro | Tipo | Predeterminado | Notas |
|---|---|---|---|
queries | []string | — | Requerido. Mismos operadores que x_posts_search. |
granularity | string | hour | minute, hour o day. |
days | int | 7 | 1..7, misma ventana. |
timeout_ms | int64 | 15000 | Limitado a [2000, 60000]. |
x_posts_lookup
| Parámetro | Tipo | Predeterminado | Notas |
|---|---|---|---|
ids | []string | — | Requerido. 1..100 ids de publicaciones. Agrúpalos; no llames una vez por id. |
x_posts_by_user
| Parámetro | Tipo | Predeterminado | Notas |
|---|---|---|---|
usernames | []string | — | Requerido. Sin el @. Limitado a POSTS_MAX_USERNAMES (5), obtenidos en paralelo. |
max_results | int | 10 | Publicaciones por usuario, 1..100. |
exclude | []string | — | replies y/o retweets. |
days | int | 7 | 1..7. |
timeout_ms | int64 | 15000 | Limitado a [2000, 60000]. |
Herramientas de línea de tiempo
x_posts_mentions, x_posts_home, x_posts_liked y x_bookmarks_list comparten una forma: max_results (1..100), pagination_token, timeout_ms. x_posts_quotes, x_post_liked_by y x_post_reposted_by agregan un id requerido.
La paginación es explícita y manual: una respuesta lleva next_token, y lo pasas de vuelta como pagination_token para la siguiente página. El servidor nunca recorre páginas por su cuenta — cada página se factura, así que esa decisión queda en manos de quien llama.
Publicaciones — escritura
| Herramienta | Anotación | Descripción |
|---|---|---|
x_post_create | escritura | Publica un post. Público y facturado ($0.015, o $0.20 con un enlace). |
x_post_delete | destructivo | Elimina uno de tus posts. Irreversible. |
x_post_like / x_post_unlike | escritura / destructivo | El "me gusta" es público. |
x_post_repost / x_post_unrepost | escritura / destructivo | El repost es público. |
x_post_create
| Parámetro | Tipo | Notas |
|---|---|---|
text | string | ≤ 280 caracteres. Requerido a menos que media_ids esté 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). No se puede combinar con media_ids. |
reply_settings | string | following, mentionedUsers, subscribers o verified. |
Publicar el mismo texto dos veces seguidas es rechazado por X como duplicado — el servidor lo presenta como un mensaje distinto en lugar de un fallo genérico.
Usuarios
| Herramienta | Anotación | Descripción |
|---|---|---|
x_users_lookup | lectura | Hasta 100 nombres de usuario o 100 ids en una sola llamada — uno u otro, no ambos. Los nombres no resueltos aparecen en not_found. |
x_users_me | lectura | El perfil detrás de las claves. |
x_user_followers / x_user_following | lectura | Una página a la vez; max_results hasta 1000. |
x_user_follow / x_user_unfollow | escritura / destructivo | Público. |
x_user_mute / x_user_unmute | escritura | Un silencio es silencioso: la otra cuenta no es notificada y aún puede verte. |
x_users_muted / x_users_blocked | lectura | Las cuentas silenciadas / bloqueadas del propietario de las claves. |
Bloquear y desbloquear están deliberadamente no expuestos. Leer la lista de bloqueos sí lo está.
Listas
| Herramienta | Anotación | Descripción |
|---|---|---|
x_list_create | escritura | Nombre ≤ 25 caracteres, descripción ≤ 100. Las listas privadas son solo del propietario. |
x_list_update | escritura | Envía solo los campos que quieras cambiar. |
x_list_delete | destructivo | Irreversible: la lista, la membresía y los seguidores desaparecen. |
x_list_get | lectura | Una lista con conteos de miembros y seguidores. |
x_lists_owned | lectura | Listas propiedad de un nombre de usuario, o del propietario de las claves si se omite. |
x_list_posts | lectura | Posts recientes de los miembros de la lista. Facturado por post. |
x_list_members | lectura | Cuentas en la lista, paginadas. |
x_list_member_add / x_list_member_remove | escritura / destructivo | Por nombre de usuario. |
x_list_follow / x_list_unfollow | escritura / destructivo | Solo listas públicas. |
x_list_pin / x_list_unpin / x_lists_pinned | escritura / escritura / lectura | Listas fijadas del propietario de las claves. |
Medios
x_media_upload
Sube una imagen, GIF o video con el flujo fragmentado INIT → APPEND → FINALIZE y devuelve un media_id para x_post_create.
| Parámetro | Tipo | Notas |
|---|---|---|
path | string | Archivo local. Debe estar dentro de MEDIA_ROOT. |
base64 | string | Contenido del archivo en lugar de una ruta; requiere mime. |
mime | string | p. ej. image/jpeg, video/mp4. Requerido con base64. |
category | string | Requerido. tweet_image (≤ 5 MB), tweet_gif (≤ 15 MB), tweet_video (≤ 512 MB). |
timeout_ms | int64 | Por defecto 120000, limitado a [5000, 600000] — cubre la subida y la transcodificación de X. |
Dos cosas a saber:
MEDIA_ROOTes un límite estricto. Es configuración requerida sin valor por defecto, y el servidor rechaza cualquier ruta que se resuelva fuera de él, incluidos los enlaces simbólicos. Sin él, un LLM con esta herramienta podría leer cualquier archivo en el host y publicarlo. Elige un directorio dedicado.- El video se transcodifica de forma asíncrona por X. El servidor consulta el estado de FINALIZE cada
MEDIA_POLL_INTERVAL_MS, pero una codificación lenta aún puede devolverstatus: "pending". Elmedia_ides válido pero aún no publicable — reintentax_post_createen breve. Unmedia_idexpira después de un tiempo y está destinado a un solo post.
Resultados parciales
Las herramientas de difusión — x_posts_search, x_posts_count, x_posts_by_user — ejecutan sus entradas en paralelo y devuelven una entrada por consulta/nombre de usuario, cada una con su propio status, de modo que una entrada mala no pierde los resultados que funcionaron. x_posts_lookup hace lo mismo por id (not found se sitúa junto a los posts que se resolvieron), y x_users_lookup recoge los nombres no resueltos en not_found.
Una llamada falla por completo solo cuando la entrada es rechazada antes de que comience cualquier trabajo, o cuando cada elemento en ella falla.
Errores
Los fallos vuelven como resultado de herramienta con isError: true y un mensaje de texto plano, no como un error JSON-RPC — el modelo lee el mensaje y puede corregir la llamada por sí mismo. Los mensajes están escritos para un modelo en lugar de un lector de registros, por lo que los que no deben reintentarse lo dicen explícitamente.
| Mensaje | Significado |
|---|---|
the access token is read-only: …regenerate the Access Token and Secret | La trampa del token de solo lectura. |
the monthly usage cap for this project is reached; …do not retry | Límite de uso alcanzado. Nada funciona hasta que se restablece. |
the project has no X API credits left; …do not retry | Créditos agotados. No se restablecen — recarga en la Consola. |
rate limit exceeded; the window resets in about 15 minutes | Retrocede; no insistas. |
X rejected the credentials; check the four keys | Claves inválidas o revocadas. |
not authorized for this resource | Protegido, eliminado o de otra persona. |
X rejected this as a duplicate of a recent post | Cambia el texto. |
user not found / post not found / list not found | Autoevidente. |
X failed to process the uploaded media; re-encode the file | La transcodificación falló. |
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 | Violación 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 una solicitud. |
every query failed; the X API may be unreachable | Todos los elementos fallaron. |
the call timed out; retry with a larger timeout_ms or fewer inputs | — |
the X API is unavailable | 5xx o fallo ascendente no clasificado. |
La clasificación ocurre en adapter/x/apierr, y es deliberadamente defensiva. X responde a los errores con Content-Type: application/problem+json, que el cliente gotwi subyacente no reconoce como JSON — por lo que todo el cuerpo del problema aterriza textualmente en un campo de mensaje en lugar de decodificarse en campos tipados. El mapeador por lo tanto concatena cada fuente que la respuesta puede llevar y compara los tipos de problema (oauth1-permissions, usage-capped, credits-depleted, rate-limit-exceeded, …) contra ese montón, recurriendo a los códigos de estado HTTP cuando nada coincide.
Limitaciones conocidas
- Solo búsqueda reciente. 7 días hacia atrás, punto. La búsqueda histórica/de archivo es un producto diferente (Enterprise) y no está conectada.
- El bloqueo no está expuesto.
x_users_blockedlee la lista; no hay herramienta de bloquear/desbloquear. - Sin endpoints de streaming. El flujo filtrado/muestreado no está implementado.
- Sin mensajes directos.
- Una cuenta por proceso. Las claves son a nivel de proceso; el propietario de las claves se resuelve una vez al inicio. Atender varias cuentas significa varios procesos.
- La paginación es manual. Las herramientas devuelven
next_token; nada recorre páginas automáticamente, porque cada página es facturada.
Inicio rápido
Instalación
Elige el que mejor se adapte — todos dan el servidor idéntico.
Contenedor (no se necesita cadena de herramientas Go):
docker pull ghcr.io/role1776/mcp-x:0.1.1 # or :latest to track the newest release
Fija una versión explícita en cualquier cosa de la que dependas. :latest se mueve en cada versión estable, y una versión puede añadir o cambiar el comportamiento de las herramientas; server.json fija la misma versión que anuncia el Registro MCP.
Binario precompilado — toma el archivo para tu plataforma de la última versión, descomprímelo y pon mcp-x en tu PATH.
Paquete MCP — para clientes que instalan archivos .mcpb, descarga mcp-x_<version>_<os>_<arch>.mcpb de la última versión y ábrelo con tu cliente. El paquete lleva el binario compilado, por lo que no necesita ni Docker ni Go, y el cliente solicita los cinco valores requeridos en la instalación. Elige el archivo que coincida con tu sistema operativo y arquitectura de CPU: un paquete contiene un solo binario nativo.
[!NOTA] Los paquetes construidos antes de v0.1.1 no declaraban campos de configuración, por lo que el cliente nunca pedía las credenciales y el servidor salía al inicio cada vez. Usa v0.1.1 o más reciente.
Desde el código fuente (necesita Go 1.26+):
git clone https://github.com/Role1776/mcp-x
cd mcp-x
make build # -> bin/mcp-x
go install también funciona, con una advertencia que vale la pena saber antes de escribirlo:
go install github.com/Role1776/mcp-x/app/cmd/mcp-x@latest
El módulo Go vive en app/, por lo que las etiquetas de versión (v0.1.0) no lo nombran — @v0.1.0 falla por completo y @latest se resuelve a una pseudo-versión del commit más reciente en main. Una compilación go install también reporta su versión como dev, porque la versión es estampada por el pipeline de lanzamiento y no por el módulo. Para una compilación que coincida exactamente con una versión, usa el binario precompilado, el contenedor, o make build desde una etiqueta verificada.
Configuración
Desde un clon, o desde un archivo de versión descomprimido (ambos incluyen la plantilla):
cp .env.example .env
$EDITOR .env # fill in the four X_* keys and MEDIA_ROOT
Instalar desde el contenedor o un paquete .mcpb no te da un checkout del que copiar — toma la plantilla de .env.example, o omite el archivo por completo y pasa los cinco valores en el bloque env de tu cliente (abajo).
Se requieren cinco valores; todo lo demás tiene un valor por defecto. MEDIA_ROOT debe ser una ruta absoluta a un directorio que ya exista — el servidor lo verifica al inicio y se niega a arrancar de lo contrario, en lugar de fallar en la primera subida.
Ejecución
./bin/mcp-x -env /absolute/path/to/.env
| Bandera | Significado |
|---|---|
-version | Imprime la versión que el binario reporta a los clientes MCP y sale. Respondible sin credenciales, a diferencia del apretón de manos. |
-env | Ruta a un archivo .env. No hay búsqueda implícita — bajo stdio el directorio de trabajo es elegido por el cliente MCP, por lo que un valor relativo por defecto sería impredecible. Si la bandera se omite, o el archivo no existe, el servidor recurre al entorno ambiente y lo dice en stderr; los cinco valores requeridos deben estar definidos en algún lugar o el inicio falla. |
Un inicio exitoso registra la cuenta autenticada:
INFO authenticated with the X API op=app.Run user_id=1234567890
INFO MCP server has started op=app.Run transport=stdio
Conectar un cliente MCP (stdio)
Claude Code — un comando, credenciales en línea (-- separa el comando propio del servidor de las banderas anteriores):
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
O apúntalo a un archivo .env en su lugar: -- /absolute/path/to/mcp-x -env /absolute/path/to/.env. Verifica el resultado con claude mcp list, o /mcp dentro de una sesión.
Claude Desktop y otros clientes que aceptan una configuración JSON — apúntalos al binario compilado:
{
"mcpServers": {
"x": {
"command": "/absolute/path/to/mcp-x",
"args": ["-env", "/absolute/path/to/.env"]
}
}
}
O omite el archivo y pasa las credenciales en el bloque env — las variables ya en el entorno ganan sobre el archivo .env, por lo que el bloque env de un cliente siempre tiene efecto:
{
"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"
}
}
}
}
Conectar un cliente MCP (contenedor)
Ejecuta la imagen en stdio. Docker necesita cada variable nombrada en la línea de comandos con -e para que llegue al proceso, y MEDIA_ROOT solo tiene sentido si el directorio está 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 es obligatorio — sin él, el contenedor no recibe stdin y el cliente ve morir el servidor de inmediato. Los clientes que instalan desde el propio Registro MCP construyen esta invocación por sí mismos y solicitan las variables declaradas en server.json — eso es una propiedad del cliente, no de todos los clientes que pueden hablar con este servidor; la CLI de Claude Code, por ejemplo, no instala desde el registro, así que usa claude mcp add arriba.
Cuando el cliente dice que la conexión se cerró
Bajo stdio, el stderr del servidor pertenece al cliente, y la mayoría de los clientes lo descartan. Así que un problema de configuración que el servidor explica perfectamente en una línea llega a ti como nada más que:
✘ Failed to connect — -32000: Connection closed
Ejecuta el binario manualmente con el mismo entorno para ver la razón real:
/absolute/path/to/mcp-x -env /absolute/path/to/.env
Imprime exactamente qué está mal — qué variables faltan y dónde obtenerlas, o que las claves fueron rechazadas, o que el token de acceso es de solo lectura — y sale. Casi todas las instalaciones fallidas son una de esas tres.
Ejecución sobre HTTP
Establece MCP_TRANSPORT=http y el servidor escucha en SERVER_PORT en MCP_PATH (por defecto http://localhost:8080/mcp).
[!CAUTION] El transporte HTTP no tiene autenticación propia. Cualquiera que pueda alcanzar el endpoint puede publicar, eliminar y seguir como tu cuenta, con tus créditos. Enlázalo a localhost o colócalo detrás de un proxy inverso autenticado — nunca lo expongas a internet abierta.
Configuración
Todo se configura mediante variables de entorno, y cada valor se valida antes del inicio: una clave requerida faltante, o un número no numérico o no positivo, es un error de inicio que nombra la variable que realmente estableciste (X_API_KEY, no APIKey). Las relaciones entre límites no se verifican al inicio — ver Límites. Las variables ya presentes en el entorno ganan sobre un archivo .env.
Consulta .env.example para la lista completa con sus valores predeterminados, lista para copiar a .env.
Requeridas
| Env | Notas |
|---|---|
X_API_KEY | Clave de consumidor OAuth 1.0a. |
X_API_KEY_SECRET | Secreto de consumidor OAuth 1.0a. |
X_ACCESS_TOKEN | Token de acceso de usuario — genéralo después de establecer Lectura y Escritura. |
X_ACCESS_TOKEN_SECRET | Secreto del token de acceso de usuario. |
MEDIA_ROOT | Ruta absoluta al único directorio del que x_media_upload puede leer. Debe existir y ser un directorio; ambos se verifican al inicio. Sin valor predeterminado, a propósito. |
Si falta alguna de ellas, el inicio se aborta con la lista de lo que falta más un puntero a la Consola de Desarrollador.
Servidor MCP
| Env | Predeterminado | Notas |
|---|---|---|
MCP_TRANSPORT | stdio | stdio o http. |
MCP_NAME | mcp-x | Nombre del servidor anunciado a los clientes. |
MCP_PATH | /mcp | Ruta HTTP (solo transporte http). |
La versión anunciada a los clientes no es configurable: se estampa en el binario en tiempo de compilación desde la etiqueta git.
Servidor HTTP (solo transporte http)
| Env | Predeterminado |
|---|---|
SERVER_PORT | 8080 |
SERVER_READ_TIMEOUT | 60s |
SERVER_WRITE_TIMEOUT | 60s |
Cliente HTTP
| Env | Predeterminado | Notas |
|---|---|---|
MAX_IDLE_CONNS_PER_HOST | 100 | Agrupación de conexiones hacia la API de X. |
X_DEBUG | false | Registra solicitudes gotwi sin procesar. Verboso; útil cuando un error de X no tiene sentido. |
Registro
| Env | Predeterminado | Notas |
|---|---|---|
LOG_MODE | local | local → manejador de texto en nivel de depuración; prod → manejador JSON en nivel de información. Los registros van a stderr (deben: stdout lleva el protocolo MCP). |
Límites
Cada grupo tiene sus propios límites, por lo que una carga de medios lenta no puede ser limitada por un tiempo de espera de búsqueda.
Publicaciones
| Env | Predeterminado | Notas |
|---|---|---|
POSTS_MAX_QUERIES | 5 | Consultas por llamada x_posts_search / x_posts_count. |
POSTS_MAX_USERNAMES | 5 | Nombres de usuario por llamada x_posts_by_user. |
POSTS_MAX_IDS | 100 | Ids por x_posts_lookup; el techo propio de la API. |
POSTS_DEFAULT_RESULTS | 10 | |
POSTS_MAX_RESULTS | 100 | |
POSTS_DEFAULT_DAYS | 7 | |
POSTS_MAX_DAYS | 7 | La búsqueda reciente no puede mirar más atrás. |
POSTS_DEFAULT_TIMEOUT_MS | 15000 | |
POSTS_MIN_TIMEOUT_MS | 2000 | |
POSTS_MAX_TIMEOUT_MS | 60000 |
Usuarios
| Env | Predeterminado |
|---|---|
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 | Predeterminado |
|---|---|
LISTS_DEFAULT_RESULTS | 25 |
LISTS_MAX_RESULTS | 100 |
LISTS_DEFAULT_TIMEOUT_MS | 15000 |
LISTS_MIN_TIMEOUT_MS | 2000 |
LISTS_MAX_TIMEOUT_MS | 60000 |
Medios
| Env | Predeterminado | Notas |
|---|---|---|
MEDIA_CHUNK_BYTES | 4194304 | 4 MB. El endpoint APPEND limita un segmento a 5 MB. |
MEDIA_POLL_INTERVAL_MS | 2000 | Con qué frecuencia se consulta el estado de FINALIZE mientras X transcodifica. |
MEDIA_DEFAULT_TIMEOUT_MS | 120000 | |
MEDIA_MIN_TIMEOUT_MS | 5000 | |
MEDIA_MAX_TIMEOUT_MS | 600000 | 10 minutos, para video grande. |
Cada valor se verifica por sí solo — debe ser mayor que cero, y los que la propia API limita (*_MAX_IDS, POSTS_MAX_RESULTS, LISTS_MAX_RESULTS, POSTS_MAX_DAYS) se limitan adicionalmente al techo de la API para que un error tipográfico no pueda producir una solicitud que X rechace. Los tríos DEFAULT_*, MIN_* y MAX_* no se verifican entre sí al inicio. Un conjunto inconsistente no detiene el servidor; se reconcilia por solicitud en su lugar:
- un valor que el llamador omite, o pasa como cero o negativo, cae al
DEFAULT_*correspondiente; - el resultado se ajusta luego dentro de
[MIN_*, MAX_*], por lo que unDEFAULT_*mayor que suMAX_*simplemente produceMAX_*; - si
MIN_*excedeMAX_*, gana el máximo.
El límite efectivo está por lo tanto siempre dentro del máximo configurado, y una mala configuración degrada a un servidor funcional en lugar de un inicio fallido. La compensación es que degrada silenciosamente: POSTS_MAX_RESULTS=1 en lugar de 10 no produce ninguna advertencia, solo respuestas más pequeñas sin aviso. Vale la pena verificar dos veces estos valores cuando los resultados parecen truncados.
Arquitectura
El proyecto sigue una estructura limpia y en capas. Las dependencias apuntan hacia adentro, hacia el dominio, y cada capa habla con la siguiente a través 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)
Flujo de solicitud para una llamada de herramienta:
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
Dos límites merecen ser señalados:
- La capa de dominio se niega a construir valores inválidos.
NewPostID,NewUserID,NewUsername,NewDrafty amigos validan al construir, por lo que un id malformado o una publicación de 300 caracteres se rechaza localmente — antes de que cueste una solicitud. Los ids se verifican contra el formato de copo de nieve y los nombres de usuario contra la regla de 1–15[A-Za-z0-9_]de X en un único paquete compartidoformat. - Cada capa habla el mismo vocabulario de errores. Los adaptadores convierten las respuestas de problema de X en los centinelas en
domain/x/errors.go; la capa de transporte es el único lugar que convierte un centinela en texto humano. Por eso la misma falla se lee de manera idéntica sin importar cuál de las 42 herramientas la produjo.
Desarrollo
Todo lo de Go vive en app/, así que usa el makefile desde la raíz del repositorio o pasa -C app a la cadena de herramientas:
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 ./...
Los mocks se generan con mockgen a partir de las directivas //go:generate junto a cada interfaz usecase:
go -C app generate ./...
Se espera que el código nuevo venga con pruebas. Consulta CONTRIBUTING.md para las pautas completas de solicitudes de extracción.
Licencia
Publicado bajo la Licencia MIT.