consciousness/src/claude/mcp-server.rs

168 lines
5.1 KiB
Rust
Raw Normal View History

// 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.
//
// Protocol: https://modelcontextprotocol.io/specification
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::io::{self, BufRead, Write};
// ── JSON-RPC types ──────────────────────────────────────────────
#[derive(Deserialize)]
struct Request {
jsonrpc: String,
method: String,
#[serde(default)]
params: Value,
id: Value,
}
#[derive(Serialize)]
struct Response {
jsonrpc: String,
result: Value,
id: Value,
}
#[derive(Serialize)]
struct ErrorResponse {
jsonrpc: String,
error: Value,
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();
let mut stdout = io::stdout().lock();
let _ = writeln!(stdout, "{json}");
let _ = stdout.flush();
}
fn respond_error(id: Value, code: i64, message: &str) {
let resp = ErrorResponse {
jsonrpc: "2.0".into(),
error: json!({ "code": code, "message": message }),
id,
};
let json = serde_json::to_string(&resp).unwrap();
let mut stdout = io::stdout().lock();
let _ = writeln!(stdout, "{json}");
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();
}
// ── Tool definitions ────────────────────────────────────────────
fn tool_definitions() -> Vec<Value> {
poc_memory::agent::tools::tools().into_iter()
.map(|t| json!({
"name": t.def.function.name,
"description": t.def.function.description,
"inputSchema": t.def.function.parameters,
}))
.collect()
}
// ── Tool dispatch ───────────────────────────────────────────────
fn dispatch_tool(name: &str, args: &Value) -> Result<String, String> {
let tools = poc_memory::agent::tools::tools();
let tool = tools.iter().find(|t| t.def.function.name == name);
let Some(tool) = tool else {
return Err(format!("unknown 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())
}
// ── Main loop ───────────────────────────────────────────────────
fn main() {
let stdin = io::stdin();
let reader = stdin.lock();
for line in reader.lines() {
let line = match line {
Ok(l) if !l.is_empty() => l,
_ => continue,
};
let req: Request = match serde_json::from_str(&line) {
Ok(r) => r,
Err(_) => continue,
};
match req.method.as_str() {
"initialize" => {
respond(req.id, json!({
"protocolVersion": "2024-11-05",
"capabilities": {
"tools": {}
},
"serverInfo": {
"name": "consciousness",
"version": "0.4.0"
}
}));
}
"notifications/initialized" => {
// Client ack — no response needed
}
"tools/list" => {
let tools = tool_definitions();
respond(req.id, json!({ "tools": tools }));
}
"tools/call" => {
let name = req.params.get("name")
.and_then(|v| v.as_str())
.unwrap_or("");
let args = req.params.get("arguments")
.cloned()
.unwrap_or(json!({}));
match dispatch_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
}));
}
}
}
_ => {
respond_error(req.id, -32601, &format!("unknown method: {}", req.method));
}
}
}
}