@productmaker/mcp 12.0.3 → 12.1.1

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.
@@ -0,0 +1,398 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="color-scheme" content="light dark" />
7
+ <title>Your creative · ProductMaker</title>
8
+ <style>
9
+ :root {
10
+ color-scheme: light dark;
11
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
12
+ color: #202635;
13
+ background: #fff;
14
+ }
15
+ * {
16
+ box-sizing: border-box;
17
+ }
18
+ body {
19
+ margin: 0;
20
+ }
21
+ main {
22
+ max-width: 760px;
23
+ margin: auto;
24
+ padding: 18px;
25
+ }
26
+ h1 {
27
+ margin: 0 0 12px;
28
+ font-size: 18px;
29
+ font-weight: 650;
30
+ }
31
+ #status {
32
+ margin: 0;
33
+ color: #657083;
34
+ font-size: 14px;
35
+ line-height: 1.5;
36
+ }
37
+ #media {
38
+ display: grid;
39
+ gap: 18px;
40
+ }
41
+ figure {
42
+ margin: 0;
43
+ min-width: 0;
44
+ }
45
+ img,
46
+ video {
47
+ display: block;
48
+ width: 100%;
49
+ max-height: 640px;
50
+ object-fit: contain;
51
+ border-radius: 12px;
52
+ background: #f3f4f6;
53
+ }
54
+ img[hidden],
55
+ video[hidden],
56
+ p[hidden] {
57
+ display: none;
58
+ }
59
+ figcaption {
60
+ margin-top: 9px;
61
+ font-size: 13px;
62
+ }
63
+ a {
64
+ color: #5140cf;
65
+ text-underline-offset: 3px;
66
+ }
67
+ a:focus-visible {
68
+ outline: 2px solid currentColor;
69
+ outline-offset: 4px;
70
+ }
71
+ .fallback {
72
+ color: #657083;
73
+ line-height: 1.5;
74
+ font-size: 14px;
75
+ }
76
+ ul {
77
+ padding-left: 20px;
78
+ margin: 0;
79
+ line-height: 1.8;
80
+ }
81
+ @media (max-width: 420px) {
82
+ main {
83
+ padding: 12px;
84
+ }
85
+ }
86
+ @media (prefers-color-scheme: dark) {
87
+ :root {
88
+ color: #f2f4f8;
89
+ background: #17191f;
90
+ }
91
+ #status,
92
+ .fallback {
93
+ color: #b5becd;
94
+ }
95
+ img,
96
+ video {
97
+ background: #222630;
98
+ }
99
+ a {
100
+ color: #b6aaff;
101
+ }
102
+ }
103
+ </style>
104
+ </head>
105
+ <body>
106
+ <main data-testid="task-results-root" data-state="loading" aria-busy="true">
107
+ <h1>Your creative</h1>
108
+ <p id="status" role="status" aria-live="polite">Loading your result…</p>
109
+ <div id="media"></div>
110
+ </main>
111
+ <script>
112
+ (() => {
113
+ "use strict";
114
+ const assetOrigin = __PRODUCTMAKER_ASSET_ORIGIN__;
115
+ const root = document.querySelector("main");
116
+ const status = document.getElementById("status");
117
+ const media = document.getElementById("media");
118
+ let receivedResult = false;
119
+ let hostReady = false;
120
+ let disposed = false;
121
+ const isRecord = (value) =>
122
+ value !== null && typeof value === "object" && !Array.isArray(value);
123
+ const safeUrl = (value) => {
124
+ if (typeof value !== "string") return null;
125
+ try {
126
+ const url = new URL(value);
127
+ return url.protocol === "https:" &&
128
+ url.origin === assetOrigin &&
129
+ url.pathname.startsWith("/assets/") &&
130
+ url.pathname.length > "/assets/".length &&
131
+ !url.username &&
132
+ !url.password
133
+ ? url.href
134
+ : null;
135
+ } catch {
136
+ return null;
137
+ }
138
+ };
139
+ const notify = (method, params) => {
140
+ if (!disposed)
141
+ window.parent.postMessage({ jsonrpc: "2.0", method, params }, "*");
142
+ };
143
+ const reportSize = () => {
144
+ if (disposed) return;
145
+ // Measure content, not the host's previous iframe height, so errors
146
+ // and smaller follow-up results can shrink instead of keeping a gap.
147
+ const height = Math.ceil(root.getBoundingClientRect().height);
148
+ if (hostReady) notify("ui/notifications/size-changed", { height });
149
+ if (typeof window.openai?.notifyIntrinsicHeight === "function")
150
+ window.openai.notifyIntrinsicHeight(height);
151
+ };
152
+ function cleanupMedia() {
153
+ media.querySelectorAll("video").forEach((video) => {
154
+ video.pause();
155
+ video.removeAttribute("src");
156
+ video.load();
157
+ });
158
+ media.replaceChildren();
159
+ }
160
+ function setState(state, message) {
161
+ root.dataset.state = state;
162
+ root.setAttribute(
163
+ "aria-busy",
164
+ state === "loading" ? "true" : "false",
165
+ );
166
+ status.textContent = message;
167
+ status.hidden = !message;
168
+ cleanupMedia();
169
+ }
170
+ function download(url, label, testId) {
171
+ const link = document.createElement("a");
172
+ link.href = url;
173
+ link.textContent = label;
174
+ link.target = "_blank";
175
+ link.rel = "noopener noreferrer";
176
+ link.dataset.testid = testId;
177
+ return link;
178
+ }
179
+ function addMedia(item, kind, index) {
180
+ const figure = document.createElement("figure");
181
+ const element = document.createElement(kind);
182
+ element.src = item.url;
183
+ element.dataset.testid = `task-results-${kind}-${index}`;
184
+ if (kind === "img")
185
+ element.alt =
186
+ typeof item.alt === "string"
187
+ ? item.alt.slice(0, 160)
188
+ : "Ad creative";
189
+ else {
190
+ element.setAttribute(
191
+ "aria-label",
192
+ typeof item.label === "string"
193
+ ? item.label.slice(0, 120)
194
+ : "ProductMaker video",
195
+ );
196
+ element.controls = true;
197
+ element.preload = "metadata";
198
+ element.playsInline = true;
199
+ }
200
+ const fallback = document.createElement("p");
201
+ fallback.className = "fallback";
202
+ fallback.hidden = true;
203
+ fallback.textContent =
204
+ "Preview unavailable. Ask ChatGPT to show it again for a fresh link, or try the download link.";
205
+ element.addEventListener("error", () => {
206
+ element.hidden = true;
207
+ fallback.hidden = false;
208
+ reportSize();
209
+ });
210
+ element.addEventListener("load", reportSize);
211
+ element.addEventListener("loadedmetadata", reportSize);
212
+ const caption = document.createElement("figcaption");
213
+ caption.append(
214
+ download(
215
+ item.url,
216
+ kind === "img" ? "Download image" : "Download video",
217
+ `task-results-download-${kind}-${index}`,
218
+ ),
219
+ );
220
+ figure.append(element, fallback, caption);
221
+ if (kind === "video" && item.scriptRewritten === true) {
222
+ const warning = document.createElement("p");
223
+ warning.dataset.testid = `task-results-script-warning-${index}`;
224
+ warning.textContent =
225
+ "This video uses a revised script instead of the one you approved.";
226
+ figure.append(warning);
227
+ }
228
+ media.append(figure);
229
+ }
230
+ function entries(value) {
231
+ return Array.isArray(value)
232
+ ? value.filter(isRecord).flatMap((item) => {
233
+ const url = safeUrl(item.url);
234
+ return url
235
+ ? [
236
+ {
237
+ url,
238
+ alt: item.alt,
239
+ label: item.label,
240
+ ...(item.scriptRewritten === true
241
+ ? { scriptRewritten: true }
242
+ : {}),
243
+ },
244
+ ]
245
+ : [];
246
+ })
247
+ : [];
248
+ }
249
+ function applyResult(result) {
250
+ if (disposed) return;
251
+ receivedResult = true;
252
+ window.clearTimeout(resultTimeout);
253
+ const metadata =
254
+ isRecord(result) && isRecord(result._meta) ? result._meta : null;
255
+ const data = metadata?.taskResults;
256
+ if (
257
+ !isRecord(result) ||
258
+ result.isError ||
259
+ !isRecord(data) ||
260
+ !["ready", "empty", "pending", "awaiting_review", "error"].includes(
261
+ data.state,
262
+ )
263
+ ) {
264
+ setState(
265
+ "error",
266
+ "We couldn't load this result. Ask ChatGPT to show it again.",
267
+ );
268
+ reportSize();
269
+ return;
270
+ }
271
+ if (data.state === "awaiting_review") {
272
+ setState(
273
+ data.state,
274
+ "Your approval is needed. Reply in the chat to continue.",
275
+ );
276
+ reportSize();
277
+ return;
278
+ }
279
+ if (data.state === "pending" || data.state === "error") {
280
+ setState(
281
+ data.state,
282
+ data.state === "pending"
283
+ ? "Your creative is still being prepared. Ask ChatGPT to check its progress."
284
+ : "This task couldn't deliver a result. Ask ChatGPT to check its status.",
285
+ );
286
+ reportSize();
287
+ return;
288
+ }
289
+ const images = entries(data.images);
290
+ const videos = entries(data.videos);
291
+ const links = entries(data.links);
292
+ if (
293
+ data.state === "empty" ||
294
+ !(images.length || videos.length || links.length)
295
+ ) {
296
+ setState(
297
+ "empty",
298
+ "No preview is available here. Your result can still be opened in ProductMaker.",
299
+ );
300
+ } else {
301
+ setState("ready", "");
302
+ images.forEach((item, index) => addMedia(item, "img", index));
303
+ videos.forEach((item, index) => addMedia(item, "video", index));
304
+ if (links.length) {
305
+ const list = document.createElement("ul");
306
+ links.forEach((item, index) => {
307
+ const row = document.createElement("li");
308
+ row.append(
309
+ download(
310
+ item.url,
311
+ typeof item.label === "string"
312
+ ? item.label.slice(0, 120)
313
+ : "Open result",
314
+ `task-results-link-${index}`,
315
+ ),
316
+ );
317
+ list.append(row);
318
+ });
319
+ media.append(list);
320
+ }
321
+ }
322
+ reportSize();
323
+ }
324
+ function handleMessage(event) {
325
+ if (
326
+ event.source !== window.parent ||
327
+ !isRecord(event.data) ||
328
+ disposed
329
+ )
330
+ return;
331
+ const message = event.data;
332
+ if (message.method === "ui/resource-teardown") {
333
+ cleanupMedia();
334
+ window.clearTimeout(initializeTimeout);
335
+ window.clearTimeout(resultTimeout);
336
+ window.removeEventListener("message", handleMessage);
337
+ window.removeEventListener("openai:set_globals", handleGlobals);
338
+ window.removeEventListener("resize", reportSize);
339
+ if (Object.prototype.hasOwnProperty.call(message, "id")) {
340
+ window.parent.postMessage(
341
+ { jsonrpc: "2.0", id: message.id, result: {} },
342
+ "*",
343
+ );
344
+ }
345
+ disposed = true;
346
+ } else if (message.id === 1) {
347
+ window.clearTimeout(initializeTimeout);
348
+ if (message.error) {
349
+ if (!receivedResult) applyResult({ isError: true });
350
+ } else {
351
+ hostReady = true;
352
+ notify("ui/notifications/initialized", {});
353
+ reportSize();
354
+ }
355
+ } else if (message.method === "ui/notifications/tool-result")
356
+ applyResult(message.params);
357
+ else if (message.method === "ui/notifications/tool-cancelled")
358
+ applyResult({ isError: true });
359
+ }
360
+ function handleGlobals(event) {
361
+ const globals = event.detail?.globals;
362
+ if (
363
+ isRecord(globals) &&
364
+ Object.prototype.hasOwnProperty.call(
365
+ globals,
366
+ "toolResponseMetadata",
367
+ )
368
+ )
369
+ applyResult({ _meta: globals.toolResponseMetadata });
370
+ }
371
+ const resultTimeout = window.setTimeout(() => {
372
+ if (!receivedResult) applyResult({ isError: true });
373
+ }, 10000);
374
+ const initializeTimeout = window.setTimeout(() => {
375
+ if (!receivedResult) applyResult({ isError: true });
376
+ }, 8000);
377
+ window.addEventListener("message", handleMessage);
378
+ window.addEventListener("openai:set_globals", handleGlobals);
379
+ window.addEventListener("resize", reportSize);
380
+ window.parent.postMessage(
381
+ {
382
+ jsonrpc: "2.0",
383
+ id: 1,
384
+ method: "ui/initialize",
385
+ params: {
386
+ appInfo: { name: "ProductMaker task results", version: "1.0.0" },
387
+ appCapabilities: { availableDisplayModes: ["inline"] },
388
+ protocolVersion: "2026-01-26",
389
+ },
390
+ },
391
+ "*",
392
+ );
393
+ if (isRecord(window.openai?.toolResponseMetadata))
394
+ applyResult({ _meta: window.openai.toolResponseMetadata });
395
+ })();
396
+ </script>
397
+ </body>
398
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "12.0.3",
3
+ "version": "12.1.1",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "description": "ProductMaker MCP server — cost products, generate landing pages, UGC videos, and ad creatives, and publish to Shopify, Meta Ads, and TikTok Ads.",
@@ -74,7 +74,7 @@
74
74
  "access": "public"
75
75
  },
76
76
  "scripts": {
77
- "build": "tsup && mkdir -p dist/ui && cp src/ui/meta-upload-app.html dist/ui/meta-upload-app.html",
77
+ "build": "tsup && mkdir -p dist/ui && cp src/ui/*.html dist/ui/",
78
78
  "dev:stdio": "tsx src/stdio.ts",
79
79
  "dev:http": "tsx src/http.ts",
80
80
  "test": "vitest run",