diff --git a/src/mcp-server.rs b/src/mcp-server.rs index 6fbc8f4..e6b901b 100644 --- a/src/mcp-server.rs +++ b/src/mcp-server.rs @@ -1,7 +1,7 @@ // mcp-server — MCP server for Claude Code integration // -// Speaks JSON-RPC over stdio (to Claude). Forwards tool calls to the -// consciousness daemon over Unix socket (~/.consciousness/mcp.sock). +// Speaks JSON-RPC over stdio (to Claude). Forwards memory/journal tool calls +// to the consciousness daemon. Handles channel tools locally. // // Protocol: https://modelcontextprotocol.io/specification @@ -12,6 +12,8 @@ use std::os::unix::net::UnixStream; use std::io::{BufReader, BufWriter}; use std::path::PathBuf; +use consciousness::agent::tools::channels; + // ── JSON-RPC types ────────────────────────────────────────────── #[derive(Deserialize)] @@ -68,6 +70,39 @@ fn respond_error(id: Value, code: i64, message: &str) { let _ = stdout.flush(); } +// ── Channel tools (handled locally) ───────────────────────────── + +fn channel_tool_definitions() -> Vec { + channels::tools().into_iter() + .map(|t| json!({ + "name": t.name, + "description": t.description, + "inputSchema": serde_json::from_str::(t.parameters_json).unwrap_or(json!({})), + })) + .collect() +} + +fn is_channel_tool(name: &str) -> bool { + name.starts_with("channel_") +} + +fn dispatch_channel_tool(name: &str, args: &Value) -> Result { + let tools = channels::tools(); + let tool = tools.iter().find(|t| t.name == name); + let Some(tool) = tool else { + return Err(format!("unknown channel tool: {name}")); + }; + + // 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()) +} + // ── Daemon connection ─────────────────────────────────────────── fn socket_path() -> PathBuf { @@ -187,21 +222,52 @@ fn main() { } "tools/list" => { + // Merge daemon tools with local channel tools match client.request("tools/list", None) { - Ok(result) => respond(req.id, result), + Ok(mut result) => { + // Add channel tools to the list + if let Some(tools) = result.get_mut("tools").and_then(|t| t.as_array_mut()) { + tools.extend(channel_tool_definitions()); + } + respond(req.id, result); + } Err(e) => respond_error(req.id, -32000, &e), } } "tools/call" => { - // Forward params directly - daemon expects same structure - match client.request("tools/call", Some(req.params.clone())) { - Ok(result) => respond(req.id, result), - Err(e) => { - respond(req.id, json!({ - "content": [{"type": "text", "text": e}], - "isError": true - })); + let name = req.params.get("name") + .and_then(|v| v.as_str()) + .unwrap_or(""); + let args = req.params.get("arguments") + .cloned() + .unwrap_or(json!({})); + + if is_channel_tool(name) { + // Handle channel tools locally + match dispatch_channel_tool(name, &args) { + Ok(text) => { + respond(req.id, json!({ + "content": [{"type": "text", "text": text}] + })); + } + Err(e) => { + respond(req.id, json!({ + "content": [{"type": "text", "text": e}], + "isError": true + })); + } + } + } else { + // Forward to daemon + match client.request("tools/call", Some(req.params.clone())) { + Ok(result) => respond(req.id, result), + Err(e) => { + respond(req.id, json!({ + "content": [{"type": "text", "text": e}], + "isError": true + })); + } } } }