Skip to content

Commit aad1f32

Browse files
authored
Merge branch 'main' into main
2 parents 1574046 + 39ff9fa commit aad1f32

2 files changed

Lines changed: 98 additions & 17 deletions

File tree

mcp_proxy_for_aws/client.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import boto3
1616
import logging
1717
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
18+
from botocore.credentials import Credentials
1819
from contextlib import _AsyncGeneratorContextManager
1920
from datetime import timedelta
2021
from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client
@@ -32,6 +33,7 @@ def aws_iam_streamablehttp_client(
3233
aws_service: str,
3334
aws_region: Optional[str] = None,
3435
aws_profile: Optional[str] = None,
36+
credentials: Optional[Credentials] = None,
3537
headers: Optional[dict[str, str]] = None,
3638
timeout: float | timedelta = 30,
3739
sse_read_timeout: float | timedelta = 60 * 5,
@@ -55,6 +57,7 @@ def aws_iam_streamablehttp_client(
5557
aws_service: The name of the AWS service the MCP server is hosted on, e.g. "bedrock-agentcore".
5658
aws_region: The AWS region name of the MCP server, e.g. "us-west-2".
5759
aws_profile: The AWS profile to use for authentication.
60+
credentials: Optional AWS credentials from boto3/botocore. If provided, takes precedence over aws_profile.
5861
headers: Optional additional HTTP headers to include in requests.
5962
timeout: Request timeout in seconds or timedelta object. Defaults to 30 seconds.
6063
sse_read_timeout: Server-sent events read timeout in seconds or timedelta object.
@@ -78,28 +81,37 @@ def aws_iam_streamablehttp_client(
7881
"""
7982
logger.debug('Preparing AWS IAM MCP client for endpoint: %s', endpoint)
8083

81-
kwargs = {}
82-
if aws_profile is not None:
83-
kwargs['profile_name'] = aws_profile
84-
if aws_region is not None:
85-
kwargs['region_name'] = aws_region
84+
if credentials is not None:
85+
creds = credentials
86+
region = aws_region
87+
if not region:
88+
raise ValueError(
89+
'AWS region must be specified via aws_region parameter when using credentials.'
90+
)
91+
logger.debug('Using provided AWS credentials')
92+
else:
93+
kwargs = {}
94+
if aws_profile is not None:
95+
kwargs['profile_name'] = aws_profile
96+
if aws_region is not None:
97+
kwargs['region_name'] = aws_region
98+
99+
session = boto3.Session(**kwargs)
100+
creds = session.get_credentials()
101+
region = session.region_name
102+
103+
if not region:
104+
raise ValueError(
105+
'AWS region must be specified via aws_region parameter, AWS_REGION environment variable, or AWS config.'
106+
)
107+
108+
logger.debug('AWS profile: %s', session.profile_name)
86109

87-
session = boto3.Session(**kwargs)
88-
89-
profile = session.profile_name
90-
region = session.region_name
91-
92-
if not region:
93-
raise ValueError(
94-
'AWS region must be specified via aws_region parameter, AWS_PROFILE environment variable, or AWS config.'
95-
)
96-
97-
logger.debug('AWS profile: %s', profile)
98110
logger.debug('AWS region: %s', region)
99111
logger.debug('AWS service: %s', aws_service)
100112

101113
# Create a SigV4 authentication handler with AWS credentials
102-
auth = SigV4HTTPXAuth(session.get_credentials(), aws_service, region)
114+
auth = SigV4HTTPXAuth(creds, aws_service, region)
103115

104116
# Return the streamable HTTP client context manager with AWS IAM authentication
105117
return streamablehttp_client(

tests/unit/test_client.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Unit tests for the client, parameterized by internal call."""
1616

1717
import pytest
18+
from botocore.credentials import Credentials
1819
from datetime import timedelta
1920
from mcp_proxy_for_aws.client import aws_iam_streamablehttp_client
2021
from unittest.mock import AsyncMock, Mock, patch
@@ -210,3 +211,71 @@ async def mock_aexit(*_):
210211
pass
211212

212213
assert cleanup_called
214+
215+
216+
@pytest.mark.asyncio
217+
async def test_credentials_parameter_with_region(mock_streams):
218+
"""Test using provided credentials with aws_region."""
219+
mock_read, mock_write, mock_get_session = mock_streams
220+
creds = Credentials('test_key', 'test_secret', 'test_token')
221+
222+
with patch('mcp_proxy_for_aws.client.SigV4HTTPXAuth') as mock_auth_cls:
223+
with patch('mcp_proxy_for_aws.client.streamablehttp_client') as mock_stream_client:
224+
mock_auth = Mock()
225+
mock_auth_cls.return_value = mock_auth
226+
mock_stream_client.return_value.__aenter__ = AsyncMock(
227+
return_value=(mock_read, mock_write, mock_get_session)
228+
)
229+
mock_stream_client.return_value.__aexit__ = AsyncMock(return_value=None)
230+
231+
async with aws_iam_streamablehttp_client(
232+
endpoint='https://test.example.com/mcp',
233+
aws_service='bedrock-agentcore',
234+
aws_region='us-east-1',
235+
credentials=creds,
236+
):
237+
pass
238+
239+
mock_auth_cls.assert_called_once_with(creds, 'bedrock-agentcore', 'us-east-1')
240+
241+
242+
@pytest.mark.asyncio
243+
async def test_credentials_parameter_without_region_raises_error():
244+
"""Test that using credentials without aws_region raises ValueError."""
245+
creds = Credentials('test_key', 'test_secret', 'test_token')
246+
247+
with pytest.raises(
248+
ValueError,
249+
match='AWS region must be specified via aws_region parameter when using credentials',
250+
):
251+
async with aws_iam_streamablehttp_client(
252+
endpoint='https://test.example.com/mcp',
253+
aws_service='bedrock-agentcore',
254+
credentials=creds,
255+
):
256+
pass
257+
258+
259+
@pytest.mark.asyncio
260+
async def test_credentials_parameter_bypasses_boto3_session(mock_streams):
261+
"""Test that providing credentials bypasses boto3.Session creation."""
262+
mock_read, mock_write, mock_get_session = mock_streams
263+
creds = Credentials('test_key', 'test_secret', 'test_token')
264+
265+
with patch('boto3.Session') as mock_boto:
266+
with patch('mcp_proxy_for_aws.client.SigV4HTTPXAuth'):
267+
with patch('mcp_proxy_for_aws.client.streamablehttp_client') as mock_stream_client:
268+
mock_stream_client.return_value.__aenter__ = AsyncMock(
269+
return_value=(mock_read, mock_write, mock_get_session)
270+
)
271+
mock_stream_client.return_value.__aexit__ = AsyncMock(return_value=None)
272+
273+
async with aws_iam_streamablehttp_client(
274+
endpoint='https://test.example.com/mcp',
275+
aws_service='bedrock-agentcore',
276+
aws_region='us-west-2',
277+
credentials=creds,
278+
):
279+
pass
280+
281+
mock_boto.assert_not_called()

0 commit comments

Comments
 (0)