@productmaker/mcp 3.4.1 → 3.4.3

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 +169 -30
  2. package/dist/stdio.js +169 -30
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -3692,6 +3692,32 @@ var StatusResponse = z24.object({
3692
3692
  requiresAction: z24.boolean().optional(),
3693
3693
  /** True when the task needs the user's attention rather than passive progress. */
3694
3694
  needsReview: z24.boolean().optional(),
3695
+ /**
3696
+ * THE answer to "does this task still owe the seller a question — now OR later
3697
+ * in this run?". The chat leaves for /tasks/:id only on an explicit `false`,
3698
+ * and /tasks/:id bounces her back on `true`. ONE field drives BOTH directions
3699
+ * on purpose: deciding them on two different facts is what looped
3700
+ * /tasks/:id ↔ wizard before (see apps/web/lib/task-routing.ts).
3701
+ *
3702
+ * STRICTLY WIDER THAN `needsReview`, and the difference is the whole point:
3703
+ * · `needsReview` answers "is a gate open RIGHT NOW", and it under-reports —
3704
+ * `buildPendingUserAction` only serves the reference-grouping question when
3705
+ * it is RENDERABLE, so an unconfirmed target reports `false` while
3706
+ * `angles.worker` will still pause at the END of the angles stage.
3707
+ * · this field adds the gates that are GUARANTEED to open: both pause flags,
3708
+ * plus that same worker prediction, through the one shared helper.
3709
+ *
3710
+ * Absent (undefined) means "an older API that cannot answer" — the client must
3711
+ * treat that as `true` and stay put. Uncertainty resolves to staying: a false
3712
+ * positive costs her a wait, a false negative strands her on a screen that
3713
+ * cannot render the gate (prod bd4d0bb1 / 52dc15a7, both refunded and
3714
+ * undelivered).
3715
+ *
3716
+ * ALWAYS false on a terminal task. `pauseForAngleReview` SURVIVES on completed
3717
+ * tasks, so without that guard a finished landing would report `true` forever
3718
+ * and get bounced into a chat with nothing left to ask.
3719
+ */
3720
+ willAskAgain: z24.boolean().optional(),
3695
3721
  /** True when an open gate exists that somebody can actually ANSWER. A strictly
3696
3722
  * narrower fact than `needsReview` — see the identical field on `TaskSummary`
3697
3723
  * below for why conflating the two produced a call to action that led nowhere. */
@@ -3826,6 +3852,9 @@ var CostEstimationType = z25.enum([
3826
3852
  "landing-regenerate",
3827
3853
  "landing-creative",
3828
3854
  "campaign-copy",
3855
+ "brand-extract",
3856
+ "persona-suggest",
3857
+ "chat-parse",
3829
3858
  "service-creative",
3830
3859
  "actor-grid"
3831
3860
  ]);
@@ -3991,11 +4020,14 @@ var LandingCreativeEstimationOptions = z25.object({
3991
4020
  landingStrategy: LandingStrategySchema.optional()
3992
4021
  });
3993
4022
  var CampaignCopyEstimationOptions = z25.object({
3994
- /** Number of creatives to generate copy for (1-4) */
3995
- nCreatives: z25.number().int().min(1).max(4),
3996
- /** Number of image creatives that will use vision analysis (0-4) */
3997
- nImages: z25.number().int().min(0).max(4).default(0)
3998
- });
4023
+ /** Number of creatives in the one copy-completion request (1-10). */
4024
+ nCreatives: z25.number().int().min(1).max(10),
4025
+ /** Number of image creatives that can each use one vision analysis (0-10). */
4026
+ nImages: z25.number().int().min(0).max(10).default(0)
4027
+ });
4028
+ var BrandExtractEstimationOptions = z25.object({}).strict();
4029
+ var PersonaSuggestEstimationOptions = z25.object({}).strict();
4030
+ var ChatParseEstimationOptions = z25.object({}).strict();
3999
4031
  var ServiceCreativeEstimationOptions = z25.object({
4000
4032
  mediaAssetCount: z25.number().int().min(0).max(8).optional(),
4001
4033
  uploadedVideoDurationSeconds: z25.number().min(0).max(240).optional()
@@ -4034,6 +4066,18 @@ var CostEstimationRequest = z25.discriminatedUnion("type", [
4034
4066
  type: z25.literal("campaign-copy"),
4035
4067
  options: CampaignCopyEstimationOptions
4036
4068
  }),
4069
+ z25.object({
4070
+ type: z25.literal("brand-extract"),
4071
+ options: BrandExtractEstimationOptions
4072
+ }),
4073
+ z25.object({
4074
+ type: z25.literal("persona-suggest"),
4075
+ options: PersonaSuggestEstimationOptions
4076
+ }),
4077
+ z25.object({
4078
+ type: z25.literal("chat-parse"),
4079
+ options: ChatParseEstimationOptions
4080
+ }),
4037
4081
  z25.object({
4038
4082
  type: z25.literal("service-creative"),
4039
4083
  options: ServiceCreativeEstimationOptions
@@ -4533,6 +4577,9 @@ var VideoMixQaJobSchema = z30.object({
4533
4577
  });
4534
4578
  var BrandExtractJobSchema = z30.object({
4535
4579
  brandId: z30.string().uuid(),
4580
+ // Generation is part of the durable paid-spend identity. Default 1 keeps
4581
+ // pre-migration BullMQ payloads parseable; their DB state fails safe.
4582
+ generation: z30.number().int().positive().default(1),
4536
4583
  url: z30.string().url().refine((value) => {
4537
4584
  const protocol = new URL(value).protocol;
4538
4585
  return protocol === "http:" || protocol === "https:";
@@ -4854,6 +4901,7 @@ var MetaCampaignType = z33.enum([
4854
4901
  "traffic",
4855
4902
  "engagement"
4856
4903
  ]);
4904
+ var MetaPublishAttemptId = z33.string().uuid();
4857
4905
  var MetaOptimizationMode = z33.enum([
4858
4906
  "advantage_plus",
4859
4907
  "manual"
@@ -5037,6 +5085,8 @@ var MetaCampaignDefaults = MetaCampaignConfigBase.omit({
5037
5085
  currency: z33.string().length(3).regex(/^[A-Z]{3}$/).nullable().optional()
5038
5086
  });
5039
5087
  var UploadToMetaRequest = z33.object({
5088
+ /** Caller-owned retry fence. Required before any provider operation. */
5089
+ publishAttemptId: MetaPublishAttemptId,
5040
5090
  /** Array of creatives to upload */
5041
5091
  creatives: z33.array(z33.object({
5042
5092
  creativeId: z33.string().min(1).optional(),
@@ -5193,6 +5243,8 @@ var MetaCampaignOverridesSchema = z34.object({
5193
5243
  destination: MetaCampaignDestination.optional()
5194
5244
  });
5195
5245
  var FacebookCreateCampaignBodySchema = z34.object({
5246
+ /** Caller-owned retry fence. Required before any provider operation. */
5247
+ publishAttemptId: MetaPublishAttemptId,
5196
5248
  adAccountId: MetaAdAccountId,
5197
5249
  portfolioId: z34.string().min(1).optional(),
5198
5250
  pageId: z34.string().min(1).optional(),
@@ -5262,6 +5314,8 @@ var FacebookCreateCampaignResponseSchema = z34.object({
5262
5314
  appliedBudget: z34.number().int().positive()
5263
5315
  });
5264
5316
  var FacebookCreateCampaignFromCreativesBodySchema = z34.object({
5317
+ /** Caller-owned retry fence. Required before any provider operation. */
5318
+ publishAttemptId: MetaPublishAttemptId,
5265
5319
  creatives: z34.array(z34.object({
5266
5320
  type: z34.enum([
5267
5321
  "image",
@@ -5409,6 +5463,8 @@ var FacebookCreativeSourceSchema = z34.discriminatedUnion("type", [
5409
5463
  FacebookMediaCreativeSourceSchema
5410
5464
  ]);
5411
5465
  var FacebookCreateCampaignFromTasksBodySchema = z34.object({
5466
+ /** Caller-owned retry fence. Required before any provider operation. */
5467
+ publishAttemptId: MetaPublishAttemptId,
5412
5468
  creatives: z34.array(z34.object({
5413
5469
  type: z34.enum([
5414
5470
  "image",
@@ -11631,7 +11687,7 @@ var ApiClient = class {
11631
11687
  };
11632
11688
 
11633
11689
  // src/index.ts
11634
- var MCP_VERSION = "3.4.1";
11690
+ var MCP_VERSION = "3.4.3";
11635
11691
 
11636
11692
  // src/meta-upload-app.ts
11637
11693
  import { readFileSync } from "fs";
@@ -11943,6 +11999,7 @@ __name(resolveOne, "resolveOne");
11943
11999
 
11944
12000
  // src/status-poller.ts
11945
12001
  var TERMINAL = /* @__PURE__ */ new Set([
12002
+ "done",
11946
12003
  "complete",
11947
12004
  "error"
11948
12005
  ]);
@@ -12618,6 +12675,7 @@ var McpErrorCode = {
12618
12675
  REVOKED_API_KEY: "REVOKED_API_KEY",
12619
12676
  // Rate limit / quota
12620
12677
  RATE_LIMITED: "RATE_LIMITED",
12678
+ CREDIT_ADMISSION_UNAVAILABLE: "CREDIT_ADMISSION_UNAVAILABLE",
12621
12679
  // Input
12622
12680
  INVALID_INPUT: "INVALID_INPUT",
12623
12681
  INSUFFICIENT_CREDITS: "INSUFFICIENT_CREDITS",
@@ -12675,6 +12733,12 @@ var McpErrorCode = {
12675
12733
  META_STAGED_ASSET_AMBIGUOUS: "META_STAGED_ASSET_AMBIGUOUS",
12676
12734
  META_STAGED_ASSET_TYPE_MISMATCH: "META_STAGED_ASSET_TYPE_MISMATCH",
12677
12735
  META_STAGED_ASSET_INVALID: "META_STAGED_ASSET_INVALID",
12736
+ META_STAGING_QUOTA_EXCEEDED: "META_STAGING_QUOTA_EXCEEDED",
12737
+ CAPACITY_UNAVAILABLE: "CAPACITY_UNAVAILABLE",
12738
+ UPLOAD_INCOMPLETE: "UPLOAD_INCOMPLETE",
12739
+ UPLOAD_EXPIRED: "UPLOAD_EXPIRED",
12740
+ META_UPLOAD_SESSION_EXPIRED: "META_UPLOAD_SESSION_EXPIRED",
12741
+ META_PUBLICATION_LEASE_STALE: "META_PUBLICATION_LEASE_STALE",
12678
12742
  META_PUBLICATION_RECONCILIATION_REQUIRED: "META_PUBLICATION_RECONCILIATION_REQUIRED",
12679
12743
  META_PUBLICATION_IN_PROGRESS: "META_PUBLICATION_IN_PROGRESS",
12680
12744
  META_NO_ADS_CREATED: "META_NO_ADS_CREATED",
@@ -12736,8 +12800,14 @@ var CODE_MESSAGES = {
12736
12800
  META_STAGED_ASSET_AMBIGUOUS: "The temporary media upload could not be uniquely resolved. Re-upload the media and retry the Meta publication.",
12737
12801
  META_STAGED_ASSET_TYPE_MISMATCH: "The temporary media type does not match the creative. Re-upload the correct image or video and retry.",
12738
12802
  META_STAGED_ASSET_INVALID: "The temporary media is empty or exceeds the server size limit. Re-upload the file and retry.",
12739
- META_PUBLICATION_RECONCILIATION_REQUIRED: "A previous Meta publication with these exact inputs may already exist. Check Meta before retrying so you do not create a duplicate campaign.",
12740
- META_PUBLICATION_IN_PROGRESS: "A Meta publication with these exact inputs is already in progress. Wait for it to finish, then check the campaign status.",
12803
+ META_STAGING_QUOTA_EXCEEDED: "The temporary Meta staging quota was reached. Keep completed files and retry only pending files after retryAt.",
12804
+ CAPACITY_UNAVAILABLE: "Temporary Meta staging capacity is unavailable. Keep completed files and retry only pending files later.",
12805
+ UPLOAD_INCOMPLETE: "A temporary Meta upload is incomplete. Keep completed files and retry only pending files.",
12806
+ UPLOAD_EXPIRED: "The temporary Meta upload expired. Re-upload this file before publishing.",
12807
+ META_UPLOAD_SESSION_EXPIRED: "The Meta upload session expired. Open a new session and retry only pending files.",
12808
+ META_PUBLICATION_LEASE_STALE: "The Meta publication lease expired or was replaced. Do not continue this stale publication; start a new explicit attempt only after confirming Meta did not receive it.",
12809
+ META_PUBLICATION_RECONCILIATION_REQUIRED: "Meta may already have received this publication. Do not resend it; check Meta Ads Manager.",
12810
+ META_PUBLICATION_IN_PROGRESS: "This Meta publication is already in progress. Do not resend it.",
12741
12811
  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.",
12742
12812
  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.",
12743
12813
  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.",
@@ -12806,6 +12876,7 @@ var CODE_MESSAGES = {
12806
12876
  REVOKED_API_KEY: "API key revoked. Generate a new one at https://productmaker.app/account/api-keys.",
12807
12877
  // Rate
12808
12878
  RATE_LIMITED: "Too many requests. Wait a few seconds.",
12879
+ CREDIT_ADMISSION_UNAVAILABLE: "Credit admission is temporarily unavailable. Do not retry a paid action until ProductMaker can verify the balance.",
12809
12880
  INSUFFICIENT_CREDITS: "Insufficient credits for this action. Check your balance and add credits at https://productmaker.app before retrying.",
12810
12881
  TASK_QUOTA_EXCEEDED: "You reached the active-task limit. Wait for a task to finish before creating another variant.",
12811
12882
  // Generic
@@ -12885,6 +12956,11 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
12885
12956
  McpErrorCode.META_STAGED_ASSET_AMBIGUOUS,
12886
12957
  McpErrorCode.META_STAGED_ASSET_TYPE_MISMATCH,
12887
12958
  McpErrorCode.META_STAGED_ASSET_INVALID,
12959
+ McpErrorCode.META_STAGING_QUOTA_EXCEEDED,
12960
+ McpErrorCode.UPLOAD_INCOMPLETE,
12961
+ McpErrorCode.UPLOAD_EXPIRED,
12962
+ McpErrorCode.META_UPLOAD_SESSION_EXPIRED,
12963
+ McpErrorCode.META_PUBLICATION_LEASE_STALE,
12888
12964
  McpErrorCode.META_PUBLICATION_RECONCILIATION_REQUIRED,
12889
12965
  McpErrorCode.META_PUBLICATION_IN_PROGRESS,
12890
12966
  McpErrorCode.TIKTOK_TOKEN_EXPIRED,
@@ -12900,6 +12976,7 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
12900
12976
  McpErrorCode.NO_META_AD_ACCOUNTS
12901
12977
  ]);
12902
12978
  var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
12979
+ McpErrorCode.CREDIT_ADMISSION_UNAVAILABLE,
12903
12980
  McpErrorCode.INSUFFICIENT_CREDITS,
12904
12981
  McpErrorCode.TASK_QUOTA_EXCEEDED,
12905
12982
  McpErrorCode.DROPSHIPPING_PROJECT_NOT_FOUND,
@@ -12943,6 +13020,14 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
12943
13020
  McpErrorCode.META_CAMPAIGN_CONFIG_INVALID,
12944
13021
  McpErrorCode.META_WHATSAPP_INVALID_NUMBER,
12945
13022
  McpErrorCode.META_INVALID_CTA,
13023
+ McpErrorCode.META_STAGING_QUOTA_EXCEEDED,
13024
+ McpErrorCode.CAPACITY_UNAVAILABLE,
13025
+ McpErrorCode.UPLOAD_INCOMPLETE,
13026
+ McpErrorCode.UPLOAD_EXPIRED,
13027
+ McpErrorCode.META_UPLOAD_SESSION_EXPIRED,
13028
+ McpErrorCode.META_PUBLICATION_LEASE_STALE,
13029
+ McpErrorCode.META_PUBLICATION_RECONCILIATION_REQUIRED,
13030
+ McpErrorCode.META_PUBLICATION_IN_PROGRESS,
12946
13031
  // TikTok
12947
13032
  McpErrorCode.TIKTOK_NOT_CONNECTED,
12948
13033
  McpErrorCode.TIKTOK_TOKEN_EXPIRED,
@@ -12998,6 +13083,7 @@ __name(looksLikeMeaningfulBackendMessage, "looksLikeMeaningfulBackendMessage");
12998
13083
  function resolveMessage(code, backendMessage) {
12999
13084
  const mapped = CODE_MESSAGES[code];
13000
13085
  const backend = typeof backendMessage === "string" ? backendMessage.trim() : "";
13086
+ if (code === McpErrorCode.CREDIT_ADMISSION_UNAVAILABLE && mapped) return mapped;
13001
13087
  if (mapped && backend && looksLikeMeaningfulBackendMessage(backend, mapped)) {
13002
13088
  return `${mapped} (Detail: ${backend})`;
13003
13089
  }
@@ -13030,6 +13116,10 @@ function translateError(err) {
13030
13116
  if (body.helpUrl !== void 0) extra.helpUrl = body.helpUrl;
13031
13117
  if (typeof body.requiredCredits === "number") extra.requiredCredits = body.requiredCredits;
13032
13118
  if (typeof body.availableCredits === "number") extra.availableCredits = body.availableCredits;
13119
+ if (typeof body.dimension === "string") extra.dimension = body.dimension;
13120
+ if (typeof body.usage === "number") extra.usage = body.usage;
13121
+ if (typeof body.limit === "number") extra.limit = body.limit;
13122
+ if (typeof body.retryAt === "string") extra.retryAt = body.retryAt;
13033
13123
  return mcp(code, resolveMessage(code, body.message), extra);
13034
13124
  }
13035
13125
  if (err.status === 401) {
@@ -14703,6 +14793,57 @@ __name(registerActorTools, "registerActorTools");
14703
14793
 
14704
14794
  // src/tools/publish.ts
14705
14795
  import { z as z56 } from "zod";
14796
+
14797
+ // src/tools/stable-uuid.ts
14798
+ import { createHash as createHash3 } from "crypto";
14799
+ function canonicalize(value) {
14800
+ if (Array.isArray(value)) return value.map(canonicalize);
14801
+ if (value === null || typeof value !== "object") return value;
14802
+ const record = value;
14803
+ return Object.fromEntries(Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => [
14804
+ key,
14805
+ canonicalize(record[key])
14806
+ ]));
14807
+ }
14808
+ __name(canonicalize, "canonicalize");
14809
+ function canonicalJson(value) {
14810
+ return JSON.stringify(canonicalize(value));
14811
+ }
14812
+ __name(canonicalJson, "canonicalJson");
14813
+ function deriveUuidV5(name, namespace) {
14814
+ const namespaceBytes = Buffer.from(namespace.replaceAll("-", ""), "hex");
14815
+ const digest = createHash3("sha1").update(namespaceBytes).update(name, "utf8").digest();
14816
+ const bytes = Buffer.from(digest.subarray(0, 16));
14817
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
14818
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
14819
+ const hex = bytes.toString("hex");
14820
+ return [
14821
+ hex.slice(0, 8),
14822
+ hex.slice(8, 12),
14823
+ hex.slice(12, 16),
14824
+ hex.slice(16, 20),
14825
+ hex.slice(20)
14826
+ ].join("-");
14827
+ }
14828
+ __name(deriveUuidV5, "deriveUuidV5");
14829
+
14830
+ // src/tools/meta-publish-attempt-id.ts
14831
+ var META_PUBLISH_NAMESPACE = "7b19c9ac-b4eb-4db1-970e-c2f24ce30b98";
14832
+ function deriveMetaPublishAttemptId(input) {
14833
+ const operationIdentity = canonicalJson([
14834
+ input.fingerprint,
14835
+ input.toolName,
14836
+ [
14837
+ typeof input.requestId,
14838
+ input.requestId
14839
+ ],
14840
+ input.canonicalInput
14841
+ ]);
14842
+ return deriveUuidV5(operationIdentity, META_PUBLISH_NAMESPACE);
14843
+ }
14844
+ __name(deriveMetaPublishAttemptId, "deriveMetaPublishAttemptId");
14845
+
14846
+ // src/tools/publish.ts
14706
14847
  var ShopifyInput = z56.object({
14707
14848
  taskId: z56.string().min(1),
14708
14849
  shopDomain: z56.string().min(1),
@@ -15030,10 +15171,19 @@ function registerPublishTools(server, api, opts = {}) {
15030
15171
  throw translateError(e);
15031
15172
  }
15032
15173
  });
15033
- server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde UNA sola tarea. Antes llama `list_meta_destinations`, deja que el usuario elija el `portfolioId` (Business portfolio), el `adAccountId` y el `pageId` exactos, y env\xEDa esos tres valores; no adivines ni uses la primera p\xE1gina. Si quieres juntar creativos de VARIAS tareas usa `publish_creatives_to_meta`. La campa\xF1a se crea SIEMPRE PAUSED y el usuario la activa manualmente. `pixelId` es opcional. `angleIndexes` permite seleccionar \xE1ngulos. `campaignConfig` admite presupuesto en USD, pa\xEDses ISO-2 y nombre. `primaryText` permite reemplazar el copy y `phoneNumber` configura WhatsApp. Despu\xE9s usa `get_meta_campaign_status`.", MetaInput.shape, CREATES_EXTERNAL, async ({ taskId, campaignConfig, primaryText, phoneNumber, ...rest }) => {
15174
+ server.tool("publish_to_meta", "Publica tus creativos en Meta sin gastar cr\xE9ditos de ProductMaker. El presupuesto de anuncios se paga directamente desde tu cuenta de Meta. Crear creativos o copy con IA sigue usando cr\xE9ditos. Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde UNA sola tarea. Antes llama `list_meta_destinations`, deja que el usuario elija el `portfolioId` (Business portfolio), el `adAccountId` y el `pageId` exactos, y env\xEDa esos tres valores; no adivines ni uses la primera p\xE1gina. Si quieres juntar creativos de VARIAS tareas usa `publish_creatives_to_meta`. La campa\xF1a se crea SIEMPRE PAUSED y el usuario la activa manualmente. `pixelId` es opcional. `angleIndexes` permite seleccionar \xE1ngulos. `campaignConfig` admite presupuesto en USD, pa\xEDses ISO-2 y nombre. `primaryText` permite reemplazar el copy y `phoneNumber` configura WhatsApp. Despu\xE9s usa `get_meta_campaign_status`. Si el resultado es ambiguo, NO reenv\xEDes: revisa Meta Ads Manager.", MetaInput.shape, CREATES_EXTERNAL, async (rawInput, extra) => {
15034
15175
  try {
15176
+ const parsed = MetaInput.safeParse(rawInput);
15177
+ if (!parsed.success) throw invalidInputFromZod(parsed.error);
15178
+ const { taskId, campaignConfig, primaryText, phoneNumber, ...rest } = parsed.data;
15035
15179
  const body = {
15036
- ...rest
15180
+ ...rest,
15181
+ publishAttemptId: deriveMetaPublishAttemptId({
15182
+ fingerprint: api.fingerprint(),
15183
+ toolName: "publish_to_meta",
15184
+ requestId: extra.requestId,
15185
+ canonicalInput: parsed.data
15186
+ })
15037
15187
  };
15038
15188
  if (campaignConfig) {
15039
15189
  body.campaignOverrides = toMetaCampaignOverrides(campaignConfig);
@@ -15058,7 +15208,7 @@ function registerPublishTools(server, api, opts = {}) {
15058
15208
  throw translateError(e);
15059
15209
  }
15060
15210
  });
15061
- server.tool("publish_creatives_to_meta", "Crea UNA campa\xF1a de Meta Ads PAUSED agrupando hasta " + CAMPAIGN_LIMITS.MAX_CREATIVES + ' creativos. Si la petici\xF3n natural adjunta o menciona archivos locales, llama primero `request_media_upload`; tras OK usa cada resultado como `source.type="asset"` con su `assetId` y el `type` que coincide con `mediaKind`. No pidas path ni URL. Despu\xE9s llama `list_meta_destinations` y pregunta en el chat por el `portfolioId`, `adAccountId` y `pageId` exactos; el App es solo para archivos. Si la carga fue parcial, publica s\xF3lo los assets confirmados; nunca inventes assets ni superes diez creativos. Las rutas locales s\xF3lo funcionan con MCP stdio. Los medios propios se eliminan despu\xE9s de publicar correctamente; si falla, quedan durante el TTL para reintento. Para videos la miniatura es opcional y ProductMaker puede generarla con FFmpeg. `campaignConfig.destination` acepta landing o WhatsApp. Para landing basta `campaignConfig.destination.landingUrl`; no dupliques la URL arriba. Si omites `budgetMode`, se infiere `ad_set` cuando cada ad set trae `dailyBudget`, o `campaign` cuando env\xEDas `campaignDailyBudget`. No actives la campa\xF1a autom\xE1ticamente.', PublishCreativesToMetaInputBase.shape, CREATES_EXTERNAL, async (rawInput) => {
15211
+ server.tool("publish_creatives_to_meta", "Crea UNA campa\xF1a de Meta Ads PAUSED agrupando hasta " + CAMPAIGN_LIMITS.MAX_CREATIVES + ' creativos. Publica tus creativos en Meta sin gastar cr\xE9ditos de ProductMaker. El presupuesto de anuncios se paga directamente desde tu cuenta de Meta. Crear creativos o copy con IA sigue usando cr\xE9ditos. Si la petici\xF3n natural adjunta o menciona archivos locales, llama primero `request_media_upload`; tras OK usa cada resultado como `source.type="asset"` con su `assetId` y el `type` que coincide con `mediaKind`. No pidas path ni URL. Despu\xE9s llama `list_meta_destinations` y pregunta en el chat por el `portfolioId`, `adAccountId` y `pageId` exactos; el App es solo para archivos. Si la carga fue parcial, conserva los assets confirmados, procesa s\xF3lo los pendientes y publica \xFAnicamente assets confirmados; nunca inventes assets ni superes diez creativos. Las rutas locales s\xF3lo funcionan con MCP stdio. Los medios propios se eliminan despu\xE9s de publicar correctamente; si falla antes de enviar una mutaci\xF3n a Meta, quedan durante el TTL. Para videos la miniatura es opcional y ProductMaker puede generarla con FFmpeg. `campaignConfig.destination` acepta landing o WhatsApp. Para landing basta `campaignConfig.destination.landingUrl`; no dupliques la URL arriba. Si omites `budgetMode`, se infiere `ad_set` cuando cada ad set trae `dailyBudget`, o `campaign` cuando env\xEDas `campaignDailyBudget`. No actives la campa\xF1a autom\xE1ticamente. Si el resultado es ambiguo, NO reenv\xEDes: revisa Meta Ads Manager.', PublishCreativesToMetaInputBase.shape, CREATES_EXTERNAL, async (rawInput, extra) => {
15062
15212
  try {
15063
15213
  const parsed = PublishCreativesToMetaInput.safeParse(rawInput);
15064
15214
  if (!parsed.success) throw invalidInputFromZod(parsed.error);
@@ -15138,7 +15288,13 @@ function registerPublishTools(server, api, opts = {}) {
15138
15288
  landingUrl: input.landingUrl,
15139
15289
  targetCountry: input.targetCountry,
15140
15290
  campaignName: input.campaignName,
15141
- campaignConfig: input.campaignConfig
15291
+ campaignConfig: input.campaignConfig,
15292
+ publishAttemptId: deriveMetaPublishAttemptId({
15293
+ fingerprint: api.fingerprint(),
15294
+ toolName: "publish_creatives_to_meta",
15295
+ requestId: extra.requestId,
15296
+ canonicalInput: input
15297
+ })
15142
15298
  });
15143
15299
  return asText({
15144
15300
  ...r,
@@ -16025,29 +16181,12 @@ __name(formatPercent, "formatPercent");
16025
16181
  import { z as z62 } from "zod";
16026
16182
 
16027
16183
  // src/tools/product-research-idempotency.ts
16028
- import { createHash as createHash3 } from "crypto";
16029
16184
  var RESEARCH_IDEMPOTENCY_WINDOW_MS = 2 * 60 * 60 * 1e3;
16030
16185
  var NAMESPACE = "6f9d3a1e-8c47-4b52-9f10-2d7e5a3c8b64";
16031
- function uuidV5(name, namespace) {
16032
- const namespaceBytes = Buffer.from(namespace.replace(/-/g, ""), "hex");
16033
- const digest = createHash3("sha1").update(namespaceBytes).update(name, "utf8").digest();
16034
- const bytes = Buffer.from(digest.subarray(0, 16));
16035
- bytes[6] = (bytes[6] ?? 0) & 15 | 80;
16036
- bytes[8] = (bytes[8] ?? 0) & 63 | 128;
16037
- const hex = bytes.toString("hex");
16038
- return [
16039
- hex.slice(0, 8),
16040
- hex.slice(8, 12),
16041
- hex.slice(12, 16),
16042
- hex.slice(16, 20),
16043
- hex.slice(20)
16044
- ].join("-");
16045
- }
16046
- __name(uuidV5, "uuidV5");
16047
16186
  function deriveIdempotencyKey(input) {
16048
16187
  const window = Math.floor(input.nowMs / RESEARCH_IDEMPOTENCY_WINDOW_MS);
16049
16188
  const name = `${input.fingerprint}:${input.targetCountry}:${input.categoryKey}:${window}`;
16050
- return uuidV5(name, NAMESPACE);
16189
+ return deriveUuidV5(name, NAMESPACE);
16051
16190
  }
16052
16191
  __name(deriveIdempotencyKey, "deriveIdempotencyKey");
16053
16192
 
package/dist/stdio.js CHANGED
@@ -264,7 +264,7 @@ var ApiClient = class {
264
264
  };
265
265
 
266
266
  // src/index.ts
267
- var MCP_VERSION = "3.4.1";
267
+ var MCP_VERSION = "3.4.3";
268
268
 
269
269
  // src/meta-upload-app.ts
270
270
  import { readFileSync } from "fs";
@@ -576,6 +576,7 @@ __name(resolveOne, "resolveOne");
576
576
 
577
577
  // src/status-poller.ts
578
578
  var TERMINAL = /* @__PURE__ */ new Set([
579
+ "done",
579
580
  "complete",
580
581
  "error"
581
582
  ]);
@@ -4313,6 +4314,32 @@ var StatusResponse = z24.object({
4313
4314
  requiresAction: z24.boolean().optional(),
4314
4315
  /** True when the task needs the user's attention rather than passive progress. */
4315
4316
  needsReview: z24.boolean().optional(),
4317
+ /**
4318
+ * THE answer to "does this task still owe the seller a question — now OR later
4319
+ * in this run?". The chat leaves for /tasks/:id only on an explicit `false`,
4320
+ * and /tasks/:id bounces her back on `true`. ONE field drives BOTH directions
4321
+ * on purpose: deciding them on two different facts is what looped
4322
+ * /tasks/:id ↔ wizard before (see apps/web/lib/task-routing.ts).
4323
+ *
4324
+ * STRICTLY WIDER THAN `needsReview`, and the difference is the whole point:
4325
+ * · `needsReview` answers "is a gate open RIGHT NOW", and it under-reports —
4326
+ * `buildPendingUserAction` only serves the reference-grouping question when
4327
+ * it is RENDERABLE, so an unconfirmed target reports `false` while
4328
+ * `angles.worker` will still pause at the END of the angles stage.
4329
+ * · this field adds the gates that are GUARANTEED to open: both pause flags,
4330
+ * plus that same worker prediction, through the one shared helper.
4331
+ *
4332
+ * Absent (undefined) means "an older API that cannot answer" — the client must
4333
+ * treat that as `true` and stay put. Uncertainty resolves to staying: a false
4334
+ * positive costs her a wait, a false negative strands her on a screen that
4335
+ * cannot render the gate (prod bd4d0bb1 / 52dc15a7, both refunded and
4336
+ * undelivered).
4337
+ *
4338
+ * ALWAYS false on a terminal task. `pauseForAngleReview` SURVIVES on completed
4339
+ * tasks, so without that guard a finished landing would report `true` forever
4340
+ * and get bounced into a chat with nothing left to ask.
4341
+ */
4342
+ willAskAgain: z24.boolean().optional(),
4316
4343
  /** True when an open gate exists that somebody can actually ANSWER. A strictly
4317
4344
  * narrower fact than `needsReview` — see the identical field on `TaskSummary`
4318
4345
  * below for why conflating the two produced a call to action that led nowhere. */
@@ -4447,6 +4474,9 @@ var CostEstimationType = z25.enum([
4447
4474
  "landing-regenerate",
4448
4475
  "landing-creative",
4449
4476
  "campaign-copy",
4477
+ "brand-extract",
4478
+ "persona-suggest",
4479
+ "chat-parse",
4450
4480
  "service-creative",
4451
4481
  "actor-grid"
4452
4482
  ]);
@@ -4612,11 +4642,14 @@ var LandingCreativeEstimationOptions = z25.object({
4612
4642
  landingStrategy: LandingStrategySchema.optional()
4613
4643
  });
4614
4644
  var CampaignCopyEstimationOptions = z25.object({
4615
- /** Number of creatives to generate copy for (1-4) */
4616
- nCreatives: z25.number().int().min(1).max(4),
4617
- /** Number of image creatives that will use vision analysis (0-4) */
4618
- nImages: z25.number().int().min(0).max(4).default(0)
4619
- });
4645
+ /** Number of creatives in the one copy-completion request (1-10). */
4646
+ nCreatives: z25.number().int().min(1).max(10),
4647
+ /** Number of image creatives that can each use one vision analysis (0-10). */
4648
+ nImages: z25.number().int().min(0).max(10).default(0)
4649
+ });
4650
+ var BrandExtractEstimationOptions = z25.object({}).strict();
4651
+ var PersonaSuggestEstimationOptions = z25.object({}).strict();
4652
+ var ChatParseEstimationOptions = z25.object({}).strict();
4620
4653
  var ServiceCreativeEstimationOptions = z25.object({
4621
4654
  mediaAssetCount: z25.number().int().min(0).max(8).optional(),
4622
4655
  uploadedVideoDurationSeconds: z25.number().min(0).max(240).optional()
@@ -4655,6 +4688,18 @@ var CostEstimationRequest = z25.discriminatedUnion("type", [
4655
4688
  type: z25.literal("campaign-copy"),
4656
4689
  options: CampaignCopyEstimationOptions
4657
4690
  }),
4691
+ z25.object({
4692
+ type: z25.literal("brand-extract"),
4693
+ options: BrandExtractEstimationOptions
4694
+ }),
4695
+ z25.object({
4696
+ type: z25.literal("persona-suggest"),
4697
+ options: PersonaSuggestEstimationOptions
4698
+ }),
4699
+ z25.object({
4700
+ type: z25.literal("chat-parse"),
4701
+ options: ChatParseEstimationOptions
4702
+ }),
4658
4703
  z25.object({
4659
4704
  type: z25.literal("service-creative"),
4660
4705
  options: ServiceCreativeEstimationOptions
@@ -5154,6 +5199,9 @@ var VideoMixQaJobSchema = z30.object({
5154
5199
  });
5155
5200
  var BrandExtractJobSchema = z30.object({
5156
5201
  brandId: z30.string().uuid(),
5202
+ // Generation is part of the durable paid-spend identity. Default 1 keeps
5203
+ // pre-migration BullMQ payloads parseable; their DB state fails safe.
5204
+ generation: z30.number().int().positive().default(1),
5157
5205
  url: z30.string().url().refine((value) => {
5158
5206
  const protocol = new URL(value).protocol;
5159
5207
  return protocol === "http:" || protocol === "https:";
@@ -5475,6 +5523,7 @@ var MetaCampaignType = z33.enum([
5475
5523
  "traffic",
5476
5524
  "engagement"
5477
5525
  ]);
5526
+ var MetaPublishAttemptId = z33.string().uuid();
5478
5527
  var MetaOptimizationMode = z33.enum([
5479
5528
  "advantage_plus",
5480
5529
  "manual"
@@ -5658,6 +5707,8 @@ var MetaCampaignDefaults = MetaCampaignConfigBase.omit({
5658
5707
  currency: z33.string().length(3).regex(/^[A-Z]{3}$/).nullable().optional()
5659
5708
  });
5660
5709
  var UploadToMetaRequest = z33.object({
5710
+ /** Caller-owned retry fence. Required before any provider operation. */
5711
+ publishAttemptId: MetaPublishAttemptId,
5661
5712
  /** Array of creatives to upload */
5662
5713
  creatives: z33.array(z33.object({
5663
5714
  creativeId: z33.string().min(1).optional(),
@@ -5814,6 +5865,8 @@ var MetaCampaignOverridesSchema = z34.object({
5814
5865
  destination: MetaCampaignDestination.optional()
5815
5866
  });
5816
5867
  var FacebookCreateCampaignBodySchema = z34.object({
5868
+ /** Caller-owned retry fence. Required before any provider operation. */
5869
+ publishAttemptId: MetaPublishAttemptId,
5817
5870
  adAccountId: MetaAdAccountId,
5818
5871
  portfolioId: z34.string().min(1).optional(),
5819
5872
  pageId: z34.string().min(1).optional(),
@@ -5883,6 +5936,8 @@ var FacebookCreateCampaignResponseSchema = z34.object({
5883
5936
  appliedBudget: z34.number().int().positive()
5884
5937
  });
5885
5938
  var FacebookCreateCampaignFromCreativesBodySchema = z34.object({
5939
+ /** Caller-owned retry fence. Required before any provider operation. */
5940
+ publishAttemptId: MetaPublishAttemptId,
5886
5941
  creatives: z34.array(z34.object({
5887
5942
  type: z34.enum([
5888
5943
  "image",
@@ -6030,6 +6085,8 @@ var FacebookCreativeSourceSchema = z34.discriminatedUnion("type", [
6030
6085
  FacebookMediaCreativeSourceSchema
6031
6086
  ]);
6032
6087
  var FacebookCreateCampaignFromTasksBodySchema = z34.object({
6088
+ /** Caller-owned retry fence. Required before any provider operation. */
6089
+ publishAttemptId: MetaPublishAttemptId,
6033
6090
  creatives: z34.array(z34.object({
6034
6091
  type: z34.enum([
6035
6092
  "image",
@@ -11367,6 +11424,7 @@ var McpErrorCode = {
11367
11424
  REVOKED_API_KEY: "REVOKED_API_KEY",
11368
11425
  // Rate limit / quota
11369
11426
  RATE_LIMITED: "RATE_LIMITED",
11427
+ CREDIT_ADMISSION_UNAVAILABLE: "CREDIT_ADMISSION_UNAVAILABLE",
11370
11428
  // Input
11371
11429
  INVALID_INPUT: "INVALID_INPUT",
11372
11430
  INSUFFICIENT_CREDITS: "INSUFFICIENT_CREDITS",
@@ -11424,6 +11482,12 @@ var McpErrorCode = {
11424
11482
  META_STAGED_ASSET_AMBIGUOUS: "META_STAGED_ASSET_AMBIGUOUS",
11425
11483
  META_STAGED_ASSET_TYPE_MISMATCH: "META_STAGED_ASSET_TYPE_MISMATCH",
11426
11484
  META_STAGED_ASSET_INVALID: "META_STAGED_ASSET_INVALID",
11485
+ META_STAGING_QUOTA_EXCEEDED: "META_STAGING_QUOTA_EXCEEDED",
11486
+ CAPACITY_UNAVAILABLE: "CAPACITY_UNAVAILABLE",
11487
+ UPLOAD_INCOMPLETE: "UPLOAD_INCOMPLETE",
11488
+ UPLOAD_EXPIRED: "UPLOAD_EXPIRED",
11489
+ META_UPLOAD_SESSION_EXPIRED: "META_UPLOAD_SESSION_EXPIRED",
11490
+ META_PUBLICATION_LEASE_STALE: "META_PUBLICATION_LEASE_STALE",
11427
11491
  META_PUBLICATION_RECONCILIATION_REQUIRED: "META_PUBLICATION_RECONCILIATION_REQUIRED",
11428
11492
  META_PUBLICATION_IN_PROGRESS: "META_PUBLICATION_IN_PROGRESS",
11429
11493
  META_NO_ADS_CREATED: "META_NO_ADS_CREATED",
@@ -11485,8 +11549,14 @@ var CODE_MESSAGES = {
11485
11549
  META_STAGED_ASSET_AMBIGUOUS: "The temporary media upload could not be uniquely resolved. Re-upload the media and retry the Meta publication.",
11486
11550
  META_STAGED_ASSET_TYPE_MISMATCH: "The temporary media type does not match the creative. Re-upload the correct image or video and retry.",
11487
11551
  META_STAGED_ASSET_INVALID: "The temporary media is empty or exceeds the server size limit. Re-upload the file and retry.",
11488
- META_PUBLICATION_RECONCILIATION_REQUIRED: "A previous Meta publication with these exact inputs may already exist. Check Meta before retrying so you do not create a duplicate campaign.",
11489
- META_PUBLICATION_IN_PROGRESS: "A Meta publication with these exact inputs is already in progress. Wait for it to finish, then check the campaign status.",
11552
+ META_STAGING_QUOTA_EXCEEDED: "The temporary Meta staging quota was reached. Keep completed files and retry only pending files after retryAt.",
11553
+ CAPACITY_UNAVAILABLE: "Temporary Meta staging capacity is unavailable. Keep completed files and retry only pending files later.",
11554
+ UPLOAD_INCOMPLETE: "A temporary Meta upload is incomplete. Keep completed files and retry only pending files.",
11555
+ UPLOAD_EXPIRED: "The temporary Meta upload expired. Re-upload this file before publishing.",
11556
+ META_UPLOAD_SESSION_EXPIRED: "The Meta upload session expired. Open a new session and retry only pending files.",
11557
+ META_PUBLICATION_LEASE_STALE: "The Meta publication lease expired or was replaced. Do not continue this stale publication; start a new explicit attempt only after confirming Meta did not receive it.",
11558
+ META_PUBLICATION_RECONCILIATION_REQUIRED: "Meta may already have received this publication. Do not resend it; check Meta Ads Manager.",
11559
+ META_PUBLICATION_IN_PROGRESS: "This Meta publication is already in progress. Do not resend it.",
11490
11560
  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.",
11491
11561
  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.",
11492
11562
  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.",
@@ -11555,6 +11625,7 @@ var CODE_MESSAGES = {
11555
11625
  REVOKED_API_KEY: "API key revoked. Generate a new one at https://productmaker.app/account/api-keys.",
11556
11626
  // Rate
11557
11627
  RATE_LIMITED: "Too many requests. Wait a few seconds.",
11628
+ CREDIT_ADMISSION_UNAVAILABLE: "Credit admission is temporarily unavailable. Do not retry a paid action until ProductMaker can verify the balance.",
11558
11629
  INSUFFICIENT_CREDITS: "Insufficient credits for this action. Check your balance and add credits at https://productmaker.app before retrying.",
11559
11630
  TASK_QUOTA_EXCEEDED: "You reached the active-task limit. Wait for a task to finish before creating another variant.",
11560
11631
  // Generic
@@ -11634,6 +11705,11 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
11634
11705
  McpErrorCode.META_STAGED_ASSET_AMBIGUOUS,
11635
11706
  McpErrorCode.META_STAGED_ASSET_TYPE_MISMATCH,
11636
11707
  McpErrorCode.META_STAGED_ASSET_INVALID,
11708
+ McpErrorCode.META_STAGING_QUOTA_EXCEEDED,
11709
+ McpErrorCode.UPLOAD_INCOMPLETE,
11710
+ McpErrorCode.UPLOAD_EXPIRED,
11711
+ McpErrorCode.META_UPLOAD_SESSION_EXPIRED,
11712
+ McpErrorCode.META_PUBLICATION_LEASE_STALE,
11637
11713
  McpErrorCode.META_PUBLICATION_RECONCILIATION_REQUIRED,
11638
11714
  McpErrorCode.META_PUBLICATION_IN_PROGRESS,
11639
11715
  McpErrorCode.TIKTOK_TOKEN_EXPIRED,
@@ -11649,6 +11725,7 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
11649
11725
  McpErrorCode.NO_META_AD_ACCOUNTS
11650
11726
  ]);
11651
11727
  var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
11728
+ McpErrorCode.CREDIT_ADMISSION_UNAVAILABLE,
11652
11729
  McpErrorCode.INSUFFICIENT_CREDITS,
11653
11730
  McpErrorCode.TASK_QUOTA_EXCEEDED,
11654
11731
  McpErrorCode.DROPSHIPPING_PROJECT_NOT_FOUND,
@@ -11692,6 +11769,14 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
11692
11769
  McpErrorCode.META_CAMPAIGN_CONFIG_INVALID,
11693
11770
  McpErrorCode.META_WHATSAPP_INVALID_NUMBER,
11694
11771
  McpErrorCode.META_INVALID_CTA,
11772
+ McpErrorCode.META_STAGING_QUOTA_EXCEEDED,
11773
+ McpErrorCode.CAPACITY_UNAVAILABLE,
11774
+ McpErrorCode.UPLOAD_INCOMPLETE,
11775
+ McpErrorCode.UPLOAD_EXPIRED,
11776
+ McpErrorCode.META_UPLOAD_SESSION_EXPIRED,
11777
+ McpErrorCode.META_PUBLICATION_LEASE_STALE,
11778
+ McpErrorCode.META_PUBLICATION_RECONCILIATION_REQUIRED,
11779
+ McpErrorCode.META_PUBLICATION_IN_PROGRESS,
11695
11780
  // TikTok
11696
11781
  McpErrorCode.TIKTOK_NOT_CONNECTED,
11697
11782
  McpErrorCode.TIKTOK_TOKEN_EXPIRED,
@@ -11747,6 +11832,7 @@ __name(looksLikeMeaningfulBackendMessage, "looksLikeMeaningfulBackendMessage");
11747
11832
  function resolveMessage(code, backendMessage) {
11748
11833
  const mapped = CODE_MESSAGES[code];
11749
11834
  const backend = typeof backendMessage === "string" ? backendMessage.trim() : "";
11835
+ if (code === McpErrorCode.CREDIT_ADMISSION_UNAVAILABLE && mapped) return mapped;
11750
11836
  if (mapped && backend && looksLikeMeaningfulBackendMessage(backend, mapped)) {
11751
11837
  return `${mapped} (Detail: ${backend})`;
11752
11838
  }
@@ -11779,6 +11865,10 @@ function translateError(err) {
11779
11865
  if (body.helpUrl !== void 0) extra.helpUrl = body.helpUrl;
11780
11866
  if (typeof body.requiredCredits === "number") extra.requiredCredits = body.requiredCredits;
11781
11867
  if (typeof body.availableCredits === "number") extra.availableCredits = body.availableCredits;
11868
+ if (typeof body.dimension === "string") extra.dimension = body.dimension;
11869
+ if (typeof body.usage === "number") extra.usage = body.usage;
11870
+ if (typeof body.limit === "number") extra.limit = body.limit;
11871
+ if (typeof body.retryAt === "string") extra.retryAt = body.retryAt;
11782
11872
  return mcp(code, resolveMessage(code, body.message), extra);
11783
11873
  }
11784
11874
  if (err.status === 401) {
@@ -13505,6 +13595,57 @@ __name(registerActorTools, "registerActorTools");
13505
13595
 
13506
13596
  // src/tools/publish.ts
13507
13597
  import { z as z55 } from "zod";
13598
+
13599
+ // src/tools/stable-uuid.ts
13600
+ import { createHash as createHash2 } from "crypto";
13601
+ function canonicalize(value) {
13602
+ if (Array.isArray(value)) return value.map(canonicalize);
13603
+ if (value === null || typeof value !== "object") return value;
13604
+ const record = value;
13605
+ return Object.fromEntries(Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => [
13606
+ key,
13607
+ canonicalize(record[key])
13608
+ ]));
13609
+ }
13610
+ __name(canonicalize, "canonicalize");
13611
+ function canonicalJson(value) {
13612
+ return JSON.stringify(canonicalize(value));
13613
+ }
13614
+ __name(canonicalJson, "canonicalJson");
13615
+ function deriveUuidV5(name, namespace) {
13616
+ const namespaceBytes = Buffer.from(namespace.replaceAll("-", ""), "hex");
13617
+ const digest = createHash2("sha1").update(namespaceBytes).update(name, "utf8").digest();
13618
+ const bytes = Buffer.from(digest.subarray(0, 16));
13619
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
13620
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
13621
+ const hex = bytes.toString("hex");
13622
+ return [
13623
+ hex.slice(0, 8),
13624
+ hex.slice(8, 12),
13625
+ hex.slice(12, 16),
13626
+ hex.slice(16, 20),
13627
+ hex.slice(20)
13628
+ ].join("-");
13629
+ }
13630
+ __name(deriveUuidV5, "deriveUuidV5");
13631
+
13632
+ // src/tools/meta-publish-attempt-id.ts
13633
+ var META_PUBLISH_NAMESPACE = "7b19c9ac-b4eb-4db1-970e-c2f24ce30b98";
13634
+ function deriveMetaPublishAttemptId(input) {
13635
+ const operationIdentity = canonicalJson([
13636
+ input.fingerprint,
13637
+ input.toolName,
13638
+ [
13639
+ typeof input.requestId,
13640
+ input.requestId
13641
+ ],
13642
+ input.canonicalInput
13643
+ ]);
13644
+ return deriveUuidV5(operationIdentity, META_PUBLISH_NAMESPACE);
13645
+ }
13646
+ __name(deriveMetaPublishAttemptId, "deriveMetaPublishAttemptId");
13647
+
13648
+ // src/tools/publish.ts
13508
13649
  var ShopifyInput = z55.object({
13509
13650
  taskId: z55.string().min(1),
13510
13651
  shopDomain: z55.string().min(1),
@@ -13832,10 +13973,19 @@ function registerPublishTools(server, api, opts = {}) {
13832
13973
  throw translateError(e);
13833
13974
  }
13834
13975
  });
13835
- server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde UNA sola tarea. Antes llama `list_meta_destinations`, deja que el usuario elija el `portfolioId` (Business portfolio), el `adAccountId` y el `pageId` exactos, y env\xEDa esos tres valores; no adivines ni uses la primera p\xE1gina. Si quieres juntar creativos de VARIAS tareas usa `publish_creatives_to_meta`. La campa\xF1a se crea SIEMPRE PAUSED y el usuario la activa manualmente. `pixelId` es opcional. `angleIndexes` permite seleccionar \xE1ngulos. `campaignConfig` admite presupuesto en USD, pa\xEDses ISO-2 y nombre. `primaryText` permite reemplazar el copy y `phoneNumber` configura WhatsApp. Despu\xE9s usa `get_meta_campaign_status`.", MetaInput.shape, CREATES_EXTERNAL, async ({ taskId, campaignConfig, primaryText, phoneNumber, ...rest }) => {
13976
+ server.tool("publish_to_meta", "Publica tus creativos en Meta sin gastar cr\xE9ditos de ProductMaker. El presupuesto de anuncios se paga directamente desde tu cuenta de Meta. Crear creativos o copy con IA sigue usando cr\xE9ditos. Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde UNA sola tarea. Antes llama `list_meta_destinations`, deja que el usuario elija el `portfolioId` (Business portfolio), el `adAccountId` y el `pageId` exactos, y env\xEDa esos tres valores; no adivines ni uses la primera p\xE1gina. Si quieres juntar creativos de VARIAS tareas usa `publish_creatives_to_meta`. La campa\xF1a se crea SIEMPRE PAUSED y el usuario la activa manualmente. `pixelId` es opcional. `angleIndexes` permite seleccionar \xE1ngulos. `campaignConfig` admite presupuesto en USD, pa\xEDses ISO-2 y nombre. `primaryText` permite reemplazar el copy y `phoneNumber` configura WhatsApp. Despu\xE9s usa `get_meta_campaign_status`. Si el resultado es ambiguo, NO reenv\xEDes: revisa Meta Ads Manager.", MetaInput.shape, CREATES_EXTERNAL, async (rawInput, extra) => {
13836
13977
  try {
13978
+ const parsed = MetaInput.safeParse(rawInput);
13979
+ if (!parsed.success) throw invalidInputFromZod(parsed.error);
13980
+ const { taskId, campaignConfig, primaryText, phoneNumber, ...rest } = parsed.data;
13837
13981
  const body = {
13838
- ...rest
13982
+ ...rest,
13983
+ publishAttemptId: deriveMetaPublishAttemptId({
13984
+ fingerprint: api.fingerprint(),
13985
+ toolName: "publish_to_meta",
13986
+ requestId: extra.requestId,
13987
+ canonicalInput: parsed.data
13988
+ })
13839
13989
  };
13840
13990
  if (campaignConfig) {
13841
13991
  body.campaignOverrides = toMetaCampaignOverrides(campaignConfig);
@@ -13860,7 +14010,7 @@ function registerPublishTools(server, api, opts = {}) {
13860
14010
  throw translateError(e);
13861
14011
  }
13862
14012
  });
13863
- server.tool("publish_creatives_to_meta", "Crea UNA campa\xF1a de Meta Ads PAUSED agrupando hasta " + CAMPAIGN_LIMITS.MAX_CREATIVES + ' creativos. Si la petici\xF3n natural adjunta o menciona archivos locales, llama primero `request_media_upload`; tras OK usa cada resultado como `source.type="asset"` con su `assetId` y el `type` que coincide con `mediaKind`. No pidas path ni URL. Despu\xE9s llama `list_meta_destinations` y pregunta en el chat por el `portfolioId`, `adAccountId` y `pageId` exactos; el App es solo para archivos. Si la carga fue parcial, publica s\xF3lo los assets confirmados; nunca inventes assets ni superes diez creativos. Las rutas locales s\xF3lo funcionan con MCP stdio. Los medios propios se eliminan despu\xE9s de publicar correctamente; si falla, quedan durante el TTL para reintento. Para videos la miniatura es opcional y ProductMaker puede generarla con FFmpeg. `campaignConfig.destination` acepta landing o WhatsApp. Para landing basta `campaignConfig.destination.landingUrl`; no dupliques la URL arriba. Si omites `budgetMode`, se infiere `ad_set` cuando cada ad set trae `dailyBudget`, o `campaign` cuando env\xEDas `campaignDailyBudget`. No actives la campa\xF1a autom\xE1ticamente.', PublishCreativesToMetaInputBase.shape, CREATES_EXTERNAL, async (rawInput) => {
14013
+ server.tool("publish_creatives_to_meta", "Crea UNA campa\xF1a de Meta Ads PAUSED agrupando hasta " + CAMPAIGN_LIMITS.MAX_CREATIVES + ' creativos. Publica tus creativos en Meta sin gastar cr\xE9ditos de ProductMaker. El presupuesto de anuncios se paga directamente desde tu cuenta de Meta. Crear creativos o copy con IA sigue usando cr\xE9ditos. Si la petici\xF3n natural adjunta o menciona archivos locales, llama primero `request_media_upload`; tras OK usa cada resultado como `source.type="asset"` con su `assetId` y el `type` que coincide con `mediaKind`. No pidas path ni URL. Despu\xE9s llama `list_meta_destinations` y pregunta en el chat por el `portfolioId`, `adAccountId` y `pageId` exactos; el App es solo para archivos. Si la carga fue parcial, conserva los assets confirmados, procesa s\xF3lo los pendientes y publica \xFAnicamente assets confirmados; nunca inventes assets ni superes diez creativos. Las rutas locales s\xF3lo funcionan con MCP stdio. Los medios propios se eliminan despu\xE9s de publicar correctamente; si falla antes de enviar una mutaci\xF3n a Meta, quedan durante el TTL. Para videos la miniatura es opcional y ProductMaker puede generarla con FFmpeg. `campaignConfig.destination` acepta landing o WhatsApp. Para landing basta `campaignConfig.destination.landingUrl`; no dupliques la URL arriba. Si omites `budgetMode`, se infiere `ad_set` cuando cada ad set trae `dailyBudget`, o `campaign` cuando env\xEDas `campaignDailyBudget`. No actives la campa\xF1a autom\xE1ticamente. Si el resultado es ambiguo, NO reenv\xEDes: revisa Meta Ads Manager.', PublishCreativesToMetaInputBase.shape, CREATES_EXTERNAL, async (rawInput, extra) => {
13864
14014
  try {
13865
14015
  const parsed = PublishCreativesToMetaInput.safeParse(rawInput);
13866
14016
  if (!parsed.success) throw invalidInputFromZod(parsed.error);
@@ -13940,7 +14090,13 @@ function registerPublishTools(server, api, opts = {}) {
13940
14090
  landingUrl: input.landingUrl,
13941
14091
  targetCountry: input.targetCountry,
13942
14092
  campaignName: input.campaignName,
13943
- campaignConfig: input.campaignConfig
14093
+ campaignConfig: input.campaignConfig,
14094
+ publishAttemptId: deriveMetaPublishAttemptId({
14095
+ fingerprint: api.fingerprint(),
14096
+ toolName: "publish_creatives_to_meta",
14097
+ requestId: extra.requestId,
14098
+ canonicalInput: input
14099
+ })
13944
14100
  });
13945
14101
  return asText({
13946
14102
  ...r,
@@ -14827,29 +14983,12 @@ __name(formatPercent, "formatPercent");
14827
14983
  import { z as z61 } from "zod";
14828
14984
 
14829
14985
  // src/tools/product-research-idempotency.ts
14830
- import { createHash as createHash2 } from "crypto";
14831
14986
  var RESEARCH_IDEMPOTENCY_WINDOW_MS = 2 * 60 * 60 * 1e3;
14832
14987
  var NAMESPACE = "6f9d3a1e-8c47-4b52-9f10-2d7e5a3c8b64";
14833
- function uuidV5(name, namespace) {
14834
- const namespaceBytes = Buffer.from(namespace.replace(/-/g, ""), "hex");
14835
- const digest = createHash2("sha1").update(namespaceBytes).update(name, "utf8").digest();
14836
- const bytes = Buffer.from(digest.subarray(0, 16));
14837
- bytes[6] = (bytes[6] ?? 0) & 15 | 80;
14838
- bytes[8] = (bytes[8] ?? 0) & 63 | 128;
14839
- const hex = bytes.toString("hex");
14840
- return [
14841
- hex.slice(0, 8),
14842
- hex.slice(8, 12),
14843
- hex.slice(12, 16),
14844
- hex.slice(16, 20),
14845
- hex.slice(20)
14846
- ].join("-");
14847
- }
14848
- __name(uuidV5, "uuidV5");
14849
14988
  function deriveIdempotencyKey(input) {
14850
14989
  const window = Math.floor(input.nowMs / RESEARCH_IDEMPOTENCY_WINDOW_MS);
14851
14990
  const name = `${input.fingerprint}:${input.targetCountry}:${input.categoryKey}:${window}`;
14852
- return uuidV5(name, NAMESPACE);
14991
+ return deriveUuidV5(name, NAMESPACE);
14853
14992
  }
14854
14993
  __name(deriveIdempotencyKey, "deriveIdempotencyKey");
14855
14994
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "3.4.1",
3
+ "version": "3.4.3",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "Product Maker MCP server — cost products, generate dropshipping landing pages, UGC videos, and ad creatives, and publish to Shopify and Meta Ads from Claude, ChatGPT, or Gemini.",