use std::io::{BufRead, Write};
use anyhow::Result;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use crate::server::NexusServer;
#[derive(Debug, Deserialize)]
struct RawMessage {
#[serde(default)]
id: Option<Value>,
#[serde(default)]
method: Option<String>,
#[serde(default)]
params: Option<Value>,
}
#[derive(Debug, Serialize)]
struct Response {
jsonrpc: &'static str,
id: Value,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
error: Option<RpcError>,
}
#[derive(Debug, Serialize)]
struct RpcError {
code: i32,
message: String,
}
impl Response {
fn ok(id: Value, result: Value) -> Self {
Self {
jsonrpc: "2.0",
id,
result: Some(result),
error: None,
}
}
fn err(id: Value, code: i32, message: String) -> Self {
Self {
jsonrpc: "2.0",
id,
result: None,
error: Some(RpcError { code, message }),
}
}
}
pub fn process_message(server: &NexusServer, line: &str) -> Option<String> {
let msg = match serde_json::from_str::<RawMessage>(line) {
Err(e) => {
let resp = Response::err(Value::Null, -32700, format!("parse error: {e}"));
return Some(serde_json::to_string(&resp).unwrap_or_default());
}
Ok(m) => m,
};
if msg.id.is_none() {
if let Some(ref m) = msg.method {
handle_notification(server, m);
}
return None;
}
let id = msg.id.clone().unwrap();
let method = match msg.method.as_deref() {
Some(m) => m,
None => {
return Some(
serde_json::to_string(&Response::err(id, -32600, "missing method".into()))
.unwrap_or_default(),
)
}
};
let resp = handle_request(server, id, method, msg.params);
Some(serde_json::to_string(&resp).unwrap_or_default())
}
pub fn run_stdio(server: &NexusServer) -> Result<()> {
let stdin = std::io::stdin();
let stdout = std::io::stdout();
let mut out = std::io::BufWriter::new(stdout.lock());
for line in stdin.lock().lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
if let Some(encoded) = process_message(server, &line) {
writeln!(out, "{encoded}")?;
out.flush()?;
}
}
atlas::vault_store::flush();
Ok(())
}
fn handle_notification(server: &NexusServer, method: &str) {
match method {
"notifications/initialized" | "notifications/cancelled" => {}
_ => tracing::debug!(method, "unhandled MCP notification"),
}
let _ = server;
}
fn handle_request(
server: &NexusServer,
id: Value,
method: &str,
params: Option<Value>,
) -> Response {
match method {
"initialize" => {
let proto = params
.as_ref()
.and_then(|p| p.get("protocolVersion"))
.and_then(|v| v.as_str())
.unwrap_or("2024-11-05");
let negotiated = if proto >= "2024-11-05" {
"2024-11-05"
} else {
proto
};
Response::ok(
id,
json!({
"protocolVersion": negotiated,
"capabilities": { "tools": {} },
"serverInfo": { "name": "bctx", "version": env!("CARGO_PKG_VERSION") }
}),
)
}
"ping" => Response::ok(id, json!({})),
"tools/list" => Response::ok(id, server.gateway.list_tools()),
"tools/call" => {
let p = params.unwrap_or(Value::Null);
let name = match p.get("name").and_then(|v| v.as_str()) {
Some(n) => n.to_string(),
None => return Response::err(id, -32602, "missing params.name".into()),
};
let args = p
.get("arguments")
.cloned()
.unwrap_or_else(|| Value::Object(Default::default()));
let caller = p
.get("_caller")
.and_then(|v| v.as_str())
.unwrap_or("mcp-client");
match server.gateway.dispatch(&name, args, caller) {
Ok(full) => {
let payload = full.get("result").unwrap_or(&full);
let text = serde_json::to_string_pretty(payload)
.unwrap_or_else(|_| payload.to_string());
Response::ok(
id,
json!({
"content": [{ "type": "text", "text": text }]
}),
)
}
Err(e) => {
Response::ok(
id,
json!({
"content": [{ "type": "text", "text": e.to_string() }],
"isError": true
}),
)
}
}
}
"prompts/list" => Response::ok(id, json!({ "prompts": [] })),
"resources/list" => Response::ok(id, json!({ "resources": [] })),
"logging/setLevel" => Response::ok(id, json!({})),
other => Response::err(id, -32601, format!("method not found: {other}")),
}
}