use std::collections::HashSet;
use std::sync::{Mutex, MutexGuard, PoisonError};
use anyhow::Result;
use serde_json::{Value, json};
use super::tools::ToolPins;
mod frame;
mod host;
mod server;
pub use frame::{pump_host, pump_server, refuse_all};
const UNKNOWN_TOOL: i64 = -32602;
const REFUSED: i64 = -32001;
const INVALID_REQUEST: i64 = -32600;
const PARSE_ERROR: i64 = -32700;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Method {
Tool,
Resource,
Prompt,
}
#[derive(Debug, Clone, Copy)]
pub struct Call<'a> {
pub method: Method,
pub name: &'a str,
pub arguments: Option<&'a Value>,
}
pub trait Gate: Send + Sync {
fn check(&self, call: &Call<'_>, held: Option<&str>) -> Result<Option<String>>;
fn server_request(&self, method: &str) -> Result<()>;
}
pub type Save = Box<dyn Fn(&ToolPins) -> Result<()> + Send + Sync>;
pub struct Relay {
server: String,
state: Mutex<State>,
gate: Box<dyn Gate>,
save: Save,
now_ms: fn() -> i64,
}
struct State {
pins: ToolPins,
first: bool,
broken: Option<String>,
pending: HashSet<String>,
relist: Option<server::Relist>,
relists: u64,
}
#[derive(Debug, PartialEq, Eq)]
pub enum Routed {
Server(String),
Host(String),
Dropped(String),
}
impl Relay {
pub fn new(
server: &str,
pins: Option<ToolPins>,
trust_first_sight: bool,
gate: Box<dyn Gate>,
save: Save,
now_ms: fn() -> i64,
) -> Self {
let first = pins.is_none() && trust_first_sight;
Self {
server: server.to_owned(),
state: Mutex::new(State {
pins: pins.unwrap_or_else(|| ToolPins::new(server)),
first,
broken: None,
pending: HashSet::new(),
relist: None,
relists: 0,
}),
gate,
save,
now_ms,
}
}
fn state(&self) -> MutexGuard<'_, State> {
self.state.lock().unwrap_or_else(PoisonError::into_inner)
}
pub fn host_line(&self, line: &str) -> Vec<Routed> {
let Some(message) = read(line) else {
if line.trim().is_empty() {
return Vec::new();
}
let why = "OpenMoat's MCP proxy cannot read this message, so it is not passed on";
return vec![Routed::Host(written(&error(
&Value::Null,
PARSE_ERROR,
why,
)))];
};
let Value::Array(batch) = message else {
return match self.on_host(message) {
host::Outcome::Forward(m) => vec![Routed::Server(written(&m))],
host::Outcome::Reply(r) => vec![Routed::Host(written(&r))],
host::Outcome::Drop => Vec::new(),
};
};
if batch.is_empty() {
let reply = error(
&Value::Null,
INVALID_REQUEST,
"an empty batch is not a request",
);
return vec![Routed::Host(written(&reply))];
}
let (mut forward, mut replies) = (Vec::new(), Vec::new());
for message in batch {
match self.on_host(message) {
host::Outcome::Forward(m) => forward.push(m),
host::Outcome::Reply(r) => replies.push(r),
host::Outcome::Drop => {}
}
}
let mut out = Vec::new();
if !forward.is_empty() {
out.push(Routed::Server(written(&Value::Array(forward))));
}
if !replies.is_empty() {
out.push(Routed::Host(written(&Value::Array(replies))));
}
out
}
pub fn server_line(&self, line: &str) -> Vec<Routed> {
let Some(message) = read(line) else {
if line.trim().is_empty() {
return Vec::new();
}
return vec![Routed::Dropped(
"a line from the server that is not JSON OpenMoat reads".to_owned(),
)];
};
let Value::Array(batch) = message else {
let mut out = Vec::new();
if let Some(m) = self.on_server(message, &mut out) {
out.push(Routed::Host(written(&m)));
}
return out;
};
let mut out = Vec::new();
let forward: Vec<Value> = batch
.into_iter()
.filter_map(|m| self.on_server(m, &mut out))
.collect();
if !forward.is_empty() {
out.push(Routed::Host(written(&Value::Array(forward))));
}
out
}
}
fn read(line: &str) -> Option<Value> {
serde_json::from_str::<Value>(line).ok()
}
pub(super) fn written(value: &Value) -> String {
let mut out = Vec::new();
let mut writer = serde_json::Serializer::with_formatter(&mut out, LineFormatter);
match serde::Serialize::serialize(value, &mut writer) {
Ok(()) => String::from_utf8(out).unwrap_or_else(|_| value.to_string()),
Err(_) => value.to_string(),
}
}
struct LineFormatter;
impl serde_json::ser::Formatter for LineFormatter {
fn write_string_fragment<W: ?Sized + std::io::Write>(
&mut self,
writer: &mut W,
fragment: &str,
) -> std::io::Result<()> {
let mut rest = fragment;
while let Some(at) = rest.find(['\u{85}', '\u{2028}', '\u{2029}']) {
writer.write_all(&rest.as_bytes()[..at])?;
let Some(c) = rest[at..].chars().next() else {
break;
};
write!(writer, "\\u{:04x}", u32::from(c))?;
rest = &rest[at + c.len_utf8()..];
}
writer.write_all(rest.as_bytes())
}
}
fn id_key(id: &Value) -> String {
id.to_string()
}
fn error(id: &Value, code: i64, message: &str) -> Value {
json!({"jsonrpc": "2.0", "id": id, "error": {"code": code, "message": message}})
}
#[cfg(test)]
mod tests;