@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.
- package/README.md +47 -14
- package/dist/http.js +242 -26
- package/dist/stdio.js +242 -26
- package/package.json +47 -22
package/README.md
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Product Maker MCP — `@productmaker/mcp`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@productmaker/mcp)
|
|
4
|
+
[](https://www.npmjs.com/package/@productmaker/mcp)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
6
|
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
39
|
+
### ChatGPT, Gemini, claude.ai web (HTTP / SSE)
|
|
24
40
|
|
|
25
|
-
|
|
26
|
-
Header: `Authorization: Bearer pm_live_...`
|
|
41
|
+
No install needed — connect to the hosted endpoint:
|
|
27
42
|
|
|
28
|
-
|
|
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
|
-
| `
|
|
42
|
-
| `
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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 = !
|
|
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:
|
|
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
|
-
|
|
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(
|
|
769
|
-
|
|
780
|
+
parts.push(`
|
|
781
|
+
### Ad creative (${outputs.images.length} ${outputs.images.length === 1 ? "variante" : "variantes"})`);
|
|
782
|
+
outputs.images.forEach((img, i) => {
|
|
783
|
+
parts.push(``);
|
|
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)
|
|
790
|
+
for (const s of outputs.landing.sections) {
|
|
791
|
+
parts.push(``);
|
|
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'
|
|
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
|
|
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
|
|
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
|
|
1092
|
-
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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 = !
|
|
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:
|
|
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
|
-
|
|
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(
|
|
725
|
-
|
|
736
|
+
parts.push(`
|
|
737
|
+
### Ad creative (${outputs.images.length} ${outputs.images.length === 1 ? "variante" : "variantes"})`);
|
|
738
|
+
outputs.images.forEach((img, i) => {
|
|
739
|
+
parts.push(``);
|
|
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)
|
|
746
|
+
for (const s of outputs.landing.sections) {
|
|
747
|
+
parts.push(``);
|
|
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'
|
|
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
|
|
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
|
|
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
|
|
1048
|
-
|
|
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.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
|
-
"
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"
|
|
11
|
-
"
|
|
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
|
-
"
|
|
14
|
-
|
|
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
|
-
|
|
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
|
+
}
|