|
1 | 1 | #!/usr/bin/env python3 |
2 | 2 | """CodeIntel MCP Server entry point. |
3 | 3 |
|
4 | | -Provides codebase intelligence tools for LLMs via Model Context Protocol. |
5 | | -All tool definitions, handlers, and formatters are in their own modules. |
| 4 | +Supports two transport modes: |
| 5 | + stdio - Local dev, Claude Desktop config (default) |
| 6 | + streamable-http - Remote deployment, Claude.ai Connectors |
| 7 | +
|
| 8 | +Usage: |
| 9 | + python server.py # stdio (default) |
| 10 | + python server.py --transport http # streamable HTTP on $PORT |
6 | 11 | """ |
7 | | -import asyncio |
| 12 | +import sys |
8 | 13 |
|
9 | | -from mcp.server import Server |
10 | | -from mcp.server.models import InitializationOptions, ServerCapabilities |
11 | | -import mcp.server.stdio |
| 14 | +from mcp.server.fastmcp import FastMCP |
12 | 15 | import mcp.types as types |
| 16 | +from starlette.routing import Route |
| 17 | +from starlette.responses import JSONResponse |
13 | 18 |
|
14 | | -from config import SERVER_NAME, SERVER_VERSION |
| 19 | +from config import SERVER_NAME, SERVER_VERSION, TRANSPORT, HOST, PORT |
15 | 20 | from tools import get_tool_schemas |
16 | 21 | from handlers import call_tool |
17 | | -from api_client import close_client |
18 | 22 |
|
19 | | -server = Server(SERVER_NAME) |
| 23 | +mcp = FastMCP( |
| 24 | + name=SERVER_NAME, |
| 25 | + instructions=( |
| 26 | + "CodeIntel provides semantic code search, dependency analysis, " |
| 27 | + "and codebase intelligence. Use list_repositories first to find " |
| 28 | + "repo IDs, then search_code or get_codebase_dna for context." |
| 29 | + ), |
| 30 | + host=HOST, |
| 31 | + port=PORT, |
| 32 | + streamable_http_path="/mcp", |
| 33 | + stateless_http=True, |
| 34 | + log_level="INFO", |
| 35 | +) |
| 36 | + |
| 37 | +# Register tools at the low-level MCP server layer. |
| 38 | +# FastMCP's @tool decorator infers schemas from function signatures, |
| 39 | +# but we have well-tested schemas in tools.py and dispatch in handlers.py. |
| 40 | +_server = mcp._mcp_server |
20 | 41 |
|
21 | 42 |
|
22 | | -@server.list_tools() |
| 43 | +@_server.list_tools() |
23 | 44 | async def handle_list_tools() -> list[types.Tool]: |
24 | 45 | """Return all available tool schemas.""" |
25 | 46 | return get_tool_schemas() |
26 | 47 |
|
27 | 48 |
|
28 | | -@server.call_tool() |
| 49 | +@_server.call_tool() |
29 | 50 | async def handle_call_tool( |
30 | 51 | name: str, arguments: dict | None |
31 | 52 | ) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: |
32 | 53 | """Dispatch tool calls to the handler layer.""" |
33 | 54 | return await call_tool(name, arguments) |
34 | 55 |
|
35 | 56 |
|
36 | | -async def main() -> None: |
37 | | - """Run the MCP server over stdio transport.""" |
38 | | - try: |
39 | | - async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): |
40 | | - await server.run( |
41 | | - read_stream, |
42 | | - write_stream, |
43 | | - InitializationOptions( |
44 | | - server_name=SERVER_NAME, |
45 | | - server_version=SERVER_VERSION, |
46 | | - capabilities=ServerCapabilities(tools={}), |
47 | | - ), |
48 | | - ) |
49 | | - finally: |
50 | | - await close_client() |
| 57 | +# Health check for Railway/load balancers |
| 58 | +async def _health(request): |
| 59 | + return JSONResponse({"status": "ok", "server": SERVER_NAME, "version": SERVER_VERSION}) |
| 60 | + |
| 61 | + |
| 62 | +def _get_http_app(): |
| 63 | + """Build the Starlette app with health check + MCP endpoint.""" |
| 64 | + app = mcp.streamable_http_app() |
| 65 | + app.routes.insert(0, Route("/health", _health, methods=["GET"])) |
| 66 | + return app |
| 67 | + |
| 68 | + |
| 69 | +def main(): |
| 70 | + """Run with configured transport.""" |
| 71 | + transport = TRANSPORT |
| 72 | + |
| 73 | + # CLI override: --transport http |
| 74 | + if "--transport" in sys.argv: |
| 75 | + idx = sys.argv.index("--transport") |
| 76 | + if idx + 1 < len(sys.argv): |
| 77 | + arg = sys.argv[idx + 1] |
| 78 | + transport = "streamable-http" if arg in ("http", "streamable-http") else arg |
| 79 | + |
| 80 | + if transport == "streamable-http": |
| 81 | + import uvicorn |
| 82 | + print(f"Starting {SERVER_NAME} v{SERVER_VERSION} on {HOST}:{PORT}/mcp") |
| 83 | + uvicorn.run(_get_http_app(), host=HOST, port=PORT) |
| 84 | + else: |
| 85 | + mcp.run(transport="stdio") |
51 | 86 |
|
52 | 87 |
|
53 | 88 | if __name__ == "__main__": |
54 | | - asyncio.run(main()) |
| 89 | + main() |
0 commit comments