google-analytics-mcp 2.7.2__tar.gz → 2.7.4__tar.gz

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 (23) hide show
  1. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/PKG-INFO +1 -1
  2. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/coordinator.py +33 -2
  3. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/resources.py +19 -0
  4. google_analytics_mcp-2.7.4/ga4_mcp/server.py +196 -0
  5. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/setup_flow.py +58 -3
  6. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/telemetry.py +36 -0
  7. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/tools/reporting.py +5 -3
  8. google_analytics_mcp-2.7.4/ga4_mcp/tools/troubleshooting.py +73 -0
  9. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/google_analytics_mcp.egg-info/PKG-INFO +1 -1
  10. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/pyproject.toml +1 -1
  11. google_analytics_mcp-2.7.2/ga4_mcp/server.py +0 -153
  12. google_analytics_mcp-2.7.2/ga4_mcp/tools/troubleshooting.py +0 -28
  13. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/LICENSE +0 -0
  14. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/README.md +0 -0
  15. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/__init__.py +0 -0
  16. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/__main__.py +0 -0
  17. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/tools/metadata.py +0 -0
  18. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/google_analytics_mcp.egg-info/SOURCES.txt +0 -0
  19. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/google_analytics_mcp.egg-info/dependency_links.txt +0 -0
  20. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/google_analytics_mcp.egg-info/entry_points.txt +0 -0
  21. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/google_analytics_mcp.egg-info/requires.txt +0 -0
  22. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/google_analytics_mcp.egg-info/top_level.txt +0 -0
  23. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-analytics-mcp
3
- Version: 2.7.2
3
+ Version: 2.7.4
4
4
  Summary: Model Context Protocol for Google Analytics 4 (Data API) allowing autonomous agents to query dimensions and metrics. Gives agents analysis-ready GA4 access with schema discovery, server-side aggregation, and smart defaults.
5
5
  Author-email: Surendran B <reachsuren@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -3,7 +3,9 @@
3
3
  """FastMCP singleton, plus the decorator that wraps each tool with telemetry
4
4
  and boot-error interception. Telemetry mechanics live in ga4_mcp.telemetry."""
5
5
 
6
+ import os
6
7
  import sys
8
+ import json
7
9
  import time
8
10
  import inspect
9
11
  import functools
@@ -31,6 +33,30 @@ SERVER_INIT_ERROR_CATEGORY = "InitError"
31
33
  mcp = FastMCP("Google Analytics 4")
32
34
  telemetry.announce_and_fire_boot_events()
33
35
 
36
+
37
+ def inspect_credentials(path):
38
+ """Report the SHAPE of a credentials file so error messages can be
39
+ auth-model-correct and hand the model exact values — without logging any
40
+ secret. Returns (model, client_email, ok):
41
+ model: service_account | adc | unknown | missing | unreadable
42
+ client_email: only for service_account (safe to show — it's the grantee)
43
+ ok: whether the file is present and parseable."""
44
+ try:
45
+ if not path or not os.path.exists(path):
46
+ return ("missing", None, False)
47
+ with open(path, encoding="utf-8") as f:
48
+ data = json.load(f)
49
+ t = data.get("type")
50
+ if t == "service_account":
51
+ return ("service_account", data.get("client_email"), True)
52
+ if t == "authorized_user":
53
+ return ("adc", None, True)
54
+ return ("unknown", None, True)
55
+ except json.JSONDecodeError:
56
+ return ("unreadable", None, False)
57
+ except Exception:
58
+ return ("unknown", None, False)
59
+
34
60
  _original_tool = mcp.tool
35
61
 
36
62
 
@@ -126,8 +152,12 @@ def _telemetry_tool(*args, **kwargs):
126
152
  is_async = inspect.iscoroutinefunction(func)
127
153
 
128
154
  def _intercept(name):
129
- return (f"Configuration Error: {SERVER_INIT_ERROR}. Please instruct the user to fix their setup."
130
- if (SERVER_INIT_ERROR and name not in _INIT_ERROR_EXEMPT) else None)
155
+ # SERVER_INIT_ERROR is already a self-contained decision brief (built
156
+ # in server.py) what broke, why, don't-retry, exact user action,
157
+ # who, forwardable, optional depth. Deliver it as-is; no extra hop.
158
+ if not SERVER_INIT_ERROR or name in _INIT_ERROR_EXEMPT:
159
+ return None
160
+ return str(SERVER_INIT_ERROR)
131
161
 
132
162
  if is_async:
133
163
  @functools.wraps(func)
@@ -243,6 +273,7 @@ def reinitialize():
243
273
  metadata.PROPERTY_SCHEMA = schema
244
274
  reporting.PROPERTY_SCHEMA = schema
245
275
  SERVER_INIT_ERROR = None
276
+ telemetry.mark_ever_worked()
246
277
  return True, "ok", "initialized"
247
278
  except Exception as e:
248
279
  err = str(e)
@@ -52,3 +52,22 @@ def get_setup_guide() -> str:
52
52
  """Provides instructions to the agent on how to heal the human's MCP setup."""
53
53
  send_telemetry("resource_read", {"resource_uri": "docs://setup_guide"})
54
54
  return SETUP_GUIDE_MD
55
+
56
+
57
+ # Category-addressed fix guides, same content the get_troubleshooting_guide tool
58
+ # serves — exposed as resources for clients that surface resources to the model.
59
+ # One source of truth (tools.troubleshooting._GUIDES); progressive enhancement
60
+ # on top of the always-reaches error playbook.
61
+ from .tools.troubleshooting import _GUIDES as _FIX_GUIDES
62
+
63
+
64
+ def _make_fix_resource(topic):
65
+ def _read() -> str:
66
+ send_telemetry("resource_read", {"resource_uri": f"docs://fix/{topic}"})
67
+ return _FIX_GUIDES[topic]
68
+ _read.__name__ = f"get_fix_{topic}"
69
+ return mcp.resource(f"docs://fix/{topic}")(_read)
70
+
71
+
72
+ for _t in _FIX_GUIDES:
73
+ _make_fix_resource(_t)
@@ -0,0 +1,196 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ import os
4
+ import sys
5
+ from .coordinator import mcp
6
+ from .tools import metadata, reporting
7
+ from . import resources
8
+
9
+ # Property schema, cached at startup.
10
+ PROPERTY_SCHEMA = None
11
+
12
+ def main():
13
+ """
14
+ Main entry point for the MCP server.
15
+
16
+ This function performs the following steps:
17
+ 1. Validates required environment variables.
18
+ 2. Fetches and caches the GA4 property schema (dimensions and metrics).
19
+ 3. Registers the tools with the MCP server.
20
+ 4. Starts the server and listens for requests.
21
+ """
22
+ print("Starting GA4 MCP server...", file=sys.stderr)
23
+ import ga4_mcp.coordinator as coordinator
24
+ from ga4_mcp import telemetry
25
+ import ga4_mcp.tools.troubleshooting # Register the troubleshooting tool
26
+ import ga4_mcp.setup_flow # Register the interactive setup-recovery tool
27
+ config_status = "valid"
28
+
29
+ # 1. Validate environment variables
30
+ credentials_path = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
31
+ property_id = os.getenv("GA4_PROPERTY_ID")
32
+
33
+ setup_url = "https://ga4.builditwithai.xyz/setup"
34
+
35
+ def _config_hint():
36
+ # Name the config surface for the detected client.
37
+ agent = getattr(coordinator, "AGENT_NAME", "")
38
+ if agent == "claude_code":
39
+ return ("In Claude Code run: claude mcp add ga4-analytics -e GA4_PROPERTY_ID=<id> "
40
+ "-e GOOGLE_APPLICATION_CREDENTIALS=<key-path> -- uvx --from google-analytics-mcp ga4-mcp-server")
41
+ if agent == "claude_desktop":
42
+ return ("In Claude Desktop: Settings > Developer > Edit Config, set these env values under "
43
+ "mcpServers > ga4-analytics in claude_desktop_config.json")
44
+ if agent == "cursor":
45
+ return "In Cursor: Settings > MCP (edit .cursor/mcp.json), set these env values for ga4-analytics"
46
+ if agent in ("vscode", "windsurf"):
47
+ return "Edit this editor's MCP settings JSON and set these env values for ga4-analytics"
48
+ return "Set these env values in your MCP client's server config for ga4-analytics"
49
+
50
+ def _guided(what, steps, anchor, topic="setup", why=None, who=None, handoff=None):
51
+ """Self-contained decision brief written FOR THE MODEL: what broke, why it
52
+ blocks everything, that retrying is futile, exactly what the user must do
53
+ (with their values), and who can do it. setup_ga4_access and docs are
54
+ OPTIONAL depth, not the path to understanding — reduce hops."""
55
+ step_text = " ".join(f"({i}) {s}" for i, s in enumerate(steps, 1))
56
+ why = why or "No GA4 data can be returned until this is resolved."
57
+ who = who or "the user (whoever set up this server's Google access)"
58
+ parts = [
59
+ f"[SETUP BLOCKED] {what}",
60
+ f"WHY: {why}",
61
+ "RETRYING WON'T HELP — every call fails identically until the user changes setup outside this tool; do not re-call data tools.",
62
+ f"WHAT MUST HAPPEN (only the user can do this): {step_text}",
63
+ f"WHO CAN DO IT: {who}.",
64
+ ]
65
+ if handoff:
66
+ parts.append(f'FORWARDABLE — the user can send this verbatim to whoever admins their GA4/Google Cloud: "{handoff}"')
67
+ parts.append(
68
+ f"OPTIONAL (not needed to understand or relay this): call setup_ga4_access to collect a missing value "
69
+ f"in-session; get_troubleshooting_guide(topic='{topic}') or resource docs://fix/{topic} for detail; "
70
+ f"full guide {setup_url}#{anchor}.")
71
+ return " ".join(parts)
72
+
73
+ if not credentials_path:
74
+ print("ERROR: GOOGLE_APPLICATION_CREDENTIALS environment variable not set.", file=sys.stderr)
75
+ coordinator.SERVER_INIT_ERROR = _guided(
76
+ "No Google credentials are configured — GOOGLE_APPLICATION_CREDENTIALS is unset.",
77
+ ["Point GOOGLE_APPLICATION_CREDENTIALS at a Google service-account JSON key "
78
+ "(Cloud Console > IAM > Service Accounts > Keys) — best for a persistent/shared setup; "
79
+ "OR if you have the gcloud CLI, run 'gcloud auth application-default login' and use that credentials file.",
80
+ _config_hint()],
81
+ "credentials",
82
+ why="The server cannot authenticate to the GA4 API, so no query can run.")
83
+ config_status = "error"
84
+ elif not property_id:
85
+ print("ERROR: GA4_PROPERTY_ID environment variable not set.", file=sys.stderr)
86
+ coordinator.SERVER_INIT_ERROR = _guided(
87
+ "No GA4 Property ID is set — GA4_PROPERTY_ID is unset.",
88
+ ["Set GA4_PROPERTY_ID to the numeric Property ID (NOT the 'G-' Measurement ID) — "
89
+ "find it at analytics.google.com > Admin > Property details (e.g. 123456789).",
90
+ _config_hint()],
91
+ "property-id",
92
+ why="Every query must target a specific property; without the ID nothing can be read.")
93
+ config_status = "error"
94
+ elif "ABSOLUTE/PATH/TO" in credentials_path:
95
+ print(f"ERROR: Dummy credentials path detected: '{credentials_path}'.", file=sys.stderr)
96
+ coordinator.SERVER_INIT_ERROR = _guided(
97
+ "The credentials path is still the copy-paste placeholder ('/ABSOLUTE/PATH/TO...'), not a real path.",
98
+ ["Replace the /ABSOLUTE/PATH/TO placeholder in the config with the real absolute path of the "
99
+ "downloaded service-account JSON key.",
100
+ _config_hint()],
101
+ "credentials",
102
+ why="The server has no real credentials file to authenticate with, so no query can run.")
103
+ config_status = "error"
104
+ elif not os.path.exists(credentials_path):
105
+ print(f"ERROR: Credentials file not found at '{credentials_path}'.", file=sys.stderr)
106
+ coordinator.SERVER_INIT_ERROR = _guided(
107
+ f"The credentials file does not exist at the configured path '{credentials_path}'.",
108
+ ["Verify the file exists at that exact absolute path (check the filename, folder, and any typo).",
109
+ "If it was moved or never downloaded, re-download the service-account JSON key from "
110
+ "Google Cloud Console > IAM > Service Accounts > Keys and point the config at it.",
111
+ _config_hint()],
112
+ "credentials",
113
+ why="The server cannot read credentials, so it cannot authenticate to GA4.")
114
+ config_status = "error"
115
+ else:
116
+ # 2. Fetch and cache the GA4 property schema
117
+ print(f"Fetching schema for property '{property_id}'...", file=sys.stderr)
118
+ global PROPERTY_SCHEMA
119
+ try:
120
+ PROPERTY_SCHEMA = metadata.get_property_schema_uncached(property_id)
121
+ print("Schema loaded successfully.", file=sys.stderr)
122
+ telemetry.mark_ever_worked()
123
+ except Exception as e:
124
+ print(f"FATAL: Could not fetch GA4 property schema: {e}", file=sys.stderr)
125
+ err_str = str(e)
126
+ if "403" in err_str or "PermissionDenied" in err_str or "permission" in err_str.lower():
127
+ model, email, _ = coordinator.inspect_credentials(credentials_path)
128
+ if model == "service_account" and email:
129
+ grantee = f"the service account {email}"
130
+ handoff = (f"Please add {email} as a Viewer on GA4 property {property_id} "
131
+ f"(analytics.google.com > Admin > Property Access Management).")
132
+ elif model == "adc":
133
+ grantee = "the Google account you authenticated with via gcloud"
134
+ handoff = (f"Please grant my Google account Viewer access on GA4 property {property_id} "
135
+ f"(Admin > Property Access Management).")
136
+ else:
137
+ grantee = "the service account (the client_email inside the JSON key)"
138
+ handoff = f"Please add my service account as a Viewer on GA4 property {property_id}."
139
+ coordinator.SERVER_INIT_ERROR = _guided(
140
+ f"Credentials are valid, but {grantee} has no access to GA4 property {property_id}.",
141
+ [f"At analytics.google.com > Admin > Property Access Management, add {grantee} with the Viewer role.",
142
+ "Wait ~1 minute for the grant to propagate, then ask me to retry (no restart needed)."],
143
+ "iam", topic="iam",
144
+ why="Authentication succeeded but this account is not authorized to read this property (Google returns 403).",
145
+ who="the user, or whoever administers this GA4 property if that is someone else",
146
+ handoff=handoff)
147
+ coordinator.SERVER_INIT_ERROR_CATEGORY = "IAMError"
148
+ elif ("Reauthentication is needed" in err_str or "invalid_grant" in err_str
149
+ or "expired or revoked" in err_str):
150
+ worked_before = telemetry.HAS_EVER_WORKED
151
+ lead = ("This server was working before — the Google credentials have now expired."
152
+ if worked_before else "The Google credentials are expired or revoked.")
153
+ coordinator.SERVER_INIT_ERROR = _guided(
154
+ lead,
155
+ ["Re-authenticate: run 'gcloud auth application-default login' in a terminal (for ADC), "
156
+ "or replace the service-account key file if that is what this server uses.",
157
+ "Then ask me to retry — no config changes needed.",
158
+ "Tip: service-account keys do not expire; prefer one if this recurs."],
159
+ "adc",
160
+ why="The stored credentials are no longer valid, so authentication to GA4 fails.",
161
+ who="the user (re-auth is a local action only they can take)")
162
+ coordinator.SERVER_INIT_ERROR_CATEGORY = "ADCExpired"
163
+ else:
164
+ coordinator.SERVER_INIT_ERROR = _guided(
165
+ f"Could not fetch GA4 property schema: {err_str}.",
166
+ ["Check that GA4_PROPERTY_ID is the numeric ID of a property this service account can access.",
167
+ "Check the credentials file is a valid service-account JSON key.",
168
+ _config_hint()],
169
+ "setup")
170
+ config_status = "error"
171
+
172
+ # 3. Register tools (importing the modules registers their @mcp.tool functions)
173
+ metadata.PROPERTY_SCHEMA = PROPERTY_SCHEMA
174
+ reporting.PROPERTY_SCHEMA = PROPERTY_SCHEMA
175
+
176
+ # 4. Run the server
177
+ from .coordinator import send_telemetry
178
+ import time as _time
179
+ start_payload = {
180
+ "config_status": config_status,
181
+ "shell": os.path.basename(os.getenv("SHELL", "") or "unknown"),
182
+ "term_program": os.getenv("TERM_PROGRAM", "unknown"),
183
+ "system_lang": os.getenv("LANG", "unknown"),
184
+ "is_ci": os.getenv("CI", "false").lower() == "true" or os.getenv("GITHUB_ACTIONS", "false").lower() == "true",
185
+ # Env var NAMES only, never values — makes harnesses we didn't
186
+ # anticipate visible instead of falling to generic_agent.
187
+ "env_var_names": sorted(os.environ.keys()),
188
+ }
189
+ if coordinator.SERVER_INIT_ERROR:
190
+ start_payload["error_message"] = str(coordinator.SERVER_INIT_ERROR)
191
+ start_payload["error_category"] = coordinator.SERVER_INIT_ERROR_CATEGORY
192
+ send_telemetry("mcp_started", start_payload)
193
+ mcp.run(transport="stdio")
194
+
195
+ if __name__ == "__main__":
196
+ main()
@@ -6,14 +6,44 @@ session, avoiding a client restart. Falls back to guided text if the client
6
6
  doesn't support elicitation."""
7
7
 
8
8
  import os
9
+ import uuid
9
10
 
10
11
  from pydantic import BaseModel, Field
11
12
  from mcp.server.fastmcp import Context
12
13
 
13
14
  from .coordinator import mcp
14
- from .telemetry import send_telemetry
15
+ from .telemetry import send_telemetry, client_supports_url_elicitation
15
16
  from . import coordinator
16
17
 
18
+ # Pages we own or that land the user exactly where the fix happens. URL-mode
19
+ # elicitation opens these at the client; no OAuth, no secrets — guided navigation.
20
+ _GA4_ADMIN_URL = "https://analytics.google.com/analytics/web/#/admin"
21
+ _SETUP_PAGE_URL = "https://ga4.builditwithai.xyz/setup"
22
+
23
+
24
+ async def _offer_page(ctx, message, url, branch):
25
+ """URL-mode elicitation: ask the client to open a helpful page before we
26
+ collect the value. No-op on clients without URL elicitation (falls through
27
+ to the existing form/text). Records whether the offer was accepted."""
28
+ if not client_supports_url_elicitation():
29
+ return None
30
+ try:
31
+ r = await ctx.request_context.session.elicit_url(
32
+ message=message,
33
+ url=url,
34
+ elicitation_id=f"setup_{uuid.uuid4().hex[:12]}",
35
+ related_request_id=ctx.request_id,
36
+ )
37
+ action = str(getattr(r, "action", None))
38
+ send_telemetry("setup_flow", {
39
+ "flow_branch": branch, "flow_outcome": "url_offered",
40
+ "url_elicit_action": action,
41
+ "error_category_at_entry": coordinator.SERVER_INIT_ERROR_CATEGORY,
42
+ })
43
+ return action
44
+ except Exception:
45
+ return None
46
+
17
47
 
18
48
  class _PropertyId(BaseModel):
19
49
  property_id: str = Field(description="Your numeric GA4 Property ID, e.g. 123456789")
@@ -62,9 +92,15 @@ async def setup_ga4_access(ctx: Context) -> str:
62
92
  branch = "other"
63
93
 
64
94
  try:
65
- # Missing Property ID — collect it.
95
+ # Missing Property ID — offer to open GA4 Admin, then collect it.
66
96
  if not prop:
67
97
  branch = "property_id"
98
+ await _offer_page(
99
+ ctx,
100
+ "I'll open Google Analytics Admin so you can copy your Property ID "
101
+ "(Admin > Property details, a numeric id like 123456789).",
102
+ _GA4_ADMIN_URL, "property_id_open_admin",
103
+ )
68
104
  r = await ctx.elicit(
69
105
  "What is your GA4 Property ID? Find it at analytics.google.com > Admin > "
70
106
  "Property details (a numeric id like 123456789).",
@@ -75,9 +111,15 @@ async def setup_ga4_access(ctx: Context) -> str:
75
111
  return "Setup paused — no Property ID provided. Re-run setup_ga4_access when ready."
76
112
  os.environ["GA4_PROPERTY_ID"] = r.data.property_id.strip()
77
113
 
78
- # Missing/invalid credentials path — collect it.
114
+ # Missing/invalid credentials path — offer the setup page, then collect.
79
115
  elif not creds or (creds and not os.path.exists(creds)):
80
116
  branch = "credentials"
117
+ await _offer_page(
118
+ ctx,
119
+ "I'll open the setup guide — it walks through creating a service-account "
120
+ "key or using gcloud, with the exact steps.",
121
+ _SETUP_PAGE_URL, "credentials_open_setup",
122
+ )
81
123
  r = await ctx.elicit(
82
124
  "Where is your Google service-account JSON key on this machine? "
83
125
  "Paste its absolute path. (Or, to use gcloud instead, run "
@@ -89,6 +131,19 @@ async def setup_ga4_access(ctx: Context) -> str:
89
131
  return "Setup paused — no credentials path provided. Re-run setup_ga4_access when ready."
90
132
  path = r.data.credentials_path.strip()
91
133
  if path.lower() != "adc":
134
+ # Validate at collection: existence alone isn't enough — a
135
+ # present-but-malformed/wrong-type file passes os.path.exists but
136
+ # fails later. Tell the model precisely which so it can re-ask.
137
+ model, _email, ok = coordinator.inspect_credentials(path)
138
+ if model == "missing":
139
+ _emit_flow(branch, r.action, "invalid_path")
140
+ return (f"No file exists at '{path}'. Ask the user for the correct absolute path to their "
141
+ "Google service-account JSON key, then re-run setup_ga4_access.")
142
+ if not ok or model in ("unreadable", "unknown"):
143
+ _emit_flow(branch, r.action, "invalid_creds")
144
+ return (f"The file at '{path}' is not a valid Google credentials JSON (expected a "
145
+ "service-account key or an authorized_user/ADC file). Ask the user to re-download "
146
+ "the service-account key from Google Cloud Console > IAM > Service Accounts > Keys.")
92
147
  os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = path
93
148
 
94
149
  # Expired credentials (ADC) — confirm the terminal fix.
@@ -72,6 +72,30 @@ def _init_anonymous_identity():
72
72
  INSTALLATION_ID, IS_FIRST_INSTALL = _init_anonymous_identity()
73
73
  SESSION_ID = f"sess_{uuid.uuid4()}" # one per process
74
74
 
75
+
76
+ def _has_ever_worked() -> bool:
77
+ """True if this install successfully initialized in a PRIOR session. Lets a
78
+ query tell a first-time setup failure (never worked) from a returning-user
79
+ credential decay (worked before, broke since). Bool only, non-PII."""
80
+ try:
81
+ return (Path.home() / ".ga4_mcp" / "ever_worked").exists()
82
+ except Exception:
83
+ return False
84
+
85
+
86
+ HAS_EVER_WORKED = _has_ever_worked()
87
+
88
+
89
+ def mark_ever_worked():
90
+ """Write the 'has successfully worked' marker once, on first successful init.
91
+ Additive to the frozen identity contract — a separate flag file, not the id."""
92
+ try:
93
+ f = Path.home() / ".ga4_mcp" / "ever_worked"
94
+ if not f.exists():
95
+ f.write_text("1", encoding="utf-8")
96
+ except Exception:
97
+ pass
98
+
75
99
  IN_VIRTUAL_ENV = sys.prefix != sys.base_prefix
76
100
  CPU_ARCH = platform.machine()
77
101
  TIMEZONE_OFFSET = -time.timezone if (time.localtime().tm_isdst == 0) else -time.altzone
@@ -333,6 +357,17 @@ def capture_client_info(mcp_server):
333
357
  pass
334
358
 
335
359
 
360
+ def client_supports_url_elicitation() -> bool:
361
+ """True if the handshake advertised URL-mode elicitation (elicitation.url).
362
+ Read from the raw capabilities we capture; used to offer guided-navigation
363
+ recovery only to clients that can open a URL."""
364
+ caps = _RUNTIME_CLIENT.get("caps_raw")
365
+ if not isinstance(caps, dict):
366
+ return False
367
+ elicit = caps.get("elicitation")
368
+ return isinstance(elicit, dict) and "url" in elicit
369
+
370
+
336
371
  # In-flight sender threads, drained briefly at exit — short-lived sessions
337
372
  # (a large share of real boots) otherwise lose their events to process death.
338
373
  _PENDING_SENDS = []
@@ -374,6 +409,7 @@ def send_telemetry(event: str, properties: dict = None):
374
409
  "run_context": RUN_CONTEXT,
375
410
  "discovery_channel": DISCOVERY_CHANNEL,
376
411
  "launch_channel": DISCOVERY_CHANNEL,
412
+ "has_ever_worked": HAS_EVER_WORKED,
377
413
  "raw_env": ENV_SIGNALS, # the raw clues behind run_context/agent_name
378
414
  "session_id": SESSION_ID,
379
415
  **(properties or {}),
@@ -172,9 +172,11 @@ def get_ga4_data(
172
172
  warning and execute the query anyway.
173
173
  enable_aggregation: (Optional) If True, uses server-side aggregation when
174
174
  beneficial. Defaults to True.
175
- intent: (Optional) The category of question this query answers. Pick ONE from:
176
- traffic_overview | acquisition | content_performance | ecommerce_revenue |
177
- user_behavior | geography_devices | campaign_analysis | seo | debugging | other
175
+ intent: (Optional) A short, plain-language description of what the user is
176
+ trying to learn from this query in your own words, not a fixed
177
+ category. Examples: "weekly retention trend for release planning",
178
+ "check for bot traffic from non-Japan countries",
179
+ "which signup flow converts best".
178
180
 
179
181
  COMMON MISTAKES (checked against real usage — read before guessing names):
180
182
  - Metric is 'keyEvents', not 'conversions'. 'totalUsers', not 'users'.
@@ -0,0 +1,73 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+
3
+ """In-server troubleshooting guides. Content lives with the code (bundled at
4
+ release), NOT fetched from the web — instant, offline-safe, and versioned with
5
+ the package. Written for the AI agent to act on and relay to the user."""
6
+
7
+ from ga4_mcp.coordinator import mcp
8
+
9
+ _SETUP_GUIDE = """--- SETUP GUIDE ---
10
+
11
+ The GA4 MCP server needs two things to work: a Property ID and Google credentials.
12
+
13
+ 1. GA4_PROPERTY_ID — the numeric Property ID (e.g. 123456789), NOT the
14
+ Measurement ID that starts with 'G-'. Find it at analytics.google.com >
15
+ Admin > Property details.
16
+ 2. GOOGLE_APPLICATION_CREDENTIALS — the absolute path to a Google Cloud
17
+ service-account JSON key, OR run 'gcloud auth application-default login'
18
+ and use that credentials file.
19
+
20
+ Set both in the env block of this server in the MCP client config, e.g.:
21
+ "env": {
22
+ "GOOGLE_APPLICATION_CREDENTIALS": "/absolute/path/to/key.json",
23
+ "GA4_PROPERTY_ID": "123456789"
24
+ }
25
+
26
+ FASTEST PATH: call setup_ga4_access — it collects the missing value
27
+ interactively and reconnects without a restart."""
28
+
29
+ _IAM_GUIDE = """--- IAM / 403 PERMISSION GUIDE ---
30
+
31
+ A 403 / PermissionDenied means the credentials are valid but the service
32
+ account has no access to the GA4 property. Grant it:
33
+
34
+ 1. Open analytics.google.com > Admin > Property Access Management.
35
+ 2. Click '+' > Add users.
36
+ 3. Enter the service account email — it is the 'client_email' field inside
37
+ the JSON key file.
38
+ 4. Assign the 'Viewer' role and save.
39
+ 5. Wait a minute for permissions to propagate, then retry.
40
+
41
+ If credentials instead need re-authentication (a 'Reauthentication is needed'
42
+ or invalid_grant / expired error), the user's Application Default Credentials
43
+ have expired — have them run: gcloud auth application-default login"""
44
+
45
+ _SCHEMA_GUIDE = """--- SCHEMA GUIDE ---
46
+
47
+ Do not guess dimension/metric names — use search_schema first. Common fixes
48
+ (verified against real usage):
49
+ - Metric is 'keyEvents', not 'conversions'. 'totalUsers', not 'users'.
50
+ 'itemsViewed', not 'itemViews'. 'ecommercePurchases', not 'purchases'.
51
+ - Dimension is 'sessionDefaultChannelGroup', not 'sessionDefaultChannelGrouping'.
52
+ - A simple dimension_filter MUST nest inside a "filter" key:
53
+ {"filter": {"fieldName": ..., "stringFilter": {...}}} — not fieldName at top level.
54
+ - Logical groups are "andGroup"/"orGroup"/"notExpression" — not and_filter/or_filter."""
55
+
56
+ _GUIDES = {"setup": _SETUP_GUIDE, "iam": _IAM_GUIDE, "schema": _SCHEMA_GUIDE}
57
+
58
+
59
+ @mcp.tool()
60
+ def get_troubleshooting_guide(topic: str) -> str:
61
+ """
62
+ Returns the troubleshooting/setup guide for a topic, served from inside the
63
+ server (no network needed). Use whenever you hit a schema error,
64
+ dimension_filter parse error, IAM / 403 authorization error, or a
65
+ boot-time setup error.
66
+
67
+ Args:
68
+ topic: One of "setup", "iam", or "schema".
69
+ """
70
+ guide = _GUIDES.get((topic or "").strip().lower())
71
+ if guide is None:
72
+ return "Invalid topic. Choose 'setup', 'iam', or 'schema'."
73
+ return guide
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: google-analytics-mcp
3
- Version: 2.7.2
3
+ Version: 2.7.4
4
4
  Summary: Model Context Protocol for Google Analytics 4 (Data API) allowing autonomous agents to query dimensions and metrics. Gives agents analysis-ready GA4 access with schema discovery, server-side aggregation, and smart defaults.
5
5
  Author-email: Surendran B <reachsuren@gmail.com>
6
6
  License-Expression: Apache-2.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "google-analytics-mcp"
7
- version = "2.7.2"
7
+ version = "2.7.4"
8
8
  description = "Model Context Protocol for Google Analytics 4 (Data API) allowing autonomous agents to query dimensions and metrics. Gives agents analysis-ready GA4 access with schema discovery, server-side aggregation, and smart defaults."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -1,153 +0,0 @@
1
- # SPDX-License-Identifier: Apache-2.0
2
-
3
- import os
4
- import sys
5
- from .coordinator import mcp
6
- from .tools import metadata, reporting
7
- from . import resources
8
-
9
- # Property schema, cached at startup.
10
- PROPERTY_SCHEMA = None
11
-
12
- def main():
13
- """
14
- Main entry point for the MCP server.
15
-
16
- This function performs the following steps:
17
- 1. Validates required environment variables.
18
- 2. Fetches and caches the GA4 property schema (dimensions and metrics).
19
- 3. Registers the tools with the MCP server.
20
- 4. Starts the server and listens for requests.
21
- """
22
- print("Starting GA4 MCP server...", file=sys.stderr)
23
- import ga4_mcp.coordinator as coordinator
24
- import ga4_mcp.tools.troubleshooting # Register the OTA tool
25
- import ga4_mcp.setup_flow # Register the interactive setup-recovery tool
26
- config_status = "valid"
27
-
28
- # 1. Validate environment variables
29
- credentials_path = os.getenv("GOOGLE_APPLICATION_CREDENTIALS")
30
- property_id = os.getenv("GA4_PROPERTY_ID")
31
-
32
- setup_url = "https://ga4.builditwithai.xyz/setup"
33
-
34
- def _config_hint():
35
- # Name the config surface for the detected client.
36
- agent = getattr(coordinator, "AGENT_NAME", "")
37
- if agent == "claude_code":
38
- return ("In Claude Code run: claude mcp add ga4-analytics -e GA4_PROPERTY_ID=<id> "
39
- "-e GOOGLE_APPLICATION_CREDENTIALS=<key-path> -- uvx --from google-analytics-mcp ga4-mcp-server")
40
- if agent == "claude_desktop":
41
- return ("In Claude Desktop: Settings > Developer > Edit Config, set these env values under "
42
- "mcpServers > ga4-analytics in claude_desktop_config.json")
43
- if agent == "cursor":
44
- return "In Cursor: Settings > MCP (edit .cursor/mcp.json), set these env values for ga4-analytics"
45
- if agent in ("vscode", "windsurf"):
46
- return "Edit this editor's MCP settings JSON and set these env values for ga4-analytics"
47
- return "Set these env values in your MCP client's server config for ga4-analytics"
48
-
49
- def _guided(message, steps, anchor, topic="setup"):
50
- step_text = " ".join(f"({i}) {s}" for i, s in enumerate(steps, 1))
51
- return (f"{message} FASTEST FIX — call the setup_ga4_access tool now: it walks the user through "
52
- f"fixing this interactively and reconnects without a restart. "
53
- f"Or relay these steps manually: {step_text} "
54
- f"Full guide: {setup_url}#{anchor}. "
55
- f"For deeper reference call get_troubleshooting_guide(topic='{topic}').")
56
-
57
- if not credentials_path:
58
- print("ERROR: GOOGLE_APPLICATION_CREDENTIALS environment variable not set.", file=sys.stderr)
59
- coordinator.SERVER_INIT_ERROR = _guided(
60
- "GOOGLE_APPLICATION_CREDENTIALS environment variable not set.",
61
- ["Create a Google Cloud service account JSON key (Cloud Console > IAM > Service Accounts > Keys), "
62
- "or run 'gcloud auth application-default login' and use that credentials file.",
63
- _config_hint()],
64
- "credentials")
65
- config_status = "error"
66
- elif not property_id:
67
- print("ERROR: GA4_PROPERTY_ID environment variable not set.", file=sys.stderr)
68
- coordinator.SERVER_INIT_ERROR = _guided(
69
- "GA4_PROPERTY_ID environment variable not set.",
70
- ["Open analytics.google.com > Admin > Property details and copy the numeric Property ID (e.g. 123456789).",
71
- _config_hint()],
72
- "property-id")
73
- config_status = "error"
74
- elif "ABSOLUTE/PATH/TO" in credentials_path:
75
- print(f"ERROR: Dummy credentials path detected: '{credentials_path}'.", file=sys.stderr)
76
- coordinator.SERVER_INIT_ERROR = _guided(
77
- "Setup failed because the dummy path is still in the config.",
78
- ["Replace the /ABSOLUTE/PATH/TO placeholder with the real absolute path of the downloaded "
79
- "service-account JSON key.",
80
- _config_hint()],
81
- "credentials")
82
- config_status = "error"
83
- elif not os.path.exists(credentials_path):
84
- print(f"ERROR: Credentials file not found at '{credentials_path}'.", file=sys.stderr)
85
- coordinator.SERVER_INIT_ERROR = _guided(
86
- f"Credentials file not found at '{credentials_path}'.",
87
- ["Verify the file exists at that exact absolute path (check the filename and folder).",
88
- "If it is missing, re-download the service account JSON key from Google Cloud Console > IAM > "
89
- "Service Accounts > Keys.",
90
- _config_hint()],
91
- "credentials")
92
- config_status = "error"
93
- else:
94
- # 2. Fetch and cache the GA4 property schema
95
- print(f"Fetching schema for property '{property_id}'...", file=sys.stderr)
96
- global PROPERTY_SCHEMA
97
- try:
98
- PROPERTY_SCHEMA = metadata.get_property_schema_uncached(property_id)
99
- print("Schema loaded successfully.", file=sys.stderr)
100
- except Exception as e:
101
- print(f"FATAL: Could not fetch GA4 property schema: {e}", file=sys.stderr)
102
- err_str = str(e)
103
- if "403" in err_str or "PermissionDenied" in err_str or "permission" in err_str.lower():
104
- coordinator.SERVER_INIT_ERROR = _guided(
105
- "IAM Error: The service account does not have Viewer access to the GA4 property.",
106
- ["Open analytics.google.com > Admin > Property Access Management.",
107
- "Add the service account email (the client_email field inside the JSON key) with the "
108
- "Viewer role.",
109
- "Wait a minute for permissions to propagate, then restart the MCP client."],
110
- "iam", topic="iam")
111
- coordinator.SERVER_INIT_ERROR_CATEGORY = "IAMError"
112
- elif ("Reauthentication is needed" in err_str or "invalid_grant" in err_str
113
- or "expired or revoked" in err_str):
114
- coordinator.SERVER_INIT_ERROR = _guided(
115
- f"Google credentials have expired: {err_str}.",
116
- ["Ask the user to run in a terminal: gcloud auth application-default login",
117
- "Then restart the MCP client — no config changes needed."],
118
- "adc")
119
- coordinator.SERVER_INIT_ERROR_CATEGORY = "ADCExpired"
120
- else:
121
- coordinator.SERVER_INIT_ERROR = _guided(
122
- f"Could not fetch GA4 property schema: {err_str}.",
123
- ["Check that GA4_PROPERTY_ID is the numeric ID of a property this service account can access.",
124
- "Check the credentials file is a valid service-account JSON key.",
125
- _config_hint()],
126
- "setup")
127
- config_status = "error"
128
-
129
- # 3. Register tools (importing the modules registers their @mcp.tool functions)
130
- metadata.PROPERTY_SCHEMA = PROPERTY_SCHEMA
131
- reporting.PROPERTY_SCHEMA = PROPERTY_SCHEMA
132
-
133
- # 4. Run the server
134
- from .coordinator import send_telemetry
135
- import time as _time
136
- start_payload = {
137
- "config_status": config_status,
138
- "shell": os.path.basename(os.getenv("SHELL", "") or "unknown"),
139
- "term_program": os.getenv("TERM_PROGRAM", "unknown"),
140
- "system_lang": os.getenv("LANG", "unknown"),
141
- "is_ci": os.getenv("CI", "false").lower() == "true" or os.getenv("GITHUB_ACTIONS", "false").lower() == "true",
142
- # Env var NAMES only, never values — makes harnesses we didn't
143
- # anticipate visible instead of falling to generic_agent.
144
- "env_var_names": sorted(os.environ.keys()),
145
- }
146
- if coordinator.SERVER_INIT_ERROR:
147
- start_payload["error_message"] = str(coordinator.SERVER_INIT_ERROR)
148
- start_payload["error_category"] = coordinator.SERVER_INIT_ERROR_CATEGORY
149
- send_telemetry("mcp_started", start_payload)
150
- mcp.run(transport="stdio")
151
-
152
- if __name__ == "__main__":
153
- main()
@@ -1,28 +0,0 @@
1
- import urllib.request
2
- import urllib.error
3
- from ga4_mcp.coordinator import mcp
4
-
5
- # Fetches troubleshooting guides directly from the documentation site.
6
- OTA_BASE_URL = "https://ga4mcp.com"
7
-
8
- @mcp.tool()
9
- def get_troubleshooting_guide(topic: str) -> str:
10
- """
11
- Fetches the latest troubleshooting and setup guides Over-The-Air (OTA) from ga4mcp.com.
12
- Use this tool whenever you encounter a schema error, dimension_filter parse error,
13
- IAM / 403 authorization error, or boot-time setup error.
14
-
15
- Args:
16
- topic: The topic of the guide to fetch. Valid options are "setup", "iam", or "schema".
17
- """
18
- if topic not in ["setup", "iam", "schema"]:
19
- return "Invalid topic. Please choose 'setup', 'iam', or 'schema'."
20
-
21
- url = f"{OTA_BASE_URL}/{topic}.md"
22
- try:
23
- with urllib.request.urlopen(url, timeout=10) as response:
24
- content = response.read().decode('utf-8')
25
- return f"--- {topic.upper()} GUIDE ---\n\n{content}"
26
- except urllib.error.URLError as e:
27
- # Fallback if network is completely unreachable (rare, since GA4 needs network anyway)
28
- return f"Warning: Could not fetch OTA guide for '{topic}' due to network error: {e}. Please use your best judgment based on generic GA4 API documentation."