#!/usr/bin/env python3 """ Neuron MCP server — talk to the local Neuron soul (:7770) from Claude Code. Stdlib only (no pip deps). stdio transport, newline-delimited JSON-RPC 2.0. Exposes: - neuron_chat(message, agentic?) -> the soul's reply - neuron_search_knowledge(query, limit?) -> lexical knowledge search - neuron_search_memory(query, limit?) -> memory/recall search """ import sys, json, urllib.request, urllib.parse SOUL = "http://127.0.0.1:7770" def _post(path, payload, timeout=180): data = json.dumps(payload).encode() req = urllib.request.Request(SOUL + path, data=data, headers={"Content-Type": "application/json"}, method="POST") with urllib.request.urlopen(req, timeout=timeout) as r: return json.loads(r.read().decode("utf-8", "replace")) def _get(path, timeout=30): req = urllib.request.Request(SOUL + path, method="GET") with urllib.request.urlopen(req, timeout=timeout) as r: return r.read().decode("utf-8", "replace") def neuron_chat(args): msg = (args.get("message") or "").strip() if not msg: return "error: message is required" agentic = bool(args.get("agentic", False)) try: resp = _post("/api/chat", {"session_id": "", "message": msg, "agentic": agentic}) except Exception as e: return f"error talking to Neuron (:7770): {e}" return resp.get("response") or resp.get("reply") or json.dumps(resp)[:2000] def _search(path_tmpl, args): q = (args.get("query") or "").strip() if not q: return "error: query is required" limit = int(args.get("limit", 5)) try: raw = _get(path_tmpl.format(q=urllib.parse.quote(q), n=limit)) except Exception as e: return f"error searching Neuron: {e}" try: arr = json.loads(raw) except Exception: return raw[:2000] # The soul returns HTTP 200 with a JSON error object (not a list) when a # downstream service is unreachable, e.g. memory recall proxies to :7771. if isinstance(arr, dict): err = str(arr.get("error", "")).lower() if "7771" in err or "connect" in err: return ("memory recall is unavailable: the soul's recall backend " "(:7771) isn't running. neuron_chat and " "neuron_search_knowledge still work.") return f"error from Neuron: {arr.get('error') or json.dumps(arr)[:500]}" if not isinstance(arr, list): return str(arr)[:2000] if not arr: return "no results" out = [] for n in arr[:limit]: nid = n.get("id", "") content = str(n.get("content", "")).replace("\n", " ")[:300] out.append(f"- [{nid}] {content}") return "\n".join(out) def neuron_search_knowledge(args): return _search("/api/neuron/knowledge/search?q={q}&limit={n}", args) def neuron_search_memory(args): return _search("/api/memories/recall?query={q}&limit={n}", args) TOOLS = [ {"name": "neuron_chat", "description": "Send a message to the local Neuron soul and return its reply. Use this to talk to Neuron.", "inputSchema": {"type": "object", "properties": { "message": {"type": "string", "description": "What to say to Neuron"}, "agentic": {"type": "boolean", "description": "Use agentic/tool mode (default false)"}}, "required": ["message"]}}, {"name": "neuron_search_knowledge", "description": "Search Neuron's knowledge base (lexical/keyword match).", "inputSchema": {"type": "object", "properties": { "query": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["query"]}}, {"name": "neuron_search_memory", "description": "Search what Neuron remembers (memory recall).", "inputSchema": {"type": "object", "properties": { "query": {"type": "string"}, "limit": {"type": "integer"}}, "required": ["query"]}}, ] HANDLERS = {"neuron_chat": neuron_chat, "neuron_search_knowledge": neuron_search_knowledge, "neuron_search_memory": neuron_search_memory} def send(msg): sys.stdout.write(json.dumps(msg) + "\n") sys.stdout.flush() def main(): for line in sys.stdin: line = line.strip() if not line: continue try: req = json.loads(line) except Exception: continue mid = req.get("id") method = req.get("method") if method == "initialize": pv = (req.get("params") or {}).get("protocolVersion") or "2024-11-05" send({"jsonrpc": "2.0", "id": mid, "result": { "protocolVersion": pv, "capabilities": {"tools": {}}, "serverInfo": {"name": "neuron", "version": "0.1.0"}}}) elif method == "notifications/initialized": pass elif method == "ping": send({"jsonrpc": "2.0", "id": mid, "result": {}}) elif method == "tools/list": send({"jsonrpc": "2.0", "id": mid, "result": {"tools": TOOLS}}) elif method == "tools/call": params = req.get("params") or {} name = params.get("name") args = params.get("arguments") or {} fn = HANDLERS.get(name) if not fn: send({"jsonrpc": "2.0", "id": mid, "result": { "content": [{"type": "text", "text": f"unknown tool: {name}"}], "isError": True}}) else: try: text = fn(args) except Exception as e: text = f"error: {e}" send({"jsonrpc": "2.0", "id": mid, "result": { "content": [{"type": "text", "text": str(text)}]}}) elif mid is not None: send({"jsonrpc": "2.0", "id": mid, "error": {"code": -32601, "message": f"method not found: {method}"}}) if __name__ == "__main__": try: main() except (BrokenPipeError, KeyboardInterrupt): pass