# 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, riesgo, antigüedad, cuellos de botella, releases, equipo, backlog, calidad, campos, actividad por día, recorrido de un ticket, propuestas y, con `since`, los cambios | | | `get_project_activity` | Actividad reciente, o la de un día (`day`) | | 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, decisiones y 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 | | | `mark_decision` | Marcar un comentario como decisión del ticket, o quitarla | | | `search_decisions` | Qué se decidió: decisiones por su texto, de un proyecto o de todos los míos | | | `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.