mobile-growth-mcp 2.0.3 → 2.0.8
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 +33 -2
- package/dist/index.js +195 -41
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -12,7 +12,26 @@ You'll need an API key — get one from the person who set up your account.
|
|
|
12
12
|
|
|
13
13
|
## Setup
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
There are three ways to provide your API key and Meta token. The server checks them in this order:
|
|
16
|
+
|
|
17
|
+
### Option 1: CLI arguments
|
|
18
|
+
|
|
19
|
+
Pass keys directly in the `args` array. Works with every MCP client.
|
|
20
|
+
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"mcpServers": {
|
|
24
|
+
"mobile-growth": {
|
|
25
|
+
"command": "npx",
|
|
26
|
+
"args": ["-y", "mobile-growth-mcp", "--api-key=me_your-api-key", "--meta-token=your-meta-access-token"]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
### Option 2: Environment variables
|
|
33
|
+
|
|
34
|
+
Use the `env` block in your MCP client config.
|
|
16
35
|
|
|
17
36
|
```json
|
|
18
37
|
{
|
|
@@ -29,7 +48,18 @@ Add this to your MCP client config (Claude Desktop, Cursor, Claude Code, Codex,
|
|
|
29
48
|
}
|
|
30
49
|
```
|
|
31
50
|
|
|
32
|
-
|
|
51
|
+
### Option 3: `.env` file
|
|
52
|
+
|
|
53
|
+
Create a `.env` file in your working directory:
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
API_KEY=me_your-api-key
|
|
57
|
+
META_ACCESS_TOKEN=your-meta-access-token
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### Keys
|
|
61
|
+
|
|
62
|
+
- `API_KEY` — authenticates with the knowledge base. Without it, the server still starts but only Meta tools are available. Call the `connection_status` tool to see what's connected.
|
|
33
63
|
- `META_ACCESS_TOKEN` (optional) — enables Meta Marketing API tools. Without it, knowledge base tools still work.
|
|
34
64
|
|
|
35
65
|
## What you get
|
|
@@ -44,6 +74,7 @@ Add this to your MCP client config (Claude Desktop, Cursor, Claude Code, Codex,
|
|
|
44
74
|
|
|
45
75
|
| Tool | Description |
|
|
46
76
|
|------|-------------|
|
|
77
|
+
| `connection_status` | Check KB and Meta API connection status; shows how to fix issues |
|
|
47
78
|
| `search_insights` | Semantic + keyword hybrid search across curated insights |
|
|
48
79
|
| `get_insight` | Full content of a specific insight by slug or ID |
|
|
49
80
|
| `get_meta_campaigns` | List campaigns with objectives, budgets, bid strategies |
|
package/dist/index.js
CHANGED
|
@@ -83,16 +83,7 @@ function jsonSchemaToZodShape(inputSchema) {
|
|
|
83
83
|
}
|
|
84
84
|
return shape;
|
|
85
85
|
}
|
|
86
|
-
|
|
87
|
-
let tools;
|
|
88
|
-
try {
|
|
89
|
-
tools = await fetchRemoteTools(apiKey2);
|
|
90
|
-
} catch (err) {
|
|
91
|
-
console.error(
|
|
92
|
-
`Failed to fetch remote tools: ${err.message}. KB tools will not be available.`
|
|
93
|
-
);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
86
|
+
function registerFetchedTools(server2, apiKey2, tools) {
|
|
96
87
|
for (const tool of tools) {
|
|
97
88
|
const zodShape = jsonSchemaToZodShape(tool.inputSchema);
|
|
98
89
|
server2.tool(tool.name, tool.description, zodShape, async (args) => {
|
|
@@ -124,17 +115,8 @@ async function getRemotePrompt(apiKey2, name, args) {
|
|
|
124
115
|
}
|
|
125
116
|
return resp.result?.messages ?? [];
|
|
126
117
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
remotePrompts = await fetchRemotePrompts(apiKey2);
|
|
131
|
-
} catch (err) {
|
|
132
|
-
console.error(
|
|
133
|
-
`Failed to fetch remote prompts: ${err.message}. Prompts will not be available.`
|
|
134
|
-
);
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
for (const prompt of remotePrompts) {
|
|
118
|
+
function registerFetchedPrompts(server2, apiKey2, prompts) {
|
|
119
|
+
for (const prompt of prompts) {
|
|
138
120
|
const zodShape = {};
|
|
139
121
|
for (const arg of prompt.arguments) {
|
|
140
122
|
let field = z.string().describe(arg.description);
|
|
@@ -729,26 +711,26 @@ function registerGetMetaAdFatigue(server2) {
|
|
|
729
711
|
const lateSpend = lateDays.reduce((s, d) => s + d.spend, 0);
|
|
730
712
|
const recentCpa = lateConv > 0 ? lateSpend / lateConv : null;
|
|
731
713
|
const cpaChange = earlyCpa !== null && recentCpa !== null && earlyCpa > 0 ? (recentCpa - earlyCpa) / earlyCpa * 100 : null;
|
|
732
|
-
let
|
|
714
|
+
let status2 = "HEALTHY";
|
|
733
715
|
let diagnosis = "Metrics stable \u2014 no fatigue detected.";
|
|
734
716
|
const highFreq = avgFreq >= freqCrit;
|
|
735
717
|
const medFreq = avgFreq >= freqWarn;
|
|
736
718
|
const ctrDeclining = ctrDecline >= ctrThreshold;
|
|
737
719
|
const cpaRising = cpaChange !== null && cpaChange > 20;
|
|
738
720
|
if (highFreq && cpaRising) {
|
|
739
|
-
|
|
721
|
+
status2 = "FATIGUED";
|
|
740
722
|
diagnosis = `Audience saturation: frequency ${avgFreq.toFixed(1)} + CPA rising ${cpaChange.toFixed(0)}% [wk-tw-001 #1, ds-pt-003]`;
|
|
741
723
|
} else if (ctrDeclining && cpaRising) {
|
|
742
|
-
|
|
724
|
+
status2 = "FATIGUED";
|
|
743
725
|
diagnosis = `Creative fatigue: CTR declined ${ctrDecline.toFixed(0)}% from peak + CPA rising ${cpaChange.toFixed(0)}% [wk-tw-001 #4]`;
|
|
744
726
|
} else if (highFreq) {
|
|
745
|
-
|
|
727
|
+
status2 = "WARNING";
|
|
746
728
|
diagnosis = `High frequency (${avgFreq.toFixed(1)}) \u2014 approaching saturation [ds-pt-003]. CPA ${cpaRising ? "rising" : "stable"}.`;
|
|
747
729
|
} else if (ctrDeclining) {
|
|
748
|
-
|
|
730
|
+
status2 = "WARNING";
|
|
749
731
|
diagnosis = `CTR declining ${ctrDecline.toFixed(0)}% from peak \u2014 early fatigue signal [wk-tw-001 #4].`;
|
|
750
732
|
} else if (medFreq && cpaRising) {
|
|
751
|
-
|
|
733
|
+
status2 = "WARNING";
|
|
752
734
|
diagnosis = `Frequency ${avgFreq.toFixed(1)} + CPA trending up ${cpaChange.toFixed(0)}% \u2014 monitor closely [wk-tw-001 #1].`;
|
|
753
735
|
}
|
|
754
736
|
results.push({
|
|
@@ -762,7 +744,7 @@ function registerGetMetaAdFatigue(server2) {
|
|
|
762
744
|
early_cpa: earlyCpa,
|
|
763
745
|
recent_cpa: recentCpa,
|
|
764
746
|
cpa_change_pct: cpaChange,
|
|
765
|
-
status,
|
|
747
|
+
status: status2,
|
|
766
748
|
diagnosis
|
|
767
749
|
});
|
|
768
750
|
}
|
|
@@ -837,6 +819,58 @@ ${text}`;
|
|
|
837
819
|
);
|
|
838
820
|
}
|
|
839
821
|
|
|
822
|
+
// src/tools/connection-status.ts
|
|
823
|
+
function registerConnectionStatus(server2, status2) {
|
|
824
|
+
server2.tool(
|
|
825
|
+
"connection_status",
|
|
826
|
+
"Check the connection status of the knowledge base and Meta API. Call this if tools seem missing or you get unexpected errors.",
|
|
827
|
+
{},
|
|
828
|
+
async () => {
|
|
829
|
+
const lines = ["# Connection Status", ""];
|
|
830
|
+
if (status2.kb.connected) {
|
|
831
|
+
lines.push(
|
|
832
|
+
`## Knowledge Base: Connected`,
|
|
833
|
+
`- ${status2.kb.toolCount} KB tools loaded`,
|
|
834
|
+
`- ${status2.kb.promptCount} prompts loaded`,
|
|
835
|
+
`- API key source: ${status2.apiKey.source}`
|
|
836
|
+
);
|
|
837
|
+
} else {
|
|
838
|
+
lines.push(
|
|
839
|
+
`## Knowledge Base: Not Connected`,
|
|
840
|
+
`- Reason: ${status2.kb.error ?? "API_KEY not configured"}`,
|
|
841
|
+
"",
|
|
842
|
+
"### How to fix",
|
|
843
|
+
"Provide your API key using one of these methods (in priority order):",
|
|
844
|
+
"1. CLI argument: add `--api-key=me_...` to the args array in your MCP config",
|
|
845
|
+
'2. Environment variable: add `"API_KEY": "me_..."` to the env block in your MCP config',
|
|
846
|
+
"3. `.env` file: create a `.env` file in your working directory with `API_KEY=me_...`"
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
lines.push("");
|
|
850
|
+
if (status2.meta.tokenConfigured) {
|
|
851
|
+
lines.push(
|
|
852
|
+
"## Meta Marketing API: Configured",
|
|
853
|
+
"- Meta tools are available and ready to use"
|
|
854
|
+
);
|
|
855
|
+
} else {
|
|
856
|
+
lines.push(
|
|
857
|
+
"## Meta Marketing API: Not Configured",
|
|
858
|
+
"- Meta tools will return an error when called",
|
|
859
|
+
"",
|
|
860
|
+
"### How to fix",
|
|
861
|
+
"Provide your Meta access token using one of these methods:",
|
|
862
|
+
"1. CLI argument: add `--meta-token=...` to the args array",
|
|
863
|
+
'2. Environment variable: add `"META_ACCESS_TOKEN": "..."` to the env block',
|
|
864
|
+
"3. `.env` file: add `META_ACCESS_TOKEN=...` to your `.env` file"
|
|
865
|
+
);
|
|
866
|
+
}
|
|
867
|
+
return {
|
|
868
|
+
content: [{ type: "text", text: lines.join("\n") }]
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
|
|
840
874
|
// ../shared/dist/types.js
|
|
841
875
|
var VALID_PLATFORMS = [
|
|
842
876
|
"meta",
|
|
@@ -1042,7 +1076,35 @@ Lists all topic tags, applies_to tags, and platforms with counts.
|
|
|
1042
1076
|
- Reports reference specific knowledge base insight IDs \u2014 use \`get_insight\` to read the full context
|
|
1043
1077
|
- For custom date ranges, use time_range instead of date_preset
|
|
1044
1078
|
`;
|
|
1045
|
-
function
|
|
1079
|
+
function buildStatusSection(status2) {
|
|
1080
|
+
if (!status2) return "";
|
|
1081
|
+
const lines = ["\n## Connection Status\n"];
|
|
1082
|
+
if (status2.kb.connected) {
|
|
1083
|
+
lines.push(
|
|
1084
|
+
`- **Knowledge Base**: Connected (${status2.kb.toolCount} tools, ${status2.kb.promptCount} prompts)`
|
|
1085
|
+
);
|
|
1086
|
+
} else {
|
|
1087
|
+
lines.push(
|
|
1088
|
+
`- **Knowledge Base**: Not connected \u2014 ${status2.kb.error ?? "API_KEY not configured"}`
|
|
1089
|
+
);
|
|
1090
|
+
lines.push(
|
|
1091
|
+
" - Fix: provide your API key via `--api-key=me_...` CLI arg, `API_KEY` env var, or `.env` file"
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
if (status2.meta.tokenConfigured) {
|
|
1095
|
+
lines.push("- **Meta Marketing API**: Token configured");
|
|
1096
|
+
} else {
|
|
1097
|
+
lines.push(
|
|
1098
|
+
"- **Meta Marketing API**: Token not configured \u2014 Meta tools will return errors"
|
|
1099
|
+
);
|
|
1100
|
+
lines.push(
|
|
1101
|
+
" - Fix: provide your token via `--meta-token=...` CLI arg, `META_ACCESS_TOKEN` env var, or `.env` file"
|
|
1102
|
+
);
|
|
1103
|
+
}
|
|
1104
|
+
return lines.join("\n");
|
|
1105
|
+
}
|
|
1106
|
+
function registerInstructionsResource(server2, status2) {
|
|
1107
|
+
const text = INSTRUCTIONS + buildStatusSection(status2);
|
|
1046
1108
|
server2.resource(
|
|
1047
1109
|
"instructions",
|
|
1048
1110
|
"instructions://getting-started",
|
|
@@ -1055,34 +1117,126 @@ function registerInstructionsResource(server2) {
|
|
|
1055
1117
|
{
|
|
1056
1118
|
uri: "instructions://getting-started",
|
|
1057
1119
|
mimeType: "text/plain",
|
|
1058
|
-
text
|
|
1120
|
+
text
|
|
1059
1121
|
}
|
|
1060
1122
|
]
|
|
1061
1123
|
})
|
|
1062
1124
|
);
|
|
1063
1125
|
}
|
|
1064
1126
|
|
|
1065
|
-
// src/
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1127
|
+
// src/config.ts
|
|
1128
|
+
import { readFileSync } from "fs";
|
|
1129
|
+
import { join } from "path";
|
|
1130
|
+
function parseEnvFile(path) {
|
|
1131
|
+
let content;
|
|
1132
|
+
try {
|
|
1133
|
+
content = readFileSync(path, "utf-8");
|
|
1134
|
+
} catch {
|
|
1135
|
+
return {};
|
|
1136
|
+
}
|
|
1137
|
+
const vars = {};
|
|
1138
|
+
for (const line of content.split("\n")) {
|
|
1139
|
+
const trimmed = line.trim();
|
|
1140
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
1141
|
+
const eq = trimmed.indexOf("=");
|
|
1142
|
+
if (eq === -1) continue;
|
|
1143
|
+
const key = trimmed.slice(0, eq).trim();
|
|
1144
|
+
let value = trimmed.slice(eq + 1).trim();
|
|
1145
|
+
if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
|
|
1146
|
+
value = value.slice(1, -1);
|
|
1147
|
+
}
|
|
1148
|
+
vars[key] = value;
|
|
1149
|
+
}
|
|
1150
|
+
return vars;
|
|
1151
|
+
}
|
|
1152
|
+
function getCliArg(name) {
|
|
1153
|
+
const prefix = `--${name}=`;
|
|
1154
|
+
for (const arg of process.argv) {
|
|
1155
|
+
if (arg.startsWith(prefix)) {
|
|
1156
|
+
return arg.slice(prefix.length).trim();
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return void 0;
|
|
1160
|
+
}
|
|
1161
|
+
var dotEnvCache;
|
|
1162
|
+
function getDotEnv() {
|
|
1163
|
+
if (!dotEnvCache) {
|
|
1164
|
+
dotEnvCache = parseEnvFile(join(process.cwd(), ".env"));
|
|
1165
|
+
}
|
|
1166
|
+
return dotEnvCache;
|
|
1167
|
+
}
|
|
1168
|
+
function resolve(envName, cliName) {
|
|
1169
|
+
const cli = getCliArg(cliName);
|
|
1170
|
+
if (cli) return { value: cli, source: `--${cliName} argument` };
|
|
1171
|
+
const env = process.env[envName]?.trim();
|
|
1172
|
+
if (env) return { value: env, source: `${envName} env var` };
|
|
1173
|
+
const dotenv = getDotEnv()[envName];
|
|
1174
|
+
if (dotenv) return { value: dotenv, source: ".env file" };
|
|
1175
|
+
return { value: void 0, source: "not configured" };
|
|
1072
1176
|
}
|
|
1177
|
+
function resolveApiKey() {
|
|
1178
|
+
return resolve("API_KEY", "api-key");
|
|
1179
|
+
}
|
|
1180
|
+
function resolveMetaToken() {
|
|
1181
|
+
return resolve("META_ACCESS_TOKEN", "meta-token");
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
// src/index.ts
|
|
1185
|
+
var apiKeyResult = resolveApiKey();
|
|
1186
|
+
var metaTokenResult = resolveMetaToken();
|
|
1187
|
+
if (apiKeyResult.value) process.env.API_KEY = apiKeyResult.value;
|
|
1188
|
+
if (metaTokenResult.value) process.env.META_ACCESS_TOKEN = metaTokenResult.value;
|
|
1189
|
+
var apiKey = apiKeyResult.value;
|
|
1190
|
+
console.error(
|
|
1191
|
+
apiKey ? `API key: ${apiKeyResult.source}` : "API key: not configured \u2014 KB tools will not be available"
|
|
1192
|
+
);
|
|
1193
|
+
console.error(
|
|
1194
|
+
metaTokenResult.value ? `Meta token: ${metaTokenResult.source}` : "Meta token: not configured \u2014 Meta tools will return errors when called"
|
|
1195
|
+
);
|
|
1073
1196
|
var server = new McpServer({
|
|
1074
1197
|
name: "mobile-growth-mcp",
|
|
1075
1198
|
version: "2.0.0"
|
|
1076
1199
|
});
|
|
1077
|
-
|
|
1078
|
-
|
|
1200
|
+
var status = {
|
|
1201
|
+
kb: { connected: false, toolCount: 0, promptCount: 0 },
|
|
1202
|
+
meta: { tokenConfigured: !!metaTokenResult.value },
|
|
1203
|
+
apiKey: { source: apiKeyResult.source }
|
|
1204
|
+
};
|
|
1205
|
+
if (apiKey) {
|
|
1206
|
+
console.error("Connecting to knowledge base...");
|
|
1207
|
+
try {
|
|
1208
|
+
const tools = await fetchRemoteTools(apiKey);
|
|
1209
|
+
registerFetchedTools(server, apiKey, tools);
|
|
1210
|
+
status.kb.toolCount = tools.length;
|
|
1211
|
+
status.kb.connected = true;
|
|
1212
|
+
console.error(`KB connected: ${tools.length} tools loaded`);
|
|
1213
|
+
} catch (err) {
|
|
1214
|
+
const msg = err.message;
|
|
1215
|
+
status.kb.error = msg;
|
|
1216
|
+
console.error(`KB tools failed: ${msg}`);
|
|
1217
|
+
}
|
|
1218
|
+
} else {
|
|
1219
|
+
status.kb.error = "API_KEY not configured";
|
|
1220
|
+
}
|
|
1079
1221
|
registerGetMetaCampaigns(server);
|
|
1080
1222
|
registerGetMetaAdSets(server);
|
|
1081
1223
|
registerGetMetaAds(server);
|
|
1082
1224
|
registerGetMetaInsights(server);
|
|
1083
1225
|
registerGetMetaAdFatigue(server);
|
|
1226
|
+
registerConnectionStatus(server, status);
|
|
1084
1227
|
registerVocabularyResource(server);
|
|
1085
|
-
registerInstructionsResource(server);
|
|
1086
|
-
|
|
1228
|
+
registerInstructionsResource(server, status);
|
|
1229
|
+
if (apiKey) {
|
|
1230
|
+
try {
|
|
1231
|
+
const prompts = await fetchRemotePrompts(apiKey);
|
|
1232
|
+
registerFetchedPrompts(server, apiKey, prompts);
|
|
1233
|
+
status.kb.promptCount = prompts.length;
|
|
1234
|
+
console.error(`KB prompts: ${prompts.length} loaded`);
|
|
1235
|
+
} catch (err) {
|
|
1236
|
+
console.error(
|
|
1237
|
+
`KB prompts failed: ${err.message}`
|
|
1238
|
+
);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1087
1241
|
var transport = new StdioServerTransport();
|
|
1088
1242
|
await server.connect(transport);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mobile-growth-mcp",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
4
4
|
"description": "MCP server for mobile growth & UA knowledge base — campaign optimization, creative strategy, and subscription app insights",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -33,5 +33,13 @@
|
|
|
33
33
|
"meta-ads",
|
|
34
34
|
"subscription-apps"
|
|
35
35
|
],
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "git+https://github.com/kubachour/mobile-growth-mcp.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/kubachour/mobile-growth-mcp#readme",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/kubachour/mobile-growth-mcp/issues"
|
|
43
|
+
},
|
|
36
44
|
"license": "MIT"
|
|
37
45
|
}
|