Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions analytics_mcp/tools/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,20 @@

"""Client initialization for the Google Analytics APIs."""

import contextlib
import subprocess
import threading
from importlib import metadata
from unittest.mock import patch

import google.auth
from google.analytics import (
admin_v1beta,
data_v1beta,
admin_v1alpha,
data_v1alpha,
)
from google.api_core.gapic_v1.client_info import ClientInfo
from importlib import metadata
import google.auth
import threading


def _get_package_version_with_fallback():
Expand Down Expand Up @@ -52,13 +56,33 @@ def _get_package_version_with_fallback():
_CREDENTIALS = None


@contextlib.contextmanager
def prevent_stdio_inheritance():
"""Prevents child processes from inheriting the parent's stdio handles.

Fixes a deadlock on Windows where `google.auth.default()` spawns `gcloud`
via subprocess without redirecting stdin, causing it to inherit the
ProactorEventLoop's overlapping I/O handles used by MCP's stdio transport.
"""
original_popen = subprocess.Popen

def safe_popen(*args, **kwargs):
if kwargs.get("stdin") is None:
kwargs["stdin"] = subprocess.DEVNULL
return original_popen(*args, **kwargs)

with patch("subprocess.Popen", new=safe_popen):
yield


def _get_credentials():
global _CREDENTIALS
# Expected to be called under _client_lock
if _CREDENTIALS is None:
_CREDENTIALS, _ = google.auth.default(
scopes=[_READ_ONLY_ANALYTICS_SCOPE]
)
with prevent_stdio_inheritance():
_CREDENTIALS, _ = google.auth.default(
scopes=[_READ_ONLY_ANALYTICS_SCOPE]
)
return _CREDENTIALS


Expand Down