eodi-mcp / src /mcp /server.py
lovelymango's picture
Upload 19 files
4c3c97b verified
"""
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()