@productmaker/mcp 2.10.1 → 3.0.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 +7 -15
- package/dist/http.js +204 -200
- package/dist/stdio.js +202 -198
- package/dist/ui/meta-upload-app.html +108 -102
- package/package.json +1 -1
|
@@ -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,23 +1192,22 @@
|
|
|
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
|
-
const checkToolName =
|
|
1183
|
-
"check_media_upload",
|
|
1184
|
-
"check_meta_media_upload",
|
|
1185
|
-
].includes(candidate.checkToolName)
|
|
1186
|
-
? candidate.checkToolName
|
|
1187
|
-
: "check_meta_media_upload";
|
|
1197
|
+
const checkToolName = "check_media_upload";
|
|
1188
1198
|
const resolveEndpoint =
|
|
1189
1199
|
boundedString(candidate.resolveEndpoint, 200) ||
|
|
1190
|
-
"/api/
|
|
1200
|
+
"/api/upload-media/resolve";
|
|
1191
1201
|
if (!sessionId || !["image", "video", "mixed"].includes(mediaKind))
|
|
1192
1202
|
return null;
|
|
1193
1203
|
if (!["media", "thumbnail"].includes(assetRole)) return null;
|
|
1194
1204
|
if (!Number.isInteger(candidate.maxFiles) || !expiresAt) return null;
|
|
1195
1205
|
try {
|
|
1196
1206
|
const parsedUrl = new URL(uploadPageUrl);
|
|
1197
|
-
|
|
1207
|
+
const parsedResolveUrl = new URL(resolveEndpoint, parsedUrl.origin);
|
|
1208
|
+
if (!safeResolveEndpointUrl(parsedResolveUrl, parsedUrl))
|
|
1209
|
+
return null;
|
|
1210
|
+
if (!safeUploadPageUrl(parsedUrl, parsedResolveUrl)) return null;
|
|
1198
1211
|
} catch {
|
|
1199
1212
|
return null;
|
|
1200
1213
|
}
|
|
@@ -1203,6 +1216,11 @@
|
|
|
1203
1216
|
uploadPageUrl,
|
|
1204
1217
|
mediaKind,
|
|
1205
1218
|
assetRole,
|
|
1219
|
+
uploadPurpose:
|
|
1220
|
+
uploadPurpose === "product_reference" ||
|
|
1221
|
+
uploadPurpose === "creative"
|
|
1222
|
+
? uploadPurpose
|
|
1223
|
+
: "generic",
|
|
1206
1224
|
maxFiles: candidate.maxFiles,
|
|
1207
1225
|
expiresAt,
|
|
1208
1226
|
checkToolName,
|
|
@@ -1210,6 +1228,26 @@
|
|
|
1210
1228
|
};
|
|
1211
1229
|
}
|
|
1212
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
|
+
|
|
1213
1251
|
function mergeToolInput(input) {
|
|
1214
1252
|
state.toolInput = { ...state.toolInput, ...input };
|
|
1215
1253
|
void consumePendingBridgeFiles();
|
|
@@ -1480,7 +1518,7 @@
|
|
|
1480
1518
|
"warning",
|
|
1481
1519
|
);
|
|
1482
1520
|
renderItems();
|
|
1483
|
-
emitAnalytics("
|
|
1521
|
+
emitAnalytics("mcp_media_upload_error", mediaKind, {
|
|
1484
1522
|
failureStage: "bridge",
|
|
1485
1523
|
errorType: "bridge_download_failed",
|
|
1486
1524
|
});
|
|
@@ -1564,18 +1602,18 @@
|
|
|
1564
1602
|
inspection.mediaKind === "image"
|
|
1565
1603
|
? await compressImage(item.file, inspection)
|
|
1566
1604
|
: item.file;
|
|
1567
|
-
emitAnalytics("
|
|
1605
|
+
emitAnalytics("mcp_media_upload_started", item.mediaKind);
|
|
1568
1606
|
await uploadWithRetries(item);
|
|
1569
1607
|
item.status = "uploaded";
|
|
1570
1608
|
item.progress = 100;
|
|
1571
1609
|
item.error = "";
|
|
1572
|
-
emitAnalytics("
|
|
1610
|
+
emitAnalytics("mcp_media_upload_completed", item.mediaKind);
|
|
1573
1611
|
} catch (error) {
|
|
1574
1612
|
const failure = safeFailure(error);
|
|
1575
1613
|
item.status = "failed";
|
|
1576
1614
|
item.error = failure.message;
|
|
1577
1615
|
item.failureStage = failure.stage;
|
|
1578
|
-
emitAnalytics("
|
|
1616
|
+
emitAnalytics("mcp_media_upload_error", item.mediaKind, {
|
|
1579
1617
|
failureStage: failure.stage,
|
|
1580
1618
|
errorType: failure.code,
|
|
1581
1619
|
});
|
|
@@ -1635,15 +1673,6 @@
|
|
|
1635
1673
|
mediaKind === "image"
|
|
1636
1674
|
? await inspectImage(file)
|
|
1637
1675
|
: await inspectVideo(file);
|
|
1638
|
-
if (
|
|
1639
|
-
inspection.width < MIN_DIMENSION ||
|
|
1640
|
-
inspection.height < MIN_DIMENSION
|
|
1641
|
-
) {
|
|
1642
|
-
throw validationError(
|
|
1643
|
-
"low_resolution",
|
|
1644
|
-
"La resolución mínima es 600 × 600 px.",
|
|
1645
|
-
);
|
|
1646
|
-
}
|
|
1647
1676
|
if (
|
|
1648
1677
|
mediaKind === "video" &&
|
|
1649
1678
|
inspection.durationSeconds > MAX_VIDEO_DURATION_SECONDS
|
|
@@ -1752,12 +1781,6 @@
|
|
|
1752
1781
|
const canvas = document.createElement("canvas");
|
|
1753
1782
|
canvas.width = Math.max(1, Math.round(inspection.width * scale));
|
|
1754
1783
|
canvas.height = Math.max(1, Math.round(inspection.height * scale));
|
|
1755
|
-
if (canvas.width < MIN_DIMENSION || canvas.height < MIN_DIMENSION) {
|
|
1756
|
-
throw validationError(
|
|
1757
|
-
"low_resolution",
|
|
1758
|
-
"La resolución mínima final es 600 × 600 px.",
|
|
1759
|
-
);
|
|
1760
|
-
}
|
|
1761
1784
|
const context = canvas.getContext("2d", {
|
|
1762
1785
|
alpha: file.type !== "image/jpeg",
|
|
1763
1786
|
});
|
|
@@ -1853,17 +1876,28 @@
|
|
|
1853
1876
|
}
|
|
1854
1877
|
const error = new Error("upload_failed");
|
|
1855
1878
|
error.safeCode = "upload_failed";
|
|
1856
|
-
error.safeMessage =
|
|
1857
|
-
"La carga falló tras 3 intentos. Puedes reintentarla.";
|
|
1858
1879
|
error.safeStage =
|
|
1859
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.";
|
|
1860
1887
|
throw error;
|
|
1861
1888
|
}
|
|
1862
1889
|
|
|
1863
1890
|
async function resolveUpload(file, uploadId) {
|
|
1864
1891
|
const uploadPage = new URL(state.session.uploadPageUrl);
|
|
1865
|
-
|
|
1866
|
-
|
|
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");
|
|
1867
1901
|
const token =
|
|
1868
1902
|
uploadPage.searchParams.get("token") ||
|
|
1869
1903
|
new URLSearchParams(uploadPage.hash.slice(1)).get("token");
|
|
@@ -1872,10 +1906,6 @@
|
|
|
1872
1906
|
error.safeStage = "resolve";
|
|
1873
1907
|
throw error;
|
|
1874
1908
|
}
|
|
1875
|
-
const resolveUrl = new URL(
|
|
1876
|
-
state.session.resolveEndpoint,
|
|
1877
|
-
uploadPage.origin,
|
|
1878
|
-
);
|
|
1879
1909
|
const response = await fetch(resolveUrl.toString(), {
|
|
1880
1910
|
method: "POST",
|
|
1881
1911
|
headers: { "content-type": "application/json" },
|
|
@@ -1887,6 +1917,7 @@
|
|
|
1887
1917
|
fileSize: file.size,
|
|
1888
1918
|
}),
|
|
1889
1919
|
credentials: "omit",
|
|
1920
|
+
redirect: "error",
|
|
1890
1921
|
signal: activeAbortSignal(),
|
|
1891
1922
|
});
|
|
1892
1923
|
if (!response.ok) {
|
|
@@ -1901,10 +1932,7 @@
|
|
|
1901
1932
|
const body = await response.json();
|
|
1902
1933
|
if (!isRecord(body)) throw new Error("invalid_resolve_response");
|
|
1903
1934
|
const uploadUrl = boundedString(body.uploadUrl, 3000);
|
|
1904
|
-
const assetId = boundedString(
|
|
1905
|
-
body.assetId || body.stagingAssetId,
|
|
1906
|
-
300,
|
|
1907
|
-
);
|
|
1935
|
+
const assetId = boundedString(body.assetId, 300);
|
|
1908
1936
|
const parsedUploadUrl = new URL(uploadUrl);
|
|
1909
1937
|
if (!safeWebProtocol(parsedUploadUrl) || !assetId) {
|
|
1910
1938
|
const error = new Error("invalid_resolve_response");
|
|
@@ -1955,7 +1983,7 @@
|
|
|
1955
1983
|
);
|
|
1956
1984
|
}
|
|
1957
1985
|
|
|
1958
|
-
function safeUploadPageUrl(url) {
|
|
1986
|
+
function safeUploadPageUrl(url, resolveUrl) {
|
|
1959
1987
|
if (url.username || url.password) return false;
|
|
1960
1988
|
if (
|
|
1961
1989
|
[
|
|
@@ -1965,11 +1993,38 @@
|
|
|
1965
1993
|
)
|
|
1966
1994
|
return true;
|
|
1967
1995
|
return (
|
|
1968
|
-
url.protocol === "http:" &&
|
|
1969
|
-
|
|
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
|
|
1970
2008
|
);
|
|
1971
2009
|
}
|
|
1972
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
|
+
|
|
1973
2028
|
function putFileWithProgress(url, file, onProgress) {
|
|
1974
2029
|
return new Promise((resolve, reject) => {
|
|
1975
2030
|
const request = new XMLHttpRequest();
|
|
@@ -2203,21 +2258,14 @@
|
|
|
2203
2258
|
state.session.sessionId !== requestedSessionId
|
|
2204
2259
|
)
|
|
2205
2260
|
return;
|
|
2206
|
-
const status = parseStatusResult(result
|
|
2261
|
+
const status = parseStatusResult(result);
|
|
2207
2262
|
state.assets = status.assets;
|
|
2208
2263
|
renderCompletionAction();
|
|
2209
2264
|
if (!status.assets.length) {
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
);
|
|
2215
|
-
} else {
|
|
2216
|
-
requireFallback(
|
|
2217
|
-
"La sesión sigue pendiente. Continúa desde el enlace seguro para terminar la carga.",
|
|
2218
|
-
"warning",
|
|
2219
|
-
);
|
|
2220
|
-
}
|
|
2265
|
+
requireFallback(
|
|
2266
|
+
"La sesión sigue pendiente. Continúa desde el enlace seguro para terminar la carga.",
|
|
2267
|
+
"warning",
|
|
2268
|
+
);
|
|
2221
2269
|
return;
|
|
2222
2270
|
}
|
|
2223
2271
|
|
|
@@ -2255,9 +2303,7 @@
|
|
|
2255
2303
|
}
|
|
2256
2304
|
|
|
2257
2305
|
async function callReadOnlyTool(name, args) {
|
|
2258
|
-
if (
|
|
2259
|
-
!["check_media_upload", "check_meta_media_upload"].includes(name)
|
|
2260
|
-
) {
|
|
2306
|
+
if (name !== "check_media_upload") {
|
|
2261
2307
|
throw new Error("tool_not_allowed");
|
|
2262
2308
|
}
|
|
2263
2309
|
const bridge = openAiBridge();
|
|
@@ -2302,58 +2348,22 @@
|
|
|
2302
2348
|
return parsed;
|
|
2303
2349
|
}
|
|
2304
2350
|
|
|
2305
|
-
function parseStatusResult(result
|
|
2351
|
+
function parseStatusResult(result) {
|
|
2306
2352
|
const payload = resultPayload(result);
|
|
2307
|
-
if (
|
|
2308
|
-
state.session.checkToolName === "check_meta_media_upload" &&
|
|
2309
|
-
payload.sessionId !== expectedSessionId
|
|
2310
|
-
)
|
|
2311
|
-
throw new Error("status_session_mismatch");
|
|
2312
2353
|
const status = payload.status;
|
|
2313
2354
|
if (!["pending", "uploaded"].includes(status))
|
|
2314
2355
|
throw new Error("invalid_status");
|
|
2315
2356
|
const assets = Array.isArray(payload.assets)
|
|
2316
2357
|
? payload.assets.slice(0, MAX_FILES).flatMap((asset) => {
|
|
2317
2358
|
if (!isRecord(asset)) return [];
|
|
2318
|
-
const assetId = boundedString(
|
|
2319
|
-
asset.assetId || asset.stagingAssetId,
|
|
2320
|
-
300,
|
|
2321
|
-
);
|
|
2359
|
+
const assetId = boundedString(asset.assetId, 300);
|
|
2322
2360
|
const mediaKind = asset.mediaKind;
|
|
2323
2361
|
return assetId && ["image", "video"].includes(mediaKind)
|
|
2324
2362
|
? [{ assetId, mediaKind }]
|
|
2325
2363
|
: [];
|
|
2326
2364
|
})
|
|
2327
2365
|
: [];
|
|
2328
|
-
|
|
2329
|
-
const legacyStagingAssetId = boundedString(
|
|
2330
|
-
payload.stagingAssetId,
|
|
2331
|
-
300,
|
|
2332
|
-
);
|
|
2333
|
-
if (!assets.length && legacyStagingAssetId) {
|
|
2334
|
-
const localKinds = new Set(
|
|
2335
|
-
state.items
|
|
2336
|
-
.filter((item) => item.status === "uploaded")
|
|
2337
|
-
.map((item) => item.mediaKind)
|
|
2338
|
-
.filter((mediaKind) => ["image", "video"].includes(mediaKind)),
|
|
2339
|
-
);
|
|
2340
|
-
const inferredMediaKind =
|
|
2341
|
-
localKinds.size === 1
|
|
2342
|
-
? Array.from(localKinds)[0]
|
|
2343
|
-
: localKinds.size === 0 &&
|
|
2344
|
-
["image", "video"].includes(state.session.mediaKind)
|
|
2345
|
-
? state.session.mediaKind
|
|
2346
|
-
: null;
|
|
2347
|
-
if (inferredMediaKind) {
|
|
2348
|
-
assets.push({
|
|
2349
|
-
assetId: legacyStagingAssetId,
|
|
2350
|
-
mediaKind: inferredMediaKind,
|
|
2351
|
-
});
|
|
2352
|
-
} else {
|
|
2353
|
-
legacyKindAmbiguous = true;
|
|
2354
|
-
}
|
|
2355
|
-
}
|
|
2356
|
-
return { status, assets, legacyKindAmbiguous };
|
|
2366
|
+
return { status, assets };
|
|
2357
2367
|
}
|
|
2358
2368
|
|
|
2359
2369
|
async function notifyChatUploadComplete() {
|
|
@@ -2370,11 +2380,7 @@
|
|
|
2370
2380
|
const assets = state.assets
|
|
2371
2381
|
.map((asset) => `${asset.mediaKind} asset_id ${asset.assetId}`)
|
|
2372
2382
|
.join(", ");
|
|
2373
|
-
const
|
|
2374
|
-
state.session.checkToolName === "check_meta_media_upload"
|
|
2375
|
-
? " Do not publish anything until you have asked me for the Meta portfolio, ad account, and Facebook Page."
|
|
2376
|
-
: "";
|
|
2377
|
-
const message = `The ProductMaker upload is complete. Continue my previous request using these uploaded assets: ${assets}. Do not ask me to upload them again.${metaGuard}`;
|
|
2383
|
+
const message = `The ProductMaker upload is complete. Continue my previous request using these uploaded assets: ${assets}. Do not ask me to upload them again. Ask only for the remaining inputs required by my original request.`;
|
|
2378
2384
|
try {
|
|
2379
2385
|
const bridge = openAiBridge();
|
|
2380
2386
|
if (bridge && typeof bridge.sendFollowUpMessage === "function") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@productmaker/mcp",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.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.",
|