Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,4 @@ jobs:
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: sbom-aws-mcp-proxy
path: sbom.json
path: sbom.json
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.11.4
rev: v0.13.2
hooks:
- id: ruff
args: [ --fix ]
Expand Down
2 changes: 1 addition & 1 deletion DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ npx @modelcontextprotocol/inspector uv run \
src/aws_mcp_proxy/server.py \
--endpoint <your endpoint>
```
Then click connect in the opened browser window.
Then click connect in the opened browser window.
5 changes: 0 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,8 @@ readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"fastmcp>=2.11.2",
"mcp>=1.12.3",
"loguru>=0.7.0",
"pydantic>=2.10.6",
"boto3>=1.34.0",
"botocore>=1.34.0",
"requests_auth_aws_sigv4",
"httpx-auth-awssigv4",
]
license = {text = "Apache-2.0"}
license-files = ["LICENSE", "NOTICE" ]
Expand Down
50 changes: 50 additions & 0 deletions src/aws_mcp_proxy/logging_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Logging configuration for AWS MCP Proxy."""

import logging
import sys
from typing import Optional


def configure_logging(level: Optional[str] = None) -> None:
"""Configure logging with a standard format and optional level.

Args:
level: Optional logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL).
If not provided, defaults to INFO.
"""
# Set default level to INFO if not provided
log_level = getattr(logging, level.upper()) if level else logging.INFO

# Configure logging format
log_format = '%(asctime)s | %(levelname)s | %(name)s | %(message)s'
date_format = '%Y-%m-%d %H:%M:%S'

# Create console handler with formatting
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(logging.Formatter(log_format, date_format))

# Configure root logger
root_logger = logging.getLogger()
root_logger.setLevel(log_level)

# Remove any existing handlers and add our console handler
root_logger.handlers.clear()
root_logger.addHandler(console_handler)

# Set httpx logging to WARNING by default to reduce noise
logging.getLogger('httpx').setLevel(logging.WARNING)
logging.getLogger('httpcore').setLevel(logging.WARNING)
28 changes: 15 additions & 13 deletions src/aws_mcp_proxy/mcp_proxy_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@

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

import logging
from fastmcp.server.server import FastMCP
from loguru import logger


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

logger = logging.getLogger(__name__)

def __init__(self, target_mcp: FastMCP, allow_write: bool = False):
"""Initialize the MCP Proxy Manager.

Expand All @@ -45,10 +47,10 @@ async def add_proxy_content(self, proxy: FastMCP) -> None:
await self._add_resources(proxy)
await self._add_prompts(proxy)

logger.info('Successfully added proxy content to MCP server')
self.logger.info('Successfully added proxy content to MCP server')

except Exception as e:
logger.error(f'Failed to add proxy content to MCP server: {e}')
self.logger.error(f'Failed to add proxy content to MCP server: {e}')
raise

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

for tool_name, tool in tools.items():
# Check the tool annotations and disable if needed
annotations = tool.annotations
if not self.allow_write:
# In readOnly mode, skip the tools with no readOnlyHint=True annotation
if annotations and not annotations.readOnlyHint or not annotations:
logger.info(f'Skipping tool {tool_name} needing write permissions')
self.logger.info(f'Skipping tool {tool_name} needing write permissions')
continue

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

for resource_uri, resource in resources.items():
self.target_mcp.add_resource(resource)
logger.debug(f'Added resource: {resource_uri}')
self.logger.debug(f'Added resource: {resource_uri}')

except AttributeError:
# Proxy doesn't have resources, which is fine
logger.debug("Proxy doesn't have resources method")
self.logger.debug("Proxy doesn't have resources method")
except Exception as e:
logger.warning(f'Failed to get resources from proxy: {e}')
self.logger.warning(f'Failed to get resources from proxy: {e}')

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

for prompt_name, prompt in prompts.items():
self.target_mcp.add_prompt(prompt)
logger.debug(f'Added prompt: {prompt_name}')
self.logger.debug(f'Added prompt: {prompt_name}')

except AttributeError:
# Proxy doesn't have prompts, which is fine
logger.debug("Proxy doesn't have prompts method")
self.logger.debug("Proxy doesn't have prompts method")
except Exception as e:
logger.warning(f'Failed to get prompts from proxy: {e}')
self.logger.warning(f'Failed to get prompts from proxy: {e}')
23 changes: 19 additions & 4 deletions src/aws_mcp_proxy/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""AWS MCP Proxy Server entry point.

This server provides a unified interface to backend servers by:
Expand All @@ -23,9 +24,10 @@

import argparse
import asyncio
import logging
import os
from fastmcp.server.server import FastMCP
from loguru import logger
from src.aws_mcp_proxy.logging_config import configure_logging
from src.aws_mcp_proxy.mcp_proxy_manager import McpProxyManager
from src.aws_mcp_proxy.utils import (
create_transport_with_sigv4,
Expand All @@ -35,6 +37,9 @@
from typing import Any


logger = logging.getLogger(__name__)


async def setup_mcp_mode(mcp: FastMCP, args) -> None:
"""Set up the server in MCP mode."""
logger.info('Setting up server in MCP mode')
Expand All @@ -47,7 +52,9 @@ async def setup_mcp_mode(mcp: FastMCP, args) -> None:
region = os.getenv('AWS_REGION', 'us-west-2')

# Log server configuration
logger.info(f'Using service: {service}, region: {region}, profile: {profile or "default"}')
logger.info(
'Using service: %s, region: %s, profile: %s', service, region, profile or 'default'
)
logger.info('Running in MCP mode')

# Normalize endpoint URL
Expand Down Expand Up @@ -104,14 +111,22 @@ def parse_args():
help='Allow tools that require write permissions to be enabled',
)

parser.add_argument(
'--log-level',
choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
default='INFO',
help='Set the logging level (default: INFO)',
)

return parser.parse_args()


def main():
"""Run the MCP server."""
args = parse_args()

# Set up logging
# Configure logging
configure_logging(args.log_level)
logger.info('Starting AWS MCP Proxy Server')

# Create FastMCP instance
Expand All @@ -131,7 +146,7 @@ async def setup_and_run():
await mcp.run_async()

except Exception as e:
logger.error(f'Failed to start server: {e}')
logger.error('Failed to start server: %s', e)
raise

# Run the server
Expand Down
Loading