use std::sync::Arc;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use crate::knowledge::Catalog;
use crate::telemetry::Telemetry;
use crate::{prompts, resources, tools};
pub const MCP_PROTOCOL_VERSION: &str = "2024-11-05";
pub const SERVER_NAME: &str = "axon-emcp";
pub async fn run_stdio(catalog: Catalog, telemetry: Telemetry) -> std::io::Result<()> {
let catalog = Arc::new(catalog);
let telemetry = Arc::new(telemetry);
let mut stdin = BufReader::new(tokio::io::stdin());
let mut stdout = tokio::io::stdout();
let mut line = String::new();
loop {
line.clear();
let n = stdin.read_line(&mut line).await?;
if n == 0 {
return Ok(());
}
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
tracing::debug!(bytes = trimmed.len(), "← request");
let response = handle_one(trimmed, &catalog, &telemetry).await;
if let Some(resp_bytes) = response {
stdout.write_all(&resp_bytes).await?;
stdout.write_all(b"\n").await?;
stdout.flush().await?;
tracing::debug!(bytes = resp_bytes.len(), "→ response");
}
}
}
async fn handle_one(
line: &str,
catalog: &Arc<Catalog>,
telemetry: &Arc<Telemetry>,
) -> Option<Vec<u8>> {
let parsed: Result<Request, serde_json::Error> = serde_json::from_str(line);
let req = match parsed {
Ok(r) => r,
Err(e) => {
return Some(error_response(
Value::Null,
JsonRpcError::parse_error(&e.to_string()),
));
}
};
let is_notification = req.id.is_none();
let id = req.id.clone().unwrap_or(Value::Null);
let outcome = dispatch(&req, catalog, telemetry).await;
if is_notification {
return None;
}
Some(match outcome {
Ok(result) => success_response(id, result),
Err(err) => error_response(id, err),
})
}
async fn dispatch(
req: &Request,
catalog: &Arc<Catalog>,
telemetry: &Arc<Telemetry>,
) -> Result<Value, JsonRpcError> {
match req.method.as_str() {
"initialize" => Ok(initialize_response()),
"notifications/initialized" => Ok(Value::Null), "ping" => Ok(Value::Object(serde_json::Map::new())),
"tools/list" => Ok(json!({ "tools": tools::list() })),
"tools/call" => tools::dispatch_call(req.params.clone(), catalog, telemetry).await,
"resources/list" => Ok(json!({ "resources": resources::list(catalog) })),
"resources/read" => resources::dispatch_read(req.params.clone(), catalog, telemetry),
"prompts/list" => Ok(json!({ "prompts": prompts::list(catalog) })),
"prompts/get" => prompts::dispatch_get(req.params.clone(), catalog, telemetry),
other => Err(JsonRpcError {
code: -32601,
message: format!("method not found: `{other}`"),
data: None,
}),
}
}
fn initialize_response() -> Value {
json!({
"protocolVersion": MCP_PROTOCOL_VERSION,
"serverInfo": {
"name": SERVER_NAME,
"version": env!("CARGO_PKG_VERSION"),
},
"capabilities": {
"tools": { "listChanged": false },
"resources": { "listChanged": false, "subscribe": false },
"prompts": { "listChanged": false },
},
"instructions": include_str!("server_instructions.txt"),
})
}
#[derive(Debug, Deserialize)]
struct Request {
#[serde(default)]
#[allow(dead_code)]
jsonrpc: String,
method: String,
#[serde(default)]
params: Value,
#[serde(default)]
id: Option<Value>,
}
#[derive(Debug, Serialize, Clone)]
pub struct JsonRpcError {
pub code: i64,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
impl JsonRpcError {
pub fn parse_error(detail: &str) -> Self {
Self { code: -32700, message: format!("parse error: {detail}"), data: None }
}
pub fn invalid_params(detail: impl Into<String>) -> Self {
Self { code: -32602, message: detail.into(), data: None }
}
pub fn internal(detail: impl Into<String>) -> Self {
Self { code: -32603, message: detail.into(), data: None }
}
}
fn success_response(id: Value, result: Value) -> Vec<u8> {
serde_json::to_vec(&json!({
"jsonrpc": "2.0",
"id": id,
"result": result,
}))
.expect("serialising a JSON-RPC success frame cannot fail")
}
fn error_response(id: Value, err: JsonRpcError) -> Vec<u8> {
serde_json::to_vec(&json!({
"jsonrpc": "2.0",
"id": id,
"error": err,
}))
.expect("serialising a JSON-RPC error frame cannot fail")
}
#[cfg(test)]
mod tests {
use super::*;
fn cat() -> Arc<Catalog> {
Arc::new(Catalog::empty_for_tests())
}
fn tel() -> Arc<Telemetry> {
Arc::new(Telemetry::new(crate::telemetry::TelemetryConfig {
jsonl_sink: None,
deployment_id: "".into(),
max_samples: 1000,
}))
}
#[tokio::test]
async fn initialize_carries_version_capabilities_and_instructions() {
let req = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#;
let resp = handle_one(req, &cat(), &tel()).await.expect("reply owed");
let v: Value = serde_json::from_slice(&resp).unwrap();
assert_eq!(v["jsonrpc"], "2.0");
assert_eq!(v["id"], 1);
assert_eq!(v["result"]["protocolVersion"], MCP_PROTOCOL_VERSION);
assert_eq!(v["result"]["serverInfo"]["name"], SERVER_NAME);
assert!(v["result"]["capabilities"]["tools"].is_object());
assert!(v["result"]["capabilities"]["resources"].is_object());
assert!(v["result"]["instructions"].as_str().unwrap().contains("AXON"));
}
#[tokio::test]
async fn notification_produces_no_reply() {
let req = r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#;
let resp = handle_one(req, &cat(), &tel()).await;
assert!(resp.is_none(), "notifications must not yield a reply");
}
#[tokio::test]
async fn unknown_method_returns_method_not_found() {
let req = r#"{"jsonrpc":"2.0","id":7,"method":"axon.does_not_exist"}"#;
let resp = handle_one(req, &cat(), &tel()).await.expect("reply owed");
let v: Value = serde_json::from_slice(&resp).unwrap();
assert_eq!(v["error"]["code"], -32601);
assert!(v["error"]["message"].as_str().unwrap().contains("not found"));
}
#[tokio::test]
async fn malformed_json_returns_parse_error_with_null_id() {
let resp = handle_one("{ not valid json", &cat(), &tel()).await.expect("reply owed");
let v: Value = serde_json::from_slice(&resp).unwrap();
assert_eq!(v["error"]["code"], -32700);
assert_eq!(v["id"], Value::Null);
}
#[tokio::test]
async fn tools_list_returns_an_array() {
let req = r#"{"jsonrpc":"2.0","id":2,"method":"tools/list"}"#;
let resp = handle_one(req, &cat(), &tel()).await.expect("reply owed");
let v: Value = serde_json::from_slice(&resp).unwrap();
let tools = v["result"]["tools"].as_array().expect("tools array");
assert!(!tools.is_empty(), "we ship at least one tool on day 0");
for t in tools {
assert!(t["name"].is_string());
assert!(t["description"].is_string());
assert!(t["inputSchema"].is_object());
}
}
#[tokio::test]
async fn resources_list_returns_an_array() {
let req = r#"{"jsonrpc":"2.0","id":3,"method":"resources/list"}"#;
let resp = handle_one(req, &cat(), &tel()).await.expect("reply owed");
let v: Value = serde_json::from_slice(&resp).unwrap();
assert!(v["result"]["resources"].is_array());
}
fn cat_embedded() -> Arc<Catalog> {
Arc::new(Catalog::load_embedded().expect("embedded corpus must load"))
}
#[tokio::test]
async fn initialize_advertises_prompts_capability() {
let req = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}"#;
let resp = handle_one(req, &cat(), &tel()).await.expect("reply owed");
let v: Value = serde_json::from_slice(&resp).unwrap();
assert!(
v["result"]["capabilities"]["prompts"].is_object(),
"initialize must advertise the prompts capability"
);
assert_eq!(
v["result"]["capabilities"]["prompts"]["listChanged"], false,
"we do not push prompt-list-changed notifications"
);
}
#[tokio::test]
async fn prompts_list_returns_an_array_of_entries() {
let req = r#"{"jsonrpc":"2.0","id":4,"method":"prompts/list"}"#;
let resp = handle_one(req, &cat_embedded(), &tel()).await.expect("reply owed");
let v: Value = serde_json::from_slice(&resp).unwrap();
let prompts = v["result"]["prompts"].as_array().expect("prompts array");
assert!(prompts.len() >= 3, "§Phase 5 ships ≥ 3 prompts");
for p in prompts {
assert!(p["name"].is_string());
assert!(p["description"].is_string());
assert!(p["arguments"].is_array());
}
}
#[tokio::test]
async fn prompts_get_renders_known_prompt_with_arguments() {
let req = serde_json::to_string(&json!({
"jsonrpc": "2.0",
"id": 5,
"method": "prompts/get",
"params": {
"name": "flow_design",
"arguments": { "intent": "summarise a patient record" }
}
}))
.unwrap();
let resp = handle_one(&req, &cat_embedded(), &tel()).await.expect("reply owed");
let v: Value = serde_json::from_slice(&resp).unwrap();
assert!(v["result"]["description"].is_string());
let msgs = v["result"]["messages"].as_array().unwrap();
assert_eq!(msgs.len(), 1);
assert_eq!(msgs[0]["role"], "user");
let text = msgs[0]["content"]["text"].as_str().unwrap();
assert!(text.contains("summarise a patient record"));
}
#[tokio::test]
async fn prompts_get_unknown_name_surfaces_structured_error() {
let req = serde_json::to_string(&json!({
"jsonrpc": "2.0",
"id": 6,
"method": "prompts/get",
"params": { "name": "does_not_exist", "arguments": {} }
}))
.unwrap();
let resp = handle_one(&req, &cat_embedded(), &tel()).await.expect("reply owed");
let v: Value = serde_json::from_slice(&resp).unwrap();
assert_eq!(v["error"]["code"], -32602);
assert!(v["error"]["message"].as_str().unwrap().contains("unknown prompt"));
}
}