# 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 | Á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 " } } } } ``` 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.