#
tokens: 17378/50000 8/8 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
  4 | [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
  5 | [![FastMCP](https://img.shields.io/badge/FastMCP-v2.8.0-green.svg)](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")
```