use anyhow::Result;
use serde::Deserialize;
use serde_json::{json, Value};
use std::io::{BufRead, Write};
pub const PROTOCOL: &str = "2025-06-18";
pub const SUPPORTED: [&str; 2] = ["2025-06-18", "2025-03-26"];
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Request {
#[serde(default)]
pub id: Option<Value>,
pub method: String,
#[serde(default)]
pub params: Value,
}
#[derive(Debug, PartialEq)]
pub enum Framed {
Message(Request),
Malformed(String),
}
pub fn parse_line(line: &str) -> Framed {
match serde_json::from_str::<Request>(line) {
Ok(req) => Framed::Message(req),
Err(e) => Framed::Malformed(e.to_string()),
}
}
pub struct Tool {
pub name: String,
pub description: String,
pub input_schema: Value,
}
pub enum ToolResult {
Text(String),
Refused(String),
}
pub trait Tools {
fn server_name(&self) -> &str;
fn client_connected(&mut self, _name: &str) {}
fn list(&mut self) -> Vec<Tool>;
fn call(&mut self, name: &str, args: &Value) -> ToolResult;
}
pub fn negotiate(client: Option<&str>) -> &'static str {
client
.and_then(|want| SUPPORTED.into_iter().find(|v| *v == want))
.unwrap_or(PROTOCOL)
}
fn ok(id: &Value, result: Value) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "result": result })
}
fn err(id: &Value, code: i64, message: &str) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "error": { "code": code, "message": message } })
}
pub fn dispatch(req: &Request, tools: &mut dyn Tools) -> Option<Value> {
let id = req.id.clone()?;
Some(match req.method.as_str() {
"initialize" => {
if let Some(client) = req
.params
.get("clientInfo")
.and_then(|c| c.get("name"))
.and_then(|n| n.as_str())
{
tools.client_connected(client);
}
let want = req.params.get("protocolVersion").and_then(|v| v.as_str());
ok(
&id,
json!({
"protocolVersion": negotiate(want),
"capabilities": { "tools": {} },
"serverInfo": { "name": tools.server_name(), "version": env!("CARGO_PKG_VERSION") },
}),
)
}
"ping" => ok(&id, json!({})),
"tools/list" => {
let listed: Vec<Value> = tools
.list()
.into_iter()
.map(|t| {
json!({
"name": t.name,
"description": t.description,
"inputSchema": t.input_schema,
})
})
.collect();
ok(&id, json!({ "tools": listed }))
}
"tools/call" => {
let Some(name) = req.params.get("name").and_then(|v| v.as_str()) else {
return Some(err(&id, -32602, "tools/call needs a `name`"));
};
let args = req.params.get("arguments").cloned().unwrap_or(json!({}));
let (text, failed) = match tools.call(name, &args) {
ToolResult::Text(text) => (text, false),
ToolResult::Refused(why) => (why, true),
};
ok(
&id,
json!({
"content": [{ "type": "text", "text": text }],
"isError": failed,
}),
)
}
other => err(&id, -32601, &format!("unknown method `{other}`")),
})
}
pub fn serve<R: BufRead, W: Write>(input: R, mut output: W, tools: &mut dyn Tools) -> Result<()> {
for line in input.lines() {
let line = line?;
if line.trim().is_empty() {
continue;
}
let reply = match parse_line(&line) {
Framed::Message(req) => dispatch(&req, tools),
Framed::Malformed(why) => {
eprintln!("omh-mcp: ignoring unparseable line: {why}");
None
}
};
if let Some(reply) = reply {
writeln!(output, "{}", serde_json::to_string(&reply)?)?;
output.flush()?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
struct Fake {
listed: usize,
called: Vec<(String, Value)>,
refuse: bool,
client: Option<String>,
}
impl Fake {
fn new() -> Self {
Self {
listed: 0,
called: Vec::new(),
refuse: false,
client: None,
}
}
}
impl Tools for Fake {
fn server_name(&self) -> &str {
"omh-memory"
}
fn client_connected(&mut self, name: &str) {
self.client = Some(name.to_string());
}
fn list(&mut self) -> Vec<Tool> {
self.listed += 1;
vec![Tool {
name: "recall".into(),
description: format!("asked {} time(s)", self.listed),
input_schema: json!({
"type": "object",
"properties": { "question": { "type": "string" } },
"required": ["question"],
}),
}]
}
fn call(&mut self, name: &str, args: &Value) -> ToolResult {
self.called.push((name.to_string(), args.clone()));
if self.refuse {
ToolResult::Refused("no".into())
} else {
ToolResult::Text("yes".into())
}
}
}
fn request(raw: &str) -> Request {
match parse_line(raw) {
Framed::Message(req) => req,
Framed::Malformed(why) => panic!("{why}"),
}
}
fn answer(raw: &str) -> Value {
dispatch(&request(raw), &mut Fake::new()).expect("expected a reply")
}
#[test]
fn the_harness_names_itself_and_the_server_is_told() {
let mut tools = Fake::new();
let req = request(
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","clientInfo":{"name":"claude","version":"9"}}}"#,
);
dispatch(&req, &mut tools);
assert_eq!(tools.client.as_deref(), Some("claude"));
}
#[test]
fn a_handshake_with_no_client_name_leaves_provenance_alone() {
let mut tools = Fake::new();
let req = request(r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#);
dispatch(&req, &mut tools);
assert_eq!(tools.client, None);
}
#[test]
fn a_request_is_answered_with_the_id_it_arrived_with() {
for id in ["1", "\"abc\"", "\"0\"", "-4"] {
let raw = format!(r#"{{"jsonrpc":"2.0","id":{id},"method":"ping"}}"#);
let got = answer(&raw);
assert_eq!(
got["id"],
serde_json::from_str::<Value>(id).unwrap(),
"id {id} came back as {}",
got["id"]
);
}
}
#[test]
fn a_notification_is_never_answered() {
let req = request(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#);
assert_eq!(dispatch(&req, &mut Fake::new()), None);
}
#[test]
fn no_response_ever_spans_more_than_one_line() {
let mut out = Vec::new();
let input = concat!(
r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#,
"\n",
r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#,
"\n",
r#"{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}"#,
"\n",
);
serve(input.as_bytes(), &mut out, &mut Fake::new()).unwrap();
let text = String::from_utf8(out).unwrap();
let lines: Vec<&str> = text.lines().collect();
assert_eq!(lines.len(), 2, "two requests, two frames: {text:?}");
for line in lines {
serde_json::from_str::<Value>(line)
.unwrap_or_else(|e| panic!("a frame must be one whole JSON value: {e}\n{line}"));
}
}
#[test]
fn a_malformed_line_does_not_end_the_session() {
let mut out = Vec::new();
let input = concat!(
"not json at all\n",
r#"{"jsonrpc":"2.0","id":7,"method":"ping"}"#,
"\n",
);
serve(input.as_bytes(), &mut out, &mut Fake::new()).unwrap();
let text = String::from_utf8(out).unwrap();
assert_eq!(text.lines().count(), 1, "the junk line is not answered");
assert_eq!(
serde_json::from_str::<Value>(text.trim()).unwrap()["id"],
json!(7),
"the request after it still is"
);
}
#[test]
fn an_unknown_field_in_a_request_is_not_a_failure() {
let raw =
r#"{"jsonrpc":"2.0","id":1,"method":"ping","params":{},"_meta":{"progressToken":"x"}}"#;
assert_eq!(answer(raw)["id"], json!(1));
}
#[test]
fn an_unknown_method_is_an_error_not_a_silent_success() {
let got = answer(r#"{"jsonrpc":"2.0","id":1,"method":"resources/list"}"#);
assert_eq!(got["error"]["code"], json!(-32601));
assert!(got.get("result").is_none(), "an error carries no result");
assert!(
got["error"]["message"]
.as_str()
.unwrap()
.contains("resources/list"),
"say which method: {got}"
);
}
#[test]
fn an_unknown_protocol_version_is_answered_with_ours_not_echoed() {
assert_eq!(negotiate(Some("1999-01-01")), PROTOCOL);
assert_eq!(negotiate(None), PROTOCOL);
for known in SUPPORTED {
assert_eq!(negotiate(Some(known)), known, "we said we speak {known}");
}
let raw = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"1999-01-01"}}"#;
assert_eq!(answer(raw)["result"]["protocolVersion"], json!(PROTOCOL));
}
#[test]
fn every_tool_declares_a_schema_the_harness_can_validate() {
let listed = answer(r#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}"#);
let tools = listed["result"]["tools"].as_array().unwrap();
assert!(!tools.is_empty());
for tool in tools {
assert!(tool["name"].is_string(), "{tool}");
assert!(!tool["description"].as_str().unwrap().is_empty(), "{tool}");
assert_eq!(tool["inputSchema"]["type"], json!("object"), "{tool}");
assert!(
tool["inputSchema"]["required"].is_array(),
"a tool with nothing required accepts an empty call: {tool}"
);
}
}
#[test]
fn the_tool_description_is_computed_per_call_not_once() {
let mut tools = Fake::new();
let req = request(r#"{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}"#);
let first = dispatch(&req, &mut tools).unwrap();
let second = dispatch(&req, &mut tools).unwrap();
assert_ne!(
first["result"]["tools"][0]["description"], second["result"]["tools"][0]["description"],
"the store is asked again, not remembered"
);
}
#[test]
fn a_refused_call_is_a_tool_error_not_a_transport_error() {
let mut tools = Fake::new();
tools.refuse = true;
let req = request(
r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"remember","arguments":{}}}"#,
);
let got = dispatch(&req, &mut tools).unwrap();
assert!(got.get("error").is_none(), "not a protocol error: {got}");
assert_eq!(got["result"]["isError"], json!(true));
assert_eq!(got["result"]["content"][0]["text"], json!("no"));
}
#[test]
fn a_successful_call_returns_its_text_and_says_it_did_not_fail() {
let mut tools = Fake::new();
let req = request(
r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"recall","arguments":{"question":"q"}}}"#,
);
let got = dispatch(&req, &mut tools).unwrap();
assert_eq!(got["result"]["isError"], json!(false));
assert_eq!(got["result"]["content"][0]["text"], json!("yes"));
assert_eq!(
tools.called,
vec![("recall".into(), json!({"question": "q"}))]
);
}
#[test]
fn a_call_naming_no_tool_is_a_protocol_error() {
let got = answer(r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{}}"#);
assert_eq!(got["error"]["code"], json!(-32602));
}
}