Skip to content

Commit b2fcd15

Browse files
committed
fix: prevent credential_process hang on Windows in stdio transport mode
Monkey-patch botocore's ProcessProvider to pass stdin=subprocess.DEVNULL when spawning credential_process subprocesses. Without this, the child inherits the MCP JSON-RPC pipe as stdin, causing Popen.communicate() to hang indefinitely on Windows (IOCP) due to the open pipe handle. Fixes D454977820
1 parent 9da6e41 commit b2fcd15

2 files changed

Lines changed: 634 additions & 1 deletion

File tree

mcp_proxy_for_aws/sigv4_helper.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@
1818
import httpx
1919
import json
2020
import logging
21+
import subprocess
2122
from botocore.auth import SigV4Auth
2223
from botocore.awsrequest import AWSRequest
23-
from botocore.credentials import Credentials
24+
from botocore.compat import compat_shell_split
25+
from botocore.credentials import Credentials, ProcessProvider
2426
from functools import partial
2527
from httpx import __version__ as httpx_version
2628
from mcp_proxy_for_aws import __version__
@@ -30,6 +32,58 @@
3032

3133
logger = logging.getLogger(__name__)
3234

35+
36+
def _patch_credential_process_stdin():
37+
"""Patch botocore ProcessProvider to pass stdin=subprocess.DEVNULL.
38+
39+
When mcp-proxy-for-aws runs in stdio transport mode, its stdin is the MCP
40+
JSON-RPC pipe. Botocore's ProcessProvider spawns credential_process
41+
subprocesses without specifying stdin, so the child inherits the MCP pipe.
42+
On Windows this causes the subprocess to hang indefinitely because it holds
43+
an open handle to the pipe, blocking Popen.communicate() from completing.
44+
"""
45+
from botocore.exceptions import CredentialRetrievalError
46+
47+
def _retrieve_credentials_using(self, credential_process):
48+
process_list = compat_shell_split(credential_process)
49+
p = self._popen(
50+
process_list,
51+
stdout=subprocess.PIPE,
52+
stderr=subprocess.PIPE,
53+
stdin=subprocess.DEVNULL,
54+
)
55+
stdout, stderr = p.communicate()
56+
if p.returncode != 0:
57+
raise CredentialRetrievalError(provider=self.METHOD, error_msg=stderr.decode('utf-8'))
58+
parsed = json.loads(stdout.decode('utf-8'))
59+
version = parsed.get('Version', '<Version key not provided>')
60+
if version != 1:
61+
raise CredentialRetrievalError(
62+
provider=self.METHOD,
63+
error_msg=(
64+
f"Unsupported version '{version}' for credential process "
65+
f'provider, supported versions: 1'
66+
),
67+
)
68+
try:
69+
return {
70+
'access_key': parsed['AccessKeyId'],
71+
'secret_key': parsed['SecretAccessKey'],
72+
'token': parsed.get('SessionToken'),
73+
'expiry_time': parsed.get('Expiration'),
74+
'account_id': self._get_account_id(parsed),
75+
}
76+
except KeyError as e:
77+
raise CredentialRetrievalError(
78+
provider=self.METHOD,
79+
error_msg=f'Missing required key in response: {e}',
80+
)
81+
82+
ProcessProvider._retrieve_credentials_using = _retrieve_credentials_using
83+
84+
85+
_patch_credential_process_stdin()
86+
3387
# Headers that should be redacted when logging to prevent credential exposure
3488
SENSITIVE_HEADERS = frozenset({'authorization', 'x-amz-security-token', 'x-amz-date'})
3589

0 commit comments

Comments
 (0)