SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-27

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 para guardarlo en un archivo que se pueda 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/api

El 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 ha configurado correctamente:

claude mcp list

Un servidor operativo muestra su comando y una marca 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_file, write_file, list_directory), y el agente ya puede utilizarlas en las rutas que haya permitido. Una herramienta de base de datos tiene la misma estructura: cambie el paquete y pase una cadena de conexión como argumento final. Consulte el repositorio del propio servidor para conocer el nombre actual del paquete, porque el servidor de Postgres de referencia ha cambiado de responsable más de una vez.

Este es el objetivo de 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 intermedio. Cuando el agente tiene write_file y read_file, conviene combinar ese alcance con una skill que lo oriente hacia el cambio mínimo que funcione, porque una herramienta de sistema de archivos hace que una reescritura extensa resulte exactamente tan sencilla como una corrección de dos líneas. La misma integración también sirve para recursos externos a 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, de modo que las consultas permanezcan en su servidor, pero el texto no confiable de las páginas se incorpore directamente al contexto sobre el que actuará el agente.

Paso 2: crear un servidor HTTP remoto

Un servidor stdio termina con su proceso principal y se inicia una vez por cliente. Por tanto, si ejecuta dos sesiones de Claude Code en el servidor que se pasan trabajo entre sí, cada una obtiene su propia copia privada de la herramienta. Cuando 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.fastmcp import FastMCP
import subprocess

mcp = FastMCP("ops-tools", host="127.0.0.1", port=8000)

@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")

Observe host="127.0.0.1". El servidor sólo se enlaza a localhost. Nada fuera del servidor puede acceder directamente a él, que es exactamente lo que necesita antes de disponer de autenticación. Instálelo en su propio virtualenv para que systemd use una ruta estable al intérprete:

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.target

La 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/mcp

status 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 prevalece en este tema

No exponga nunca un endpoint MCP sin autenticación. Un servidor MCP no es una API de sólo lectura. Concede acceso a herramientas que pueden acceder a sus archivos, su base de datos y, en algunos casos, un shell. Un /mcp abierto en Internet es un desconocido con el mismo alcance que su agente de IA: enumera sus herramientas y después las invoca. 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 servidor completo.

Tres defensas, en orden de preferencia:

  1. No lo publique. Mantenga el servidor en 127.0.0.1 y 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 usar http://127.0.0.1:8000/mcp. Nunca se expone nada.
  2. Colóquelo en una red privada. Vincule la dirección del túnel a una VPN WireGuard autohospedada y permita que sólo los peers de la VPN accedan al servicio. Internet público verá un puerto cerrado.
  3. 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 comprueba en el proxy. Es económico y bloquea 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 vincule el servidor directamente a 0.0.0.0 sin colocar una de estas defensas delante. Después, el cliente envía el token como 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 se escriba en .mcp.json en texto plano. Claude Code expande ${MCP_TOKEN} desde el entorno cuando lo lee.

Todas las defensas anteriores protegen el endpoint, no al agente que ya tiene el token. Esa es la otra mitad del problema: si su cliente es DeepSeek Harness, los plugins que controlan las herramientas que un agente puede invocar 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. Contrólelo directamente con Inspector, el cliente de pruebas web oficial. Para un servidor stdio, proporciónele el mismo comando que ejecuta el agente:

npx @modelcontextprotocol/inspector \
  npx -y @modelcontextprotocol/server-filesystem /tmp

Inicia una interfaz en http://localhost:6274 (las versiones recientes muestran una URL con una cadena de consulta MCP_PROXY_AUTH_TOKEN; use ese enlace exacto o la interfaz rechazará la conexión) y un proxy en 6277. Haga clic en Connect, después en List Tools y, por último, en Call Tool con argumentos reales. Si funciona en Inspector pero falla en el agente, el problema está en la configuración del cliente, no en el servidor. Para el servidor HTTP remoto, seleccione el transporte Streamable HTTP, introduzca https://mcp.example.com/mcp, añada la cabecera Authorization y conecte. Es la forma más rápida de confirmar que la autenticación y el proxy son correctos antes de involucrar al agente.

Mantener los servidores actualizados

MCP evoluciona rápidamente, por lo que debe aplicar los parches según un calendario. Los servidores de Node iniciados con npx -y obtienen la versión más reciente en cada ejecución. Esto resulta práctico, pero no permite reproducir exactamente el entorno. Fije la versión exacta que probó, léala desde 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 de 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. Al actualizar, supervise la revisión de la especificación a la que apunta su SDK. Un salto entre SSE y Streamable-HTTP puede cambiar el transporte que deben solicitar sus clientes.

Modos de fallo y mensajes que verá

El agente indica que el servidor ha fallado. claude mcp list muestra ✗ Failed to connect y la TUI informa de MCP server 'filesystem' failed to start. 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 ruta que busca el agente: Node no está instalado, falta npx o se ha indicado un Python de virtualenv por su nombre sin ruta. Corrija el comando para usar una ruta absoluta o instale el runtime. Después, vuelva a conectarse.

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 es siempre la misma: el servidor escribió una línea de registro en stdout. En stdio, stdout es el canal JSON-RPC. 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 personas 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 no muestra ninguna herramienta. Detrás de nginx, la causa es el buffering: el proxy retiene el flujo SSE en lugar de enviarlo progresivamente. Por eso el cliente espera una respuesta que nunca llega. Añada proxy_buffering off;, junto con el resto del bloque del paso 4, al location. Compruébelo con curl -N contra la URL pública. Debe ver que los datos de eventos llegan de forma progresiva, no todos al final.

La autenticación se rechaza. El cliente informa de Error POSTing to endpoint (HTTP 401) o simplemente de 401 Unauthorized. Puede faltar la cabecera, el token puede ser incorrecto o la variable de shell puede estar vacía cuando el cliente leyó la configuración. Es un error habitual: ${MCP_TOKEN} se expande a una cadena vacía si la variable no está definida y nginx recibe Bearer sin ningún valor. Muestre el valor de la variable, vuelva a añadir la cabecera y compruebe que los bytes coincidan exactamente con el token del 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. O bien Address already in use: otro proceso ya está usando 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 la herramienta directamente. 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 los transportes stdio y HTTP?

Un servidor stdio lo inicia el cliente como proceso hijo y 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 al mismo tiempo. Por eso requiere TLS y autenticación. Use stdio para herramientas locales de un solo usuario. Use HTTP (Streamable HTTP en los servidores actuales) para cualquier servicio compartido o persistente.

¿Cómo protejo un servidor MCP remoto?

Suponga que proporciona 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?

Primero compruebe claude mcp list. ✗ Failed to connect con spawn ... ENOENT indica que falta el comando o el entorno de ejecución. Corrija la ruta o instálelo. Si se conecta y después se desconecta con un error de análisis JSON, el servidor está escribiendo registros en stdout y corrompiendo el flujo JSON-RPC. Traslade todos los registros a stderr. Para cualquier otro problema, ejecute el comando exacto con MCP Inspector. Esta herramienta controla el servidor de forma aislada, de modo que puede distinguir un error del servidor de un error de configuración del cliente.