Skip to content

Commit 587e6d3

Browse files
committed
Cleanup: Removed dependencies, ran pre-commit with --run-all
1 parent 93c8bcf commit 587e6d3

14 files changed

Lines changed: 272 additions & 210 deletions

.github/workflows/python.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,4 @@ jobs:
178178
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
179179
with:
180180
name: sbom-aws-mcp-proxy
181-
path: sbom.json
181+
path: sbom.json

DEVELOPMENT.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ npx @modelcontextprotocol/inspector uv run \
1010
src/aws_mcp_proxy/server.py \
1111
--endpoint <your endpoint>
1212
```
13-
Then click connect in the opened browser window.
13+
Then click connect in the opened browser window.

pyproject.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,9 @@ requires-python = ">=3.10"
1010
dependencies = [
1111
"fastmcp>=2.11.2",
1212
"mcp>=1.12.3",
13-
"loguru>=0.7.0",
1413
"pydantic>=2.10.6",
1514
"boto3>=1.34.0",
1615
"botocore>=1.34.0",
17-
"requests_auth_aws_sigv4",
18-
"httpx-auth-awssigv4",
1916
]
2017
license = {text = "Apache-2.0"}
2118
license-files = ["LICENSE", "NOTICE" ]
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Logging configuration for AWS MCP Proxy."""
2+
3+
import logging
4+
import sys
5+
from typing import Optional
6+
7+
8+
def configure_logging(level: Optional[str] = None) -> None:
9+
"""Configure logging with a standard format and optional level.
10+
11+
Args:
12+
level: Optional logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
13+
If not provided, defaults to INFO.
14+
"""
15+
# Set default level to INFO if not provided
16+
log_level = getattr(logging, level.upper()) if level else logging.INFO
17+
18+
# Configure logging format
19+
log_format = '%(asctime)s | %(levelname)s | %(name)s | %(message)s'
20+
date_format = '%Y-%m-%d %H:%M:%S'
21+
22+
# Create console handler with formatting
23+
console_handler = logging.StreamHandler(sys.stdout)
24+
console_handler.setFormatter(logging.Formatter(log_format, date_format))
25+
26+
# Configure root logger
27+
root_logger = logging.getLogger()
28+
root_logger.setLevel(log_level)
29+
30+
# Remove any existing handlers and add our console handler
31+
root_logger.handlers.clear()
32+
root_logger.addHandler(console_handler)
33+
34+
# Set httpx logging to WARNING by default to reduce noise
35+
logging.getLogger('httpx').setLevel(logging.WARNING)
36+
logging.getLogger('httpcore').setLevel(logging.WARNING)

src/aws_mcp_proxy/mcp_proxy_manager.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414

1515
"""MCP Proxy Manager for handling proxy content integration."""
1616

17+
import logging
1718
from fastmcp.server.server import FastMCP
18-
from loguru import logger
1919

2020

2121
class McpProxyManager:
2222
"""Manages the integration of proxy content (tools, resources, prompts) into MCP servers."""
2323

24+
logger = logging.getLogger(__name__)
25+
2426
def __init__(self, target_mcp: FastMCP, allow_write: bool = False):
2527
"""Initialize the MCP Proxy Manager.
2628
@@ -45,10 +47,10 @@ async def add_proxy_content(self, proxy: FastMCP) -> None:
4547
await self._add_resources(proxy)
4648
await self._add_prompts(proxy)
4749

48-
logger.info('Successfully added proxy content to MCP server')
50+
self.logger.info('Successfully added proxy content to MCP server')
4951

5052
except Exception as e:
51-
logger.error(f'Failed to add proxy content to MCP server: {e}')
53+
self.logger.error(f'Failed to add proxy content to MCP server: {e}')
5254
raise
5355

5456
async def _add_tools(self, proxy: FastMCP) -> None:
@@ -61,15 +63,15 @@ async def _add_tools(self, proxy: FastMCP) -> None:
6163
Exception: If tools cannot be retrieved or added
6264
"""
6365
tools = await proxy.get_tools()
64-
logger.info(f'Found {len(tools)} tools in proxy')
66+
self.logger.info(f'Found {len(tools)} tools in proxy')
6567

6668
for tool_name, tool in tools.items():
6769
# Check the tool annotations and disable if needed
6870
annotations = tool.annotations
6971
if not self.allow_write:
7072
# In readOnly mode, skip the tools with no readOnlyHint=True annotation
7173
if annotations and not annotations.readOnlyHint or not annotations:
72-
logger.info(f'Skipping tool {tool_name} needing write permissions')
74+
self.logger.info(f'Skipping tool {tool_name} needing write permissions')
7375
continue
7476

7577
self.target_mcp.add_tool(tool)
@@ -82,17 +84,17 @@ async def _add_resources(self, proxy: FastMCP) -> None:
8284
"""
8385
try:
8486
resources = await proxy.get_resources()
85-
logger.info(f'Found {len(resources)} resources in proxy')
87+
self.logger.info(f'Found {len(resources)} resources in proxy')
8688

8789
for resource_uri, resource in resources.items():
8890
self.target_mcp.add_resource(resource)
89-
logger.debug(f'Added resource: {resource_uri}')
91+
self.logger.debug(f'Added resource: {resource_uri}')
9092

9193
except AttributeError:
9294
# Proxy doesn't have resources, which is fine
93-
logger.debug("Proxy doesn't have resources method")
95+
self.logger.debug("Proxy doesn't have resources method")
9496
except Exception as e:
95-
logger.warning(f'Failed to get resources from proxy: {e}')
97+
self.logger.warning(f'Failed to get resources from proxy: {e}')
9698

9799
async def _add_prompts(self, proxy: FastMCP) -> None:
98100
"""Add prompts from proxy to target MCP server.
@@ -102,14 +104,14 @@ async def _add_prompts(self, proxy: FastMCP) -> None:
102104
"""
103105
try:
104106
prompts = await proxy.get_prompts()
105-
logger.info(f'Found {len(prompts)} prompts in proxy')
107+
self.logger.info(f'Found {len(prompts)} prompts in proxy')
106108

107109
for prompt_name, prompt in prompts.items():
108110
self.target_mcp.add_prompt(prompt)
109-
logger.debug(f'Added prompt: {prompt_name}')
111+
self.logger.debug(f'Added prompt: {prompt_name}')
110112

111113
except AttributeError:
112114
# Proxy doesn't have prompts, which is fine
113-
logger.debug("Proxy doesn't have prompts method")
115+
self.logger.debug("Proxy doesn't have prompts method")
114116
except Exception as e:
115-
logger.warning(f'Failed to get prompts from proxy: {e}')
117+
self.logger.warning(f'Failed to get prompts from proxy: {e}')

src/aws_mcp_proxy/server.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@
2323

2424
import argparse
2525
import asyncio
26+
import logging
2627
import os
2728
from fastmcp.server.server import FastMCP
28-
from loguru import logger
29+
from src.aws_mcp_proxy.logging_config import configure_logging
2930
from src.aws_mcp_proxy.mcp_proxy_manager import McpProxyManager
3031
from src.aws_mcp_proxy.utils import (
3132
create_transport_with_sigv4,
@@ -35,6 +36,9 @@
3536
from typing import Any
3637

3738

39+
logger = logging.getLogger(__name__)
40+
41+
3842
async def setup_mcp_mode(mcp: FastMCP, args) -> None:
3943
"""Set up the server in MCP mode."""
4044
logger.info('Setting up server in MCP mode')
@@ -47,7 +51,9 @@ async def setup_mcp_mode(mcp: FastMCP, args) -> None:
4751
region = os.getenv('AWS_REGION', 'us-west-2')
4852

4953
# Log server configuration
50-
logger.info(f'Using service: {service}, region: {region}, profile: {profile or "default"}')
54+
logger.info(
55+
'Using service: %s, region: %s, profile: %s', service, region, profile or 'default'
56+
)
5157
logger.info('Running in MCP mode')
5258

5359
# Normalize endpoint URL
@@ -104,14 +110,22 @@ def parse_args():
104110
help='Allow tools that require write permissions to be enabled',
105111
)
106112

113+
parser.add_argument(
114+
'--log-level',
115+
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
116+
default='INFO',
117+
help='Set the logging level (default: INFO)',
118+
)
119+
107120
return parser.parse_args()
108121

109122

110123
def main():
111124
"""Run the MCP server."""
112125
args = parse_args()
113126

114-
# Set up logging
127+
# Configure logging
128+
configure_logging(args.log_level)
115129
logger.info('Starting AWS MCP Proxy Server')
116130

117131
# Create FastMCP instance
@@ -131,7 +145,7 @@ async def setup_and_run():
131145
await mcp.run_async()
132146

133147
except Exception as e:
134-
logger.error(f'Failed to start server: {e}')
148+
logger.error('Failed to start server: %s', e)
135149
raise
136150

137151
# Run the server

src/aws_mcp_proxy/sigv4_helper.py

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,60 @@
1616

1717
import boto3
1818
import httpx
19+
import logging
1920
import os
20-
from httpx_auth_awssigv4 import SigV4Auth
21-
from loguru import logger
22-
from typing import Optional
21+
from botocore.auth import SigV4Auth
22+
from botocore.awsrequest import AWSRequest
23+
from botocore.credentials import Credentials
24+
from typing import Any, Dict, Generator, Optional
25+
26+
27+
logger = logging.getLogger(__name__)
28+
29+
30+
class SigV4HTTPXAuth(httpx.Auth):
31+
"""HTTPX Auth class that signs requests with AWS SigV4."""
32+
33+
def __init__(
34+
self,
35+
credentials: Credentials,
36+
service: str,
37+
region: str,
38+
):
39+
"""Initialize SigV4HTTPXAuth.
40+
41+
Args:
42+
credentials: AWS credentials to use for signing
43+
service: AWS service name to sign requests for
44+
region: AWS region to sign requests for
45+
"""
46+
self.credentials = credentials
47+
self.service = service
48+
self.region = region
49+
self.signer = SigV4Auth(credentials, service, region)
50+
51+
def auth_flow(self, request: httpx.Request) -> Generator[httpx.Request, httpx.Response, None]:
52+
"""Signs the request with SigV4 and adds the signature to the request headers."""
53+
# Create an AWS request
54+
headers = dict(request.headers)
55+
# Header 'connection' = 'keep-alive' is not used in calculating the request
56+
# signature on the server-side, and results in a signature mismatch if included
57+
headers.pop('connection', None) # Remove if present, ignore if not
58+
59+
aws_request = AWSRequest(
60+
method=request.method,
61+
url=str(request.url),
62+
data=request.content,
63+
headers=headers,
64+
)
65+
66+
# Sign the request with SigV4
67+
self.signer.add_auth(aws_request)
68+
69+
# Add the signature header to the original request
70+
request.headers.update(dict(aws_request.headers))
71+
72+
yield request
2373

2474

2575
async def _handle_error_response(response: httpx.Response) -> None:
@@ -39,24 +89,24 @@ async def _handle_error_response(response: httpx.Response) -> None:
3989
# Read response content to extract error details
4090
await response.aread()
4191
except Exception as e:
42-
logger.error(f'Failed to read response: {e}')
92+
logger.error('Failed to read response: %s', e)
4393

4494
# Try to extract error details with fallbacks
4595
error_msg = ''
4696
try:
4797
# Try to parse JSON error details
4898
error_details = response.json()
49-
logger.error(f'HTTP {response.status_code} Error Details: {error_details}')
99+
logger.error('HTTP %d Error Details: %s', response.status_code, error_details)
50100
error_msg = f'HTTP {response.status_code}: {error_details} for url {response.url}'
51101
except Exception:
52102
# If JSON parsing fails, use response text or status code
53103
try:
54104
response_text = response.text
55-
logger.error(f'HTTP {response.status_code} Error: {response_text}')
105+
logger.error('HTTP %d Error: %s', response.status_code, response_text)
56106
error_msg = f'HTTP {response.status_code}: {response_text} for url {response.url}'
57107
except Exception:
58108
# Fallback to just status code and URL
59-
logger.error(f'HTTP {response.status_code} Error for url {response.url}')
109+
logger.error('HTTP %d Error for url %s', response.status_code, response.url)
60110
error_msg = f'HTTP {response.status_code} Error for url {response.url}'
61111

62112
# Raise the status error with enhanced message
@@ -102,7 +152,7 @@ def create_aws_session(profile: Optional[str] = None) -> boto3.Session:
102152

103153
def create_sigv4_auth(
104154
service: str, profile: Optional[str] = None, region: Optional[str] = None
105-
) -> SigV4Auth:
155+
) -> SigV4HTTPXAuth:
106156
"""Create SigV4 authentication for AWS requests.
107157
108158
Args:
@@ -111,7 +161,7 @@ def create_sigv4_auth(
111161
region: AWS region (defaults to AWS_REGION env var or us-west-2)
112162
113163
Returns:
114-
SigV4Auth instance
164+
SigV4HTTPXAuth instance
115165
116166
Raises:
117167
ValueError: If credentials cannot be obtained
@@ -125,25 +175,23 @@ def create_sigv4_auth(
125175
region = os.environ.get('AWS_REGION', 'us-west-2')
126176

127177
# Create SigV4Auth with explicit credentials
128-
sigv4_auth = SigV4Auth(
129-
access_key=credentials.access_key,
130-
secret_key=credentials.secret_key,
178+
sigv4_auth = SigV4HTTPXAuth(
179+
credentials=credentials,
131180
service=service,
132181
region=region,
133-
token=credentials.token,
134182
)
135183

136-
logger.info(f"Created SigV4 authentication for service '{service}' in region '{region}'")
184+
logger.info("Created SigV4 authentication for service '%s' in region '%s'", service, region)
137185
return sigv4_auth
138186

139187

140188
def create_sigv4_client(
141189
service: str = 'eks-mcp',
142190
profile: Optional[str] = None,
143191
region: Optional[str] = None,
144-
headers=None,
145-
auth=None,
146-
**kwargs,
192+
headers: Optional[Dict[str, str]] = None,
193+
auth: Optional[httpx.Auth] = None,
194+
**kwargs: Any,
147195
) -> httpx.AsyncClient:
148196
"""Create an httpx.AsyncClient with SigV4 authentication.
149197
@@ -180,7 +228,7 @@ def create_sigv4_client(
180228
sigv4_auth = create_sigv4_auth(service, profile, region)
181229

182230
# Create the client with SigV4 auth and error handling event hook
183-
logger.info(f"Creating httpx.AsyncClient with SigV4 authentication for service '{service}'")
231+
logger.info("Creating httpx.AsyncClient with SigV4 authentication for service '%s'", service)
184232

185233
return httpx.AsyncClient(
186234
auth=sigv4_auth,

0 commit comments

Comments
 (0)