@productmaker/mcp 0.1.6 → 0.3.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.
Files changed (4) hide show
  1. package/README.md +57 -15
  2. package/dist/http.js +450 -118
  3. package/dist/stdio.js +448 -117
  4. package/package.json +47 -22
package/dist/stdio.js CHANGED
@@ -35,14 +35,14 @@ var ApiClient = class {
35
35
  Authorization: `Bearer ${this.apiKey}`
36
36
  };
37
37
  }
38
- async getJson(path) {
39
- const res = await fetch(`${this.baseUrl}${path}`, {
38
+ async getJson(path2) {
39
+ const res = await fetch(`${this.baseUrl}${path2}`, {
40
40
  headers: this.authHeaders()
41
41
  });
42
42
  return this.parse(res);
43
43
  }
44
- async postJson(path, body) {
45
- const res = await fetch(`${this.baseUrl}${path}`, {
44
+ async postJson(path2, body) {
45
+ const res = await fetch(`${this.baseUrl}${path2}`, {
46
46
  method: "POST",
47
47
  headers: {
48
48
  ...this.authHeaders(),
@@ -52,8 +52,8 @@ var ApiClient = class {
52
52
  });
53
53
  return this.parse(res);
54
54
  }
55
- async patchJson(path, body) {
56
- const res = await fetch(`${this.baseUrl}${path}`, {
55
+ async patchJson(path2, body) {
56
+ const res = await fetch(`${this.baseUrl}${path2}`, {
57
57
  method: "PATCH",
58
58
  headers: {
59
59
  ...this.authHeaders(),
@@ -63,14 +63,14 @@ var ApiClient = class {
63
63
  });
64
64
  return this.parse(res);
65
65
  }
66
- async deleteJson(path) {
67
- const res = await fetch(`${this.baseUrl}${path}`, {
66
+ async deleteJson(path2) {
67
+ const res = await fetch(`${this.baseUrl}${path2}`, {
68
68
  method: "DELETE",
69
69
  headers: this.authHeaders()
70
70
  });
71
71
  return this.parse(res);
72
72
  }
73
- async postMultipart(path, files, fields) {
73
+ async postMultipart(path2, files, fields) {
74
74
  const form = new FormData();
75
75
  for (const [name, value] of Object.entries(files)) {
76
76
  const parts = Array.isArray(value) ? value : [
@@ -88,7 +88,7 @@ var ApiClient = class {
88
88
  for (const [k, v] of Object.entries(fields)) {
89
89
  if (v !== void 0) form.append(k, v);
90
90
  }
91
- const res = await fetch(`${this.baseUrl}${path}`, {
91
+ const res = await fetch(`${this.baseUrl}${path2}`, {
92
92
  method: "POST",
93
93
  headers: this.authHeaders(),
94
94
  body: form
@@ -110,19 +110,40 @@ var ApiClient = class {
110
110
  };
111
111
 
112
112
  // src/index.ts
113
- var MCP_VERSION = "0.1.0";
113
+ var MCP_VERSION = "0.3.0";
114
114
 
115
115
  // src/tools/tasks.ts
116
116
  import { z as z2 } from "zod";
117
117
 
118
118
  // src/resolve-image.ts
119
119
  import { lookup as dnsLookup } from "dns/promises";
120
+ import { promises as fs, createReadStream } from "fs";
121
+ import * as path from "path";
122
+ import * as os from "os";
120
123
  var MAX_BYTES = 20 * 1024 * 1024;
121
- var ALLOWED = /* @__PURE__ */ new Set([
122
- "image/jpeg",
123
- "image/png",
124
- "image/webp"
125
- ]);
124
+ var IMAGE_FORMATS = [
125
+ {
126
+ mime: "image/jpeg",
127
+ exts: [
128
+ ".jpg",
129
+ ".jpeg"
130
+ ]
131
+ },
132
+ {
133
+ mime: "image/png",
134
+ exts: [
135
+ ".png"
136
+ ]
137
+ },
138
+ {
139
+ mime: "image/webp",
140
+ exts: [
141
+ ".webp"
142
+ ]
143
+ }
144
+ ];
145
+ var ALLOWED_MIMES = new Set(IMAGE_FORMATS.map((f) => f.mime));
146
+ var ALLOWED_EXTS = new Set(IMAGE_FORMATS.flatMap((f) => f.exts));
126
147
  var ResolveImageError = class extends Error {
127
148
  static {
128
149
  __name(this, "ResolveImageError");
@@ -189,69 +210,139 @@ async function assertHostResolvesPublic(hostname) {
189
210
  }
190
211
  }
191
212
  __name(assertHostResolvesPublic, "assertHostResolvesPublic");
192
- async function resolveImages(images) {
213
+ function expandUserPath(raw) {
214
+ let s = raw.trim();
215
+ if (s === "~") return os.homedir();
216
+ if (s.startsWith("~/") || s.startsWith("~\\")) s = path.join(os.homedir(), s.slice(2));
217
+ s = s.replace(/^\$HOME(?=$|[/\\])/, os.homedir());
218
+ s = s.replace(/^%USERPROFILE%(?=$|[/\\])/i, os.homedir());
219
+ return s;
220
+ }
221
+ __name(expandUserPath, "expandUserPath");
222
+ async function readFileWithCap(absPath, max) {
223
+ const chunks = [];
224
+ let total = 0;
225
+ let exceeded = false;
226
+ const stream = createReadStream(absPath);
227
+ try {
228
+ for await (const chunk of stream) {
229
+ total += chunk.length;
230
+ if (total > max) {
231
+ exceeded = true;
232
+ break;
233
+ }
234
+ chunks.push(chunk);
235
+ }
236
+ } catch (e) {
237
+ const code = e.code;
238
+ if (code === "ENOENT") throw new ResolveImageError("IMAGE_PATH_NOT_FOUND", "file disappeared during read");
239
+ if (code === "EACCES" || code === "EPERM") throw new ResolveImageError("IMAGE_PATH_NOT_READABLE", `cannot read file (${code})`);
240
+ throw new ResolveImageError("IMAGE_PATH_NOT_READABLE", e.message);
241
+ } finally {
242
+ if (!stream.destroyed) stream.destroy();
243
+ }
244
+ if (exceeded) throw new ResolveImageError("IMAGE_TOO_LARGE", `file exceeds ${max} bytes`);
245
+ return Buffer.concat(chunks, total);
246
+ }
247
+ __name(readFileWithCap, "readFileWithCap");
248
+ async function resolvePath(rawPath) {
249
+ const expanded = expandUserPath(rawPath);
250
+ const abs = path.resolve(expanded);
251
+ const ext = path.extname(abs).toLowerCase();
252
+ if (!ALLOWED_EXTS.has(ext)) {
253
+ throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `path extension ${ext || "(none)"} is not in the image allowlist (.jpg .jpeg .png .webp)`);
254
+ }
255
+ let stat;
256
+ try {
257
+ stat = await fs.stat(abs);
258
+ } catch (e) {
259
+ const code = e.code;
260
+ if (code === "ENOENT") {
261
+ throw new ResolveImageError("IMAGE_PATH_NOT_FOUND", `no such file: ${abs}`);
262
+ }
263
+ if (code === "EACCES" || code === "EPERM") {
264
+ throw new ResolveImageError("IMAGE_PATH_NOT_READABLE", `cannot access ${abs} (${code})`);
265
+ }
266
+ throw new ResolveImageError("IMAGE_PATH_NOT_READABLE", `stat failed: ${e.message}`);
267
+ }
268
+ if (!stat.isFile()) {
269
+ throw new ResolveImageError("IMAGE_PATH_NOT_A_FILE", `${abs} is not a regular file`);
270
+ }
271
+ if (stat.size > MAX_BYTES) {
272
+ throw new ResolveImageError("IMAGE_TOO_LARGE", `${stat.size} bytes (cap ${MAX_BYTES})`);
273
+ }
274
+ const buf = await readFileWithCap(abs, MAX_BYTES);
275
+ const sniffed = sniffMime(buf);
276
+ if (!sniffed) {
277
+ throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `magic bytes do not match PNG/JPEG/WEBP for ${abs}`);
278
+ }
279
+ return {
280
+ buffer: buf,
281
+ mimetype: sniffed
282
+ };
283
+ }
284
+ __name(resolvePath, "resolvePath");
285
+ async function resolveImageUrl(rawUrl) {
286
+ let u;
287
+ try {
288
+ u = new URL(rawUrl);
289
+ } catch {
290
+ throw new ResolveImageError("INVALID_INPUT", "imageUrl is not a valid URL");
291
+ }
292
+ if (u.protocol !== "https:") throw new ResolveImageError("INVALID_INPUT", "imageUrl must be HTTPS");
293
+ if (isPrivateHost(u.hostname)) throw new ResolveImageError("INVALID_INPUT", "imageUrl host not allowed");
294
+ await assertHostResolvesPublic(u.hostname);
295
+ let res;
296
+ try {
297
+ res = await fetch(rawUrl, {
298
+ signal: AbortSignal.timeout(3e4),
299
+ redirect: "error"
300
+ });
301
+ } catch (e) {
302
+ throw new ResolveImageError("IMAGE_DOWNLOAD_FAILED", e.message);
303
+ }
304
+ if (!res.ok) throw new ResolveImageError("IMAGE_DOWNLOAD_FAILED", `HTTP ${res.status}`);
305
+ const cl = res.headers.get("content-length");
306
+ if (cl && Number(cl) > MAX_BYTES) {
307
+ throw new ResolveImageError("IMAGE_TOO_LARGE", `content-length ${cl}`);
308
+ }
309
+ const ct = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
310
+ if (!ALLOWED_MIMES.has(ct)) throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `content-type ${ct}`);
311
+ const buf = Buffer.from(await res.arrayBuffer());
312
+ if (buf.length > MAX_BYTES) throw new ResolveImageError("IMAGE_TOO_LARGE", `${buf.length} bytes`);
313
+ const sniffed = sniffMime(buf);
314
+ if (!sniffed || sniffed !== ct) {
315
+ throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `magic bytes (${sniffed ?? "unknown"}) do not match content-type (${ct})`);
316
+ }
317
+ return {
318
+ buffer: buf,
319
+ mimetype: sniffed
320
+ };
321
+ }
322
+ __name(resolveImageUrl, "resolveImageUrl");
323
+ async function resolveImages(images, opts = {
324
+ transport: "http"
325
+ }) {
193
326
  if (!images.length) throw new ResolveImageError("INVALID_INPUT", "images: at least one entry required");
194
327
  if (images.length > 5) throw new ResolveImageError("INVALID_INPUT", "images: max 5 per request");
195
- return Promise.all(images.map((ref) => resolveImage({
196
- imageUrl: ref.url,
197
- imageBase64: ref.base64
198
- })));
328
+ return Promise.all(images.map((ref) => resolveOne(ref, opts.transport)));
199
329
  }
200
330
  __name(resolveImages, "resolveImages");
201
- async function resolveImage(input) {
202
- if (input.imageUrl && input.imageBase64) {
203
- throw new ResolveImageError("INVALID_INPUT", "Provide only one of imageUrl or imageBase64");
331
+ async function resolveOne(ref, transport) {
332
+ const hasUrl = Boolean(ref.url);
333
+ const hasPath = Boolean(ref.path);
334
+ if (hasUrl === hasPath) {
335
+ throw new ResolveImageError("INVALID_INPUT", "each image entry must have exactly one of `url` or `path`");
204
336
  }
205
- if (input.imageUrl) {
206
- let u;
207
- try {
208
- u = new URL(input.imageUrl);
209
- } catch {
210
- throw new ResolveImageError("INVALID_INPUT", "imageUrl is not a valid URL");
211
- }
212
- if (u.protocol !== "https:") throw new ResolveImageError("INVALID_INPUT", "imageUrl must be HTTPS");
213
- if (isPrivateHost(u.hostname)) throw new ResolveImageError("INVALID_INPUT", "imageUrl host not allowed");
214
- await assertHostResolvesPublic(u.hostname);
215
- let res;
216
- try {
217
- res = await fetch(input.imageUrl, {
218
- signal: AbortSignal.timeout(3e4),
219
- redirect: "error"
220
- });
221
- } catch (e) {
222
- throw new ResolveImageError("IMAGE_DOWNLOAD_FAILED", e.message);
223
- }
224
- if (!res.ok) throw new ResolveImageError("IMAGE_DOWNLOAD_FAILED", `HTTP ${res.status}`);
225
- const cl = res.headers.get("content-length");
226
- if (cl && Number(cl) > MAX_BYTES) {
227
- throw new ResolveImageError("IMAGE_TOO_LARGE", `content-length ${cl}`);
337
+ if (hasPath) {
338
+ if (transport === "http") {
339
+ throw new ResolveImageError("PATH_NOT_SUPPORTED_IN_HTTP_MODE", "Local file paths are only available when running the MCP locally (npm). For the hosted endpoint at mcp.productmaker.app, pass a public HTTPS `url` instead, or ask the user to host the photo first.");
228
340
  }
229
- const ct = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
230
- if (!ALLOWED.has(ct)) throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `content-type ${ct}`);
231
- const buf = Buffer.from(await res.arrayBuffer());
232
- if (buf.length > MAX_BYTES) throw new ResolveImageError("IMAGE_TOO_LARGE", `${buf.length} bytes`);
233
- const sniffed = sniffMime(buf);
234
- if (!sniffed || sniffed !== ct) {
235
- throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `magic bytes (${sniffed ?? "unknown"}) do not match content-type (${ct})`);
236
- }
237
- return {
238
- buffer: buf,
239
- mimetype: sniffed
240
- };
241
- }
242
- if (input.imageBase64) {
243
- const buf = Buffer.from(input.imageBase64, "base64");
244
- if (buf.length > MAX_BYTES) throw new ResolveImageError("IMAGE_TOO_LARGE", `${buf.length} bytes`);
245
- const mime = sniffMime(buf);
246
- if (!mime) throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", "cannot detect image type");
247
- return {
248
- buffer: buf,
249
- mimetype: mime
250
- };
341
+ return resolvePath(ref.path);
251
342
  }
252
- throw new ResolveImageError("INVALID_INPUT", "imageUrl or imageBase64 required");
343
+ return resolveImageUrl(ref.url);
253
344
  }
254
- __name(resolveImage, "resolveImage");
345
+ __name(resolveOne, "resolveOne");
255
346
 
256
347
  // src/status-poller.ts
257
348
  var TERMINAL = /* @__PURE__ */ new Set([
@@ -279,14 +370,14 @@ async function pollStatus(fetcher, waitSeconds, signal, opts = {}) {
279
370
  }
280
371
  __name(pollStatus, "pollStatus");
281
372
  function sleep(ms, signal) {
282
- return new Promise((resolve, reject) => {
373
+ return new Promise((resolve2, reject) => {
283
374
  const onAbort = /* @__PURE__ */ __name(() => {
284
375
  clearTimeout(t);
285
376
  reject(new Error("aborted"));
286
377
  }, "onAbort");
287
378
  const t = setTimeout(() => {
288
379
  signal?.removeEventListener("abort", onAbort);
289
- resolve();
380
+ resolve2();
290
381
  }, ms);
291
382
  signal?.addEventListener("abort", onAbort, {
292
383
  once: true
@@ -305,12 +396,18 @@ var McpErrorCode = {
305
396
  IMAGE_DOWNLOAD_FAILED: "IMAGE_DOWNLOAD_FAILED",
306
397
  IMAGE_TOO_LARGE: "IMAGE_TOO_LARGE",
307
398
  IMAGE_TYPE_UNSUPPORTED: "IMAGE_TYPE_UNSUPPORTED",
399
+ IMAGE_PATH_NOT_FOUND: "IMAGE_PATH_NOT_FOUND",
400
+ IMAGE_PATH_NOT_READABLE: "IMAGE_PATH_NOT_READABLE",
401
+ IMAGE_PATH_NOT_A_FILE: "IMAGE_PATH_NOT_A_FILE",
402
+ PATH_NOT_SUPPORTED_IN_HTTP_MODE: "PATH_NOT_SUPPORTED_IN_HTTP_MODE",
308
403
  TASK_NOT_FOUND: "TASK_NOT_FOUND",
309
404
  TASK_NOT_READY: "TASK_NOT_READY",
405
+ CAMPAIGN_NOT_FOUND: "CAMPAIGN_NOT_FOUND",
310
406
  NO_SHOPIFY_SHOPS: "NO_SHOPIFY_SHOPS",
311
407
  NO_META_AD_ACCOUNTS: "NO_META_AD_ACCOUNTS",
312
408
  SHOPIFY_NOT_CONNECTED: "SHOPIFY_NOT_CONNECTED",
313
409
  META_CAPABILITY_MISSING: "META_CAPABILITY_MISSING",
410
+ TIKTOK_NOT_CONNECTED: "TIKTOK_NOT_CONNECTED",
314
411
  UPSTREAM_TIMEOUT: "UPSTREAM_TIMEOUT",
315
412
  UPSTREAM_UNAVAILABLE: "UPSTREAM_UNAVAILABLE",
316
413
  INTERNAL: "INTERNAL"
@@ -318,7 +415,15 @@ var McpErrorCode = {
318
415
  var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
319
416
  McpErrorCode.INVALID_INPUT,
320
417
  McpErrorCode.INVALID_API_KEY,
321
- McpErrorCode.REVOKED_API_KEY
418
+ McpErrorCode.REVOKED_API_KEY,
419
+ McpErrorCode.IMAGE_PATH_NOT_FOUND,
420
+ McpErrorCode.IMAGE_PATH_NOT_READABLE,
421
+ McpErrorCode.IMAGE_PATH_NOT_A_FILE,
422
+ McpErrorCode.PATH_NOT_SUPPORTED_IN_HTTP_MODE,
423
+ // The caller passed a campaignId that does not exist — semantically a
424
+ // bad parameter, not an internal failure. Matches the spec contract
425
+ // (mcp-server.spec.ts: "McpError INVALID_PARAMS code=CAMPAIGN_NOT_FOUND").
426
+ McpErrorCode.CAMPAIGN_NOT_FOUND
322
427
  ]);
323
428
  function mcp(code, message, extra = {}) {
324
429
  const errCode = INVALID_PARAM_CODES.has(code) ? ErrorCode.InvalidParams : ErrorCode.InternalError;
@@ -333,7 +438,9 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
333
438
  "NO_SHOPIFY_SHOPS",
334
439
  "NO_META_AD_ACCOUNTS",
335
440
  "SHOPIFY_NOT_CONNECTED",
336
- "REVOKED_API_KEY"
441
+ "TIKTOK_NOT_CONNECTED",
442
+ "REVOKED_API_KEY",
443
+ "CAMPAIGN_NOT_FOUND"
337
444
  ]);
338
445
  function isRecord(v) {
339
446
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -433,14 +540,13 @@ var ImageCreativeAspectRatioEnum = z.enum([
433
540
  "9:16",
434
541
  "16:9"
435
542
  ]);
436
- var ImageBase64 = z.string().max(75e5);
437
543
  var ImageRefSchema = z.object({
438
- url: z.string().url().optional().describe("URL p\xFAblica HTTPS de la foto."),
439
- base64: ImageBase64.optional().describe("Bytes de la foto en base64 (sin prefijo data:). \xDAsalo SOLO para archivos locales <500 KB.")
440
- }).refine((v) => Boolean(v.url) !== Boolean(v.base64), {
441
- message: "each image entry must have exactly one of `url` or `base64`"
544
+ url: z.string().url().optional().describe('URL p\xFAblica HTTPS de la foto. Ej: una URL de Shopify CDN, Cloudinary, Imgur. \xDAsala cuando el usuario te diga "est\xE1 en X.com/foto.jpg" o cuando la foto ya est\xE9 hospedada.'),
545
+ path: z.string().min(1).optional().describe('Ruta del archivo en el computador del usuario. Ej: "/Users/maria/Desktop/foto.jpg", "~/Pictures/camiseta.png", "C:\\Users\\Maria\\Desktop\\foto.jpg". El MCP local la lee del disco y la sube. SOLO funciona cuando el MCP corre local (stdio). NO funciona en mcp.productmaker.app (HTTP) \u2014 ah\xED usa `url`.')
546
+ }).refine((v) => Boolean(v.url) !== Boolean(v.path), {
547
+ message: "each image entry must have exactly one of `url` or `path`"
442
548
  });
443
- var ImagesInput = z.array(ImageRefSchema).min(1).max(5).describe("1 a 5 fotos del MISMO producto. La PRIMERA es la principal (hero); las dem\xE1s son \xE1ngulos adicionales. Cada entrada usa `url` (preferido \u2014 p\xFAblico HTTPS) o `base64` (solo para archivos locales <500 KB; clientes MCP truncan argumentos >1 MB). PREFIERE siempre `url` si el usuario te dio una. NO mezcles productos distintos en una sola llamada.");
549
+ var ImagesInput = z.array(ImageRefSchema).min(1).max(5).describe('1 a 5 fotos del MISMO producto. La PRIMERA es la principal (hero); las dem\xE1s son \xE1ngulos adicionales. NO mezcles productos distintos en una sola llamada.\n\nCada entrada tiene EXACTAMENTE uno de:\n\u2022 `url` \u2014 URL p\xFAblica HTTPS (Shopify CDN, Cloudinary). Prefi\xE9rela si la foto ya est\xE1 hospedada.\n\u2022 `path` \u2014 Ruta del archivo en el computador del usuario (ej. "/Users/maria/Desktop/foto.jpg", "~/Downloads/camiseta.png"). \xDAsala cuando el usuario te dijo d\xF3nde guard\xF3 la foto. Solo MCP local \u2014 el cliente web rechaza paths.\n\nSi el usuario te peg\xF3 la foto en el chat pero no te dio ni URL ni ruta: PREG\xDANTALE d\xF3nde la tiene guardada (Escritorio, Descargas, etc.). Si el `path` falla con IMAGE_PATH_NOT_FOUND, prueba 1-2 ubicaciones obvias alternativas (~/Desktop, ~/Downloads) antes de molestar al usuario. Tras 3 intentos fallidos, p\xEDdele la ruta completa.');
444
550
  function asText(value) {
445
551
  return {
446
552
  content: [
@@ -452,6 +558,10 @@ function asText(value) {
452
558
  };
453
559
  }
454
560
  __name(asText, "asText");
561
+ function escapeMdLabel(s) {
562
+ return s.replace(/[\[\]\(\)\{\}`<>\r\n\\*_]/g, " ").replace(/\s+/g, " ").trim();
563
+ }
564
+ __name(escapeMdLabel, "escapeMdLabel");
455
565
  function flatten(obj) {
456
566
  const out = {};
457
567
  for (const [k, v] of Object.entries(obj)) {
@@ -465,7 +575,9 @@ function flatten(obj) {
465
575
  __name(flatten, "flatten");
466
576
  async function submitCreative(api, opts) {
467
577
  const { images, ...rest } = opts.input;
468
- const resolved = await resolveImages(images);
578
+ const resolved = await resolveImages(images, {
579
+ transport: opts.transport
580
+ });
469
581
  const parts = resolved.map((img, i) => ({
470
582
  ...img,
471
583
  filename: `image-${i}.png`
@@ -473,8 +585,9 @@ async function submitCreative(api, opts) {
473
585
  const r = await api.postMultipart(opts.path, {
474
586
  images: parts
475
587
  }, flatten(rest));
588
+ const taskId = r.taskId ?? r.jobId;
476
589
  return asText({
477
- taskId: r.taskId,
590
+ taskId,
478
591
  status: "queued",
479
592
  estimatedSeconds: opts.estimatedSeconds
480
593
  });
@@ -572,18 +685,18 @@ __name(sanitizeFilename, "sanitizeFilename");
572
685
  async function fetchTaskOutputs(api, taskId) {
573
686
  const raw = await api.getJson(`/v1/results/${encodeURIComponent(taskId)}?signedUrls=true`);
574
687
  const videos = (raw.videoGenerators ?? []).map((vg) => {
575
- const variants = (vg.variants ?? []).filter((v) => Boolean(v.kind) && Boolean(v.url)).map((v) => ({
688
+ const variants2 = (vg.variants ?? []).filter((v) => Boolean(v.kind) && Boolean(v.url)).map((v) => ({
576
689
  kind: v.kind,
577
690
  url: v.url
578
691
  }));
579
- const fallback = !variants.length && Array.isArray(vg.resultUrls) && vg.resultUrls.length ? vg.resultUrls.map((url) => ({
692
+ const fallback = !variants2.length && Array.isArray(vg.resultUrls) && vg.resultUrls.length ? vg.resultUrls.map((url) => ({
580
693
  kind: "mp4",
581
694
  url
582
695
  })) : [];
583
696
  return {
584
697
  angleKey: vg.angleKey ?? "",
585
698
  angleIndex: vg.angleIndex ?? 0,
586
- variants: variants.length ? variants : fallback
699
+ variants: variants2.length ? variants2 : fallback
587
700
  };
588
701
  }).filter((v) => v.angleKey && v.variants.length);
589
702
  const sections = (raw.landing?.images ?? []).filter((img) => Boolean(img.role) && Boolean(img.url)).map((img) => ({
@@ -591,18 +704,21 @@ async function fetchTaskOutputs(api, taskId) {
591
704
  url: img.url
592
705
  }));
593
706
  const images = [];
594
- if (raw.adCreative?.url) {
707
+ const variants = raw.adCreative?.variants ?? [];
708
+ const hasVariants = variants.some((v) => typeof v === "string");
709
+ if (hasVariants) {
710
+ for (const variant of variants) {
711
+ if (typeof variant === "string") images.push({
712
+ url: variant
713
+ });
714
+ }
715
+ } else if (raw.adCreative?.url) {
595
716
  images.push({
596
717
  url: raw.adCreative.url,
597
718
  width: raw.adCreative.width,
598
719
  height: raw.adCreative.height
599
720
  });
600
721
  }
601
- for (const variant of raw.adCreative?.variants ?? []) {
602
- if (typeof variant === "string") images.push({
603
- url: variant
604
- });
605
- }
606
722
  return {
607
723
  webappUrl: `${WEBAPP_BASE}/tasks/${taskId}`,
608
724
  videos,
@@ -613,21 +729,24 @@ async function fetchTaskOutputs(api, taskId) {
613
729
  };
614
730
  }
615
731
  __name(fetchTaskOutputs, "fetchTaskOutputs");
616
- function escapeMdLabel(s) {
617
- return s.replace(/[\[\]\(\)\{\}`<>\r\n\\]/g, " ").replace(/\s+/g, " ").trim();
618
- }
619
- __name(escapeMdLabel, "escapeMdLabel");
620
732
  function renderOutputsMarkdown(outputs) {
621
733
  const parts = [];
622
734
  parts.push(`\u2705 **Tu tarea est\xE1 lista** \u2014 [Abrir en Product Maker](${outputs.webappUrl})`);
623
735
  if (outputs.images.length) {
624
- parts.push("\n### Ad creative");
625
- for (const img of outputs.images) parts.push(`![](${img.url})`);
736
+ parts.push(`
737
+ ### Ad creative (${outputs.images.length} ${outputs.images.length === 1 ? "variante" : "variantes"})`);
738
+ outputs.images.forEach((img, i) => {
739
+ parts.push(`![v${i}](${img.url})`);
740
+ parts.push(`[\u2B07 Descargar v${i}](${img.url})`);
741
+ });
626
742
  }
627
743
  if (outputs.landing.sections.length) {
628
744
  parts.push(`
629
745
  ### Landing (${outputs.landing.sections.length} secciones)`);
630
- for (const s of outputs.landing.sections) parts.push(`![${escapeMdLabel(s.role)}](${s.url})`);
746
+ for (const s of outputs.landing.sections) {
747
+ parts.push(`![${escapeMdLabel(s.role)}](${s.url})`);
748
+ parts.push(`[\u2B07 Descargar ${escapeMdLabel(s.role)}](${s.url})`);
749
+ }
631
750
  }
632
751
  if (outputs.videos.length) {
633
752
  parts.push("\n### Video");
@@ -682,10 +801,12 @@ var EditTaskDraftInput = z2.object({
682
801
  scriptNotes: z2.string().max(2e3).optional()
683
802
  })
684
803
  });
685
- function registerTaskTools(server, api, proxyBaseUrl = null) {
804
+ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
686
805
  server.tool("create_product_task", 'Crea una tarea de producto desde una o varias fotos. Por defecto genera los 3 outputs (landing + video + image creatives). Devuelve un taskId.\n\nINPUTS REQUERIDOS: images (1-5 fotos del MISMO producto; la primera es la hero), language, country.\n\nANTES de invocar, PREG\xDANTALE al usuario por price y offer si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. NO defaultes country/language desde el contexto del chat: preg\xFAntale expl\xEDcitamente, incluso si el usuario est\xE1 en LATAM. Si te dice "t\xFA elige" para price/offer, om\xEDtelos.\n\nSi el usuario tiene varias fotos del MISMO producto (frente, lateral, detalle, uso), incl\xFAyelas todas en images[] hasta 5. Si tiene fotos de productos DIFERENTES, son llamadas separadas (una tarea por producto).\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos):\n- videoStyle, narrativeStyle, videoModel (enums \u2014 ver descripci\xF3n de cada campo)\n- multiAngleVideos (booleano)\n- outputs (subconjunto de {landing,video,image})\n- productUrl\n\nUsa SOLO valores declarados en los enums. Si el usuario pide algo no soportado (ej. "estilo anime"), p\xEDdele una opci\xF3n v\xE1lida.', CreateProductTaskInput.shape, async (input) => {
687
806
  try {
688
- const imgs = await resolveImages(input.images);
807
+ const imgs = await resolveImages(input.images, {
808
+ transport
809
+ });
689
810
  const parts = imgs.map((img, i) => ({
690
811
  ...img,
691
812
  filename: `image-${i}.png`
@@ -754,8 +875,8 @@ function registerTaskTools(server, api, proxyBaseUrl = null) {
754
875
  if (input.limit != null) qs.set("limit", String(input.limit));
755
876
  if (input.offset != null) qs.set("offset", String(input.offset));
756
877
  if (input.search) qs.set("search", input.search);
757
- const path = `/v1/tasks${qs.toString() ? "?" + qs.toString() : ""}`;
758
- const r = await api.getJson(path);
878
+ const path2 = `/v1/tasks${qs.toString() ? "?" + qs.toString() : ""}`;
879
+ const r = await api.getJson(path2);
759
880
  return asText(r);
760
881
  } catch (e) {
761
882
  throw translateError(e);
@@ -844,13 +965,14 @@ var LandingInput = z3.object({
844
965
  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.'),
845
966
  isCombo: CreativeRefs.isCombo
846
967
  });
847
- function registerCreativeTools(server, api) {
968
+ function registerCreativeTools(server, api, { transport = "http" } = {}) {
848
969
  server.tool("generate_video_creative", 'Genera UN video creativo independiente. Devuelve un taskId.\n\nREQUERIDOS: images (array de 1-5 fotos), title, primaryAngle, language, country, aspectRatio.\n\nANTES de invocar, PREG\xDANTALE al usuario por price y offer si no los mencion\xF3 \u2014 son los inputs de mayor impacto en conversi\xF3n y nunca debes asumirlos. NO defaultes country/language desde el contexto: preg\xFAntale expl\xEDcitamente. Si el usuario te dice "t\xFA elige" para price/offer, om\xEDtelos.\n\nOTROS OPCIONALES (puedes invocar sin ellos y dejar que la IA elija defaults sensatos): durationSeconds, narrativeStyle, videoStyle, videoModel, features, cta, scriptNotes.\n\nUsa SOLO valores declarados en los enums. Si el usuario pide algo no soportado, p\xEDdele una opci\xF3n v\xE1lida.', VideoInput.shape, async (input) => {
849
970
  try {
850
971
  return await submitCreative(api, {
851
972
  path: "/v1/creatives/videos",
852
973
  estimatedSeconds: 300,
853
- input
974
+ input,
975
+ transport
854
976
  });
855
977
  } catch (e) {
856
978
  throw translateError(e);
@@ -861,7 +983,8 @@ function registerCreativeTools(server, api) {
861
983
  return await submitCreative(api, {
862
984
  path: "/v1/creatives/images",
863
985
  estimatedSeconds: 60,
864
- input
986
+ input,
987
+ transport
865
988
  });
866
989
  } catch (e) {
867
990
  throw translateError(e);
@@ -872,7 +995,8 @@ function registerCreativeTools(server, api) {
872
995
  return await submitCreative(api, {
873
996
  path: "/v1/creatives/landing",
874
997
  estimatedSeconds: 60,
875
- input
998
+ input,
999
+ transport
876
1000
  });
877
1001
  } catch (e) {
878
1002
  throw translateError(e);
@@ -900,6 +1024,14 @@ function registerConnectionTools(server, api) {
900
1024
  throw translateError(e);
901
1025
  }
902
1026
  });
1027
+ server.tool("list_tiktok_advertisers", "Lista las cuentas de TikTok Ads conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar TikTok en productmaker.app/integrations.", z4.object({}).shape, async () => {
1028
+ try {
1029
+ const r = await api.getJson("/v1/tiktok/advertisers");
1030
+ return asText(r);
1031
+ } catch (e) {
1032
+ throw translateError(e);
1033
+ }
1034
+ });
903
1035
  }
904
1036
  __name(registerConnectionTools, "registerConnectionTools");
905
1037
 
@@ -910,14 +1042,38 @@ var ShopifyInput = z5.object({
910
1042
  shopDomain: z5.string().min(1),
911
1043
  numLandingImages: z5.number().int().min(1).max(7).optional(),
912
1044
  variantIndex: z5.number().int().min(0).optional(),
1045
+ /** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
1046
+ * When true, the landing renders variant-picker pills; when false, it ships
1047
+ * a single-option product page. Defaults to false. */
913
1048
  showVariantPicker: z5.boolean().optional()
914
1049
  });
1050
+ var McpCampaignConfig = z5.object({
1051
+ dailyBudgetUsd: z5.number().positive().optional(),
1052
+ /** ISO-3166-1 alpha-2 country codes. Meta accepts max 1 (single-country
1053
+ * ad set). TikTok takes only the first entry. */
1054
+ targetCountries: z5.array(z5.string().length(2)).max(1).optional(),
1055
+ campaignName: z5.string().trim().min(1).max(100).optional()
1056
+ });
915
1057
  var MetaInput = z5.object({
916
1058
  taskId: z5.string().min(1),
917
- adAccountId: z5.string().min(1)
1059
+ adAccountId: z5.string().min(1),
1060
+ pixelId: z5.string().min(1).optional(),
1061
+ landingUrl: z5.string().url().optional(),
1062
+ /** Select which angles to publish by 0-based index. Omit to publish all
1063
+ * successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
1064
+ angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
1065
+ campaignConfig: McpCampaignConfig.optional()
1066
+ });
1067
+ var TikTokInput = z5.object({
1068
+ taskId: z5.string().min(1),
1069
+ advertiserId: z5.string().min(1),
1070
+ pixelId: z5.string().min(1).optional(),
1071
+ landingUrl: z5.string().url().optional(),
1072
+ angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
1073
+ campaignConfig: McpCampaignConfig.optional()
918
1074
  });
919
1075
  function registerPublishTools(server, api) {
920
- 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) => {
1076
+ 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' \u2014 usa `get_task_status` antes. ANTES de llamar preg\xFAntale al usuario \"\xBFTu producto tiene variantes (talla, color, etc.)?\" y pasa `showVariantPicker: true` si responde que s\xED \u2014 eso renderiza los pills de variantes en la landing. Por defecto incluye 3 im\xE1genes del hero. La respuesta es markdown con el link a la landing y al producto.", ShopifyInput.shape, async (input) => {
921
1077
  try {
922
1078
  const numImages = input.numLandingImages ?? 3;
923
1079
  const variantIndex = input.variantIndex ?? 0;
@@ -932,16 +1088,37 @@ function registerPublishTools(server, api) {
932
1088
  selectedLandingImages,
933
1089
  showVariantPicker: input.showVariantPicker
934
1090
  });
935
- return asText(r);
1091
+ return renderShopifyPublishMarkdown(r);
936
1092
  } catch (e) {
937
1093
  throw translateError(e);
938
1094
  }
939
1095
  });
940
- 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) => {
1096
+ 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 la activa manualmente en Meta Ads Manager. Use `list_meta_ad_accounts` antes para un adAccountId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 si la tarea gener\xF3 3 \xE1ngulos y el usuario solo quiere los #0 y #2, pasa [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, m\xE1x 1 para Meta), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_meta_campaign_status` para chequear el estado.", MetaInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
941
1097
  try {
942
- const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(input.taskId)}`, {
943
- adAccountId: input.adAccountId
1098
+ const body = {
1099
+ ...rest
1100
+ };
1101
+ if (campaignConfig) {
1102
+ body.campaignOverrides = toMetaCampaignOverrides(campaignConfig);
1103
+ }
1104
+ const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1105
+ return asText({
1106
+ ...r,
1107
+ status: "PAUSED"
944
1108
  });
1109
+ } catch (e) {
1110
+ throw translateError(e);
1111
+ }
1112
+ });
1113
+ server.tool("publish_to_tiktok", "Crea una campa\xF1a de TikTok Ads desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en TikTok Ads Manager. Use `list_tiktok_advertisers` antes para un advertiserId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 ej. [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, primero se usa), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_tiktok_campaign_status` para chequear el estado.", TikTokInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
1114
+ try {
1115
+ const body = {
1116
+ ...rest
1117
+ };
1118
+ if (campaignConfig) {
1119
+ body.campaignConfig = toTikTokCampaignConfig(campaignConfig);
1120
+ }
1121
+ const r = await api.postJson(`/v1/tiktok/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
945
1122
  return asText({
946
1123
  ...r,
947
1124
  status: "PAUSED"
@@ -952,9 +1129,153 @@ function registerPublishTools(server, api) {
952
1129
  });
953
1130
  }
954
1131
  __name(registerPublishTools, "registerPublishTools");
1132
+ function renderShopifyPublishMarkdown(r) {
1133
+ const lines = [];
1134
+ const statusEmoji = r.status === "success" ? "\u2705" : r.status === "error" ? "\u274C" : "\u23F3";
1135
+ lines.push(`${statusEmoji} **Publicaci\xF3n en Shopify** \u2014 ${r.shopDomain}`);
1136
+ lines.push(`Estado: \`${r.status}\``);
1137
+ if (r.pageUrl) lines.push(`
1138
+ ### Landing
1139
+ [Ver landing publicada](${r.pageUrl})`);
1140
+ if (r.productUrl) lines.push(`
1141
+ ### Producto
1142
+ [Ver producto](${r.productUrl})`);
1143
+ if (!r.pageUrl && !r.productUrl && r.status !== "success") {
1144
+ lines.push("\n_La publicaci\xF3n a\xFAn est\xE1 proces\xE1ndose. Usa `get_task_status` para verificar progreso._");
1145
+ }
1146
+ return {
1147
+ content: [
1148
+ {
1149
+ type: "text",
1150
+ text: lines.join("\n")
1151
+ }
1152
+ ]
1153
+ };
1154
+ }
1155
+ __name(renderShopifyPublishMarkdown, "renderShopifyPublishMarkdown");
1156
+ function toMetaCampaignOverrides(cfg) {
1157
+ const out = {};
1158
+ if (cfg.dailyBudgetUsd !== void 0) out.dailyBudgetUsd = cfg.dailyBudgetUsd;
1159
+ if (cfg.targetCountries !== void 0) out.targetCountries = cfg.targetCountries;
1160
+ if (cfg.campaignName !== void 0) out.campaignName = cfg.campaignName;
1161
+ return out;
1162
+ }
1163
+ __name(toMetaCampaignOverrides, "toMetaCampaignOverrides");
1164
+ function toTikTokCampaignConfig(cfg) {
1165
+ const out = {};
1166
+ if (cfg.dailyBudgetUsd !== void 0) out.dailyBudget = Math.round(cfg.dailyBudgetUsd * 100);
1167
+ if (cfg.targetCountries && cfg.targetCountries.length > 0) out.targetCountry = cfg.targetCountries[0];
1168
+ if (cfg.campaignName !== void 0) out.campaignName = cfg.campaignName;
1169
+ return out;
1170
+ }
1171
+ __name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
1172
+
1173
+ // src/tools/campaigns.ts
1174
+ import { z as z6 } from "zod";
1175
+ var MetaStatusInput = z6.object({
1176
+ campaignId: z6.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
1177
+ });
1178
+ var TikTokStatusInput = z6.object({
1179
+ campaignId: z6.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
1180
+ advertiserId: z6.string().min(1).describe("The same advertiserId used to publish the campaign.")
1181
+ });
1182
+ function registerCampaignTools(server, api) {
1183
+ server.tool("get_meta_campaign_status", "Consulta el estado actual + m\xE9tricas lifetime (spend, impressions, clicks, reach) de una campa\xF1a Meta publicada. Llama esto DESPU\xC9S de `publish_to_meta` para confirmar que la campa\xF1a se cre\xF3 correctamente y para reportar performance al usuario. Es read-only \u2014 no modifica nada en Meta Ads Manager.", MetaStatusInput.shape, async (input) => {
1184
+ try {
1185
+ const r = await api.getJson(`/v1/facebook/campaigns/${encodeURIComponent(input.campaignId)}`);
1186
+ return renderMetaStatusMarkdown(r);
1187
+ } catch (e) {
1188
+ throw translateError(e);
1189
+ }
1190
+ });
1191
+ server.tool("get_tiktok_campaign_status", "Consulta el estado actual + reporte lifetime (spend, impressions, clicks, reach) de una campa\xF1a TikTok publicada. Requiere `advertiserId` (el mismo con el que se cre\xF3). Llama esto DESPU\xC9S de `publish_to_tiktok` para confirmar creaci\xF3n correcta y reportar performance. Es read-only.", TikTokStatusInput.shape, async (input) => {
1192
+ try {
1193
+ const r = await api.getJson(`/v1/tiktok/campaigns/${encodeURIComponent(input.campaignId)}?advertiserId=${encodeURIComponent(input.advertiserId)}`);
1194
+ return renderTikTokStatusMarkdown(r);
1195
+ } catch (e) {
1196
+ throw translateError(e);
1197
+ }
1198
+ });
1199
+ }
1200
+ __name(registerCampaignTools, "registerCampaignTools");
1201
+ function renderCampaignStatusMarkdown(opts) {
1202
+ const lines = [
1203
+ `\u{1F4CA} **${escapeMdLabel(opts.name)}** (\`${opts.campaignId}\`)`
1204
+ ];
1205
+ for (const [label, value] of opts.statusLines) {
1206
+ lines.push(`- ${label}: \`${value}\``);
1207
+ }
1208
+ if (opts.metrics) {
1209
+ lines.push("\n### M\xE9tricas (lifetime)");
1210
+ lines.push(`- Spend: ${opts.metrics.spend}`);
1211
+ lines.push(`- Impressions: ${opts.metrics.impressions}`);
1212
+ lines.push(`- Clicks: ${opts.metrics.clicks}`);
1213
+ lines.push(`- Reach: ${opts.metrics.reach}`);
1214
+ } else {
1215
+ lines.push(`
1216
+ _${opts.noMetricsHint ?? "A\xFAn no hay m\xE9tricas reportadas."}_`);
1217
+ }
1218
+ return {
1219
+ content: [
1220
+ {
1221
+ type: "text",
1222
+ text: lines.join("\n")
1223
+ }
1224
+ ]
1225
+ };
1226
+ }
1227
+ __name(renderCampaignStatusMarkdown, "renderCampaignStatusMarkdown");
1228
+ function renderMetaStatusMarkdown(r) {
1229
+ const statusLines = [
1230
+ [
1231
+ "Estado efectivo",
1232
+ r.effectiveStatus
1233
+ ],
1234
+ [
1235
+ "Estado configurado",
1236
+ r.configuredStatus
1237
+ ]
1238
+ ];
1239
+ if (r.objective) statusLines.push([
1240
+ "Objetivo",
1241
+ r.objective
1242
+ ]);
1243
+ return renderCampaignStatusMarkdown({
1244
+ name: r.name,
1245
+ campaignId: r.campaignId,
1246
+ statusLines,
1247
+ metrics: r.insights,
1248
+ noMetricsHint: "A\xFAn no hay m\xE9tricas reportadas (la campa\xF1a puede estar pausada o sin entregar)."
1249
+ });
1250
+ }
1251
+ __name(renderMetaStatusMarkdown, "renderMetaStatusMarkdown");
1252
+ function renderTikTokStatusMarkdown(r) {
1253
+ const statusLines = [
1254
+ [
1255
+ "Estado de operaci\xF3n",
1256
+ r.operationStatus
1257
+ ]
1258
+ ];
1259
+ if (r.secondaryStatus) statusLines.push([
1260
+ "Estado secundario",
1261
+ r.secondaryStatus
1262
+ ]);
1263
+ if (r.objectiveType) statusLines.push([
1264
+ "Objetivo",
1265
+ r.objectiveType
1266
+ ]);
1267
+ return renderCampaignStatusMarkdown({
1268
+ name: r.name,
1269
+ campaignId: r.campaignId,
1270
+ statusLines,
1271
+ metrics: r.report
1272
+ });
1273
+ }
1274
+ __name(renderTikTokStatusMarkdown, "renderTikTokStatusMarkdown");
955
1275
 
956
1276
  // src/server.ts
957
- function createServer(apiKey, apiBaseUrl, proxyBaseUrl = null) {
1277
+ function createServer(opts) {
1278
+ const { apiKey, apiBaseUrl, proxyBaseUrl = null, transport = "http" } = opts;
958
1279
  const server = new McpServer({
959
1280
  name: "productmaker",
960
1281
  version: MCP_VERSION
@@ -967,10 +1288,16 @@ function createServer(apiKey, apiBaseUrl, proxyBaseUrl = null) {
967
1288
  }
968
1289
  });
969
1290
  const api = new ApiClient(apiBaseUrl, apiKey);
970
- registerTaskTools(server, api, proxyBaseUrl);
971
- registerCreativeTools(server, api);
1291
+ registerTaskTools(server, api, {
1292
+ proxyBaseUrl,
1293
+ transport
1294
+ });
1295
+ registerCreativeTools(server, api, {
1296
+ transport
1297
+ });
972
1298
  registerConnectionTools(server, api);
973
1299
  registerPublishTools(server, api);
1300
+ registerCampaignTools(server, api);
974
1301
  return server;
975
1302
  }
976
1303
  __name(createServer, "createServer");
@@ -1010,7 +1337,11 @@ async function main() {
1010
1337
  console.error(e.message);
1011
1338
  process.exit(1);
1012
1339
  }
1013
- const server = createServer(apiKey, apiBaseUrl);
1340
+ const server = createServer({
1341
+ apiKey,
1342
+ apiBaseUrl,
1343
+ transport: "stdio"
1344
+ });
1014
1345
  await server.connect(new StdioServerTransport());
1015
1346
  }
1016
1347
  __name(main, "main");