use serde_json::{Map, Value};
use super::{REFUSED, Relay, Routed, State, error, id_key, written};
const RELAY_ID_PREFIX: &str = "openmoat-relay-";
const RELIST_WAIT_MS: i64 = 10_000;
const MAX_RELIST_PAGES: u32 = 100;
const UNRECORDED_REQUESTS: &[&str] = &["ping"];
pub(super) struct Relist {
id: String,
since_ms: i64,
pages: u32,
}
pub(super) fn is_relay_id(id: &Value) -> bool {
id.as_str().is_some_and(|s| s.starts_with(RELAY_ID_PREFIX))
}
impl Relay {
pub(super) fn on_server(&self, mut message: Value, out: &mut Vec<Routed>) -> Option<Value> {
let Value::Object(fields) = &message else {
out.push(dropped(
"a message from the server that is not a JSON-RPC object",
));
return None;
};
let answers = fields.contains_key("result") || fields.contains_key("error");
match (fields.get("method"), answers) {
(Some(Value::String(method)), false) => {
self.server_message(method, fields, out).then_some(message)
}
(None, true) => {
let id = fields.get("id").cloned().unwrap_or(Value::Null);
if self.response_of(&id, fields, out) {
self.filter_listing(&mut message);
Some(message)
} else {
None
}
}
_ => {
out.push(dropped(
"a message from the server that is neither a request, a notification nor \
a response",
));
None
}
}
}
fn server_message(
&self,
method: &str,
fields: &Map<String, Value>,
out: &mut Vec<Routed>,
) -> bool {
if method == "notifications/tools/list_changed" {
let mut state = self.state();
if !self.relisting(&mut state)
&& let Some(ask) = self.ask_listing(&mut state, None)
{
out.push(ask);
}
return true;
}
let Some(id) = fields.get("id") else {
return true;
};
if UNRECORDED_REQUESTS.contains(&method) {
return true;
}
let Err(e) = self.gate.server_request(method) else {
return true;
};
let why = format!("OpenMoat cannot record this request ({e:#}), so it is refused");
out.push(Routed::Server(written(&error(id, REFUSED, &why))));
out.push(dropped(&format!("the server's `{method}` request: {e:#}")));
false
}
fn response_of(&self, id: &Value, fields: &Map<String, Value>, out: &mut Vec<Routed>) -> bool {
let mut state = self.state();
if state
.relist
.as_ref()
.is_some_and(|r| id.as_str() == Some(&r.id))
{
self.read_relisting(&mut state, fields, out);
return false;
}
if id.is_null() && fields.contains_key("error") {
return true;
}
if state.pending.remove(&id_key(id)) {
return true;
}
out.push(dropped(&format!(
"a response from the server with id {id}, which answers no request the host is \
waiting for"
)));
false
}
fn filter_listing(&self, message: &mut Value) {
let Some(Value::Array(tools)) = message.pointer_mut("/result/tools") else {
return;
};
let mut state = self.state();
let hidden = self.list(&mut state, tools);
tools.retain(|t| {
t.get("name")
.and_then(Value::as_str)
.is_some_and(|n| !hidden.contains(n))
});
}
fn list(&self, state: &mut State, tools: &[Value]) -> std::collections::BTreeSet<String> {
let first = state.first;
let listing = state.pins.list(tools, first, (self.now_ms)());
state.first = false;
if listing.changed
&& let Err(e) = (self.save)(&state.pins)
{
state.broken = Some(format!("{e:#}"));
}
listing.hidden
}
pub(super) fn relisting(&self, state: &mut State) -> bool {
let now = (self.now_ms)();
match &state.relist {
Some(relist) if now.saturating_sub(relist.since_ms) <= RELIST_WAIT_MS => true,
Some(_) => {
state.relist = None;
false
}
None => false,
}
}
fn ask_listing(&self, state: &mut State, cursor: Option<&str>) -> Option<Routed> {
let pages = state.relist.as_ref().map_or(0, |r| r.pages);
if pages >= MAX_RELIST_PAGES {
state.relist = None;
return None;
}
state.relists += 1;
let id = format!("{RELAY_ID_PREFIX}{}", state.relists);
let since_ms = state
.relist
.as_ref()
.map_or_else(|| (self.now_ms)(), |r| r.since_ms);
let request = openmoat_hosts::mcp_tools_list_request(&id, cursor);
state.relist = Some(Relist {
id,
since_ms,
pages: pages + 1,
});
Some(Routed::Server(written(&request)))
}
fn read_relisting(
&self,
state: &mut State,
fields: &Map<String, Value>,
out: &mut Vec<Routed>,
) {
let result = fields.get("result");
let Some(Value::Array(tools)) = result.and_then(|r| r.get("tools")) else {
state.relist = None;
return;
};
self.list(state, tools);
match result
.and_then(|r| r.get("nextCursor"))
.and_then(Value::as_str)
{
Some(cursor) => {
if let Some(ask) = self.ask_listing(state, Some(cursor)) {
out.push(ask);
}
}
None => state.relist = None,
}
}
}
fn dropped(why: &str) -> Routed {
Routed::Dropped(format!("dropped {why}"))
}