@productmaker/mcp 0.2.0 → 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 +47 -14
  2. package/dist/http.js +242 -26
  3. package/dist/stdio.js +242 -26
  4. package/package.json +47 -22
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);
@@ -553,6 +561,10 @@ function asText(value) {
553
561
  };
554
562
  }
555
563
  __name(asText, "asText");
564
+ function escapeMdLabel(s) {
565
+ return s.replace(/[\[\]\(\)\{\}`<>\r\n\\*_]/g, " ").replace(/\s+/g, " ").trim();
566
+ }
567
+ __name(escapeMdLabel, "escapeMdLabel");
556
568
  function flatten(obj) {
557
569
  const out = {};
558
570
  for (const [k, v] of Object.entries(obj)) {
@@ -576,8 +588,9 @@ async function submitCreative(api, opts) {
576
588
  const r = await api.postMultipart(opts.path, {
577
589
  images: parts
578
590
  }, flatten(rest));
591
+ const taskId = r.taskId ?? r.jobId;
579
592
  return asText({
580
- taskId: r.taskId,
593
+ taskId,
581
594
  status: "queued",
582
595
  estimatedSeconds: opts.estimatedSeconds
583
596
  });
@@ -716,18 +729,18 @@ __name(sanitizeFilename, "sanitizeFilename");
716
729
  async function fetchTaskOutputs(api, taskId) {
717
730
  const raw = await api.getJson(`/v1/results/${encodeURIComponent(taskId)}?signedUrls=true`);
718
731
  const videos = (raw.videoGenerators ?? []).map((vg) => {
719
- 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) => ({
720
733
  kind: v.kind,
721
734
  url: v.url
722
735
  }));
723
- 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) => ({
724
737
  kind: "mp4",
725
738
  url
726
739
  })) : [];
727
740
  return {
728
741
  angleKey: vg.angleKey ?? "",
729
742
  angleIndex: vg.angleIndex ?? 0,
730
- variants: variants.length ? variants : fallback
743
+ variants: variants2.length ? variants2 : fallback
731
744
  };
732
745
  }).filter((v) => v.angleKey && v.variants.length);
733
746
  const sections = (raw.landing?.images ?? []).filter((img) => Boolean(img.role) && Boolean(img.url)).map((img) => ({
@@ -735,18 +748,21 @@ async function fetchTaskOutputs(api, taskId) {
735
748
  url: img.url
736
749
  }));
737
750
  const images = [];
738
- 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) {
739
760
  images.push({
740
761
  url: raw.adCreative.url,
741
762
  width: raw.adCreative.width,
742
763
  height: raw.adCreative.height
743
764
  });
744
765
  }
745
- for (const variant of raw.adCreative?.variants ?? []) {
746
- if (typeof variant === "string") images.push({
747
- url: variant
748
- });
749
- }
750
766
  return {
751
767
  webappUrl: `${WEBAPP_BASE}/tasks/${taskId}`,
752
768
  videos,
@@ -757,21 +773,24 @@ async function fetchTaskOutputs(api, taskId) {
757
773
  };
758
774
  }
759
775
  __name(fetchTaskOutputs, "fetchTaskOutputs");
760
- function escapeMdLabel(s) {
761
- return s.replace(/[\[\]\(\)\{\}`<>\r\n\\]/g, " ").replace(/\s+/g, " ").trim();
762
- }
763
- __name(escapeMdLabel, "escapeMdLabel");
764
776
  function renderOutputsMarkdown(outputs) {
765
777
  const parts = [];
766
778
  parts.push(`\u2705 **Tu tarea est\xE1 lista** \u2014 [Abrir en Product Maker](${outputs.webappUrl})`);
767
779
  if (outputs.images.length) {
768
- parts.push("\n### Ad creative");
769
- 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
+ });
770
786
  }
771
787
  if (outputs.landing.sections.length) {
772
788
  parts.push(`
773
789
  ### Landing (${outputs.landing.sections.length} secciones)`);
774
- 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
+ }
775
794
  }
776
795
  if (outputs.videos.length) {
777
796
  parts.push("\n### Video");
@@ -1049,6 +1068,14 @@ function registerConnectionTools(server, api) {
1049
1068
  throw translateError(e);
1050
1069
  }
1051
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
+ });
1052
1079
  }
1053
1080
  __name(registerConnectionTools, "registerConnectionTools");
1054
1081
 
@@ -1059,14 +1086,38 @@ var ShopifyInput = z5.object({
1059
1086
  shopDomain: z5.string().min(1),
1060
1087
  numLandingImages: z5.number().int().min(1).max(7).optional(),
1061
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. */
1062
1092
  showVariantPicker: z5.boolean().optional()
1063
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
+ });
1064
1101
  var MetaInput = z5.object({
1065
1102
  taskId: z5.string().min(1),
1066
- 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()
1067
1118
  });
1068
1119
  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) => {
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) => {
1070
1121
  try {
1071
1122
  const numImages = input.numLandingImages ?? 3;
1072
1123
  const variantIndex = input.variantIndex ?? 0;
@@ -1081,16 +1132,37 @@ function registerPublishTools(server, api) {
1081
1132
  selectedLandingImages,
1082
1133
  showVariantPicker: input.showVariantPicker
1083
1134
  });
1084
- return asText(r);
1135
+ return renderShopifyPublishMarkdown(r);
1085
1136
  } catch (e) {
1086
1137
  throw translateError(e);
1087
1138
  }
1088
1139
  });
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) => {
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 }) => {
1090
1141
  try {
1091
- const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(input.taskId)}`, {
1092
- 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"
1093
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);
1094
1166
  return asText({
1095
1167
  ...r,
1096
1168
  status: "PAUSED"
@@ -1101,6 +1173,149 @@ function registerPublishTools(server, api) {
1101
1173
  });
1102
1174
  }
1103
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");
1104
1319
 
1105
1320
  // src/server.ts
1106
1321
  function createServer(opts) {
@@ -1126,6 +1341,7 @@ function createServer(opts) {
1126
1341
  });
1127
1342
  registerConnectionTools(server, api);
1128
1343
  registerPublishTools(server, api);
1344
+ registerCampaignTools(server, api);
1129
1345
  return server;
1130
1346
  }
1131
1347
  __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);
@@ -550,6 +558,10 @@ function asText(value) {
550
558
  };
551
559
  }
552
560
  __name(asText, "asText");
561
+ function escapeMdLabel(s) {
562
+ return s.replace(/[\[\]\(\)\{\}`<>\r\n\\*_]/g, " ").replace(/\s+/g, " ").trim();
563
+ }
564
+ __name(escapeMdLabel, "escapeMdLabel");
553
565
  function flatten(obj) {
554
566
  const out = {};
555
567
  for (const [k, v] of Object.entries(obj)) {
@@ -573,8 +585,9 @@ async function submitCreative(api, opts) {
573
585
  const r = await api.postMultipart(opts.path, {
574
586
  images: parts
575
587
  }, flatten(rest));
588
+ const taskId = r.taskId ?? r.jobId;
576
589
  return asText({
577
- taskId: r.taskId,
590
+ taskId,
578
591
  status: "queued",
579
592
  estimatedSeconds: opts.estimatedSeconds
580
593
  });
@@ -672,18 +685,18 @@ __name(sanitizeFilename, "sanitizeFilename");
672
685
  async function fetchTaskOutputs(api, taskId) {
673
686
  const raw = await api.getJson(`/v1/results/${encodeURIComponent(taskId)}?signedUrls=true`);
674
687
  const videos = (raw.videoGenerators ?? []).map((vg) => {
675
- const variants = (vg.variants ?? []).filter((v) => Boolean(v.kind) && Boolean(v.url)).map((v) => ({
688
+ const variants2 = (vg.variants ?? []).filter((v) => Boolean(v.kind) && Boolean(v.url)).map((v) => ({
676
689
  kind: v.kind,
677
690
  url: v.url
678
691
  }));
679
- const fallback = !variants.length && Array.isArray(vg.resultUrls) && vg.resultUrls.length ? vg.resultUrls.map((url) => ({
692
+ const fallback = !variants2.length && Array.isArray(vg.resultUrls) && vg.resultUrls.length ? vg.resultUrls.map((url) => ({
680
693
  kind: "mp4",
681
694
  url
682
695
  })) : [];
683
696
  return {
684
697
  angleKey: vg.angleKey ?? "",
685
698
  angleIndex: vg.angleIndex ?? 0,
686
- variants: variants.length ? variants : fallback
699
+ variants: variants2.length ? variants2 : fallback
687
700
  };
688
701
  }).filter((v) => v.angleKey && v.variants.length);
689
702
  const sections = (raw.landing?.images ?? []).filter((img) => Boolean(img.role) && Boolean(img.url)).map((img) => ({
@@ -691,18 +704,21 @@ async function fetchTaskOutputs(api, taskId) {
691
704
  url: img.url
692
705
  }));
693
706
  const images = [];
694
- if (raw.adCreative?.url) {
707
+ const variants = raw.adCreative?.variants ?? [];
708
+ const hasVariants = variants.some((v) => typeof v === "string");
709
+ if (hasVariants) {
710
+ for (const variant of variants) {
711
+ if (typeof variant === "string") images.push({
712
+ url: variant
713
+ });
714
+ }
715
+ } else if (raw.adCreative?.url) {
695
716
  images.push({
696
717
  url: raw.adCreative.url,
697
718
  width: raw.adCreative.width,
698
719
  height: raw.adCreative.height
699
720
  });
700
721
  }
701
- for (const variant of raw.adCreative?.variants ?? []) {
702
- if (typeof variant === "string") images.push({
703
- url: variant
704
- });
705
- }
706
722
  return {
707
723
  webappUrl: `${WEBAPP_BASE}/tasks/${taskId}`,
708
724
  videos,
@@ -713,21 +729,24 @@ async function fetchTaskOutputs(api, taskId) {
713
729
  };
714
730
  }
715
731
  __name(fetchTaskOutputs, "fetchTaskOutputs");
716
- function escapeMdLabel(s) {
717
- return s.replace(/[\[\]\(\)\{\}`<>\r\n\\]/g, " ").replace(/\s+/g, " ").trim();
718
- }
719
- __name(escapeMdLabel, "escapeMdLabel");
720
732
  function renderOutputsMarkdown(outputs) {
721
733
  const parts = [];
722
734
  parts.push(`\u2705 **Tu tarea est\xE1 lista** \u2014 [Abrir en Product Maker](${outputs.webappUrl})`);
723
735
  if (outputs.images.length) {
724
- parts.push("\n### Ad creative");
725
- for (const img of outputs.images) parts.push(`![](${img.url})`);
736
+ parts.push(`
737
+ ### Ad creative (${outputs.images.length} ${outputs.images.length === 1 ? "variante" : "variantes"})`);
738
+ outputs.images.forEach((img, i) => {
739
+ parts.push(`![v${i}](${img.url})`);
740
+ parts.push(`[\u2B07 Descargar v${i}](${img.url})`);
741
+ });
726
742
  }
727
743
  if (outputs.landing.sections.length) {
728
744
  parts.push(`
729
745
  ### Landing (${outputs.landing.sections.length} secciones)`);
730
- for (const s of outputs.landing.sections) parts.push(`![${escapeMdLabel(s.role)}](${s.url})`);
746
+ for (const s of outputs.landing.sections) {
747
+ parts.push(`![${escapeMdLabel(s.role)}](${s.url})`);
748
+ parts.push(`[\u2B07 Descargar ${escapeMdLabel(s.role)}](${s.url})`);
749
+ }
731
750
  }
732
751
  if (outputs.videos.length) {
733
752
  parts.push("\n### Video");
@@ -1005,6 +1024,14 @@ function registerConnectionTools(server, api) {
1005
1024
  throw translateError(e);
1006
1025
  }
1007
1026
  });
1027
+ server.tool("list_tiktok_advertisers", "Lista las cuentas de TikTok Ads conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar TikTok en productmaker.app/integrations.", z4.object({}).shape, async () => {
1028
+ try {
1029
+ const r = await api.getJson("/v1/tiktok/advertisers");
1030
+ return asText(r);
1031
+ } catch (e) {
1032
+ throw translateError(e);
1033
+ }
1034
+ });
1008
1035
  }
1009
1036
  __name(registerConnectionTools, "registerConnectionTools");
1010
1037
 
@@ -1015,14 +1042,38 @@ var ShopifyInput = z5.object({
1015
1042
  shopDomain: z5.string().min(1),
1016
1043
  numLandingImages: z5.number().int().min(1).max(7).optional(),
1017
1044
  variantIndex: z5.number().int().min(0).optional(),
1045
+ /** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
1046
+ * When true, the landing renders variant-picker pills; when false, it ships
1047
+ * a single-option product page. Defaults to false. */
1018
1048
  showVariantPicker: z5.boolean().optional()
1019
1049
  });
1050
+ var McpCampaignConfig = z5.object({
1051
+ dailyBudgetUsd: z5.number().positive().optional(),
1052
+ /** ISO-3166-1 alpha-2 country codes. Meta accepts max 1 (single-country
1053
+ * ad set). TikTok takes only the first entry. */
1054
+ targetCountries: z5.array(z5.string().length(2)).max(1).optional(),
1055
+ campaignName: z5.string().trim().min(1).max(100).optional()
1056
+ });
1020
1057
  var MetaInput = z5.object({
1021
1058
  taskId: z5.string().min(1),
1022
- adAccountId: z5.string().min(1)
1059
+ adAccountId: z5.string().min(1),
1060
+ pixelId: z5.string().min(1).optional(),
1061
+ landingUrl: z5.string().url().optional(),
1062
+ /** Select which angles to publish by 0-based index. Omit to publish all
1063
+ * successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
1064
+ angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
1065
+ campaignConfig: McpCampaignConfig.optional()
1066
+ });
1067
+ var TikTokInput = z5.object({
1068
+ taskId: z5.string().min(1),
1069
+ advertiserId: z5.string().min(1),
1070
+ pixelId: z5.string().min(1).optional(),
1071
+ landingUrl: z5.string().url().optional(),
1072
+ angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
1073
+ campaignConfig: McpCampaignConfig.optional()
1023
1074
  });
1024
1075
  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) => {
1076
+ server.tool("publish_to_shopify", "Publica la landing y producto generados en una tienda Shopify conectada. Precondici\xF3n: la tarea debe estar en status='complete' \u2014 usa `get_task_status` antes. ANTES de llamar preg\xFAntale al usuario \"\xBFTu producto tiene variantes (talla, color, etc.)?\" y pasa `showVariantPicker: true` si responde que s\xED \u2014 eso renderiza los pills de variantes en la landing. Por defecto incluye 3 im\xE1genes del hero. La respuesta es markdown con el link a la landing y al producto.", ShopifyInput.shape, async (input) => {
1026
1077
  try {
1027
1078
  const numImages = input.numLandingImages ?? 3;
1028
1079
  const variantIndex = input.variantIndex ?? 0;
@@ -1037,16 +1088,37 @@ function registerPublishTools(server, api) {
1037
1088
  selectedLandingImages,
1038
1089
  showVariantPicker: input.showVariantPicker
1039
1090
  });
1040
- return asText(r);
1091
+ return renderShopifyPublishMarkdown(r);
1041
1092
  } catch (e) {
1042
1093
  throw translateError(e);
1043
1094
  }
1044
1095
  });
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) => {
1096
+ server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en Meta Ads Manager. Use `list_meta_ad_accounts` antes para un adAccountId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 si la tarea gener\xF3 3 \xE1ngulos y el usuario solo quiere los #0 y #2, pasa [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, m\xE1x 1 para Meta), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_meta_campaign_status` para chequear el estado.", MetaInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
1046
1097
  try {
1047
- const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(input.taskId)}`, {
1048
- adAccountId: input.adAccountId
1098
+ const body = {
1099
+ ...rest
1100
+ };
1101
+ if (campaignConfig) {
1102
+ body.campaignOverrides = toMetaCampaignOverrides(campaignConfig);
1103
+ }
1104
+ const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1105
+ return asText({
1106
+ ...r,
1107
+ status: "PAUSED"
1049
1108
  });
1109
+ } catch (e) {
1110
+ throw translateError(e);
1111
+ }
1112
+ });
1113
+ server.tool("publish_to_tiktok", "Crea una campa\xF1a de TikTok Ads desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en TikTok Ads Manager. Use `list_tiktok_advertisers` antes para un advertiserId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 ej. [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, primero se usa), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_tiktok_campaign_status` para chequear el estado.", TikTokInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
1114
+ try {
1115
+ const body = {
1116
+ ...rest
1117
+ };
1118
+ if (campaignConfig) {
1119
+ body.campaignConfig = toTikTokCampaignConfig(campaignConfig);
1120
+ }
1121
+ const r = await api.postJson(`/v1/tiktok/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
1050
1122
  return asText({
1051
1123
  ...r,
1052
1124
  status: "PAUSED"
@@ -1057,6 +1129,149 @@ function registerPublishTools(server, api) {
1057
1129
  });
1058
1130
  }
1059
1131
  __name(registerPublishTools, "registerPublishTools");
1132
+ function renderShopifyPublishMarkdown(r) {
1133
+ const lines = [];
1134
+ const statusEmoji = r.status === "success" ? "\u2705" : r.status === "error" ? "\u274C" : "\u23F3";
1135
+ lines.push(`${statusEmoji} **Publicaci\xF3n en Shopify** \u2014 ${r.shopDomain}`);
1136
+ lines.push(`Estado: \`${r.status}\``);
1137
+ if (r.pageUrl) lines.push(`
1138
+ ### Landing
1139
+ [Ver landing publicada](${r.pageUrl})`);
1140
+ if (r.productUrl) lines.push(`
1141
+ ### Producto
1142
+ [Ver producto](${r.productUrl})`);
1143
+ if (!r.pageUrl && !r.productUrl && r.status !== "success") {
1144
+ lines.push("\n_La publicaci\xF3n a\xFAn est\xE1 proces\xE1ndose. Usa `get_task_status` para verificar progreso._");
1145
+ }
1146
+ return {
1147
+ content: [
1148
+ {
1149
+ type: "text",
1150
+ text: lines.join("\n")
1151
+ }
1152
+ ]
1153
+ };
1154
+ }
1155
+ __name(renderShopifyPublishMarkdown, "renderShopifyPublishMarkdown");
1156
+ function toMetaCampaignOverrides(cfg) {
1157
+ const out = {};
1158
+ if (cfg.dailyBudgetUsd !== void 0) out.dailyBudgetUsd = cfg.dailyBudgetUsd;
1159
+ if (cfg.targetCountries !== void 0) out.targetCountries = cfg.targetCountries;
1160
+ if (cfg.campaignName !== void 0) out.campaignName = cfg.campaignName;
1161
+ return out;
1162
+ }
1163
+ __name(toMetaCampaignOverrides, "toMetaCampaignOverrides");
1164
+ function toTikTokCampaignConfig(cfg) {
1165
+ const out = {};
1166
+ if (cfg.dailyBudgetUsd !== void 0) out.dailyBudget = Math.round(cfg.dailyBudgetUsd * 100);
1167
+ if (cfg.targetCountries && cfg.targetCountries.length > 0) out.targetCountry = cfg.targetCountries[0];
1168
+ if (cfg.campaignName !== void 0) out.campaignName = cfg.campaignName;
1169
+ return out;
1170
+ }
1171
+ __name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
1172
+
1173
+ // src/tools/campaigns.ts
1174
+ import { z as z6 } from "zod";
1175
+ var MetaStatusInput = z6.object({
1176
+ campaignId: z6.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
1177
+ });
1178
+ var TikTokStatusInput = z6.object({
1179
+ campaignId: z6.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
1180
+ advertiserId: z6.string().min(1).describe("The same advertiserId used to publish the campaign.")
1181
+ });
1182
+ function registerCampaignTools(server, api) {
1183
+ server.tool("get_meta_campaign_status", "Consulta el estado actual + m\xE9tricas lifetime (spend, impressions, clicks, reach) de una campa\xF1a Meta publicada. Llama esto DESPU\xC9S de `publish_to_meta` para confirmar que la campa\xF1a se cre\xF3 correctamente y para reportar performance al usuario. Es read-only \u2014 no modifica nada en Meta Ads Manager.", MetaStatusInput.shape, async (input) => {
1184
+ try {
1185
+ const r = await api.getJson(`/v1/facebook/campaigns/${encodeURIComponent(input.campaignId)}`);
1186
+ return renderMetaStatusMarkdown(r);
1187
+ } catch (e) {
1188
+ throw translateError(e);
1189
+ }
1190
+ });
1191
+ server.tool("get_tiktok_campaign_status", "Consulta el estado actual + reporte lifetime (spend, impressions, clicks, reach) de una campa\xF1a TikTok publicada. Requiere `advertiserId` (el mismo con el que se cre\xF3). Llama esto DESPU\xC9S de `publish_to_tiktok` para confirmar creaci\xF3n correcta y reportar performance. Es read-only.", TikTokStatusInput.shape, async (input) => {
1192
+ try {
1193
+ const r = await api.getJson(`/v1/tiktok/campaigns/${encodeURIComponent(input.campaignId)}?advertiserId=${encodeURIComponent(input.advertiserId)}`);
1194
+ return renderTikTokStatusMarkdown(r);
1195
+ } catch (e) {
1196
+ throw translateError(e);
1197
+ }
1198
+ });
1199
+ }
1200
+ __name(registerCampaignTools, "registerCampaignTools");
1201
+ function renderCampaignStatusMarkdown(opts) {
1202
+ const lines = [
1203
+ `\u{1F4CA} **${escapeMdLabel(opts.name)}** (\`${opts.campaignId}\`)`
1204
+ ];
1205
+ for (const [label, value] of opts.statusLines) {
1206
+ lines.push(`- ${label}: \`${value}\``);
1207
+ }
1208
+ if (opts.metrics) {
1209
+ lines.push("\n### M\xE9tricas (lifetime)");
1210
+ lines.push(`- Spend: ${opts.metrics.spend}`);
1211
+ lines.push(`- Impressions: ${opts.metrics.impressions}`);
1212
+ lines.push(`- Clicks: ${opts.metrics.clicks}`);
1213
+ lines.push(`- Reach: ${opts.metrics.reach}`);
1214
+ } else {
1215
+ lines.push(`
1216
+ _${opts.noMetricsHint ?? "A\xFAn no hay m\xE9tricas reportadas."}_`);
1217
+ }
1218
+ return {
1219
+ content: [
1220
+ {
1221
+ type: "text",
1222
+ text: lines.join("\n")
1223
+ }
1224
+ ]
1225
+ };
1226
+ }
1227
+ __name(renderCampaignStatusMarkdown, "renderCampaignStatusMarkdown");
1228
+ function renderMetaStatusMarkdown(r) {
1229
+ const statusLines = [
1230
+ [
1231
+ "Estado efectivo",
1232
+ r.effectiveStatus
1233
+ ],
1234
+ [
1235
+ "Estado configurado",
1236
+ r.configuredStatus
1237
+ ]
1238
+ ];
1239
+ if (r.objective) statusLines.push([
1240
+ "Objetivo",
1241
+ r.objective
1242
+ ]);
1243
+ return renderCampaignStatusMarkdown({
1244
+ name: r.name,
1245
+ campaignId: r.campaignId,
1246
+ statusLines,
1247
+ metrics: r.insights,
1248
+ noMetricsHint: "A\xFAn no hay m\xE9tricas reportadas (la campa\xF1a puede estar pausada o sin entregar)."
1249
+ });
1250
+ }
1251
+ __name(renderMetaStatusMarkdown, "renderMetaStatusMarkdown");
1252
+ function renderTikTokStatusMarkdown(r) {
1253
+ const statusLines = [
1254
+ [
1255
+ "Estado de operaci\xF3n",
1256
+ r.operationStatus
1257
+ ]
1258
+ ];
1259
+ if (r.secondaryStatus) statusLines.push([
1260
+ "Estado secundario",
1261
+ r.secondaryStatus
1262
+ ]);
1263
+ if (r.objectiveType) statusLines.push([
1264
+ "Objetivo",
1265
+ r.objectiveType
1266
+ ]);
1267
+ return renderCampaignStatusMarkdown({
1268
+ name: r.name,
1269
+ campaignId: r.campaignId,
1270
+ statusLines,
1271
+ metrics: r.report
1272
+ });
1273
+ }
1274
+ __name(renderTikTokStatusMarkdown, "renderTikTokStatusMarkdown");
1060
1275
 
1061
1276
  // src/server.ts
1062
1277
  function createServer(opts) {
@@ -1082,6 +1297,7 @@ function createServer(opts) {
1082
1297
  });
1083
1298
  registerConnectionTools(server, api);
1084
1299
  registerPublishTools(server, api);
1300
+ registerCampaignTools(server, api);
1085
1301
  return server;
1086
1302
  }
1087
1303
  __name(createServer, "createServer");
package/package.json CHANGED
@@ -1,17 +1,51 @@
1
1
  {
2
2
  "name": "@productmaker/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "private": false,
6
- "bin": {
7
- "productmaker-mcp": "./dist/stdio.js"
8
- },
9
- "files": [
10
- "dist",
11
- "README.md"
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"
12
33
  ],
13
- "engines": {
14
- "node": ">=20"
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",
40
+ "bin": { "productmaker-mcp": "./dist/stdio.js" },
41
+ "files": ["dist", "README.md"],
42
+ "engines": { "node": ">=20" },
43
+ "scripts": {
44
+ "build": "tsup",
45
+ "dev:stdio": "tsx src/stdio.ts",
46
+ "dev:http": "tsx src/http.ts",
47
+ "test": "vitest run",
48
+ "typecheck": "tsc --noEmit"
15
49
  },
16
50
  "dependencies": {
17
51
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -21,23 +55,14 @@
21
55
  "zod": "^3.23.8"
22
56
  },
23
57
  "devDependencies": {
58
+ "@packages/types": "workspace:*",
24
59
  "@types/express": "^4.17.21",
25
60
  "@types/node": "^20.11.0",
26
61
  "msw": "^2.4.0",
27
62
  "tsup": "^8.3.0",
28
63
  "tsx": "^4.16.0",
29
64
  "typescript": "^5.4.5",
30
- "vitest": "^2.1.9",
31
- "@packages/types": "0.1.0"
65
+ "vitest": "^2.1.9"
32
66
  },
33
- "publishConfig": {
34
- "access": "public"
35
- },
36
- "scripts": {
37
- "build": "tsup",
38
- "dev:stdio": "tsx src/stdio.ts",
39
- "dev:http": "tsx src/http.ts",
40
- "test": "vitest run",
41
- "typecheck": "tsc --noEmit"
42
- }
43
- }
67
+ "publishConfig": { "access": "public" }
68
+ }