# Directory Structure ``` ├── .dxtignore ├── .gitignore ├── env.example ├── favicon.png ├── LICENSE ├── manifest.json ├── oauth │ ├── __init__.py │ └── google_auth.py ├── README.md ├── requirements.txt └── server.py ``` # Files -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` # Environment .env # Python __pycache__/ *.pyc .venv/ lib/ ``` -------------------------------------------------------------------------------- /.dxtignore: -------------------------------------------------------------------------------- ``` Dockerfile LICENSE README_OAUTH.md README.md .gitignore env.example ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown # Google Analytics MCP Server 📊 [](https://opensource.org/licenses/MIT) [](https://www.python.org/downloads/) [](https://github.com/jlowin/fastmcp) **A FastMCP-powered Model Context Protocol server for Google Analytics 4 API integration with automatic OAuth 2.0 authentication** Connect Google Analytics 4 data directly to Claude Desktop and other MCP clients with seamless OAuth 2.0 authentication, automatic token refresh, comprehensive reporting, and analytics capabilities. ## 🌟 Open Source & Community ### GoMarble AI Open Source Projects Check out our other open source contributions at [GoMarble AI GitHub](https://github.com/gomarble-ai): - **Analytics Tools** - Advanced analytics and reporting solutions - **AI Integration** - Tools for integrating AI with marketing platforms - **MCP Servers** - Additional Model Context Protocol implementations - **Marketing Automation** - Open source marketing automation tools ### Join Our Community Connect with other developers and marketers using AI in advertising: **[Join our Slack Community - AI in Ads](https://join.slack.com/t/ai-in-ads/shared_invite/zt-36hntbyf8-FSFixmwLb9mtEzVZhsToJQ)** - 💬 **Discuss** AI applications in advertising - 🤝 **Share** your projects and get feedback - 📚 **Learn** from industry experts - 🚀 **Collaborate** on open source projects - 🔧 **Get help** with technical implementation ### 🚀 Try Our One-Click Integration Skip the manual setup and get started instantly: **[One-Click MCP Integration](https://gomarble.ai/mcp)** - Connect Google Analytics and other tools to Claude Desktop in seconds - ⚡ **Instant Setup** - No manual configuration required - 🔐 **Secure Authentication** - Built-in OAuth handling - 📊 **Multiple Integrations** - Google Analytics, Google Ads, Meta Ads, and more - 📖 **Documentation** - Complete integration guide at **[gomarble.ai/docs](https://gomarble.ai/docs)** ## ✨ Features - 🔐 **Automatic OAuth 2.0** - One-time browser authentication with auto-refresh - 🔄 **Smart Token Management** - Handles expired tokens automatically - 📊 **Comprehensive Reporting** - Access all GA4 metrics and dimensions - 🏢 **Property Management** - List and manage Google Analytics properties - 📈 **Advanced Analytics** - Page views, users, events, traffic sources, and more - 🚀 **FastMCP Framework** - Built on the modern MCP standard - 🖥️ **Claude Desktop Ready** - Direct integration with Claude Desktop - 🛡️ **Secure Local Storage** - Tokens stored locally, never exposed ## 📋 Available Tools | Tool | Description | Parameters | Example Usage | |------|-------------|------------|---------------| | `list_properties` | List all GA4 accounts and properties | `account_id` (optional) | "List all my Google Analytics properties" | | `get_page_views` | Get page view metrics | `property_id`, `start_date`, `end_date`, `dimensions` (optional) | "Show me page views for last month" | | `get_active_users` | Get active users metrics | `property_id`, `start_date`, `end_date`, `dimensions` (optional) | "Get active users by day for last week" | | `get_events` | Get event metrics | `property_id`, `start_date`, `end_date`, `dimensions` (optional) | "Show me events data for property 123456789" | | `get_traffic_sources` | Get traffic source data | `property_id`, `start_date`, `end_date`, `dimensions` (optional) | "Analyze traffic sources for last 30 days" | | `get_device_metrics` | Get device-based metrics | `property_id`, `start_date`, `end_date`, `dimensions` (optional) | "Show device breakdown for last month" | | `run_report` | Comprehensive custom reporting | `property_id`, `start_date`, `end_date`, `metrics`, `dimensions`, filters, etc. | "Create custom report with sessions and conversions by country" | **Note:** All tools automatically handle authentication - no token parameters required! ## 🚀 Quick Start ### Prerequisites Before setting up the MCP server, you'll need: - Python 3.10+ installed - A Google Cloud Platform account - A Google Analytics 4 property with data access ## 🔧 Step 1: Google Cloud Platform Setup ### 1.1 Create Google Cloud Project 1. **Go to [Google Cloud Console](https://console.cloud.google.com/)** 2. **Create a new project:** - Click "Select a project" → "New Project" - Enter project name (e.g., "Google Analytics MCP") - Click "Create" ### 1.2 Enable Google Analytics APIs 1. **In your Google Cloud Console:** - Go to "APIs & Services" → "Library" - Search for "Google Analytics Data API" and enable it ### 1.3 Create OAuth 2.0 Credentials 1. **Go to "APIs & Services" → "Credentials"** 2. **Click "+ CREATE CREDENTIALS" → "OAuth 2.0 Client ID"** 3. **Configure consent screen (if first time):** - Click "Configure Consent Screen" - Choose "External" (unless you have Google Workspace) - Fill required fields: - App name: "Google Analytics MCP" - User support email: Your email - Developer contact: Your email - Add scopes: - `https://www.googleapis.com/auth/analytics` - `https://www.googleapis.com/auth/analytics.readonly` - Click "Save and Continue" through all steps 4. **Create OAuth Client:** - Application type: **"Desktop application"** - Name: "Google Analytics MCP Client" - Click "Create" 5. **Download credentials:** - Click "Download JSON" button - Save file as `client_secret_[long-string].json` in your project directory ## 🔧 Step 2: Google Analytics Access ### 2.1 Ensure Analytics Access 1. **Sign in to [Google Analytics](https://analytics.google.com)** 2. **Verify you have access to GA4 properties** 3. **Note your property IDs** (found in GA4 Admin → Property Settings) 4. **Ensure your Google account has at least Viewer access** to the properties you want to query ## 🔧 Step 3: Installation & Setup ### 3.1 Clone and Install ```bash # Clone the repository git clone https://github.com/yourusername/google-analytics-mcp-server.git cd google-analytics-mcp-server # Create virtual environment (recommended) python3 -m venv .venv source .venv/bin/activate # On Windows: .venv\Scripts\activate # Install dependencies pip install -r requirements.txt ``` ### 3.2 Environment Configuration Create a `.env` file in your project directory: ```bash # Copy the example file cp .env.example .env ``` Edit `.env` with your credentials: ```bash # Required: Path to OAuth credentials JSON file (downloaded from Google Cloud) GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH=/full/path/to/your/client_secret_file.json ``` **Example `.env` file:** ```bash GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH=/Users/john/google-analytics-mcp/client_secret_138737274875-abc123.apps.googleusercontent.com.json ``` ## 🖥️ Step 4: Claude Desktop Integration ### 4.1 Locate Claude Configuration Find your Claude Desktop configuration file: **macOS:** ```bash ~/Library/Application Support/Claude/claude_desktop_config.json ``` **Windows:** ```bash %APPDATA%\Claude\claude_desktop_config.json ``` ### 4.2 Add MCP Server Configuration Edit the configuration file and add your Google Analytics MCP server: ```json { "mcpServers": { "google-analytics": { "command": "/full/path/to/your/project/.venv/bin/python", "args": [ "/full/path/to/your/project/server.py" ] } } } ``` **Real Example:** ```json { "mcpServers": { "google-analytics": { "command": "/Users/marble-dev-01/workspace/google_analytics_mcp/.venv/bin/python", "args": [ "/Users/marble-dev-01/workspace/google_analytics_mcp/server.py" ] } } } ``` **Important:** - Use **absolute paths** for all file locations - On Windows, use forward slashes `/` or double backslashes `\\` in paths ### 4.3 Restart Claude Desktop Close and restart Claude Desktop to load the new configuration. ## 🔐 Step 5: First-Time Authentication ### 5.1 Trigger OAuth Flow 1. **Open Claude Desktop** 2. **Try any Google Analytics command**, for example: ``` "List all my Google Analytics properties" ``` ### 5.2 Complete Authentication 1. **Browser opens automatically** to Google OAuth page 2. **Sign in** with your Google account (the one with Analytics access) 3. **Grant permissions** by clicking "Allow" 4. **Browser shows success page** 5. **Return to Claude** - your command will complete automatically! ### 5.3 Verify Setup After authentication, you should see: - A `google_analytics_token.json` file created in your project directory - Your Google Analytics properties listed in Claude's response ## 📖 Usage Examples ### Property Management ``` "List all my Google Analytics properties" "Show me properties for account 123456789" "What GA4 properties do I have access to?" ``` ### Page View Analysis ``` "Get page views for property 421301275 from 2025-01-01 to 2025-01-31" "Show me top pages by page views for last month for property 421301275" "Analyze page performance by country for property 421301275" ``` ### User Analytics ``` "Get active users for property 421301275 in the last 7 days" "Show me user metrics by device category for property 421301275" "Compare new vs returning users for last month" ``` ### Traffic Source Analysis ``` "Analyze traffic sources for property 421301275 from 2025-01-01 to 2025-01-31" "Show me which channels drive the most users to my site" "Compare organic vs paid traffic performance" ``` ### Event Tracking ``` "Get events data for property 421301275 in the last 30 days" "Show me conversion events by source/medium" "Which events are most popular on my site?" ``` ### Custom Reports ``` "Create a report for property 421301275 with sessions, users, and page views by country from 2025-01-01 to 2025-01-31" "Run a custom report showing bounce rate and engagement rate by device category" "Generate a comprehensive traffic report with sessions, conversions, and revenue by source/medium" ``` ## 🔍 Advanced GA4 Examples ### Sessions and Users by Country ```python run_report( property_id="421301275", start_date="2025-01-01", end_date="2025-01-31", metrics=["sessions", "totalUsers", "screenPageViews"], dimensions=["country"], limit=20 ) ``` ### Device Performance Analysis ```python run_report( property_id="421301275", start_date="2025-01-01", end_date="2025-01-31", metrics=["sessions", "bounceRate", "engagementRate"], dimensions=["deviceCategory", "operatingSystem"], limit=50 ) ``` ### Traffic Sources with Conversions ```python run_report( property_id="421301275", start_date="2025-01-01", end_date="2025-01-31", metrics=["sessions", "conversions", "totalRevenue"], dimensions=["source", "medium", "campaignName"], limit=100 ) ``` ### Daily Trend Analysis ```python run_report( property_id="421301275", start_date="2025-01-01", end_date="2025-01-31", metrics=["sessions", "activeUsers", "screenPageViews"], dimensions=["date"], limit=31 ) ``` ## 📁 Project Structure ``` google-analytics-mcp-server/ ├── server.py # Main MCP server ├── oauth/ │ ├── __init__.py # Package initialization │ └── google_auth.py # OAuth authentication logic ├── google_analytics_token.json # Auto-generated token storage (gitignored) ├── client_secret_[long-string].json # Your OAuth credentials (gitignored) ├── .env # Environment variables (gitignored) ├── .env.example # Environment template ├── .gitignore # Git ignore file ├── requirements.txt # Python dependencies ├── LICENSE # MIT License └── README.md # This file ``` ## 🔒 Security & Best Practices ### File Security - ✅ **Credential files are gitignored** - Never committed to version control - ✅ **Local token storage** - Tokens stored in `google_analytics_token.json` locally - ✅ **Environment variables** - Sensitive data in `.env` file - ✅ **Automatic refresh** - Minimal token exposure time ### Recommended File Permissions ```bash # Set secure permissions for sensitive files chmod 600 .env chmod 600 google_analytics_token.json chmod 600 client_secret_*.json ``` ### Production Considerations 1. **Use environment variables** instead of `.env` files in production 2. **Implement rate limiting** to respect API quotas 3. **Monitor API usage** in Google Cloud Console 4. **Secure token storage** with proper access controls 5. **Regular token rotation** for enhanced security ## 🛠️ Troubleshooting ### Authentication Issues | Issue | Symptoms | Solution | |-------|----------|----------| | **No tokens found** | "Starting OAuth flow" message | ✅ Normal for first-time setup - complete browser authentication | | **Token refresh failed** | "Refreshing token failed" error | ✅ Delete `google_analytics_token.json` and re-authenticate | | **OAuth flow failed** | Browser error or no response | Check credentials file path and internet connection | | **Permission denied** | "Access denied" in browser | Ensure Google account has Analytics access | ### Configuration Issues | Issue | Symptoms | Solution | |-------|----------|----------| | **Environment variables missing** | "Environment variable not set" | Check `.env` file and Claude config `env` section | | **File not found** | "FileNotFoundError" | Verify absolute paths in configuration | | **Module import errors** | "ModuleNotFoundError" | Run `pip install -r requirements.txt` | | **Python path issues** | "Command not found" | Use absolute path to Python executable | ### Claude Desktop Issues | Issue | Symptoms | Solution | |-------|----------|----------| | **Server not connecting** | No Google Analytics tools available | Restart Claude Desktop, check config file syntax | | **Invalid JSON config** | Claude startup errors | Validate JSON syntax in config file | | **Permission errors** | "Permission denied" on startup | Check file permissions and paths | ### API Issues | Issue | Symptoms | Solution | |-------|----------|----------| | **Invalid property ID** | "Property not found" | Use numeric format: `421301275` | | **API quota exceeded** | "Quota exceeded" error | Wait for quota reset or request increase | | **Invalid date format** | "Invalid date" | Use YYYY-MM-DD format: `2025-01-31` | | **No data returned** | Empty results | Check date range and property access | ### Debug Mode Enable detailed logging for troubleshooting: ```python # Add to server.py for debugging import logging logging.basicConfig(level=logging.DEBUG) ``` ## 🚀 Advanced Configuration ### HTTP Transport Mode For web deployment or remote access: ```bash # Start server in HTTP mode python3 server.py --http ``` **Claude Desktop config for HTTP:** ```json { "mcpServers": { "google-analytics": { "url": "http://127.0.0.1:8000/mcp" } } } ``` ### Custom Token Storage Modify token storage location in `oauth/google_auth.py`: ```python # Custom token file location def get_token_path(): return "/custom/secure/path/google_analytics_token.json" ``` ## 🤝 Contributing We welcome contributions! Here's how to get started: ### Development Setup ```bash # Fork and clone the repository git clone https://github.com/yourusername/google-analytics-mcp-server.git cd google-analytics-mcp-server # Create development environment python3 -m venv .venv source .venv/bin/activate # Install dependencies pip install -r requirements.txt # Set up development environment cp .env.example .env # Add your development credentials to .env ``` ### Making Changes 1. **Create a feature branch:** `git checkout -b feature/amazing-feature` 2. **Make your changes** with appropriate tests 3. **Test thoroughly** with different property configurations 4. **Update documentation** as needed 5. **Commit changes:** `git commit -m 'Add amazing feature'` 6. **Push to branch:** `git push origin feature/amazing-feature` 7. **Open a Pull Request** with detailed description ## 📊 API Limits and Quotas ### Google Analytics API Quotas - **Core Reporting API:** 100,000 requests per day per project - **Realtime API:** 10,000 requests per day per project - **Request rate:** 10 queries per second per project ### Best Practices for API Usage 1. **Cache results** when possible to reduce API calls 2. **Use appropriate date ranges** to limit data volume 3. **Batch requests** when supported 4. **Monitor usage** in Google Cloud Console 5. **Implement retry logic** for rate limit errors ### Quota Management ```bash # Monitor usage in Google Cloud Console # Go to APIs & Services → Quotas # Search for "Google Analytics" to see current usage ``` ## 📄 License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. --- ### MIT License ``` Copyright (c) 2025 Google Analytics MCP Server Contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ``` ## 📈 Roadmap ### Upcoming Features - 🔄 **Enhanced real-time analytics** with streaming data - 📊 **Built-in data visualization** with charts and graphs - 🤖 **AI-powered insights** and anomaly detection - 📝 **Custom dashboard creation** tools - 🔍 **Advanced segmentation** capabilities - 🌐 **Multi-property reporting** --- **Made with ❤️ for the MCP community** *Connect your Google Analytics 4 data directly to AI assistants and unlock powerful web analytics insights through natural language conversations.* ``` -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- ``` fastmcp>=0.8.0 # HTTP requests for API calls requests>=2.31.0 # Environment configuration python-dotenv>=1.0.0 # Google OAuth and Authentication (cohnen's approach) google-auth>=2.23.0 google-auth-oauthlib>=1.1.0 google-auth-httplib2>=0.1.1 # Additional dependencies urllib3>=2.0.0 typing-extensions>=4.0.0 ``` -------------------------------------------------------------------------------- /oauth/__init__.py: -------------------------------------------------------------------------------- ```python """ OAuth module for Google Analytics authentication. """ from .google_auth import ( get_headers_with_auto_token, get_oauth_credentials ) __all__ = [ 'get_headers_with_auto_token', 'get_oauth_credentials' ] # Version information __version__ = "2.0.0" __author__ = "Google Analytics MCP Server Contributors" __description__ = "OAuth 2.0 authentication module for Google Analytics API" ``` -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- ```json { "dxt_version": "0.1", "name": "google-analytics-mcp-server", "display_name": "Google Analytics MCP Server", "version": "0.1.0", "description": "A Python MCP server for Google Analytics 4 API integration with OAuth 2.0 authentication", "long_description": "This extension provides comprehensive Google Analytics 4 reporting and analysis capabilities through a Python MCP server. It enables programmatic access to GA4 data including property management, page views, user metrics, events, traffic sources, device analytics, and comprehensive custom reporting through the Google Analytics Data API and Admin API with automatic OAuth 2.0 authentication and token refresh.", "author": { "name": "GoMarble AI", "email": "[email protected]", "url": "https://github.com/gomarble-ai" }, "icon": "favicon.png", "server": { "type": "python", "entry_point": "server.py", "mcp_config": { "command": "python", "args": [ "${__dirname}/server.py" ], "env": { "GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH": "${user_config.oauth_config_path}", "PYTHONPATH": "${__dirname}/lib" }, "cwd": "${__dirname}" } }, "tools": [ { "name": "list_properties", "description": "List all Google Analytics 4 accounts with their associated properties in a hierarchical structure" }, { "name": "get_page_views", "description": "Get page view metrics for a specific date range from Google Analytics 4" }, { "name": "get_active_users", "description": "Get active users metrics for a specific date range from Google Analytics 4" }, { "name": "get_events", "description": "Get event metrics for a specific date range from Google Analytics 4" }, { "name": "get_traffic_sources", "description": "Get traffic source metrics for a specific date range from Google Analytics 4" }, { "name": "get_device_metrics", "description": "Get device metrics for a specific date range from Google Analytics 4" }, { "name": "run_report", "description": "Execute comprehensive Google Analytics 4 reports with full customization capabilities" } ], "resources": [ { "name": "ga4://reference", "description": "Google Analytics 4 API reference documentation with metrics, dimensions, and examples" } ], "keywords": [ "google", "analytics", "ga4", "web-analytics", "reporting", "metrics", "dimensions", "traffic", "users", "pageviews", "events", "oauth" ], "license": "MIT", "user_config": { "oauth_config_path": { "type": "string", "title": "OAuth Configuration Path", "description": "Full path to your Google Cloud OAuth 2.0 client credentials JSON file for Google Analytics API access", "required": true, "sensitive": false } }, "compatibility": { "claude_desktop": ">=0.10.0", "platforms": [ "darwin", "win32", "linux" ], "runtimes": { "python": ">=3.10.0 <4" } } } ``` -------------------------------------------------------------------------------- /oauth/google_auth.py: -------------------------------------------------------------------------------- ```python """ Google Analytics OAuth Authentication - integrated into tool calls """ import os import json import requests import logging from typing import Dict, Any # Google Auth libraries from google_auth_oauthlib.flow import InstalledAppFlow from google.oauth2.credentials import Credentials from google.auth.transport.requests import Request from google.auth.exceptions import RefreshError # Load environment variables try: from dotenv import load_dotenv load_dotenv() except ImportError: pass logger = logging.getLogger(__name__) # Constants - Updated for Google Analytics SCOPES = [ 'https://www.googleapis.com/auth/analytics', 'https://www.googleapis.com/auth/analytics.readonly' ] # Environment variables - Updated for Google Analytics GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH = os.environ.get("GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH") def get_oauth_credentials(): """Get and refresh OAuth user credentials for Google Analytics.""" if not GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH: raise ValueError( "GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH environment variable not set. " "Please set it to point to your OAuth credentials JSON file." ) if not os.path.exists(GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH): raise FileNotFoundError(f"OAuth config file not found: {GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH}") creds = None # Path to store the token (same directory as OAuth config) config_dir = os.path.dirname(os.path.abspath(GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH)) token_path = os.path.join(config_dir, 'google_analytics_token.json') # Load existing token if it exists if os.path.exists(token_path): try: logger.info(f"Loading existing OAuth token from {token_path}") creds = Credentials.from_authorized_user_file(token_path, SCOPES) except Exception as e: logger.warning(f"Error loading existing token: {e}") creds = None # Check if credentials are valid if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: try: logger.info("Refreshing expired OAuth token") creds.refresh(Request()) logger.info("Token successfully refreshed") except RefreshError as e: logger.warning(f"Token refresh failed: {e}, will get new token") creds = None except Exception as e: logger.error(f"Unexpected error refreshing token: {e}") raise # Need new credentials - run OAuth flow if not creds: logger.info("Starting OAuth authentication flow") try: # Load client configuration with open(GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH, 'r') as f: client_config = json.load(f) # Create flow flow = InstalledAppFlow.from_client_config(client_config, SCOPES) # Run OAuth flow with automatic local server try: creds = flow.run_local_server(port=0) logger.info("OAuth flow completed successfully using local server") except Exception as e: logger.warning(f"Local server failed: {e}, falling back to console") creds = flow.run_console() logger.info("OAuth flow completed successfully using console") except Exception as e: logger.error(f"OAuth flow failed: {e}") raise # Save the credentials if creds: try: logger.info(f"Saving credentials to {token_path}") os.makedirs(os.path.dirname(token_path), exist_ok=True) with open(token_path, 'w') as f: f.write(creds.to_json()) logger.info("Credentials saved successfully") except Exception as e: logger.warning(f"Could not save credentials: {e}") return creds def get_headers_with_auto_token() -> Dict[str, str]: """Get API headers with automatically managed token - integrated OAuth for Google Analytics.""" # This will automatically trigger OAuth flow if needed creds = get_oauth_credentials() headers = { 'Authorization': f'Bearer {creds.token}', 'Content-Type': 'application/json' } return headers ``` -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- ```python from fastmcp import FastMCP, Context from typing import Any, Dict, List, Optional import os import logging import requests import json # Load environment variables FIRST from dotenv import load_dotenv load_dotenv() # Import OAuth modules after environment is loaded from oauth.google_auth import get_headers_with_auto_token # Configure logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') logger = logging.getLogger('google_analytics_server') mcp = FastMCP("Google Analytics Tools") # Server startup logger.info("Starting Google Analytics MCP Server...") @mcp.tool def list_properties( account_id: str = "", ctx: Context = None ) -> Dict[str, Any]: """List all Google Analytics 4 accounts with their associated properties in a hierarchical structure. Args: account_id: Optional specific Google Analytics account ID to list properties for. If not provided, will list all accessible accounts with their properties. Returns: Hierarchical structure showing Account ID/Name with all associated Property IDs/Names """ if ctx: if account_id: ctx.info(f"Listing properties for account {account_id}...") else: ctx.info("Listing all accessible Google Analytics accounts and properties...") try: # This will automatically trigger OAuth flow if needed headers = get_headers_with_auto_token() accounts_with_properties = [] if account_id: # Get specific account info - try v1 then v1beta account_url = f"https://analyticsadmin.googleapis.com/v1/accounts/{account_id}" account_response = requests.get(account_url, headers=headers) api_version = 'v1' # Try v1beta if v1 fails if not account_response.ok: account_url = f"https://analyticsadmin.googleapis.com/v1beta/accounts/{account_id}" account_response = requests.get(account_url, headers=headers) api_version = 'v1beta' if not account_response.ok: if ctx: ctx.error(f"Failed to get account {account_id}: {account_response.status_code} {account_response.reason}") raise Exception(f"Admin API error: {account_response.status_code} {account_response.reason} - {account_response.text}") account = account_response.json() account_name = account.get('name', '') # Format: accounts/297364605 # Get properties for this account properties_url = f"https://analyticsadmin.googleapis.com/{api_version}/{account_name}/properties" properties = [] try: properties_response = requests.get(properties_url, headers=headers) if properties_response.ok: properties_results = properties_response.json() properties = properties_results.get('properties', []) else: # Try alternative format alt_properties_url = f"https://analyticsadmin.googleapis.com/{api_version}/properties?filter=parent:{account_name}" alt_response = requests.get(alt_properties_url, headers=headers) if alt_response.ok: alt_results = alt_response.json() properties = alt_results.get('properties', []) else: if ctx: ctx.error(f"Failed to get properties: {properties_response.status_code} {properties_response.reason}") raise Exception(f"Admin API error: {properties_response.status_code} {properties_response.reason} - {properties_response.text}") except Exception as property_error: if ctx: ctx.error(f"Error fetching properties for account {account_id}: {str(property_error)}") raise accounts_with_properties.append({ 'accountId': account_id, 'accountName': account.get('displayName', 'Unnamed Account'), 'accountCreateTime': account.get('createTime', 'Unknown'), 'propertyCount': len(properties), 'apiVersion': api_version, 'properties': [ { 'propertyId': prop.get('name', '').split('/')[-1] if prop.get('name') else 'Unknown', 'displayName': prop.get('displayName', 'Unnamed Property'), 'propertyType': prop.get('propertyType', 'PROPERTY_TYPE_UNSPECIFIED'), 'timeZone': prop.get('timeZone', 'Unknown'), 'currencyCode': prop.get('currencyCode', 'Unknown'), 'industryCategory': prop.get('industryCategory', 'Unknown'), 'createTime': prop.get('createTime', 'Unknown') } for prop in properties ] }) else: # Get all accounts accounts_url = 'https://analyticsadmin.googleapis.com/v1/accounts' accounts_response = requests.get(accounts_url, headers=headers) api_version = 'v1' if not accounts_response.ok: accounts_url = 'https://analyticsadmin.googleapis.com/v1beta/accounts' accounts_response = requests.get(accounts_url, headers=headers) api_version = 'v1beta' if not accounts_response.ok: if ctx: ctx.error(f"Failed to list accounts: {accounts_response.status_code} {accounts_response.reason}") raise Exception(f"Admin API error: {accounts_response.status_code} {accounts_response.reason} - {accounts_response.text}") accounts_results = accounts_response.json() accounts = accounts_results.get('accounts', []) # Get properties for each account for account in accounts: account_name = account.get('name', '') # Format: accounts/123456789 account_id_extracted = account_name.split('/')[-1] if account_name else 'Unknown' properties_url = f"https://analyticsadmin.googleapis.com/{api_version}/{account_name}/properties" properties = [] try: properties_response = requests.get(properties_url, headers=headers) if properties_response.ok: properties_results = properties_response.json() properties = properties_results.get('properties', []) else: # Try alternative format alt_properties_url = f"https://analyticsadmin.googleapis.com/{api_version}/properties?filter=parent:{account_name}" alt_response = requests.get(alt_properties_url, headers=headers) if alt_response.ok: alt_results = alt_response.json() properties = alt_results.get('properties', []) except Exception as property_error: if ctx: ctx.warning(f"Error fetching properties for account {account_name}: {str(property_error)}") accounts_with_properties.append({ 'accountId': account_id_extracted, 'accountName': account.get('displayName', 'Unnamed Account'), 'accountCreateTime': account.get('createTime', 'Unknown'), 'propertyCount': len(properties), 'apiVersion': api_version, 'properties': [ { 'propertyId': prop.get('name', '').split('/')[-1] if prop.get('name') else 'Unknown', 'displayName': prop.get('displayName', 'Unnamed Property'), 'propertyType': prop.get('propertyType', 'PROPERTY_TYPE_UNSPECIFIED'), 'timeZone': prop.get('timeZone', 'Unknown'), 'currencyCode': prop.get('currencyCode', 'Unknown'), 'industryCategory': prop.get('industryCategory', 'Unknown'), 'createTime': prop.get('createTime', 'Unknown') } for prop in properties ] }) total_accounts = len(accounts_with_properties) total_properties = sum(account['propertyCount'] for account in accounts_with_properties) if total_accounts == 0: message = f"No account found with ID {account_id}" if account_id else "No accounts found or no access to any accounts" if ctx: ctx.info(message) return { 'message': message, 'summary': { 'totalAccounts': 0, 'totalProperties': 0 }, 'accounts': [] } if ctx: ctx.info(f"Found {total_accounts} accounts with {total_properties} total properties.") return { 'summary': { 'totalAccounts': total_accounts, 'totalProperties': total_properties, 'queriedAccountId': account_id if account_id else None }, 'accounts': accounts_with_properties } except Exception as e: if ctx: ctx.error(f"Error listing properties: {str(e)}") raise @mcp.tool def get_page_views( property_id: str, start_date: str, end_date: str, dimensions: Optional[List[str]] = None, ctx: Context = None ) -> Dict[str, Any]: """Get page view metrics for a specific date range from Google Analytics 4. Args: property_id: Google Analytics 4 property ID (numeric, e.g., "123456789") start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format dimensions: List of dimensions to group by (optional, defaults to ["pagePath"]) Returns: Page view metrics grouped by specified dimensions in JSON format """ if ctx: ctx.info(f"Getting page views for property {property_id} from {start_date} to {end_date}...") try: # This will automatically trigger OAuth flow if needed headers = get_headers_with_auto_token() url = f"https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport" # Build payload payload = { 'dateRanges': [{'startDate': start_date, 'endDate': end_date}], 'metrics': [{'name': 'screenPageViews'}] } # Add dimensions if provided if dimensions and len(dimensions) > 0: payload['dimensions'] = [{'name': dim} for dim in dimensions] else: # Default to pagePath if no dimensions specified payload['dimensions'] = [{'name': 'pagePath'}] response = requests.post(url, headers=headers, json=payload) if not response.ok: if ctx: ctx.error(f"Google Analytics API error: {response.status_code} {response.reason}") raise Exception(f"Google Analytics API error: {response.status_code} {response.reason} - {response.text}") results = response.json() # Check if no results found if not results.get('rows') or len(results.get('rows', [])) == 0: message = f"No page view data found for property {property_id} from {start_date} to {end_date}" if ctx: ctx.info(message) return {'message': message} if ctx: ctx.info(f"Found {len(results.get('rows', []))} rows of page view data.") return results except Exception as e: if ctx: ctx.error(f"Error getting page views: {str(e)}") raise @mcp.tool def get_active_users( property_id: str, start_date: str, end_date: str, dimensions: Optional[List[str]] = None, ctx: Context = None ) -> Dict[str, Any]: """Get active users metrics for a specific date range from Google Analytics 4. Args: property_id: Google Analytics 4 property ID (numeric, e.g., "123456789") start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format dimensions: List of dimensions to group by (optional, defaults to ["date"]) Returns: Active users metrics grouped by specified dimensions in JSON format """ if ctx: ctx.info(f"Getting active users for property {property_id} from {start_date} to {end_date}...") try: # This will automatically trigger OAuth flow if needed headers = get_headers_with_auto_token() url = f"https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport" # Build payload payload = { 'dateRanges': [{'startDate': start_date, 'endDate': end_date}], 'metrics': [{'name': 'activeUsers'}] } # Add dimensions if provided if dimensions and len(dimensions) > 0: payload['dimensions'] = [{'name': dim} for dim in dimensions] else: # Default to date if no dimensions specified payload['dimensions'] = [{'name': 'date'}] response = requests.post(url, headers=headers, json=payload) if not response.ok: if ctx: ctx.error(f"Google Analytics API error: {response.status_code} {response.reason}") raise Exception(f"Google Analytics API error: {response.status_code} {response.reason} - {response.text}") results = response.json() # Check if no results found if not results.get('rows') or len(results.get('rows', [])) == 0: message = f"No active users data found for property {property_id} from {start_date} to {end_date}" if ctx: ctx.info(message) return {'message': message} if ctx: ctx.info(f"Found {len(results.get('rows', []))} rows of active users data.") return results except Exception as e: if ctx: ctx.error(f"Error getting active users: {str(e)}") raise @mcp.tool def get_events( property_id: str, start_date: str, end_date: str, dimensions: Optional[List[str]] = None, ctx: Context = None ) -> Dict[str, Any]: """Get event metrics for a specific date range from Google Analytics 4. Args: property_id: Google Analytics 4 property ID (numeric, e.g., "123456789") start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format dimensions: List of dimensions to group by (optional, defaults to ["eventName"]) Returns: Event metrics grouped by specified dimensions in JSON format """ if ctx: ctx.info(f"Getting events for property {property_id} from {start_date} to {end_date}...") try: # This will automatically trigger OAuth flow if needed headers = get_headers_with_auto_token() url = f"https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport" # Build payload payload = { 'dateRanges': [{'startDate': start_date, 'endDate': end_date}], 'metrics': [{'name': 'eventCount'}] } # Add dimensions if provided if dimensions and len(dimensions) > 0: payload['dimensions'] = [{'name': dim} for dim in dimensions] else: # Default to eventName if no dimensions specified payload['dimensions'] = [{'name': 'eventName'}] response = requests.post(url, headers=headers, json=payload) if not response.ok: if ctx: ctx.error(f"Google Analytics API error: {response.status_code} {response.reason}") raise Exception(f"Google Analytics API error: {response.status_code} {response.reason} - {response.text}") results = response.json() # Check if no results found if not results.get('rows') or len(results.get('rows', [])) == 0: message = f"No events data found for property {property_id} from {start_date} to {end_date}" if ctx: ctx.info(message) return {'message': message} if ctx: ctx.info(f"Found {len(results.get('rows', []))} rows of events data.") return results except Exception as e: if ctx: ctx.error(f"Error getting events: {str(e)}") raise @mcp.tool def get_traffic_sources( property_id: str, start_date: str, end_date: str, dimensions: Optional[List[str]] = None, ctx: Context = None ) -> Dict[str, Any]: """Get traffic source metrics for a specific date range from Google Analytics 4. Args: property_id: Google Analytics 4 property ID (numeric, e.g., "123456789") start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format dimensions: List of dimensions to group by (optional, defaults to ["source", "medium"]) Returns: Traffic source metrics grouped by specified dimensions in JSON format """ if ctx: ctx.info(f"Getting traffic sources for property {property_id} from {start_date} to {end_date}...") try: # This will automatically trigger OAuth flow if needed headers = get_headers_with_auto_token() url = f"https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport" # Build payload payload = { 'dateRanges': [{'startDate': start_date, 'endDate': end_date}], 'metrics': [{'name': 'sessions'}, {'name': 'totalUsers'}] } # Add dimensions if provided if dimensions and len(dimensions) > 0: payload['dimensions'] = [{'name': dim} for dim in dimensions] else: # Default to source and medium if no dimensions specified payload['dimensions'] = [{'name': 'source'}, {'name': 'medium'}] response = requests.post(url, headers=headers, json=payload) if not response.ok: if ctx: ctx.error(f"Google Analytics API error: {response.status_code} {response.reason}") raise Exception(f"Google Analytics API error: {response.status_code} {response.reason} - {response.text}") results = response.json() # Check if no results found if not results.get('rows') or len(results.get('rows', [])) == 0: message = f"No traffic sources data found for property {property_id} from {start_date} to {end_date}" if ctx: ctx.info(message) return {'message': message} if ctx: ctx.info(f"Found {len(results.get('rows', []))} rows of traffic sources data.") return results except Exception as e: if ctx: ctx.error(f"Error getting traffic sources: {str(e)}") raise @mcp.tool def get_device_metrics( property_id: str, start_date: str, end_date: str, dimensions: Optional[List[str]] = None, ctx: Context = None ) -> Dict[str, Any]: """Get device metrics for a specific date range from Google Analytics 4. Args: property_id: Google Analytics 4 property ID (numeric, e.g., "123456789") start_date: Start date in YYYY-MM-DD format end_date: End date in YYYY-MM-DD format dimensions: List of dimensions to group by (optional, defaults to ["deviceCategory"]) Returns: Device metrics grouped by specified dimensions in JSON format """ if ctx: ctx.info(f"Getting device metrics for property {property_id} from {start_date} to {end_date}...") try: # This will automatically trigger OAuth flow if needed headers = get_headers_with_auto_token() url = f"https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport" # Build payload payload = { 'dateRanges': [{'startDate': start_date, 'endDate': end_date}], 'metrics': [{'name': 'sessions'}, {'name': 'screenPageViews'}] } # Add dimensions if provided if dimensions and len(dimensions) > 0: payload['dimensions'] = [{'name': dim} for dim in dimensions] else: # Default to deviceCategory if no dimensions specified payload['dimensions'] = [{'name': 'deviceCategory'}] response = requests.post(url, headers=headers, json=payload) if not response.ok: if ctx: ctx.error(f"Google Analytics API error: {response.status_code} {response.reason}") raise Exception(f"Google Analytics API error: {response.status_code} {response.reason} - {response.text}") results = response.json() # Check if no results found if not results.get('rows') or len(results.get('rows', [])) == 0: message = f"No device metrics data found for property {property_id} from {start_date} to {end_date}" if ctx: ctx.info(message) return {'message': message} if ctx: ctx.info(f"Found {len(results.get('rows', []))} rows of device metrics data.") return results except Exception as e: if ctx: ctx.error(f"Error getting device metrics: {str(e)}") raise @mcp.tool def run_report( property_id: str, start_date: str, end_date: str, metrics: List[str], dimensions: Optional[List[str]] = None, limit: Optional[int] = None, offset: Optional[int] = None, order_bys: Optional[List[Dict[str, Any]]] = None, dimension_filter: Optional[Dict[str, Any]] = None, metric_filter: Optional[Dict[str, Any]] = None, keep_empty_rows: Optional[bool] = None, ctx: Context = None ) -> Dict[str, Any]: """Execute a comprehensive Google Analytics 4 report with full customization capabilities. IMPORTANT: Use STRING ARRAYS for metrics and dimensions, NOT objects! CORRECT FORMAT: - metrics: ["sessions", "totalUsers", "screenPageViews"] - dimensions: ["country", "deviceCategory"] INCORRECT FORMAT (will fail): - metrics: [{"name": "sessions"}] - dimensions: [{"name": "country"}] VALID GA4 METRICS: - sessions, totalUsers, activeUsers, newUsers - screenPageViews, pageviews, bounceRate, engagementRate - averageSessionDuration, userEngagementDuration, engagedSessions - conversions, totalRevenue, purchaseRevenue - eventCount, eventsPerSession COMMON METRIC MISTAKES: - uniquePageviews (not valid) → use screenPageViews - pageViews (not valid) → use screenPageViews - users (not valid) → use totalUsers or activeUsers - sessionDuration (not valid) → use averageSessionDuration - conversionsPerSession (not valid) → use eventsPerSession - conversionRate (not valid) → calculate manually VALID GA4 DIMENSIONS: - country, city, region, continent - deviceCategory, operatingSystem, browser - source, medium, campaignName, sessionDefaultChannelGroup - pagePath, pageTitle, landingPage - date, month, year, hour, dayOfWeek - sessionSource, sessionMedium, sessionCampaignName COMMON DIMENSION MISTAKES: - channelGroup (not valid) → use sessionDefaultChannelGroup - sessionCampaign (not valid) → use sessionCampaignName - campaign (not valid) → use campaignName SORTING (order_bys) - EXPERIMENTAL: - For metrics: [{"metric": {"metricName": "sessions"}, "desc": true}] - For dimensions: [{"dimension": {"dimensionName": "country"}, "desc": false}] - WARNING: Sorting may fail due to JSON parsing issues. Test without sorting first. Args: property_id: Google Analytics 4 property ID (numeric, e.g., "421301275") start_date: Start date in YYYY-MM-DD format (e.g., "2025-01-01") end_date: End date in YYYY-MM-DD format (e.g., "2025-01-31") metrics: Array of metric names as STRINGS (e.g., ["sessions", "totalUsers"]) dimensions: Optional array of dimension names as STRINGS (e.g., ["country", "deviceCategory"]) limit: Optional maximum number of rows (default: 100) offset: Optional number of rows to skip (default: 0) order_bys: Optional sorting - see format above dimension_filter: Optional filter for dimensions metric_filter: Optional filter for metrics keep_empty_rows: Optional boolean to include empty rows Returns: Comprehensive JSON report with requested metrics and dimensions WORKING EXAMPLES: Basic Sessions Report: { "property_id": "421301275", "start_date": "2025-01-01", "end_date": "2025-01-31", "metrics": ["sessions", "totalUsers", "screenPageViews"] } Traffic by Country: { "property_id": "421301275", "start_date": "2025-01-01", "end_date": "2025-01-31", "metrics": ["sessions", "totalUsers"], "dimensions": ["country", "deviceCategory"], "limit": 20 } Top Pages Report: { "property_id": "421301275", "start_date": "2025-01-01", "end_date": "2025-01-31", "metrics": ["screenPageViews", "sessions"], "dimensions": ["pagePath"], "limit": 10 } """ if ctx: ctx.info(f"Running comprehensive report for property {property_id} from {start_date} to {end_date}...") ctx.info(f"Metrics: {', '.join(metrics)}") if dimensions: ctx.info(f"Dimensions: {', '.join(dimensions)}") try: # Basic validation only if not property_id or not isinstance(property_id, str): raise ValueError("property_id is required and must be a string") if not start_date or not isinstance(start_date, str): raise ValueError("start_date is required and must be a string in YYYY-MM-DD format") if not end_date or not isinstance(end_date, str): raise ValueError("end_date is required and must be a string in YYYY-MM-DD format") if not metrics or not isinstance(metrics, list) or len(metrics) == 0: raise ValueError("metrics is required and must be a non-empty array") # This will automatically trigger OAuth flow if needed headers = get_headers_with_auto_token() url = f"https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport" # Construct the payload payload = { 'dateRanges': [{'startDate': start_date, 'endDate': end_date}], 'metrics': [{'name': metric.strip()} for metric in metrics] } # Add optional parameters if dimensions and isinstance(dimensions, list) and len(dimensions) > 0: payload['dimensions'] = [{'name': dimension.strip()} for dimension in dimensions] if limit is not None and isinstance(limit, int) and limit > 0: payload['limit'] = limit if offset is not None and isinstance(offset, int) and offset >= 0: payload['offset'] = offset if order_bys and isinstance(order_bys, list) and len(order_bys) > 0: payload['orderBys'] = order_bys if dimension_filter and isinstance(dimension_filter, dict): payload['dimensionFilter'] = dimension_filter if metric_filter and isinstance(metric_filter, dict): payload['metricFilter'] = metric_filter if keep_empty_rows is not None and isinstance(keep_empty_rows, bool): payload['keepEmptyRows'] = keep_empty_rows response = requests.post(url, headers=headers, json=payload) if not response.ok: if ctx: ctx.error(f"Google Analytics API error: {response.status_code} {response.reason}") raise Exception(f"Google Analytics API error: {response.status_code} {response.reason} - {response.text}") results = response.json() # Check if no results found if not results.get('rows') or len(results.get('rows', [])) == 0: message = f"No data found for property {property_id} from {start_date} to {end_date}" if ctx: ctx.info(message) return { 'message': message, 'property_id': property_id, 'start_date': start_date, 'end_date': end_date, 'metrics_requested': metrics, 'dimensions_requested': dimensions or [], 'total_rows': 0 } if ctx: ctx.info(f"Report completed successfully. Found {len(results.get('rows', []))} rows of data.") # Add metadata to results results['metadata'] = { 'property_id': property_id, 'start_date': start_date, 'end_date': end_date, 'metrics_requested': metrics, 'dimensions_requested': dimensions or [], 'total_rows': len(results.get('rows', [])) } return results except Exception as e: if ctx: ctx.error(f"Error running report: {str(e)}") raise @mcp.resource("ga4://reference") def ga4_reference() -> str: """Google Analytics 4 API reference documentation.""" return """ ## Google Analytics 4 API Reference ### Common Metrics - sessions: Number of sessions - totalUsers: Total number of users - activeUsers: Number of active users - newUsers: Number of new users - screenPageViews: Number of page/screen views - bounceRate: Bounce rate percentage - engagementRate: Engagement rate percentage - averageSessionDuration: Average session duration - conversions: Number of conversions - totalRevenue: Total revenue - eventCount: Number of events ### Common Dimensions - country: Country name - city: City name - deviceCategory: Device category (mobile, desktop, tablet) - source: Traffic source - medium: Traffic medium - campaignName: Campaign name - pagePath: Page path - eventName: Event name - date: Date (YYYYMMDD format) - month: Month - year: Year ### Date Format All dates should be in YYYY-MM-DD format (e.g., "2025-01-01") ### Example API Calls 1. Basic page views: get_page_views(property_id="123456789", start_date="2025-01-01", end_date="2025-01-31") 2. Traffic sources: get_traffic_sources(property_id="123456789", start_date="2025-01-01", end_date="2025-01-31") 3. Custom report: run_report( property_id="123456789", start_date="2025-01-01", end_date="2025-01-31", metrics=["sessions", "totalUsers", "screenPageViews"], dimensions=["country", "deviceCategory"] ) """ if __name__ == "__main__": import sys # Check command line arguments for transport mode if "--http" in sys.argv: logger.info("Starting with HTTP transport on http://127.0.0.1:8000/mcp") mcp.run(transport="streamable-http", host="127.0.0.1", port=8000, path="/mcp") else: # Default to STDIO for Claude Desktop compatibility logger.info("Starting with STDIO transport for Claude Desktop") mcp.run(transport="stdio") ```