mcp-ga4 2.0.3 → 2.0.5

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 CHANGED
@@ -19,7 +19,7 @@ npm install mcp-ga4
19
19
  Or clone the repository:
20
20
 
21
21
  ```bash
22
- git clone https://github.com/drak-marketing/mcp-ga4.git
22
+ git clone https://github.com/mharnett/mcp-ga4.git
23
23
  cd mcp-ga4
24
24
  npm install
25
25
  npm run build
@@ -36,13 +36,6 @@ GA4_PROPERTY_ID=123456789
36
36
  GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
37
37
  ```
38
38
 
39
- For OAuth credentials instead of a service account:
40
-
41
- ```bash
42
- GA4_PROPERTY_ID=123456789
43
- GA4_CREDENTIALS_FILE=/path/to/oauth-credentials.json
44
- ```
45
-
46
39
  ### Mode 2: Multi-Client (config.json)
47
40
 
48
41
  Create a `config.json` in the project root to map multiple GA4 properties to project directories. The server auto-detects which property to use based on the caller's working directory.
@@ -1 +1 @@
1
- {"sha":"991317b","builtAt":"2026-04-09T19:37:26.093Z"}
1
+ {"sha":"29cbd4f","builtAt":"2026-04-09T21:28:40.127Z"}
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@ import { GoogleAuth } from "google-auth-library";
11
11
  import { Ga4AuthError, Ga4RateLimitError, Ga4ServiceError, classifyError, } from "./errors.js";
12
12
  import { tools } from "./tools.js";
13
13
  import { withResilience, safeResponse, logger } from "./resilience.js";
14
+ // CLI package info
15
+ const __cliPkg = JSON.parse(readFileSync(join(dirname(new URL(import.meta.url).pathname), "..", "package.json"), "utf-8"));
14
16
  // Log build fingerprint at startup
15
17
  try {
16
18
  const __buildInfoDir = dirname(new URL(import.meta.url).pathname);
@@ -18,22 +20,21 @@ try {
18
20
  console.error(`[build] SHA: ${buildInfo.sha} (${buildInfo.builtAt})`);
19
21
  }
20
22
  catch {
21
- // dev mode
23
+ console.error(`[build] ${__cliPkg.name}@${__cliPkg.version} (dev mode)`);
22
24
  }
23
25
  // CLI flags
24
- const __cliPkg = JSON.parse(readFileSync(join(dirname(new URL(import.meta.url).pathname), "..", "package.json"), "utf-8"));
25
26
  if (process.argv.includes("--help") || process.argv.includes("-h")) {
26
- console.log(`${__cliPkg.name} v${__cliPkg.version}\n`);
27
- console.log(`Usage: ${__cliPkg.name} [options]\n`);
28
- console.log("MCP server communicating via stdio. Configure in your .mcp.json.\n");
29
- console.log("Options:");
30
- console.log(" --help, -h Show this help message");
31
- console.log(" --version, -v Show version number");
32
- console.log(`\nDocumentation: https://github.com/mharnett/mcp-ga4`);
27
+ console.error(`${__cliPkg.name} v${__cliPkg.version}\n`);
28
+ console.error(`Usage: ${__cliPkg.name} [options]\n`);
29
+ console.error("MCP server communicating via stdio. Configure in your .mcp.json.\n");
30
+ console.error("Options:");
31
+ console.error(" --help, -h Show this help message");
32
+ console.error(" --version, -v Show version number");
33
+ console.error(`\nDocumentation: https://github.com/mharnett/mcp-ga4`);
33
34
  process.exit(0);
34
35
  }
35
36
  if (process.argv.includes("--version") || process.argv.includes("-v")) {
36
- console.log(__cliPkg.version);
37
+ console.error(__cliPkg.version);
37
38
  process.exit(0);
38
39
  }
39
40
  function loadConfig() {
@@ -314,7 +315,7 @@ Metrics: sessions, totalUsers, newUsers, activeUsers, screenPageViews,
314
315
 
315
316
  ## Date Formats
316
317
  Use YYYY-MM-DD or relative: "today", "yesterday", "7daysAgo", "30daysAgo", "90daysAgo"`;
317
- const server = new Server({ name: "mcp-ga4", version: "1.0.0" }, { capabilities: { tools: {} } });
318
+ const server = new Server({ name: __cliPkg.name, version: __cliPkg.version }, { capabilities: { tools: {} } });
318
319
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
319
320
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
320
321
  const { name, arguments: args } = request.params;
@@ -443,4 +444,15 @@ async function main() {
443
444
  await server.connect(transport);
444
445
  console.error("[startup] MCP GA4 server running");
445
446
  }
447
+ process.on("SIGTERM", () => {
448
+ console.error("[shutdown] SIGTERM received, exiting");
449
+ process.exit(0);
450
+ });
451
+ process.on("SIGINT", () => {
452
+ console.error("[shutdown] SIGINT received, exiting");
453
+ process.exit(0);
454
+ });
455
+ process.on("SIGPIPE", () => {
456
+ // Client disconnected -- expected during shutdown
457
+ });
446
458
  main().catch(console.error);
@@ -1,4 +1,4 @@
1
- import { retry, circuitBreaker, wrap, handleAll, timeout, TimeoutStrategy, ExponentialBackoff, ConsecutiveBreaker, } from "cockatiel";
1
+ import { retry, circuitBreaker, wrap, handleWhen, timeout, TimeoutStrategy, ExponentialBackoff, ConsecutiveBreaker, } from "cockatiel";
2
2
  import pino from "pino";
3
3
  export const logger = pino({
4
4
  level: process.env.LOG_LEVEL || "info",
@@ -9,34 +9,64 @@ export const logger = pino({
9
9
  colorize: true,
10
10
  singleLine: true,
11
11
  translateTime: "SYS:standard",
12
+ destination: 2, // stderr -- stdout is reserved for MCP JSON-RPC
12
13
  },
13
14
  },
14
15
  }),
15
- });
16
+ },
17
+ // When no transport (test mode), write to stderr directly
18
+ process.env.NODE_ENV === "test" ? pino.destination(2) : undefined);
16
19
  const MAX_RESPONSE_SIZE = 200_000;
17
20
  export function safeResponse(data, context) {
18
- const jsonStr = JSON.stringify(data);
19
- const sizeBytes = Buffer.byteLength(jsonStr, "utf-8");
20
- if (sizeBytes > MAX_RESPONSE_SIZE) {
21
- logger.warn({ sizeBytes, maxSize: MAX_RESPONSE_SIZE, context }, "Response exceeds size limit, truncating");
22
- if (Array.isArray(data)) {
23
- return data.slice(0, Math.max(1, Math.floor(data.length * 0.5)));
21
+ let current = data;
22
+ for (let pass = 0; pass < 10; pass++) {
23
+ const jsonStr = JSON.stringify(current);
24
+ const sizeBytes = Buffer.byteLength(jsonStr, "utf-8");
25
+ if (sizeBytes <= MAX_RESPONSE_SIZE)
26
+ return current;
27
+ logger.warn({ sizeBytes, maxSize: MAX_RESPONSE_SIZE, context, pass }, "Response exceeds size limit, truncating");
28
+ if (Array.isArray(current)) {
29
+ current = current.slice(0, Math.max(1, Math.floor(current.length * 0.5)));
30
+ continue;
24
31
  }
25
- if (typeof data === "object" && data !== null) {
26
- const obj = data;
27
- for (const key of ["items", "results", "data", "rows"]) {
28
- if (Array.isArray(obj[key])) {
32
+ if (typeof current === "object" && current !== null) {
33
+ const obj = current;
34
+ let truncated = false;
35
+ for (const key of ["items", "results", "data", "rows", "tags", "triggers", "variables"]) {
36
+ if (Array.isArray(obj[key]) && obj[key].length > 1) {
29
37
  obj[key] = obj[key].slice(0, Math.max(1, Math.floor(obj[key].length * 0.5)));
30
- return obj;
38
+ if ("count" in obj)
39
+ obj.count = obj[key].length;
40
+ if ("row_count" in obj)
41
+ obj.row_count = obj[key].length;
42
+ obj.truncated = true;
43
+ truncated = true;
44
+ break;
31
45
  }
32
46
  }
47
+ if (truncated)
48
+ continue;
33
49
  }
50
+ break;
34
51
  }
35
- return data;
52
+ return current;
36
53
  }
37
54
  const backoff = new ExponentialBackoff({ initialDelay: 100, maxDelay: 5_000 });
38
- const retryPolicy = retry(handleAll, { maxAttempts: 3, backoff });
39
- const circuitBreakerPolicy = circuitBreaker(handleAll, { halfOpenAfter: 60_000, breaker: new ConsecutiveBreaker(5) });
55
+ const isTransient = handleWhen((err) => {
56
+ const msg = (err?.message || "").toLowerCase();
57
+ const code = err?.code || err?.status;
58
+ if (code === 401 || code === 403 || code === 7 || code === 16)
59
+ return false;
60
+ if (msg.includes("unauthenticated") || msg.includes("permission_denied") || msg.includes("invalid_grant"))
61
+ return false;
62
+ if (code === 429 || msg.includes("rate"))
63
+ return true;
64
+ if (code >= 400 && code < 500)
65
+ return false;
66
+ return true;
67
+ });
68
+ const retryPolicy = retry(isTransient, { maxAttempts: 3, backoff });
69
+ const circuitBreakerPolicy = circuitBreaker(isTransient, { halfOpenAfter: 60_000, breaker: new ConsecutiveBreaker(5) });
40
70
  const timeoutPolicy = timeout(30_000, TimeoutStrategy.Cooperative);
41
71
  const policy = wrap(timeoutPolicy, circuitBreakerPolicy, retryPolicy);
42
72
  export async function withResilience(fn, operationName) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mcp-ga4",
3
3
  "mcpName": "io.github.mharnett/ga4",
4
- "version": "2.0.3",
4
+ "version": "2.0.5",
5
5
  "description": "MCP server for Google Analytics 4 - query GA4 data with natural language via Claude.",
6
6
  "main": "dist/index.js",
7
7
  "bin": {