@productmaker/mcp 12.1.0 → 12.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.
@@ -0,0 +1,461 @@
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 initializationSettled = false;
121
+ let disposed = false;
122
+ const initializeRequestId = 1;
123
+ const isRecord = (value) =>
124
+ value !== null && typeof value === "object" && !Array.isArray(value);
125
+ const resultStates = new Set([
126
+ "ready",
127
+ "empty",
128
+ "pending",
129
+ "awaiting_review",
130
+ "error",
131
+ ]);
132
+ const safeUrl = (value) => {
133
+ if (typeof value !== "string") return null;
134
+ try {
135
+ const url = new URL(value);
136
+ return url.protocol === "https:" &&
137
+ url.origin === assetOrigin &&
138
+ url.pathname.startsWith("/assets/") &&
139
+ url.pathname.length > "/assets/".length &&
140
+ !url.username &&
141
+ !url.password
142
+ ? url.href
143
+ : null;
144
+ } catch {
145
+ return null;
146
+ }
147
+ };
148
+ const notify = (method, params) => {
149
+ if (!disposed)
150
+ window.parent.postMessage({ jsonrpc: "2.0", method, params }, "*");
151
+ };
152
+ const reportSize = () => {
153
+ if (disposed) return;
154
+ // Measure content, not the host's previous iframe height, so errors
155
+ // and smaller follow-up results can shrink instead of keeping a gap.
156
+ const height = Math.ceil(root.getBoundingClientRect().height);
157
+ if (hostReady) notify("ui/notifications/size-changed", { height });
158
+ if (typeof window.openai?.notifyIntrinsicHeight === "function")
159
+ window.openai.notifyIntrinsicHeight(height);
160
+ };
161
+ function cleanupMedia() {
162
+ media.querySelectorAll("video").forEach((video) => {
163
+ video.pause();
164
+ video.removeAttribute("src");
165
+ video.load();
166
+ });
167
+ media.replaceChildren();
168
+ }
169
+ function setState(state, message) {
170
+ root.dataset.state = state;
171
+ root.setAttribute(
172
+ "aria-busy",
173
+ state === "loading" ? "true" : "false",
174
+ );
175
+ status.textContent = message;
176
+ status.hidden = !message;
177
+ cleanupMedia();
178
+ }
179
+ function download(url, label, testId) {
180
+ const link = document.createElement("a");
181
+ link.href = url;
182
+ link.textContent = label;
183
+ link.target = "_blank";
184
+ link.rel = "noopener noreferrer";
185
+ link.dataset.testid = testId;
186
+ return link;
187
+ }
188
+ function addMedia(item, kind, index) {
189
+ const figure = document.createElement("figure");
190
+ const element = document.createElement(kind);
191
+ element.src = item.url;
192
+ element.dataset.testid = `task-results-${kind}-${index}`;
193
+ if (kind === "img")
194
+ element.alt =
195
+ typeof item.alt === "string"
196
+ ? item.alt.slice(0, 160)
197
+ : "Ad creative";
198
+ else {
199
+ element.setAttribute(
200
+ "aria-label",
201
+ typeof item.label === "string"
202
+ ? item.label.slice(0, 120)
203
+ : "ProductMaker video",
204
+ );
205
+ element.controls = true;
206
+ element.preload = "metadata";
207
+ element.playsInline = true;
208
+ }
209
+ const fallback = document.createElement("p");
210
+ fallback.className = "fallback";
211
+ fallback.hidden = true;
212
+ fallback.textContent =
213
+ "Preview unavailable. Ask ChatGPT to show it again for a fresh link, or try the download link.";
214
+ element.addEventListener("error", () => {
215
+ element.hidden = true;
216
+ fallback.hidden = false;
217
+ reportSize();
218
+ });
219
+ element.addEventListener("load", reportSize);
220
+ element.addEventListener("loadedmetadata", reportSize);
221
+ const caption = document.createElement("figcaption");
222
+ caption.append(
223
+ download(
224
+ item.url,
225
+ kind === "img" ? "Download image" : "Download video",
226
+ `task-results-download-${kind}-${index}`,
227
+ ),
228
+ );
229
+ figure.append(element, fallback, caption);
230
+ if (kind === "video" && item.scriptRewritten === true) {
231
+ const warning = document.createElement("p");
232
+ warning.dataset.testid = `task-results-script-warning-${index}`;
233
+ warning.textContent =
234
+ "This video uses a revised script instead of the one you approved.";
235
+ figure.append(warning);
236
+ }
237
+ media.append(figure);
238
+ }
239
+ function entries(value) {
240
+ return Array.isArray(value)
241
+ ? value.filter(isRecord).flatMap((item) => {
242
+ const url = safeUrl(item.url);
243
+ return url
244
+ ? [
245
+ {
246
+ url,
247
+ alt: item.alt,
248
+ label: item.label,
249
+ ...(item.scriptRewritten === true
250
+ ? { scriptRewritten: true }
251
+ : {}),
252
+ },
253
+ ]
254
+ : [];
255
+ })
256
+ : [];
257
+ }
258
+ function applyResult(result) {
259
+ if (disposed) return;
260
+ receivedResult = true;
261
+ window.clearTimeout(resultTimeout);
262
+ const metadata =
263
+ isRecord(result) && isRecord(result._meta) ? result._meta : null;
264
+ const data = metadata?.taskResults;
265
+ if (
266
+ !isRecord(result) ||
267
+ result.isError ||
268
+ !isRecord(data) ||
269
+ !resultStates.has(data.state)
270
+ ) {
271
+ setState(
272
+ "error",
273
+ "We couldn't load this result. Ask ChatGPT to show it again.",
274
+ );
275
+ reportSize();
276
+ return;
277
+ }
278
+ if (data.state === "awaiting_review") {
279
+ setState(
280
+ data.state,
281
+ "Your approval is needed. Reply in the chat to continue.",
282
+ );
283
+ reportSize();
284
+ return;
285
+ }
286
+ if (data.state === "pending" || data.state === "error") {
287
+ setState(
288
+ data.state,
289
+ data.state === "pending"
290
+ ? "Your creative is still being prepared. Ask ChatGPT to check its progress."
291
+ : "This task couldn't deliver a result. Ask ChatGPT to check its status.",
292
+ );
293
+ reportSize();
294
+ return;
295
+ }
296
+ const images = entries(data.images);
297
+ const videos = entries(data.videos);
298
+ const links = entries(data.links);
299
+ if (
300
+ data.state === "empty" ||
301
+ !(images.length || videos.length || links.length)
302
+ ) {
303
+ setState(
304
+ "empty",
305
+ "No preview is available here. Your result can still be opened in ProductMaker.",
306
+ );
307
+ } else {
308
+ setState("ready", "");
309
+ images.forEach((item, index) => addMedia(item, "img", index));
310
+ videos.forEach((item, index) => addMedia(item, "video", index));
311
+ if (links.length) {
312
+ const list = document.createElement("ul");
313
+ links.forEach((item, index) => {
314
+ const row = document.createElement("li");
315
+ row.append(
316
+ download(
317
+ item.url,
318
+ typeof item.label === "string"
319
+ ? item.label.slice(0, 120)
320
+ : "Open result",
321
+ `task-results-link-${index}`,
322
+ ),
323
+ );
324
+ list.append(row);
325
+ });
326
+ media.append(list);
327
+ }
328
+ }
329
+ reportSize();
330
+ }
331
+ function applyOpenAiMetadata(metadata) {
332
+ if (!isRecord(metadata) || disposed) return;
333
+ // ChatGPT preserves the MCP envelope in toolResponseMetadata:
334
+ // https://developers.openai.com/plugins/reference#capabilities
335
+ // Read only the known wrappers; never search arbitrary nested data.
336
+ const callResult = isRecord(metadata.call_tool_result)
337
+ ? metadata.call_tool_result
338
+ : null;
339
+ const candidates = [
340
+ metadata.mcp_tool_result,
341
+ callResult?.mcp_tool_result,
342
+ callResult,
343
+ metadata,
344
+ { _meta: metadata },
345
+ ];
346
+ const result = candidates.find((candidate) => {
347
+ if (!isRecord(candidate)) return false;
348
+ if (candidate.isError === true) return true;
349
+ const data = isRecord(candidate._meta)
350
+ ? candidate._meta.taskResults
351
+ : null;
352
+ return (
353
+ isRecord(data) &&
354
+ resultStates.has(data.state) &&
355
+ Array.isArray(data.images) &&
356
+ Array.isArray(data.videos) &&
357
+ Array.isArray(data.links)
358
+ );
359
+ });
360
+ // Globals can arrive before a result or with unrelated host updates.
361
+ // Keep delivered playback until a valid result or explicit error.
362
+ if (result) applyResult(result);
363
+ }
364
+ function handleMessage(event) {
365
+ if (
366
+ event.source !== window.parent ||
367
+ !isRecord(event.data) ||
368
+ event.data.jsonrpc !== "2.0" ||
369
+ disposed
370
+ )
371
+ return;
372
+ const message = event.data;
373
+ if (message.method === "ui/resource-teardown") {
374
+ cleanupMedia();
375
+ window.clearTimeout(initializeTimeout);
376
+ window.clearTimeout(resultTimeout);
377
+ window.removeEventListener("message", handleMessage);
378
+ window.removeEventListener("openai:set_globals", handleGlobals);
379
+ window.removeEventListener("resize", reportSize);
380
+ if (Object.prototype.hasOwnProperty.call(message, "id")) {
381
+ window.parent.postMessage(
382
+ { jsonrpc: "2.0", id: message.id, result: {} },
383
+ "*",
384
+ );
385
+ }
386
+ disposed = true;
387
+ } else if (message.method === "ping") {
388
+ if (
389
+ (typeof message.id === "string" ||
390
+ Number.isInteger(message.id)) &&
391
+ !Object.prototype.hasOwnProperty.call(message, "result") &&
392
+ !Object.prototype.hasOwnProperty.call(message, "error") &&
393
+ (message.params === undefined || isRecord(message.params))
394
+ )
395
+ window.parent.postMessage(
396
+ { jsonrpc: "2.0", id: message.id, result: {} },
397
+ "*",
398
+ );
399
+ } else if (
400
+ !initializationSettled &&
401
+ message.id === initializeRequestId &&
402
+ !Object.prototype.hasOwnProperty.call(message, "method") &&
403
+ ((isRecord(message.result) &&
404
+ !Object.prototype.hasOwnProperty.call(message, "error")) ||
405
+ (isRecord(message.error) &&
406
+ Number.isInteger(message.error.code) &&
407
+ typeof message.error.message === "string" &&
408
+ !Object.prototype.hasOwnProperty.call(message, "result")))
409
+ ) {
410
+ initializationSettled = true;
411
+ window.clearTimeout(initializeTimeout);
412
+ if (message.error) {
413
+ if (!receivedResult) applyResult({ isError: true });
414
+ } else {
415
+ hostReady = true;
416
+ notify("ui/notifications/initialized", {});
417
+ reportSize();
418
+ }
419
+ } else if (message.method === "ui/notifications/tool-result")
420
+ applyResult(message.params);
421
+ else if (message.method === "ui/notifications/tool-cancelled")
422
+ applyResult({ isError: true });
423
+ }
424
+ function handleGlobals(event) {
425
+ const globals = event.detail?.globals;
426
+ if (
427
+ isRecord(globals) &&
428
+ Object.prototype.hasOwnProperty.call(
429
+ globals,
430
+ "toolResponseMetadata",
431
+ )
432
+ )
433
+ applyOpenAiMetadata(globals.toolResponseMetadata);
434
+ }
435
+ const resultTimeout = window.setTimeout(() => {
436
+ if (!receivedResult) applyResult({ isError: true });
437
+ }, 10000);
438
+ const initializeTimeout = window.setTimeout(() => {
439
+ if (!receivedResult) applyResult({ isError: true });
440
+ }, 8000);
441
+ window.addEventListener("message", handleMessage);
442
+ window.addEventListener("openai:set_globals", handleGlobals);
443
+ window.addEventListener("resize", reportSize);
444
+ window.parent.postMessage(
445
+ {
446
+ jsonrpc: "2.0",
447
+ id: initializeRequestId,
448
+ method: "ui/initialize",
449
+ params: {
450
+ appInfo: { name: "ProductMaker task results", version: "1.0.0" },
451
+ appCapabilities: { availableDisplayModes: ["inline"] },
452
+ protocolVersion: "2026-01-26",
453
+ },
454
+ },
455
+ "*",
456
+ );
457
+ applyOpenAiMetadata(window.openai?.toolResponseMetadata);
458
+ })();
459
+ </script>
460
+ </body>
461
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "12.1.0",
3
+ "version": "12.1.2",
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",