diff --git a/.env.template b/.env.template index 59dd2aa..adf766b 100644 --- a/.env.template +++ b/.env.template @@ -34,4 +34,8 @@ POSTGRES_DB=postgres_db_name POSTGRES_USER=postgres_user POSTGRES_PASSWORD=postgres_password POSTGRES_HOST=localhost -POSTGRES_PORT=5432 \ No newline at end of file +POSTGRES_PORT=5432 + +# MCP Server +MCP_SERVER_HOST=localhost +MCP_SERVER_PORT=8001 \ No newline at end of file diff --git a/compose/dev/docker-compose.yml b/compose/dev/docker-compose.yml index bf152ba..e431e42 100644 --- a/compose/dev/docker-compose.yml +++ b/compose/dev/docker-compose.yml @@ -81,6 +81,24 @@ services: condition: service_healthy fyp-postgres-dev: condition: service_healthy + + fyp-mcp-dev: + container_name: fyp-mcp-dev + build: + context: ../../ + dockerfile: compose/dev/mcp/Dockerfile + env_file: + - ../../.env + volumes: + - ../../:/app + ports: + - "0.0.0.0:8001:8001" + depends_on: + fyp-redis-dev: + condition: service_healthy + fyp-postgres-dev: + condition: service_healthy + volumes: fyp_postgres_data: diff --git a/compose/dev/mcp/Dockerfile b/compose/dev/mcp/Dockerfile new file mode 100644 index 0000000..d501675 --- /dev/null +++ b/compose/dev/mcp/Dockerfile @@ -0,0 +1,39 @@ +FROM nvidia/cuda:11.8.0-runtime-ubuntu22.04 + +WORKDIR /app + +RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ + python3 \ + python3-pip \ + build-essential \ + git \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* \ + && ln -sf /usr/bin/python3 /usr/bin/python \ + && ln -sf /usr/bin/pip3 /usr/bin/pip + +RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \ + apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y \ + python3-dev \ + libffi-dev \ + libssl-dev \ + cmake \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +RUN if [ ! -e /usr/lib/x86_64-linux-gnu/libcudart.so.11.0 ]; then \ + found=$(ls /usr/local/cuda/lib64/libcudart.so* 2>/dev/null | head -n1 || true); \ + if [ -n "$found" ]; then \ + mkdir -p /usr/lib/x86_64-linux-gnu || true; \ + ln -sf "$found" /usr/lib/x86_64-linux-gnu/libcudart.so.11.0 || true; \ + fi; \ + fi + +COPY requirements/mcp.txt . +RUN pip install --no-cache-dir --requirement mcp.txt + +ENV PYTHONUNBUFFERED=1 +ENV DJANGO_SETTINGS_MODULE=config.settings +EXPOSE 8001 + +CMD ["python", "-m", "mcp_agent.mcp_server"] \ No newline at end of file diff --git a/mcp_agent/__init__.py b/mcp_agent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/mcp_agent/mcp_client.py b/mcp_agent/mcp_client.py new file mode 100644 index 0000000..485d599 --- /dev/null +++ b/mcp_agent/mcp_client.py @@ -0,0 +1,42 @@ +import httpx +import asyncio + +class MCPClient: + def __init__(self, server_url: str): + self.server_url = server_url + self.client = httpx.AsyncClient(timeout=60) + + async def send(self, tool: str, arguments: dict): + response = await self.client.post( + f"{self.server_url}/execute", + json={ + "tool": tool, + "arguments": arguments, + }, + ) + response.raise_for_status() + return response.json() + + async def health(self): + response = await self.client.get(f"{self.server_url}/health") + response.raise_for_status() + return response.json() + + async def close(self): + await self.client.aclose() + + +async def main(): + client = MCPClient("http://localhost:8001") + + result = await client.send( + tool="echo", + arguments={"message": "hello from client"}, + ) + + print(result) + await client.close() + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/mcp_agent/mcp_server.py b/mcp_agent/mcp_server.py new file mode 100644 index 0000000..ae98b61 --- /dev/null +++ b/mcp_agent/mcp_server.py @@ -0,0 +1,95 @@ +import asyncio +import json +import os +import sys +from aiohttp import web +from mcp.server import Server +from mcp.types import Tool, TextContent + +app = Server("minimal-mcp-server") + + +@app.list_tools() +async def list_tools(): + return [ + Tool( + name="echo", + description="Echo back the provided input", + inputSchema={ + "type": "object", + "properties": { + "message": {"type": "string"} + }, + "required": ["message"] + }, + ) + ] + + +@app.call_tool() +async def call_tool(name: str, arguments: dict): + if name != "echo": + raise ValueError(f"Unknown tool: {name}") + + return [ + TextContent( + type="text", + text=json.dumps( + { + "received": arguments, + "status": "ok", + }, + indent=2, + ), + ) + ] + + +async def handle_execute(request: web.Request) -> web.Response: + try: + payload = await request.json() + tool = payload.get("tool") + arguments = payload.get("arguments", {}) + + if not tool: + return web.json_response( + {"error": "Missing 'tool' field"}, status=400 + ) + + result = await call_tool(tool, arguments) + return web.json_response( + { + "tool": tool, + "result": [c.text for c in result], + } + ) + + except json.JSONDecodeError: + return web.json_response({"error": "Invalid JSON"}, status=400) + except Exception as e: + return web.json_response({"error": str(e)}, status=500) + + +async def handle_health(request: web.Request) -> web.Response: + return web.json_response({"status": "healthy"}) + + +async def run_http_server(): + host = os.getenv("MCP_HTTP_HOST", "0.0.0.0") + port = int(os.getenv("MCP_HTTP_PORT", "8001")) + + app_http = web.Application() + app_http.router.add_post("/execute", handle_execute) + app_http.router.add_get("/health", handle_health) + + runner = web.AppRunner(app_http) + await runner.setup() + site = web.TCPSite(runner, host, port) + await site.start() + + print(f"HTTP server running on {host}:{port}", file=sys.stderr) + await asyncio.Event().wait() + + +if __name__ == "__main__": + asyncio.run(run_http_server()) diff --git a/requirements/mcp.txt b/requirements/mcp.txt new file mode 100644 index 0000000..6c2b8fa --- /dev/null +++ b/requirements/mcp.txt @@ -0,0 +1,6 @@ +aiohttp==3.8.4 +mcp==1.25.0 +pyjwt==2.10.1 +python-multipart==0.0.21 +sse-starlette==3.2.0 +starlette==0.52.1