@productmaker/mcp 0.1.5

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/stdio.js ADDED
@@ -0,0 +1,843 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
4
+
5
+ // src/stdio.ts
6
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
+
8
+ // src/server.ts
9
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
+
11
+ // src/api-client.ts
12
+ var ApiHttpError = class extends Error {
13
+ static {
14
+ __name(this, "ApiHttpError");
15
+ }
16
+ status;
17
+ body;
18
+ constructor(status, body, message) {
19
+ super(message), this.status = status, this.body = body;
20
+ this.name = "ApiHttpError";
21
+ }
22
+ };
23
+ var ApiClient = class {
24
+ static {
25
+ __name(this, "ApiClient");
26
+ }
27
+ baseUrl;
28
+ apiKey;
29
+ constructor(baseUrl, apiKey) {
30
+ this.baseUrl = baseUrl;
31
+ this.apiKey = apiKey;
32
+ }
33
+ authHeaders() {
34
+ return {
35
+ Authorization: `Bearer ${this.apiKey}`
36
+ };
37
+ }
38
+ async getJson(path) {
39
+ const res = await fetch(`${this.baseUrl}${path}`, {
40
+ headers: this.authHeaders()
41
+ });
42
+ return this.parse(res);
43
+ }
44
+ async postJson(path, body) {
45
+ const res = await fetch(`${this.baseUrl}${path}`, {
46
+ method: "POST",
47
+ headers: {
48
+ ...this.authHeaders(),
49
+ "Content-Type": "application/json"
50
+ },
51
+ body: JSON.stringify(body)
52
+ });
53
+ return this.parse(res);
54
+ }
55
+ async patchJson(path, body) {
56
+ const res = await fetch(`${this.baseUrl}${path}`, {
57
+ method: "PATCH",
58
+ headers: {
59
+ ...this.authHeaders(),
60
+ "Content-Type": "application/json"
61
+ },
62
+ body: JSON.stringify(body)
63
+ });
64
+ return this.parse(res);
65
+ }
66
+ async deleteJson(path) {
67
+ const res = await fetch(`${this.baseUrl}${path}`, {
68
+ method: "DELETE",
69
+ headers: this.authHeaders()
70
+ });
71
+ return this.parse(res);
72
+ }
73
+ async postMultipart(path, files, fields) {
74
+ const form = new FormData();
75
+ for (const [name, file] of Object.entries(files)) {
76
+ const blob = new Blob([
77
+ new Uint8Array(file.buffer)
78
+ ], {
79
+ type: file.mimetype
80
+ });
81
+ form.append(name, blob, file.filename);
82
+ }
83
+ for (const [k, v] of Object.entries(fields)) {
84
+ if (v !== void 0) form.append(k, v);
85
+ }
86
+ const res = await fetch(`${this.baseUrl}${path}`, {
87
+ method: "POST",
88
+ headers: this.authHeaders(),
89
+ body: form
90
+ });
91
+ return this.parse(res);
92
+ }
93
+ async parse(res) {
94
+ if (!res.ok) {
95
+ let body;
96
+ try {
97
+ body = await res.json();
98
+ } catch {
99
+ body = await res.text().catch(() => "");
100
+ }
101
+ throw new ApiHttpError(res.status, body, `HTTP ${res.status} from ${res.url}`);
102
+ }
103
+ return await res.json();
104
+ }
105
+ };
106
+
107
+ // src/index.ts
108
+ var MCP_VERSION = "0.1.0";
109
+
110
+ // src/tools/tasks.ts
111
+ import { z as z2 } from "zod";
112
+
113
+ // src/resolve-image.ts
114
+ var MAX_BYTES = 20 * 1024 * 1024;
115
+ var ALLOWED = /* @__PURE__ */ new Set([
116
+ "image/jpeg",
117
+ "image/png",
118
+ "image/webp"
119
+ ]);
120
+ var ResolveImageError = class extends Error {
121
+ static {
122
+ __name(this, "ResolveImageError");
123
+ }
124
+ code;
125
+ constructor(code, message) {
126
+ super(message), this.code = code;
127
+ this.name = "ResolveImageError";
128
+ }
129
+ };
130
+ function sniffMime(buf) {
131
+ if (buf.length < 4) return null;
132
+ if (buf.readUInt8(0) === 137 && buf.readUInt8(1) === 80 && buf.readUInt8(2) === 78 && buf.readUInt8(3) === 71) return "image/png";
133
+ if (buf.readUInt8(0) === 255 && buf.readUInt8(1) === 216 && buf.readUInt8(2) === 255) return "image/jpeg";
134
+ if (buf.length < 12) return null;
135
+ if (buf.readUInt8(0) === 82 && buf.readUInt8(1) === 73 && buf.readUInt8(2) === 70 && buf.readUInt8(3) === 70 && buf.readUInt8(8) === 87 && buf.readUInt8(9) === 69 && buf.readUInt8(10) === 66 && buf.readUInt8(11) === 80) return "image/webp";
136
+ return null;
137
+ }
138
+ __name(sniffMime, "sniffMime");
139
+ function isPrivateHost(host) {
140
+ if (host === "localhost" || host === "0.0.0.0") return true;
141
+ if (host === "metadata.google.internal" || host === "169.254.169.254") return true;
142
+ if (/^127\./.test(host)) return true;
143
+ if (/^10\./.test(host)) return true;
144
+ if (/^192\.168\./.test(host)) return true;
145
+ if (/^172\.(1[6-9]|2\d|3[0-1])\./.test(host)) return true;
146
+ if (host.startsWith("169.254.")) return true;
147
+ if (host === "[::1]" || host === "::1") return true;
148
+ if (/^\[?f[cd][0-9a-f]{2}:/i.test(host)) return true;
149
+ if (/^\[?fe[89ab][0-9a-f]:/i.test(host)) return true;
150
+ return false;
151
+ }
152
+ __name(isPrivateHost, "isPrivateHost");
153
+ async function resolveImage(input) {
154
+ if (input.imageUrl && input.imageBase64) {
155
+ throw new ResolveImageError("INVALID_INPUT", "Provide only one of imageUrl or imageBase64");
156
+ }
157
+ if (input.imageUrl) {
158
+ let u;
159
+ try {
160
+ u = new URL(input.imageUrl);
161
+ } catch {
162
+ throw new ResolveImageError("INVALID_INPUT", "imageUrl is not a valid URL");
163
+ }
164
+ if (u.protocol !== "https:") throw new ResolveImageError("INVALID_INPUT", "imageUrl must be HTTPS");
165
+ if (isPrivateHost(u.hostname)) throw new ResolveImageError("INVALID_INPUT", "imageUrl host not allowed");
166
+ let res;
167
+ try {
168
+ res = await fetch(input.imageUrl, {
169
+ signal: AbortSignal.timeout(3e4),
170
+ redirect: "error"
171
+ });
172
+ } catch (e) {
173
+ throw new ResolveImageError("IMAGE_DOWNLOAD_FAILED", e.message);
174
+ }
175
+ if (!res.ok) throw new ResolveImageError("IMAGE_DOWNLOAD_FAILED", `HTTP ${res.status}`);
176
+ const cl = res.headers.get("content-length");
177
+ if (cl && Number(cl) > MAX_BYTES) {
178
+ throw new ResolveImageError("IMAGE_TOO_LARGE", `content-length ${cl}`);
179
+ }
180
+ const ct = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
181
+ if (!ALLOWED.has(ct)) throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `content-type ${ct}`);
182
+ const buf = Buffer.from(await res.arrayBuffer());
183
+ if (buf.length > MAX_BYTES) throw new ResolveImageError("IMAGE_TOO_LARGE", `${buf.length} bytes`);
184
+ const sniffed = sniffMime(buf);
185
+ if (!sniffed || sniffed !== ct) {
186
+ throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `magic bytes (${sniffed ?? "unknown"}) do not match content-type (${ct})`);
187
+ }
188
+ return {
189
+ buffer: buf,
190
+ mimetype: sniffed
191
+ };
192
+ }
193
+ if (input.imageBase64) {
194
+ const buf = Buffer.from(input.imageBase64, "base64");
195
+ if (buf.length > MAX_BYTES) throw new ResolveImageError("IMAGE_TOO_LARGE", `${buf.length} bytes`);
196
+ const mime = sniffMime(buf);
197
+ if (!mime) throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", "cannot detect image type");
198
+ return {
199
+ buffer: buf,
200
+ mimetype: mime
201
+ };
202
+ }
203
+ throw new ResolveImageError("INVALID_INPUT", "imageUrl or imageBase64 required");
204
+ }
205
+ __name(resolveImage, "resolveImage");
206
+
207
+ // src/status-poller.ts
208
+ var TERMINAL = /* @__PURE__ */ new Set([
209
+ "complete",
210
+ "error"
211
+ ]);
212
+ async function pollStatus(fetcher, waitSeconds, signal, opts = {}) {
213
+ const intervalMs = opts.intervalMs ?? 3e3;
214
+ const maxWait = opts.maxWaitSeconds ?? 120;
215
+ const effective = Math.min(Math.max(0, waitSeconds), maxWait);
216
+ let snapshot = await fetcher();
217
+ if (effective === 0) return snapshot;
218
+ if (TERMINAL.has(snapshot.status) || snapshot.status === "awaiting_review") return snapshot;
219
+ const deadline = Date.now() + effective * 1e3;
220
+ while (Date.now() < deadline) {
221
+ if (signal?.aborted) throw new Error("aborted");
222
+ await sleep(intervalMs, signal);
223
+ const next = await fetcher();
224
+ if (next.status !== snapshot.status || next.phase !== snapshot.phase || TERMINAL.has(next.status) || next.status === "awaiting_review") {
225
+ return next;
226
+ }
227
+ snapshot = next;
228
+ }
229
+ return snapshot;
230
+ }
231
+ __name(pollStatus, "pollStatus");
232
+ function sleep(ms, signal) {
233
+ return new Promise((resolve, reject) => {
234
+ const onAbort = /* @__PURE__ */ __name(() => {
235
+ clearTimeout(t);
236
+ reject(new Error("aborted"));
237
+ }, "onAbort");
238
+ const t = setTimeout(() => {
239
+ signal?.removeEventListener("abort", onAbort);
240
+ resolve();
241
+ }, ms);
242
+ signal?.addEventListener("abort", onAbort, {
243
+ once: true
244
+ });
245
+ });
246
+ }
247
+ __name(sleep, "sleep");
248
+
249
+ // src/errors.ts
250
+ import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
251
+ var McpErrorCode = {
252
+ INVALID_API_KEY: "INVALID_API_KEY",
253
+ REVOKED_API_KEY: "REVOKED_API_KEY",
254
+ RATE_LIMITED: "RATE_LIMITED",
255
+ INVALID_INPUT: "INVALID_INPUT",
256
+ IMAGE_DOWNLOAD_FAILED: "IMAGE_DOWNLOAD_FAILED",
257
+ IMAGE_TOO_LARGE: "IMAGE_TOO_LARGE",
258
+ IMAGE_TYPE_UNSUPPORTED: "IMAGE_TYPE_UNSUPPORTED",
259
+ TASK_NOT_FOUND: "TASK_NOT_FOUND",
260
+ TASK_NOT_READY: "TASK_NOT_READY",
261
+ NO_SHOPIFY_SHOPS: "NO_SHOPIFY_SHOPS",
262
+ NO_META_AD_ACCOUNTS: "NO_META_AD_ACCOUNTS",
263
+ SHOPIFY_NOT_CONNECTED: "SHOPIFY_NOT_CONNECTED",
264
+ META_CAPABILITY_MISSING: "META_CAPABILITY_MISSING",
265
+ UPSTREAM_TIMEOUT: "UPSTREAM_TIMEOUT",
266
+ UPSTREAM_UNAVAILABLE: "UPSTREAM_UNAVAILABLE",
267
+ INTERNAL: "INTERNAL"
268
+ };
269
+ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
270
+ McpErrorCode.INVALID_INPUT,
271
+ McpErrorCode.INVALID_API_KEY,
272
+ McpErrorCode.REVOKED_API_KEY
273
+ ]);
274
+ function mcp(code, message, extra = {}) {
275
+ const errCode = INVALID_PARAM_CODES.has(code) ? ErrorCode.InvalidParams : ErrorCode.InternalError;
276
+ return new McpError(errCode, message, {
277
+ code,
278
+ ...extra
279
+ });
280
+ }
281
+ __name(mcp, "mcp");
282
+ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
283
+ "META_CAPABILITY_MISSING",
284
+ "NO_SHOPIFY_SHOPS",
285
+ "NO_META_AD_ACCOUNTS",
286
+ "SHOPIFY_NOT_CONNECTED",
287
+ "REVOKED_API_KEY"
288
+ ]);
289
+ function isRecord(v) {
290
+ return typeof v === "object" && v !== null && !Array.isArray(v);
291
+ }
292
+ __name(isRecord, "isRecord");
293
+ function translateError(err) {
294
+ if (err instanceof ResolveImageError) {
295
+ return mcp(McpErrorCode[err.code], err.message);
296
+ }
297
+ if (err instanceof ApiHttpError) {
298
+ const body = isRecord(err.body) ? err.body : {};
299
+ if (typeof body.code === "string" && PASSTHROUGH_CODES.has(body.code)) {
300
+ return mcp(body.code, String(body.message ?? body.code), {
301
+ ...body.helpUrl ? {
302
+ helpUrl: body.helpUrl
303
+ } : {}
304
+ });
305
+ }
306
+ if (err.status === 401) {
307
+ return mcp(McpErrorCode.INVALID_API_KEY, "API key inv\xE1lida. Revisa tu PM_API_KEY en la configuraci\xF3n del cliente MCP.");
308
+ }
309
+ if (err.status === 429) {
310
+ return mcp(McpErrorCode.RATE_LIMITED, "Demasiadas solicitudes. Espera unos segundos.", {
311
+ retryAfterSeconds: Number(body.retryAfterSeconds ?? 30)
312
+ });
313
+ }
314
+ if (err.status === 400) {
315
+ return mcp(McpErrorCode.INVALID_INPUT, typeof body.message === "string" ? body.message : "Entrada inv\xE1lida.");
316
+ }
317
+ if (err.status === 404) {
318
+ const isTask = body.resource === "task" || /task/i.test(String(body.message ?? ""));
319
+ return mcp(isTask ? McpErrorCode.TASK_NOT_FOUND : McpErrorCode.INVALID_INPUT, "Recurso no encontrado.");
320
+ }
321
+ if (err.status === 409) {
322
+ return mcp(McpErrorCode.TASK_NOT_READY, "La tarea a\xFAn no est\xE1 lista para esta acci\xF3n.");
323
+ }
324
+ if (err.status >= 500) {
325
+ return mcp(McpErrorCode.UPSTREAM_UNAVAILABLE, "El servicio Product Maker no respondi\xF3. Intenta de nuevo en un momento.");
326
+ }
327
+ return mcp(McpErrorCode.INTERNAL, `HTTP ${err.status}`);
328
+ }
329
+ if (err instanceof Error && /aborted|timeout/i.test(err.message)) {
330
+ return mcp(McpErrorCode.UPSTREAM_TIMEOUT, "Tiempo de espera agotado.");
331
+ }
332
+ return mcp(McpErrorCode.INTERNAL, err instanceof Error ? err.message : "Error interno.");
333
+ }
334
+ __name(translateError, "translateError");
335
+
336
+ // src/tools/shared.ts
337
+ import { z } from "zod";
338
+ var Lang = z.string().min(2).max(10);
339
+ var Country = z.string().min(2).max(3);
340
+ var VideoStyleEnum = z.enum([
341
+ "ugc",
342
+ "cartoon_3d",
343
+ "product_only",
344
+ "ai_dynamic"
345
+ ]);
346
+ var NarrativeStyleEnum = z.enum([
347
+ "review",
348
+ "problem_product_cta",
349
+ "product_showcase",
350
+ "metaphor_product_cta",
351
+ "wearable_showcase"
352
+ ]);
353
+ var VideoModelEnum = z.enum([
354
+ "veo",
355
+ "wan",
356
+ "bytedance",
357
+ "seedance",
358
+ "infinitalk",
359
+ "grok",
360
+ "kling"
361
+ ]);
362
+ var ImageBase64 = z.string().max(75e5);
363
+ function asText(value) {
364
+ return {
365
+ content: [
366
+ {
367
+ type: "text",
368
+ text: JSON.stringify(value)
369
+ }
370
+ ]
371
+ };
372
+ }
373
+ __name(asText, "asText");
374
+ function flatten(obj) {
375
+ const out = {};
376
+ for (const [k, v] of Object.entries(obj)) {
377
+ if (v === void 0) continue;
378
+ if (Array.isArray(v)) out[k] = v.join(",");
379
+ else if (typeof v === "boolean") out[k] = String(v);
380
+ else out[k] = String(v);
381
+ }
382
+ return out;
383
+ }
384
+ __name(flatten, "flatten");
385
+ async function submitCreative(api, opts) {
386
+ const { imageUrl, imageBase64, ...rest } = opts.input;
387
+ const img = await resolveImage({
388
+ imageUrl,
389
+ imageBase64
390
+ });
391
+ const r = await api.postMultipart(opts.path, {
392
+ [opts.fileField]: {
393
+ ...img,
394
+ filename: "image.png"
395
+ }
396
+ }, flatten(rest));
397
+ return asText({
398
+ taskId: r.taskId,
399
+ status: "queued",
400
+ estimatedSeconds: opts.estimatedSeconds
401
+ });
402
+ }
403
+ __name(submitCreative, "submitCreative");
404
+
405
+ // src/tools/task-outputs.ts
406
+ var WEBAPP_BASE = "https://productmaker.app";
407
+ async function fetchTaskOutputs(api, taskId) {
408
+ const raw = await api.getJson(`/v1/results/${encodeURIComponent(taskId)}?signedUrls=true`);
409
+ const videos = (raw.videoGenerators ?? []).map((vg) => {
410
+ const variants = (vg.variants ?? []).filter((v) => Boolean(v.kind) && Boolean(v.url)).map((v) => ({
411
+ kind: v.kind,
412
+ url: v.url
413
+ }));
414
+ const fallback = !variants.length && Array.isArray(vg.resultUrls) && vg.resultUrls.length ? vg.resultUrls.map((url) => ({
415
+ kind: "mp4",
416
+ url
417
+ })) : [];
418
+ return {
419
+ angleKey: vg.angleKey ?? "",
420
+ angleIndex: vg.angleIndex ?? 0,
421
+ variants: variants.length ? variants : fallback
422
+ };
423
+ }).filter((v) => v.angleKey && v.variants.length);
424
+ const sections = (raw.landing?.images ?? []).filter((img) => Boolean(img.role) && Boolean(img.url)).map((img) => ({
425
+ role: img.role,
426
+ url: img.url
427
+ }));
428
+ const images = [];
429
+ if (raw.adCreative?.url) {
430
+ images.push({
431
+ url: raw.adCreative.url,
432
+ width: raw.adCreative.width,
433
+ height: raw.adCreative.height
434
+ });
435
+ }
436
+ for (const variant of raw.adCreative?.variants ?? []) {
437
+ if (typeof variant === "string") images.push({
438
+ url: variant
439
+ });
440
+ }
441
+ return {
442
+ webappUrl: `${WEBAPP_BASE}/tasks/${taskId}`,
443
+ videos,
444
+ landing: {
445
+ sections
446
+ },
447
+ images
448
+ };
449
+ }
450
+ __name(fetchTaskOutputs, "fetchTaskOutputs");
451
+ function escapeMdLabel(s) {
452
+ return s.replace(/[\[\]()]/g, "");
453
+ }
454
+ __name(escapeMdLabel, "escapeMdLabel");
455
+ function renderOutputsMarkdown(outputs) {
456
+ const parts = [];
457
+ parts.push(`\u2705 **Tu tarea est\xE1 lista** \u2014 [Abrir en Product Maker](${outputs.webappUrl})`);
458
+ if (outputs.images.length) {
459
+ parts.push("\n### Ad creative");
460
+ for (const img of outputs.images) parts.push(`![](${img.url})`);
461
+ }
462
+ if (outputs.landing.sections.length) {
463
+ parts.push(`
464
+ ### Landing (${outputs.landing.sections.length} secciones)`);
465
+ for (const s of outputs.landing.sections) parts.push(`![${escapeMdLabel(s.role)}](${s.url})`);
466
+ }
467
+ if (outputs.videos.length) {
468
+ parts.push("\n### Video");
469
+ for (const v of outputs.videos) {
470
+ for (const variant of v.variants) {
471
+ parts.push(`- [${escapeMdLabel(v.angleKey)} \xB7 ${escapeMdLabel(variant.kind)}](${variant.url})`);
472
+ }
473
+ }
474
+ }
475
+ return parts.join("\n");
476
+ }
477
+ __name(renderOutputsMarkdown, "renderOutputsMarkdown");
478
+
479
+ // src/tools/tasks.ts
480
+ var CreateProductTaskInput = z2.object({
481
+ imageUrl: z2.string().url().optional().describe("URL p\xFAblica de la foto del producto. Provee este O imageBase64 (uno solo). Requerido si no se pasa imageBase64."),
482
+ imageBase64: ImageBase64.optional().describe("Imagen del producto en base64 (sin prefijo data:). Provee este O imageUrl (uno solo)."),
483
+ productUrl: z2.string().url().optional().describe("Opcional. URL de la p\xE1gina del producto (Shopify, AliExpress, Amazon, etc.) para enriquecer el contexto. Si se omite, la IA infiere todo desde la imagen."),
484
+ language: Lang.describe('C\xF3digo de idioma para el contenido generado. Ejemplos: "es" (espa\xF1ol), "en" (ingl\xE9s), "pt" (portugu\xE9s). Si el usuario no lo especific\xF3, preg\xFAntale o usa "es" como default LATAM.'),
485
+ country: Country.describe('C\xF3digo ISO del pa\xEDs objetivo. Ejemplos: "CO" (Colombia), "MX" (M\xE9xico), "US" (Estados Unidos), "AR" (Argentina). Define moneda, m\xE9todos de pago locales y tono. Si el usuario no lo especific\xF3, preg\xFAntale.'),
486
+ price: z2.string().optional().describe('Opcional. Precio que se mostrar\xE1 en la landing y video. Cualquier formato libre \u2014 ej. "$89.900", "USD 29.99", "MXN 599". Si se omite, la IA sugiere uno basado en producto+pa\xEDs.'),
487
+ offer: z2.string().optional().describe('Opcional. Oferta o promoci\xF3n a destacar. Ej. "2x1", "Env\xEDo gratis hoy", "50% OFF por lanzamiento". Si se omite, la IA decide si a\xF1adir una.'),
488
+ videoStyle: VideoStyleEnum.optional().describe('Opcional. Estilo visual del video. "ugc" = creador hablando a c\xE1mara (default, mejor CTR LATAM). "cartoon_3d" = animaci\xF3n 3D. "product_only" = sin actor. "ai_dynamic" = cinematogr\xE1fico AI. Si se omite, default "ugc".'),
489
+ narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" = testimonial (default, alta conversi\xF3n). "problem_product_cta" = problema\u2192soluci\xF3n. "product_showcase" = features. "metaphor_product_cta" = met\xE1fora visual. "wearable_showcase" = para ropa/accesorios. Si se omite, default "review".'),
490
+ videoModel: VideoModelEnum.optional().describe('Opcional. Modelo de video. "veo" (default, calidad Google). "bytedance"/"seedance" = TikTok-style. "kling"/"wan" = alternativos. "infinitalk" = lipsync largo. "grok" = grok-imagine. Si se omite, el sistema escoge seg\xFAn costo/calidad.'),
491
+ multiAngleVideos: z2.boolean().optional().describe("Opcional. Si true, genera m\xFAltiples \xE1ngulos de venta en paralelo (3-5 videos). Si false/omitido, genera 1 video del \xE1ngulo principal. Default false."),
492
+ outputs: z2.array(z2.enum([
493
+ "landing",
494
+ "video",
495
+ "image"
496
+ ])).min(2).max(3).optional().describe('Opcional. Qu\xE9 outputs generar. Default: todos ["landing","video","image"]. Para 2 outputs: ["landing","image"] (sin video), ["video","image"] (sin landing), ["landing","video"] (sin imagen). Para 1 solo output, usa generate_landing / generate_video_creative / generate_image_creatives.')
497
+ });
498
+ var GetTaskStatusInput = z2.object({
499
+ taskId: z2.string().min(1),
500
+ waitSeconds: z2.number().int().min(0).max(120).optional()
501
+ });
502
+ var ListTasksInput = z2.object({
503
+ limit: z2.number().int().min(1).max(50).optional(),
504
+ offset: z2.number().int().min(0).optional(),
505
+ search: z2.string().optional()
506
+ });
507
+ var EditTaskDraftInput = z2.object({
508
+ taskId: z2.string().min(1),
509
+ angleIndex: z2.number().int().min(0),
510
+ angle: z2.object({
511
+ angle: z2.string().min(1).max(480),
512
+ buyerPersona: z2.string().min(1).max(360),
513
+ actor: z2.string().min(1).max(360).optional(),
514
+ gender: z2.enum([
515
+ "female",
516
+ "male"
517
+ ]).optional(),
518
+ scriptNotes: z2.string().max(2e3).optional()
519
+ })
520
+ });
521
+ function registerTaskTools(server, api) {
522
+ server.tool("create_product_task", 'Crea una tarea de producto desde una imagen. Por defecto genera los 3 outputs (landing + video + image creatives). Devuelve un taskId.\n\nINPUTS REQUERIDOS: imageUrl o imageBase64 (uno solo), language, country.\n\nINPUTS OPCIONALES (TODOS los de abajo se pueden omitir \u2014 si el usuario no los mencion\xF3, PREG\xDANTALE primero si quiere personalizar precio/oferta/estilo, o llama la tool sin ellos y la IA elige defaults sensatos):\n- price, offer (texto libre del precio y promoci\xF3n)\n- videoStyle, narrativeStyle, videoModel (enums \u2014 ver descripci\xF3n de cada campo para valores v\xE1lidos)\n- multiAngleVideos (booleano \u2014 generar varios \xE1ngulos en paralelo)\n- outputs (subconjunto de {landing,video,image})\n- productUrl (URL del producto en Shopify/AliExpress/etc.)\n\nNUNCA inventes valores fuera de los enums declarados. Si el usuario pide algo no soportado (ej. "estilo anime"), preg\xFAntale por una opci\xF3n v\xE1lida.', CreateProductTaskInput.shape, async (input) => {
523
+ try {
524
+ const img = await resolveImage(input);
525
+ const fields = flatten({
526
+ "options.languageCode": input.language,
527
+ "options.country": input.country,
528
+ "options.price": input.price,
529
+ "options.offer": input.offer,
530
+ "options.videoStyle": input.videoStyle,
531
+ "options.narrativeStyle": input.narrativeStyle,
532
+ "options.videoModel": input.videoModel,
533
+ "options.multiAngleVideos": input.multiAngleVideos,
534
+ "options.selectedOutputs": input.outputs,
535
+ url: input.productUrl
536
+ });
537
+ const r = await api.postMultipart("/v1/ingest", {
538
+ image: {
539
+ ...img,
540
+ filename: "image.png"
541
+ }
542
+ }, fields);
543
+ return asText({
544
+ taskId: r.taskId,
545
+ status: "queued",
546
+ estimatedSeconds: 300
547
+ });
548
+ } catch (e) {
549
+ throw translateError(e);
550
+ }
551
+ });
552
+ server.tool("get_task_status", "Devuelve el estado y URLs de outputs de una tarea. Si waitSeconds>0, hace polling hasta que cambie la fase o llegue al timeout (max 120s). Cuando la tarea queda en `done`, la respuesta incluye un segundo bloque markdown con previews inline de las im\xE1genes, links descargables al video MP4 y un enlace a Product Maker para ver el resultado completo. Use this to check on a task progress; pass waitSeconds for a near-synchronous response.", GetTaskStatusInput.shape, async (input, extra) => {
553
+ try {
554
+ const fetcher = /* @__PURE__ */ __name(() => api.getJson(`/v1/status/${encodeURIComponent(input.taskId)}`), "fetcher");
555
+ const signal = extra?.signal;
556
+ const snapshot = await pollStatus(fetcher, input.waitSeconds ?? 0, signal);
557
+ if (snapshot.status !== "done") return asText(snapshot);
558
+ try {
559
+ const outputs = await fetchTaskOutputs(api, input.taskId);
560
+ return {
561
+ content: [
562
+ {
563
+ type: "text",
564
+ text: JSON.stringify({
565
+ ...snapshot,
566
+ outputs
567
+ })
568
+ },
569
+ {
570
+ type: "text",
571
+ text: renderOutputsMarkdown(outputs)
572
+ }
573
+ ]
574
+ };
575
+ } catch (err) {
576
+ const detail = err instanceof Error ? err.stack ?? err.message : String(err);
577
+ process.stderr.write(`mcp get_task_status: fetchTaskOutputs failed for ${input.taskId}: ${detail}
578
+ `);
579
+ return asText(snapshot);
580
+ }
581
+ } catch (e) {
582
+ throw translateError(e);
583
+ }
584
+ });
585
+ server.tool("list_tasks", "Lista las tareas recientes del usuario. Soporta paginaci\xF3n con limit/offset y filtro de texto. Use this to find previous tasks by name or to browse the user history.", ListTasksInput.shape, async (input) => {
586
+ try {
587
+ const qs = new URLSearchParams();
588
+ if (input.limit != null) qs.set("limit", String(input.limit));
589
+ if (input.offset != null) qs.set("offset", String(input.offset));
590
+ if (input.search) qs.set("search", input.search);
591
+ const path = `/v1/tasks${qs.toString() ? "?" + qs.toString() : ""}`;
592
+ const r = await api.getJson(path);
593
+ return asText(r);
594
+ } catch (e) {
595
+ throw translateError(e);
596
+ }
597
+ });
598
+ server.tool("edit_task_draft", "Edita el draft de un angle de marketing antes de la generaci\xF3n. Campos editables: angle (texto del angle), buyerPersona, actor (descripci\xF3n del personaje), gender (female|male), scriptNotes. Use this only before generation completes. Editing already-generated images or videos is not supported.", EditTaskDraftInput.shape, async (input) => {
599
+ try {
600
+ const angle = await api.patchJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/angles/${input.angleIndex}`, input.angle ?? {});
601
+ return asText({
602
+ angle
603
+ });
604
+ } catch (e) {
605
+ throw translateError(e);
606
+ }
607
+ });
608
+ }
609
+ __name(registerTaskTools, "registerTaskTools");
610
+
611
+ // src/tools/creatives.ts
612
+ import { z as z3 } from "zod";
613
+ var ImageInputRefs = {
614
+ imageUrl: z3.string().url().optional().describe("URL p\xFAblica de la foto del producto. Provee este O imageBase64 (uno solo)."),
615
+ imageBase64: ImageBase64.optional().describe("Imagen del producto en base64 (sin prefijo data:). Provee este O imageUrl (uno solo)."),
616
+ language: Lang.describe('Idioma del contenido. Ej. "es", "en", "pt". Si el usuario no lo dijo, preg\xFAntale.'),
617
+ country: Country.describe('Pa\xEDs objetivo ISO. Ej. "CO", "MX", "US", "AR". Define moneda y tono. Si el usuario no lo dijo, preg\xFAntale.'),
618
+ price: z3.string().optional().describe('Opcional. Precio en formato libre \u2014 ej. "$89.900", "USD 29.99". Om\xEDtelo y la IA sugiere uno.'),
619
+ offer: z3.string().optional().describe('Opcional. Oferta a destacar \u2014 ej. "2x1", "Env\xEDo gratis", "50% OFF".')
620
+ };
621
+ var VideoInput = z3.object({
622
+ imageUrl: ImageInputRefs.imageUrl,
623
+ imageBase64: ImageInputRefs.imageBase64,
624
+ title: z3.string().min(1).describe('T\xEDtulo corto del producto. Aparece en la primera vista del video. Ej. "Crema Anti-Edad Premium".'),
625
+ primaryAngle: z3.string().min(1).describe('\xC1ngulo de venta principal (frase del beneficio). Ej. "Reduce arrugas en 14 d\xEDas" o "Para quienes odian las cremas grasosas".'),
626
+ language: ImageInputRefs.language,
627
+ country: ImageInputRefs.country,
628
+ aspectRatio: z3.enum([
629
+ "9:16",
630
+ "16:9",
631
+ "Auto"
632
+ ]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
633
+ durationSeconds: z3.number().int().min(5).max(60).optional().describe("Opcional. Duraci\xF3n del video en segundos (5-60). Default 15s. Para hooks cortos usa 8, para storytelling usa 30."),
634
+ narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" (testimonial, default), "problem_product_cta", "product_showcase", "metaphor_product_cta", "wearable_showcase".'),
635
+ videoStyle: VideoStyleEnum.optional().describe('Opcional. Estilo visual. "ugc" (creador hablando, default), "cartoon_3d", "product_only", "ai_dynamic".'),
636
+ videoModel: VideoModelEnum.optional().describe('Opcional. Modelo de video. "veo" (default, calidad Google). Ver lista de valores v\xE1lidos.'),
637
+ features: z3.array(z3.string()).optional().describe('Opcional. Lista de features/beneficios clave a mencionar. Ej. ["resistente al agua", "bater\xEDa 24h", "carga inal\xE1mbrica"].'),
638
+ price: ImageInputRefs.price,
639
+ offer: ImageInputRefs.offer,
640
+ cta: z3.string().optional().describe('Opcional. Call-to-action final del video. Ej. "Compra ahora", "Pide el tuyo hoy", "Link en bio". Si se omite, la IA escribe uno.'),
641
+ scriptNotes: z3.string().optional().describe('Opcional. Notas/restricciones para el guion. Ej. "No mencionar la marca X", "Tono divertido, no cl\xEDnico".')
642
+ });
643
+ var ImageInput = z3.object({
644
+ imageUrl: ImageInputRefs.imageUrl,
645
+ imageBase64: ImageInputRefs.imageBase64,
646
+ productTitle: z3.string().min(1).describe('Nombre del producto. Ej. "Crema Anti-Edad Premium".'),
647
+ primaryAngle: z3.string().min(1).describe('\xC1ngulo de venta principal. Ej. "Reduce arrugas en 14 d\xEDas".'),
648
+ language: ImageInputRefs.language,
649
+ country: ImageInputRefs.country,
650
+ nVariants: z3.number().int().min(1).max(6).optional().describe("Opcional. N\xFAmero de variantes a generar (1-6). Default 3."),
651
+ imageModel: z3.string().optional().describe('Opcional. Modelo de imagen. Ej. "gpt-image-2", "nano-banana-2". Om\xEDtelo para usar el default optimizado por costo/calidad.'),
652
+ imageStyle: z3.string().optional().describe('Opcional. Estilo visual libre. Ej. "minimalista fondo blanco", "lifestyle outdoor", "ne\xF3n cyberpunk".'),
653
+ generateAdCopy: z3.boolean().optional().describe("Opcional. Si true, tambi\xE9n genera headline/body/CTA para el ad. Default false."),
654
+ features: z3.array(z3.string()).optional().describe('Opcional. Features/beneficios a destacar visualmente. Ej. ["resistente al agua", "ultra liviano"].'),
655
+ brandColors: z3.array(z3.string()).optional().describe('Opcional. Paleta de marca en hex. Ej. ["#FF6B35", "#004E89"]. Si se omite, la IA elige seg\xFAn el producto.'),
656
+ price: ImageInputRefs.price,
657
+ offer: ImageInputRefs.offer
658
+ });
659
+ var LandingInput = z3.object({
660
+ imageUrl: ImageInputRefs.imageUrl,
661
+ imageBase64: ImageInputRefs.imageBase64,
662
+ language: ImageInputRefs.language,
663
+ country: ImageInputRefs.country,
664
+ salesAngle: z3.string().optional().describe('Opcional. \xC1ngulo de venta principal. Ej. "Reduce arrugas en 14 d\xEDas". Si se omite, la IA lo deduce de la imagen.'),
665
+ buyerPersona: z3.string().optional().describe('Opcional. Descripci\xF3n del comprador objetivo. Ej. "Mujeres 35-50, profesionales urbanas". Si se omite, la IA infiere.'),
666
+ price: ImageInputRefs.price,
667
+ offer: ImageInputRefs.offer,
668
+ paymentMethods: z3.string().optional().describe('Opcional. M\xE9todos de pago a mostrar (lista libre). Ej. "Tarjeta, PSE, Nequi, Contraentrega". Si se omite, la IA usa los m\xE1s comunes del pa\xEDs.'),
669
+ isCombo: z3.boolean().optional().describe("Opcional. Si true, la landing presenta el producto como combo/pack. Default false.")
670
+ });
671
+ function registerCreativeTools(server, api) {
672
+ server.tool("generate_video_creative", "Genera UN video creativo independiente. Devuelve un taskId.\n\nREQUERIDOS: imageUrl o imageBase64, title, primaryAngle, language, country, aspectRatio.\n\nOPCIONALES (si el usuario no los mencion\xF3, PREG\xDANTALE antes de llamar \u2014 o invoca sin ellos y la IA decide): durationSeconds, narrativeStyle, videoStyle, videoModel (enums \u2014 usa SOLO valores declarados), features, price, offer, cta, scriptNotes.\n\nNo inventes valores fuera de los enums; si el usuario pide algo no soportado, p\xEDdele una opci\xF3n v\xE1lida.", VideoInput.shape, async (input) => {
673
+ try {
674
+ return await submitCreative(api, {
675
+ path: "/v1/creatives/videos",
676
+ fileField: "media",
677
+ estimatedSeconds: 300,
678
+ input
679
+ });
680
+ } catch (e) {
681
+ throw translateError(e);
682
+ }
683
+ });
684
+ server.tool("generate_image_creatives", "Genera N variantes de imagen creativa (ads est\xE1ticos). Devuelve un taskId.\n\nREQUERIDOS: imageUrl o imageBase64, productTitle, primaryAngle, language, country.\n\nOPCIONALES (si el usuario no los mencion\xF3, PREG\xDANTALE \u2014 o invoca sin ellos y la IA decide): nVariants (default 3), imageModel, imageStyle (texto libre), generateAdCopy, features, brandColors (hex), price, offer.", ImageInput.shape, async (input) => {
685
+ try {
686
+ return await submitCreative(api, {
687
+ path: "/v1/creatives/images",
688
+ fileField: "image",
689
+ estimatedSeconds: 60,
690
+ input
691
+ });
692
+ } catch (e) {
693
+ throw translateError(e);
694
+ }
695
+ });
696
+ server.tool("generate_landing", "Genera una landing page lista para Shopify. Devuelve un taskId.\n\nREQUERIDOS: imageUrl o imageBase64, language, country.\n\nOPCIONALES (si el usuario no los mencion\xF3, PREG\xDANTALE primero por price/offer/paymentMethods \u2014 son los m\xE1s impactantes para conversi\xF3n \u2014 o invoca sin ellos y la IA elige): salesAngle, buyerPersona, price, offer, paymentMethods (texto libre con m\xE9todos locales del pa\xEDs), isCombo.", LandingInput.shape, async (input) => {
697
+ try {
698
+ return await submitCreative(api, {
699
+ path: "/v1/creatives/landing",
700
+ fileField: "image",
701
+ estimatedSeconds: 60,
702
+ input
703
+ });
704
+ } catch (e) {
705
+ throw translateError(e);
706
+ }
707
+ });
708
+ }
709
+ __name(registerCreativeTools, "registerCreativeTools");
710
+
711
+ // src/tools/connections.ts
712
+ import { z as z4 } from "zod";
713
+ function registerConnectionTools(server, api) {
714
+ server.tool("list_shopify_shops", "Lista las tiendas Shopify conectadas por este usuario. Si la lista est\xE1 vac\xEDa, dile al usuario que conecte una tienda en productmaker.app/integrations antes de intentar publicar.", z4.object({}).shape, async () => {
715
+ try {
716
+ const r = await api.getJson("/v1/shopify/shops");
717
+ return asText(r);
718
+ } catch (e) {
719
+ throw translateError(e);
720
+ }
721
+ });
722
+ server.tool("list_meta_ad_accounts", "Lista las cuentas publicitarias de Meta conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar Meta en productmaker.app/integrations.", z4.object({}).shape, async () => {
723
+ try {
724
+ const r = await api.getJson("/v1/facebook/ad-accounts");
725
+ return asText(r);
726
+ } catch (e) {
727
+ throw translateError(e);
728
+ }
729
+ });
730
+ }
731
+ __name(registerConnectionTools, "registerConnectionTools");
732
+
733
+ // src/tools/publish.ts
734
+ import { z as z5 } from "zod";
735
+ var ShopifyInput = z5.object({
736
+ taskId: z5.string().min(1),
737
+ shopDomain: z5.string().min(1),
738
+ numLandingImages: z5.number().int().min(1).max(7).optional(),
739
+ variantIndex: z5.number().int().min(0).optional(),
740
+ showVariantPicker: z5.boolean().optional()
741
+ });
742
+ var MetaInput = z5.object({
743
+ taskId: z5.string().min(1),
744
+ adAccountId: z5.string().min(1)
745
+ });
746
+ function registerPublishTools(server, api) {
747
+ server.tool("publish_to_shopify", "Publica la landing y producto generados en una tienda Shopify conectada. Precondici\xF3n: la tarea debe estar en status='complete'. Use `get_task_status` antes para verificar. Por defecto incluye 3 im\xE1genes de landing del hero.", ShopifyInput.shape, async (input) => {
748
+ try {
749
+ const numImages = input.numLandingImages ?? 3;
750
+ const variantIndex = input.variantIndex ?? 0;
751
+ const selectedLandingImages = Array.from({
752
+ length: numImages
753
+ }, (_, i) => ({
754
+ imageIndex: i,
755
+ variantIndex
756
+ }));
757
+ const r = await api.postJson(`/v1/tasks/${encodeURIComponent(input.taskId)}/publish/shopify`, {
758
+ shopDomain: input.shopDomain,
759
+ selectedLandingImages,
760
+ showVariantPicker: input.showVariantPicker
761
+ });
762
+ return asText(r);
763
+ } catch (e) {
764
+ throw translateError(e);
765
+ }
766
+ });
767
+ server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario debe activarla manualmente desde Meta Ads Manager, donde tambi\xE9n puede ajustar presupuesto, targeting y nombre de campa\xF1a. Use `list_meta_ad_accounts` antes para obtener un adAccountId v\xE1lido.", MetaInput.shape, async (input) => {
768
+ try {
769
+ const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(input.taskId)}`, {
770
+ adAccountId: input.adAccountId
771
+ });
772
+ return asText({
773
+ ...r,
774
+ status: "PAUSED"
775
+ });
776
+ } catch (e) {
777
+ throw translateError(e);
778
+ }
779
+ });
780
+ }
781
+ __name(registerPublishTools, "registerPublishTools");
782
+
783
+ // src/server.ts
784
+ function createServer(apiKey, apiBaseUrl) {
785
+ const server = new McpServer({
786
+ name: "productmaker",
787
+ version: MCP_VERSION
788
+ }, {
789
+ capabilities: {
790
+ tools: {
791
+ listChanged: false
792
+ },
793
+ logging: {}
794
+ }
795
+ });
796
+ const api = new ApiClient(apiBaseUrl, apiKey);
797
+ registerTaskTools(server, api);
798
+ registerCreativeTools(server, api);
799
+ registerConnectionTools(server, api);
800
+ registerPublishTools(server, api);
801
+ return server;
802
+ }
803
+ __name(createServer, "createServer");
804
+
805
+ // src/validate-api-url.ts
806
+ function assertSafeApiBaseUrl(url) {
807
+ let u;
808
+ try {
809
+ u = new URL(url);
810
+ } catch {
811
+ throw new Error(`PM_API_URL is not a valid URL: ${url}`);
812
+ }
813
+ if (u.protocol !== "https:") {
814
+ throw new Error(`PM_API_URL must use https (got ${u.protocol})`);
815
+ }
816
+ if (isPrivateHost(u.hostname)) {
817
+ throw new Error(`PM_API_URL must not point to a private/loopback host: ${u.hostname}`);
818
+ }
819
+ }
820
+ __name(assertSafeApiBaseUrl, "assertSafeApiBaseUrl");
821
+
822
+ // src/stdio.ts
823
+ async function main() {
824
+ const apiKey = process.env.PM_API_KEY;
825
+ const apiBaseUrl = process.env.PM_API_URL ?? "https://api.productmaker.app";
826
+ if (!apiKey) {
827
+ console.error("PM_API_KEY env var required. Create one at https://productmaker.app/account/api-keys");
828
+ process.exit(1);
829
+ }
830
+ try {
831
+ assertSafeApiBaseUrl(apiBaseUrl);
832
+ } catch (e) {
833
+ console.error(e.message);
834
+ process.exit(1);
835
+ }
836
+ const server = createServer(apiKey, apiBaseUrl);
837
+ await server.connect(new StdioServerTransport());
838
+ }
839
+ __name(main, "main");
840
+ main().catch((e) => {
841
+ console.error("mcp-stdio fatal error", e);
842
+ process.exit(1);
843
+ });