@productmaker/mcp 3.1.0 → 3.1.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.
@@ -548,6 +548,7 @@
548
548
  const IMAGE_COMPRESSION_THRESHOLD_BYTES = 1.8 * 1024 * 1024;
549
549
  const IMAGE_COMPRESSION_MAX_BYTES = 1.8 * 1024 * 1024;
550
550
  const IMAGE_MAX_DIMENSION = 2048;
551
+ const PREVIEW_MAX_DIMENSION = 256;
551
552
  const MAX_UPLOAD_ATTEMPTS = 3;
552
553
  const TOOL_RESULT_TEXT_LIMIT = 100000;
553
554
  const RPC_TIMEOUT_MS = 15000;
@@ -1560,6 +1561,8 @@
1560
1561
  const item = createItem(file, mediaKind);
1561
1562
  state.items.push(item);
1562
1563
  renderItems();
1564
+ // Thumbnail is cosmetic and must never delay or block the upload.
1565
+ void upgradePreviewToDataUrl(item);
1563
1566
  await prepareAndUpload(item);
1564
1567
  }
1565
1568
  state.busyBatch = false;
@@ -1685,39 +1688,77 @@
1685
1688
  return inspection;
1686
1689
  }
1687
1690
 
1688
- function inspectImage(file) {
1689
- return new Promise((resolve, reject) => {
1691
+ /**
1692
+ * Decodes `file` into a canvas-drawable source WITHOUT a `blob:` URL.
1693
+ *
1694
+ * Host sandboxes restrict `img-src`, and ChatGPT's does not allow `blob:`
1695
+ * ("Loading the image 'blob:…' violates the following Content Security Policy
1696
+ * directive: img-src 'self' data: …"), so `<img>.src = URL.createObjectURL(file)`
1697
+ * is blocked, fires onerror, and every image was rejected as unreadable —
1698
+ * regardless of its format. `createImageBitmap` reads the File directly, so no
1699
+ * URL and no CSP is involved. The object-URL path stays only as a fallback for
1700
+ * hosts without it, where nothing blocked us in the first place.
1701
+ *
1702
+ * Callers own the result: call releaseImageSource() when done.
1703
+ */
1704
+ async function decodeImageSource(file) {
1705
+ if (typeof createImageBitmap === "function") {
1706
+ try {
1707
+ return await createImageBitmap(file);
1708
+ } catch {
1709
+ // fall through to the object-URL path
1710
+ }
1711
+ }
1712
+ return await new Promise((resolve, reject) => {
1690
1713
  const image = new Image();
1691
1714
  const objectUrl = URL.createObjectURL(file);
1692
- const cleanup = () => URL.revokeObjectURL(objectUrl);
1693
1715
  image.onload = () => {
1694
- cleanup();
1695
- const width = image.naturalWidth;
1696
- const height = image.naturalHeight;
1697
- if (!validDimension(width) || !validDimension(height)) {
1698
- reject(
1699
- validationError(
1700
- "invalid_metadata",
1701
- "No se pudo leer la imagen.",
1702
- ),
1703
- );
1704
- return;
1705
- }
1706
- resolve({ mediaKind: "image", width, height, image });
1716
+ URL.revokeObjectURL(objectUrl);
1717
+ resolve(image);
1707
1718
  };
1708
1719
  image.onerror = () => {
1709
- cleanup();
1710
- reject(
1711
- validationError(
1712
- "invalid_metadata",
1713
- "No se pudo leer la imagen.",
1714
- ),
1715
- );
1720
+ URL.revokeObjectURL(objectUrl);
1721
+ reject(new Error("decode_failed"));
1716
1722
  };
1717
1723
  image.src = objectUrl;
1718
1724
  });
1719
1725
  }
1720
1726
 
1727
+ function releaseImageSource(source) {
1728
+ if (source && typeof source.close === "function") source.close();
1729
+ }
1730
+
1731
+ function sourceDimensions(source) {
1732
+ return {
1733
+ width: source.width ?? source.naturalWidth,
1734
+ height: source.height ?? source.naturalHeight,
1735
+ };
1736
+ }
1737
+
1738
+ async function inspectImage(file) {
1739
+ let source;
1740
+ try {
1741
+ source = await decodeImageSource(file);
1742
+ } catch {
1743
+ throw validationError(
1744
+ "invalid_metadata",
1745
+ "No se pudo leer la imagen.",
1746
+ );
1747
+ }
1748
+ try {
1749
+ const { width, height } = sourceDimensions(source);
1750
+ if (!validDimension(width) || !validDimension(height)) {
1751
+ throw validationError(
1752
+ "invalid_metadata",
1753
+ "No se pudo leer la imagen.",
1754
+ );
1755
+ }
1756
+ return { mediaKind: "image", width, height };
1757
+ } finally {
1758
+ releaseImageSource(source);
1759
+ }
1760
+ }
1761
+
1721
1762
  function inspectVideo(file) {
1722
1763
  return new Promise((resolve, reject) => {
1723
1764
  const video = document.createElement("video");
@@ -1772,6 +1813,15 @@
1772
1813
 
1773
1814
  async function compressImage(file, inspection) {
1774
1815
  if (file.size <= IMAGE_COMPRESSION_THRESHOLD_BYTES) return file;
1816
+ // Decoded here rather than carried over from inspectImage so the source's
1817
+ // lifetime never spans two functions: an ImageBitmap must be closed, and only
1818
+ // oversized files reach this path at all.
1819
+ let source;
1820
+ try {
1821
+ source = await decodeImageSource(file);
1822
+ } catch {
1823
+ return file;
1824
+ }
1775
1825
  try {
1776
1826
  const scale = Math.min(
1777
1827
  1,
@@ -1791,13 +1841,7 @@
1791
1841
  context.fillRect(0, 0, canvas.width, canvas.height);
1792
1842
  context.restore();
1793
1843
  }
1794
- context.drawImage(
1795
- inspection.image,
1796
- 0,
1797
- 0,
1798
- canvas.width,
1799
- canvas.height,
1800
- );
1844
+ context.drawImage(source, 0, 0, canvas.width, canvas.height);
1801
1845
  let blob = await canvasBlob(canvas, file.type);
1802
1846
  if (
1803
1847
  file.type === "image/png" &&
@@ -1830,6 +1874,8 @@
1830
1874
  safeError.safeMessage = "No se pudo comprimir la imagen.";
1831
1875
  safeError.safeStage = "compression";
1832
1876
  throw safeError;
1877
+ } finally {
1878
+ releaseImageSource(source);
1833
1879
  }
1834
1880
  }
1835
1881
 
@@ -2112,21 +2158,24 @@
2112
2158
 
2113
2159
  function revokePreviewUrl(item) {
2114
2160
  if (
2115
- item.previewUrl &&
2161
+ item.previewUrl.startsWith("blob:") &&
2116
2162
  typeof window.URL.revokeObjectURL === "function"
2117
2163
  ) {
2118
2164
  window.URL.revokeObjectURL(item.previewUrl);
2119
- item.previewUrl = "";
2120
2165
  }
2166
+ item.previewUrl = "";
2167
+ }
2168
+
2169
+ function previewableImage(item) {
2170
+ return Boolean(
2171
+ item.file &&
2172
+ item.mediaKind === "image" &&
2173
+ IMAGE_TYPES.has(item.file.type),
2174
+ );
2121
2175
  }
2122
2176
 
2123
2177
  function previewUrlFor(item) {
2124
- if (
2125
- !item.file ||
2126
- item.mediaKind !== "image" ||
2127
- !IMAGE_TYPES.has(item.file.type)
2128
- )
2129
- return "";
2178
+ if (!previewableImage(item)) return "";
2130
2179
  if (
2131
2180
  !item.previewUrl &&
2132
2181
  typeof window.URL.createObjectURL === "function"
@@ -2136,6 +2185,44 @@
2136
2185
  return item.previewUrl;
2137
2186
  }
2138
2187
 
2188
+ /**
2189
+ * Upgrades the thumbnail from the `blob:` object URL to a small `data:` URL.
2190
+ *
2191
+ * ChatGPT's sandbox blocks `blob:` in `img-src` (see decodeImageSource), so the
2192
+ * object URL renders as a broken image there; `data:` is allowed. This only ever
2193
+ * replaces a working-or-broken preview with a working one, so a failure here is
2194
+ * cosmetic and leaves the existing behaviour untouched on hosts that allow
2195
+ * `blob:`. Downscaled first so the inline URL stays a few KB, not the whole file.
2196
+ */
2197
+ async function upgradePreviewToDataUrl(item) {
2198
+ if (!previewableImage(item)) return;
2199
+ let source;
2200
+ try {
2201
+ source = await decodeImageSource(item.file);
2202
+ const { width, height } = sourceDimensions(source);
2203
+ if (!validDimension(width) || !validDimension(height)) return;
2204
+ const scale = Math.min(
2205
+ 1,
2206
+ PREVIEW_MAX_DIMENSION / Math.max(width, height),
2207
+ );
2208
+ const canvas = document.createElement("canvas");
2209
+ canvas.width = Math.max(1, Math.round(width * scale));
2210
+ canvas.height = Math.max(1, Math.round(height * scale));
2211
+ const context = canvas.getContext("2d");
2212
+ if (!context) return;
2213
+ context.drawImage(source, 0, 0, canvas.width, canvas.height);
2214
+ const dataUrl = canvas.toDataURL("image/jpeg", 0.7);
2215
+ if (!dataUrl.startsWith("data:image/")) return;
2216
+ revokePreviewUrl(item);
2217
+ item.previewUrl = dataUrl;
2218
+ renderItems();
2219
+ } catch {
2220
+ // Cosmetic only: the object-URL preview and the upload both stand on their own.
2221
+ } finally {
2222
+ releaseImageSource(source);
2223
+ }
2224
+ }
2225
+
2139
2226
  function renderItems() {
2140
2227
  const fragment = document.createDocumentFragment();
2141
2228
  state.items.forEach((item, index) => {
@@ -2273,19 +2360,27 @@
2273
2360
  status.status === "pending" ||
2274
2361
  (state.items.length > 0 &&
2275
2362
  status.assets.length < state.items.length);
2363
+ const canContinueInChat = canMessageHost();
2276
2364
  if (partial) {
2277
2365
  requireFallback(
2278
2366
  "Carga parcial verificada. Puedes continuar desde el enlace seguro si algún archivo no terminó.",
2279
2367
  "warning",
2280
2368
  );
2369
+ } else if (!canContinueInChat) {
2370
+ requireFallback(
2371
+ "Carga completada. Este chat no puede continuar desde el widget. Abre el enlace seguro para seguir.",
2372
+ "warning",
2373
+ );
2281
2374
  } else {
2282
2375
  setFallbackVisibility(state.fallbackRequired);
2283
2376
  }
2284
2377
  setSessionStatus(
2285
2378
  partial
2286
2379
  ? "Carga parcial verificada. Continuaremos solo con los assets listos."
2287
- : "Carga completada.",
2288
- partial ? "warning" : "success",
2380
+ : canContinueInChat
2381
+ ? "Carga completada."
2382
+ : "Carga completada. Abre el enlace seguro para seguir.",
2383
+ partial || !canContinueInChat ? "warning" : "success",
2289
2384
  );
2290
2385
  } catch (error) {
2291
2386
  const expired =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "3.1.0",
3
+ "version": "3.1.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.",
@@ -53,7 +53,9 @@
53
53
  "@modelcontextprotocol/sdk": "^1.30.0",
54
54
  "express": "^4.19.2",
55
55
  "express-rate-limit": "^7.4.0",
56
+ "jose": "^5.2.0",
56
57
  "pino": "^9.0.0",
58
+ "posthog-node": "^5.24.11",
57
59
  "zod": "^3.23.8"
58
60
  },
59
61
  "devDependencies": {
@@ -64,8 +66,8 @@
64
66
  "tsx": "^4.16.0",
65
67
  "typescript": "^7.0.2",
66
68
  "vitest": "^2.1.9",
67
- "@packages/product-research": "0.1.0",
68
69
  "@packages/product-economics": "0.1.0",
70
+ "@packages/product-research": "0.1.0",
69
71
  "@packages/types": "0.1.0"
70
72
  },
71
73
  "publishConfig": {