@productmaker/mcp 1.13.0 → 1.15.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 +24 -0
- package/dist/http.js +225 -22
- package/dist/stdio.js +225 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,6 +78,7 @@ Sign up at [productmaker.app](https://productmaker.app) and create a key (prefix
|
|
|
78
78
|
| `regenerate_video_script` | Re-roll the pending video script (optionally with a different narrative style) |
|
|
79
79
|
| `resolve_pending_action` | Resolve a gate the pipeline is waiting on (product-reference confirmation or angle-review approval) when `get_task_status` returns `pendingUserAction` |
|
|
80
80
|
| `list_shopify_shops` | List connected Shopify stores |
|
|
81
|
+
| `list_shopify_products` | Search products in a connected Shopify store before choosing `targetProductId` for `publish_to_shopify` |
|
|
81
82
|
| `list_meta_ad_accounts` | List connected Meta ad accounts |
|
|
82
83
|
| `list_tiktok_advertisers` | List connected TikTok Ads advertiser accounts |
|
|
83
84
|
| `publish_to_shopify` | Publish a completed task to a Shopify store (returns markdown with landing + product URLs) |
|
|
@@ -86,6 +87,29 @@ Sign up at [productmaker.app](https://productmaker.app) and create a key (prefix
|
|
|
86
87
|
| `get_meta_campaign_status` | Read-only status + lifetime insights (spend/impressions/clicks/reach) of a Meta campaign |
|
|
87
88
|
| `get_tiktok_campaign_status` | Read-only status + lifetime report of a TikTok campaign (requires `advertiserId`) |
|
|
88
89
|
|
|
90
|
+
## Video duration and winner review
|
|
91
|
+
|
|
92
|
+
`create_product_task` and `generate_video_creative` accept `durationSeconds`:
|
|
93
|
+
use `15` for the default single-call render or `30` for the existing
|
|
94
|
+
approximately-30-second multi-scene format. Thirty-second renders are billed
|
|
95
|
+
per scene, so tell the user about the higher cost before requesting one. The
|
|
96
|
+
MCP keeps the internal `standard` tier hidden and selects it only from this
|
|
97
|
+
explicit duration choice.
|
|
98
|
+
|
|
99
|
+
To let a user decide after competition analysis, pass
|
|
100
|
+
`pauseForAngleReview: true` when creating the task. When `get_task_status`
|
|
101
|
+
returns `pendingUserAction.kind = "angle_review_approval"` with a
|
|
102
|
+
`gapDurationChoice`, relay `recommendedSec` and `estimatedDeltaCredits` to the
|
|
103
|
+
user. After explicit confirmation, call `resolve_pending_action` with
|
|
104
|
+
`action: { kind: "angle_review_approval", gapDurationSeconds: 30 }` to upgrade
|
|
105
|
+
only the recommended angle. Omit the field or send `15` to keep the quoted
|
|
106
|
+
duration. A 30-second upgrade is rejected unless the current status contains a
|
|
107
|
+
matching 30-second recommendation.
|
|
108
|
+
|
|
109
|
+
The resolver uses the task kind from status to call the matching backend
|
|
110
|
+
approval contract, preserving multi-output autopilot tasks and the incremental
|
|
111
|
+
credit gate for the upgrade.
|
|
112
|
+
|
|
89
113
|
## Multiple photos per product
|
|
90
114
|
|
|
91
115
|
Creative tools accept an `images` array of **1–5 photos of the same product**. The first entry is the hero; the rest are alternate angles. Don't mix different products in one call — that's a separate task each time.
|
package/dist/http.js
CHANGED
|
@@ -113,7 +113,7 @@ var ApiClient = class {
|
|
|
113
113
|
};
|
|
114
114
|
|
|
115
115
|
// src/index.ts
|
|
116
|
-
var MCP_VERSION = "1.
|
|
116
|
+
var MCP_VERSION = "1.15.0";
|
|
117
117
|
|
|
118
118
|
// src/tools/tasks.ts
|
|
119
119
|
import { z as z2 } from "zod";
|
|
@@ -476,6 +476,14 @@ function resolveVideoTier(_tier) {
|
|
|
476
476
|
return "single_call";
|
|
477
477
|
}
|
|
478
478
|
__name(resolveVideoTier, "resolveVideoTier");
|
|
479
|
+
function resolveWireVideoTier(tier, durationSeconds) {
|
|
480
|
+
return durationSeconds === 30 ? "standard" : resolveVideoTier(tier);
|
|
481
|
+
}
|
|
482
|
+
__name(resolveWireVideoTier, "resolveWireVideoTier");
|
|
483
|
+
function resolveVideoFormatForDuration(durationSeconds) {
|
|
484
|
+
return durationSeconds === 30 ? "double_30s" : "single_15s";
|
|
485
|
+
}
|
|
486
|
+
__name(resolveVideoFormatForDuration, "resolveVideoFormatForDuration");
|
|
479
487
|
var VideoResolutionEnum = z.enum([
|
|
480
488
|
"480p",
|
|
481
489
|
"720p"
|
|
@@ -1192,8 +1200,13 @@ var CreateProductTaskInput = z2.object({
|
|
|
1192
1200
|
paymentMethods: z2.string().min(1).max(240).optional().describe('Opcional. M\xE9todos de pago que puede mostrar la landing. Ej. "Tarjeta, PSE, Nequi, Contraentrega". Si se omite, la landing no muestra m\xE9todos de pago.'),
|
|
1193
1201
|
productContext: z2.string().max(3e3).optional().describe("Opcional. Informacion adicional del producto que la IA debe usar como contexto prioritario: uso real, comprador, restricciones, diferenciadores o direccion creativa. No reemplaza la investigacion de competencia; si hay competencia, guia el primer angulo y los outputs no deben contradecirlo."),
|
|
1194
1202
|
productLabel: z2.string().trim().max(80).optional().describe('Opcional. Qu\xE9 es el producto, en pocas palabras (ej. "quita pelusas para ropa"). Es AUTORITATIVO: si lo env\xEDas, la IA usa esta identidad y no la reemplaza por otra categor\xEDa. Si lo omites, la IA detecta el producto desde la imagen.'),
|
|
1195
|
-
videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo de
|
|
1196
|
-
|
|
1203
|
+
videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla (en la web es el \xFAnico dropdown). Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan a 15s y 30s; 30s usa el ritmo Multiescena. Opciones: "auto", "ugc_dynamic" (UGC testimonial), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n), "ugc_demo_review" (demo en uso), "problem_solution" (problema\u2192producto\u2192CTA), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla), "product_showcase" (solo producto, sin actor), "wearable_showcase" (ropa/accesorios puestos). Si el usuario pide algo fuera de esta lista (ej. "estilo anime"), p\xEDdele una opci\xF3n v\xE1lida.'),
|
|
1204
|
+
durationSeconds: z2.union([
|
|
1205
|
+
z2.literal(15),
|
|
1206
|
+
z2.literal(30)
|
|
1207
|
+
]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
|
|
1208
|
+
pauseForAngleReview: z2.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. \xDAsalo si quiere decidir 15/30s cuando aparezca un winner. Default false."),
|
|
1209
|
+
videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista" para 15s; al pedir durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
|
|
1197
1210
|
videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "480p" = excelente calidad y m\xE1s econ\xF3mico (RECOMENDADO, default si se omite); "720p" = m\xE1xima nitidez (~el doble de precio). Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 480p).'),
|
|
1198
1211
|
multiAngleVideos: z2.boolean().optional().describe("Opcional. Si true, genera m\xFAltiples \xE1ngulos de venta en paralelo (3-5 videos). Si false/omitido, genera 1 video del \xE1ngulo principal. Default false."),
|
|
1199
1212
|
angleNarratives: PublicNarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). Valores actuales: "review", "ugc_unboxing_chaos", "problem_product_cta", "product_showcase", "wearable_showcase", "cinematic", "animated_problem_pixar", "sassy_product_pixar". angleNarratives[i] fuerza la narrativa del \xE1ngulo i; las posiciones restantes las elige la IA. Requiere multiAngleVideos:true para varios \xE1ngulos.'),
|
|
@@ -1251,13 +1264,19 @@ var PendingActionPayload = z2.discriminatedUnion("kind", [
|
|
|
1251
1264
|
z2.object({
|
|
1252
1265
|
kind: z2.literal("angle_review_approval"),
|
|
1253
1266
|
// All edits are optional — empty action body just approves the
|
|
1254
|
-
// backend-generated draft as-is. Field shapes mirror
|
|
1255
|
-
// in @packages/types/wizard.ts
|
|
1256
|
-
//
|
|
1267
|
+
// backend-generated draft as-is. Field shapes mirror the angle-review
|
|
1268
|
+
// request contracts in @packages/types/wizard.ts; the handler routes to
|
|
1269
|
+
// the dedicated video/image/autopilot endpoint based on StatusResponse.kind.
|
|
1257
1270
|
customAngle: z2.string().min(1).max(480).optional(),
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1271
|
+
// Keep the shared MCP action within the strictest downstream contract:
|
|
1272
|
+
// autopilot/image resume endpoints cap these fields at 1500 characters.
|
|
1273
|
+
painPoints: z2.string().max(1500).optional(),
|
|
1274
|
+
benefits: z2.string().max(1500).optional(),
|
|
1275
|
+
features: z2.array(z2.string().min(1).max(300)).max(10).optional(),
|
|
1276
|
+
gapDurationSeconds: z2.union([
|
|
1277
|
+
z2.literal(15),
|
|
1278
|
+
z2.literal(30)
|
|
1279
|
+
]).optional().describe("Opcional. Solo para aceptar expl\xEDcitamente la recomendaci\xF3n gapDurationChoice del status. 30 env\xEDa el override double_30s para el \xE1ngulo recomendado; 15 u omitido conserva la duraci\xF3n cotizada.")
|
|
1261
1280
|
})
|
|
1262
1281
|
]);
|
|
1263
1282
|
var ResolvePendingActionInput = z2.object({
|
|
@@ -1269,6 +1288,51 @@ function resolveVariantConfirmVideoTier(action) {
|
|
|
1269
1288
|
}
|
|
1270
1289
|
__name(resolveVariantConfirmVideoTier, "resolveVariantConfirmVideoTier");
|
|
1271
1290
|
var StatusForResolveSchema = z2.object({
|
|
1291
|
+
// Metadata used to select the backend resume contract. Older API responses
|
|
1292
|
+
// may omit these fields, so the resolver keeps the generic fallback.
|
|
1293
|
+
kind: z2.enum([
|
|
1294
|
+
"autopilot",
|
|
1295
|
+
"video",
|
|
1296
|
+
"image",
|
|
1297
|
+
"landing",
|
|
1298
|
+
"mix"
|
|
1299
|
+
]).optional(),
|
|
1300
|
+
selectedOutputs: z2.array(z2.enum([
|
|
1301
|
+
"landing",
|
|
1302
|
+
"video",
|
|
1303
|
+
"image"
|
|
1304
|
+
])).optional(),
|
|
1305
|
+
videoAngleCount: z2.number().int().min(1).max(3).optional(),
|
|
1306
|
+
ugcFormat: z2.enum([
|
|
1307
|
+
"single_8s",
|
|
1308
|
+
"single_15s",
|
|
1309
|
+
"double_30s"
|
|
1310
|
+
]).optional(),
|
|
1311
|
+
videoStyle: z2.enum([
|
|
1312
|
+
"ugc",
|
|
1313
|
+
"cartoon_3d",
|
|
1314
|
+
"product_only",
|
|
1315
|
+
"ai_dynamic"
|
|
1316
|
+
]).optional(),
|
|
1317
|
+
videoModel: z2.enum([
|
|
1318
|
+
"veo",
|
|
1319
|
+
"wan",
|
|
1320
|
+
"bytedance",
|
|
1321
|
+
"seedance",
|
|
1322
|
+
"grok",
|
|
1323
|
+
"kling"
|
|
1324
|
+
]).optional(),
|
|
1325
|
+
videoTier: z2.enum([
|
|
1326
|
+
"legacy",
|
|
1327
|
+
"standard",
|
|
1328
|
+
"pro",
|
|
1329
|
+
"single_call"
|
|
1330
|
+
]).optional(),
|
|
1331
|
+
videoResolution: z2.enum([
|
|
1332
|
+
"480p",
|
|
1333
|
+
"720p"
|
|
1334
|
+
]).optional(),
|
|
1335
|
+
narrativeStyle: NarrativeStyleEnum.optional(),
|
|
1272
1336
|
pendingUserAction: z2.object({
|
|
1273
1337
|
kind: z2.string(),
|
|
1274
1338
|
candidateGrouping: z2.string().optional(),
|
|
@@ -1278,10 +1342,8 @@ var StatusForResolveSchema = z2.object({
|
|
|
1278
1342
|
painPointsDraft: z2.string().optional(),
|
|
1279
1343
|
benefitsDraft: z2.string().optional(),
|
|
1280
1344
|
featuresDraft: z2.array(z2.string()).optional(),
|
|
1281
|
-
//
|
|
1282
|
-
//
|
|
1283
|
-
// it — mirrored here purely so it survives the parse instead of being
|
|
1284
|
-
// silently stripped by this object's default unknown-key behavior.
|
|
1345
|
+
// Recommendation consumed only when the caller explicitly sends
|
|
1346
|
+
// gapDurationSeconds: 30 in the angle approval action.
|
|
1285
1347
|
gapDurationChoice: GapDurationChoiceMirror.optional()
|
|
1286
1348
|
}).optional()
|
|
1287
1349
|
}).passthrough();
|
|
@@ -1302,6 +1364,23 @@ function formatOutputList(outputs) {
|
|
|
1302
1364
|
return `${labels.slice(0, -1).join(" + ")} + ${labels[labels.length - 1]}`;
|
|
1303
1365
|
}
|
|
1304
1366
|
__name(formatOutputList, "formatOutputList");
|
|
1367
|
+
function resolveSelectedAngleIndices(status, gapDurationChoice) {
|
|
1368
|
+
const count = Math.min(3, Math.max(status.videoAngleCount ?? 1, gapDurationChoice ? gapDurationChoice.angleIndex + 1 : 1));
|
|
1369
|
+
return Array.from({
|
|
1370
|
+
length: count
|
|
1371
|
+
}, (_, index) => index);
|
|
1372
|
+
}
|
|
1373
|
+
__name(resolveSelectedAngleIndices, "resolveSelectedAngleIndices");
|
|
1374
|
+
function resolveStatusTaskKind(status) {
|
|
1375
|
+
if (status.kind) return status.kind;
|
|
1376
|
+
const outputs = status.selectedOutputs ?? [];
|
|
1377
|
+
if (outputs.length === 1 && outputs[0] === "video") return "video";
|
|
1378
|
+
if (outputs.includes("video")) return "autopilot";
|
|
1379
|
+
if (outputs.length === 1 && outputs[0] === "image") return "image";
|
|
1380
|
+
if (outputs.length === 1 && outputs[0] === "landing") return "landing";
|
|
1381
|
+
return void 0;
|
|
1382
|
+
}
|
|
1383
|
+
__name(resolveStatusTaskKind, "resolveStatusTaskKind");
|
|
1305
1384
|
function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
|
|
1306
1385
|
server.tool("create_product_task", 'Crea una tarea de producto desde una o varias fotos. Por defecto genera los 3 outputs (landing + video + image creatives) EN UN MISMO TASK. Devuelve un taskId.\n\nINPUTS REQUERIDOS: images (1-5 fotos del MISMO producto; la primera es la hero), language, country.\n\nANTES de invocar, PREG\xDANTALE al usuario por price, offer y paymentMethods si no los mencion\xF3 \u2014 son inputs de alto impacto en la landing y nunca debes asumirlos. Si hay descuento, pregunta tambi\xE9n compareAtPrice. NO defaultes country/language desde el contexto del chat: preg\xFAntale expl\xEDcitamente, incluso si el usuario est\xE1 en LATAM. Si te dice "t\xFA elige", omite esos campos.\n\nANTES de invocar, DILE CLARAMENTE al usuario qu\xE9 outputs vas a generar y que `create_product_task` los genera juntos en un mismo task. Ejemplo: "Voy a generar landing + video en un mismo task; esto consume cr\xE9ditos de ambos outputs". Si el usuario s\xF3lo quiere un output, usa generate_landing / generate_video_creative / generate_image_creatives en vez de este tool.\n\nSi el usuario tiene varias fotos del MISMO producto (frente, lateral, detalle, uso), incl\xFAyelas todas en images[] hasta 5. Si tiene fotos de productos DIFERENTES, son llamadas separadas (una tarea por producto).\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos):\n- videoPreset (tipo/narrativa del video \u2014 \xFAnico campo de estilo)\n- multiAngleVideos (booleano)\n- angleNarratives (override per-angle de la narrativa, avanzado)\n- outputs (subconjunto de {landing,video,image}); omitido = ["landing","video","image"] en un mismo task\n- productUrl\n- creativeDirection (direcci\xF3n creativa para im\xE1genes y landing; el estilo del video se dirige con scriptNotes)\n\nGUION: para revisar/editar el guion del video antes de renderizar, usa `generate_video_creative` con `pauseForVideoScriptReview: true` en vez de este tool. Despu\xE9s de la pausa: get_video_script para leer, approve_video_script para aprobar con ediciones.\n\nUsa SOLO valores declarados en los enums. Si el usuario pide algo no soportado (ej. "estilo anime", "modelo X"), p\xEDdele una opci\xF3n v\xE1lida del enum correspondiente.', CreateProductTaskInput.shape, async (input) => {
|
|
1307
1386
|
try {
|
|
@@ -1314,7 +1393,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
1314
1393
|
}));
|
|
1315
1394
|
const selectedOutputs = input.outputs ?? DEFAULT_PRODUCT_TASK_OUTPUTS;
|
|
1316
1395
|
const resolved = resolveVideoPreset(input.videoPreset);
|
|
1317
|
-
const wireTier =
|
|
1396
|
+
const wireTier = resolveWireVideoTier(input.videoTier, input.durationSeconds);
|
|
1318
1397
|
const fields = flatten({
|
|
1319
1398
|
"options.languageCode": input.language,
|
|
1320
1399
|
"options.country": input.country,
|
|
@@ -1328,6 +1407,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
1328
1407
|
"options.videoStyle": resolved?.videoStyle,
|
|
1329
1408
|
"options.narrativeStyle": resolved?.narrativeStyle,
|
|
1330
1409
|
"options.videoTier": wireTier,
|
|
1410
|
+
"options.ugcFormat": resolveVideoFormatForDuration(input.durationSeconds),
|
|
1331
1411
|
"options.videoResolution": input.videoResolution,
|
|
1332
1412
|
"options.multiAngleVideos": input.multiAngleVideos,
|
|
1333
1413
|
"options.angleNarratives": input.angleNarratives,
|
|
@@ -1337,6 +1417,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
1337
1417
|
"options.creativeDirection": input.creativeDirection,
|
|
1338
1418
|
"options.landingStrategy": input.landingStrategy,
|
|
1339
1419
|
"options.landingImageFormat": input.landingImageFormat,
|
|
1420
|
+
pauseForAngleReview: input.pauseForAngleReview,
|
|
1340
1421
|
url: input.productUrl
|
|
1341
1422
|
});
|
|
1342
1423
|
const r = await api.postMultipart("/v1/ingest", {
|
|
@@ -1354,7 +1435,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
1354
1435
|
throw translateError(e);
|
|
1355
1436
|
}
|
|
1356
1437
|
});
|
|
1357
|
-
server.tool("get_task_status", 'Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, la respuesta incluye un segundo bloque markdown con previews inline de las im\xE1genes, links descargables al video MP4 y un enlace a Product Maker para ver el resultado completo.\n\nIMPORTANTE \u2014 `pendingUserAction`: si est\xE1 presente, el pipeline est\xE1 PARADO esperando que el usuario decida algo. Mira el `kind` y llama `resolve_pending_action` con `action: { kind: <mismo kind>, ... }`; sin esto la tarea NUNCA avanza por m\xE1s que esperes. Kinds actuales:\n- `product_reference_confirmation`: el clasificador no est\xE1 seguro si las fotos son variantes del mismo producto o productos distintos. Mu\xE9strale al usuario `targetProductLabel` + `reason`, recoge su respuesta, y usa SOLO valores presentes en `options`.\n- `angle_review_approval`: la IA ya gener\xF3 el \xE1ngulo/revisi\xF3n antes de renderizar. Mu\xE9strale `customAngleDraft`, `painPointsDraft`, `benefitsDraft`, `featuresDraft` si vienen; si no vienen, puedes pedir aprobaci\xF3n general y llamar `resolve_pending_action` con solo `{ kind: "angle_review_approval" }` para aprobar tal cual.\n\nSi ves `gapDurationChoice` (a nivel ra\xEDz de la respuesta, o dentro de `pendingUserAction` cuando el kind es `angle_review_approval`),
|
|
1438
|
+
server.tool("get_task_status", 'Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, la respuesta incluye un segundo bloque markdown con previews inline de las im\xE1genes, links descargables al video MP4 y un enlace a Product Maker para ver el resultado completo.\n\nIMPORTANTE \u2014 `pendingUserAction`: si est\xE1 presente, el pipeline est\xE1 PARADO esperando que el usuario decida algo. Mira el `kind` y llama `resolve_pending_action` con `action: { kind: <mismo kind>, ... }`; sin esto la tarea NUNCA avanza por m\xE1s que esperes. Kinds actuales:\n- `product_reference_confirmation`: el clasificador no est\xE1 seguro si las fotos son variantes del mismo producto o productos distintos. Mu\xE9strale al usuario `targetProductLabel` + `reason`, recoge su respuesta, y usa SOLO valores presentes en `options`.\n- `angle_review_approval`: la IA ya gener\xF3 el \xE1ngulo/revisi\xF3n antes de renderizar. Mu\xE9strale `customAngleDraft`, `painPointsDraft`, `benefitsDraft`, `featuresDraft` si vienen; si no vienen, puedes pedir aprobaci\xF3n general y llamar `resolve_pending_action` con solo `{ kind: "angle_review_approval" }` para aprobar tal cual.\n\nSi ves `gapDurationChoice` (a nivel ra\xEDz de la respuesta, o dentro de `pendingUserAction` cuando el kind es `angle_review_approval`), mu\xE9strale al usuario la duraci\xF3n recomendada y `estimatedDeltaCredits` si aparece. Con confirmaci\xF3n expl\xEDcita, llama `resolve_pending_action` con `action: { kind: "angle_review_approval", gapDurationSeconds: 30 }` para actualizar solo ese \xE1ngulo. Omitir el campo o enviar 15 conserva la duraci\xF3n cotizada; 30 se rechaza si no hay una recomendaci\xF3n pendiente de 30s.\nUse this to check on a task progress; pass waitSeconds for a near-synchronous response.', GetTaskStatusInput.shape, async (input, extra) => {
|
|
1358
1439
|
try {
|
|
1359
1440
|
const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`), "fetcher");
|
|
1360
1441
|
const signal = extra?.signal;
|
|
@@ -1411,7 +1492,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
1411
1492
|
throw translateError(e);
|
|
1412
1493
|
}
|
|
1413
1494
|
});
|
|
1414
|
-
server.tool("resolve_pending_action", 'Resuelve una acci\xF3n que el pipeline est\xE1 esperando del usuario. La llamas cuando `get_task_status` devuelve `pendingUserAction` (sin esto la tarea NUNCA avanza por m\xE1s que esperes). El `action.kind` que mandas DEBE coincidir con el `pendingUserAction.kind` que viste en el status \u2014 es el contrato de simetr\xEDa que mantiene este tool como \xFAnica superficie de resoluci\xF3n de gates.\n\nKINDS soportados hoy:\n\n\u2022 `product_reference_confirmation` \u2014
|
|
1495
|
+
server.tool("resolve_pending_action", 'Resuelve una acci\xF3n que el pipeline est\xE1 esperando del usuario. La llamas cuando `get_task_status` devuelve `pendingUserAction` (sin esto la tarea NUNCA avanza por m\xE1s que esperes). El `action.kind` que mandas DEBE coincidir con el `pendingUserAction.kind` que viste en el status \u2014 es el contrato de simetr\xEDa que mantiene este tool como \xFAnica superficie de resoluci\xF3n de gates.\n\nKINDS soportados hoy:\n\n\u2022 `product_reference_confirmation` \u2014 confirma c\xF3mo agrupar las fotos usando solo los valores de `pendingUserAction.options`.\n\n\u2022 `angle_review_approval` \u2014 aprueba o edita el draft despu\xE9s de `pauseForAngleReview: true`. Puedes enviar `customAngle`, `painPoints`, `benefits` y `features`; todos son opcionales y el MCP conserva el draft persistido cuando no los cambias. Si `pendingUserAction.gapDurationChoice` recomienda 30s para un winner, muestra al usuario `recommendedSec` y `estimatedDeltaCredits`, pide confirmaci\xF3n expl\xEDcita y env\xEDa `gapDurationSeconds: 30` en esta misma acci\xF3n. El MCP valida que exista una recomendaci\xF3n pendiente de 30s y entonces actualiza solo el \xE1ngulo recomendado con `double_30s`; omitir el campo o enviar 15 mantiene la duraci\xF3n cotizada.\n\nFuturos kinds (no implementados a\xFAn) se agregar\xE1n como nuevas variantes del `action` discriminated union \u2014 el flujo "lee kind del status \u2192 manda mismo kind aqu\xED" sigue igual.', ResolvePendingActionInput.shape, async (input) => {
|
|
1415
1496
|
try {
|
|
1416
1497
|
const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
|
|
1417
1498
|
const status = StatusForResolveSchema.parse(raw);
|
|
@@ -1464,7 +1545,108 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
1464
1545
|
if (input.action.painPoints != null) body.painPoints = input.action.painPoints;
|
|
1465
1546
|
if (input.action.benefits != null) body.benefits = input.action.benefits;
|
|
1466
1547
|
if (input.action.features != null) body.features = input.action.features;
|
|
1467
|
-
|
|
1548
|
+
if (input.action.gapDurationSeconds != null && !pending.gapDurationChoice) {
|
|
1549
|
+
throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para elegir una duraci\xF3n. Consulta get_task_status y confirma el winner primero.");
|
|
1550
|
+
}
|
|
1551
|
+
if (input.action.gapDurationSeconds === 30) {
|
|
1552
|
+
const choice = pending.gapDurationChoice;
|
|
1553
|
+
if (!choice) throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para aceptar 30s.");
|
|
1554
|
+
if (choice.recommendedSec !== 30) {
|
|
1555
|
+
throw new Error(`La recomendaci\xF3n de duraci\xF3n para este winner es ${choice.recommendedSec}s; no se puede solicitar 30s con este estado.`);
|
|
1556
|
+
}
|
|
1557
|
+
body.angleVideoOptions = [
|
|
1558
|
+
{
|
|
1559
|
+
angleIndex: choice.angleIndex,
|
|
1560
|
+
ugcFormat: "double_30s"
|
|
1561
|
+
}
|
|
1562
|
+
];
|
|
1563
|
+
}
|
|
1564
|
+
const selectedAngleIndices = resolveSelectedAngleIndices(status, pending.gapDurationChoice);
|
|
1565
|
+
const resolvedTaskKind = resolveStatusTaskKind(status);
|
|
1566
|
+
const isVideoTask = resolvedTaskKind === "video";
|
|
1567
|
+
const isAutopilotTask = resolvedTaskKind === "autopilot";
|
|
1568
|
+
if ((isVideoTask || isAutopilotTask) && body.customAngle != null) {
|
|
1569
|
+
const existingOptions = Array.isArray(body.angleVideoOptions) ? body.angleVideoOptions : [];
|
|
1570
|
+
const optionsByIndex = new Map(existingOptions.map((option) => [
|
|
1571
|
+
option.angleIndex,
|
|
1572
|
+
{
|
|
1573
|
+
...option
|
|
1574
|
+
}
|
|
1575
|
+
]));
|
|
1576
|
+
for (const angleIndex of selectedAngleIndices) {
|
|
1577
|
+
optionsByIndex.set(angleIndex, {
|
|
1578
|
+
...optionsByIndex.get(angleIndex) ?? {
|
|
1579
|
+
angleIndex
|
|
1580
|
+
},
|
|
1581
|
+
angle: body.customAngle
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
body.angleVideoOptions = [
|
|
1585
|
+
...optionsByIndex.values()
|
|
1586
|
+
].sort((left, right) => Number(left.angleIndex) - Number(right.angleIndex));
|
|
1587
|
+
}
|
|
1588
|
+
if (isVideoTask) {
|
|
1589
|
+
body.selectedAngleIndices = selectedAngleIndices;
|
|
1590
|
+
const r2 = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/approve-video-angles`, body);
|
|
1591
|
+
return asText(r2);
|
|
1592
|
+
}
|
|
1593
|
+
if (isAutopilotTask) {
|
|
1594
|
+
const resumeBody = {
|
|
1595
|
+
selectedAngleIndices,
|
|
1596
|
+
...status.ugcFormat ? {
|
|
1597
|
+
ugcFormat: status.ugcFormat
|
|
1598
|
+
} : {
|
|
1599
|
+
ugcFormat: "single_15s"
|
|
1600
|
+
},
|
|
1601
|
+
...status.videoStyle ? {
|
|
1602
|
+
videoStyle: status.videoStyle
|
|
1603
|
+
} : {},
|
|
1604
|
+
...status.videoModel ? {
|
|
1605
|
+
videoModel: status.videoModel
|
|
1606
|
+
} : {},
|
|
1607
|
+
...status.videoTier ? {
|
|
1608
|
+
videoTier: status.videoTier
|
|
1609
|
+
} : {},
|
|
1610
|
+
...status.videoResolution ? {
|
|
1611
|
+
videoResolution: status.videoResolution
|
|
1612
|
+
} : {},
|
|
1613
|
+
...status.narrativeStyle ? {
|
|
1614
|
+
narrativeStyle: status.narrativeStyle
|
|
1615
|
+
} : {},
|
|
1616
|
+
...body.customAngle != null ? {
|
|
1617
|
+
customAngleOverride: body.customAngle
|
|
1618
|
+
} : {},
|
|
1619
|
+
...body.painPoints != null ? {
|
|
1620
|
+
painPoints: body.painPoints
|
|
1621
|
+
} : {},
|
|
1622
|
+
...body.benefits != null ? {
|
|
1623
|
+
benefits: body.benefits
|
|
1624
|
+
} : {},
|
|
1625
|
+
...body.angleVideoOptions ? {
|
|
1626
|
+
angleVideoOptions: body.angleVideoOptions
|
|
1627
|
+
} : {}
|
|
1628
|
+
};
|
|
1629
|
+
if (body.features !== void 0) {
|
|
1630
|
+
await api.patchJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/update-product-features`, {
|
|
1631
|
+
features: body.features
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
const r2 = await api.postJson(`/v1/ingest/${encodeURIComponent(input.taskId)}/resume`, resumeBody);
|
|
1635
|
+
return asText(r2);
|
|
1636
|
+
}
|
|
1637
|
+
const route = resolvedTaskKind === "image" ? `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-image-angles` : `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-angles`;
|
|
1638
|
+
const imageBody = status.kind === "image" ? {
|
|
1639
|
+
...body.customAngle != null ? {
|
|
1640
|
+
customAngle: body.customAngle
|
|
1641
|
+
} : {},
|
|
1642
|
+
...body.painPoints != null ? {
|
|
1643
|
+
painPoints: body.painPoints
|
|
1644
|
+
} : {},
|
|
1645
|
+
...body.features != null ? {
|
|
1646
|
+
features: body.features
|
|
1647
|
+
} : {}
|
|
1648
|
+
} : body;
|
|
1649
|
+
const r = await api.postJson(route, imageBody);
|
|
1468
1650
|
return asText(r);
|
|
1469
1651
|
}
|
|
1470
1652
|
default: {
|
|
@@ -1508,9 +1690,12 @@ var VideoInput = z3.object({
|
|
|
1508
1690
|
"16:9",
|
|
1509
1691
|
"Auto"
|
|
1510
1692
|
]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
|
|
1511
|
-
durationSeconds: z3.
|
|
1512
|
-
|
|
1513
|
-
|
|
1693
|
+
durationSeconds: z3.union([
|
|
1694
|
+
z3.literal(15),
|
|
1695
|
+
z3.literal(30)
|
|
1696
|
+
]).default(15).describe("Opcional. Duraci\xF3n objetivo: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
|
|
1697
|
+
videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla (en la web es el \xFAnico dropdown). Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan tanto a 15s como a 30s; el formato de 30s conserva el ritmo multi-escena. Opciones: "auto", "ugc_dynamic" (UGC cl\xE1sico testimonial multi-hook), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones numeradas), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n), "ugc_demo_review" (demo en uso a lo largo de d\xEDas), "problem_solution" (problema\u2192producto\u2192CTA), "cinematic" (historia visual premium), "pixar_problem" (problema animado 3D estilo Pixar), "pixar_sassy" (producto que habla con personalidad), "product_showcase" (sin actor, solo producto), "wearable_showcase" (ropa/accesorios puestos).'),
|
|
1698
|
+
videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista"; para durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
|
|
1514
1699
|
videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "720p" = m\xE1xima nitidez (RECOMENDADO, default si se omite); "480p" = render m\xE1s econ\xF3mico. Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 720p).'),
|
|
1515
1700
|
features: z3.array(z3.string()).optional().describe('Opcional. Lista de features/beneficios clave a mencionar. Ej. ["resistente al agua", "bater\xEDa 24h", "carga inal\xE1mbrica"].'),
|
|
1516
1701
|
price: CreativeRefs.price,
|
|
@@ -1520,6 +1705,7 @@ var VideoInput = z3.object({
|
|
|
1520
1705
|
cta: z3.string().optional().describe('Opcional. Call-to-action final del video. Ej. "Compra ahora", "Pide el tuyo hoy", "Link en bio". Si se omite, la IA escribe uno.'),
|
|
1521
1706
|
scriptNotes: z3.string().optional().describe('Opcional. Notas/restricciones para el guion (la IA las usa al redactar). Ej. "No mencionar la marca X", "Tono divertido, no cl\xEDnico", "Mencionar 2x1". NO es el guion literal \u2014 para editar el guion palabra-por-palabra, pasa `pauseForVideoScriptReview: true` y usa get_video_script / approve_video_script tras la pausa.'),
|
|
1522
1707
|
pauseForVideoScriptReview: z3.boolean().optional().describe("Opcional. Si true, la tarea pausa despu\xE9s de generar el guion y antes de renderizar. \xDAsalo cuando el usuario quiera editar el guion antes de gastar cr\xE9ditos en el video. Tras la pausa: get_video_script para leerlo, approve_video_script para aprobar (con o sin ediciones). Default false."),
|
|
1708
|
+
pauseForAngleReview: z3.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. Default false."),
|
|
1523
1709
|
competitorScanEnabled: CreativeRefs.competitorScanEnabled,
|
|
1524
1710
|
brandId: CreativeRefs.brandId
|
|
1525
1711
|
}).strict();
|
|
@@ -1575,10 +1761,10 @@ var LandingInput = z3.object({
|
|
|
1575
1761
|
landingImageFormat: LandingImageFormatEnum.default("9:16").describe('Opcional. Formato/relaci\xF3n de aspecto de las im\xE1genes de la landing cl\xE1sica: "9:16" (vertical, por defecto), "4:5" (vertical compacto para feed) o "1:1" (cuadrado). Default "9:16".')
|
|
1576
1762
|
});
|
|
1577
1763
|
function registerCreativeTools(server, api, { transport = "http" } = {}) {
|
|
1578
|
-
server.tool("generate_video_creative", 'Genera UN video creativo independiente. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), title, primaryAngle, language, country, aspectRatio.\n\nANTES de invocar, PREG\xDANTALE al usuario por price y offer si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para price/offer, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos): durationSeconds (
|
|
1764
|
+
server.tool("generate_video_creative", 'Genera UN video creativo independiente. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), title, primaryAngle, language, country, aspectRatio.\n\nANTES de invocar, PREG\xDANTALE al usuario por price y offer si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para price/offer, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos): durationSeconds (15 o 30), videoPreset (tipo/narrativa del video), features, cta, scriptNotes (dirige el guion Y el ambiente/escenario/est\xE9tica del video, ej. "en una playa, opulento").\n\nESTILO: 15s usa "Realista" (single_call); 30s usa "Multiescena" (multi-toma, aproximadamente 30s). Multiescena se factura por escena, as\xED que avisa el costo antes de activarlo.\n\nGUION: el usuario NO escribe el guion desde cero \u2014 la IA lo redacta. Si quiere REVISARLO/EDITARLO antes de renderizar (igual que en la web), pasa `pauseForVideoScriptReview: true`; cuando la tarea quede pausada, usa get_video_script para leer las escenas, ofr\xE9celas al usuario, y llama approve_video_script con las ediciones. Para dirigir el guion ANTES de generar (sin pausar), usa scriptNotes. Para revisar el \xE1ngulo y decidir duraci\xF3n tras el an\xE1lisis de competencia, pasa `pauseForAngleReview: true` y luego resuelve `angle_review_approval` con `gapDurationSeconds`.', VideoInput.shape, async (input) => {
|
|
1579
1765
|
try {
|
|
1580
1766
|
const resolved = resolveVideoPreset(input.videoPreset);
|
|
1581
|
-
const wireTier =
|
|
1767
|
+
const wireTier = resolveWireVideoTier(input.videoTier, input.durationSeconds);
|
|
1582
1768
|
const { videoTier: _publicTier, videoResolution: _vr, ...rest } = input;
|
|
1583
1769
|
const wirePayload = {
|
|
1584
1770
|
...rest
|
|
@@ -1695,6 +1881,23 @@ function registerConnectionTools(server, api) {
|
|
|
1695
1881
|
throw translateError(e);
|
|
1696
1882
|
}
|
|
1697
1883
|
});
|
|
1884
|
+
server.tool("list_shopify_products", 'Busca productos en una tienda Shopify conectada para elegir el target de una republicaci\xF3n. Usa el `id` devuelto junto con `publicationMode="existing"` en `publish_to_shopify`. Si `taskId` se proporciona, la respuesta tambi\xE9n indica el producto publicado actualmente para esa tarea y tienda. No modifica Shopify.', z5.object({
|
|
1885
|
+
shopDomain: z5.string().min(3).max(120),
|
|
1886
|
+
query: z5.string().trim().max(120).optional(),
|
|
1887
|
+
taskId: z5.string().uuid().optional()
|
|
1888
|
+
}).shape, async ({ shopDomain, query, taskId }) => {
|
|
1889
|
+
try {
|
|
1890
|
+
const params = new URLSearchParams({
|
|
1891
|
+
shopDomain
|
|
1892
|
+
});
|
|
1893
|
+
if (query) params.set("query", query);
|
|
1894
|
+
if (taskId) params.set("taskId", taskId);
|
|
1895
|
+
const r = await api.getJson(`/v1/shopify/products?${params.toString()}`);
|
|
1896
|
+
return asText(r);
|
|
1897
|
+
} catch (e) {
|
|
1898
|
+
throw translateError(e);
|
|
1899
|
+
}
|
|
1900
|
+
});
|
|
1698
1901
|
server.tool("list_meta_ad_accounts", "Lista las cuentas publicitarias de Meta conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar Meta en productmaker.app/integrations.", z5.object({}).shape, async () => {
|
|
1699
1902
|
try {
|
|
1700
1903
|
const r = await api.getJson("/v1/facebook/ad-accounts");
|
package/dist/stdio.js
CHANGED
|
@@ -110,7 +110,7 @@ var ApiClient = class {
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
// src/index.ts
|
|
113
|
-
var MCP_VERSION = "1.
|
|
113
|
+
var MCP_VERSION = "1.15.0";
|
|
114
114
|
|
|
115
115
|
// src/tools/tasks.ts
|
|
116
116
|
import { z as z2 } from "zod";
|
|
@@ -473,6 +473,14 @@ function resolveVideoTier(_tier) {
|
|
|
473
473
|
return "single_call";
|
|
474
474
|
}
|
|
475
475
|
__name(resolveVideoTier, "resolveVideoTier");
|
|
476
|
+
function resolveWireVideoTier(tier, durationSeconds) {
|
|
477
|
+
return durationSeconds === 30 ? "standard" : resolveVideoTier(tier);
|
|
478
|
+
}
|
|
479
|
+
__name(resolveWireVideoTier, "resolveWireVideoTier");
|
|
480
|
+
function resolveVideoFormatForDuration(durationSeconds) {
|
|
481
|
+
return durationSeconds === 30 ? "double_30s" : "single_15s";
|
|
482
|
+
}
|
|
483
|
+
__name(resolveVideoFormatForDuration, "resolveVideoFormatForDuration");
|
|
476
484
|
var VideoResolutionEnum = z.enum([
|
|
477
485
|
"480p",
|
|
478
486
|
"720p"
|
|
@@ -1148,8 +1156,13 @@ var CreateProductTaskInput = z2.object({
|
|
|
1148
1156
|
paymentMethods: z2.string().min(1).max(240).optional().describe('Opcional. M\xE9todos de pago que puede mostrar la landing. Ej. "Tarjeta, PSE, Nequi, Contraentrega". Si se omite, la landing no muestra m\xE9todos de pago.'),
|
|
1149
1157
|
productContext: z2.string().max(3e3).optional().describe("Opcional. Informacion adicional del producto que la IA debe usar como contexto prioritario: uso real, comprador, restricciones, diferenciadores o direccion creativa. No reemplaza la investigacion de competencia; si hay competencia, guia el primer angulo y los outputs no deben contradecirlo."),
|
|
1150
1158
|
productLabel: z2.string().trim().max(80).optional().describe('Opcional. Qu\xE9 es el producto, en pocas palabras (ej. "quita pelusas para ropa"). Es AUTORITATIVO: si lo env\xEDas, la IA usa esta identidad y no la reemplaza por otra categor\xEDa. Si lo omites, la IA detecta el producto desde la imagen.'),
|
|
1151
|
-
videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo de
|
|
1152
|
-
|
|
1159
|
+
videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla (en la web es el \xFAnico dropdown). Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan a 15s y 30s; 30s usa el ritmo Multiescena. Opciones: "auto", "ugc_dynamic" (UGC testimonial), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n), "ugc_demo_review" (demo en uso), "problem_solution" (problema\u2192producto\u2192CTA), "pixar_problem" (problema animado 3D), "pixar_sassy" (producto que habla), "product_showcase" (solo producto, sin actor), "wearable_showcase" (ropa/accesorios puestos). Si el usuario pide algo fuera de esta lista (ej. "estilo anime"), p\xEDdele una opci\xF3n v\xE1lida.'),
|
|
1160
|
+
durationSeconds: z2.union([
|
|
1161
|
+
z2.literal(15),
|
|
1162
|
+
z2.literal(30)
|
|
1163
|
+
]).default(15).describe("Opcional. Duraci\xF3n objetivo del video: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
|
|
1164
|
+
pauseForAngleReview: z2.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. \xDAsalo si quiere decidir 15/30s cuando aparezca un winner. Default false."),
|
|
1165
|
+
videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista" para 15s; al pedir durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
|
|
1153
1166
|
videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "480p" = excelente calidad y m\xE1s econ\xF3mico (RECOMENDADO, default si se omite); "720p" = m\xE1xima nitidez (~el doble de precio). Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 480p).'),
|
|
1154
1167
|
multiAngleVideos: z2.boolean().optional().describe("Opcional. Si true, genera m\xFAltiples \xE1ngulos de venta en paralelo (3-5 videos). Si false/omitido, genera 1 video del \xE1ngulo principal. Default false."),
|
|
1155
1168
|
angleNarratives: PublicNarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). Valores actuales: "review", "ugc_unboxing_chaos", "problem_product_cta", "product_showcase", "wearable_showcase", "cinematic", "animated_problem_pixar", "sassy_product_pixar". angleNarratives[i] fuerza la narrativa del \xE1ngulo i; las posiciones restantes las elige la IA. Requiere multiAngleVideos:true para varios \xE1ngulos.'),
|
|
@@ -1207,13 +1220,19 @@ var PendingActionPayload = z2.discriminatedUnion("kind", [
|
|
|
1207
1220
|
z2.object({
|
|
1208
1221
|
kind: z2.literal("angle_review_approval"),
|
|
1209
1222
|
// All edits are optional — empty action body just approves the
|
|
1210
|
-
// backend-generated draft as-is. Field shapes mirror
|
|
1211
|
-
// in @packages/types/wizard.ts
|
|
1212
|
-
//
|
|
1223
|
+
// backend-generated draft as-is. Field shapes mirror the angle-review
|
|
1224
|
+
// request contracts in @packages/types/wizard.ts; the handler routes to
|
|
1225
|
+
// the dedicated video/image/autopilot endpoint based on StatusResponse.kind.
|
|
1213
1226
|
customAngle: z2.string().min(1).max(480).optional(),
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1227
|
+
// Keep the shared MCP action within the strictest downstream contract:
|
|
1228
|
+
// autopilot/image resume endpoints cap these fields at 1500 characters.
|
|
1229
|
+
painPoints: z2.string().max(1500).optional(),
|
|
1230
|
+
benefits: z2.string().max(1500).optional(),
|
|
1231
|
+
features: z2.array(z2.string().min(1).max(300)).max(10).optional(),
|
|
1232
|
+
gapDurationSeconds: z2.union([
|
|
1233
|
+
z2.literal(15),
|
|
1234
|
+
z2.literal(30)
|
|
1235
|
+
]).optional().describe("Opcional. Solo para aceptar expl\xEDcitamente la recomendaci\xF3n gapDurationChoice del status. 30 env\xEDa el override double_30s para el \xE1ngulo recomendado; 15 u omitido conserva la duraci\xF3n cotizada.")
|
|
1217
1236
|
})
|
|
1218
1237
|
]);
|
|
1219
1238
|
var ResolvePendingActionInput = z2.object({
|
|
@@ -1225,6 +1244,51 @@ function resolveVariantConfirmVideoTier(action) {
|
|
|
1225
1244
|
}
|
|
1226
1245
|
__name(resolveVariantConfirmVideoTier, "resolveVariantConfirmVideoTier");
|
|
1227
1246
|
var StatusForResolveSchema = z2.object({
|
|
1247
|
+
// Metadata used to select the backend resume contract. Older API responses
|
|
1248
|
+
// may omit these fields, so the resolver keeps the generic fallback.
|
|
1249
|
+
kind: z2.enum([
|
|
1250
|
+
"autopilot",
|
|
1251
|
+
"video",
|
|
1252
|
+
"image",
|
|
1253
|
+
"landing",
|
|
1254
|
+
"mix"
|
|
1255
|
+
]).optional(),
|
|
1256
|
+
selectedOutputs: z2.array(z2.enum([
|
|
1257
|
+
"landing",
|
|
1258
|
+
"video",
|
|
1259
|
+
"image"
|
|
1260
|
+
])).optional(),
|
|
1261
|
+
videoAngleCount: z2.number().int().min(1).max(3).optional(),
|
|
1262
|
+
ugcFormat: z2.enum([
|
|
1263
|
+
"single_8s",
|
|
1264
|
+
"single_15s",
|
|
1265
|
+
"double_30s"
|
|
1266
|
+
]).optional(),
|
|
1267
|
+
videoStyle: z2.enum([
|
|
1268
|
+
"ugc",
|
|
1269
|
+
"cartoon_3d",
|
|
1270
|
+
"product_only",
|
|
1271
|
+
"ai_dynamic"
|
|
1272
|
+
]).optional(),
|
|
1273
|
+
videoModel: z2.enum([
|
|
1274
|
+
"veo",
|
|
1275
|
+
"wan",
|
|
1276
|
+
"bytedance",
|
|
1277
|
+
"seedance",
|
|
1278
|
+
"grok",
|
|
1279
|
+
"kling"
|
|
1280
|
+
]).optional(),
|
|
1281
|
+
videoTier: z2.enum([
|
|
1282
|
+
"legacy",
|
|
1283
|
+
"standard",
|
|
1284
|
+
"pro",
|
|
1285
|
+
"single_call"
|
|
1286
|
+
]).optional(),
|
|
1287
|
+
videoResolution: z2.enum([
|
|
1288
|
+
"480p",
|
|
1289
|
+
"720p"
|
|
1290
|
+
]).optional(),
|
|
1291
|
+
narrativeStyle: NarrativeStyleEnum.optional(),
|
|
1228
1292
|
pendingUserAction: z2.object({
|
|
1229
1293
|
kind: z2.string(),
|
|
1230
1294
|
candidateGrouping: z2.string().optional(),
|
|
@@ -1234,10 +1298,8 @@ var StatusForResolveSchema = z2.object({
|
|
|
1234
1298
|
painPointsDraft: z2.string().optional(),
|
|
1235
1299
|
benefitsDraft: z2.string().optional(),
|
|
1236
1300
|
featuresDraft: z2.array(z2.string()).optional(),
|
|
1237
|
-
//
|
|
1238
|
-
//
|
|
1239
|
-
// it — mirrored here purely so it survives the parse instead of being
|
|
1240
|
-
// silently stripped by this object's default unknown-key behavior.
|
|
1301
|
+
// Recommendation consumed only when the caller explicitly sends
|
|
1302
|
+
// gapDurationSeconds: 30 in the angle approval action.
|
|
1241
1303
|
gapDurationChoice: GapDurationChoiceMirror.optional()
|
|
1242
1304
|
}).optional()
|
|
1243
1305
|
}).passthrough();
|
|
@@ -1258,6 +1320,23 @@ function formatOutputList(outputs) {
|
|
|
1258
1320
|
return `${labels.slice(0, -1).join(" + ")} + ${labels[labels.length - 1]}`;
|
|
1259
1321
|
}
|
|
1260
1322
|
__name(formatOutputList, "formatOutputList");
|
|
1323
|
+
function resolveSelectedAngleIndices(status, gapDurationChoice) {
|
|
1324
|
+
const count = Math.min(3, Math.max(status.videoAngleCount ?? 1, gapDurationChoice ? gapDurationChoice.angleIndex + 1 : 1));
|
|
1325
|
+
return Array.from({
|
|
1326
|
+
length: count
|
|
1327
|
+
}, (_, index) => index);
|
|
1328
|
+
}
|
|
1329
|
+
__name(resolveSelectedAngleIndices, "resolveSelectedAngleIndices");
|
|
1330
|
+
function resolveStatusTaskKind(status) {
|
|
1331
|
+
if (status.kind) return status.kind;
|
|
1332
|
+
const outputs = status.selectedOutputs ?? [];
|
|
1333
|
+
if (outputs.length === 1 && outputs[0] === "video") return "video";
|
|
1334
|
+
if (outputs.includes("video")) return "autopilot";
|
|
1335
|
+
if (outputs.length === 1 && outputs[0] === "image") return "image";
|
|
1336
|
+
if (outputs.length === 1 && outputs[0] === "landing") return "landing";
|
|
1337
|
+
return void 0;
|
|
1338
|
+
}
|
|
1339
|
+
__name(resolveStatusTaskKind, "resolveStatusTaskKind");
|
|
1261
1340
|
function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
|
|
1262
1341
|
server.tool("create_product_task", 'Crea una tarea de producto desde una o varias fotos. Por defecto genera los 3 outputs (landing + video + image creatives) EN UN MISMO TASK. Devuelve un taskId.\n\nINPUTS REQUERIDOS: images (1-5 fotos del MISMO producto; la primera es la hero), language, country.\n\nANTES de invocar, PREG\xDANTALE al usuario por price, offer y paymentMethods si no los mencion\xF3 \u2014 son inputs de alto impacto en la landing y nunca debes asumirlos. Si hay descuento, pregunta tambi\xE9n compareAtPrice. NO defaultes country/language desde el contexto del chat: preg\xFAntale expl\xEDcitamente, incluso si el usuario est\xE1 en LATAM. Si te dice "t\xFA elige", omite esos campos.\n\nANTES de invocar, DILE CLARAMENTE al usuario qu\xE9 outputs vas a generar y que `create_product_task` los genera juntos en un mismo task. Ejemplo: "Voy a generar landing + video en un mismo task; esto consume cr\xE9ditos de ambos outputs". Si el usuario s\xF3lo quiere un output, usa generate_landing / generate_video_creative / generate_image_creatives en vez de este tool.\n\nSi el usuario tiene varias fotos del MISMO producto (frente, lateral, detalle, uso), incl\xFAyelas todas en images[] hasta 5. Si tiene fotos de productos DIFERENTES, son llamadas separadas (una tarea por producto).\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos):\n- videoPreset (tipo/narrativa del video \u2014 \xFAnico campo de estilo)\n- multiAngleVideos (booleano)\n- angleNarratives (override per-angle de la narrativa, avanzado)\n- outputs (subconjunto de {landing,video,image}); omitido = ["landing","video","image"] en un mismo task\n- productUrl\n- creativeDirection (direcci\xF3n creativa para im\xE1genes y landing; el estilo del video se dirige con scriptNotes)\n\nGUION: para revisar/editar el guion del video antes de renderizar, usa `generate_video_creative` con `pauseForVideoScriptReview: true` en vez de este tool. Despu\xE9s de la pausa: get_video_script para leer, approve_video_script para aprobar con ediciones.\n\nUsa SOLO valores declarados en los enums. Si el usuario pide algo no soportado (ej. "estilo anime", "modelo X"), p\xEDdele una opci\xF3n v\xE1lida del enum correspondiente.', CreateProductTaskInput.shape, async (input) => {
|
|
1263
1342
|
try {
|
|
@@ -1270,7 +1349,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
1270
1349
|
}));
|
|
1271
1350
|
const selectedOutputs = input.outputs ?? DEFAULT_PRODUCT_TASK_OUTPUTS;
|
|
1272
1351
|
const resolved = resolveVideoPreset(input.videoPreset);
|
|
1273
|
-
const wireTier =
|
|
1352
|
+
const wireTier = resolveWireVideoTier(input.videoTier, input.durationSeconds);
|
|
1274
1353
|
const fields = flatten({
|
|
1275
1354
|
"options.languageCode": input.language,
|
|
1276
1355
|
"options.country": input.country,
|
|
@@ -1284,6 +1363,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
1284
1363
|
"options.videoStyle": resolved?.videoStyle,
|
|
1285
1364
|
"options.narrativeStyle": resolved?.narrativeStyle,
|
|
1286
1365
|
"options.videoTier": wireTier,
|
|
1366
|
+
"options.ugcFormat": resolveVideoFormatForDuration(input.durationSeconds),
|
|
1287
1367
|
"options.videoResolution": input.videoResolution,
|
|
1288
1368
|
"options.multiAngleVideos": input.multiAngleVideos,
|
|
1289
1369
|
"options.angleNarratives": input.angleNarratives,
|
|
@@ -1293,6 +1373,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
1293
1373
|
"options.creativeDirection": input.creativeDirection,
|
|
1294
1374
|
"options.landingStrategy": input.landingStrategy,
|
|
1295
1375
|
"options.landingImageFormat": input.landingImageFormat,
|
|
1376
|
+
pauseForAngleReview: input.pauseForAngleReview,
|
|
1296
1377
|
url: input.productUrl
|
|
1297
1378
|
});
|
|
1298
1379
|
const r = await api.postMultipart("/v1/ingest", {
|
|
@@ -1310,7 +1391,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
1310
1391
|
throw translateError(e);
|
|
1311
1392
|
}
|
|
1312
1393
|
});
|
|
1313
|
-
server.tool("get_task_status", 'Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, la respuesta incluye un segundo bloque markdown con previews inline de las im\xE1genes, links descargables al video MP4 y un enlace a Product Maker para ver el resultado completo.\n\nIMPORTANTE \u2014 `pendingUserAction`: si est\xE1 presente, el pipeline est\xE1 PARADO esperando que el usuario decida algo. Mira el `kind` y llama `resolve_pending_action` con `action: { kind: <mismo kind>, ... }`; sin esto la tarea NUNCA avanza por m\xE1s que esperes. Kinds actuales:\n- `product_reference_confirmation`: el clasificador no est\xE1 seguro si las fotos son variantes del mismo producto o productos distintos. Mu\xE9strale al usuario `targetProductLabel` + `reason`, recoge su respuesta, y usa SOLO valores presentes en `options`.\n- `angle_review_approval`: la IA ya gener\xF3 el \xE1ngulo/revisi\xF3n antes de renderizar. Mu\xE9strale `customAngleDraft`, `painPointsDraft`, `benefitsDraft`, `featuresDraft` si vienen; si no vienen, puedes pedir aprobaci\xF3n general y llamar `resolve_pending_action` con solo `{ kind: "angle_review_approval" }` para aprobar tal cual.\n\nSi ves `gapDurationChoice` (a nivel ra\xEDz de la respuesta, o dentro de `pendingUserAction` cuando el kind es `angle_review_approval`),
|
|
1394
|
+
server.tool("get_task_status", 'Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase, aparezca `pendingUserAction`, o llegue al timeout (max 120s). Cuando la tarea queda en `done`, la respuesta incluye un segundo bloque markdown con previews inline de las im\xE1genes, links descargables al video MP4 y un enlace a Product Maker para ver el resultado completo.\n\nIMPORTANTE \u2014 `pendingUserAction`: si est\xE1 presente, el pipeline est\xE1 PARADO esperando que el usuario decida algo. Mira el `kind` y llama `resolve_pending_action` con `action: { kind: <mismo kind>, ... }`; sin esto la tarea NUNCA avanza por m\xE1s que esperes. Kinds actuales:\n- `product_reference_confirmation`: el clasificador no est\xE1 seguro si las fotos son variantes del mismo producto o productos distintos. Mu\xE9strale al usuario `targetProductLabel` + `reason`, recoge su respuesta, y usa SOLO valores presentes en `options`.\n- `angle_review_approval`: la IA ya gener\xF3 el \xE1ngulo/revisi\xF3n antes de renderizar. Mu\xE9strale `customAngleDraft`, `painPointsDraft`, `benefitsDraft`, `featuresDraft` si vienen; si no vienen, puedes pedir aprobaci\xF3n general y llamar `resolve_pending_action` con solo `{ kind: "angle_review_approval" }` para aprobar tal cual.\n\nSi ves `gapDurationChoice` (a nivel ra\xEDz de la respuesta, o dentro de `pendingUserAction` cuando el kind es `angle_review_approval`), mu\xE9strale al usuario la duraci\xF3n recomendada y `estimatedDeltaCredits` si aparece. Con confirmaci\xF3n expl\xEDcita, llama `resolve_pending_action` con `action: { kind: "angle_review_approval", gapDurationSeconds: 30 }` para actualizar solo ese \xE1ngulo. Omitir el campo o enviar 15 conserva la duraci\xF3n cotizada; 30 se rechaza si no hay una recomendaci\xF3n pendiente de 30s.\nUse this to check on a task progress; pass waitSeconds for a near-synchronous response.', GetTaskStatusInput.shape, async (input, extra) => {
|
|
1314
1395
|
try {
|
|
1315
1396
|
const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`), "fetcher");
|
|
1316
1397
|
const signal = extra?.signal;
|
|
@@ -1367,7 +1448,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
1367
1448
|
throw translateError(e);
|
|
1368
1449
|
}
|
|
1369
1450
|
});
|
|
1370
|
-
server.tool("resolve_pending_action", 'Resuelve una acci\xF3n que el pipeline est\xE1 esperando del usuario. La llamas cuando `get_task_status` devuelve `pendingUserAction` (sin esto la tarea NUNCA avanza por m\xE1s que esperes). El `action.kind` que mandas DEBE coincidir con el `pendingUserAction.kind` que viste en el status \u2014 es el contrato de simetr\xEDa que mantiene este tool como \xFAnica superficie de resoluci\xF3n de gates.\n\nKINDS soportados hoy:\n\n\u2022 `product_reference_confirmation` \u2014
|
|
1451
|
+
server.tool("resolve_pending_action", 'Resuelve una acci\xF3n que el pipeline est\xE1 esperando del usuario. La llamas cuando `get_task_status` devuelve `pendingUserAction` (sin esto la tarea NUNCA avanza por m\xE1s que esperes). El `action.kind` que mandas DEBE coincidir con el `pendingUserAction.kind` que viste en el status \u2014 es el contrato de simetr\xEDa que mantiene este tool como \xFAnica superficie de resoluci\xF3n de gates.\n\nKINDS soportados hoy:\n\n\u2022 `product_reference_confirmation` \u2014 confirma c\xF3mo agrupar las fotos usando solo los valores de `pendingUserAction.options`.\n\n\u2022 `angle_review_approval` \u2014 aprueba o edita el draft despu\xE9s de `pauseForAngleReview: true`. Puedes enviar `customAngle`, `painPoints`, `benefits` y `features`; todos son opcionales y el MCP conserva el draft persistido cuando no los cambias. Si `pendingUserAction.gapDurationChoice` recomienda 30s para un winner, muestra al usuario `recommendedSec` y `estimatedDeltaCredits`, pide confirmaci\xF3n expl\xEDcita y env\xEDa `gapDurationSeconds: 30` en esta misma acci\xF3n. El MCP valida que exista una recomendaci\xF3n pendiente de 30s y entonces actualiza solo el \xE1ngulo recomendado con `double_30s`; omitir el campo o enviar 15 mantiene la duraci\xF3n cotizada.\n\nFuturos kinds (no implementados a\xFAn) se agregar\xE1n como nuevas variantes del `action` discriminated union \u2014 el flujo "lee kind del status \u2192 manda mismo kind aqu\xED" sigue igual.', ResolvePendingActionInput.shape, async (input) => {
|
|
1371
1452
|
try {
|
|
1372
1453
|
const raw = await api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`);
|
|
1373
1454
|
const status = StatusForResolveSchema.parse(raw);
|
|
@@ -1420,7 +1501,108 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
1420
1501
|
if (input.action.painPoints != null) body.painPoints = input.action.painPoints;
|
|
1421
1502
|
if (input.action.benefits != null) body.benefits = input.action.benefits;
|
|
1422
1503
|
if (input.action.features != null) body.features = input.action.features;
|
|
1423
|
-
|
|
1504
|
+
if (input.action.gapDurationSeconds != null && !pending.gapDurationChoice) {
|
|
1505
|
+
throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para elegir una duraci\xF3n. Consulta get_task_status y confirma el winner primero.");
|
|
1506
|
+
}
|
|
1507
|
+
if (input.action.gapDurationSeconds === 30) {
|
|
1508
|
+
const choice = pending.gapDurationChoice;
|
|
1509
|
+
if (!choice) throw new Error("No hay una recomendaci\xF3n gapDurationChoice pendiente para aceptar 30s.");
|
|
1510
|
+
if (choice.recommendedSec !== 30) {
|
|
1511
|
+
throw new Error(`La recomendaci\xF3n de duraci\xF3n para este winner es ${choice.recommendedSec}s; no se puede solicitar 30s con este estado.`);
|
|
1512
|
+
}
|
|
1513
|
+
body.angleVideoOptions = [
|
|
1514
|
+
{
|
|
1515
|
+
angleIndex: choice.angleIndex,
|
|
1516
|
+
ugcFormat: "double_30s"
|
|
1517
|
+
}
|
|
1518
|
+
];
|
|
1519
|
+
}
|
|
1520
|
+
const selectedAngleIndices = resolveSelectedAngleIndices(status, pending.gapDurationChoice);
|
|
1521
|
+
const resolvedTaskKind = resolveStatusTaskKind(status);
|
|
1522
|
+
const isVideoTask = resolvedTaskKind === "video";
|
|
1523
|
+
const isAutopilotTask = resolvedTaskKind === "autopilot";
|
|
1524
|
+
if ((isVideoTask || isAutopilotTask) && body.customAngle != null) {
|
|
1525
|
+
const existingOptions = Array.isArray(body.angleVideoOptions) ? body.angleVideoOptions : [];
|
|
1526
|
+
const optionsByIndex = new Map(existingOptions.map((option) => [
|
|
1527
|
+
option.angleIndex,
|
|
1528
|
+
{
|
|
1529
|
+
...option
|
|
1530
|
+
}
|
|
1531
|
+
]));
|
|
1532
|
+
for (const angleIndex of selectedAngleIndices) {
|
|
1533
|
+
optionsByIndex.set(angleIndex, {
|
|
1534
|
+
...optionsByIndex.get(angleIndex) ?? {
|
|
1535
|
+
angleIndex
|
|
1536
|
+
},
|
|
1537
|
+
angle: body.customAngle
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
body.angleVideoOptions = [
|
|
1541
|
+
...optionsByIndex.values()
|
|
1542
|
+
].sort((left, right) => Number(left.angleIndex) - Number(right.angleIndex));
|
|
1543
|
+
}
|
|
1544
|
+
if (isVideoTask) {
|
|
1545
|
+
body.selectedAngleIndices = selectedAngleIndices;
|
|
1546
|
+
const r2 = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/approve-video-angles`, body);
|
|
1547
|
+
return asText(r2);
|
|
1548
|
+
}
|
|
1549
|
+
if (isAutopilotTask) {
|
|
1550
|
+
const resumeBody = {
|
|
1551
|
+
selectedAngleIndices,
|
|
1552
|
+
...status.ugcFormat ? {
|
|
1553
|
+
ugcFormat: status.ugcFormat
|
|
1554
|
+
} : {
|
|
1555
|
+
ugcFormat: "single_15s"
|
|
1556
|
+
},
|
|
1557
|
+
...status.videoStyle ? {
|
|
1558
|
+
videoStyle: status.videoStyle
|
|
1559
|
+
} : {},
|
|
1560
|
+
...status.videoModel ? {
|
|
1561
|
+
videoModel: status.videoModel
|
|
1562
|
+
} : {},
|
|
1563
|
+
...status.videoTier ? {
|
|
1564
|
+
videoTier: status.videoTier
|
|
1565
|
+
} : {},
|
|
1566
|
+
...status.videoResolution ? {
|
|
1567
|
+
videoResolution: status.videoResolution
|
|
1568
|
+
} : {},
|
|
1569
|
+
...status.narrativeStyle ? {
|
|
1570
|
+
narrativeStyle: status.narrativeStyle
|
|
1571
|
+
} : {},
|
|
1572
|
+
...body.customAngle != null ? {
|
|
1573
|
+
customAngleOverride: body.customAngle
|
|
1574
|
+
} : {},
|
|
1575
|
+
...body.painPoints != null ? {
|
|
1576
|
+
painPoints: body.painPoints
|
|
1577
|
+
} : {},
|
|
1578
|
+
...body.benefits != null ? {
|
|
1579
|
+
benefits: body.benefits
|
|
1580
|
+
} : {},
|
|
1581
|
+
...body.angleVideoOptions ? {
|
|
1582
|
+
angleVideoOptions: body.angleVideoOptions
|
|
1583
|
+
} : {}
|
|
1584
|
+
};
|
|
1585
|
+
if (body.features !== void 0) {
|
|
1586
|
+
await api.patchJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/update-product-features`, {
|
|
1587
|
+
features: body.features
|
|
1588
|
+
});
|
|
1589
|
+
}
|
|
1590
|
+
const r2 = await api.postJson(`/v1/ingest/${encodeURIComponent(input.taskId)}/resume`, resumeBody);
|
|
1591
|
+
return asText(r2);
|
|
1592
|
+
}
|
|
1593
|
+
const route = resolvedTaskKind === "image" ? `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-image-angles` : `/v1/tasks/${encodeURIComponent(input.taskId)}/approve-angles`;
|
|
1594
|
+
const imageBody = status.kind === "image" ? {
|
|
1595
|
+
...body.customAngle != null ? {
|
|
1596
|
+
customAngle: body.customAngle
|
|
1597
|
+
} : {},
|
|
1598
|
+
...body.painPoints != null ? {
|
|
1599
|
+
painPoints: body.painPoints
|
|
1600
|
+
} : {},
|
|
1601
|
+
...body.features != null ? {
|
|
1602
|
+
features: body.features
|
|
1603
|
+
} : {}
|
|
1604
|
+
} : body;
|
|
1605
|
+
const r = await api.postJson(route, imageBody);
|
|
1424
1606
|
return asText(r);
|
|
1425
1607
|
}
|
|
1426
1608
|
default: {
|
|
@@ -1464,9 +1646,12 @@ var VideoInput = z3.object({
|
|
|
1464
1646
|
"16:9",
|
|
1465
1647
|
"Auto"
|
|
1466
1648
|
]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
|
|
1467
|
-
durationSeconds: z3.
|
|
1468
|
-
|
|
1469
|
-
|
|
1649
|
+
durationSeconds: z3.union([
|
|
1650
|
+
z3.literal(15),
|
|
1651
|
+
z3.literal(30)
|
|
1652
|
+
]).default(15).describe("Opcional. Duraci\xF3n objetivo: 15s (single_call, default) o 30s (Multiescena, aproximadamente 30s y facturado por escena)."),
|
|
1653
|
+
videoPreset: VideoPresetEnum.default("auto").describe('Opcional. Tipo/narrativa del video \u2014 \xFAnico campo de "estilo" que el usuario controla (en la web es el \xFAnico dropdown). Default "auto" (la IA escoge tras analizar el producto). Los presets funcionan tanto a 15s como a 30s; el formato de 30s conserva el ritmo multi-escena. Opciones: "auto", "ugc_dynamic" (UGC cl\xE1sico testimonial multi-hook), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones numeradas), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n), "ugc_demo_review" (demo en uso a lo largo de d\xEDas), "problem_solution" (problema\u2192producto\u2192CTA), "cinematic" (historia visual premium), "pixar_problem" (problema animado 3D estilo Pixar), "pixar_sassy" (producto que habla con personalidad), "product_showcase" (sin actor, solo producto), "wearable_showcase" (ropa/accesorios puestos).'),
|
|
1654
|
+
videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video. "basic" = "Realista"; para durationSeconds=30 el sistema selecciona internamente Multiescena. No se expone el tier interno directamente.'),
|
|
1470
1655
|
videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render. En la web se pregunta "\xBFResoluci\xF3n del video?". "720p" = m\xE1xima nitidez (RECOMENDADO, default si se omite); "480p" = render m\xE1s econ\xF3mico. Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 720p).'),
|
|
1471
1656
|
features: z3.array(z3.string()).optional().describe('Opcional. Lista de features/beneficios clave a mencionar. Ej. ["resistente al agua", "bater\xEDa 24h", "carga inal\xE1mbrica"].'),
|
|
1472
1657
|
price: CreativeRefs.price,
|
|
@@ -1476,6 +1661,7 @@ var VideoInput = z3.object({
|
|
|
1476
1661
|
cta: z3.string().optional().describe('Opcional. Call-to-action final del video. Ej. "Compra ahora", "Pide el tuyo hoy", "Link en bio". Si se omite, la IA escribe uno.'),
|
|
1477
1662
|
scriptNotes: z3.string().optional().describe('Opcional. Notas/restricciones para el guion (la IA las usa al redactar). Ej. "No mencionar la marca X", "Tono divertido, no cl\xEDnico", "Mencionar 2x1". NO es el guion literal \u2014 para editar el guion palabra-por-palabra, pasa `pauseForVideoScriptReview: true` y usa get_video_script / approve_video_script tras la pausa.'),
|
|
1478
1663
|
pauseForVideoScriptReview: z3.boolean().optional().describe("Opcional. Si true, la tarea pausa despu\xE9s de generar el guion y antes de renderizar. \xDAsalo cuando el usuario quiera editar el guion antes de gastar cr\xE9ditos en el video. Tras la pausa: get_video_script para leerlo, approve_video_script para aprobar (con o sin ediciones). Default false."),
|
|
1664
|
+
pauseForAngleReview: z3.boolean().optional().describe("Opcional. Si true, pausa despu\xE9s de generar el \xE1ngulo para que el usuario lo confirme o edite antes del render. Default false."),
|
|
1479
1665
|
competitorScanEnabled: CreativeRefs.competitorScanEnabled,
|
|
1480
1666
|
brandId: CreativeRefs.brandId
|
|
1481
1667
|
}).strict();
|
|
@@ -1531,10 +1717,10 @@ var LandingInput = z3.object({
|
|
|
1531
1717
|
landingImageFormat: LandingImageFormatEnum.default("9:16").describe('Opcional. Formato/relaci\xF3n de aspecto de las im\xE1genes de la landing cl\xE1sica: "9:16" (vertical, por defecto), "4:5" (vertical compacto para feed) o "1:1" (cuadrado). Default "9:16".')
|
|
1532
1718
|
});
|
|
1533
1719
|
function registerCreativeTools(server, api, { transport = "http" } = {}) {
|
|
1534
|
-
server.tool("generate_video_creative", 'Genera UN video creativo independiente. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), title, primaryAngle, language, country, aspectRatio.\n\nANTES de invocar, PREG\xDANTALE al usuario por price y offer si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para price/offer, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos): durationSeconds (
|
|
1720
|
+
server.tool("generate_video_creative", 'Genera UN video creativo independiente. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), title, primaryAngle, language, country, aspectRatio.\n\nANTES de invocar, PREG\xDANTALE al usuario por price y offer si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para price/offer, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos): durationSeconds (15 o 30), videoPreset (tipo/narrativa del video), features, cta, scriptNotes (dirige el guion Y el ambiente/escenario/est\xE9tica del video, ej. "en una playa, opulento").\n\nESTILO: 15s usa "Realista" (single_call); 30s usa "Multiescena" (multi-toma, aproximadamente 30s). Multiescena se factura por escena, as\xED que avisa el costo antes de activarlo.\n\nGUION: el usuario NO escribe el guion desde cero \u2014 la IA lo redacta. Si quiere REVISARLO/EDITARLO antes de renderizar (igual que en la web), pasa `pauseForVideoScriptReview: true`; cuando la tarea quede pausada, usa get_video_script para leer las escenas, ofr\xE9celas al usuario, y llama approve_video_script con las ediciones. Para dirigir el guion ANTES de generar (sin pausar), usa scriptNotes. Para revisar el \xE1ngulo y decidir duraci\xF3n tras el an\xE1lisis de competencia, pasa `pauseForAngleReview: true` y luego resuelve `angle_review_approval` con `gapDurationSeconds`.', VideoInput.shape, async (input) => {
|
|
1535
1721
|
try {
|
|
1536
1722
|
const resolved = resolveVideoPreset(input.videoPreset);
|
|
1537
|
-
const wireTier =
|
|
1723
|
+
const wireTier = resolveWireVideoTier(input.videoTier, input.durationSeconds);
|
|
1538
1724
|
const { videoTier: _publicTier, videoResolution: _vr, ...rest } = input;
|
|
1539
1725
|
const wirePayload = {
|
|
1540
1726
|
...rest
|
|
@@ -1651,6 +1837,23 @@ function registerConnectionTools(server, api) {
|
|
|
1651
1837
|
throw translateError(e);
|
|
1652
1838
|
}
|
|
1653
1839
|
});
|
|
1840
|
+
server.tool("list_shopify_products", 'Busca productos en una tienda Shopify conectada para elegir el target de una republicaci\xF3n. Usa el `id` devuelto junto con `publicationMode="existing"` en `publish_to_shopify`. Si `taskId` se proporciona, la respuesta tambi\xE9n indica el producto publicado actualmente para esa tarea y tienda. No modifica Shopify.', z5.object({
|
|
1841
|
+
shopDomain: z5.string().min(3).max(120),
|
|
1842
|
+
query: z5.string().trim().max(120).optional(),
|
|
1843
|
+
taskId: z5.string().uuid().optional()
|
|
1844
|
+
}).shape, async ({ shopDomain, query, taskId }) => {
|
|
1845
|
+
try {
|
|
1846
|
+
const params = new URLSearchParams({
|
|
1847
|
+
shopDomain
|
|
1848
|
+
});
|
|
1849
|
+
if (query) params.set("query", query);
|
|
1850
|
+
if (taskId) params.set("taskId", taskId);
|
|
1851
|
+
const r = await api.getJson(`/v1/shopify/products?${params.toString()}`);
|
|
1852
|
+
return asText(r);
|
|
1853
|
+
} catch (e) {
|
|
1854
|
+
throw translateError(e);
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1654
1857
|
server.tool("list_meta_ad_accounts", "Lista las cuentas publicitarias de Meta conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar Meta en productmaker.app/integrations.", z5.object({}).shape, async () => {
|
|
1655
1858
|
try {
|
|
1656
1859
|
const r = await api.getJson("/v1/facebook/ad-accounts");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@productmaker/mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Product Maker MCP server — cost products, generate dropshipping landing pages, UGC videos, and ad creatives, and publish to Shopify and Meta Ads from Claude, ChatGPT, or Gemini.",
|