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>
20 KiB
9. Servidor MCP
apps/mcp es un servidor MCP (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 2.x, y consume la API REST como cualquier
otra app: con tipos generados desde apps/api/openapi.json.
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:
/mcprechaza (403) las peticiones cuyoHostno esté enMCP_ALLOWED_HOSTS, y las de un navegador cuyoOrigintampoco 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íanOriginy 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_issueoget_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):
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:
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):
{
"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
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:
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.