feat(mcp): MCP server for AI clients, consuming the REST API

New app apps/mcp (NestJS + @nestjs-mcp/server 2): Streamable HTTP at
POST /mcp, stateless.

- 24 tools: projects (mine, public, details with settings, board, stats,
  analytics, activity), tickets (my work, search, details with comments and
  history, create, update, move, assign, release, comment, link), releases
  (list, upcoming, details with progress and burnup, create, publish) and
  users (whoami, search). Admin operations stay in the web.
- No credentials of its own: each call's Authorization header (a Pireon
  token) is forwarded to the API; without it only public projects are read.
- Typed REST client generated from apps/api/openapi.json (openapi-fetch +
  openapi-typescript via `nx run mcp:openapi-types`); API refusals become
  readable tool errors; parameters validated with zod.
- Tests: the real MCP against a simulated API (token forwarding, errors,
  validation, writes).
- Docker target `mcp` (Node 24) and an optional compose service under the
  `mcp` profile; zod declared as a root dependency.
- Docs: chapter 9 «Servidor MCP», references in 01/06/07 and the index;
  Word/PDF regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 18:59:38 +02:00
parent 71dc2d6f2d
commit 9236cb73c6
36 changed files with 6054 additions and 38 deletions

View File

@@ -44,12 +44,13 @@ proyecto (`OWNER`, `ADMIN`, `MEMBER`, `VIEWER`).
apps/
api/ composición de la API (NestJS): config, GraphQL, conexiones, e2e
web/ composición del cliente (Angular): rutas, providers, estilos, nginx
mcp/ servidor MCP para asistentes de IA (NestJS), consume la API REST
libs/
api/{shared,identity,tracker}/{domain,application,infrastructure,presenters}
web/{shared,identity,tracker}/{domain,application,infrastructure,presenters} (+ web/shared/ui)
docs/ esta documentación
codegen.ts tipos GraphQL del cliente generados desde apps/api/schema.gql
Dockerfile dos destinos: api y web
Dockerfile tres destinos: api, web y mcp
docker-compose.yml
```

View File

@@ -25,16 +25,18 @@ la web y la API comparten origen igual que en Docker.
## 6.3 Comandos
| Comando | Qué hace |
| -------------------------------- | --------------------------------------------------------------------------------- |
| `npx nx run-many -t lint` | Lint de todo, incluidas las reglas de capas y contextos |
| `npx nx run-many -t test` | Tests unitarios (dominio y aplicación, API y web) |
| `npx nx e2e api` | Flujo completo de la API contra Docker, con Pireon simulado |
| `npx nx run-many -t build` | Builds de la API y la web |
| `npx nx run web:codegen` | Regenera los tipos GraphQL del cliente desde `apps/api/schema.gql` |
| `npx nx run api:prune` | Build de la API con `package.json` y lockfile podados (lo usa Docker) |
| `npx nx graph` | Grafo de dependencias entre proyectos |
| `npx nx test api-tracker-domain` | Tests de una librería concreta (`api-<contexto>-<capa>`, `web-<contexto>-<capa>`) |
| Comando | Qué hace |
| -------------------------------- | ----------------------------------------------------------------------------------- |
| `npx nx run-many -t lint` | Lint de todo, incluidas las reglas de capas y contextos |
| `npx nx run-many -t test` | Tests unitarios (dominio y aplicación, API y web) |
| `npx nx e2e api` | Flujo completo de la API contra Docker, con Pireon simulado |
| `npx nx run-many -t build` | Builds de la API y la web |
| `npx nx run web:codegen` | Regenera los tipos GraphQL del cliente desde `apps/api/schema.gql` |
| `npx nx serve mcp` | Servidor MCP en `http://localhost:3010/mcp`, contra la API local ([MCP](09-mcp.md)) |
| `npx nx run mcp:openapi-types` | Regenera los tipos REST del MCP desde `apps/api/openapi.json` |
| `npx nx run api:prune` | Build de la API con `package.json` y lockfile podados (lo usa Docker) |
| `npx nx graph` | Grafo de dependencias entre proyectos |
| `npx nx test api-tracker-domain` | Tests de una librería concreta (`api-<contexto>-<capa>`, `web-<contexto>-<capa>`) |
### Tests

View File

@@ -40,7 +40,7 @@ Todos tienen healthcheck; `web` espera a que `api` esté sana, y `api` a las tre
### Imágenes
El `Dockerfile` tiene dos destinos que comparten la instalación de dependencias:
El `Dockerfile` tiene tres destinos que comparten la instalación de dependencias:
- **Instalación**: `npm ci` del workspace. `@pi/ui` viene del Nexus privado; el token llega como **build secret**
desde la variable `NEXUS_TOKEN` (`secrets.nexus_token` en `docker-compose.yml`), se escribe en un `npmrc` temporal y
@@ -48,6 +48,8 @@ El `Dockerfile` tiene dos destinos que comparten la instalación de dependencias
- **`api`**: `nx run api:prune` genera el bundle con un `package.json` y un lockfile podados a partir de
`apps/api/package.json`. La imagen final solo instala esas dependencias (sin Angular ni paquetes privados).
Si la API empieza a usar un paquete nuevo **en tiempo de ejecución**, hay que añadirlo a `apps/api/package.json`.
- **`mcp`**: el [servidor MCP](09-mcp.md), podado igual que la API (`nx run mcp:prune`), sobre Node 24. Su servicio
en `docker-compose.yml` va en el perfil `mcp`: solo arranca con `--profile mcp` (en Dokploy, `COMPOSE_PROFILES=mcp`).
- **`web`**: build de producción de Angular servido por nginx (`apps/web/nginx.conf`): fallback de rutas a
`index.html`, caché larga para los bundles con hash y sin caché para `index.html`.

132
docs/09-mcp.md Normal file
View File

@@ -0,0 +1,132 @@
# 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.
- **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_LOG_LEVEL` | `log` | `error`, `warn`, `log`, `debug` o `verbose` |
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. 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.

View File

@@ -3,16 +3,17 @@
TaskStream es una alternativa a Jira para equipos de desarrollo: proyectos, tickets (bugs, features, cambios de UI,
tareas, QA), releases, comentarios y relaciones entre tickets, todo en tiempo real.
| Documento | Para qué |
| ----------------------------------------- | ------------------------------------------------------------------------ |
| [1. Visión general](01-vision-general.md) | Qué hace el producto, stack y estructura del monorepo |
| [2. Arquitectura](02-arquitectura.md) | Hexagonal, CQRS, event sourcing, flujo escritura → lectura y tiempo real |
| [3. API](03-api.md) | Dominio (agregados, workflow, eventos) y la API GraphQL |
| [4. Cliente web](04-cliente-web.md) | Angular, NgRx Signals con eventos, GraphQL y `@pi/ui` |
| [5. Autenticación](05-autenticacion.md) | Pireon: tokens, login en el navegador, sesiones y webhooks |
| [6. Desarrollo](06-desarrollo.md) | Puesta en marcha, comandos, tests y cómo añadir una funcionalidad |
| [7. Despliegue](07-despliegue.md) | Docker, variables de entorno y escalado |
| [8. Decisiones](08-decisiones.md) | Por qué está hecho así (y qué alternativas se descartaron) |
| Documento | Para qué |
| ----------------------------------------- | ------------------------------------------------------------------------- |
| [1. Visión general](01-vision-general.md) | Qué hace el producto, stack y estructura del monorepo |
| [2. Arquitectura](02-arquitectura.md) | Hexagonal, CQRS, event sourcing, flujo escritura → lectura y tiempo real |
| [3. API](03-api.md) | Dominio (agregados, workflow, eventos) y la API GraphQL |
| [4. Cliente web](04-cliente-web.md) | Angular, NgRx Signals con eventos, GraphQL y `@pi/ui` |
| [5. Autenticación](05-autenticacion.md) | Pireon: tokens, login en el navegador, sesiones y webhooks |
| [6. Desarrollo](06-desarrollo.md) | Puesta en marcha, comandos, tests y cómo añadir una funcionalidad |
| [7. Despliegue](07-despliegue.md) | Docker, variables de entorno y escalado |
| [8. Decisiones](08-decisiones.md) | Por qué está hecho así (y qué alternativas se descartaron) |
| [9. Servidor MCP](09-mcp.md) | Usar TaskStream desde asistentes de IA (Claude…): herramientas y conexión |
Cada librería de `libs/` tiene además su propio `README.md` con su responsabilidad y su contenido.

Binary file not shown.

Binary file not shown.