@productmaker/mcp 2.9.3 → 2.10.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.
@@ -4,7 +4,7 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <meta name="color-scheme" content="light dark" />
7
- <title>Carga segura para Meta · ProductMaker</title>
7
+ <title>Upload media · ProductMaker</title>
8
8
  <style>
9
9
  :root {
10
10
  color-scheme: light dark;
@@ -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;
@@ -957,7 +1011,7 @@
957
1011
  async function initializeStandardHost() {
958
1012
  try {
959
1013
  const result = await rpcRequest("ui/initialize", {
960
- appInfo: { name: "ProductMaker Meta upload", version: "1.0.0" },
1014
+ appInfo: { name: "ProductMaker media upload", version: "1.0.0" },
961
1015
  appCapabilities: {
962
1016
  availableDisplayModes: ["inline", "fullscreen"],
963
1017
  },
@@ -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
  }
@@ -1113,6 +1179,15 @@
1113
1179
  const mediaKind = candidate.mediaKind;
1114
1180
  const assetRole = candidate.assetRole;
1115
1181
  const expiresAt = boundedString(candidate.expiresAt, 100);
1182
+ const checkToolName = [
1183
+ "check_media_upload",
1184
+ "check_meta_media_upload",
1185
+ ].includes(candidate.checkToolName)
1186
+ ? candidate.checkToolName
1187
+ : "check_meta_media_upload";
1188
+ const resolveEndpoint =
1189
+ boundedString(candidate.resolveEndpoint, 200) ||
1190
+ "/api/meta-upload/resolve";
1116
1191
  if (!sessionId || !["image", "video", "mixed"].includes(mediaKind))
1117
1192
  return null;
1118
1193
  if (!["media", "thumbnail"].includes(assetRole)) return null;
@@ -1130,6 +1205,8 @@
1130
1205
  assetRole,
1131
1206
  maxFiles: candidate.maxFiles,
1132
1207
  expiresAt,
1208
+ checkToolName,
1209
+ resolveEndpoint,
1133
1210
  };
1134
1211
  }
1135
1212
 
@@ -1218,7 +1295,9 @@
1218
1295
  state.disposed ||
1219
1296
  !state.session ||
1220
1297
  sessionExpired() ||
1221
- state.uploadBlocked
1298
+ state.uploadBlocked ||
1299
+ state.completionNotified ||
1300
+ state.completionNotificationInFlight
1222
1301
  )
1223
1302
  return;
1224
1303
  state.workCancelled = false;
@@ -1237,7 +1316,9 @@
1237
1316
  state.disposed ||
1238
1317
  !state.session ||
1239
1318
  sessionExpired() ||
1240
- state.uploadBlocked
1319
+ state.uploadBlocked ||
1320
+ state.completionNotified ||
1321
+ state.completionNotificationInFlight
1241
1322
  )
1242
1323
  return;
1243
1324
  const bridge = openAiBridge();
@@ -1411,6 +1492,8 @@
1411
1492
  !state.session ||
1412
1493
  sessionExpired() ||
1413
1494
  state.uploadBlocked ||
1495
+ state.completionNotified ||
1496
+ state.completionNotificationInFlight ||
1414
1497
  state.busyBatch
1415
1498
  )
1416
1499
  return;
@@ -1430,7 +1513,10 @@
1430
1513
  if (!selected.length) return;
1431
1514
 
1432
1515
  state.busyBatch = true;
1516
+ state.assets = [];
1517
+ state.completionNotified = false;
1433
1518
  setPickerAvailability();
1519
+ renderCompletionAction();
1434
1520
  for (const file of selected) {
1435
1521
  const mediaKind = mediaKindForType(file.type) || "image";
1436
1522
  const item = createItem(file, mediaKind);
@@ -1440,6 +1526,7 @@
1440
1526
  }
1441
1527
  state.busyBatch = false;
1442
1528
  setPickerAvailability();
1529
+ renderCompletionAction();
1443
1530
  if (state.items.some((item) => item.status === "uploaded")) {
1444
1531
  await refreshSessionStatus();
1445
1532
  }
@@ -1458,7 +1545,7 @@
1458
1545
  attempts: 0,
1459
1546
  error: "",
1460
1547
  failureStage: "",
1461
- stagingAssetId: "",
1548
+ assetId: "",
1462
1549
  previewUrl: "",
1463
1550
  };
1464
1551
  }
@@ -1756,7 +1843,7 @@
1756
1843
  renderItems();
1757
1844
  },
1758
1845
  );
1759
- item.stagingAssetId = resolved.stagingAssetId;
1846
+ item.assetId = resolved.assetId;
1760
1847
  return;
1761
1848
  } catch (error) {
1762
1849
  if (error && error.message === "session_expired") throw error;
@@ -1777,14 +1864,16 @@
1777
1864
  const uploadPage = new URL(state.session.uploadPageUrl);
1778
1865
  if (!safeUploadPageUrl(uploadPage))
1779
1866
  throw new Error("unsafe_upload_page");
1780
- const token = uploadPage.searchParams.get("token");
1867
+ const token =
1868
+ uploadPage.searchParams.get("token") ||
1869
+ new URLSearchParams(uploadPage.hash.slice(1)).get("token");
1781
1870
  if (!token) {
1782
1871
  const error = new Error("missing_token");
1783
1872
  error.safeStage = "resolve";
1784
1873
  throw error;
1785
1874
  }
1786
1875
  const resolveUrl = new URL(
1787
- "/api/meta-upload/resolve",
1876
+ state.session.resolveEndpoint,
1788
1877
  uploadPage.origin,
1789
1878
  );
1790
1879
  const response = await fetch(resolveUrl.toString(), {
@@ -1812,14 +1901,17 @@
1812
1901
  const body = await response.json();
1813
1902
  if (!isRecord(body)) throw new Error("invalid_resolve_response");
1814
1903
  const uploadUrl = boundedString(body.uploadUrl, 3000);
1815
- const stagingAssetId = boundedString(body.stagingAssetId, 300);
1904
+ const assetId = boundedString(
1905
+ body.assetId || body.stagingAssetId,
1906
+ 300,
1907
+ );
1816
1908
  const parsedUploadUrl = new URL(uploadUrl);
1817
- if (!safeWebProtocol(parsedUploadUrl) || !stagingAssetId) {
1909
+ if (!safeWebProtocol(parsedUploadUrl) || !assetId) {
1818
1910
  const error = new Error("invalid_resolve_response");
1819
1911
  error.safeStage = "resolve";
1820
1912
  throw error;
1821
1913
  }
1822
- return { uploadUrl: parsedUploadUrl.toString(), stagingAssetId };
1914
+ return { uploadUrl: parsedUploadUrl.toString(), assetId };
1823
1915
  }
1824
1916
 
1825
1917
  function createClientUuid() {
@@ -2085,6 +2177,7 @@
2085
2177
  fragment.append(card);
2086
2178
  });
2087
2179
  elements.fileList.replaceChildren(fragment);
2180
+ renderCompletionAction();
2088
2181
  scheduleIntrinsicSizeReport(true);
2089
2182
  }
2090
2183
 
@@ -2098,13 +2191,21 @@
2098
2191
 
2099
2192
  async function refreshSessionStatus() {
2100
2193
  if (!state.session) return;
2194
+ const requestedSessionId = state.session.sessionId;
2101
2195
  setSessionStatus("Comprobando el staging privado…", "neutral");
2102
2196
  try {
2103
- const result = await callReadOnlyTool("check_meta_media_upload", {
2104
- sessionId: state.session.sessionId,
2197
+ const result = await callReadOnlyTool(state.session.checkToolName, {
2198
+ sessionId: requestedSessionId,
2105
2199
  });
2106
- const status = parseStatusResult(result);
2200
+ if (
2201
+ state.disposed ||
2202
+ !state.session ||
2203
+ state.session.sessionId !== requestedSessionId
2204
+ )
2205
+ return;
2206
+ const status = parseStatusResult(result, requestedSessionId);
2107
2207
  state.assets = status.assets;
2208
+ renderCompletionAction();
2108
2209
  if (!status.assets.length) {
2109
2210
  if (status.legacyKindAmbiguous) {
2110
2211
  requireFallback(
@@ -2138,7 +2239,6 @@
2138
2239
  : "Carga completada.",
2139
2240
  partial ? "warning" : "success",
2140
2241
  );
2141
- await notifyChatUploadComplete();
2142
2242
  } catch (error) {
2143
2243
  const expired =
2144
2244
  sessionExpired() ||
@@ -2155,7 +2255,9 @@
2155
2255
  }
2156
2256
 
2157
2257
  async function callReadOnlyTool(name, args) {
2158
- if (name !== "check_meta_media_upload") {
2258
+ if (
2259
+ !["check_media_upload", "check_meta_media_upload"].includes(name)
2260
+ ) {
2159
2261
  throw new Error("tool_not_allowed");
2160
2262
  }
2161
2263
  const bridge = openAiBridge();
@@ -2200,18 +2302,26 @@
2200
2302
  return parsed;
2201
2303
  }
2202
2304
 
2203
- function parseStatusResult(result) {
2305
+ function parseStatusResult(result, expectedSessionId) {
2204
2306
  const payload = resultPayload(result);
2307
+ if (
2308
+ state.session.checkToolName === "check_meta_media_upload" &&
2309
+ payload.sessionId !== expectedSessionId
2310
+ )
2311
+ throw new Error("status_session_mismatch");
2205
2312
  const status = payload.status;
2206
2313
  if (!["pending", "uploaded"].includes(status))
2207
2314
  throw new Error("invalid_status");
2208
2315
  const assets = Array.isArray(payload.assets)
2209
2316
  ? payload.assets.slice(0, MAX_FILES).flatMap((asset) => {
2210
2317
  if (!isRecord(asset)) return [];
2211
- const stagingAssetId = boundedString(asset.stagingAssetId, 300);
2318
+ const assetId = boundedString(
2319
+ asset.assetId || asset.stagingAssetId,
2320
+ 300,
2321
+ );
2212
2322
  const mediaKind = asset.mediaKind;
2213
- return stagingAssetId && ["image", "video"].includes(mediaKind)
2214
- ? [{ stagingAssetId, mediaKind }]
2323
+ return assetId && ["image", "video"].includes(mediaKind)
2324
+ ? [{ assetId, mediaKind }]
2215
2325
  : [];
2216
2326
  })
2217
2327
  : [];
@@ -2236,7 +2346,7 @@
2236
2346
  : null;
2237
2347
  if (inferredMediaKind) {
2238
2348
  assets.push({
2239
- stagingAssetId: legacyStagingAssetId,
2349
+ assetId: legacyStagingAssetId,
2240
2350
  mediaKind: inferredMediaKind,
2241
2351
  });
2242
2352
  } else {
@@ -2253,38 +2363,79 @@
2253
2363
  !state.session
2254
2364
  )
2255
2365
  return;
2366
+ const completionSessionId = state.session.sessionId;
2256
2367
  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.";
2368
+ setPickerAvailability();
2369
+ renderCompletionAction();
2370
+ const assets = state.assets
2371
+ .map((asset) => `${asset.mediaKind} asset_id ${asset.assetId}`)
2372
+ .join(", ");
2373
+ const metaGuard =
2374
+ state.session.checkToolName === "check_meta_media_upload"
2375
+ ? " Do not publish anything until you have asked me for the Meta portfolio, ad account, and Facebook Page."
2376
+ : "";
2377
+ const message = `The ProductMaker upload is complete. Continue my previous request using these uploaded assets: ${assets}. Do not ask me to upload them again.${metaGuard}`;
2259
2378
  try {
2260
2379
  const bridge = openAiBridge();
2261
2380
  if (bridge && typeof bridge.sendFollowUpMessage === "function") {
2262
2381
  await bridge.sendFollowUpMessage({ prompt: message });
2382
+ if (
2383
+ !state.session ||
2384
+ state.session.sessionId !== completionSessionId
2385
+ )
2386
+ return;
2263
2387
  state.completionNotified = true;
2388
+ setPickerAvailability();
2389
+ renderCompletionAction();
2264
2390
  return;
2265
2391
  }
2266
2392
  if (state.hostCanMessage) {
2267
- await rpcRequest("ui/message", {
2393
+ const messageResult = await rpcRequest("ui/message", {
2268
2394
  role: "user",
2269
2395
  content: [{ type: "text", text: message }],
2270
2396
  });
2397
+ if (isRecord(messageResult) && messageResult.isError === true)
2398
+ throw new Error("host_message_rejected");
2399
+ if (
2400
+ !state.session ||
2401
+ state.session.sessionId !== completionSessionId
2402
+ )
2403
+ return;
2271
2404
  state.completionNotified = true;
2405
+ setPickerAvailability();
2406
+ renderCompletionAction();
2272
2407
  return;
2273
2408
  }
2274
2409
  setSessionStatus(
2275
- "Carga completada. Escribe “listo” en el chat para continuar.",
2276
- "success",
2410
+ "La carga terminó, pero este host no puede continuar el chat desde el widget. Vuelve al chat para seguir.",
2411
+ "warning",
2277
2412
  );
2278
2413
  } catch {
2414
+ if (
2415
+ !state.session ||
2416
+ state.session.sessionId !== completionSessionId
2417
+ )
2418
+ return;
2279
2419
  setSessionStatus(
2280
- "Carga completada. Escribe “listo” en el chat para continuar.",
2281
- "success",
2420
+ "No pudimos continuar el chat. Intenta nuevamente.",
2421
+ "danger",
2282
2422
  );
2283
2423
  } finally {
2284
- state.completionNotificationInFlight = false;
2424
+ if (
2425
+ state.session &&
2426
+ state.session.sessionId === completionSessionId
2427
+ ) {
2428
+ state.completionNotificationInFlight = false;
2429
+ setPickerAvailability();
2430
+ renderCompletionAction();
2431
+ }
2285
2432
  }
2286
2433
  }
2287
2434
 
2435
+ elements.completeButton.addEventListener("click", () => {
2436
+ if (canCompleteUpload()) void notifyChatUploadComplete();
2437
+ });
2438
+
2288
2439
  document.addEventListener("DOMContentLoaded", initialize, {
2289
2440
  once: true,
2290
2441
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "2.9.3",
3
+ "version": "2.10.0",
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.",