diff --git a/analytics_mcp/tools/client.py b/analytics_mcp/tools/client.py index f5ec0c4..93d2325 100644 --- a/analytics_mcp/tools/client.py +++ b/analytics_mcp/tools/client.py @@ -14,6 +14,13 @@ """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, @@ -21,9 +28,6 @@ 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(): @@ -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