orquestra-mcp 1.23.1 → 1.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # orquestra-mcp
2
2
 
3
- Servidor MCP (Model Context Protocol) para Orquestra. Expone **99 tools**
3
+ Servidor MCP (Model Context Protocol) para Orquestra. Expone **100 tools**
4
4
  que llaman a la API de `orquestra-infra`: lectura de contexto/dashboard/
5
5
  workspace/infra/paridad/listados, escritura de tareas/ideas/changelog/
6
6
  notas/docs/módulos/flujo/categorías/infra/proyectos, y corridas de QA —
@@ -111,6 +111,9 @@ Lectura de contexto:
111
111
  | `list_tasks` | `projectId?`, `statusId?` | Lista de tareas, filtro opcional por estatus |
112
112
  | `search` | `projectId?`, `q` | Búsqueda por texto en tareas/notas/ideas/docs |
113
113
  | `get_dashboard_summary` | `projectId?` | Hoy/vencidas/bloqueadas, salud de módulos, actividad reciente |
114
+ | `get_project_findings` | `projectId?` | Qué le falta al proyecto, priorizado por severidad (determinístico, sin créditos de IA) |
115
+ | `ask_assistant` | `question`, `history?`, `projectId?` | Pregunta al Asistente sobre el proyecto; responde con propuestas (1 crédito de IA) |
116
+ | `assess_project` | `projectId?` | Resumen y prioridades del proyecto con IA; se reusa 1 h si no cambian los hallazgos |
114
117
  | `get_infra_status` | `projectId?` | Servicios, entornos y distribuciones con conteos operational/degraded/down |
115
118
  | `get_parity_summary` | `projectId?` | Desfases de paridad multiplataforma (tasks y modules por separado) |
116
119
  | `get_workspace_summary` | — | Progreso agregado de todos los proyectos del workspace |
@@ -192,7 +195,7 @@ create/update sigue siendo criterio del agente y del humano.
192
195
 
193
196
  ## Estado
194
197
 
195
- 99 tools: lectura (contexto, dashboard, workspace, infra, paridad, standup,
198
+ 100 tools: lectura (contexto, dashboard, workspace, infra, paridad, standup,
196
199
  sync report, setup guide, playbooks de etapa y de plataforma, catálogo de
197
200
  skills, search) + `list_*` de cada colección + creación/edición de tareas
198
201
  (con checklist, tiempo, adjuntos, merge), ideas (con comentarios), notas,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "orquestra-mcp",
3
- "version": "1.23.1",
4
- "description": "Servidor MCP de Orquestra -- 89 tools para leer y escribir tareas, ideas, changelog, infraestructura, contenido, QA y más desde un agente de IA",
3
+ "version": "1.26.0",
4
+ "description": "Servidor MCP de Orquestra -- 109 tools para leer y escribir tareas, ideas, changelog, infraestructura, contenido, QA y más desde un agente de IA",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "orquestra-mcp": "bin/orquestra-mcp.mjs"
package/src/api.mjs CHANGED
@@ -94,6 +94,19 @@ export async function apiPatch(config, path, { projectId, id, ...body }) {
94
94
  return parseResponse(res)
95
95
  }
96
96
 
97
+ // Mismo método que apiPatch, pero para un doc singleton SIN id en la ruta
98
+ // (ej. /v1/content-config) -- set_hourly_rate (8 oct 2026, ORQ-247) es el
99
+ // primer caso real.
100
+ export async function apiPatchPath(config, path, { projectId, ...body }) {
101
+ const url = buildUrl(config, path, { projectId })
102
+ const res = await fetch(url, {
103
+ method: 'PATCH',
104
+ headers: { Authorization: `Bearer ${config.apiToken}`, 'Content-Type': 'application/json' },
105
+ body: JSON.stringify(body),
106
+ })
107
+ return parseResponse(res)
108
+ }
109
+
97
110
  // Único método DELETE del cliente (21 ago 2026, para delete_checklist_item
98
111
  // -- ver CLAUDE.md, "único delete_* real de la API/MCP"). Mismo shape que
99
112
  // apiPatch (arma la URL como `${path}/${id}`), sin body -- ningún delete de
package/src/server.mjs CHANGED
@@ -3,7 +3,7 @@ import path from 'node:path'
3
3
  import os from 'node:os'
4
4
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
5
5
  import { z } from 'zod'
6
- import { loadConfig, apiGet, apiPost, apiPatch, apiDelete, putToPresignedUrl, ApiError } from './api.mjs'
6
+ import { loadConfig, apiGet, apiPost, apiPatch, apiPatchPath, apiDelete, putToPresignedUrl, ApiError } from './api.mjs'
7
7
  import { STAGES, PLAYBOOKS } from './stages.mjs'
8
8
  import { FOUNDATIONS } from './foundations.mjs'
9
9
  import { PLATFORM_PLAYBOOKS, TRACKING_TOOLS } from './platformPlaybooks.mjs'
@@ -63,7 +63,7 @@ const SETUP_GUIDE = {
63
63
  { step: 3, section: 'Infraestructura', check: 'list_services / list_environments / list_distributions (o get_infra_status para los 3 juntos)', createOrUpdate: 'create_service|environment|distribution / update_*', matchBy: 'name (o type para servicios/distribuciones)' },
64
64
  { step: 4, section: 'Módulos', check: 'list_modules', createOrUpdate: 'create_module / update_module', matchBy: 'name', dependsOn: ['statusId (paso 1)', 'categoryId (paso 2)'] },
65
65
  { step: 5, section: 'Tareas', check: 'list_tasks', createOrUpdate: 'create_task / update_task', matchBy: 'title', dependsOn: ['statusId', 'categoryId', 'moduleId'] },
66
- { step: 6, section: 'Ideas', check: 'search', createOrUpdate: 'create_idea / update_idea / convert_idea_to_task', matchBy: 'title' },
66
+ { step: 6, section: 'Ideas', check: 'list_ideas', createOrUpdate: 'create_idea / update_idea / convert_idea_to_task', matchBy: 'title' },
67
67
  { step: 7, section: 'Notas', check: 'list_notes', createOrUpdate: 'create_note / update_note', matchBy: 'title' },
68
68
  { step: 8, section: 'Changelog', check: 'list_changelog', createOrUpdate: 'create_changelog_entry / update_changelog_entry', matchBy: 'text' },
69
69
  { step: 9, section: 'Docs', check: 'search', createOrUpdate: 'create_doc (sin update_doc todavía)', matchBy: 'title' },
@@ -117,7 +117,7 @@ No le preguntes al usuario por workspaceId ni projectId -- ya están configurado
117
117
 
118
118
  No crees tareas ni entradas de changelog por preguntas triviales o exploración sin cambios reales -- solo por trabajo que de verdad avanza el proyecto.
119
119
 
120
- Si estás iniciando una sesión de trabajo real en un proyecto que YA tiene código (no un "vacío" recién conectado, ver arriba) y hace rato no se corrió este chequeo, o el usuario pide explícito "revisa fundamentos"/"qué le falta a este proyecto" (no lo hagas en cada mensaje ni en exploración sin cambios): llamá a get_foundations_checklist y, con acceso real al filesystem del repo (grep de dependencias, archivos de config, workflows de CI -- seguí el detectionHint de cada ítem), confirmá cuáles de verdad faltan -- filtrá por "scales" contra el scale real del proyecto (ver project.scale en get_project_context; un ítem con scales:["platform"] no aplica a una landing). Por cada uno que confirmes ausente con evidencia concreta, llamá a report_foundation_gap con esa evidencia específica -- nunca lo reportes solo porque el checklist lo menciona, sin haber revisado el código de verdad. No repitas un chequeo ya hecho recientemente en la misma sesión.
120
+ Si estás iniciando una sesión de trabajo real en un proyecto que YA tiene código (no un "vacío" recién conectado, ver arriba) y hace rato no se corrió este chequeo, o el usuario pide explícito "revisa fundamentos"/"qué le falta a este proyecto" (no lo hagas en cada mensaje ni en exploración sin cambios): primero llamá a get_project_findings -- ya trae, calculado por el servidor y priorizado, lo que falta o está mal dentro de Orquestra (vencidas, bloqueadas, módulos en riesgo, paridad, infraestructura, servicios que faltan, duplicadas) -- y preséntaselo al usuario por severidad antes de seguir; luego, para lo que solo se ve en el código, llamá a get_foundations_checklist y, con acceso real al filesystem del repo (grep de dependencias, archivos de config, workflows de CI -- seguí el detectionHint de cada ítem), confirmá cuáles de verdad faltan -- filtrá por "scales" contra el scale real del proyecto (ver project.scale en get_project_context; un ítem con scales:["platform"] no aplica a una landing). Por cada uno que confirmes ausente con evidencia concreta, llamá a report_foundation_gap con esa evidencia específica -- nunca lo reportes solo porque el checklist lo menciona, sin haber revisado el código de verdad. No repitas un chequeo ya hecho recientemente en la misma sesión.
121
121
 
122
122
  Si el usuario pregunta explícito qué Claude Skills de terceros le faltan (o pide instalar alguna) -- no lo hagas sin que te lo pidan: llamá a get_skills_catalog y revisá con acceso real al filesystem cuáles de esos keys ya existen en .claude/skills/ (del proyecto) o ~/.claude/skills/ (global). Antes de instalar cualquiera con install_skill, llamá primero a preview_skill y mostrale al usuario el skillMdContent completo -- nunca instales sin que haya visto ese contenido y confirmado explícitamente, los SKILL.md de terceros pueden traer instrucciones que un agente ejecuta solo con cargarlas.`
123
123
 
@@ -228,9 +228,29 @@ export function createServer() {
228
228
  registerRead(
229
229
  'search',
230
230
  '/v1/search',
231
- 'Busca por texto en tareas, notas, ideas y docs de un proyecto de Orquestra.',
232
- { q: z.string().describe('Texto a buscar') },
233
- (args, projectId) => ({ projectId, q: args.q })
231
+ 'Busca por texto en tareas, notas, ideas y docs de un proyecto de Orquestra. Tope de 30 resultados con las tareas primero: si buscas ideas o docs en un proyecto con muchas tareas, pasa `types` para que no queden tapadas.',
232
+ {
233
+ q: z.string().describe('Texto a buscar'),
234
+ types: z.array(z.enum(['task', 'note', 'idea', 'doc'])).optional().describe('Limitar a estos tipos (opcional). Sin esto busca en los cuatro.'),
235
+ },
236
+ (args, projectId) => ({ projectId, q: args.q, types: args.types?.join(',') })
237
+ )
238
+
239
+ registerRead(
240
+ 'list_ideas',
241
+ '/v1/ideas',
242
+ 'Lista las ideas de un proyecto de Orquestra. Por default excluye las hechas y descartadas (mismo criterio que list_tasks): pide un `status` puntual (se respeta tal cual, incluso uno cerrado) o includeClosed=true para verlas. Filtros AND por módulo, prioridad y origen.',
243
+ {
244
+ status: z.enum(IDEA_STATUSES).optional().describe('Un estatus puntual (opcional)'),
245
+ includeClosed: z.boolean().optional().describe('true para incluir también hechas y descartadas'),
246
+ moduleId: z.string().optional().describe('ID de módulo para filtrar (opcional)'),
247
+ priority: z.enum(TASK_PRIORITIES).optional().describe('Prioridad para filtrar (opcional)'),
248
+ origin: z.enum(['cliente', 'soporte', 'equipo', 'ventas']).optional().describe('Origen para filtrar (opcional)'),
249
+ },
250
+ (args, projectId) => ({
251
+ projectId, status: args.status, includeClosed: args.includeClosed ? 'true' : undefined,
252
+ moduleId: args.moduleId, priority: args.priority, origin: args.origin,
253
+ })
234
254
  )
235
255
 
236
256
  // ═══ LECTURA (nivel A -- dashboard, workspace, infra, paridad, playbooks) ═══
@@ -243,6 +263,31 @@ export function createServer() {
243
263
  (args, projectId) => ({ projectId })
244
264
  )
245
265
 
266
+ registerRead(
267
+ 'get_project_findings',
268
+ '/v1/findings',
269
+ 'Qué le falta o qué está mal en un proyecto, ya calculado por el servidor y priorizado por severidad (high/medium/low): tareas vencidas o bloqueadas, módulos en riesgo, tareas sin módulo/fecha, referencias rotas, desfases de paridad, infraestructura caída, servicios que faltan (backend/base de datos/auth/analytics/push), posibles tareas duplicadas y fundamentos olvidados que ya se reportaron. Cada hallazgo trae título, detalle, entidades afectadas (con id) y una acción sugerida. Es determinístico, rápido y no consume créditos de IA. Lo ya descartado o aceptado como sugerencia no aparece.',
270
+ {},
271
+ (args, projectId) => ({ projectId })
272
+ )
273
+
274
+ registerCreate(
275
+ 'ask_assistant',
276
+ '/v1/assistant/ask',
277
+ 'Pregúntale al Asistente de Orquestra sobre el proyecto (qué hacer hoy, qué está en riesgo, por qué algo está atorado). El servidor arma el contexto real del proyecto -- tareas abiertas, hallazgos, módulos -- y responde { answer, proposals } citando tareas por código. Consume 1 crédito de IA del workspace por pregunta; si solo necesitas saber qué falta, usa get_project_findings, que es gratis. Solo recomienda: no cambia nada en el proyecto.',
278
+ {
279
+ question: z.string().describe('La pregunta, en lenguaje natural (máx. 1000 caracteres)'),
280
+ history: z.array(z.object({ role: z.enum(['user', 'assistant']), text: z.string() })).optional().describe('Turnos previos de la conversación, si hay'),
281
+ }
282
+ )
283
+
284
+ registerCreate(
285
+ 'assess_project',
286
+ '/v1/assistant/assess',
287
+ 'Diagnóstico corto del proyecto con IA: un resumen del estado y de 1 a 3 prioridades, partiendo de los hallazgos de get_project_findings. El servidor lo guarda 1 hora mientras los hallazgos no cambien, así que repetirlo es gratis; solo cobra 1 crédito de IA cuando lo recalcula. Si no hay hallazgos no usa IA.',
288
+ {}
289
+ )
290
+
246
291
  registerRead(
247
292
  'get_infra_status',
248
293
  '/v1/infra',
@@ -290,6 +335,54 @@ export function createServer() {
290
335
  }
291
336
  )
292
337
 
338
+ // Config "suelta" que hasta el 8 oct 2026 solo se podía tocar desde la
339
+ // web (ver workspaceSettings.mjs en orquestra-infra) -- José pidió poder
340
+ // darla de alta también por acá.
341
+ server.registerTool(
342
+ 'set_slack_webhook',
343
+ {
344
+ description: 'Configura (o quita, con url:"") el webhook de Slack del workspace -- cada notificación (tareas asignadas, QA, infra, contenido...) también se manda ahí, además de in-app/push/correo. Requiere un token de servicio SIN projectId (mismo criterio que get_workspace_summary/init_project) -- un token scopeado a un proyecto puntual no puede tocar esto.',
345
+ inputSchema: {
346
+ url: z.string().describe('URL del Incoming Webhook de Slack (https://hooks.slack.com/services/...), o "" para desactivarlo'),
347
+ },
348
+ },
349
+ async ({ url }) => {
350
+ try {
351
+ const data = await apiPatch(config, '/v1/workspaces', { id: config.workspaceId, slackWebhookUrl: url })
352
+ return textResult(data)
353
+ } catch (err) {
354
+ return errorResult(err)
355
+ }
356
+ }
357
+ )
358
+
359
+ registerRead('get_content_budget', '/v1/content-config',
360
+ 'Lee los topes numéricos de presupuesto/tarifa de Inversión de Contenido de un proyecto (hourlyRate, monthlyBudget, dailyCap, perCampaignCap, alertThresholdPct). No trae el resto de contentConfig (guía de marca, imágenes, fragmentos aprobados) -- eso es contenido rico pensado para autoría humana en la web.',
361
+ {}, (args, projectId) => ({ projectId }))
362
+
363
+ server.registerTool(
364
+ 'set_hourly_rate',
365
+ {
366
+ description: 'Configura la tarifa por hora (u otros topes numéricos de presupuesto de Inversión de Contenido) de un proyecto -- alimenta "Horas del equipo" (Σ timeEntries del mes en curso × esta tarifa) y el reporte de horas facturables de la web (sección Horas). Al menos un campo es obligatorio.',
367
+ inputSchema: {
368
+ projectId: z.string().optional().describe(PROJECT_ID_DESC),
369
+ hourlyRate: z.number().optional().describe('Tarifa por hora única del proyecto (misma moneda que el resto de Inversión)'),
370
+ monthlyBudget: z.number().optional().describe('Presupuesto asignado del mes'),
371
+ dailyCap: z.number().optional().describe('Tope diario -- informativo, no se aplica solo'),
372
+ perCampaignCap: z.number().optional().describe('Tope por campaña -- sí se aplica solo (pausa la campaña al llegar acá)'),
373
+ alertThresholdPct: z.number().optional().describe('Default 80 -- aviso cuando el gasto llega a este % del monthlyBudget'),
374
+ },
375
+ },
376
+ async ({ projectId, ...body }) => {
377
+ try {
378
+ const data = await apiPatchPath(config, '/v1/content-config', { projectId: resolveProjectId(projectId), ...body })
379
+ return textResult(data)
380
+ } catch (err) {
381
+ return errorResult(err)
382
+ }
383
+ }
384
+ )
385
+
293
386
  registerRead(
294
387
  'list_modules',
295
388
  '/v1/modules',
@@ -516,7 +609,8 @@ export function createServer() {
516
609
  scope: z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean() })).optional()
517
610
  .describe('Alcance por distribución (paridad multiplataforma) -- para setearlo desde la creación en vez de crear y después update_task. Usa get_parity_summary/list_distributions para ver qué distId existen. Si una distId tiene hermanas (mismo productKey), el server suma las que falten solo, con done:false -- la respuesta trae parityAddedDistIds con lo que agregó: avísale al usuario que la tarea también quedó pendiente en esas plataformas.'),
518
611
  skipParity: z.boolean().optional().describe('true = NO sumar las plataformas hermanas al scope aunque tengan el mismo productKey. Solo cuando el trabajo es genuinamente de UNA plataforma (ej. un ajuste de Gradle que no tiene contraparte en iOS) -- no lo mandes por default. Las tareas type="bug" nunca propagan, sin necesidad de esto.'),
519
- relatedTaskId: z.string().optional().describe('"Bloqueada por" -- id de otra tarea. Esta tarea se muestra como bloqueada mientras esa otra no llegue a un estatus final (isFinal). Úsalo cuando el trabajo real depende de que otra tarea termine primero.'),
612
+ relatedTaskId: z.string().optional().describe('"Bloqueada por" -- id de otra tarea. LEGACY desde ORQ-248: si vas a bloquear por más de una tarea, usa blockedByIds en vez de esto (relatedTaskId se sigue llenando solo, como blockedByIds[0], para paridad con iOS/Android).'),
613
+ blockedByIds: z.array(z.string()).optional().describe('"Bloqueada por" -- ids de otras tareas (puede ser más de una). Esta tarea se muestra como bloqueada mientras ALGUNA de esas no llegue a un estatus final (isFinal). Reemplaza a relatedTaskId para bloqueos múltiples -- preferí este campo, no relatedTaskId, salvo que de verdad sea un solo bloqueador.'),
520
614
  location: z.string().optional().describe('Solo relevante para type "meeting"/"call" (plataforma o lugar, ej. "Zoom", "Oficina") o "followup"/"business" (contexto libre) -- ignóralo para el resto de los tipos.'),
521
615
  estimatedDays: z.number().optional().describe('Estimación en días de trabajo.'),
522
616
  stack: z.array(z.string()).optional().describe('Tecnologías involucradas (mismo vocabulario que el stack de una distribución, ej. "node", "react", "postgres").'),
@@ -551,7 +645,8 @@ export function createServer() {
551
645
  tags: z.array(z.string()).optional(),
552
646
  scope: z.array(z.object({ distId: z.string(), layer: z.string().optional(), done: z.boolean() })).optional()
553
647
  .describe('Alcance por distribución -- usa get_parity_summary para ver el scope actual antes de editarlo'),
554
- relatedTaskId: z.string().optional().describe('"Bloqueada por" -- id de otra tarea, o "" para quitar el bloqueo. Ver create_task.'),
648
+ relatedTaskId: z.string().optional().describe('"Bloqueada por" -- id de otra tarea, o "" para quitar el bloqueo. LEGACY, ver create_task -- preferí blockedByIds.'),
649
+ blockedByIds: z.array(z.string()).optional().describe('"Bloqueada por" -- reemplaza el array COMPLETO (no hace merge), [] quita todos los bloqueos. Ver create_task.'),
555
650
  location: z.string().optional().describe('Solo relevante para type "meeting"/"call"/"followup"/"business". Ver create_task.'),
556
651
  comment: z.string().optional().describe('Explica qué hiciste o por qué cambia la tarea -- queda como comentario visible en ella. Obligatorio en la práctica cuando mandas statusId: sin esto, alguien ve que la tarea cambió de estatus pero no por qué.'),
557
652
  estimatedDays: z.number().optional().describe('Estimación en días de trabajo.'),
@@ -1652,7 +1747,7 @@ export function createServer() {
1652
1747
  deployMode: z.enum(['auto', 'manual']).optional().describe('Modo de deploy -- web'),
1653
1748
  hasPush: z.boolean().optional().describe('Tiene push notifications -- apps (ios/android)'),
1654
1749
  storeUrl: z.string().optional(),
1655
- stack: z.array(z.string()).optional(),
1750
+ stack: z.array(z.string()).optional().describe('Tecnologías de la plataforma. Puedes mandar nombres ("Kotlin", "Jetpack Compose", "Node.js"): la API los normaliza a las claves de su catálogo (schema/enums/TechStack.json en orquestra-infra) y conserva lo que no reconoce.'),
1656
1751
  version: z.string().optional().describe('Versión en store'),
1657
1752
  reviewVersion: z.string().optional().describe('Build en revisión -- apps'),
1658
1753
  repoUrl: z.string().optional().describe('URL del repo de este código (ej. de "git remote get-url origin") -- solo informativo, no requiere credenciales. Distinto de conectar el repo con PAT (eso es Workspace → Conexiones, un paso deliberado del usuario, nunca lo hagas solo).'),
@@ -1673,7 +1768,7 @@ export function createServer() {
1673
1768
  deployMode: z.enum(['auto', 'manual']).optional(),
1674
1769
  hasPush: z.boolean().optional(),
1675
1770
  storeUrl: z.string().optional(),
1676
- stack: z.array(z.string()).optional(),
1771
+ stack: z.array(z.string()).optional().describe('Tecnologías de la plataforma. Puedes mandar nombres ("Kotlin", "Jetpack Compose", "Node.js"): la API los normaliza a las claves de su catálogo (schema/enums/TechStack.json en orquestra-infra) y conserva lo que no reconoce.'),
1677
1772
  version: z.string().optional(),
1678
1773
  reviewVersion: z.string().optional(),
1679
1774
  repoUrl: z.string().optional().describe('URL del repo de este código -- solo informativo, no requiere credenciales.'),
@@ -1763,6 +1858,39 @@ export function createServer() {
1763
1858
  status: z.enum(CAMPAIGN_STATUSES).optional(),
1764
1859
  })
1765
1860
 
1861
+ // Registro de riesgos a nivel de proyecto (ORQ-246, 8 oct 2026) --
1862
+ // distinto de "bloqueada" en una tarea puntual (relatedTaskId/
1863
+ // isTaskBlocked): esto es "qué puede hacer fallar el proyecto" en
1864
+ // general, sin necesitar estar ligado a una tarea.
1865
+ const RISK_LEVELS = ['low', 'medium', 'high']
1866
+ const RISK_STATUSES = ['open', 'mitigating', 'closed']
1867
+
1868
+ registerRead('list_risks', '/v1/risks',
1869
+ 'Lista los riesgos/bloqueos a nivel de proyecto (dependencias externas, decisiones pendientes de un cliente, etc. -- no ligados necesariamente a una tarea puntual). Úsala antes de create_risk para no duplicar.',
1870
+ {}, (args, projectId) => ({ projectId }))
1871
+
1872
+ registerCreate('create_risk', '/v1/risks', 'Registra un riesgo/bloqueo a nivel de proyecto -- algo que puede hacer fallar o atrasar el proyecto en general, no el bloqueo puntual de una tarea (para eso usa relatedTaskId en update_task).', {
1873
+ title: z.string(),
1874
+ description: z.string().optional(),
1875
+ probability: z.enum(RISK_LEVELS).optional().describe('Default: medium'),
1876
+ impact: z.enum(RISK_LEVELS).optional().describe('Default: medium'),
1877
+ mitigation: z.string().optional().describe('Plan o texto libre de qué se hace para reducir este riesgo'),
1878
+ status: z.enum(RISK_STATUSES).optional().describe('Default: open. "closed" en vez de borrar -- nunca se borra un riesgo.'),
1879
+ ownerUid: z.string().optional().describe('Dueño del riesgo -- le avisa solo a esa persona al asignarla (ver list de miembros del workspace)'),
1880
+ moduleId: z.string().optional().describe('Módulo relacionado, si aplica (ver list_modules)'),
1881
+ })
1882
+
1883
+ registerUpdate('update_risk', '/v1/risks', 'Edita un riesgo existente -- típicamente para actualizar su mitigación o cerrarlo (status: "closed") cuando deja de aplicar.', {
1884
+ title: z.string().optional(),
1885
+ description: z.string().optional(),
1886
+ probability: z.enum(RISK_LEVELS).optional(),
1887
+ impact: z.enum(RISK_LEVELS).optional(),
1888
+ mitigation: z.string().optional(),
1889
+ status: z.enum(RISK_STATUSES).optional(),
1890
+ ownerUid: z.string().optional(),
1891
+ moduleId: z.string().optional(),
1892
+ })
1893
+
1766
1894
  registerRead('list_content', '/v1/content',
1767
1895
  'Lista las publicaciones de contenido (posts programados/publicados) de un proyecto.',
1768
1896
  {}, (args, projectId) => ({ projectId }))