@productmaker/mcp 2.9.3 → 2.9.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.
package/dist/http.js CHANGED
@@ -153,7 +153,7 @@ var ApiClient = class {
153
153
  };
154
154
 
155
155
  // src/index.ts
156
- var MCP_VERSION = "2.9.3";
156
+ var MCP_VERSION = "2.9.4";
157
157
 
158
158
  // src/meta-upload-app.ts
159
159
  import { readFileSync } from "fs";
@@ -11386,8 +11386,7 @@ function uploadSessionText(result) {
11386
11386
  text: [
11387
11387
  `### Prepara tu ${mediaLabel} en ProductMaker`,
11388
11388
  `Usa primero el App inline de este chat. Si tu host no soporta el bridge de archivos, usa este fallback: [Abrir carga segura](${result.uploadPageUrl})`,
11389
- `Puedes seleccionar hasta ${result.maxFiles} ${result.mediaKind === "mixed" ? "archivos" : "archivo"}. Cuando termine, escribe **\u201Clisto\u201D** en este chat para que ProductMaker contin\xFAe con Meta.`,
11390
- `sessionId: \`${result.sessionId}\``,
11389
+ `Puedes seleccionar hasta ${result.maxFiles} ${result.mediaKind === "mixed" ? "archivos" : "archivo"}. Cuando termines, pulsa **OK** dentro del App para que ProductMaker contin\xFAe en este chat.`,
11391
11390
  `Este enlace expira: ${result.expiresAt}`
11392
11391
  ].join("\n\n")
11393
11392
  }
@@ -11398,7 +11397,7 @@ __name(uploadSessionText, "uploadSessionText");
11398
11397
  function registerMetaUploadTools(server, api) {
11399
11398
  server.registerTool("request_meta_media_upload", {
11400
11399
  title: "Solicitar carga segura de Meta",
11401
- description: "Inicia una carga segura desde la laptop del usuario para creativos propios de Meta. Por defecto crea una sesi\xF3n `mixed` de hasta diez archivos; tambi\xE9n admite una sola imagen o video. Si el usuario adjunta una foto o video en el mismo mensaje de ChatGPT, pasa esas referencias opcionales en `files` para que el MCP App inline las consuma dentro del chat. ProductMaker conserva `files` solo como metadata del UI bridge: NO los descarga y NO los reenv\xEDa. No pidas URL CDN, ruta local, bytes ni base64. El App es exclusivamente para cargar archivos: nunca debe pedir ni mostrar portafolio, cuenta publicitaria, p\xE1gina, IDs o JSON. El enlace externo de ProductMaker queda como fallback para hosts sin bridge de archivos. Conserva privadamente el `sessionId`; cuando el usuario termine, llama `check_meta_media_upload`, luego `list_meta_destinations` y pregunta en el chat qu\xE9 `portfolioId`, `adAccountId` y `pageId` desea usar. No muestres el `sessionId`, `stagingAssetId` ni un handoff JSON al usuario. Esta herramienta no publica ni consume cr\xE9ditos.",
11400
+ description: "Inicia una carga segura desde la laptop del usuario para creativos propios de Meta. Esta es la ruta DIRECTA para peticiones puntuales como \u2018sube esta imagen/video a Meta Ads\u2019 o \u2018publica este creativo\u2019; no conviertas esa intenci\xF3n en una generaci\xF3n completa de producto. No llames `guide_dropshipping_launch` para esta operaci\xF3n puntual. Por defecto crea una sesi\xF3n `mixed` de hasta diez archivos; tambi\xE9n admite una sola imagen o video. Si el usuario adjunta una foto o video en el mismo mensaje de ChatGPT, pasa esas referencias opcionales en `files` para que el MCP App inline las consuma dentro del chat. ProductMaker conserva `files` solo como metadata del UI bridge: NO los descarga y NO los reenv\xEDa. No pidas URL CDN, ruta local, bytes ni base64. El App es exclusivamente para cargar archivos: nunca debe pedir ni mostrar portafolio, cuenta publicitaria, p\xE1gina, IDs o JSON. El enlace externo de ProductMaker queda como fallback para hosts sin bridge de archivos. El usuario pulsa OK cuando termina; el App inserta en el chat los asset_id verificados para continuar la petici\xF3n anterior. Luego llama `list_meta_destinations` y pregunta en el chat qu\xE9 `portfolioId`, `adAccountId` y `pageId` desea usar. No muestres tokens, URLs temporales, `sessionId` ni un bloque JSON. Esta herramienta no publica ni consume cr\xE9ditos.",
11402
11401
  inputSchema: MetaUploadSessionToolInputShape,
11403
11402
  outputSchema: MetaUploadSessionResponse2,
11404
11403
  _meta: {
@@ -12386,20 +12385,21 @@ var CostingInputSchema = z59.object({
12386
12385
  ]
12387
12386
  });
12388
12387
  });
12388
+ var FiniteCostingNumber = z59.number().finite();
12389
12389
  var CostingResultSchema = z59.object({
12390
12390
  country: z59.string().min(2).max(3),
12391
- deliveryRate: z59.number().finite(),
12392
- adjustedShippingCost: z59.number().finite(),
12393
- adjustedCpa: z59.number().finite(),
12394
- totalCost: z59.number().finite(),
12395
- suggestedPrice: z59.number().finite(),
12396
- baseCommercialPrice: z59.number().finite(),
12397
- minimumSafePrice: z59.number().finite(),
12398
- commercialPrice: z59.number().finite(),
12399
- comparisonPrice: z59.number().finite(),
12400
- estimatedProfit: z59.number().finite(),
12401
- actualMargin: z59.number().finite(),
12402
- breakEvenAdsManagerCpa: z59.number().finite(),
12391
+ deliveryRate: FiniteCostingNumber,
12392
+ adjustedShippingCost: FiniteCostingNumber,
12393
+ adjustedCpa: FiniteCostingNumber,
12394
+ totalCost: FiniteCostingNumber,
12395
+ suggestedPrice: FiniteCostingNumber,
12396
+ baseCommercialPrice: FiniteCostingNumber,
12397
+ minimumSafePrice: FiniteCostingNumber,
12398
+ commercialPrice: FiniteCostingNumber,
12399
+ comparisonPrice: FiniteCostingNumber,
12400
+ estimatedProfit: FiniteCostingNumber,
12401
+ actualMargin: FiniteCostingNumber,
12402
+ breakEvenAdsManagerCpa: FiniteCostingNumber,
12403
12403
  wasPriceAutoAdjusted: z59.boolean(),
12404
12404
  profitabilityStatus: z59.literal("healthy")
12405
12405
  });
@@ -12488,13 +12488,13 @@ function registerDropshippingProjectTools(server, api) {
12488
12488
  throw translateError(error);
12489
12489
  }
12490
12490
  });
12491
- server.tool("guide_dropshipping_launch", "Gu\xEDa read-only del lanzamiento. Sin projectId explica cu\xE1ndo usar un flujo directo versus uno guiado; con projectId devuelve la fase y la \xFAnica nextAction calculada por ProductMaker. Despu\xE9s de cada mutaci\xF3n vuelve a llamar este tool o lee el proyecto. Nunca activa Meta.", GuideInput.shape, async (input) => {
12491
+ server.tool("guide_dropshipping_launch", "Gu\xEDa read-only de un recorrido COMPLETO de dropshipping, desde producto/costeo hasta creativos, tienda y publicidad. No uses este tool para subir o publicar un creativo puntual en Meta: llama directamente `request_meta_media_upload`, `list_meta_destinations` y `publish_creatives_to_meta`. Sin projectId explica cu\xE1ndo usar un flujo directo versus uno guiado; con projectId devuelve la fase y la \xFAnica nextAction calculada por ProductMaker. Despu\xE9s de cada mutaci\xF3n vuelve a llamar este tool o lee el proyecto. Nunca activa Meta.", GuideInput.shape, async (input) => {
12492
12492
  try {
12493
12493
  if (!input.projectId) {
12494
12494
  return asText({
12495
12495
  mode: "choose",
12496
12496
  guided: "Usa start_dropshipping_project cuando el usuario quiera encontrar un producto y dejarlo listo para publicar.",
12497
- direct: "En modo directo usa cost_product, create_product_task, publish_to_shopify o publish_to_meta cuando pidi\xF3 una operaci\xF3n espec\xEDfica.",
12497
+ direct: "En modo directo usa la herramienta puntual. Para subir/publicar un creativo propio en Meta: request_meta_media_upload \u2192 list_meta_destinations \u2192 publish_creatives_to_meta. No crees un proyecto guiado.",
12498
12498
  confirmation: "Antes de acciones pagadas o externas, muestra el estimado y pide confirmaci\xF3n.",
12499
12499
  safety: "Las campa\xF1as Meta quedan PAUSED; el usuario las activa manualmente en Ads Manager."
12500
12500
  });
package/dist/stdio.js CHANGED
@@ -150,7 +150,7 @@ var ApiClient = class {
150
150
  };
151
151
 
152
152
  // src/index.ts
153
- var MCP_VERSION = "2.9.3";
153
+ var MCP_VERSION = "2.9.4";
154
154
 
155
155
  // src/meta-upload-app.ts
156
156
  import { readFileSync } from "fs";
@@ -11342,8 +11342,7 @@ function uploadSessionText(result) {
11342
11342
  text: [
11343
11343
  `### Prepara tu ${mediaLabel} en ProductMaker`,
11344
11344
  `Usa primero el App inline de este chat. Si tu host no soporta el bridge de archivos, usa este fallback: [Abrir carga segura](${result.uploadPageUrl})`,
11345
- `Puedes seleccionar hasta ${result.maxFiles} ${result.mediaKind === "mixed" ? "archivos" : "archivo"}. Cuando termine, escribe **\u201Clisto\u201D** en este chat para que ProductMaker contin\xFAe con Meta.`,
11346
- `sessionId: \`${result.sessionId}\``,
11345
+ `Puedes seleccionar hasta ${result.maxFiles} ${result.mediaKind === "mixed" ? "archivos" : "archivo"}. Cuando termines, pulsa **OK** dentro del App para que ProductMaker contin\xFAe en este chat.`,
11347
11346
  `Este enlace expira: ${result.expiresAt}`
11348
11347
  ].join("\n\n")
11349
11348
  }
@@ -11354,7 +11353,7 @@ __name(uploadSessionText, "uploadSessionText");
11354
11353
  function registerMetaUploadTools(server, api) {
11355
11354
  server.registerTool("request_meta_media_upload", {
11356
11355
  title: "Solicitar carga segura de Meta",
11357
- description: "Inicia una carga segura desde la laptop del usuario para creativos propios de Meta. Por defecto crea una sesi\xF3n `mixed` de hasta diez archivos; tambi\xE9n admite una sola imagen o video. Si el usuario adjunta una foto o video en el mismo mensaje de ChatGPT, pasa esas referencias opcionales en `files` para que el MCP App inline las consuma dentro del chat. ProductMaker conserva `files` solo como metadata del UI bridge: NO los descarga y NO los reenv\xEDa. No pidas URL CDN, ruta local, bytes ni base64. El App es exclusivamente para cargar archivos: nunca debe pedir ni mostrar portafolio, cuenta publicitaria, p\xE1gina, IDs o JSON. El enlace externo de ProductMaker queda como fallback para hosts sin bridge de archivos. Conserva privadamente el `sessionId`; cuando el usuario termine, llama `check_meta_media_upload`, luego `list_meta_destinations` y pregunta en el chat qu\xE9 `portfolioId`, `adAccountId` y `pageId` desea usar. No muestres el `sessionId`, `stagingAssetId` ni un handoff JSON al usuario. Esta herramienta no publica ni consume cr\xE9ditos.",
11356
+ description: "Inicia una carga segura desde la laptop del usuario para creativos propios de Meta. Esta es la ruta DIRECTA para peticiones puntuales como \u2018sube esta imagen/video a Meta Ads\u2019 o \u2018publica este creativo\u2019; no conviertas esa intenci\xF3n en una generaci\xF3n completa de producto. No llames `guide_dropshipping_launch` para esta operaci\xF3n puntual. Por defecto crea una sesi\xF3n `mixed` de hasta diez archivos; tambi\xE9n admite una sola imagen o video. Si el usuario adjunta una foto o video en el mismo mensaje de ChatGPT, pasa esas referencias opcionales en `files` para que el MCP App inline las consuma dentro del chat. ProductMaker conserva `files` solo como metadata del UI bridge: NO los descarga y NO los reenv\xEDa. No pidas URL CDN, ruta local, bytes ni base64. El App es exclusivamente para cargar archivos: nunca debe pedir ni mostrar portafolio, cuenta publicitaria, p\xE1gina, IDs o JSON. El enlace externo de ProductMaker queda como fallback para hosts sin bridge de archivos. El usuario pulsa OK cuando termina; el App inserta en el chat los asset_id verificados para continuar la petici\xF3n anterior. Luego llama `list_meta_destinations` y pregunta en el chat qu\xE9 `portfolioId`, `adAccountId` y `pageId` desea usar. No muestres tokens, URLs temporales, `sessionId` ni un bloque JSON. Esta herramienta no publica ni consume cr\xE9ditos.",
11358
11357
  inputSchema: MetaUploadSessionToolInputShape,
11359
11358
  outputSchema: MetaUploadSessionResponse2,
11360
11359
  _meta: {
@@ -12342,20 +12341,21 @@ var CostingInputSchema = z59.object({
12342
12341
  ]
12343
12342
  });
12344
12343
  });
12344
+ var FiniteCostingNumber = z59.number().finite();
12345
12345
  var CostingResultSchema = z59.object({
12346
12346
  country: z59.string().min(2).max(3),
12347
- deliveryRate: z59.number().finite(),
12348
- adjustedShippingCost: z59.number().finite(),
12349
- adjustedCpa: z59.number().finite(),
12350
- totalCost: z59.number().finite(),
12351
- suggestedPrice: z59.number().finite(),
12352
- baseCommercialPrice: z59.number().finite(),
12353
- minimumSafePrice: z59.number().finite(),
12354
- commercialPrice: z59.number().finite(),
12355
- comparisonPrice: z59.number().finite(),
12356
- estimatedProfit: z59.number().finite(),
12357
- actualMargin: z59.number().finite(),
12358
- breakEvenAdsManagerCpa: z59.number().finite(),
12347
+ deliveryRate: FiniteCostingNumber,
12348
+ adjustedShippingCost: FiniteCostingNumber,
12349
+ adjustedCpa: FiniteCostingNumber,
12350
+ totalCost: FiniteCostingNumber,
12351
+ suggestedPrice: FiniteCostingNumber,
12352
+ baseCommercialPrice: FiniteCostingNumber,
12353
+ minimumSafePrice: FiniteCostingNumber,
12354
+ commercialPrice: FiniteCostingNumber,
12355
+ comparisonPrice: FiniteCostingNumber,
12356
+ estimatedProfit: FiniteCostingNumber,
12357
+ actualMargin: FiniteCostingNumber,
12358
+ breakEvenAdsManagerCpa: FiniteCostingNumber,
12359
12359
  wasPriceAutoAdjusted: z59.boolean(),
12360
12360
  profitabilityStatus: z59.literal("healthy")
12361
12361
  });
@@ -12444,13 +12444,13 @@ function registerDropshippingProjectTools(server, api) {
12444
12444
  throw translateError(error);
12445
12445
  }
12446
12446
  });
12447
- server.tool("guide_dropshipping_launch", "Gu\xEDa read-only del lanzamiento. Sin projectId explica cu\xE1ndo usar un flujo directo versus uno guiado; con projectId devuelve la fase y la \xFAnica nextAction calculada por ProductMaker. Despu\xE9s de cada mutaci\xF3n vuelve a llamar este tool o lee el proyecto. Nunca activa Meta.", GuideInput.shape, async (input) => {
12447
+ server.tool("guide_dropshipping_launch", "Gu\xEDa read-only de un recorrido COMPLETO de dropshipping, desde producto/costeo hasta creativos, tienda y publicidad. No uses este tool para subir o publicar un creativo puntual en Meta: llama directamente `request_meta_media_upload`, `list_meta_destinations` y `publish_creatives_to_meta`. Sin projectId explica cu\xE1ndo usar un flujo directo versus uno guiado; con projectId devuelve la fase y la \xFAnica nextAction calculada por ProductMaker. Despu\xE9s de cada mutaci\xF3n vuelve a llamar este tool o lee el proyecto. Nunca activa Meta.", GuideInput.shape, async (input) => {
12448
12448
  try {
12449
12449
  if (!input.projectId) {
12450
12450
  return asText({
12451
12451
  mode: "choose",
12452
12452
  guided: "Usa start_dropshipping_project cuando el usuario quiera encontrar un producto y dejarlo listo para publicar.",
12453
- direct: "En modo directo usa cost_product, create_product_task, publish_to_shopify o publish_to_meta cuando pidi\xF3 una operaci\xF3n espec\xEDfica.",
12453
+ direct: "En modo directo usa la herramienta puntual. Para subir/publicar un creativo propio en Meta: request_meta_media_upload \u2192 list_meta_destinations \u2192 publish_creatives_to_meta. No crees un proyecto guiado.",
12454
12454
  confirmation: "Antes de acciones pagadas o externas, muestra el estimado y pide confirmaci\xF3n.",
12455
12455
  safety: "Las campa\xF1as Meta quedan PAUSED; el usuario las activa manualmente en Ads Manager."
12456
12456
  });
@@ -98,10 +98,8 @@
98
98
  }
99
99
 
100
100
  .upload-feedback {
101
- display: flex;
102
- align-items: center;
103
- flex-wrap: wrap;
104
- gap: 10px;
101
+ display: grid;
102
+ gap: 8px;
105
103
  margin-top: 10px;
106
104
  }
107
105
 
@@ -202,21 +200,31 @@
202
200
  cursor: not-allowed;
203
201
  }
204
202
 
205
- .drop-icon {
206
- display: grid;
207
- width: 48px;
208
- height: 48px;
209
- place-items: center;
210
- border: 1px solid var(--pm-line);
211
- border-radius: 50%;
212
- background: var(--pm-card);
213
- box-shadow: 0 7px 20px rgb(27 36 66 / 8%);
203
+ .add-symbol {
214
204
  color: var(--pm-text);
215
- font-size: 29px;
216
- font-weight: 350;
205
+ font-size: 44px;
206
+ font-weight: 300;
217
207
  line-height: 1;
218
208
  }
219
209
 
210
+ .complete-button {
211
+ justify-self: start;
212
+ min-width: 76px;
213
+ margin-top: 10px;
214
+ padding: 10px 20px;
215
+ border: 0;
216
+ border-radius: 12px;
217
+ background: var(--pm-primary);
218
+ color: #ffffff;
219
+ cursor: pointer;
220
+ font-weight: 780;
221
+ }
222
+
223
+ .complete-button:disabled {
224
+ cursor: not-allowed;
225
+ opacity: 0.5;
226
+ }
227
+
220
228
  .drop-help {
221
229
  position: absolute;
222
230
  width: 1px;
@@ -447,7 +455,7 @@
447
455
  aria-label="Agregar otro archivo"
448
456
  disabled
449
457
  >
450
- <span class="drop-icon" aria-hidden="true">+</span>
458
+ <span class="add-symbol" aria-hidden="true">+</span>
451
459
  </button>
452
460
  <button
453
461
  class="compatibility-control"
@@ -493,6 +501,15 @@
493
501
  aria-live="polite"
494
502
  ></p>
495
503
  </div>
504
+ <button
505
+ class="complete-button"
506
+ type="button"
507
+ data-testid="meta-upload-complete-btn"
508
+ hidden
509
+ disabled
510
+ >
511
+ OK
512
+ </button>
496
513
  </section>
497
514
  </article>
498
515
  </main>
@@ -548,6 +565,9 @@
548
565
  batchError: document.querySelector(
549
566
  '[data-testid="meta-upload-batch-error"]',
550
567
  ),
568
+ completeButton: document.querySelector(
569
+ '[data-testid="meta-upload-complete-btn"]',
570
+ ),
551
571
  fileList: document.querySelector(
552
572
  '[data-testid="meta-upload-file-list"]',
553
573
  ),
@@ -643,6 +663,38 @@
643
663
  scheduleIntrinsicSizeReport(true);
644
664
  }
645
665
 
666
+ function canMessageHost() {
667
+ return (
668
+ hasOpenAiFunction("sendFollowUpMessage") || state.hostCanMessage
669
+ );
670
+ }
671
+
672
+ function canCompleteUpload() {
673
+ return (
674
+ !state.disposed &&
675
+ Boolean(state.session) &&
676
+ !sessionExpired() &&
677
+ !state.busyBatch &&
678
+ !state.completionNotified &&
679
+ !state.completionNotificationInFlight &&
680
+ state.assets.length > 0 &&
681
+ state.items.length > 0 &&
682
+ state.assets.length === state.items.length &&
683
+ state.items.every((item) => item.status === "uploaded") &&
684
+ canMessageHost()
685
+ );
686
+ }
687
+
688
+ function renderCompletionAction() {
689
+ const hasVerifiedAssets = state.assets.length > 0;
690
+ elements.completeButton.hidden =
691
+ state.completionNotified || !hasVerifiedAssets || !canMessageHost();
692
+ elements.completeButton.disabled = !canCompleteUpload();
693
+ elements.completeButton.textContent =
694
+ state.completionNotificationInFlight ? "…" : "OK";
695
+ scheduleIntrinsicSizeReport(true);
696
+ }
697
+
646
698
  function parseFinitePositiveNumber(value) {
647
699
  return typeof value === "number" &&
648
700
  Number.isFinite(value) &&
@@ -861,6 +913,8 @@
861
913
  !state.session ||
862
914
  sessionExpired() ||
863
915
  state.uploadBlocked ||
916
+ state.completionNotified ||
917
+ state.completionNotificationInFlight ||
864
918
  state.busyBatch ||
865
919
  state.items.length >= safeSessionLimit();
866
920
  elements.input.disabled = disabled;
@@ -1081,6 +1135,17 @@
1081
1135
  const sessionChanged =
1082
1136
  !state.session || state.session.sessionId !== session.sessionId;
1083
1137
  if (sessionChanged) {
1138
+ abortActiveWork();
1139
+ for (const item of state.items) revokePreviewUrl(item);
1140
+ state.items = [];
1141
+ state.assets = [];
1142
+ state.consumedBridgeIds.clear();
1143
+ state.consumingBridgeFiles = false;
1144
+ state.bridgeConsumptionQueued = false;
1145
+ state.ingestionQueue = Promise.resolve();
1146
+ state.busyBatch = false;
1147
+ elements.input.value = "";
1148
+ renderItems();
1084
1149
  state.uploadBlocked = false;
1085
1150
  state.fallbackRequired = false;
1086
1151
  state.completionNotified = false;
@@ -1103,6 +1168,7 @@
1103
1168
  setSessionStatus("Sesión lista para recibir archivos.", "neutral");
1104
1169
  }
1105
1170
  setPickerAvailability();
1171
+ renderCompletionAction();
1106
1172
  void consumePendingBridgeFiles();
1107
1173
  scheduleIntrinsicSizeReport(true);
1108
1174
  }
@@ -1218,7 +1284,9 @@
1218
1284
  state.disposed ||
1219
1285
  !state.session ||
1220
1286
  sessionExpired() ||
1221
- state.uploadBlocked
1287
+ state.uploadBlocked ||
1288
+ state.completionNotified ||
1289
+ state.completionNotificationInFlight
1222
1290
  )
1223
1291
  return;
1224
1292
  state.workCancelled = false;
@@ -1237,7 +1305,9 @@
1237
1305
  state.disposed ||
1238
1306
  !state.session ||
1239
1307
  sessionExpired() ||
1240
- state.uploadBlocked
1308
+ state.uploadBlocked ||
1309
+ state.completionNotified ||
1310
+ state.completionNotificationInFlight
1241
1311
  )
1242
1312
  return;
1243
1313
  const bridge = openAiBridge();
@@ -1411,6 +1481,8 @@
1411
1481
  !state.session ||
1412
1482
  sessionExpired() ||
1413
1483
  state.uploadBlocked ||
1484
+ state.completionNotified ||
1485
+ state.completionNotificationInFlight ||
1414
1486
  state.busyBatch
1415
1487
  )
1416
1488
  return;
@@ -1430,7 +1502,10 @@
1430
1502
  if (!selected.length) return;
1431
1503
 
1432
1504
  state.busyBatch = true;
1505
+ state.assets = [];
1506
+ state.completionNotified = false;
1433
1507
  setPickerAvailability();
1508
+ renderCompletionAction();
1434
1509
  for (const file of selected) {
1435
1510
  const mediaKind = mediaKindForType(file.type) || "image";
1436
1511
  const item = createItem(file, mediaKind);
@@ -1440,6 +1515,7 @@
1440
1515
  }
1441
1516
  state.busyBatch = false;
1442
1517
  setPickerAvailability();
1518
+ renderCompletionAction();
1443
1519
  if (state.items.some((item) => item.status === "uploaded")) {
1444
1520
  await refreshSessionStatus();
1445
1521
  }
@@ -2085,6 +2161,7 @@
2085
2161
  fragment.append(card);
2086
2162
  });
2087
2163
  elements.fileList.replaceChildren(fragment);
2164
+ renderCompletionAction();
2088
2165
  scheduleIntrinsicSizeReport(true);
2089
2166
  }
2090
2167
 
@@ -2098,13 +2175,21 @@
2098
2175
 
2099
2176
  async function refreshSessionStatus() {
2100
2177
  if (!state.session) return;
2178
+ const requestedSessionId = state.session.sessionId;
2101
2179
  setSessionStatus("Comprobando el staging privado…", "neutral");
2102
2180
  try {
2103
2181
  const result = await callReadOnlyTool("check_meta_media_upload", {
2104
- sessionId: state.session.sessionId,
2182
+ sessionId: requestedSessionId,
2105
2183
  });
2106
- const status = parseStatusResult(result);
2184
+ if (
2185
+ state.disposed ||
2186
+ !state.session ||
2187
+ state.session.sessionId !== requestedSessionId
2188
+ )
2189
+ return;
2190
+ const status = parseStatusResult(result, requestedSessionId);
2107
2191
  state.assets = status.assets;
2192
+ renderCompletionAction();
2108
2193
  if (!status.assets.length) {
2109
2194
  if (status.legacyKindAmbiguous) {
2110
2195
  requireFallback(
@@ -2138,7 +2223,6 @@
2138
2223
  : "Carga completada.",
2139
2224
  partial ? "warning" : "success",
2140
2225
  );
2141
- await notifyChatUploadComplete();
2142
2226
  } catch (error) {
2143
2227
  const expired =
2144
2228
  sessionExpired() ||
@@ -2200,8 +2284,10 @@
2200
2284
  return parsed;
2201
2285
  }
2202
2286
 
2203
- function parseStatusResult(result) {
2287
+ function parseStatusResult(result, expectedSessionId) {
2204
2288
  const payload = resultPayload(result);
2289
+ if (payload.sessionId !== expectedSessionId)
2290
+ throw new Error("status_session_mismatch");
2205
2291
  const status = payload.status;
2206
2292
  if (!["pending", "uploaded"].includes(status))
2207
2293
  throw new Error("invalid_status");
@@ -2253,38 +2339,77 @@
2253
2339
  !state.session
2254
2340
  )
2255
2341
  return;
2342
+ const completionSessionId = state.session.sessionId;
2256
2343
  state.completionNotificationInFlight = true;
2257
- const message =
2258
- "Terminé de subir los archivos. Verifica esta misma sesión de carga y continúa en el chat: muéstrame las opciones disponibles y pregúntame por el portafolio, la cuenta publicitaria y la página de Meta. No publiques nada todavía.";
2344
+ setPickerAvailability();
2345
+ renderCompletionAction();
2346
+ const assets = state.assets
2347
+ .map(
2348
+ (asset) => `${asset.mediaKind} asset_id ${asset.stagingAssetId}`,
2349
+ )
2350
+ .join(", ");
2351
+ const message = `The ProductMaker upload is complete. Continue my previous request using these uploaded assets: ${assets}. Do not ask me to upload them again. Do not publish anything until you have asked me for the Meta portfolio, ad account, and Facebook Page.`;
2259
2352
  try {
2260
2353
  const bridge = openAiBridge();
2261
2354
  if (bridge && typeof bridge.sendFollowUpMessage === "function") {
2262
2355
  await bridge.sendFollowUpMessage({ prompt: message });
2356
+ if (
2357
+ !state.session ||
2358
+ state.session.sessionId !== completionSessionId
2359
+ )
2360
+ return;
2263
2361
  state.completionNotified = true;
2362
+ setPickerAvailability();
2363
+ renderCompletionAction();
2264
2364
  return;
2265
2365
  }
2266
2366
  if (state.hostCanMessage) {
2267
- await rpcRequest("ui/message", {
2367
+ const messageResult = await rpcRequest("ui/message", {
2268
2368
  role: "user",
2269
2369
  content: [{ type: "text", text: message }],
2270
2370
  });
2371
+ if (isRecord(messageResult) && messageResult.isError === true)
2372
+ throw new Error("host_message_rejected");
2373
+ if (
2374
+ !state.session ||
2375
+ state.session.sessionId !== completionSessionId
2376
+ )
2377
+ return;
2271
2378
  state.completionNotified = true;
2379
+ setPickerAvailability();
2380
+ renderCompletionAction();
2272
2381
  return;
2273
2382
  }
2274
2383
  setSessionStatus(
2275
- "Carga completada. Escribe “listo” en el chat para continuar.",
2276
- "success",
2384
+ "La carga terminó, pero este host no puede continuar el chat desde el widget. Vuelve al chat para seguir.",
2385
+ "warning",
2277
2386
  );
2278
2387
  } catch {
2388
+ if (
2389
+ !state.session ||
2390
+ state.session.sessionId !== completionSessionId
2391
+ )
2392
+ return;
2279
2393
  setSessionStatus(
2280
- "Carga completada. Escribe “listo” en el chat para continuar.",
2281
- "success",
2394
+ "No pudimos continuar el chat. Intenta nuevamente.",
2395
+ "danger",
2282
2396
  );
2283
2397
  } finally {
2284
- state.completionNotificationInFlight = false;
2398
+ if (
2399
+ state.session &&
2400
+ state.session.sessionId === completionSessionId
2401
+ ) {
2402
+ state.completionNotificationInFlight = false;
2403
+ setPickerAvailability();
2404
+ renderCompletionAction();
2405
+ }
2285
2406
  }
2286
2407
  }
2287
2408
 
2409
+ elements.completeButton.addEventListener("click", () => {
2410
+ if (canCompleteUpload()) void notifyChatUploadComplete();
2411
+ });
2412
+
2288
2413
  document.addEventListener("DOMContentLoaded", initialize, {
2289
2414
  once: true,
2290
2415
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "2.9.3",
3
+ "version": "2.9.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.",