poc5-mcp-http-remote

poc5-mcp-http-remote

This MCP server provides a Streamable HTTP endpoint with bearer token authentication, exposing echo and add tools, and an info resource for remote client integration.

Category
访问服务器

README

PoC 5 — MCP HTTP Server con Auth (deployado)

Migración del Echo server (PoC 1) de transporte stdio a Streamable HTTP, con autenticación por bearer token, empaquetado en Docker y listo para Cloud Run.

Estructura

.
├── server.py       # MCP server (FastMCP) sobre Streamable HTTP + /health
├── auth.py         # middleware de bearer token (Starlette)
├── Dockerfile
├── .dockerignore
├── .env.example
├── pyproject.toml
└── .github/workflows/deploy-cloud-run.yml   # CI/CD: build + deploy a Cloud Run por PR aceptada

Tools expuestas: echo(message), add(a, b). Resource: info://server. Endpoint MCP: /mcp. Endpoint público sin auth (health check): /health.

Fase 1 — Correr local

uv venv .venv
uv pip install --python .venv -e .
cp .env.example .env
# completar AUTH_TOKEN en .env, por ejemplo:
python -c "import secrets; print(secrets.token_urlsafe(32))"

.venv/Scripts/python.exe server.py   # Windows
# .venv/bin/python server.py         # macOS/Linux

Registrar en Claude CLI:

claude mcp add --transport http poc5 http://localhost:8000/mcp --header "Authorization: Bearer <AUTH_TOKEN>"
claude mcp list   # debe mostrar poc5 ... Connected

Sin el header correcto, /mcp devuelve 401. /health responde 200 sin auth (para health checks de infraestructura).

Fase 2 — Auth

Implementada en auth.py (BearerAuthMiddleware, Starlette BaseHTTPMiddleware):

  • Lee AUTH_TOKEN de entorno (nunca hardcodeado).
  • Compara con hmac.compare_digest (evita timing attacks).
  • Sin AUTH_TOKEN configurado → 500 (falla cerrado, no abre el server sin querer).
  • Header ausente/inválido → 401 + WWW-Authenticate: Bearer.
  • /health queda exento (whitelist explícita) para health checks del orquestador.

Gotcha: el SDK mcp tiene su propia protección anti DNS-rebinding

Además de nuestro auth.py, FastMCP trae activada por default una protección anti DNS-rebinding (mcp/server/transport_security.py) que valida el header Host de cada request contra una whitelist. Esa whitelist viene hardcodeada por el SDK solo a localhost / 127.0.0.1 / [::1]. Contra cualquier otro hostname (como el de Cloud Run), rechaza con 421 Invalid Host headerdespués de pasar nuestro propio bearer auth, así que solo se nota con un token válido (sin token, auth.py corta antes de llegar a esa capa, y parece que todo funciona).

Se soluciona pasando el hostname público real vía la variable MCP_ALLOWED_HOSTS (ver server.py). Importante: el formato de la URL de Cloud Run no es siempre el mismo — según el proyecto/región puede ser el formato con hash (servicio-xxxxx-uc.a.run.app) o el simplificado (servicio-<numero-proyecto>.region.run.app). No conviene armarlo a mano/adivinarlo — el workflow de CI le pregunta a la propia API de Cloud Run cuál es la URL real después del deploy y recién ahí setea MCP_ALLOWED_HOSTS con ese valor (ver sección CI/CD abajo).

Fase 3 — Deployment a Cloud Run

Build local (ya probado)

docker build -t poc5-mcp-http-remote:latest .
docker run -d -p 8080:8080 -e PORT=8080 -e AUTH_TOKEN=<token> poc5-mcp-http-remote:latest
curl http://localhost:8080/health   # 200

Deploy — pasos a seguir (requieren gcloud CLI + proyecto GCP)

  1. Instalar y autenticar gcloud (no está instalado en este entorno):

    gcloud auth login
    gcloud config set project <TU_PROJECT_ID>
    
  2. Guardar el token en Secret Manager (no como env var plana, no en la imagen):

    gcloud services enable secretmanager.googleapis.com run.googleapis.com artifactregistry.googleapis.com
    
    python -c "import secrets; print(secrets.token_urlsafe(32))" > token.txt
    gcloud secrets create poc5-auth-token --data-file=token.txt
    rm token.txt   # no dejar el token en disco
    
  3. Build y push de la imagen (Artifact Registry vía Cloud Build, sin necesitar Docker local):

    gcloud artifacts repositories create poc-repo --repository-format=docker --location=us-central1
    
    gcloud builds submit --tag us-central1-docker.pkg.dev/<TU_PROJECT_ID>/poc-repo/poc5-mcp-http-remote:latest .
    
  4. Deploy a Cloud Run, inyectando el secret como variable de entorno:

    gcloud run deploy poc5-mcp-http-remote \
      --image us-central1-docker.pkg.dev/<TU_PROJECT_ID>/poc-repo/poc5-mcp-http-remote:latest \
      --region us-central1 \
      --allow-unauthenticated \
      --set-secrets AUTH_TOKEN=poc5-auth-token:latest
    
    • --allow-unauthenticated: el endpoint queda público a nivel red (Cloud Run IAM), pero sigue exigiendo el bearer token propio vía auth.py. Es el modelo esperado para un MCP server remoto consumido por distintos clientes.
    • Cloud Run inyecta $PORT automáticamente; server.py ya lo respeta.
    • Cloud Run da HTTPS por defecto — no hay que gestionar TLS.

    Después del primer deploy, preguntale a Cloud Run cuál es la URL real y seteala como MCP_ALLOWED_HOSTS (no la armes a mano — el formato de URL varía según el proyecto/región, ver el gotcha de la sección anterior):

    URL=$(gcloud run services describe poc5-mcp-http-remote --region us-central1 --format='value(status.url)')
    HOST="${URL#https://}"
    
    gcloud run services update poc5-mcp-http-remote \
      --region us-central1 \
      --update-env-vars MCP_ALLOWED_HOSTS="$HOST"
    
  5. Registrar la URL pública en el CLI:

    gcloud run services describe poc5-mcp-http-remote --region us-central1 --format='value(status.url)'
    
    claude mcp add --transport http poc5 https://<URL_DE_CLOUD_RUN>/mcp --header "Authorization: Bearer <AUTH_TOKEN>"
    claude mcp list   # Connected
    
  6. Verificar rechazo sin token contra la URL pública:

    curl -i https://<URL_DE_CLOUD_RUN>/mcp   # 401
    curl -i https://<URL_DE_CLOUD_RUN>/health  # 200
    

CI/CD — GitHub Actions (deploy automático por cada PR aceptada)

Workflow: .github/workflows/deploy-cloud-run.yml.

Se dispara cuando una Pull Request contra main es mergeada (pull_request.types: closed + guard github.event.pull_request.merged == true — un cierre sin mergear no dispara nada). Hace: build de la imagen, push a Artifact Registry, gcloud run deploy, le pregunta a la API cuál es la URL real del servicio y la setea como MCP_ALLOWED_HOSTS (ver el gotcha de Fase 2 — el formato de URL de Cloud Run varía según el proyecto, por eso se pide en runtime en vez de armarlo a mano), y corre tres smoke tests contra la URL pública (/health → 200, /mcp sin token → 401, /mcp con token real → 200 con handshake MCP completo).

Los pasos 2 y 3 de la sección anterior (Secret Manager + Artifact Registry repo) son setup único, hacelos una sola vez a mano antes del primer merge. El pipeline asume que ya existen.

1. Crear la Service Account que va a deployar desde CI

PROJECT_ID=<TU_PROJECT_ID>

gcloud iam service-accounts create poc5-deployer \
  --display-name="PoC5 GitHub Actions Deployer" \
  --project="$PROJECT_ID"

DEPLOYER_SA="poc5-deployer@${PROJECT_ID}.iam.gserviceaccount.com"

# Roles mínimos: deployar en Cloud Run, pushear a Artifact Registry,
# actuar como la service account de runtime del servicio, y leer el secret
# al bindearlo en el deploy.
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:${DEPLOYER_SA}" --role="roles/run.admin"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:${DEPLOYER_SA}" --role="roles/artifactregistry.writer"
gcloud projects add-iam-policy-binding "$PROJECT_ID" \
  --member="serviceAccount:${DEPLOYER_SA}" --role="roles/iam.serviceAccountUser"
gcloud secrets add-iam-policy-binding poc5-auth-token \
  --member="serviceAccount:${DEPLOYER_SA}" --role="roles/secretmanager.secretAccessor" \
  --project="$PROJECT_ID"

# poc5-runtime es la SA con la que corre el propio servicio Cloud Run (no la de deploy).
# Solo necesita leer el secret en runtime — nada más.
gcloud iam service-accounts create poc5-runtime \
  --display-name="PoC5 Cloud Run Runtime SA" \
  --project="$PROJECT_ID"

RUNTIME_SA="poc5-runtime@${PROJECT_ID}.iam.gserviceaccount.com"

gcloud secrets add-iam-policy-binding poc5-auth-token \
  --member="serviceAccount:${RUNTIME_SA}" \
  --role="roles/secretmanager.secretAccessor" \
  --project="$PROJECT_ID"

2. Generar la key JSON y copiarla a GitHub (nunca commitear el archivo)

gcloud iam service-accounts keys create poc5-deployer-key.json \
  --iam-account="${DEPLOYER_SA}"

Copiá el contenido completo del .json en el secreto GCP_SA_KEY de GitHub (paso siguiente) y después borrá el archivo local:

rm poc5-deployer-key.json

3. Configurar los secretos del repositorio en GitHub

Settings → Secrets and variables → Actions → New repository secret. Todos los parámetros del pipeline (credenciales y config) van como secreto, ninguno queda hardcodeado en el workflow:

Secreto Valor
GCP_SA_KEY Contenido completo del JSON generado en el paso 2
GCP_PROJECT_ID Tu project id de GCP
GCP_REGION Región de Cloud Run / Artifact Registry, ej. us-central1
GCP_SERVICE_NAME Nombre del servicio Cloud Run, ej. poc5-mcp-http-remote
GCP_ARTIFACT_REPO Nombre del repo de Artifact Registry, ej. poc-repo
GCP_RUNTIME_SA Email de la SA de runtime: poc5-runtime@<PROJECT_ID>.iam.gserviceaccount.com
GCP_AUTH_TOKEN_SECRET_NAME Nombre del secret en Secret Manager, ej. poc5-auth-token

4. Probar el pipeline

Abrí una PR contra main, mergeala, y mirá la tab Actions del repo. El job build-and-deploy corre los smoke tests al final; si /health o el 401 de /mcp fallan, el pipeline falla (no queda un deploy roto en verde).

Seguridad — checklist

  • [x] /mcp exige auth; sin token válido devuelve 401.
  • [x] AUTH_TOKEN nunca en el repo ni en la imagen (.env gitignored; en Cloud Run vive en Secret Manager).
  • [x] Comparación de token con hmac.compare_digest (timing-safe).
  • [x] stateless_http=True: sin estado en memoria del proceso, apto para múltiples réplicas de Cloud Run.
  • [x] HTTPS por Cloud Run (no hay transporte MCP en HTTP plano).
  • [ ] Rate limiting — no implementado en esta PoC. Próximo paso: Cloud Armor o un middleware tipo token-bucket por IP/token si el server pasa a producción real.
  • [ ] OAuth 2.1 — el bearer token estático cubre el mínimo del estándar MCP; OAuth 2.1 completo (authorization code + PKCE, refresh tokens) queda documentado como evolución natural, no bloquea esta PoC.
  • [ ] CI/CD usa una Service Account JSON Key (GCP_SA_KEY) en vez de Workload Identity Federation, por simplicidad de setup. Es una credencial de larga duración: rotarla periódicamente (gcloud iam service-accounts keys create + borrar la vieja con gcloud iam service-accounts keys delete) y migrar a WIF si esto pasa a ser algo más que una PoC.

Diferencias vs. stdio (PoC 1-4)

stdio Streamable HTTP (esta PoC)
Transporte proceso local, stdin/stdout endpoint de red, JSON-RPC sobre HTTP/SSE
Clientes 1:1 multi-cliente concurrente
Auth ninguna (confía en el proceso padre) bearer token obligatorio
Estado vive en el proceso stateless_http=True, sin estado entre requests
Deploy "corre en mi máquina" contenedor en Cloud Run, HTTPS, escalable
<img width="884" height="216" alt="1" src="https://github.com/user-attachments/assets/2a092c1d-a357-48c5-aea8-d26d36299a14" />
<img width="873" height="289" alt="2" src="https://github.com/user-attachments/assets/a3e6202c-c672-4766-97c5-f1cc5a871903" />
<img width="881" height="181" alt="3" src="https://github.com/user-attachments/assets/7d4fbc43-eac9-4e5e-8355-22e58571bf52" />

推荐服务器

Baidu Map

Baidu Map

百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。

官方
精选
JavaScript
Playwright MCP Server

Playwright MCP Server

一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。

官方
精选
TypeScript
Magic Component Platform (MCP)

Magic Component Platform (MCP)

一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。

官方
精选
本地
TypeScript
Audiense Insights MCP Server

Audiense Insights MCP Server

通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。

官方
精选
本地
TypeScript
VeyraX

VeyraX

一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。

官方
精选
本地
graphlit-mcp-server

graphlit-mcp-server

模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。

官方
精选
TypeScript
Kagi MCP Server

Kagi MCP Server

一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。

官方
精选
Python
e2b-mcp-server

e2b-mcp-server

使用 MCP 通过 e2b 运行代码。

官方
精选
Neon MCP Server

Neon MCP Server

用于与 Neon 管理 API 和数据库交互的 MCP 服务器

官方
精选
Exa MCP Server

Exa MCP Server

模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。

官方
精选