@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.
- package/README.md +45 -21
- package/dist/http.js +2956 -1527
- package/dist/stdio.js +222 -58
- package/dist/ui/meta-upload-app.html +135 -40
- package/package.json +4 -2
|
@@ -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
|
-
|
|
1689
|
-
|
|
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
|
-
|
|
1695
|
-
|
|
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
|
-
|
|
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
|
-
:
|
|
2288
|
-
|
|
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.
|
|
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": {
|