@productmaker/mcp 3.1.1 → 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/dist/http.js +1 -4
- package/dist/stdio.js +1 -4
- package/dist/ui/meta-upload-app.html +125 -38
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -11368,13 +11368,12 @@ var ApiClient = class {
|
|
|
11368
11368
|
};
|
|
11369
11369
|
|
|
11370
11370
|
// src/index.ts
|
|
11371
|
-
var MCP_VERSION = "3.1.
|
|
11371
|
+
var MCP_VERSION = "3.1.2";
|
|
11372
11372
|
|
|
11373
11373
|
// src/meta-upload-app.ts
|
|
11374
11374
|
import { readFileSync } from "fs";
|
|
11375
11375
|
import { registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
|
|
11376
11376
|
var MEDIA_UPLOAD_APP_RESOURCE_URI = "ui://media-upload/v1.html";
|
|
11377
|
-
var MEDIA_UPLOAD_APP_DOMAIN = "https://productmaker.app";
|
|
11378
11377
|
var MEDIA_UPLOAD_APP_NAME = "ProductMaker media upload";
|
|
11379
11378
|
var MEDIA_UPLOAD_APP_HTML_URL = new URL("./ui/meta-upload-app.html", import.meta.url);
|
|
11380
11379
|
var MEDIA_UPLOAD_APP_CSP = {
|
|
@@ -11420,7 +11419,6 @@ function registerMediaUploadAppResource(server, resolveEndpoint) {
|
|
|
11420
11419
|
mimeType: RESOURCE_MIME_TYPE,
|
|
11421
11420
|
_meta: {
|
|
11422
11421
|
ui: {
|
|
11423
|
-
domain: MEDIA_UPLOAD_APP_DOMAIN,
|
|
11424
11422
|
csp
|
|
11425
11423
|
}
|
|
11426
11424
|
}
|
|
@@ -11432,7 +11430,6 @@ function registerMediaUploadAppResource(server, resolveEndpoint) {
|
|
|
11432
11430
|
text: html,
|
|
11433
11431
|
_meta: {
|
|
11434
11432
|
ui: {
|
|
11435
|
-
domain: MEDIA_UPLOAD_APP_DOMAIN,
|
|
11436
11433
|
csp
|
|
11437
11434
|
}
|
|
11438
11435
|
}
|
package/dist/stdio.js
CHANGED
|
@@ -264,13 +264,12 @@ var ApiClient = class {
|
|
|
264
264
|
};
|
|
265
265
|
|
|
266
266
|
// src/index.ts
|
|
267
|
-
var MCP_VERSION = "3.1.
|
|
267
|
+
var MCP_VERSION = "3.1.2";
|
|
268
268
|
|
|
269
269
|
// src/meta-upload-app.ts
|
|
270
270
|
import { readFileSync } from "fs";
|
|
271
271
|
import { registerAppResource, RESOURCE_MIME_TYPE } from "@modelcontextprotocol/ext-apps/server";
|
|
272
272
|
var MEDIA_UPLOAD_APP_RESOURCE_URI = "ui://media-upload/v1.html";
|
|
273
|
-
var MEDIA_UPLOAD_APP_DOMAIN = "https://productmaker.app";
|
|
274
273
|
var MEDIA_UPLOAD_APP_NAME = "ProductMaker media upload";
|
|
275
274
|
var MEDIA_UPLOAD_APP_HTML_URL = new URL("./ui/meta-upload-app.html", import.meta.url);
|
|
276
275
|
var MEDIA_UPLOAD_APP_CSP = {
|
|
@@ -316,7 +315,6 @@ function registerMediaUploadAppResource(server, resolveEndpoint) {
|
|
|
316
315
|
mimeType: RESOURCE_MIME_TYPE,
|
|
317
316
|
_meta: {
|
|
318
317
|
ui: {
|
|
319
|
-
domain: MEDIA_UPLOAD_APP_DOMAIN,
|
|
320
318
|
csp
|
|
321
319
|
}
|
|
322
320
|
}
|
|
@@ -328,7 +326,6 @@ function registerMediaUploadAppResource(server, resolveEndpoint) {
|
|
|
328
326
|
text: html,
|
|
329
327
|
_meta: {
|
|
330
328
|
ui: {
|
|
331
|
-
domain: MEDIA_UPLOAD_APP_DOMAIN,
|
|
332
329
|
csp
|
|
333
330
|
}
|
|
334
331
|
}
|
|
@@ -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) => {
|
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.",
|