● Zion Boggan
repos/Darwin/cortex/mcp_server.py
zionboggan.com ↗
116 lines · python
History for this file →
1
"""Darwin MCP server - exposes darwin_ask as a single unified tool.
2
 
3
Scaffold: tool definition is complete.
4
Wiring: run `python mcp_server.py` to start an stdio MCP server.
5
For SSE shim on :9891, use mcp-proxy or the run_sse_async pattern.
6
 
7
To wire into an agent's MCP config:
8
  {
9
    "mcpServers": {
10
      "darwin": {
11
        "command": "python",
12
        "args": ["/opt/darwin/cortex/mcp_server.py"],
13
        "env": {}
14
      }
15
    }
16
  }
17
 
18
For SSE shim at :9891 (remote agents / Cockpit / Odysseus):
19
  Run: mcp-proxy --port 9891 -- python /opt/darwin/cortex/mcp_server.py
20
  Then register: http://REDACTED-IP:9891/sse
21
"""
22
from __future__ import annotations
23
 
24
import asyncio
25
import json
26
import sys
27
from typing import Any
28
 
29
import httpx
30
from mcp.server import Server
31
from mcp.server.stdio import stdio_server
32
from mcp import types as mcp_types
33
 
34
CORTEX_URL = "http://127.0.0.1:9890"
35
 
36
app = Server("darwin-cortex")
37
 
38
 
39
@app.list_tools()
40
async def list_tools() -> list[mcp_types.Tool]:
41
    return [
42
        mcp_types.Tool(
43
            name="darwin_ask",
44
            description=(
45
                "Query Darwin's unified second brain. Fans out to homelab-rag (content), "
46
                "cbm (code graph), Serena (symbols), memory-graph (decisions), and Memos inbox. "
47
                "Returns a ranked answer with clickable receipts and conflict detection. "
48
                "Use this instead of calling the four memory tools separately."
49
            ),
50
            inputSchema={
51
                "type": "object",
52
                "properties": {
53
                    "query": {
54
                        "type": "string",
55
                        "description": "Natural-language question",
56
                    },
57
                    "mode": {
58
                        "type": "string",
59
                        "enum": ["chat", "search", "both"],
60
                        "default": "both",
61
                        "description": "chat=LLM synthesis, search=raw hits, both=everything",
62
                    },
63
                    "scope": {
64
                        "type": "array",
65
                        "items": {"type": "string"},
66
                        "description": "Limit to specific engines: rag, cbm, serena, memgraph, inbox",
67
                    },
68
                    "k": {
69
                        "type": "integer",
70
                        "default": 12,
71
                        "description": "Max hits per engine",
72
                    },
73
                    "synthesize": {
74
                        "type": "boolean",
75
                        "default": True,
76
                        "description": "Run LLM synthesis if available",
77
                    },
78
                },
79
                "required": ["query"],
80
            },
81
        )
82
    ]
83
 
84
 
85
@app.call_tool()
86
async def call_tool(name: str, arguments: dict[str, Any]) -> list[mcp_types.TextContent]:
87
    if name != "darwin_ask":
88
        raise ValueError(f"Unknown tool: {name}")
89
 
90
    payload = {
91
        "query": arguments["query"],
92
        "mode": arguments.get("mode", "both"),
93
        "k": arguments.get("k", 12),
94
        "synthesize": arguments.get("synthesize", True),
95
    }
96
    if "scope" in arguments:
97
        payload["scope"] = arguments["scope"]
98
 
99
    try:
100
        async with httpx.AsyncClient(timeout=10.0) as client:
101
            resp = await client.post(f"{CORTEX_URL}/ask", json=payload)
102
            resp.raise_for_status()
103
            data = resp.json()
104
    except Exception as e:
105
        data = {"error": str(e), "answer": None, "hits": [], "receipts": []}
106
 
107
    return [mcp_types.TextContent(type="text", text=json.dumps(data, default=str))]
108
 
109
 
110
async def main():
111
    async with stdio_server() as (read, write):
112
        await app.run(read, write, app.create_initialization_options())
113
 
114
 
115
if __name__ == "__main__":
116
    asyncio.run(main())