orquestra-mcp 1.44.0 → 1.46.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "orquestra-mcp",
3
- "version": "1.44.0",
3
+ "version": "1.46.0",
4
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",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,7 +15,7 @@
15
15
  "engines": {
16
16
  "node": ">=18"
17
17
  },
18
- "homepage": "https://orquestra.me/mcp",
18
+ "homepage": "https://www.orquestra.me/mcp",
19
19
  "keywords": [
20
20
  "mcp",
21
21
  "model-context-protocol",
@@ -526,7 +526,7 @@ export const PLATFORM_PLAYBOOKS = {
526
526
  {
527
527
  title: 'Medir y corregir Core Web Vitals en PageSpeed Insights (móvil)',
528
528
  description: 'Correr PageSpeed Insights sobre la home y las páginas clave. Objetivo: LCP < 2.5s, CLS < 0.1, INP < 200ms. Atacar primero imágenes sin comprimir, fuentes bloqueantes y JS sin dividir.',
529
- type: 'tech', priority: 'medium', tags: ['seo', 'web', 'performance'], offsetDays: 5,
529
+ type: 'tech', priority: 'medium', tags: ['seo', 'web', 'performance'], offsetDays: 5, checkKey: 'core_web_vitals',
530
530
  },
531
531
  {
532
532
  title: 'Fijar dominio canónico y redirecciones 301 (www / http → https)',
package/src/server.mjs CHANGED
@@ -337,6 +337,26 @@ export function createServer() {
337
337
  {}
338
338
  )
339
339
 
340
+ // PageSpeed Insights por distribución web (ORQ-377). La última medición
341
+ // también viene en list_distributions (campo `pagespeed` de cada una).
342
+ registerRead(
343
+ 'get_pagespeed',
344
+ '/v1/pagespeed',
345
+ 'Historial de mediciones de PageSpeed Insights (móvil) de una distribución web, más reciente primero: scores de Performance/Accesibilidad/Buenas prácticas/SEO, métricas de laboratorio (LCP, CLS, TBT), Core Web Vitals de usuarios reales (CrUX: LCP, INP, CLS con categoría FAST/AVERAGE/SLOW; null si el sitio no tiene tráfico suficiente), oportunidades con ahorro estimado y auditorías que fallan. Se mide solo cada lunes; para una medición nueva usa measure_pagespeed.',
346
+ {
347
+ distributionId: z.string().describe('Distribución type="web" con domain configurado (ver list_distributions)'),
348
+ limit: z.number().int().min(1).max(100).optional().describe('Cuántas mediciones traer (default 20)'),
349
+ },
350
+ (args, projectId) => ({ projectId, distributionId: args.distributionId, limit: args.limit })
351
+ )
352
+
353
+ registerCreate(
354
+ 'measure_pagespeed',
355
+ '/v1/pagespeed',
356
+ 'Pide una medición nueva de PageSpeed Insights (móvil) para una distribución web con dominio. Corre en segundo plano (10-40 s): responde de inmediato con status "running"; el resultado aparece después en el campo `pagespeed` de la distribución (list_distributions) y en get_pagespeed. 409 si ya hay una en curso.',
357
+ { distributionId: z.string().describe('Distribución type="web" con domain configurado') }
358
+ )
359
+
340
360
  registerRead(
341
361
  'get_infra_status',
342
362
  '/v1/infra',
@@ -2637,12 +2657,12 @@ export function createServer() {
2637
2657
 
2638
2658
  registerRead('list_agents', '/v1/agents',
2639
2659
  'Lista los agentes (kind+máquina+stack+ruta local) configurados en un proyecto. Úsala antes de create_agent o de create_agent_run con stack, para saber si ya existe uno.',
2640
- { kind: z.enum(AGENT_KINDS).optional().describe('Filtra por kind: "qa" (lint/test/build/E2E), "security" (npm audit + gitleaks), "performance" (build size + Lighthouse) o "screenshots" (captura de pantallas para el checklist de publicación, solo ios/android)') },
2660
+ { kind: z.enum(AGENT_KINDS).optional().describe('Filtra por kind: "qa" (lint/test/build/E2E), "security" (npm audit + gitleaks), "performance" (build size + Lighthouse: performance/accesibilidad/buenas prácticas/SEO + LCP/CLS/TBT) o "screenshots" (captura de pantallas para el checklist de publicación, solo ios/android)') },
2641
2661
  (args, projectId) => ({ projectId, ...(args.kind ? { kind: args.kind } : {}) }))
2642
2662
 
2643
2663
  registerCreate('create_agent', '/v1/agents', 'Crea un agente: asocia un kind ("qa" | "security" | "performance" | "screenshots") a una máquina (runner, ver Workspace → Máquinas), un stack de este proyecto y una ruta de checkout en ESA máquina. kind="performance" además necesita perfUrl.', {
2644
2664
  name: z.string().describe('Nombre a mostrar, ej. "Agente web"'),
2645
- kind: z.enum(AGENT_KINDS).default('qa').describe('"qa" (lint/test/build/E2E), "security" (npm audit + gitleaks), "performance" (build size + Lighthouse) o "screenshots" (captura de pantallas para tiendas, solo stack ios/android) -- default "qa"'),
2665
+ kind: z.enum(AGENT_KINDS).default('qa').describe('"qa" (lint/test/build/E2E), "security" (npm audit + gitleaks), "performance" (build size + Lighthouse: performance/accesibilidad/buenas prácticas/SEO + LCP/CLS/TBT) o "screenshots" (captura de pantallas para tiendas, solo stack ios/android) -- default "qa"'),
2646
2666
  runnerId: z.string().describe('ID de la máquina emparejada -- ver list_agents o Workspace → Máquinas'),
2647
2667
  stack: z.enum(AGENT_STACKS).optional().describe('Requerido para los 4 kinds. security/performance solo tienen lógica real para "web" hoy; screenshots solo para "ios"/"android"'),
2648
2668
  localPath: z.string().optional().describe('Requerido para los 4 kinds -- ruta absoluta del checkout en esa máquina'),
@@ -2657,7 +2677,14 @@ export function createServer() {
2657
2677
  plannedRunAt: z.string().optional().describe('YYYY-MM-DD -- recordatorio manual de "próxima corrida planeada". Ningún kind tiene scheduling real (todo trigger es manual/push): esto NO dispara ninguna corrida sola, solo alimenta el calendario global.'),
2658
2678
  perfUrl: z.string().optional().describe('Requerido si kind="performance" -- URL viva (ej. staging/prod) contra la que corre Lighthouse'),
2659
2679
  maxBundleSizeKb: z.number().optional().describe('Específico de kind="performance" -- umbral en KB para el paso de tamaño de build (default 5000)'),
2660
- minPerfScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de Lighthouse (0-100) para pasar (default 50)'),
2680
+ minPerfScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de Lighthouse en Performance (0-100) para pasar (default 50)'),
2681
+ perfUrls: z.array(z.string()).optional().describe('Específico de kind="performance" -- URLs ADICIONALES a perfUrl (ej. /precios, /blog): Lighthouse corre un paso por cada una'),
2682
+ minAccessibilityScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de Accesibilidad (0-100, default 90)'),
2683
+ minBestPracticesScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de Buenas prácticas (0-100, default 90)'),
2684
+ minSeoScore: z.number().optional().describe('Específico de kind="performance" -- score mínimo de SEO (0-100, default 90)'),
2685
+ maxLcpMs: z.number().optional().describe('Específico de kind="performance" -- LCP máximo en ms (default 2500, umbral "bueno" de Google)'),
2686
+ maxCls: z.number().optional().describe('Específico de kind="performance" -- CLS máximo (default 0.1)'),
2687
+ maxTbtMs: z.number().optional().describe('Específico de kind="performance" -- TBT máximo en ms (default 200, proxy de laboratorio de INP)'),
2661
2688
  })
2662
2689
 
2663
2690
  registerUpdate('update_agent', '/v1/agents', 'Edita un agente existente.', {
@@ -2678,6 +2705,13 @@ export function createServer() {
2678
2705
  perfUrl: z.string().optional().describe('Ver create_agent -- específico de kind="performance"'),
2679
2706
  maxBundleSizeKb: z.number().optional(),
2680
2707
  minPerfScore: z.number().optional(),
2708
+ perfUrls: z.array(z.string()).optional().describe('Ver create_agent -- reemplaza la lista completa, [] la vacía'),
2709
+ minAccessibilityScore: z.number().optional(),
2710
+ minBestPracticesScore: z.number().optional(),
2711
+ minSeoScore: z.number().optional(),
2712
+ maxLcpMs: z.number().optional(),
2713
+ maxCls: z.number().optional(),
2714
+ maxTbtMs: z.number().optional(),
2681
2715
  })
2682
2716
 
2683
2717
  registerCreate('create_agent_run', '/v1/agent-runs', 'Dispara una corrida de agente (qa: lint/tests/build/E2E; security: npm audit + gitleaks; performance: build size + Lighthouse; screenshots: captura de pantallas para el checklist de publicación) en la rama indicada. Necesita al menos un agente activo para el stack (o falla explícito -- ver list_agents/create_agent). La corrida queda "queued" -- nunca asumas que pasó, confirma con get_agent_run.', {
package/src/setup.mjs CHANGED
@@ -78,22 +78,72 @@ export async function runSetup() {
78
78
  const agentLabelAnswer = await prompt('¿Cómo se llama este agente/herramienta? [Claude Code]: ')
79
79
  const agentLabel = agentLabelAnswer.trim() || 'Claude Code'
80
80
 
81
- const res = await fetch(`${apiBase}/v1/bootstrap`, {
82
- method: 'POST',
83
- headers: { Authorization: `Bearer ${idToken}`, 'Content-Type': 'application/json' },
84
- body: JSON.stringify({ agentLabel }),
85
- })
86
- const body = await res.json()
87
- if (!res.ok) throw new Error(body?.error || `HTTP ${res.status}`)
81
+ // askIfMissing: si no hay workspace la API no crea nada solo -- avisa
82
+ // (needsWorkspace) y aquí se le pide el nombre al usuario.
83
+ const callBootstrap = async (extra = {}) => {
84
+ const res = await fetch(`${apiBase}/v1/bootstrap`, {
85
+ method: 'POST',
86
+ headers: { Authorization: `Bearer ${idToken}`, 'Content-Type': 'application/json' },
87
+ body: JSON.stringify({ agentLabel, askIfMissing: true, ...extra }),
88
+ })
89
+ const body = await res.json()
90
+ if (!res.ok) throw new Error(body?.error || `HTTP ${res.status}`)
91
+ return body
92
+ }
93
+ const askName = async (question) => {
94
+ for (;;) {
95
+ const name = (await prompt(question)).trim()
96
+ if (name) return name
97
+ console.log('El nombre no puede estar vacío.')
98
+ }
99
+ }
100
+
101
+ let body = await callBootstrap()
102
+ if (body.needsWorkspaceChoice) {
103
+ // Miembro de varios workspaces: elegir aquí no cambia el workspace
104
+ // actual de la web, solo a cuál queda conectada esta carpeta.
105
+ const { workspaces, currentWorkspaceId } = body
106
+ console.log('\nPerteneces a varios workspaces. ¿En cuál quieres trabajar desde esta carpeta?')
107
+ workspaces.forEach((w, i) => console.log(` ${i + 1}. ${w.name}${w.id === currentWorkspaceId ? ' (el que tienes abierto en la web)' : ''}`))
108
+ const currentIdx = workspaces.findIndex(w => w.id === currentWorkspaceId) + 1
109
+ let idx
110
+ for (;;) {
111
+ const answer = (await prompt(currentIdx ? `Número [Enter para usar "${workspaces[currentIdx - 1].name}"]: ` : 'Número: ')).trim()
112
+ idx = answer === '' && currentIdx ? currentIdx : parseInt(answer, 10)
113
+ if (idx >= 1 && idx <= workspaces.length) break
114
+ console.log('Selección inválida.')
115
+ }
116
+ body = await callBootstrap({ workspaceId: workspaces[idx - 1].id })
117
+ } else if (body.needsWorkspace) {
118
+ console.log('\nTodavía no tienes un workspace en Orquestra. Primero vamos a crearlo')
119
+ console.log('(agrupa todos tus proyectos: tu empresa, tu equipo o tu nombre si trabajas solo).')
120
+ const workspaceName = await askName('Nombre del workspace: ')
121
+ body = await callBootstrap({ workspaceName })
122
+ console.log(`Workspace "${workspaceName}" creado.`)
123
+ }
88
124
 
89
- const { workspaceId, token, created, projects } = body
125
+ const { workspaceId, token, projects } = body
126
+ // La API de producción anterior a ORQ-388 no manda workspaceName.
127
+ const wsLabel = body.workspaceName ? ` del workspace "${body.workspaceName}"` : ''
90
128
  let defaultProjectId
91
129
 
92
- if (created) {
93
- defaultProjectId = body.projectId
94
- console.log(`Se creó un workspace y un proyecto default para ti ("${projects[0].name}") -- puedes renombrarlos cuando quieras desde orquestra-web.`)
130
+ const createProject = async (name) => {
131
+ const res2 = await fetch(`${apiBase}/v1/projects?workspaceId=${workspaceId}`, {
132
+ method: 'POST',
133
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
134
+ body: JSON.stringify({ name }),
135
+ })
136
+ const newProject = await res2.json()
137
+ if (!res2.ok) throw new Error(newProject?.error || `HTTP ${res2.status}`)
138
+ console.log(`Proyecto "${newProject.name}"${wsLabel} creado y configurado como default.`)
139
+ return newProject.id
140
+ }
141
+
142
+ if (projects.length === 0) {
143
+ console.log(`\nAhora crea el proyecto${wsLabel} que este MCP va a usar (puedes agregar más después).`)
144
+ defaultProjectId = await createProject(await askName('Nombre del proyecto: '))
95
145
  } else {
96
- console.log('\nElige el proyecto que este MCP va a usar por default:')
146
+ console.log(`\nElige el proyecto${wsLabel} que este MCP va a usar por default:`)
97
147
  console.log(' 0. (crear un proyecto nuevo)')
98
148
  projects.forEach((p, i) => console.log(` ${i + 1}. ${p.name}`))
99
149
  const singleDefaultIdx = projects.length === 1 ? 1 : null
@@ -105,20 +155,10 @@ export async function runSetup() {
105
155
  if (Number.isNaN(idx) || idx < 0 || idx > projects.length) throw new Error('Selección inválida')
106
156
 
107
157
  if (idx === 0) {
108
- const name = await prompt('Nombre del proyecto nuevo: ')
109
- if (!name.trim()) throw new Error('El nombre no puede estar vacío')
110
- const res2 = await fetch(`${apiBase}/v1/projects?workspaceId=${workspaceId}`, {
111
- method: 'POST',
112
- headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' },
113
- body: JSON.stringify({ name: name.trim() }),
114
- })
115
- const newProject = await res2.json()
116
- if (!res2.ok) throw new Error(newProject?.error || `HTTP ${res2.status}`)
117
- defaultProjectId = newProject.id
118
- console.log(`Proyecto "${newProject.name}" creado y configurado como default.`)
158
+ defaultProjectId = await createProject(await askName('Nombre del proyecto nuevo: '))
119
159
  } else {
120
160
  defaultProjectId = projects[idx - 1].id
121
- console.log(`Usando tu proyecto "${projects[idx - 1].name}" como default.`)
161
+ console.log(`Usando tu proyecto "${projects[idx - 1].name}"${wsLabel} como default.`)
122
162
  }
123
163
  }
124
164
 
@@ -141,6 +181,6 @@ export async function runSetup() {
141
181
  console.log(' Claude Desktop -- agrega esto a tu claude_desktop_config.json:')
142
182
  console.log(' { "mcpServers": { "orquestra": { "command": "npx", "args": ["orquestra-mcp"] } } }\n')
143
183
  console.log(' ¿Usas Cursor, ChatGPT/Codex o Gemini CLI? La config de cada uno está en:')
144
- console.log(' https://orquestra.me/mcp#instalacion\n')
145
- console.log('Docs completas y las 35 tools disponibles: https://orquestra.me/mcp')
184
+ console.log(' https://www.orquestra.me/mcp#instalacion\n')
185
+ console.log('Docs completas y todas las tools disponibles: https://www.orquestra.me/mcp')
146
186
  }