@productmaker/mcp 8.2.0 → 8.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,7 +64,7 @@ their connector immediately. New connections should always use OAuth; once the
64
64
  client supports it, replace the saved connection with the clean URL above.
65
65
 
66
66
  In claude.ai: _Settings → Integrations → Add MCP server_.
67
- In ChatGPT: _Settings → Connectors → Add custom connector_.
67
+ In ChatGPT: _Plugins (sidebar) → Add → Create MCP app_ (OAuth).
68
68
  In Claude Desktop: _Settings → Connectors → Add custom connector_.
69
69
 
70
70
  If your saved connection still uses an API key, it continues to use the latest
@@ -268,6 +268,11 @@ The `base64` field was removed. MCP JSON-RPC transports truncate large tool argu
268
268
 
269
269
  ## Version history
270
270
 
271
+ ### 8.3.0 — Idempotencia de tools pagadas + refresh OAuth
272
+
273
+ - Tools pagadas (`create_product_task`, `create_service_video`, `generate_video_creative`, `generate_free_video`, `generate_image_creatives`, `generate_landing`, `create_creative_variant`): envían un `Idempotency-Key` derivado de los argumentos semánticos (ventana de 15 min). Un reintento con los mismos argumentos devuelve la misma tarea (`replayed: true`) sin segundo cargo; una tarea en `error` sí se reintenta.
274
+ - OAuth alojado: se anuncia `offline_access` + grant `refresh_token`; un token a punto de expirar responde `401` + `WWW-Authenticate` para que el host refresque en silencio (`MCP_SUBJECT_TOKEN_REAUTH_WINDOW_MS`, por defecto 180000; `0` lo desactiva).
275
+
271
276
  ### 8.2.0 — Fase 3 (modelos)
272
277
 
273
278
  - Imagen: `generate_image_creatives` (setup guiado) acepta `image:1k` / `image:2k`; `gpt-image-2-5:*` y `gpt-image-2:*` se siguen aceptando.
package/dist/http.js CHANGED
@@ -1830,6 +1830,9 @@ var VideoCreativeJobStatus = z14.object({
1830
1830
  var VideoCreativeJobPayload = z14.object({
1831
1831
  jobId: z14.string().optional(),
1832
1832
  status: VideoCreativeJobStatusEnum.optional(),
1833
+ /** True when the submission replayed an existing task instead of billing
1834
+ * new work — callers surface the twin verbatim. Omitted on fresh creates. */
1835
+ replayed: z14.boolean().optional(),
1833
1836
  videoStructure: VideoStructure.optional(),
1834
1837
  videoQuality: VideoQuality.optional(),
1835
1838
  videoRouteId: VideoRouteId.optional(),
@@ -1923,7 +1926,8 @@ var ImageCreativeJobStatus = z14.object({
1923
1926
  });
1924
1927
  var ImageCreativeJobPayload = z14.object({
1925
1928
  jobId: z14.string().uuid(),
1926
- status: ImageCreativeJobStatusEnum
1929
+ status: ImageCreativeJobStatusEnum,
1930
+ replayed: z14.boolean().optional()
1927
1931
  });
1928
1932
  var ImageCreativeHistoryEntry = z14.object({
1929
1933
  id: z14.string().uuid(),
@@ -1950,7 +1954,8 @@ var LandingCreativeJobStatus = z14.object({
1950
1954
  });
1951
1955
  var LandingCreativeJobPayload = z14.object({
1952
1956
  jobId: z14.string().uuid(),
1953
- status: LandingCreativeJobStatusEnum
1957
+ status: LandingCreativeJobStatusEnum,
1958
+ replayed: z14.boolean().optional()
1954
1959
  });
1955
1960
  var LandingCreativeHistoryEntry = z14.object({
1956
1961
  id: z14.string().uuid(),
@@ -5621,6 +5626,10 @@ var CreateShopifySubscriptionRequest = z28.object({
5621
5626
  var CreateShopifyBillingChargeRequest = z28.object({
5622
5627
  packId: z28.string().min(1)
5623
5628
  });
5629
+ var ShopifyBillingCallbackQuery = z28.object({
5630
+ charge_id: z28.string().regex(/^\d+$/),
5631
+ transactionId: z28.string().uuid()
5632
+ });
5624
5633
  var CancelShopifySubscriptionRequest = z28.object({
5625
5634
  subscriptionId: z28.string().min(1)
5626
5635
  });
@@ -10848,6 +10857,7 @@ var MixSceneResult = z48.object({
10848
10857
  index: z48.number().int().positive(),
10849
10858
  status: z48.enum([
10850
10859
  "queued",
10860
+ "submitting",
10851
10861
  "submitted",
10852
10862
  "done",
10853
10863
  "failed"
@@ -13446,7 +13456,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
13446
13456
  ], [
13447
13457
  "product_costing"
13448
13458
  ]),
13449
- defineCapability("create_creative_variant", "6a777fbe7ad17966d16502e8872e15e4db2dcfacdfd2992d24853340d4f22a00", "credit_consuming", "creative", [
13459
+ defineCapability("create_creative_variant", "3e02399a7785df328f1c4d63cfbb6d7481abaac9f91d4a5cf51d410e657d63aa", "credit_consuming", "creative", [
13450
13460
  "credit_consuming",
13451
13461
  "state_change"
13452
13462
  ], [
@@ -13696,7 +13706,9 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
13696
13706
  // MCP 7.0.0 (Task 11, .agent/plans/2026-09-15-model-agnostic-video-routes.md):
13697
13707
  // `freeProModel` retired in favor of `modelId` (registry ids). Recomputed
13698
13708
  // from `pnpm --filter @productmaker/mcp test` printing the real digest.
13699
- "ead1f563a1915452157628d023ba203ecfa8f502a48259a5b5b15be4c84bf768",
13709
+ // Re-baselined 2026-09-26: `idempotencyKey` field description corrected —
13710
+ // paid tools ignore caller-provided keys and derive their own semantic key.
13711
+ "35616a32a42a0bc062218fdfbb4cc7f4ecfa814571fb86b4f11ef9e4184855e1",
13700
13712
  "credit_consuming",
13701
13713
  "creative",
13702
13714
  [
@@ -16195,6 +16207,9 @@ function buildProxyUrl(opts) {
16195
16207
  }
16196
16208
  __name(buildProxyUrl, "buildProxyUrl");
16197
16209
 
16210
+ // src/http.ts
16211
+ import { getOAuthProtectedResourceMetadataUrl as getOAuthProtectedResourceMetadataUrl2 } from "@modelcontextprotocol/sdk/server/auth/router.js";
16212
+
16198
16213
  // src/auth/http-oauth.ts
16199
16214
  import { requireBearerAuth } from "@modelcontextprotocol/sdk/server/auth/middleware/bearerAuth.js";
16200
16215
  import { getOAuthProtectedResourceMetadataUrl, mcpAuthMetadataRouter } from "@modelcontextprotocol/sdk/server/auth/router.js";
@@ -16604,6 +16619,7 @@ import { createRemoteJWKSet as createRemoteJWKSet2, jwtVerify as jwtVerify2 } fr
16604
16619
  import { z as z73 } from "zod";
16605
16620
  var MCP_RESOURCE = new URL("https://mcp.productmaker.app/mcp");
16606
16621
  var MCP_SCOPE = "mcp:access";
16622
+ var REFRESH_SCOPE = "offline_access";
16607
16623
  var STATIC_CLIENT_ISSUER = new URL(MCP_RESOURCE.origin);
16608
16624
  var oauthEnvSchema = z73.object({
16609
16625
  MCP_REMOTE_AUTH_MODE: z73.literal("oauth"),
@@ -16987,13 +17003,10 @@ function createOAuthProxyRouter(options) {
16987
17003
  });
16988
17004
  return;
16989
17005
  }
17006
+ body.set("client_secret", options.staticClient.clientSecret);
16990
17007
  const headers = {
16991
17008
  "content-type": "application/x-www-form-urlencoded"
16992
17009
  };
16993
- const authorization = req.headers.authorization;
16994
- if (typeof authorization === "string") {
16995
- headers.authorization = authorization;
16996
- }
16997
17010
  let upstream;
16998
17011
  let payload;
16999
17012
  try {
@@ -17026,11 +17039,12 @@ var REGISTRATION_RATE_LIMIT = {
17026
17039
  windowMs: 6e4,
17027
17040
  max: 120
17028
17041
  };
17042
+ var PUBLIC_CLIENT_SECRET_PLACEHOLDER = "pm-public-client";
17029
17043
  function buildStaticClientInformation(staticClient, requested) {
17030
17044
  return {
17031
17045
  client_id: staticClient.clientId,
17032
- client_secret: staticClient.clientSecret,
17033
- // 0 = never expires (RFC 7591 §3.2.1); the secret is Auth0's, not ours.
17046
+ client_secret: PUBLIC_CLIENT_SECRET_PLACEHOLDER,
17047
+ // 0 = never expires (RFC 7591 §3.2.1); the placeholder is a constant.
17034
17048
  client_secret_expires_at: 0,
17035
17049
  token_endpoint_auth_method: "client_secret_post",
17036
17050
  redirect_uris: requested.redirect_uris,
@@ -17041,7 +17055,12 @@ function buildStaticClientInformation(staticClient, requested) {
17041
17055
  response_types: [
17042
17056
  "code"
17043
17057
  ],
17044
- scope: requested.scope ?? MCP_SCOPE,
17058
+ // Clients that defer scope choice to the server get the full intended
17059
+ // set: `mcp:access` for the resource plus `offline_access` so the session
17060
+ // can refresh instead of dying an interactive re-auth every 900s access
17061
+ // token (the auth loop seen in production). A client that names its own
17062
+ // scope list is echoed verbatim — its choice wins.
17063
+ scope: requested.scope ?? `${MCP_SCOPE} ${REFRESH_SCOPE}`,
17045
17064
  ...requested.client_name !== void 0 ? {
17046
17065
  client_name: requested.client_name
17047
17066
  } : {}
@@ -17088,8 +17107,12 @@ function buildStaticClientAuthorizationServerMetadata(auth0Metadata) {
17088
17107
  token_endpoint_auth_methods_supported: [
17089
17108
  "client_secret_post"
17090
17109
  ],
17110
+ // `offline_access` is what lets a client ASK for a refresh token; without
17111
+ // it in scopes_supported, conforming clients never request it and every
17112
+ // 900s access-token expiry becomes an interactive re-auth loop.
17091
17113
  scopes_supported: [
17092
- MCP_SCOPE
17114
+ MCP_SCOPE,
17115
+ REFRESH_SCOPE
17093
17116
  ],
17094
17117
  service_documentation: "https://productmaker.app/"
17095
17118
  };
@@ -17601,7 +17624,10 @@ var ApiClient = class {
17601
17624
  method: "POST",
17602
17625
  headers: {
17603
17626
  ...authHeaders,
17604
- "Content-Type": "application/json"
17627
+ "Content-Type": "application/json",
17628
+ ...options?.idempotencyKey ? {
17629
+ "Idempotency-Key": options.idempotencyKey
17630
+ } : {}
17605
17631
  },
17606
17632
  body: JSON.stringify(body)
17607
17633
  }, options);
@@ -17723,7 +17749,7 @@ var ApiClient = class {
17723
17749
  };
17724
17750
 
17725
17751
  // src/index.ts
17726
- var MCP_VERSION = "8.2.0";
17752
+ var MCP_VERSION = "8.3.0";
17727
17753
 
17728
17754
  // src/meta-upload-app.ts
17729
17755
  import { readFileSync } from "fs";
@@ -18707,8 +18733,13 @@ async function submitCreative(api, opts) {
18707
18733
  const taskId = r.taskId ?? r.jobId;
18708
18734
  return asText({
18709
18735
  taskId,
18710
- status: "queued",
18711
- estimatedSeconds: opts.estimatedSeconds
18736
+ // Replay-aware: a keyed retry may land on a task that is already
18737
+ // processing/done — report the real status, not a hopeful "queued".
18738
+ status: r.status ?? "queued",
18739
+ estimatedSeconds: opts.estimatedSeconds,
18740
+ ...r.replayed === true ? {
18741
+ replayed: true
18742
+ } : {}
18712
18743
  });
18713
18744
  }
18714
18745
  __name(submitCreative, "submitCreative");
@@ -20072,6 +20103,170 @@ ${summary}`,
20072
20103
  }
20073
20104
  __name(requirePaidCreativeConfirmation, "requirePaidCreativeConfirmation");
20074
20105
 
20106
+ // src/tools/stable-uuid.ts
20107
+ import { createHash as createHash3 } from "crypto";
20108
+ function canonicalize(value) {
20109
+ if (Array.isArray(value)) return value.map(canonicalize);
20110
+ if (value === null || typeof value !== "object") return value;
20111
+ const record = value;
20112
+ return Object.fromEntries(Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => [
20113
+ key,
20114
+ canonicalize(record[key])
20115
+ ]));
20116
+ }
20117
+ __name(canonicalize, "canonicalize");
20118
+ function canonicalJson(value) {
20119
+ return JSON.stringify(canonicalize(value));
20120
+ }
20121
+ __name(canonicalJson, "canonicalJson");
20122
+ function deriveUuidV5(name, namespace) {
20123
+ const namespaceBytes = Buffer.from(namespace.replaceAll("-", ""), "hex");
20124
+ const digest = createHash3("sha1").update(namespaceBytes).update(name, "utf8").digest();
20125
+ const bytes = Buffer.from(digest.subarray(0, 16));
20126
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
20127
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
20128
+ const hex = bytes.toString("hex");
20129
+ return [
20130
+ hex.slice(0, 8),
20131
+ hex.slice(8, 12),
20132
+ hex.slice(12, 16),
20133
+ hex.slice(16, 20),
20134
+ hex.slice(20)
20135
+ ].join("-");
20136
+ }
20137
+ __name(deriveUuidV5, "deriveUuidV5");
20138
+
20139
+ // src/tools/request-key.ts
20140
+ var PAID_REQUEST_KEY_WINDOW_MS = 15 * 60 * 1e3;
20141
+ var PAID_REQUEST_KEY_NAMESPACE = "8f3d6b1c-2a47-4e5b-9c0d-6e7f8a9b0c1d";
20142
+ var PARAPHRASE_PRONE_FIELDS = /* @__PURE__ */ new Set([
20143
+ "additionalContext",
20144
+ "productContext",
20145
+ "scriptNotes",
20146
+ "creativeDirection",
20147
+ "buyerPersona",
20148
+ "actorDescription",
20149
+ "description",
20150
+ "idempotencyKey",
20151
+ "clientRequestId"
20152
+ ]);
20153
+ var URL_FIELDS = /* @__PURE__ */ new Set([
20154
+ "landingUrl",
20155
+ "productUrl",
20156
+ "url",
20157
+ "serviceLandingUrl"
20158
+ ]);
20159
+ var TRACKING_PARAM_PATTERN = /^(utm_\w+|fbclid|gclid|msclkid|mc_cid|mc_eid|igshid)$/;
20160
+ var LANGUAGE_FIELDS = /* @__PURE__ */ new Set([
20161
+ "language",
20162
+ "lang"
20163
+ ]);
20164
+ var CASE_FOLD_FIELDS = /* @__PURE__ */ new Set([
20165
+ "country",
20166
+ "language",
20167
+ "lang"
20168
+ ]);
20169
+ var ORDER_INSIGNIFICANT_ARRAY_FIELDS = /* @__PURE__ */ new Set([
20170
+ "outputs",
20171
+ "selectedOutputs",
20172
+ "features"
20173
+ ]);
20174
+ function sortCanonical(items) {
20175
+ return [
20176
+ ...items
20177
+ ].sort((a, b) => {
20178
+ const ka = canonicalJson(a);
20179
+ const kb = canonicalJson(b);
20180
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
20181
+ });
20182
+ }
20183
+ __name(sortCanonical, "sortCanonical");
20184
+ function isRecord3(value) {
20185
+ return typeof value === "object" && value !== null && !Array.isArray(value);
20186
+ }
20187
+ __name(isRecord3, "isRecord");
20188
+ function collectLanguageCodes(value, out) {
20189
+ if (Array.isArray(value)) {
20190
+ const items = value;
20191
+ for (const item of items) collectLanguageCodes(item, out);
20192
+ return out;
20193
+ }
20194
+ if (!isRecord3(value)) return out;
20195
+ for (const [key, entry] of Object.entries(value)) {
20196
+ if (LANGUAGE_FIELDS.has(key) && typeof entry === "string") {
20197
+ const base = entry.trim().split(/[-_]/)[0]?.toLowerCase();
20198
+ if (base) out.add(base);
20199
+ }
20200
+ collectLanguageCodes(entry, out);
20201
+ }
20202
+ return out;
20203
+ }
20204
+ __name(collectLanguageCodes, "collectLanguageCodes");
20205
+ function normalizeSearchForKey(search) {
20206
+ if (!search) return "";
20207
+ const params = [
20208
+ ...new URLSearchParams(search).entries()
20209
+ ].filter(([key]) => !TRACKING_PARAM_PATTERN.test(key.toLowerCase())).sort(([ka, va], [kb, vb]) => ka === kb ? va < vb ? -1 : va > vb ? 1 : 0 : ka < kb ? -1 : 1);
20210
+ if (!params.length) return "";
20211
+ const out = new URLSearchParams();
20212
+ for (const [key, value] of params) out.append(key, value);
20213
+ return `?${out.toString()}`;
20214
+ }
20215
+ __name(normalizeSearchForKey, "normalizeSearchForKey");
20216
+ function normalizeUrlForKey(value, languages) {
20217
+ const trimmed = value.trim();
20218
+ let url;
20219
+ try {
20220
+ url = new URL(trimmed);
20221
+ } catch {
20222
+ return trimmed.toLowerCase();
20223
+ }
20224
+ const host = url.hostname.toLowerCase().replace(/^www\./, "");
20225
+ const port = url.port ? `:${url.port}` : "";
20226
+ const segments = url.pathname.split("/").filter(Boolean);
20227
+ const soleSegment = segments.length === 1 ? segments[0] : void 0;
20228
+ if (soleSegment !== void 0 && languages.has(soleSegment.toLowerCase())) {
20229
+ segments.pop();
20230
+ }
20231
+ const path3 = segments.length ? `/${segments.join("/")}` : "";
20232
+ return host + port + path3 + normalizeSearchForKey(url.search);
20233
+ }
20234
+ __name(normalizeUrlForKey, "normalizeUrlForKey");
20235
+ function normalizeForKey(value, languages) {
20236
+ if (Array.isArray(value)) {
20237
+ const items = value;
20238
+ return items.map((item) => normalizeForKey(item, languages));
20239
+ }
20240
+ if (!isRecord3(value)) return value;
20241
+ const out = {};
20242
+ for (const [key, entry] of Object.entries(value)) {
20243
+ if (PARAPHRASE_PRONE_FIELDS.has(key)) {
20244
+ if (key === "description" && value.mode === "described") {
20245
+ out[key] = normalizeForKey(entry, languages);
20246
+ }
20247
+ continue;
20248
+ }
20249
+ out[key] = URL_FIELDS.has(key) && typeof entry === "string" ? normalizeUrlForKey(entry, languages) : CASE_FOLD_FIELDS.has(key) && typeof entry === "string" ? (
20250
+ // separator too or a host alternating between them splits the key.
20251
+ entry.trim().toLowerCase().replace(/_/g, "-")
20252
+ ) : ORDER_INSIGNIFICANT_ARRAY_FIELDS.has(key) && Array.isArray(entry) ? sortCanonical(entry.map((item) => normalizeForKey(item, languages))) : normalizeForKey(entry, languages);
20253
+ }
20254
+ return out;
20255
+ }
20256
+ __name(normalizeForKey, "normalizeForKey");
20257
+ function derivePaidRequestKey(args) {
20258
+ const languages = collectLanguageCodes(args.input, /* @__PURE__ */ new Set());
20259
+ const semanticInput = normalizeForKey(args.input, languages);
20260
+ const bucket = Math.floor(args.nowMs / PAID_REQUEST_KEY_WINDOW_MS);
20261
+ return deriveUuidV5(canonicalJson({
20262
+ user: args.fingerprint,
20263
+ tool: args.tool,
20264
+ input: semanticInput,
20265
+ bucket
20266
+ }), PAID_REQUEST_KEY_NAMESPACE);
20267
+ }
20268
+ __name(derivePaidRequestKey, "derivePaidRequestKey");
20269
+
20075
20270
  // src/tools/winning-product-image.ts
20076
20271
  import { z as z76 } from "zod";
20077
20272
  var WinnerReferenceInput = z76.object({
@@ -20608,10 +20803,25 @@ ANTES de invocar, resume outputs, cantidad de videos y datos comerciales, explic
20608
20803
  };
20609
20804
  const r = await api.postMultipart("/v1/ingest", {
20610
20805
  images: parts
20611
- }, fields);
20806
+ }, fields, {
20807
+ // LLM hosts retry paid tools with rephrased arguments — the API's
20808
+ // byte-fingerprint can't see the duplicate, this key can. Derived
20809
+ // from the semantic inputs, not the wire fields, for the same
20810
+ // reason (see request-key.ts, which also canonicalizes the
20811
+ // order-insignificant `outputs` set).
20812
+ idempotencyKey: derivePaidRequestKey({
20813
+ fingerprint: api.fingerprint(),
20814
+ tool: "create_product_task",
20815
+ input,
20816
+ nowMs: Date.now()
20817
+ })
20818
+ });
20612
20819
  return asText({
20613
20820
  taskId: r.taskId,
20614
- status: "queued",
20821
+ status: r.status ?? "queued",
20822
+ ...r.replayed ? {
20823
+ replayed: true
20824
+ } : {},
20615
20825
  estimatedSeconds: 300,
20616
20826
  outputs: selectedOutputs,
20617
20827
  outputMode: "single_task",
@@ -20648,26 +20858,56 @@ ANTES de invocar, resume outputs, cantidad de videos y datos comerciales, explic
20648
20858
  assets: []
20649
20859
  },
20650
20860
  appDemoVideos: []
20861
+ }, {
20862
+ // The 2026-09-25 incident: four retries of this exact tool created
20863
+ // four charged service tasks because the host rephrased
20864
+ // `additionalContext` and flipped the landing between `/` and
20865
+ // `/es`. The derived key is stable across both rewrites (see
20866
+ // request-key.ts).
20867
+ idempotencyKey: derivePaidRequestKey({
20868
+ fingerprint: api.fingerprint(),
20869
+ tool: "create_service_video",
20870
+ input,
20871
+ nowMs: Date.now()
20872
+ })
20651
20873
  });
20652
20874
  const angles = await api.postJson(`/v1/tasks/${encodeURIComponent(created.taskId)}/service-angles`, {});
20653
20875
  const first = angles[0];
20876
+ const replayed = created.replayed === true;
20654
20877
  if (input.reviewMode === "automatic") {
20655
20878
  if (!first) throw new Error("ProductMaker no pudo crear un concepto para este servicio.");
20656
- await api.postJson(`/v1/tasks/${encodeURIComponent(created.taskId)}/service-angles/select`, {
20657
- angleIds: [
20658
- first.id
20659
- ]
20660
- });
20879
+ if (!(replayed && created.status !== "queued")) {
20880
+ await api.postJson(`/v1/tasks/${encodeURIComponent(created.taskId)}/service-angles/select`, {
20881
+ angleIds: [
20882
+ first.id
20883
+ ]
20884
+ });
20885
+ }
20661
20886
  return asText({
20662
20887
  taskId: created.taskId,
20663
- status: "processing",
20888
+ status: replayed ? created.status ?? "processing" : "processing",
20889
+ ...replayed ? {
20890
+ replayed: true
20891
+ } : {},
20664
20892
  reviewMode: input.reviewMode,
20665
20893
  selectedConcept: first
20666
20894
  });
20667
20895
  }
20896
+ if (replayed && created.status !== "queued") {
20897
+ return asText({
20898
+ taskId: created.taskId,
20899
+ status: created.status,
20900
+ replayed: true,
20901
+ reviewMode: input.reviewMode,
20902
+ angles
20903
+ });
20904
+ }
20668
20905
  return asText({
20669
20906
  taskId: created.taskId,
20670
20907
  status: "waiting_for_user",
20908
+ ...replayed ? {
20909
+ replayed: true
20910
+ } : {},
20671
20911
  reviewMode: input.reviewMode,
20672
20912
  pendingUserAction: {
20673
20913
  kind: "service_angle_approval",
@@ -21243,7 +21483,6 @@ ANTES de invocar, resume outputs, cantidad de videos y datos comerciales, explic
21243
21483
  __name(registerTaskTools, "registerTaskTools");
21244
21484
 
21245
21485
  // src/tools/creatives.ts
21246
- import { randomUUID } from "crypto";
21247
21486
  import { z as z78 } from "zod";
21248
21487
  var generateFreeVideoGuidance = getProductMakerCapabilityModelGuidance("generate_free_video");
21249
21488
  var generateVideoCreativeGuidance = getProductMakerCapabilityModelGuidance("generate_video_creative");
@@ -21364,7 +21603,7 @@ var FreeVideoInput = z78.object({
21364
21603
  videoResolution: VideoResolutionEnum.describe("Resoluci\xF3n del video: 480p o 720p."),
21365
21604
  generateAiMusic: z78.boolean().default(false).describe("Opcional. Agrega m\xFAsica instrumental IA debajo del audio nativo. Default false."),
21366
21605
  addSubtitles: z78.boolean().default(false).describe("Opcional. Quema subt\xEDtulos usando el rawPrompt exacto. Default false."),
21367
- idempotencyKey: z78.string().uuid().optional().describe("Opcional. UUID para reintentar exactamente esta misma solicitud sin crear ni cobrar otro video. Reutiliza el mismo valor si el cliente no recibi\xF3 la respuesta.")
21606
+ idempotencyKey: z78.string().uuid().optional().describe("Opcional. Se acepta por compatibilidad pero se ignora: la identidad del reintento se deriva autom\xE1ticamente de los argumentos sem\xE1nticos, as\xED que un retry con los mismos argumentos ya es idempotente sin este campo.")
21368
21607
  }).strict();
21369
21608
  var imageStyleList = ImageCreativeStyleEnum.options.join(" | ");
21370
21609
  var ImageCreativeSetup = z78.discriminatedUnion("mode", [
@@ -21586,7 +21825,16 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
21586
21825
  addSubtitles: input.addSubtitles
21587
21826
  },
21588
21827
  allowEmptyImages: true,
21589
- idempotencyKey: input.idempotencyKey ?? randomUUID(),
21828
+ // Always derived — never `input.idempotencyKey ??`: a host that
21829
+ // mints a fresh UUID per retry would defeat the dedupe entirely.
21830
+ // The semantic key is also a strictly better retry identity: a
21831
+ // reused caller key still lands on it (see request-key.ts).
21832
+ idempotencyKey: derivePaidRequestKey({
21833
+ fingerprint: api.fingerprint(),
21834
+ tool: "generate_free_video",
21835
+ input,
21836
+ nowMs: Date.now()
21837
+ }),
21590
21838
  transport
21591
21839
  });
21592
21840
  } catch (e) {
@@ -21651,6 +21899,12 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
21651
21899
  path: "/v1/creatives/videos",
21652
21900
  estimatedSeconds: 300,
21653
21901
  input: wirePayload,
21902
+ idempotencyKey: derivePaidRequestKey({
21903
+ fingerprint: api.fingerprint(),
21904
+ tool: "generate_video_creative",
21905
+ input,
21906
+ nowMs: Date.now()
21907
+ }),
21654
21908
  transport
21655
21909
  });
21656
21910
  } catch (e) {
@@ -21685,6 +21939,12 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
21685
21939
  pauseForAngleReview: true
21686
21940
  }
21687
21941
  },
21942
+ idempotencyKey: derivePaidRequestKey({
21943
+ fingerprint: api.fingerprint(),
21944
+ tool: "generate_image_creatives",
21945
+ input,
21946
+ nowMs: Date.now()
21947
+ }),
21688
21948
  transport
21689
21949
  });
21690
21950
  } catch (e) {
@@ -21728,6 +21988,12 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
21728
21988
  pauseForAngleReview: true
21729
21989
  }
21730
21990
  },
21991
+ idempotencyKey: derivePaidRequestKey({
21992
+ fingerprint: api.fingerprint(),
21993
+ tool: "generate_landing",
21994
+ input,
21995
+ nowMs: Date.now()
21996
+ }),
21731
21997
  transport
21732
21998
  });
21733
21999
  } catch (e) {
@@ -21859,7 +22125,7 @@ var CreateCreativeVariantInputBase = z79.object({
21859
22125
  sourceTaskId: z79.string().uuid().describe("ID de la tarea original completada."),
21860
22126
  kind: CreativeVariantKindEnum,
21861
22127
  confirm: z79.literal(true).describe("Confirmaci\xF3n expl\xEDcita del usuario despu\xE9s de mostrarle el estimado de cr\xE9ditos."),
21862
- clientRequestId: z79.string().uuid().describe("UUID estable y obligatorio para esta intenci\xF3n. Reutil\xEDzalo ante timeouts o transporte ambiguo. Si la API devuelve status=error, una nueva generaci\xF3n exige otra confirmaci\xF3n y un UUID nuevo."),
22128
+ clientRequestId: z79.string().uuid().describe("UUID estable para esta intenci\xF3n (obligatorio por compatibilidad de contrato; el servidor deriva la identidad real del request desde los argumentos sem\xE1nticos, as\xED que un UUID nuevo en un retry NO crea una variante duplicada). Si la API devuelve status=error, una nueva generaci\xF3n exige otra confirmaci\xF3n."),
21863
22129
  angle: z79.string().trim().min(1).max(500).optional().describe("\xC1ngulo nuevo para video o imagen."),
21864
22130
  buyerPersona: z79.string().trim().min(1).max(360).optional().describe("Buyer persona coherente con el \xE1ngulo."),
21865
22131
  scriptNotes: z79.string().trim().min(1).max(2e3).optional().describe("Direcci\xF3n breve del guion para una variante de video."),
@@ -21981,7 +22247,7 @@ function registerCreativeVariantTools(server, api) {
21981
22247
  throw translateError(error);
21982
22248
  }
21983
22249
  });
21984
- server.tool("create_creative_variant", createCreativeVariantGuidance.summary + " ES UNA ACCI\xD3N DE PAGO: primero llama estimate_creative_variant, muestra el rango de cr\xE9ditos al usuario y solo despu\xE9s invoca este tool con confirm=true. Las recomendaciones IA tambi\xE9n son acciones pagas a 2x del costo real. clientRequestId es obligatorio: genera un UUID por intenci\xF3n y reutiliza el mismo ante timeouts o transporte ambiguo. Si recibes status=error, pide confirmaci\xF3n para un intento nuevo y usa otro UUID. Reutiliza el producto y referencias del task origen; crea un child task independiente y devuelve variantTaskId. Para landing puedes enviar instructions en texto libre y omitir sectionKey: el backend lo clasifica con IA. No expone ni permite cambiar videoModel, videoTier, videoStyle o narrativeStyle; esos valores se heredan del task origen.", CreateCreativeVariantInputBase.shape, CREATES_PAID, async (input) => {
22250
+ server.tool("create_creative_variant", createCreativeVariantGuidance.summary + " ES UNA ACCI\xD3N DE PAGO: primero llama estimate_creative_variant, muestra el rango de cr\xE9ditos al usuario y solo despu\xE9s invoca este tool con confirm=true. Las recomendaciones IA tambi\xE9n son acciones pagas a 2x del costo real. clientRequestId es obligatorio por contrato: genera un UUID por intenci\xF3n. El servidor deriva la identidad real del request desde los argumentos sem\xE1nticos, as\xED que un UUID nuevo NO desbloquea un intento distinto \u2014 si recibes status=error, pide confirmaci\xF3n al usuario y reinvoca con los mismos argumentos. Reutiliza el producto y referencias del task origen; crea un child task independiente y devuelve variantTaskId. Para landing puedes enviar instructions en texto libre y omitir sectionKey: el backend lo clasifica con IA. No expone ni permite cambiar videoModel, videoTier, videoStyle o narrativeStyle; esos valores se heredan del task origen.", CreateCreativeVariantInputBase.shape, CREATES_PAID, async (input) => {
21985
22251
  try {
21986
22252
  const validatedInput = parseVariantInput(CreateCreativeVariantInput, input);
21987
22253
  const estimateInput = EstimateCreativeVariantInput.parse({
@@ -22008,8 +22274,16 @@ function registerCreativeVariantTools(server, api) {
22008
22274
  });
22009
22275
  sectionKey = LandingSectionKeyEnum.parse(recommendation.value);
22010
22276
  }
22277
+ const derivedClientRequestId = derivePaidRequestKey({
22278
+ fingerprint: api.fingerprint(),
22279
+ tool: "create_creative_variant",
22280
+ input: {
22281
+ ...validatedInput
22282
+ },
22283
+ nowMs: Date.now()
22284
+ });
22011
22285
  const payload = {
22012
- clientRequestId: validatedInput.clientRequestId,
22286
+ clientRequestId: derivedClientRequestId,
22013
22287
  kind: validatedInput.kind,
22014
22288
  ...validatedInput.kind !== "landing" ? {
22015
22289
  angle: validatedInput.angle,
@@ -22035,7 +22309,7 @@ function registerCreativeVariantTools(server, api) {
22035
22309
  };
22036
22310
  const created = await api.postJson(`/v1/tasks/${encodeURIComponent(validatedInput.sourceTaskId)}/creative-variants`, payload);
22037
22311
  if (created.status === "error") {
22038
- throw mcp(McpErrorCode.CREATIVE_VARIANT_FAILED, "La variante anterior termin\xF3 con error. Pide confirmaci\xF3n para un intento nuevo y usa otro clientRequestId.", {
22312
+ throw mcp(McpErrorCode.CREATIVE_VARIANT_FAILED, "La variante anterior termin\xF3 con error. Pide confirmaci\xF3n al usuario y reinvoca con los mismos argumentos para un intento nuevo.", {
22039
22313
  variantTaskId: created.variantTaskId
22040
22314
  });
22041
22315
  }
@@ -22250,39 +22524,6 @@ __name(registerActorTools, "registerActorTools");
22250
22524
  // src/tools/publish.ts
22251
22525
  import { z as z83 } from "zod";
22252
22526
 
22253
- // src/tools/stable-uuid.ts
22254
- import { createHash as createHash3 } from "crypto";
22255
- function canonicalize(value) {
22256
- if (Array.isArray(value)) return value.map(canonicalize);
22257
- if (value === null || typeof value !== "object") return value;
22258
- const record = value;
22259
- return Object.fromEntries(Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => [
22260
- key,
22261
- canonicalize(record[key])
22262
- ]));
22263
- }
22264
- __name(canonicalize, "canonicalize");
22265
- function canonicalJson(value) {
22266
- return JSON.stringify(canonicalize(value));
22267
- }
22268
- __name(canonicalJson, "canonicalJson");
22269
- function deriveUuidV5(name, namespace) {
22270
- const namespaceBytes = Buffer.from(namespace.replaceAll("-", ""), "hex");
22271
- const digest = createHash3("sha1").update(namespaceBytes).update(name, "utf8").digest();
22272
- const bytes = Buffer.from(digest.subarray(0, 16));
22273
- bytes[6] = (bytes[6] ?? 0) & 15 | 80;
22274
- bytes[8] = (bytes[8] ?? 0) & 63 | 128;
22275
- const hex = bytes.toString("hex");
22276
- return [
22277
- hex.slice(0, 8),
22278
- hex.slice(8, 12),
22279
- hex.slice(12, 16),
22280
- hex.slice(16, 20),
22281
- hex.slice(20)
22282
- ].join("-");
22283
- }
22284
- __name(deriveUuidV5, "deriveUuidV5");
22285
-
22286
22527
  // src/tools/meta-publish-attempt-id.ts
22287
22528
  var META_PUBLISH_NAMESPACE = "7b19c9ac-b4eb-4db1-970e-c2f24ce30b98";
22288
22529
  function deriveMetaPublishAttemptId(input) {
@@ -24931,6 +25172,16 @@ function firstHeader(value) {
24931
25172
  }
24932
25173
  __name(firstHeader, "firstHeader");
24933
25174
  var PROXY_FETCH_TIMEOUT_MS = 3e4;
25175
+ var DEFAULT_SUBJECT_TOKEN_REAUTH_WINDOW_MS = 18e4;
25176
+ function subjectTokenReauthWindowMs(env) {
25177
+ const raw = env.MCP_SUBJECT_TOKEN_REAUTH_WINDOW_MS;
25178
+ if (raw === void 0 || raw.trim() === "") {
25179
+ return DEFAULT_SUBJECT_TOKEN_REAUTH_WINDOW_MS;
25180
+ }
25181
+ const parsed = Number.parseInt(raw, 10);
25182
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : DEFAULT_SUBJECT_TOKEN_REAUTH_WINDOW_MS;
25183
+ }
25184
+ __name(subjectTokenReauthWindowMs, "subjectTokenReauthWindowMs");
24934
25185
  var INLINE_RENDERABLE_MIMES = /* @__PURE__ */ new Set([
24935
25186
  "image/jpeg",
24936
25187
  "image/png",
@@ -25061,7 +25312,8 @@ function resolveDownstreamAuth(req, opts, oauthAnalytics) {
25061
25312
  credential,
25062
25313
  subject,
25063
25314
  clientId,
25064
- isLegacy: false
25315
+ isLegacy: false,
25316
+ subjectExpiresAt: expiresAt
25065
25317
  };
25066
25318
  }
25067
25319
  __name(resolveDownstreamAuth, "resolveDownstreamAuth");
@@ -25085,6 +25337,14 @@ function createAuthenticatedMcpHandler(opts, oauthAnalytics) {
25085
25337
  return;
25086
25338
  }
25087
25339
  const { credential, subject, clientId, isLegacy } = authContext;
25340
+ const reauthWindowMs = subjectTokenReauthWindowMs(process.env);
25341
+ if (reauthWindowMs > 0 && !isLegacy && typeof authContext.subjectExpiresAt === "number" && authContext.subjectExpiresAt * 1e3 <= Date.now() + reauthWindowMs) {
25342
+ res.status(401).set("WWW-Authenticate", `Bearer error="invalid_token", error_description="Access token expired or expiring soon", scope="${MCP_SCOPE}", resource_metadata="${getOAuthProtectedResourceMetadataUrl2(MCP_RESOURCE)}"`).json({
25343
+ error: "invalid_token",
25344
+ error_description: "Access token expired or expiring soon; refresh or re-authenticate."
25345
+ });
25346
+ return;
25347
+ }
25088
25348
  const abortController = new AbortController();
25089
25349
  const abortRequest = /* @__PURE__ */ __name(() => abortController.abort(), "abortRequest");
25090
25350
  req.once("aborted", abortRequest);
package/dist/stdio.js CHANGED
@@ -1852,6 +1852,9 @@ var VideoCreativeJobStatus = z14.object({
1852
1852
  var VideoCreativeJobPayload = z14.object({
1853
1853
  jobId: z14.string().optional(),
1854
1854
  status: VideoCreativeJobStatusEnum.optional(),
1855
+ /** True when the submission replayed an existing task instead of billing
1856
+ * new work — callers surface the twin verbatim. Omitted on fresh creates. */
1857
+ replayed: z14.boolean().optional(),
1855
1858
  videoStructure: VideoStructure.optional(),
1856
1859
  videoQuality: VideoQuality.optional(),
1857
1860
  videoRouteId: VideoRouteId.optional(),
@@ -1945,7 +1948,8 @@ var ImageCreativeJobStatus = z14.object({
1945
1948
  });
1946
1949
  var ImageCreativeJobPayload = z14.object({
1947
1950
  jobId: z14.string().uuid(),
1948
- status: ImageCreativeJobStatusEnum
1951
+ status: ImageCreativeJobStatusEnum,
1952
+ replayed: z14.boolean().optional()
1949
1953
  });
1950
1954
  var ImageCreativeHistoryEntry = z14.object({
1951
1955
  id: z14.string().uuid(),
@@ -1972,7 +1976,8 @@ var LandingCreativeJobStatus = z14.object({
1972
1976
  });
1973
1977
  var LandingCreativeJobPayload = z14.object({
1974
1978
  jobId: z14.string().uuid(),
1975
- status: LandingCreativeJobStatusEnum
1979
+ status: LandingCreativeJobStatusEnum,
1980
+ replayed: z14.boolean().optional()
1976
1981
  });
1977
1982
  var LandingCreativeHistoryEntry = z14.object({
1978
1983
  id: z14.string().uuid(),
@@ -5643,6 +5648,10 @@ var CreateShopifySubscriptionRequest = z28.object({
5643
5648
  var CreateShopifyBillingChargeRequest = z28.object({
5644
5649
  packId: z28.string().min(1)
5645
5650
  });
5651
+ var ShopifyBillingCallbackQuery = z28.object({
5652
+ charge_id: z28.string().regex(/^\d+$/),
5653
+ transactionId: z28.string().uuid()
5654
+ });
5646
5655
  var CancelShopifySubscriptionRequest = z28.object({
5647
5656
  subscriptionId: z28.string().min(1)
5648
5657
  });
@@ -10870,6 +10879,7 @@ var MixSceneResult = z48.object({
10870
10879
  index: z48.number().int().positive(),
10871
10880
  status: z48.enum([
10872
10881
  "queued",
10882
+ "submitting",
10873
10883
  "submitted",
10874
10884
  "done",
10875
10885
  "failed"
@@ -13468,7 +13478,7 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
13468
13478
  ], [
13469
13479
  "product_costing"
13470
13480
  ]),
13471
- defineCapability("create_creative_variant", "6a777fbe7ad17966d16502e8872e15e4db2dcfacdfd2992d24853340d4f22a00", "credit_consuming", "creative", [
13481
+ defineCapability("create_creative_variant", "3e02399a7785df328f1c4d63cfbb6d7481abaac9f91d4a5cf51d410e657d63aa", "credit_consuming", "creative", [
13472
13482
  "credit_consuming",
13473
13483
  "state_change"
13474
13484
  ], [
@@ -13718,7 +13728,9 @@ var PRODUCTMAKER_CAPABILITY_REGISTRY = deepFreeze([
13718
13728
  // MCP 7.0.0 (Task 11, .agent/plans/2026-09-15-model-agnostic-video-routes.md):
13719
13729
  // `freeProModel` retired in favor of `modelId` (registry ids). Recomputed
13720
13730
  // from `pnpm --filter @productmaker/mcp test` printing the real digest.
13721
- "ead1f563a1915452157628d023ba203ecfa8f502a48259a5b5b15be4c84bf768",
13731
+ // Re-baselined 2026-09-26: `idempotencyKey` field description corrected —
13732
+ // paid tools ignore caller-provided keys and derive their own semantic key.
13733
+ "35616a32a42a0bc062218fdfbb4cc7f4ecfa814571fb86b4f11ef9e4184855e1",
13722
13734
  "credit_consuming",
13723
13735
  "creative",
13724
13736
  [
@@ -15980,7 +15992,10 @@ var ApiClient = class {
15980
15992
  method: "POST",
15981
15993
  headers: {
15982
15994
  ...authHeaders,
15983
- "Content-Type": "application/json"
15995
+ "Content-Type": "application/json",
15996
+ ...options?.idempotencyKey ? {
15997
+ "Idempotency-Key": options.idempotencyKey
15998
+ } : {}
15984
15999
  },
15985
16000
  body: JSON.stringify(body)
15986
16001
  }, options);
@@ -16102,7 +16117,7 @@ var ApiClient = class {
16102
16117
  };
16103
16118
 
16104
16119
  // src/index.ts
16105
- var MCP_VERSION = "8.2.0";
16120
+ var MCP_VERSION = "8.3.0";
16106
16121
 
16107
16122
  // src/meta-upload-app.ts
16108
16123
  import { readFileSync } from "fs";
@@ -17086,8 +17101,13 @@ async function submitCreative(api, opts) {
17086
17101
  const taskId = r.taskId ?? r.jobId;
17087
17102
  return asText({
17088
17103
  taskId,
17089
- status: "queued",
17090
- estimatedSeconds: opts.estimatedSeconds
17104
+ // Replay-aware: a keyed retry may land on a task that is already
17105
+ // processing/done — report the real status, not a hopeful "queued".
17106
+ status: r.status ?? "queued",
17107
+ estimatedSeconds: opts.estimatedSeconds,
17108
+ ...r.replayed === true ? {
17109
+ replayed: true
17110
+ } : {}
17091
17111
  });
17092
17112
  }
17093
17113
  __name(submitCreative, "submitCreative");
@@ -18504,6 +18524,170 @@ ${summary}`,
18504
18524
  }
18505
18525
  __name(requirePaidCreativeConfirmation, "requirePaidCreativeConfirmation");
18506
18526
 
18527
+ // src/tools/stable-uuid.ts
18528
+ import { createHash as createHash2 } from "crypto";
18529
+ function canonicalize(value) {
18530
+ if (Array.isArray(value)) return value.map(canonicalize);
18531
+ if (value === null || typeof value !== "object") return value;
18532
+ const record = value;
18533
+ return Object.fromEntries(Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => [
18534
+ key,
18535
+ canonicalize(record[key])
18536
+ ]));
18537
+ }
18538
+ __name(canonicalize, "canonicalize");
18539
+ function canonicalJson(value) {
18540
+ return JSON.stringify(canonicalize(value));
18541
+ }
18542
+ __name(canonicalJson, "canonicalJson");
18543
+ function deriveUuidV5(name, namespace) {
18544
+ const namespaceBytes = Buffer.from(namespace.replaceAll("-", ""), "hex");
18545
+ const digest = createHash2("sha1").update(namespaceBytes).update(name, "utf8").digest();
18546
+ const bytes = Buffer.from(digest.subarray(0, 16));
18547
+ bytes[6] = (bytes[6] ?? 0) & 15 | 80;
18548
+ bytes[8] = (bytes[8] ?? 0) & 63 | 128;
18549
+ const hex = bytes.toString("hex");
18550
+ return [
18551
+ hex.slice(0, 8),
18552
+ hex.slice(8, 12),
18553
+ hex.slice(12, 16),
18554
+ hex.slice(16, 20),
18555
+ hex.slice(20)
18556
+ ].join("-");
18557
+ }
18558
+ __name(deriveUuidV5, "deriveUuidV5");
18559
+
18560
+ // src/tools/request-key.ts
18561
+ var PAID_REQUEST_KEY_WINDOW_MS = 15 * 60 * 1e3;
18562
+ var PAID_REQUEST_KEY_NAMESPACE = "8f3d6b1c-2a47-4e5b-9c0d-6e7f8a9b0c1d";
18563
+ var PARAPHRASE_PRONE_FIELDS = /* @__PURE__ */ new Set([
18564
+ "additionalContext",
18565
+ "productContext",
18566
+ "scriptNotes",
18567
+ "creativeDirection",
18568
+ "buyerPersona",
18569
+ "actorDescription",
18570
+ "description",
18571
+ "idempotencyKey",
18572
+ "clientRequestId"
18573
+ ]);
18574
+ var URL_FIELDS = /* @__PURE__ */ new Set([
18575
+ "landingUrl",
18576
+ "productUrl",
18577
+ "url",
18578
+ "serviceLandingUrl"
18579
+ ]);
18580
+ var TRACKING_PARAM_PATTERN = /^(utm_\w+|fbclid|gclid|msclkid|mc_cid|mc_eid|igshid)$/;
18581
+ var LANGUAGE_FIELDS = /* @__PURE__ */ new Set([
18582
+ "language",
18583
+ "lang"
18584
+ ]);
18585
+ var CASE_FOLD_FIELDS = /* @__PURE__ */ new Set([
18586
+ "country",
18587
+ "language",
18588
+ "lang"
18589
+ ]);
18590
+ var ORDER_INSIGNIFICANT_ARRAY_FIELDS = /* @__PURE__ */ new Set([
18591
+ "outputs",
18592
+ "selectedOutputs",
18593
+ "features"
18594
+ ]);
18595
+ function sortCanonical(items) {
18596
+ return [
18597
+ ...items
18598
+ ].sort((a, b) => {
18599
+ const ka = canonicalJson(a);
18600
+ const kb = canonicalJson(b);
18601
+ return ka < kb ? -1 : ka > kb ? 1 : 0;
18602
+ });
18603
+ }
18604
+ __name(sortCanonical, "sortCanonical");
18605
+ function isRecord3(value) {
18606
+ return typeof value === "object" && value !== null && !Array.isArray(value);
18607
+ }
18608
+ __name(isRecord3, "isRecord");
18609
+ function collectLanguageCodes(value, out) {
18610
+ if (Array.isArray(value)) {
18611
+ const items = value;
18612
+ for (const item of items) collectLanguageCodes(item, out);
18613
+ return out;
18614
+ }
18615
+ if (!isRecord3(value)) return out;
18616
+ for (const [key, entry] of Object.entries(value)) {
18617
+ if (LANGUAGE_FIELDS.has(key) && typeof entry === "string") {
18618
+ const base = entry.trim().split(/[-_]/)[0]?.toLowerCase();
18619
+ if (base) out.add(base);
18620
+ }
18621
+ collectLanguageCodes(entry, out);
18622
+ }
18623
+ return out;
18624
+ }
18625
+ __name(collectLanguageCodes, "collectLanguageCodes");
18626
+ function normalizeSearchForKey(search) {
18627
+ if (!search) return "";
18628
+ const params = [
18629
+ ...new URLSearchParams(search).entries()
18630
+ ].filter(([key]) => !TRACKING_PARAM_PATTERN.test(key.toLowerCase())).sort(([ka, va], [kb, vb]) => ka === kb ? va < vb ? -1 : va > vb ? 1 : 0 : ka < kb ? -1 : 1);
18631
+ if (!params.length) return "";
18632
+ const out = new URLSearchParams();
18633
+ for (const [key, value] of params) out.append(key, value);
18634
+ return `?${out.toString()}`;
18635
+ }
18636
+ __name(normalizeSearchForKey, "normalizeSearchForKey");
18637
+ function normalizeUrlForKey(value, languages) {
18638
+ const trimmed = value.trim();
18639
+ let url;
18640
+ try {
18641
+ url = new URL(trimmed);
18642
+ } catch {
18643
+ return trimmed.toLowerCase();
18644
+ }
18645
+ const host = url.hostname.toLowerCase().replace(/^www\./, "");
18646
+ const port = url.port ? `:${url.port}` : "";
18647
+ const segments = url.pathname.split("/").filter(Boolean);
18648
+ const soleSegment = segments.length === 1 ? segments[0] : void 0;
18649
+ if (soleSegment !== void 0 && languages.has(soleSegment.toLowerCase())) {
18650
+ segments.pop();
18651
+ }
18652
+ const path3 = segments.length ? `/${segments.join("/")}` : "";
18653
+ return host + port + path3 + normalizeSearchForKey(url.search);
18654
+ }
18655
+ __name(normalizeUrlForKey, "normalizeUrlForKey");
18656
+ function normalizeForKey(value, languages) {
18657
+ if (Array.isArray(value)) {
18658
+ const items = value;
18659
+ return items.map((item) => normalizeForKey(item, languages));
18660
+ }
18661
+ if (!isRecord3(value)) return value;
18662
+ const out = {};
18663
+ for (const [key, entry] of Object.entries(value)) {
18664
+ if (PARAPHRASE_PRONE_FIELDS.has(key)) {
18665
+ if (key === "description" && value.mode === "described") {
18666
+ out[key] = normalizeForKey(entry, languages);
18667
+ }
18668
+ continue;
18669
+ }
18670
+ out[key] = URL_FIELDS.has(key) && typeof entry === "string" ? normalizeUrlForKey(entry, languages) : CASE_FOLD_FIELDS.has(key) && typeof entry === "string" ? (
18671
+ // separator too or a host alternating between them splits the key.
18672
+ entry.trim().toLowerCase().replace(/_/g, "-")
18673
+ ) : ORDER_INSIGNIFICANT_ARRAY_FIELDS.has(key) && Array.isArray(entry) ? sortCanonical(entry.map((item) => normalizeForKey(item, languages))) : normalizeForKey(entry, languages);
18674
+ }
18675
+ return out;
18676
+ }
18677
+ __name(normalizeForKey, "normalizeForKey");
18678
+ function derivePaidRequestKey(args) {
18679
+ const languages = collectLanguageCodes(args.input, /* @__PURE__ */ new Set());
18680
+ const semanticInput = normalizeForKey(args.input, languages);
18681
+ const bucket = Math.floor(args.nowMs / PAID_REQUEST_KEY_WINDOW_MS);
18682
+ return deriveUuidV5(canonicalJson({
18683
+ user: args.fingerprint,
18684
+ tool: args.tool,
18685
+ input: semanticInput,
18686
+ bucket
18687
+ }), PAID_REQUEST_KEY_NAMESPACE);
18688
+ }
18689
+ __name(derivePaidRequestKey, "derivePaidRequestKey");
18690
+
18507
18691
  // src/tools/winning-product-image.ts
18508
18692
  import { z as z75 } from "zod";
18509
18693
  var WinnerReferenceInput = z75.object({
@@ -19040,10 +19224,25 @@ ANTES de invocar, resume outputs, cantidad de videos y datos comerciales, explic
19040
19224
  };
19041
19225
  const r = await api.postMultipart("/v1/ingest", {
19042
19226
  images: parts
19043
- }, fields);
19227
+ }, fields, {
19228
+ // LLM hosts retry paid tools with rephrased arguments — the API's
19229
+ // byte-fingerprint can't see the duplicate, this key can. Derived
19230
+ // from the semantic inputs, not the wire fields, for the same
19231
+ // reason (see request-key.ts, which also canonicalizes the
19232
+ // order-insignificant `outputs` set).
19233
+ idempotencyKey: derivePaidRequestKey({
19234
+ fingerprint: api.fingerprint(),
19235
+ tool: "create_product_task",
19236
+ input,
19237
+ nowMs: Date.now()
19238
+ })
19239
+ });
19044
19240
  return asText({
19045
19241
  taskId: r.taskId,
19046
- status: "queued",
19242
+ status: r.status ?? "queued",
19243
+ ...r.replayed ? {
19244
+ replayed: true
19245
+ } : {},
19047
19246
  estimatedSeconds: 300,
19048
19247
  outputs: selectedOutputs,
19049
19248
  outputMode: "single_task",
@@ -19080,26 +19279,56 @@ ANTES de invocar, resume outputs, cantidad de videos y datos comerciales, explic
19080
19279
  assets: []
19081
19280
  },
19082
19281
  appDemoVideos: []
19282
+ }, {
19283
+ // The 2026-09-25 incident: four retries of this exact tool created
19284
+ // four charged service tasks because the host rephrased
19285
+ // `additionalContext` and flipped the landing between `/` and
19286
+ // `/es`. The derived key is stable across both rewrites (see
19287
+ // request-key.ts).
19288
+ idempotencyKey: derivePaidRequestKey({
19289
+ fingerprint: api.fingerprint(),
19290
+ tool: "create_service_video",
19291
+ input,
19292
+ nowMs: Date.now()
19293
+ })
19083
19294
  });
19084
19295
  const angles = await api.postJson(`/v1/tasks/${encodeURIComponent(created.taskId)}/service-angles`, {});
19085
19296
  const first = angles[0];
19297
+ const replayed = created.replayed === true;
19086
19298
  if (input.reviewMode === "automatic") {
19087
19299
  if (!first) throw new Error("ProductMaker no pudo crear un concepto para este servicio.");
19088
- await api.postJson(`/v1/tasks/${encodeURIComponent(created.taskId)}/service-angles/select`, {
19089
- angleIds: [
19090
- first.id
19091
- ]
19092
- });
19300
+ if (!(replayed && created.status !== "queued")) {
19301
+ await api.postJson(`/v1/tasks/${encodeURIComponent(created.taskId)}/service-angles/select`, {
19302
+ angleIds: [
19303
+ first.id
19304
+ ]
19305
+ });
19306
+ }
19093
19307
  return asText({
19094
19308
  taskId: created.taskId,
19095
- status: "processing",
19309
+ status: replayed ? created.status ?? "processing" : "processing",
19310
+ ...replayed ? {
19311
+ replayed: true
19312
+ } : {},
19096
19313
  reviewMode: input.reviewMode,
19097
19314
  selectedConcept: first
19098
19315
  });
19099
19316
  }
19317
+ if (replayed && created.status !== "queued") {
19318
+ return asText({
19319
+ taskId: created.taskId,
19320
+ status: created.status,
19321
+ replayed: true,
19322
+ reviewMode: input.reviewMode,
19323
+ angles
19324
+ });
19325
+ }
19100
19326
  return asText({
19101
19327
  taskId: created.taskId,
19102
19328
  status: "waiting_for_user",
19329
+ ...replayed ? {
19330
+ replayed: true
19331
+ } : {},
19103
19332
  reviewMode: input.reviewMode,
19104
19333
  pendingUserAction: {
19105
19334
  kind: "service_angle_approval",
@@ -19675,7 +19904,6 @@ ANTES de invocar, resume outputs, cantidad de videos y datos comerciales, explic
19675
19904
  __name(registerTaskTools, "registerTaskTools");
19676
19905
 
19677
19906
  // src/tools/creatives.ts
19678
- import { randomUUID } from "crypto";
19679
19907
  import { z as z77 } from "zod";
19680
19908
  var generateFreeVideoGuidance = getProductMakerCapabilityModelGuidance("generate_free_video");
19681
19909
  var generateVideoCreativeGuidance = getProductMakerCapabilityModelGuidance("generate_video_creative");
@@ -19796,7 +20024,7 @@ var FreeVideoInput = z77.object({
19796
20024
  videoResolution: VideoResolutionEnum.describe("Resoluci\xF3n del video: 480p o 720p."),
19797
20025
  generateAiMusic: z77.boolean().default(false).describe("Opcional. Agrega m\xFAsica instrumental IA debajo del audio nativo. Default false."),
19798
20026
  addSubtitles: z77.boolean().default(false).describe("Opcional. Quema subt\xEDtulos usando el rawPrompt exacto. Default false."),
19799
- idempotencyKey: z77.string().uuid().optional().describe("Opcional. UUID para reintentar exactamente esta misma solicitud sin crear ni cobrar otro video. Reutiliza el mismo valor si el cliente no recibi\xF3 la respuesta.")
20027
+ idempotencyKey: z77.string().uuid().optional().describe("Opcional. Se acepta por compatibilidad pero se ignora: la identidad del reintento se deriva autom\xE1ticamente de los argumentos sem\xE1nticos, as\xED que un retry con los mismos argumentos ya es idempotente sin este campo.")
19800
20028
  }).strict();
19801
20029
  var imageStyleList = ImageCreativeStyleEnum.options.join(" | ");
19802
20030
  var ImageCreativeSetup = z77.discriminatedUnion("mode", [
@@ -20018,7 +20246,16 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
20018
20246
  addSubtitles: input.addSubtitles
20019
20247
  },
20020
20248
  allowEmptyImages: true,
20021
- idempotencyKey: input.idempotencyKey ?? randomUUID(),
20249
+ // Always derived — never `input.idempotencyKey ??`: a host that
20250
+ // mints a fresh UUID per retry would defeat the dedupe entirely.
20251
+ // The semantic key is also a strictly better retry identity: a
20252
+ // reused caller key still lands on it (see request-key.ts).
20253
+ idempotencyKey: derivePaidRequestKey({
20254
+ fingerprint: api.fingerprint(),
20255
+ tool: "generate_free_video",
20256
+ input,
20257
+ nowMs: Date.now()
20258
+ }),
20022
20259
  transport
20023
20260
  });
20024
20261
  } catch (e) {
@@ -20083,6 +20320,12 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
20083
20320
  path: "/v1/creatives/videos",
20084
20321
  estimatedSeconds: 300,
20085
20322
  input: wirePayload,
20323
+ idempotencyKey: derivePaidRequestKey({
20324
+ fingerprint: api.fingerprint(),
20325
+ tool: "generate_video_creative",
20326
+ input,
20327
+ nowMs: Date.now()
20328
+ }),
20086
20329
  transport
20087
20330
  });
20088
20331
  } catch (e) {
@@ -20117,6 +20360,12 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
20117
20360
  pauseForAngleReview: true
20118
20361
  }
20119
20362
  },
20363
+ idempotencyKey: derivePaidRequestKey({
20364
+ fingerprint: api.fingerprint(),
20365
+ tool: "generate_image_creatives",
20366
+ input,
20367
+ nowMs: Date.now()
20368
+ }),
20120
20369
  transport
20121
20370
  });
20122
20371
  } catch (e) {
@@ -20160,6 +20409,12 @@ function registerCreativeTools(server, api, { transport = "http" } = {}) {
20160
20409
  pauseForAngleReview: true
20161
20410
  }
20162
20411
  },
20412
+ idempotencyKey: derivePaidRequestKey({
20413
+ fingerprint: api.fingerprint(),
20414
+ tool: "generate_landing",
20415
+ input,
20416
+ nowMs: Date.now()
20417
+ }),
20163
20418
  transport
20164
20419
  });
20165
20420
  } catch (e) {
@@ -20291,7 +20546,7 @@ var CreateCreativeVariantInputBase = z78.object({
20291
20546
  sourceTaskId: z78.string().uuid().describe("ID de la tarea original completada."),
20292
20547
  kind: CreativeVariantKindEnum,
20293
20548
  confirm: z78.literal(true).describe("Confirmaci\xF3n expl\xEDcita del usuario despu\xE9s de mostrarle el estimado de cr\xE9ditos."),
20294
- clientRequestId: z78.string().uuid().describe("UUID estable y obligatorio para esta intenci\xF3n. Reutil\xEDzalo ante timeouts o transporte ambiguo. Si la API devuelve status=error, una nueva generaci\xF3n exige otra confirmaci\xF3n y un UUID nuevo."),
20549
+ clientRequestId: z78.string().uuid().describe("UUID estable para esta intenci\xF3n (obligatorio por compatibilidad de contrato; el servidor deriva la identidad real del request desde los argumentos sem\xE1nticos, as\xED que un UUID nuevo en un retry NO crea una variante duplicada). Si la API devuelve status=error, una nueva generaci\xF3n exige otra confirmaci\xF3n."),
20295
20550
  angle: z78.string().trim().min(1).max(500).optional().describe("\xC1ngulo nuevo para video o imagen."),
20296
20551
  buyerPersona: z78.string().trim().min(1).max(360).optional().describe("Buyer persona coherente con el \xE1ngulo."),
20297
20552
  scriptNotes: z78.string().trim().min(1).max(2e3).optional().describe("Direcci\xF3n breve del guion para una variante de video."),
@@ -20413,7 +20668,7 @@ function registerCreativeVariantTools(server, api) {
20413
20668
  throw translateError(error);
20414
20669
  }
20415
20670
  });
20416
- server.tool("create_creative_variant", createCreativeVariantGuidance.summary + " ES UNA ACCI\xD3N DE PAGO: primero llama estimate_creative_variant, muestra el rango de cr\xE9ditos al usuario y solo despu\xE9s invoca este tool con confirm=true. Las recomendaciones IA tambi\xE9n son acciones pagas a 2x del costo real. clientRequestId es obligatorio: genera un UUID por intenci\xF3n y reutiliza el mismo ante timeouts o transporte ambiguo. Si recibes status=error, pide confirmaci\xF3n para un intento nuevo y usa otro UUID. Reutiliza el producto y referencias del task origen; crea un child task independiente y devuelve variantTaskId. Para landing puedes enviar instructions en texto libre y omitir sectionKey: el backend lo clasifica con IA. No expone ni permite cambiar videoModel, videoTier, videoStyle o narrativeStyle; esos valores se heredan del task origen.", CreateCreativeVariantInputBase.shape, CREATES_PAID, async (input) => {
20671
+ server.tool("create_creative_variant", createCreativeVariantGuidance.summary + " ES UNA ACCI\xD3N DE PAGO: primero llama estimate_creative_variant, muestra el rango de cr\xE9ditos al usuario y solo despu\xE9s invoca este tool con confirm=true. Las recomendaciones IA tambi\xE9n son acciones pagas a 2x del costo real. clientRequestId es obligatorio por contrato: genera un UUID por intenci\xF3n. El servidor deriva la identidad real del request desde los argumentos sem\xE1nticos, as\xED que un UUID nuevo NO desbloquea un intento distinto \u2014 si recibes status=error, pide confirmaci\xF3n al usuario y reinvoca con los mismos argumentos. Reutiliza el producto y referencias del task origen; crea un child task independiente y devuelve variantTaskId. Para landing puedes enviar instructions en texto libre y omitir sectionKey: el backend lo clasifica con IA. No expone ni permite cambiar videoModel, videoTier, videoStyle o narrativeStyle; esos valores se heredan del task origen.", CreateCreativeVariantInputBase.shape, CREATES_PAID, async (input) => {
20417
20672
  try {
20418
20673
  const validatedInput = parseVariantInput(CreateCreativeVariantInput, input);
20419
20674
  const estimateInput = EstimateCreativeVariantInput.parse({
@@ -20440,8 +20695,16 @@ function registerCreativeVariantTools(server, api) {
20440
20695
  });
20441
20696
  sectionKey = LandingSectionKeyEnum.parse(recommendation.value);
20442
20697
  }
20698
+ const derivedClientRequestId = derivePaidRequestKey({
20699
+ fingerprint: api.fingerprint(),
20700
+ tool: "create_creative_variant",
20701
+ input: {
20702
+ ...validatedInput
20703
+ },
20704
+ nowMs: Date.now()
20705
+ });
20443
20706
  const payload = {
20444
- clientRequestId: validatedInput.clientRequestId,
20707
+ clientRequestId: derivedClientRequestId,
20445
20708
  kind: validatedInput.kind,
20446
20709
  ...validatedInput.kind !== "landing" ? {
20447
20710
  angle: validatedInput.angle,
@@ -20467,7 +20730,7 @@ function registerCreativeVariantTools(server, api) {
20467
20730
  };
20468
20731
  const created = await api.postJson(`/v1/tasks/${encodeURIComponent(validatedInput.sourceTaskId)}/creative-variants`, payload);
20469
20732
  if (created.status === "error") {
20470
- throw mcp(McpErrorCode.CREATIVE_VARIANT_FAILED, "La variante anterior termin\xF3 con error. Pide confirmaci\xF3n para un intento nuevo y usa otro clientRequestId.", {
20733
+ throw mcp(McpErrorCode.CREATIVE_VARIANT_FAILED, "La variante anterior termin\xF3 con error. Pide confirmaci\xF3n al usuario y reinvoca con los mismos argumentos para un intento nuevo.", {
20471
20734
  variantTaskId: created.variantTaskId
20472
20735
  });
20473
20736
  }
@@ -20682,39 +20945,6 @@ __name(registerActorTools, "registerActorTools");
20682
20945
  // src/tools/publish.ts
20683
20946
  import { z as z82 } from "zod";
20684
20947
 
20685
- // src/tools/stable-uuid.ts
20686
- import { createHash as createHash2 } from "crypto";
20687
- function canonicalize(value) {
20688
- if (Array.isArray(value)) return value.map(canonicalize);
20689
- if (value === null || typeof value !== "object") return value;
20690
- const record = value;
20691
- return Object.fromEntries(Object.keys(record).sort().filter((key) => record[key] !== void 0).map((key) => [
20692
- key,
20693
- canonicalize(record[key])
20694
- ]));
20695
- }
20696
- __name(canonicalize, "canonicalize");
20697
- function canonicalJson(value) {
20698
- return JSON.stringify(canonicalize(value));
20699
- }
20700
- __name(canonicalJson, "canonicalJson");
20701
- function deriveUuidV5(name, namespace) {
20702
- const namespaceBytes = Buffer.from(namespace.replaceAll("-", ""), "hex");
20703
- const digest = createHash2("sha1").update(namespaceBytes).update(name, "utf8").digest();
20704
- const bytes = Buffer.from(digest.subarray(0, 16));
20705
- bytes[6] = (bytes[6] ?? 0) & 15 | 80;
20706
- bytes[8] = (bytes[8] ?? 0) & 63 | 128;
20707
- const hex = bytes.toString("hex");
20708
- return [
20709
- hex.slice(0, 8),
20710
- hex.slice(8, 12),
20711
- hex.slice(12, 16),
20712
- hex.slice(16, 20),
20713
- hex.slice(20)
20714
- ].join("-");
20715
- }
20716
- __name(deriveUuidV5, "deriveUuidV5");
20717
-
20718
20948
  // src/tools/meta-publish-attempt-id.ts
20719
20949
  var META_PUBLISH_NAMESPACE = "7b19c9ac-b4eb-4db1-970e-c2f24ce30b98";
20720
20950
  function deriveMetaPublishAttemptId(input) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "8.2.0",
3
+ "version": "8.3.0",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "ProductMaker MCP server — cost products, generate landing pages, UGC videos, and ad creatives, and publish to Shopify, Meta Ads, and TikTok Ads.",