|
18 | 18 | import httpx |
19 | 19 | import json |
20 | 20 | import logging |
| 21 | +import subprocess |
21 | 22 | from botocore.auth import SigV4Auth |
22 | 23 | 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 |
24 | 26 | from functools import partial |
25 | 27 | from httpx import __version__ as httpx_version |
26 | 28 | from mcp_proxy_for_aws import __version__ |
|
30 | 32 |
|
31 | 33 | logger = logging.getLogger(__name__) |
32 | 34 |
|
| 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 | + |
33 | 87 | # Headers that should be redacted when logging to prevent credential exposure |
34 | 88 | SENSITIVE_HEADERS = frozenset({'authorization', 'x-amz-security-token', 'x-amz-date'}) |
35 | 89 |
|
|
0 commit comments