@productmaker/mcp 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -14
- package/dist/http.js +254 -29
- package/dist/stdio.js +254 -29
- package/package.json +35 -1
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);
|
|
@@ -499,7 +507,14 @@ var NarrativeStyleEnum = z.enum([
|
|
|
499
507
|
"problem_product_cta",
|
|
500
508
|
"product_showcase",
|
|
501
509
|
"metaphor_product_cta",
|
|
502
|
-
"wearable_showcase"
|
|
510
|
+
"wearable_showcase",
|
|
511
|
+
"animated_problem_pixar",
|
|
512
|
+
"sassy_product_pixar",
|
|
513
|
+
"ugc_unboxing_chaos",
|
|
514
|
+
"ugc_curiosity_reveal",
|
|
515
|
+
"ugc_listicle_3",
|
|
516
|
+
"ugc_skeptic_confession",
|
|
517
|
+
"ugc_demo_review"
|
|
503
518
|
]);
|
|
504
519
|
var VideoModelEnum = z.enum([
|
|
505
520
|
"veo",
|
|
@@ -553,6 +568,10 @@ function asText(value) {
|
|
|
553
568
|
};
|
|
554
569
|
}
|
|
555
570
|
__name(asText, "asText");
|
|
571
|
+
function escapeMdLabel(s) {
|
|
572
|
+
return s.replace(/[\[\]\(\)\{\}`<>\r\n\\*_]/g, " ").replace(/\s+/g, " ").trim();
|
|
573
|
+
}
|
|
574
|
+
__name(escapeMdLabel, "escapeMdLabel");
|
|
556
575
|
function flatten(obj) {
|
|
557
576
|
const out = {};
|
|
558
577
|
for (const [k, v] of Object.entries(obj)) {
|
|
@@ -576,8 +595,9 @@ async function submitCreative(api, opts) {
|
|
|
576
595
|
const r = await api.postMultipart(opts.path, {
|
|
577
596
|
images: parts
|
|
578
597
|
}, flatten(rest));
|
|
598
|
+
const taskId = r.taskId ?? r.jobId;
|
|
579
599
|
return asText({
|
|
580
|
-
taskId
|
|
600
|
+
taskId,
|
|
581
601
|
status: "queued",
|
|
582
602
|
estimatedSeconds: opts.estimatedSeconds
|
|
583
603
|
});
|
|
@@ -716,18 +736,18 @@ __name(sanitizeFilename, "sanitizeFilename");
|
|
|
716
736
|
async function fetchTaskOutputs(api, taskId) {
|
|
717
737
|
const raw = await api.getJson(`/v1/results/${encodeURIComponent(taskId)}?signedUrls=true`);
|
|
718
738
|
const videos = (raw.videoGenerators ?? []).map((vg) => {
|
|
719
|
-
const
|
|
739
|
+
const variants2 = (vg.variants ?? []).filter((v) => Boolean(v.kind) && Boolean(v.url)).map((v) => ({
|
|
720
740
|
kind: v.kind,
|
|
721
741
|
url: v.url
|
|
722
742
|
}));
|
|
723
|
-
const fallback = !
|
|
743
|
+
const fallback = !variants2.length && Array.isArray(vg.resultUrls) && vg.resultUrls.length ? vg.resultUrls.map((url) => ({
|
|
724
744
|
kind: "mp4",
|
|
725
745
|
url
|
|
726
746
|
})) : [];
|
|
727
747
|
return {
|
|
728
748
|
angleKey: vg.angleKey ?? "",
|
|
729
749
|
angleIndex: vg.angleIndex ?? 0,
|
|
730
|
-
variants:
|
|
750
|
+
variants: variants2.length ? variants2 : fallback
|
|
731
751
|
};
|
|
732
752
|
}).filter((v) => v.angleKey && v.variants.length);
|
|
733
753
|
const sections = (raw.landing?.images ?? []).filter((img) => Boolean(img.role) && Boolean(img.url)).map((img) => ({
|
|
@@ -735,18 +755,21 @@ async function fetchTaskOutputs(api, taskId) {
|
|
|
735
755
|
url: img.url
|
|
736
756
|
}));
|
|
737
757
|
const images = [];
|
|
738
|
-
|
|
758
|
+
const variants = raw.adCreative?.variants ?? [];
|
|
759
|
+
const hasVariants = variants.some((v) => typeof v === "string");
|
|
760
|
+
if (hasVariants) {
|
|
761
|
+
for (const variant of variants) {
|
|
762
|
+
if (typeof variant === "string") images.push({
|
|
763
|
+
url: variant
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
} else if (raw.adCreative?.url) {
|
|
739
767
|
images.push({
|
|
740
768
|
url: raw.adCreative.url,
|
|
741
769
|
width: raw.adCreative.width,
|
|
742
770
|
height: raw.adCreative.height
|
|
743
771
|
});
|
|
744
772
|
}
|
|
745
|
-
for (const variant of raw.adCreative?.variants ?? []) {
|
|
746
|
-
if (typeof variant === "string") images.push({
|
|
747
|
-
url: variant
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
773
|
return {
|
|
751
774
|
webappUrl: `${WEBAPP_BASE}/tasks/${taskId}`,
|
|
752
775
|
videos,
|
|
@@ -757,21 +780,24 @@ async function fetchTaskOutputs(api, taskId) {
|
|
|
757
780
|
};
|
|
758
781
|
}
|
|
759
782
|
__name(fetchTaskOutputs, "fetchTaskOutputs");
|
|
760
|
-
function escapeMdLabel(s) {
|
|
761
|
-
return s.replace(/[\[\]\(\)\{\}`<>\r\n\\]/g, " ").replace(/\s+/g, " ").trim();
|
|
762
|
-
}
|
|
763
|
-
__name(escapeMdLabel, "escapeMdLabel");
|
|
764
783
|
function renderOutputsMarkdown(outputs) {
|
|
765
784
|
const parts = [];
|
|
766
785
|
parts.push(`\u2705 **Tu tarea est\xE1 lista** \u2014 [Abrir en Product Maker](${outputs.webappUrl})`);
|
|
767
786
|
if (outputs.images.length) {
|
|
768
|
-
parts.push(
|
|
769
|
-
|
|
787
|
+
parts.push(`
|
|
788
|
+
### Ad creative (${outputs.images.length} ${outputs.images.length === 1 ? "variante" : "variantes"})`);
|
|
789
|
+
outputs.images.forEach((img, i) => {
|
|
790
|
+
parts.push(``);
|
|
791
|
+
parts.push(`[\u2B07 Descargar v${i}](${img.url})`);
|
|
792
|
+
});
|
|
770
793
|
}
|
|
771
794
|
if (outputs.landing.sections.length) {
|
|
772
795
|
parts.push(`
|
|
773
796
|
### Landing (${outputs.landing.sections.length} secciones)`);
|
|
774
|
-
for (const s of outputs.landing.sections)
|
|
797
|
+
for (const s of outputs.landing.sections) {
|
|
798
|
+
parts.push(``);
|
|
799
|
+
parts.push(`[\u2B07 Descargar ${escapeMdLabel(s.role)}](${s.url})`);
|
|
800
|
+
}
|
|
775
801
|
}
|
|
776
802
|
if (outputs.videos.length) {
|
|
777
803
|
parts.push("\n### Video");
|
|
@@ -794,9 +820,10 @@ var CreateProductTaskInput = z2.object({
|
|
|
794
820
|
price: z2.string().optional().describe('Precio que se mostrar\xE1 en la landing y video. Formato libre \u2014 ej. "$89.900", "USD 29.99", "MXN 599". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar. Si te dice "t\xFA elige", om\xEDtelo y el backend sugiere uno.'),
|
|
795
821
|
offer: z2.string().optional().describe('Oferta o promoci\xF3n a destacar. Ej. "2x1", "Env\xEDo gratis hoy", "50% OFF". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar (es v\xE1lido que diga "sin oferta"). Si te dice "t\xFA elige", om\xEDtelo.'),
|
|
796
822
|
videoStyle: VideoStyleEnum.optional().describe('Opcional. Estilo visual del video. "ugc" = creador hablando a c\xE1mara (default, mejor CTR LATAM). "cartoon_3d" = animaci\xF3n 3D. "product_only" = sin actor. "ai_dynamic" = cinematogr\xE1fico AI. Si se omite, default "ugc".'),
|
|
797
|
-
narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" =
|
|
823
|
+
narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" = UGC cl\xE1sico multi-hook (default, alta conversi\xF3n). "problem_product_cta" = problema\u2192soluci\xF3n. "product_showcase" = features. "metaphor_product_cta" = met\xE1fora visual. "wearable_showcase" = para ropa/accesorios. "ugc_unboxing_chaos" = unboxing agresivo. "ugc_curiosity_reveal" = hook con producto oculto. "ugc_listicle_3" = 3 razones (30s recomendado). "ugc_skeptic_confession" = esc\xE9ptico\u2192conversi\xF3n (30s recomendado). "ugc_demo_review" = demo en uso a lo largo de d\xEDas. Si se omite, default "review".'),
|
|
798
824
|
videoModel: VideoModelEnum.optional().describe('Opcional. Modelo de video. "veo" (default, calidad Google). "bytedance"/"seedance" = TikTok-style. "kling"/"wan" = alternativos. "infinitalk" = lipsync largo. "grok" = grok-imagine. Si se omite, el sistema escoge seg\xFAn costo/calidad.'),
|
|
799
825
|
multiAngleVideos: z2.boolean().optional().describe("Opcional. Si true, genera m\xFAltiples \xE1ngulos de venta en paralelo (3-5 videos). Si false/omitido, genera 1 video del \xE1ngulo principal. Default false."),
|
|
826
|
+
angleNarratives: NarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). angleNarratives[i] fuerza la narrativa del \xE1ngulo en la posici\xF3n i; usa los mismos valores que narrativeStyle. Si la lista es m\xE1s corta que la cantidad de \xE1ngulos, las posiciones restantes las elige la IA. Ej: ["animated_problem_pixar","review","ugc_skeptic_confession"] para 3 \xE1ngulos distintos. Requiere multiAngleVideos:true (o videoAngleCount>1) para que haya varios \xE1ngulos. Si se omite, todas las narrativas se auto-seleccionan.'),
|
|
800
827
|
outputs: z2.array(z2.enum([
|
|
801
828
|
"landing",
|
|
802
829
|
"video",
|
|
@@ -843,6 +870,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
843
870
|
"options.offer": input.offer,
|
|
844
871
|
"options.videoStyle": input.videoStyle,
|
|
845
872
|
"options.narrativeStyle": input.narrativeStyle,
|
|
873
|
+
"options.angleNarratives": input.angleNarratives,
|
|
846
874
|
"options.videoModel": input.videoModel,
|
|
847
875
|
"options.multiAngleVideos": input.multiAngleVideos,
|
|
848
876
|
"options.selectedOutputs": input.outputs,
|
|
@@ -943,7 +971,7 @@ var VideoInput = z3.object({
|
|
|
943
971
|
"Auto"
|
|
944
972
|
]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
|
|
945
973
|
durationSeconds: z3.number().int().min(5).max(60).optional().describe("Opcional. Duraci\xF3n del video en segundos (5-60). Default 15s. Para hooks cortos usa 8, para storytelling usa 30."),
|
|
946
|
-
narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" (
|
|
974
|
+
narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" (UGC cl\xE1sico multi-hook, default), "problem_product_cta", "product_showcase", "metaphor_product_cta", "wearable_showcase", "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones numeradas, recomendado 30s), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n, recomendado 30s), "ugc_demo_review" (demo en uso a lo largo de d\xEDas).'),
|
|
947
975
|
videoStyle: VideoStyleEnum.optional().describe('Opcional. Estilo visual. "ugc" (creador hablando, default), "cartoon_3d", "product_only", "ai_dynamic".'),
|
|
948
976
|
videoModel: VideoModelEnum.optional().describe('Opcional. Modelo de video. "veo" (default, calidad Google). Ver lista de valores v\xE1lidos.'),
|
|
949
977
|
features: z3.array(z3.string()).optional().describe('Opcional. Lista de features/beneficios clave a mencionar. Ej. ["resistente al agua", "bater\xEDa 24h", "carga inal\xE1mbrica"].'),
|
|
@@ -1049,6 +1077,14 @@ function registerConnectionTools(server, api) {
|
|
|
1049
1077
|
throw translateError(e);
|
|
1050
1078
|
}
|
|
1051
1079
|
});
|
|
1080
|
+
server.tool("list_tiktok_advertisers", "Lista las cuentas de TikTok Ads conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar TikTok en productmaker.app/integrations.", z4.object({}).shape, async () => {
|
|
1081
|
+
try {
|
|
1082
|
+
const r = await api.getJson("/v1/tiktok/advertisers");
|
|
1083
|
+
return asText(r);
|
|
1084
|
+
} catch (e) {
|
|
1085
|
+
throw translateError(e);
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1052
1088
|
}
|
|
1053
1089
|
__name(registerConnectionTools, "registerConnectionTools");
|
|
1054
1090
|
|
|
@@ -1059,14 +1095,38 @@ var ShopifyInput = z5.object({
|
|
|
1059
1095
|
shopDomain: z5.string().min(1),
|
|
1060
1096
|
numLandingImages: z5.number().int().min(1).max(7).optional(),
|
|
1061
1097
|
variantIndex: z5.number().int().min(0).optional(),
|
|
1098
|
+
/** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
|
|
1099
|
+
* When true, the landing renders variant-picker pills; when false, it ships
|
|
1100
|
+
* a single-option product page. Defaults to false. */
|
|
1062
1101
|
showVariantPicker: z5.boolean().optional()
|
|
1063
1102
|
});
|
|
1103
|
+
var McpCampaignConfig = z5.object({
|
|
1104
|
+
dailyBudgetUsd: z5.number().positive().optional(),
|
|
1105
|
+
/** ISO-3166-1 alpha-2 country codes. Meta accepts max 1 (single-country
|
|
1106
|
+
* ad set). TikTok takes only the first entry. */
|
|
1107
|
+
targetCountries: z5.array(z5.string().length(2)).max(1).optional(),
|
|
1108
|
+
campaignName: z5.string().trim().min(1).max(100).optional()
|
|
1109
|
+
});
|
|
1064
1110
|
var MetaInput = z5.object({
|
|
1065
1111
|
taskId: z5.string().min(1),
|
|
1066
|
-
adAccountId: z5.string().min(1)
|
|
1112
|
+
adAccountId: z5.string().min(1),
|
|
1113
|
+
pixelId: z5.string().min(1).optional(),
|
|
1114
|
+
landingUrl: z5.string().url().optional(),
|
|
1115
|
+
/** Select which angles to publish by 0-based index. Omit to publish all
|
|
1116
|
+
* successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
|
|
1117
|
+
angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
|
|
1118
|
+
campaignConfig: McpCampaignConfig.optional()
|
|
1119
|
+
});
|
|
1120
|
+
var TikTokInput = z5.object({
|
|
1121
|
+
taskId: z5.string().min(1),
|
|
1122
|
+
advertiserId: z5.string().min(1),
|
|
1123
|
+
pixelId: z5.string().min(1).optional(),
|
|
1124
|
+
landingUrl: z5.string().url().optional(),
|
|
1125
|
+
angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
|
|
1126
|
+
campaignConfig: McpCampaignConfig.optional()
|
|
1067
1127
|
});
|
|
1068
1128
|
function registerPublishTools(server, api) {
|
|
1069
|
-
server.tool("publish_to_shopify", "Publica la landing y producto generados en una tienda Shopify conectada. Precondici\xF3n: la tarea debe estar en status='complete'
|
|
1129
|
+
server.tool("publish_to_shopify", "Publica la landing y producto generados en una tienda Shopify conectada. Precondici\xF3n: la tarea debe estar en status='complete' \u2014 usa `get_task_status` antes. ANTES de llamar preg\xFAntale al usuario \"\xBFTu producto tiene variantes (talla, color, etc.)?\" y pasa `showVariantPicker: true` si responde que s\xED \u2014 eso renderiza los pills de variantes en la landing. Por defecto incluye 3 im\xE1genes del hero. La respuesta es markdown con el link a la landing y al producto.", ShopifyInput.shape, async (input) => {
|
|
1070
1130
|
try {
|
|
1071
1131
|
const numImages = input.numLandingImages ?? 3;
|
|
1072
1132
|
const variantIndex = input.variantIndex ?? 0;
|
|
@@ -1081,16 +1141,37 @@ function registerPublishTools(server, api) {
|
|
|
1081
1141
|
selectedLandingImages,
|
|
1082
1142
|
showVariantPicker: input.showVariantPicker
|
|
1083
1143
|
});
|
|
1084
|
-
return
|
|
1144
|
+
return renderShopifyPublishMarkdown(r);
|
|
1085
1145
|
} catch (e) {
|
|
1086
1146
|
throw translateError(e);
|
|
1087
1147
|
}
|
|
1088
1148
|
});
|
|
1089
|
-
server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario
|
|
1149
|
+
server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en Meta Ads Manager. Use `list_meta_ad_accounts` antes para un adAccountId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 si la tarea gener\xF3 3 \xE1ngulos y el usuario solo quiere los #0 y #2, pasa [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, m\xE1x 1 para Meta), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_meta_campaign_status` para chequear el estado.", MetaInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
|
|
1090
1150
|
try {
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1151
|
+
const body = {
|
|
1152
|
+
...rest
|
|
1153
|
+
};
|
|
1154
|
+
if (campaignConfig) {
|
|
1155
|
+
body.campaignOverrides = toMetaCampaignOverrides(campaignConfig);
|
|
1156
|
+
}
|
|
1157
|
+
const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
|
|
1158
|
+
return asText({
|
|
1159
|
+
...r,
|
|
1160
|
+
status: "PAUSED"
|
|
1093
1161
|
});
|
|
1162
|
+
} catch (e) {
|
|
1163
|
+
throw translateError(e);
|
|
1164
|
+
}
|
|
1165
|
+
});
|
|
1166
|
+
server.tool("publish_to_tiktok", "Crea una campa\xF1a de TikTok Ads desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en TikTok Ads Manager. Use `list_tiktok_advertisers` antes para un advertiserId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 ej. [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, primero se usa), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_tiktok_campaign_status` para chequear el estado.", TikTokInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
|
|
1167
|
+
try {
|
|
1168
|
+
const body = {
|
|
1169
|
+
...rest
|
|
1170
|
+
};
|
|
1171
|
+
if (campaignConfig) {
|
|
1172
|
+
body.campaignConfig = toTikTokCampaignConfig(campaignConfig);
|
|
1173
|
+
}
|
|
1174
|
+
const r = await api.postJson(`/v1/tiktok/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
|
|
1094
1175
|
return asText({
|
|
1095
1176
|
...r,
|
|
1096
1177
|
status: "PAUSED"
|
|
@@ -1101,6 +1182,149 @@ function registerPublishTools(server, api) {
|
|
|
1101
1182
|
});
|
|
1102
1183
|
}
|
|
1103
1184
|
__name(registerPublishTools, "registerPublishTools");
|
|
1185
|
+
function renderShopifyPublishMarkdown(r) {
|
|
1186
|
+
const lines = [];
|
|
1187
|
+
const statusEmoji = r.status === "success" ? "\u2705" : r.status === "error" ? "\u274C" : "\u23F3";
|
|
1188
|
+
lines.push(`${statusEmoji} **Publicaci\xF3n en Shopify** \u2014 ${r.shopDomain}`);
|
|
1189
|
+
lines.push(`Estado: \`${r.status}\``);
|
|
1190
|
+
if (r.pageUrl) lines.push(`
|
|
1191
|
+
### Landing
|
|
1192
|
+
[Ver landing publicada](${r.pageUrl})`);
|
|
1193
|
+
if (r.productUrl) lines.push(`
|
|
1194
|
+
### Producto
|
|
1195
|
+
[Ver producto](${r.productUrl})`);
|
|
1196
|
+
if (!r.pageUrl && !r.productUrl && r.status !== "success") {
|
|
1197
|
+
lines.push("\n_La publicaci\xF3n a\xFAn est\xE1 proces\xE1ndose. Usa `get_task_status` para verificar progreso._");
|
|
1198
|
+
}
|
|
1199
|
+
return {
|
|
1200
|
+
content: [
|
|
1201
|
+
{
|
|
1202
|
+
type: "text",
|
|
1203
|
+
text: lines.join("\n")
|
|
1204
|
+
}
|
|
1205
|
+
]
|
|
1206
|
+
};
|
|
1207
|
+
}
|
|
1208
|
+
__name(renderShopifyPublishMarkdown, "renderShopifyPublishMarkdown");
|
|
1209
|
+
function toMetaCampaignOverrides(cfg) {
|
|
1210
|
+
const out = {};
|
|
1211
|
+
if (cfg.dailyBudgetUsd !== void 0) out.dailyBudgetUsd = cfg.dailyBudgetUsd;
|
|
1212
|
+
if (cfg.targetCountries !== void 0) out.targetCountries = cfg.targetCountries;
|
|
1213
|
+
if (cfg.campaignName !== void 0) out.campaignName = cfg.campaignName;
|
|
1214
|
+
return out;
|
|
1215
|
+
}
|
|
1216
|
+
__name(toMetaCampaignOverrides, "toMetaCampaignOverrides");
|
|
1217
|
+
function toTikTokCampaignConfig(cfg) {
|
|
1218
|
+
const out = {};
|
|
1219
|
+
if (cfg.dailyBudgetUsd !== void 0) out.dailyBudget = Math.round(cfg.dailyBudgetUsd * 100);
|
|
1220
|
+
if (cfg.targetCountries && cfg.targetCountries.length > 0) out.targetCountry = cfg.targetCountries[0];
|
|
1221
|
+
if (cfg.campaignName !== void 0) out.campaignName = cfg.campaignName;
|
|
1222
|
+
return out;
|
|
1223
|
+
}
|
|
1224
|
+
__name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
|
|
1225
|
+
|
|
1226
|
+
// src/tools/campaigns.ts
|
|
1227
|
+
import { z as z6 } from "zod";
|
|
1228
|
+
var MetaStatusInput = z6.object({
|
|
1229
|
+
campaignId: z6.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
|
|
1230
|
+
});
|
|
1231
|
+
var TikTokStatusInput = z6.object({
|
|
1232
|
+
campaignId: z6.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
|
|
1233
|
+
advertiserId: z6.string().min(1).describe("The same advertiserId used to publish the campaign.")
|
|
1234
|
+
});
|
|
1235
|
+
function registerCampaignTools(server, api) {
|
|
1236
|
+
server.tool("get_meta_campaign_status", "Consulta el estado actual + m\xE9tricas lifetime (spend, impressions, clicks, reach) de una campa\xF1a Meta publicada. Llama esto DESPU\xC9S de `publish_to_meta` para confirmar que la campa\xF1a se cre\xF3 correctamente y para reportar performance al usuario. Es read-only \u2014 no modifica nada en Meta Ads Manager.", MetaStatusInput.shape, async (input) => {
|
|
1237
|
+
try {
|
|
1238
|
+
const r = await api.getJson(`/v1/facebook/campaigns/${encodeURIComponent(input.campaignId)}`);
|
|
1239
|
+
return renderMetaStatusMarkdown(r);
|
|
1240
|
+
} catch (e) {
|
|
1241
|
+
throw translateError(e);
|
|
1242
|
+
}
|
|
1243
|
+
});
|
|
1244
|
+
server.tool("get_tiktok_campaign_status", "Consulta el estado actual + reporte lifetime (spend, impressions, clicks, reach) de una campa\xF1a TikTok publicada. Requiere `advertiserId` (el mismo con el que se cre\xF3). Llama esto DESPU\xC9S de `publish_to_tiktok` para confirmar creaci\xF3n correcta y reportar performance. Es read-only.", TikTokStatusInput.shape, async (input) => {
|
|
1245
|
+
try {
|
|
1246
|
+
const r = await api.getJson(`/v1/tiktok/campaigns/${encodeURIComponent(input.campaignId)}?advertiserId=${encodeURIComponent(input.advertiserId)}`);
|
|
1247
|
+
return renderTikTokStatusMarkdown(r);
|
|
1248
|
+
} catch (e) {
|
|
1249
|
+
throw translateError(e);
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
__name(registerCampaignTools, "registerCampaignTools");
|
|
1254
|
+
function renderCampaignStatusMarkdown(opts) {
|
|
1255
|
+
const lines = [
|
|
1256
|
+
`\u{1F4CA} **${escapeMdLabel(opts.name)}** (\`${opts.campaignId}\`)`
|
|
1257
|
+
];
|
|
1258
|
+
for (const [label, value] of opts.statusLines) {
|
|
1259
|
+
lines.push(`- ${label}: \`${value}\``);
|
|
1260
|
+
}
|
|
1261
|
+
if (opts.metrics) {
|
|
1262
|
+
lines.push("\n### M\xE9tricas (lifetime)");
|
|
1263
|
+
lines.push(`- Spend: ${opts.metrics.spend}`);
|
|
1264
|
+
lines.push(`- Impressions: ${opts.metrics.impressions}`);
|
|
1265
|
+
lines.push(`- Clicks: ${opts.metrics.clicks}`);
|
|
1266
|
+
lines.push(`- Reach: ${opts.metrics.reach}`);
|
|
1267
|
+
} else {
|
|
1268
|
+
lines.push(`
|
|
1269
|
+
_${opts.noMetricsHint ?? "A\xFAn no hay m\xE9tricas reportadas."}_`);
|
|
1270
|
+
}
|
|
1271
|
+
return {
|
|
1272
|
+
content: [
|
|
1273
|
+
{
|
|
1274
|
+
type: "text",
|
|
1275
|
+
text: lines.join("\n")
|
|
1276
|
+
}
|
|
1277
|
+
]
|
|
1278
|
+
};
|
|
1279
|
+
}
|
|
1280
|
+
__name(renderCampaignStatusMarkdown, "renderCampaignStatusMarkdown");
|
|
1281
|
+
function renderMetaStatusMarkdown(r) {
|
|
1282
|
+
const statusLines = [
|
|
1283
|
+
[
|
|
1284
|
+
"Estado efectivo",
|
|
1285
|
+
r.effectiveStatus
|
|
1286
|
+
],
|
|
1287
|
+
[
|
|
1288
|
+
"Estado configurado",
|
|
1289
|
+
r.configuredStatus
|
|
1290
|
+
]
|
|
1291
|
+
];
|
|
1292
|
+
if (r.objective) statusLines.push([
|
|
1293
|
+
"Objetivo",
|
|
1294
|
+
r.objective
|
|
1295
|
+
]);
|
|
1296
|
+
return renderCampaignStatusMarkdown({
|
|
1297
|
+
name: r.name,
|
|
1298
|
+
campaignId: r.campaignId,
|
|
1299
|
+
statusLines,
|
|
1300
|
+
metrics: r.insights,
|
|
1301
|
+
noMetricsHint: "A\xFAn no hay m\xE9tricas reportadas (la campa\xF1a puede estar pausada o sin entregar)."
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
__name(renderMetaStatusMarkdown, "renderMetaStatusMarkdown");
|
|
1305
|
+
function renderTikTokStatusMarkdown(r) {
|
|
1306
|
+
const statusLines = [
|
|
1307
|
+
[
|
|
1308
|
+
"Estado de operaci\xF3n",
|
|
1309
|
+
r.operationStatus
|
|
1310
|
+
]
|
|
1311
|
+
];
|
|
1312
|
+
if (r.secondaryStatus) statusLines.push([
|
|
1313
|
+
"Estado secundario",
|
|
1314
|
+
r.secondaryStatus
|
|
1315
|
+
]);
|
|
1316
|
+
if (r.objectiveType) statusLines.push([
|
|
1317
|
+
"Objetivo",
|
|
1318
|
+
r.objectiveType
|
|
1319
|
+
]);
|
|
1320
|
+
return renderCampaignStatusMarkdown({
|
|
1321
|
+
name: r.name,
|
|
1322
|
+
campaignId: r.campaignId,
|
|
1323
|
+
statusLines,
|
|
1324
|
+
metrics: r.report
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
1327
|
+
__name(renderTikTokStatusMarkdown, "renderTikTokStatusMarkdown");
|
|
1104
1328
|
|
|
1105
1329
|
// src/server.ts
|
|
1106
1330
|
function createServer(opts) {
|
|
@@ -1126,6 +1350,7 @@ function createServer(opts) {
|
|
|
1126
1350
|
});
|
|
1127
1351
|
registerConnectionTools(server, api);
|
|
1128
1352
|
registerPublishTools(server, api);
|
|
1353
|
+
registerCampaignTools(server, api);
|
|
1129
1354
|
return server;
|
|
1130
1355
|
}
|
|
1131
1356
|
__name(createServer, "createServer");
|
package/dist/stdio.js
CHANGED
|
@@ -110,7 +110,7 @@ var ApiClient = class {
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
// src/index.ts
|
|
113
|
-
var MCP_VERSION = "0.
|
|
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);
|
|
@@ -496,7 +504,14 @@ var NarrativeStyleEnum = z.enum([
|
|
|
496
504
|
"problem_product_cta",
|
|
497
505
|
"product_showcase",
|
|
498
506
|
"metaphor_product_cta",
|
|
499
|
-
"wearable_showcase"
|
|
507
|
+
"wearable_showcase",
|
|
508
|
+
"animated_problem_pixar",
|
|
509
|
+
"sassy_product_pixar",
|
|
510
|
+
"ugc_unboxing_chaos",
|
|
511
|
+
"ugc_curiosity_reveal",
|
|
512
|
+
"ugc_listicle_3",
|
|
513
|
+
"ugc_skeptic_confession",
|
|
514
|
+
"ugc_demo_review"
|
|
500
515
|
]);
|
|
501
516
|
var VideoModelEnum = z.enum([
|
|
502
517
|
"veo",
|
|
@@ -550,6 +565,10 @@ function asText(value) {
|
|
|
550
565
|
};
|
|
551
566
|
}
|
|
552
567
|
__name(asText, "asText");
|
|
568
|
+
function escapeMdLabel(s) {
|
|
569
|
+
return s.replace(/[\[\]\(\)\{\}`<>\r\n\\*_]/g, " ").replace(/\s+/g, " ").trim();
|
|
570
|
+
}
|
|
571
|
+
__name(escapeMdLabel, "escapeMdLabel");
|
|
553
572
|
function flatten(obj) {
|
|
554
573
|
const out = {};
|
|
555
574
|
for (const [k, v] of Object.entries(obj)) {
|
|
@@ -573,8 +592,9 @@ async function submitCreative(api, opts) {
|
|
|
573
592
|
const r = await api.postMultipart(opts.path, {
|
|
574
593
|
images: parts
|
|
575
594
|
}, flatten(rest));
|
|
595
|
+
const taskId = r.taskId ?? r.jobId;
|
|
576
596
|
return asText({
|
|
577
|
-
taskId
|
|
597
|
+
taskId,
|
|
578
598
|
status: "queued",
|
|
579
599
|
estimatedSeconds: opts.estimatedSeconds
|
|
580
600
|
});
|
|
@@ -672,18 +692,18 @@ __name(sanitizeFilename, "sanitizeFilename");
|
|
|
672
692
|
async function fetchTaskOutputs(api, taskId) {
|
|
673
693
|
const raw = await api.getJson(`/v1/results/${encodeURIComponent(taskId)}?signedUrls=true`);
|
|
674
694
|
const videos = (raw.videoGenerators ?? []).map((vg) => {
|
|
675
|
-
const
|
|
695
|
+
const variants2 = (vg.variants ?? []).filter((v) => Boolean(v.kind) && Boolean(v.url)).map((v) => ({
|
|
676
696
|
kind: v.kind,
|
|
677
697
|
url: v.url
|
|
678
698
|
}));
|
|
679
|
-
const fallback = !
|
|
699
|
+
const fallback = !variants2.length && Array.isArray(vg.resultUrls) && vg.resultUrls.length ? vg.resultUrls.map((url) => ({
|
|
680
700
|
kind: "mp4",
|
|
681
701
|
url
|
|
682
702
|
})) : [];
|
|
683
703
|
return {
|
|
684
704
|
angleKey: vg.angleKey ?? "",
|
|
685
705
|
angleIndex: vg.angleIndex ?? 0,
|
|
686
|
-
variants:
|
|
706
|
+
variants: variants2.length ? variants2 : fallback
|
|
687
707
|
};
|
|
688
708
|
}).filter((v) => v.angleKey && v.variants.length);
|
|
689
709
|
const sections = (raw.landing?.images ?? []).filter((img) => Boolean(img.role) && Boolean(img.url)).map((img) => ({
|
|
@@ -691,18 +711,21 @@ async function fetchTaskOutputs(api, taskId) {
|
|
|
691
711
|
url: img.url
|
|
692
712
|
}));
|
|
693
713
|
const images = [];
|
|
694
|
-
|
|
714
|
+
const variants = raw.adCreative?.variants ?? [];
|
|
715
|
+
const hasVariants = variants.some((v) => typeof v === "string");
|
|
716
|
+
if (hasVariants) {
|
|
717
|
+
for (const variant of variants) {
|
|
718
|
+
if (typeof variant === "string") images.push({
|
|
719
|
+
url: variant
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
} else if (raw.adCreative?.url) {
|
|
695
723
|
images.push({
|
|
696
724
|
url: raw.adCreative.url,
|
|
697
725
|
width: raw.adCreative.width,
|
|
698
726
|
height: raw.adCreative.height
|
|
699
727
|
});
|
|
700
728
|
}
|
|
701
|
-
for (const variant of raw.adCreative?.variants ?? []) {
|
|
702
|
-
if (typeof variant === "string") images.push({
|
|
703
|
-
url: variant
|
|
704
|
-
});
|
|
705
|
-
}
|
|
706
729
|
return {
|
|
707
730
|
webappUrl: `${WEBAPP_BASE}/tasks/${taskId}`,
|
|
708
731
|
videos,
|
|
@@ -713,21 +736,24 @@ async function fetchTaskOutputs(api, taskId) {
|
|
|
713
736
|
};
|
|
714
737
|
}
|
|
715
738
|
__name(fetchTaskOutputs, "fetchTaskOutputs");
|
|
716
|
-
function escapeMdLabel(s) {
|
|
717
|
-
return s.replace(/[\[\]\(\)\{\}`<>\r\n\\]/g, " ").replace(/\s+/g, " ").trim();
|
|
718
|
-
}
|
|
719
|
-
__name(escapeMdLabel, "escapeMdLabel");
|
|
720
739
|
function renderOutputsMarkdown(outputs) {
|
|
721
740
|
const parts = [];
|
|
722
741
|
parts.push(`\u2705 **Tu tarea est\xE1 lista** \u2014 [Abrir en Product Maker](${outputs.webappUrl})`);
|
|
723
742
|
if (outputs.images.length) {
|
|
724
|
-
parts.push(
|
|
725
|
-
|
|
743
|
+
parts.push(`
|
|
744
|
+
### Ad creative (${outputs.images.length} ${outputs.images.length === 1 ? "variante" : "variantes"})`);
|
|
745
|
+
outputs.images.forEach((img, i) => {
|
|
746
|
+
parts.push(``);
|
|
747
|
+
parts.push(`[\u2B07 Descargar v${i}](${img.url})`);
|
|
748
|
+
});
|
|
726
749
|
}
|
|
727
750
|
if (outputs.landing.sections.length) {
|
|
728
751
|
parts.push(`
|
|
729
752
|
### Landing (${outputs.landing.sections.length} secciones)`);
|
|
730
|
-
for (const s of outputs.landing.sections)
|
|
753
|
+
for (const s of outputs.landing.sections) {
|
|
754
|
+
parts.push(``);
|
|
755
|
+
parts.push(`[\u2B07 Descargar ${escapeMdLabel(s.role)}](${s.url})`);
|
|
756
|
+
}
|
|
731
757
|
}
|
|
732
758
|
if (outputs.videos.length) {
|
|
733
759
|
parts.push("\n### Video");
|
|
@@ -750,9 +776,10 @@ var CreateProductTaskInput = z2.object({
|
|
|
750
776
|
price: z2.string().optional().describe('Precio que se mostrar\xE1 en la landing y video. Formato libre \u2014 ej. "$89.900", "USD 29.99", "MXN 599". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar. Si te dice "t\xFA elige", om\xEDtelo y el backend sugiere uno.'),
|
|
751
777
|
offer: z2.string().optional().describe('Oferta o promoci\xF3n a destacar. Ej. "2x1", "Env\xEDo gratis hoy", "50% OFF". Opcional pero MUY IMPACTANTE: si el usuario no lo mencion\xF3, PREG\xDANTALE antes de invocar (es v\xE1lido que diga "sin oferta"). Si te dice "t\xFA elige", om\xEDtelo.'),
|
|
752
778
|
videoStyle: VideoStyleEnum.optional().describe('Opcional. Estilo visual del video. "ugc" = creador hablando a c\xE1mara (default, mejor CTR LATAM). "cartoon_3d" = animaci\xF3n 3D. "product_only" = sin actor. "ai_dynamic" = cinematogr\xE1fico AI. Si se omite, default "ugc".'),
|
|
753
|
-
narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" =
|
|
779
|
+
narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" = UGC cl\xE1sico multi-hook (default, alta conversi\xF3n). "problem_product_cta" = problema\u2192soluci\xF3n. "product_showcase" = features. "metaphor_product_cta" = met\xE1fora visual. "wearable_showcase" = para ropa/accesorios. "ugc_unboxing_chaos" = unboxing agresivo. "ugc_curiosity_reveal" = hook con producto oculto. "ugc_listicle_3" = 3 razones (30s recomendado). "ugc_skeptic_confession" = esc\xE9ptico\u2192conversi\xF3n (30s recomendado). "ugc_demo_review" = demo en uso a lo largo de d\xEDas. Si se omite, default "review".'),
|
|
754
780
|
videoModel: VideoModelEnum.optional().describe('Opcional. Modelo de video. "veo" (default, calidad Google). "bytedance"/"seedance" = TikTok-style. "kling"/"wan" = alternativos. "infinitalk" = lipsync largo. "grok" = grok-imagine. Si se omite, el sistema escoge seg\xFAn costo/calidad.'),
|
|
755
781
|
multiAngleVideos: z2.boolean().optional().describe("Opcional. Si true, genera m\xFAltiples \xE1ngulos de venta en paralelo (3-5 videos). Si false/omitido, genera 1 video del \xE1ngulo principal. Default false."),
|
|
782
|
+
angleNarratives: NarrativeStyleEnum.array().max(3).optional().describe('Opcional. Narrativa POR \xC1NGULO (posicional). angleNarratives[i] fuerza la narrativa del \xE1ngulo en la posici\xF3n i; usa los mismos valores que narrativeStyle. Si la lista es m\xE1s corta que la cantidad de \xE1ngulos, las posiciones restantes las elige la IA. Ej: ["animated_problem_pixar","review","ugc_skeptic_confession"] para 3 \xE1ngulos distintos. Requiere multiAngleVideos:true (o videoAngleCount>1) para que haya varios \xE1ngulos. Si se omite, todas las narrativas se auto-seleccionan.'),
|
|
756
783
|
outputs: z2.array(z2.enum([
|
|
757
784
|
"landing",
|
|
758
785
|
"video",
|
|
@@ -799,6 +826,7 @@ function registerTaskTools(server, api, { proxyBaseUrl = null, transport = "http
|
|
|
799
826
|
"options.offer": input.offer,
|
|
800
827
|
"options.videoStyle": input.videoStyle,
|
|
801
828
|
"options.narrativeStyle": input.narrativeStyle,
|
|
829
|
+
"options.angleNarratives": input.angleNarratives,
|
|
802
830
|
"options.videoModel": input.videoModel,
|
|
803
831
|
"options.multiAngleVideos": input.multiAngleVideos,
|
|
804
832
|
"options.selectedOutputs": input.outputs,
|
|
@@ -899,7 +927,7 @@ var VideoInput = z3.object({
|
|
|
899
927
|
"Auto"
|
|
900
928
|
]).describe('Relaci\xF3n de aspecto. "9:16" = vertical (TikTok/Reels \u2014 default recomendado para ads). "16:9" = horizontal (YouTube). "Auto" = decide la IA.'),
|
|
901
929
|
durationSeconds: z3.number().int().min(5).max(60).optional().describe("Opcional. Duraci\xF3n del video en segundos (5-60). Default 15s. Para hooks cortos usa 8, para storytelling usa 30."),
|
|
902
|
-
narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" (
|
|
930
|
+
narrativeStyle: NarrativeStyleEnum.optional().describe('Opcional. Estructura narrativa. "review" (UGC cl\xE1sico multi-hook, default), "problem_product_cta", "product_showcase", "metaphor_product_cta", "wearable_showcase", "ugc_unboxing_chaos" (unboxing agresivo), "ugc_curiosity_reveal" (hook con producto oculto), "ugc_listicle_3" (3 razones numeradas, recomendado 30s), "ugc_skeptic_confession" (esc\xE9ptico\u2192conversi\xF3n, recomendado 30s), "ugc_demo_review" (demo en uso a lo largo de d\xEDas).'),
|
|
903
931
|
videoStyle: VideoStyleEnum.optional().describe('Opcional. Estilo visual. "ugc" (creador hablando, default), "cartoon_3d", "product_only", "ai_dynamic".'),
|
|
904
932
|
videoModel: VideoModelEnum.optional().describe('Opcional. Modelo de video. "veo" (default, calidad Google). Ver lista de valores v\xE1lidos.'),
|
|
905
933
|
features: z3.array(z3.string()).optional().describe('Opcional. Lista de features/beneficios clave a mencionar. Ej. ["resistente al agua", "bater\xEDa 24h", "carga inal\xE1mbrica"].'),
|
|
@@ -1005,6 +1033,14 @@ function registerConnectionTools(server, api) {
|
|
|
1005
1033
|
throw translateError(e);
|
|
1006
1034
|
}
|
|
1007
1035
|
});
|
|
1036
|
+
server.tool("list_tiktok_advertisers", "Lista las cuentas de TikTok Ads conectadas. Si est\xE1 vac\xEDa, ind\xEDcale al usuario conectar TikTok en productmaker.app/integrations.", z4.object({}).shape, async () => {
|
|
1037
|
+
try {
|
|
1038
|
+
const r = await api.getJson("/v1/tiktok/advertisers");
|
|
1039
|
+
return asText(r);
|
|
1040
|
+
} catch (e) {
|
|
1041
|
+
throw translateError(e);
|
|
1042
|
+
}
|
|
1043
|
+
});
|
|
1008
1044
|
}
|
|
1009
1045
|
__name(registerConnectionTools, "registerConnectionTools");
|
|
1010
1046
|
|
|
@@ -1015,14 +1051,38 @@ var ShopifyInput = z5.object({
|
|
|
1015
1051
|
shopDomain: z5.string().min(1),
|
|
1016
1052
|
numLandingImages: z5.number().int().min(1).max(7).optional(),
|
|
1017
1053
|
variantIndex: z5.number().int().min(0).optional(),
|
|
1054
|
+
/** ASK THE USER before setting: "¿Tu producto tiene variantes (talla, color, etc.)?".
|
|
1055
|
+
* When true, the landing renders variant-picker pills; when false, it ships
|
|
1056
|
+
* a single-option product page. Defaults to false. */
|
|
1018
1057
|
showVariantPicker: z5.boolean().optional()
|
|
1019
1058
|
});
|
|
1059
|
+
var McpCampaignConfig = z5.object({
|
|
1060
|
+
dailyBudgetUsd: z5.number().positive().optional(),
|
|
1061
|
+
/** ISO-3166-1 alpha-2 country codes. Meta accepts max 1 (single-country
|
|
1062
|
+
* ad set). TikTok takes only the first entry. */
|
|
1063
|
+
targetCountries: z5.array(z5.string().length(2)).max(1).optional(),
|
|
1064
|
+
campaignName: z5.string().trim().min(1).max(100).optional()
|
|
1065
|
+
});
|
|
1020
1066
|
var MetaInput = z5.object({
|
|
1021
1067
|
taskId: z5.string().min(1),
|
|
1022
|
-
adAccountId: z5.string().min(1)
|
|
1068
|
+
adAccountId: z5.string().min(1),
|
|
1069
|
+
pixelId: z5.string().min(1).optional(),
|
|
1070
|
+
landingUrl: z5.string().url().optional(),
|
|
1071
|
+
/** Select which angles to publish by 0-based index. Omit to publish all
|
|
1072
|
+
* successful angles. Example: a task with 3 angles → `[0,2]` publishes #0 + #2. */
|
|
1073
|
+
angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
|
|
1074
|
+
campaignConfig: McpCampaignConfig.optional()
|
|
1075
|
+
});
|
|
1076
|
+
var TikTokInput = z5.object({
|
|
1077
|
+
taskId: z5.string().min(1),
|
|
1078
|
+
advertiserId: z5.string().min(1),
|
|
1079
|
+
pixelId: z5.string().min(1).optional(),
|
|
1080
|
+
landingUrl: z5.string().url().optional(),
|
|
1081
|
+
angleIndexes: z5.array(z5.number().int().nonnegative()).optional(),
|
|
1082
|
+
campaignConfig: McpCampaignConfig.optional()
|
|
1023
1083
|
});
|
|
1024
1084
|
function registerPublishTools(server, api) {
|
|
1025
|
-
server.tool("publish_to_shopify", "Publica la landing y producto generados en una tienda Shopify conectada. Precondici\xF3n: la tarea debe estar en status='complete'
|
|
1085
|
+
server.tool("publish_to_shopify", "Publica la landing y producto generados en una tienda Shopify conectada. Precondici\xF3n: la tarea debe estar en status='complete' \u2014 usa `get_task_status` antes. ANTES de llamar preg\xFAntale al usuario \"\xBFTu producto tiene variantes (talla, color, etc.)?\" y pasa `showVariantPicker: true` si responde que s\xED \u2014 eso renderiza los pills de variantes en la landing. Por defecto incluye 3 im\xE1genes del hero. La respuesta es markdown con el link a la landing y al producto.", ShopifyInput.shape, async (input) => {
|
|
1026
1086
|
try {
|
|
1027
1087
|
const numImages = input.numLandingImages ?? 3;
|
|
1028
1088
|
const variantIndex = input.variantIndex ?? 0;
|
|
@@ -1037,16 +1097,37 @@ function registerPublishTools(server, api) {
|
|
|
1037
1097
|
selectedLandingImages,
|
|
1038
1098
|
showVariantPicker: input.showVariantPicker
|
|
1039
1099
|
});
|
|
1040
|
-
return
|
|
1100
|
+
return renderShopifyPublishMarkdown(r);
|
|
1041
1101
|
} catch (e) {
|
|
1042
1102
|
throw translateError(e);
|
|
1043
1103
|
}
|
|
1044
1104
|
});
|
|
1045
|
-
server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario
|
|
1105
|
+
server.tool("publish_to_meta", "Crea una campa\xF1a de Meta Ads (Facebook/Instagram) desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en Meta Ads Manager. Use `list_meta_ad_accounts` antes para un adAccountId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 si la tarea gener\xF3 3 \xE1ngulos y el usuario solo quiere los #0 y #2, pasa [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, m\xE1x 1 para Meta), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_meta_campaign_status` para chequear el estado.", MetaInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
|
|
1046
1106
|
try {
|
|
1047
|
-
const
|
|
1048
|
-
|
|
1107
|
+
const body = {
|
|
1108
|
+
...rest
|
|
1109
|
+
};
|
|
1110
|
+
if (campaignConfig) {
|
|
1111
|
+
body.campaignOverrides = toMetaCampaignOverrides(campaignConfig);
|
|
1112
|
+
}
|
|
1113
|
+
const r = await api.postJson(`/v1/facebook/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
|
|
1114
|
+
return asText({
|
|
1115
|
+
...r,
|
|
1116
|
+
status: "PAUSED"
|
|
1049
1117
|
});
|
|
1118
|
+
} catch (e) {
|
|
1119
|
+
throw translateError(e);
|
|
1120
|
+
}
|
|
1121
|
+
});
|
|
1122
|
+
server.tool("publish_to_tiktok", "Crea una campa\xF1a de TikTok Ads desde la tarea. La campa\xF1a se crea SIEMPRE en estado PAUSED \u2014 el usuario la activa manualmente en TikTok Ads Manager. Use `list_tiktok_advertisers` antes para un advertiserId v\xE1lido. `pixelId` opcional pero recomendado para tracking. `angleIndexes` (opcional): selecciona qu\xE9 \xE1ngulos publicar \u2014 ej. [0,2]; omitir = publicar todos. `campaignConfig` (opcional): `dailyBudgetUsd` (presupuesto diario en USD, ej. 25), `targetCountries` (c\xF3digos ISO-2, primero se usa), `campaignName` (m\xE1x 100 chars). Despu\xE9s de publicar usa `get_tiktok_campaign_status` para chequear el estado.", TikTokInput.shape, async ({ taskId, campaignConfig, ...rest }) => {
|
|
1123
|
+
try {
|
|
1124
|
+
const body = {
|
|
1125
|
+
...rest
|
|
1126
|
+
};
|
|
1127
|
+
if (campaignConfig) {
|
|
1128
|
+
body.campaignConfig = toTikTokCampaignConfig(campaignConfig);
|
|
1129
|
+
}
|
|
1130
|
+
const r = await api.postJson(`/v1/tiktok/campaigns/from-task/${encodeURIComponent(taskId)}`, body);
|
|
1050
1131
|
return asText({
|
|
1051
1132
|
...r,
|
|
1052
1133
|
status: "PAUSED"
|
|
@@ -1057,6 +1138,149 @@ function registerPublishTools(server, api) {
|
|
|
1057
1138
|
});
|
|
1058
1139
|
}
|
|
1059
1140
|
__name(registerPublishTools, "registerPublishTools");
|
|
1141
|
+
function renderShopifyPublishMarkdown(r) {
|
|
1142
|
+
const lines = [];
|
|
1143
|
+
const statusEmoji = r.status === "success" ? "\u2705" : r.status === "error" ? "\u274C" : "\u23F3";
|
|
1144
|
+
lines.push(`${statusEmoji} **Publicaci\xF3n en Shopify** \u2014 ${r.shopDomain}`);
|
|
1145
|
+
lines.push(`Estado: \`${r.status}\``);
|
|
1146
|
+
if (r.pageUrl) lines.push(`
|
|
1147
|
+
### Landing
|
|
1148
|
+
[Ver landing publicada](${r.pageUrl})`);
|
|
1149
|
+
if (r.productUrl) lines.push(`
|
|
1150
|
+
### Producto
|
|
1151
|
+
[Ver producto](${r.productUrl})`);
|
|
1152
|
+
if (!r.pageUrl && !r.productUrl && r.status !== "success") {
|
|
1153
|
+
lines.push("\n_La publicaci\xF3n a\xFAn est\xE1 proces\xE1ndose. Usa `get_task_status` para verificar progreso._");
|
|
1154
|
+
}
|
|
1155
|
+
return {
|
|
1156
|
+
content: [
|
|
1157
|
+
{
|
|
1158
|
+
type: "text",
|
|
1159
|
+
text: lines.join("\n")
|
|
1160
|
+
}
|
|
1161
|
+
]
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
__name(renderShopifyPublishMarkdown, "renderShopifyPublishMarkdown");
|
|
1165
|
+
function toMetaCampaignOverrides(cfg) {
|
|
1166
|
+
const out = {};
|
|
1167
|
+
if (cfg.dailyBudgetUsd !== void 0) out.dailyBudgetUsd = cfg.dailyBudgetUsd;
|
|
1168
|
+
if (cfg.targetCountries !== void 0) out.targetCountries = cfg.targetCountries;
|
|
1169
|
+
if (cfg.campaignName !== void 0) out.campaignName = cfg.campaignName;
|
|
1170
|
+
return out;
|
|
1171
|
+
}
|
|
1172
|
+
__name(toMetaCampaignOverrides, "toMetaCampaignOverrides");
|
|
1173
|
+
function toTikTokCampaignConfig(cfg) {
|
|
1174
|
+
const out = {};
|
|
1175
|
+
if (cfg.dailyBudgetUsd !== void 0) out.dailyBudget = Math.round(cfg.dailyBudgetUsd * 100);
|
|
1176
|
+
if (cfg.targetCountries && cfg.targetCountries.length > 0) out.targetCountry = cfg.targetCountries[0];
|
|
1177
|
+
if (cfg.campaignName !== void 0) out.campaignName = cfg.campaignName;
|
|
1178
|
+
return out;
|
|
1179
|
+
}
|
|
1180
|
+
__name(toTikTokCampaignConfig, "toTikTokCampaignConfig");
|
|
1181
|
+
|
|
1182
|
+
// src/tools/campaigns.ts
|
|
1183
|
+
import { z as z6 } from "zod";
|
|
1184
|
+
var MetaStatusInput = z6.object({
|
|
1185
|
+
campaignId: z6.string().min(1).describe("Meta campaign ID returned by publish_to_meta.")
|
|
1186
|
+
});
|
|
1187
|
+
var TikTokStatusInput = z6.object({
|
|
1188
|
+
campaignId: z6.string().min(1).describe("TikTok campaign ID returned by publish_to_tiktok."),
|
|
1189
|
+
advertiserId: z6.string().min(1).describe("The same advertiserId used to publish the campaign.")
|
|
1190
|
+
});
|
|
1191
|
+
function registerCampaignTools(server, api) {
|
|
1192
|
+
server.tool("get_meta_campaign_status", "Consulta el estado actual + m\xE9tricas lifetime (spend, impressions, clicks, reach) de una campa\xF1a Meta publicada. Llama esto DESPU\xC9S de `publish_to_meta` para confirmar que la campa\xF1a se cre\xF3 correctamente y para reportar performance al usuario. Es read-only \u2014 no modifica nada en Meta Ads Manager.", MetaStatusInput.shape, async (input) => {
|
|
1193
|
+
try {
|
|
1194
|
+
const r = await api.getJson(`/v1/facebook/campaigns/${encodeURIComponent(input.campaignId)}`);
|
|
1195
|
+
return renderMetaStatusMarkdown(r);
|
|
1196
|
+
} catch (e) {
|
|
1197
|
+
throw translateError(e);
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
server.tool("get_tiktok_campaign_status", "Consulta el estado actual + reporte lifetime (spend, impressions, clicks, reach) de una campa\xF1a TikTok publicada. Requiere `advertiserId` (el mismo con el que se cre\xF3). Llama esto DESPU\xC9S de `publish_to_tiktok` para confirmar creaci\xF3n correcta y reportar performance. Es read-only.", TikTokStatusInput.shape, async (input) => {
|
|
1201
|
+
try {
|
|
1202
|
+
const r = await api.getJson(`/v1/tiktok/campaigns/${encodeURIComponent(input.campaignId)}?advertiserId=${encodeURIComponent(input.advertiserId)}`);
|
|
1203
|
+
return renderTikTokStatusMarkdown(r);
|
|
1204
|
+
} catch (e) {
|
|
1205
|
+
throw translateError(e);
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
__name(registerCampaignTools, "registerCampaignTools");
|
|
1210
|
+
function renderCampaignStatusMarkdown(opts) {
|
|
1211
|
+
const lines = [
|
|
1212
|
+
`\u{1F4CA} **${escapeMdLabel(opts.name)}** (\`${opts.campaignId}\`)`
|
|
1213
|
+
];
|
|
1214
|
+
for (const [label, value] of opts.statusLines) {
|
|
1215
|
+
lines.push(`- ${label}: \`${value}\``);
|
|
1216
|
+
}
|
|
1217
|
+
if (opts.metrics) {
|
|
1218
|
+
lines.push("\n### M\xE9tricas (lifetime)");
|
|
1219
|
+
lines.push(`- Spend: ${opts.metrics.spend}`);
|
|
1220
|
+
lines.push(`- Impressions: ${opts.metrics.impressions}`);
|
|
1221
|
+
lines.push(`- Clicks: ${opts.metrics.clicks}`);
|
|
1222
|
+
lines.push(`- Reach: ${opts.metrics.reach}`);
|
|
1223
|
+
} else {
|
|
1224
|
+
lines.push(`
|
|
1225
|
+
_${opts.noMetricsHint ?? "A\xFAn no hay m\xE9tricas reportadas."}_`);
|
|
1226
|
+
}
|
|
1227
|
+
return {
|
|
1228
|
+
content: [
|
|
1229
|
+
{
|
|
1230
|
+
type: "text",
|
|
1231
|
+
text: lines.join("\n")
|
|
1232
|
+
}
|
|
1233
|
+
]
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
__name(renderCampaignStatusMarkdown, "renderCampaignStatusMarkdown");
|
|
1237
|
+
function renderMetaStatusMarkdown(r) {
|
|
1238
|
+
const statusLines = [
|
|
1239
|
+
[
|
|
1240
|
+
"Estado efectivo",
|
|
1241
|
+
r.effectiveStatus
|
|
1242
|
+
],
|
|
1243
|
+
[
|
|
1244
|
+
"Estado configurado",
|
|
1245
|
+
r.configuredStatus
|
|
1246
|
+
]
|
|
1247
|
+
];
|
|
1248
|
+
if (r.objective) statusLines.push([
|
|
1249
|
+
"Objetivo",
|
|
1250
|
+
r.objective
|
|
1251
|
+
]);
|
|
1252
|
+
return renderCampaignStatusMarkdown({
|
|
1253
|
+
name: r.name,
|
|
1254
|
+
campaignId: r.campaignId,
|
|
1255
|
+
statusLines,
|
|
1256
|
+
metrics: r.insights,
|
|
1257
|
+
noMetricsHint: "A\xFAn no hay m\xE9tricas reportadas (la campa\xF1a puede estar pausada o sin entregar)."
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
__name(renderMetaStatusMarkdown, "renderMetaStatusMarkdown");
|
|
1261
|
+
function renderTikTokStatusMarkdown(r) {
|
|
1262
|
+
const statusLines = [
|
|
1263
|
+
[
|
|
1264
|
+
"Estado de operaci\xF3n",
|
|
1265
|
+
r.operationStatus
|
|
1266
|
+
]
|
|
1267
|
+
];
|
|
1268
|
+
if (r.secondaryStatus) statusLines.push([
|
|
1269
|
+
"Estado secundario",
|
|
1270
|
+
r.secondaryStatus
|
|
1271
|
+
]);
|
|
1272
|
+
if (r.objectiveType) statusLines.push([
|
|
1273
|
+
"Objetivo",
|
|
1274
|
+
r.objectiveType
|
|
1275
|
+
]);
|
|
1276
|
+
return renderCampaignStatusMarkdown({
|
|
1277
|
+
name: r.name,
|
|
1278
|
+
campaignId: r.campaignId,
|
|
1279
|
+
statusLines,
|
|
1280
|
+
metrics: r.report
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
__name(renderTikTokStatusMarkdown, "renderTikTokStatusMarkdown");
|
|
1060
1284
|
|
|
1061
1285
|
// src/server.ts
|
|
1062
1286
|
function createServer(opts) {
|
|
@@ -1082,6 +1306,7 @@ function createServer(opts) {
|
|
|
1082
1306
|
});
|
|
1083
1307
|
registerConnectionTools(server, api);
|
|
1084
1308
|
registerPublishTools(server, api);
|
|
1309
|
+
registerCampaignTools(server, api);
|
|
1085
1310
|
return server;
|
|
1086
1311
|
}
|
|
1087
1312
|
__name(createServer, "createServer");
|
package/package.json
CHANGED
|
@@ -1,8 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@productmaker/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"private": false,
|
|
6
|
+
"description": "Product Maker MCP server — connect Claude Desktop, Claude Code, ChatGPT, or Gemini to Product Maker AI to generate dropshipping landing pages, UGC videos, and ad creatives, and publish to Shopify and Meta Ads from your AI assistant.",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"mcp",
|
|
9
|
+
"mcp-server",
|
|
10
|
+
"model-context-protocol",
|
|
11
|
+
"claude",
|
|
12
|
+
"claude-desktop",
|
|
13
|
+
"claude-code",
|
|
14
|
+
"chatgpt",
|
|
15
|
+
"gemini",
|
|
16
|
+
"anthropic",
|
|
17
|
+
"openai",
|
|
18
|
+
"ai",
|
|
19
|
+
"productmaker",
|
|
20
|
+
"dropshipping",
|
|
21
|
+
"ecommerce",
|
|
22
|
+
"shopify",
|
|
23
|
+
"meta-ads",
|
|
24
|
+
"facebook-ads",
|
|
25
|
+
"tiktok-ads",
|
|
26
|
+
"ugc",
|
|
27
|
+
"video-generation",
|
|
28
|
+
"video-ads",
|
|
29
|
+
"landing-page",
|
|
30
|
+
"creative-generation",
|
|
31
|
+
"ad-creative",
|
|
32
|
+
"marketing"
|
|
33
|
+
],
|
|
34
|
+
"homepage": "https://productmaker.app/install",
|
|
35
|
+
"bugs": {
|
|
36
|
+
"email": "support@productmaker.ai"
|
|
37
|
+
},
|
|
38
|
+
"author": "Product Maker AI <support@productmaker.ai> (https://productmaker.app)",
|
|
39
|
+
"license": "MIT",
|
|
6
40
|
"bin": {
|
|
7
41
|
"productmaker-mcp": "./dist/stdio.js"
|
|
8
42
|
},
|