mobile-growth-mcp 2.3.7 → 2.3.9
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/dist/index.js +111 -50
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -63,43 +63,50 @@ async function jsonRpcRequest(apiKey2, method, params) {
|
|
|
63
63
|
throw err;
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
async function jsonRpcRequestWithRetry(apiKey2, method, params, maxAttempts = 2) {
|
|
67
|
+
let lastError;
|
|
68
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
69
|
+
try {
|
|
70
|
+
return await jsonRpcRequest(apiKey2, method, params);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
lastError = err;
|
|
73
|
+
const isRetryable = lastError.name === "AbortError" || lastError.name === "TimeoutError" || lastError.message?.includes("fetch failed");
|
|
74
|
+
if (!isRetryable || attempt === maxAttempts) break;
|
|
75
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
throw lastError ?? new Error(`${method} failed after ${maxAttempts} attempts`);
|
|
79
|
+
}
|
|
66
80
|
async function fetchRemoteTools(apiKey2) {
|
|
67
|
-
const resp = await
|
|
81
|
+
const resp = await jsonRpcRequestWithRetry(apiKey2, "tools/list");
|
|
68
82
|
if (resp.error) {
|
|
69
83
|
throw new Error(`tools/list error: ${resp.error.message}`);
|
|
70
84
|
}
|
|
71
85
|
return resp.result?.tools ?? [];
|
|
72
86
|
}
|
|
73
87
|
async function callRemoteTool(apiKey2, name, args) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
arguments: args
|
|
81
|
-
});
|
|
82
|
-
if (resp.error) {
|
|
83
|
-
return {
|
|
84
|
-
content: [{ type: "text", text: `Remote error: ${resp.error.message}` }],
|
|
85
|
-
isError: true
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
+
try {
|
|
89
|
+
const resp = await jsonRpcRequestWithRetry(apiKey2, "tools/call", {
|
|
90
|
+
name,
|
|
91
|
+
arguments: args
|
|
92
|
+
});
|
|
93
|
+
if (resp.error) {
|
|
88
94
|
return {
|
|
89
|
-
content:
|
|
90
|
-
isError:
|
|
95
|
+
content: [{ type: "text", text: `Remote error: ${resp.error.message}` }],
|
|
96
|
+
isError: true
|
|
91
97
|
};
|
|
92
|
-
} catch (err) {
|
|
93
|
-
lastError = err;
|
|
94
|
-
const isRetryable = lastError.name === "AbortError" || lastError.name === "TimeoutError" || lastError.message?.includes("fetch failed");
|
|
95
|
-
if (!isRetryable || attempt === maxAttempts) break;
|
|
96
|
-
await new Promise((r) => setTimeout(r, 2e3));
|
|
97
98
|
}
|
|
99
|
+
return {
|
|
100
|
+
content: resp.result?.content ?? [{ type: "text", text: "No content returned" }],
|
|
101
|
+
isError: resp.result?.isError
|
|
102
|
+
};
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const lastError = err;
|
|
105
|
+
return {
|
|
106
|
+
content: [{ type: "text", text: `Remote call failed after retry: ${lastError.message ?? "unknown error"}` }],
|
|
107
|
+
isError: true
|
|
108
|
+
};
|
|
98
109
|
}
|
|
99
|
-
return {
|
|
100
|
-
content: [{ type: "text", text: `Remote call failed after retry: ${lastError?.message ?? "unknown error"}` }],
|
|
101
|
-
isError: true
|
|
102
|
-
};
|
|
103
110
|
}
|
|
104
111
|
function jsonSchemaToZodShape(inputSchema) {
|
|
105
112
|
const properties = inputSchema.properties ?? {};
|
|
@@ -146,14 +153,14 @@ function registerFetchedTools(server2, apiKey2, tools) {
|
|
|
146
153
|
}
|
|
147
154
|
}
|
|
148
155
|
async function fetchRemotePrompts(apiKey2) {
|
|
149
|
-
const resp = await
|
|
156
|
+
const resp = await jsonRpcRequestWithRetry(apiKey2, "prompts/list");
|
|
150
157
|
if (resp.error) {
|
|
151
158
|
throw new Error(`prompts/list error: ${resp.error.message}`);
|
|
152
159
|
}
|
|
153
160
|
return resp.result?.prompts ?? [];
|
|
154
161
|
}
|
|
155
162
|
async function getRemotePrompt(apiKey2, name, args) {
|
|
156
|
-
const resp = await
|
|
163
|
+
const resp = await jsonRpcRequestWithRetry(apiKey2, "prompts/get", {
|
|
157
164
|
name,
|
|
158
165
|
arguments: args
|
|
159
166
|
});
|
|
@@ -1556,13 +1563,16 @@ Sources: goog-pdf-018, ab-pt-008, goog-pdf-019, ab-pt-007
|
|
|
1556
1563
|
import { z as z8 } from "zod";
|
|
1557
1564
|
import { readFile } from "fs/promises";
|
|
1558
1565
|
import { basename, resolve } from "path";
|
|
1566
|
+
var FETCH_TIMEOUT_MS = 3e4;
|
|
1559
1567
|
async function fetchAsBase64(url) {
|
|
1560
1568
|
if (url.startsWith("data:")) {
|
|
1561
1569
|
const commaIdx = url.indexOf(",");
|
|
1562
1570
|
if (commaIdx === -1) throw new Error("Invalid data URI");
|
|
1563
1571
|
return url.slice(commaIdx + 1);
|
|
1564
1572
|
}
|
|
1565
|
-
const res = await fetch(url
|
|
1573
|
+
const res = await fetch(url, {
|
|
1574
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
1575
|
+
});
|
|
1566
1576
|
if (!res.ok) throw new Error(`Failed to fetch ${url}: ${res.status}`);
|
|
1567
1577
|
const buf = Buffer.from(await res.arrayBuffer());
|
|
1568
1578
|
return buf.toString("base64");
|
|
@@ -1625,38 +1635,68 @@ Would link to campaign ${campaign_id} as ${effectiveFieldType}`;
|
|
|
1625
1635
|
}
|
|
1626
1636
|
return { content: [{ type: "text", text: text2 }] };
|
|
1627
1637
|
}
|
|
1638
|
+
const assetNames = images.map(
|
|
1639
|
+
(img) => img.name ?? basename(img.source).replace(/\?.*$/, "") ?? "unnamed"
|
|
1640
|
+
);
|
|
1641
|
+
const fetchResults = await Promise.allSettled(
|
|
1642
|
+
images.map((img) => {
|
|
1643
|
+
const isUrl = img.source.startsWith("http://") || img.source.startsWith("https://") || img.source.startsWith("data:");
|
|
1644
|
+
return isUrl ? fetchAsBase64(img.source) : readFileAsBase64(img.source);
|
|
1645
|
+
})
|
|
1646
|
+
);
|
|
1628
1647
|
const assetOps = [];
|
|
1629
|
-
const
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
imageAsset: { data }
|
|
1648
|
+
const opOriginIndex = [];
|
|
1649
|
+
const fetchFailures = [];
|
|
1650
|
+
fetchResults.forEach((r, i) => {
|
|
1651
|
+
if (r.status === "fulfilled") {
|
|
1652
|
+
assetOps.push({
|
|
1653
|
+
assetOperation: {
|
|
1654
|
+
create: {
|
|
1655
|
+
name: assetNames[i],
|
|
1656
|
+
type: "IMAGE",
|
|
1657
|
+
imageAsset: { data: r.value }
|
|
1658
|
+
}
|
|
1641
1659
|
}
|
|
1642
|
-
}
|
|
1660
|
+
});
|
|
1661
|
+
opOriginIndex.push(i);
|
|
1662
|
+
} else {
|
|
1663
|
+
fetchFailures.push({
|
|
1664
|
+
index: i,
|
|
1665
|
+
error: r.reason instanceof Error ? r.reason.message : String(r.reason)
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
const createdResourceNames = new Array(images.length).fill("");
|
|
1670
|
+
let successCount = 0;
|
|
1671
|
+
if (assetOps.length > 0) {
|
|
1672
|
+
const assetResult = await googleAdsMutate(normalizedId, assetOps);
|
|
1673
|
+
const responses = assetResult.mutateOperationResponses;
|
|
1674
|
+
if (responses.length !== assetOps.length) {
|
|
1675
|
+
throw new Error(
|
|
1676
|
+
`Google Ads response length mismatch: sent ${assetOps.length} ops, got ${responses.length} responses`
|
|
1677
|
+
);
|
|
1678
|
+
}
|
|
1679
|
+
responses.forEach((r, i) => {
|
|
1680
|
+
const originalIdx = opOriginIndex[i];
|
|
1681
|
+
const rn = r.assetResult?.resourceName ?? "";
|
|
1682
|
+
createdResourceNames[originalIdx] = rn;
|
|
1683
|
+
if (rn) successCount++;
|
|
1643
1684
|
});
|
|
1644
1685
|
}
|
|
1645
|
-
const assetResult = await googleAdsMutate(normalizedId, assetOps);
|
|
1646
|
-
const createdResourceNames = assetResult.mutateOperationResponses.map(
|
|
1647
|
-
(r) => r.assetResult?.resourceName ?? ""
|
|
1648
|
-
);
|
|
1649
|
-
const successCount = createdResourceNames.filter(Boolean).length;
|
|
1650
1686
|
let text = `**Uploaded ${successCount}/${images.length} image assets**
|
|
1651
1687
|
|
|
1652
1688
|
`;
|
|
1653
1689
|
for (let i = 0; i < images.length; i++) {
|
|
1654
1690
|
const rn = createdResourceNames[i];
|
|
1691
|
+
const failure = fetchFailures.find((f) => f.index === i);
|
|
1655
1692
|
if (rn) {
|
|
1656
1693
|
text += `\u2713 ${assetNames[i]} \u2192 ${rn}
|
|
1694
|
+
`;
|
|
1695
|
+
} else if (failure) {
|
|
1696
|
+
text += `\u2717 ${assetNames[i]} \u2014 fetch failed: ${failure.error}
|
|
1657
1697
|
`;
|
|
1658
1698
|
} else {
|
|
1659
|
-
text += `\u2717 ${assetNames[i]} \u2014 failed
|
|
1699
|
+
text += `\u2717 ${assetNames[i]} \u2014 upload failed
|
|
1660
1700
|
`;
|
|
1661
1701
|
}
|
|
1662
1702
|
}
|
|
@@ -1715,13 +1755,19 @@ Assets exist in the account and can be linked manually.`;
|
|
|
1715
1755
|
}
|
|
1716
1756
|
|
|
1717
1757
|
// src/tools/connection-status.ts
|
|
1758
|
+
var SERVER_VERSION = true ? "2.3.9" : "dev";
|
|
1718
1759
|
function registerConnectionStatus(server2, status2) {
|
|
1719
1760
|
server2.tool(
|
|
1720
1761
|
"connection_status",
|
|
1721
1762
|
"Check the connection status of the knowledge base and Google Ads API. Call this if tools seem missing or you get unexpected errors.",
|
|
1722
1763
|
{},
|
|
1723
1764
|
async () => {
|
|
1724
|
-
const lines = [
|
|
1765
|
+
const lines = [
|
|
1766
|
+
"# Connection Status",
|
|
1767
|
+
"",
|
|
1768
|
+
`**Server version:** mobile-growth-mcp@${SERVER_VERSION}`,
|
|
1769
|
+
""
|
|
1770
|
+
];
|
|
1725
1771
|
if (status2.kb.connected) {
|
|
1726
1772
|
lines.push(
|
|
1727
1773
|
`## Knowledge Base: Connected`,
|
|
@@ -2330,8 +2376,10 @@ function saveToEnv(vars) {
|
|
|
2330
2376
|
`);
|
|
2331
2377
|
}
|
|
2332
2378
|
}
|
|
2379
|
+
var OAUTH_CALLBACK_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2333
2380
|
async function waitForOAuthCallback(clientId, clientSecret) {
|
|
2334
2381
|
return new Promise((resolve3, reject) => {
|
|
2382
|
+
let timer;
|
|
2335
2383
|
const server2 = createServer(
|
|
2336
2384
|
async (req, res) => {
|
|
2337
2385
|
const url = new URL(req.url ?? "/", `http://localhost:${OAUTH_PORT}`);
|
|
@@ -2398,8 +2446,20 @@ async function waitForOAuthCallback(clientId, clientSecret) {
|
|
|
2398
2446
|
}
|
|
2399
2447
|
);
|
|
2400
2448
|
server2.listen(OAUTH_PORT, () => {
|
|
2449
|
+
timer = setTimeout(() => {
|
|
2450
|
+
server2.close();
|
|
2451
|
+
reject(
|
|
2452
|
+
new Error(
|
|
2453
|
+
`OAuth authorization timed out after ${OAUTH_CALLBACK_TIMEOUT_MS / 1e3}s. Re-run the auth command and complete the consent screen in the browser.`
|
|
2454
|
+
)
|
|
2455
|
+
);
|
|
2456
|
+
}, OAUTH_CALLBACK_TIMEOUT_MS);
|
|
2457
|
+
});
|
|
2458
|
+
server2.on("close", () => {
|
|
2459
|
+
if (timer) clearTimeout(timer);
|
|
2401
2460
|
});
|
|
2402
2461
|
server2.on("error", (err) => {
|
|
2462
|
+
if (timer) clearTimeout(timer);
|
|
2403
2463
|
if (err.code === "EADDRINUSE") {
|
|
2404
2464
|
reject(
|
|
2405
2465
|
new Error(
|
|
@@ -2502,6 +2562,7 @@ if (googleAdsResult.refreshToken)
|
|
|
2502
2562
|
process.env.GOOGLE_ADS_REFRESH_TOKEN = googleAdsResult.refreshToken;
|
|
2503
2563
|
if (googleAdsResult.loginCustomerId)
|
|
2504
2564
|
process.env.GOOGLE_ADS_LOGIN_CUSTOMER_ID = googleAdsResult.loginCustomerId;
|
|
2565
|
+
console.error(`mobile-growth-mcp@${SERVER_VERSION} starting`);
|
|
2505
2566
|
var apiKey = apiKeyResult.value;
|
|
2506
2567
|
console.error(
|
|
2507
2568
|
apiKey ? `API key: ${apiKeyResult.source}` : "API key: not configured \u2014 KB tools will not be available"
|
|
@@ -2511,7 +2572,7 @@ console.error(
|
|
|
2511
2572
|
);
|
|
2512
2573
|
var server = new McpServer({
|
|
2513
2574
|
name: "mobile-growth-mcp",
|
|
2514
|
-
version:
|
|
2575
|
+
version: SERVER_VERSION
|
|
2515
2576
|
});
|
|
2516
2577
|
var status = {
|
|
2517
2578
|
kb: { connected: false, toolCount: 0, promptCount: 0 },
|
package/package.json
CHANGED