use std::io::{BufRead, Write};
use serde_json::{json, Value};
use crate::tool_registry::ToolRegistry;
pub fn run_stdio_server(registry: ToolRegistry) -> Result<(), std::io::Error> {
let stdin = std::io::stdin();
let stdout = std::io::stdout();
tracing::info!("mcp stdio server started");
for line in stdin.lock().lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
if let Some(resp) = handle_line(&line, ®istry) {
let mut out = stdout.lock();
serde_json::to_writer(&mut out, &resp).map_err(std::io::Error::other)?;
writeln!(out)?;
out.flush()?;
}
}
tracing::info!("mcp stdio server shutdown — EOF");
Ok(())
}
pub fn handle_line(line: &str, registry: &ToolRegistry) -> Option<Value> {
let request: Value = match serde_json::from_str(line) {
Ok(v) => v,
Err(e) => {
tracing::warn!(error = %e, "json parse failure");
return Some(error_response(Value::Null, -32700, "parse error"));
}
};
if request.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
tracing::warn!("missing or invalid jsonrpc field");
return Some(error_response(
Value::Null,
-32700,
"parse error: jsonrpc must be \"2.0\"",
));
}
let method = match request.get("method").and_then(Value::as_str) {
Some(m) => m,
None => {
tracing::warn!("missing method field");
return Some(error_response(
Value::Null,
-32700,
"parse error: method field missing",
));
}
};
let id_field = request.get("id");
let id: Value = match id_field {
None => {
tracing::debug!(method, "notification (no id) — no response emitted");
return None;
}
Some(Value::Null) => {
tracing::debug!(method, "notification (id=null) — no response emitted");
return None;
}
Some(v) => v.clone(),
};
let params = request.get("params").cloned().unwrap_or(Value::Null);
match registry.dispatch(method, params) {
None => {
tracing::warn!(method, "method not found");
Some(error_response(id, -32601, "method not found"))
}
Some(Ok(result)) => {
tracing::debug!(method, "tool call succeeded");
Some(success_response(id, result))
}
Some(Err(e)) => {
tracing::warn!(method, error = %e, "tool call failed");
Some(error_response(id, -32000, &e.to_string()))
}
}
}
fn success_response(id: Value, result: Value) -> Value {
json!({
"jsonrpc": "2.0",
"result": result,
"id": id
})
}
fn error_response(id: Value, code: i32, message: &str) -> Value {
json!({
"jsonrpc": "2.0",
"error": {
"code": code,
"message": message
},
"id": id
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tool_handler::{GateId, ToolError, ToolHandler};
use crate::tool_registry::ToolRegistry;
struct EchoTool;
impl ToolHandler for EchoTool {
fn name(&self) -> &'static str {
"echo"
}
fn gate_set(&self) -> &'static [GateId] {
&[GateId::HealthRead]
}
fn call(&self, params: serde_json::Value) -> Result<serde_json::Value, ToolError> {
Ok(params)
}
}
fn registry_with_echo() -> ToolRegistry {
let mut r = ToolRegistry::new();
r.register(Box::new(EchoTool));
r
}
#[test]
fn parse_error_returns_minus32700() {
let r = ToolRegistry::new();
let resp = handle_line("not json {{{", &r).unwrap();
assert_eq!(resp["error"]["code"], -32700);
assert_eq!(resp["id"], Value::Null);
}
#[test]
fn missing_jsonrpc_field_returns_parse_error() {
let r = ToolRegistry::new();
let resp = handle_line(r#"{"method":"echo","id":1}"#, &r).unwrap();
assert_eq!(resp["error"]["code"], -32700);
}
#[test]
fn method_not_found_returns_minus32601() {
let r = ToolRegistry::new();
let resp = handle_line(r#"{"jsonrpc":"2.0","method":"nope","id":1}"#, &r).unwrap();
assert_eq!(resp["error"]["code"], -32601);
assert_eq!(resp["id"], 1);
}
#[test]
fn notification_with_null_id_returns_none() {
let r = registry_with_echo();
let result = handle_line(
r#"{"jsonrpc":"2.0","method":"echo","params":{},"id":null}"#,
&r,
);
assert!(result.is_none());
}
#[test]
fn notification_without_id_returns_none() {
let r = registry_with_echo();
let result = handle_line(r#"{"jsonrpc":"2.0","method":"echo","params":{}}"#, &r);
assert!(result.is_none());
}
#[test]
fn successful_call_returns_result() {
let r = registry_with_echo();
let resp = handle_line(
r#"{"jsonrpc":"2.0","method":"echo","params":{"x":42},"id":"req-1"}"#,
&r,
)
.unwrap();
assert_eq!(resp["result"]["x"], 42);
assert_eq!(resp["id"], "req-1");
assert!(resp.get("error").is_none());
}
}