orquestra-mcp 1.58.0 → 1.70.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 **149 tools**
3
+ Servidor MCP (Model Context Protocol) para Orquestra. Expone **161 tools**
4
4
  (conteo al 24 oct 2026, v1.49.0; la lista real está en `src/server.mjs`)
5
5
  que llaman a la API de `orquestra-infra`: lectura de contexto/dashboard/
6
6
  workspace/infra/paridad/listados, escritura de tareas/ideas/changelog/
@@ -42,6 +42,14 @@ de su caché.
42
42
  publicada. Con `@latest` cada arranque la usa; el servidor que ya está corriendo conserva
43
43
  la suya hasta que reconectas (en Claude Code: `/mcp` → reconectar).
44
44
 
45
+ **Forzar la actualización:** reinicia la sesión (o `/mcp` → reconectar) y, si sigue la
46
+ versión vieja, `npx clear-npx-cache`. Si registraste el MCP como `orquestra-mcp` sin
47
+ `@latest`, migra con un comando:
48
+ `claude mcp remove orquestra && claude mcp add orquestra -e ORQUESTRA_AGENT=claude-code -- npx -y orquestra-mcp@latest`.
49
+ `diagnostico_conexion` (cuando no hay conexión) muestra la versión en uso frente a la
50
+ última publicada y detecta configs sin `@latest`; con conexión, `get_project_context`
51
+ trae `orquestraMcpUpdate` si hay versión nueva.
52
+
45
53
  Variable opcional `ORQUESTRA_WEB_URL` para apuntar el login a otro lugar
46
54
  (ej. `http://localhost:5173` mientras desarrollas `orquestra-web` local en
47
55
  vez de contra producción).
@@ -133,6 +141,17 @@ claude mcp add orquestra \
133
141
  -- npx -y orquestra-mcp@latest
134
142
  ```
135
143
 
144
+ ## Novedades 1.66.0 (ORQ-532..535)
145
+
146
+ - `close_task({id, comment, completeChecklist?, waiveVerifications?})`: cierra una tarea desde cualquier estatus
147
+ recorriendo el flujo (camino más corto por `nextIds` hasta el final, pasando por "En revisión"); un solo
148
+ `comment` (en el último paso). Respeta las guardas de checklist/verificaciones.
149
+ - `get_tool_guide({topic})`: reglas completas de `update_task`, `create_task`, `update_tasks`, `close_task` y
150
+ `related_impact`; las descripciones de esas tools son cortas a propósito (test de tope en `test/mejorasMcp.test.mjs`).
151
+ - `update_tasks` acepta `compact:true` (recomendado): cada resultado trae solo `{ index, id, ok, status, shortCode, statusId, relatedImpact?, unresolvedLinks?, error? }`.
152
+ - `create_task` acepta `checklist: [{text, done?, assignedTo?}]` y responde `checklistCount` (y `checklistFailed` si algún ítem falló).
153
+ - `search` documenta `statusId`/`tags` en los resultados (requiere la API con ORQ-536).
154
+
136
155
  ## Tools
137
156
 
138
157
  Lectura de contexto:
@@ -140,7 +159,9 @@ Lectura de contexto:
140
159
  | Tool | Args | Descripción |
141
160
  |---|---|---|
142
161
  | `get_project_context` | `projectId?` | Snapshot compacto del proyecto (fase, tareas abiertas/vencidas/bloqueadas, módulos, ideas, changelog reciente) |
143
- | `list_tasks` | `projectId?`, `statusId?` | Lista de tareas, filtro opcional por estatus |
162
+ | `list_tasks` | `projectId?`, `statusId?`, `summary?`, `fields?`, `limit?`, `cursor?` | Lista de tareas, filtro opcional por estatus; en proyectos grandes usa `summary`/`fields`/`limit` y pagina con `nextCursor` |
163
+ | `get_task` | `code?` o `id?` | Una tarea completa (checklist y verificaciones) por folio o id; 404 si no existe, 409 si el código es ambiguo |
164
+ | `update_tasks` | `items[]` (1-50), `opId?` | Edita varias tareas en una llamada; respuesta parcial por ítem |
144
165
  | `search` | `projectId?`, `q` | Búsqueda por texto en tareas/notas/ideas/docs |
145
166
  | `get_dashboard_summary` | `projectId?` | Hoy/vencidas/bloqueadas, salud de módulos, actividad reciente |
146
167
  | `get_project_findings` | `projectId?` | Qué le falta al proyecto, priorizado por severidad (determinístico, sin créditos de IA) |
@@ -166,7 +187,7 @@ parcial: la lista completa está en `src/server.mjs`:
166
187
  | Entidad | Tools | Notas |
167
188
  |---|---|---|
168
189
  | Tareas | `create_task`, `update_task` | `update_task` incluye `scope` — úsalo para resolver desfases de paridad |
169
- | Verificaciones por plataforma | `list_verifications`, `add_verification`, `update_verification`, `delete_verification`, `verify_with_agent` | Confirmaciones (humanas o de agente) de que la tarea quedó bien en web/iOS/Android/API — NO van en el checklist. La tarea no cierra con verificaciones abiertas y se cierra sola al aprobarse la última; `failed` (con `note`) la regresa a su estatus anterior. También: `verifications` en `create_task`, `waiveVerifications` en `update_task` (solo con confirmación del usuario) y `verificationId` en `create_agent_run` |
190
+ | Verificaciones por plataforma | `list_verifications`, `add_verification`, `add_verifications` (lote), `update_verification`, `delete_verification`, `verify_with_agent` | Confirmaciones (humanas o de agente) de que la tarea quedó bien en web/iOS/Android/API — NO van en el checklist. La tarea no cierra con verificaciones abiertas y se cierra sola al aprobarse la última; `failed` (con `note`) la regresa a su estatus anterior. También: `verifications` en `create_task`, `waiveVerifications` en `update_task` (solo con confirmación del usuario) y `verificationId` en `create_agent_run` |
170
191
  | Ideas | `list_ideas`, `create_idea`, `update_idea` | |
171
192
  | Changelog | `list_changelog`, `create_changelog_entry`, `update_changelog_entry` | Estilo Keep a Changelog |
172
193
  | Notas | `list_notes`, `create_note`, `update_note` | |
@@ -229,7 +250,7 @@ create/update sigue siendo criterio del agente y del humano.
229
250
 
230
251
  ## Estado
231
252
 
232
- 149 tools (ver arriba): lectura (contexto, dashboard, workspace, infra, paridad, standup,
253
+ 161 tools (ver arriba): lectura (contexto, dashboard, workspace, infra, paridad, standup,
233
254
  sync report, setup guide, playbooks de etapa y de plataforma, catálogo de
234
255
  skills, search) + `list_*` de cada colección + creación/edición de tareas
235
256
  (con checklist, verificaciones por plataforma, 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.58.0",
4
- "description": "Servidor MCP de Orquestra -- 135 tools para leer y escribir tareas, ideas, changelog, infraestructura, contenido, QA y más desde un agente de IA",
3
+ "version": "1.70.0",
4
+ "description": "Servidor MCP de Orquestra -- 161 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
@@ -68,19 +68,24 @@ function buildUrl(config, path, params) {
68
68
  // Todas pasan por request() (http.mjs, ORQ-343): plazo por intento, cancelación de la tool y
69
69
  // reintentos acotados -- las lecturas ante fallas transitorias, las escrituras solo ante el
70
70
  // throttling del API Gateway (no llegó a la Lambda, no puede duplicar nada).
71
- const auth = config => ({ Authorization: `Bearer ${config.apiToken}` })
71
+ // x-orquestra-client: la API lo lee para el origen de la bitácora de acceso (ORQ-480).
72
+ const auth = config => ({ Authorization: `Bearer ${config.apiToken}`, 'x-orquestra-client': 'mcp' })
72
73
  const jsonHeaders = config => ({ ...auth(config), 'Content-Type': 'application/json' })
73
74
 
74
75
  export async function apiGet(config, path, params = {}) {
75
76
  return request(buildUrl(config, path, params), { headers: auth(config) })
76
77
  }
77
78
 
78
- export async function apiPost(config, path, { projectId, ...body }) {
79
- return request(buildUrl(config, path, { projectId }), { method: 'POST', headers: jsonHeaders(config), body: JSON.stringify(body) })
79
+ export async function apiPost(config, path, { projectId, ...body }, query = {}) {
80
+ return request(buildUrl(config, path, { projectId, ...query }), { method: 'POST', headers: jsonHeaders(config), body: JSON.stringify(body) })
80
81
  }
81
82
 
82
- export async function apiPatch(config, path, { projectId, id, ...body }) {
83
- return request(buildUrl(config, `${path}/${id}`, { projectId }), { method: 'PATCH', headers: jsonHeaders(config), body: JSON.stringify(body) })
83
+ export async function apiPut(config, path, { projectId, ...body }, query = {}) {
84
+ return request(buildUrl(config, path, { projectId, ...query }), { method: 'PUT', headers: jsonHeaders(config), body: JSON.stringify(body) })
85
+ }
86
+
87
+ export async function apiPatch(config, path, { projectId, id, ...body }, query = {}) {
88
+ return request(buildUrl(config, `${path}/${id}`, { projectId, ...query }), { method: 'PATCH', headers: jsonHeaders(config), body: JSON.stringify(body) })
84
89
  }
85
90
 
86
91
  // Mismo método que apiPatch, pero para un doc singleton SIN id en la ruta
@@ -94,14 +99,14 @@ export async function apiPatchPath(config, path, { projectId, ...body }) {
94
99
  // -- ver CLAUDE.md, "único delete_* real de la API/MCP"). Mismo shape que
95
100
  // apiPatch (arma la URL como `${path}/${id}`), sin body -- ningún delete de
96
101
  // esta API necesita mandar datos, solo identificar el recurso.
97
- export async function apiDelete(config, path, { projectId, id }) {
98
- return request(buildUrl(config, `${path}/${id}`, { projectId }), { method: 'DELETE', headers: auth(config) })
102
+ export async function apiDelete(config, path, { projectId, id }, query = {}) {
103
+ return request(buildUrl(config, `${path}/${id}`, { projectId, ...query }), { method: 'DELETE', headers: auth(config) })
99
104
  }
100
105
 
101
106
  // DELETE sobre un recurso singleton SIN id en la ruta (ej. /v1/client-portal,
102
107
  // ORQ-332) -- mismo criterio que apiPatchPath.
103
- export async function apiDeletePath(config, path, { projectId }) {
104
- return request(buildUrl(config, path, { projectId }), { method: 'DELETE', headers: auth(config) })
108
+ export async function apiDeletePath(config, path, { projectId, ...query }) {
109
+ return request(buildUrl(config, path, { projectId, ...query }), { method: 'DELETE', headers: auth(config) })
105
110
  }
106
111
 
107
112
  // PUT crudo a una presigned URL de S3 (createAttachmentUploadUrl en
@@ -0,0 +1,65 @@
1
+ // ORQ-532: lógica pura de close_task. La API no recorre estatus sola (cada estatus solo permite pasar a su
2
+ // `nextIds`), así que cerrar una tarea que está en Backlog/Bloqueado/etc. exige varios PATCH. Aquí se calcula
3
+ // el camino más corto hacia el estatus de cierre normal (final, no cancelado ni archivo), sin tocar la red.
4
+
5
+ const isClosed = s => !!(s && (s.isFinal || s.isCancel || s.isArchive))
6
+ const isReview = s => /revisi[oó]n|review/i.test(s?.name || '')
7
+
8
+ /** Normaliza la respuesta de GET /v1/statuses (arreglo directo o { items }). */
9
+ export function normalizeStatuses(data) {
10
+ const list = Array.isArray(data) ? data : Array.isArray(data?.items) ? data.items : Array.isArray(data?.statuses) ? data.statuses : []
11
+ return list.filter(s => s && s.id)
12
+ }
13
+
14
+ /** Estatus de cierre normal: final y ni cancelado ni archivo. */
15
+ export function closeTargets(statuses) {
16
+ return statuses.filter(s => s.isFinal && !s.isCancel && !s.isArchive)
17
+ }
18
+
19
+ /** BFS desde `fromId` por nextIds; no transita por estatus cerrados (final/cancelado/archivo) salvo como destino.
20
+ * Devuelve el camino más corto como arreglo de estatus (sin incluir el origen) hacia `targetId`, o null. */
21
+ function shortestPath(byId, fromId, targetId) {
22
+ const prev = new Map([[fromId, null]])
23
+ const queue = [fromId]
24
+ while (queue.length) {
25
+ const cur = queue.shift()
26
+ if (cur === targetId) break
27
+ for (const nid of byId.get(cur)?.nextIds || []) {
28
+ const next = byId.get(nid)
29
+ if (!next || prev.has(nid)) continue
30
+ if (isClosed(next) && nid !== targetId) continue
31
+ prev.set(nid, cur)
32
+ queue.push(nid)
33
+ }
34
+ }
35
+ if (!prev.has(targetId)) return null
36
+ const out = []
37
+ for (let id = targetId; id !== fromId; id = prev.get(id)) out.unshift(byId.get(id))
38
+ return out
39
+ }
40
+
41
+ /**
42
+ * @param {object[]} statuses estatuses del proyecto ({ id, name, order, nextIds, isFinal, isCancel, isArchive })
43
+ * @param {string} fromId estatus actual de la tarea
44
+ * @returns {{ alreadyClosed: true, current: object } | { path: object[], target: object } | { error: string }}
45
+ */
46
+ export function findClosePath(statuses, fromId) {
47
+ const byId = new Map(statuses.map(s => [s.id, s]))
48
+ const current = byId.get(fromId)
49
+ if (!current) return { error: `El estatus actual de la tarea (${fromId}) no existe en el proyecto` }
50
+ if (isClosed(current)) return { alreadyClosed: true, current }
51
+ const targets = closeTargets(statuses)
52
+ if (!targets.length) return { error: 'El proyecto no tiene un estatus final (isFinal) al que cerrar' }
53
+ const candidates = targets
54
+ .map(t => ({ target: t, path: shortestPath(byId, fromId, t.id) }))
55
+ .filter(c => c.path)
56
+ if (!candidates.length) {
57
+ return { error: `No hay camino por nextIds desde "${current.name}" hasta un estatus final (${targets.map(t => `"${t.name}"`).join(', ')}). Revisa el flujo con list_statuses.` }
58
+ }
59
+ // Más corto; a igual largo, el que pasa por "En revisión" (flujo normal de cierre); luego el de menor order.
60
+ candidates.sort((a, b) =>
61
+ a.path.length - b.path.length
62
+ || Number(b.path.some(isReview)) - Number(a.path.some(isReview))
63
+ || (a.target.order ?? 0) - (b.target.order ?? 0))
64
+ return { path: candidates[0].path, target: candidates[0].target }
65
+ }
@@ -3,7 +3,7 @@
3
3
  // lo lee directo desde aca (mismo criterio que stages.mjs/foundations.mjs).
4
4
  // Puerto de TRACKING_TOOLS de la web: herramientas de medición conectables a una
5
5
  // distribución web. El ID vive en distributions.publishForm[key]. `pattern` valida
6
- // el formato (se serializa como string en la web, acá es el RegExp directo).
6
+ // el formato (se serializa como string en la web, aquí es el RegExp directo).
7
7
  export const TRACKING_TOOLS = [
8
8
  { key: 'ga4_measurement_id', label: 'Google Analytics 4', idLabel: 'ID de medición', pattern: /^G-[A-Z0-9]{6,12}$/, example: 'G-ABC123DEF4', where: 'Google Analytics → Admin → Flujos de datos' },
9
9
  { key: 'meta_pixel_id', label: 'Meta Pixel', idLabel: 'ID del Pixel', pattern: /^\d{8,20}$/, example: '1234567890123456', where: 'Meta Business → Events Manager' },
@@ -312,7 +312,7 @@ export const PLATFORM_PLAYBOOKS = {
312
312
  {
313
313
  key: 'privacy_policy',
314
314
  title: 'Privacy policy URL',
315
- body: 'Obligatoria para publicar -- Play Console valida que la URL responda 200 al guardarla, no solo que tenga formato de URL. Ojo con apps de una sola página (React Router y similares): si el hosting no tiene configurado el rewrite de SPA (servir index.html para cualquier ruta), una URL como /privacidad puede dar 404 real aunque el deploy haya salido bien -- probar la URL en el navegador ANTES de pegarla acá, no confiar en que "ya se desplegó" alcanza.',
315
+ body: 'Obligatoria para publicar -- Play Console valida que la URL responda 200 al guardarla, no solo que tenga formato de URL. Ojo con apps de una sola página (React Router y similares): si el hosting no tiene configurado el rewrite de SPA (servir index.html para cualquier ruta), una URL como /privacidad puede dar 404 real aunque el deploy haya salido bien -- probar la URL en el navegador ANTES de pegarla aquí, no confiar en que "ya se desplegó" alcanza.',
316
316
  },
317
317
  {
318
318
  key: 'ads_declaration',
@@ -332,7 +332,7 @@ export const PLATFORM_PLAYBOOKS = {
332
332
  {
333
333
  key: 'content_rating',
334
334
  title: 'Content rating questionnaire (IARC)',
335
- body: 'Cuestionario largo, con sub-preguntas que se despliegan según tus respuestas. Puntos donde el criterio NO es obvio: "User Content Sharing" (¿la app permite nativamente intercambiar texto/voz/imágenes/audio entre usuarios?) es MÁS AMPLIO que el "User-Generated Content" de Apple -- Apple exime contenido privado dentro de un equipo, Google NO tiene esa excepción, así que comentarios/notas compartidas entre miembros de un workspace SÍ cuentan como "Yes" acá aunque sean privados. Si contestas "Yes" ahí, "¿las interacciones pueden limitarse a usuarios invitados?" responde "Yes" si el acceso es por invitación (esto ayuda a mantener la clasificación baja). El resto (violencia, sexual, lenguaje, drogas, apuestas) va en "No"/"None" para una app B2B típica -- resultado esperado: la clasificación más baja (3+/PEGI 3/Everyone).',
335
+ body: 'Cuestionario largo, con sub-preguntas que se despliegan según tus respuestas. Puntos donde el criterio NO es obvio: "User Content Sharing" (¿la app permite nativamente intercambiar texto/voz/imágenes/audio entre usuarios?) es MÁS AMPLIO que el "User-Generated Content" de Apple -- Apple exime contenido privado dentro de un equipo, Google NO tiene esa excepción, así que comentarios/notas compartidas entre miembros de un workspace SÍ cuentan como "Yes" aquí aunque sean privados. Si contestas "Yes" ahí, "¿las interacciones pueden limitarse a usuarios invitados?" responde "Yes" si el acceso es por invitación (esto ayuda a mantener la clasificación baja). El resto (violencia, sexual, lenguaje, drogas, apuestas) va en "No"/"None" para una app B2B típica -- resultado esperado: la clasificación más baja (3+/PEGI 3/Everyone).',
336
336
  },
337
337
  {
338
338
  key: 'target_audience',