mcp-server: handle channel tools locally
Hybrid approach: forward memory/journal tools to daemon via socket, but dispatch channel tools locally. Avoids extra daemon hop for channel operations. Adds channel_tool_definitions(), is_channel_tool(), dispatch_channel_tool() and merges channel tools into tools/list response. Co-Authored-By: Kent Overstreet <kent.overstreet@linux.dev>
This commit is contained in:
parent
6694d8a0bc
commit
55e5b08692
1 changed files with 77 additions and 11 deletions
|
|
@ -1,7 +1,7 @@
|
||||||
// mcp-server — MCP server for Claude Code integration
|
// mcp-server — MCP server for Claude Code integration
|
||||||
//
|
//
|
||||||
// Speaks JSON-RPC over stdio (to Claude). Forwards tool calls to the
|
// Speaks JSON-RPC over stdio (to Claude). Forwards memory/journal tool calls
|
||||||
// consciousness daemon over Unix socket (~/.consciousness/mcp.sock).
|
// to the consciousness daemon. Handles channel tools locally.
|
||||||
//
|
//
|
||||||
// Protocol: https://modelcontextprotocol.io/specification
|
// Protocol: https://modelcontextprotocol.io/specification
|
||||||
|
|
||||||
|
|
@ -12,6 +12,8 @@ use std::os::unix::net::UnixStream;
|
||||||
use std::io::{BufReader, BufWriter};
|
use std::io::{BufReader, BufWriter};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use consciousness::agent::tools::channels;
|
||||||
|
|
||||||
// ── JSON-RPC types ──────────────────────────────────────────────
|
// ── JSON-RPC types ──────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
|
@ -68,6 +70,39 @@ fn respond_error(id: Value, code: i64, message: &str) {
|
||||||
let _ = stdout.flush();
|
let _ = stdout.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Channel tools (handled locally) ─────────────────────────────
|
||||||
|
|
||||||
|
fn channel_tool_definitions() -> Vec<Value> {
|
||||||
|
channels::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()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_channel_tool(name: &str) -> bool {
|
||||||
|
name.starts_with("channel_")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_channel_tool(name: &str, args: &Value) -> Result<String, String> {
|
||||||
|
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 ───────────────────────────────────────────
|
// ── Daemon connection ───────────────────────────────────────────
|
||||||
|
|
||||||
fn socket_path() -> PathBuf {
|
fn socket_path() -> PathBuf {
|
||||||
|
|
@ -187,21 +222,52 @@ fn main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
"tools/list" => {
|
"tools/list" => {
|
||||||
|
// Merge daemon tools with local channel tools
|
||||||
match client.request("tools/list", None) {
|
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),
|
Err(e) => respond_error(req.id, -32000, &e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"tools/call" => {
|
"tools/call" => {
|
||||||
// Forward params directly - daemon expects same structure
|
let name = req.params.get("name")
|
||||||
match client.request("tools/call", Some(req.params.clone())) {
|
.and_then(|v| v.as_str())
|
||||||
Ok(result) => respond(req.id, result),
|
.unwrap_or("");
|
||||||
Err(e) => {
|
let args = req.params.get("arguments")
|
||||||
respond(req.id, json!({
|
.cloned()
|
||||||
"content": [{"type": "text", "text": e}],
|
.unwrap_or(json!({}));
|
||||||
"isError": true
|
|
||||||
}));
|
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
|
||||||
|
}));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue