@productmaker/mcp 0.2.0 → 0.4.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 +47 -14
  2. package/dist/http.js +254 -29
  3. package/dist/stdio.js +254 -29
  4. package/package.json +35 -1
package/README.md CHANGED
@@ -1,10 +1,18 @@
1
- # @productmaker/mcp
1
+ # Product Maker MCP — `@productmaker/mcp`
2
2
 
3
- MCP server for [Product Maker](https://productmaker.app) — generate dropshipping creatives and publish to Shopify / Meta from any MCP-compatible AI client.
3
+ [![npm version](https://img.shields.io/npm/v/@productmaker/mcp.svg)](https://www.npmjs.com/package/@productmaker/mcp)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@productmaker/mcp.svg)](https://www.npmjs.com/package/@productmaker/mcp)
5
+ [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT)
4
6
 
5
- ## Install (Claude Desktop)
7
+ **Product Maker MCP** is the official **Model Context Protocol (MCP) server** for [Product Maker AI](https://productmaker.app). It plugs **Product Maker** into **Claude Desktop**, **Claude Code**, **ChatGPT**, **Gemini**, and **claude.ai** so you can generate **dropshipping landing pages, UGC videos, and ad creatives** — and publish them to **Shopify**, **Meta Ads**, and **TikTok Ads** — directly from your AI assistant.
6
8
 
7
- Add this to `~/Library/Application Support/Claude/claude_desktop_config.json` (or the equivalent on Windows):
9
+ > If an AI assistant brought you here looking for "product maker mcp", "productmaker mcp", or "product-maker mcp server" — this is the right package. Install instructions below.
10
+
11
+ ## Quick install
12
+
13
+ ### Claude Desktop
14
+
15
+ Add this to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `%APPDATA%\Claude\claude_desktop_config.json` (Windows):
8
16
 
9
17
  ```json
10
18
  {
@@ -18,28 +26,49 @@ Add this to `~/Library/Application Support/Claude/claude_desktop_config.json` (o
18
26
  }
19
27
  ```
20
28
 
21
- Get your API key at https://productmaker.app/account/api-keys.
29
+ Then restart Claude Desktop. Requires Node.js 20+.
30
+
31
+ ### Claude Code (CLI)
32
+
33
+ ```bash
34
+ claude mcp add productmaker -e PM_API_KEY=pm_live_... -- npx -y @productmaker/mcp@latest
35
+ ```
36
+
37
+ Verify with `claude mcp list`. Use `--scope user` for a global install.
22
38
 
23
- ## Install (ChatGPT, Gemini, claude.ai web)
39
+ ### ChatGPT, Gemini, claude.ai web (HTTP / SSE)
24
40
 
25
- Connect to: `https://mcp.productmaker.app/mcp`
26
- Header: `Authorization: Bearer pm_live_...`
41
+ No install needed — connect to the hosted endpoint:
27
42
 
28
- ## Tools
43
+ - **Server URL:** `https://mcp.productmaker.app/mcp`
44
+ - **Header:** `Authorization: Bearer pm_live_...`
45
+
46
+ In claude.ai: *Settings → Integrations → Add MCP server*.
47
+ In ChatGPT: *Settings → Connectors → Add custom connector*.
48
+
49
+ ### Get your API key
50
+
51
+ Sign up at [productmaker.app](https://productmaker.app) and create a key (prefix `pm_live_`) at [productmaker.app/account/api-keys](https://productmaker.app/account/api-keys). Full setup walkthrough: [productmaker.app/install](https://productmaker.app/install).
52
+
53
+ ## What you can do from your AI assistant
29
54
 
30
55
  | Tool | What it does |
31
56
  |---|---|
32
- | `create_product_task` | Full pipeline from product image (extract → angles → video → landing → creatives) |
57
+ | `create_product_task` | Full pipeline from a product image (extract → angles → video → landing → image creatives) |
33
58
  | `get_task_status` | Check task progress (optional `waitSeconds` for near-sync polling) |
34
59
  | `list_tasks` | List recent tasks |
35
60
  | `edit_task_draft` | Edit angle / pain point / persona before generation |
36
- | `generate_video_creative` | Standalone video creative |
61
+ | `generate_video_creative` | Standalone UGC video creative |
37
62
  | `generate_image_creatives` | Standalone image creatives (multiple variants) |
38
63
  | `generate_landing` | Standalone landing page |
39
64
  | `list_shopify_shops` | List connected Shopify stores |
40
65
  | `list_meta_ad_accounts` | List connected Meta ad accounts |
41
- | `publish_to_shopify` | Publish a completed task to a Shopify store |
42
- | `publish_to_meta` | Create a Meta Ads campaign from a task (always created PAUSED) |
66
+ | `list_tiktok_advertisers` | List connected TikTok Ads advertiser accounts |
67
+ | `publish_to_shopify` | Publish a completed task to a Shopify store (returns markdown with landing + product URLs) |
68
+ | `publish_to_meta` | Create a Meta Ads campaign from a task (always created PAUSED). Accepts optional `angleIndexes[]` to publish a subset of angles and flat `campaignConfig {dailyBudgetUsd, targetCountries[], campaignName}` for tunables |
69
+ | `publish_to_tiktok` | Create a TikTok Ads campaign from a task (always created PAUSED). Same `angleIndexes` + `campaignConfig` shape as Meta |
70
+ | `get_meta_campaign_status` | Read-only status + lifetime insights (spend/impressions/clicks/reach) of a Meta campaign |
71
+ | `get_tiktok_campaign_status` | Read-only status + lifetime report of a TikTok campaign (requires `advertiserId`) |
43
72
 
44
73
  ## Multiple photos per product
45
74
 
@@ -70,9 +99,13 @@ The `base64` field was removed. MCP JSON-RPC transports truncate large tool argu
70
99
  | `MCP_PROXY_BASE_URL` | `https://mcp.productmaker.app` | http only |
71
100
  | `PM_PROXY_SECRET` | — | http only (32+ chars; enables asset proxy) |
72
101
 
102
+ ## Keywords
103
+
104
+ `productmaker mcp` · `product maker mcp` · `product-maker mcp` · `claude mcp dropshipping` · `chatgpt mcp ecommerce` · `mcp server shopify` · `mcp server meta ads` · `ai ugc video mcp` · `landing page mcp` · `dropshipping ai agent`.
105
+
73
106
  ## Support
74
107
 
75
- Issues with the MCP itself: see [productmaker.app/install](https://productmaker.app/install) for setup help, or contact Product Maker support.
108
+ Setup help and full docs: [productmaker.app/install](https://productmaker.app/install). For account/API key questions: support@productmaker.ai.
76
109
 
77
110
  ## License
78
111
 
package/dist/http.js CHANGED
@@ -113,7 +113,7 @@ var ApiClient = class {
113
113
  };
114
114
 
115
115
  // src/index.ts
116
- var MCP_VERSION = "0.2.0";
116
+ var MCP_VERSION = "0.3.0";
117
117
 
118
118
  // src/tools/tasks.ts
119
119
  import { z as z2 } from "zod";
@@ -405,10 +405,12 @@ var McpErrorCode = {
405
405
  PATH_NOT_SUPPORTED_IN_HTTP_MODE: "PATH_NOT_SUPPORTED_IN_HTTP_MODE",
406
406
  TASK_NOT_FOUND: "TASK_NOT_FOUND",
407
407
  TASK_NOT_READY: "TASK_NOT_READY",
408
+ CAMPAIGN_NOT_FOUND: "CAMPAIGN_NOT_FOUND",
408
409
  NO_SHOPIFY_SHOPS: "NO_SHOPIFY_SHOPS",
409
410
  NO_META_AD_ACCOUNTS: "NO_META_AD_ACCOUNTS",
410
411
  SHOPIFY_NOT_CONNECTED: "SHOPIFY_NOT_CONNECTED",
411
412
  META_CAPABILITY_MISSING: "META_CAPABILITY_MISSING",
413
+ TIKTOK_NOT_CONNECTED: "TIKTOK_NOT_CONNECTED",
412
414
  UPSTREAM_TIMEOUT: "UPSTREAM_TIMEOUT",
413
415
  UPSTREAM_UNAVAILABLE: "UPSTREAM_UNAVAILABLE",
414
416
  INTERNAL: "INTERNAL"
@@ -420,7 +422,11 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
420
422
  McpErrorCode.IMAGE_PATH_NOT_FOUND,
421
423
  McpErrorCode.IMAGE_PATH_NOT_READABLE,
422
424
  McpErrorCode.IMAGE_PATH_NOT_A_FILE,
423
- McpErrorCode.PATH_NOT_SUPPORTED_IN_HTTP_MODE
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
424
430
  ]);
425
431
  function mcp(code, message, extra = {}) {
426
432
  const errCode = INVALID_PARAM_CODES.has(code) ? ErrorCode.InvalidParams : ErrorCode.InternalError;
@@ -435,7 +441,9 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
435
441
  "NO_SHOPIFY_SHOPS",
436
442
  "NO_META_AD_ACCOUNTS",
437
443
  "SHOPIFY_NOT_CONNECTED",
438
- "REVOKED_API_KEY"
444
+ "TIKTOK_NOT_CONNECTED",
445
+ "REVOKED_API_KEY",
446
+ "CAMPAIGN_NOT_FOUND"
439
447
  ]);
440
448
  function isRecord(v) {
441
449
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -499,7 +507,14 @@ var NarrativeStyleEnum = z.enum([
499
507
  "problem_product_cta",
500
508
  "product_showcase",
501
509
  "metaphor_product_cta",
502
- "wearable_showcase"
510
+ "wearable_showcase",
511
+ "animated_problem_pixar",
512
+ "sassy_product_pixar",
513
+ "ugc_unboxing_chaos",
514
+ "ugc_curiosity_reveal",
515
+ "ugc_listicle_3",
516
+ "ugc_skeptic_confession",
517
+ "ugc_demo_review"
503
518
  ]);
504
519
  var VideoModelEnum = z.enum([
505
520
  "veo",
@@ -553,6 +568,10 @@ function asText(value) {
553
568
  };
554
569
  }
555
570
  __name(asText, "asText");
571
+ function escapeMdLabel(s) {
572
+ return s.replace(/[\[\]\(\)\{\}`<>\r\n\\*_]/g, " ").replace(/\s+/g, " ").trim();
573
+ }
574
+ __name(escapeMdLabel, "escapeMdLabel");
556
575
  function flatten(obj) {
557
576
  const out = {};
558
577
  for (const [k, v] of Object.entries(obj)) {
@@ -576,8 +595,9 @@ async function submitCreative(api, opts) {
576
595
  const r = await api.postMultipart(opts.path, {
577
596
  images: parts
578
597
  }, flatten(rest));
598
+ const taskId = r.taskId ?? r.jobId;
579
599
  return asText({
580
- taskId: r.taskId,
600
+ taskId,
581
601
  status: "queued",
582
602
  estimatedSeconds: opts.estimatedSeconds
583
603
  });
@@ -716,18 +736,18 @@ __name(sanitizeFilename, "sanitizeFilename");
716
736
  async function fetchTaskOutputs(api, taskId) {
717
737
  const raw = await api.getJson(`/v1/results/${encodeURIComponent(taskId)}?signedUrls=true`);
718
738
  const videos = (raw.videoGenerators ?? []).map((vg) => {
719
- const variants = (vg.variants ?? []).filter((v) => Boolean(v.kind) && Boolean(v.url)).map((v) => ({
739
+ const variants2 = (vg.variants ?? []).filter((v) => Boolean(v.kind) && Boolean(v.url)).map((v) => ({
720
740
  kind: v.kind,
721
741
  url: v.url
722
742
  }));
723
- const fallback = !variants.length && Array.isArray(vg.resultUrls) && vg.resultUrls.length ? vg.resultUrls.map((url) => ({
743
+ const fallback = !variants2.length && Array.isArray(vg.resultUrls) && vg.resultUrls.length ? vg.resultUrls.map((url) => ({
724
744
  kind: "mp4",
725
745
  url
726
746
  })) : [];
727
747
  return {
728
748
  angleKey: vg.angleKey ?? "",
729
749
  angleIndex: vg.angleIndex ?? 0,
730
- variants: variants.length ? variants : fallback
750
+ variants: variants2.length ? variants2 : fallback
731
751
  };
732
752
  }).filter((v) => v.angleKey && v.variants.length);
733
753
  const sections = (raw.landing?.images ?? []).filter((img) => Boolean(img.role) && Boolean(img.url)).map((img) => ({
@@ -735,18 +755,21 @@ async function fetchTaskOutputs(api, taskId) {
735
755
  url: img.url
736
756
  }));
737
757
  const images = [];
738
- if (raw.adCreative?.url) {
758
+ const variants = raw.adCreative?.variants ?? [];
759
+ const hasVariants = variants.some((v) => typeof v === "string");
760
+ if (hasVariants) {
761
+ for (const variant of variants) {
762
+ if (typeof variant === "string") images.push({
763
+ url: variant
764
+ });
765
+ }
766
+ } else if (raw.adCreative?.url) {
739
767
  images.push({
740
768
  url: raw.adCreative.url,
741
769
  width: raw.adCreative.width,
742
770
  height: raw.adCreative.height
743
771
  });
744
772
  }
745
- for (const variant of raw.adCreative?.variants ?? []) {
746
- if (typeof variant === "string") images.push({
747
- url: variant
748
- });
749
- }
750
773
  return {
751
774
  webappUrl: `${WEBAPP_BASE}/tasks/${taskId}`,
752
775
  videos,
@@ -757,21 +780,24 @@ async function fetchTaskOutputs(api, taskId) {
757
780
  };
758
781
  }
759
782
  __name(fetchTaskOutputs, "fetchTaskOutputs");
760
- function escapeMdLabel(s) {
761
- return s.replace(/[\[\]\(\)\{\}`<>\r\n\\]/g, " ").replace(/\s+/g, " ").trim();
762
- }
763
- __name(escapeMdLabel, "escapeMdLabel");
764
783
  function renderOutputsMarkdown(outputs) {
765
784
  const parts = [];
766
785
  parts.push(`\u2705 **Tu tarea est\xE1 lista** \u2014 [Abrir en Product Maker](${outputs.webappUrl})`);
767
786
  if (outputs.images.length) {
768
- parts.push("\n### Ad creative");
769
- for (const img of outputs.images) parts.push(`![](${img.url})`);
787
+ parts.push(`
788
+ ### Ad creative (${outputs.images.length} ${outputs.images.length === 1 ? "variante" : "variantes"})`);
789
+ outputs.images.forEach((img, i) => {
790
+ parts.push(`![v${i}](${img.url})`);
791
+ parts.push(`[\u2B07 Descargar v${i}](${img.url})`);
792
+ });
770
793
  }
771
794
  if (outputs.landing.sections.length) {
772
795
  parts.push(`
773
796
  ### Landing (${outputs.landing.sections.length} secciones)`);
774
- for (const s of outputs.landing.sections) parts.push(`![${escapeMdLabel(s.role)}](${s.url})`);
797
+ for (const s of outputs.landing.sections) {
798
+ parts.push(`![${escapeMdLabel(s.role)}](${s.url})`);
799
+ parts.push(`[\u2B07 Descargar ${escapeMdLabel(s.role)}](${s.url})`);
800
+ }
775
801
  }
776
802
  if (outputs.videos.length) {
777
803
  parts.push("\n### Video");
@@ -794,9 +820,10 @@ var CreateProductTaskInput = z2.object({
794
820
  price: z2.string().optional().describe('Precio que se mostrar\xE1 en la landing y video. Formato libre \u2014 ej. "$89.900", "USD 29.99", "MXN 599". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar. Si te dice "t\xFA elige", om\xEDtelo y el backend sugiere uno.'),
795
821
  offer: z2.string().optional().describe('Oferta o promoci\xF3n a destacar. Ej. "2x1", "Env\xEDo gratis hoy", "50% OFF". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar (es v\xE1lido que diga "sin oferta"). Si te dice "t\xFA elige", om\xEDtelo.'),
796
822
  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".'),
797
- 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".'),
823
+ narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" = UGC cl\xE1sico multi-hook (default, alta conversi\xF3n). "problem_product_cta" = problema\u2192soluci\xF3n. "product_showcase" = features. "metaphor_product_cta" = met\xE1fora visual. "wearable_showcase" = para ropa/accesorios. "ugc_unboxing_chaos" = unboxing agresivo. "ugc_curiosity_reveal" = hook con producto oculto. "ugc_listicle_3" = 3 razones (30s recomendado). "ugc_skeptic_confession" = esc\xE9ptico\u2192conversi\xF3n (30s recomendado). "ugc_demo_review" = demo en uso a lo largo de d\xEDas. Si se omite, default "review".'),
798
824
  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.'),
799
825
  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."),
826
+ angleNarratives: NarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). angleNarratives[i] fuerza la narrativa del \xE1ngulo en la posici\xF3n i; usa los mismos valores que narrativeStyle. Si la lista es m\xE1s corta que la cantidad de \xE1ngulos, las posiciones restantes las elige la IA. Ej: ["animated_problem_pixar","review","ugc_skeptic_confession"] para 3 \xE1ngulos distintos. Requiere multiAngleVideos:true (o videoAngleCount>1) para que haya varios \xE1ngulos. Si se omite, todas las narrativas se auto-seleccionan.'),
800
827
  outputs: z2.array(z2.enum([
801
828
  "landing",
802
829
  "video",
@@ -843,6 +870,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
843
870
  "options.offer": input.offer,
844
871
  "options.videoStyle": input.videoStyle,
845
872
  "options.narrativeStyle": input.narrativeStyle,
873
+ "options.angleNarratives": input.angleNarratives,
846
874
  "options.videoModel": input.videoModel,
847
875
  "options.multiAngleVideos": input.multiAngleVideos,
848
876
  "options.selectedOutputs": input.outputs,
@@ -943,7 +971,7 @@ var VideoInput = z3.object({
943
971
  "Auto"
944
972
  ]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
945
973
  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."),
946
- narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" (testimonial, default), "problem_product_cta", "product_showcase", "metaphor_product_cta", "wearable_showcase".'),
974
+ narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" (UGC cl\xE1sico multi-hook, default), "problem_product_cta", "product_showcase", "metaphor_product_cta", "wearable_showcase", "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones numeradas, recomendado 30s), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n, recomendado 30s), "ugc_demo_review" (demo en uso a lo largo de d\xEDas).'),
947
975
  videoStyle: VideoStyleEnum.optional().describe('Opcional. Estilo visual. "ugc" (creador hablando, default), "cartoon_3d", "product_only", "ai_dynamic".'),
948
976
  videoModel: VideoModelEnum.optional().describe('Opcional. Modelo de video. "veo" (default, calidad Google). Ver lista de valores v\xE1lidos.'),
949
977
  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"].'),
@@ -1049,6 +1077,14 @@ function registerConnectionTools(server, api) {
1049
1077
  throw translateError(e);
1050
1078
  }
1051
1079
  });
1080
+ 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 () => {
1081
+ try {
1082
+ const r = await api.getJson("/v1/tiktok/advertisers");
1083
+ return asText(r);
1084
+ } catch (e) {
1085
+ throw translateError(e);
1086
+ }
1087
+ });
1052
1088
  }
1053
1089
  __name(registerConnectionTools, "registerConnectionTools");
1054
1090
 
@@ -1059,14 +1095,38 @@ var ShopifyInput = z5.object({
1059
1095
  shopDomain: z5.string().min(1),
1060
1096
  numLandingImages: z5.number().int().min(1).max(7).optional(),
1061
1097
  variantIndex: z5.number().int().min(0).optional(),
1098
+ /** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
1099
+ * When true, the landing renders variant-picker pills; when false, it ships
1100
+ * a single-option product page. Defaults to false. */
1062
1101
  showVariantPicker: z5.boolean().optional()
1063
1102
  });
1103
+ var McpCampaignConfig = z5.object({
1104
+ dailyBudgetUsd: z5.number().positive().optional(),
1105
+ /** ISO-3166-1 alpha-2 country codes. Meta accepts max 1 (single-country
1106
+ * ad set). TikTok takes only the first entry. */
1107
+ targetCountries: z5.array(z5.string().length(2)).max(1).optional(),
1108
+ campaignName: z5.string().trim().min(1).max(100).optional()
1109
+ });
1064
1110
  var MetaInput = z5.object({
1065
1111
  taskId: z5.string().min(1),
1066
- adAccountId: z5.string().min(1)
1112
+ adAccountId: z5.string().min(1),
1113
+ pixelId: z5.string().min(1).optional(),
1114
+ landingUrl: z5.string().url().optional(),
1115
+ /** Select which angles to publish by 0-based index. Omit to publish all
1116
+ * successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
1117
+ angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
1118
+ campaignConfig: McpCampaignConfig.optional()
1119
+ });
1120
+ var TikTokInput = z5.object({
1121
+ taskId: z5.string().min(1),
1122
+ advertiserId: z5.string().min(1),
1123
+ pixelId: z5.string().min(1).optional(),
1124
+ landingUrl: z5.string().url().optional(),
1125
+ angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
1126
+ campaignConfig: McpCampaignConfig.optional()
1067
1127
  });
1068
1128
  function registerPublishTools(server, api) {
1069
- 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) => {
1129
+ 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) => {
1070
1130
  try {
1071
1131
  const numImages = input.numLandingImages ?? 3;
1072
1132
  const variantIndex = input.variantIndex ?? 0;
@@ -1081,16 +1141,37 @@ function registerPublishTools(server, api) {
1081
1141
  selectedLandingImages,
1082
1142
  showVariantPicker: input.showVariantPicker
1083
1143
  });
1084
- return asText(r);
1144
+ return renderShopifyPublishMarkdown(r);
1085
1145
  } catch (e) {
1086
1146
  throw translateError(e);
1087
1147
  }
1088
1148
  });
1089
- 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) => {
1149
+ 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 }) => {
1090
1150
  try {
1091
- const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(input.taskId)}`, {
1092
- adAccountId: input.adAccountId
1151
+ const body = {
1152
+ ...rest
1153
+ };
1154
+ if (campaignConfig) {
1155
+ body.campaignOverrides = toMetaCampaignOverrides(campaignConfig);
1156
+ }
1157
+ const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1158
+ return asText({
1159
+ ...r,
1160
+ status: "PAUSED"
1093
1161
  });
1162
+ } catch (e) {
1163
+ throw translateError(e);
1164
+ }
1165
+ });
1166
+ 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 }) => {
1167
+ try {
1168
+ const body = {
1169
+ ...rest
1170
+ };
1171
+ if (campaignConfig) {
1172
+ body.campaignConfig = toTikTokCampaignConfig(campaignConfig);
1173
+ }
1174
+ const r = await api.postJson(`/v1/tiktok/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1094
1175
  return asText({
1095
1176
  ...r,
1096
1177
  status: "PAUSED"
@@ -1101,6 +1182,149 @@ function registerPublishTools(server, api) {
1101
1182
  });
1102
1183
  }
1103
1184
  __name(registerPublishTools, "registerPublishTools");
1185
+ function renderShopifyPublishMarkdown(r) {
1186
+ const lines = [];
1187
+ const statusEmoji = r.status === "success" ? "\u2705" : r.status === "error" ? "\u274C" : "\u23F3";
1188
+ lines.push(`${statusEmoji} **Publicaci\xF3n en Shopify** \u2014 ${r.shopDomain}`);
1189
+ lines.push(`Estado: \`${r.status}\``);
1190
+ if (r.pageUrl) lines.push(`
1191
+ ### Landing
1192
+ [Ver landing publicada](${r.pageUrl})`);
1193
+ if (r.productUrl) lines.push(`
1194
+ ### Producto
1195
+ [Ver producto](${r.productUrl})`);
1196
+ if (!r.pageUrl && !r.productUrl && r.status !== "success") {
1197
+ lines.push("\n_La publicaci\xF3n a\xFAn est\xE1 proces\xE1ndose. Usa `get_task_status` para verificar progreso._");
1198
+ }
1199
+ return {
1200
+ content: [
1201
+ {
1202
+ type: "text",
1203
+ text: lines.join("\n")
1204
+ }
1205
+ ]
1206
+ };
1207
+ }
1208
+ __name(renderShopifyPublishMarkdown, "renderShopifyPublishMarkdown");
1209
+ function toMetaCampaignOverrides(cfg) {
1210
+ const out = {};
1211
+ if (cfg.dailyBudgetUsd !== void 0) out.dailyBudgetUsd = cfg.dailyBudgetUsd;
1212
+ if (cfg.targetCountries !== void 0) out.targetCountries = cfg.targetCountries;
1213
+ if (cfg.campaignName !== void 0) out.campaignName = cfg.campaignName;
1214
+ return out;
1215
+ }
1216
+ __name(toMetaCampaignOverrides, "toMetaCampaignOverrides");
1217
+ function toTikTokCampaignConfig(cfg) {
1218
+ const out = {};
1219
+ if (cfg.dailyBudgetUsd !== void 0) out.dailyBudget = Math.round(cfg.dailyBudgetUsd * 100);
1220
+ if (cfg.targetCountries && cfg.targetCountries.length > 0) out.targetCountry = cfg.targetCountries[0];
1221
+ if (cfg.campaignName !== void 0) out.campaignName = cfg.campaignName;
1222
+ return out;
1223
+ }
1224
+ __name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
1225
+
1226
+ // src/tools/campaigns.ts
1227
+ import { z as z6 } from "zod";
1228
+ var MetaStatusInput = z6.object({
1229
+ campaignId: z6.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
1230
+ });
1231
+ var TikTokStatusInput = z6.object({
1232
+ campaignId: z6.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
1233
+ advertiserId: z6.string().min(1).describe("The same advertiserId used to publish the campaign.")
1234
+ });
1235
+ function registerCampaignTools(server, api) {
1236
+ 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) => {
1237
+ try {
1238
+ const r = await api.getJson(`/v1/facebook/campaigns/${encodeURIComponent(input.campaignId)}`);
1239
+ return renderMetaStatusMarkdown(r);
1240
+ } catch (e) {
1241
+ throw translateError(e);
1242
+ }
1243
+ });
1244
+ 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) => {
1245
+ try {
1246
+ const r = await api.getJson(`/v1/tiktok/campaigns/${encodeURIComponent(input.campaignId)}?advertiserId=${encodeURIComponent(input.advertiserId)}`);
1247
+ return renderTikTokStatusMarkdown(r);
1248
+ } catch (e) {
1249
+ throw translateError(e);
1250
+ }
1251
+ });
1252
+ }
1253
+ __name(registerCampaignTools, "registerCampaignTools");
1254
+ function renderCampaignStatusMarkdown(opts) {
1255
+ const lines = [
1256
+ `\u{1F4CA} **${escapeMdLabel(opts.name)}** (\`${opts.campaignId}\`)`
1257
+ ];
1258
+ for (const [label, value] of opts.statusLines) {
1259
+ lines.push(`- ${label}: \`${value}\``);
1260
+ }
1261
+ if (opts.metrics) {
1262
+ lines.push("\n### M\xE9tricas (lifetime)");
1263
+ lines.push(`- Spend: ${opts.metrics.spend}`);
1264
+ lines.push(`- Impressions: ${opts.metrics.impressions}`);
1265
+ lines.push(`- Clicks: ${opts.metrics.clicks}`);
1266
+ lines.push(`- Reach: ${opts.metrics.reach}`);
1267
+ } else {
1268
+ lines.push(`
1269
+ _${opts.noMetricsHint ?? "A\xFAn no hay m\xE9tricas reportadas."}_`);
1270
+ }
1271
+ return {
1272
+ content: [
1273
+ {
1274
+ type: "text",
1275
+ text: lines.join("\n")
1276
+ }
1277
+ ]
1278
+ };
1279
+ }
1280
+ __name(renderCampaignStatusMarkdown, "renderCampaignStatusMarkdown");
1281
+ function renderMetaStatusMarkdown(r) {
1282
+ const statusLines = [
1283
+ [
1284
+ "Estado efectivo",
1285
+ r.effectiveStatus
1286
+ ],
1287
+ [
1288
+ "Estado configurado",
1289
+ r.configuredStatus
1290
+ ]
1291
+ ];
1292
+ if (r.objective) statusLines.push([
1293
+ "Objetivo",
1294
+ r.objective
1295
+ ]);
1296
+ return renderCampaignStatusMarkdown({
1297
+ name: r.name,
1298
+ campaignId: r.campaignId,
1299
+ statusLines,
1300
+ metrics: r.insights,
1301
+ noMetricsHint: "A\xFAn no hay m\xE9tricas reportadas (la campa\xF1a puede estar pausada o sin entregar)."
1302
+ });
1303
+ }
1304
+ __name(renderMetaStatusMarkdown, "renderMetaStatusMarkdown");
1305
+ function renderTikTokStatusMarkdown(r) {
1306
+ const statusLines = [
1307
+ [
1308
+ "Estado de operaci\xF3n",
1309
+ r.operationStatus
1310
+ ]
1311
+ ];
1312
+ if (r.secondaryStatus) statusLines.push([
1313
+ "Estado secundario",
1314
+ r.secondaryStatus
1315
+ ]);
1316
+ if (r.objectiveType) statusLines.push([
1317
+ "Objetivo",
1318
+ r.objectiveType
1319
+ ]);
1320
+ return renderCampaignStatusMarkdown({
1321
+ name: r.name,
1322
+ campaignId: r.campaignId,
1323
+ statusLines,
1324
+ metrics: r.report
1325
+ });
1326
+ }
1327
+ __name(renderTikTokStatusMarkdown, "renderTikTokStatusMarkdown");
1104
1328
 
1105
1329
  // src/server.ts
1106
1330
  function createServer(opts) {
@@ -1126,6 +1350,7 @@ function createServer(opts) {
1126
1350
  });
1127
1351
  registerConnectionTools(server, api);
1128
1352
  registerPublishTools(server, api);
1353
+ registerCampaignTools(server, api);
1129
1354
  return server;
1130
1355
  }
1131
1356
  __name(createServer, "createServer");
package/dist/stdio.js CHANGED
@@ -110,7 +110,7 @@ var ApiClient = class {
110
110
  };
111
111
 
112
112
  // src/index.ts
113
- var MCP_VERSION = "0.2.0";
113
+ var MCP_VERSION = "0.3.0";
114
114
 
115
115
  // src/tools/tasks.ts
116
116
  import { z as z2 } from "zod";
@@ -402,10 +402,12 @@ var McpErrorCode = {
402
402
  PATH_NOT_SUPPORTED_IN_HTTP_MODE: "PATH_NOT_SUPPORTED_IN_HTTP_MODE",
403
403
  TASK_NOT_FOUND: "TASK_NOT_FOUND",
404
404
  TASK_NOT_READY: "TASK_NOT_READY",
405
+ CAMPAIGN_NOT_FOUND: "CAMPAIGN_NOT_FOUND",
405
406
  NO_SHOPIFY_SHOPS: "NO_SHOPIFY_SHOPS",
406
407
  NO_META_AD_ACCOUNTS: "NO_META_AD_ACCOUNTS",
407
408
  SHOPIFY_NOT_CONNECTED: "SHOPIFY_NOT_CONNECTED",
408
409
  META_CAPABILITY_MISSING: "META_CAPABILITY_MISSING",
410
+ TIKTOK_NOT_CONNECTED: "TIKTOK_NOT_CONNECTED",
409
411
  UPSTREAM_TIMEOUT: "UPSTREAM_TIMEOUT",
410
412
  UPSTREAM_UNAVAILABLE: "UPSTREAM_UNAVAILABLE",
411
413
  INTERNAL: "INTERNAL"
@@ -417,7 +419,11 @@ var INVALID_PARAM_CODES = /* @__PURE__ */ new Set([
417
419
  McpErrorCode.IMAGE_PATH_NOT_FOUND,
418
420
  McpErrorCode.IMAGE_PATH_NOT_READABLE,
419
421
  McpErrorCode.IMAGE_PATH_NOT_A_FILE,
420
- McpErrorCode.PATH_NOT_SUPPORTED_IN_HTTP_MODE
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
421
427
  ]);
422
428
  function mcp(code, message, extra = {}) {
423
429
  const errCode = INVALID_PARAM_CODES.has(code) ? ErrorCode.InvalidParams : ErrorCode.InternalError;
@@ -432,7 +438,9 @@ var PASSTHROUGH_CODES = /* @__PURE__ */ new Set([
432
438
  "NO_SHOPIFY_SHOPS",
433
439
  "NO_META_AD_ACCOUNTS",
434
440
  "SHOPIFY_NOT_CONNECTED",
435
- "REVOKED_API_KEY"
441
+ "TIKTOK_NOT_CONNECTED",
442
+ "REVOKED_API_KEY",
443
+ "CAMPAIGN_NOT_FOUND"
436
444
  ]);
437
445
  function isRecord(v) {
438
446
  return typeof v === "object" && v !== null && !Array.isArray(v);
@@ -496,7 +504,14 @@ var NarrativeStyleEnum = z.enum([
496
504
  "problem_product_cta",
497
505
  "product_showcase",
498
506
  "metaphor_product_cta",
499
- "wearable_showcase"
507
+ "wearable_showcase",
508
+ "animated_problem_pixar",
509
+ "sassy_product_pixar",
510
+ "ugc_unboxing_chaos",
511
+ "ugc_curiosity_reveal",
512
+ "ugc_listicle_3",
513
+ "ugc_skeptic_confession",
514
+ "ugc_demo_review"
500
515
  ]);
501
516
  var VideoModelEnum = z.enum([
502
517
  "veo",
@@ -550,6 +565,10 @@ function asText(value) {
550
565
  };
551
566
  }
552
567
  __name(asText, "asText");
568
+ function escapeMdLabel(s) {
569
+ return s.replace(/[\[\]\(\)\{\}`<>\r\n\\*_]/g, " ").replace(/\s+/g, " ").trim();
570
+ }
571
+ __name(escapeMdLabel, "escapeMdLabel");
553
572
  function flatten(obj) {
554
573
  const out = {};
555
574
  for (const [k, v] of Object.entries(obj)) {
@@ -573,8 +592,9 @@ async function submitCreative(api, opts) {
573
592
  const r = await api.postMultipart(opts.path, {
574
593
  images: parts
575
594
  }, flatten(rest));
595
+ const taskId = r.taskId ?? r.jobId;
576
596
  return asText({
577
- taskId: r.taskId,
597
+ taskId,
578
598
  status: "queued",
579
599
  estimatedSeconds: opts.estimatedSeconds
580
600
  });
@@ -672,18 +692,18 @@ __name(sanitizeFilename, "sanitizeFilename");
672
692
  async function fetchTaskOutputs(api, taskId) {
673
693
  const raw = await api.getJson(`/v1/results/${encodeURIComponent(taskId)}?signedUrls=true`);
674
694
  const videos = (raw.videoGenerators ?? []).map((vg) => {
675
- const variants = (vg.variants ?? []).filter((v) => Boolean(v.kind) && Boolean(v.url)).map((v) => ({
695
+ const variants2 = (vg.variants ?? []).filter((v) => Boolean(v.kind) && Boolean(v.url)).map((v) => ({
676
696
  kind: v.kind,
677
697
  url: v.url
678
698
  }));
679
- const fallback = !variants.length && Array.isArray(vg.resultUrls) && vg.resultUrls.length ? vg.resultUrls.map((url) => ({
699
+ const fallback = !variants2.length && Array.isArray(vg.resultUrls) && vg.resultUrls.length ? vg.resultUrls.map((url) => ({
680
700
  kind: "mp4",
681
701
  url
682
702
  })) : [];
683
703
  return {
684
704
  angleKey: vg.angleKey ?? "",
685
705
  angleIndex: vg.angleIndex ?? 0,
686
- variants: variants.length ? variants : fallback
706
+ variants: variants2.length ? variants2 : fallback
687
707
  };
688
708
  }).filter((v) => v.angleKey && v.variants.length);
689
709
  const sections = (raw.landing?.images ?? []).filter((img) => Boolean(img.role) && Boolean(img.url)).map((img) => ({
@@ -691,18 +711,21 @@ async function fetchTaskOutputs(api, taskId) {
691
711
  url: img.url
692
712
  }));
693
713
  const images = [];
694
- if (raw.adCreative?.url) {
714
+ const variants = raw.adCreative?.variants ?? [];
715
+ const hasVariants = variants.some((v) => typeof v === "string");
716
+ if (hasVariants) {
717
+ for (const variant of variants) {
718
+ if (typeof variant === "string") images.push({
719
+ url: variant
720
+ });
721
+ }
722
+ } else if (raw.adCreative?.url) {
695
723
  images.push({
696
724
  url: raw.adCreative.url,
697
725
  width: raw.adCreative.width,
698
726
  height: raw.adCreative.height
699
727
  });
700
728
  }
701
- for (const variant of raw.adCreative?.variants ?? []) {
702
- if (typeof variant === "string") images.push({
703
- url: variant
704
- });
705
- }
706
729
  return {
707
730
  webappUrl: `${WEBAPP_BASE}/tasks/${taskId}`,
708
731
  videos,
@@ -713,21 +736,24 @@ async function fetchTaskOutputs(api, taskId) {
713
736
  };
714
737
  }
715
738
  __name(fetchTaskOutputs, "fetchTaskOutputs");
716
- function escapeMdLabel(s) {
717
- return s.replace(/[\[\]\(\)\{\}`<>\r\n\\]/g, " ").replace(/\s+/g, " ").trim();
718
- }
719
- __name(escapeMdLabel, "escapeMdLabel");
720
739
  function renderOutputsMarkdown(outputs) {
721
740
  const parts = [];
722
741
  parts.push(`\u2705 **Tu tarea est\xE1 lista** \u2014 [Abrir en Product Maker](${outputs.webappUrl})`);
723
742
  if (outputs.images.length) {
724
- parts.push("\n### Ad creative");
725
- for (const img of outputs.images) parts.push(`![](${img.url})`);
743
+ parts.push(`
744
+ ### Ad creative (${outputs.images.length} ${outputs.images.length === 1 ? "variante" : "variantes"})`);
745
+ outputs.images.forEach((img, i) => {
746
+ parts.push(`![v${i}](${img.url})`);
747
+ parts.push(`[\u2B07 Descargar v${i}](${img.url})`);
748
+ });
726
749
  }
727
750
  if (outputs.landing.sections.length) {
728
751
  parts.push(`
729
752
  ### Landing (${outputs.landing.sections.length} secciones)`);
730
- for (const s of outputs.landing.sections) parts.push(`![${escapeMdLabel(s.role)}](${s.url})`);
753
+ for (const s of outputs.landing.sections) {
754
+ parts.push(`![${escapeMdLabel(s.role)}](${s.url})`);
755
+ parts.push(`[\u2B07 Descargar ${escapeMdLabel(s.role)}](${s.url})`);
756
+ }
731
757
  }
732
758
  if (outputs.videos.length) {
733
759
  parts.push("\n### Video");
@@ -750,9 +776,10 @@ var CreateProductTaskInput = z2.object({
750
776
  price: z2.string().optional().describe('Precio que se mostrar\xE1 en la landing y video. Formato libre \u2014 ej. "$89.900", "USD 29.99", "MXN 599". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar. Si te dice "t\xFA elige", om\xEDtelo y el backend sugiere uno.'),
751
777
  offer: z2.string().optional().describe('Oferta o promoci\xF3n a destacar. Ej. "2x1", "Env\xEDo gratis hoy", "50% OFF". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar (es v\xE1lido que diga "sin oferta"). Si te dice "t\xFA elige", om\xEDtelo.'),
752
778
  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".'),
753
- 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".'),
779
+ narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" = UGC cl\xE1sico multi-hook (default, alta conversi\xF3n). "problem_product_cta" = problema\u2192soluci\xF3n. "product_showcase" = features. "metaphor_product_cta" = met\xE1fora visual. "wearable_showcase" = para ropa/accesorios. "ugc_unboxing_chaos" = unboxing agresivo. "ugc_curiosity_reveal" = hook con producto oculto. "ugc_listicle_3" = 3 razones (30s recomendado). "ugc_skeptic_confession" = esc\xE9ptico\u2192conversi\xF3n (30s recomendado). "ugc_demo_review" = demo en uso a lo largo de d\xEDas. Si se omite, default "review".'),
754
780
  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.'),
755
781
  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."),
782
+ angleNarratives: NarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). angleNarratives[i] fuerza la narrativa del \xE1ngulo en la posici\xF3n i; usa los mismos valores que narrativeStyle. Si la lista es m\xE1s corta que la cantidad de \xE1ngulos, las posiciones restantes las elige la IA. Ej: ["animated_problem_pixar","review","ugc_skeptic_confession"] para 3 \xE1ngulos distintos. Requiere multiAngleVideos:true (o videoAngleCount>1) para que haya varios \xE1ngulos. Si se omite, todas las narrativas se auto-seleccionan.'),
756
783
  outputs: z2.array(z2.enum([
757
784
  "landing",
758
785
  "video",
@@ -799,6 +826,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
799
826
  "options.offer": input.offer,
800
827
  "options.videoStyle": input.videoStyle,
801
828
  "options.narrativeStyle": input.narrativeStyle,
829
+ "options.angleNarratives": input.angleNarratives,
802
830
  "options.videoModel": input.videoModel,
803
831
  "options.multiAngleVideos": input.multiAngleVideos,
804
832
  "options.selectedOutputs": input.outputs,
@@ -899,7 +927,7 @@ var VideoInput = z3.object({
899
927
  "Auto"
900
928
  ]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
901
929
  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."),
902
- narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" (testimonial, default), "problem_product_cta", "product_showcase", "metaphor_product_cta", "wearable_showcase".'),
930
+ narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" (UGC cl\xE1sico multi-hook, default), "problem_product_cta", "product_showcase", "metaphor_product_cta", "wearable_showcase", "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones numeradas, recomendado 30s), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n, recomendado 30s), "ugc_demo_review" (demo en uso a lo largo de d\xEDas).'),
903
931
  videoStyle: VideoStyleEnum.optional().describe('Opcional. Estilo visual. "ugc" (creador hablando, default), "cartoon_3d", "product_only", "ai_dynamic".'),
904
932
  videoModel: VideoModelEnum.optional().describe('Opcional. Modelo de video. "veo" (default, calidad Google). Ver lista de valores v\xE1lidos.'),
905
933
  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"].'),
@@ -1005,6 +1033,14 @@ function registerConnectionTools(server, api) {
1005
1033
  throw translateError(e);
1006
1034
  }
1007
1035
  });
1036
+ 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 () => {
1037
+ try {
1038
+ const r = await api.getJson("/v1/tiktok/advertisers");
1039
+ return asText(r);
1040
+ } catch (e) {
1041
+ throw translateError(e);
1042
+ }
1043
+ });
1008
1044
  }
1009
1045
  __name(registerConnectionTools, "registerConnectionTools");
1010
1046
 
@@ -1015,14 +1051,38 @@ var ShopifyInput = z5.object({
1015
1051
  shopDomain: z5.string().min(1),
1016
1052
  numLandingImages: z5.number().int().min(1).max(7).optional(),
1017
1053
  variantIndex: z5.number().int().min(0).optional(),
1054
+ /** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
1055
+ * When true, the landing renders variant-picker pills; when false, it ships
1056
+ * a single-option product page. Defaults to false. */
1018
1057
  showVariantPicker: z5.boolean().optional()
1019
1058
  });
1059
+ var McpCampaignConfig = z5.object({
1060
+ dailyBudgetUsd: z5.number().positive().optional(),
1061
+ /** ISO-3166-1 alpha-2 country codes. Meta accepts max 1 (single-country
1062
+ * ad set). TikTok takes only the first entry. */
1063
+ targetCountries: z5.array(z5.string().length(2)).max(1).optional(),
1064
+ campaignName: z5.string().trim().min(1).max(100).optional()
1065
+ });
1020
1066
  var MetaInput = z5.object({
1021
1067
  taskId: z5.string().min(1),
1022
- adAccountId: z5.string().min(1)
1068
+ adAccountId: z5.string().min(1),
1069
+ pixelId: z5.string().min(1).optional(),
1070
+ landingUrl: z5.string().url().optional(),
1071
+ /** Select which angles to publish by 0-based index. Omit to publish all
1072
+ * successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
1073
+ angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
1074
+ campaignConfig: McpCampaignConfig.optional()
1075
+ });
1076
+ var TikTokInput = z5.object({
1077
+ taskId: z5.string().min(1),
1078
+ advertiserId: z5.string().min(1),
1079
+ pixelId: z5.string().min(1).optional(),
1080
+ landingUrl: z5.string().url().optional(),
1081
+ angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
1082
+ campaignConfig: McpCampaignConfig.optional()
1023
1083
  });
1024
1084
  function registerPublishTools(server, api) {
1025
- 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) => {
1085
+ 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) => {
1026
1086
  try {
1027
1087
  const numImages = input.numLandingImages ?? 3;
1028
1088
  const variantIndex = input.variantIndex ?? 0;
@@ -1037,16 +1097,37 @@ function registerPublishTools(server, api) {
1037
1097
  selectedLandingImages,
1038
1098
  showVariantPicker: input.showVariantPicker
1039
1099
  });
1040
- return asText(r);
1100
+ return renderShopifyPublishMarkdown(r);
1041
1101
  } catch (e) {
1042
1102
  throw translateError(e);
1043
1103
  }
1044
1104
  });
1045
- 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) => {
1105
+ 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 }) => {
1046
1106
  try {
1047
- const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(input.taskId)}`, {
1048
- adAccountId: input.adAccountId
1107
+ const body = {
1108
+ ...rest
1109
+ };
1110
+ if (campaignConfig) {
1111
+ body.campaignOverrides = toMetaCampaignOverrides(campaignConfig);
1112
+ }
1113
+ const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1114
+ return asText({
1115
+ ...r,
1116
+ status: "PAUSED"
1049
1117
  });
1118
+ } catch (e) {
1119
+ throw translateError(e);
1120
+ }
1121
+ });
1122
+ 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 }) => {
1123
+ try {
1124
+ const body = {
1125
+ ...rest
1126
+ };
1127
+ if (campaignConfig) {
1128
+ body.campaignConfig = toTikTokCampaignConfig(campaignConfig);
1129
+ }
1130
+ const r = await api.postJson(`/v1/tiktok/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1050
1131
  return asText({
1051
1132
  ...r,
1052
1133
  status: "PAUSED"
@@ -1057,6 +1138,149 @@ function registerPublishTools(server, api) {
1057
1138
  });
1058
1139
  }
1059
1140
  __name(registerPublishTools, "registerPublishTools");
1141
+ function renderShopifyPublishMarkdown(r) {
1142
+ const lines = [];
1143
+ const statusEmoji = r.status === "success" ? "\u2705" : r.status === "error" ? "\u274C" : "\u23F3";
1144
+ lines.push(`${statusEmoji} **Publicaci\xF3n en Shopify** \u2014 ${r.shopDomain}`);
1145
+ lines.push(`Estado: \`${r.status}\``);
1146
+ if (r.pageUrl) lines.push(`
1147
+ ### Landing
1148
+ [Ver landing publicada](${r.pageUrl})`);
1149
+ if (r.productUrl) lines.push(`
1150
+ ### Producto
1151
+ [Ver producto](${r.productUrl})`);
1152
+ if (!r.pageUrl && !r.productUrl && r.status !== "success") {
1153
+ lines.push("\n_La publicaci\xF3n a\xFAn est\xE1 proces\xE1ndose. Usa `get_task_status` para verificar progreso._");
1154
+ }
1155
+ return {
1156
+ content: [
1157
+ {
1158
+ type: "text",
1159
+ text: lines.join("\n")
1160
+ }
1161
+ ]
1162
+ };
1163
+ }
1164
+ __name(renderShopifyPublishMarkdown, "renderShopifyPublishMarkdown");
1165
+ function toMetaCampaignOverrides(cfg) {
1166
+ const out = {};
1167
+ if (cfg.dailyBudgetUsd !== void 0) out.dailyBudgetUsd = cfg.dailyBudgetUsd;
1168
+ if (cfg.targetCountries !== void 0) out.targetCountries = cfg.targetCountries;
1169
+ if (cfg.campaignName !== void 0) out.campaignName = cfg.campaignName;
1170
+ return out;
1171
+ }
1172
+ __name(toMetaCampaignOverrides, "toMetaCampaignOverrides");
1173
+ function toTikTokCampaignConfig(cfg) {
1174
+ const out = {};
1175
+ if (cfg.dailyBudgetUsd !== void 0) out.dailyBudget = Math.round(cfg.dailyBudgetUsd * 100);
1176
+ if (cfg.targetCountries && cfg.targetCountries.length > 0) out.targetCountry = cfg.targetCountries[0];
1177
+ if (cfg.campaignName !== void 0) out.campaignName = cfg.campaignName;
1178
+ return out;
1179
+ }
1180
+ __name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
1181
+
1182
+ // src/tools/campaigns.ts
1183
+ import { z as z6 } from "zod";
1184
+ var MetaStatusInput = z6.object({
1185
+ campaignId: z6.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
1186
+ });
1187
+ var TikTokStatusInput = z6.object({
1188
+ campaignId: z6.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
1189
+ advertiserId: z6.string().min(1).describe("The same advertiserId used to publish the campaign.")
1190
+ });
1191
+ function registerCampaignTools(server, api) {
1192
+ 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) => {
1193
+ try {
1194
+ const r = await api.getJson(`/v1/facebook/campaigns/${encodeURIComponent(input.campaignId)}`);
1195
+ return renderMetaStatusMarkdown(r);
1196
+ } catch (e) {
1197
+ throw translateError(e);
1198
+ }
1199
+ });
1200
+ 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) => {
1201
+ try {
1202
+ const r = await api.getJson(`/v1/tiktok/campaigns/${encodeURIComponent(input.campaignId)}?advertiserId=${encodeURIComponent(input.advertiserId)}`);
1203
+ return renderTikTokStatusMarkdown(r);
1204
+ } catch (e) {
1205
+ throw translateError(e);
1206
+ }
1207
+ });
1208
+ }
1209
+ __name(registerCampaignTools, "registerCampaignTools");
1210
+ function renderCampaignStatusMarkdown(opts) {
1211
+ const lines = [
1212
+ `\u{1F4CA} **${escapeMdLabel(opts.name)}** (\`${opts.campaignId}\`)`
1213
+ ];
1214
+ for (const [label, value] of opts.statusLines) {
1215
+ lines.push(`- ${label}: \`${value}\``);
1216
+ }
1217
+ if (opts.metrics) {
1218
+ lines.push("\n### M\xE9tricas (lifetime)");
1219
+ lines.push(`- Spend: ${opts.metrics.spend}`);
1220
+ lines.push(`- Impressions: ${opts.metrics.impressions}`);
1221
+ lines.push(`- Clicks: ${opts.metrics.clicks}`);
1222
+ lines.push(`- Reach: ${opts.metrics.reach}`);
1223
+ } else {
1224
+ lines.push(`
1225
+ _${opts.noMetricsHint ?? "A\xFAn no hay m\xE9tricas reportadas."}_`);
1226
+ }
1227
+ return {
1228
+ content: [
1229
+ {
1230
+ type: "text",
1231
+ text: lines.join("\n")
1232
+ }
1233
+ ]
1234
+ };
1235
+ }
1236
+ __name(renderCampaignStatusMarkdown, "renderCampaignStatusMarkdown");
1237
+ function renderMetaStatusMarkdown(r) {
1238
+ const statusLines = [
1239
+ [
1240
+ "Estado efectivo",
1241
+ r.effectiveStatus
1242
+ ],
1243
+ [
1244
+ "Estado configurado",
1245
+ r.configuredStatus
1246
+ ]
1247
+ ];
1248
+ if (r.objective) statusLines.push([
1249
+ "Objetivo",
1250
+ r.objective
1251
+ ]);
1252
+ return renderCampaignStatusMarkdown({
1253
+ name: r.name,
1254
+ campaignId: r.campaignId,
1255
+ statusLines,
1256
+ metrics: r.insights,
1257
+ noMetricsHint: "A\xFAn no hay m\xE9tricas reportadas (la campa\xF1a puede estar pausada o sin entregar)."
1258
+ });
1259
+ }
1260
+ __name(renderMetaStatusMarkdown, "renderMetaStatusMarkdown");
1261
+ function renderTikTokStatusMarkdown(r) {
1262
+ const statusLines = [
1263
+ [
1264
+ "Estado de operaci\xF3n",
1265
+ r.operationStatus
1266
+ ]
1267
+ ];
1268
+ if (r.secondaryStatus) statusLines.push([
1269
+ "Estado secundario",
1270
+ r.secondaryStatus
1271
+ ]);
1272
+ if (r.objectiveType) statusLines.push([
1273
+ "Objetivo",
1274
+ r.objectiveType
1275
+ ]);
1276
+ return renderCampaignStatusMarkdown({
1277
+ name: r.name,
1278
+ campaignId: r.campaignId,
1279
+ statusLines,
1280
+ metrics: r.report
1281
+ });
1282
+ }
1283
+ __name(renderTikTokStatusMarkdown, "renderTikTokStatusMarkdown");
1060
1284
 
1061
1285
  // src/server.ts
1062
1286
  function createServer(opts) {
@@ -1082,6 +1306,7 @@ function createServer(opts) {
1082
1306
  });
1083
1307
  registerConnectionTools(server, api);
1084
1308
  registerPublishTools(server, api);
1309
+ registerCampaignTools(server, api);
1085
1310
  return server;
1086
1311
  }
1087
1312
  __name(createServer, "createServer");
package/package.json CHANGED
@@ -1,8 +1,42 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
5
  "private": false,
6
+ "description": "Product Maker MCP server — connect Claude Desktop, Claude Code, ChatGPT, or Gemini to Product Maker AI to generate dropshipping landing pages, UGC videos, and ad creatives, and publish to Shopify and Meta Ads from your AI assistant.",
7
+ "keywords": [
8
+ "mcp",
9
+ "mcp-server",
10
+ "model-context-protocol",
11
+ "claude",
12
+ "claude-desktop",
13
+ "claude-code",
14
+ "chatgpt",
15
+ "gemini",
16
+ "anthropic",
17
+ "openai",
18
+ "ai",
19
+ "productmaker",
20
+ "dropshipping",
21
+ "ecommerce",
22
+ "shopify",
23
+ "meta-ads",
24
+ "facebook-ads",
25
+ "tiktok-ads",
26
+ "ugc",
27
+ "video-generation",
28
+ "video-ads",
29
+ "landing-page",
30
+ "creative-generation",
31
+ "ad-creative",
32
+ "marketing"
33
+ ],
34
+ "homepage": "https://productmaker.app/install",
35
+ "bugs": {
36
+ "email": "support@productmaker.ai"
37
+ },
38
+ "author": "Product Maker AI <support@productmaker.ai> (https://productmaker.app)",
39
+ "license": "MIT",
6
40
  "bin": {
7
41
  "productmaker-mcp": "./dist/stdio.js"
8
42
  },