|
1 | | -from fastapi import FastAPI, HTTPException, WebSocket |
2 | | -from pydantic import BaseModel |
3 | | -from typing import Dict, Any |
4 | | -import uvicorn |
5 | | -from tools.auth_tool import AuthenticationTool |
| 1 | +from fastapi import FastAPI, Request, HTTPException |
| 2 | +from fastapi.middleware.cors import CORSMiddleware |
| 3 | +from tools.auth_tool import AuthTool |
6 | 4 | from tools.vial_management import VialManagementTool |
7 | 5 | from tools.health import HealthTool |
8 | 6 | from tools.blockchain import BlockchainTool |
9 | 7 | from tools.claude_tool import ClaudeTool |
10 | 8 | from tools.wallet import WalletTool |
11 | | -from lib.mcp_transport import MCPTransport |
| 9 | +from config.config import DatabaseConfig, ServerConfig, limiter, batch_sync_limiter |
12 | 10 | from lib.notifications import NotificationHandler |
13 | | -from config.config import DatabaseConfig |
14 | | -import logging |
| 11 | +from fastapi.responses import JSONResponse |
| 12 | +import uvicorn |
| 13 | +import os |
| 14 | +from typing import Dict |
15 | 15 |
|
16 | 16 | app = FastAPI() |
17 | | -logger = logging.getLogger("mcp") |
18 | | -logger.setLevel(logging.INFO) |
19 | | -handler = logging.StreamHandler() |
20 | | -handler.setFormatter(logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')) |
21 | | -logger.addHandler(handler) |
22 | | - |
23 | | -class MCPRequest(BaseModel): |
24 | | - jsonrpc: str = "2.0" |
25 | | - method: str |
26 | | - params: Dict[str, Any] |
27 | | - id: int |
| 17 | +app.state.config = ServerConfig() |
| 18 | +app.state.db = DatabaseConfig() |
| 19 | +app.state.notification_handler = NotificationHandler() |
28 | 20 |
|
29 | | -class MCPResponse(BaseModel): |
30 | | - jsonrpc: str = "2.0" |
31 | | - result: Any = None |
32 | | - error: Any = None |
33 | | - id: int |
| 21 | +app.add_middleware( |
| 22 | + CORSMiddleware, |
| 23 | + allow_origins=["*"], |
| 24 | + allow_credentials=True, |
| 25 | + allow_methods=["*"], |
| 26 | + allow_headers=["*"], |
| 27 | +) |
34 | 28 |
|
35 | 29 | class MCPServer: |
36 | 30 | def __init__(self): |
37 | | - self.db = DatabaseConfig() |
38 | | - self.notification_handler = NotificationHandler() |
39 | 31 | self.tools = { |
40 | | - "authentication": AuthenticationTool(self.db), |
41 | | - "vial-management": VialManagementTool(self.db), |
42 | | - "health": HealthTool(self.db, self.tools), |
43 | | - "blockchain": BlockchainTool(self.db), |
44 | | - "claude": ClaudeTool(self.db), |
45 | | - "wallet": WalletTool(self.db) |
| 32 | + "authentication": AuthTool(app.state.config), |
| 33 | + "vial_management": VialManagementTool(app.state.db), |
| 34 | + "health": HealthTool(), |
| 35 | + "blockchain": BlockchainTool(app.state.db), |
| 36 | + "claude": ClaudeTool(app.state.db), |
| 37 | + "wallet": WalletTool(app.state.db), |
46 | 38 | } |
47 | | - self.transport = MCPTransport(self.handle_request) |
48 | 39 |
|
49 | | - async def start(self): |
50 | | - await self.db.connect() |
51 | | - logger.info("MCP Server started on port 8000") |
52 | | - |
53 | | - async def handle_request(self, request: MCPRequest) -> MCPResponse: |
| 40 | + async def execute(self, request: Dict) -> Dict: |
54 | 41 | try: |
55 | | - tool_name, *method_parts = request.method.split(".") |
56 | | - if tool_name not in self.tools: |
57 | | - raise HTTPException(404, f"Unknown tool: {tool_name}") |
58 | | - tool = self.tools[tool_name] |
59 | | - result = await tool.execute(request.params) |
60 | | - # Send notification for wallet and Claude operations |
61 | | - if tool_name in ["claude", "wallet"]: |
62 | | - notification_method = f"{tool_name}.{method_parts[0] if method_parts else 'operationComplete'}" |
63 | | - await self.notification_handler.send_notification( |
64 | | - request.params.get("user_id", "default"), |
65 | | - {"jsonrpc": "2.0", "method": notification_method, "params": result} |
| 42 | + if request.get("jsonrpc") != "2.0": |
| 43 | + raise HTTPException(400, "Invalid JSON-RPC request") |
| 44 | + |
| 45 | + method = request.get("method") |
| 46 | + if not method: |
| 47 | + raise HTTPException(400, "Method not specified") |
| 48 | + |
| 49 | + tool_name, method_name = method.split(".", 1) if "." in method else (method, "") |
| 50 | + tool = self.tools.get(tool_name) |
| 51 | + if not tool: |
| 52 | + raise HTTPException(400, "Invalid tool") |
| 53 | + |
| 54 | + params = request.get("params", {}) |
| 55 | + params["method"] = method_name |
| 56 | + result = await tool.execute(params) |
| 57 | + |
| 58 | + # Send notification for wallet operations |
| 59 | + if tool_name == "wallet": |
| 60 | + await app.state.notification_handler.send_notification( |
| 61 | + params.get("user_id"), {"method": method, "params": result.dict()} |
66 | 62 | ) |
67 | | - return MCPResponse(id=request.id, result=result, error=None) |
| 63 | + |
| 64 | + return {"jsonrpc": "2.0", "result": result.dict(), "id": request.get("id")} |
68 | 65 | except Exception as e: |
69 | | - logger.error(f"Request error: {str(e)}") |
70 | | - return MCPResponse( |
71 | | - id=request.id, |
72 | | - error={"code": -32000, "message": str(e)}, |
73 | | - result=None |
74 | | - ) |
| 66 | + return {"jsonrpc": "2.0", "error": {"message": str(e)}, "id": request.get("id")} |
75 | 67 |
|
76 | 68 | server = MCPServer() |
77 | 69 |
|
78 | | -@app.on_event("startup") |
79 | | -async def startup_event(): |
80 | | - await server.start() |
| 70 | +@app.get("/mcp/health") |
| 71 | +async def health_check(request: Request): |
| 72 | + if request.headers.get("X-Forwarded-Proto", "http") != "https": |
| 73 | + raise HTTPException(400, "HTTPS required") |
| 74 | + return {"status": "healthy"} |
81 | 75 |
|
82 | 76 | @app.post("/mcp/execute") |
83 | | -async def execute(request: MCPRequest): |
84 | | - return await server.transport.handle(request) |
| 77 | +@limiter.limit("10/minute") # General rate limit |
| 78 | +async def execute(request: Request, body: Dict): |
| 79 | + if request.headers.get("X-Forwarded-Proto", "http") != "https": |
| 80 | + raise HTTPException(400, "HTTPS required") |
| 81 | + return await server.execute(body) |
85 | 82 |
|
86 | | -@app.get("/mcp/health") |
87 | | -async def health_check(): |
88 | | - return await server.tools["health"].execute({}) |
| 83 | +@app.post("/mcp/execute/wallet.batchSync") |
| 84 | +@batch_sync_limiter |
| 85 | +async def batch_sync(request: Request, body: Dict): |
| 86 | + if request.headers.get("X-Forwarded-Proto", "http") != "https": |
| 87 | + raise HTTPException(400, "HTTPS required") |
| 88 | + return await server.execute(body) |
89 | 89 |
|
90 | 90 | @app.websocket("/mcp/notifications") |
91 | | -async def websocket_endpoint(websocket: WebSocket, client_id: str): |
92 | | - await server.notification_handler.connect(websocket, client_id) |
| 91 | +async def websocket_endpoint(websocket, client_id: str): |
| 92 | + await app.state.notification_handler.connect(websocket, client_id) |
93 | 93 | try: |
94 | 94 | while True: |
95 | | - await websocket.receive_text() # Keep connection alive |
96 | | - except Exception as e: |
97 | | - logger.error(f"WebSocket error: {str(e)}") |
98 | | - await server.notification_handler.disconnect(client_id) |
| 95 | + await websocket.receive_text() |
| 96 | + except Exception: |
| 97 | + await app.state.notification_handler.disconnect(client_id) |
99 | 98 |
|
100 | 99 | if __name__ == "__main__": |
101 | | - uvicorn.run(app, host="0.0.0.0", port=8000) |
| 100 | + uvicorn.run( |
| 101 | + app, |
| 102 | + host=app.state.config.host, |
| 103 | + port=app.state.config.port, |
| 104 | + ssl_keyfile=app.state.config.ssl_key_path, |
| 105 | + ssl_certfile=app.state.config.ssl_cert_path |
| 106 | + ) |
0 commit comments