2ea1d50fa3
Neuron Soul CI / build (pull_request) Successful in 5m10s
Tooling built on Tim's machine to run Neuron from the terminal as a
Claude Code session (identity + graph memory + agency) instead of
relaying to the soul's /api/chat.
- cli/neuron_recall.py BM25 read over the engram snapshot + CLI memories
(works around pinned-only soul search)
- cli/neuron_remember.py reliable local memory writes with read-back verify
(works around the corrupting capture endpoint)
- cli/neuron-chat.py standalone direct-chat REPL with per-turn memory injection
- cli/neuron_mcp.py stdlib MCP server (chat/search) with graceful degradation
- cli/CLAUDE.md.example the operating identity that makes Claude Code run as Neuron
- cli/HANDOFF.md soul-side bugs to fix so this becomes unnecessary
Scaffolding/proposal - intended to be retired once the soul does native
retrieval, correct persistence, and a real CLI identity/voice surface.
Pairs with the runtime model-passthrough + UTF-8 fixes in the el repo.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
158 lines
5.9 KiB
Python
Executable File
158 lines
5.9 KiB
Python
Executable File
#!/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
|