@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/http.js CHANGED
@@ -38,14 +38,14 @@ var ApiClient = class {
38
38
  Authorization: `Bearer ${this.apiKey}`
39
39
  };
40
40
  }
41
- async getJson(path) {
42
- const res = await fetch(`${this.baseUrl}${path}`, {
41
+ async getJson(path2) {
42
+ const res = await fetch(`${this.baseUrl}${path2}`, {
43
43
  headers: this.authHeaders()
44
44
  });
45
45
  return this.parse(res);
46
46
  }
47
- async postJson(path, body) {
48
- const res = await fetch(`${this.baseUrl}${path}`, {
47
+ async postJson(path2, body) {
48
+ const res = await fetch(`${this.baseUrl}${path2}`, {
49
49
  method: "POST",
50
50
  headers: {
51
51
  ...this.authHeaders(),
@@ -55,8 +55,8 @@ var ApiClient = class {
55
55
  });
56
56
  return this.parse(res);
57
57
  }
58
- async patchJson(path, body) {
59
- const res = await fetch(`${this.baseUrl}${path}`, {
58
+ async patchJson(path2, body) {
59
+ const res = await fetch(`${this.baseUrl}${path2}`, {
60
60
  method: "PATCH",
61
61
  headers: {
62
62
  ...this.authHeaders(),
@@ -66,14 +66,14 @@ var ApiClient = class {
66
66
  });
67
67
  return this.parse(res);
68
68
  }
69
- async deleteJson(path) {
70
- const res = await fetch(`${this.baseUrl}${path}`, {
69
+ async deleteJson(path2) {
70
+ const res = await fetch(`${this.baseUrl}${path2}`, {
71
71
  method: "DELETE",
72
72
  headers: this.authHeaders()
73
73
  });
74
74
  return this.parse(res);
75
75
  }
76
- async postMultipart(path, files, fields) {
76
+ async postMultipart(path2, files, fields) {
77
77
  const form = new FormData();
78
78
  for (const [name, value] of Object.entries(files)) {
79
79
  const parts = Array.isArray(value) ? value : [
@@ -91,7 +91,7 @@ var ApiClient = class {
91
91
  for (const [k, v] of Object.entries(fields)) {
92
92
  if (v !== void 0) form.append(k, v);
93
93
  }
94
- const res = await fetch(`${this.baseUrl}${path}`, {
94
+ const res = await fetch(`${this.baseUrl}${path2}`, {
95
95
  method: "POST",
96
96
  headers: this.authHeaders(),
97
97
  body: form
@@ -113,19 +113,40 @@ var ApiClient = class {
113
113
  };
114
114
 
115
115
  // src/index.ts
116
- var MCP_VERSION = "0.1.0";
116
+ var MCP_VERSION = "0.3.0";
117
117
 
118
118
  // src/tools/tasks.ts
119
119
  import { z as z2 } from "zod";
120
120
 
121
121
  // src/resolve-image.ts
122
122
  import { lookup as dnsLookup } from "dns/promises";
123
+ import { promises as fs, createReadStream } from "fs";
124
+ import * as path from "path";
125
+ import * as os from "os";
123
126
  var MAX_BYTES = 20 * 1024 * 1024;
124
- var ALLOWED = /* @__PURE__ */ new Set([
125
- "image/jpeg",
126
- "image/png",
127
- "image/webp"
128
- ]);
127
+ var IMAGE_FORMATS = [
128
+ {
129
+ mime: "image/jpeg",
130
+ exts: [
131
+ ".jpg",
132
+ ".jpeg"
133
+ ]
134
+ },
135
+ {
136
+ mime: "image/png",
137
+ exts: [
138
+ ".png"
139
+ ]
140
+ },
141
+ {
142
+ mime: "image/webp",
143
+ exts: [
144
+ ".webp"
145
+ ]
146
+ }
147
+ ];
148
+ var ALLOWED_MIMES = new Set(IMAGE_FORMATS.map((f) => f.mime));
149
+ var ALLOWED_EXTS = new Set(IMAGE_FORMATS.flatMap((f) => f.exts));
129
150
  var ResolveImageError = class extends Error {
130
151
  static {
131
152
  __name(this, "ResolveImageError");
@@ -192,69 +213,139 @@ async function assertHostResolvesPublic(hostname) {
192
213
  }
193
214
  }
194
215
  __name(assertHostResolvesPublic, "assertHostResolvesPublic");
195
- async function resolveImages(images) {
216
+ function expandUserPath(raw) {
217
+ let s = raw.trim();
218
+ if (s === "~") return os.homedir();
219
+ if (s.startsWith("~/") || s.startsWith("~\\")) s = path.join(os.homedir(), s.slice(2));
220
+ s = s.replace(/^\$HOME(?=$|[/\\])/, os.homedir());
221
+ s = s.replace(/^%USERPROFILE%(?=$|[/\\])/i, os.homedir());
222
+ return s;
223
+ }
224
+ __name(expandUserPath, "expandUserPath");
225
+ async function readFileWithCap(absPath, max) {
226
+ const chunks = [];
227
+ let total = 0;
228
+ let exceeded = false;
229
+ const stream = createReadStream(absPath);
230
+ try {
231
+ for await (const chunk of stream) {
232
+ total += chunk.length;
233
+ if (total > max) {
234
+ exceeded = true;
235
+ break;
236
+ }
237
+ chunks.push(chunk);
238
+ }
239
+ } catch (e) {
240
+ const code = e.code;
241
+ if (code === "ENOENT") throw new ResolveImageError("IMAGE_PATH_NOT_FOUND", "file disappeared during read");
242
+ if (code === "EACCES" || code === "EPERM") throw new ResolveImageError("IMAGE_PATH_NOT_READABLE", `cannot read file (${code})`);
243
+ throw new ResolveImageError("IMAGE_PATH_NOT_READABLE", e.message);
244
+ } finally {
245
+ if (!stream.destroyed) stream.destroy();
246
+ }
247
+ if (exceeded) throw new ResolveImageError("IMAGE_TOO_LARGE", `file exceeds ${max} bytes`);
248
+ return Buffer.concat(chunks, total);
249
+ }
250
+ __name(readFileWithCap, "readFileWithCap");
251
+ async function resolvePath(rawPath) {
252
+ const expanded = expandUserPath(rawPath);
253
+ const abs = path.resolve(expanded);
254
+ const ext = path.extname(abs).toLowerCase();
255
+ if (!ALLOWED_EXTS.has(ext)) {
256
+ throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `path extension ${ext || "(none)"} is not in the image allowlist (.jpg .jpeg .png .webp)`);
257
+ }
258
+ let stat;
259
+ try {
260
+ stat = await fs.stat(abs);
261
+ } catch (e) {
262
+ const code = e.code;
263
+ if (code === "ENOENT") {
264
+ throw new ResolveImageError("IMAGE_PATH_NOT_FOUND", `no such file: ${abs}`);
265
+ }
266
+ if (code === "EACCES" || code === "EPERM") {
267
+ throw new ResolveImageError("IMAGE_PATH_NOT_READABLE", `cannot access ${abs} (${code})`);
268
+ }
269
+ throw new ResolveImageError("IMAGE_PATH_NOT_READABLE", `stat failed: ${e.message}`);
270
+ }
271
+ if (!stat.isFile()) {
272
+ throw new ResolveImageError("IMAGE_PATH_NOT_A_FILE", `${abs} is not a regular file`);
273
+ }
274
+ if (stat.size > MAX_BYTES) {
275
+ throw new ResolveImageError("IMAGE_TOO_LARGE", `${stat.size} bytes (cap ${MAX_BYTES})`);
276
+ }
277
+ const buf = await readFileWithCap(abs, MAX_BYTES);
278
+ const sniffed = sniffMime(buf);
279
+ if (!sniffed) {
280
+ throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `magic bytes do not match PNG/JPEG/WEBP for ${abs}`);
281
+ }
282
+ return {
283
+ buffer: buf,
284
+ mimetype: sniffed
285
+ };
286
+ }
287
+ __name(resolvePath, "resolvePath");
288
+ async function resolveImageUrl(rawUrl) {
289
+ let u;
290
+ try {
291
+ u = new URL(rawUrl);
292
+ } catch {
293
+ throw new ResolveImageError("INVALID_INPUT", "imageUrl is not a valid URL");
294
+ }
295
+ if (u.protocol !== "https:") throw new ResolveImageError("INVALID_INPUT", "imageUrl must be HTTPS");
296
+ if (isPrivateHost(u.hostname)) throw new ResolveImageError("INVALID_INPUT", "imageUrl host not allowed");
297
+ await assertHostResolvesPublic(u.hostname);
298
+ let res;
299
+ try {
300
+ res = await fetch(rawUrl, {
301
+ signal: AbortSignal.timeout(3e4),
302
+ redirect: "error"
303
+ });
304
+ } catch (e) {
305
+ throw new ResolveImageError("IMAGE_DOWNLOAD_FAILED", e.message);
306
+ }
307
+ if (!res.ok) throw new ResolveImageError("IMAGE_DOWNLOAD_FAILED", `HTTP ${res.status}`);
308
+ const cl = res.headers.get("content-length");
309
+ if (cl && Number(cl) > MAX_BYTES) {
310
+ throw new ResolveImageError("IMAGE_TOO_LARGE", `content-length ${cl}`);
311
+ }
312
+ const ct = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
313
+ if (!ALLOWED_MIMES.has(ct)) throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `content-type ${ct}`);
314
+ const buf = Buffer.from(await res.arrayBuffer());
315
+ if (buf.length > MAX_BYTES) throw new ResolveImageError("IMAGE_TOO_LARGE", `${buf.length} bytes`);
316
+ const sniffed = sniffMime(buf);
317
+ if (!sniffed || sniffed !== ct) {
318
+ throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `magic bytes (${sniffed ?? "unknown"}) do not match content-type (${ct})`);
319
+ }
320
+ return {
321
+ buffer: buf,
322
+ mimetype: sniffed
323
+ };
324
+ }
325
+ __name(resolveImageUrl, "resolveImageUrl");
326
+ async function resolveImages(images, opts = {
327
+ transport: "http"
328
+ }) {
196
329
  if (!images.length) throw new ResolveImageError("INVALID_INPUT", "images: at least one entry required");
197
330
  if (images.length > 5) throw new ResolveImageError("INVALID_INPUT", "images: max 5 per request");
198
- return Promise.all(images.map((ref) => resolveImage({
199
- imageUrl: ref.url,
200
- imageBase64: ref.base64
201
- })));
331
+ return Promise.all(images.map((ref) => resolveOne(ref, opts.transport)));
202
332
  }
203
333
  __name(resolveImages, "resolveImages");
204
- async function resolveImage(input) {
205
- if (input.imageUrl && input.imageBase64) {
206
- throw new ResolveImageError("INVALID_INPUT", "Provide only one of imageUrl or imageBase64");
334
+ async function resolveOne(ref, transport) {
335
+ const hasUrl = Boolean(ref.url);
336
+ const hasPath = Boolean(ref.path);
337
+ if (hasUrl === hasPath) {
338
+ throw new ResolveImageError("INVALID_INPUT", "each image entry must have exactly one of `url` or `path`");
207
339
  }
208
- if (input.imageUrl) {
209
- let u;
210
- try {
211
- u = new URL(input.imageUrl);
212
- } catch {
213
- throw new ResolveImageError("INVALID_INPUT", "imageUrl is not a valid URL");
340
+ if (hasPath) {
341
+ if (transport === "http") {
342
+ 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.");
214
343
  }
215
- if (u.protocol !== "https:") throw new ResolveImageError("INVALID_INPUT", "imageUrl must be HTTPS");
216
- if (isPrivateHost(u.hostname)) throw new ResolveImageError("INVALID_INPUT", "imageUrl host not allowed");
217
- await assertHostResolvesPublic(u.hostname);
218
- let res;
219
- try {
220
- res = await fetch(input.imageUrl, {
221
- signal: AbortSignal.timeout(3e4),
222
- redirect: "error"
223
- });
224
- } catch (e) {
225
- throw new ResolveImageError("IMAGE_DOWNLOAD_FAILED", e.message);
226
- }
227
- if (!res.ok) throw new ResolveImageError("IMAGE_DOWNLOAD_FAILED", `HTTP ${res.status}`);
228
- const cl = res.headers.get("content-length");
229
- if (cl && Number(cl) > MAX_BYTES) {
230
- throw new ResolveImageError("IMAGE_TOO_LARGE", `content-length ${cl}`);
231
- }
232
- const ct = (res.headers.get("content-type") ?? "").split(";")[0]?.trim() ?? "";
233
- if (!ALLOWED.has(ct)) throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `content-type ${ct}`);
234
- const buf = Buffer.from(await res.arrayBuffer());
235
- if (buf.length > MAX_BYTES) throw new ResolveImageError("IMAGE_TOO_LARGE", `${buf.length} bytes`);
236
- const sniffed = sniffMime(buf);
237
- if (!sniffed || sniffed !== ct) {
238
- throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", `magic bytes (${sniffed ?? "unknown"}) do not match content-type (${ct})`);
239
- }
240
- return {
241
- buffer: buf,
242
- mimetype: sniffed
243
- };
244
- }
245
- if (input.imageBase64) {
246
- const buf = Buffer.from(input.imageBase64, "base64");
247
- if (buf.length > MAX_BYTES) throw new ResolveImageError("IMAGE_TOO_LARGE", `${buf.length} bytes`);
248
- const mime = sniffMime(buf);
249
- if (!mime) throw new ResolveImageError("IMAGE_TYPE_UNSUPPORTED", "cannot detect image type");
250
- return {
251
- buffer: buf,
252
- mimetype: mime
253
- };
344
+ return resolvePath(ref.path);
254
345
  }
255
- throw new ResolveImageError("INVALID_INPUT", "imageUrl or imageBase64 required");
346
+ return resolveImageUrl(ref.url);
256
347
  }
257
- __name(resolveImage, "resolveImage");
348
+ __name(resolveOne, "resolveOne");
258
349
 
259
350
  // src/status-poller.ts
260
351
  var TERMINAL = /* @__PURE__ */ new Set([
@@ -282,14 +373,14 @@ async function pollStatus(fetcher, waitSeconds, signal, opts = {}) {
282
373
  }
283
374
  __name(pollStatus, "pollStatus");
284
375
  function sleep(ms, signal) {
285
- return new Promise((resolve, reject) => {
376
+ return new Promise((resolve2, reject) => {
286
377
  const onAbort = /* @__PURE__ */ __name(() => {
287
378
  clearTimeout(t);
288
379
  reject(new Error("aborted"));
289
380
  }, "onAbort");
290
381
  const t = setTimeout(() => {
291
382
  signal?.removeEventListener("abort", onAbort);
292
- resolve();
383
+ resolve2();
293
384
  }, ms);
294
385
  signal?.addEventListener("abort", onAbort, {
295
386
  once: true
@@ -308,12 +399,18 @@ var McpErrorCode = {
308
399
  IMAGE_DOWNLOAD_FAILED: "IMAGE_DOWNLOAD_FAILED",
309
400
  IMAGE_TOO_LARGE: "IMAGE_TOO_LARGE",
310
401
  IMAGE_TYPE_UNSUPPORTED: "IMAGE_TYPE_UNSUPPORTED",
402
+ IMAGE_PATH_NOT_FOUND: "IMAGE_PATH_NOT_FOUND",
403
+ IMAGE_PATH_NOT_READABLE: "IMAGE_PATH_NOT_READABLE",
404
+ IMAGE_PATH_NOT_A_FILE: "IMAGE_PATH_NOT_A_FILE",
405
+ PATH_NOT_SUPPORTED_IN_HTTP_MODE: "PATH_NOT_SUPPORTED_IN_HTTP_MODE",
311
406
  TASK_NOT_FOUND: "TASK_NOT_FOUND",
312
407
  TASK_NOT_READY: "TASK_NOT_READY",
408
+ CAMPAIGN_NOT_FOUND: "CAMPAIGN_NOT_FOUND",
313
409
  NO_SHOPIFY_SHOPS: "NO_SHOPIFY_SHOPS",
314
410
  NO_META_AD_ACCOUNTS: "NO_META_AD_ACCOUNTS",
315
411
  SHOPIFY_NOT_CONNECTED: "SHOPIFY_NOT_CONNECTED",
316
412
  META_CAPABILITY_MISSING: "META_CAPABILITY_MISSING",
413
+ TIKTOK_NOT_CONNECTED: "TIKTOK_NOT_CONNECTED",
317
414
  UPSTREAM_TIMEOUT: "UPSTREAM_TIMEOUT",
318
415
  UPSTREAM_UNAVAILABLE: "UPSTREAM_UNAVAILABLE",
319
416
  INTERNAL: "INTERNAL"
@@ -321,7 +418,15 @@ var McpErrorCode = {
321
418
  var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
322
419
  McpErrorCode.INVALID_INPUT,
323
420
  McpErrorCode.INVALID_API_KEY,
324
- McpErrorCode.REVOKED_API_KEY
421
+ McpErrorCode.REVOKED_API_KEY,
422
+ McpErrorCode.IMAGE_PATH_NOT_FOUND,
423
+ McpErrorCode.IMAGE_PATH_NOT_READABLE,
424
+ McpErrorCode.IMAGE_PATH_NOT_A_FILE,
425
+ McpErrorCode.PATH_NOT_SUPPORTED_IN_HTTP_MODE,
426
+ // The caller passed a campaignId that does not exist — semantically a
427
+ // bad parameter, not an internal failure. Matches the spec contract
428
+ // (mcp-server.spec.ts: "McpError INVALID_PARAMS code=CAMPAIGN_NOT_FOUND").
429
+ McpErrorCode.CAMPAIGN_NOT_FOUND
325
430
  ]);
326
431
  function mcp(code, message, extra = {}) {
327
432
  const errCode = INVALID_PARAM_CODES.has(code) ? ErrorCode.InvalidParams : ErrorCode.InternalError;
@@ -336,7 +441,9 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
336
441
  "NO_SHOPIFY_SHOPS",
337
442
  "NO_META_AD_ACCOUNTS",
338
443
  "SHOPIFY_NOT_CONNECTED",
339
- "REVOKED_API_KEY"
444
+ "TIKTOK_NOT_CONNECTED",
445
+ "REVOKED_API_KEY",
446
+ "CAMPAIGN_NOT_FOUND"
340
447
  ]);
341
448
  function isRecord(v) {
342
449
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -436,14 +543,13 @@ var ImageCreativeAspectRatioEnum = z.enum([
436
543
  "9:16",
437
544
  "16:9"
438
545
  ]);
439
- var ImageBase64 = z.string().max(75e5);
440
546
  var ImageRefSchema = z.object({
441
- url: z.string().url().optional().describe("URL p\xFAblica HTTPS de la foto."),
442
- base64: ImageBase64.optional().describe("Bytes de la foto en base64 (sin prefijo data:). \xDAsalo SOLO para archivos locales <500 KB.")
443
- }).refine((v) => Boolean(v.url) !== Boolean(v.base64), {
444
- message: "each image entry must have exactly one of `url` or `base64`"
547
+ 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.'),
548
+ 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`.')
549
+ }).refine((v) => Boolean(v.url) !== Boolean(v.path), {
550
+ message: "each image entry must have exactly one of `url` or `path`"
445
551
  });
446
- 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.");
552
+ 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.');
447
553
  function asText(value) {
448
554
  return {
449
555
  content: [
@@ -455,6 +561,10 @@ function asText(value) {
455
561
  };
456
562
  }
457
563
  __name(asText, "asText");
564
+ function escapeMdLabel(s) {
565
+ return s.replace(/[\[\]\(\)\{\}`<>\r\n\\*_]/g, " ").replace(/\s+/g, " ").trim();
566
+ }
567
+ __name(escapeMdLabel, "escapeMdLabel");
458
568
  function flatten(obj) {
459
569
  const out = {};
460
570
  for (const [k, v] of Object.entries(obj)) {
@@ -468,7 +578,9 @@ function flatten(obj) {
468
578
  __name(flatten, "flatten");
469
579
  async function submitCreative(api, opts) {
470
580
  const { images, ...rest } = opts.input;
471
- const resolved = await resolveImages(images);
581
+ const resolved = await resolveImages(images, {
582
+ transport: opts.transport
583
+ });
472
584
  const parts = resolved.map((img, i) => ({
473
585
  ...img,
474
586
  filename: `image-${i}.png`
@@ -476,8 +588,9 @@ async function submitCreative(api, opts) {
476
588
  const r = await api.postMultipart(opts.path, {
477
589
  images: parts
478
590
  }, flatten(rest));
591
+ const taskId = r.taskId ?? r.jobId;
479
592
  return asText({
480
- taskId: r.taskId,
593
+ taskId,
481
594
  status: "queued",
482
595
  estimatedSeconds: opts.estimatedSeconds
483
596
  });
@@ -616,18 +729,18 @@ __name(sanitizeFilename, "sanitizeFilename");
616
729
  async function fetchTaskOutputs(api, taskId) {
617
730
  const raw = await api.getJson(`/v1/results/${encodeURIComponent(taskId)}?signedUrls=true`);
618
731
  const videos = (raw.videoGenerators ?? []).map((vg) => {
619
- const variants = (vg.variants ?? []).filter((v) => Boolean(v.kind) && Boolean(v.url)).map((v) => ({
732
+ const variants2 = (vg.variants ?? []).filter((v) => Boolean(v.kind) && Boolean(v.url)).map((v) => ({
620
733
  kind: v.kind,
621
734
  url: v.url
622
735
  }));
623
- const fallback = !variants.length && Array.isArray(vg.resultUrls) && vg.resultUrls.length ? vg.resultUrls.map((url) => ({
736
+ const fallback = !variants2.length && Array.isArray(vg.resultUrls) && vg.resultUrls.length ? vg.resultUrls.map((url) => ({
624
737
  kind: "mp4",
625
738
  url
626
739
  })) : [];
627
740
  return {
628
741
  angleKey: vg.angleKey ?? "",
629
742
  angleIndex: vg.angleIndex ?? 0,
630
- variants: variants.length ? variants : fallback
743
+ variants: variants2.length ? variants2 : fallback
631
744
  };
632
745
  }).filter((v) => v.angleKey && v.variants.length);
633
746
  const sections = (raw.landing?.images ?? []).filter((img) => Boolean(img.role) && Boolean(img.url)).map((img) => ({
@@ -635,18 +748,21 @@ async function fetchTaskOutputs(api, taskId) {
635
748
  url: img.url
636
749
  }));
637
750
  const images = [];
638
- if (raw.adCreative?.url) {
751
+ const variants = raw.adCreative?.variants ?? [];
752
+ const hasVariants = variants.some((v) => typeof v === "string");
753
+ if (hasVariants) {
754
+ for (const variant of variants) {
755
+ if (typeof variant === "string") images.push({
756
+ url: variant
757
+ });
758
+ }
759
+ } else if (raw.adCreative?.url) {
639
760
  images.push({
640
761
  url: raw.adCreative.url,
641
762
  width: raw.adCreative.width,
642
763
  height: raw.adCreative.height
643
764
  });
644
765
  }
645
- for (const variant of raw.adCreative?.variants ?? []) {
646
- if (typeof variant === "string") images.push({
647
- url: variant
648
- });
649
- }
650
766
  return {
651
767
  webappUrl: `${WEBAPP_BASE}/tasks/${taskId}`,
652
768
  videos,
@@ -657,21 +773,24 @@ async function fetchTaskOutputs(api, taskId) {
657
773
  };
658
774
  }
659
775
  __name(fetchTaskOutputs, "fetchTaskOutputs");
660
- function escapeMdLabel(s) {
661
- return s.replace(/[\[\]\(\)\{\}`<>\r\n\\]/g, " ").replace(/\s+/g, " ").trim();
662
- }
663
- __name(escapeMdLabel, "escapeMdLabel");
664
776
  function renderOutputsMarkdown(outputs) {
665
777
  const parts = [];
666
778
  parts.push(`\u2705 **Tu tarea est\xE1 lista** \u2014 [Abrir en Product Maker](${outputs.webappUrl})`);
667
779
  if (outputs.images.length) {
668
- parts.push("\n### Ad creative");
669
- for (const img of outputs.images) parts.push(`![](${img.url})`);
780
+ parts.push(`
781
+ ### Ad creative (${outputs.images.length} ${outputs.images.length === 1 ? "variante" : "variantes"})`);
782
+ outputs.images.forEach((img, i) => {
783
+ parts.push(`![v${i}](${img.url})`);
784
+ parts.push(`[\u2B07 Descargar v${i}](${img.url})`);
785
+ });
670
786
  }
671
787
  if (outputs.landing.sections.length) {
672
788
  parts.push(`
673
789
  ### Landing (${outputs.landing.sections.length} secciones)`);
674
- for (const s of outputs.landing.sections) parts.push(`![${escapeMdLabel(s.role)}](${s.url})`);
790
+ for (const s of outputs.landing.sections) {
791
+ parts.push(`![${escapeMdLabel(s.role)}](${s.url})`);
792
+ parts.push(`[\u2B07 Descargar ${escapeMdLabel(s.role)}](${s.url})`);
793
+ }
675
794
  }
676
795
  if (outputs.videos.length) {
677
796
  parts.push("\n### Video");
@@ -726,10 +845,12 @@ var EditTaskDraftInput = z2.object({
726
845
  scriptNotes: z2.string().max(2e3).optional()
727
846
  })
728
847
  });
729
- function registerTaskTools(server, api, proxyBaseUrl = null) {
848
+ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http" } = {}) {
730
849
  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) => {
731
850
  try {
732
- const imgs = await resolveImages(input.images);
851
+ const imgs = await resolveImages(input.images, {
852
+ transport
853
+ });
733
854
  const parts = imgs.map((img, i) => ({
734
855
  ...img,
735
856
  filename: `image-${i}.png`
@@ -798,8 +919,8 @@ function registerTaskTools(server, api, proxyBaseUrl = null) {
798
919
  if (input.limit != null) qs.set("limit", String(input.limit));
799
920
  if (input.offset != null) qs.set("offset", String(input.offset));
800
921
  if (input.search) qs.set("search", input.search);
801
- const path = `/v1/tasks${qs.toString() ? "?" + qs.toString() : ""}`;
802
- const r = await api.getJson(path);
922
+ const path2 = `/v1/tasks${qs.toString() ? "?" + qs.toString() : ""}`;
923
+ const r = await api.getJson(path2);
803
924
  return asText(r);
804
925
  } catch (e) {
805
926
  throw translateError(e);
@@ -888,13 +1009,14 @@ var LandingInput = z3.object({
888
1009
  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.'),
889
1010
  isCombo: CreativeRefs.isCombo
890
1011
  });
891
- function registerCreativeTools(server, api) {
1012
+ function registerCreativeTools(server, api, { transport = "http" } = {}) {
892
1013
  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) => {
893
1014
  try {
894
1015
  return await submitCreative(api, {
895
1016
  path: "/v1/creatives/videos",
896
1017
  estimatedSeconds: 300,
897
- input
1018
+ input,
1019
+ transport
898
1020
  });
899
1021
  } catch (e) {
900
1022
  throw translateError(e);
@@ -905,7 +1027,8 @@ function registerCreativeTools(server, api) {
905
1027
  return await submitCreative(api, {
906
1028
  path: "/v1/creatives/images",
907
1029
  estimatedSeconds: 60,
908
- input
1030
+ input,
1031
+ transport
909
1032
  });
910
1033
  } catch (e) {
911
1034
  throw translateError(e);
@@ -916,7 +1039,8 @@ function registerCreativeTools(server, api) {
916
1039
  return await submitCreative(api, {
917
1040
  path: "/v1/creatives/landing",
918
1041
  estimatedSeconds: 60,
919
- input
1042
+ input,
1043
+ transport
920
1044
  });
921
1045
  } catch (e) {
922
1046
  throw translateError(e);
@@ -944,6 +1068,14 @@ function registerConnectionTools(server, api) {
944
1068
  throw translateError(e);
945
1069
  }
946
1070
  });
1071
+ 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 () => {
1072
+ try {
1073
+ const r = await api.getJson("/v1/tiktok/advertisers");
1074
+ return asText(r);
1075
+ } catch (e) {
1076
+ throw translateError(e);
1077
+ }
1078
+ });
947
1079
  }
948
1080
  __name(registerConnectionTools, "registerConnectionTools");
949
1081
 
@@ -954,14 +1086,38 @@ var ShopifyInput = z5.object({
954
1086
  shopDomain: z5.string().min(1),
955
1087
  numLandingImages: z5.number().int().min(1).max(7).optional(),
956
1088
  variantIndex: z5.number().int().min(0).optional(),
1089
+ /** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
1090
+ * When true, the landing renders variant-picker pills; when false, it ships
1091
+ * a single-option product page. Defaults to false. */
957
1092
  showVariantPicker: z5.boolean().optional()
958
1093
  });
1094
+ var McpCampaignConfig = z5.object({
1095
+ dailyBudgetUsd: z5.number().positive().optional(),
1096
+ /** ISO-3166-1 alpha-2 country codes. Meta accepts max 1 (single-country
1097
+ * ad set). TikTok takes only the first entry. */
1098
+ targetCountries: z5.array(z5.string().length(2)).max(1).optional(),
1099
+ campaignName: z5.string().trim().min(1).max(100).optional()
1100
+ });
959
1101
  var MetaInput = z5.object({
960
1102
  taskId: z5.string().min(1),
961
- adAccountId: z5.string().min(1)
1103
+ adAccountId: z5.string().min(1),
1104
+ pixelId: z5.string().min(1).optional(),
1105
+ landingUrl: z5.string().url().optional(),
1106
+ /** Select which angles to publish by 0-based index. Omit to publish all
1107
+ * successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
1108
+ angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
1109
+ campaignConfig: McpCampaignConfig.optional()
1110
+ });
1111
+ var TikTokInput = z5.object({
1112
+ taskId: z5.string().min(1),
1113
+ advertiserId: z5.string().min(1),
1114
+ pixelId: z5.string().min(1).optional(),
1115
+ landingUrl: z5.string().url().optional(),
1116
+ angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
1117
+ campaignConfig: McpCampaignConfig.optional()
962
1118
  });
963
1119
  function registerPublishTools(server, api) {
964
- 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) => {
1120
+ 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) => {
965
1121
  try {
966
1122
  const numImages = input.numLandingImages ?? 3;
967
1123
  const variantIndex = input.variantIndex ?? 0;
@@ -976,16 +1132,37 @@ function registerPublishTools(server, api) {
976
1132
  selectedLandingImages,
977
1133
  showVariantPicker: input.showVariantPicker
978
1134
  });
979
- return asText(r);
1135
+ return renderShopifyPublishMarkdown(r);
980
1136
  } catch (e) {
981
1137
  throw translateError(e);
982
1138
  }
983
1139
  });
984
- 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) => {
1140
+ 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 }) => {
985
1141
  try {
986
- const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(input.taskId)}`, {
987
- adAccountId: input.adAccountId
1142
+ const body = {
1143
+ ...rest
1144
+ };
1145
+ if (campaignConfig) {
1146
+ body.campaignOverrides = toMetaCampaignOverrides(campaignConfig);
1147
+ }
1148
+ const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1149
+ return asText({
1150
+ ...r,
1151
+ status: "PAUSED"
988
1152
  });
1153
+ } catch (e) {
1154
+ throw translateError(e);
1155
+ }
1156
+ });
1157
+ 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 }) => {
1158
+ try {
1159
+ const body = {
1160
+ ...rest
1161
+ };
1162
+ if (campaignConfig) {
1163
+ body.campaignConfig = toTikTokCampaignConfig(campaignConfig);
1164
+ }
1165
+ const r = await api.postJson(`/v1/tiktok/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
989
1166
  return asText({
990
1167
  ...r,
991
1168
  status: "PAUSED"
@@ -996,9 +1173,153 @@ function registerPublishTools(server, api) {
996
1173
  });
997
1174
  }
998
1175
  __name(registerPublishTools, "registerPublishTools");
1176
+ function renderShopifyPublishMarkdown(r) {
1177
+ const lines = [];
1178
+ const statusEmoji = r.status === "success" ? "\u2705" : r.status === "error" ? "\u274C" : "\u23F3";
1179
+ lines.push(`${statusEmoji} **Publicaci\xF3n en Shopify** \u2014 ${r.shopDomain}`);
1180
+ lines.push(`Estado: \`${r.status}\``);
1181
+ if (r.pageUrl) lines.push(`
1182
+ ### Landing
1183
+ [Ver landing publicada](${r.pageUrl})`);
1184
+ if (r.productUrl) lines.push(`
1185
+ ### Producto
1186
+ [Ver producto](${r.productUrl})`);
1187
+ if (!r.pageUrl && !r.productUrl && r.status !== "success") {
1188
+ lines.push("\n_La publicaci\xF3n a\xFAn est\xE1 proces\xE1ndose. Usa `get_task_status` para verificar progreso._");
1189
+ }
1190
+ return {
1191
+ content: [
1192
+ {
1193
+ type: "text",
1194
+ text: lines.join("\n")
1195
+ }
1196
+ ]
1197
+ };
1198
+ }
1199
+ __name(renderShopifyPublishMarkdown, "renderShopifyPublishMarkdown");
1200
+ function toMetaCampaignOverrides(cfg) {
1201
+ const out = {};
1202
+ if (cfg.dailyBudgetUsd !== void 0) out.dailyBudgetUsd = cfg.dailyBudgetUsd;
1203
+ if (cfg.targetCountries !== void 0) out.targetCountries = cfg.targetCountries;
1204
+ if (cfg.campaignName !== void 0) out.campaignName = cfg.campaignName;
1205
+ return out;
1206
+ }
1207
+ __name(toMetaCampaignOverrides, "toMetaCampaignOverrides");
1208
+ function toTikTokCampaignConfig(cfg) {
1209
+ const out = {};
1210
+ if (cfg.dailyBudgetUsd !== void 0) out.dailyBudget = Math.round(cfg.dailyBudgetUsd * 100);
1211
+ if (cfg.targetCountries && cfg.targetCountries.length > 0) out.targetCountry = cfg.targetCountries[0];
1212
+ if (cfg.campaignName !== void 0) out.campaignName = cfg.campaignName;
1213
+ return out;
1214
+ }
1215
+ __name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
1216
+
1217
+ // src/tools/campaigns.ts
1218
+ import { z as z6 } from "zod";
1219
+ var MetaStatusInput = z6.object({
1220
+ campaignId: z6.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
1221
+ });
1222
+ var TikTokStatusInput = z6.object({
1223
+ campaignId: z6.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
1224
+ advertiserId: z6.string().min(1).describe("The same advertiserId used to publish the campaign.")
1225
+ });
1226
+ function registerCampaignTools(server, api) {
1227
+ 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) => {
1228
+ try {
1229
+ const r = await api.getJson(`/v1/facebook/campaigns/${encodeURIComponent(input.campaignId)}`);
1230
+ return renderMetaStatusMarkdown(r);
1231
+ } catch (e) {
1232
+ throw translateError(e);
1233
+ }
1234
+ });
1235
+ 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) => {
1236
+ try {
1237
+ const r = await api.getJson(`/v1/tiktok/campaigns/${encodeURIComponent(input.campaignId)}?advertiserId=${encodeURIComponent(input.advertiserId)}`);
1238
+ return renderTikTokStatusMarkdown(r);
1239
+ } catch (e) {
1240
+ throw translateError(e);
1241
+ }
1242
+ });
1243
+ }
1244
+ __name(registerCampaignTools, "registerCampaignTools");
1245
+ function renderCampaignStatusMarkdown(opts) {
1246
+ const lines = [
1247
+ `\u{1F4CA} **${escapeMdLabel(opts.name)}** (\`${opts.campaignId}\`)`
1248
+ ];
1249
+ for (const [label, value] of opts.statusLines) {
1250
+ lines.push(`- ${label}: \`${value}\``);
1251
+ }
1252
+ if (opts.metrics) {
1253
+ lines.push("\n### M\xE9tricas (lifetime)");
1254
+ lines.push(`- Spend: ${opts.metrics.spend}`);
1255
+ lines.push(`- Impressions: ${opts.metrics.impressions}`);
1256
+ lines.push(`- Clicks: ${opts.metrics.clicks}`);
1257
+ lines.push(`- Reach: ${opts.metrics.reach}`);
1258
+ } else {
1259
+ lines.push(`
1260
+ _${opts.noMetricsHint ?? "A\xFAn no hay m\xE9tricas reportadas."}_`);
1261
+ }
1262
+ return {
1263
+ content: [
1264
+ {
1265
+ type: "text",
1266
+ text: lines.join("\n")
1267
+ }
1268
+ ]
1269
+ };
1270
+ }
1271
+ __name(renderCampaignStatusMarkdown, "renderCampaignStatusMarkdown");
1272
+ function renderMetaStatusMarkdown(r) {
1273
+ const statusLines = [
1274
+ [
1275
+ "Estado efectivo",
1276
+ r.effectiveStatus
1277
+ ],
1278
+ [
1279
+ "Estado configurado",
1280
+ r.configuredStatus
1281
+ ]
1282
+ ];
1283
+ if (r.objective) statusLines.push([
1284
+ "Objetivo",
1285
+ r.objective
1286
+ ]);
1287
+ return renderCampaignStatusMarkdown({
1288
+ name: r.name,
1289
+ campaignId: r.campaignId,
1290
+ statusLines,
1291
+ metrics: r.insights,
1292
+ noMetricsHint: "A\xFAn no hay m\xE9tricas reportadas (la campa\xF1a puede estar pausada o sin entregar)."
1293
+ });
1294
+ }
1295
+ __name(renderMetaStatusMarkdown, "renderMetaStatusMarkdown");
1296
+ function renderTikTokStatusMarkdown(r) {
1297
+ const statusLines = [
1298
+ [
1299
+ "Estado de operaci\xF3n",
1300
+ r.operationStatus
1301
+ ]
1302
+ ];
1303
+ if (r.secondaryStatus) statusLines.push([
1304
+ "Estado secundario",
1305
+ r.secondaryStatus
1306
+ ]);
1307
+ if (r.objectiveType) statusLines.push([
1308
+ "Objetivo",
1309
+ r.objectiveType
1310
+ ]);
1311
+ return renderCampaignStatusMarkdown({
1312
+ name: r.name,
1313
+ campaignId: r.campaignId,
1314
+ statusLines,
1315
+ metrics: r.report
1316
+ });
1317
+ }
1318
+ __name(renderTikTokStatusMarkdown, "renderTikTokStatusMarkdown");
999
1319
 
1000
1320
  // src/server.ts
1001
- function createServer(apiKey, apiBaseUrl, proxyBaseUrl = null) {
1321
+ function createServer(opts) {
1322
+ const { apiKey, apiBaseUrl, proxyBaseUrl = null, transport = "http" } = opts;
1002
1323
  const server = new McpServer({
1003
1324
  name: "productmaker",
1004
1325
  version: MCP_VERSION
@@ -1011,10 +1332,16 @@ function createServer(apiKey, apiBaseUrl, proxyBaseUrl = null) {
1011
1332
  }
1012
1333
  });
1013
1334
  const api = new ApiClient(apiBaseUrl, apiKey);
1014
- registerTaskTools(server, api, proxyBaseUrl);
1015
- registerCreativeTools(server, api);
1335
+ registerTaskTools(server, api, {
1336
+ proxyBaseUrl,
1337
+ transport
1338
+ });
1339
+ registerCreativeTools(server, api, {
1340
+ transport
1341
+ });
1016
1342
  registerConnectionTools(server, api);
1017
1343
  registerPublishTools(server, api);
1344
+ registerCampaignTools(server, api);
1018
1345
  return server;
1019
1346
  }
1020
1347
  __name(createServer, "createServer");
@@ -1154,7 +1481,7 @@ function createApp(opts) {
1154
1481
  legacyHeaders: false
1155
1482
  }));
1156
1483
  app.use(express.json({
1157
- limit: "10mb"
1484
+ limit: "1mb"
1158
1485
  }));
1159
1486
  app.get("/healthz", (_req, res) => {
1160
1487
  res.json({
@@ -1176,7 +1503,12 @@ function createApp(opts) {
1176
1503
  return;
1177
1504
  }
1178
1505
  try {
1179
- const server = createServer(apiKey, opts.apiBaseUrl, opts.proxyBaseUrl ?? null);
1506
+ const server = createServer({
1507
+ apiKey,
1508
+ apiBaseUrl: opts.apiBaseUrl,
1509
+ proxyBaseUrl: opts.proxyBaseUrl ?? null,
1510
+ transport: "http"
1511
+ });
1180
1512
  const transport = new StreamableHTTPServerTransport({
1181
1513
  sessionIdGenerator: void 0
1182
1514
  });