@productmaker/mcp 2.10.1 → 3.0.2

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.
@@ -147,6 +147,13 @@
147
147
  padding: 18px;
148
148
  }
149
149
 
150
+ .upload-prompt {
151
+ margin: 0 0 12px;
152
+ color: var(--pm-text);
153
+ font-size: 15px;
154
+ font-weight: 700;
155
+ }
156
+
150
157
  .media-grid {
151
158
  display: grid;
152
159
  grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
@@ -435,6 +442,9 @@
435
442
  <main class="app" data-testid="meta-upload-root">
436
443
  <article class="card">
437
444
  <section class="section" aria-label="Upload media">
445
+ <p class="upload-prompt" data-testid="meta-upload-prompt">
446
+ Sube tus archivos
447
+ </p>
438
448
  <div class="media-grid">
439
449
  <div
440
450
  class="file-list"
@@ -535,7 +545,6 @@
535
545
  const MAX_FILES = 10;
536
546
  const MAX_FILE_SIZE_BYTES = 350 * 1024 * 1024;
537
547
  const MAX_VIDEO_DURATION_SECONDS = 180;
538
- const MIN_DIMENSION = 600;
539
548
  const IMAGE_COMPRESSION_THRESHOLD_BYTES = 1.8 * 1024 * 1024;
540
549
  const IMAGE_COMPRESSION_MAX_BYTES = 1.8 * 1024 * 1024;
541
550
  const IMAGE_MAX_DIMENSION = 2048;
@@ -543,6 +552,7 @@
543
552
  const TOOL_RESULT_TEXT_LIMIT = 100000;
544
553
  const RPC_TIMEOUT_MS = 15000;
545
554
  const APP_PROTOCOL_VERSION = "2026-01-26";
555
+ const CONFIGURED_RESOLVE_ENDPOINT = "__PRODUCTMAKER_RESOLVE_ENDPOINT__";
546
556
 
547
557
  const elements = {
548
558
  root: document.querySelector('[data-testid="meta-upload-root"]'),
@@ -568,6 +578,9 @@
568
578
  completeButton: document.querySelector(
569
579
  '[data-testid="meta-upload-complete-btn"]',
570
580
  ),
581
+ uploadPrompt: document.querySelector(
582
+ '[data-testid="meta-upload-prompt"]',
583
+ ),
571
584
  fileList: document.querySelector(
572
585
  '[data-testid="meta-upload-file-list"]',
573
586
  ),
@@ -1156,6 +1169,7 @@
1156
1169
  }
1157
1170
  }
1158
1171
  state.session = session;
1172
+ elements.uploadPrompt.textContent = uploadPromptForSession(session);
1159
1173
  elements.fallbackLink.href = session.uploadPageUrl;
1160
1174
  if (sessionExpired()) {
1161
1175
  requireFallback(
@@ -1178,23 +1192,22 @@
1178
1192
  const uploadPageUrl = boundedString(candidate.uploadPageUrl, 2000);
1179
1193
  const mediaKind = candidate.mediaKind;
1180
1194
  const assetRole = candidate.assetRole;
1195
+ const uploadPurpose = candidate.uploadPurpose;
1181
1196
  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";
1197
+ const checkToolName = "check_media_upload";
1188
1198
  const resolveEndpoint =
1189
1199
  boundedString(candidate.resolveEndpoint, 200) ||
1190
- "/api/meta-upload/resolve";
1200
+ "/api/upload-media/resolve";
1191
1201
  if (!sessionId || !["image", "video", "mixed"].includes(mediaKind))
1192
1202
  return null;
1193
1203
  if (!["media", "thumbnail"].includes(assetRole)) return null;
1194
1204
  if (!Number.isInteger(candidate.maxFiles) || !expiresAt) return null;
1195
1205
  try {
1196
1206
  const parsedUrl = new URL(uploadPageUrl);
1197
- if (!safeUploadPageUrl(parsedUrl)) return null;
1207
+ const parsedResolveUrl = new URL(resolveEndpoint, parsedUrl.origin);
1208
+ if (!safeResolveEndpointUrl(parsedResolveUrl, parsedUrl))
1209
+ return null;
1210
+ if (!safeUploadPageUrl(parsedUrl, parsedResolveUrl)) return null;
1198
1211
  } catch {
1199
1212
  return null;
1200
1213
  }
@@ -1203,6 +1216,11 @@
1203
1216
  uploadPageUrl,
1204
1217
  mediaKind,
1205
1218
  assetRole,
1219
+ uploadPurpose:
1220
+ uploadPurpose === "product_reference" ||
1221
+ uploadPurpose === "creative"
1222
+ ? uploadPurpose
1223
+ : "generic",
1206
1224
  maxFiles: candidate.maxFiles,
1207
1225
  expiresAt,
1208
1226
  checkToolName,
@@ -1210,6 +1228,26 @@
1210
1228
  };
1211
1229
  }
1212
1230
 
1231
+ function uploadPromptForSession(session) {
1232
+ if (session.uploadPurpose === "product_reference") {
1233
+ return session.mediaKind === "image"
1234
+ ? "Sube la foto del producto"
1235
+ : session.mediaKind === "video"
1236
+ ? "Sube el video del producto"
1237
+ : "Sube los archivos del producto";
1238
+ }
1239
+ if (session.uploadPurpose === "creative") {
1240
+ return session.mediaKind === "video"
1241
+ ? "Sube tu video creativo"
1242
+ : "Sube tus creativos";
1243
+ }
1244
+ return session.mediaKind === "image"
1245
+ ? "Sube tus imágenes"
1246
+ : session.mediaKind === "video"
1247
+ ? "Sube tu video"
1248
+ : "Sube tus archivos";
1249
+ }
1250
+
1213
1251
  function mergeToolInput(input) {
1214
1252
  state.toolInput = { ...state.toolInput, ...input };
1215
1253
  void consumePendingBridgeFiles();
@@ -1480,7 +1518,7 @@
1480
1518
  "warning",
1481
1519
  );
1482
1520
  renderItems();
1483
- emitAnalytics("ads_meta_remote_upload_error", mediaKind, {
1521
+ emitAnalytics("mcp_media_upload_error", mediaKind, {
1484
1522
  failureStage: "bridge",
1485
1523
  errorType: "bridge_download_failed",
1486
1524
  });
@@ -1564,18 +1602,18 @@
1564
1602
  inspection.mediaKind === "image"
1565
1603
  ? await compressImage(item.file, inspection)
1566
1604
  : item.file;
1567
- emitAnalytics("ads_meta_remote_upload_started", item.mediaKind);
1605
+ emitAnalytics("mcp_media_upload_started", item.mediaKind);
1568
1606
  await uploadWithRetries(item);
1569
1607
  item.status = "uploaded";
1570
1608
  item.progress = 100;
1571
1609
  item.error = "";
1572
- emitAnalytics("ads_meta_remote_upload_completed", item.mediaKind);
1610
+ emitAnalytics("mcp_media_upload_completed", item.mediaKind);
1573
1611
  } catch (error) {
1574
1612
  const failure = safeFailure(error);
1575
1613
  item.status = "failed";
1576
1614
  item.error = failure.message;
1577
1615
  item.failureStage = failure.stage;
1578
- emitAnalytics("ads_meta_remote_upload_error", item.mediaKind, {
1616
+ emitAnalytics("mcp_media_upload_error", item.mediaKind, {
1579
1617
  failureStage: failure.stage,
1580
1618
  errorType: failure.code,
1581
1619
  });
@@ -1635,15 +1673,6 @@
1635
1673
  mediaKind === "image"
1636
1674
  ? await inspectImage(file)
1637
1675
  : await inspectVideo(file);
1638
- if (
1639
- inspection.width < MIN_DIMENSION ||
1640
- inspection.height < MIN_DIMENSION
1641
- ) {
1642
- throw validationError(
1643
- "low_resolution",
1644
- "La resolución mínima es 600 × 600 px.",
1645
- );
1646
- }
1647
1676
  if (
1648
1677
  mediaKind === "video" &&
1649
1678
  inspection.durationSeconds > MAX_VIDEO_DURATION_SECONDS
@@ -1752,12 +1781,6 @@
1752
1781
  const canvas = document.createElement("canvas");
1753
1782
  canvas.width = Math.max(1, Math.round(inspection.width * scale));
1754
1783
  canvas.height = Math.max(1, Math.round(inspection.height * scale));
1755
- if (canvas.width < MIN_DIMENSION || canvas.height < MIN_DIMENSION) {
1756
- throw validationError(
1757
- "low_resolution",
1758
- "La resolución mínima final es 600 × 600 px.",
1759
- );
1760
- }
1761
1784
  const context = canvas.getContext("2d", {
1762
1785
  alpha: file.type !== "image/jpeg",
1763
1786
  });
@@ -1853,17 +1876,28 @@
1853
1876
  }
1854
1877
  const error = new Error("upload_failed");
1855
1878
  error.safeCode = "upload_failed";
1856
- error.safeMessage =
1857
- "La carga falló tras 3 intentos. Puedes reintentarla.";
1858
1879
  error.safeStage =
1859
1880
  lastError && lastError.safeStage ? lastError.safeStage : "upload";
1881
+ error.safeMessage =
1882
+ error.safeStage === "resolve"
1883
+ ? "No pudimos preparar la carga. Reintenta o abre la carga segura."
1884
+ : error.safeStage === "put"
1885
+ ? "No pudimos enviar el archivo al almacenamiento. Reintenta o abre la carga segura."
1886
+ : "La carga falló tras 3 intentos. Puedes reintentarla.";
1860
1887
  throw error;
1861
1888
  }
1862
1889
 
1863
1890
  async function resolveUpload(file, uploadId) {
1864
1891
  const uploadPage = new URL(state.session.uploadPageUrl);
1865
- if (!safeUploadPageUrl(uploadPage))
1866
- throw new Error("unsafe_upload_page");
1892
+ const resolveUrl = new URL(
1893
+ state.session.resolveEndpoint,
1894
+ uploadPage.origin,
1895
+ );
1896
+ if (
1897
+ !safeResolveEndpointUrl(resolveUrl, uploadPage) ||
1898
+ !safeUploadPageUrl(uploadPage, resolveUrl)
1899
+ )
1900
+ throw new Error("unsafe_upload_session");
1867
1901
  const token =
1868
1902
  uploadPage.searchParams.get("token") ||
1869
1903
  new URLSearchParams(uploadPage.hash.slice(1)).get("token");
@@ -1872,10 +1906,6 @@
1872
1906
  error.safeStage = "resolve";
1873
1907
  throw error;
1874
1908
  }
1875
- const resolveUrl = new URL(
1876
- state.session.resolveEndpoint,
1877
- uploadPage.origin,
1878
- );
1879
1909
  const response = await fetch(resolveUrl.toString(), {
1880
1910
  method: "POST",
1881
1911
  headers: { "content-type": "application/json" },
@@ -1887,6 +1917,7 @@
1887
1917
  fileSize: file.size,
1888
1918
  }),
1889
1919
  credentials: "omit",
1920
+ redirect: "error",
1890
1921
  signal: activeAbortSignal(),
1891
1922
  });
1892
1923
  if (!response.ok) {
@@ -1901,10 +1932,7 @@
1901
1932
  const body = await response.json();
1902
1933
  if (!isRecord(body)) throw new Error("invalid_resolve_response");
1903
1934
  const uploadUrl = boundedString(body.uploadUrl, 3000);
1904
- const assetId = boundedString(
1905
- body.assetId || body.stagingAssetId,
1906
- 300,
1907
- );
1935
+ const assetId = boundedString(body.assetId, 300);
1908
1936
  const parsedUploadUrl = new URL(uploadUrl);
1909
1937
  if (!safeWebProtocol(parsedUploadUrl) || !assetId) {
1910
1938
  const error = new Error("invalid_resolve_response");
@@ -1955,7 +1983,7 @@
1955
1983
  );
1956
1984
  }
1957
1985
 
1958
- function safeUploadPageUrl(url) {
1986
+ function safeUploadPageUrl(url, resolveUrl) {
1959
1987
  if (url.username || url.password) return false;
1960
1988
  if (
1961
1989
  [
@@ -1965,11 +1993,38 @@
1965
1993
  )
1966
1994
  return true;
1967
1995
  return (
1968
- url.protocol === "http:" &&
1969
- ["localhost", "127.0.0.1", "::1"].includes(url.hostname)
1996
+ (url.protocol === "http:" &&
1997
+ ["localhost", "127.0.0.1", "::1"].includes(url.hostname)) ||
1998
+ (url.protocol === "https:" &&
1999
+ /^\/(?:[a-z]{2}\/)?upload-media\/?$/.test(url.pathname) &&
2000
+ isConfiguredResolveEndpoint(resolveUrl))
2001
+ );
2002
+ }
2003
+
2004
+ function isConfiguredResolveEndpoint(url) {
2005
+ return (
2006
+ /^https?:\/\//.test(CONFIGURED_RESOLVE_ENDPOINT) &&
2007
+ url.toString() === CONFIGURED_RESOLVE_ENDPOINT
1970
2008
  );
1971
2009
  }
1972
2010
 
2011
+ function safeResolveEndpointUrl(url, uploadPageUrl) {
2012
+ if (url.username || url.password) return false;
2013
+ if (url.search || url.hash) return false;
2014
+ const officialApi =
2015
+ url.origin === "https://api.productmaker.app" &&
2016
+ url.pathname === "/v1/uploaded-assets/sessions/resolve";
2017
+ const sameOriginWebProxy =
2018
+ url.origin === uploadPageUrl.origin &&
2019
+ url.pathname === "/api/upload-media/resolve";
2020
+ const localApi =
2021
+ url.protocol === "http:" &&
2022
+ ["localhost", "127.0.0.1", "::1"].includes(url.hostname) &&
2023
+ url.pathname === "/v1/uploaded-assets/sessions/resolve";
2024
+ const configuredApi = isConfiguredResolveEndpoint(url);
2025
+ return officialApi || sameOriginWebProxy || localApi || configuredApi;
2026
+ }
2027
+
1973
2028
  function putFileWithProgress(url, file, onProgress) {
1974
2029
  return new Promise((resolve, reject) => {
1975
2030
  const request = new XMLHttpRequest();
@@ -2203,21 +2258,14 @@
2203
2258
  state.session.sessionId !== requestedSessionId
2204
2259
  )
2205
2260
  return;
2206
- const status = parseStatusResult(result, requestedSessionId);
2261
+ const status = parseStatusResult(result);
2207
2262
  state.assets = status.assets;
2208
2263
  renderCompletionAction();
2209
2264
  if (!status.assets.length) {
2210
- if (status.legacyKindAmbiguous) {
2211
- requireFallback(
2212
- "El tipo del asset legacy es ambiguo. Vuelve a comprobar o usa el enlace seguro.",
2213
- "warning",
2214
- );
2215
- } else {
2216
- requireFallback(
2217
- "La sesión sigue pendiente. Continúa desde el enlace seguro para terminar la carga.",
2218
- "warning",
2219
- );
2220
- }
2265
+ requireFallback(
2266
+ "La sesión sigue pendiente. Continúa desde el enlace seguro para terminar la carga.",
2267
+ "warning",
2268
+ );
2221
2269
  return;
2222
2270
  }
2223
2271
 
@@ -2255,9 +2303,7 @@
2255
2303
  }
2256
2304
 
2257
2305
  async function callReadOnlyTool(name, args) {
2258
- if (
2259
- !["check_media_upload", "check_meta_media_upload"].includes(name)
2260
- ) {
2306
+ if (name !== "check_media_upload") {
2261
2307
  throw new Error("tool_not_allowed");
2262
2308
  }
2263
2309
  const bridge = openAiBridge();
@@ -2302,58 +2348,22 @@
2302
2348
  return parsed;
2303
2349
  }
2304
2350
 
2305
- function parseStatusResult(result, expectedSessionId) {
2351
+ function parseStatusResult(result) {
2306
2352
  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");
2312
2353
  const status = payload.status;
2313
2354
  if (!["pending", "uploaded"].includes(status))
2314
2355
  throw new Error("invalid_status");
2315
2356
  const assets = Array.isArray(payload.assets)
2316
2357
  ? payload.assets.slice(0, MAX_FILES).flatMap((asset) => {
2317
2358
  if (!isRecord(asset)) return [];
2318
- const assetId = boundedString(
2319
- asset.assetId || asset.stagingAssetId,
2320
- 300,
2321
- );
2359
+ const assetId = boundedString(asset.assetId, 300);
2322
2360
  const mediaKind = asset.mediaKind;
2323
2361
  return assetId && ["image", "video"].includes(mediaKind)
2324
2362
  ? [{ assetId, mediaKind }]
2325
2363
  : [];
2326
2364
  })
2327
2365
  : [];
2328
- let legacyKindAmbiguous = false;
2329
- const legacyStagingAssetId = boundedString(
2330
- payload.stagingAssetId,
2331
- 300,
2332
- );
2333
- if (!assets.length && legacyStagingAssetId) {
2334
- const localKinds = new Set(
2335
- state.items
2336
- .filter((item) => item.status === "uploaded")
2337
- .map((item) => item.mediaKind)
2338
- .filter((mediaKind) => ["image", "video"].includes(mediaKind)),
2339
- );
2340
- const inferredMediaKind =
2341
- localKinds.size === 1
2342
- ? Array.from(localKinds)[0]
2343
- : localKinds.size === 0 &&
2344
- ["image", "video"].includes(state.session.mediaKind)
2345
- ? state.session.mediaKind
2346
- : null;
2347
- if (inferredMediaKind) {
2348
- assets.push({
2349
- assetId: legacyStagingAssetId,
2350
- mediaKind: inferredMediaKind,
2351
- });
2352
- } else {
2353
- legacyKindAmbiguous = true;
2354
- }
2355
- }
2356
- return { status, assets, legacyKindAmbiguous };
2366
+ return { status, assets };
2357
2367
  }
2358
2368
 
2359
2369
  async function notifyChatUploadComplete() {
@@ -2370,11 +2380,7 @@
2370
2380
  const assets = state.assets
2371
2381
  .map((asset) => `${asset.mediaKind} asset_id ${asset.assetId}`)
2372
2382
  .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}`;
2383
+ const message = `The ProductMaker upload is complete. Continue my previous request using these uploaded assets: ${assets}. Do not ask me to upload them again. Ask only for the remaining inputs required by my original request.`;
2378
2384
  try {
2379
2385
  const bridge = openAiBridge();
2380
2386
  if (bridge && typeof bridge.sendFollowUpMessage === "function") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "2.10.1",
3
+ "version": "3.0.2",
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.",