Montar servidores MCP en un VPS para agentes de IA
Configura servidores MCP stdio y HTTP remoto en un VPS con systemd, nginx, TLS y autenticación. Evita errores JSON-RPC y endpoints expuestos sin protección.
Qué va a crear
Dos configuraciones MCP operativas en un solo VPS. La primera es un servidor stdio: una herramienta de sistema de archivos o de base de datos que Claude Code inicia como proceso secundario y con la que se comunica mediante una tubería. La segunda es un servidor HTTP remoto que se ejecuta como servicio de red de larga duración detrás de systemd y un proxy inverso nginx con TLS. Puede acceder a él cualquier cliente MCP que configure para usarlo. La instalación de cualquiera de las dos opciones es breve. La mayor parte de esta guía trata los dos problemas que suelen causar fallos: mantener limpio el flujo JSON-RPC y no exponer nunca un endpoint de herramientas sin autenticación en Internet pública.
Qué es realmente MCP
El Model Context Protocol es una forma estándar para que un cliente de IA, como Claude Code, Claude Desktop, Gemini CLI en un VPS o un script propio, llame a herramientas externas y lea recursos externos. El modelo no ejecuta nada directamente. Solicita una acción al cliente, el cliente se comunica mediante JSON-RPC 2.0 con un servidor MCP, el servidor ejecuta la herramienta y devuelve el resultado. Ese cliente es lo que normalmente se denomina agent harness: el bucle que rodea al modelo y administra la lista de herramientas, las comprobaciones de permisos y el estado de la sesión. MCP es simplemente el mecanismo para ampliar la parte de herramientas. Como usa un único protocolo, un servidor que escriba una vez funcionará con todos los clientes compatibles con MCP. Si esta separación es nueva para usted, especialmente la cuestión de cómo decide un modelo cuándo utilizar una herramienta, conviene dedicar una hora a un recorrido gradual por los fundamentos de los agentes antes de proporcionar credenciales reales a uno de estos servidores.
Hay dos transportes, y el resto de esta guía se divide según ellos:
- stdio. El cliente inicia el servidor como un proceso hijo e intercambia mensajes JSON-RPC delimitados por saltos de línea mediante su entrada estándar y su salida estándar. No hay red, puerto ni autenticación. El límite de confianza es el propio proceso. Casi todas las herramientas locales se distribuyen de esta forma.
- Streamable HTTP (y su predecesor, HTTP+SSE). El servidor es un servicio web de larga duración. El cliente se conecta mediante HTTP y el servidor puede transmitir las respuestas como Server-Sent Events. Esta opción permite compartir un servidor entre varios clientes o ejecutar una herramienta que deba permanecer activa en el equipo.
Elija stdio cuando la herramienta pertenezca a una sola máquina y un solo usuario. Elija HTTP cuando sea un servicio compartido.
Requisitos previos y advertencias importantes
Suponga un VPS KVM Ubuntu 24.04 recién instalado con acceso como root o mediante sudo. Además:
- Un entorno de ejecución en el que esté escrito el servidor. La mayoría de los servidores de referencia están escritos en Node o Python. Ubuntu 24.04 incluye Node 18, y varios paquetes MCP actuales requieren Node 20 o una versión posterior. Por eso, instale una versión LTS actual desde NodeSource o nvm en lugar de confiar en
apt. Python 3.12 ya está disponible. - Un dominio y un registro DNS A, pero sólo para el servidor HTTP remoto. TLS necesita un nombre que resuelva a este VPS. El ejemplo con stdio no necesita DNS.
- 512 MB de RAM son suficientes. Los servidores MCP son procesos JSON-RPC ligeros. El consumo de memoria depende de lo que utilice la herramienta, como un controlador de base de datos o una caché de archivos, no del protocolo.
- La especificación es reciente y sigue cambiando. La revisión de 2025-03-26 sustituyó HTTP+SSE por Streamable HTTP y marcó SSE como obsoleto. SSE todavía funciona y muchos servidores siguen utilizándolo. Por tanto, considere cualquier configuración fija del transporte como un aspecto que debe volver a comprobarse en las notas de la versión del servidor, no como una regla definitiva.
Paso 1: conectar un servidor stdio a Claude Code
Empiece con el servidor de sistema de archivos. Es oficial, recibe mantenimiento activo y sólo necesita Node. El siguiente comando lo registra en Claude Code y lo limita al proyecto actual, de modo que queda en un archivo que se puede confirmar en el repositorio:
cd /home/matt/projects/api
claude mcp add --scope project --transport stdio filesystem \
-- npx -y @modelcontextprotocol/server-filesystem /home/matt/projects/apiEl separador -- es importante: todo lo que aparece después es el comando que ejecutará Claude Code, no una opción de Claude Code. Esto escribe un archivo .mcp.json en la raíz del proyecto:
{
"mcpServers": {
"filesystem": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/server-filesystem",
"/home/matt/projects/api"
]
}
}
}Todavía no se está ejecutando nada. La próxima vez que inicie Claude Code en este directorio, el agente leerá .mcp.json, iniciará npx -y @modelcontextprotocol/server-filesystem ... como proceso hijo y realizará el handshake de MCP mediante la entrada y salida estándar de ese proceso. Confirme que se haya aplicado:
claude mcp listUn servidor operativo muestra su comando y un indicador verde, filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✔ Connected. Dentro de la sesión, el comando slash /mcp muestra las herramientas que expone el servidor (read_text_file, write_file, list_directory), y el agente ya puede invocarlas en las rutas que haya autorizado. Una herramienta de base de datos tiene la misma estructura: cambie el paquete y pase una cadena de conexión como argumento final, pero consulte el repositorio del propio servidor para conocer el nombre actual del paquete, porque el servidor de Postgres de referencia ha cambiado de responsables más de una vez.
Este es el motivo principal para ejecutar el agente en el propio servidor: la sesión de Claude Code se ejecuta en el VPS dentro de tmux, y sus servidores stdio se ejecutan junto a ella con acceso directo a los archivos del proyecto y a los servicios locales, sin un recorrido de red adicional. Cuando el agente tiene write_file y read_text_file, conviene combinar ese alcance con una skill que lo oriente hacia el cambio mínimo que funciona, porque una herramienta de sistema de archivos hace que una reescritura extensa resulte tan sencilla como una corrección de dos líneas. La misma conexión puede ampliarse más allá de los archivos locales: si ya ejecuta un motor de búsqueda en el VPS, puede proporcionar al agente su propia instancia de SearXNG como herramienta de búsqueda, lo que mantiene las consultas en su servidor, pero incorpora directamente en el contexto el texto no confiable de las páginas sobre el que el agente actuará después.
Paso 2: crear un servidor HTTP remoto
Un servidor stdio termina cuando termina su proceso padre y se inicia una vez por cliente. Por tanto, si ejecuta dos sesiones de Claude Code en el equipo que se pasan trabajo entre sí, cada una obtiene su propia copia privada de la herramienta. Si necesita una herramienta que permanezca activa para todos los clientes, una herramienta de operaciones compartida, una puerta de enlace de base de datos o un servicio al que accedan tanto su portátil como su CI, necesita el transporte HTTP y un servicio real. Este es un servidor Python mínimo que usa el SDK oficial y expone una herramienta:
# /opt/mcp-ops/server.py
from mcp.server.mcpserver import MCPServer
import subprocess
mcp = MCPServer("ops-tools")
@mcp.tool()
def disk_free() -> str:
"""Return `df -h` for the server."""
out = subprocess.run(["df", "-h"], capture_output=True, text=True)
return out.stdout
if __name__ == "__main__":
# Serves Streamable HTTP at /mcp on 127.0.0.1:8000
mcp.run(transport="streamable-http", host="127.0.0.1", port=8000)Nota: host="127.0.0.1". El servidor se enlaza sólo a localhost. Nada fuera del equipo puede acceder directamente a él, que es exactamente lo que necesita antes de implementar la autenticación. Este es el SDK 2.x que pip install "mcp[cli]" obtiene actualmente (2.3.0 a fecha de 2026-10-20). Cambió el nombre de FastMCP a MCPServer y trasladó host y port del constructor a run(). Además, la importación antigua de mcp.server.fastmcp ahora falla con ModuleNotFoundError. Instálelo en su propio virtualenv para que systemd disponga de una ruta de intérprete estable:
sudo useradd --system --home /opt/mcp-ops --shell /usr/sbin/nologin mcp
sudo install -d -o mcp -g mcp /opt/mcp-ops
sudo -H -u mcp python3 -m venv /opt/mcp-ops/.venv
sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install "mcp[cli]"Paso 3: mantenerlo activo con systemd
Una herramienta que está caída cuando el agente intenta usarla es peor que no tener herramienta. Esto es especialmente importante cuando el cliente es un proceso de larga duración: un agente siempre activo que conserva su memoria y sus tareas programadas tras los reinicios llamará a estas herramientas según un horario sin nadie supervisándolo, por lo que el servidor también debe volver a iniciarse por sí solo. Escriba /etc/systemd/system/mcp-ops.service:
[Unit]
Description=MCP ops-tools server
After=network.target
[Service]
Type=simple
User=mcp
WorkingDirectory=/opt/mcp-ops
ExecStart=/opt/mcp-ops/.venv/bin/python /opt/mcp-ops/server.py
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
[Install]
WantedBy=multi-user.targetLa ruta absoluta al Python del venv en ExecStart no es opcional. Apúntelo a /usr/bin/python3 y el proceso se iniciará con ModuleNotFoundError: No module named 'mcp', porque el intérprete del sistema nunca vio su pip install. Habilítelo y compruébelo:
sudo systemctl daemon-reload
sudo systemctl enable --now mcp-ops
sudo systemctl status mcp-ops
curl -si -H 'Accept: application/json, text/event-stream' \
-H 'Content-Type: application/json' \
-X POST http://127.0.0.1:8000/mcpstatus debería mostrar active (running). curl devuelve HTTP/1.1 400 Bad Request con un error JSON-RPC en el cuerpo. La solicitud no incluía ninguna sesión ni una carga JSON válida, y eso es exactamente lo que necesita: demuestra que el puerto responde y habla el protocolo. Connection refused o una respuesta vacía significa que el proceso no está escuchando donde cree. Lea journalctl -u mcp-ops -n 50.
Paso 4: poner TLS y un proxy inverso delante
El servidor escucha en localhost. Para acceder a él desde cualquier lugar, termine TLS en nginx y haga proxy hacia el servicio interno. Instale nginx, obtenga un certificado con Certbot y Let's Encrypt en nginx y escriba el bloque location. La parte crítica es desactivar el almacenamiento en búfer, porque el comportamiento predeterminado de nginx retiene una respuesta hasta que está completa. Esto bloquea un flujo SSE indefinidamente:
server {
listen 443 ssl;
server_name mcp.example.com;
# ssl_certificate lines managed by Certbot
location /mcp {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
# The four lines that make SSE work through nginx:
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
chunked_transfer_encoding off;
}
}Recargue nginx con sudo nginx -t && sudo systemctl reload nginx. Si ya ejecuta una flota de contenedores, un proxy inverso Traefik con TLS automático realiza la misma tarea: emite el certificado y enruta por nombre de host. Sólo tiene que añadir etiquetas al contenedor MCP. En ambos casos, el proxy inverso es ahora el único componente expuesto en un puerto público y apunta a un servicio que todavía no ha protegido. Corrija esto antes de registrar la URL en ningún sitio.
Paso 5: la regla de seguridad que predomina en este tema
Nunca exponga un endpoint MCP sin autenticación. Un servidor MCP no es una API de sólo lectura. Concede acceso a herramientas, a sus archivos, a su base de datos y, en algunos casos, a un shell. Un /mcp abierto en Internet público equivale a dar a un desconocido el mismo alcance que tiene su agente de IA: puede enumerar las herramientas y luego llamarlas. Trátelo exactamente como un socket de administración sin autenticación, porque eso es lo que es. El alcance de un token robado también depende del servidor que haya detrás: el servidor MCP de sólo lectura incluido con el rastreador de entrenamientos openGym sólo puede devolver datos de entrenamiento, mientras que una herramienta de sistema de archivos o de shell entrega el control del equipo.
Tres defensas, en orden de preferencia:
- No lo publique. Mantenga el servidor en
127.0.0.1y acceda desde su portátil mediante un túnel SSH:ssh -L 8000:127.0.0.1:8000 matt@vps; después, configure el cliente para usarhttp://127.0.0.1:8000/mcp. No se expone nada. - Colóquelo en una red privada. Asocie la dirección del túnel de una VPN WireGuard autohospedada y permita que sólo los pares de la VPN accedan a ella. Internet público verá un puerto cerrado.
- Si debe ser público, exija un token. La opción adecuada es el flujo OAuth de MCP que el transporte HTTP admite de forma nativa. El mínimo práctico es un token bearer compartido que se valide en el proxy. Es barato y detiene por completo los accesos oportunistas:
location /mcp {
if ($http_authorization != "Bearer REPLACE_WITH_LONG_RANDOM") {
return 401;
}
proxy_pass http://127.0.0.1:8000;
# ...buffering-off block from above...
}Genere el token con openssl rand -hex 32 y nunca asocie el servidor directamente a 0.0.0.0 sin colocar una de estas defensas delante. El cliente envía el token como una cabecera. En Claude Code:
claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
--header 'Authorization: Bearer ${MCP_TOKEN}'Defina MCP_TOKEN en su shell para que el secreto nunca termine en .mcp.json en texto plano. Claude Code expande ${MCP_TOKEN} desde el entorno en el momento de leerlo. Si la variable no está definida, Claude Code no sustituye su valor por una cadena vacía: marca la variable ausente en claude mcp list y usa el texto literal ${MCP_TOKEN}. Elija también un nombre propio, porque en las cabeceras de un servidor remoto Claude Code lee deliberadamente sus propias variables y las variables de credenciales de su proveedor cloud, como ANTHROPIC_API_KEY, como valores vacíos, junto con otras credenciales que pueda contener su entorno, como NPM_TOKEN.
Todas las defensas anteriores protegen el endpoint, no al agente que ya posee el token. Esa es la otra mitad del problema: si su cliente es DeepSeek Harness, los plugins que restringen las herramientas que un agente puede llamar y analizan la salida de las herramientas en busca de instrucciones inyectadas cubren ese aspecto.
Paso 6: depurar con MCP Inspector
Cuando un servidor funciona de forma incorrecta, no intente adivinar la causa desde el agente. Conéctelo directamente mediante Inspector, el cliente de pruebas web oficial. Para un servidor stdio, indíquele el mismo comando que ejecuta el agente:
npx @modelcontextprotocol/inspector \
npx -y @modelcontextprotocol/server-filesystem /tmpInspector sirve su interfaz desde un único servidor web en http://localhost:6274 e imprime una URL que contiene una cadena de consulta MCP_INSPECTOR_API_TOKEN. Abra ese enlace en lugar de escribir la dirección de memoria. Desde v2 (2.11.0 es la versión actual a fecha de 2026-10-20), el backend también inserta el token en la página que sirve en cada carga. Por eso, la recarga sigue funcionando. El token protege las rutas /api/* que pueden iniciar procesos. El proxy independiente de v1 en 6277 ya no existe. Sin embargo, el servidor web no es el único proceso que escucha. v2 también inicia un entorno aislado para MCP Apps en MCP_SANDBOX_PORT (6275 de forma predeterminada) y un servidor dedicado para el origen de las aplicaciones en MCP_APP_ORIGIN_PORT (6278 de forma predeterminada). Este servidor sirve cualquier MCP App cuyo recurso de interfaz declare _meta.ui.domain. Si accede a Inspector mediante un túnel SSH o lo ejecuta en un contenedor y necesita la pestaña Apps, reenvíe 6275 y 6278 junto con 6274. Conéctese, abra la pestaña Tools, seleccione una herramienta, introduzca argumentos reales y ejecútela. Si funciona en Inspector pero falla en el agente, el error está en la configuración del cliente, no en el servidor. Para el servidor HTTP remoto, inicie Inspector sin ningún comando, añada un servidor con el transporte Streamable HTTP y https://mcp.example.com/mcp, introduzca la cabecera Authorization en su configuración y conéctese. Es la forma más rápida de comprobar que la autenticación y el proxy son correctos antes de involucrar al agente. Inspector guarda las cabeceras en su catálogo de servidores exactamente como se escriben, por lo que ese token también queda almacenado en ~/.mcp-inspector/mcp.json.
Mantener los servidores actualizados
MCP evoluciona rápidamente, por lo que debe aplicar los parches según un calendario. Los servidores Node iniciados con un npx -y sin versión fijada ejecutan la versión que resuelva npx: la instalada en el proyecto local, si existe, o una copia que instala en la caché de npm. -y sólo omite la solicitud de instalación; no fuerza una actualización. Por tanto, no sigue necesariamente la versión más reciente ni sabe con certeza cuál está ejecutando. Fije la versión exacta que haya probado, consulte esa versión en npm view @modelcontextprotocol/server-filesystem version y añádala al nombre del paquete en .mcp.json (@modelcontextprotocol/server-filesystem@<version>) cuando el servidor sea importante. Después, actualícela de forma deliberada. Los servidores Python gestionados por systemd se actualizan con sudo -H -u mcp /opt/mcp-ops/.venv/bin/pip install -U "mcp[cli]" seguido de sudo systemctl restart mcp-ops. Lea las notas de la versión antes de aplicar una actualización principal. El cambio del SDK de Python de 1.x a 2.x rompe el código 1.x durante la importación. La guía de migración del propio SDK recomienda mantener un límite superior <2, como "mcp[cli]<2", hasta completar la migración. Al actualizar, compruebe la revisión de la especificación a la que apunta su SDK. Un cambio que atraviese la transición de SSE a Streamable-HTTP puede modificar el transporte que deben solicitar sus clientes.
Modos de fallo y cadenas que verá
El agente indica que el servidor ha fallado. claude mcp list muestra ✘ Failed to connect, y las versiones actuales añaden el detalle del fallo a esa línea. Ejecute claude --debug. Normalmente verá Error: spawn npx ENOENT: el comando no está en el PATH del agente. Falta el runtime o no está en la ubicación que busca el agente: Node no está instalado, falta npx o se ha indicado un Python de virtualenv mediante un nombre sin ruta. Corrija el comando para usar una ruta absoluta o instale el runtime. Después, vuelva a conectar.
Un servidor stdio se conecta y se desconecta de inmediato. El cliente registra un error de análisis JSON, como Unexpected token 'S', "Server sta"... is not valid JSON o Failed to parse message. La causa siempre es la misma: el servidor escribió una línea de registro en stdout. En stdio, stdout es el canal JSON-RPC. Por tanto, cualquier texto adicional corrompe el flujo y el handshake falla. En Node, console.log escribe en stdout; use console.error. En Python, un print() sin configuración escribe en stdout. Escriba los registros con logging configurado en sys.stderr o pase file=sys.stderr. La regla es absoluta: en stdio, stdout sólo debe contener JSON-RPC. Todo el texto destinado a usuarios debe ir a stderr.
Un servidor remoto agota el tiempo de espera o cierra la conexión durante el handshake. El cliente falla con MCP error -32000: Connection closed, o Inspector se queda bloqueado en Connect y nunca muestra las herramientas. Detrás de nginx, esto se debe al buffering: el proxy retiene el flujo SSE en lugar de enviarlo de forma progresiva. El cliente espera una respuesta que nunca llega. Añada proxy_buffering off;, junto con el resto del bloque del paso 4, a location. Confirme el comportamiento con curl -N contra la URL pública. Debe ver que los datos de eventos llegan progresivamente, no todos al final de una vez.
La autenticación se rechaza. El cliente informa de Error POSTing to endpoint (HTTP 401) o muestra claramente 401 Unauthorized. Puede faltar la cabecera, el token puede ser incorrecto o la variable puede no haberse expandido. Este último caso es frecuente. Si MCP_TOKEN no está definida cuando el cliente lee la configuración, Claude Code advierte de la variable ausente en claude mcp list y envía el texto literal. Por tanto, nginx recibe Bearer ${MCP_TOKEN}. Si la variable tiene el mismo nombre que otra credencial de Claude Code, del proveedor cloud o del entorno, se lee como vacía. En ese caso, nginx recibe Bearer sin ningún valor. Muestre la variable, vuelva a añadir la cabecera y compruebe que la secuencia exacta de bytes coincide con el token de if de nginx.
El servicio no se inicia con systemd. journalctl -u mcp-ops muestra ModuleNotFoundError: No module named 'mcp' y ExecStart apunta al Python del sistema en lugar del intérprete del virtualenv. También puede aparecer Address already in use porque otro proceso ya usa 8000. Encuéntrelo con sudo ss -ltnp | grep 8000.
FAQ
¿Qué es exactamente un servidor MCP?
Es un programa que expone herramientas y recursos a un cliente de IA mediante el Model Context Protocol, usando JSON-RPC 2.0. El modelo de IA nunca ejecuta directamente la herramienta. Se lo solicita a su cliente, el cliente llama al servidor MCP y el servidor la ejecuta y devuelve un resultado. Como el protocolo es estándar, un servidor funciona con cualquier cliente compatible, ya sea Claude Code, Claude Desktop o Gemini CLI.
¿Cuál es la diferencia entre el transporte stdio y HTTP?
El cliente inicia un servidor stdio como proceso hijo, que se comunica mediante stdin/stdout. Por tanto, vive y termina con un único cliente en una máquina y no necesita red ni autenticación. Un servidor HTTP es un servicio de red de larga duración al que pueden conectarse varios clientes simultáneamente. Por eso requiere TLS y autenticación. Use stdio para herramientas locales y de un solo usuario. Use HTTP (Streamable HTTP en los servidores actuales) para cualquier herramienta compartida o persistente.
¿Cómo protejo un servidor MCP remoto?
Suponga que concede acceso mediante herramientas a sus archivos, base de datos o shell, y no lo exponga nunca sin autenticación. Lo mejor es mantenerlo enlazado a localhost y acceder mediante un túnel SSH o una VPN privada. Si debe ser público, colóquelo detrás de un reverse proxy que aplique un bearer token o el flujo OAuth de MCP. Genere el token con openssl rand -hex 32 y no enlace nunca el servidor a 0.0.0.0 sin uno de estos mecanismos delante.
¿Cómo depuro un servidor que no se inicia?
Compruebe primero claude mcp list. ✘ Failed to connect con spawn ... ENOENT indica que falta el comando o el runtime, por lo que debe corregir la ruta o instalarlo. Si se conecta y después se desconecta con un error de análisis de JSON, el servidor está registrando mensajes en stdout y corrompe el flujo JSON-RPC. Mueva todos los registros a stderr. Para cualquier otro caso, ejecute el comando exacto con MCP Inspector. Esta herramienta ejecuta el servidor de forma aislada, de modo que puede distinguir un error del servidor de un error de configuración del cliente.