# 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: -------------------------------------------------------------------------------- ``` 1 | # Environment 2 | .env 3 | 4 | 5 | # Python 6 | __pycache__/ 7 | *.pyc 8 | .venv/ 9 | lib/ ``` -------------------------------------------------------------------------------- /.dxtignore: -------------------------------------------------------------------------------- ``` 1 | Dockerfile 2 | LICENSE 3 | README_OAUTH.md 4 | README.md 5 | .gitignore 6 | env.example ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Google Analytics MCP Server 📊 2 | 3 | [](https://opensource.org/licenses/MIT) 4 | [](https://www.python.org/downloads/) 5 | [](https://github.com/jlowin/fastmcp) 6 | 7 | **A FastMCP-powered Model Context Protocol server for Google Analytics 4 API integration with automatic OAuth 2.0 authentication** 8 | 9 | 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. 10 | 11 | ## 🌟 Open Source & Community 12 | 13 | ### GoMarble AI Open Source Projects 14 | 15 | Check out our other open source contributions at [GoMarble AI GitHub](https://github.com/gomarble-ai): 16 | 17 | - **Analytics Tools** - Advanced analytics and reporting solutions 18 | - **AI Integration** - Tools for integrating AI with marketing platforms 19 | - **MCP Servers** - Additional Model Context Protocol implementations 20 | - **Marketing Automation** - Open source marketing automation tools 21 | 22 | ### Join Our Community 23 | 24 | Connect with other developers and marketers using AI in advertising: 25 | 26 | **[Join our Slack Community - AI in Ads](https://join.slack.com/t/ai-in-ads/shared_invite/zt-36hntbyf8-FSFixmwLb9mtEzVZhsToJQ)** 27 | 28 | - 💬 **Discuss** AI applications in advertising 29 | - 🤝 **Share** your projects and get feedback 30 | - 📚 **Learn** from industry experts 31 | - 🚀 **Collaborate** on open source projects 32 | - 🔧 **Get help** with technical implementation 33 | 34 | ### 🚀 Try Our One-Click Integration 35 | 36 | Skip the manual setup and get started instantly: 37 | 38 | **[One-Click MCP Integration](https://gomarble.ai/mcp)** - Connect Google Analytics and other tools to Claude Desktop in seconds 39 | 40 | - ⚡ **Instant Setup** - No manual configuration required 41 | - 🔐 **Secure Authentication** - Built-in OAuth handling 42 | - 📊 **Multiple Integrations** - Google Analytics, Google Ads, Meta Ads, and more 43 | - 📖 **Documentation** - Complete integration guide at **[gomarble.ai/docs](https://gomarble.ai/docs)** 44 | 45 | ## ✨ Features 46 | 47 | - 🔐 **Automatic OAuth 2.0** - One-time browser authentication with auto-refresh 48 | - 🔄 **Smart Token Management** - Handles expired tokens automatically 49 | - 📊 **Comprehensive Reporting** - Access all GA4 metrics and dimensions 50 | - 🏢 **Property Management** - List and manage Google Analytics properties 51 | - 📈 **Advanced Analytics** - Page views, users, events, traffic sources, and more 52 | - 🚀 **FastMCP Framework** - Built on the modern MCP standard 53 | - 🖥️ **Claude Desktop Ready** - Direct integration with Claude Desktop 54 | - 🛡️ **Secure Local Storage** - Tokens stored locally, never exposed 55 | 56 | ## 📋 Available Tools 57 | 58 | | Tool | Description | Parameters | Example Usage | 59 | |------|-------------|------------|---------------| 60 | | `list_properties` | List all GA4 accounts and properties | `account_id` (optional) | "List all my Google Analytics properties" | 61 | | `get_page_views` | Get page view metrics | `property_id`, `start_date`, `end_date`, `dimensions` (optional) | "Show me page views for last month" | 62 | | `get_active_users` | Get active users metrics | `property_id`, `start_date`, `end_date`, `dimensions` (optional) | "Get active users by day for last week" | 63 | | `get_events` | Get event metrics | `property_id`, `start_date`, `end_date`, `dimensions` (optional) | "Show me events data for property 123456789" | 64 | | `get_traffic_sources` | Get traffic source data | `property_id`, `start_date`, `end_date`, `dimensions` (optional) | "Analyze traffic sources for last 30 days" | 65 | | `get_device_metrics` | Get device-based metrics | `property_id`, `start_date`, `end_date`, `dimensions` (optional) | "Show device breakdown for last month" | 66 | | `run_report` | Comprehensive custom reporting | `property_id`, `start_date`, `end_date`, `metrics`, `dimensions`, filters, etc. | "Create custom report with sessions and conversions by country" | 67 | 68 | **Note:** All tools automatically handle authentication - no token parameters required! 69 | 70 | ## 🚀 Quick Start 71 | 72 | ### Prerequisites 73 | 74 | Before setting up the MCP server, you'll need: 75 | - Python 3.10+ installed 76 | - A Google Cloud Platform account 77 | - A Google Analytics 4 property with data access 78 | 79 | ## 🔧 Step 1: Google Cloud Platform Setup 80 | 81 | ### 1.1 Create Google Cloud Project 82 | 83 | 1. **Go to [Google Cloud Console](https://console.cloud.google.com/)** 84 | 2. **Create a new project:** 85 | - Click "Select a project" → "New Project" 86 | - Enter project name (e.g., "Google Analytics MCP") 87 | - Click "Create" 88 | 89 | ### 1.2 Enable Google Analytics APIs 90 | 91 | 1. **In your Google Cloud Console:** 92 | - Go to "APIs & Services" → "Library" 93 | - Search for "Google Analytics Data API" and enable it 94 | 95 | ### 1.3 Create OAuth 2.0 Credentials 96 | 97 | 1. **Go to "APIs & Services" → "Credentials"** 98 | 2. **Click "+ CREATE CREDENTIALS" → "OAuth 2.0 Client ID"** 99 | 3. **Configure consent screen (if first time):** 100 | - Click "Configure Consent Screen" 101 | - Choose "External" (unless you have Google Workspace) 102 | - Fill required fields: 103 | - App name: "Google Analytics MCP" 104 | - User support email: Your email 105 | - Developer contact: Your email 106 | - Add scopes: 107 | - `https://www.googleapis.com/auth/analytics` 108 | - `https://www.googleapis.com/auth/analytics.readonly` 109 | - Click "Save and Continue" through all steps 110 | 4. **Create OAuth Client:** 111 | - Application type: **"Desktop application"** 112 | - Name: "Google Analytics MCP Client" 113 | - Click "Create" 114 | 5. **Download credentials:** 115 | - Click "Download JSON" button 116 | - Save file as `client_secret_[long-string].json` in your project directory 117 | 118 | ## 🔧 Step 2: Google Analytics Access 119 | 120 | ### 2.1 Ensure Analytics Access 121 | 122 | 1. **Sign in to [Google Analytics](https://analytics.google.com)** 123 | 2. **Verify you have access to GA4 properties** 124 | 3. **Note your property IDs** (found in GA4 Admin → Property Settings) 125 | 4. **Ensure your Google account has at least Viewer access** to the properties you want to query 126 | 127 | ## 🔧 Step 3: Installation & Setup 128 | 129 | ### 3.1 Clone and Install 130 | 131 | ```bash 132 | # Clone the repository 133 | git clone https://github.com/yourusername/google-analytics-mcp-server.git 134 | cd google-analytics-mcp-server 135 | 136 | # Create virtual environment (recommended) 137 | python3 -m venv .venv 138 | source .venv/bin/activate # On Windows: .venv\Scripts\activate 139 | 140 | # Install dependencies 141 | pip install -r requirements.txt 142 | ``` 143 | 144 | ### 3.2 Environment Configuration 145 | 146 | Create a `.env` file in your project directory: 147 | 148 | ```bash 149 | # Copy the example file 150 | cp .env.example .env 151 | ``` 152 | 153 | Edit `.env` with your credentials: 154 | 155 | ```bash 156 | # Required: Path to OAuth credentials JSON file (downloaded from Google Cloud) 157 | GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH=/full/path/to/your/client_secret_file.json 158 | ``` 159 | 160 | **Example `.env` file:** 161 | ```bash 162 | GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH=/Users/john/google-analytics-mcp/client_secret_138737274875-abc123.apps.googleusercontent.com.json 163 | ``` 164 | 165 | ## 🖥️ Step 4: Claude Desktop Integration 166 | 167 | ### 4.1 Locate Claude Configuration 168 | 169 | Find your Claude Desktop configuration file: 170 | 171 | **macOS:** 172 | ```bash 173 | ~/Library/Application Support/Claude/claude_desktop_config.json 174 | ``` 175 | 176 | **Windows:** 177 | ```bash 178 | %APPDATA%\Claude\claude_desktop_config.json 179 | ``` 180 | 181 | ### 4.2 Add MCP Server Configuration 182 | 183 | Edit the configuration file and add your Google Analytics MCP server: 184 | 185 | ```json 186 | { 187 | "mcpServers": { 188 | "google-analytics": { 189 | "command": "/full/path/to/your/project/.venv/bin/python", 190 | "args": [ 191 | "/full/path/to/your/project/server.py" 192 | ] 193 | } 194 | } 195 | } 196 | ``` 197 | 198 | **Real Example:** 199 | ```json 200 | { 201 | "mcpServers": { 202 | "google-analytics": { 203 | "command": "/Users/marble-dev-01/workspace/google_analytics_mcp/.venv/bin/python", 204 | "args": [ 205 | "/Users/marble-dev-01/workspace/google_analytics_mcp/server.py" 206 | ] 207 | } 208 | } 209 | } 210 | ``` 211 | 212 | **Important:** 213 | - Use **absolute paths** for all file locations 214 | - On Windows, use forward slashes `/` or double backslashes `\\` in paths 215 | 216 | ### 4.3 Restart Claude Desktop 217 | 218 | Close and restart Claude Desktop to load the new configuration. 219 | 220 | ## 🔐 Step 5: First-Time Authentication 221 | 222 | ### 5.1 Trigger OAuth Flow 223 | 224 | 1. **Open Claude Desktop** 225 | 2. **Try any Google Analytics command**, for example: 226 | ``` 227 | "List all my Google Analytics properties" 228 | ``` 229 | 230 | ### 5.2 Complete Authentication 231 | 232 | 1. **Browser opens automatically** to Google OAuth page 233 | 2. **Sign in** with your Google account (the one with Analytics access) 234 | 3. **Grant permissions** by clicking "Allow" 235 | 4. **Browser shows success page** 236 | 5. **Return to Claude** - your command will complete automatically! 237 | 238 | ### 5.3 Verify Setup 239 | 240 | After authentication, you should see: 241 | - A `google_analytics_token.json` file created in your project directory 242 | - Your Google Analytics properties listed in Claude's response 243 | 244 | ## 📖 Usage Examples 245 | 246 | ### Property Management 247 | 248 | ``` 249 | "List all my Google Analytics properties" 250 | 251 | "Show me properties for account 123456789" 252 | 253 | "What GA4 properties do I have access to?" 254 | ``` 255 | 256 | ### Page View Analysis 257 | 258 | ``` 259 | "Get page views for property 421301275 from 2025-01-01 to 2025-01-31" 260 | 261 | "Show me top pages by page views for last month for property 421301275" 262 | 263 | "Analyze page performance by country for property 421301275" 264 | ``` 265 | 266 | ### User Analytics 267 | 268 | ``` 269 | "Get active users for property 421301275 in the last 7 days" 270 | 271 | "Show me user metrics by device category for property 421301275" 272 | 273 | "Compare new vs returning users for last month" 274 | ``` 275 | 276 | ### Traffic Source Analysis 277 | 278 | ``` 279 | "Analyze traffic sources for property 421301275 from 2025-01-01 to 2025-01-31" 280 | 281 | "Show me which channels drive the most users to my site" 282 | 283 | "Compare organic vs paid traffic performance" 284 | ``` 285 | 286 | ### Event Tracking 287 | 288 | ``` 289 | "Get events data for property 421301275 in the last 30 days" 290 | 291 | "Show me conversion events by source/medium" 292 | 293 | "Which events are most popular on my site?" 294 | ``` 295 | 296 | ### Custom Reports 297 | 298 | ``` 299 | "Create a report for property 421301275 with sessions, users, and page views by country from 2025-01-01 to 2025-01-31" 300 | 301 | "Run a custom report showing bounce rate and engagement rate by device category" 302 | 303 | "Generate a comprehensive traffic report with sessions, conversions, and revenue by source/medium" 304 | ``` 305 | 306 | ## 🔍 Advanced GA4 Examples 307 | 308 | ### Sessions and Users by Country 309 | ```python 310 | run_report( 311 | property_id="421301275", 312 | start_date="2025-01-01", 313 | end_date="2025-01-31", 314 | metrics=["sessions", "totalUsers", "screenPageViews"], 315 | dimensions=["country"], 316 | limit=20 317 | ) 318 | ``` 319 | 320 | ### Device Performance Analysis 321 | ```python 322 | run_report( 323 | property_id="421301275", 324 | start_date="2025-01-01", 325 | end_date="2025-01-31", 326 | metrics=["sessions", "bounceRate", "engagementRate"], 327 | dimensions=["deviceCategory", "operatingSystem"], 328 | limit=50 329 | ) 330 | ``` 331 | 332 | ### Traffic Sources with Conversions 333 | ```python 334 | run_report( 335 | property_id="421301275", 336 | start_date="2025-01-01", 337 | end_date="2025-01-31", 338 | metrics=["sessions", "conversions", "totalRevenue"], 339 | dimensions=["source", "medium", "campaignName"], 340 | limit=100 341 | ) 342 | ``` 343 | 344 | ### Daily Trend Analysis 345 | ```python 346 | run_report( 347 | property_id="421301275", 348 | start_date="2025-01-01", 349 | end_date="2025-01-31", 350 | metrics=["sessions", "activeUsers", "screenPageViews"], 351 | dimensions=["date"], 352 | limit=31 353 | ) 354 | ``` 355 | 356 | ## 📁 Project Structure 357 | 358 | ``` 359 | google-analytics-mcp-server/ 360 | ├── server.py # Main MCP server 361 | ├── oauth/ 362 | │ ├── __init__.py # Package initialization 363 | │ └── google_auth.py # OAuth authentication logic 364 | ├── google_analytics_token.json # Auto-generated token storage (gitignored) 365 | ├── client_secret_[long-string].json # Your OAuth credentials (gitignored) 366 | ├── .env # Environment variables (gitignored) 367 | ├── .env.example # Environment template 368 | ├── .gitignore # Git ignore file 369 | ├── requirements.txt # Python dependencies 370 | ├── LICENSE # MIT License 371 | └── README.md # This file 372 | ``` 373 | 374 | ## 🔒 Security & Best Practices 375 | 376 | ### File Security 377 | - ✅ **Credential files are gitignored** - Never committed to version control 378 | - ✅ **Local token storage** - Tokens stored in `google_analytics_token.json` locally 379 | - ✅ **Environment variables** - Sensitive data in `.env` file 380 | - ✅ **Automatic refresh** - Minimal token exposure time 381 | 382 | ### Recommended File Permissions 383 | ```bash 384 | # Set secure permissions for sensitive files 385 | chmod 600 .env 386 | chmod 600 google_analytics_token.json 387 | chmod 600 client_secret_*.json 388 | ``` 389 | 390 | ### Production Considerations 391 | 1. **Use environment variables** instead of `.env` files in production 392 | 2. **Implement rate limiting** to respect API quotas 393 | 3. **Monitor API usage** in Google Cloud Console 394 | 4. **Secure token storage** with proper access controls 395 | 5. **Regular token rotation** for enhanced security 396 | 397 | ## 🛠️ Troubleshooting 398 | 399 | ### Authentication Issues 400 | 401 | | Issue | Symptoms | Solution | 402 | |-------|----------|----------| 403 | | **No tokens found** | "Starting OAuth flow" message | ✅ Normal for first-time setup - complete browser authentication | 404 | | **Token refresh failed** | "Refreshing token failed" error | ✅ Delete `google_analytics_token.json` and re-authenticate | 405 | | **OAuth flow failed** | Browser error or no response | Check credentials file path and internet connection | 406 | | **Permission denied** | "Access denied" in browser | Ensure Google account has Analytics access | 407 | 408 | ### Configuration Issues 409 | 410 | | Issue | Symptoms | Solution | 411 | |-------|----------|----------| 412 | | **Environment variables missing** | "Environment variable not set" | Check `.env` file and Claude config `env` section | 413 | | **File not found** | "FileNotFoundError" | Verify absolute paths in configuration | 414 | | **Module import errors** | "ModuleNotFoundError" | Run `pip install -r requirements.txt` | 415 | | **Python path issues** | "Command not found" | Use absolute path to Python executable | 416 | 417 | ### Claude Desktop Issues 418 | 419 | | Issue | Symptoms | Solution | 420 | |-------|----------|----------| 421 | | **Server not connecting** | No Google Analytics tools available | Restart Claude Desktop, check config file syntax | 422 | | **Invalid JSON config** | Claude startup errors | Validate JSON syntax in config file | 423 | | **Permission errors** | "Permission denied" on startup | Check file permissions and paths | 424 | 425 | ### API Issues 426 | 427 | | Issue | Symptoms | Solution | 428 | |-------|----------|----------| 429 | | **Invalid property ID** | "Property not found" | Use numeric format: `421301275` | 430 | | **API quota exceeded** | "Quota exceeded" error | Wait for quota reset or request increase | 431 | | **Invalid date format** | "Invalid date" | Use YYYY-MM-DD format: `2025-01-31` | 432 | | **No data returned** | Empty results | Check date range and property access | 433 | 434 | ### Debug Mode 435 | 436 | Enable detailed logging for troubleshooting: 437 | 438 | ```python 439 | # Add to server.py for debugging 440 | import logging 441 | logging.basicConfig(level=logging.DEBUG) 442 | ``` 443 | 444 | ## 🚀 Advanced Configuration 445 | 446 | ### HTTP Transport Mode 447 | 448 | For web deployment or remote access: 449 | 450 | ```bash 451 | # Start server in HTTP mode 452 | python3 server.py --http 453 | ``` 454 | 455 | **Claude Desktop config for HTTP:** 456 | ```json 457 | { 458 | "mcpServers": { 459 | "google-analytics": { 460 | "url": "http://127.0.0.1:8000/mcp" 461 | } 462 | } 463 | } 464 | ``` 465 | 466 | ### Custom Token Storage 467 | 468 | Modify token storage location in `oauth/google_auth.py`: 469 | 470 | ```python 471 | # Custom token file location 472 | def get_token_path(): 473 | return "/custom/secure/path/google_analytics_token.json" 474 | ``` 475 | 476 | ## 🤝 Contributing 477 | 478 | We welcome contributions! Here's how to get started: 479 | 480 | ### Development Setup 481 | 482 | ```bash 483 | # Fork and clone the repository 484 | git clone https://github.com/yourusername/google-analytics-mcp-server.git 485 | cd google-analytics-mcp-server 486 | 487 | # Create development environment 488 | python3 -m venv .venv 489 | source .venv/bin/activate 490 | 491 | # Install dependencies 492 | pip install -r requirements.txt 493 | 494 | # Set up development environment 495 | cp .env.example .env 496 | # Add your development credentials to .env 497 | ``` 498 | 499 | ### Making Changes 500 | 501 | 1. **Create a feature branch:** `git checkout -b feature/amazing-feature` 502 | 2. **Make your changes** with appropriate tests 503 | 3. **Test thoroughly** with different property configurations 504 | 4. **Update documentation** as needed 505 | 5. **Commit changes:** `git commit -m 'Add amazing feature'` 506 | 6. **Push to branch:** `git push origin feature/amazing-feature` 507 | 7. **Open a Pull Request** with detailed description 508 | 509 | ## 📊 API Limits and Quotas 510 | 511 | ### Google Analytics API Quotas 512 | 513 | - **Core Reporting API:** 100,000 requests per day per project 514 | - **Realtime API:** 10,000 requests per day per project 515 | - **Request rate:** 10 queries per second per project 516 | 517 | ### Best Practices for API Usage 518 | 519 | 1. **Cache results** when possible to reduce API calls 520 | 2. **Use appropriate date ranges** to limit data volume 521 | 3. **Batch requests** when supported 522 | 4. **Monitor usage** in Google Cloud Console 523 | 5. **Implement retry logic** for rate limit errors 524 | 525 | ### Quota Management 526 | 527 | ```bash 528 | # Monitor usage in Google Cloud Console 529 | # Go to APIs & Services → Quotas 530 | # Search for "Google Analytics" to see current usage 531 | ``` 532 | 533 | ## 📄 License 534 | 535 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 536 | 537 | --- 538 | 539 | ### MIT License 540 | 541 | ``` 542 | Copyright (c) 2025 Google Analytics MCP Server Contributors 543 | 544 | Permission is hereby granted, free of charge, to any person obtaining a copy 545 | of this software and associated documentation files (the "Software"), to deal 546 | in the Software without restriction, including without limitation the rights 547 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 548 | copies of the Software, and to permit persons to whom the Software is 549 | furnished to do so, subject to the following conditions: 550 | 551 | The above copyright notice and this permission notice shall be included in all 552 | copies or substantial portions of the Software. 553 | 554 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 555 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 556 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 557 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 558 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 559 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 560 | SOFTWARE. 561 | ``` 562 | 563 | ## 📈 Roadmap 564 | 565 | ### Upcoming Features 566 | - 🔄 **Enhanced real-time analytics** with streaming data 567 | - 📊 **Built-in data visualization** with charts and graphs 568 | - 🤖 **AI-powered insights** and anomaly detection 569 | - 📝 **Custom dashboard creation** tools 570 | - 🔍 **Advanced segmentation** capabilities 571 | - 🌐 **Multi-property reporting** 572 | 573 | --- 574 | 575 | **Made with ❤️ for the MCP community** 576 | 577 | *Connect your Google Analytics 4 data directly to AI assistants and unlock powerful web analytics insights through natural language conversations.* 578 | ``` -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- ``` 1 | fastmcp>=0.8.0 2 | 3 | # HTTP requests for API calls 4 | requests>=2.31.0 5 | 6 | # Environment configuration 7 | python-dotenv>=1.0.0 8 | 9 | # Google OAuth and Authentication (cohnen's approach) 10 | google-auth>=2.23.0 11 | google-auth-oauthlib>=1.1.0 12 | google-auth-httplib2>=0.1.1 13 | 14 | # Additional dependencies 15 | urllib3>=2.0.0 16 | typing-extensions>=4.0.0 17 | ``` -------------------------------------------------------------------------------- /oauth/__init__.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | OAuth module for Google Analytics authentication. 3 | """ 4 | 5 | from .google_auth import ( 6 | get_headers_with_auto_token, 7 | get_oauth_credentials 8 | ) 9 | 10 | __all__ = [ 11 | 'get_headers_with_auto_token', 12 | 'get_oauth_credentials' 13 | ] 14 | 15 | # Version information 16 | __version__ = "2.0.0" 17 | __author__ = "Google Analytics MCP Server Contributors" 18 | __description__ = "OAuth 2.0 authentication module for Google Analytics API" ``` -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- ```json 1 | { 2 | "dxt_version": "0.1", 3 | "name": "google-analytics-mcp-server", 4 | "display_name": "Google Analytics MCP Server", 5 | "version": "0.1.0", 6 | "description": "A Python MCP server for Google Analytics 4 API integration with OAuth 2.0 authentication", 7 | "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.", 8 | "author": { 9 | "name": "GoMarble AI", 10 | "email": "[email protected]", 11 | "url": "https://github.com/gomarble-ai" 12 | }, 13 | "icon": "favicon.png", 14 | "server": { 15 | "type": "python", 16 | "entry_point": "server.py", 17 | "mcp_config": { 18 | "command": "python", 19 | "args": [ 20 | "${__dirname}/server.py" 21 | ], 22 | "env": { 23 | "GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH": "${user_config.oauth_config_path}", 24 | "PYTHONPATH": "${__dirname}/lib" 25 | }, 26 | "cwd": "${__dirname}" 27 | } 28 | }, 29 | "tools": [ 30 | { 31 | "name": "list_properties", 32 | "description": "List all Google Analytics 4 accounts with their associated properties in a hierarchical structure" 33 | }, 34 | { 35 | "name": "get_page_views", 36 | "description": "Get page view metrics for a specific date range from Google Analytics 4" 37 | }, 38 | { 39 | "name": "get_active_users", 40 | "description": "Get active users metrics for a specific date range from Google Analytics 4" 41 | }, 42 | { 43 | "name": "get_events", 44 | "description": "Get event metrics for a specific date range from Google Analytics 4" 45 | }, 46 | { 47 | "name": "get_traffic_sources", 48 | "description": "Get traffic source metrics for a specific date range from Google Analytics 4" 49 | }, 50 | { 51 | "name": "get_device_metrics", 52 | "description": "Get device metrics for a specific date range from Google Analytics 4" 53 | }, 54 | { 55 | "name": "run_report", 56 | "description": "Execute comprehensive Google Analytics 4 reports with full customization capabilities" 57 | } 58 | ], 59 | "resources": [ 60 | { 61 | "name": "ga4://reference", 62 | "description": "Google Analytics 4 API reference documentation with metrics, dimensions, and examples" 63 | } 64 | ], 65 | "keywords": [ 66 | "google", 67 | "analytics", 68 | "ga4", 69 | "web-analytics", 70 | "reporting", 71 | "metrics", 72 | "dimensions", 73 | "traffic", 74 | "users", 75 | "pageviews", 76 | "events", 77 | "oauth" 78 | ], 79 | "license": "MIT", 80 | "user_config": { 81 | "oauth_config_path": { 82 | "type": "string", 83 | "title": "OAuth Configuration Path", 84 | "description": "Full path to your Google Cloud OAuth 2.0 client credentials JSON file for Google Analytics API access", 85 | "required": true, 86 | "sensitive": false 87 | } 88 | }, 89 | "compatibility": { 90 | "claude_desktop": ">=0.10.0", 91 | "platforms": [ 92 | "darwin", 93 | "win32", 94 | "linux" 95 | ], 96 | "runtimes": { 97 | "python": ">=3.10.0 <4" 98 | } 99 | } 100 | } ``` -------------------------------------------------------------------------------- /oauth/google_auth.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | Google Analytics OAuth Authentication - integrated into tool calls 3 | """ 4 | 5 | import os 6 | import json 7 | import requests 8 | import logging 9 | from typing import Dict, Any 10 | 11 | # Google Auth libraries 12 | from google_auth_oauthlib.flow import InstalledAppFlow 13 | from google.oauth2.credentials import Credentials 14 | from google.auth.transport.requests import Request 15 | from google.auth.exceptions import RefreshError 16 | 17 | # Load environment variables 18 | try: 19 | from dotenv import load_dotenv 20 | load_dotenv() 21 | except ImportError: 22 | pass 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | # Constants - Updated for Google Analytics 27 | SCOPES = [ 28 | 'https://www.googleapis.com/auth/analytics', 29 | 'https://www.googleapis.com/auth/analytics.readonly' 30 | ] 31 | 32 | # Environment variables - Updated for Google Analytics 33 | GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH = os.environ.get("GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH") 34 | 35 | def get_oauth_credentials(): 36 | """Get and refresh OAuth user credentials for Google Analytics.""" 37 | if not GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH: 38 | raise ValueError( 39 | "GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH environment variable not set. " 40 | "Please set it to point to your OAuth credentials JSON file." 41 | ) 42 | 43 | if not os.path.exists(GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH): 44 | raise FileNotFoundError(f"OAuth config file not found: {GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH}") 45 | 46 | creds = None 47 | 48 | # Path to store the token (same directory as OAuth config) 49 | config_dir = os.path.dirname(os.path.abspath(GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH)) 50 | token_path = os.path.join(config_dir, 'google_analytics_token.json') 51 | 52 | # Load existing token if it exists 53 | if os.path.exists(token_path): 54 | try: 55 | logger.info(f"Loading existing OAuth token from {token_path}") 56 | creds = Credentials.from_authorized_user_file(token_path, SCOPES) 57 | except Exception as e: 58 | logger.warning(f"Error loading existing token: {e}") 59 | creds = None 60 | 61 | # Check if credentials are valid 62 | if not creds or not creds.valid: 63 | if creds and creds.expired and creds.refresh_token: 64 | try: 65 | logger.info("Refreshing expired OAuth token") 66 | creds.refresh(Request()) 67 | logger.info("Token successfully refreshed") 68 | except RefreshError as e: 69 | logger.warning(f"Token refresh failed: {e}, will get new token") 70 | creds = None 71 | except Exception as e: 72 | logger.error(f"Unexpected error refreshing token: {e}") 73 | raise 74 | 75 | # Need new credentials - run OAuth flow 76 | if not creds: 77 | logger.info("Starting OAuth authentication flow") 78 | 79 | try: 80 | # Load client configuration 81 | with open(GOOGLE_ANALYTICS_OAUTH_CONFIG_PATH, 'r') as f: 82 | client_config = json.load(f) 83 | 84 | # Create flow 85 | flow = InstalledAppFlow.from_client_config(client_config, SCOPES) 86 | 87 | # Run OAuth flow with automatic local server 88 | try: 89 | creds = flow.run_local_server(port=0) 90 | logger.info("OAuth flow completed successfully using local server") 91 | except Exception as e: 92 | logger.warning(f"Local server failed: {e}, falling back to console") 93 | creds = flow.run_console() 94 | logger.info("OAuth flow completed successfully using console") 95 | 96 | except Exception as e: 97 | logger.error(f"OAuth flow failed: {e}") 98 | raise 99 | 100 | # Save the credentials 101 | if creds: 102 | try: 103 | logger.info(f"Saving credentials to {token_path}") 104 | os.makedirs(os.path.dirname(token_path), exist_ok=True) 105 | with open(token_path, 'w') as f: 106 | f.write(creds.to_json()) 107 | logger.info("Credentials saved successfully") 108 | except Exception as e: 109 | logger.warning(f"Could not save credentials: {e}") 110 | 111 | return creds 112 | 113 | def get_headers_with_auto_token() -> Dict[str, str]: 114 | """Get API headers with automatically managed token - integrated OAuth for Google Analytics.""" 115 | # This will automatically trigger OAuth flow if needed 116 | creds = get_oauth_credentials() 117 | 118 | headers = { 119 | 'Authorization': f'Bearer {creds.token}', 120 | 'Content-Type': 'application/json' 121 | } 122 | 123 | return headers ``` -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- ```python 1 | from fastmcp import FastMCP, Context 2 | from typing import Any, Dict, List, Optional 3 | import os 4 | import logging 5 | import requests 6 | import json 7 | 8 | # Load environment variables FIRST 9 | from dotenv import load_dotenv 10 | load_dotenv() 11 | 12 | # Import OAuth modules after environment is loaded 13 | from oauth.google_auth import get_headers_with_auto_token 14 | 15 | # Configure logging 16 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 17 | logger = logging.getLogger('google_analytics_server') 18 | 19 | mcp = FastMCP("Google Analytics Tools") 20 | 21 | # Server startup 22 | logger.info("Starting Google Analytics MCP Server...") 23 | 24 | @mcp.tool 25 | def list_properties( 26 | account_id: str = "", 27 | ctx: Context = None 28 | ) -> Dict[str, Any]: 29 | """List all Google Analytics 4 accounts with their associated properties in a hierarchical structure. 30 | 31 | Args: 32 | account_id: Optional specific Google Analytics account ID to list properties for. 33 | If not provided, will list all accessible accounts with their properties. 34 | 35 | Returns: 36 | Hierarchical structure showing Account ID/Name with all associated Property IDs/Names 37 | """ 38 | if ctx: 39 | if account_id: 40 | ctx.info(f"Listing properties for account {account_id}...") 41 | else: 42 | ctx.info("Listing all accessible Google Analytics accounts and properties...") 43 | 44 | try: 45 | # This will automatically trigger OAuth flow if needed 46 | headers = get_headers_with_auto_token() 47 | 48 | accounts_with_properties = [] 49 | 50 | if account_id: 51 | # Get specific account info - try v1 then v1beta 52 | account_url = f"https://analyticsadmin.googleapis.com/v1/accounts/{account_id}" 53 | 54 | account_response = requests.get(account_url, headers=headers) 55 | api_version = 'v1' 56 | 57 | # Try v1beta if v1 fails 58 | if not account_response.ok: 59 | account_url = f"https://analyticsadmin.googleapis.com/v1beta/accounts/{account_id}" 60 | account_response = requests.get(account_url, headers=headers) 61 | api_version = 'v1beta' 62 | 63 | if not account_response.ok: 64 | if ctx: 65 | ctx.error(f"Failed to get account {account_id}: {account_response.status_code} {account_response.reason}") 66 | raise Exception(f"Admin API error: {account_response.status_code} {account_response.reason} - {account_response.text}") 67 | 68 | account = account_response.json() 69 | account_name = account.get('name', '') # Format: accounts/297364605 70 | 71 | # Get properties for this account 72 | properties_url = f"https://analyticsadmin.googleapis.com/{api_version}/{account_name}/properties" 73 | 74 | properties = [] 75 | try: 76 | properties_response = requests.get(properties_url, headers=headers) 77 | 78 | if properties_response.ok: 79 | properties_results = properties_response.json() 80 | properties = properties_results.get('properties', []) 81 | else: 82 | # Try alternative format 83 | alt_properties_url = f"https://analyticsadmin.googleapis.com/{api_version}/properties?filter=parent:{account_name}" 84 | alt_response = requests.get(alt_properties_url, headers=headers) 85 | 86 | if alt_response.ok: 87 | alt_results = alt_response.json() 88 | properties = alt_results.get('properties', []) 89 | else: 90 | if ctx: 91 | ctx.error(f"Failed to get properties: {properties_response.status_code} {properties_response.reason}") 92 | raise Exception(f"Admin API error: {properties_response.status_code} {properties_response.reason} - {properties_response.text}") 93 | except Exception as property_error: 94 | if ctx: 95 | ctx.error(f"Error fetching properties for account {account_id}: {str(property_error)}") 96 | raise 97 | 98 | accounts_with_properties.append({ 99 | 'accountId': account_id, 100 | 'accountName': account.get('displayName', 'Unnamed Account'), 101 | 'accountCreateTime': account.get('createTime', 'Unknown'), 102 | 'propertyCount': len(properties), 103 | 'apiVersion': api_version, 104 | 'properties': [ 105 | { 106 | 'propertyId': prop.get('name', '').split('/')[-1] if prop.get('name') else 'Unknown', 107 | 'displayName': prop.get('displayName', 'Unnamed Property'), 108 | 'propertyType': prop.get('propertyType', 'PROPERTY_TYPE_UNSPECIFIED'), 109 | 'timeZone': prop.get('timeZone', 'Unknown'), 110 | 'currencyCode': prop.get('currencyCode', 'Unknown'), 111 | 'industryCategory': prop.get('industryCategory', 'Unknown'), 112 | 'createTime': prop.get('createTime', 'Unknown') 113 | } 114 | for prop in properties 115 | ] 116 | }) 117 | 118 | else: 119 | # Get all accounts 120 | accounts_url = 'https://analyticsadmin.googleapis.com/v1/accounts' 121 | 122 | accounts_response = requests.get(accounts_url, headers=headers) 123 | api_version = 'v1' 124 | 125 | if not accounts_response.ok: 126 | accounts_url = 'https://analyticsadmin.googleapis.com/v1beta/accounts' 127 | accounts_response = requests.get(accounts_url, headers=headers) 128 | api_version = 'v1beta' 129 | 130 | if not accounts_response.ok: 131 | if ctx: 132 | ctx.error(f"Failed to list accounts: {accounts_response.status_code} {accounts_response.reason}") 133 | raise Exception(f"Admin API error: {accounts_response.status_code} {accounts_response.reason} - {accounts_response.text}") 134 | 135 | accounts_results = accounts_response.json() 136 | accounts = accounts_results.get('accounts', []) 137 | 138 | # Get properties for each account 139 | for account in accounts: 140 | account_name = account.get('name', '') # Format: accounts/123456789 141 | account_id_extracted = account_name.split('/')[-1] if account_name else 'Unknown' 142 | 143 | properties_url = f"https://analyticsadmin.googleapis.com/{api_version}/{account_name}/properties" 144 | 145 | properties = [] 146 | try: 147 | properties_response = requests.get(properties_url, headers=headers) 148 | 149 | if properties_response.ok: 150 | properties_results = properties_response.json() 151 | properties = properties_results.get('properties', []) 152 | else: 153 | # Try alternative format 154 | alt_properties_url = f"https://analyticsadmin.googleapis.com/{api_version}/properties?filter=parent:{account_name}" 155 | alt_response = requests.get(alt_properties_url, headers=headers) 156 | 157 | if alt_response.ok: 158 | alt_results = alt_response.json() 159 | properties = alt_results.get('properties', []) 160 | except Exception as property_error: 161 | if ctx: 162 | ctx.warning(f"Error fetching properties for account {account_name}: {str(property_error)}") 163 | 164 | accounts_with_properties.append({ 165 | 'accountId': account_id_extracted, 166 | 'accountName': account.get('displayName', 'Unnamed Account'), 167 | 'accountCreateTime': account.get('createTime', 'Unknown'), 168 | 'propertyCount': len(properties), 169 | 'apiVersion': api_version, 170 | 'properties': [ 171 | { 172 | 'propertyId': prop.get('name', '').split('/')[-1] if prop.get('name') else 'Unknown', 173 | 'displayName': prop.get('displayName', 'Unnamed Property'), 174 | 'propertyType': prop.get('propertyType', 'PROPERTY_TYPE_UNSPECIFIED'), 175 | 'timeZone': prop.get('timeZone', 'Unknown'), 176 | 'currencyCode': prop.get('currencyCode', 'Unknown'), 177 | 'industryCategory': prop.get('industryCategory', 'Unknown'), 178 | 'createTime': prop.get('createTime', 'Unknown') 179 | } 180 | for prop in properties 181 | ] 182 | }) 183 | 184 | total_accounts = len(accounts_with_properties) 185 | total_properties = sum(account['propertyCount'] for account in accounts_with_properties) 186 | 187 | if total_accounts == 0: 188 | message = f"No account found with ID {account_id}" if account_id else "No accounts found or no access to any accounts" 189 | if ctx: 190 | ctx.info(message) 191 | return { 192 | 'message': message, 193 | 'summary': { 194 | 'totalAccounts': 0, 195 | 'totalProperties': 0 196 | }, 197 | 'accounts': [] 198 | } 199 | 200 | if ctx: 201 | ctx.info(f"Found {total_accounts} accounts with {total_properties} total properties.") 202 | 203 | return { 204 | 'summary': { 205 | 'totalAccounts': total_accounts, 206 | 'totalProperties': total_properties, 207 | 'queriedAccountId': account_id if account_id else None 208 | }, 209 | 'accounts': accounts_with_properties 210 | } 211 | 212 | except Exception as e: 213 | if ctx: 214 | ctx.error(f"Error listing properties: {str(e)}") 215 | raise 216 | 217 | @mcp.tool 218 | def get_page_views( 219 | property_id: str, 220 | start_date: str, 221 | end_date: str, 222 | dimensions: Optional[List[str]] = None, 223 | ctx: Context = None 224 | ) -> Dict[str, Any]: 225 | """Get page view metrics for a specific date range from Google Analytics 4. 226 | 227 | Args: 228 | property_id: Google Analytics 4 property ID (numeric, e.g., "123456789") 229 | start_date: Start date in YYYY-MM-DD format 230 | end_date: End date in YYYY-MM-DD format 231 | dimensions: List of dimensions to group by (optional, defaults to ["pagePath"]) 232 | 233 | Returns: 234 | Page view metrics grouped by specified dimensions in JSON format 235 | """ 236 | if ctx: 237 | ctx.info(f"Getting page views for property {property_id} from {start_date} to {end_date}...") 238 | 239 | try: 240 | # This will automatically trigger OAuth flow if needed 241 | headers = get_headers_with_auto_token() 242 | 243 | url = f"https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport" 244 | 245 | # Build payload 246 | payload = { 247 | 'dateRanges': [{'startDate': start_date, 'endDate': end_date}], 248 | 'metrics': [{'name': 'screenPageViews'}] 249 | } 250 | 251 | # Add dimensions if provided 252 | if dimensions and len(dimensions) > 0: 253 | payload['dimensions'] = [{'name': dim} for dim in dimensions] 254 | else: 255 | # Default to pagePath if no dimensions specified 256 | payload['dimensions'] = [{'name': 'pagePath'}] 257 | 258 | response = requests.post(url, headers=headers, json=payload) 259 | 260 | if not response.ok: 261 | if ctx: 262 | ctx.error(f"Google Analytics API error: {response.status_code} {response.reason}") 263 | raise Exception(f"Google Analytics API error: {response.status_code} {response.reason} - {response.text}") 264 | 265 | results = response.json() 266 | 267 | # Check if no results found 268 | if not results.get('rows') or len(results.get('rows', [])) == 0: 269 | message = f"No page view data found for property {property_id} from {start_date} to {end_date}" 270 | if ctx: 271 | ctx.info(message) 272 | return {'message': message} 273 | 274 | if ctx: 275 | ctx.info(f"Found {len(results.get('rows', []))} rows of page view data.") 276 | 277 | return results 278 | 279 | except Exception as e: 280 | if ctx: 281 | ctx.error(f"Error getting page views: {str(e)}") 282 | raise 283 | 284 | @mcp.tool 285 | def get_active_users( 286 | property_id: str, 287 | start_date: str, 288 | end_date: str, 289 | dimensions: Optional[List[str]] = None, 290 | ctx: Context = None 291 | ) -> Dict[str, Any]: 292 | """Get active users metrics for a specific date range from Google Analytics 4. 293 | 294 | Args: 295 | property_id: Google Analytics 4 property ID (numeric, e.g., "123456789") 296 | start_date: Start date in YYYY-MM-DD format 297 | end_date: End date in YYYY-MM-DD format 298 | dimensions: List of dimensions to group by (optional, defaults to ["date"]) 299 | 300 | Returns: 301 | Active users metrics grouped by specified dimensions in JSON format 302 | """ 303 | if ctx: 304 | ctx.info(f"Getting active users for property {property_id} from {start_date} to {end_date}...") 305 | 306 | try: 307 | # This will automatically trigger OAuth flow if needed 308 | headers = get_headers_with_auto_token() 309 | 310 | url = f"https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport" 311 | 312 | # Build payload 313 | payload = { 314 | 'dateRanges': [{'startDate': start_date, 'endDate': end_date}], 315 | 'metrics': [{'name': 'activeUsers'}] 316 | } 317 | 318 | # Add dimensions if provided 319 | if dimensions and len(dimensions) > 0: 320 | payload['dimensions'] = [{'name': dim} for dim in dimensions] 321 | else: 322 | # Default to date if no dimensions specified 323 | payload['dimensions'] = [{'name': 'date'}] 324 | 325 | response = requests.post(url, headers=headers, json=payload) 326 | 327 | if not response.ok: 328 | if ctx: 329 | ctx.error(f"Google Analytics API error: {response.status_code} {response.reason}") 330 | raise Exception(f"Google Analytics API error: {response.status_code} {response.reason} - {response.text}") 331 | 332 | results = response.json() 333 | 334 | # Check if no results found 335 | if not results.get('rows') or len(results.get('rows', [])) == 0: 336 | message = f"No active users data found for property {property_id} from {start_date} to {end_date}" 337 | if ctx: 338 | ctx.info(message) 339 | return {'message': message} 340 | 341 | if ctx: 342 | ctx.info(f"Found {len(results.get('rows', []))} rows of active users data.") 343 | 344 | return results 345 | 346 | except Exception as e: 347 | if ctx: 348 | ctx.error(f"Error getting active users: {str(e)}") 349 | raise 350 | 351 | @mcp.tool 352 | def get_events( 353 | property_id: str, 354 | start_date: str, 355 | end_date: str, 356 | dimensions: Optional[List[str]] = None, 357 | ctx: Context = None 358 | ) -> Dict[str, Any]: 359 | """Get event metrics for a specific date range from Google Analytics 4. 360 | 361 | Args: 362 | property_id: Google Analytics 4 property ID (numeric, e.g., "123456789") 363 | start_date: Start date in YYYY-MM-DD format 364 | end_date: End date in YYYY-MM-DD format 365 | dimensions: List of dimensions to group by (optional, defaults to ["eventName"]) 366 | 367 | Returns: 368 | Event metrics grouped by specified dimensions in JSON format 369 | """ 370 | if ctx: 371 | ctx.info(f"Getting events for property {property_id} from {start_date} to {end_date}...") 372 | 373 | try: 374 | # This will automatically trigger OAuth flow if needed 375 | headers = get_headers_with_auto_token() 376 | 377 | url = f"https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport" 378 | 379 | # Build payload 380 | payload = { 381 | 'dateRanges': [{'startDate': start_date, 'endDate': end_date}], 382 | 'metrics': [{'name': 'eventCount'}] 383 | } 384 | 385 | # Add dimensions if provided 386 | if dimensions and len(dimensions) > 0: 387 | payload['dimensions'] = [{'name': dim} for dim in dimensions] 388 | else: 389 | # Default to eventName if no dimensions specified 390 | payload['dimensions'] = [{'name': 'eventName'}] 391 | 392 | response = requests.post(url, headers=headers, json=payload) 393 | 394 | if not response.ok: 395 | if ctx: 396 | ctx.error(f"Google Analytics API error: {response.status_code} {response.reason}") 397 | raise Exception(f"Google Analytics API error: {response.status_code} {response.reason} - {response.text}") 398 | 399 | results = response.json() 400 | 401 | # Check if no results found 402 | if not results.get('rows') or len(results.get('rows', [])) == 0: 403 | message = f"No events data found for property {property_id} from {start_date} to {end_date}" 404 | if ctx: 405 | ctx.info(message) 406 | return {'message': message} 407 | 408 | if ctx: 409 | ctx.info(f"Found {len(results.get('rows', []))} rows of events data.") 410 | 411 | return results 412 | 413 | except Exception as e: 414 | if ctx: 415 | ctx.error(f"Error getting events: {str(e)}") 416 | raise 417 | 418 | @mcp.tool 419 | def get_traffic_sources( 420 | property_id: str, 421 | start_date: str, 422 | end_date: str, 423 | dimensions: Optional[List[str]] = None, 424 | ctx: Context = None 425 | ) -> Dict[str, Any]: 426 | """Get traffic source metrics for a specific date range from Google Analytics 4. 427 | 428 | Args: 429 | property_id: Google Analytics 4 property ID (numeric, e.g., "123456789") 430 | start_date: Start date in YYYY-MM-DD format 431 | end_date: End date in YYYY-MM-DD format 432 | dimensions: List of dimensions to group by (optional, defaults to ["source", "medium"]) 433 | 434 | Returns: 435 | Traffic source metrics grouped by specified dimensions in JSON format 436 | """ 437 | if ctx: 438 | ctx.info(f"Getting traffic sources for property {property_id} from {start_date} to {end_date}...") 439 | 440 | try: 441 | # This will automatically trigger OAuth flow if needed 442 | headers = get_headers_with_auto_token() 443 | 444 | url = f"https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport" 445 | 446 | # Build payload 447 | payload = { 448 | 'dateRanges': [{'startDate': start_date, 'endDate': end_date}], 449 | 'metrics': [{'name': 'sessions'}, {'name': 'totalUsers'}] 450 | } 451 | 452 | # Add dimensions if provided 453 | if dimensions and len(dimensions) > 0: 454 | payload['dimensions'] = [{'name': dim} for dim in dimensions] 455 | else: 456 | # Default to source and medium if no dimensions specified 457 | payload['dimensions'] = [{'name': 'source'}, {'name': 'medium'}] 458 | 459 | response = requests.post(url, headers=headers, json=payload) 460 | 461 | if not response.ok: 462 | if ctx: 463 | ctx.error(f"Google Analytics API error: {response.status_code} {response.reason}") 464 | raise Exception(f"Google Analytics API error: {response.status_code} {response.reason} - {response.text}") 465 | 466 | results = response.json() 467 | 468 | # Check if no results found 469 | if not results.get('rows') or len(results.get('rows', [])) == 0: 470 | message = f"No traffic sources data found for property {property_id} from {start_date} to {end_date}" 471 | if ctx: 472 | ctx.info(message) 473 | return {'message': message} 474 | 475 | if ctx: 476 | ctx.info(f"Found {len(results.get('rows', []))} rows of traffic sources data.") 477 | 478 | return results 479 | 480 | except Exception as e: 481 | if ctx: 482 | ctx.error(f"Error getting traffic sources: {str(e)}") 483 | raise 484 | 485 | @mcp.tool 486 | def get_device_metrics( 487 | property_id: str, 488 | start_date: str, 489 | end_date: str, 490 | dimensions: Optional[List[str]] = None, 491 | ctx: Context = None 492 | ) -> Dict[str, Any]: 493 | """Get device metrics for a specific date range from Google Analytics 4. 494 | 495 | Args: 496 | property_id: Google Analytics 4 property ID (numeric, e.g., "123456789") 497 | start_date: Start date in YYYY-MM-DD format 498 | end_date: End date in YYYY-MM-DD format 499 | dimensions: List of dimensions to group by (optional, defaults to ["deviceCategory"]) 500 | 501 | Returns: 502 | Device metrics grouped by specified dimensions in JSON format 503 | """ 504 | if ctx: 505 | ctx.info(f"Getting device metrics for property {property_id} from {start_date} to {end_date}...") 506 | 507 | try: 508 | # This will automatically trigger OAuth flow if needed 509 | headers = get_headers_with_auto_token() 510 | 511 | url = f"https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport" 512 | 513 | # Build payload 514 | payload = { 515 | 'dateRanges': [{'startDate': start_date, 'endDate': end_date}], 516 | 'metrics': [{'name': 'sessions'}, {'name': 'screenPageViews'}] 517 | } 518 | 519 | # Add dimensions if provided 520 | if dimensions and len(dimensions) > 0: 521 | payload['dimensions'] = [{'name': dim} for dim in dimensions] 522 | else: 523 | # Default to deviceCategory if no dimensions specified 524 | payload['dimensions'] = [{'name': 'deviceCategory'}] 525 | 526 | response = requests.post(url, headers=headers, json=payload) 527 | 528 | if not response.ok: 529 | if ctx: 530 | ctx.error(f"Google Analytics API error: {response.status_code} {response.reason}") 531 | raise Exception(f"Google Analytics API error: {response.status_code} {response.reason} - {response.text}") 532 | 533 | results = response.json() 534 | 535 | # Check if no results found 536 | if not results.get('rows') or len(results.get('rows', [])) == 0: 537 | message = f"No device metrics data found for property {property_id} from {start_date} to {end_date}" 538 | if ctx: 539 | ctx.info(message) 540 | return {'message': message} 541 | 542 | if ctx: 543 | ctx.info(f"Found {len(results.get('rows', []))} rows of device metrics data.") 544 | 545 | return results 546 | 547 | except Exception as e: 548 | if ctx: 549 | ctx.error(f"Error getting device metrics: {str(e)}") 550 | raise 551 | 552 | @mcp.tool 553 | def run_report( 554 | property_id: str, 555 | start_date: str, 556 | end_date: str, 557 | metrics: List[str], 558 | dimensions: Optional[List[str]] = None, 559 | limit: Optional[int] = None, 560 | offset: Optional[int] = None, 561 | order_bys: Optional[List[Dict[str, Any]]] = None, 562 | dimension_filter: Optional[Dict[str, Any]] = None, 563 | metric_filter: Optional[Dict[str, Any]] = None, 564 | keep_empty_rows: Optional[bool] = None, 565 | ctx: Context = None 566 | ) -> Dict[str, Any]: 567 | """Execute a comprehensive Google Analytics 4 report with full customization capabilities. 568 | 569 | IMPORTANT: Use STRING ARRAYS for metrics and dimensions, NOT objects! 570 | 571 | CORRECT FORMAT: 572 | - metrics: ["sessions", "totalUsers", "screenPageViews"] 573 | - dimensions: ["country", "deviceCategory"] 574 | 575 | INCORRECT FORMAT (will fail): 576 | - metrics: [{"name": "sessions"}] 577 | - dimensions: [{"name": "country"}] 578 | 579 | VALID GA4 METRICS: 580 | - sessions, totalUsers, activeUsers, newUsers 581 | - screenPageViews, pageviews, bounceRate, engagementRate 582 | - averageSessionDuration, userEngagementDuration, engagedSessions 583 | - conversions, totalRevenue, purchaseRevenue 584 | - eventCount, eventsPerSession 585 | 586 | COMMON METRIC MISTAKES: 587 | - uniquePageviews (not valid) → use screenPageViews 588 | - pageViews (not valid) → use screenPageViews 589 | - users (not valid) → use totalUsers or activeUsers 590 | - sessionDuration (not valid) → use averageSessionDuration 591 | - conversionsPerSession (not valid) → use eventsPerSession 592 | - conversionRate (not valid) → calculate manually 593 | 594 | VALID GA4 DIMENSIONS: 595 | - country, city, region, continent 596 | - deviceCategory, operatingSystem, browser 597 | - source, medium, campaignName, sessionDefaultChannelGroup 598 | - pagePath, pageTitle, landingPage 599 | - date, month, year, hour, dayOfWeek 600 | - sessionSource, sessionMedium, sessionCampaignName 601 | 602 | COMMON DIMENSION MISTAKES: 603 | - channelGroup (not valid) → use sessionDefaultChannelGroup 604 | - sessionCampaign (not valid) → use sessionCampaignName 605 | - campaign (not valid) → use campaignName 606 | 607 | SORTING (order_bys) - EXPERIMENTAL: 608 | - For metrics: [{"metric": {"metricName": "sessions"}, "desc": true}] 609 | - For dimensions: [{"dimension": {"dimensionName": "country"}, "desc": false}] 610 | - WARNING: Sorting may fail due to JSON parsing issues. Test without sorting first. 611 | 612 | Args: 613 | property_id: Google Analytics 4 property ID (numeric, e.g., "421301275") 614 | start_date: Start date in YYYY-MM-DD format (e.g., "2025-01-01") 615 | end_date: End date in YYYY-MM-DD format (e.g., "2025-01-31") 616 | metrics: Array of metric names as STRINGS (e.g., ["sessions", "totalUsers"]) 617 | dimensions: Optional array of dimension names as STRINGS (e.g., ["country", "deviceCategory"]) 618 | limit: Optional maximum number of rows (default: 100) 619 | offset: Optional number of rows to skip (default: 0) 620 | order_bys: Optional sorting - see format above 621 | dimension_filter: Optional filter for dimensions 622 | metric_filter: Optional filter for metrics 623 | keep_empty_rows: Optional boolean to include empty rows 624 | 625 | Returns: 626 | Comprehensive JSON report with requested metrics and dimensions 627 | 628 | WORKING EXAMPLES: 629 | 630 | Basic Sessions Report: 631 | { 632 | "property_id": "421301275", 633 | "start_date": "2025-01-01", 634 | "end_date": "2025-01-31", 635 | "metrics": ["sessions", "totalUsers", "screenPageViews"] 636 | } 637 | 638 | Traffic by Country: 639 | { 640 | "property_id": "421301275", 641 | "start_date": "2025-01-01", 642 | "end_date": "2025-01-31", 643 | "metrics": ["sessions", "totalUsers"], 644 | "dimensions": ["country", "deviceCategory"], 645 | "limit": 20 646 | } 647 | 648 | Top Pages Report: 649 | { 650 | "property_id": "421301275", 651 | "start_date": "2025-01-01", 652 | "end_date": "2025-01-31", 653 | "metrics": ["screenPageViews", "sessions"], 654 | "dimensions": ["pagePath"], 655 | "limit": 10 656 | } 657 | """ 658 | if ctx: 659 | ctx.info(f"Running comprehensive report for property {property_id} from {start_date} to {end_date}...") 660 | ctx.info(f"Metrics: {', '.join(metrics)}") 661 | if dimensions: 662 | ctx.info(f"Dimensions: {', '.join(dimensions)}") 663 | 664 | try: 665 | # Basic validation only 666 | if not property_id or not isinstance(property_id, str): 667 | raise ValueError("property_id is required and must be a string") 668 | 669 | if not start_date or not isinstance(start_date, str): 670 | raise ValueError("start_date is required and must be a string in YYYY-MM-DD format") 671 | 672 | if not end_date or not isinstance(end_date, str): 673 | raise ValueError("end_date is required and must be a string in YYYY-MM-DD format") 674 | 675 | if not metrics or not isinstance(metrics, list) or len(metrics) == 0: 676 | raise ValueError("metrics is required and must be a non-empty array") 677 | 678 | # This will automatically trigger OAuth flow if needed 679 | headers = get_headers_with_auto_token() 680 | 681 | url = f"https://analyticsdata.googleapis.com/v1beta/properties/{property_id}:runReport" 682 | 683 | # Construct the payload 684 | payload = { 685 | 'dateRanges': [{'startDate': start_date, 'endDate': end_date}], 686 | 'metrics': [{'name': metric.strip()} for metric in metrics] 687 | } 688 | 689 | # Add optional parameters 690 | if dimensions and isinstance(dimensions, list) and len(dimensions) > 0: 691 | payload['dimensions'] = [{'name': dimension.strip()} for dimension in dimensions] 692 | 693 | if limit is not None and isinstance(limit, int) and limit > 0: 694 | payload['limit'] = limit 695 | 696 | if offset is not None and isinstance(offset, int) and offset >= 0: 697 | payload['offset'] = offset 698 | 699 | if order_bys and isinstance(order_bys, list) and len(order_bys) > 0: 700 | payload['orderBys'] = order_bys 701 | 702 | if dimension_filter and isinstance(dimension_filter, dict): 703 | payload['dimensionFilter'] = dimension_filter 704 | 705 | if metric_filter and isinstance(metric_filter, dict): 706 | payload['metricFilter'] = metric_filter 707 | 708 | if keep_empty_rows is not None and isinstance(keep_empty_rows, bool): 709 | payload['keepEmptyRows'] = keep_empty_rows 710 | 711 | response = requests.post(url, headers=headers, json=payload) 712 | 713 | if not response.ok: 714 | if ctx: 715 | ctx.error(f"Google Analytics API error: {response.status_code} {response.reason}") 716 | raise Exception(f"Google Analytics API error: {response.status_code} {response.reason} - {response.text}") 717 | 718 | results = response.json() 719 | 720 | # Check if no results found 721 | if not results.get('rows') or len(results.get('rows', [])) == 0: 722 | message = f"No data found for property {property_id} from {start_date} to {end_date}" 723 | if ctx: 724 | ctx.info(message) 725 | return { 726 | 'message': message, 727 | 'property_id': property_id, 728 | 'start_date': start_date, 729 | 'end_date': end_date, 730 | 'metrics_requested': metrics, 731 | 'dimensions_requested': dimensions or [], 732 | 'total_rows': 0 733 | } 734 | 735 | if ctx: 736 | ctx.info(f"Report completed successfully. Found {len(results.get('rows', []))} rows of data.") 737 | 738 | # Add metadata to results 739 | results['metadata'] = { 740 | 'property_id': property_id, 741 | 'start_date': start_date, 742 | 'end_date': end_date, 743 | 'metrics_requested': metrics, 744 | 'dimensions_requested': dimensions or [], 745 | 'total_rows': len(results.get('rows', [])) 746 | } 747 | 748 | return results 749 | 750 | except Exception as e: 751 | if ctx: 752 | ctx.error(f"Error running report: {str(e)}") 753 | raise 754 | 755 | @mcp.resource("ga4://reference") 756 | def ga4_reference() -> str: 757 | """Google Analytics 4 API reference documentation.""" 758 | return """ 759 | ## Google Analytics 4 API Reference 760 | 761 | ### Common Metrics 762 | - sessions: Number of sessions 763 | - totalUsers: Total number of users 764 | - activeUsers: Number of active users 765 | - newUsers: Number of new users 766 | - screenPageViews: Number of page/screen views 767 | - bounceRate: Bounce rate percentage 768 | - engagementRate: Engagement rate percentage 769 | - averageSessionDuration: Average session duration 770 | - conversions: Number of conversions 771 | - totalRevenue: Total revenue 772 | - eventCount: Number of events 773 | 774 | ### Common Dimensions 775 | - country: Country name 776 | - city: City name 777 | - deviceCategory: Device category (mobile, desktop, tablet) 778 | - source: Traffic source 779 | - medium: Traffic medium 780 | - campaignName: Campaign name 781 | - pagePath: Page path 782 | - eventName: Event name 783 | - date: Date (YYYYMMDD format) 784 | - month: Month 785 | - year: Year 786 | 787 | ### Date Format 788 | All dates should be in YYYY-MM-DD format (e.g., "2025-01-01") 789 | 790 | ### Example API Calls 791 | 792 | 1. Basic page views: 793 | get_page_views(property_id="123456789", start_date="2025-01-01", end_date="2025-01-31") 794 | 795 | 2. Traffic sources: 796 | get_traffic_sources(property_id="123456789", start_date="2025-01-01", end_date="2025-01-31") 797 | 798 | 3. Custom report: 799 | run_report( 800 | property_id="123456789", 801 | start_date="2025-01-01", 802 | end_date="2025-01-31", 803 | metrics=["sessions", "totalUsers", "screenPageViews"], 804 | dimensions=["country", "deviceCategory"] 805 | ) 806 | """ 807 | 808 | if __name__ == "__main__": 809 | import sys 810 | 811 | # Check command line arguments for transport mode 812 | if "--http" in sys.argv: 813 | logger.info("Starting with HTTP transport on http://127.0.0.1:8000/mcp") 814 | mcp.run(transport="streamable-http", host="127.0.0.1", port=8000, path="/mcp") 815 | else: 816 | # Default to STDIO for Claude Desktop compatibility 817 | logger.info("Starting with STDIO transport for Claude Desktop") 818 | mcp.run(transport="stdio") ```