liminal_cli/mcp/
negotiate.rs1use serde_json::{Map, Value, json};
13
14use super::framing::{INVALID_PARAMS, UNSUPPORTED_PROTOCOL_VERSION};
15
16pub const MODERN: &str = "2026-07-28";
18pub const LEGACY: &str = "2025-11-25";
20pub const SUPPORTED: [&str; 2] = [MODERN, LEGACY];
22
23const META: &str = "_meta";
24const META_PROTOCOL_VERSION: &str = "io.modelcontextprotocol/protocolVersion";
25const META_CLIENT_CAPABILITIES: &str = "io.modelcontextprotocol/clientCapabilities";
26const META_SERVER_INFO: &str = "io.modelcontextprotocol/serverInfo";
27
28#[must_use]
30pub fn server_info() -> Value {
31 json!({ "name": "liminal", "version": env!("CARGO_PKG_VERSION") })
32}
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum Era {
37 Modern,
39 Legacy,
41}
42
43#[derive(Debug, PartialEq, Eq)]
45pub struct Refusal {
46 pub code: i64,
48 pub message: String,
50 pub data: Option<Value>,
52}
53
54#[derive(Debug, Default)]
56pub struct Session {
57 legacy: bool,
58}
59
60impl Session {
61 #[must_use]
63 pub const fn new() -> Self {
64 Self { legacy: false }
65 }
66
67 #[must_use]
69 pub const fn is_legacy(&self) -> bool {
70 self.legacy
71 }
72
73 pub fn era_of(&self, params: &Map<String, Value>) -> Result<Era, Refusal> {
82 let meta = params.get(META).and_then(Value::as_object);
83 let version = meta.and_then(|meta| meta.get(META_PROTOCOL_VERSION));
84 match version {
85 Some(Value::String(version)) if version == MODERN => {
86 if meta
87 .and_then(|meta| meta.get(META_CLIENT_CAPABILITIES))
88 .is_none()
89 {
90 return Err(invalid_params(META_CLIENT_CAPABILITIES));
91 }
92 Ok(Era::Modern)
93 }
94 Some(Value::String(version)) => Err(unsupported(version)),
95 None if self.legacy => Ok(Era::Legacy),
96 Some(_) | None => Err(invalid_params(META_PROTOCOL_VERSION)),
97 }
98 }
99
100 pub fn initialize(&mut self, params: &Map<String, Value>) -> Result<Value, Refusal> {
106 params
107 .get("protocolVersion")
108 .and_then(Value::as_str)
109 .ok_or_else(|| invalid_params("protocolVersion"))?;
110 self.legacy = true;
111 Ok(json!({
112 "protocolVersion": LEGACY,
113 "capabilities": { "tools": {}, "logging": {}, "experimental": { "claude/channel": {} } },
114 "serverInfo": server_info(),
115 }))
116 }
117
118 #[must_use]
120 pub fn discover() -> Value {
121 json!({
122 "resultType": "complete",
123 "supportedVersions": [MODERN],
124 "capabilities": { "tools": {}, "logging": {}, "experimental": { "claude/channel": {} } },
125 META: { META_SERVER_INFO: server_info() },
126 "instructions": "The ticket to a liminal network: list the stations, describe one, call one by its subject, or run a flow of calls. What call may reach is the pass's grant.",
127 })
128 }
129}
130
131#[must_use]
134pub fn shaped(era: Era, mut result: Map<String, Value>) -> Value {
135 if era == Era::Modern {
136 result.insert("resultType".to_owned(), json!("complete"));
137 result.insert(META.to_owned(), json!({ META_SERVER_INFO: server_info() }));
138 }
139 Value::Object(result)
140}
141
142fn unsupported(requested: &str) -> Refusal {
143 Refusal {
144 code: UNSUPPORTED_PROTOCOL_VERSION,
145 message: "Unsupported protocol version".to_owned(),
146 data: Some(json!({ "supported": SUPPORTED, "requested": requested })),
147 }
148}
149
150fn invalid_params(field: &str) -> Refusal {
151 Refusal {
152 code: INVALID_PARAMS,
153 message: format!("the request is missing {field}"),
154 data: None,
155 }
156}
157
158#[cfg(test)]
159#[path = "negotiate_tests.rs"]
160mod tests;