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 <poc@bcachefs.org>
This commit is contained in:
parent
899cdd0165
commit
8b808e44af
2 changed files with 168 additions and 53 deletions
|
|
@ -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<Value>,
|
||||
error: Option<Value>,
|
||||
#[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<Value> {
|
||||
consciousness::agent::tools::tools().into_iter()
|
||||
.map(|t| json!({
|
||||
"name": t.name,
|
||||
"description": t.description,
|
||||
"inputSchema": serde_json::from_str::<Value>(t.parameters_json).unwrap_or(json!({})),
|
||||
}))
|
||||
.collect()
|
||||
struct DaemonClient {
|
||||
reader: BufReader<UnixStream>,
|
||||
writer: BufWriter<UnixStream>,
|
||||
next_id: u64,
|
||||
}
|
||||
|
||||
// ── Tool dispatch ───────────────────────────────────────────────
|
||||
impl DaemonClient {
|
||||
fn connect() -> Result<Self, String> {
|
||||
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<String, String> {
|
||||
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<Value>) -> Result<Value, String> {
|
||||
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}],
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue