Skip to content

Commit 3d2d45a

Browse files
Deprecating aws_iam_streamablehttp_client
1 parent ab72798 commit 3d2d45a

File tree

7 files changed

+361
-64
lines changed

7 files changed

+361
-64
lines changed

CHANGELOG.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## v1.2.0 (2026-01-08)
9+
10+
### Added
11+
12+
- New `aws_iam_streamable_http_client` function to replace deprecated `aws_iam_streamablehttp_client`
13+
14+
### Changed
15+
16+
- Updated minimum `fastmcp` version to 2.14.2 to support `streamable_http_client` function from mcp>=1.25.0
17+
18+
### Deprecated
19+
20+
- `aws_iam_streamablehttp_client` is now deprecated in favor of `aws_iam_streamable_http_client`
21+
to align with upstream MCP package naming conventions. The old function will be removed in version 2.0.0.
22+
- `sse_read_timeout` parameter in `aws_iam_streamable_http_client` is deprecated and will be removed in version 2.0.0
23+
824
## v1.1.5 (2025-12-15)
925

1026
### Fix

mcp_proxy_for_aws/client.py

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
# limitations under the License.
1414

1515
import boto3
16+
import httpx
1617
import logging
18+
import warnings
1719
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
1820
from botocore.credentials import Credentials
1921
from contextlib import _AsyncGeneratorContextManager
2022
from datetime import timedelta
21-
from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client
23+
from mcp.client.streamable_http import GetSessionIdCallback, streamable_http_client
2224
from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client
2325
from mcp.shared.message import SessionMessage
2426
from mcp_proxy_for_aws.sigv4_helper import SigV4HTTPXAuth
@@ -28,7 +30,7 @@
2830
logger = logging.getLogger(__name__)
2931

3032

31-
def aws_iam_streamablehttp_client(
33+
def aws_iam_streamable_http_client(
3234
endpoint: str,
3335
aws_service: str,
3436
aws_region: Optional[str] = None,
@@ -60,7 +62,7 @@ def aws_iam_streamablehttp_client(
6062
credentials: Optional AWS credentials from boto3/botocore. If provided, takes precedence over aws_profile.
6163
headers: Optional additional HTTP headers to include in requests.
6264
timeout: Request timeout in seconds or timedelta object. Defaults to 30 seconds.
63-
sse_read_timeout: Server-sent events read timeout in seconds or timedelta object.
65+
sse_read_timeout: Deprecated. This parameter is no longer used and will be removed in version 2.0.0.
6466
terminate_on_close: Whether to terminate the connection on close.
6567
httpx_client_factory: Factory function for creating HTTPX clients.
6668
@@ -71,7 +73,7 @@ def aws_iam_streamablehttp_client(
7173
- get_session_id: Callback function to retrieve the current session ID
7274
7375
Example:
74-
async with aws_iam_mcp_client(
76+
async with aws_iam_streamable_http_client(
7577
endpoint="https://example.com/mcp",
7678
aws_service="bedrock-agentcore",
7779
aws_region="us-west-2"
@@ -81,6 +83,13 @@ def aws_iam_streamablehttp_client(
8183
"""
8284
logger.debug('Preparing AWS IAM MCP client for endpoint: %s', endpoint)
8385

86+
# Warn if sse_read_timeout is set to a non-default value
87+
if sse_read_timeout != 60 * 5:
88+
logger.warning(
89+
'sse_read_timeout parameter is deprecated and will be removed in version 2.0.0. '
90+
'The value is ignored in the current implementation.'
91+
)
92+
8493
if credentials is not None:
8594
creds = credentials
8695
region = aws_region
@@ -113,13 +122,74 @@ def aws_iam_streamablehttp_client(
113122
# Create a SigV4 authentication handler with AWS credentials
114123
auth = SigV4HTTPXAuth(creds, aws_service, region)
115124

125+
# Convert timeout to httpx.Timeout if it's a number or timedelta
126+
httpx_timeout = None
127+
if timeout is not None:
128+
if isinstance(timeout, (int, float)):
129+
httpx_timeout = httpx.Timeout(timeout)
130+
elif isinstance(timeout, timedelta):
131+
httpx_timeout = httpx.Timeout(timeout.total_seconds())
132+
else:
133+
httpx_timeout = timeout
134+
135+
# Create HTTP client using the factory with authentication and custom headers
136+
http_client = httpx_client_factory(
137+
auth=auth,
138+
timeout=httpx_timeout,
139+
headers=headers,
140+
)
141+
116142
# Return the streamable HTTP client context manager with AWS IAM authentication
117-
return streamablehttp_client(
143+
return streamable_http_client(
118144
url=endpoint,
145+
http_client=http_client,
146+
terminate_on_close=terminate_on_close,
147+
)
148+
149+
150+
def aws_iam_streamablehttp_client(
151+
endpoint: str,
152+
aws_service: str,
153+
aws_region: Optional[str] = None,
154+
aws_profile: Optional[str] = None,
155+
credentials: Optional[Credentials] = None,
156+
headers: Optional[dict[str, str]] = None,
157+
timeout: float | timedelta = 30,
158+
sse_read_timeout: float | timedelta = 60 * 5,
159+
terminate_on_close: bool = True,
160+
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
161+
) -> _AsyncGeneratorContextManager[
162+
tuple[
163+
MemoryObjectReceiveStream[SessionMessage | Exception],
164+
MemoryObjectSendStream[SessionMessage],
165+
GetSessionIdCallback,
166+
],
167+
None,
168+
]:
169+
"""Create an AWS IAM-authenticated MCP streamable HTTP client.
170+
171+
.. deprecated:: 1.2.0
172+
Use :func:`aws_iam_streamable_http_client` instead.
173+
This function will be removed in version 2.0.0.
174+
175+
This is a deprecated alias for aws_iam_streamable_http_client.
176+
Please update your code to use aws_iam_streamable_http_client instead.
177+
"""
178+
warnings.warn(
179+
"aws_iam_streamablehttp_client is deprecated and will be removed in version 2.0.0. "
180+
"Use aws_iam_streamable_http_client instead.",
181+
DeprecationWarning,
182+
stacklevel=2,
183+
)
184+
return aws_iam_streamable_http_client(
185+
endpoint=endpoint,
186+
aws_service=aws_service,
187+
aws_region=aws_region,
188+
aws_profile=aws_profile,
189+
credentials=credentials,
119190
headers=headers,
120191
timeout=timeout,
121192
sse_read_timeout=sse_read_timeout,
122193
terminate_on_close=terminate_on_close,
123194
httpx_client_factory=httpx_client_factory,
124-
auth=auth,
125195
)

mcp_proxy_for_aws/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def client_factory(
5757
headers: Optional[Dict[str, str]] = None,
5858
timeout: Optional[httpx.Timeout] = None,
5959
auth: Optional[httpx.Auth] = None,
60+
**kwargs, # Accept additional parameters from fastmcp (e.g., follow_redirects)
6061
) -> httpx.AsyncClient:
6162
return create_sigv4_client(
6263
service=service,

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@ members = [
1010
name = "mcp-proxy-for-aws"
1111

1212
# NOTE: "Patch"=9223372036854775807 bumps next release to zero.
13-
version = "1.1.5"
13+
version = "1.2.0"
1414

1515
description = "MCP Proxy for AWS"
1616
readme = "README.md"
1717
requires-python = ">=3.10,<3.14"
1818
dependencies = [
19-
"fastmcp (>=2.13.1,<2.14.1)",
19+
"fastmcp>=2.14.2",
2020
"boto3>=1.41.0",
2121
"botocore[crt]>=1.41.0",
2222
]

tests/integ/mcp/simple_mcp_client.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,11 @@ def _build_mcp_config(endpoint: str, region_name: str, metadata: Optional[Dict[s
6969
'AWS_REGION': region_name,
7070
'AWS_ACCESS_KEY_ID': credentials.access_key,
7171
'AWS_SECRET_ACCESS_KEY': credentials.secret_key,
72-
'AWS_SESSION_TOKEN': credentials.token,
7372
}
73+
74+
# Only include AWS_SESSION_TOKEN if it's not None (e.g., for temporary credentials)
75+
if credentials.token:
76+
environment_variables['AWS_SESSION_TOKEN'] = credentials.token
7477

7578
args = _build_args(endpoint, region_name, metadata)
7679

tests/unit/test_client.py

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ async def test_boto3_session_parameters(
6060
mock_read, mock_write, mock_get_session = mock_streams
6161

6262
with patch('boto3.Session', return_value=mock_session) as mock_boto:
63-
with patch('mcp_proxy_for_aws.client.streamablehttp_client') as mock_stream_client:
63+
with patch('mcp_proxy_for_aws.client.streamable_http_client') as mock_stream_client:
6464
mock_stream_client.return_value.__aenter__ = AsyncMock(
6565
return_value=(mock_read, mock_write, mock_get_session)
6666
)
@@ -94,9 +94,14 @@ async def test_sigv4_auth_is_created_and_used(mock_session, mock_streams, servic
9494

9595
with patch('boto3.Session', return_value=mock_session):
9696
with patch('mcp_proxy_for_aws.client.SigV4HTTPXAuth') as mock_auth_cls:
97-
with patch('mcp_proxy_for_aws.client.streamablehttp_client') as mock_stream_client:
97+
with patch('mcp_proxy_for_aws.client.streamable_http_client') as mock_stream_client:
9898
mock_auth = Mock()
9999
mock_auth_cls.return_value = mock_auth
100+
101+
# Mock the factory to capture its calls
102+
mock_http_client = Mock()
103+
mock_factory = Mock(return_value=mock_http_client)
104+
100105
mock_stream_client.return_value.__aenter__ = AsyncMock(
101106
return_value=(mock_read, mock_write, mock_get_session)
102107
)
@@ -106,17 +111,22 @@ async def test_sigv4_auth_is_created_and_used(mock_session, mock_streams, servic
106111
endpoint='https://test.example.com/mcp',
107112
aws_service=service_name,
108113
aws_region=region,
114+
httpx_client_factory=mock_factory,
109115
):
110116
pass
111117

112118
mock_auth_cls.assert_called_once_with(
113119
# Auth should be constructed with the resolved credentials, service, and region,
114-
# and passed into the streamable client.
120+
# and passed to the httpx client factory.
115121
mock_session.get_credentials.return_value,
116122
service_name,
117123
region,
118124
)
119-
assert mock_stream_client.call_args[1]['auth'] is mock_auth
125+
# Check that factory was called with auth
126+
assert mock_factory.called
127+
assert mock_factory.call_args[1]['auth'] is mock_auth
128+
# Check that http_client was passed to streamable_http_client
129+
assert mock_stream_client.call_args[1]['http_client'] is mock_http_client
120130

121131

122132
@pytest.mark.asyncio
@@ -137,7 +147,10 @@ async def test_streamable_client_parameters(
137147
mock_read, mock_write, mock_get_session = mock_streams
138148

139149
with patch('boto3.Session', return_value=mock_session):
140-
with patch('mcp_proxy_for_aws.client.streamablehttp_client') as mock_stream_client:
150+
with patch('mcp_proxy_for_aws.client.streamable_http_client') as mock_stream_client:
151+
mock_http_client = Mock()
152+
mock_factory = Mock(return_value=mock_http_client)
153+
141154
mock_stream_client.return_value.__aenter__ = AsyncMock(
142155
return_value=(mock_read, mock_write, mock_get_session)
143156
)
@@ -150,16 +163,30 @@ async def test_streamable_client_parameters(
150163
timeout=timeout_value,
151164
sse_read_timeout=sse_value,
152165
terminate_on_close=terminate_value,
166+
httpx_client_factory=mock_factory,
153167
):
154168
pass
155169

156-
call_kwargs = mock_stream_client.call_args[1]
157-
# Confirm each parameter is forwarded unchanged.
158-
assert call_kwargs['url'] == 'https://test.example.com/mcp'
159-
assert call_kwargs['headers'] == headers
160-
assert call_kwargs['timeout'] == timeout_value
161-
assert call_kwargs['sse_read_timeout'] == sse_value
162-
assert call_kwargs['terminate_on_close'] == terminate_value
170+
# Check that factory was called with headers and timeout
171+
assert mock_factory.called
172+
factory_kwargs = mock_factory.call_args[1]
173+
assert factory_kwargs['headers'] == headers
174+
# Check timeout conversion
175+
if isinstance(timeout_value, timedelta):
176+
expected_timeout = timeout_value.total_seconds()
177+
else:
178+
expected_timeout = timeout_value
179+
# httpx.Timeout sets all timeout types (connect, read, write, pool) to the same value
180+
assert factory_kwargs['timeout'].connect == expected_timeout
181+
assert factory_kwargs['timeout'].read == expected_timeout
182+
assert factory_kwargs['timeout'].write == expected_timeout
183+
assert factory_kwargs['timeout'].pool == expected_timeout
184+
185+
# Check streamable_http_client was called correctly
186+
stream_kwargs = mock_stream_client.call_args[1]
187+
assert stream_kwargs['url'] == 'https://test.example.com/mcp'
188+
assert stream_kwargs['http_client'] is mock_http_client
189+
assert stream_kwargs['terminate_on_close'] == terminate_value
163190

164191

165192
@pytest.mark.asyncio
@@ -170,7 +197,9 @@ async def test_custom_httpx_client_factory_is_passed(mock_session, mock_streams)
170197
custom_factory = Mock()
171198

172199
with patch('boto3.Session', return_value=mock_session):
173-
with patch('mcp_proxy_for_aws.client.streamablehttp_client') as mock_stream_client:
200+
with patch('mcp_proxy_for_aws.client.streamable_http_client') as mock_stream_client:
201+
mock_http_client = Mock()
202+
custom_factory.return_value = mock_http_client
174203
mock_stream_client.return_value.__aenter__ = AsyncMock(
175204
return_value=(mock_read, mock_write, mock_get_session)
176205
)
@@ -183,7 +212,10 @@ async def test_custom_httpx_client_factory_is_passed(mock_session, mock_streams)
183212
):
184213
pass
185214

186-
assert mock_stream_client.call_args[1]['httpx_client_factory'] is custom_factory
215+
# Check that the custom factory was called
216+
assert custom_factory.called
217+
# Check that the http_client from custom factory was passed to streamable_http_client
218+
assert mock_stream_client.call_args[1]['http_client'] is mock_http_client
187219

188220

189221
@pytest.mark.asyncio
@@ -198,7 +230,7 @@ async def mock_aexit(*_):
198230
cleanup_called = True
199231

200232
with patch('boto3.Session', return_value=mock_session):
201-
with patch('mcp_proxy_for_aws.client.streamablehttp_client') as mock_stream_client:
233+
with patch('mcp_proxy_for_aws.client.streamable_http_client') as mock_stream_client:
202234
mock_stream_client.return_value.__aenter__ = AsyncMock(
203235
return_value=(mock_read, mock_write, mock_get_session)
204236
)
@@ -220,7 +252,7 @@ async def test_credentials_parameter_with_region(mock_streams):
220252
creds = Credentials('test_key', 'test_secret', 'test_token')
221253

222254
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:
255+
with patch('mcp_proxy_for_aws.client.streamable_http_client') as mock_stream_client:
224256
mock_auth = Mock()
225257
mock_auth_cls.return_value = mock_auth
226258
mock_stream_client.return_value.__aenter__ = AsyncMock(
@@ -264,7 +296,7 @@ async def test_credentials_parameter_bypasses_boto3_session(mock_streams):
264296

265297
with patch('boto3.Session') as mock_boto:
266298
with patch('mcp_proxy_for_aws.client.SigV4HTTPXAuth'):
267-
with patch('mcp_proxy_for_aws.client.streamablehttp_client') as mock_stream_client:
299+
with patch('mcp_proxy_for_aws.client.streamable_http_client') as mock_stream_client:
268300
mock_stream_client.return_value.__aenter__ = AsyncMock(
269301
return_value=(mock_read, mock_write, mock_get_session)
270302
)

0 commit comments

Comments
 (0)