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.
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/PKG-INFO +1 -1
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/coordinator.py +33 -2
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/resources.py +19 -0
- google_analytics_mcp-2.7.4/ga4_mcp/server.py +196 -0
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/setup_flow.py +58 -3
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/telemetry.py +36 -0
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/tools/reporting.py +5 -3
- google_analytics_mcp-2.7.4/ga4_mcp/tools/troubleshooting.py +73 -0
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/google_analytics_mcp.egg-info/PKG-INFO +1 -1
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/pyproject.toml +1 -1
- google_analytics_mcp-2.7.2/ga4_mcp/server.py +0 -153
- google_analytics_mcp-2.7.2/ga4_mcp/tools/troubleshooting.py +0 -28
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/LICENSE +0 -0
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/README.md +0 -0
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/__init__.py +0 -0
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/__main__.py +0 -0
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/ga4_mcp/tools/metadata.py +0 -0
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/google_analytics_mcp.egg-info/SOURCES.txt +0 -0
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/google_analytics_mcp.egg-info/dependency_links.txt +0 -0
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/google_analytics_mcp.egg-info/entry_points.txt +0 -0
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/google_analytics_mcp.egg-info/requires.txt +0 -0
- {google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/google_analytics_mcp.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
-
|
|
130
|
-
|
|
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
|
|
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)
|
|
176
|
-
|
|
177
|
-
|
|
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
|
{google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/google_analytics_mcp.egg-info/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: google-analytics-mcp
|
|
3
|
-
Version: 2.7.
|
|
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.
|
|
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."
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/google_analytics_mcp.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{google_analytics_mcp-2.7.2 → google_analytics_mcp-2.7.4}/google_analytics_mcp.egg-info/requires.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|