@productmaker/mcp 3.4.2 → 3.4.4

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 +126 -30
  2. package/dist/stdio.js +126 -30
  3. package/package.json +1 -1
package/dist/http.js CHANGED
@@ -3509,7 +3509,15 @@ var TaskCreativeVariantResult = TaskCreativeVariantBaseResult.extend({
3509
3509
  var MVPResult = z24.object({
3510
3510
  taskId: z24.string().uuid(),
3511
3511
  product: Product,
3512
- angles: z24.array(Angle).max(3),
3512
+ // READ contract, NOT a generation cap. The generator writes exactly 3 angles
3513
+ // (`replaceAngles` renumbers positions 0..2), but a duplicated angles run
3514
+ // leaves TWO full sets on the task — alert on task a1954a35 (2026-08-21):
3515
+ // 6 rows, positions 0,0,1,1,2,2. Because the controller guards the whole
3516
+ // response with `MVPResult.parse()`, a cap here turns extra-but-valid rows
3517
+ // into a 500 on GET /v1/results/:taskId and hides EVERY deliverable of a
3518
+ // task that already finished. Same failure shape as the variant landing cap
3519
+ // below (alert 88badc8). The write path is what must stay at 3.
3520
+ angles: z24.array(Angle),
3513
3521
  topVideos: z24.array(AdVideo).max(20),
3514
3522
  landing: z24.object({
3515
3523
  // Allow legacy data (10 sections × 2 variants) and new data (7 sections × 2)
@@ -3613,7 +3621,7 @@ var PendingUserActionSchema = z24.discriminatedUnion("kind", [
3613
3621
  actor: z24.string(),
3614
3622
  /** Saved custom actor (Actor.id) backing this angle, when one was chosen. */
3615
3623
  actorId: z24.string().optional()
3616
- })).min(1).max(3)
3624
+ })).min(1)
3617
3625
  }),
3618
3626
  z24.object({
3619
3627
  kind: z24.literal("service_script_approval"),
@@ -4901,6 +4909,7 @@ var MetaCampaignType = z33.enum([
4901
4909
  "traffic",
4902
4910
  "engagement"
4903
4911
  ]);
4912
+ var MetaPublishAttemptId = z33.string().uuid();
4904
4913
  var MetaOptimizationMode = z33.enum([
4905
4914
  "advantage_plus",
4906
4915
  "manual"
@@ -5084,6 +5093,8 @@ var MetaCampaignDefaults = MetaCampaignConfigBase.omit({
5084
5093
  currency: z33.string().length(3).regex(/^[A-Z]{3}$/).nullable().optional()
5085
5094
  });
5086
5095
  var UploadToMetaRequest = z33.object({
5096
+ /** Caller-owned retry fence. Required before any provider operation. */
5097
+ publishAttemptId: MetaPublishAttemptId,
5087
5098
  /** Array of creatives to upload */
5088
5099
  creatives: z33.array(z33.object({
5089
5100
  creativeId: z33.string().min(1).optional(),
@@ -5240,6 +5251,8 @@ var MetaCampaignOverridesSchema = z34.object({
5240
5251
  destination: MetaCampaignDestination.optional()
5241
5252
  });
5242
5253
  var FacebookCreateCampaignBodySchema = z34.object({
5254
+ /** Caller-owned retry fence. Required before any provider operation. */
5255
+ publishAttemptId: MetaPublishAttemptId,
5243
5256
  adAccountId: MetaAdAccountId,
5244
5257
  portfolioId: z34.string().min(1).optional(),
5245
5258
  pageId: z34.string().min(1).optional(),
@@ -5309,6 +5322,8 @@ var FacebookCreateCampaignResponseSchema = z34.object({
5309
5322
  appliedBudget: z34.number().int().positive()
5310
5323
  });
5311
5324
  var FacebookCreateCampaignFromCreativesBodySchema = z34.object({
5325
+ /** Caller-owned retry fence. Required before any provider operation. */
5326
+ publishAttemptId: MetaPublishAttemptId,
5312
5327
  creatives: z34.array(z34.object({
5313
5328
  type: z34.enum([
5314
5329
  "image",
@@ -5456,6 +5471,8 @@ var FacebookCreativeSourceSchema = z34.discriminatedUnion("type", [
5456
5471
  FacebookMediaCreativeSourceSchema
5457
5472
  ]);
5458
5473
  var FacebookCreateCampaignFromTasksBodySchema = z34.object({
5474
+ /** Caller-owned retry fence. Required before any provider operation. */
5475
+ publishAttemptId: MetaPublishAttemptId,
5459
5476
  creatives: z34.array(z34.object({
5460
5477
  type: z34.enum([
5461
5478
  "image",
@@ -11318,8 +11335,9 @@ var OAuthOboCredential = class {
11318
11335
  // src/guidance/productmaker-guidance.ts
11319
11336
  var START_DROPSHIPPING_PROMPT_NAME = "start_dropshipping";
11320
11337
  var PRODUCTMAKER_SERVER_INSTRUCTIONS = [
11321
- "When a ProductMaker request needs an image or video the user attached, pasted, or has locally and no reusable ProductMaker assetId is already available, call request_media_upload immediately before asking for any missing business inputs. Do this even when the user says not to generate or publish yet: uploading only stages the file and does not generate, publish, or consume credits, unless the user explicitly says not to upload, share, or store the file. Treat natural requests such as 'create a landing with this product', 'make creatives with this photo', or 'upload this video to Meta' as upload intent; never require the user to name the uploader.",
11338
+ "Whenever a ProductMaker request needs a new image or video and no reusable ProductMaker assetId is available, call request_media_upload immediately in the same response\u2014whether media is the next missing input or the user attached, pasted, mentioned, or has the file locally. The inline uploader is how ProductMaker asks for the file. Never merely ask the user to attach or upload it in chat and wait for a later turn. Call it before asking for any missing business inputs, even when the user says not to generate or publish yet: uploading only stages the file and does not generate, publish, or consume credits, unless the user explicitly says not to upload, share, or store the file. Treat natural requests such as 'create a landing with this product', 'make creatives with this photo', or 'upload this video to Meta' as upload intent; never require the user to name the uploader.",
11322
11339
  "For requests that do not need a new media upload, understand the user's intent before choosing a ProductMaker tool.",
11340
+ "For every concrete ProductMaker request to calculate how much to charge, profitability, or margin for a dropshipping product, you must call cost_product after collecting its missing country and real supplier cost. Never calculate or reproduce ProductMaker's costing formula yourself; answer from the tool result.",
11323
11341
  "For remote MCP hosts, never ask for a local path, CDN URL, bytes, or base64. Pass supported host file references to request_media_upload; otherwise let its inline App open the file picker. The external upload page is fallback only.",
11324
11342
  "After the upload App sends its OK continuation, resume the original request with the verified assetId values, do not ask for another upload, and ask only for remaining business inputs in chat. The host-inserted continuation may contain opaque asset IDs; do not repeat them in narrative prose or expose session IDs, tokens, private URLs, file names, or JSON handoff payloads.",
11325
11343
  "Keep focused requests direct: creatives, landing pages, Shopify publication, Meta publication, and status checks do not require a DropshippingProject.",
@@ -11678,7 +11696,7 @@ var ApiClient = class {
11678
11696
  };
11679
11697
 
11680
11698
  // src/index.ts
11681
- var MCP_VERSION = "3.4.2";
11699
+ var MCP_VERSION = "3.4.4";
11682
11700
 
11683
11701
  // src/meta-upload-app.ts
11684
11702
  import { readFileSync } from "fs";
@@ -12724,6 +12742,12 @@ var McpErrorCode = {
12724
12742
  META_STAGED_ASSET_AMBIGUOUS: "META_STAGED_ASSET_AMBIGUOUS",
12725
12743
  META_STAGED_ASSET_TYPE_MISMATCH: "META_STAGED_ASSET_TYPE_MISMATCH",
12726
12744
  META_STAGED_ASSET_INVALID: "META_STAGED_ASSET_INVALID",
12745
+ META_STAGING_QUOTA_EXCEEDED: "META_STAGING_QUOTA_EXCEEDED",
12746
+ CAPACITY_UNAVAILABLE: "CAPACITY_UNAVAILABLE",
12747
+ UPLOAD_INCOMPLETE: "UPLOAD_INCOMPLETE",
12748
+ UPLOAD_EXPIRED: "UPLOAD_EXPIRED",
12749
+ META_UPLOAD_SESSION_EXPIRED: "META_UPLOAD_SESSION_EXPIRED",
12750
+ META_PUBLICATION_LEASE_STALE: "META_PUBLICATION_LEASE_STALE",
12727
12751
  META_PUBLICATION_RECONCILIATION_REQUIRED: "META_PUBLICATION_RECONCILIATION_REQUIRED",
12728
12752
  META_PUBLICATION_IN_PROGRESS: "META_PUBLICATION_IN_PROGRESS",
12729
12753
  META_NO_ADS_CREATED: "META_NO_ADS_CREATED",
@@ -12785,8 +12809,14 @@ var CODE_MESSAGES = {
12785
12809
  META_STAGED_ASSET_AMBIGUOUS: "The temporary media upload could not be uniquely resolved. Re-upload the media and retry the Meta publication.",
12786
12810
  META_STAGED_ASSET_TYPE_MISMATCH: "The temporary media type does not match the creative. Re-upload the correct image or video and retry.",
12787
12811
  META_STAGED_ASSET_INVALID: "The temporary media is empty or exceeds the server size limit. Re-upload the file and retry.",
12788
- 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.",
12789
- 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.",
12812
+ META_STAGING_QUOTA_EXCEEDED: "The temporary Meta staging quota was reached. Keep completed files and retry only pending files after retryAt.",
12813
+ CAPACITY_UNAVAILABLE: "Temporary Meta staging capacity is unavailable. Keep completed files and retry only pending files later.",
12814
+ UPLOAD_INCOMPLETE: "A temporary Meta upload is incomplete. Keep completed files and retry only pending files.",
12815
+ UPLOAD_EXPIRED: "The temporary Meta upload expired. Re-upload this file before publishing.",
12816
+ META_UPLOAD_SESSION_EXPIRED: "The Meta upload session expired. Open a new session and retry only pending files.",
12817
+ 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.",
12818
+ META_PUBLICATION_RECONCILIATION_REQUIRED: "Meta may already have received this publication. Do not resend it; check Meta Ads Manager.",
12819
+ META_PUBLICATION_IN_PROGRESS: "This Meta publication is already in progress. Do not resend it.",
12790
12820
  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.",
12791
12821
  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.",
12792
12822
  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.",
@@ -12935,6 +12965,11 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
12935
12965
  McpErrorCode.META_STAGED_ASSET_AMBIGUOUS,
12936
12966
  McpErrorCode.META_STAGED_ASSET_TYPE_MISMATCH,
12937
12967
  McpErrorCode.META_STAGED_ASSET_INVALID,
12968
+ McpErrorCode.META_STAGING_QUOTA_EXCEEDED,
12969
+ McpErrorCode.UPLOAD_INCOMPLETE,
12970
+ McpErrorCode.UPLOAD_EXPIRED,
12971
+ McpErrorCode.META_UPLOAD_SESSION_EXPIRED,
12972
+ McpErrorCode.META_PUBLICATION_LEASE_STALE,
12938
12973
  McpErrorCode.META_PUBLICATION_RECONCILIATION_REQUIRED,
12939
12974
  McpErrorCode.META_PUBLICATION_IN_PROGRESS,
12940
12975
  McpErrorCode.TIKTOK_TOKEN_EXPIRED,
@@ -12994,6 +13029,14 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
12994
13029
  McpErrorCode.META_CAMPAIGN_CONFIG_INVALID,
12995
13030
  McpErrorCode.META_WHATSAPP_INVALID_NUMBER,
12996
13031
  McpErrorCode.META_INVALID_CTA,
13032
+ McpErrorCode.META_STAGING_QUOTA_EXCEEDED,
13033
+ McpErrorCode.CAPACITY_UNAVAILABLE,
13034
+ McpErrorCode.UPLOAD_INCOMPLETE,
13035
+ McpErrorCode.UPLOAD_EXPIRED,
13036
+ McpErrorCode.META_UPLOAD_SESSION_EXPIRED,
13037
+ McpErrorCode.META_PUBLICATION_LEASE_STALE,
13038
+ McpErrorCode.META_PUBLICATION_RECONCILIATION_REQUIRED,
13039
+ McpErrorCode.META_PUBLICATION_IN_PROGRESS,
12997
13040
  // TikTok
12998
13041
  McpErrorCode.TIKTOK_NOT_CONNECTED,
12999
13042
  McpErrorCode.TIKTOK_TOKEN_EXPIRED,
@@ -13082,6 +13125,10 @@ function translateError(err) {
13082
13125
  if (body.helpUrl !== void 0) extra.helpUrl = body.helpUrl;
13083
13126
  if (typeof body.requiredCredits === "number") extra.requiredCredits = body.requiredCredits;
13084
13127
  if (typeof body.availableCredits === "number") extra.availableCredits = body.availableCredits;
13128
+ if (typeof body.dimension === "string") extra.dimension = body.dimension;
13129
+ if (typeof body.usage === "number") extra.usage = body.usage;
13130
+ if (typeof body.limit === "number") extra.limit = body.limit;
13131
+ if (typeof body.retryAt === "string") extra.retryAt = body.retryAt;
13085
13132
  return mcp(code, resolveMessage(code, body.message), extra);
13086
13133
  }
13087
13134
  if (err.status === 401) {
@@ -14755,6 +14802,57 @@ __name(registerActorTools, "registerActorTools");
14755
14802
 
14756
14803
  // src/tools/publish.ts
14757
14804
  import { z as z56 } from "zod";
14805
+
14806
+ // src/tools/stable-uuid.ts
14807
+ import { createHash as createHash3 } from "crypto";
14808
+ function canonicalize(value) {
14809
+ if (Array.isArray(value)) return value.map(canonicalize);
14810
+ if (value === null || typeof value !== "object") return value;
14811
+ const record = value;
14812
+ return Object.fromEntries(Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => [
14813
+ key,
14814
+ canonicalize(record[key])
14815
+ ]));
14816
+ }
14817
+ __name(canonicalize, "canonicalize");
14818
+ function canonicalJson(value) {
14819
+ return JSON.stringify(canonicalize(value));
14820
+ }
14821
+ __name(canonicalJson, "canonicalJson");
14822
+ function deriveUuidV5(name, namespace) {
14823
+ const namespaceBytes = Buffer.from(namespace.replaceAll("-", ""), "hex");
14824
+ const digest = createHash3("sha1").update(namespaceBytes).update(name, "utf8").digest();
14825
+ const bytes = Buffer.from(digest.subarray(0, 16));
14826
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
14827
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
14828
+ const hex = bytes.toString("hex");
14829
+ return [
14830
+ hex.slice(0, 8),
14831
+ hex.slice(8, 12),
14832
+ hex.slice(12, 16),
14833
+ hex.slice(16, 20),
14834
+ hex.slice(20)
14835
+ ].join("-");
14836
+ }
14837
+ __name(deriveUuidV5, "deriveUuidV5");
14838
+
14839
+ // src/tools/meta-publish-attempt-id.ts
14840
+ var META_PUBLISH_NAMESPACE = "7b19c9ac-b4eb-4db1-970e-c2f24ce30b98";
14841
+ function deriveMetaPublishAttemptId(input) {
14842
+ const operationIdentity = canonicalJson([
14843
+ input.fingerprint,
14844
+ input.toolName,
14845
+ [
14846
+ typeof input.requestId,
14847
+ input.requestId
14848
+ ],
14849
+ input.canonicalInput
14850
+ ]);
14851
+ return deriveUuidV5(operationIdentity, META_PUBLISH_NAMESPACE);
14852
+ }
14853
+ __name(deriveMetaPublishAttemptId, "deriveMetaPublishAttemptId");
14854
+
14855
+ // src/tools/publish.ts
14758
14856
  var ShopifyInput = z56.object({
14759
14857
  taskId: z56.string().min(1),
14760
14858
  shopDomain: z56.string().min(1),
@@ -15082,10 +15180,19 @@ function registerPublishTools(server, api, opts = {}) {
15082
15180
  throw translateError(e);
15083
15181
  }
15084
15182
  });
15085
- 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 }) => {
15183
+ 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) => {
15086
15184
  try {
15185
+ const parsed = MetaInput.safeParse(rawInput);
15186
+ if (!parsed.success) throw invalidInputFromZod(parsed.error);
15187
+ const { taskId, campaignConfig, primaryText, phoneNumber, ...rest } = parsed.data;
15087
15188
  const body = {
15088
- ...rest
15189
+ ...rest,
15190
+ publishAttemptId: deriveMetaPublishAttemptId({
15191
+ fingerprint: api.fingerprint(),
15192
+ toolName: "publish_to_meta",
15193
+ requestId: extra.requestId,
15194
+ canonicalInput: parsed.data
15195
+ })
15089
15196
  };
15090
15197
  if (campaignConfig) {
15091
15198
  body.campaignOverrides = toMetaCampaignOverrides(campaignConfig);
@@ -15110,7 +15217,7 @@ function registerPublishTools(server, api, opts = {}) {
15110
15217
  throw translateError(e);
15111
15218
  }
15112
15219
  });
15113
- 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) => {
15220
+ 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) => {
15114
15221
  try {
15115
15222
  const parsed = PublishCreativesToMetaInput.safeParse(rawInput);
15116
15223
  if (!parsed.success) throw invalidInputFromZod(parsed.error);
@@ -15190,7 +15297,13 @@ function registerPublishTools(server, api, opts = {}) {
15190
15297
  landingUrl: input.landingUrl,
15191
15298
  targetCountry: input.targetCountry,
15192
15299
  campaignName: input.campaignName,
15193
- campaignConfig: input.campaignConfig
15300
+ campaignConfig: input.campaignConfig,
15301
+ publishAttemptId: deriveMetaPublishAttemptId({
15302
+ fingerprint: api.fingerprint(),
15303
+ toolName: "publish_creatives_to_meta",
15304
+ requestId: extra.requestId,
15305
+ canonicalInput: input
15306
+ })
15194
15307
  });
15195
15308
  return asText({
15196
15309
  ...r,
@@ -15424,7 +15537,7 @@ __name(resolveEndpointForSession, "resolveEndpointForSession");
15424
15537
  function registerMediaUploadTools(server, api) {
15425
15538
  registerAppTool(server, "request_media_upload", {
15426
15539
  title: "Subir archivos",
15427
- description: "Abre el selector universal de ProductMaker para im\xE1genes o videos del usuario. Ll\xE1malo autom\xE1ticamente y antes de preguntar por campos faltantes cuando una petici\xF3n natural adjunte o mencione un archivo local, por ejemplo \u2018crea una landing con este producto\u2019, \u2018haz creativos con esta foto\u2019 o \u2018sube este video a Meta\u2019; hazlo aunque el usuario diga que todav\xEDa no generes ni publiques, porque esta herramienta solo prepara el archivo y no consume cr\xE9ditos, salvo que proh\xEDba expl\xEDcitamente subir, compartir o almacenar el archivo; no pidas una ruta local ni URL. Es la \xFAnica ruta de carga para create_product_task, generate_image_creatives, generate_video_creative, generate_free_video, generate_landing y publish_creatives_to_meta. Pasa las referencias opcionales de ChatGPT en files; si el host no puede transferirlas, el mismo App muestra el selector inline. Usa uploadPurpose=product_reference para fotos de producto y landings, creative para creativos o anuncios, y generic solo cuando ninguno aplique. Usa mediaKind=image para referencias de producto y landings; mixed solo cuando el consumidor acepte ambos tipos. El App NO te avisa cuando termina: en cuanto el usuario diga que ya subi\xF3 el archivo o que sigas, llama `check_media_upload` con este sessionId para obtener los assetId, y reanuda la intenci\xF3n original usando \xFAnicamente assetId y mediaKind, sin pedir que vuelva a subir ni mostrar datos t\xE9cnicos. El enlace externo es fallback solo si el App inline falla. Esta herramienta no genera, publica ni consume cr\xE9ditos.",
15540
+ description: "Abre el selector universal de ProductMaker para im\xE1genes o videos del usuario. Cuando la foto o el video sea el siguiente dato faltante, ll\xE1malo en esa misma respuesta: no te limites a pedir que lo adjunte en el chat y esperar otro turno. Ll\xE1malo tambi\xE9n autom\xE1ticamente y antes de preguntar por campos faltantes adicionales cuando una petici\xF3n natural adjunte o mencione un archivo local, por ejemplo \u2018crea una landing con este producto\u2019, \u2018haz creativos con esta foto\u2019 o \u2018sube este video a Meta\u2019; hazlo aunque el usuario diga que todav\xEDa no generes ni publiques, porque esta herramienta solo prepara el archivo y no consume cr\xE9ditos, salvo que proh\xEDba expl\xEDcitamente subir, compartir o almacenar el archivo; no pidas una ruta local ni URL. Es la \xFAnica ruta de carga para create_product_task, generate_image_creatives, generate_video_creative, generate_free_video, generate_landing y publish_creatives_to_meta. Pasa las referencias opcionales de ChatGPT en files; si el host no puede transferirlas, el mismo App muestra el selector inline. Usa uploadPurpose=product_reference para fotos de producto y landings, creative para creativos o anuncios, y generic solo cuando ninguno aplique. Usa mediaKind=image para referencias de producto y landings; mixed solo cuando el consumidor acepte ambos tipos. El App NO te avisa cuando termina: en cuanto el usuario diga que ya subi\xF3 el archivo o que sigas, llama `check_media_upload` con este sessionId para obtener los assetId, y reanuda la intenci\xF3n original usando \xFAnicamente assetId y mediaKind, sin pedir que vuelva a subir ni mostrar datos t\xE9cnicos. El enlace externo es fallback solo si el App inline falla. Esta herramienta no genera, publica ni consume cr\xE9ditos.",
15428
15541
  inputSchema: UploadSessionToolInputShape,
15429
15542
  annotations: CREATES_INTERNAL,
15430
15543
  outputSchema: UniversalUploadStructuredResponse.shape,
@@ -15953,7 +16066,7 @@ var ProductCostingToolInput = z61.object({
15953
16066
  desiredMarginPercent: optionalPercent("Margen de ganancia deseado. Ejemplo: 10 significa 10%. Omite para usar el valor gu\xEDa editable del pa\xEDs.")
15954
16067
  });
15955
16068
  function registerProductCostingTools(server) {
15956
- server.tool("cost_product", "Calcula GRATIS y sin cr\xE9ditos cu\xE1nto cobrar por un producto de dropshipping en cualquiera de los pa\xEDses disponibles en ProductMaker. Es READ-ONLY: no crea tareas ni publica nada. Pide country y el costo real del proveedor; los dem\xE1s campos pueden usar valores gu\xEDa editables del pa\xEDs. Devuelve `price` y `compareAtPrice` listos para `create_product_task`. No inventes un costo de proveedor.", ProductCostingToolInput.shape, READS_INTERNAL, async (input) => {
16069
+ server.tool("cost_product", "Calcula GRATIS y sin cr\xE9ditos cu\xE1nto cobrar por un producto de dropshipping en cualquiera de los pa\xEDses disponibles en ProductMaker. Ll\xE1mala siempre que el usuario pida calcular cu\xE1nto cobrar, la rentabilidad o el margen de un producto concreto; no hagas el c\xE1lculo ni reconstruyas la f\xF3rmula por tu cuenta. Es READ-ONLY: no crea tareas ni publica nada. Pide country y el costo real del proveedor; los dem\xE1s campos pueden usar valores gu\xEDa editables del pa\xEDs. Devuelve `price` y `compareAtPrice` listos para `create_product_task`. No inventes un costo de proveedor.", ProductCostingToolInput.shape, READS_INTERNAL, async (input) => {
15957
16070
  if (input.country !== void 0 && input.country !== "CO" && [
15958
16071
  input.supplierCostCop,
15959
16072
  input.shippingCostCop,
@@ -16077,29 +16190,12 @@ __name(formatPercent, "formatPercent");
16077
16190
  import { z as z62 } from "zod";
16078
16191
 
16079
16192
  // src/tools/product-research-idempotency.ts
16080
- import { createHash as createHash3 } from "crypto";
16081
16193
  var RESEARCH_IDEMPOTENCY_WINDOW_MS = 2 * 60 * 60 * 1e3;
16082
16194
  var NAMESPACE = "6f9d3a1e-8c47-4b52-9f10-2d7e5a3c8b64";
16083
- function uuidV5(name, namespace) {
16084
- const namespaceBytes = Buffer.from(namespace.replace(/-/g, ""), "hex");
16085
- const digest = createHash3("sha1").update(namespaceBytes).update(name, "utf8").digest();
16086
- const bytes = Buffer.from(digest.subarray(0, 16));
16087
- bytes[6] = (bytes[6] ?? 0) & 15 | 80;
16088
- bytes[8] = (bytes[8] ?? 0) & 63 | 128;
16089
- const hex = bytes.toString("hex");
16090
- return [
16091
- hex.slice(0, 8),
16092
- hex.slice(8, 12),
16093
- hex.slice(12, 16),
16094
- hex.slice(16, 20),
16095
- hex.slice(20)
16096
- ].join("-");
16097
- }
16098
- __name(uuidV5, "uuidV5");
16099
16195
  function deriveIdempotencyKey(input) {
16100
16196
  const window = Math.floor(input.nowMs / RESEARCH_IDEMPOTENCY_WINDOW_MS);
16101
16197
  const name = `${input.fingerprint}:${input.targetCountry}:${input.categoryKey}:${window}`;
16102
- return uuidV5(name, NAMESPACE);
16198
+ return deriveUuidV5(name, NAMESPACE);
16103
16199
  }
16104
16200
  __name(deriveIdempotencyKey, "deriveIdempotencyKey");
16105
16201
 
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.2";
267
+ var MCP_VERSION = "3.4.4";
268
268
 
269
269
  // src/meta-upload-app.ts
270
270
  import { readFileSync } from "fs";
@@ -4131,7 +4131,15 @@ var TaskCreativeVariantResult = TaskCreativeVariantBaseResult.extend({
4131
4131
  var MVPResult = z24.object({
4132
4132
  taskId: z24.string().uuid(),
4133
4133
  product: Product,
4134
- angles: z24.array(Angle).max(3),
4134
+ // READ contract, NOT a generation cap. The generator writes exactly 3 angles
4135
+ // (`replaceAngles` renumbers positions 0..2), but a duplicated angles run
4136
+ // leaves TWO full sets on the task — alert on task a1954a35 (2026-08-21):
4137
+ // 6 rows, positions 0,0,1,1,2,2. Because the controller guards the whole
4138
+ // response with `MVPResult.parse()`, a cap here turns extra-but-valid rows
4139
+ // into a 500 on GET /v1/results/:taskId and hides EVERY deliverable of a
4140
+ // task that already finished. Same failure shape as the variant landing cap
4141
+ // below (alert 88badc8). The write path is what must stay at 3.
4142
+ angles: z24.array(Angle),
4135
4143
  topVideos: z24.array(AdVideo).max(20),
4136
4144
  landing: z24.object({
4137
4145
  // Allow legacy data (10 sections × 2 variants) and new data (7 sections × 2)
@@ -4235,7 +4243,7 @@ var PendingUserActionSchema = z24.discriminatedUnion("kind", [
4235
4243
  actor: z24.string(),
4236
4244
  /** Saved custom actor (Actor.id) backing this angle, when one was chosen. */
4237
4245
  actorId: z24.string().optional()
4238
- })).min(1).max(3)
4246
+ })).min(1)
4239
4247
  }),
4240
4248
  z24.object({
4241
4249
  kind: z24.literal("service_script_approval"),
@@ -5523,6 +5531,7 @@ var MetaCampaignType = z33.enum([
5523
5531
  "traffic",
5524
5532
  "engagement"
5525
5533
  ]);
5534
+ var MetaPublishAttemptId = z33.string().uuid();
5526
5535
  var MetaOptimizationMode = z33.enum([
5527
5536
  "advantage_plus",
5528
5537
  "manual"
@@ -5706,6 +5715,8 @@ var MetaCampaignDefaults = MetaCampaignConfigBase.omit({
5706
5715
  currency: z33.string().length(3).regex(/^[A-Z]{3}$/).nullable().optional()
5707
5716
  });
5708
5717
  var UploadToMetaRequest = z33.object({
5718
+ /** Caller-owned retry fence. Required before any provider operation. */
5719
+ publishAttemptId: MetaPublishAttemptId,
5709
5720
  /** Array of creatives to upload */
5710
5721
  creatives: z33.array(z33.object({
5711
5722
  creativeId: z33.string().min(1).optional(),
@@ -5862,6 +5873,8 @@ var MetaCampaignOverridesSchema = z34.object({
5862
5873
  destination: MetaCampaignDestination.optional()
5863
5874
  });
5864
5875
  var FacebookCreateCampaignBodySchema = z34.object({
5876
+ /** Caller-owned retry fence. Required before any provider operation. */
5877
+ publishAttemptId: MetaPublishAttemptId,
5865
5878
  adAccountId: MetaAdAccountId,
5866
5879
  portfolioId: z34.string().min(1).optional(),
5867
5880
  pageId: z34.string().min(1).optional(),
@@ -5931,6 +5944,8 @@ var FacebookCreateCampaignResponseSchema = z34.object({
5931
5944
  appliedBudget: z34.number().int().positive()
5932
5945
  });
5933
5946
  var FacebookCreateCampaignFromCreativesBodySchema = z34.object({
5947
+ /** Caller-owned retry fence. Required before any provider operation. */
5948
+ publishAttemptId: MetaPublishAttemptId,
5934
5949
  creatives: z34.array(z34.object({
5935
5950
  type: z34.enum([
5936
5951
  "image",
@@ -6078,6 +6093,8 @@ var FacebookCreativeSourceSchema = z34.discriminatedUnion("type", [
6078
6093
  FacebookMediaCreativeSourceSchema
6079
6094
  ]);
6080
6095
  var FacebookCreateCampaignFromTasksBodySchema = z34.object({
6096
+ /** Caller-owned retry fence. Required before any provider operation. */
6097
+ publishAttemptId: MetaPublishAttemptId,
6081
6098
  creatives: z34.array(z34.object({
6082
6099
  type: z34.enum([
6083
6100
  "image",
@@ -11473,6 +11490,12 @@ var McpErrorCode = {
11473
11490
  META_STAGED_ASSET_AMBIGUOUS: "META_STAGED_ASSET_AMBIGUOUS",
11474
11491
  META_STAGED_ASSET_TYPE_MISMATCH: "META_STAGED_ASSET_TYPE_MISMATCH",
11475
11492
  META_STAGED_ASSET_INVALID: "META_STAGED_ASSET_INVALID",
11493
+ META_STAGING_QUOTA_EXCEEDED: "META_STAGING_QUOTA_EXCEEDED",
11494
+ CAPACITY_UNAVAILABLE: "CAPACITY_UNAVAILABLE",
11495
+ UPLOAD_INCOMPLETE: "UPLOAD_INCOMPLETE",
11496
+ UPLOAD_EXPIRED: "UPLOAD_EXPIRED",
11497
+ META_UPLOAD_SESSION_EXPIRED: "META_UPLOAD_SESSION_EXPIRED",
11498
+ META_PUBLICATION_LEASE_STALE: "META_PUBLICATION_LEASE_STALE",
11476
11499
  META_PUBLICATION_RECONCILIATION_REQUIRED: "META_PUBLICATION_RECONCILIATION_REQUIRED",
11477
11500
  META_PUBLICATION_IN_PROGRESS: "META_PUBLICATION_IN_PROGRESS",
11478
11501
  META_NO_ADS_CREATED: "META_NO_ADS_CREATED",
@@ -11534,8 +11557,14 @@ var CODE_MESSAGES = {
11534
11557
  META_STAGED_ASSET_AMBIGUOUS: "The temporary media upload could not be uniquely resolved. Re-upload the media and retry the Meta publication.",
11535
11558
  META_STAGED_ASSET_TYPE_MISMATCH: "The temporary media type does not match the creative. Re-upload the correct image or video and retry.",
11536
11559
  META_STAGED_ASSET_INVALID: "The temporary media is empty or exceeds the server size limit. Re-upload the file and retry.",
11537
- 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.",
11538
- 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.",
11560
+ META_STAGING_QUOTA_EXCEEDED: "The temporary Meta staging quota was reached. Keep completed files and retry only pending files after retryAt.",
11561
+ CAPACITY_UNAVAILABLE: "Temporary Meta staging capacity is unavailable. Keep completed files and retry only pending files later.",
11562
+ UPLOAD_INCOMPLETE: "A temporary Meta upload is incomplete. Keep completed files and retry only pending files.",
11563
+ UPLOAD_EXPIRED: "The temporary Meta upload expired. Re-upload this file before publishing.",
11564
+ META_UPLOAD_SESSION_EXPIRED: "The Meta upload session expired. Open a new session and retry only pending files.",
11565
+ 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.",
11566
+ META_PUBLICATION_RECONCILIATION_REQUIRED: "Meta may already have received this publication. Do not resend it; check Meta Ads Manager.",
11567
+ META_PUBLICATION_IN_PROGRESS: "This Meta publication is already in progress. Do not resend it.",
11539
11568
  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.",
11540
11569
  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.",
11541
11570
  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.",
@@ -11684,6 +11713,11 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
11684
11713
  McpErrorCode.META_STAGED_ASSET_AMBIGUOUS,
11685
11714
  McpErrorCode.META_STAGED_ASSET_TYPE_MISMATCH,
11686
11715
  McpErrorCode.META_STAGED_ASSET_INVALID,
11716
+ McpErrorCode.META_STAGING_QUOTA_EXCEEDED,
11717
+ McpErrorCode.UPLOAD_INCOMPLETE,
11718
+ McpErrorCode.UPLOAD_EXPIRED,
11719
+ McpErrorCode.META_UPLOAD_SESSION_EXPIRED,
11720
+ McpErrorCode.META_PUBLICATION_LEASE_STALE,
11687
11721
  McpErrorCode.META_PUBLICATION_RECONCILIATION_REQUIRED,
11688
11722
  McpErrorCode.META_PUBLICATION_IN_PROGRESS,
11689
11723
  McpErrorCode.TIKTOK_TOKEN_EXPIRED,
@@ -11743,6 +11777,14 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
11743
11777
  McpErrorCode.META_CAMPAIGN_CONFIG_INVALID,
11744
11778
  McpErrorCode.META_WHATSAPP_INVALID_NUMBER,
11745
11779
  McpErrorCode.META_INVALID_CTA,
11780
+ McpErrorCode.META_STAGING_QUOTA_EXCEEDED,
11781
+ McpErrorCode.CAPACITY_UNAVAILABLE,
11782
+ McpErrorCode.UPLOAD_INCOMPLETE,
11783
+ McpErrorCode.UPLOAD_EXPIRED,
11784
+ McpErrorCode.META_UPLOAD_SESSION_EXPIRED,
11785
+ McpErrorCode.META_PUBLICATION_LEASE_STALE,
11786
+ McpErrorCode.META_PUBLICATION_RECONCILIATION_REQUIRED,
11787
+ McpErrorCode.META_PUBLICATION_IN_PROGRESS,
11746
11788
  // TikTok
11747
11789
  McpErrorCode.TIKTOK_NOT_CONNECTED,
11748
11790
  McpErrorCode.TIKTOK_TOKEN_EXPIRED,
@@ -11831,6 +11873,10 @@ function translateError(err) {
11831
11873
  if (body.helpUrl !== void 0) extra.helpUrl = body.helpUrl;
11832
11874
  if (typeof body.requiredCredits === "number") extra.requiredCredits = body.requiredCredits;
11833
11875
  if (typeof body.availableCredits === "number") extra.availableCredits = body.availableCredits;
11876
+ if (typeof body.dimension === "string") extra.dimension = body.dimension;
11877
+ if (typeof body.usage === "number") extra.usage = body.usage;
11878
+ if (typeof body.limit === "number") extra.limit = body.limit;
11879
+ if (typeof body.retryAt === "string") extra.retryAt = body.retryAt;
11834
11880
  return mcp(code, resolveMessage(code, body.message), extra);
11835
11881
  }
11836
11882
  if (err.status === 401) {
@@ -13557,6 +13603,57 @@ __name(registerActorTools, "registerActorTools");
13557
13603
 
13558
13604
  // src/tools/publish.ts
13559
13605
  import { z as z55 } from "zod";
13606
+
13607
+ // src/tools/stable-uuid.ts
13608
+ import { createHash as createHash2 } from "crypto";
13609
+ function canonicalize(value) {
13610
+ if (Array.isArray(value)) return value.map(canonicalize);
13611
+ if (value === null || typeof value !== "object") return value;
13612
+ const record = value;
13613
+ return Object.fromEntries(Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => [
13614
+ key,
13615
+ canonicalize(record[key])
13616
+ ]));
13617
+ }
13618
+ __name(canonicalize, "canonicalize");
13619
+ function canonicalJson(value) {
13620
+ return JSON.stringify(canonicalize(value));
13621
+ }
13622
+ __name(canonicalJson, "canonicalJson");
13623
+ function deriveUuidV5(name, namespace) {
13624
+ const namespaceBytes = Buffer.from(namespace.replaceAll("-", ""), "hex");
13625
+ const digest = createHash2("sha1").update(namespaceBytes).update(name, "utf8").digest();
13626
+ const bytes = Buffer.from(digest.subarray(0, 16));
13627
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
13628
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
13629
+ const hex = bytes.toString("hex");
13630
+ return [
13631
+ hex.slice(0, 8),
13632
+ hex.slice(8, 12),
13633
+ hex.slice(12, 16),
13634
+ hex.slice(16, 20),
13635
+ hex.slice(20)
13636
+ ].join("-");
13637
+ }
13638
+ __name(deriveUuidV5, "deriveUuidV5");
13639
+
13640
+ // src/tools/meta-publish-attempt-id.ts
13641
+ var META_PUBLISH_NAMESPACE = "7b19c9ac-b4eb-4db1-970e-c2f24ce30b98";
13642
+ function deriveMetaPublishAttemptId(input) {
13643
+ const operationIdentity = canonicalJson([
13644
+ input.fingerprint,
13645
+ input.toolName,
13646
+ [
13647
+ typeof input.requestId,
13648
+ input.requestId
13649
+ ],
13650
+ input.canonicalInput
13651
+ ]);
13652
+ return deriveUuidV5(operationIdentity, META_PUBLISH_NAMESPACE);
13653
+ }
13654
+ __name(deriveMetaPublishAttemptId, "deriveMetaPublishAttemptId");
13655
+
13656
+ // src/tools/publish.ts
13560
13657
  var ShopifyInput = z55.object({
13561
13658
  taskId: z55.string().min(1),
13562
13659
  shopDomain: z55.string().min(1),
@@ -13884,10 +13981,19 @@ function registerPublishTools(server, api, opts = {}) {
13884
13981
  throw translateError(e);
13885
13982
  }
13886
13983
  });
13887
- 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 }) => {
13984
+ 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) => {
13888
13985
  try {
13986
+ const parsed = MetaInput.safeParse(rawInput);
13987
+ if (!parsed.success) throw invalidInputFromZod(parsed.error);
13988
+ const { taskId, campaignConfig, primaryText, phoneNumber, ...rest } = parsed.data;
13889
13989
  const body = {
13890
- ...rest
13990
+ ...rest,
13991
+ publishAttemptId: deriveMetaPublishAttemptId({
13992
+ fingerprint: api.fingerprint(),
13993
+ toolName: "publish_to_meta",
13994
+ requestId: extra.requestId,
13995
+ canonicalInput: parsed.data
13996
+ })
13891
13997
  };
13892
13998
  if (campaignConfig) {
13893
13999
  body.campaignOverrides = toMetaCampaignOverrides(campaignConfig);
@@ -13912,7 +14018,7 @@ function registerPublishTools(server, api, opts = {}) {
13912
14018
  throw translateError(e);
13913
14019
  }
13914
14020
  });
13915
- 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) => {
14021
+ 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) => {
13916
14022
  try {
13917
14023
  const parsed = PublishCreativesToMetaInput.safeParse(rawInput);
13918
14024
  if (!parsed.success) throw invalidInputFromZod(parsed.error);
@@ -13992,7 +14098,13 @@ function registerPublishTools(server, api, opts = {}) {
13992
14098
  landingUrl: input.landingUrl,
13993
14099
  targetCountry: input.targetCountry,
13994
14100
  campaignName: input.campaignName,
13995
- campaignConfig: input.campaignConfig
14101
+ campaignConfig: input.campaignConfig,
14102
+ publishAttemptId: deriveMetaPublishAttemptId({
14103
+ fingerprint: api.fingerprint(),
14104
+ toolName: "publish_creatives_to_meta",
14105
+ requestId: extra.requestId,
14106
+ canonicalInput: input
14107
+ })
13996
14108
  });
13997
14109
  return asText({
13998
14110
  ...r,
@@ -14226,7 +14338,7 @@ __name(resolveEndpointForSession, "resolveEndpointForSession");
14226
14338
  function registerMediaUploadTools(server, api) {
14227
14339
  registerAppTool(server, "request_media_upload", {
14228
14340
  title: "Subir archivos",
14229
- description: "Abre el selector universal de ProductMaker para im\xE1genes o videos del usuario. Ll\xE1malo autom\xE1ticamente y antes de preguntar por campos faltantes cuando una petici\xF3n natural adjunte o mencione un archivo local, por ejemplo \u2018crea una landing con este producto\u2019, \u2018haz creativos con esta foto\u2019 o \u2018sube este video a Meta\u2019; hazlo aunque el usuario diga que todav\xEDa no generes ni publiques, porque esta herramienta solo prepara el archivo y no consume cr\xE9ditos, salvo que proh\xEDba expl\xEDcitamente subir, compartir o almacenar el archivo; no pidas una ruta local ni URL. Es la \xFAnica ruta de carga para create_product_task, generate_image_creatives, generate_video_creative, generate_free_video, generate_landing y publish_creatives_to_meta. Pasa las referencias opcionales de ChatGPT en files; si el host no puede transferirlas, el mismo App muestra el selector inline. Usa uploadPurpose=product_reference para fotos de producto y landings, creative para creativos o anuncios, y generic solo cuando ninguno aplique. Usa mediaKind=image para referencias de producto y landings; mixed solo cuando el consumidor acepte ambos tipos. El App NO te avisa cuando termina: en cuanto el usuario diga que ya subi\xF3 el archivo o que sigas, llama `check_media_upload` con este sessionId para obtener los assetId, y reanuda la intenci\xF3n original usando \xFAnicamente assetId y mediaKind, sin pedir que vuelva a subir ni mostrar datos t\xE9cnicos. El enlace externo es fallback solo si el App inline falla. Esta herramienta no genera, publica ni consume cr\xE9ditos.",
14341
+ description: "Abre el selector universal de ProductMaker para im\xE1genes o videos del usuario. Cuando la foto o el video sea el siguiente dato faltante, ll\xE1malo en esa misma respuesta: no te limites a pedir que lo adjunte en el chat y esperar otro turno. Ll\xE1malo tambi\xE9n autom\xE1ticamente y antes de preguntar por campos faltantes adicionales cuando una petici\xF3n natural adjunte o mencione un archivo local, por ejemplo \u2018crea una landing con este producto\u2019, \u2018haz creativos con esta foto\u2019 o \u2018sube este video a Meta\u2019; hazlo aunque el usuario diga que todav\xEDa no generes ni publiques, porque esta herramienta solo prepara el archivo y no consume cr\xE9ditos, salvo que proh\xEDba expl\xEDcitamente subir, compartir o almacenar el archivo; no pidas una ruta local ni URL. Es la \xFAnica ruta de carga para create_product_task, generate_image_creatives, generate_video_creative, generate_free_video, generate_landing y publish_creatives_to_meta. Pasa las referencias opcionales de ChatGPT en files; si el host no puede transferirlas, el mismo App muestra el selector inline. Usa uploadPurpose=product_reference para fotos de producto y landings, creative para creativos o anuncios, y generic solo cuando ninguno aplique. Usa mediaKind=image para referencias de producto y landings; mixed solo cuando el consumidor acepte ambos tipos. El App NO te avisa cuando termina: en cuanto el usuario diga que ya subi\xF3 el archivo o que sigas, llama `check_media_upload` con este sessionId para obtener los assetId, y reanuda la intenci\xF3n original usando \xFAnicamente assetId y mediaKind, sin pedir que vuelva a subir ni mostrar datos t\xE9cnicos. El enlace externo es fallback solo si el App inline falla. Esta herramienta no genera, publica ni consume cr\xE9ditos.",
14230
14342
  inputSchema: UploadSessionToolInputShape,
14231
14343
  annotations: CREATES_INTERNAL,
14232
14344
  outputSchema: UniversalUploadStructuredResponse.shape,
@@ -14755,7 +14867,7 @@ var ProductCostingToolInput = z60.object({
14755
14867
  desiredMarginPercent: optionalPercent("Margen de ganancia deseado. Ejemplo: 10 significa 10%. Omite para usar el valor gu\xEDa editable del pa\xEDs.")
14756
14868
  });
14757
14869
  function registerProductCostingTools(server) {
14758
- server.tool("cost_product", "Calcula GRATIS y sin cr\xE9ditos cu\xE1nto cobrar por un producto de dropshipping en cualquiera de los pa\xEDses disponibles en ProductMaker. Es READ-ONLY: no crea tareas ni publica nada. Pide country y el costo real del proveedor; los dem\xE1s campos pueden usar valores gu\xEDa editables del pa\xEDs. Devuelve `price` y `compareAtPrice` listos para `create_product_task`. No inventes un costo de proveedor.", ProductCostingToolInput.shape, READS_INTERNAL, async (input) => {
14870
+ server.tool("cost_product", "Calcula GRATIS y sin cr\xE9ditos cu\xE1nto cobrar por un producto de dropshipping en cualquiera de los pa\xEDses disponibles en ProductMaker. Ll\xE1mala siempre que el usuario pida calcular cu\xE1nto cobrar, la rentabilidad o el margen de un producto concreto; no hagas el c\xE1lculo ni reconstruyas la f\xF3rmula por tu cuenta. Es READ-ONLY: no crea tareas ni publica nada. Pide country y el costo real del proveedor; los dem\xE1s campos pueden usar valores gu\xEDa editables del pa\xEDs. Devuelve `price` y `compareAtPrice` listos para `create_product_task`. No inventes un costo de proveedor.", ProductCostingToolInput.shape, READS_INTERNAL, async (input) => {
14759
14871
  if (input.country !== void 0 && input.country !== "CO" && [
14760
14872
  input.supplierCostCop,
14761
14873
  input.shippingCostCop,
@@ -14879,29 +14991,12 @@ __name(formatPercent, "formatPercent");
14879
14991
  import { z as z61 } from "zod";
14880
14992
 
14881
14993
  // src/tools/product-research-idempotency.ts
14882
- import { createHash as createHash2 } from "crypto";
14883
14994
  var RESEARCH_IDEMPOTENCY_WINDOW_MS = 2 * 60 * 60 * 1e3;
14884
14995
  var NAMESPACE = "6f9d3a1e-8c47-4b52-9f10-2d7e5a3c8b64";
14885
- function uuidV5(name, namespace) {
14886
- const namespaceBytes = Buffer.from(namespace.replace(/-/g, ""), "hex");
14887
- const digest = createHash2("sha1").update(namespaceBytes).update(name, "utf8").digest();
14888
- const bytes = Buffer.from(digest.subarray(0, 16));
14889
- bytes[6] = (bytes[6] ?? 0) & 15 | 80;
14890
- bytes[8] = (bytes[8] ?? 0) & 63 | 128;
14891
- const hex = bytes.toString("hex");
14892
- return [
14893
- hex.slice(0, 8),
14894
- hex.slice(8, 12),
14895
- hex.slice(12, 16),
14896
- hex.slice(16, 20),
14897
- hex.slice(20)
14898
- ].join("-");
14899
- }
14900
- __name(uuidV5, "uuidV5");
14901
14996
  function deriveIdempotencyKey(input) {
14902
14997
  const window = Math.floor(input.nowMs / RESEARCH_IDEMPOTENCY_WINDOW_MS);
14903
14998
  const name = `${input.fingerprint}:${input.targetCountry}:${input.categoryKey}:${window}`;
14904
- return uuidV5(name, NAMESPACE);
14999
+ return deriveUuidV5(name, NAMESPACE);
14905
15000
  }
14906
15001
  __name(deriveIdempotencyKey, "deriveIdempotencyKey");
14907
15002
 
@@ -15377,8 +15472,9 @@ __name(registerDropshippingProjectTools, "registerDropshippingProjectTools");
15377
15472
  // src/guidance/productmaker-guidance.ts
15378
15473
  var START_DROPSHIPPING_PROMPT_NAME = "start_dropshipping";
15379
15474
  var PRODUCTMAKER_SERVER_INSTRUCTIONS = [
15380
- "When a ProductMaker request needs an image or video the user attached, pasted, or has locally and no reusable ProductMaker assetId is already available, call request_media_upload immediately before asking for any missing business inputs. Do this even when the user says not to generate or publish yet: uploading only stages the file and does not generate, publish, or consume credits, unless the user explicitly says not to upload, share, or store the file. Treat natural requests such as 'create a landing with this product', 'make creatives with this photo', or 'upload this video to Meta' as upload intent; never require the user to name the uploader.",
15475
+ "Whenever a ProductMaker request needs a new image or video and no reusable ProductMaker assetId is available, call request_media_upload immediately in the same response\u2014whether media is the next missing input or the user attached, pasted, mentioned, or has the file locally. The inline uploader is how ProductMaker asks for the file. Never merely ask the user to attach or upload it in chat and wait for a later turn. Call it before asking for any missing business inputs, even when the user says not to generate or publish yet: uploading only stages the file and does not generate, publish, or consume credits, unless the user explicitly says not to upload, share, or store the file. Treat natural requests such as 'create a landing with this product', 'make creatives with this photo', or 'upload this video to Meta' as upload intent; never require the user to name the uploader.",
15381
15476
  "For requests that do not need a new media upload, understand the user's intent before choosing a ProductMaker tool.",
15477
+ "For every concrete ProductMaker request to calculate how much to charge, profitability, or margin for a dropshipping product, you must call cost_product after collecting its missing country and real supplier cost. Never calculate or reproduce ProductMaker's costing formula yourself; answer from the tool result.",
15382
15478
  "For remote MCP hosts, never ask for a local path, CDN URL, bytes, or base64. Pass supported host file references to request_media_upload; otherwise let its inline App open the file picker. The external upload page is fallback only.",
15383
15479
  "After the upload App sends its OK continuation, resume the original request with the verified assetId values, do not ask for another upload, and ask only for remaining business inputs in chat. The host-inserted continuation may contain opaque asset IDs; do not repeat them in narrative prose or expose session IDs, tokens, private URLs, file names, or JSON handoff payloads.",
15384
15480
  "Keep focused requests direct: creatives, landing pages, Shopify publication, Meta publication, and status checks do not require a DropshippingProject.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "3.4.2",
3
+ "version": "3.4.4",
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.",