SSD Nodes Learn
Guías Matt ConnorPor Matt Connor · Actualizado 2026-07-24

Cómo ejecutar servidores MCP en un VPS

Guía para configurar servidores MCP mediante stdio y HTTP remoto con systemd, nginx y TLS para dar herramientas reales a agentes de IA en un VPS propio.

Qué va a construir

Dos configuraciones de MCP funcionales en un solo VPS. Primero, un servidor stdio: una herramienta de sistema de archivos o base de datos que Claude Code lanza como un proceso hijo y con la que se comunica mediante un pipe. Segundo, un servidor remote HTTP que se ejecuta como un servicio de red persistente bajo systemd y un proxy inverso nginx con TLS, accesible por cualquier cliente MCP que se conecte a él. La instalación de ambos es sencilla. La mayor parte de esta guía se centra en los dos aspectos críticos: mantener limpio el flujo JSON-RPC y no exponer nunca un endpoint de herramienta sin autenticación en internet.

Qué es MCP realmente

El Model Context Protocol es un estándar para que un cliente de IA —Claude Code, Claude Desktop, el Gemini CLI en un VPS o su propio script— ejecute herramientas externas y lea recursos externos. El modelo no ejecuta nada directamente. El modelo realiza la petición al cliente, el cliente envía mensajes JSON-RPC 2.0 a un servidor MCP, y el servidor ejecuta la herramienta y devuelve el resultado. Un solo protocolo permite que un servidor escrito una vez funcione con cualquier cliente que soporte MCP.

Existen dos tipos de transporte, 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 a través de su standard input y standard output. Sin red, sin puertos y sin autenticación; el límite de confianza es el propio proceso. Casi todas las herramientas locales funcionan de esta manera.
  • Streamable HTTP (y su versión anterior, HTTP+SSE). El servidor es un servicio web de ejecución continua. El cliente se conecta vía HTTP y el servidor puede enviar respuestas mediante Server-Sent Events. Este método permite compartir un servidor con múltiples clientes o ejecutar una herramienta que debe residir permanentemente en el equipo.

Use stdio cuando la herramienta pertenezca a una sola máquina y a un solo usuario. Use HTTP cuando se trate de un servicio compartido.

Requisitos previos y detalles importantes

Asuma que dispone de un VPS Ubuntu 24.04 KVM recién instalado con privilegios root o sudo. Además de eso:

  • Un entorno de ejecución para el servidor. La mayoría de los servidores de referencia usan Node o Python. Ubuntu 24.04 incluye Node 18, pero varios paquetes MCP actuales requieren Node 20 o superior; instale una versión LTS reciente desde NodeSource o nvm en lugar de confiar en apt. Python 3.12 ya está instalado.
  • Un dominio y un registro DNS A, pero solo para el servidor HTTP remoto; TLS requiere un nombre que resuelva hacia este VPS. El ejemplo de stdio no requiere DNS.
  • 512 MB de RAM son suficientes. Los servidores MCP son procesos JSON-RPC ligeros; el consumo de memoria depende de lo que utilice su herramienta (un driver de base de datos, un caché de archivos), no del protocolo.
  • La especificación es reciente y está en evolución. La revisión del 2025-03-26 reemplazó HTTP+SSE por Streamable HTTP y marcó SSE como deprecated. SSE sigue funcionando y muchos servidores aún lo utilizan, por lo que cualquier restricción de transporte debe verificarse contra las notas de la versión del servidor en lugar de considerarse definitiva.

Paso 1: conectar un servidor stdio en Claude Code

Comience con el servidor de filesystem; es oficial, tiene mantenimiento activo y solo requiere Node. El siguiente comando lo registra en Claude Code y lo limita al proyecto actual para que se guarde en un archivo committable:

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 un flag de Claude Code. Esto escribe un .mcp.json en la raíz del proyecto:

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/home/matt/projects/api"
      ]
    }
  }
}

Nada se está ejecutando todavía. La próxima vez que inicie Claude Code en este directorio, el agente leerá .mcp.json, iniciará npx -y @modelcontextprotocol/server-filesystem ... como un proceso hijo y realizará el handshake de MCP a través del stdin/stdout de dicho proceso. Confirme que se ha configurado correctamente:

claude mcp list

Un servidor funcional imprime su comando y una marca de verificación verde — filesystem: npx -y @modelcontextprotocol/server-filesystem ... - ✓ Connected. Dentro de la sesión, el comando slash /mcp enumera las herramientas que el servidor expone (read_file, write_file, list_directory), y el agente ahora puede llamarlas en las rutas que usted autorizó. Una herramienta de base de datos tiene la misma estructura — cambie el paquete y pase una cadena de conexión como último argumento — pero verifique el nombre del paquete actual en el repositorio del servidor, ya que el servidor de referencia de Postgres ha cambiado de manos varias veces.

Este es el objetivo principal de ejecutar el agente en el equipo: la sesión de Claude Code reside en el VPS dentro de tmux, y sus servidores stdio se ejecutan junto a ella con acceso directo a los archivos del proyecto y servicios locales, sin latencia de red.

Paso 2: construir un servidor HTTP remoto

Un servidor stdio termina cuando muere su proceso padre. Si necesita una herramienta que permanezca activa para cada cliente —una herramienta de operaciones compartida, una pasarela de base de datos o algo que utilicen tanto su laptop como su CI— requiere el transporte HTTP y un servicio real. Aquí tiene un servidor Python mínimo que utiliza 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")

Considere host="127.0.0.1". El servidor solo se vincula a localhost; nada fuera del equipo puede alcanzarlo directamente, que es el comportamiento deseado antes de implementar la autenticación. Instálelo en su propio virtualenv para que systemd tenga 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 no responde cuando el agente la solicita es peor que no tener ninguna herramienta. 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 es obligatoria — apunte a /usr/bin/python3 y el proceso iniciará con ModuleNotFoundError: No module named 'mcp', ya que el intérprete del sistema no reconoce su pip install. Habilite y verifique:

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). El curl devuelve HTTP/1.1 400 Bad Request con un error JSON-RPC en el cuerpo — la solicitud no incluyó sesión ni un payload JSON válido — y eso es exactamente lo que se busca: confirma que el puerto responde y utiliza el protocolo. Connection refused o una respuesta vacía significa que el proceso no está vinculado en la dirección indicada; revise journalctl -u mcp-ops -n 50.

Paso 4: configurar TLS y un proxy inverso

El servidor escucha en localhost. Para acceder desde cualquier lugar, debe terminar la conexión TLS en nginx y realizar el proxy hacia el interior. Instale nginx, obtenga un certificado con Certbot y Let's Encrypt en nginx y luego escriba el bloque location. El paso crítico es desactivar el buffering. El comportamiento por defecto de nginx retiene la respuesta hasta que se completa, lo que detiene un flujo SSE permanentemente:

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 con sudo nginx -t && sudo systemctl reload nginx. Si ya utiliza un grupo de contenedores, el mismo proceso lo realiza un proxy inverso Traefik con TLS automático; este emite el certificado y enruta por hostname, y usted solo debe añadir labels al contenedor MCP. En cualquier caso, el proxy inverso es ahora el único elemento en un puerto público y apunta a un servicio que aún no ha asegurado. Corrija esto antes de registrar la URL en cualquier lugar.

Paso 5: la regla de seguridad fundamental de este tema

Nunca exponga un endpoint de MCP sin autenticación. Un servidor MCP no es una API de solo lectura. Otorga acceso a herramientas: a sus archivos, su base de datos y, en ocasiones, a una shell. Un /mcp abierto en internet es un extraño con el mismo alcance que su agente de IA: listan sus herramientas y luego las ejecutan. Trátelo exactamente como un socket de administración sin autenticación, porque eso es lo que es.

Tres defensas, en orden de preferencia:

  1. No lo publique. Mantenga el servidor en 127.0.0.1 y acceda desde su laptop mediante un túnel SSH: ssh -L 8000:127.0.0.1:8000 matt@vps, luego apunte el cliente a http://127.0.0.1:8000/mcp. Nada queda expuesto.
  2. Colóquelo en una red privada. Vincule la dirección del túnel de un VPN WireGuard self-hosted y permita que solo los pares de la VPN tengan acceso. Internet solo verá un puerto cerrado.
  3. Si debe ser público, requiera un token. La solución correcta es el flujo MCP OAuth que el transporte HTTP soporta de forma nativa. El mínimo pragmático es un bearer token compartido verificado en el proxy; es sencillo y detiene ataques accidentales por completo:
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 uno de estos métodos delante. El cliente enviará el token como un header. En Claude Code:

claude mcp add --scope project --transport http ops-tools https://mcp.example.com/mcp \
  --header 'Authorization: Bearer ${MCP_TOKEN}'

Configure MCP_TOKEN en su shell para que el secreto nunca se guarde en .mcp.json en texto plano; Claude Code expande ${MCP_TOKEN} desde el entorno en el momento de la lectura.

Paso 6: depurar con el MCP Inspector

Cuando un servidor presenta errores, no intente adivinar desde el agente; ejecútelo directamente con el Inspector, el cliente de prueba oficial basado en web. Para un servidor stdio, utilice el mismo comando que ejecuta el agente:

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

Esto inicia una interfaz de usuario 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 el puerto 6277. Haga clic en Connect, luego en List Tools, y después en Call Tool con argumentos reales. Si funciona en el Inspector pero falla en el agente, el error está en la configuración del cliente y no en el servidor. Para un servidor HTTP remoto, elija el transporte Streamable HTTP, ingrese https://mcp.example.com/mcp, añada el encabezado Authorization y conecte; este es el método más rápido para verificar que la autenticación y el proxy son correctos antes de usar un agente.

Mantener los servidores actualizados

MCP evoluciona rápido; aplique parches siguiendo un cronograma. Los servidores Node lanzados con npx -y obtienen la última versión en cada ejecución; esto es conveniente pero no es reproducible. Fije la versión exacta que haya probado —obtenga la versión de npm view @modelcontextprotocol/server-filesystem version y añádala al nombre del paquete en .mcp.json (@modelcontextprotocol/server-filesystem@<version>)— cuando un servidor sea crítico, y actualícelo de forma deliberada. Los servidores Python bajo 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. Verifique la revisión de la especificación que utiliza su SDK al actualizar; un salto a través del límite SSE-to-Streamable-HTTP puede cambiar el transporte que sus clientes deben solicitar.

Modos de fallo y los strings que verá

El agente indica que el servidor falló. claude mcp list imprime ✗ Failed to connect y la TUI reporta MCP server 'filesystem' failed to start. Ejecute claude --debug y normalmente verá Error: spawn npx ENOENT — el comando no está en el PATH del agente. El runtime no está instalado o no se encuentra en la ruta que busca el agente: Node no está instalado, npx está ausente, o se referencia un virtualenv de Python mediante su nombre simple. Corrija el comando usando una ruta absoluta o instale el runtime, y luego reconecte.

Un servidor stdio se conecta y se desconecta instantáneamente. El cliente registra un error de parseo de JSON — algo 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 log en stdout. En modo stdio, stdout es el canal JSON-RPC, por lo que cualquier texto adicional corrompe el flujo y el handshake falla. En Node, console.log se envía a stdout — use console.error. En Python, un print() simple se envía a stdout — escriba los logs con logging configurado hacia sys.stderr, o pase file=sys.stderr. La regla es absoluta: en stdio, solo JSON-RPC en stdout; todo el contenido legible para humanos debe ir en stderr.

Un servidor remoto agota el tiempo de espera (timeout) o se cierra durante el handshake. El cliente falla con MCP error -32000: Connection closed, o el Inspector se queda bloqueado en Connect y nunca muestra las herramientas. Detrás de nginx esto es un problema de buffering: el proxy retiene el flujo SSE en lugar de vaciarlo (flush), por lo que el cliente espera una respuesta que nunca llega. Añada proxy_buffering off; (y el resto del bloque en el Paso 4) a la location. Confirme con curl -N contra la URL pública — debería ver los datos de los eventos llegar de forma incremental, no todos a la vez al final.

La autenticación es rechazada. El cliente reporta Error POSTing to endpoint (HTTP 401) o simplemente 401 Unauthorized. El header falta, el token es incorrecto, o la variable de shell estaba vacía cuando el cliente leyó la configuración — una trampa común, ya que ${MCP_TOKEN} se expande a nada si la variable no está definida y nginx recibe Bearer sin valor. Ejecute un echo de la variable, vuelva a añadir el header y verifique que los bytes coincidan exactamente con el token en la if de nginx.

El servicio no inicia bajo systemd. journalctl -u mcp-ops muestra ModuleNotFoundError: No module named 'mcp'ExecStart apunta al Python del sistema en lugar del intérprete del venv. O Address already in use — otro proceso está usando el puerto 8000; identifíquelo 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; solicita la acción al cliente, el cliente llama al servidor MCP, y el servidor ejecuta la tarea y devuelve el resultado. Al ser un protocolo estándar, un mismo servidor funciona con cualquier cliente compatible, como Claude Code, Claude Desktop o Gemini CLI.

¿Cuál es la diferencia entre el transporte stdio y HTTP?

Un servidor stdio es iniciado por el cliente como un proceso hijo y se comunica mediante stdin/stdout. Por tanto, su ciclo de vida depende de un único cliente en una sola máquina y no requiere red ni autenticación. Un servidor HTTP es un servicio de red persistente al que pueden acceder múltiples clientes simultáneamente, por lo que requiere TLS y autenticación. Use stdio para herramientas locales de un solo usuario; use HTTP (Streamable HTTP en servidores actuales) para cualquier recurso compartido o persistente.

¿Cómo aseguro un servidor MCP remoto?

Considere que el servidor otorga acceso a sus archivos, bases de datos o shell; nunca lo exponga sin autenticación. La mejor opción es mantenerlo vinculado 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 exija un bearer token o el flujo MCP OAuth. Genere el token con openssl rand -hex 32 y nunca vincule el servidor a 0.0.0.0 sin uno de estos mecanismos de seguridad delante.

¿Cómo depuro un servidor que no inicia?

Primero verifique claude mcp list. ✗ Failed to connect con spawn ... ENOENT indica que falta el comando o el runtime; corrija la ruta o instálelo. Si el servidor se conecta pero se desconecta con un error de parseo JSON, el servidor está enviando logs a stdout y corrompiendo el flujo JSON-RPC; redirija todos los logs a stderr. Para cualquier otro problema, ejecute el comando exacto bajo el MCP Inspector; este ejecuta el servidor de forma aislada para distinguir si el error es del servidor o de la configuración del cliente.