Three more blocks on the project dashboard, also by REST, GraphQL and MCP: - Activity: what moved each day of the last 12 weeks, in a heatmap, with the busiest weekday and the longest lull. - A ticket's way: for the tickets done in the last 90 days, where the time from created to done went, status by status (waiting included), and how much of it was work (flow efficiency). - The public's proposals: received, waiting, under way, done and discarded, and the most voted still unanswered. 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 | |
| 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, 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 | |
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.