use std::io::{BufRead, Write};
use std::sync::{Arc, Mutex};
use serde_json::{Map, Value};
use super::server::{Executor, Server};
use super::{http, tools};
use crate::debug::dispatch::handle_request;
use crate::debug::state::DebugState;
pub(crate) struct AppServer(Server<Dispatcher>);
impl AppServer {
pub(crate) fn new(shared: Arc<Mutex<DebugState>>) -> Self {
Self(Server::new(Dispatcher { shared }))
}
pub(crate) fn serve<R: BufRead, W: Write>(
&self,
input: &mut R,
output: &mut W,
) -> std::io::Result<()> {
http::serve(&self.0, input, output)
}
}
struct Dispatcher {
shared: Arc<Mutex<DebugState>>,
}
impl Executor for Dispatcher {
fn call(&self, name: &str, arguments: &Map<String, Value>) -> Value {
let reply = handle_request(&tools::payload(name, arguments), &self.shared);
tools::text_result(&reply, !accepted(&reply))
}
}
fn accepted(reply: &str) -> bool {
serde_json::from_str::<Value>(reply)
.ok()
.and_then(|v| v.get("ok").and_then(Value::as_bool))
.unwrap_or(false)
}
#[cfg(test)]
mod tests {
use std::io::Cursor;
use serde_json::json;
use super::*;
fn snapshot() -> Arc<Mutex<DebugState>> {
Arc::new(Mutex::new(DebugState::default()))
}
fn call(name: &str, arguments: Value) -> Value {
let dispatcher = Dispatcher { shared: snapshot() };
let arguments = tools::arguments(&arguments).expect("valid arguments");
dispatcher.call(name, &arguments)
}
#[test]
fn a_read_only_verb_answers_from_the_snapshot() {
let result = call("state", Value::Null);
assert_eq!(result["isError"], json!(false));
let reply: Value =
serde_json::from_str(result["content"][0]["text"].as_str().expect("text")).unwrap();
assert_eq!(reply["ok"], json!(true));
assert_eq!(reply["frame"], json!(0));
}
#[test]
fn a_verb_the_world_cannot_serve_is_a_failed_call() {
let result = call("camera-get", Value::Null);
assert_eq!(result["isError"], json!(true));
}
#[test]
fn arguments_reach_the_dispatcher_as_the_request_body() {
let result = call(
"quality-set",
json!({ "setting": "shadows", "op": "sideways" }),
);
assert_eq!(result["isError"], json!(true));
assert!(
result["content"][0]["text"]
.as_str()
.expect("text")
.contains("sideways")
);
}
#[test]
fn a_call_over_the_transport_answers_from_the_same_snapshot() {
let body = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"ping"}}"#;
let request = format!(
"POST /mcp HTTP/1.1\r\nContent-Length: {}\r\n\r\n{body}",
body.len()
);
let mut output = Vec::new();
AppServer::new(snapshot())
.serve(&mut Cursor::new(request), &mut output)
.expect("serve");
let response = String::from_utf8(output).expect("utf-8");
let (head, body) = response.split_once("\r\n\r\n").expect("a header block");
assert!(head.starts_with("HTTP/1.1 200 OK"), "{head}");
let parsed: Value = serde_json::from_str(body).expect("a JSON-RPC response");
assert_eq!(parsed["result"]["isError"], json!(false));
assert!(
parsed["result"]["content"][0]["text"]
.as_str()
.expect("text")
.contains(r#""pong":true"#)
);
}
#[test]
fn an_unparseable_reply_is_a_failed_call() {
assert!(!accepted("not json"));
assert!(!accepted(r#"{"ok":false}"#));
assert!(accepted(r#"{"ok":true}"#));
}
}