@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.
- package/dist/http.js +374 -107
- package/dist/stdio.js +374 -107
- 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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.",
|