feat(mcp): ready for a remote server: quiet logs and allowed domains

- Logs default to warnings and errors (MCP_LOG_LEVEL): the library logged
  every tool on every request (~48 lines per call).
- DNS rebinding protection on /mcp, as the MCP spec asks of HTTP servers:
  the Host header must be in MCP_ALLOWED_HOSTS (default: localhost; compose
  default mcp.taskstream.pireon.dev,localhost) and a browser Origin, when
  present, too; otherwise 403 with a JSON-RPC error. Uses the official
  validators from @modelcontextprotocol/server.
- Test for allowed/foreign Host and Origin; .env.example, compose and
  docs/09-mcp.md document the variables; Word/PDF regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 19:19:51 +02:00
parent 9236cb73c6
commit 80d9bee6e0
8 changed files with 131 additions and 10 deletions

View File

@@ -24,6 +24,9 @@ flowchart LR
responde con un error que el modelo puede leer y corregir («Cannot move WEB-16 from Backlog to Hecho. Allowed: Por
hacer, En curso, Cancelado»), en vez de cortar la llamada.
- **Validación**: los parámetros de cada herramienta se validan con zod antes de llamar a la API.
- **Solo en su dominio**: `/mcp` rechaza (403) las peticiones cuyo `Host` no esté en `MCP_ALLOWED_HOSTS`, y las de un
navegador cuyo `Origin` tampoco lo esté. Es la protección contra _DNS rebinding_ que pide la especificación de MCP;
los clientes que llaman desde un servidor no envían `Origin` y solo necesitan el dominio correcto.
- **Respuestas compactas**: los listados devuelven lo que el modelo necesita (clave, título, estado, persona…); el
detalle completo, con `get_issue` o `get_project`.
@@ -106,11 +109,12 @@ npx nx test mcp # MCP real contra una API simulada: token r
npx nx run mcp:openapi-types # regenerar los tipos tras cambiar la API (apps/api/openapi.json)
```
| Variable | Por defecto | Qué es |
| -------------------- | ----------------------- | ------------------------------------------------------------------------------------------ |
| `MCP_PORT` | `3010` | Puerto del MCP (no `PORT`: Nx carga el `.env` del workspace, donde `PORT` es el de la API) |
| `TASKSTREAM_API_URL` | `http://localhost:3000` | Origen de la API (sus rutas ya llevan `/api`) |
| `MCP_LOG_LEVEL` | `log` | `error`, `warn`, `log`, `debug` o `verbose` |
| Variable | Por defecto | Qué es |
| -------------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `MCP_PORT` | `3010` | Puerto del MCP (no `PORT`: Nx carga el `.env` del workspace, donde `PORT` es el de la API) |
| `TASKSTREAM_API_URL` | `http://localhost:3000` | Origen de la API (sus rutas ya llevan `/api`) |
| `MCP_ALLOWED_HOSTS` | (solo `localhost`) | Dominios en los que responde, separados por comas |
| `MCP_LOG_LEVEL` | `warn` | `error`, `warn`, `log`, `debug` o `verbose`. Por encima de `warn`, la librería registra cada herramienta en cada llamada |
Añadir una herramienta: un método con `@Tool({ name, description, paramsSchema: z.object(…), annotations })` en el
resolver de su ámbito (`apps/mcp/src/app/tools`), que llame a la API con `this.api.as(ctx)` (cliente tipado con el
@@ -128,5 +132,7 @@ docker compose --profile mcp up -d --build # local: http://localhost:3010/mcp
```
En Dokploy: `COMPOSE_PROFILES=mcp` en las variables del proyecto y un dominio (p. ej. `mcp.taskstream.pireon.dev`) →
servicio `mcp`, puerto 3010. Habla con la API por la red interna (`http://task-stream-api:3000`). La imagen usa Node 24,
servicio `mcp`, puerto 3010. Si el dominio es otro, ponlo en `MCP_ALLOWED_HOSTS` (por defecto
`mcp.taskstream.pireon.dev,localhost`); si no, el MCP responderá 403. Habla con la API por la red interna
(`http://task-stream-api:3000`). La imagen usa Node 24,
que es lo que pide `@nestjs-mcp/server` 2.

Binary file not shown.

Binary file not shown.