Análisis Exhaustivo: Despliegue de Servidores MCP en Google Cloud Run



This content originally appeared on DEV Community and was authored by Angel Jose VARGAS GUTIERREZ

description: Problemas técnicos reales al desplegar servidores MCP en Cloud Run

tags: mcp, cloudrun, serverless, review

Análisis Crítico: Servidores MCP en Google Cloud Run

🎯 Contexto

Revisé un tutorial sobre desplegar servidores MCP (Model Context Protocol) en Google Cloud Run. Encontré varios problemas técnicos críticos que impiden su funcionamiento real.

🚨 Problema #1: Transporte Incompatible

Error Fatal: El tutorial usa StdioServerTransport en Cloud Run.

Por qué falla:

  • Cloud Run requiere HTTP/HTTPS
  • StdioServerTransport usa stdin/stdout
  • Resultado: servidor no funcional

✅ Solución:

import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express";

const app = express();
const PORT = process.env.PORT || 8080;

const server = new Server({
  name: "weather-mcp-server",
  version: "1.0.0"
});

app.get("/sse", async (req, res) => {
  const transport = new SSEServerTransport("/messages", res);
  await server.connect(transport);
});

app.post("/messages", express.json(), async (req, res) => {
  // Manejar mensajes MCP
});

app.listen(PORT);

🚨 Problema #2: package.json Incompleto

El tutorial menciona npm run build pero nunca lo define.

✅ Solución:

{
  "name": "mcp-server",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "start": "node dist/server.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.5.0",
    "express": "^4.18.2"
  },
  "devDependencies": {
    "typescript": "^5.3.3",
    "@types/node": "^20.10.0"
  }
}

🚨 Problema #3: Configuración Cliente Incorrecta

Error: Usa curl como comando MCP

{
  "mcpServers": {
    "weather": {
      "command": "curl",
      "args": [...]
    }
  }
}

Claude Desktop necesita un proceso stdio, no HTTP directo.

✅ Solución: Proxy Local

// mcp-proxy.ts
import fetch from "node-fetch";
import { stdin, stdout } from "process";

const SERVER = process.env.MCP_SERVER_URL;

stdin.on("data", async (chunk) => {
  const req = JSON.parse(chunk.toString());

  const res = await fetch(`${SERVER}/messages`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(req)
  });

  const data = await res.json();
  stdout.write(JSON.stringify(data) + "\n");
});

Configuración:

{
  "mcpServers": {
    "weather": {
      "command": "node",
      "args": ["/path/to/mcp-proxy.js"],
      "env": {
        "MCP_SERVER_URL": "https://your-server.run.app"
      }
    }
  }
}

🚨 Problema #4: Sin Seguridad

Tutorial menciona autenticación pero no la implementa.

✅ API Keys:

function authenticateAPIKey(req, res, next) {
  const apiKey = req.headers["x-api-key"];
  const validKeys = process.env.API_KEYS?.split(",") || [];

  if (!validKeys.includes(apiKey)) {
    return res.status(403).json({ error: "Invalid key" });
  }
  next();
}

app.use("/sse", authenticateAPIKey);
app.use("/messages", authenticateAPIKey);

🚨 Problema #5: Dockerfile Básico

✅ Versión Optimizada:

FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src/ ./src/
RUN npm run build

FROM node:18-alpine
WORKDIR /app
RUN addgroup -g 1001 nodejs && adduser -S nodejs -u 1001
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package*.json ./
RUN npm ci --only=production
USER nodejs
EXPOSE 8080
CMD ["node", "dist/server.js"]

💰 Costos Reales

Cloud Run Pricing:

  • 100K requests/mes
  • 200ms promedio
  • 512MB memoria

Costo: ~$0.55/mes

Free tier cubre:

  • 2M requests/mes
  • 360K vCPU-segundos
  • 180K GiB-segundos

Para desarrollo: $0 🎉

🧪 Tests Esenciales

import { describe, it, expect } from "vitest";

describe("MCP Server", () => {
  it("lista herramientas", async () => {
    const res = await server.handleRequest({
      method: "tools/list"
    });
    expect(res.tools).toHaveLength(1);
  });

  it("maneja errores", async () => {
    const res = await server.handleRequest({
      method: "tools/call",
      params: { arguments: { city: "" } }
    });
    expect(res.isError).toBe(true);
  });
});

🎯 Caso de Uso: Firestore

import { Firestore } from "@google-cloud/firestore";
const db = new Firestore();

server.setRequestHandler(CallToolRequest, async (req) => {
  if (req.params.name === "query_db") {
    const { collection } = req.params.arguments;
    const snapshot = await db.collection(collection).limit(10).get();

    return {
      content: [{
        type: "text",
        text: JSON.stringify(snapshot.docs.map(d => d.data()))
      }]
    };
  }
});

📊 Valoración

Problemas:

  • ❌ Transporte incompatible
  • ❌ Config cliente incorrecta
  • ❌ Sin seguridad implementada
  • ❌ Scripts faltantes

Fortalezas:

  • ✅ Buena estructura
  • ✅ Ejemplo claro
  • ✅ Menciona best practices

Puntuación: 6.5/10

Con correcciones: 9/10

⏱ Tiempo Real

  • Básico funcional: 30-45 min
  • Production-ready: 2-4 horas
  • Completo con CI/CD: 1-2 días

La promesa de “10 minutos” es muy optimista.

🔗 Recursos

💭 Conclusión

El tutorial tiene buenas intenciones pero fallas técnicas críticas. Con las correcciones aquí propuestas, puede convertirse en un recurso valioso para la comunidad MCP.


This content originally appeared on DEV Community and was authored by Angel Jose VARGAS GUTIERREZ