Skip to content

Commit 1b3d52d

Browse files
IvanBirukclaude
andcommitted
Upgrade to FastMCP 2.0 with dual-mode authentication for AWS Fargate deployment
Major improvements: - Upgrade from MCP 1.6.0 to FastMCP 2.10.5 with enhanced HTTP transport - Implement dual-mode authentication: STDIO (env var) vs HTTP (Bearer token) - Add Bearer token flow: client → MCP server → CodeAlive API - Add /health endpoint for AWS ALB health checks - Update Docker container to default to HTTP mode for cloud deployment - Fix Python version specifier and update uv.lock Ready for production deployment on AWS Fargate behind Application Load Balancer. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 92bce83 commit 1b3d52d

File tree

4 files changed

+517
-181
lines changed

4 files changed

+517
-181
lines changed

Dockerfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ RUN apt-get update \
1212

1313
# Install Python dependencies
1414
RUN pip install --no-cache-dir \
15-
mcp[cli]>=1.6.0 \
15+
fastmcp>=2.0.0 \
1616
httpx>=0.26.0 \
1717
python-dotenv>=1.0.0
1818

1919
# Copy application code
2020
COPY . /app
2121

22-
# Expose port for SSE transport
22+
# Expose port for HTTP transport
2323
EXPOSE 8000
2424

25-
# Default command: stdio transport
26-
CMD ["python", "src/codealive_mcp_server.py", "--transport", "stdio"]
25+
# Default command: HTTP transport for containerized deployment
26+
CMD ["python", "src/codealive_mcp_server.py", "--transport", "http", "--host", "0.0.0.0", "--port", "8000"]

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ name = "codealive-mcp"
33
version = "0.1.0"
44
description = "MCP server for the CodeAlive API"
55
readme = "README.md"
6-
requires-python = "~=3.11"
6+
requires-python = "~=3.11.0"
77
dependencies = [
8-
"mcp[cli]>=1.6.0",
8+
"fastmcp>=2.0.0",
99
"httpx>=0.26.0",
1010
"python-dotenv>=1.0.0",
1111
]

src/codealive_mcp_server.py

Lines changed: 117 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@
1919
load_dotenv(dotenv_path=dotenv_path)
2020

2121
# Import FastMCP components
22-
from mcp.server.fastmcp import Context, FastMCP
22+
from fastmcp import Context, FastMCP
23+
from starlette.requests import Request
24+
from starlette.responses import JSONResponse
25+
import datetime
2326

2427
@dataclass
2528
class CodeAliveContext:
@@ -28,12 +31,32 @@ class CodeAliveContext:
2831
api_key: str
2932
base_url: str
3033

34+
def get_api_key_from_context(ctx: Context) -> str:
35+
"""Extract API key based on transport mode"""
36+
transport_mode = os.environ.get("TRANSPORT_MODE", "stdio")
37+
38+
if transport_mode == "http":
39+
# HTTP mode - extract from Authorization header
40+
# Check if we have HTTP request context
41+
if hasattr(ctx, 'request') and ctx.request:
42+
auth_header = ctx.request.headers.get("Authorization", "")
43+
if not auth_header or not auth_header.startswith("Bearer "):
44+
raise ValueError("HTTP mode: Authorization: Bearer <api-key> header required")
45+
return auth_header[7:] # Remove "Bearer "
46+
else:
47+
raise ValueError("HTTP mode: No request context available for Authorization header")
48+
else:
49+
# STDIO mode - use environment variable
50+
api_key = os.environ.get("CODEALIVE_API_KEY", "")
51+
if not api_key:
52+
raise ValueError("STDIO mode: CODEALIVE_API_KEY environment variable required")
53+
return api_key
54+
3155
@asynccontextmanager
3256
async def codealive_lifespan(server: FastMCP) -> AsyncIterator[CodeAliveContext]:
3357
"""Manage CodeAlive API client lifecycle"""
34-
# Get environment variables or use defaults - with stronger logging
35-
api_key = os.environ.get("CODEALIVE_API_KEY", "")
36-
58+
transport_mode = os.environ.get("TRANSPORT_MODE", "stdio")
59+
3760
# Get base URL from environment or use default
3861
if os.environ.get("CODEALIVE_BASE_URL") is None:
3962
print("WARNING: CODEALIVE_BASE_URL not found in environment, using default")
@@ -45,28 +68,45 @@ async def codealive_lifespan(server: FastMCP) -> AsyncIterator[CodeAliveContext]
4568
# Check if we should bypass SSL verification
4669
verify_ssl = not os.environ.get("CODEALIVE_IGNORE_SSL", "").lower() in ["true", "1", "yes"]
4770

48-
# Log environment configuration
49-
print(f"CodeAlive MCP Server starting with:")
50-
print(f" - API Key: {'*' * 5}{api_key[-5:] if api_key else 'Not set'}")
51-
print(f" - Base URL: {base_url}")
52-
print(f" - SSL Verification: {'Enabled' if verify_ssl else 'Disabled'}")
53-
print(f" - Environment variables found: {list(filter(lambda x: x.startswith('CODEALIVE_'), os.environ.keys()))}")
54-
55-
# Create a client
56-
client = httpx.AsyncClient(
57-
base_url=base_url,
58-
headers={
59-
"X-Api-Key": api_key,
60-
"Content-Type": "application/json",
61-
},
62-
timeout=60.0, # Longer timeout for chat completions
63-
verify=verify_ssl, # Only verify SSL if not in debug mode
64-
)
71+
if transport_mode == "stdio":
72+
# STDIO mode: create client with fixed API key
73+
api_key = os.environ.get("CODEALIVE_API_KEY", "")
74+
print(f"CodeAlive MCP Server starting in STDIO mode:")
75+
print(f" - API Key: {'*' * 5}{api_key[-5:] if api_key else 'Not set'}")
76+
print(f" - Base URL: {base_url}")
77+
print(f" - SSL Verification: {'Enabled' if verify_ssl else 'Disabled'}")
78+
79+
# Create client with fixed headers for STDIO mode
80+
client = httpx.AsyncClient(
81+
base_url=base_url,
82+
headers={
83+
"Authorization": f"Bearer {api_key}",
84+
"Content-Type": "application/json",
85+
},
86+
timeout=60.0,
87+
verify=verify_ssl,
88+
)
89+
else:
90+
# HTTP mode: create client factory (no fixed API key)
91+
print(f"CodeAlive MCP Server starting in HTTP mode:")
92+
print(f" - API Keys: Extracted from Authorization headers per request")
93+
print(f" - Base URL: {base_url}")
94+
print(f" - SSL Verification: {'Enabled' if verify_ssl else 'Disabled'}")
95+
96+
# Create base client without authentication headers
97+
client = httpx.AsyncClient(
98+
base_url=base_url,
99+
headers={
100+
"Content-Type": "application/json",
101+
},
102+
timeout=60.0,
103+
verify=verify_ssl,
104+
)
65105

66106
try:
67107
yield CodeAliveContext(
68108
client=client,
69-
api_key=api_key,
109+
api_key="", # Will be set per-request in HTTP mode
70110
base_url=base_url
71111
)
72112
finally:
@@ -120,6 +160,16 @@ async def codealive_lifespan(server: FastMCP) -> AsyncIterator[CodeAliveContext]
120160
lifespan=codealive_lifespan
121161
)
122162

163+
# Add health check endpoint for AWS ALB
164+
@mcp.custom_route("/health", methods=["GET"])
165+
async def health_check(request: Request) -> JSONResponse:
166+
"""Health check endpoint for load balancer"""
167+
return JSONResponse({
168+
"status": "healthy",
169+
"timestamp": datetime.datetime.now(datetime.timezone.utc).isoformat(),
170+
"service": "codealive-mcp-server"
171+
})
172+
123173
@mcp.tool()
124174
async def chat_completions(
125175
ctx: Context,
@@ -229,14 +279,21 @@ async def chat_completions(
229279
request_data["dataSources"] = valid_data_sources
230280

231281
try:
282+
# Get API key based on transport mode
283+
api_key = get_api_key_from_context(ctx)
284+
232285
# Log the attempt
233286
await ctx.info(f"Requesting chat completion with {len(messages)} messages" +
234287
(f" in conversation {conversation_id}" if conversation_id else " in a new conversation"))
235288

289+
# Create headers with authorization
290+
headers = {"Authorization": f"Bearer {api_key}"}
291+
236292
# Make API request
237293
response = await context.client.post(
238294
"/api/chat/completions",
239-
json=request_data
295+
json=request_data,
296+
headers=headers
240297
)
241298

242299
# Check for errors
@@ -341,11 +398,17 @@ async def get_data_sources(
341398
context: CodeAliveContext = ctx.request_context.lifespan_context
342399

343400
try:
401+
# Get API key based on transport mode
402+
api_key = get_api_key_from_context(ctx)
403+
344404
# Determine the endpoint based on alive_only flag
345405
endpoint = "/api/datasources/alive" if alive_only else "/api/datasources/all"
346406

407+
# Create headers with authorization
408+
headers = {"Authorization": f"Bearer {api_key}"}
409+
347410
# Make API request
348-
response = await context.client.get(endpoint)
411+
response = await context.client.get(endpoint, headers=headers)
349412

350413
# Check for errors
351414
response.raise_for_status()
@@ -492,8 +555,14 @@ async def search_code(
492555
else:
493556
await ctx.info("Using API key's default data source (if available)")
494557

558+
# Get API key based on transport mode
559+
api_key = get_api_key_from_context(ctx)
560+
561+
# Create headers with authorization
562+
headers = {"Authorization": f"Bearer {api_key}"}
563+
495564
# Make API request
496-
response = await context.client.get("/api/search", params=params)
565+
response = await context.client.get("/api/search", params=params, headers=headers)
497566

498567
# Check for errors
499568
response.raise_for_status()
@@ -595,9 +664,9 @@ async def search_code(
595664
parser = argparse.ArgumentParser(description="CodeAlive MCP Server")
596665
parser.add_argument("--api-key", help="CodeAlive API Key")
597666
parser.add_argument("--base-url", help="CodeAlive Base URL")
598-
parser.add_argument("--transport", help="Transport type (stdio or sse)", default="stdio")
599-
parser.add_argument("--host", help="Host for SSE transport", default="0.0.0.0")
600-
parser.add_argument("--port", help="Port for SSE transport", type=int, default=8000)
667+
parser.add_argument("--transport", help="Transport type (stdio or http)", default="stdio")
668+
parser.add_argument("--host", help="Host for HTTP transport", default="0.0.0.0")
669+
parser.add_argument("--port", help="Port for HTTP transport", type=int, default=8000)
601670
parser.add_argument("--debug", action="store_true", help="Enable debug mode for verbose logging")
602671
parser.add_argument("--ignore-ssl", action="store_true", help="Ignore SSL certificate validation")
603672

@@ -636,20 +705,34 @@ async def search_code(
636705
masked_env = env_content.replace(os.environ.get("CODEALIVE_API_KEY", ""), "****API_KEY****")
637706
print(f" - Dotenv content:\n{masked_env}")
638707

639-
# Check environment variables before starting server
708+
# Set transport mode for validation
709+
os.environ["TRANSPORT_MODE"] = args.transport
710+
711+
# Validate configuration based on transport mode
640712
api_key = os.environ.get("CODEALIVE_API_KEY", "")
641713
base_url = os.environ.get("CODEALIVE_BASE_URL", "")
642714

643-
if not api_key:
644-
print("WARNING: CODEALIVE_API_KEY environment variable is not set.")
645-
print("Please set this in your .env file or environment.")
715+
if args.transport == "stdio":
716+
# STDIO mode: require API key in environment
717+
if not api_key:
718+
print("ERROR: STDIO mode requires CODEALIVE_API_KEY environment variable.")
719+
print("Please set this in your .env file or environment.")
720+
sys.exit(1)
721+
print(f"STDIO mode: Using API key from environment (ends with: ...{api_key[-4:] if len(api_key) > 4 else '****'})")
722+
else:
723+
# HTTP mode: prohibit API key in environment
724+
if api_key:
725+
print("ERROR: HTTP mode detected CODEALIVE_API_KEY in environment.")
726+
print("Remove the environment variable. API keys must be provided via Authorization: Bearer headers.")
727+
sys.exit(1)
728+
print("HTTP mode: API keys will be extracted from Authorization: Bearer headers")
646729

647730
if not base_url:
648731
print("WARNING: CODEALIVE_BASE_URL environment variable is not set, using default.")
649732
print("CodeAlive will connect to the production API at https://app.codealive.ai")
650733

651734
# Run the server with the selected transport
652-
if args.transport == "sse":
653-
mcp.run(transport="sse", host=args.host, port=args.port)
735+
if args.transport == "http":
736+
mcp.run(transport="http", host=args.host, port=args.port)
654737
else:
655738
mcp.run(transport="stdio")

0 commit comments

Comments
 (0)