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 +1 -8
- package/dist/build-info.json +1 -1
- package/dist/index.js +23 -11
- package/dist/resilience.js +46 -16
- package/package.json +1 -1
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/
|
|
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.
|
package/dist/build-info.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"sha":"
|
|
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
|
-
|
|
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.
|
|
27
|
-
console.
|
|
28
|
-
console.
|
|
29
|
-
console.
|
|
30
|
-
console.
|
|
31
|
-
console.
|
|
32
|
-
console.
|
|
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.
|
|
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:
|
|
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);
|
package/dist/resilience.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { retry, circuitBreaker, wrap,
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
return
|
|
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
|
|
26
|
-
const obj =
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
|
52
|
+
return current;
|
|
36
53
|
}
|
|
37
54
|
const backoff = new ExponentialBackoff({ initialDelay: 100, maxDelay: 5_000 });
|
|
38
|
-
const
|
|
39
|
-
const
|
|
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