1use super::framing::{self, INVALID_PARAMS, METHOD_NOT_FOUND};
4use super::negotiate::{Era, Refusal, Session, shaped};
5use super::tools::{TOOLS_TTL_MS, ToolResult, object, string, tool_table};
6use crate::network::{Rail, call, directory, flow};
7use serde_json::{Map, Value, json};
8
9pub struct Server<R> {
11 rail: R,
12 session: Session,
13 pub(super) frame_bytes: usize,
14 pub(super) subscriptions: std::sync::Arc<super::subscriptions::Subscriptions>,
15 pub(super) warning_only: std::sync::Arc<std::sync::atomic::AtomicBool>,
16 pub(super) listens: std::sync::Arc<super::listen::Listens>,
17 pub(super) listen_config: super::listen_config::ListenConfig,
18 pub(super) channel_source: Option<super::channel_source::Source>,
19 pub(super) initial_listens: Vec<String>,
20 pub(super) own_inbox: Option<String>,
23}
24
25impl<R> std::fmt::Debug for Server<R> {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 f.debug_struct("Server")
28 .field("session", &self.session)
29 .field("frame_bytes", &self.frame_bytes)
30 .field("listens", &self.listens)
31 .field("channel_source", &self.channel_source)
32 .finish_non_exhaustive()
33 }
34}
35
36impl<R: Rail> Server<R> {
37 #[must_use]
39 pub fn new(rail: R, frame_bytes: usize) -> Self {
40 Self {
41 rail,
42 session: Session::new(),
43 frame_bytes,
44 subscriptions: std::sync::Arc::default(),
45 warning_only: std::sync::Arc::default(),
46 listens: std::sync::Arc::default(),
47 listen_config: super::listen_config::ListenConfig::default(),
48 channel_source: None,
49 initial_listens: Vec::new(),
50 own_inbox: None,
51 }
52 }
53
54 pub fn configured(rail: R, config: &crate::config::Config) -> Result<Self, String> {
60 config.listen.validate(config.agent.as_deref())?;
61 let source = super::channel_source::Source::new(config)?;
62 let mut server = Self::new(rail, config.limits.frame_bytes);
63 server.listen_config = config.listen.clone();
64 server.initial_listens = config.listen.initial_subjects(config.agent.as_deref());
65 server.own_inbox = config
66 .agent
67 .as_deref()
68 .map(|agent| format!("agents.{agent}.inbox"))
69 .filter(|inbox| !config.listen.subjects.contains(inbox));
70 server.channel_source = Some(source);
71 Ok(server)
72 }
73
74 pub fn listen_counts(&self) -> Result<(usize, usize), String> {
80 Ok((
81 self.listens.count()?,
82 self.listens
83 .receivers
84 .load(std::sync::atomic::Ordering::SeqCst),
85 ))
86 }
87
88 pub(super) fn start_listens(&self) -> std::io::Result<()> {
89 let Some(source) = &self.channel_source else {
90 return Ok(());
91 };
92 for subject in &self.initial_listens {
93 let answer = self.listens.listen(source, &self.listen_config, subject);
94 if answer.is_error {
95 let words = answer.structured["error"]
96 .as_str()
97 .ok_or_else(|| std::io::Error::other("the listen refusal has no words"))?;
98 if self.own_inbox.as_deref() == Some(subject.as_str()) {
101 eprintln!("liminal mcp: {subject} is not listened: {words}");
102 continue;
103 }
104 let closed = self.listens.close_all();
105 return Err(std::io::Error::other(match closed {
106 Ok(()) => words.to_owned(),
107 Err(close) => format!("{words}; {close}"),
108 }));
109 }
110 }
111 Ok(())
112 }
113
114 #[must_use]
116 pub const fn rail(&self) -> &R {
117 &self.rail
118 }
119
120 pub fn answer(&mut self, id: &Value, method: &str, params: &Map<String, Value>) -> Value {
122 match self.respond(method, params) {
123 Ok(result) => framing::result(id, &result),
124 Err(refusal) => framing::error(id, refusal.code, &refusal.message, refusal.data),
125 }
126 }
127
128 fn respond(&mut self, method: &str, params: &Map<String, Value>) -> Result<Value, Refusal> {
129 match method {
130 "initialize" => return self.session.initialize(params),
131 "server/discover" => {
132 self.session.era_of(params)?;
133 return Ok(Session::discover());
134 }
135 _ => {}
136 }
137 let era = self.session.era_of(params)?;
138 match method {
139 "ping" => Ok(shaped(era, Map::new())),
140 "logging/setLevel" => {
141 super::notifications::set_level(params, &self.warning_only)?;
142 Ok(shaped(era, Map::new()))
143 }
144 "tools/list" => {
145 let mut result = Map::new();
146 result.insert("tools".to_owned(), tool_table());
147 if era == Era::Modern {
148 result.insert("ttlMs".to_owned(), json!(TOOLS_TTL_MS));
153 result.insert("cacheScope".to_owned(), json!("public"));
154 }
155 Ok(shaped(era, result))
156 }
157 "tools/call" => {
158 let name = params
159 .get("name")
160 .and_then(Value::as_str)
161 .ok_or_else(|| Refusal {
162 code: INVALID_PARAMS,
163 message: "the request is missing name".to_owned(),
164 data: None,
165 })?;
166 let arguments = params
167 .get("arguments")
168 .and_then(Value::as_object)
169 .cloned()
170 .unwrap_or_default();
171 let outcome = self.tool(name, &arguments)?;
172 Ok(outcome.into_result(era))
173 }
174 other => Err(Refusal {
175 code: METHOD_NOT_FOUND,
176 message: format!("the ticket has no method {other}"),
177 data: None,
178 }),
179 }
180 }
181
182 pub fn tool(&self, name: &str, arguments: &Map<String, Value>) -> Result<ToolResult, Refusal> {
187 if name == "send" {
188 return super::channel_send::send(self, arguments);
189 }
190 if matches!(name, "listen" | "unlisten") {
191 let subject = super::listen::argument(arguments)?;
192 if name == "unlisten" {
193 return Ok(self.listens.unlisten(subject));
194 }
195 let Some(source) = &self.channel_source else {
196 return Ok(ToolResult::failed(
197 "the ticket has no seat configuration for channel tools",
198 json!({"subject":subject}),
199 ));
200 };
201 return Ok(self.listens.listen(source, &self.listen_config, subject));
202 }
203 if matches!(name, "subscribe" | "unsubscribe") {
204 let service = super::subscriptions::service_argument(arguments)?;
205 return Ok(self.subscriptions.tool(&self.rail, name, service));
206 }
207 if let Some(outcome) = crate::network::lifecycle::tool(&self.rail, name, arguments) {
208 return Ok(outcome);
209 }
210 if let Some(outcome) = super::plan_tools::tool(&self.rail, name, arguments)
211 .or_else(|| super::template_tools::tool(&self.rail, name, arguments))
212 {
213 return Ok(outcome);
214 }
215 let outcome = match name {
216 "list" => directory::list(&self.rail),
217 "describe" => {
218 let subject = string(arguments, "subject")?;
219 directory::describe(&self.rail, subject)
220 }
221 "call" => {
222 let subject = string(arguments, "subject")?;
223 let input = object(arguments, "input")?;
224 let reply = arguments
225 .get("reply")
226 .and_then(Value::as_bool)
227 .unwrap_or(true);
228 let pass_for = match arguments.get("pass_for") {
229 None => None,
230 Some(Value::String(audience)) => Some(audience.as_str()),
231 Some(_) => {
232 return Err(Refusal {
233 code: INVALID_PARAMS,
234 message: "the tool call's pass_for is not a string".to_owned(),
235 data: None,
236 });
237 }
238 };
239 call::call(&self.rail, subject, input, reply, pass_for)
240 }
241 "flow" => {
242 let steps = arguments
243 .get("steps")
244 .and_then(Value::as_array)
245 .ok_or_else(|| Refusal {
246 code: INVALID_PARAMS,
247 message: "the flow is missing steps".to_owned(),
248 data: None,
249 })?;
250 flow::flow(&self.rail, steps)
251 }
252 other => {
253 return Err(Refusal {
254 code: METHOD_NOT_FOUND,
255 message: format!("the ticket has no tool {other}"),
256 data: None,
257 });
258 }
259 };
260 Ok(outcome)
261 }
262}