From 8b808e44afc177b6cc548eab638cc853b059b4c0 Mon Sep 17 00:00:00 2001 From: Kent Overstreet Date: Sun, 12 Apr 2026 21:10:01 -0400 Subject: [PATCH] consciousness-mcp: forward to daemon socket instead of direct calls Now connects to ~/.consciousness/mcp.sock and forwards tool calls to the consciousness daemon instead of calling tool handlers directly. Requires the consciousness daemon to be running with MCP server. Co-Authored-By: Proof of Concept --- Cargo.lock | 84 ++++++++++++++++++++++++++--- src/mcp-server.rs | 135 ++++++++++++++++++++++++++++++---------------- 2 files changed, 167 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f153ae9..459fa62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -550,12 +550,13 @@ dependencies = [ "redb", "regex", "rkyv", + "rusqlite", "rustls", "rustls-native-certs", "serde", "serde_json", "serde_urlencoded", - "skillratings", + "textwrap", "tokenizers", "tokio", "tokio-rustls", @@ -985,6 +986,18 @@ dependencies = [ "num-traits", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fancy-regex" version = "0.11.0" @@ -1283,6 +1296,15 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" +[[package]] +name = "hashlink" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" +dependencies = [ + "hashbrown 0.15.5", +] + [[package]] name = "heck" version = "0.5.0" @@ -1592,6 +1614,17 @@ dependencies = [ "libc", ] +[[package]] +name = "libsqlite3-sys" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "133c182a6a2c87864fe97778797e46c7e999672690dc9fa3ee8e241aa4a9c13f" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "line-clipping" version = "0.3.7" @@ -2489,6 +2522,20 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rusqlite" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "165ca6e57b20e1351573e3729b958bc62f0e48025386970b6e4d29e7a7e71f3f" +dependencies = [ + "bitflags 2.11.0", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + [[package]] name = "rustc_version" version = "0.4.1" @@ -2752,12 +2799,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" -[[package]] -name = "skillratings" -version = "0.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a6ee7559737c1adcd9184f168a04dc360c84878907c3ecc5c33c2320be1d47a" - [[package]] name = "slab" version = "0.4.12" @@ -2770,6 +2811,12 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "socket2" version = "0.6.3" @@ -2949,6 +2996,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -3469,6 +3527,12 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + [[package]] name = "unicode-normalization-alignments" version = "0.1.12" @@ -3537,6 +3601,12 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" diff --git a/src/mcp-server.rs b/src/mcp-server.rs index 3de577f..2547ac4 100644 --- a/src/mcp-server.rs +++ b/src/mcp-server.rs @@ -1,13 +1,16 @@ // mcp-server — MCP server for Claude Code integration // -// Speaks JSON-RPC over stdio. Exposes memory tools and channel -// operations. Replaces the Python MCP bridge entirely. +// Speaks JSON-RPC over stdio (to Claude). Forwards tool calls to the +// consciousness daemon over Unix socket (~/.consciousness/mcp.sock). // // Protocol: https://modelcontextprotocol.io/specification use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::io::{self, BufRead, Write}; +use std::os::unix::net::UnixStream; +use std::io::{BufReader, BufWriter}; +use std::path::PathBuf; // ── JSON-RPC types ────────────────────────────────────────────── @@ -35,6 +38,16 @@ struct ErrorResponse { id: Value, } +#[derive(Deserialize)] +struct DaemonResponse { + #[allow(dead_code)] + jsonrpc: String, + result: Option, + error: Option, + #[allow(dead_code)] + id: Value, +} + fn respond(id: Value, result: Value) { let resp = Response { jsonrpc: "2.0".into(), result, id }; let json = serde_json::to_string(&resp).unwrap(); @@ -55,52 +68,83 @@ fn respond_error(id: Value, code: i64, message: &str) { let _ = stdout.flush(); } -fn notify(method: &str, params: Value) { - let json = serde_json::to_string(&json!({ - "jsonrpc": "2.0", - "method": method, - "params": params, - })).unwrap(); - let mut stdout = io::stdout().lock(); - let _ = writeln!(stdout, "{json}"); - let _ = stdout.flush(); +// ── Daemon connection ─────────────────────────────────────────── + +fn socket_path() -> PathBuf { + dirs::home_dir() + .unwrap_or_default() + .join(".consciousness/mcp.sock") } -// ── Tool definitions ──────────────────────────────────────────── - -fn tool_definitions() -> Vec { - consciousness::agent::tools::tools().into_iter() - .map(|t| json!({ - "name": t.name, - "description": t.description, - "inputSchema": serde_json::from_str::(t.parameters_json).unwrap_or(json!({})), - })) - .collect() +struct DaemonClient { + reader: BufReader, + writer: BufWriter, + next_id: u64, } -// ── Tool dispatch ─────────────────────────────────────────────── +impl DaemonClient { + fn connect() -> Result { + let path = socket_path(); + let stream = UnixStream::connect(&path) + .map_err(|e| format!("connect to {:?}: {}", path, e))?; + let reader = BufReader::new(stream.try_clone().map_err(|e| e.to_string())?); + let writer = BufWriter::new(stream); + Ok(Self { reader, writer, next_id: 0 }) + } -fn dispatch_tool(name: &str, args: &Value) -> Result { - let tools = consciousness::agent::tools::tools(); - let tool = tools.iter().find(|t| t.name == name); - let Some(tool) = tool else { - return Err(format!("unknown tool: {name}")); - }; + fn request(&mut self, method: &str, params: Option) -> Result { + self.next_id += 1; + let req = json!({ + "jsonrpc": "2.0", + "id": self.next_id, + "method": method, + "params": params + }); + let mut line = serde_json::to_string(&req).map_err(|e| e.to_string())?; + line.push('\n'); + self.writer.write_all(line.as_bytes()).map_err(|e| e.to_string())?; + self.writer.flush().map_err(|e| e.to_string())?; - // Run async handler on a blocking runtime - let rt = tokio::runtime::Builder::new_current_thread() - .enable_all() - .build() - .map_err(|e| e.to_string())?; - let local = tokio::task::LocalSet::new(); - local.block_on(&rt, (tool.handler)(None, args.clone())) - .map_err(|e| e.to_string()) + let mut buf = String::new(); + self.reader.read_line(&mut buf).map_err(|e| e.to_string())?; + let resp: DaemonResponse = serde_json::from_str(&buf) + .map_err(|e| format!("parse response: {}", e))?; + + if let Some(err) = resp.error { + return Err(format!("daemon error: {}", err)); + } + Ok(resp.result.unwrap_or(Value::Null)) + } } // ── Main loop ─────────────────────────────────────────────────── fn main() { eprintln!("consciousness-mcp: starting"); + + // Connect to daemon + let mut client = match DaemonClient::connect() { + Ok(c) => c, + Err(e) => { + eprintln!("consciousness-mcp: failed to connect to daemon: {}", e); + eprintln!("consciousness-mcp: is consciousness running?"); + std::process::exit(1); + } + }; + + // Initialize with daemon + if let Err(e) = client.request("initialize", Some(json!({ + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "consciousness-mcp", "version": "0.4.0"} + }))) { + eprintln!("consciousness-mcp: daemon initialize failed: {}", e); + std::process::exit(1); + } + let _ = client.request("notifications/initialized", None); + + eprintln!("consciousness-mcp: connected to daemon"); + let stdin = io::stdin(); let reader = stdin.lock(); @@ -132,7 +176,7 @@ fn main() { "tools": {} }, "serverInfo": { - "name": "consciousness", + "name": "consciousness-mcp", "version": "0.4.0" } })); @@ -143,8 +187,10 @@ fn main() { } "tools/list" => { - let tools = tool_definitions(); - respond(req.id, json!({ "tools": tools })); + match client.request("tools/list", None) { + Ok(result) => respond(req.id, result), + Err(e) => respond_error(req.id, -32000, &e), + } } "tools/call" => { @@ -155,12 +201,11 @@ fn main() { .cloned() .unwrap_or(json!({})); - match dispatch_tool(name, &args) { - Ok(text) => { - respond(req.id, json!({ - "content": [{"type": "text", "text": text}] - })); - } + match client.request("tools/call", Some(json!({ + "name": name, + "arguments": args + }))) { + Ok(result) => respond(req.id, result), Err(e) => { respond(req.id, json!({ "content": [{"type": "text", "text": e}],