@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.
- package/dist/http.js +3536 -2286
- package/dist/stdio.js +3536 -2286
- package/dist/ui/meta-upload-app.html +91 -28
- package/package.json +3 -3
|
@@ -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
|
-
|
|
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
|
-
|
|
1861
|
-
|
|
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
|
-
|
|
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.
|
|
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"
|