Construir y alojar un servidor MCP personalizado en un VPS

12 min de lectura·Matthieu·mcptypescriptnodejsnginxself-hostingai-agents|

Construye un servidor MCP en TypeScript desde cero, desplegalo en un VPS con systemd y ejecutalo detras de Nginx con TLS. Conecta Claude Desktop, Claude Code y Cursor a tu servidor autoalojado.

Todos los tutoriales de servidores MCP terminan en "ejecutalo en local con stdio." Este cubre el camino completo: construir un servidor MCP en TypeScript, desplegarlo en un VPS detras de Nginx con TLS, protegerlo con autenticacion y reglas de firewall, y conectar clientes MCP por internet.

Al final, tendras un servidor MCP en produccion al que Claude Desktop, Claude Code, Cursor o cualquier cliente compatible con MCP puede conectarse de forma remota.

Que vas a construir?

Los clientes MCP envian peticiones JSON-RPC sobre HTTPS. Nginx termina TLS y redirige las peticiones al proceso del servidor MCP que escucha en un puerto local. El servidor MCP valida un bearer token, procesa la peticion y devuelve los resultados.

MCP Client (Claude, Cursor, ...)
        |
        | HTTPS (port 443)
        v
   Nginx (TLS termination + reverse proxy)
        |
        | HTTP (port 3000, localhost only)
        v
   MCP Server (Node.js + Express)
        |
        v
   SQLite database

El ejemplo funcional es un servidor de notas. Expone dos herramientas (buscar notas, crear nota) y un recurso (listar todas las notas). Lo bastante simple para seguirlo, lo bastante util para extenderlo.

Que transporte usar para un servidor MCP remoto?

Streamable HTTP es el estandar actual de la especificacion MCP para despliegues remotos. Reemplazo al transporte SSE (obsoleto) y funciona con peticiones HTTP POST normales. Stdio sigue siendo el transporte para servidores locales donde el cliente lanza el servidor como subproceso.

Transport Caso de uso Soporte remoto Soporte de clientes
stdio Herramientas locales, integraciones CLI No Todos los clientes
SSE (obsoleto) Servidores remotos legacy Si La mayoria (en desuso)
Streamable HTTP Servidores remotos, produccion Si Claude Desktop, Claude Code, Cursor

Streamable HTTP envia cada mensaje del cliente como un HTTP POST a un unico endpoint (p.ej., /mcp). El servidor responde con application/json para respuestas simples o text/event-stream cuando necesita enviar varios mensajes de vuelta. Esto significa que funciona con infraestructura HTTP estandar, incluyendo reverse proxies y balanceadores de carga.

Requisitos previos

  • Un VPS con Debian 12 o Ubuntu 24.04 (esta guia usa Debian 12)
  • Un nombre de dominio con DNS apuntando a tu VPS (p.ej., mcp.example.com)
  • Node.js 20+ instalado en el VPS
  • Familiaridad basica con TypeScript y Linux

Como configurar un proyecto de servidor MCP en TypeScript?

Empieza en tu maquina local o directamente en el VPS. Inicializa el proyecto, instala el SDK de MCP y configura TypeScript.

mkdir mcp-notes-server && cd mcp-notes-server
npm init -y
npm install @modelcontextprotocol/sdk zod express better-sqlite3
npm install -D typescript @types/node @types/express @types/better-sqlite3

Crea la configuracion de TypeScript:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "Node16",
    "moduleResolution": "Node16",
    "outDir": "./build",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

Guarda esto como tsconfig.json en la raiz del proyecto.

Actualiza package.json para agregar los scripts de build y start, y establece el tipo de modulo:

{
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node build/index.js"
  }
}

Crea el directorio de codigo fuente:

mkdir src

Como implementar herramientas MCP con validacion de entrada?

Un servidor MCP expone tools (herramientas), resources (recursos) y prompts a los clientes de IA. Las herramientas son funciones que la IA puede llamar. Los recursos son datos de solo lectura que la IA puede consultar. Implementaremos ambos.

Crea src/index.ts con la configuracion de la base de datos y una funcion factory que construye un servidor MCP nuevo para cada peticion. Streamable HTTP sin estado requiere una nueva instancia de McpServer por peticion porque el SDK no permite reconectar el mismo servidor a un transporte diferente.

import express from "express";
import { randomUUID } from "crypto";
import Database from "better-sqlite3";
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";

// --- Database setup ---

const db = new Database("notes.db");
db.pragma("journal_mode = WAL");
db.exec(`
  CREATE TABLE IF NOT EXISTS notes (
    id TEXT PRIMARY KEY,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    created_at TEXT DEFAULT (datetime('now'))
  )
`);

// --- MCP Server factory ---
// Each stateless request needs its own McpServer + transport pair.
// The SDK binds a server to a single transport on connect() and
// throws if you try to reuse it. A factory function keeps it clean.

function createServer(): McpServer {
  const server = new McpServer({
    name: "notes-server",
    version: "1.0.0",
  });

Ahora registra las herramientas dentro del factory. Cada herramienta usa Zod para validacion de entrada. El SDK valida las entradas automaticamente antes de que se ejecute tu handler.

  // --- Tools ---

  server.tool(
    "search_notes",
    "Search notes by keyword in title or content",
    {
      query: z.string().min(1).describe("Search keyword"),
    },
    async ({ query }) => {
      const stmt = db.prepare(
        "SELECT id, title, content, created_at FROM notes WHERE title LIKE ? OR content LIKE ? ORDER BY created_at DESC LIMIT 20"
      );
      const rows = stmt.all(`%${query}%`, `%${query}%`);
      return {
        content: [
          {
            type: "text" as const,
            text: JSON.stringify(rows, null, 2),
          },
        ],
      };
    }
  );

  server.tool(
    "create_note",
    "Create a new note with a title and content",
    {
      title: z.string().min(1).max(200).describe("Note title"),
      content: z.string().min(1).describe("Note content"),
    },
    async ({ title, content }) => {
      const id = randomUUID();
      const stmt = db.prepare(
        "INSERT INTO notes (id, title, content) VALUES (?, ?, ?)"
      );
      stmt.run(id, title, content);
      return {
        content: [
          {
            type: "text" as const,
            text: `Note created with id: ${id}`,
          },
        ],
      };
    }
  );

Cada entrada se valida con Zod antes de llegar al handler. Si un cliente envia { query: "" }, el SDK devuelve un error de validacion automaticamente. No hace falta parsear nada manualmente.

Como agregar recursos a un servidor MCP?

Los recursos dan a los clientes de IA acceso de solo lectura a datos. Se referencian por URI. Agrega un recurso que liste todas las notas, todavia dentro del factory createServer:

  // --- Resources ---

  server.resource(
    "all-notes",
    "notes://all",
    {
      description: "List all notes in the database",
      mimeType: "application/json",
    },
    async (uri) => {
      const rows = db.prepare(
        "SELECT id, title, created_at FROM notes ORDER BY created_at DESC LIMIT 100"
      ).all();
      return {
        contents: [
          {
            uri: uri.href,
            mimeType: "application/json",
            text: JSON.stringify(rows, null, 2),
          },
        ],
      };
    }
  );

  return server;
}

Los clientes MCP pueden descubrir este recurso y leerlo sin invocar una herramienta. Claude Desktop muestra los recursos en el menu de adjuntos. Claude Code los expone a traves de menciones con @.

Como proteger un servidor MCP con autenticacion por token?

Un servidor MCP en produccion necesita autenticacion. Sin ella, cualquiera que descubra tu endpoint puede invocar tus herramientas. Usa un bearer token verificado en un middleware de Express antes de que las peticiones lleguen al transporte MCP.

Genera un token seguro:

openssl rand -base64 32

Guarda la salida. La necesitaras tanto para la configuracion del servidor como para la del cliente.

Agrega el middleware de autenticacion y las rutas de Express a src/index.ts:

// --- Auth middleware ---

const AUTH_TOKEN = process.env.MCP_AUTH_TOKEN;
if (!AUTH_TOKEN) {
  console.error("MCP_AUTH_TOKEN environment variable is required");
  process.exit(1);
}

function authenticateRequest(
  req: express.Request,
  res: express.Response,
  next: express.NextFunction
): void {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    res.status(401).json({
      jsonrpc: "2.0",
      error: { code: -32001, message: "Missing or invalid authorization" },
      id: null,
    });
    return;
  }
  const token = authHeader.slice(7);
  if (token !== AUTH_TOKEN) {
    res.status(403).json({
      jsonrpc: "2.0",
      error: { code: -32001, message: "Invalid token" },
      id: null,
    });
    return;
  }
  next();
}

// --- Express app ---

const app = express();
app.use(express.json());
app.use("/mcp", authenticateRequest);

El middleware se ejecuta antes de cada peticion a /mcp. Comprueba que exista un token Bearer en el header Authorization y rechaza las peticiones no autorizadas con una respuesta de error JSON-RPC.

Como configurar el transporte Streamable HTTP?

Agrega la configuracion del transporte y los handlers de rutas. Cada peticion crea un McpServer nuevo desde el factory y una instancia de transporte nueva:

// --- Streamable HTTP transport ---

app.post("/mcp", async (req, res) => {
  const server = createServer();
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
  });
  await server.connect(transport);
  try {
    await transport.handleRequest(req, res, req.body);
  } catch (error) {
    console.error("MCP request error:", error);
    if (!res.headersSent) {
      res.status(500).json({
        jsonrpc: "2.0",
        error: { code: -32603, message: "Internal server error" },
        id: null,
      });
    }
  }
});

app.get("/mcp", (_req, res) => {
  res.status(405).json({
    jsonrpc: "2.0",
    error: { code: -32000, message: "Method not allowed. Use POST." },
    id: null,
  });
});

app.delete("/mcp", (_req, res) => {
  res.status(405).json({
    jsonrpc: "2.0",
    error: { code: -32000, message: "Method not allowed." },
    id: null,
  });
});

// --- Health check ---

app.get("/health", (_req, res) => {
  res.json({ status: "ok" });
});

// --- Start ---

const PORT = parseInt(process.env.PORT || "3000", 10);
app.listen(PORT, "127.0.0.1", () => {
  console.log(`MCP server listening on http://127.0.0.1:${PORT}/mcp`);
});

Configurar sessionIdGenerator: undefined crea un servidor sin estado. Cada peticion es independiente, con su propio par de McpServer y transporte. Esto es mas simple de desplegar y funciona bien detras de un reverse proxy sin necesidad de sesiones fijas. La conexion a la base de datos (db) vive a nivel de modulo y se comparte entre peticiones, asi que los datos persisten entre llamadas.

El servidor escucha en 127.0.0.1, no en 0.0.0.0. Solo acepta conexiones desde localhost. Nginx sera la unica forma de acceder desde fuera.

Compila y prueba en local:

npm run build

Verifica que la compilacion termine sin errores.

Como probar un servidor MCP en local?

El MCP Inspector es la herramienta oficial de pruebas. Se conecta a tu servidor y te permite invocar herramientas de forma interactiva.

Inicia tu servidor en un terminal:

MCP_AUTH_TOKEN=test-token-local npm start

En otro terminal, prueba con curl:

curl -X POST http://127.0.0.1:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer test-token-local" \
  -d '{
    "jsonrpc": "2.0",
    "method": "initialize",
    "params": {
      "protocolVersion": "2025-03-26",
      "capabilities": {},
      "clientInfo": { "name": "test-client", "version": "1.0.0" }
    },
    "id": 1
  }'

Deberias ver una respuesta SSE (event: message seguido de una linea data:) que contiene las capacidades del servidor, incluyendo los objetos tools y resources. Esto confirma que el servidor acepta conexiones y responde al handshake de inicializacion MCP.

Prueba la herramienta create_note:

curl -X POST http://127.0.0.1:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer test-token-local" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "create_note",
      "arguments": { "title": "Test Note", "content": "Hello from curl" }
    },
    "id": 2
  }'

La respuesta deberia contener "Note created with id: ...". Ahora prueba la herramienta de busqueda:

curl -X POST http://127.0.0.1:3000/mcp \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -H "Authorization: Bearer test-token-local" \
  -d '{
    "jsonrpc": "2.0",
    "method": "tools/call",
    "params": {
      "name": "search_notes",
      "arguments": { "query": "Test" }
    },
    "id": 3
  }'

Deberias ver la nota que acabas de crear en los resultados. Verifica que enviar una peticion sin el header Authorization devuelve un error 401:

curl -s -o /dev/null -w "%{http_code}" -X POST http://127.0.0.1:3000/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","method":"initialize","params":{},"id":1}'

Esto deberia imprimir 401. Si es asi, tu middleware de autenticacion funciona.

Tambien puedes usar el MCP Inspector para pruebas interactivas con interfaz web:

npx @modelcontextprotocol/inspector

El Inspector abre una interfaz en el navegador donde puedes conectarte a tu servidor, explorar herramientas y recursos, y probar invocaciones.

Como desplegar un servidor MCP en un VPS?

Transfiere tu proyecto al VPS usando rsync, scp, o sube a un repositorio Git y clona en el servidor. Los siguientes pasos asumen que has iniciado sesion en el VPS.

Crear un usuario dedicado

Nunca ejecutes procesos de aplicacion como root. Crea un usuario de servicio:

sudo useradd -r -m -s /usr/sbin/nologin mcpserver

Instalar Node.js

Si Node.js 20+ no esta instalado:

curl -fsSL https://deb.nodesource.com/setup_20.x -o nodesource_setup.sh
sudo bash nodesource_setup.sh
sudo apt-get install -y nodejs
node --version
npm --version

El primer comando descarga el script de configuracion a un archivo para que puedas inspeccionarlo antes de ejecutarlo. Verifica que la salida muestre v20.x.x o superior para Node.js y 10.x.x para npm. El SDK de MCP requiere Node.js 18+ pero se recomienda 20 LTS para produccion.

Configurar el directorio del proyecto

sudo mkdir -p /opt/mcp-notes-server
sudo cp -r /path/to/your/project/* /opt/mcp-notes-server/
cd /opt/mcp-notes-server
sudo npm ci --omit=dev
sudo npm run build
sudo chown -R mcpserver:mcpserver /opt/mcp-notes-server

Verifica la propiedad:

ls -la /opt/mcp-notes-server/

Todos los archivos deben pertenecer a mcpserver:mcpserver.

Crear el archivo de entorno

Almacena el token de autenticacion en un archivo de entorno con permisos restringidos:

sudo mkdir -p /etc/mcp-notes-server
echo "MCP_AUTH_TOKEN=$(openssl rand -base64 32)" | sudo tee /etc/mcp-notes-server/env > /dev/null
sudo chmod 600 /etc/mcp-notes-server/env
sudo chown mcpserver:mcpserver /etc/mcp-notes-server/env

Lee el token generado y guardalo en un lugar seguro. Lo necesitaras para la configuracion del cliente:

sudo cat /etc/mcp-notes-server/env

Crear el servicio systemd

sudo tee /etc/systemd/system/mcp-notes-server.service > /dev/null << 'EOF'
[Unit]
Description=MCP Notes Server
After=network.target

[Service]
Type=simple
User=mcpserver
Group=mcpserver
WorkingDirectory=/opt/mcp-notes-server
EnvironmentFile=/etc/mcp-notes-server/env
ExecStart=/usr/bin/node build/index.js
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal

# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/mcp-notes-server
PrivateTmp=true

[Install]
WantedBy=multi-user.target
EOF

EnvironmentFile carga MCP_AUTH_TOKEN desde el archivo restringido. La seccion de hardening limita lo que el proceso puede hacer. NoNewPrivileges previene la escalada de privilegios. ProtectSystem=strict hace el sistema de archivos de solo lectura excepto las rutas listadas en ReadWritePaths. ProtectHome bloquea el acceso a los directorios home de los usuarios.

Habilita e inicia el servicio:

sudo systemctl enable --now mcp-notes-server

enable hace que el servicio arranque en el inicio del sistema. --now lo inicia de inmediato.

Comprueba el estado:

sudo systemctl status mcp-notes-server

Deberias ver active (running). Si el servicio fallo, revisa los logs:

journalctl -u mcp-notes-server -n 50 --no-pager

Verifica que el servidor responde en local:

TOKEN=$(sudo grep MCP_AUTH_TOKEN /etc/mcp-notes-server/env | cut -d= -f2)
curl -s http://127.0.0.1:3000/health

Esto deberia devolver {"status":"ok"}.

Como ejecutar un servidor MCP detras de Nginx con TLS?

Nginx termina TLS y redirige las peticiones al servidor MCP. El servidor MCP nunca maneja TLS directamente. Este es el patron estandar para despliegues de Node.js en produccion.

Instalar Nginx y Certbot

sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx

Configurar el bloque de servidor Nginx

sudo tee /etc/nginx/sites-available/mcp.example.com.conf > /dev/null << 'NGINX'
server {
    listen 80;
    listen [::]:80;
    server_name mcp.example.com;

    # Certbot will add the redirect to HTTPS
    location / {
        return 444;
    }
}
NGINX

Reemplaza mcp.example.com con tu dominio real.

Habilita el sitio y obten un certificado TLS:

sudo ln -s /etc/nginx/sites-available/mcp.example.com.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d mcp.example.com

Certbot modifica la configuracion para agregar los ajustes TLS y una redireccion de HTTP a HTTPS. Ahora actualiza la configuracion para agregar el reverse proxy para el endpoint MCP:

sudo tee /etc/nginx/sites-available/mcp.example.com.conf > /dev/null << 'NGINX'
server {
    listen 80;
    listen [::]:80;
    server_name mcp.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name mcp.example.com;

    ssl_certificate /etc/letsencrypt/live/mcp.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/mcp.example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    # Hide Nginx version
    server_tokens off;

    # MCP endpoint
    location /mcp {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;

        # Required for Streamable HTTP / SSE responses
        proxy_buffering off;
        proxy_cache off;
        chunked_transfer_encoding off;

        # Keep connection open for streaming responses
        proxy_set_header Connection "";

        # Extended timeouts for long-running tool calls
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;

        # Forward client info
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # Pass through Authorization header
        proxy_set_header Authorization $http_authorization;
    }

    # Health check (no auth required)
    location /health {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
    }

    # Block everything else
    location / {
        return 444;
    }
}
NGINX

Las directivas clave para compatibilidad con MCP:

  • proxy_buffering off evita que Nginx almacene en buffer las respuestas SSE. Sin esto, las respuestas en streaming se quedan colgadas hasta que la conexion se cierra.
  • proxy_cache off asegura que no se cacheen respuestas dinamicas.
  • chunked_transfer_encoding off evita problemas de chunking con los flujos de eventos SSE.
  • Connection "" mantiene la conexion viva sin activar el comportamiento de upgrade WebSocket.
  • proxy_read_timeout 300s permite llamadas a herramientas que tardan hasta 5 minutos en completarse. El valor por defecto de 60s es demasiado corto para operaciones complejas.

Prueba y recarga:

sudo nginx -t
sudo systemctl reload nginx

Verifica desde tu maquina local (no el servidor):

curl -s https://mcp.example.com/health

Esto deberia devolver {"status":"ok"}. Si es asi, Nginx esta redirigiendo correctamente al servidor MCP sobre TLS.

Reglas de firewall

Bloquea el acceso directo al puerto 3000 desde fuera. Solo Nginx (en localhost) deberia llegar al servidor MCP:

sudo apt install -y ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Verifica que el puerto 3000 no es accesible desde fuera:

# From your local machine, this should time out or be refused:
curl -s --connect-timeout 5 http://YOUR_VPS_IP:3000/health

La conexion deberia fallar. Tu servidor MCP solo es accesible a traves de Nginx en el puerto 443.

Como conectar clientes MCP a un servidor remoto?

Con el servidor corriendo detras de Nginx con TLS, configura tus clientes MCP para conectarse.

Claude Code

Agrega el servidor remoto usando el CLI:

claude mcp add --transport http \
  --header "Authorization: Bearer YOUR_TOKEN_HERE" \
  notes-server https://mcp.example.com/mcp

Reemplaza YOUR_TOKEN_HERE con el token de /etc/mcp-notes-server/env.

Verifica la conexion:

claude mcp list

Dentro de Claude Code, escribe /mcp para comprobar el estado del servidor. El notes-server deberia aparecer como conectado con las herramientas search_notes y create_note disponibles.

Claude Desktop

Claude Desktop se conecta a servidores MCP remotos a traves de la interfaz de Configuracion. Ve a Settings > Connectors y agrega un nuevo servidor remoto con la URL https://mcp.example.com/mcp.

Si necesitas pasar el bearer token, tambien puedes configurarlo usando un archivo .mcp.json local o el comando claude mcp add del CLI mostrado arriba e importar el servidor en Claude Desktop:

claude mcp add-from-claude-desktop

Cursor

Abre la configuracion de Cursor y navega a la configuracion MCP. Agrega un nuevo servidor en el archivo mcp.json (o a traves de la interfaz de configuracion de Cursor):

{
  "mcpServers": {
    "notes-server": {
      "url": "https://mcp.example.com/mcp",
      "headers": {
        "Authorization": "Bearer YOUR_TOKEN_HERE"
      }
    }
  }
}

Cursor intenta Streamable HTTP primero al conectarse a una URL, asi que no hace falta configuracion de transporte adicional.

Verificar la conexion de extremo a extremo

Desde cualquier cliente conectado, pide a la IA que:

  1. Cree una nota: "Crea una nota titulada 'Server Test' con contenido 'Connected successfully'"
  2. La busque: "Busca en mis notas 'Server Test'"

Si ambas operaciones devuelven resultados, la cadena completa funciona: cliente a Nginx a servidor MCP a SQLite y de vuelta.

Referencia de primitivas MCP

Primitiva Proposito Ejemplo
Tools Funciones que la IA puede llamar con entradas estructuradas search_notes, create_note
Resources Datos de solo lectura que la IA puede consultar Listado de notas notes://all
Prompts Plantillas predefinidas para tareas comunes No usado en este tutorial

Las herramientas son la primitiva mas implementada. Los recursos son utiles para exponer datos de referencia sin computacion. Los prompts permiten crear plantillas reutilizables que guian a la IA en flujos de trabajo especificos.

Algo salio mal?

El servidor no arranca: Revisa el journal en busca de errores:

journalctl -u mcp-notes-server -n 50 --no-pager

Causas comunes: variable de entorno MCP_AUTH_TOKEN faltante, permisos incorrectos en notes.db, version de Node.js demasiado antigua.

Nginx devuelve 502 Bad Gateway: El servidor MCP no esta corriendo o no escucha en el puerto 3000. Verifica:

sudo systemctl status mcp-notes-server
curl -s http://127.0.0.1:3000/health

El cliente recibe 401 Unauthorized: El token en la configuracion del cliente no coincide con el token del servidor. Comprueba:

sudo cat /etc/mcp-notes-server/env

Las respuestas en streaming expiran: Aumenta proxy_read_timeout en la configuracion de Nginx. El valor de 300s cubre la mayoria de casos, pero herramientas de larga ejecucion pueden necesitar mas.

Errores de certificado: Verifica que el certificado es valido y cubre tu dominio:

sudo certbot certificates

Si el certificado expiro, renovalo:

sudo certbot renew

Errores de base de datos bloqueada: El modo WAL de SQLite maneja bien las lecturas concurrentes, pero las escrituras concurrentes pueden generar conflictos. Para servidores con mucho trafico, considera cambiar a PostgreSQL.

Permiso denegado en notes.db: El usuario mcpserver necesita acceso de escritura al directorio de trabajo para SQLite. Comprueba:

ls -la /opt/mcp-notes-server/notes.db

El archivo debe pertenecer a mcpserver:mcpserver. Si fue creado por root durante las pruebas, corrigelo:

sudo chown mcpserver:mcpserver /opt/mcp-notes-server/notes.db
sudo chmod 640 /opt/mcp-notes-server/notes.db

El cliente muestra "connection refused" o "timeout": Comprueba que tu registro DNS A apunta a la IP del VPS y que los puertos 80 y 443 estan abiertos en el firewall:

sudo ufw status
dig +short mcp.example.com

La salida de dig deberia mostrar la IP de tu VPS. Si no lo hace, actualiza tus registros DNS y espera la propagacion.

Proximos pasos

Tu servidor MCP esta en produccion y protegido. Por donde seguir:

  • Agrega mas herramientas que se conecten a tus bases de datos, APIs o sistemas de archivos
  • Implementa OAuth 2.1 en lugar de bearer tokens para acceso multiusuario
  • Agrega limitacion de velocidad en Express o Nginx para proteger contra abusos
  • Configura monitorizacion con journalctl -u mcp-notes-server -f y alertas
  • Despliega multiples servidores MCP detras de la misma instancia Nginx en diferentes rutas

La documentacion oficial del SDK de MCP cubre patrones avanzados incluyendo sesiones con estado, reporte de progreso y esquemas de salida estructurados.


Copyright 2026 Virtua.Cloud. Todos los derechos reservados. Este contenido es una obra original del equipo de Virtua.Cloud. La reproduccion, republicacion o redistribucion sin permiso escrito esta prohibida.

¿Listo para probarlo?

Despliega tu propio servidor en segundos. Linux, Windows o FreeBSD.

Ver planes VPS