@productmaker/mcp 0.9.0 → 0.11.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/dist/http.js +374 -104
- package/dist/stdio.js +374 -104
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -391,106 +391,6 @@ __name(sleep, "sleep");
|
|
|
391
391
|
|
|
392
392
|
// src/errors.ts
|
|
393
393
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
394
|
-
var McpErrorCode = {
|
|
395
|
-
INVALID_API_KEY: "INVALID_API_KEY",
|
|
396
|
-
REVOKED_API_KEY: "REVOKED_API_KEY",
|
|
397
|
-
RATE_LIMITED: "RATE_LIMITED",
|
|
398
|
-
INVALID_INPUT: "INVALID_INPUT",
|
|
399
|
-
IMAGE_DOWNLOAD_FAILED: "IMAGE_DOWNLOAD_FAILED",
|
|
400
|
-
IMAGE_TOO_LARGE: "IMAGE_TOO_LARGE",
|
|
401
|
-
IMAGE_TYPE_UNSUPPORTED: "IMAGE_TYPE_UNSUPPORTED",
|
|
402
|
-
IMAGE_PATH_NOT_FOUND: "IMAGE_PATH_NOT_FOUND",
|
|
403
|
-
IMAGE_PATH_NOT_READABLE: "IMAGE_PATH_NOT_READABLE",
|
|
404
|
-
IMAGE_PATH_NOT_A_FILE: "IMAGE_PATH_NOT_A_FILE",
|
|
405
|
-
PATH_NOT_SUPPORTED_IN_HTTP_MODE: "PATH_NOT_SUPPORTED_IN_HTTP_MODE",
|
|
406
|
-
TASK_NOT_FOUND: "TASK_NOT_FOUND",
|
|
407
|
-
TASK_NOT_READY: "TASK_NOT_READY",
|
|
408
|
-
CAMPAIGN_NOT_FOUND: "CAMPAIGN_NOT_FOUND",
|
|
409
|
-
NO_SHOPIFY_SHOPS: "NO_SHOPIFY_SHOPS",
|
|
410
|
-
NO_META_AD_ACCOUNTS: "NO_META_AD_ACCOUNTS",
|
|
411
|
-
SHOPIFY_NOT_CONNECTED: "SHOPIFY_NOT_CONNECTED",
|
|
412
|
-
META_CAPABILITY_MISSING: "META_CAPABILITY_MISSING",
|
|
413
|
-
TIKTOK_NOT_CONNECTED: "TIKTOK_NOT_CONNECTED",
|
|
414
|
-
UPSTREAM_TIMEOUT: "UPSTREAM_TIMEOUT",
|
|
415
|
-
UPSTREAM_UNAVAILABLE: "UPSTREAM_UNAVAILABLE",
|
|
416
|
-
INTERNAL: "INTERNAL"
|
|
417
|
-
};
|
|
418
|
-
var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
|
|
419
|
-
McpErrorCode.INVALID_INPUT,
|
|
420
|
-
McpErrorCode.INVALID_API_KEY,
|
|
421
|
-
McpErrorCode.REVOKED_API_KEY,
|
|
422
|
-
McpErrorCode.IMAGE_PATH_NOT_FOUND,
|
|
423
|
-
McpErrorCode.IMAGE_PATH_NOT_READABLE,
|
|
424
|
-
McpErrorCode.IMAGE_PATH_NOT_A_FILE,
|
|
425
|
-
McpErrorCode.PATH_NOT_SUPPORTED_IN_HTTP_MODE,
|
|
426
|
-
// The caller passed a campaignId that does not exist — semantically a
|
|
427
|
-
// bad parameter, not an internal failure. Matches the spec contract
|
|
428
|
-
// (mcp-server.spec.ts: "McpError INVALID_PARAMS code=CAMPAIGN_NOT_FOUND").
|
|
429
|
-
McpErrorCode.CAMPAIGN_NOT_FOUND
|
|
430
|
-
]);
|
|
431
|
-
function mcp(code, message, extra = {}) {
|
|
432
|
-
const errCode = INVALID_PARAM_CODES.has(code) ? ErrorCode.InvalidParams : ErrorCode.InternalError;
|
|
433
|
-
return new McpError(errCode, message, {
|
|
434
|
-
code,
|
|
435
|
-
...extra
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
__name(mcp, "mcp");
|
|
439
|
-
var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
|
|
440
|
-
"META_CAPABILITY_MISSING",
|
|
441
|
-
"NO_SHOPIFY_SHOPS",
|
|
442
|
-
"NO_META_AD_ACCOUNTS",
|
|
443
|
-
"SHOPIFY_NOT_CONNECTED",
|
|
444
|
-
"TIKTOK_NOT_CONNECTED",
|
|
445
|
-
"REVOKED_API_KEY",
|
|
446
|
-
"CAMPAIGN_NOT_FOUND"
|
|
447
|
-
]);
|
|
448
|
-
function isRecord(v) {
|
|
449
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
450
|
-
}
|
|
451
|
-
__name(isRecord, "isRecord");
|
|
452
|
-
function translateError(err) {
|
|
453
|
-
if (err instanceof ResolveImageError) {
|
|
454
|
-
return mcp(McpErrorCode[err.code], err.message);
|
|
455
|
-
}
|
|
456
|
-
if (err instanceof ApiHttpError) {
|
|
457
|
-
const body = isRecord(err.body) ? err.body : {};
|
|
458
|
-
if (typeof body.code === "string" && PASSTHROUGH_CODES.has(body.code)) {
|
|
459
|
-
return mcp(body.code, String(body.message ?? body.code), {
|
|
460
|
-
...body.helpUrl ? {
|
|
461
|
-
helpUrl: body.helpUrl
|
|
462
|
-
} : {}
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
if (err.status === 401) {
|
|
466
|
-
return mcp(McpErrorCode.INVALID_API_KEY, "API key inv\xE1lida. Revisa tu PM_API_KEY en la configuraci\xF3n del cliente MCP.");
|
|
467
|
-
}
|
|
468
|
-
if (err.status === 429) {
|
|
469
|
-
return mcp(McpErrorCode.RATE_LIMITED, "Demasiadas solicitudes. Espera unos segundos.", {
|
|
470
|
-
retryAfterSeconds: Number(body.retryAfterSeconds ?? 30)
|
|
471
|
-
});
|
|
472
|
-
}
|
|
473
|
-
if (err.status === 400) {
|
|
474
|
-
return mcp(McpErrorCode.INVALID_INPUT, typeof body.message === "string" ? body.message : "Entrada inv\xE1lida.");
|
|
475
|
-
}
|
|
476
|
-
if (err.status === 404) {
|
|
477
|
-
const isTask = body.resource === "task" || /task/i.test(String(body.message ?? ""));
|
|
478
|
-
return mcp(isTask ? McpErrorCode.TASK_NOT_FOUND : McpErrorCode.INVALID_INPUT, "Recurso no encontrado.");
|
|
479
|
-
}
|
|
480
|
-
if (err.status === 409) {
|
|
481
|
-
return mcp(McpErrorCode.TASK_NOT_READY, "La tarea a\xFAn no est\xE1 lista para esta acci\xF3n.");
|
|
482
|
-
}
|
|
483
|
-
if (err.status >= 500) {
|
|
484
|
-
return mcp(McpErrorCode.UPSTREAM_UNAVAILABLE, "El servicio Product Maker no respondi\xF3. Intenta de nuevo en un momento.");
|
|
485
|
-
}
|
|
486
|
-
return mcp(McpErrorCode.INTERNAL, `HTTP ${err.status}`);
|
|
487
|
-
}
|
|
488
|
-
if (err instanceof Error && /aborted|timeout/i.test(err.message)) {
|
|
489
|
-
return mcp(McpErrorCode.UPSTREAM_TIMEOUT, "Tiempo de espera agotado.");
|
|
490
|
-
}
|
|
491
|
-
return mcp(McpErrorCode.INTERNAL, err instanceof Error ? err.message : "Error interno.");
|
|
492
|
-
}
|
|
493
|
-
__name(translateError, "translateError");
|
|
494
394
|
|
|
495
395
|
// src/tools/shared.ts
|
|
496
396
|
import { z } from "zod";
|
|
@@ -582,6 +482,10 @@ function resolveVideoTier(tier) {
|
|
|
582
482
|
return tier === "basic" ? "grok" : "standard";
|
|
583
483
|
}
|
|
584
484
|
__name(resolveVideoTier, "resolveVideoTier");
|
|
485
|
+
var VideoResolutionEnum = z.enum([
|
|
486
|
+
"480p",
|
|
487
|
+
"720p"
|
|
488
|
+
]);
|
|
585
489
|
var VideoStyleEnum = z.enum([
|
|
586
490
|
"ugc",
|
|
587
491
|
"cartoon_3d",
|
|
@@ -678,6 +582,10 @@ function asText(value) {
|
|
|
678
582
|
};
|
|
679
583
|
}
|
|
680
584
|
__name(asText, "asText");
|
|
585
|
+
function isRecord(v) {
|
|
586
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
587
|
+
}
|
|
588
|
+
__name(isRecord, "isRecord");
|
|
681
589
|
function escapeMdLabel(s) {
|
|
682
590
|
return s.replace(/[\[\]\(\)\{\}`<>\r\n\\*_]/g, " ").replace(/\s+/g, " ").trim();
|
|
683
591
|
}
|
|
@@ -714,6 +622,341 @@ async function submitCreative(api, opts) {
|
|
|
714
622
|
}
|
|
715
623
|
__name(submitCreative, "submitCreative");
|
|
716
624
|
|
|
625
|
+
// src/errors.ts
|
|
626
|
+
var McpErrorCode = {
|
|
627
|
+
// Auth / API key
|
|
628
|
+
INVALID_API_KEY: "INVALID_API_KEY",
|
|
629
|
+
REVOKED_API_KEY: "REVOKED_API_KEY",
|
|
630
|
+
// Rate limit / quota
|
|
631
|
+
RATE_LIMITED: "RATE_LIMITED",
|
|
632
|
+
// Input
|
|
633
|
+
INVALID_INPUT: "INVALID_INPUT",
|
|
634
|
+
// Image upload (MCP-local)
|
|
635
|
+
IMAGE_DOWNLOAD_FAILED: "IMAGE_DOWNLOAD_FAILED",
|
|
636
|
+
IMAGE_TOO_LARGE: "IMAGE_TOO_LARGE",
|
|
637
|
+
IMAGE_TYPE_UNSUPPORTED: "IMAGE_TYPE_UNSUPPORTED",
|
|
638
|
+
IMAGE_PATH_NOT_FOUND: "IMAGE_PATH_NOT_FOUND",
|
|
639
|
+
IMAGE_PATH_NOT_READABLE: "IMAGE_PATH_NOT_READABLE",
|
|
640
|
+
IMAGE_PATH_NOT_A_FILE: "IMAGE_PATH_NOT_A_FILE",
|
|
641
|
+
PATH_NOT_SUPPORTED_IN_HTTP_MODE: "PATH_NOT_SUPPORTED_IN_HTTP_MODE",
|
|
642
|
+
// Task lifecycle
|
|
643
|
+
TASK_NOT_FOUND: "TASK_NOT_FOUND",
|
|
644
|
+
TASK_NOT_READY: "TASK_NOT_READY",
|
|
645
|
+
// Connection presence
|
|
646
|
+
NO_SHOPIFY_SHOPS: "NO_SHOPIFY_SHOPS",
|
|
647
|
+
NO_META_AD_ACCOUNTS: "NO_META_AD_ACCOUNTS",
|
|
648
|
+
// Campaigns (shared Meta + TikTok)
|
|
649
|
+
CAMPAIGN_NOT_FOUND: "CAMPAIGN_NOT_FOUND",
|
|
650
|
+
CAMPAIGN_TASK_NOT_FOUND: "CAMPAIGN_TASK_NOT_FOUND",
|
|
651
|
+
CAMPAIGN_MISSING_LANDING_URL: "CAMPAIGN_MISSING_LANDING_URL",
|
|
652
|
+
CAMPAIGN_NO_CREATIVES: "CAMPAIGN_NO_CREATIVES",
|
|
653
|
+
CAMPAIGN_MISSING_THUMBNAILS: "CAMPAIGN_MISSING_THUMBNAILS",
|
|
654
|
+
// Shopify
|
|
655
|
+
SHOPIFY_NOT_CONNECTED: "SHOPIFY_NOT_CONNECTED",
|
|
656
|
+
SHOPIFY_TOKEN_EXPIRED: "SHOPIFY_TOKEN_EXPIRED",
|
|
657
|
+
SHOPIFY_SCOPE_UPGRADE_REQUIRED: "SHOPIFY_SCOPE_UPGRADE_REQUIRED",
|
|
658
|
+
SHOPIFY_PUBLISH_FAILED: "SHOPIFY_PUBLISH_FAILED",
|
|
659
|
+
// Meta / Facebook Ads
|
|
660
|
+
META_NO_FB_PAGE: "META_NO_FB_PAGE",
|
|
661
|
+
META_NO_ADS_CREATED: "META_NO_ADS_CREATED",
|
|
662
|
+
META_NO_ASSETS_READY: "META_NO_ASSETS_READY",
|
|
663
|
+
META_NO_THUMBNAIL_SOURCE: "META_NO_THUMBNAIL_SOURCE",
|
|
664
|
+
META_NO_LANDING_URL: "META_NO_LANDING_URL",
|
|
665
|
+
META_TASK_NOT_FOUND: "META_TASK_NOT_FOUND",
|
|
666
|
+
META_INVALID_DATA: "META_INVALID_DATA",
|
|
667
|
+
META_INVALID_AD_ACCOUNT: "META_INVALID_AD_ACCOUNT",
|
|
668
|
+
META_MISSING_PAYMENT: "META_MISSING_PAYMENT",
|
|
669
|
+
META_TOKEN_EXPIRED: "META_TOKEN_EXPIRED",
|
|
670
|
+
META_ACCOUNT_RESTRICTED: "META_ACCOUNT_RESTRICTED",
|
|
671
|
+
META_CAPABILITY_MISSING: "META_CAPABILITY_MISSING",
|
|
672
|
+
META_PERMISSION_DENIED: "META_PERMISSION_DENIED",
|
|
673
|
+
META_PIXEL_NOT_FOUND: "META_PIXEL_NOT_FOUND",
|
|
674
|
+
META_OBJECT_NOT_FOUND: "META_OBJECT_NOT_FOUND",
|
|
675
|
+
META_AI_DISCLOSURE_REQUIRED: "META_AI_DISCLOSURE_REQUIRED",
|
|
676
|
+
META_BID_STRATEGY_REQUIRES_AMOUNT: "META_BID_STRATEGY_REQUIRES_AMOUNT",
|
|
677
|
+
META_CBO_BUDGET_CONFLICT: "META_CBO_BUDGET_CONFLICT",
|
|
678
|
+
META_BUDGET_MODE_CONFLICT: "META_BUDGET_MODE_CONFLICT",
|
|
679
|
+
META_AD_SET_BUDGET_REQUIRED: "META_AD_SET_BUDGET_REQUIRED",
|
|
680
|
+
META_DAILY_BUDGET_TOO_HIGH: "META_DAILY_BUDGET_TOO_HIGH",
|
|
681
|
+
META_CAMPAIGN_CONFIG_INVALID: "META_CAMPAIGN_CONFIG_INVALID",
|
|
682
|
+
META_WHATSAPP_DESTINATION_DISABLED: "META_WHATSAPP_DESTINATION_DISABLED",
|
|
683
|
+
// TikTok Ads
|
|
684
|
+
TIKTOK_NOT_CONNECTED: "TIKTOK_NOT_CONNECTED",
|
|
685
|
+
TIKTOK_TOKEN_EXPIRED: "TIKTOK_TOKEN_EXPIRED",
|
|
686
|
+
TIKTOK_PERMISSION_DENIED: "TIKTOK_PERMISSION_DENIED",
|
|
687
|
+
TIKTOK_MISSING_PAYMENT: "TIKTOK_MISSING_PAYMENT",
|
|
688
|
+
TIKTOK_PIXEL_NOT_FOUND: "TIKTOK_PIXEL_NOT_FOUND",
|
|
689
|
+
TIKTOK_INVALID_ADVERTISER: "TIKTOK_INVALID_ADVERTISER",
|
|
690
|
+
TIKTOK_ADVERTISER_REQUIRED: "TIKTOK_ADVERTISER_REQUIRED",
|
|
691
|
+
TIKTOK_IDENTITY_REQUIRED: "TIKTOK_IDENTITY_REQUIRED",
|
|
692
|
+
TIKTOK_IDENTITY_API_REQUIRED: "TIKTOK_IDENTITY_API_REQUIRED",
|
|
693
|
+
TIKTOK_VIDEO_UPLOAD_FAILED: "TIKTOK_VIDEO_UPLOAD_FAILED",
|
|
694
|
+
TIKTOK_VIDEO_UPLOAD_TIMEOUT: "TIKTOK_VIDEO_UPLOAD_TIMEOUT",
|
|
695
|
+
TIKTOK_RATE_LIMITED: "TIKTOK_RATE_LIMITED",
|
|
696
|
+
TIKTOK_SERVER_ERROR: "TIKTOK_SERVER_ERROR",
|
|
697
|
+
TIKTOK_INVALID_DATA: "TIKTOK_INVALID_DATA",
|
|
698
|
+
// Generic transport / catch-all
|
|
699
|
+
UPSTREAM_TIMEOUT: "UPSTREAM_TIMEOUT",
|
|
700
|
+
UPSTREAM_UNAVAILABLE: "UPSTREAM_UNAVAILABLE",
|
|
701
|
+
INTERNAL: "INTERNAL"
|
|
702
|
+
};
|
|
703
|
+
var CODE_MESSAGES = {
|
|
704
|
+
// Shopify
|
|
705
|
+
SHOPIFY_NOT_CONNECTED: "No tienes ninguna tienda Shopify conectada. Conecta una en https://productmaker.app/integrations antes de publicar.",
|
|
706
|
+
SHOPIFY_TOKEN_EXPIRED: "La sesi\xF3n con Shopify expir\xF3. Reconecta la tienda en https://productmaker.app/integrations.",
|
|
707
|
+
SHOPIFY_SCOPE_UPGRADE_REQUIRED: "La conexi\xF3n con Shopify necesita permisos adicionales. Recon\xE9ctala en https://productmaker.app/integrations para aprobar los nuevos permisos.",
|
|
708
|
+
SHOPIFY_PUBLISH_FAILED: "Shopify rechaz\xF3 la publicaci\xF3n. Verifica que el producto/landing no exista ya en la tienda y que tu plan permita publicar m\xE1s productos.",
|
|
709
|
+
// Meta — high-frequency
|
|
710
|
+
META_NO_FB_PAGE: "No encontramos una P\xE1gina de Facebook asociada a tu cuenta. Crea una en https://www.facebook.com/pages/create antes de publicar (Meta exige una Page para correr ads).",
|
|
711
|
+
META_NO_ADS_CREATED: "No se pudieron crear los ads. Lo m\xE1s probable: la cuenta de Meta Ads no tiene m\xE9todo de pago, falta vincular una P\xE1gina, o la app de Facebook perdi\xF3 permisos. Verifica los tres en https://business.facebook.com.",
|
|
712
|
+
META_NO_ASSETS_READY: "La tarea a\xFAn no tiene videos ni ad creative listos para publicar. Espera a que termine la generaci\xF3n o regenera los assets.",
|
|
713
|
+
META_NO_THUMBNAIL_SOURCE: "No hay imagen disponible para usar de portada del video. Sube una imagen de producto o genera el ad creative antes de publicar.",
|
|
714
|
+
META_NO_LANDING_URL: "Falta la URL de la landing. P\xE1sala en `landingUrl` o publica primero a Shopify con `publish_to_shopify`.",
|
|
715
|
+
META_TASK_NOT_FOUND: "La tarea no existe o no pertenece a este usuario. Verifica el `taskId`.",
|
|
716
|
+
META_INVALID_DATA: "Meta rechaz\xF3 la solicitud por datos inv\xE1lidos. Verifica que `adAccountId` y `pixelId` sean correctos.",
|
|
717
|
+
META_INVALID_AD_ACCOUNT: "La cuenta publicitaria no existe o no tienes acceso. Confirma el `adAccountId` con `list_meta_ad_accounts`.",
|
|
718
|
+
META_MISSING_PAYMENT: "Tu cuenta de Meta Ads no tiene m\xE9todo de pago configurado. Visita https://business.facebook.com/billing_hub/payment_settings, selecciona la cuenta publicitaria y agrega una tarjeta antes de reintentar.",
|
|
719
|
+
META_TOKEN_EXPIRED: "Tu sesi\xF3n con Meta expir\xF3. Reconecta Facebook en https://productmaker.app/integrations.",
|
|
720
|
+
META_ACCOUNT_RESTRICTED: "Tu cuenta de Meta Ads est\xE1 temporalmente restringida para crear anuncios. Revisa avisos en https://business.facebook.com y completa la verificaci\xF3n requerida.",
|
|
721
|
+
META_CAPABILITY_MISSING: "Tu app de Facebook no tiene los permisos requeridos para esta cuenta publicitaria. Reconecta Facebook en https://productmaker.app/integrations y acepta TODOS los permisos solicitados (incluyendo business_management).",
|
|
722
|
+
META_PERMISSION_DENIED: "Tu conexi\xF3n de Facebook no tiene los permisos suficientes. Recon\xE9ctala en https://productmaker.app/integrations y acepta todos los permisos.",
|
|
723
|
+
META_PIXEL_NOT_FOUND: "El Pixel no existe o no est\xE1 asociado a la cuenta publicitaria. Verifica el `pixelId` en https://business.facebook.com/events_manager.",
|
|
724
|
+
META_OBJECT_NOT_FOUND: "El objeto solicitado (campa\xF1a/ad set/ad) no existe en Meta. Verifica el ID o crea uno nuevo.",
|
|
725
|
+
META_AI_DISCLOSURE_REQUIRED: "Meta rechaz\xF3 el ad por contenido IA no declarado. El sistema agrega la divulgaci\xF3n autom\xE1ticamente; si persiste, edita la copy para evitar lenguaje que Meta marque como enga\xF1oso.",
|
|
726
|
+
META_BID_STRATEGY_REQUIRES_AMOUNT: "Meta requiere un monto de puja expl\xEDcito para esta cuenta/presupuesto. Sube el presupuesto diario o desactiva las pujas con tope a nivel cuenta.",
|
|
727
|
+
META_CBO_BUDGET_CONFLICT: "Conflicto de presupuesto entre la campa\xF1a (CBO) y el ad set. No se puede mezclar.",
|
|
728
|
+
META_BUDGET_MODE_CONFLICT: "Meta recibi\xF3 presupuesto a nivel campa\xF1a Y a nivel ad set. Define s\xF3lo uno.",
|
|
729
|
+
META_AD_SET_BUDGET_REQUIRED: "Esta configuraci\xF3n de campa\xF1a requiere presupuesto a nivel ad set, no a nivel campa\xF1a.",
|
|
730
|
+
META_DAILY_BUDGET_TOO_HIGH: "El presupuesto diario excede el m\xE1ximo permitido por Meta para tu cuenta. B\xE1jalo y reintenta.",
|
|
731
|
+
META_CAMPAIGN_CONFIG_INVALID: "La configuraci\xF3n de la campa\xF1a es inv\xE1lida. Verifica `dailyBudgetUsd`, `targetCountries` y `campaignName`.",
|
|
732
|
+
META_WHATSAPP_DESTINATION_DISABLED: "El destino WhatsApp no est\xE1 habilitado para esta cuenta de Meta. Cambia el destino del ad o habilita WhatsApp Business en la cuenta.",
|
|
733
|
+
// TikTok
|
|
734
|
+
TIKTOK_NOT_CONNECTED: "No tienes TikTok Ads conectado. Con\xE9ctalo en https://productmaker.app/integrations.",
|
|
735
|
+
TIKTOK_TOKEN_EXPIRED: "La sesi\xF3n con TikTok expir\xF3. Recon\xE9ctalo en https://productmaker.app/integrations.",
|
|
736
|
+
TIKTOK_PERMISSION_DENIED: "Tu conexi\xF3n de TikTok no tiene los permisos suficientes. Recon\xE9ctala y acepta todos los permisos solicitados.",
|
|
737
|
+
TIKTOK_MISSING_PAYMENT: "Tu cuenta de TikTok Ads no tiene m\xE9todo de pago configurado. Agrega uno en https://ads.tiktok.com.",
|
|
738
|
+
TIKTOK_PIXEL_NOT_FOUND: "El Pixel de TikTok no existe o no est\xE1 asociado al advertiser. Verifica el `pixelId`.",
|
|
739
|
+
TIKTOK_INVALID_ADVERTISER: "El advertiser no existe o no tienes acceso. Confirma el `advertiserId` con `list_tiktok_advertisers`.",
|
|
740
|
+
TIKTOK_ADVERTISER_REQUIRED: "Falta el `advertiserId`. Obt\xE9n uno con `list_tiktok_advertisers` antes de publicar.",
|
|
741
|
+
TIKTOK_IDENTITY_REQUIRED: "TikTok requiere una identidad (perfil del anunciante) para publicar. Crea una en https://ads.tiktok.com.",
|
|
742
|
+
TIKTOK_IDENTITY_API_REQUIRED: "TikTok requiere habilitar la API de identidades en tu cuenta. Contacta a TikTok Ads Support.",
|
|
743
|
+
TIKTOK_VIDEO_UPLOAD_FAILED: "TikTok rechaz\xF3 la subida del video. Verifica que el formato sea MP4 < 500MB.",
|
|
744
|
+
TIKTOK_VIDEO_UPLOAD_TIMEOUT: "La subida del video a TikTok se demor\xF3 demasiado. Reintenta en unos minutos.",
|
|
745
|
+
TIKTOK_RATE_LIMITED: "TikTok limit\xF3 las solicitudes. Espera unos minutos antes de reintentar.",
|
|
746
|
+
TIKTOK_SERVER_ERROR: "TikTok devolvi\xF3 un error de servidor. Reintenta en unos minutos.",
|
|
747
|
+
TIKTOK_INVALID_DATA: "TikTok rechaz\xF3 la solicitud por datos inv\xE1lidos. Verifica `advertiserId`, `pixelId` y `campaignConfig`.",
|
|
748
|
+
// Campaign (shared)
|
|
749
|
+
CAMPAIGN_NOT_FOUND: "La campa\xF1a no existe o no pertenece a este usuario. Verifica el `campaignId`.",
|
|
750
|
+
CAMPAIGN_TASK_NOT_FOUND: "La tarea de la campa\xF1a no existe. Verifica el `taskId`.",
|
|
751
|
+
CAMPAIGN_MISSING_LANDING_URL: "Falta la URL de la landing en la campa\xF1a. Publica primero a Shopify o pasa `landingUrl` expl\xEDcitamente.",
|
|
752
|
+
CAMPAIGN_NO_CREATIVES: "La tarea no tiene creativos (video/imagen) listos para la campa\xF1a. Espera a que termine la generaci\xF3n.",
|
|
753
|
+
CAMPAIGN_MISSING_THUMBNAILS: "Faltan miniaturas para los videos. Verifica que la generaci\xF3n de imagen/video haya terminado.",
|
|
754
|
+
// Connections
|
|
755
|
+
NO_SHOPIFY_SHOPS: "No hay tiendas Shopify conectadas. Conecta una en https://productmaker.app/integrations.",
|
|
756
|
+
NO_META_AD_ACCOUNTS: "No hay cuentas de Meta Ads conectadas. Conecta Facebook en https://productmaker.app/integrations.",
|
|
757
|
+
// Task lifecycle
|
|
758
|
+
TASK_NOT_FOUND: "La tarea no existe o no pertenece a este usuario. Verifica el `taskId`.",
|
|
759
|
+
TASK_NOT_READY: "La tarea a\xFAn no est\xE1 lista para esta acci\xF3n. Espera a que termine y reintenta.",
|
|
760
|
+
// API key
|
|
761
|
+
INVALID_API_KEY: "API key inv\xE1lida. Revisa tu PM_API_KEY en la configuraci\xF3n del cliente MCP.",
|
|
762
|
+
REVOKED_API_KEY: "API key revocada. Genera una nueva en https://productmaker.app/account/api-keys.",
|
|
763
|
+
// Rate
|
|
764
|
+
RATE_LIMITED: "Demasiadas solicitudes. Espera unos segundos.",
|
|
765
|
+
// Generic
|
|
766
|
+
UPSTREAM_UNAVAILABLE: "El servicio Product Maker no respondi\xF3. Intenta de nuevo en un momento.",
|
|
767
|
+
UPSTREAM_TIMEOUT: "Tiempo de espera agotado. Reintenta en un momento.",
|
|
768
|
+
INTERNAL: "Error interno.",
|
|
769
|
+
INVALID_INPUT: "Entrada inv\xE1lida."
|
|
770
|
+
};
|
|
771
|
+
var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
|
|
772
|
+
// Input/auth
|
|
773
|
+
McpErrorCode.INVALID_INPUT,
|
|
774
|
+
McpErrorCode.INVALID_API_KEY,
|
|
775
|
+
McpErrorCode.REVOKED_API_KEY,
|
|
776
|
+
McpErrorCode.IMAGE_PATH_NOT_FOUND,
|
|
777
|
+
McpErrorCode.IMAGE_PATH_NOT_READABLE,
|
|
778
|
+
McpErrorCode.IMAGE_PATH_NOT_A_FILE,
|
|
779
|
+
McpErrorCode.PATH_NOT_SUPPORTED_IN_HTTP_MODE,
|
|
780
|
+
// Identifier mismatches
|
|
781
|
+
McpErrorCode.CAMPAIGN_NOT_FOUND,
|
|
782
|
+
McpErrorCode.CAMPAIGN_TASK_NOT_FOUND,
|
|
783
|
+
McpErrorCode.TASK_NOT_FOUND,
|
|
784
|
+
McpErrorCode.META_TASK_NOT_FOUND,
|
|
785
|
+
McpErrorCode.META_INVALID_AD_ACCOUNT,
|
|
786
|
+
McpErrorCode.META_PIXEL_NOT_FOUND,
|
|
787
|
+
McpErrorCode.META_OBJECT_NOT_FOUND,
|
|
788
|
+
McpErrorCode.TIKTOK_INVALID_ADVERTISER,
|
|
789
|
+
McpErrorCode.TIKTOK_ADVERTISER_REQUIRED,
|
|
790
|
+
McpErrorCode.TIKTOK_PIXEL_NOT_FOUND,
|
|
791
|
+
McpErrorCode.CAMPAIGN_MISSING_LANDING_URL,
|
|
792
|
+
McpErrorCode.META_NO_LANDING_URL,
|
|
793
|
+
McpErrorCode.META_DAILY_BUDGET_TOO_HIGH,
|
|
794
|
+
McpErrorCode.META_CAMPAIGN_CONFIG_INVALID,
|
|
795
|
+
McpErrorCode.META_BID_STRATEGY_REQUIRES_AMOUNT,
|
|
796
|
+
McpErrorCode.META_CBO_BUDGET_CONFLICT,
|
|
797
|
+
McpErrorCode.META_BUDGET_MODE_CONFLICT,
|
|
798
|
+
McpErrorCode.META_AD_SET_BUDGET_REQUIRED,
|
|
799
|
+
// Validation-shape backend errors (data-level rejection)
|
|
800
|
+
McpErrorCode.META_INVALID_DATA,
|
|
801
|
+
McpErrorCode.TIKTOK_INVALID_DATA,
|
|
802
|
+
// Out-of-band user-resolvable auth/billing/connection — caller fixes by
|
|
803
|
+
// going to Facebook/TikTok/Shopify and reconnecting, finishing
|
|
804
|
+
// verification, or adding a payment method. None of these are transient,
|
|
805
|
+
// so InternalError would mislead the agent into retrying blindly.
|
|
806
|
+
McpErrorCode.META_PERMISSION_DENIED,
|
|
807
|
+
McpErrorCode.META_CAPABILITY_MISSING,
|
|
808
|
+
McpErrorCode.META_TOKEN_EXPIRED,
|
|
809
|
+
McpErrorCode.META_MISSING_PAYMENT,
|
|
810
|
+
McpErrorCode.META_ACCOUNT_RESTRICTED,
|
|
811
|
+
McpErrorCode.META_NO_FB_PAGE,
|
|
812
|
+
McpErrorCode.TIKTOK_TOKEN_EXPIRED,
|
|
813
|
+
McpErrorCode.TIKTOK_PERMISSION_DENIED,
|
|
814
|
+
McpErrorCode.TIKTOK_MISSING_PAYMENT,
|
|
815
|
+
McpErrorCode.TIKTOK_IDENTITY_REQUIRED,
|
|
816
|
+
McpErrorCode.TIKTOK_IDENTITY_API_REQUIRED,
|
|
817
|
+
McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
|
|
818
|
+
McpErrorCode.SHOPIFY_SCOPE_UPGRADE_REQUIRED,
|
|
819
|
+
McpErrorCode.SHOPIFY_NOT_CONNECTED,
|
|
820
|
+
McpErrorCode.TIKTOK_NOT_CONNECTED,
|
|
821
|
+
McpErrorCode.NO_SHOPIFY_SHOPS,
|
|
822
|
+
McpErrorCode.NO_META_AD_ACCOUNTS
|
|
823
|
+
]);
|
|
824
|
+
var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
|
|
825
|
+
// Shopify
|
|
826
|
+
McpErrorCode.SHOPIFY_NOT_CONNECTED,
|
|
827
|
+
McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
|
|
828
|
+
McpErrorCode.SHOPIFY_SCOPE_UPGRADE_REQUIRED,
|
|
829
|
+
McpErrorCode.SHOPIFY_PUBLISH_FAILED,
|
|
830
|
+
// Meta
|
|
831
|
+
McpErrorCode.META_NO_FB_PAGE,
|
|
832
|
+
McpErrorCode.META_NO_ADS_CREATED,
|
|
833
|
+
McpErrorCode.META_NO_ASSETS_READY,
|
|
834
|
+
McpErrorCode.META_NO_THUMBNAIL_SOURCE,
|
|
835
|
+
McpErrorCode.META_NO_LANDING_URL,
|
|
836
|
+
McpErrorCode.META_TASK_NOT_FOUND,
|
|
837
|
+
McpErrorCode.META_INVALID_DATA,
|
|
838
|
+
McpErrorCode.META_INVALID_AD_ACCOUNT,
|
|
839
|
+
McpErrorCode.META_MISSING_PAYMENT,
|
|
840
|
+
McpErrorCode.META_TOKEN_EXPIRED,
|
|
841
|
+
McpErrorCode.META_ACCOUNT_RESTRICTED,
|
|
842
|
+
McpErrorCode.META_CAPABILITY_MISSING,
|
|
843
|
+
McpErrorCode.META_PERMISSION_DENIED,
|
|
844
|
+
McpErrorCode.META_PIXEL_NOT_FOUND,
|
|
845
|
+
McpErrorCode.META_OBJECT_NOT_FOUND,
|
|
846
|
+
McpErrorCode.META_AI_DISCLOSURE_REQUIRED,
|
|
847
|
+
McpErrorCode.META_BID_STRATEGY_REQUIRES_AMOUNT,
|
|
848
|
+
McpErrorCode.META_CBO_BUDGET_CONFLICT,
|
|
849
|
+
McpErrorCode.META_BUDGET_MODE_CONFLICT,
|
|
850
|
+
McpErrorCode.META_AD_SET_BUDGET_REQUIRED,
|
|
851
|
+
McpErrorCode.META_DAILY_BUDGET_TOO_HIGH,
|
|
852
|
+
McpErrorCode.META_CAMPAIGN_CONFIG_INVALID,
|
|
853
|
+
McpErrorCode.META_WHATSAPP_DESTINATION_DISABLED,
|
|
854
|
+
// TikTok
|
|
855
|
+
McpErrorCode.TIKTOK_NOT_CONNECTED,
|
|
856
|
+
McpErrorCode.TIKTOK_TOKEN_EXPIRED,
|
|
857
|
+
McpErrorCode.TIKTOK_PERMISSION_DENIED,
|
|
858
|
+
McpErrorCode.TIKTOK_MISSING_PAYMENT,
|
|
859
|
+
McpErrorCode.TIKTOK_PIXEL_NOT_FOUND,
|
|
860
|
+
McpErrorCode.TIKTOK_INVALID_ADVERTISER,
|
|
861
|
+
McpErrorCode.TIKTOK_ADVERTISER_REQUIRED,
|
|
862
|
+
McpErrorCode.TIKTOK_IDENTITY_REQUIRED,
|
|
863
|
+
McpErrorCode.TIKTOK_IDENTITY_API_REQUIRED,
|
|
864
|
+
McpErrorCode.TIKTOK_VIDEO_UPLOAD_FAILED,
|
|
865
|
+
McpErrorCode.TIKTOK_VIDEO_UPLOAD_TIMEOUT,
|
|
866
|
+
McpErrorCode.TIKTOK_RATE_LIMITED,
|
|
867
|
+
McpErrorCode.TIKTOK_SERVER_ERROR,
|
|
868
|
+
McpErrorCode.TIKTOK_INVALID_DATA,
|
|
869
|
+
// Campaign (shared)
|
|
870
|
+
McpErrorCode.CAMPAIGN_NOT_FOUND,
|
|
871
|
+
McpErrorCode.CAMPAIGN_TASK_NOT_FOUND,
|
|
872
|
+
McpErrorCode.CAMPAIGN_MISSING_LANDING_URL,
|
|
873
|
+
McpErrorCode.CAMPAIGN_NO_CREATIVES,
|
|
874
|
+
McpErrorCode.CAMPAIGN_MISSING_THUMBNAILS,
|
|
875
|
+
// Connections
|
|
876
|
+
McpErrorCode.NO_SHOPIFY_SHOPS,
|
|
877
|
+
McpErrorCode.NO_META_AD_ACCOUNTS,
|
|
878
|
+
// Auth
|
|
879
|
+
McpErrorCode.REVOKED_API_KEY
|
|
880
|
+
]);
|
|
881
|
+
function mcp(code, message, extra = {}) {
|
|
882
|
+
const errCode = INVALID_PARAM_CODES.has(code) ? ErrorCode.InvalidParams : ErrorCode.InternalError;
|
|
883
|
+
return new McpError(errCode, message, {
|
|
884
|
+
code,
|
|
885
|
+
...extra
|
|
886
|
+
});
|
|
887
|
+
}
|
|
888
|
+
__name(mcp, "mcp");
|
|
889
|
+
var GENERIC_BACKEND_MESSAGES = /* @__PURE__ */ new Set([
|
|
890
|
+
"No ads could be created for the campaign.",
|
|
891
|
+
"No assets could be created for the campaign.",
|
|
892
|
+
"Invalid data provided to Meta API",
|
|
893
|
+
"Insufficient permissions for Meta account",
|
|
894
|
+
"Meta session has expired",
|
|
895
|
+
"Meta Ads account has no payment method configured"
|
|
896
|
+
]);
|
|
897
|
+
function looksLikeMeaningfulBackendMessage(message, mapped) {
|
|
898
|
+
if (!message || message.length === 0) return false;
|
|
899
|
+
if (GENERIC_BACKEND_MESSAGES.has(message)) return false;
|
|
900
|
+
const norm = /* @__PURE__ */ __name((s) => s.toLowerCase().replace(/[.\s]+$/, ""), "norm");
|
|
901
|
+
if (norm(mapped).includes(norm(message))) return false;
|
|
902
|
+
return true;
|
|
903
|
+
}
|
|
904
|
+
__name(looksLikeMeaningfulBackendMessage, "looksLikeMeaningfulBackendMessage");
|
|
905
|
+
function resolveMessage(code, backendMessage) {
|
|
906
|
+
const mapped = CODE_MESSAGES[code];
|
|
907
|
+
const backend = typeof backendMessage === "string" ? backendMessage.trim() : "";
|
|
908
|
+
if (mapped && backend && looksLikeMeaningfulBackendMessage(backend, mapped)) {
|
|
909
|
+
return `${mapped} (Detalle: ${backend})`;
|
|
910
|
+
}
|
|
911
|
+
if (mapped) return mapped;
|
|
912
|
+
if (backend) return backend;
|
|
913
|
+
return `El servicio devolvi\xF3 un error (c\xF3digo: ${code}). Intenta de nuevo en un momento o contacta soporte si persiste.`;
|
|
914
|
+
}
|
|
915
|
+
__name(resolveMessage, "resolveMessage");
|
|
916
|
+
function translateError(err) {
|
|
917
|
+
if (err instanceof ResolveImageError) {
|
|
918
|
+
return mcp(McpErrorCode[err.code], err.message);
|
|
919
|
+
}
|
|
920
|
+
if (err instanceof ApiHttpError) {
|
|
921
|
+
const body = isRecord(err.body) ? err.body : {};
|
|
922
|
+
if (typeof body.code === "string" && PASSTHROUGH_CODES.has(body.code)) {
|
|
923
|
+
const code = body.code;
|
|
924
|
+
return mcp(code, resolveMessage(code, body.message), {
|
|
925
|
+
...body.helpUrl ? {
|
|
926
|
+
helpUrl: body.helpUrl
|
|
927
|
+
} : {}
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
if (err.status === 401) {
|
|
931
|
+
return mcp(McpErrorCode.INVALID_API_KEY, resolveMessage(McpErrorCode.INVALID_API_KEY, void 0));
|
|
932
|
+
}
|
|
933
|
+
if (err.status === 429) {
|
|
934
|
+
return mcp(McpErrorCode.RATE_LIMITED, resolveMessage(McpErrorCode.RATE_LIMITED, void 0), {
|
|
935
|
+
retryAfterSeconds: Number(body.retryAfterSeconds ?? 30)
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
if (err.status === 400) {
|
|
939
|
+
return mcp(McpErrorCode.INVALID_INPUT, typeof body.message === "string" ? body.message : resolveMessage(McpErrorCode.INVALID_INPUT, void 0));
|
|
940
|
+
}
|
|
941
|
+
if (err.status === 404) {
|
|
942
|
+
const isTask = body.resource === "task" || /task/i.test(String(body.message ?? ""));
|
|
943
|
+
return mcp(isTask ? McpErrorCode.TASK_NOT_FOUND : McpErrorCode.INVALID_INPUT, resolveMessage(isTask ? McpErrorCode.TASK_NOT_FOUND : McpErrorCode.INVALID_INPUT, void 0));
|
|
944
|
+
}
|
|
945
|
+
if (err.status === 409) {
|
|
946
|
+
return mcp(McpErrorCode.TASK_NOT_READY, resolveMessage(McpErrorCode.TASK_NOT_READY, void 0));
|
|
947
|
+
}
|
|
948
|
+
if (err.status >= 500) {
|
|
949
|
+
return mcp(McpErrorCode.UPSTREAM_UNAVAILABLE, resolveMessage(McpErrorCode.UPSTREAM_UNAVAILABLE, void 0));
|
|
950
|
+
}
|
|
951
|
+
return mcp(McpErrorCode.INTERNAL, `HTTP ${err.status}`);
|
|
952
|
+
}
|
|
953
|
+
if (err instanceof Error && /aborted|timeout/i.test(err.message)) {
|
|
954
|
+
return mcp(McpErrorCode.UPSTREAM_TIMEOUT, resolveMessage(McpErrorCode.UPSTREAM_TIMEOUT, void 0));
|
|
955
|
+
}
|
|
956
|
+
return mcp(McpErrorCode.INTERNAL, err instanceof Error ? err.message : resolveMessage(McpErrorCode.INTERNAL, void 0));
|
|
957
|
+
}
|
|
958
|
+
__name(translateError, "translateError");
|
|
959
|
+
|
|
717
960
|
// src/asset-proxy.ts
|
|
718
961
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
719
962
|
var TOKEN_TTL_SECONDS = 3600;
|
|
@@ -931,6 +1174,7 @@ var CreateProductTaskInput = z2.object({
|
|
|
931
1174
|
offer: z2.string().optional().describe('Oferta o promoci\xF3n a destacar. Ej. "2x1", "Env\xEDo gratis hoy", "50% OFF". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar (es v\xE1lido que diga "sin oferta"). Si te dice "t\xFA elige", om\xEDtelo.'),
|
|
932
1175
|
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). Opciones: "auto", "ugc_dynamic" (UGC testimonial), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones, ideal 30s), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n, ideal 30s), "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.'),
|
|
933
1176
|
videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video (en la web la etiqueta es "\xBFCalidad de video?", 2 opciones \u2014 sin escalera de calidad, son ESTILOS distintos). "basic" = "Realista" (RECOMENDADO por la web): render de 15s con audio nativo, el m\xE1s natural \u2014 S\xD3LO v\xE1lido si la duraci\xF3n del video es 15s. "standard" = "Multiescena": m\xFAltiples tomas, hasta 30s. Si el usuario no menciona estilo, OMITE este campo y el backend usa su default. Para duraci\xF3n 30s s\xF3lo aplica "standard" (Realista est\xE1 vetada).'),
|
|
1177
|
+
videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render \u2014 aplica S\xD3LO al tier "basic" (Realista); "standard" la deriva solo. En la web se pregunta "\xBFResoluci\xF3n del video?" tras elegir "basic". "720p" = m\xE1xima nitidez (default si se omite); "480p" = m\xE1s econ\xF3mico (~la mitad del precio). Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 720p para basic).'),
|
|
934
1178
|
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."),
|
|
935
1179
|
angleNarratives: NarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional, override avanzado). Usa los mismos identificadores de narrativa que videoPreset (ej. "animated_problem_pixar", "review", "ugc_skeptic_confession"). angleNarratives[i] fuerza la narrativa del \xE1ngulo en la posici\xF3n i; si la lista es m\xE1s corta que la cantidad de \xE1ngulos, las posiciones restantes las elige la IA. Requiere multiAngleVideos:true para que haya varios \xE1ngulos. Si se omite, todas las narrativas se auto-seleccionan seg\xFAn el producto.'),
|
|
936
1180
|
outputs: z2.array(z2.enum([
|
|
@@ -1015,6 +1259,11 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
1015
1259
|
"options.videoStyle": resolved?.videoStyle,
|
|
1016
1260
|
"options.narrativeStyle": resolved?.narrativeStyle,
|
|
1017
1261
|
"options.videoTier": wireTier,
|
|
1262
|
+
// Resolution is meaningful ONLY for the basic/grok tier; for every
|
|
1263
|
+
// other tier the backend derives it. Forward it ONLY for grok so the
|
|
1264
|
+
// MCP never presents a "user choice where there is none" (the web has
|
|
1265
|
+
// no resolution knob for standard) — for non-grok it's dropped.
|
|
1266
|
+
"options.videoResolution": wireTier === "grok" ? input.videoResolution : void 0,
|
|
1018
1267
|
"options.multiAngleVideos": input.multiAngleVideos,
|
|
1019
1268
|
"options.angleNarratives": input.angleNarratives,
|
|
1020
1269
|
"options.selectedOutputs": input.outputs,
|
|
@@ -1173,6 +1422,7 @@ var VideoInput = z3.object({
|
|
|
1173
1422
|
durationSeconds: z3.number().int().min(5).max(60).optional().describe("Opcional. Duraci\xF3n del video en segundos (5-60). Default 15s. Para hooks cortos usa 8, para storytelling usa 30."),
|
|
1174
1423
|
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). 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, recomendado 30s), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n, recomendado 30s), "ugc_demo_review" (demo en uso a lo largo de d\xEDas), "problem_solution" (problema\u2192producto\u2192CTA), "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).'),
|
|
1175
1424
|
videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video (en la web la etiqueta es "\xBFCalidad de video?", 2 opciones \u2014 sin escalera de calidad, son ESTILOS distintos). "basic" = "Realista" (RECOMENDADO por la web): render de 15s con audio nativo, el m\xE1s natural \u2014 S\xD3LO v\xE1lido cuando durationSeconds \u2264 15 (el handler lo valida). "standard" = "Multiescena": m\xFAltiples tomas, hasta 30s. Si la duraci\xF3n es 30s, s\xF3lo aplica "standard". Si el usuario no menciona estilo, OMITE este campo y el backend usa su default.'),
|
|
1425
|
+
videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render \u2014 aplica S\xD3LO al tier "basic" (Realista); el tier "standard" la deriva solo. En la web se pregunta "\xBFResoluci\xF3n del video?" tras elegir "basic". "720p" = m\xE1xima nitidez (default si se omite); "480p" = m\xE1s econ\xF3mico (~la mitad del precio). Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 720p para basic).'),
|
|
1176
1426
|
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"].'),
|
|
1177
1427
|
price: CreativeRefs.price,
|
|
1178
1428
|
offer: CreativeRefs.offer,
|
|
@@ -1226,7 +1476,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
|
|
|
1226
1476
|
}
|
|
1227
1477
|
const resolved = resolveVideoPreset(input.videoPreset);
|
|
1228
1478
|
const wireTier = resolveVideoTier(input.videoTier);
|
|
1229
|
-
const { videoTier: _publicTier, ...rest } = input;
|
|
1479
|
+
const { videoTier: _publicTier, videoResolution: _vr, ...rest } = input;
|
|
1230
1480
|
const wirePayload = {
|
|
1231
1481
|
...rest
|
|
1232
1482
|
};
|
|
@@ -1235,6 +1485,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
|
|
|
1235
1485
|
wirePayload.narrativeStyle = resolved.narrativeStyle;
|
|
1236
1486
|
}
|
|
1237
1487
|
if (wireTier !== void 0) wirePayload.videoTier = wireTier;
|
|
1488
|
+
if (wireTier === "grok" && input.videoResolution) wirePayload.videoResolution = input.videoResolution;
|
|
1238
1489
|
return await submitCreative(api, {
|
|
1239
1490
|
path: "/v1/creatives/videos",
|
|
1240
1491
|
estimatedSeconds: 300,
|
|
@@ -1365,7 +1616,11 @@ import { z as z6 } from "zod";
|
|
|
1365
1616
|
var ShopifyInput = z6.object({
|
|
1366
1617
|
taskId: z6.string().min(1),
|
|
1367
1618
|
shopDomain: z6.string().min(1),
|
|
1368
|
-
|
|
1619
|
+
/** Override: number of landing images to include. Default = ALL images the
|
|
1620
|
+
* task generated (the MCP fetches /v1/results first to count). Bounded
|
|
1621
|
+
* generously here; the backend validates each index against
|
|
1622
|
+
* landing.images.length and rejects out-of-range. */
|
|
1623
|
+
numLandingImages: z6.number().int().min(1).max(20).optional(),
|
|
1369
1624
|
variantIndex: z6.number().int().min(0).optional(),
|
|
1370
1625
|
/** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
|
|
1371
1626
|
* When true, the landing renders variant-picker pills; when false, it ships
|
|
@@ -1398,10 +1653,12 @@ var TikTokInput = z6.object({
|
|
|
1398
1653
|
campaignConfig: McpCampaignConfig.optional()
|
|
1399
1654
|
});
|
|
1400
1655
|
function registerPublishTools(server, api) {
|
|
1401
|
-
server.tool("publish_to_shopify", "Publica la landing y producto generados en una tienda Shopify conectada. Precondici\xF3n: la tarea debe estar en status='complete' \u2014 usa `get_task_status` antes. ANTES de llamar preg\xFAntale al usuario \"\xBFTu producto tiene variantes (talla, color, etc.)?\" y pasa `showVariantPicker: true` si responde que s\xED \u2014 eso renderiza los pills de variantes en la landing. Por defecto
|
|
1656
|
+
server.tool("publish_to_shopify", "Publica la landing y producto generados en una tienda Shopify conectada. Precondici\xF3n: la tarea debe estar en status='complete' \u2014 usa `get_task_status` antes. ANTES de llamar preg\xFAntale al usuario \"\xBFTu producto tiene variantes (talla, color, etc.)?\" y pasa `showVariantPicker: true` si responde que s\xED \u2014 eso renderiza los pills de variantes en la landing. Por defecto publica TODAS las im\xE1genes que la tarea gener\xF3 para la landing; pasa `numLandingImages` s\xF3lo si el usuario te pide menos. La respuesta es markdown con el link a la landing y al producto.", ShopifyInput.shape, async (input) => {
|
|
1402
1657
|
try {
|
|
1403
|
-
const numImages = input.numLandingImages ?? 3;
|
|
1404
1658
|
const variantIndex = input.variantIndex ?? 0;
|
|
1659
|
+
const availableImages = await fetchLandingImageCount(api, input.taskId);
|
|
1660
|
+
const effectiveAvailable = availableImages > 0 ? availableImages : 3;
|
|
1661
|
+
const numImages = Math.min(input.numLandingImages ?? effectiveAvailable, effectiveAvailable);
|
|
1405
1662
|
const selectedLandingImages = Array.from({
|
|
1406
1663
|
length: numImages
|
|
1407
1664
|
}, (_, i) => ({
|
|
@@ -1478,6 +1735,19 @@ function renderShopifyPublishMarkdown(r) {
|
|
|
1478
1735
|
};
|
|
1479
1736
|
}
|
|
1480
1737
|
__name(renderShopifyPublishMarkdown, "renderShopifyPublishMarkdown");
|
|
1738
|
+
async function fetchLandingImageCount(api, taskId) {
|
|
1739
|
+
try {
|
|
1740
|
+
const r = await api.getJson(`/v1/results/${encodeURIComponent(taskId)}`);
|
|
1741
|
+
if (!isRecord(r)) return 0;
|
|
1742
|
+
const landing = r.landing;
|
|
1743
|
+
if (!isRecord(landing)) return 0;
|
|
1744
|
+
const images = landing.images;
|
|
1745
|
+
return Array.isArray(images) ? images.length : 0;
|
|
1746
|
+
} catch {
|
|
1747
|
+
return 0;
|
|
1748
|
+
}
|
|
1749
|
+
}
|
|
1750
|
+
__name(fetchLandingImageCount, "fetchLandingImageCount");
|
|
1481
1751
|
function toMetaCampaignOverrides(cfg) {
|
|
1482
1752
|
const out = {};
|
|
1483
1753
|
if (cfg.dailyBudgetUsd !== void 0) out.dailyBudgetUsd = cfg.dailyBudgetUsd;
|
package/dist/stdio.js
CHANGED
|
@@ -388,106 +388,6 @@ __name(sleep, "sleep");
|
|
|
388
388
|
|
|
389
389
|
// src/errors.ts
|
|
390
390
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
391
|
-
var McpErrorCode = {
|
|
392
|
-
INVALID_API_KEY: "INVALID_API_KEY",
|
|
393
|
-
REVOKED_API_KEY: "REVOKED_API_KEY",
|
|
394
|
-
RATE_LIMITED: "RATE_LIMITED",
|
|
395
|
-
INVALID_INPUT: "INVALID_INPUT",
|
|
396
|
-
IMAGE_DOWNLOAD_FAILED: "IMAGE_DOWNLOAD_FAILED",
|
|
397
|
-
IMAGE_TOO_LARGE: "IMAGE_TOO_LARGE",
|
|
398
|
-
IMAGE_TYPE_UNSUPPORTED: "IMAGE_TYPE_UNSUPPORTED",
|
|
399
|
-
IMAGE_PATH_NOT_FOUND: "IMAGE_PATH_NOT_FOUND",
|
|
400
|
-
IMAGE_PATH_NOT_READABLE: "IMAGE_PATH_NOT_READABLE",
|
|
401
|
-
IMAGE_PATH_NOT_A_FILE: "IMAGE_PATH_NOT_A_FILE",
|
|
402
|
-
PATH_NOT_SUPPORTED_IN_HTTP_MODE: "PATH_NOT_SUPPORTED_IN_HTTP_MODE",
|
|
403
|
-
TASK_NOT_FOUND: "TASK_NOT_FOUND",
|
|
404
|
-
TASK_NOT_READY: "TASK_NOT_READY",
|
|
405
|
-
CAMPAIGN_NOT_FOUND: "CAMPAIGN_NOT_FOUND",
|
|
406
|
-
NO_SHOPIFY_SHOPS: "NO_SHOPIFY_SHOPS",
|
|
407
|
-
NO_META_AD_ACCOUNTS: "NO_META_AD_ACCOUNTS",
|
|
408
|
-
SHOPIFY_NOT_CONNECTED: "SHOPIFY_NOT_CONNECTED",
|
|
409
|
-
META_CAPABILITY_MISSING: "META_CAPABILITY_MISSING",
|
|
410
|
-
TIKTOK_NOT_CONNECTED: "TIKTOK_NOT_CONNECTED",
|
|
411
|
-
UPSTREAM_TIMEOUT: "UPSTREAM_TIMEOUT",
|
|
412
|
-
UPSTREAM_UNAVAILABLE: "UPSTREAM_UNAVAILABLE",
|
|
413
|
-
INTERNAL: "INTERNAL"
|
|
414
|
-
};
|
|
415
|
-
var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
|
|
416
|
-
McpErrorCode.INVALID_INPUT,
|
|
417
|
-
McpErrorCode.INVALID_API_KEY,
|
|
418
|
-
McpErrorCode.REVOKED_API_KEY,
|
|
419
|
-
McpErrorCode.IMAGE_PATH_NOT_FOUND,
|
|
420
|
-
McpErrorCode.IMAGE_PATH_NOT_READABLE,
|
|
421
|
-
McpErrorCode.IMAGE_PATH_NOT_A_FILE,
|
|
422
|
-
McpErrorCode.PATH_NOT_SUPPORTED_IN_HTTP_MODE,
|
|
423
|
-
// The caller passed a campaignId that does not exist — semantically a
|
|
424
|
-
// bad parameter, not an internal failure. Matches the spec contract
|
|
425
|
-
// (mcp-server.spec.ts: "McpError INVALID_PARAMS code=CAMPAIGN_NOT_FOUND").
|
|
426
|
-
McpErrorCode.CAMPAIGN_NOT_FOUND
|
|
427
|
-
]);
|
|
428
|
-
function mcp(code, message, extra = {}) {
|
|
429
|
-
const errCode = INVALID_PARAM_CODES.has(code) ? ErrorCode.InvalidParams : ErrorCode.InternalError;
|
|
430
|
-
return new McpError(errCode, message, {
|
|
431
|
-
code,
|
|
432
|
-
...extra
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
__name(mcp, "mcp");
|
|
436
|
-
var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
|
|
437
|
-
"META_CAPABILITY_MISSING",
|
|
438
|
-
"NO_SHOPIFY_SHOPS",
|
|
439
|
-
"NO_META_AD_ACCOUNTS",
|
|
440
|
-
"SHOPIFY_NOT_CONNECTED",
|
|
441
|
-
"TIKTOK_NOT_CONNECTED",
|
|
442
|
-
"REVOKED_API_KEY",
|
|
443
|
-
"CAMPAIGN_NOT_FOUND"
|
|
444
|
-
]);
|
|
445
|
-
function isRecord(v) {
|
|
446
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
447
|
-
}
|
|
448
|
-
__name(isRecord, "isRecord");
|
|
449
|
-
function translateError(err) {
|
|
450
|
-
if (err instanceof ResolveImageError) {
|
|
451
|
-
return mcp(McpErrorCode[err.code], err.message);
|
|
452
|
-
}
|
|
453
|
-
if (err instanceof ApiHttpError) {
|
|
454
|
-
const body = isRecord(err.body) ? err.body : {};
|
|
455
|
-
if (typeof body.code === "string" && PASSTHROUGH_CODES.has(body.code)) {
|
|
456
|
-
return mcp(body.code, String(body.message ?? body.code), {
|
|
457
|
-
...body.helpUrl ? {
|
|
458
|
-
helpUrl: body.helpUrl
|
|
459
|
-
} : {}
|
|
460
|
-
});
|
|
461
|
-
}
|
|
462
|
-
if (err.status === 401) {
|
|
463
|
-
return mcp(McpErrorCode.INVALID_API_KEY, "API key inv\xE1lida. Revisa tu PM_API_KEY en la configuraci\xF3n del cliente MCP.");
|
|
464
|
-
}
|
|
465
|
-
if (err.status === 429) {
|
|
466
|
-
return mcp(McpErrorCode.RATE_LIMITED, "Demasiadas solicitudes. Espera unos segundos.", {
|
|
467
|
-
retryAfterSeconds: Number(body.retryAfterSeconds ?? 30)
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
if (err.status === 400) {
|
|
471
|
-
return mcp(McpErrorCode.INVALID_INPUT, typeof body.message === "string" ? body.message : "Entrada inv\xE1lida.");
|
|
472
|
-
}
|
|
473
|
-
if (err.status === 404) {
|
|
474
|
-
const isTask = body.resource === "task" || /task/i.test(String(body.message ?? ""));
|
|
475
|
-
return mcp(isTask ? McpErrorCode.TASK_NOT_FOUND : McpErrorCode.INVALID_INPUT, "Recurso no encontrado.");
|
|
476
|
-
}
|
|
477
|
-
if (err.status === 409) {
|
|
478
|
-
return mcp(McpErrorCode.TASK_NOT_READY, "La tarea a\xFAn no est\xE1 lista para esta acci\xF3n.");
|
|
479
|
-
}
|
|
480
|
-
if (err.status >= 500) {
|
|
481
|
-
return mcp(McpErrorCode.UPSTREAM_UNAVAILABLE, "El servicio Product Maker no respondi\xF3. Intenta de nuevo en un momento.");
|
|
482
|
-
}
|
|
483
|
-
return mcp(McpErrorCode.INTERNAL, `HTTP ${err.status}`);
|
|
484
|
-
}
|
|
485
|
-
if (err instanceof Error && /aborted|timeout/i.test(err.message)) {
|
|
486
|
-
return mcp(McpErrorCode.UPSTREAM_TIMEOUT, "Tiempo de espera agotado.");
|
|
487
|
-
}
|
|
488
|
-
return mcp(McpErrorCode.INTERNAL, err instanceof Error ? err.message : "Error interno.");
|
|
489
|
-
}
|
|
490
|
-
__name(translateError, "translateError");
|
|
491
391
|
|
|
492
392
|
// src/tools/shared.ts
|
|
493
393
|
import { z } from "zod";
|
|
@@ -579,6 +479,10 @@ function resolveVideoTier(tier) {
|
|
|
579
479
|
return tier === "basic" ? "grok" : "standard";
|
|
580
480
|
}
|
|
581
481
|
__name(resolveVideoTier, "resolveVideoTier");
|
|
482
|
+
var VideoResolutionEnum = z.enum([
|
|
483
|
+
"480p",
|
|
484
|
+
"720p"
|
|
485
|
+
]);
|
|
582
486
|
var VideoStyleEnum = z.enum([
|
|
583
487
|
"ugc",
|
|
584
488
|
"cartoon_3d",
|
|
@@ -675,6 +579,10 @@ function asText(value) {
|
|
|
675
579
|
};
|
|
676
580
|
}
|
|
677
581
|
__name(asText, "asText");
|
|
582
|
+
function isRecord(v) {
|
|
583
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
584
|
+
}
|
|
585
|
+
__name(isRecord, "isRecord");
|
|
678
586
|
function escapeMdLabel(s) {
|
|
679
587
|
return s.replace(/[\[\]\(\)\{\}`<>\r\n\\*_]/g, " ").replace(/\s+/g, " ").trim();
|
|
680
588
|
}
|
|
@@ -711,6 +619,341 @@ async function submitCreative(api, opts) {
|
|
|
711
619
|
}
|
|
712
620
|
__name(submitCreative, "submitCreative");
|
|
713
621
|
|
|
622
|
+
// src/errors.ts
|
|
623
|
+
var McpErrorCode = {
|
|
624
|
+
// Auth / API key
|
|
625
|
+
INVALID_API_KEY: "INVALID_API_KEY",
|
|
626
|
+
REVOKED_API_KEY: "REVOKED_API_KEY",
|
|
627
|
+
// Rate limit / quota
|
|
628
|
+
RATE_LIMITED: "RATE_LIMITED",
|
|
629
|
+
// Input
|
|
630
|
+
INVALID_INPUT: "INVALID_INPUT",
|
|
631
|
+
// Image upload (MCP-local)
|
|
632
|
+
IMAGE_DOWNLOAD_FAILED: "IMAGE_DOWNLOAD_FAILED",
|
|
633
|
+
IMAGE_TOO_LARGE: "IMAGE_TOO_LARGE",
|
|
634
|
+
IMAGE_TYPE_UNSUPPORTED: "IMAGE_TYPE_UNSUPPORTED",
|
|
635
|
+
IMAGE_PATH_NOT_FOUND: "IMAGE_PATH_NOT_FOUND",
|
|
636
|
+
IMAGE_PATH_NOT_READABLE: "IMAGE_PATH_NOT_READABLE",
|
|
637
|
+
IMAGE_PATH_NOT_A_FILE: "IMAGE_PATH_NOT_A_FILE",
|
|
638
|
+
PATH_NOT_SUPPORTED_IN_HTTP_MODE: "PATH_NOT_SUPPORTED_IN_HTTP_MODE",
|
|
639
|
+
// Task lifecycle
|
|
640
|
+
TASK_NOT_FOUND: "TASK_NOT_FOUND",
|
|
641
|
+
TASK_NOT_READY: "TASK_NOT_READY",
|
|
642
|
+
// Connection presence
|
|
643
|
+
NO_SHOPIFY_SHOPS: "NO_SHOPIFY_SHOPS",
|
|
644
|
+
NO_META_AD_ACCOUNTS: "NO_META_AD_ACCOUNTS",
|
|
645
|
+
// Campaigns (shared Meta + TikTok)
|
|
646
|
+
CAMPAIGN_NOT_FOUND: "CAMPAIGN_NOT_FOUND",
|
|
647
|
+
CAMPAIGN_TASK_NOT_FOUND: "CAMPAIGN_TASK_NOT_FOUND",
|
|
648
|
+
CAMPAIGN_MISSING_LANDING_URL: "CAMPAIGN_MISSING_LANDING_URL",
|
|
649
|
+
CAMPAIGN_NO_CREATIVES: "CAMPAIGN_NO_CREATIVES",
|
|
650
|
+
CAMPAIGN_MISSING_THUMBNAILS: "CAMPAIGN_MISSING_THUMBNAILS",
|
|
651
|
+
// Shopify
|
|
652
|
+
SHOPIFY_NOT_CONNECTED: "SHOPIFY_NOT_CONNECTED",
|
|
653
|
+
SHOPIFY_TOKEN_EXPIRED: "SHOPIFY_TOKEN_EXPIRED",
|
|
654
|
+
SHOPIFY_SCOPE_UPGRADE_REQUIRED: "SHOPIFY_SCOPE_UPGRADE_REQUIRED",
|
|
655
|
+
SHOPIFY_PUBLISH_FAILED: "SHOPIFY_PUBLISH_FAILED",
|
|
656
|
+
// Meta / Facebook Ads
|
|
657
|
+
META_NO_FB_PAGE: "META_NO_FB_PAGE",
|
|
658
|
+
META_NO_ADS_CREATED: "META_NO_ADS_CREATED",
|
|
659
|
+
META_NO_ASSETS_READY: "META_NO_ASSETS_READY",
|
|
660
|
+
META_NO_THUMBNAIL_SOURCE: "META_NO_THUMBNAIL_SOURCE",
|
|
661
|
+
META_NO_LANDING_URL: "META_NO_LANDING_URL",
|
|
662
|
+
META_TASK_NOT_FOUND: "META_TASK_NOT_FOUND",
|
|
663
|
+
META_INVALID_DATA: "META_INVALID_DATA",
|
|
664
|
+
META_INVALID_AD_ACCOUNT: "META_INVALID_AD_ACCOUNT",
|
|
665
|
+
META_MISSING_PAYMENT: "META_MISSING_PAYMENT",
|
|
666
|
+
META_TOKEN_EXPIRED: "META_TOKEN_EXPIRED",
|
|
667
|
+
META_ACCOUNT_RESTRICTED: "META_ACCOUNT_RESTRICTED",
|
|
668
|
+
META_CAPABILITY_MISSING: "META_CAPABILITY_MISSING",
|
|
669
|
+
META_PERMISSION_DENIED: "META_PERMISSION_DENIED",
|
|
670
|
+
META_PIXEL_NOT_FOUND: "META_PIXEL_NOT_FOUND",
|
|
671
|
+
META_OBJECT_NOT_FOUND: "META_OBJECT_NOT_FOUND",
|
|
672
|
+
META_AI_DISCLOSURE_REQUIRED: "META_AI_DISCLOSURE_REQUIRED",
|
|
673
|
+
META_BID_STRATEGY_REQUIRES_AMOUNT: "META_BID_STRATEGY_REQUIRES_AMOUNT",
|
|
674
|
+
META_CBO_BUDGET_CONFLICT: "META_CBO_BUDGET_CONFLICT",
|
|
675
|
+
META_BUDGET_MODE_CONFLICT: "META_BUDGET_MODE_CONFLICT",
|
|
676
|
+
META_AD_SET_BUDGET_REQUIRED: "META_AD_SET_BUDGET_REQUIRED",
|
|
677
|
+
META_DAILY_BUDGET_TOO_HIGH: "META_DAILY_BUDGET_TOO_HIGH",
|
|
678
|
+
META_CAMPAIGN_CONFIG_INVALID: "META_CAMPAIGN_CONFIG_INVALID",
|
|
679
|
+
META_WHATSAPP_DESTINATION_DISABLED: "META_WHATSAPP_DESTINATION_DISABLED",
|
|
680
|
+
// TikTok Ads
|
|
681
|
+
TIKTOK_NOT_CONNECTED: "TIKTOK_NOT_CONNECTED",
|
|
682
|
+
TIKTOK_TOKEN_EXPIRED: "TIKTOK_TOKEN_EXPIRED",
|
|
683
|
+
TIKTOK_PERMISSION_DENIED: "TIKTOK_PERMISSION_DENIED",
|
|
684
|
+
TIKTOK_MISSING_PAYMENT: "TIKTOK_MISSING_PAYMENT",
|
|
685
|
+
TIKTOK_PIXEL_NOT_FOUND: "TIKTOK_PIXEL_NOT_FOUND",
|
|
686
|
+
TIKTOK_INVALID_ADVERTISER: "TIKTOK_INVALID_ADVERTISER",
|
|
687
|
+
TIKTOK_ADVERTISER_REQUIRED: "TIKTOK_ADVERTISER_REQUIRED",
|
|
688
|
+
TIKTOK_IDENTITY_REQUIRED: "TIKTOK_IDENTITY_REQUIRED",
|
|
689
|
+
TIKTOK_IDENTITY_API_REQUIRED: "TIKTOK_IDENTITY_API_REQUIRED",
|
|
690
|
+
TIKTOK_VIDEO_UPLOAD_FAILED: "TIKTOK_VIDEO_UPLOAD_FAILED",
|
|
691
|
+
TIKTOK_VIDEO_UPLOAD_TIMEOUT: "TIKTOK_VIDEO_UPLOAD_TIMEOUT",
|
|
692
|
+
TIKTOK_RATE_LIMITED: "TIKTOK_RATE_LIMITED",
|
|
693
|
+
TIKTOK_SERVER_ERROR: "TIKTOK_SERVER_ERROR",
|
|
694
|
+
TIKTOK_INVALID_DATA: "TIKTOK_INVALID_DATA",
|
|
695
|
+
// Generic transport / catch-all
|
|
696
|
+
UPSTREAM_TIMEOUT: "UPSTREAM_TIMEOUT",
|
|
697
|
+
UPSTREAM_UNAVAILABLE: "UPSTREAM_UNAVAILABLE",
|
|
698
|
+
INTERNAL: "INTERNAL"
|
|
699
|
+
};
|
|
700
|
+
var CODE_MESSAGES = {
|
|
701
|
+
// Shopify
|
|
702
|
+
SHOPIFY_NOT_CONNECTED: "No tienes ninguna tienda Shopify conectada. Conecta una en https://productmaker.app/integrations antes de publicar.",
|
|
703
|
+
SHOPIFY_TOKEN_EXPIRED: "La sesi\xF3n con Shopify expir\xF3. Reconecta la tienda en https://productmaker.app/integrations.",
|
|
704
|
+
SHOPIFY_SCOPE_UPGRADE_REQUIRED: "La conexi\xF3n con Shopify necesita permisos adicionales. Recon\xE9ctala en https://productmaker.app/integrations para aprobar los nuevos permisos.",
|
|
705
|
+
SHOPIFY_PUBLISH_FAILED: "Shopify rechaz\xF3 la publicaci\xF3n. Verifica que el producto/landing no exista ya en la tienda y que tu plan permita publicar m\xE1s productos.",
|
|
706
|
+
// Meta — high-frequency
|
|
707
|
+
META_NO_FB_PAGE: "No encontramos una P\xE1gina de Facebook asociada a tu cuenta. Crea una en https://www.facebook.com/pages/create antes de publicar (Meta exige una Page para correr ads).",
|
|
708
|
+
META_NO_ADS_CREATED: "No se pudieron crear los ads. Lo m\xE1s probable: la cuenta de Meta Ads no tiene m\xE9todo de pago, falta vincular una P\xE1gina, o la app de Facebook perdi\xF3 permisos. Verifica los tres en https://business.facebook.com.",
|
|
709
|
+
META_NO_ASSETS_READY: "La tarea a\xFAn no tiene videos ni ad creative listos para publicar. Espera a que termine la generaci\xF3n o regenera los assets.",
|
|
710
|
+
META_NO_THUMBNAIL_SOURCE: "No hay imagen disponible para usar de portada del video. Sube una imagen de producto o genera el ad creative antes de publicar.",
|
|
711
|
+
META_NO_LANDING_URL: "Falta la URL de la landing. P\xE1sala en `landingUrl` o publica primero a Shopify con `publish_to_shopify`.",
|
|
712
|
+
META_TASK_NOT_FOUND: "La tarea no existe o no pertenece a este usuario. Verifica el `taskId`.",
|
|
713
|
+
META_INVALID_DATA: "Meta rechaz\xF3 la solicitud por datos inv\xE1lidos. Verifica que `adAccountId` y `pixelId` sean correctos.",
|
|
714
|
+
META_INVALID_AD_ACCOUNT: "La cuenta publicitaria no existe o no tienes acceso. Confirma el `adAccountId` con `list_meta_ad_accounts`.",
|
|
715
|
+
META_MISSING_PAYMENT: "Tu cuenta de Meta Ads no tiene m\xE9todo de pago configurado. Visita https://business.facebook.com/billing_hub/payment_settings, selecciona la cuenta publicitaria y agrega una tarjeta antes de reintentar.",
|
|
716
|
+
META_TOKEN_EXPIRED: "Tu sesi\xF3n con Meta expir\xF3. Reconecta Facebook en https://productmaker.app/integrations.",
|
|
717
|
+
META_ACCOUNT_RESTRICTED: "Tu cuenta de Meta Ads est\xE1 temporalmente restringida para crear anuncios. Revisa avisos en https://business.facebook.com y completa la verificaci\xF3n requerida.",
|
|
718
|
+
META_CAPABILITY_MISSING: "Tu app de Facebook no tiene los permisos requeridos para esta cuenta publicitaria. Reconecta Facebook en https://productmaker.app/integrations y acepta TODOS los permisos solicitados (incluyendo business_management).",
|
|
719
|
+
META_PERMISSION_DENIED: "Tu conexi\xF3n de Facebook no tiene los permisos suficientes. Recon\xE9ctala en https://productmaker.app/integrations y acepta todos los permisos.",
|
|
720
|
+
META_PIXEL_NOT_FOUND: "El Pixel no existe o no est\xE1 asociado a la cuenta publicitaria. Verifica el `pixelId` en https://business.facebook.com/events_manager.",
|
|
721
|
+
META_OBJECT_NOT_FOUND: "El objeto solicitado (campa\xF1a/ad set/ad) no existe en Meta. Verifica el ID o crea uno nuevo.",
|
|
722
|
+
META_AI_DISCLOSURE_REQUIRED: "Meta rechaz\xF3 el ad por contenido IA no declarado. El sistema agrega la divulgaci\xF3n autom\xE1ticamente; si persiste, edita la copy para evitar lenguaje que Meta marque como enga\xF1oso.",
|
|
723
|
+
META_BID_STRATEGY_REQUIRES_AMOUNT: "Meta requiere un monto de puja expl\xEDcito para esta cuenta/presupuesto. Sube el presupuesto diario o desactiva las pujas con tope a nivel cuenta.",
|
|
724
|
+
META_CBO_BUDGET_CONFLICT: "Conflicto de presupuesto entre la campa\xF1a (CBO) y el ad set. No se puede mezclar.",
|
|
725
|
+
META_BUDGET_MODE_CONFLICT: "Meta recibi\xF3 presupuesto a nivel campa\xF1a Y a nivel ad set. Define s\xF3lo uno.",
|
|
726
|
+
META_AD_SET_BUDGET_REQUIRED: "Esta configuraci\xF3n de campa\xF1a requiere presupuesto a nivel ad set, no a nivel campa\xF1a.",
|
|
727
|
+
META_DAILY_BUDGET_TOO_HIGH: "El presupuesto diario excede el m\xE1ximo permitido por Meta para tu cuenta. B\xE1jalo y reintenta.",
|
|
728
|
+
META_CAMPAIGN_CONFIG_INVALID: "La configuraci\xF3n de la campa\xF1a es inv\xE1lida. Verifica `dailyBudgetUsd`, `targetCountries` y `campaignName`.",
|
|
729
|
+
META_WHATSAPP_DESTINATION_DISABLED: "El destino WhatsApp no est\xE1 habilitado para esta cuenta de Meta. Cambia el destino del ad o habilita WhatsApp Business en la cuenta.",
|
|
730
|
+
// TikTok
|
|
731
|
+
TIKTOK_NOT_CONNECTED: "No tienes TikTok Ads conectado. Con\xE9ctalo en https://productmaker.app/integrations.",
|
|
732
|
+
TIKTOK_TOKEN_EXPIRED: "La sesi\xF3n con TikTok expir\xF3. Recon\xE9ctalo en https://productmaker.app/integrations.",
|
|
733
|
+
TIKTOK_PERMISSION_DENIED: "Tu conexi\xF3n de TikTok no tiene los permisos suficientes. Recon\xE9ctala y acepta todos los permisos solicitados.",
|
|
734
|
+
TIKTOK_MISSING_PAYMENT: "Tu cuenta de TikTok Ads no tiene m\xE9todo de pago configurado. Agrega uno en https://ads.tiktok.com.",
|
|
735
|
+
TIKTOK_PIXEL_NOT_FOUND: "El Pixel de TikTok no existe o no est\xE1 asociado al advertiser. Verifica el `pixelId`.",
|
|
736
|
+
TIKTOK_INVALID_ADVERTISER: "El advertiser no existe o no tienes acceso. Confirma el `advertiserId` con `list_tiktok_advertisers`.",
|
|
737
|
+
TIKTOK_ADVERTISER_REQUIRED: "Falta el `advertiserId`. Obt\xE9n uno con `list_tiktok_advertisers` antes de publicar.",
|
|
738
|
+
TIKTOK_IDENTITY_REQUIRED: "TikTok requiere una identidad (perfil del anunciante) para publicar. Crea una en https://ads.tiktok.com.",
|
|
739
|
+
TIKTOK_IDENTITY_API_REQUIRED: "TikTok requiere habilitar la API de identidades en tu cuenta. Contacta a TikTok Ads Support.",
|
|
740
|
+
TIKTOK_VIDEO_UPLOAD_FAILED: "TikTok rechaz\xF3 la subida del video. Verifica que el formato sea MP4 < 500MB.",
|
|
741
|
+
TIKTOK_VIDEO_UPLOAD_TIMEOUT: "La subida del video a TikTok se demor\xF3 demasiado. Reintenta en unos minutos.",
|
|
742
|
+
TIKTOK_RATE_LIMITED: "TikTok limit\xF3 las solicitudes. Espera unos minutos antes de reintentar.",
|
|
743
|
+
TIKTOK_SERVER_ERROR: "TikTok devolvi\xF3 un error de servidor. Reintenta en unos minutos.",
|
|
744
|
+
TIKTOK_INVALID_DATA: "TikTok rechaz\xF3 la solicitud por datos inv\xE1lidos. Verifica `advertiserId`, `pixelId` y `campaignConfig`.",
|
|
745
|
+
// Campaign (shared)
|
|
746
|
+
CAMPAIGN_NOT_FOUND: "La campa\xF1a no existe o no pertenece a este usuario. Verifica el `campaignId`.",
|
|
747
|
+
CAMPAIGN_TASK_NOT_FOUND: "La tarea de la campa\xF1a no existe. Verifica el `taskId`.",
|
|
748
|
+
CAMPAIGN_MISSING_LANDING_URL: "Falta la URL de la landing en la campa\xF1a. Publica primero a Shopify o pasa `landingUrl` expl\xEDcitamente.",
|
|
749
|
+
CAMPAIGN_NO_CREATIVES: "La tarea no tiene creativos (video/imagen) listos para la campa\xF1a. Espera a que termine la generaci\xF3n.",
|
|
750
|
+
CAMPAIGN_MISSING_THUMBNAILS: "Faltan miniaturas para los videos. Verifica que la generaci\xF3n de imagen/video haya terminado.",
|
|
751
|
+
// Connections
|
|
752
|
+
NO_SHOPIFY_SHOPS: "No hay tiendas Shopify conectadas. Conecta una en https://productmaker.app/integrations.",
|
|
753
|
+
NO_META_AD_ACCOUNTS: "No hay cuentas de Meta Ads conectadas. Conecta Facebook en https://productmaker.app/integrations.",
|
|
754
|
+
// Task lifecycle
|
|
755
|
+
TASK_NOT_FOUND: "La tarea no existe o no pertenece a este usuario. Verifica el `taskId`.",
|
|
756
|
+
TASK_NOT_READY: "La tarea a\xFAn no est\xE1 lista para esta acci\xF3n. Espera a que termine y reintenta.",
|
|
757
|
+
// API key
|
|
758
|
+
INVALID_API_KEY: "API key inv\xE1lida. Revisa tu PM_API_KEY en la configuraci\xF3n del cliente MCP.",
|
|
759
|
+
REVOKED_API_KEY: "API key revocada. Genera una nueva en https://productmaker.app/account/api-keys.",
|
|
760
|
+
// Rate
|
|
761
|
+
RATE_LIMITED: "Demasiadas solicitudes. Espera unos segundos.",
|
|
762
|
+
// Generic
|
|
763
|
+
UPSTREAM_UNAVAILABLE: "El servicio Product Maker no respondi\xF3. Intenta de nuevo en un momento.",
|
|
764
|
+
UPSTREAM_TIMEOUT: "Tiempo de espera agotado. Reintenta en un momento.",
|
|
765
|
+
INTERNAL: "Error interno.",
|
|
766
|
+
INVALID_INPUT: "Entrada inv\xE1lida."
|
|
767
|
+
};
|
|
768
|
+
var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
|
|
769
|
+
// Input/auth
|
|
770
|
+
McpErrorCode.INVALID_INPUT,
|
|
771
|
+
McpErrorCode.INVALID_API_KEY,
|
|
772
|
+
McpErrorCode.REVOKED_API_KEY,
|
|
773
|
+
McpErrorCode.IMAGE_PATH_NOT_FOUND,
|
|
774
|
+
McpErrorCode.IMAGE_PATH_NOT_READABLE,
|
|
775
|
+
McpErrorCode.IMAGE_PATH_NOT_A_FILE,
|
|
776
|
+
McpErrorCode.PATH_NOT_SUPPORTED_IN_HTTP_MODE,
|
|
777
|
+
// Identifier mismatches
|
|
778
|
+
McpErrorCode.CAMPAIGN_NOT_FOUND,
|
|
779
|
+
McpErrorCode.CAMPAIGN_TASK_NOT_FOUND,
|
|
780
|
+
McpErrorCode.TASK_NOT_FOUND,
|
|
781
|
+
McpErrorCode.META_TASK_NOT_FOUND,
|
|
782
|
+
McpErrorCode.META_INVALID_AD_ACCOUNT,
|
|
783
|
+
McpErrorCode.META_PIXEL_NOT_FOUND,
|
|
784
|
+
McpErrorCode.META_OBJECT_NOT_FOUND,
|
|
785
|
+
McpErrorCode.TIKTOK_INVALID_ADVERTISER,
|
|
786
|
+
McpErrorCode.TIKTOK_ADVERTISER_REQUIRED,
|
|
787
|
+
McpErrorCode.TIKTOK_PIXEL_NOT_FOUND,
|
|
788
|
+
McpErrorCode.CAMPAIGN_MISSING_LANDING_URL,
|
|
789
|
+
McpErrorCode.META_NO_LANDING_URL,
|
|
790
|
+
McpErrorCode.META_DAILY_BUDGET_TOO_HIGH,
|
|
791
|
+
McpErrorCode.META_CAMPAIGN_CONFIG_INVALID,
|
|
792
|
+
McpErrorCode.META_BID_STRATEGY_REQUIRES_AMOUNT,
|
|
793
|
+
McpErrorCode.META_CBO_BUDGET_CONFLICT,
|
|
794
|
+
McpErrorCode.META_BUDGET_MODE_CONFLICT,
|
|
795
|
+
McpErrorCode.META_AD_SET_BUDGET_REQUIRED,
|
|
796
|
+
// Validation-shape backend errors (data-level rejection)
|
|
797
|
+
McpErrorCode.META_INVALID_DATA,
|
|
798
|
+
McpErrorCode.TIKTOK_INVALID_DATA,
|
|
799
|
+
// Out-of-band user-resolvable auth/billing/connection — caller fixes by
|
|
800
|
+
// going to Facebook/TikTok/Shopify and reconnecting, finishing
|
|
801
|
+
// verification, or adding a payment method. None of these are transient,
|
|
802
|
+
// so InternalError would mislead the agent into retrying blindly.
|
|
803
|
+
McpErrorCode.META_PERMISSION_DENIED,
|
|
804
|
+
McpErrorCode.META_CAPABILITY_MISSING,
|
|
805
|
+
McpErrorCode.META_TOKEN_EXPIRED,
|
|
806
|
+
McpErrorCode.META_MISSING_PAYMENT,
|
|
807
|
+
McpErrorCode.META_ACCOUNT_RESTRICTED,
|
|
808
|
+
McpErrorCode.META_NO_FB_PAGE,
|
|
809
|
+
McpErrorCode.TIKTOK_TOKEN_EXPIRED,
|
|
810
|
+
McpErrorCode.TIKTOK_PERMISSION_DENIED,
|
|
811
|
+
McpErrorCode.TIKTOK_MISSING_PAYMENT,
|
|
812
|
+
McpErrorCode.TIKTOK_IDENTITY_REQUIRED,
|
|
813
|
+
McpErrorCode.TIKTOK_IDENTITY_API_REQUIRED,
|
|
814
|
+
McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
|
|
815
|
+
McpErrorCode.SHOPIFY_SCOPE_UPGRADE_REQUIRED,
|
|
816
|
+
McpErrorCode.SHOPIFY_NOT_CONNECTED,
|
|
817
|
+
McpErrorCode.TIKTOK_NOT_CONNECTED,
|
|
818
|
+
McpErrorCode.NO_SHOPIFY_SHOPS,
|
|
819
|
+
McpErrorCode.NO_META_AD_ACCOUNTS
|
|
820
|
+
]);
|
|
821
|
+
var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
|
|
822
|
+
// Shopify
|
|
823
|
+
McpErrorCode.SHOPIFY_NOT_CONNECTED,
|
|
824
|
+
McpErrorCode.SHOPIFY_TOKEN_EXPIRED,
|
|
825
|
+
McpErrorCode.SHOPIFY_SCOPE_UPGRADE_REQUIRED,
|
|
826
|
+
McpErrorCode.SHOPIFY_PUBLISH_FAILED,
|
|
827
|
+
// Meta
|
|
828
|
+
McpErrorCode.META_NO_FB_PAGE,
|
|
829
|
+
McpErrorCode.META_NO_ADS_CREATED,
|
|
830
|
+
McpErrorCode.META_NO_ASSETS_READY,
|
|
831
|
+
McpErrorCode.META_NO_THUMBNAIL_SOURCE,
|
|
832
|
+
McpErrorCode.META_NO_LANDING_URL,
|
|
833
|
+
McpErrorCode.META_TASK_NOT_FOUND,
|
|
834
|
+
McpErrorCode.META_INVALID_DATA,
|
|
835
|
+
McpErrorCode.META_INVALID_AD_ACCOUNT,
|
|
836
|
+
McpErrorCode.META_MISSING_PAYMENT,
|
|
837
|
+
McpErrorCode.META_TOKEN_EXPIRED,
|
|
838
|
+
McpErrorCode.META_ACCOUNT_RESTRICTED,
|
|
839
|
+
McpErrorCode.META_CAPABILITY_MISSING,
|
|
840
|
+
McpErrorCode.META_PERMISSION_DENIED,
|
|
841
|
+
McpErrorCode.META_PIXEL_NOT_FOUND,
|
|
842
|
+
McpErrorCode.META_OBJECT_NOT_FOUND,
|
|
843
|
+
McpErrorCode.META_AI_DISCLOSURE_REQUIRED,
|
|
844
|
+
McpErrorCode.META_BID_STRATEGY_REQUIRES_AMOUNT,
|
|
845
|
+
McpErrorCode.META_CBO_BUDGET_CONFLICT,
|
|
846
|
+
McpErrorCode.META_BUDGET_MODE_CONFLICT,
|
|
847
|
+
McpErrorCode.META_AD_SET_BUDGET_REQUIRED,
|
|
848
|
+
McpErrorCode.META_DAILY_BUDGET_TOO_HIGH,
|
|
849
|
+
McpErrorCode.META_CAMPAIGN_CONFIG_INVALID,
|
|
850
|
+
McpErrorCode.META_WHATSAPP_DESTINATION_DISABLED,
|
|
851
|
+
// TikTok
|
|
852
|
+
McpErrorCode.TIKTOK_NOT_CONNECTED,
|
|
853
|
+
McpErrorCode.TIKTOK_TOKEN_EXPIRED,
|
|
854
|
+
McpErrorCode.TIKTOK_PERMISSION_DENIED,
|
|
855
|
+
McpErrorCode.TIKTOK_MISSING_PAYMENT,
|
|
856
|
+
McpErrorCode.TIKTOK_PIXEL_NOT_FOUND,
|
|
857
|
+
McpErrorCode.TIKTOK_INVALID_ADVERTISER,
|
|
858
|
+
McpErrorCode.TIKTOK_ADVERTISER_REQUIRED,
|
|
859
|
+
McpErrorCode.TIKTOK_IDENTITY_REQUIRED,
|
|
860
|
+
McpErrorCode.TIKTOK_IDENTITY_API_REQUIRED,
|
|
861
|
+
McpErrorCode.TIKTOK_VIDEO_UPLOAD_FAILED,
|
|
862
|
+
McpErrorCode.TIKTOK_VIDEO_UPLOAD_TIMEOUT,
|
|
863
|
+
McpErrorCode.TIKTOK_RATE_LIMITED,
|
|
864
|
+
McpErrorCode.TIKTOK_SERVER_ERROR,
|
|
865
|
+
McpErrorCode.TIKTOK_INVALID_DATA,
|
|
866
|
+
// Campaign (shared)
|
|
867
|
+
McpErrorCode.CAMPAIGN_NOT_FOUND,
|
|
868
|
+
McpErrorCode.CAMPAIGN_TASK_NOT_FOUND,
|
|
869
|
+
McpErrorCode.CAMPAIGN_MISSING_LANDING_URL,
|
|
870
|
+
McpErrorCode.CAMPAIGN_NO_CREATIVES,
|
|
871
|
+
McpErrorCode.CAMPAIGN_MISSING_THUMBNAILS,
|
|
872
|
+
// Connections
|
|
873
|
+
McpErrorCode.NO_SHOPIFY_SHOPS,
|
|
874
|
+
McpErrorCode.NO_META_AD_ACCOUNTS,
|
|
875
|
+
// Auth
|
|
876
|
+
McpErrorCode.REVOKED_API_KEY
|
|
877
|
+
]);
|
|
878
|
+
function mcp(code, message, extra = {}) {
|
|
879
|
+
const errCode = INVALID_PARAM_CODES.has(code) ? ErrorCode.InvalidParams : ErrorCode.InternalError;
|
|
880
|
+
return new McpError(errCode, message, {
|
|
881
|
+
code,
|
|
882
|
+
...extra
|
|
883
|
+
});
|
|
884
|
+
}
|
|
885
|
+
__name(mcp, "mcp");
|
|
886
|
+
var GENERIC_BACKEND_MESSAGES = /* @__PURE__ */ new Set([
|
|
887
|
+
"No ads could be created for the campaign.",
|
|
888
|
+
"No assets could be created for the campaign.",
|
|
889
|
+
"Invalid data provided to Meta API",
|
|
890
|
+
"Insufficient permissions for Meta account",
|
|
891
|
+
"Meta session has expired",
|
|
892
|
+
"Meta Ads account has no payment method configured"
|
|
893
|
+
]);
|
|
894
|
+
function looksLikeMeaningfulBackendMessage(message, mapped) {
|
|
895
|
+
if (!message || message.length === 0) return false;
|
|
896
|
+
if (GENERIC_BACKEND_MESSAGES.has(message)) return false;
|
|
897
|
+
const norm = /* @__PURE__ */ __name((s) => s.toLowerCase().replace(/[.\s]+$/, ""), "norm");
|
|
898
|
+
if (norm(mapped).includes(norm(message))) return false;
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
901
|
+
__name(looksLikeMeaningfulBackendMessage, "looksLikeMeaningfulBackendMessage");
|
|
902
|
+
function resolveMessage(code, backendMessage) {
|
|
903
|
+
const mapped = CODE_MESSAGES[code];
|
|
904
|
+
const backend = typeof backendMessage === "string" ? backendMessage.trim() : "";
|
|
905
|
+
if (mapped && backend && looksLikeMeaningfulBackendMessage(backend, mapped)) {
|
|
906
|
+
return `${mapped} (Detalle: ${backend})`;
|
|
907
|
+
}
|
|
908
|
+
if (mapped) return mapped;
|
|
909
|
+
if (backend) return backend;
|
|
910
|
+
return `El servicio devolvi\xF3 un error (c\xF3digo: ${code}). Intenta de nuevo en un momento o contacta soporte si persiste.`;
|
|
911
|
+
}
|
|
912
|
+
__name(resolveMessage, "resolveMessage");
|
|
913
|
+
function translateError(err) {
|
|
914
|
+
if (err instanceof ResolveImageError) {
|
|
915
|
+
return mcp(McpErrorCode[err.code], err.message);
|
|
916
|
+
}
|
|
917
|
+
if (err instanceof ApiHttpError) {
|
|
918
|
+
const body = isRecord(err.body) ? err.body : {};
|
|
919
|
+
if (typeof body.code === "string" && PASSTHROUGH_CODES.has(body.code)) {
|
|
920
|
+
const code = body.code;
|
|
921
|
+
return mcp(code, resolveMessage(code, body.message), {
|
|
922
|
+
...body.helpUrl ? {
|
|
923
|
+
helpUrl: body.helpUrl
|
|
924
|
+
} : {}
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
if (err.status === 401) {
|
|
928
|
+
return mcp(McpErrorCode.INVALID_API_KEY, resolveMessage(McpErrorCode.INVALID_API_KEY, void 0));
|
|
929
|
+
}
|
|
930
|
+
if (err.status === 429) {
|
|
931
|
+
return mcp(McpErrorCode.RATE_LIMITED, resolveMessage(McpErrorCode.RATE_LIMITED, void 0), {
|
|
932
|
+
retryAfterSeconds: Number(body.retryAfterSeconds ?? 30)
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
if (err.status === 400) {
|
|
936
|
+
return mcp(McpErrorCode.INVALID_INPUT, typeof body.message === "string" ? body.message : resolveMessage(McpErrorCode.INVALID_INPUT, void 0));
|
|
937
|
+
}
|
|
938
|
+
if (err.status === 404) {
|
|
939
|
+
const isTask = body.resource === "task" || /task/i.test(String(body.message ?? ""));
|
|
940
|
+
return mcp(isTask ? McpErrorCode.TASK_NOT_FOUND : McpErrorCode.INVALID_INPUT, resolveMessage(isTask ? McpErrorCode.TASK_NOT_FOUND : McpErrorCode.INVALID_INPUT, void 0));
|
|
941
|
+
}
|
|
942
|
+
if (err.status === 409) {
|
|
943
|
+
return mcp(McpErrorCode.TASK_NOT_READY, resolveMessage(McpErrorCode.TASK_NOT_READY, void 0));
|
|
944
|
+
}
|
|
945
|
+
if (err.status >= 500) {
|
|
946
|
+
return mcp(McpErrorCode.UPSTREAM_UNAVAILABLE, resolveMessage(McpErrorCode.UPSTREAM_UNAVAILABLE, void 0));
|
|
947
|
+
}
|
|
948
|
+
return mcp(McpErrorCode.INTERNAL, `HTTP ${err.status}`);
|
|
949
|
+
}
|
|
950
|
+
if (err instanceof Error && /aborted|timeout/i.test(err.message)) {
|
|
951
|
+
return mcp(McpErrorCode.UPSTREAM_TIMEOUT, resolveMessage(McpErrorCode.UPSTREAM_TIMEOUT, void 0));
|
|
952
|
+
}
|
|
953
|
+
return mcp(McpErrorCode.INTERNAL, err instanceof Error ? err.message : resolveMessage(McpErrorCode.INTERNAL, void 0));
|
|
954
|
+
}
|
|
955
|
+
__name(translateError, "translateError");
|
|
956
|
+
|
|
714
957
|
// src/asset-proxy.ts
|
|
715
958
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
716
959
|
var TOKEN_TTL_SECONDS = 3600;
|
|
@@ -887,6 +1130,7 @@ var CreateProductTaskInput = z2.object({
|
|
|
887
1130
|
offer: z2.string().optional().describe('Oferta o promoci\xF3n a destacar. Ej. "2x1", "Env\xEDo gratis hoy", "50% OFF". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar (es v\xE1lido que diga "sin oferta"). Si te dice "t\xFA elige", om\xEDtelo.'),
|
|
888
1131
|
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). Opciones: "auto", "ugc_dynamic" (UGC testimonial), "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones, ideal 30s), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n, ideal 30s), "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.'),
|
|
889
1132
|
videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video (en la web la etiqueta es "\xBFCalidad de video?", 2 opciones \u2014 sin escalera de calidad, son ESTILOS distintos). "basic" = "Realista" (RECOMENDADO por la web): render de 15s con audio nativo, el m\xE1s natural \u2014 S\xD3LO v\xE1lido si la duraci\xF3n del video es 15s. "standard" = "Multiescena": m\xFAltiples tomas, hasta 30s. Si el usuario no menciona estilo, OMITE este campo y el backend usa su default. Para duraci\xF3n 30s s\xF3lo aplica "standard" (Realista est\xE1 vetada).'),
|
|
1133
|
+
videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render \u2014 aplica S\xD3LO al tier "basic" (Realista); "standard" la deriva solo. En la web se pregunta "\xBFResoluci\xF3n del video?" tras elegir "basic". "720p" = m\xE1xima nitidez (default si se omite); "480p" = m\xE1s econ\xF3mico (~la mitad del precio). Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 720p para basic).'),
|
|
890
1134
|
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."),
|
|
891
1135
|
angleNarratives: NarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional, override avanzado). Usa los mismos identificadores de narrativa que videoPreset (ej. "animated_problem_pixar", "review", "ugc_skeptic_confession"). angleNarratives[i] fuerza la narrativa del \xE1ngulo en la posici\xF3n i; si la lista es m\xE1s corta que la cantidad de \xE1ngulos, las posiciones restantes las elige la IA. Requiere multiAngleVideos:true para que haya varios \xE1ngulos. Si se omite, todas las narrativas se auto-seleccionan seg\xFAn el producto.'),
|
|
892
1136
|
outputs: z2.array(z2.enum([
|
|
@@ -971,6 +1215,11 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
971
1215
|
"options.videoStyle": resolved?.videoStyle,
|
|
972
1216
|
"options.narrativeStyle": resolved?.narrativeStyle,
|
|
973
1217
|
"options.videoTier": wireTier,
|
|
1218
|
+
// Resolution is meaningful ONLY for the basic/grok tier; for every
|
|
1219
|
+
// other tier the backend derives it. Forward it ONLY for grok so the
|
|
1220
|
+
// MCP never presents a "user choice where there is none" (the web has
|
|
1221
|
+
// no resolution knob for standard) — for non-grok it's dropped.
|
|
1222
|
+
"options.videoResolution": wireTier === "grok" ? input.videoResolution : void 0,
|
|
974
1223
|
"options.multiAngleVideos": input.multiAngleVideos,
|
|
975
1224
|
"options.angleNarratives": input.angleNarratives,
|
|
976
1225
|
"options.selectedOutputs": input.outputs,
|
|
@@ -1129,6 +1378,7 @@ var VideoInput = z3.object({
|
|
|
1129
1378
|
durationSeconds: z3.number().int().min(5).max(60).optional().describe("Opcional. Duraci\xF3n del video en segundos (5-60). Default 15s. Para hooks cortos usa 8, para storytelling usa 30."),
|
|
1130
1379
|
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). 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, recomendado 30s), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n, recomendado 30s), "ugc_demo_review" (demo en uso a lo largo de d\xEDas), "problem_solution" (problema\u2192producto\u2192CTA), "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).'),
|
|
1131
1380
|
videoTier: VideoTierEnum.optional().describe('Opcional. Estilo del video (en la web la etiqueta es "\xBFCalidad de video?", 2 opciones \u2014 sin escalera de calidad, son ESTILOS distintos). "basic" = "Realista" (RECOMENDADO por la web): render de 15s con audio nativo, el m\xE1s natural \u2014 S\xD3LO v\xE1lido cuando durationSeconds \u2264 15 (el handler lo valida). "standard" = "Multiescena": m\xFAltiples tomas, hasta 30s. Si la duraci\xF3n es 30s, s\xF3lo aplica "standard". Si el usuario no menciona estilo, OMITE este campo y el backend usa su default.'),
|
|
1381
|
+
videoResolution: VideoResolutionEnum.optional().describe('Opcional. Resoluci\xF3n del render \u2014 aplica S\xD3LO al tier "basic" (Realista); el tier "standard" la deriva solo. En la web se pregunta "\xBFResoluci\xF3n del video?" tras elegir "basic". "720p" = m\xE1xima nitidez (default si se omite); "480p" = m\xE1s econ\xF3mico (~la mitad del precio). Si el usuario no menciona resoluci\xF3n, OMITE este campo (el backend usa 720p para basic).'),
|
|
1132
1382
|
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"].'),
|
|
1133
1383
|
price: CreativeRefs.price,
|
|
1134
1384
|
offer: CreativeRefs.offer,
|
|
@@ -1182,7 +1432,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
|
|
|
1182
1432
|
}
|
|
1183
1433
|
const resolved = resolveVideoPreset(input.videoPreset);
|
|
1184
1434
|
const wireTier = resolveVideoTier(input.videoTier);
|
|
1185
|
-
const { videoTier: _publicTier, ...rest } = input;
|
|
1435
|
+
const { videoTier: _publicTier, videoResolution: _vr, ...rest } = input;
|
|
1186
1436
|
const wirePayload = {
|
|
1187
1437
|
...rest
|
|
1188
1438
|
};
|
|
@@ -1191,6 +1441,7 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
|
|
|
1191
1441
|
wirePayload.narrativeStyle = resolved.narrativeStyle;
|
|
1192
1442
|
}
|
|
1193
1443
|
if (wireTier !== void 0) wirePayload.videoTier = wireTier;
|
|
1444
|
+
if (wireTier === "grok" && input.videoResolution) wirePayload.videoResolution = input.videoResolution;
|
|
1194
1445
|
return await submitCreative(api, {
|
|
1195
1446
|
path: "/v1/creatives/videos",
|
|
1196
1447
|
estimatedSeconds: 300,
|
|
@@ -1321,7 +1572,11 @@ import { z as z6 } from "zod";
|
|
|
1321
1572
|
var ShopifyInput = z6.object({
|
|
1322
1573
|
taskId: z6.string().min(1),
|
|
1323
1574
|
shopDomain: z6.string().min(1),
|
|
1324
|
-
|
|
1575
|
+
/** Override: number of landing images to include. Default = ALL images the
|
|
1576
|
+
* task generated (the MCP fetches /v1/results first to count). Bounded
|
|
1577
|
+
* generously here; the backend validates each index against
|
|
1578
|
+
* landing.images.length and rejects out-of-range. */
|
|
1579
|
+
numLandingImages: z6.number().int().min(1).max(20).optional(),
|
|
1325
1580
|
variantIndex: z6.number().int().min(0).optional(),
|
|
1326
1581
|
/** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
|
|
1327
1582
|
* When true, the landing renders variant-picker pills; when false, it ships
|
|
@@ -1354,10 +1609,12 @@ var TikTokInput = z6.object({
|
|
|
1354
1609
|
campaignConfig: McpCampaignConfig.optional()
|
|
1355
1610
|
});
|
|
1356
1611
|
function registerPublishTools(server, api) {
|
|
1357
|
-
server.tool("publish_to_shopify", "Publica la landing y producto generados en una tienda Shopify conectada. Precondici\xF3n: la tarea debe estar en status='complete' \u2014 usa `get_task_status` antes. ANTES de llamar preg\xFAntale al usuario \"\xBFTu producto tiene variantes (talla, color, etc.)?\" y pasa `showVariantPicker: true` si responde que s\xED \u2014 eso renderiza los pills de variantes en la landing. Por defecto
|
|
1612
|
+
server.tool("publish_to_shopify", "Publica la landing y producto generados en una tienda Shopify conectada. Precondici\xF3n: la tarea debe estar en status='complete' \u2014 usa `get_task_status` antes. ANTES de llamar preg\xFAntale al usuario \"\xBFTu producto tiene variantes (talla, color, etc.)?\" y pasa `showVariantPicker: true` si responde que s\xED \u2014 eso renderiza los pills de variantes en la landing. Por defecto publica TODAS las im\xE1genes que la tarea gener\xF3 para la landing; pasa `numLandingImages` s\xF3lo si el usuario te pide menos. La respuesta es markdown con el link a la landing y al producto.", ShopifyInput.shape, async (input) => {
|
|
1358
1613
|
try {
|
|
1359
|
-
const numImages = input.numLandingImages ?? 3;
|
|
1360
1614
|
const variantIndex = input.variantIndex ?? 0;
|
|
1615
|
+
const availableImages = await fetchLandingImageCount(api, input.taskId);
|
|
1616
|
+
const effectiveAvailable = availableImages > 0 ? availableImages : 3;
|
|
1617
|
+
const numImages = Math.min(input.numLandingImages ?? effectiveAvailable, effectiveAvailable);
|
|
1361
1618
|
const selectedLandingImages = Array.from({
|
|
1362
1619
|
length: numImages
|
|
1363
1620
|
}, (_, i) => ({
|
|
@@ -1434,6 +1691,19 @@ function renderShopifyPublishMarkdown(r) {
|
|
|
1434
1691
|
};
|
|
1435
1692
|
}
|
|
1436
1693
|
__name(renderShopifyPublishMarkdown, "renderShopifyPublishMarkdown");
|
|
1694
|
+
async function fetchLandingImageCount(api, taskId) {
|
|
1695
|
+
try {
|
|
1696
|
+
const r = await api.getJson(`/v1/results/${encodeURIComponent(taskId)}`);
|
|
1697
|
+
if (!isRecord(r)) return 0;
|
|
1698
|
+
const landing = r.landing;
|
|
1699
|
+
if (!isRecord(landing)) return 0;
|
|
1700
|
+
const images = landing.images;
|
|
1701
|
+
return Array.isArray(images) ? images.length : 0;
|
|
1702
|
+
} catch {
|
|
1703
|
+
return 0;
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
__name(fetchLandingImageCount, "fetchLandingImageCount");
|
|
1437
1707
|
function toMetaCampaignOverrides(cfg) {
|
|
1438
1708
|
const out = {};
|
|
1439
1709
|
if (cfg.dailyBudgetUsd !== void 0) out.dailyBudgetUsd = cfg.dailyBudgetUsd;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@productmaker/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Product Maker MCP server — connect Claude Desktop, Claude Code, ChatGPT, or Gemini to Product Maker AI to generate dropshipping landing pages, UGC videos, and ad creatives, and publish to Shopify and Meta Ads from your AI assistant.",
|