2026-04-03 20:30:07 -04:00
|
|
|
// 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> {
|
2026-04-04 14:45:22 -04:00
|
|
|
use poc_memory::agent::tools;
|
2026-04-03 20:30:07 -04:00
|
|
|
|
2026-04-04 14:45:22 -04:00
|
|
|
let all_defs = tools::memory::definitions().into_iter()
|
|
|
|
|
.chain(tools::channels::definitions());
|
2026-04-03 20:30:07 -04:00
|
|
|
|
2026-04-04 14:45:22 -04:00
|
|
|
all_defs.map(|td| json!({
|
|
|
|
|
"name": td.function.name,
|
|
|
|
|
"description": td.function.description,
|
|
|
|
|
"inputSchema": td.function.parameters,
|
|
|
|
|
})).collect()
|
2026-04-03 20:30:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Tool dispatch ───────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
fn dispatch_tool(name: &str, args: &Value) -> Result<String, String> {
|
2026-04-04 14:45:22 -04:00
|
|
|
use poc_memory::agent::tools;
|
2026-04-03 20:30:07 -04:00
|
|
|
|
2026-04-04 14:45:22 -04:00
|
|
|
if name.starts_with("memory_") || name.starts_with("journal_") || name == "output" {
|
|
|
|
|
return tools::memory::dispatch(name, args, None)
|
|
|
|
|
.map_err(|e| e.to_string());
|
2026-04-03 20:30:07 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 14:45:22 -04:00
|
|
|
if name.starts_with("channel_") {
|
|
|
|
|
return tools::channels::dispatch_blocking(name, args)
|
|
|
|
|
.map_err(|e| e.to_string());
|
2026-04-03 20:30:07 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-04 14:45:22 -04:00
|
|
|
Err(format!("unknown tool: {name}"))
|
2026-04-03 20:30:07 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── 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));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|