1919load_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
2528class 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
3256async 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 ()
124174async 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