Spaces:
Running
Running
| """ | |
| Eodi MCP Server | |
| =============== | |
| ํธํ ๋ก์ดํฐ ํ๋ก๊ทธ๋จ ์ง์ ๋ฒ ์ด์ค MCP ์๋ฒ. | |
| Transport: | |
| - stdio: ๋ก์ปฌ ๊ฐ๋ฐ ๋ฐ Claude Desktop ์ฐ๋ | |
| - HTTP: ํฅํ ๋ฐฐํฌ์ฉ (Streamable HTTP) | |
| ์คํ: | |
| python -m src.mcp.server | |
| ๋๋ | |
| python src/mcp/server.py | |
| """ | |
| import os | |
| import sys | |
| import json | |
| import asyncio | |
| from typing import Dict, Any, Optional | |
| from pathlib import Path | |
| # ํ๋ก์ ํธ ๋ฃจํธ๋ฅผ ๊ฒฝ๋ก์ ์ถ๊ฐ (์ ๋ ๊ฒฝ๋ก ์ฌ์ฉ!) | |
| PROJECT_ROOT = Path(__file__).parent.parent.parent.absolute() | |
| sys.path.insert(0, str(PROJECT_ROOT)) | |
| # .env ํ์ผ ๋ก๋ (์ ๋ ๊ฒฝ๋ก๋ก!) | |
| from dotenv import load_dotenv | |
| env_path = PROJECT_ROOT / ".env" | |
| if env_path.exists(): | |
| load_dotenv(env_path) | |
| from src.mcp.tools import TOOLS, execute_tool | |
| def log(msg: str): | |
| """stderr๋ก ๋ก๊น (stdout์ MCP ํ๋กํ ์ฝ์ฉ)""" | |
| print(msg, file=sys.stderr, flush=True) | |
| class EodiMCPServer: | |
| """ | |
| Eodi MCP ์๋ฒ - stdio ํธ๋์คํฌํธ ๊ตฌํ | |
| MCP ํ๋กํ ์ฝ ๋ฉ์์ง ์ฒ๋ฆฌ: | |
| - initialize: ์๋ฒ ์ ๋ณด ๋ฐํ | |
| - tools/list: ์ฌ์ฉ ๊ฐ๋ฅํ ํด ๋ชฉ๋ก ๋ฐํ | |
| - tools/call: ํด ์คํ | |
| - resources/list: ๋ฆฌ์์ค ๋ชฉ๋ก (ํฅํ) | |
| - resources/read: ๋ฆฌ์์ค ์ฝ๊ธฐ (ํฅํ) | |
| """ | |
| def __init__(self): | |
| self.server_info = { | |
| "name": "eodi-kb", | |
| "version": "0.1.0", | |
| "description": "ํธํ ๋ก์ดํฐ ํ๋ก๊ทธ๋จ ์ง์ ๋ฒ ์ด์ค MCP ์๋ฒ" | |
| } | |
| self.capabilities = { | |
| "tools": {}, | |
| "resources": {} | |
| } | |
| log(f"[eodi-kb] Server initialized at {PROJECT_ROOT}") | |
| log(f"[eodi-kb] SUPABASE_URL: {'set' if os.getenv('SUPABASE_URL') else 'NOT SET'}") | |
| log(f"[eodi-kb] GEMINI_API_KEY: {'set' if os.getenv('GEMINI_API_KEY') else 'NOT SET'}") | |
| async def handle_message(self, message: Dict[str, Any]) -> Dict[str, Any]: | |
| """MCP ๋ฉ์์ง ์ฒ๋ฆฌ""" | |
| method = message.get("method", "") | |
| params = message.get("params", {}) | |
| msg_id = message.get("id") | |
| log(f"[eodi-kb] <- {method}") | |
| try: | |
| if method == "initialize": | |
| result = await self._handle_initialize(params) | |
| elif method == "tools/list": | |
| result = await self._handle_tools_list() | |
| elif method == "tools/call": | |
| result = await self._handle_tools_call(params) | |
| elif method == "resources/list": | |
| result = await self._handle_resources_list() | |
| elif method == "resources/read": | |
| result = await self._handle_resources_read(params) | |
| elif method == "notifications/initialized": | |
| return None | |
| elif method == "prompts/list": | |
| result = {"prompts": []} | |
| else: | |
| log(f"[eodi-kb] Unknown method: {method}") | |
| result = {"error": {"code": -32601, "message": f"Unknown method: {method}"}} | |
| if msg_id is not None: | |
| return { | |
| "jsonrpc": "2.0", | |
| "id": msg_id, | |
| "result": result | |
| } | |
| return None | |
| except Exception as e: | |
| log(f"[eodi-kb] Error: {e}") | |
| if msg_id is not None: | |
| return { | |
| "jsonrpc": "2.0", | |
| "id": msg_id, | |
| "error": { | |
| "code": -32603, | |
| "message": str(e) | |
| } | |
| } | |
| return None | |
| async def _handle_initialize(self, params: Dict[str, Any]) -> Dict[str, Any]: | |
| """initialize ํธ๋ค๋ฌ""" | |
| log(f"[eodi-kb] Initialize from: {params.get('clientInfo', {}).get('name', 'unknown')}") | |
| return { | |
| "protocolVersion": "2024-11-05", | |
| "serverInfo": self.server_info, | |
| "capabilities": self.capabilities | |
| } | |
| async def _handle_tools_list(self) -> Dict[str, Any]: | |
| """tools/list ํธ๋ค๋ฌ""" | |
| log(f"[eodi-kb] Returning {len(TOOLS)} tools") | |
| return {"tools": TOOLS} | |
| async def _handle_tools_call(self, params: Dict[str, Any]) -> Dict[str, Any]: | |
| """tools/call ํธ๋ค๋ฌ""" | |
| tool_name = params.get("name") | |
| arguments = params.get("arguments", {}) | |
| log(f"[eodi-kb] Calling tool: {tool_name}") | |
| if not tool_name: | |
| raise ValueError("Tool name is required") | |
| # ๋๊ธฐ ํจ์๋ฅผ ๋น๋๊ธฐ๋ก ์คํ | |
| loop = asyncio.get_event_loop() | |
| result = await loop.run_in_executor( | |
| None, | |
| lambda: execute_tool(tool_name, arguments) | |
| ) | |
| log(f"[eodi-kb] Tool result: success={result.get('success', False)}") | |
| # MCP ์๋ต ํ์ | |
| return { | |
| "content": [ | |
| { | |
| "type": "text", | |
| "text": json.dumps(result, ensure_ascii=False, indent=2) | |
| } | |
| ] | |
| } | |
| async def _handle_resources_list(self) -> Dict[str, Any]: | |
| """resources/list ํธ๋ค๋ฌ""" | |
| return { | |
| "resources": [ | |
| { | |
| "uri": "eodi://kb/stats", | |
| "name": "KB Statistics", | |
| "description": "์ง์ ๋ฒ ์ด์ค ํต๊ณ ์ ๋ณด", | |
| "mimeType": "application/json" | |
| } | |
| ] | |
| } | |
| async def _handle_resources_read(self, params: Dict[str, Any]) -> Dict[str, Any]: | |
| """resources/read ํธ๋ค๋ฌ""" | |
| uri = params.get("uri", "") | |
| if uri == "eodi://kb/stats": | |
| from src.db import SupabaseAdapter | |
| adapter = SupabaseAdapter() | |
| stats = adapter.get_stats() | |
| return { | |
| "contents": [ | |
| { | |
| "uri": uri, | |
| "mimeType": "application/json", | |
| "text": json.dumps(stats, ensure_ascii=False) | |
| } | |
| ] | |
| } | |
| raise ValueError(f"Unknown resource: {uri}") | |
| async def run_stdio(self): | |
| """stdio ํธ๋์คํฌํธ๋ก ์๋ฒ ์คํ""" | |
| log(f"๐ Eodi MCP Server v{self.server_info['version']} started (stdio)") | |
| # stdin์ ๋น๋๊ธฐ๋ก ์ฝ๊ธฐ ์ํ reader | |
| reader = asyncio.StreamReader() | |
| protocol = asyncio.StreamReaderProtocol(reader) | |
| await asyncio.get_event_loop().connect_read_pipe( | |
| lambda: protocol, sys.stdin | |
| ) | |
| while True: | |
| try: | |
| # ํ ์ค ์ฝ๊ธฐ | |
| line = await reader.readline() | |
| if not line: | |
| log("[eodi-kb] EOF received, shutting down") | |
| break | |
| line_str = line.decode('utf-8').strip() | |
| if not line_str: | |
| continue | |
| # JSON-RPC ๋ฉ์์ง ํ์ฑ | |
| try: | |
| message = json.loads(line_str) | |
| except json.JSONDecodeError as e: | |
| log(f"[eodi-kb] JSON parse error: {e}") | |
| continue | |
| # ๋ฉ์์ง ์ฒ๋ฆฌ | |
| response = await self.handle_message(message) | |
| # ์๋ต ์ ์ก (stdout) | |
| if response: | |
| response_line = json.dumps(response, ensure_ascii=False) | |
| sys.stdout.write(response_line + "\n") | |
| sys.stdout.flush() | |
| except asyncio.CancelledError: | |
| break | |
| except Exception as e: | |
| log(f"[eodi-kb] Error: {e}") | |
| log("[eodi-kb] Server stopped") | |
| async def main(): | |
| """๋น๋๊ธฐ ๋ฉ์ธ ์ง์ ์ """ | |
| server = EodiMCPServer() | |
| await server.run_stdio() | |
| def main_sync(): | |
| """๋๊ธฐ ๋ฉ์ธ ์ง์ ์ (CLI ์ํธ๋ฆฌํฌ์ธํธ์ฉ)""" | |
| asyncio.run(main()) | |
| if __name__ == "__main__": | |
| main_sync() | |