Whoever can change the ticket flags a comment as a decision (new event
CommentDecisionMarkedEvent on the comment). The ticket page lists its
decisions above the description, with a link to the comment in the
conversation (`?comment=<id>` scrolls to it and points at it), and the
search palette finds them by their text across my projects.
API: markDecision / PUT-DELETE /api/comments/{id}/decision; Issue.decisions
and GET /api/issues/{issue}/decisions; decisions(projectId?, search) and
GET /api/projects/{project}/decisions, GET /api/me/decisions. MCP:
mark_decision, search_decisions, and get_issue includes them.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
20 KiB
Markdown
168 lines
20 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
|
|
|
|
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 <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.
|