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) answer_bytes: usize,
16 pub(super) answer_dir: Option<std::path::PathBuf>,
17 pub(super) subscriptions: std::sync::Arc<super::subscriptions::Subscriptions>,
18 pub(super) warning_only: std::sync::Arc<std::sync::atomic::AtomicBool>,
19 pub(super) listens: std::sync::Arc<super::listen::Listens>,
20 pub(super) listen_config: super::listen_config::ListenConfig,
21 pub(super) channel_source: Option<super::channel_source::Source>,
22 pub(super) initial_listens: Vec<String>,
23 pub(super) own_inbox: Option<String>,
26}
27
28fn answer_dir(name: Option<&str>) -> Option<std::path::PathBuf> {
33 let home = std::env::var_os("HOME").filter(|home| !home.is_empty())?;
34 Some(
35 std::path::PathBuf::from(home)
36 .join(".liminal")
37 .join("answers")
38 .join(format!("{}-{}", name.unwrap_or("seat"), std::process::id())),
39 )
40}
41
42impl<R> std::fmt::Debug for Server<R> {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 f.debug_struct("Server")
45 .field("session", &self.session)
46 .field("frame_bytes", &self.frame_bytes)
47 .field("listens", &self.listens)
48 .field("channel_source", &self.channel_source)
49 .finish_non_exhaustive()
50 }
51}
52
53impl<R: Rail> Server<R> {
54 #[must_use]
56 pub fn new(rail: R, frame_bytes: usize) -> Self {
57 Self {
58 rail,
59 session: Session::new(),
60 frame_bytes,
61 answer_bytes: 16_777_216,
62 answer_dir: answer_dir(None),
63 subscriptions: std::sync::Arc::default(),
64 warning_only: std::sync::Arc::default(),
65 listens: std::sync::Arc::default(),
66 listen_config: super::listen_config::ListenConfig::default(),
67 channel_source: None,
68 initial_listens: Vec::new(),
69 own_inbox: None,
70 }
71 }
72
73 pub fn configured(rail: R, config: &crate::config::Config) -> Result<Self, String> {
79 config.listen.validate(config.agent.as_deref())?;
80 let source = super::channel_source::Source::new(config)?;
81 let mut server = Self::new(rail, config.limits.frame_bytes);
82 server.answer_bytes = config.limits.answer_bytes;
83 server.answer_dir = answer_dir(config.agent.as_deref());
84 server.listen_config = config.listen.clone();
85 server.initial_listens = config.listen.initial_subjects(config.agent.as_deref());
86 server.own_inbox = config
87 .agent
88 .as_deref()
89 .map(|agent| format!("agents.{agent}.inbox"))
90 .filter(|inbox| !config.listen.subjects.contains(inbox));
91 server.channel_source = Some(source);
92 Ok(server)
93 }
94
95 pub fn listen_counts(&self) -> Result<(usize, usize), String> {
101 Ok((
102 self.listens.count()?,
103 self.listens
104 .receivers
105 .load(std::sync::atomic::Ordering::SeqCst),
106 ))
107 }
108
109 pub(super) fn start_listens(&self) -> std::io::Result<()> {
110 let Some(source) = &self.channel_source else {
111 return Ok(());
112 };
113 for subject in &self.initial_listens {
114 let answer = self.listens.listen(source, &self.listen_config, subject);
115 if answer.is_error {
116 let words = answer.structured["error"]
117 .as_str()
118 .ok_or_else(|| std::io::Error::other("the listen refusal has no words"))?;
119 if self.own_inbox.as_deref() == Some(subject.as_str()) {
122 eprintln!("liminal mcp: {subject} is not listened: {words}");
123 continue;
124 }
125 let closed = self.listens.close_all();
126 return Err(std::io::Error::other(match closed {
127 Ok(()) => words.to_owned(),
128 Err(close) => format!("{words}; {close}"),
129 }));
130 }
131 }
132 Ok(())
133 }
134
135 #[must_use]
137 pub const fn rail(&self) -> &R {
138 &self.rail
139 }
140
141 pub fn answer(&mut self, id: &Value, method: &str, params: &Map<String, Value>) -> Value {
143 match self.respond(method, params) {
144 Ok(result) => framing::result(id, &result),
145 Err(refusal) => framing::error(id, refusal.code, &refusal.message, refusal.data),
146 }
147 }
148
149 fn respond(&mut self, method: &str, params: &Map<String, Value>) -> Result<Value, Refusal> {
150 match method {
151 "initialize" => return self.session.initialize(params),
152 "server/discover" => {
153 self.session.era_of(params)?;
154 return Ok(Session::discover());
155 }
156 _ => {}
157 }
158 let era = self.session.era_of(params)?;
159 match method {
160 "ping" => Ok(shaped(era, Map::new())),
161 "logging/setLevel" => {
162 super::notifications::set_level(params, &self.warning_only)?;
163 Ok(shaped(era, Map::new()))
164 }
165 "tools/list" => {
166 let mut result = Map::new();
167 result.insert("tools".to_owned(), tool_table());
168 if era == Era::Modern {
169 result.insert("ttlMs".to_owned(), json!(TOOLS_TTL_MS));
174 result.insert("cacheScope".to_owned(), json!("public"));
175 }
176 Ok(shaped(era, result))
177 }
178 "tools/call" => {
179 let name = params
180 .get("name")
181 .and_then(Value::as_str)
182 .ok_or_else(|| Refusal {
183 code: INVALID_PARAMS,
184 message: "the request is missing name".to_owned(),
185 data: None,
186 })?;
187 let arguments = params
188 .get("arguments")
189 .and_then(Value::as_object)
190 .cloned()
191 .unwrap_or_default();
192 let outcome = self.tool(name, &arguments)?;
193 Ok(outcome.into_result(era))
194 }
195 other => Err(Refusal {
196 code: METHOD_NOT_FOUND,
197 message: format!("the ticket has no method {other}"),
198 data: None,
199 }),
200 }
201 }
202
203 pub fn tool(&self, name: &str, arguments: &Map<String, Value>) -> Result<ToolResult, Refusal> {
208 if name == "send" {
209 return super::channel_send::send(self, arguments);
210 }
211 if matches!(name, "listen" | "unlisten") {
212 let subject = super::listen::argument(arguments)?;
213 if name == "unlisten" {
214 return Ok(self.listens.unlisten(subject));
215 }
216 let Some(source) = &self.channel_source else {
217 return Ok(ToolResult::failed(
218 "the ticket has no seat configuration for channel tools",
219 json!({"subject":subject}),
220 ));
221 };
222 return Ok(self.listens.listen(source, &self.listen_config, subject));
223 }
224 if matches!(name, "subscribe" | "unsubscribe") {
225 let service = super::subscriptions::service_argument(arguments)?;
226 return Ok(self.subscriptions.tool(&self.rail, name, service));
227 }
228 if let Some(outcome) = crate::network::lifecycle::tool(&self.rail, name, arguments) {
229 return Ok(outcome);
230 }
231 if let Some(outcome) = super::plan_tools::tool(&self.rail, name, arguments)
232 .or_else(|| super::template_tools::tool(&self.rail, name, arguments))
233 {
234 return Ok(outcome);
235 }
236 let outcome = match name {
237 "list" => directory::list(&self.rail),
238 "describe" => {
239 let subject = string(arguments, "subject")?;
240 directory::describe(&self.rail, subject)
241 }
242 "call" => {
243 let subject = string(arguments, "subject")?;
244 let input = object(arguments, "input")?;
245 let reply = arguments
246 .get("reply")
247 .and_then(Value::as_bool)
248 .unwrap_or(true);
249 let pass_for = match arguments.get("pass_for") {
250 None => None,
251 Some(Value::String(audience)) => Some(audience.as_str()),
252 Some(_) => {
253 return Err(Refusal {
254 code: INVALID_PARAMS,
255 message: "the tool call's pass_for is not a string".to_owned(),
256 data: None,
257 });
258 }
259 };
260 call::call(&self.rail, subject, input, reply, pass_for)
261 }
262 "flow" => {
263 let steps = arguments
264 .get("steps")
265 .and_then(Value::as_array)
266 .ok_or_else(|| Refusal {
267 code: INVALID_PARAMS,
268 message: "the flow is missing steps".to_owned(),
269 data: None,
270 })?;
271 let last = match arguments.get("answer").and_then(Value::as_str) {
272 None | Some("all") => false,
273 Some("last") => true,
274 Some(other) => {
275 return Err(Refusal {
276 code: INVALID_PARAMS,
277 message: format!("the flow's answer is {other}; it is all or last"),
278 data: None,
279 });
280 }
281 };
282 flow::flow_answering(&self.rail, steps, last)
283 }
284 other => {
285 return Err(Refusal {
286 code: METHOD_NOT_FOUND,
287 message: format!("the ticket has no tool {other}"),
288 data: None,
289 });
290 }
291 };
292 Ok(outcome)
293 }
294}