@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.
Files changed (3) hide show
  1. package/dist/http.js +374 -104
  2. package/dist/stdio.js +374 -104
  3. 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
- numLandingImages: z6.number().int().min(1).max(7).optional(),
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 incluye 3 im\xE1genes del hero. La respuesta es markdown con el link a la landing y al producto.", ShopifyInput.shape, async (input) => {
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
- numLandingImages: z6.number().int().min(1).max(7).optional(),
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 incluye 3 im\xE1genes del hero. La respuesta es markdown con el link a la landing y al producto.", ShopifyInput.shape, async (input) => {
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.9.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.",