@productmaker/mcp 0.10.0 → 0.12.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 -107
  2. package/dist/stdio.js +374 -107
  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";
@@ -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 Shopify store is connected. Connect one at https://productmaker.app/integrations before publishing.",
706
+ SHOPIFY_TOKEN_EXPIRED: "The Shopify session expired. Reconnect the store at https://productmaker.app/integrations.",
707
+ SHOPIFY_SCOPE_UPGRADE_REQUIRED: "The Shopify connection needs additional scopes. Reconnect it at https://productmaker.app/integrations to approve the new permissions.",
708
+ SHOPIFY_PUBLISH_FAILED: "Shopify rejected the publish. Check that the product/landing does not already exist on the store and that your Shopify plan allows additional products.",
709
+ // Meta — high-frequency
710
+ META_NO_FB_PAGE: "No Facebook Page is associated with this account. Create one at https://www.facebook.com/pages/create before publishing (Meta requires a Page to run ads).",
711
+ META_NO_ADS_CREATED: "No ads could be created. Most likely: the Meta Ads account has no payment method, no Facebook Page is linked, or the Facebook app lost permissions. Verify all three at https://business.facebook.com.",
712
+ META_NO_ASSETS_READY: "The task has no videos or ad creative ready to publish yet. Wait for generation to finish or regenerate the assets.",
713
+ META_NO_THUMBNAIL_SOURCE: "No image is available to use as the video thumbnail. Upload a product image or generate the ad creative before publishing.",
714
+ META_NO_LANDING_URL: "Missing landing URL. Pass it in `landingUrl` or publish to Shopify first with `publish_to_shopify`.",
715
+ META_TASK_NOT_FOUND: "Task not found or does not belong to this user. Verify the `taskId`.",
716
+ META_INVALID_DATA: "Meta rejected the request due to invalid data. Verify that `adAccountId` and `pixelId` are correct.",
717
+ META_INVALID_AD_ACCOUNT: "Ad account not found or no access. Confirm the `adAccountId` with `list_meta_ad_accounts`.",
718
+ META_MISSING_PAYMENT: "Your Meta Ads account has no payment method configured. Visit https://business.facebook.com/billing_hub/payment_settings, select the ad account, and add a card before retrying.",
719
+ META_TOKEN_EXPIRED: "Your Meta session expired. Reconnect Facebook at https://productmaker.app/integrations.",
720
+ META_ACCOUNT_RESTRICTED: "Your Meta Ads account is temporarily restricted from creating ads. Check the notices at https://business.facebook.com and complete the required verification.",
721
+ META_CAPABILITY_MISSING: "The Facebook app lacks required permissions for this ad account. Reconnect Facebook at https://productmaker.app/integrations and accept ALL requested permissions (including business_management).",
722
+ META_PERMISSION_DENIED: "The Facebook connection lacks sufficient permissions. Reconnect it at https://productmaker.app/integrations and accept all permissions.",
723
+ META_PIXEL_NOT_FOUND: "Pixel not found or not associated with the ad account. Verify the `pixelId` at https://business.facebook.com/events_manager.",
724
+ META_OBJECT_NOT_FOUND: "The requested object (campaign/ad set/ad) does not exist in Meta. Verify the ID or create a new one.",
725
+ META_AI_DISCLOSURE_REQUIRED: "Meta rejected the ad for undisclosed AI content. The system adds the disclosure automatically; if it persists, edit the copy to avoid language Meta flags as misleading.",
726
+ META_BID_STRATEGY_REQUIRES_AMOUNT: "Meta requires an explicit bid amount for this account/budget. Raise the daily budget or disable account-level bid caps.",
727
+ META_CBO_BUDGET_CONFLICT: "Budget conflict between campaign (CBO) and ad set. They cannot be mixed.",
728
+ META_BUDGET_MODE_CONFLICT: "Meta received budget at BOTH campaign and ad set level. Set only one.",
729
+ META_AD_SET_BUDGET_REQUIRED: "This campaign configuration requires ad-set-level budget, not campaign-level.",
730
+ META_DAILY_BUDGET_TOO_HIGH: "The daily budget exceeds the maximum allowed by Meta for this account. Lower it and retry.",
731
+ META_CAMPAIGN_CONFIG_INVALID: "Campaign configuration is invalid. Verify `dailyBudgetUsd`, `targetCountries`, and `campaignName`.",
732
+ META_WHATSAPP_DESTINATION_DISABLED: "WhatsApp destination is not enabled for this Meta account. Change the ad destination or enable WhatsApp Business on the account.",
733
+ // TikTok
734
+ TIKTOK_NOT_CONNECTED: "TikTok Ads is not connected. Connect it at https://productmaker.app/integrations.",
735
+ TIKTOK_TOKEN_EXPIRED: "The TikTok session expired. Reconnect it at https://productmaker.app/integrations.",
736
+ TIKTOK_PERMISSION_DENIED: "The TikTok connection lacks sufficient permissions. Reconnect it and accept all requested permissions.",
737
+ TIKTOK_MISSING_PAYMENT: "Your TikTok Ads account has no payment method configured. Add one at https://ads.tiktok.com.",
738
+ TIKTOK_PIXEL_NOT_FOUND: "TikTok Pixel not found or not associated with the advertiser. Verify the `pixelId`.",
739
+ TIKTOK_INVALID_ADVERTISER: "Advertiser not found or no access. Confirm the `advertiserId` with `list_tiktok_advertisers`.",
740
+ TIKTOK_ADVERTISER_REQUIRED: "Missing `advertiserId`. Obtain one with `list_tiktok_advertisers` before publishing.",
741
+ TIKTOK_IDENTITY_REQUIRED: "TikTok requires an identity (advertiser profile) to publish. Create one at https://ads.tiktok.com.",
742
+ TIKTOK_IDENTITY_API_REQUIRED: "TikTok requires the identities API to be enabled on this account. Contact TikTok Ads Support.",
743
+ TIKTOK_VIDEO_UPLOAD_FAILED: "TikTok rejected the video upload. Verify the format is MP4 under 500MB.",
744
+ TIKTOK_VIDEO_UPLOAD_TIMEOUT: "The video upload to TikTok timed out. Retry in a few minutes.",
745
+ TIKTOK_RATE_LIMITED: "TikTok rate-limited the request. Wait a few minutes before retrying.",
746
+ TIKTOK_SERVER_ERROR: "TikTok returned a server error. Retry in a few minutes.",
747
+ TIKTOK_INVALID_DATA: "TikTok rejected the request due to invalid data. Verify `advertiserId`, `pixelId`, and `campaignConfig`.",
748
+ // Campaign (shared)
749
+ CAMPAIGN_NOT_FOUND: "Campaign not found or does not belong to this user. Verify the `campaignId`.",
750
+ CAMPAIGN_TASK_NOT_FOUND: "Campaign task not found. Verify the `taskId`.",
751
+ CAMPAIGN_MISSING_LANDING_URL: "Missing landing URL for the campaign. Publish to Shopify first or pass `landingUrl` explicitly.",
752
+ CAMPAIGN_NO_CREATIVES: "The task has no creatives (video/image) ready for the campaign. Wait for generation to finish.",
753
+ CAMPAIGN_MISSING_THUMBNAILS: "Missing thumbnails for the videos. Verify that image/video generation has finished.",
754
+ // Connections
755
+ NO_SHOPIFY_SHOPS: "No Shopify stores connected. Connect one at https://productmaker.app/integrations.",
756
+ NO_META_AD_ACCOUNTS: "No Meta Ads accounts connected. Connect Facebook at https://productmaker.app/integrations.",
757
+ // Task lifecycle
758
+ TASK_NOT_FOUND: "Task not found or does not belong to this user. Verify the `taskId`.",
759
+ TASK_NOT_READY: "The task is not ready for this action yet. Wait for it to finish and retry.",
760
+ // API key
761
+ INVALID_API_KEY: "Invalid API key. Check your PM_API_KEY in the MCP client configuration.",
762
+ REVOKED_API_KEY: "API key revoked. Generate a new one at https://productmaker.app/account/api-keys.",
763
+ // Rate
764
+ RATE_LIMITED: "Too many requests. Wait a few seconds.",
765
+ // Generic
766
+ UPSTREAM_UNAVAILABLE: "The Product Maker service did not respond. Try again shortly.",
767
+ UPSTREAM_TIMEOUT: "Request timed out. Retry shortly.",
768
+ INTERNAL: "Internal error.",
769
+ INVALID_INPUT: "Invalid input."
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} (Detail: ${backend})`;
910
+ }
911
+ if (mapped) return mapped;
912
+ if (backend) return backend;
913
+ return `The service returned an error (code: ${code}). Try again shortly or contact support if it persists.`;
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
- 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(),
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
@@ -1399,7 +1642,11 @@ var MetaInput = z6.object({
1399
1642
  /** Select which angles to publish by 0-based index. Omit to publish all
1400
1643
  * successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
1401
1644
  angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
1402
- campaignConfig: McpCampaignConfig.optional()
1645
+ campaignConfig: McpCampaignConfig.optional(),
1646
+ /** Override the ad's primary text (body copy). If omitted, ProductMaker
1647
+ * uses the auto-generated copy attached to the task. Capped at 500 chars
1648
+ * (Meta's primary-text limit). */
1649
+ primaryText: z6.string().trim().min(1).max(500).optional()
1403
1650
  });
1404
1651
  var TikTokInput = z6.object({
1405
1652
  taskId: z6.string().min(1),
@@ -1407,13 +1654,18 @@ var TikTokInput = z6.object({
1407
1654
  pixelId: z6.string().min(1).optional(),
1408
1655
  landingUrl: z6.string().url().optional(),
1409
1656
  angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
1410
- campaignConfig: McpCampaignConfig.optional()
1657
+ campaignConfig: McpCampaignConfig.optional(),
1658
+ /** Override the ad's description (TikTok's body copy). If omitted,
1659
+ * ProductMaker uses the auto-generated copy attached to the task. */
1660
+ primaryText: z6.string().trim().min(1).max(500).optional()
1411
1661
  });
1412
1662
  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 incluye 3 im\xE1genes del hero. La respuesta es markdown con el link a la landing y al producto.", ShopifyInput.shape, async (input) => {
1663
+ 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
1664
  try {
1415
- const numImages = input.numLandingImages ?? 3;
1416
1665
  const variantIndex = input.variantIndex ?? 0;
1666
+ const availableImages = await fetchLandingImageCount(api, input.taskId);
1667
+ const effectiveAvailable = availableImages > 0 ? availableImages : 3;
1668
+ const numImages = Math.min(input.numLandingImages ?? effectiveAvailable, effectiveAvailable);
1417
1669
  const selectedLandingImages = Array.from({
1418
1670
  length: numImages
1419
1671
  }, (_, i) => ({
@@ -1430,7 +1682,7 @@ function registerPublishTools(server, api) {
1430
1682
  throw translateError(e);
1431
1683
  }
1432
1684
  });
1433
- server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en Meta Ads Manager. Use `list_meta_ad_accounts` antes para un adAccountId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 si la tarea gener\xF3 3 \xE1ngulos y el usuario solo quiere los #0 y #2, pasa [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, m\xE1x 1 para Meta), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_meta_campaign_status` para chequear el estado.", MetaInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
1685
+ server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en Meta Ads Manager. Use `list_meta_ad_accounts` antes para un adAccountId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 si la tarea gener\xF3 3 \xE1ngulos y el usuario solo quiere los #0 y #2, pasa [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, m\xE1x 1 para Meta), `campaignName` (m\xE1x 100 chars). `primaryText` (opcional, m\xE1x 500 chars): override del copy del anuncio \u2014 solo \xFAsalo si el usuario te pas\xF3 un texto espec\xEDfico; si no, ProductMaker usa el copy auto-generado de la tarea. Despu\xE9s de publicar usa `get_meta_campaign_status` para chequear el estado.", MetaInput.shape, async ({ taskId, campaignConfig, primaryText, ...rest }) => {
1434
1686
  try {
1435
1687
  const body = {
1436
1688
  ...rest
@@ -1438,6 +1690,7 @@ function registerPublishTools(server, api) {
1438
1690
  if (campaignConfig) {
1439
1691
  body.campaignOverrides = toMetaCampaignOverrides(campaignConfig);
1440
1692
  }
1693
+ if (primaryText) body.primaryText = primaryText;
1441
1694
  const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1442
1695
  return asText({
1443
1696
  ...r,
@@ -1447,7 +1700,7 @@ function registerPublishTools(server, api) {
1447
1700
  throw translateError(e);
1448
1701
  }
1449
1702
  });
1450
- server.tool("publish_to_tiktok", "Crea una campa\xF1a de TikTok Ads desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en TikTok Ads Manager. Use `list_tiktok_advertisers` antes para un advertiserId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 ej. [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, primero se usa), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_tiktok_campaign_status` para chequear el estado.", TikTokInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
1703
+ server.tool("publish_to_tiktok", "Crea una campa\xF1a de TikTok Ads desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en TikTok Ads Manager. Use `list_tiktok_advertisers` antes para un advertiserId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 ej. [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, primero se usa), `campaignName` (m\xE1x 100 chars). `primaryText` (opcional, m\xE1x 500 chars): override del copy del anuncio \u2014 solo \xFAsalo si el usuario te pas\xF3 un texto espec\xEDfico; si no, ProductMaker usa el copy auto-generado de la tarea. Despu\xE9s de publicar usa `get_tiktok_campaign_status` para chequear el estado.", TikTokInput.shape, async ({ taskId, campaignConfig, primaryText, ...rest }) => {
1451
1704
  try {
1452
1705
  const body = {
1453
1706
  ...rest
@@ -1455,6 +1708,7 @@ function registerPublishTools(server, api) {
1455
1708
  if (campaignConfig) {
1456
1709
  body.campaignConfig = toTikTokCampaignConfig(campaignConfig);
1457
1710
  }
1711
+ if (primaryText) body.primaryText = primaryText;
1458
1712
  const r = await api.postJson(`/v1/tiktok/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1459
1713
  return asText({
1460
1714
  ...r,
@@ -1490,6 +1744,19 @@ function renderShopifyPublishMarkdown(r) {
1490
1744
  };
1491
1745
  }
1492
1746
  __name(renderShopifyPublishMarkdown, "renderShopifyPublishMarkdown");
1747
+ async function fetchLandingImageCount(api, taskId) {
1748
+ try {
1749
+ const r = await api.getJson(`/v1/results/${encodeURIComponent(taskId)}`);
1750
+ if (!isRecord(r)) return 0;
1751
+ const landing = r.landing;
1752
+ if (!isRecord(landing)) return 0;
1753
+ const images = landing.images;
1754
+ return Array.isArray(images) ? images.length : 0;
1755
+ } catch {
1756
+ return 0;
1757
+ }
1758
+ }
1759
+ __name(fetchLandingImageCount, "fetchLandingImageCount");
1493
1760
  function toMetaCampaignOverrides(cfg) {
1494
1761
  const out = {};
1495
1762
  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 Shopify store is connected. Connect one at https://productmaker.app/integrations before publishing.",
703
+ SHOPIFY_TOKEN_EXPIRED: "The Shopify session expired. Reconnect the store at https://productmaker.app/integrations.",
704
+ SHOPIFY_SCOPE_UPGRADE_REQUIRED: "The Shopify connection needs additional scopes. Reconnect it at https://productmaker.app/integrations to approve the new permissions.",
705
+ SHOPIFY_PUBLISH_FAILED: "Shopify rejected the publish. Check that the product/landing does not already exist on the store and that your Shopify plan allows additional products.",
706
+ // Meta — high-frequency
707
+ META_NO_FB_PAGE: "No Facebook Page is associated with this account. Create one at https://www.facebook.com/pages/create before publishing (Meta requires a Page to run ads).",
708
+ META_NO_ADS_CREATED: "No ads could be created. Most likely: the Meta Ads account has no payment method, no Facebook Page is linked, or the Facebook app lost permissions. Verify all three at https://business.facebook.com.",
709
+ META_NO_ASSETS_READY: "The task has no videos or ad creative ready to publish yet. Wait for generation to finish or regenerate the assets.",
710
+ META_NO_THUMBNAIL_SOURCE: "No image is available to use as the video thumbnail. Upload a product image or generate the ad creative before publishing.",
711
+ META_NO_LANDING_URL: "Missing landing URL. Pass it in `landingUrl` or publish to Shopify first with `publish_to_shopify`.",
712
+ META_TASK_NOT_FOUND: "Task not found or does not belong to this user. Verify the `taskId`.",
713
+ META_INVALID_DATA: "Meta rejected the request due to invalid data. Verify that `adAccountId` and `pixelId` are correct.",
714
+ META_INVALID_AD_ACCOUNT: "Ad account not found or no access. Confirm the `adAccountId` with `list_meta_ad_accounts`.",
715
+ META_MISSING_PAYMENT: "Your Meta Ads account has no payment method configured. Visit https://business.facebook.com/billing_hub/payment_settings, select the ad account, and add a card before retrying.",
716
+ META_TOKEN_EXPIRED: "Your Meta session expired. Reconnect Facebook at https://productmaker.app/integrations.",
717
+ META_ACCOUNT_RESTRICTED: "Your Meta Ads account is temporarily restricted from creating ads. Check the notices at https://business.facebook.com and complete the required verification.",
718
+ META_CAPABILITY_MISSING: "The Facebook app lacks required permissions for this ad account. Reconnect Facebook at https://productmaker.app/integrations and accept ALL requested permissions (including business_management).",
719
+ META_PERMISSION_DENIED: "The Facebook connection lacks sufficient permissions. Reconnect it at https://productmaker.app/integrations and accept all permissions.",
720
+ META_PIXEL_NOT_FOUND: "Pixel not found or not associated with the ad account. Verify the `pixelId` at https://business.facebook.com/events_manager.",
721
+ META_OBJECT_NOT_FOUND: "The requested object (campaign/ad set/ad) does not exist in Meta. Verify the ID or create a new one.",
722
+ META_AI_DISCLOSURE_REQUIRED: "Meta rejected the ad for undisclosed AI content. The system adds the disclosure automatically; if it persists, edit the copy to avoid language Meta flags as misleading.",
723
+ META_BID_STRATEGY_REQUIRES_AMOUNT: "Meta requires an explicit bid amount for this account/budget. Raise the daily budget or disable account-level bid caps.",
724
+ META_CBO_BUDGET_CONFLICT: "Budget conflict between campaign (CBO) and ad set. They cannot be mixed.",
725
+ META_BUDGET_MODE_CONFLICT: "Meta received budget at BOTH campaign and ad set level. Set only one.",
726
+ META_AD_SET_BUDGET_REQUIRED: "This campaign configuration requires ad-set-level budget, not campaign-level.",
727
+ META_DAILY_BUDGET_TOO_HIGH: "The daily budget exceeds the maximum allowed by Meta for this account. Lower it and retry.",
728
+ META_CAMPAIGN_CONFIG_INVALID: "Campaign configuration is invalid. Verify `dailyBudgetUsd`, `targetCountries`, and `campaignName`.",
729
+ META_WHATSAPP_DESTINATION_DISABLED: "WhatsApp destination is not enabled for this Meta account. Change the ad destination or enable WhatsApp Business on the account.",
730
+ // TikTok
731
+ TIKTOK_NOT_CONNECTED: "TikTok Ads is not connected. Connect it at https://productmaker.app/integrations.",
732
+ TIKTOK_TOKEN_EXPIRED: "The TikTok session expired. Reconnect it at https://productmaker.app/integrations.",
733
+ TIKTOK_PERMISSION_DENIED: "The TikTok connection lacks sufficient permissions. Reconnect it and accept all requested permissions.",
734
+ TIKTOK_MISSING_PAYMENT: "Your TikTok Ads account has no payment method configured. Add one at https://ads.tiktok.com.",
735
+ TIKTOK_PIXEL_NOT_FOUND: "TikTok Pixel not found or not associated with the advertiser. Verify the `pixelId`.",
736
+ TIKTOK_INVALID_ADVERTISER: "Advertiser not found or no access. Confirm the `advertiserId` with `list_tiktok_advertisers`.",
737
+ TIKTOK_ADVERTISER_REQUIRED: "Missing `advertiserId`. Obtain one with `list_tiktok_advertisers` before publishing.",
738
+ TIKTOK_IDENTITY_REQUIRED: "TikTok requires an identity (advertiser profile) to publish. Create one at https://ads.tiktok.com.",
739
+ TIKTOK_IDENTITY_API_REQUIRED: "TikTok requires the identities API to be enabled on this account. Contact TikTok Ads Support.",
740
+ TIKTOK_VIDEO_UPLOAD_FAILED: "TikTok rejected the video upload. Verify the format is MP4 under 500MB.",
741
+ TIKTOK_VIDEO_UPLOAD_TIMEOUT: "The video upload to TikTok timed out. Retry in a few minutes.",
742
+ TIKTOK_RATE_LIMITED: "TikTok rate-limited the request. Wait a few minutes before retrying.",
743
+ TIKTOK_SERVER_ERROR: "TikTok returned a server error. Retry in a few minutes.",
744
+ TIKTOK_INVALID_DATA: "TikTok rejected the request due to invalid data. Verify `advertiserId`, `pixelId`, and `campaignConfig`.",
745
+ // Campaign (shared)
746
+ CAMPAIGN_NOT_FOUND: "Campaign not found or does not belong to this user. Verify the `campaignId`.",
747
+ CAMPAIGN_TASK_NOT_FOUND: "Campaign task not found. Verify the `taskId`.",
748
+ CAMPAIGN_MISSING_LANDING_URL: "Missing landing URL for the campaign. Publish to Shopify first or pass `landingUrl` explicitly.",
749
+ CAMPAIGN_NO_CREATIVES: "The task has no creatives (video/image) ready for the campaign. Wait for generation to finish.",
750
+ CAMPAIGN_MISSING_THUMBNAILS: "Missing thumbnails for the videos. Verify that image/video generation has finished.",
751
+ // Connections
752
+ NO_SHOPIFY_SHOPS: "No Shopify stores connected. Connect one at https://productmaker.app/integrations.",
753
+ NO_META_AD_ACCOUNTS: "No Meta Ads accounts connected. Connect Facebook at https://productmaker.app/integrations.",
754
+ // Task lifecycle
755
+ TASK_NOT_FOUND: "Task not found or does not belong to this user. Verify the `taskId`.",
756
+ TASK_NOT_READY: "The task is not ready for this action yet. Wait for it to finish and retry.",
757
+ // API key
758
+ INVALID_API_KEY: "Invalid API key. Check your PM_API_KEY in the MCP client configuration.",
759
+ REVOKED_API_KEY: "API key revoked. Generate a new one at https://productmaker.app/account/api-keys.",
760
+ // Rate
761
+ RATE_LIMITED: "Too many requests. Wait a few seconds.",
762
+ // Generic
763
+ UPSTREAM_UNAVAILABLE: "The Product Maker service did not respond. Try again shortly.",
764
+ UPSTREAM_TIMEOUT: "Request timed out. Retry shortly.",
765
+ INTERNAL: "Internal error.",
766
+ INVALID_INPUT: "Invalid input."
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} (Detail: ${backend})`;
907
+ }
908
+ if (mapped) return mapped;
909
+ if (backend) return backend;
910
+ return `The service returned an error (code: ${code}). Try again shortly or contact support if it persists.`;
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
- 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(),
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
@@ -1355,7 +1598,11 @@ var MetaInput = z6.object({
1355
1598
  /** Select which angles to publish by 0-based index. Omit to publish all
1356
1599
  * successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
1357
1600
  angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
1358
- campaignConfig: McpCampaignConfig.optional()
1601
+ campaignConfig: McpCampaignConfig.optional(),
1602
+ /** Override the ad's primary text (body copy). If omitted, ProductMaker
1603
+ * uses the auto-generated copy attached to the task. Capped at 500 chars
1604
+ * (Meta's primary-text limit). */
1605
+ primaryText: z6.string().trim().min(1).max(500).optional()
1359
1606
  });
1360
1607
  var TikTokInput = z6.object({
1361
1608
  taskId: z6.string().min(1),
@@ -1363,13 +1610,18 @@ var TikTokInput = z6.object({
1363
1610
  pixelId: z6.string().min(1).optional(),
1364
1611
  landingUrl: z6.string().url().optional(),
1365
1612
  angleIndexes: z6.array(z6.number().int().nonnegative()).optional(),
1366
- campaignConfig: McpCampaignConfig.optional()
1613
+ campaignConfig: McpCampaignConfig.optional(),
1614
+ /** Override the ad's description (TikTok's body copy). If omitted,
1615
+ * ProductMaker uses the auto-generated copy attached to the task. */
1616
+ primaryText: z6.string().trim().min(1).max(500).optional()
1367
1617
  });
1368
1618
  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 incluye 3 im\xE1genes del hero. La respuesta es markdown con el link a la landing y al producto.", ShopifyInput.shape, async (input) => {
1619
+ 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
1620
  try {
1371
- const numImages = input.numLandingImages ?? 3;
1372
1621
  const variantIndex = input.variantIndex ?? 0;
1622
+ const availableImages = await fetchLandingImageCount(api, input.taskId);
1623
+ const effectiveAvailable = availableImages > 0 ? availableImages : 3;
1624
+ const numImages = Math.min(input.numLandingImages ?? effectiveAvailable, effectiveAvailable);
1373
1625
  const selectedLandingImages = Array.from({
1374
1626
  length: numImages
1375
1627
  }, (_, i) => ({
@@ -1386,7 +1638,7 @@ function registerPublishTools(server, api) {
1386
1638
  throw translateError(e);
1387
1639
  }
1388
1640
  });
1389
- server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en Meta Ads Manager. Use `list_meta_ad_accounts` antes para un adAccountId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 si la tarea gener\xF3 3 \xE1ngulos y el usuario solo quiere los #0 y #2, pasa [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, m\xE1x 1 para Meta), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_meta_campaign_status` para chequear el estado.", MetaInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
1641
+ server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en Meta Ads Manager. Use `list_meta_ad_accounts` antes para un adAccountId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 si la tarea gener\xF3 3 \xE1ngulos y el usuario solo quiere los #0 y #2, pasa [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, m\xE1x 1 para Meta), `campaignName` (m\xE1x 100 chars). `primaryText` (opcional, m\xE1x 500 chars): override del copy del anuncio \u2014 solo \xFAsalo si el usuario te pas\xF3 un texto espec\xEDfico; si no, ProductMaker usa el copy auto-generado de la tarea. Despu\xE9s de publicar usa `get_meta_campaign_status` para chequear el estado.", MetaInput.shape, async ({ taskId, campaignConfig, primaryText, ...rest }) => {
1390
1642
  try {
1391
1643
  const body = {
1392
1644
  ...rest
@@ -1394,6 +1646,7 @@ function registerPublishTools(server, api) {
1394
1646
  if (campaignConfig) {
1395
1647
  body.campaignOverrides = toMetaCampaignOverrides(campaignConfig);
1396
1648
  }
1649
+ if (primaryText) body.primaryText = primaryText;
1397
1650
  const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1398
1651
  return asText({
1399
1652
  ...r,
@@ -1403,7 +1656,7 @@ function registerPublishTools(server, api) {
1403
1656
  throw translateError(e);
1404
1657
  }
1405
1658
  });
1406
- server.tool("publish_to_tiktok", "Crea una campa\xF1a de TikTok Ads desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en TikTok Ads Manager. Use `list_tiktok_advertisers` antes para un advertiserId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 ej. [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, primero se usa), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_tiktok_campaign_status` para chequear el estado.", TikTokInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
1659
+ server.tool("publish_to_tiktok", "Crea una campa\xF1a de TikTok Ads desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en TikTok Ads Manager. Use `list_tiktok_advertisers` antes para un advertiserId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 ej. [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, primero se usa), `campaignName` (m\xE1x 100 chars). `primaryText` (opcional, m\xE1x 500 chars): override del copy del anuncio \u2014 solo \xFAsalo si el usuario te pas\xF3 un texto espec\xEDfico; si no, ProductMaker usa el copy auto-generado de la tarea. Despu\xE9s de publicar usa `get_tiktok_campaign_status` para chequear el estado.", TikTokInput.shape, async ({ taskId, campaignConfig, primaryText, ...rest }) => {
1407
1660
  try {
1408
1661
  const body = {
1409
1662
  ...rest
@@ -1411,6 +1664,7 @@ function registerPublishTools(server, api) {
1411
1664
  if (campaignConfig) {
1412
1665
  body.campaignConfig = toTikTokCampaignConfig(campaignConfig);
1413
1666
  }
1667
+ if (primaryText) body.primaryText = primaryText;
1414
1668
  const r = await api.postJson(`/v1/tiktok/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1415
1669
  return asText({
1416
1670
  ...r,
@@ -1446,6 +1700,19 @@ function renderShopifyPublishMarkdown(r) {
1446
1700
  };
1447
1701
  }
1448
1702
  __name(renderShopifyPublishMarkdown, "renderShopifyPublishMarkdown");
1703
+ async function fetchLandingImageCount(api, taskId) {
1704
+ try {
1705
+ const r = await api.getJson(`/v1/results/${encodeURIComponent(taskId)}`);
1706
+ if (!isRecord(r)) return 0;
1707
+ const landing = r.landing;
1708
+ if (!isRecord(landing)) return 0;
1709
+ const images = landing.images;
1710
+ return Array.isArray(images) ? images.length : 0;
1711
+ } catch {
1712
+ return 0;
1713
+ }
1714
+ }
1715
+ __name(fetchLandingImageCount, "fetchLandingImageCount");
1449
1716
  function toMetaCampaignOverrides(cfg) {
1450
1717
  const out = {};
1451
1718
  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.10.0",
3
+ "version": "0.12.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.",