google-analytics-mcp 2.7.2__tar.gz → 2.7.3__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 (22) hide show
  1. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/PKG-INFO +1 -1
  2. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/ga4_mcp/coordinator.py +15 -2
  3. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/ga4_mcp/resources.py +19 -0
  4. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/ga4_mcp/server.py +3 -1
  5. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/ga4_mcp/setup_flow.py +45 -3
  6. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/ga4_mcp/telemetry.py +36 -0
  7. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/ga4_mcp/tools/reporting.py +5 -3
  8. google_analytics_mcp-2.7.3/ga4_mcp/tools/troubleshooting.py +73 -0
  9. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/google_analytics_mcp.egg-info/PKG-INFO +1 -1
  10. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/pyproject.toml +1 -1
  11. google_analytics_mcp-2.7.2/ga4_mcp/tools/troubleshooting.py +0 -28
  12. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/LICENSE +0 -0
  13. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/README.md +0 -0
  14. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/ga4_mcp/__init__.py +0 -0
  15. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/ga4_mcp/__main__.py +0 -0
  16. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/ga4_mcp/tools/metadata.py +0 -0
  17. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/google_analytics_mcp.egg-info/SOURCES.txt +0 -0
  18. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/google_analytics_mcp.egg-info/dependency_links.txt +0 -0
  19. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/google_analytics_mcp.egg-info/entry_points.txt +0 -0
  20. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/google_analytics_mcp.egg-info/requires.txt +0 -0
  21. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/google_analytics_mcp.egg-info/top_level.txt +0 -0
  22. {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.3}/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.3
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
@@ -126,8 +126,20 @@ def _telemetry_tool(*args, **kwargs):
126
126
  is_async = inspect.iscoroutinefunction(func)
127
127
 
128
128
  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)
129
+ if not SERVER_INIT_ERROR or name in _INIT_ERROR_EXEMPT:
130
+ return None
131
+ # Agent-directed recovery playbook, not a description. The old wording
132
+ # ("Configuration Error: ...") read as retryable, so agents re-called
133
+ # the data tool in a loop. Lead with STOP + category, then the fix.
134
+ lead = {
135
+ "ADCExpired": "The user's Google credentials have EXPIRED (a returning-user auth issue, not a query problem).",
136
+ "IAMError": "The service account lacks access to this GA4 property (a permissions grant is missing).",
137
+ "InitError": "The GA4 MCP server is not configured yet (missing Property ID or credentials).",
138
+ }.get(SERVER_INIT_ERROR_CATEGORY,
139
+ "The GA4 MCP server setup is incomplete.")
140
+ return (f"STOP — do not retry this tool; it will fail identically until setup is fixed. {lead} "
141
+ f"NEXT ACTION: call setup_ga4_access now — it fixes this interactively and reconnects "
142
+ f"without a restart. If that is unavailable, relay this to the user. Details: {SERVER_INIT_ERROR}")
131
143
 
132
144
  if is_async:
133
145
  @functools.wraps(func)
@@ -243,6 +255,7 @@ def reinitialize():
243
255
  metadata.PROPERTY_SCHEMA = schema
244
256
  reporting.PROPERTY_SCHEMA = schema
245
257
  SERVER_INIT_ERROR = None
258
+ telemetry.mark_ever_worked()
246
259
  return True, "ok", "initialized"
247
260
  except Exception as e:
248
261
  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)
@@ -21,7 +21,8 @@ def main():
21
21
  """
22
22
  print("Starting GA4 MCP server...", file=sys.stderr)
23
23
  import ga4_mcp.coordinator as coordinator
24
- import ga4_mcp.tools.troubleshooting # Register the OTA tool
24
+ from ga4_mcp import telemetry
25
+ import ga4_mcp.tools.troubleshooting # Register the troubleshooting tool
25
26
  import ga4_mcp.setup_flow # Register the interactive setup-recovery tool
26
27
  config_status = "valid"
27
28
 
@@ -97,6 +98,7 @@ def main():
97
98
  try:
98
99
  PROPERTY_SCHEMA = metadata.get_property_schema_uncached(property_id)
99
100
  print("Schema loaded successfully.", file=sys.stderr)
101
+ telemetry.mark_ever_worked()
100
102
  except Exception as e:
101
103
  print(f"FATAL: Could not fetch GA4 property schema: {e}", file=sys.stderr)
102
104
  err_str = str(e)
@@ -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 "
@@ -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.3
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.3"
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,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."