# 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
(Claude…)"] -- "POST /mcp
Authorization: Bearer token" --> M["apps/mcp
(NestJS)"]
M -- "REST /api/…
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 `, 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
60 herramientas, agrupadas por ámbito:
| Ámbito | Herramienta | Qué hace |
| ------------ | ------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Usuario | `whoami` | El usuario del token |
| | `search_users` | Buscar personas de Pireon (para asignar) |
| | `my_notifications` | Mis notificaciones (por defecto, las no leídas) |
| | `mark_notifications_read` | Marcar como leídas unas o todas |
| Proyectos | `list_my_projects` | Mis proyectos |
| | `list_public_projects` | Proyectos públicos |
| | `create_project` | Crear un proyecto (quien lo crea es su dueño), también copiando la configuración de otro (`template`) |
| | `get_project` | Equipo y configuración: ids de estados, tipos y áreas que aceptan las demás herramientas |
| | `get_project_showcase` | El escaparate público: hoja de ruta, releases, sprint en curso y tickets para empezar |
| | `get_board` | Tablero por columnas (filtros por persona, release, área, sprint) |
| | `get_project_stats` | Cifras de seguimiento |
| | `get_project_analytics` | Estadísticas de los últimos días (creados/resueltos, flujo, tiempos, throughput, QA) |
| | `get_portfolio` | Todos mis proyectos activos de un vistazo: abiertos y en riesgo, sprint, previsión de releases y épicas |
| | `get_my_week` | Mi semana: lo mío que vence en 7 días, fin de sprints y releases; lo terminado por mí y por mis equipos, racha; sprints en curso y qué podría coger |
| | `get_project_insights` | El panel: sprint en curso, riesgo, antigüedad, cuellos de botella, previsión de releases, carga del equipo, salud del backlog, calidad, abiertos por campo de lista y, con `since`, los cambios |
| | `get_project_activity` | Actividad reciente |
| Tickets | `my_issues` | Mi trabajo en todos los proyectos (o de cualquiera, por categoría; o los que sigo); también por prioridad, solo bugs o vencidos |
| | `search_issues` | Buscar por texto y filtros (épica, sprint, fechas, campos…); el backlog: `noSprint` y orden `RANK` |
| | `get_issue` | Detalle: épica, lista de comprobación, relaciones, commits, seguidores, transiciones, comentarios |
| | `create_issue` | Crear un ticket (también colgado de una épica) |
| | `update_issue` | Cambiar título, descripción, tipo, área, prioridad, etiquetas, fechas, campos personalizados… |
| | `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` / `edit_comment` / `delete_comment` | Comentar (o responder a un comentario), corregir o borrar |
| | `link_issues` / `unlink_issues` | Relacionar tickets (`DEPENDS_ON`, `BLOCKS`, `TESTS`…) o quitar la relación |
| | `watch_issue` | Seguir un ticket o dejar de seguirlo |
| | `vote_issue` | Votar un ticket o quitar el voto |
| | `duplicate_issue` | Crear otro ticket con lo mismo (sin estado, persona ni comentarios) |
| | `react_to_comment` | Reaccionar a un comentario (👍 🎉…) o quitar la reacción |
| | `add_checklist_item` / `update_checklist_item` / `remove_checklist_item` | La lista de comprobación: añadir, marcar o reescribir, quitar |
| Organización | `list_epics` | Épicas con el progreso de lo que cuelga de cada una |
| | `set_issue_parent` | Colgar un ticket de una épica u otro ticket, o soltarlo |
| | `rank_issue` | Mover un ticket en el backlog |
| | `bulk_update_issues` | El mismo cambio en hasta 100 tickets (por clave); dice cuáles no se dejaron |
| | `delete_issue` / `restore_issue` / `list_trash` | Papelera: borrar (30 días para recuperar), recuperar y ver |
| Sprints | `list_sprints` | Sprints de un proyecto |
| | `get_sprint` | Progreso, tickets y burndown |
| | `create_sprint` / `update_sprint` / `delete_sprint` | Planificar, cambiar o borrar (si no empezó) un sprint |
| | `start_sprint` / `close_sprint` | Empezarlo; cerrarlo pasando lo pendiente a otro sprint o al backlog |
| | `set_issue_sprint` | Meter un ticket en un sprint o devolverlo al backlog |
| Releases | `list_releases` | Releases de un proyecto |
| | `upcoming_releases` | Próximas releases de mis proyectos |
| | `get_release` | Progreso, notas, tickets y burnup |
| | `create_release` / `update_release` | Planificar una release o cambiar su nombre, descripción o fecha |
| | `draft_release_notes` / `set_release_notes` | Borrador de las notas desde los tickets terminados, y guardarlas |
| | `publish_release` | Publicar (con `force` si quedan tickets sin terminar) |
| | `archive_release` | Archivar una release que no saldrá; lo pendiente pasa a `moveTo` (otra release abierta) o la deja |
Las de lectura van marcadas como `readOnlyHint`, para que el cliente no pida confirmación al usarlas, y las que borran o
archivan, como `destructiveHint`, para que la pida. Lo que es de administración (miembros, configuración, visibilidad,
borrar proyectos, webhooks, integración con git, importar y exportar CSV) 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 " }
}
}
}
```
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.