@productmaker/mcp 3.0.0 → 3.1.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.
@@ -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,6 +1192,7 @@
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
1197
  const checkToolName = "check_media_upload";
1183
1198
  const resolveEndpoint =
@@ -1189,7 +1204,10 @@
1189
1204
  if (!Number.isInteger(candidate.maxFiles) || !expiresAt) return null;
1190
1205
  try {
1191
1206
  const parsedUrl = new URL(uploadPageUrl);
1192
- 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;
1193
1211
  } catch {
1194
1212
  return null;
1195
1213
  }
@@ -1198,6 +1216,11 @@
1198
1216
  uploadPageUrl,
1199
1217
  mediaKind,
1200
1218
  assetRole,
1219
+ uploadPurpose:
1220
+ uploadPurpose === "product_reference" ||
1221
+ uploadPurpose === "creative"
1222
+ ? uploadPurpose
1223
+ : "generic",
1201
1224
  maxFiles: candidate.maxFiles,
1202
1225
  expiresAt,
1203
1226
  checkToolName,
@@ -1205,6 +1228,26 @@
1205
1228
  };
1206
1229
  }
1207
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
+
1208
1251
  function mergeToolInput(input) {
1209
1252
  state.toolInput = { ...state.toolInput, ...input };
1210
1253
  void consumePendingBridgeFiles();
@@ -1630,15 +1673,6 @@
1630
1673
  mediaKind === "image"
1631
1674
  ? await inspectImage(file)
1632
1675
  : await inspectVideo(file);
1633
- if (
1634
- inspection.width < MIN_DIMENSION ||
1635
- inspection.height < MIN_DIMENSION
1636
- ) {
1637
- throw validationError(
1638
- "low_resolution",
1639
- "La resolución mínima es 600 × 600 px.",
1640
- );
1641
- }
1642
1676
  if (
1643
1677
  mediaKind === "video" &&
1644
1678
  inspection.durationSeconds > MAX_VIDEO_DURATION_SECONDS
@@ -1747,12 +1781,6 @@
1747
1781
  const canvas = document.createElement("canvas");
1748
1782
  canvas.width = Math.max(1, Math.round(inspection.width * scale));
1749
1783
  canvas.height = Math.max(1, Math.round(inspection.height * scale));
1750
- if (canvas.width < MIN_DIMENSION || canvas.height < MIN_DIMENSION) {
1751
- throw validationError(
1752
- "low_resolution",
1753
- "La resolución mínima final es 600 × 600 px.",
1754
- );
1755
- }
1756
1784
  const context = canvas.getContext("2d", {
1757
1785
  alpha: file.type !== "image/jpeg",
1758
1786
  });
@@ -1848,17 +1876,28 @@
1848
1876
  }
1849
1877
  const error = new Error("upload_failed");
1850
1878
  error.safeCode = "upload_failed";
1851
- error.safeMessage =
1852
- "La carga falló tras 3 intentos. Puedes reintentarla.";
1853
1879
  error.safeStage =
1854
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.";
1855
1887
  throw error;
1856
1888
  }
1857
1889
 
1858
1890
  async function resolveUpload(file, uploadId) {
1859
1891
  const uploadPage = new URL(state.session.uploadPageUrl);
1860
- if (!safeUploadPageUrl(uploadPage))
1861
- 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");
1862
1901
  const token =
1863
1902
  uploadPage.searchParams.get("token") ||
1864
1903
  new URLSearchParams(uploadPage.hash.slice(1)).get("token");
@@ -1867,10 +1906,6 @@
1867
1906
  error.safeStage = "resolve";
1868
1907
  throw error;
1869
1908
  }
1870
- const resolveUrl = new URL(
1871
- state.session.resolveEndpoint,
1872
- uploadPage.origin,
1873
- );
1874
1909
  const response = await fetch(resolveUrl.toString(), {
1875
1910
  method: "POST",
1876
1911
  headers: { "content-type": "application/json" },
@@ -1882,6 +1917,7 @@
1882
1917
  fileSize: file.size,
1883
1918
  }),
1884
1919
  credentials: "omit",
1920
+ redirect: "error",
1885
1921
  signal: activeAbortSignal(),
1886
1922
  });
1887
1923
  if (!response.ok) {
@@ -1947,7 +1983,7 @@
1947
1983
  );
1948
1984
  }
1949
1985
 
1950
- function safeUploadPageUrl(url) {
1986
+ function safeUploadPageUrl(url, resolveUrl) {
1951
1987
  if (url.username || url.password) return false;
1952
1988
  if (
1953
1989
  [
@@ -1957,11 +1993,38 @@
1957
1993
  )
1958
1994
  return true;
1959
1995
  return (
1960
- url.protocol === "http:" &&
1961
- ["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
1962
2008
  );
1963
2009
  }
1964
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
+
1965
2028
  function putFileWithProgress(url, file, onProgress) {
1966
2029
  return new Promise((resolve, reject) => {
1967
2030
  const request = new XMLHttpRequest();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "3.0.0",
3
+ "version": "3.1.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.",
@@ -64,9 +64,9 @@
64
64
  "tsx": "^4.16.0",
65
65
  "typescript": "^7.0.2",
66
66
  "vitest": "^2.1.9",
67
+ "@packages/product-research": "0.1.0",
67
68
  "@packages/product-economics": "0.1.0",
68
- "@packages/types": "0.1.0",
69
- "@packages/product-research": "0.1.0"
69
+ "@packages/types": "0.1.0"
70
70
  },
71
71
  "publishConfig": {
72
72
  "access": "public"