- 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>
139 lines
9.5 KiB
Markdown
139 lines
9.5 KiB
Markdown
# 9. Servidor MCP
|
|
|
|
`apps/mcp` es un servidor [MCP](https://modelcontextprotocol.io) (Model Context Protocol) para que asistentes de IA
|
|
(Claude Code, Claude Desktop, Cursor…) trabajen con TaskStream: consultar tableros y tickets, crear y mover tickets,
|
|
comentar, gestionar releases o leer las estadísticas. Está hecho con NestJS y
|
|
[`@nestjs-mcp/server`](https://www.npmjs.com/package/@nestjs-mcp/server) 2.x, y consume la **API REST** como cualquier
|
|
otra app: con tipos generados desde `apps/api/openapi.json`.
|
|
|
|
```mermaid
|
|
flowchart LR
|
|
C["Cliente MCP<br/>(Claude…)"] -- "POST /mcp<br/>Authorization: Bearer token" --> M["apps/mcp<br/>(NestJS)"]
|
|
M -- "REST /api/…<br/>mismo token" --> A["apps/api"]
|
|
A -- "valida el token" --> P["Pireon"]
|
|
```
|
|
|
|
## 9.1 Cómo funciona
|
|
|
|
- **Un endpoint**: `POST /mcp`, transporte _Streamable HTTP_. Sin estado (la versión 2026-07-28 del protocolo retiró
|
|
las sesiones): cualquier instancia atiende cualquier llamada.
|
|
- **Sin credenciales propias**: cada llamada lleva su `Authorization: Bearer <token de Pireon>`, y el MCP la reenvía
|
|
tal cual a la API. Cada persona ve y puede cambiar exactamente lo que la API le permite (su rol en cada proyecto);
|
|
**sin token**, solo se leen los proyectos públicos.
|
|
- **Errores legibles**: si la API rechaza algo (permiso, regla de negocio, transición no permitida…), la herramienta
|
|
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`.
|
|
|
|
## 9.2 Herramientas
|
|
|
|
| Ámbito | Herramienta | Qué hace |
|
|
| --------- | ----------------------- | ------------------------------------------------------------------------------------------- |
|
|
| Usuario | `whoami` | El usuario del token |
|
|
| | `search_users` | Buscar personas de Pireon (para asignar) |
|
|
| Proyectos | `list_my_projects` | Mis proyectos |
|
|
| | `list_public_projects` | Proyectos públicos |
|
|
| | `get_project` | Equipo y configuración: ids de estados, tipos y áreas que aceptan las demás herramientas |
|
|
| | `get_board` | Tablero por columnas (filtros por persona, release, área) |
|
|
| | `get_project_stats` | Cifras de seguimiento |
|
|
| | `get_project_analytics` | Estadísticas de los últimos días (creados/resueltos, flujo, tiempos, throughput, QA) |
|
|
| | `get_project_activity` | Actividad reciente |
|
|
| Tickets | `my_issues` | Mi trabajo en todos los proyectos (o de cualquiera, por categoría: p. ej. lo que espera QA) |
|
|
| | `search_issues` | Buscar por texto y filtros |
|
|
| | `get_issue` | Detalle, transiciones permitidas, enlaces, comentarios e historial |
|
|
| | `create_issue` | Crear un ticket |
|
|
| | `update_issue` | Cambiar título, descripción, tipo, área, prioridad, etiquetas… |
|
|
| | `change_issue_status` | Mover de estado (con motivo cuando el estado lo pide) |
|
|
| | `assign_issue` | Asignar o desasignar |
|
|
| | `set_issue_release` | Meter en una release o sacar |
|
|
| | `add_comment` | Comentar |
|
|
| | `link_issues` | Relacionar tickets (`DEPENDS_ON`, `BLOCKS`, `TESTS`…) |
|
|
| Releases | `list_releases` | Releases de un proyecto |
|
|
| | `upcoming_releases` | Próximas releases de mis proyectos |
|
|
| | `get_release` | Progreso, tickets y burnup |
|
|
| | `create_release` | Planificar una release |
|
|
| | `publish_release` | Publicar (con `force` si quedan tickets sin terminar) |
|
|
|
|
Las de lectura van marcadas como `readOnlyHint`, para que el cliente no pida confirmación al usarlas. Lo que es de
|
|
administración (miembros, configuración, visibilidad, borrar proyectos) no se expone: se hace desde la web.
|
|
|
|
## 9.3 Conectar un cliente
|
|
|
|
Hace falta un token de Pireon. Con email y contraseña (si la API tiene configurado el cliente `password`):
|
|
|
|
```sh
|
|
curl -s https://api.taskstream.pireon.dev/api/auth/token \
|
|
-H 'content-type: application/json' \
|
|
-d '{"grant_type":"password","username":"tu@email.com","password":"…"}'
|
|
# → { "access_token": "…", "refresh_token": "…", "expires_in": … }
|
|
```
|
|
|
|
Claude Code:
|
|
|
|
```sh
|
|
claude mcp add --transport http taskstream https://mcp.taskstream.pireon.dev/mcp \
|
|
--header "Authorization: Bearer $TASKSTREAM_TOKEN"
|
|
# en local: http://localhost:3010/mcp
|
|
```
|
|
|
|
Otros clientes (JSON de configuración):
|
|
|
|
```json
|
|
{
|
|
"mcpServers": {
|
|
"taskstream": {
|
|
"type": "http",
|
|
"url": "https://mcp.taskstream.pireon.dev/mcp",
|
|
"headers": { "Authorization": "Bearer <token de Pireon>" }
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
El token caduca como cualquier sesión de Pireon: cuando la herramienta responda «Error 401», hay que pedir otro (o
|
|
renovarlo con `grant_type: refresh_token`). El MCP todavía no implementa el flujo de autorización OAuth del propio
|
|
protocolo MCP, así que el cliente no lo obtiene solo.
|
|
|
|
## 9.4 Desarrollo
|
|
|
|
```sh
|
|
npx nx serve api # la API en :3000
|
|
npx nx serve mcp # el MCP en http://localhost:3010/mcp (TASKSTREAM_API_URL=http://localhost:3000)
|
|
npx @modelcontextprotocol/inspector # probar las herramientas en el navegador
|
|
npx nx test mcp # MCP real contra una API simulada: token reenviado, errores, validación
|
|
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_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
|
|
token de quien llama) y devuelva `toolResult(…)`.
|
|
|
|
La imagen solo instala lo que lista `apps/mcp/package.json`: si el MCP empieza a usar un paquete nuevo en tiempo de
|
|
ejecución, hay que añadirlo ahí (igual que en la API).
|
|
|
|
## 9.5 Despliegue
|
|
|
|
El servicio `mcp` de `docker-compose.yml` va en el perfil `mcp`, así que no arranca salvo que se pida:
|
|
|
|
```sh
|
|
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. 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.
|