@productmaker/mcp 0.10.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 +361 -103
- package/dist/stdio.js +361 -103
- 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";
|
|
@@ -682,6 +582,10 @@ function asText(value) {
|
|
|
682
582
|
};
|
|
683
583
|
}
|
|
684
584
|
__name(asText, "asText");
|
|
585
|
+
function isRecord(v) {
|
|
586
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
587
|
+
}
|
|
588
|
+
__name(isRecord, "isRecord");
|
|
685
589
|
function escapeMdLabel(s) {
|
|
686
590
|
return s.replace(/[\[\]\(\)\{\}`<>\r\n\\*_]/g, " ").replace(/\s+/g, " ").trim();
|
|
687
591
|
}
|
|
@@ -718,6 +622,341 @@ async function submitCreative(api, opts) {
|
|
|
718
622
|
}
|
|
719
623
|
__name(submitCreative, "submitCreative");
|
|
720
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
|
+
|
|
721
960
|
// src/asset-proxy.ts
|
|
722
961
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
723
962
|
var TOKEN_TTL_SECONDS = 3600;
|
|
@@ -1377,7 +1616,11 @@ import { z as z6 } from "zod";
|
|
|
1377
1616
|
var ShopifyInput = z6.object({
|
|
1378
1617
|
taskId: z6.string().min(1),
|
|
1379
1618
|
shopDomain: z6.string().min(1),
|
|
1380
|
-
|
|
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(),
|
|
1381
1624
|
variantIndex: z6.number().int().min(0).optional(),
|
|
1382
1625
|
/** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
|
|
1383
1626
|
* When true, the landing renders variant-picker pills; when false, it ships
|
|
@@ -1410,10 +1653,12 @@ var TikTokInput = z6.object({
|
|
|
1410
1653
|
campaignConfig: McpCampaignConfig.optional()
|
|
1411
1654
|
});
|
|
1412
1655
|
function registerPublishTools(server, api) {
|
|
1413
|
-
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) => {
|
|
1414
1657
|
try {
|
|
1415
|
-
const numImages = input.numLandingImages ?? 3;
|
|
1416
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);
|
|
1417
1662
|
const selectedLandingImages = Array.from({
|
|
1418
1663
|
length: numImages
|
|
1419
1664
|
}, (_, i) => ({
|
|
@@ -1490,6 +1735,19 @@ function renderShopifyPublishMarkdown(r) {
|
|
|
1490
1735
|
};
|
|
1491
1736
|
}
|
|
1492
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");
|
|
1493
1751
|
function toMetaCampaignOverrides(cfg) {
|
|
1494
1752
|
const out = {};
|
|
1495
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";
|
|
@@ -679,6 +579,10 @@ function asText(value) {
|
|
|
679
579
|
};
|
|
680
580
|
}
|
|
681
581
|
__name(asText, "asText");
|
|
582
|
+
function isRecord(v) {
|
|
583
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
584
|
+
}
|
|
585
|
+
__name(isRecord, "isRecord");
|
|
682
586
|
function escapeMdLabel(s) {
|
|
683
587
|
return s.replace(/[\[\]\(\)\{\}`<>\r\n\\*_]/g, " ").replace(/\s+/g, " ").trim();
|
|
684
588
|
}
|
|
@@ -715,6 +619,341 @@ async function submitCreative(api, opts) {
|
|
|
715
619
|
}
|
|
716
620
|
__name(submitCreative, "submitCreative");
|
|
717
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
|
+
|
|
718
957
|
// src/asset-proxy.ts
|
|
719
958
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
720
959
|
var TOKEN_TTL_SECONDS = 3600;
|
|
@@ -1333,7 +1572,11 @@ import { z as z6 } from "zod";
|
|
|
1333
1572
|
var ShopifyInput = z6.object({
|
|
1334
1573
|
taskId: z6.string().min(1),
|
|
1335
1574
|
shopDomain: z6.string().min(1),
|
|
1336
|
-
|
|
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(),
|
|
1337
1580
|
variantIndex: z6.number().int().min(0).optional(),
|
|
1338
1581
|
/** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
|
|
1339
1582
|
* When true, the landing renders variant-picker pills; when false, it ships
|
|
@@ -1366,10 +1609,12 @@ var TikTokInput = z6.object({
|
|
|
1366
1609
|
campaignConfig: McpCampaignConfig.optional()
|
|
1367
1610
|
});
|
|
1368
1611
|
function registerPublishTools(server, api) {
|
|
1369
|
-
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) => {
|
|
1370
1613
|
try {
|
|
1371
|
-
const numImages = input.numLandingImages ?? 3;
|
|
1372
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);
|
|
1373
1618
|
const selectedLandingImages = Array.from({
|
|
1374
1619
|
length: numImages
|
|
1375
1620
|
}, (_, i) => ({
|
|
@@ -1446,6 +1691,19 @@ function renderShopifyPublishMarkdown(r) {
|
|
|
1446
1691
|
};
|
|
1447
1692
|
}
|
|
1448
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");
|
|
1449
1707
|
function toMetaCampaignOverrides(cfg) {
|
|
1450
1708
|
const out = {};
|
|
1451
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.",
|