orquestra-mcp 1.70.0 → 1.72.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/server.mjs +40 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orquestra-mcp",
3
- "version": "1.70.0",
3
+ "version": "1.72.0",
4
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": {
package/src/server.mjs CHANGED
@@ -2961,15 +2961,16 @@ export function createServer() {
2961
2961
  server.registerTool(
2962
2962
  'upsert_design_tokens',
2963
2963
  {
2964
- description: `Carga en lote (hasta 100) tokens de identidad de diseño: es el camino para poblar o sincronizar la identidad visual de una vez. ${DESIGN_TOKEN_PURPOSE} Cada token se identifica por (name, kind, platform): si existe se actualiza, si no se crea; nada se borra. Se valida todo antes de escribir (un token inválido o repetido en el lote rechaza el lote entero). Devuelve { created, updated, items }.`,
2964
+ description: `Carga en lote (hasta 100) tokens de identidad de diseño: es el camino para poblar o sincronizar la identidad visual de una vez. ${DESIGN_TOKEN_PURPOSE} Cada token se identifica por (name, kind, platform): si existe se actualiza, si no se crea; nada se borra. Se valida todo antes de escribir (un token inválido o repetido en el lote rechaza el lote entero). Devuelve { created, updated, items }; por defecto compact=true y items trae solo {id,name,key?,platform?,action}, con compact=false devuelve las filas completas.`,
2965
2965
  inputSchema: {
2966
2966
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
2967
+ compact: z.boolean().optional().describe('true (default) = el lote devuelve items resumidos {id,name,key?,platform?,action}; false = filas completas'),
2967
2968
  tokens: z.array(z.object(designTokenFields(false))).min(1).max(100).describe('Tokens a crear/actualizar (1-100)'),
2968
2969
  },
2969
2970
  },
2970
- async ({ projectId, tokens }) => {
2971
+ async ({ projectId, tokens, compact }) => {
2971
2972
  try {
2972
- return textResult(await apiPost(config, '/v1/design-tokens/bulk', { projectId: resolveProjectId(projectId), tokens }))
2973
+ return textResult(await apiPost(config, '/v1/design-tokens/bulk', { projectId: resolveProjectId(projectId), tokens, compact: compact ?? true }))
2973
2974
  } catch (err) {
2974
2975
  return errorResult(err)
2975
2976
  }
@@ -2985,6 +2986,24 @@ export function createServer() {
2985
2986
  name: z.string().describe('Nombre del bloque, ej: "Encabezado", "Lista de tareas"'),
2986
2987
  description: z.string().optional().describe('Qué contiene o hace'),
2987
2988
  })).describe('Bloques de la pantalla de arriba a abajo, ej: [{"name":"Encabezado","description":"Título y filtros"},{"name":"Lista de tareas"}]')
2989
+ const navItemKey = z.string().min(1).max(60).regex(/^[a-z0-9.-]+$/, 'key en minúsculas, dígitos, punto o guion').describe('minúsculas, dígitos, punto o guion (≤60), única en el nodo')
2990
+ const uniqueKeys = (arr, ctx) => {
2991
+ const seen = new Set()
2992
+ arr.forEach((it, i) => {
2993
+ if (seen.has(it.key)) ctx.addIssue({ code: 'custom', path: [i, 'key'], message: `key repetida en el nodo: ${it.key}` })
2994
+ seen.add(it.key)
2995
+ })
2996
+ }
2997
+ const navActionsSchema = z.array(z.object({
2998
+ key: navItemKey.describe('Key de la acción, convención entidad.verbo, ej: "task.create", "task.assign"; minúsculas, dígitos, punto o guion (≤60), única en el nodo'),
2999
+ name: z.string().min(1).max(120).describe('Nombre visible de la acción, ej: "Crear tarea" (≤120)'),
3000
+ description: z.string().optional().describe('Qué hace'),
3001
+ roles: z.array(z.string()).optional().describe('Roles que pueden ejecutarla, ej: ["owner","admin"]'),
3002
+ })).max(60).superRefine(uniqueKeys).describe('Acciones que el usuario puede hacer en el nodo (hasta 60). Convención entidad.verbo (task.create); la misma acción lleva la MISMA key en web, iOS y Android: eso permite compararlas en get_design_parity.')
3003
+ const navStatesSchema = z.array(z.object({
3004
+ key: navItemKey.describe('Key del estado; recomendados: loading, empty, error, readonly, no-access, offline, refreshing, search-empty'),
3005
+ note: z.string().optional().describe('Cómo se ve o se comporta en ese estado'),
3006
+ })).max(20).superRefine(uniqueKeys).describe('Estados de la pantalla (hasta 20). Recomendados: loading, empty, error, readonly, no-access, offline, refreshing, search-empty. Misma key en las tres plataformas.')
2988
3007
  const navNodeFields = optional => {
2989
3008
  const f = {
2990
3009
  name: z.string().describe('Nombre visible del nodo, ej: "Tareas"'),
@@ -2998,6 +3017,8 @@ export function createServer() {
2998
3017
  order: z.number().optional().describe('Orden entre hermanos'),
2999
3018
  description: z.string().optional().describe('Qué hace o muestra'),
3000
3019
  blocks: navBlocksSchema.optional(),
3020
+ actions: navActionsSchema.optional(),
3021
+ states: navStatesSchema.optional(),
3001
3022
  sourceRef: z.string().optional().describe('Dónde vive en el código, ej: archivo y símbolo'),
3002
3023
  }
3003
3024
  if (optional) { f.name = f.name.optional(); f.platform = f.platform.optional(); f.kind = f.kind.optional() }
@@ -3027,10 +3048,10 @@ export function createServer() {
3027
3048
  }
3028
3049
  )
3029
3050
  registerCreate('create_design_nav_node', '/v1/design-nav',
3030
- `Crea un nodo de navegación de diseño. ${NAV_PURPOSE} Para cargar muchos de una vez usa upsert_design_nav, no esta tool en bucle.`,
3051
+ `Crea un nodo de navegación de diseño. ${NAV_PURPOSE} Para cargar muchos de una vez usa upsert_design_nav, no esta tool en bucle. Cada nodo acepta "actions" (convención entidad.verbo, ej. task.create; la misma acción lleva la MISMA key en web, iOS y Android) y "states" (recomendados: loading, empty, error, readonly, no-access, offline, refreshing, search-empty). La matriz de paridad compara primero por capacidades (acciones y estados) y cae a bloques si la referencia no las define.`,
3031
3052
  navNodeFields(false))
3032
3053
  registerUpdate('update_design_nav_node', '/v1/design-nav',
3033
- `Edita un nodo de navegación de diseño existente (parche parcial: manda solo lo que cambia; parentId puede mover el nodo o ponerlo en null para dejarlo en la raíz). ${NAV_PURPOSE}`,
3054
+ `Edita un nodo de navegación de diseño existente (parche parcial: manda solo lo que cambia; parentId puede mover el nodo o ponerlo en null para dejarlo en la raíz). Ojo: "actions" y "states" REEMPLAZAN el array completo del nodo (manda la lista final; para mezclar por key usa upsert_design_nav). Convención de actions: entidad.verbo (task.create), misma key en web, iOS y Android; states recomendados: loading, empty, error, readonly, no-access, offline, refreshing, search-empty. La matriz de paridad compara primero por capacidades (acciones y estados) y cae a bloques si la referencia no las define. ${NAV_PURPOSE}`,
3034
3055
  navNodeFields(true))
3035
3056
 
3036
3057
  server.registerTool(
@@ -3075,18 +3096,19 @@ export function createServer() {
3075
3096
  server.registerTool(
3076
3097
  'upsert_design_nav',
3077
3098
  {
3078
- description: `Carga en lote (hasta 300 nodos) la navegación de diseño: es el camino para cargar el árbol completo de una plataforma leyendo su código (rutas del router, tab bar, sidebar, pantallas y sus bloques). ${NAV_PURPOSE} En vez de parentId cada nodo lleva parentKey (la key del padre, que puede venir en el mismo lote; sin parentKey = raíz). Un nodo existente se identifica por (platform, key) o, si no hay key, por (platform, parentId, name): se actualiza, si no se crea; nada se borra. Ejemplo: [{"platform":"web","kind":"section","name":"Ajustes","key":"settings","route":"/settings"},{"platform":"web","kind":"screen","name":"General","key":"settings.general","parentKey":"settings","route":"/settings/general","blocks":[{"name":"Datos del workspace","description":"Nombre y logo"},{"name":"Zona de peligro"}],"sourceRef":"src/pages/Settings/General.jsx"}]. Usa la misma key para la misma pantalla en web, iOS y Android. Devuelve { created, updated, items }.`,
3099
+ description: `Carga en lote (hasta 300 nodos) la navegación de diseño: es el camino para cargar el árbol completo de una plataforma leyendo su código (rutas del router, tab bar, sidebar, pantallas y sus bloques). ${NAV_PURPOSE} En vez de parentId cada nodo lleva parentKey (la key del padre, que puede venir en el mismo lote; sin parentKey = raíz). Un nodo existente se identifica por (platform, key) o, si no hay key, por (platform, parentId, name): se actualiza, si no se crea; nada se borra. "actions" (convención entidad.verbo, ej. task.create; la misma acción = la MISMA key en web, iOS y Android, es lo que permite compararlas) y "states" (recomendados: loading, empty, error, readonly, no-access, offline, refreshing, search-empty) se MEZCLAN por key al actualizar un nodo existente (update_design_nav_node reemplaza el array completo). La paridad compara por capacidades y cae a bloques si la referencia no las define. Ejemplo: [{"platform":"web","kind":"section","name":"Ajustes","key":"settings","route":"/settings"},{"platform":"web","kind":"screen","name":"General","key":"settings.general","parentKey":"settings","route":"/settings/general","blocks":[{"name":"Datos del workspace","description":"Nombre y logo"},{"name":"Zona de peligro"}],"actions":[{"key":"workspace.rename","name":"Renombrar workspace","roles":["owner"]}],"states":[{"key":"loading"},{"key":"readonly"}]}]. Usa la misma key para la misma pantalla en web, iOS y Android. Devuelve { created, updated, items }; por defecto compact=true y items trae solo {id,name,key?,platform?,action}, con compact=false devuelve las filas completas.`,
3079
3100
  inputSchema: {
3080
3101
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
3102
+ compact: z.boolean().optional().describe('true (default) = el lote devuelve items resumidos {id,name,key?,platform?,action}; false = filas completas'),
3081
3103
  nodes: z.array(z.object({
3082
3104
  ...navBulkBase,
3083
3105
  parentKey: z.string().optional().describe('Key del nodo padre (en este lote o ya existente); ausente = raíz'),
3084
3106
  })).min(1).max(300).describe('Nodos a crear/actualizar (1-300)'),
3085
3107
  },
3086
3108
  },
3087
- async ({ projectId, nodes }) => {
3109
+ async ({ projectId, nodes, compact }) => {
3088
3110
  try {
3089
- return textResult(await apiPost(config, '/v1/design-nav/bulk', { projectId: resolveProjectId(projectId), nodes }))
3111
+ return textResult(await apiPost(config, '/v1/design-nav/bulk', { projectId: resolveProjectId(projectId), nodes, compact: compact ?? true }))
3090
3112
  } catch (err) {
3091
3113
  return errorResult(err)
3092
3114
  }
@@ -3158,15 +3180,16 @@ export function createServer() {
3158
3180
  server.registerTool(
3159
3181
  'upsert_design_components',
3160
3182
  {
3161
- description: `Carga en lote (hasta 200) componentes de diseño: es el camino para catalogar los componentes reales leyendo el código de cada plataforma (web, iOS y Android) y llenar platforms[].sourceRef con el archivo y símbolo de cada una. ${COMP_PURPOSE} Cada componente se identifica por name (sin distinguir mayúsculas): si existe se actualiza, si no se crea; nada se borra. Usa el mismo name para el mismo componente en las tres plataformas. Devuelve { created, updated, items }.`,
3183
+ description: `Carga en lote (hasta 200) componentes de diseño: es el camino para catalogar los componentes reales leyendo el código de cada plataforma (web, iOS y Android) y llenar platforms[].sourceRef con el archivo y símbolo de cada una. ${COMP_PURPOSE} Cada componente se identifica por name (sin distinguir mayúsculas): si existe se crea o se MEZCLA, nada se borra. Cargar el MISMO name desde otra plataforma mezcla en vez de reemplazar: platforms se fusiona por plataforma, tokens/usedIn/states se unen y variants se combinan por nombre; para reemplazar de verdad usa update_design_component. Usa el mismo name para el mismo componente en las tres plataformas. Devuelve { created, updated, items, warnings }: warnings ([{name,field,value}], siempre presente) avisa de tokens o usedIn que no existen, corrígelos. Por defecto compact=true y items trae solo {id,name,key?,platform?,action}; con compact=false devuelve las filas completas.`,
3162
3184
  inputSchema: {
3163
3185
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
3186
+ compact: z.boolean().optional().describe('true (default) = el lote devuelve items resumidos {id,name,key?,platform?,action}; false = filas completas'),
3164
3187
  components: z.array(z.object(designComponentFields(false))).min(1).max(200).describe('Componentes a crear/actualizar (1-200)'),
3165
3188
  },
3166
3189
  },
3167
- async ({ projectId, components }) => {
3190
+ async ({ projectId, components, compact }) => {
3168
3191
  try {
3169
- return textResult(await apiPost(config, '/v1/design-components/bulk', { projectId: resolveProjectId(projectId), components }))
3192
+ return textResult(await apiPost(config, '/v1/design-components/bulk', { projectId: resolveProjectId(projectId), components, compact: compact ?? true }))
3170
3193
  } catch (err) {
3171
3194
  return errorResult(err)
3172
3195
  }
@@ -3178,17 +3201,18 @@ export function createServer() {
3178
3201
  server.registerTool(
3179
3202
  'get_design_parity',
3180
3203
  {
3181
- description: 'Matriz de paridad de diseño del proyecto: una fila por pantalla/componente (key, name, kind, route) y, por plataforma (web, ios, android), una celda con status (same | different | missing | not_applicable), source (auto | manual), nodeId, path, note, taskId y diff. Devuelve además summary (conteos por plataforma y total) y gaps (los huecos: [{key,name,platform,status}]). `ref` es la plataforma de referencia (default web). "distinto" (different) sale de comparar kind y bloques del nodo contra la plataforma de referencia; "falta" (missing) de que no exista un nodo con esa key en esa plataforma. Flujo para cerrar huecos: cada hueco (different | missing) puede convertirse en una tarea hermana con create_task (scope de esa plataforma y parityOf cuando aplique) y luego enlazarse a su celda con set_design_parity_cell(taskId=folio). Si una celda automática es incorrecta (ej. no aplica), fíjala a mano con set_design_parity_cell. Con soloHuecos:true devuelve solo { summary, gaps } para ahorrar tokens.',
3204
+ description: 'Matriz de paridad de diseño del proyecto: una fila por pantalla/componente (key, name, kind, route) y, por plataforma (web, ios, android), una celda con status (same | different | missing | not_applicable), source (auto | manual), nodeId, path, note, taskId y diff. Cuando hay diff, cada celda suma además: basis ("capabilities" si se comparó por acciones y estados, "blocks" si la referencia no los define y se cayó a bloques), coverage, actionsMissing (acciones de la referencia que faltan aquí), actionsOnlyHere, statesMissing y statesOnlyHere; la raíz suma capabilitiesNodes. Devuelve además summary (conteos por plataforma y total) y gaps (los huecos: [{key,name,platform,status}]). `ref` es la plataforma de referencia (default web). "distinto" (different) sale de comparar kind y bloques del nodo contra la plataforma de referencia; "falta" (missing) de que no exista un nodo con esa key en esa plataforma. Flujo para cerrar huecos: cada hueco (different | missing) puede convertirse en una tarea hermana con create_task (scope de esa plataforma y parityOf cuando aplique) y luego enlazarse a su celda con set_design_parity_cell(taskId=folio). Si una celda automática es incorrecta (ej. no aplica), fíjala a mano con set_design_parity_cell. "Igual" (same) significa mismo tipo (kind) y similitud de bloques >= threshold (0..1, default 0.6); cada diff trae su similarity y la respuesta el threshold usado: sube el umbral para ser más estricto, bájalo para tolerar diferencias. Con soloHuecos:true devuelve solo { summary, gaps, threshold } para ahorrar tokens.',
3182
3205
  inputSchema: {
3183
3206
  projectId: z.string().optional().describe(PROJECT_ID_DESC),
3184
3207
  ref: z.enum(PARITY_PLATFORMS).optional().describe('Plataforma de referencia: web | ios | android (default web)'),
3185
- soloHuecos: z.boolean().optional().describe('true = devuelve solo summary y gaps (sin las filas), para ahorrar tokens'),
3208
+ soloHuecos: z.boolean().optional().describe('true = devuelve solo summary, gaps y threshold (sin las filas), para ahorrar tokens'),
3209
+ threshold: z.number().min(0).max(1).optional().describe('Umbral de similitud de bloques (0..1, default 0.6) para considerar "same"'),
3186
3210
  },
3187
3211
  },
3188
- async ({ projectId, ref, soloHuecos }) => {
3212
+ async ({ projectId, ref, soloHuecos, threshold }) => {
3189
3213
  try {
3190
- const data = await apiGet(config, '/v1/design-parity', { projectId: resolveProjectId(projectId), ref })
3191
- if (soloHuecos && data && typeof data === 'object') return textResult({ summary: data.summary, gaps: data.gaps })
3214
+ const data = await apiGet(config, '/v1/design-parity', { projectId: resolveProjectId(projectId), ref, threshold })
3215
+ if (soloHuecos && data && typeof data === 'object') return textResult({ summary: data.summary, gaps: data.gaps, threshold: data.threshold })
3192
3216
  return textResult(data)
3193
3217
  } catch (err) {
3194
3218
  return errorResult(err)