Files
task-stream/docs/09-mcp.md
Josep Piguillem 0946e9517d feat: a home page with life
Greeting and summary of the day, the coming week, the latest across my
projects, who is online now, the week in numbers (mine and the team's,
with a streak), sprints under way, «Retomar», project cards with their
activity, what to pick up when nothing is mine and first steps for
newcomers. REST (GET /api/me/home), GraphQL (myHome) and MCP
(get_my_week).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:20:41 +02:00

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: /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 en curso, riesgo, antigüedad, cuellos de botella, previsión de releases, carga del equipo, salud del backlog, calidad, abiertos por campo de lista 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.