orquestra-mcp 1.72.0 → 1.74.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 +1 -1
- package/src/api.mjs +2 -2
- package/src/server.mjs +213 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orquestra-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.74.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/api.mjs
CHANGED
|
@@ -115,9 +115,9 @@ export async function apiDeletePath(config, path, { projectId, ...query }) {
|
|
|
115
115
|
// es la autorización completa. Plazo más largo (sube bytes); un PUT a la
|
|
116
116
|
// misma key es idempotente, pero igual no se reintenta para no duplicar
|
|
117
117
|
// tiempos largos de subida.
|
|
118
|
-
export async function putToPresignedUrl(uploadUrl, buffer, contentType) {
|
|
118
|
+
export async function putToPresignedUrl(uploadUrl, buffer, contentType, headers) {
|
|
119
119
|
try {
|
|
120
|
-
await request(uploadUrl, { method: 'PUT', headers: { 'Content-Type': contentType }, body: buffer }, { timeoutMs: 120_000, retries: 0 })
|
|
120
|
+
await request(uploadUrl, { method: 'PUT', headers: { 'Content-Type': contentType, ...(headers || {}) }, body: buffer }, { timeoutMs: 120_000, retries: 0 })
|
|
121
121
|
} catch (err) {
|
|
122
122
|
throw new Error(`Falló la subida a S3: ${err.status ? `HTTP ${err.status}` : err.message}`)
|
|
123
123
|
}
|
package/src/server.mjs
CHANGED
|
@@ -3019,6 +3019,14 @@ export function createServer() {
|
|
|
3019
3019
|
blocks: navBlocksSchema.optional(),
|
|
3020
3020
|
actions: navActionsSchema.optional(),
|
|
3021
3021
|
states: navStatesSchema.optional(),
|
|
3022
|
+
screenshots: z.array(z.object({
|
|
3023
|
+
key: z.string().describe('Key de S3 (de add_design_screenshot)'),
|
|
3024
|
+
caption: z.string().max(200).optional(),
|
|
3025
|
+
state: z.string().optional().describe('Estado que muestra'),
|
|
3026
|
+
width: z.number().int().positive().optional(),
|
|
3027
|
+
height: z.number().int().positive().optional(),
|
|
3028
|
+
takenAt: z.number().optional().describe('ms epoch'),
|
|
3029
|
+
})).max(12).optional().describe('Capturas (máx. 12); usa add_design_screenshot. PATCH reemplaza la lista'),
|
|
3022
3030
|
sourceRef: z.string().optional().describe('Dónde vive en el código, ej: archivo y símbolo'),
|
|
3023
3031
|
}
|
|
3024
3032
|
if (optional) { f.name = f.name.optional(); f.platform = f.platform.optional(); f.kind = f.kind.optional() }
|
|
@@ -3092,7 +3100,7 @@ export function createServer() {
|
|
|
3092
3100
|
}
|
|
3093
3101
|
)
|
|
3094
3102
|
|
|
3095
|
-
const { parentId: _navParentId, ...navBulkBase } = navNodeFields(false)
|
|
3103
|
+
const { parentId: _navParentId, screenshots: _navShots, ...navBulkBase } = navNodeFields(false) // capturas: solo con add/remove_design_screenshot
|
|
3096
3104
|
server.registerTool(
|
|
3097
3105
|
'upsert_design_nav',
|
|
3098
3106
|
{
|
|
@@ -3115,6 +3123,76 @@ export function createServer() {
|
|
|
3115
3123
|
}
|
|
3116
3124
|
)
|
|
3117
3125
|
|
|
3126
|
+
// ═══ CAPTURAS POR PANTALLA (ORQ-573) ═══════════════════════════════════════
|
|
3127
|
+
const SHOT_MIME_BY_EXT = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp' }
|
|
3128
|
+
const SHOT_MAX_BYTES = 5 * 1024 * 1024
|
|
3129
|
+
const SHOT_NOTE = 'Las capturas son INTERNAS del equipo (se guardan en un bucket privado y se ven con URL firmada de 1 h). No uses capturas del workspace real si van a usarse en marketing o videos: arma un usuario demo.'
|
|
3130
|
+
const findNavNode = async (pid, platform, key) => {
|
|
3131
|
+
const list = await apiGet(config, '/v1/design-nav', { projectId: pid, platform })
|
|
3132
|
+
const node = (list.items || []).find(n => n.key === key)
|
|
3133
|
+
if (!node) throw new Error(`No hay un nodo con key "${key}" en ${platform}; revisa list_design_nav o créalo con upsert_design_nav`)
|
|
3134
|
+
return node
|
|
3135
|
+
}
|
|
3136
|
+
server.registerTool(
|
|
3137
|
+
'add_design_screenshot',
|
|
3138
|
+
{
|
|
3139
|
+
description: `Sube una captura de pantalla (imagen local png/jpg/webp, máx. 5 MB) y la agrega al nodo de navegación de diseño (platform + key) para ver cómo se ve esa pantalla en cada plataforma y compararlas lado a lado. Valida extensión y tamaño ANTES de subir, pide la URL firmada, hace el PUT a S3 y guarda la clave en el nodo MEZCLANDO por key (no pisa las otras capturas; máx. 12 por nodo). "state" marca qué estado muestra (empty, loading, error...). ${SHOT_NOTE} Devuelve { key, name, screenshots }.`,
|
|
3140
|
+
inputSchema: {
|
|
3141
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
3142
|
+
platform: z.enum(NAV_PLATFORMS).describe('Plataforma del nodo: web | ios | android'),
|
|
3143
|
+
key: z.string().describe('Key del nodo de navegación (ej: "tasks", "settings.general")'),
|
|
3144
|
+
filePath: z.string().describe('Ruta local a la imagen (png/jpg/jpeg/webp, máx. 5 MB)'),
|
|
3145
|
+
caption: z.string().max(200).optional().describe('Pie de la captura (máx. 200)'),
|
|
3146
|
+
state: z.string().optional().describe('Estado que muestra, ej: empty, loading, error'),
|
|
3147
|
+
},
|
|
3148
|
+
},
|
|
3149
|
+
async ({ projectId, platform, key, filePath, caption, state }) => {
|
|
3150
|
+
try {
|
|
3151
|
+
const pid = resolveProjectId(projectId)
|
|
3152
|
+
const ext = path.extname(filePath).slice(1).toLowerCase()
|
|
3153
|
+
const mime = SHOT_MIME_BY_EXT[ext]
|
|
3154
|
+
if (!mime) throw new Error('Solo imágenes png, jpg/jpeg o webp')
|
|
3155
|
+
const stat = statSync(filePath)
|
|
3156
|
+
if (!stat.isFile()) throw new Error('filePath no es un archivo')
|
|
3157
|
+
if (stat.size > SHOT_MAX_BYTES) throw new Error(`La imagen pesa ${(stat.size / 1048576).toFixed(1)} MB; el máximo es 5 MB`)
|
|
3158
|
+
const node = await findNavNode(pid, platform, key)
|
|
3159
|
+
const name = path.basename(filePath)
|
|
3160
|
+
const { uploadUrl, key: shotKey, headers } = await apiPost(config, '/v1/design-assets/presign', { projectId: pid, nodeId: node.id, filename: name, contentType: mime, size: stat.size })
|
|
3161
|
+
await putToPresignedUrl(uploadUrl, readFileSync(filePath), mime, headers)
|
|
3162
|
+
const shot = { key: shotKey, takenAt: Date.now(), ...(caption ? { caption } : {}), ...(state ? { state } : {}) }
|
|
3163
|
+
const res = await apiPost(config, '/v1/design-nav/bulk', { projectId: pid, compact: false, nodes: [{ platform, kind: node.kind, name: node.name, key, screenshots: [shot] }] })
|
|
3164
|
+
const updated = res.items?.[0]
|
|
3165
|
+
return textResult({ key: shotKey, name, nodeId: node.id, screenshots: updated?.screenshots ?? [shot] })
|
|
3166
|
+
} catch (err) {
|
|
3167
|
+
return errorResult(err)
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
)
|
|
3171
|
+
server.registerTool(
|
|
3172
|
+
'remove_design_screenshot',
|
|
3173
|
+
{
|
|
3174
|
+
description: `Quita una captura de un nodo de navegación de diseño (platform + key del nodo + screenshotKey de la captura, la que devolvió add_design_screenshot o list_design_nav). La API también intenta borrar el archivo de S3. ${SHOT_NOTE}`,
|
|
3175
|
+
inputSchema: {
|
|
3176
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
3177
|
+
platform: z.enum(NAV_PLATFORMS).describe('Plataforma del nodo: web | ios | android'),
|
|
3178
|
+
key: z.string().describe('Key del nodo de navegación'),
|
|
3179
|
+
screenshotKey: z.string().describe('Key de S3 de la captura a quitar'),
|
|
3180
|
+
},
|
|
3181
|
+
},
|
|
3182
|
+
async ({ projectId, platform, key, screenshotKey }) => {
|
|
3183
|
+
try {
|
|
3184
|
+
const pid = resolveProjectId(projectId)
|
|
3185
|
+
const node = await findNavNode(pid, platform, key)
|
|
3186
|
+
const current = node.screenshots || []
|
|
3187
|
+
if (!current.some(s => s.key === screenshotKey)) throw new Error(`El nodo "${key}" no tiene una captura con key "${screenshotKey}"`)
|
|
3188
|
+
const data = await apiPatch(config, '/v1/design-nav', { projectId: pid, id: node.id, screenshots: current.filter(s => s.key !== screenshotKey) })
|
|
3189
|
+
return textResult({ removed: screenshotKey, nodeId: node.id, screenshots: data.screenshots ?? [] })
|
|
3190
|
+
} catch (err) {
|
|
3191
|
+
return errorResult(err)
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
)
|
|
3195
|
+
|
|
3118
3196
|
// ═══ COMPONENTES DE DISEÑO (ORQ-562) ═══════════════════════════════════════
|
|
3119
3197
|
const COMP_PLATFORMS = ['web', 'ios', 'android']
|
|
3120
3198
|
const COMP_PURPOSE = 'Cataloga los componentes de UI del proyecto (botones, tarjetas, modales, campos...) con sus variantes, estados y en qué plataformas existen, para comparar paridad entre web, iOS y Android. `tokens` enlaza con los nombres de las tools de design tokens (list_design_tokens) y `usedIn` con las keys de nodos de navegación (list_design_nav).'
|
|
@@ -3223,7 +3301,7 @@ export function createServer() {
|
|
|
3223
3301
|
server.registerTool(
|
|
3224
3302
|
'set_design_parity_cell',
|
|
3225
3303
|
{
|
|
3226
|
-
description: 'Fija a mano una celda de la matriz de paridad de diseño (get_design_parity): el override manual gana sobre el cálculo automático. Úsala para marcar not_applicable (la pantalla no existe en esa plataforma a propósito), corregir un estado, dejar una nota o enlazar la tarea hermana que cierra el hueco (taskId = folio, ej. ORQ-123). Con status:null quita el override y la celda vuelve al cálculo automático.',
|
|
3304
|
+
description: 'Fija a mano una celda de la matriz de paridad de diseño (get_design_parity): el override manual gana sobre el cálculo automático. Úsala para marcar not_applicable (la pantalla no existe en esa plataforma a propósito), corregir un estado, dejar una nota o enlazar la tarea hermana que cierra el hueco (taskId = folio, ej. ORQ-123). Con status:null y SIN note ni taskId quita el override y la celda vuelve al cálculo automático; con status:null y note o taskId conserva la nota y el folio sin fijar estado manual (la celda sigue automática). Regla: fijar status:"not_applicable" EXIGE un `reason` (motivo) no vacío; para los demás estados es opcional. `adr` enlaza la celda a un doc del proyecto (normalmente un ADR que documenta la decisión): pásale el id del doc (obtenlo con search types=["doc"] o listando los docs); "" lo quita. get_design_parity devuelve en la celda reason y decision {docId,title,type}.',
|
|
3227
3305
|
inputSchema: {
|
|
3228
3306
|
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
3229
3307
|
key: z.string().min(1).describe('Key de la fila (ver get_design_parity)'),
|
|
@@ -3231,11 +3309,142 @@ export function createServer() {
|
|
|
3231
3309
|
status: z.enum(['same', 'different', 'missing', 'not_applicable']).nullable().describe('Estado manual; null = quitar el override'),
|
|
3232
3310
|
note: z.string().optional().describe('Nota de la celda'),
|
|
3233
3311
|
taskId: z.string().optional().describe('Folio de la tarea que cierra el hueco, ej: ORQ-123'),
|
|
3312
|
+
reason: z.string().max(500).optional().describe('Motivo de la decisión (máx. 500). Obligatorio al fijar status not_applicable'),
|
|
3313
|
+
adr: z.string().max(60).optional().describe('Id del doc (ADR) que documenta la decisión; se envía como docId. "" lo quita'),
|
|
3314
|
+
},
|
|
3315
|
+
},
|
|
3316
|
+
async ({ projectId, key, platform, status, note, taskId, reason, adr }) => {
|
|
3317
|
+
try {
|
|
3318
|
+
if (status === 'not_applicable' && !(reason && reason.trim())) {
|
|
3319
|
+
return errorResult(new Error('motivo obligatorio al fijar No aplica: pasa reason con el porqué (y opcionalmente adr con el id del doc)'))
|
|
3320
|
+
}
|
|
3321
|
+
return textResult(await apiPut(config, '/v1/design-parity/cell', { projectId: resolveProjectId(projectId), key, platform, status, note, taskId, reason, docId: adr }))
|
|
3322
|
+
} catch (err) {
|
|
3323
|
+
return errorResult(err)
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
)
|
|
3327
|
+
|
|
3328
|
+
server.registerTool(
|
|
3329
|
+
'create_parity_tasks',
|
|
3330
|
+
{
|
|
3331
|
+
description: 'Convierte huecos de la paridad de diseño en tareas. Flujo: primero get_design_parity con soloHuecos:true para ver los huecos (gaps); luego create_parity_tasks con las celdas {key, platform} que quieras cerrar. Crea UNA tarea por celda, con scope de esa plataforma y el detalle de la diferencia contra la plataforma de referencia (ref, default web; threshold para el umbral de similitud). No duplica: si la celda ya tiene folio la omite (aparece en skipped con su reason) salvo force:true; el folio nuevo se guarda en la celda. La tarea exige módulo y fecha: si no pasas moduleId/dueDate se resuelven solos (módulo de la plataforma, hoy + 14 días); priority es opcional. Responde { created: [{key,platform,taskId,id}], skipped: [{key,platform,reason}], warnings }. No la uses para cientos de celdas a la vez: máximo 50 por llamada.',
|
|
3332
|
+
inputSchema: {
|
|
3333
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
3334
|
+
cells: z.array(z.object({
|
|
3335
|
+
key: z.string().min(1).describe('Key de la fila (ver get_design_parity)'),
|
|
3336
|
+
platform: z.enum(PARITY_PLATFORMS).describe('Plataforma de la celda: web | ios | android'),
|
|
3337
|
+
})).min(1).max(50).describe('Celdas a convertir en tarea (1 a 50)'),
|
|
3338
|
+
ref: z.enum(PARITY_PLATFORMS).optional().describe('Plataforma de referencia: web | ios | android (default web)'),
|
|
3339
|
+
threshold: z.number().min(0).max(1).optional().describe('Umbral de similitud de bloques (0..1, default 0.6)'),
|
|
3340
|
+
moduleId: z.string().optional().describe('Módulo de las tareas; si falta se usa el de la plataforma'),
|
|
3341
|
+
dueDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Formato YYYY-MM-DD').optional().describe('Fecha límite YYYY-MM-DD; si falta, hoy + 14 días'),
|
|
3342
|
+
priority: z.enum(['high', 'medium', 'low']).optional().describe('Prioridad de las tareas'),
|
|
3343
|
+
force: z.boolean().optional().describe('true = crea la tarea aunque la celda ya tenga folio'),
|
|
3344
|
+
},
|
|
3345
|
+
},
|
|
3346
|
+
async ({ projectId, cells, ref, threshold, moduleId, dueDate, priority, force }) => {
|
|
3347
|
+
try {
|
|
3348
|
+
return textResult(await apiPost(config, '/v1/design-parity/tasks', { projectId: resolveProjectId(projectId), cells, ref, threshold, moduleId, dueDate, priority, force }))
|
|
3349
|
+
} catch (err) {
|
|
3350
|
+
return errorResult(err)
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
)
|
|
3354
|
+
|
|
3355
|
+
// ═══ DERIVA DE TOKENS (ORQ-574) ═══════════════════════════════════════════
|
|
3356
|
+
const DRIFT_KINDS = ['color', 'radius', 'spacing', 'typography', 'shadow']
|
|
3357
|
+
const DRIFT_STATUSES = ['open', 'ignored', 'fixed']
|
|
3358
|
+
const DRIFT_FLOW = 'Detector de deriva de tokens: valores de diseño (colores hex, radios, spacings, fuentes, sombras) escritos a mano en el código de una plataforma FUERA de la definición de tokens. FLUJO: 1) list_design_tokens para conocer los tokens de la plataforma; 2) recorre el código de UNA plataforma (web, ios o android) buscando colores hex, radios, spacings y fuentes escritos a mano; 3) compara cada valor con los tokens de esa MISMA plataforma y, si coincide, propón suggestedToken con el name del token; 4) reporta en lote con report_design_drift (hasta 200 por llamada; guarda la hora de inicio de la pasada, Date.now()); 5) al terminar una pasada COMPLETA de esa plataforma, llama a resolve_missing_design_drift en dryRun (platform + reportedBefore = hora de inicio de la pasada) para ver qué hallazgos abiertos ya no aparecen y, si es correcto, repítela con dryRun:false (se marcan fixed). NO es deriva: el archivo donde se DEFINEN los tokens (theme, tokens.css, Colors.swift, Color.kt, variables.css...), tests y fixtures, previews/storybook/SwiftUI previews, código generado, node_modules/build, ni valores dinámicos (calculados por el usuario o datos).'
|
|
3359
|
+
const driftItemSchema = z.object({
|
|
3360
|
+
platform: z.enum(PARITY_PLATFORMS).describe('Plataforma donde está el código: web | ios | android'),
|
|
3361
|
+
file: z.string().min(1).max(300).describe('Ruta RELATIVA al repo de la plataforma, ej: src/pages/Tasks.jsx'),
|
|
3362
|
+
line: z.number().int().min(1).optional().describe('Línea (>= 1); parte de la identidad del hallazgo'),
|
|
3363
|
+
kind: z.enum(DRIFT_KINDS).describe('color | radius | spacing | typography | shadow'),
|
|
3364
|
+
value: z.string().min(1).max(120).describe('Valor escrito a mano tal cual, ej: "#2F7A55", "13dp", "12px"'),
|
|
3365
|
+
context: z.string().max(200).optional().describe('Símbolo o línea de código donde aparece (≤200)'),
|
|
3366
|
+
suggestedToken: z.string().max(80).optional().describe('name de un design token EXISTENTE de esa plataforma que lo reemplazaría (≤80); si no existe la API avisa en warnings'),
|
|
3367
|
+
note: z.string().max(300).optional().describe('Nota (≤300), solo se guarda al crear'),
|
|
3368
|
+
reopen: z.boolean().optional().describe('true = reabre este hallazgo si estaba ignored o fixed (por default un reporte repetido NO los reabre)'),
|
|
3369
|
+
})
|
|
3370
|
+
|
|
3371
|
+
server.registerTool(
|
|
3372
|
+
'report_design_drift',
|
|
3373
|
+
{
|
|
3374
|
+
description: `Reporta en lote (1 a 200) hallazgos de deriva de tokens. ${DRIFT_FLOW} Cada hallazgo se identifica por (platform, file, line, value): reportar de nuevo uno existente solo refresca seenAt, suggestedToken y context y NO reabre los ignorados o corregidos (salvo reopen:true). Responde { created, updated, reopened, items (resumidos por default), warnings } donde warnings avisa de suggestedToken que no existe en los tokens. Valida todo el lote antes de escribir.`,
|
|
3375
|
+
inputSchema: {
|
|
3376
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
3377
|
+
compact: z.boolean().optional().describe('true (default) = items resumidos {id,platform,file,line?,value,status,action}; false = filas completas'),
|
|
3378
|
+
items: z.array(driftItemSchema).min(1).max(200).describe('Hallazgos a reportar (1-200)'),
|
|
3379
|
+
},
|
|
3380
|
+
},
|
|
3381
|
+
async ({ projectId, items, compact }) => {
|
|
3382
|
+
try {
|
|
3383
|
+
return textResult(await apiPost(config, '/v1/design-drift/bulk', { projectId: resolveProjectId(projectId), items, compact: compact ?? true }))
|
|
3384
|
+
} catch (err) {
|
|
3385
|
+
return errorResult(err)
|
|
3386
|
+
}
|
|
3387
|
+
}
|
|
3388
|
+
)
|
|
3389
|
+
|
|
3390
|
+
server.registerTool(
|
|
3391
|
+
'list_design_drift',
|
|
3392
|
+
{
|
|
3393
|
+
description: 'Lista los hallazgos de deriva de tokens (valores de diseño escritos a mano que deberían ser un token), ordenados por archivo y línea, con filtros por plataforma, tipo y estado. Responde { items, summary: { open, ignored, fixed, byPlatform: { web, ios, android } } }; el summary es de todo el proyecto, no depende de los filtros. Úsala para ver qué falta migrar a tokens, antes de reportar de nuevo (no repitas lo ya ignorado) o para armar tareas de limpieza. Se llena con report_design_drift; si viene vacía, nadie ha recorrido el código todavía.',
|
|
3394
|
+
inputSchema: {
|
|
3395
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
3396
|
+
platform: z.enum(PARITY_PLATFORMS).optional().describe('Filtra por plataforma'),
|
|
3397
|
+
kind: z.enum(DRIFT_KINDS).optional().describe('Filtra por tipo'),
|
|
3398
|
+
status: z.enum(DRIFT_STATUSES).optional().describe('Filtra por estado: open | ignored | fixed'),
|
|
3399
|
+
},
|
|
3400
|
+
},
|
|
3401
|
+
async ({ projectId, platform, kind, status }) => {
|
|
3402
|
+
try {
|
|
3403
|
+
return textResult(await apiGet(config, '/v1/design-drift', { projectId: resolveProjectId(projectId), platform, kind, status }))
|
|
3404
|
+
} catch (err) {
|
|
3405
|
+
return errorResult(err)
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
)
|
|
3409
|
+
|
|
3410
|
+
server.registerTool(
|
|
3411
|
+
'update_design_drift',
|
|
3412
|
+
{
|
|
3413
|
+
description: 'Cambia el estado de un hallazgo de deriva (list_design_drift): ignored (es intencional o no aplica: un preview, un valor dinámico; ponle una nota con el motivo), fixed (ya se migró a un token) u open (reabrir). También puede ajustar la nota y el suggestedToken. Un hallazgo ignorado o corregido NO se reabre solo cuando se vuelve a reportar. Para marcar como corregidos los que ya no aparecen tras una pasada completa usa resolve_missing_design_drift, no esta tool en bucle.',
|
|
3414
|
+
inputSchema: {
|
|
3415
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
3416
|
+
driftId: z.string().describe('ID del hallazgo (ver list_design_drift)'),
|
|
3417
|
+
status: z.enum(DRIFT_STATUSES).optional().describe('open | ignored | fixed'),
|
|
3418
|
+
note: z.string().max(300).optional().describe('Nota (≤300), ej. el motivo de ignorarlo'),
|
|
3419
|
+
suggestedToken: z.string().max(80).optional().describe('name del token que lo reemplazaría (≤80)'),
|
|
3420
|
+
},
|
|
3421
|
+
},
|
|
3422
|
+
async ({ projectId, driftId, status, note, suggestedToken }) => {
|
|
3423
|
+
if (status === undefined && note === undefined && suggestedToken === undefined) {
|
|
3424
|
+
return errorResult(new Error('Nada que actualizar: manda status, note o suggestedToken'))
|
|
3425
|
+
}
|
|
3426
|
+
try {
|
|
3427
|
+
return textResult(await apiPatch(config, '/v1/design-drift', { projectId: resolveProjectId(projectId), id: driftId, status, note, suggestedToken }))
|
|
3428
|
+
} catch (err) {
|
|
3429
|
+
return errorResult(err)
|
|
3430
|
+
}
|
|
3431
|
+
}
|
|
3432
|
+
)
|
|
3433
|
+
|
|
3434
|
+
server.registerTool(
|
|
3435
|
+
'resolve_missing_design_drift',
|
|
3436
|
+
{
|
|
3437
|
+
description: 'Cierre de una pasada COMPLETA de deriva sobre una plataforma: marca como fixed los hallazgos OPEN de esa plataforma que no se vieron desde reportedBefore (porque la pasada ya no los encontró en el código). Úsala solo si recorriste TODA la plataforma; con una pasada parcial marcarías como corregido lo que simplemente no revisaste. reportedBefore = hora de inicio de la pasada en ms epoch (Date.now() guardado ANTES de empezar a reportar). dryRun es true por default: solo informa { dryRun, platform, reportedBefore, count, items } sin escribir; revisa la lista y repite con dryRun:false para aplicar. No toca los ignorados ni los ya corregidos.',
|
|
3438
|
+
inputSchema: {
|
|
3439
|
+
projectId: z.string().optional().describe(PROJECT_ID_DESC),
|
|
3440
|
+
platform: z.enum(PARITY_PLATFORMS).describe('Plataforma de la pasada: web | ios | android'),
|
|
3441
|
+
reportedBefore: z.number().int().positive().describe('ms epoch: se marcan los open con seenAt anterior a este valor'),
|
|
3442
|
+
dryRun: z.boolean().optional().describe('true (default) = solo informa; false = marca fixed'),
|
|
3234
3443
|
},
|
|
3235
3444
|
},
|
|
3236
|
-
async ({ projectId,
|
|
3445
|
+
async ({ projectId, platform, reportedBefore, dryRun }) => {
|
|
3237
3446
|
try {
|
|
3238
|
-
return textResult(await
|
|
3447
|
+
return textResult(await apiPost(config, '/v1/design-drift/resolve-missing', { projectId: resolveProjectId(projectId), platform, reportedBefore, dryRun: dryRun ?? true }))
|
|
3239
3448
|
} catch (err) {
|
|
3240
3449
|
return errorResult(err)
|
|
3241
3450
|
}
|