Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## v1.2.0 (2026-01-09)

### Added

- New `aws_iam_streamable_http_client` function to replace deprecated `aws_iam_streamablehttp_client`

### Changed

- Updated minimum `fastmcp` version to 2.14.2 to support `streamable_http_client` function from mcp>=1.25.0
- **BREAKING**: Updated `aws_iam_streamable_http_client` signature to match upstream MCP patterns:
- Removed `headers`, `timeout`, `sse_read_timeout`, and `httpx_client_factory` parameters
- Added `http_client: httpx.AsyncClient | None` parameter for passing pre-configured clients
- Added `*` to make `http_client` and `terminate_on_close` keyword-only arguments
- Changed return type from `_AsyncGeneratorContextManager` to `AsyncGenerator` for consistency with upstream

### Deprecated

- `aws_iam_streamablehttp_client` is now deprecated in favor of `aws_iam_streamable_http_client`
to align with upstream MCP package naming conventions. The old function will be removed in version 2.0.0.

## v1.1.5 (2025-12-15)

### Fix
Expand Down
149 changes: 126 additions & 23 deletions mcp_proxy_for_aws/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,33 +13,34 @@
# limitations under the License.

import boto3
import httpx
import logging
from anyio.streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream
from botocore.credentials import Credentials
from contextlib import _AsyncGeneratorContextManager
from collections.abc import AsyncGenerator
from datetime import timedelta
from mcp.client.streamable_http import GetSessionIdCallback, streamablehttp_client
from mcp.client.streamable_http import GetSessionIdCallback, streamable_http_client
from mcp.shared._httpx_utils import McpHttpClientFactory, create_mcp_http_client
from mcp.shared.message import SessionMessage
from mcp_proxy_for_aws.sigv4_helper import SigV4HTTPXAuth
from typing import Optional
from typing_extensions import deprecated
from contextlib import asynccontextmanager


logger = logging.getLogger(__name__)


def aws_iam_streamablehttp_client(
@asynccontextmanager
async def aws_iam_streamable_http_client(
endpoint: str,
aws_service: str,
aws_region: Optional[str] = None,
aws_profile: Optional[str] = None,
credentials: Optional[Credentials] = None,
headers: Optional[dict[str, str]] = None,
timeout: float | timedelta = 30,
sse_read_timeout: float | timedelta = 60 * 5,
aws_region: str | None = None,
aws_profile: str | None = None,
credentials: Credentials | None = None,
*,
http_client: httpx.AsyncClient | None = None,
terminate_on_close: bool = True,
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
) -> _AsyncGeneratorContextManager[
) -> AsyncGenerator[
tuple[
MemoryObjectReceiveStream[SessionMessage | Exception],
MemoryObjectSendStream[SessionMessage],
Expand All @@ -58,20 +59,17 @@ def aws_iam_streamablehttp_client(
aws_region: The AWS region name of the MCP server, e.g. "us-west-2".
aws_profile: The AWS profile to use for authentication.
credentials: Optional AWS credentials from boto3/botocore. If provided, takes precedence over aws_profile.
headers: Optional additional HTTP headers to include in requests.
timeout: Request timeout in seconds or timedelta object. Defaults to 30 seconds.
sse_read_timeout: Server-sent events read timeout in seconds or timedelta object.
http_client: Optional pre-configured httpx.AsyncClient. If not provided, one will be created with SigV4 auth.
terminate_on_close: Whether to terminate the connection on close.
httpx_client_factory: Factory function for creating HTTPX clients.

Returns:
An async generator context manager that yields a tuple of transport components:
An async generator yielding a tuple containing:
- read_stream: MemoryObjectReceiveStream for reading server responses
- write_stream: MemoryObjectSendStream for sending requests to server
- get_session_id: Callback function to retrieve the current session ID

Example:
async with aws_iam_mcp_client(
async with aws_iam_streamable_http_client(
endpoint="https://example.com/mcp",
aws_service="bedrock-agentcore",
aws_region="us-west-2"
Expand All @@ -81,6 +79,18 @@ def aws_iam_streamablehttp_client(
"""
logger.debug('Preparing AWS IAM MCP client for endpoint: %s', endpoint)

# If http_client is provided, use it directly
if http_client is not None:
logger.debug('Using provided http_client')
async with streamable_http_client(
url=endpoint,
http_client=http_client,
terminate_on_close=terminate_on_close,
) as streams:
yield streams
return

# Otherwise, create http_client with AWS IAM authentication
if credentials is not None:
creds = credentials
region = aws_region
Expand Down Expand Up @@ -113,13 +123,106 @@ def aws_iam_streamablehttp_client(
# Create a SigV4 authentication handler with AWS credentials
auth = SigV4HTTPXAuth(creds, aws_service, region)

# Create HTTP client with AWS IAM authentication
client = httpx.AsyncClient(
auth=auth,
headers={'Accept': 'application/json, text/event-stream'},
)

# Return the streamable HTTP client context manager with AWS IAM authentication
return streamablehttp_client(
async with streamable_http_client(
url=endpoint,
headers=headers,
timeout=timeout,
sse_read_timeout=sse_read_timeout,
http_client=client,
terminate_on_close=terminate_on_close,
httpx_client_factory=httpx_client_factory,
) as streams:
yield streams


@asynccontextmanager
@deprecated("Use `aws_iam_streamable_http_client` instead.")
async def aws_iam_streamablehttp_client(
endpoint: str,
aws_service: str,
aws_region: str | None = None,
aws_profile: str | None = None,
credentials: Credentials | None = None,
headers: dict[str, str] | None = None,
timeout: float | timedelta = 30,
sse_read_timeout: float | timedelta = 60 * 5,
terminate_on_close: bool = True,
httpx_client_factory: McpHttpClientFactory = create_mcp_http_client,
) -> AsyncGenerator[
tuple[
MemoryObjectReceiveStream[SessionMessage | Exception],
MemoryObjectSendStream[SessionMessage],
GetSessionIdCallback,
],
None,
]:
"""Create an AWS IAM-authenticated MCP streamable HTTP client.

This is a deprecated alias for aws_iam_streamable_http_client.
Please update your code to use aws_iam_streamable_http_client instead.

This function maintains backward compatibility by accepting the legacy parameters
and creating a properly configured httpx.AsyncClient to pass to the new implementation.
"""
# Resolve credentials and region
if credentials is not None:
creds = credentials
region = aws_region
if not region:
raise ValueError(
'AWS region must be specified via aws_region parameter when using credentials.'
)
else:
kwargs = {}
if aws_profile is not None:
kwargs['profile_name'] = aws_profile
if aws_region is not None:
kwargs['region_name'] = aws_region

session = boto3.Session(**kwargs)
creds = session.get_credentials()
region = session.region_name

if not region:
raise ValueError(
'AWS region must be specified via aws_region parameter, AWS_REGION environment variable, or AWS config.'
)

# Create SigV4 authentication
auth = SigV4HTTPXAuth(creds, aws_service, region)

# Convert timeout to httpx.Timeout
if isinstance(timeout, timedelta):
timeout_seconds = timeout.total_seconds()
else:
timeout_seconds = timeout

if isinstance(sse_read_timeout, timedelta):
sse_timeout_seconds = sse_read_timeout.total_seconds()
else:
sse_timeout_seconds = sse_read_timeout

httpx_timeout = httpx.Timeout(timeout_seconds, read=sse_timeout_seconds)

# Create httpx client using the factory with legacy parameters
http_client = httpx_client_factory(
headers=headers,
timeout=httpx_timeout,
auth=auth,
)

# Delegate to the new function with the configured client
async with aws_iam_streamable_http_client(
endpoint=endpoint,
aws_service=aws_service,
aws_region=region,
aws_profile=aws_profile,
credentials=creds,
http_client=http_client,
terminate_on_close=terminate_on_close,
) as streams:
# Yield the streams tuple - @asynccontextmanager handles the rest
yield streams
1 change: 1 addition & 0 deletions mcp_proxy_for_aws/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ def client_factory(
headers: Optional[Dict[str, str]] = None,
timeout: Optional[httpx.Timeout] = None,
auth: Optional[httpx.Auth] = None,
**kwargs, # Accept additional parameters from fastmcp (e.g., follow_redirects)
) -> httpx.AsyncClient:
return create_sigv4_client(
service=service,
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ members = [
name = "mcp-proxy-for-aws"

# NOTE: "Patch"=9223372036854775807 bumps next release to zero.
version = "1.1.5"
version = "1.2.0"

description = "MCP Proxy for AWS"
readme = "README.md"
requires-python = ">=3.10,<3.14"
dependencies = [
"fastmcp (>=2.13.1,<2.14.1)",
"fastmcp>=2.14.2",
"boto3>=1.41.0",
"botocore[crt]>=1.41.0",
]
Expand Down
5 changes: 4 additions & 1 deletion tests/integ/mcp/simple_mcp_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,11 @@ def _build_mcp_config(endpoint: str, region_name: str, metadata: Optional[Dict[s
'AWS_REGION': region_name,
'AWS_ACCESS_KEY_ID': credentials.access_key,
'AWS_SECRET_ACCESS_KEY': credentials.secret_key,
'AWS_SESSION_TOKEN': credentials.token,
}

# Only include AWS_SESSION_TOKEN if it's not None (e.g., for temporary credentials)
if credentials.token:
environment_variables['AWS_SESSION_TOKEN'] = credentials.token

args = _build_args(endpoint, region_name, metadata)

Expand Down
Loading