mobile-growth-mcp 2.3.8 → 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.
Files changed (2) hide show
  1. package/dist/index.js +103 -49
  2. 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 jsonRpcRequest(apiKey2, "tools/list");
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
- const maxAttempts = 2;
75
- let lastError;
76
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
77
- try {
78
- const resp = await jsonRpcRequest(apiKey2, "tools/call", {
79
- name,
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: resp.result?.content ?? [{ type: "text", text: "No content returned" }],
90
- isError: resp.result?.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 jsonRpcRequest(apiKey2, "prompts/list");
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 jsonRpcRequest(apiKey2, "prompts/get", {
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 assetNames = [];
1630
- for (const img of images) {
1631
- const name = img.name ?? basename(img.source).replace(/\?.*$/, "") ?? "unnamed";
1632
- assetNames.push(name);
1633
- const isUrl = img.source.startsWith("http://") || img.source.startsWith("https://") || img.source.startsWith("data:");
1634
- const data = isUrl ? await fetchAsBase64(img.source) : await readFileAsBase64(img.source);
1635
- assetOps.push({
1636
- assetOperation: {
1637
- create: {
1638
- name,
1639
- type: "IMAGE",
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,7 +1755,7 @@ Assets exist in the account and can be linked manually.`;
1715
1755
  }
1716
1756
 
1717
1757
  // src/tools/connection-status.ts
1718
- var SERVER_VERSION = true ? "2.3.8" : "dev";
1758
+ var SERVER_VERSION = true ? "2.3.9" : "dev";
1719
1759
  function registerConnectionStatus(server2, status2) {
1720
1760
  server2.tool(
1721
1761
  "connection_status",
@@ -2336,8 +2376,10 @@ function saveToEnv(vars) {
2336
2376
  `);
2337
2377
  }
2338
2378
  }
2379
+ var OAUTH_CALLBACK_TIMEOUT_MS = 5 * 60 * 1e3;
2339
2380
  async function waitForOAuthCallback(clientId, clientSecret) {
2340
2381
  return new Promise((resolve3, reject) => {
2382
+ let timer;
2341
2383
  const server2 = createServer(
2342
2384
  async (req, res) => {
2343
2385
  const url = new URL(req.url ?? "/", `http://localhost:${OAUTH_PORT}`);
@@ -2404,8 +2446,20 @@ async function waitForOAuthCallback(clientId, clientSecret) {
2404
2446
  }
2405
2447
  );
2406
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);
2407
2460
  });
2408
2461
  server2.on("error", (err) => {
2462
+ if (timer) clearTimeout(timer);
2409
2463
  if (err.code === "EADDRINUSE") {
2410
2464
  reject(
2411
2465
  new Error(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mobile-growth-mcp",
3
- "version": "2.3.8",
3
+ "version": "2.3.9",
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",