Skip to main content

flymcprust_core/
legacy.rs

1//! Compatibility with clients of the 2025-11-25 and 2025-06-18 protocol
2//! versions ("legacy" clients), on top of the same stateless core.
3//!
4//! Legacy clients open with `initialize`, then send requests without the
5//! per-request `_meta` of 2026-07-28, and expect the server to *ask* them for
6//! input (elicitation, sampling, roots) with server-to-client requests. The
7//! adapter:
8//!
9//! - answers `initialize` with the negotiated version and the server's
10//!   capabilities, and remembers what the client declared in a
11//!   [`LegacySession`] (per connection on stdio; on HTTP, in a signed
12//!   `MCP-Session-Id` token, so the server stays stateless);
13//! - runs each request through the normal dispatch, with the session
14//!   standing in for `_meta`;
15//! - turns multi round-trip results into server-to-client requests through
16//!   a transport-provided [`Peer`], feeds the answers back, and reruns the
17//!   handler, so handlers using `ctx.elicit` work unchanged;
18//! - adapts results and errors to the older wire format.
19
20// The transports use most of this module.
21#![cfg_attr(not(all(feature = "stdio", feature = "http")), allow(dead_code))]
22// Legacy clients still use logging, deprecated in 2026-07-28 (SEP-2577).
23#![allow(deprecated)]
24
25use std::collections::HashMap;
26use std::future::Future;
27use std::pin::Pin;
28use std::sync::{Arc, Mutex, PoisonError};
29use std::time::Duration;
30
31use serde::{Deserialize, Serialize};
32use serde_json::value::RawValue;
33use serde_json::{Map, Value};
34use tokio::sync::{mpsc, oneshot};
35use tokio_util::sync::CancellationToken;
36
37use crate::context::{RequestMeta, TransportContext};
38use crate::error::McpError;
39use crate::jsonrpc::{ClientResponse, Message, Request, Response};
40use crate::mrtr::now_secs;
41use crate::server::{McpRequest, Server};
42use crate::spec::{self, RequestId};
43
44/// Legacy protocol versions the server accepts, newest first.
45pub const LEGACY_PROTOCOL_VERSIONS: &[&str] = &["2025-11-25", "2025-06-18"];
46
47/// Domain separation for session tokens.
48const SESSION_CONTEXT: &[u8] = b"flymcprust/legacy-session/v1\0";
49
50/// Upper bound on input round trips for one legacy request.
51const MAX_INPUT_ROUNDS: usize = 16;
52
53/// Largest serialized `capabilities` plus `clientInfo` kept from
54/// `initialize`; on HTTP they travel in every request's session header.
55const MAX_DECLARATION_BYTES: usize = 8 * 1024;
56
57/// The 2025-11-25 code for "resource not found".
58const LEGACY_RESOURCE_NOT_FOUND: i64 = -32002;
59
60/// What a legacy client declared at `initialize`.
61#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
62pub struct LegacySession {
63    /// Negotiated protocol version.
64    #[serde(rename = "v")]
65    pub(crate) version: String,
66    /// Client capabilities, as sent.
67    #[serde(rename = "c", default)]
68    pub(crate) capabilities: Value,
69    /// Client implementation info, as sent.
70    #[serde(rename = "i", default, skip_serializing_if = "Option::is_none")]
71    pub(crate) client_info: Option<Value>,
72    /// Expiry of the session token (seconds since the Unix epoch).
73    #[serde(rename = "e", default)]
74    pub(crate) expires: u64,
75    /// Random nonce: two clients declaring the same things at the same time
76    /// still get different session ids.
77    #[serde(rename = "n", default)]
78    pub(crate) nonce: String,
79    /// Minimum log level set with `logging/setLevel` (stdio only; HTTP
80    /// sessions are immutable tokens). `None`: every level.
81    #[allow(deprecated)]
82    #[serde(rename = "l", default, skip_serializing_if = "Option::is_none")]
83    pub(crate) log_level: Option<spec::LoggingLevel>,
84    /// Digest of the authenticated principal (HTTP with OAuth): the session
85    /// is only valid with a token of the same user.
86    #[serde(rename = "p", default, skip_serializing_if = "Option::is_none")]
87    pub(crate) principal: Option<String>,
88}
89
90impl LegacySession {
91    /// A session for a request that carries a legacy version but no session
92    /// (the client's capabilities are then unknown, so none are assumed).
93    pub(crate) fn sessionless(version: &str) -> Self {
94        Self {
95            version: version.to_owned(),
96            ..Self::default()
97        }
98    }
99
100    /// The negotiated protocol version.
101    #[must_use]
102    pub fn version(&self) -> &str {
103        &self.version
104    }
105
106    /// Stands in for the per-request `_meta` of 2026-07-28.
107    pub(crate) fn request_meta(&self) -> RequestMeta {
108        RequestMeta {
109            protocol_version: self.version.clone(),
110            client_capabilities: serde_json::from_value(self.capabilities.clone())
111                .unwrap_or_default(),
112            client_info: self
113                .client_info
114                .clone()
115                .and_then(|info| serde_json::from_value(info).ok()),
116            progress_token: None,
117            #[allow(deprecated)]
118            log_level: Some(self.log_level.unwrap_or(spec::LoggingLevel::Debug)),
119        }
120    }
121}
122
123/// Whether a protocol version is one of the supported legacy versions.
124#[must_use]
125pub fn is_legacy_version(version: &str) -> bool {
126    LEGACY_PROTOCOL_VERSIONS.contains(&version)
127}
128
129/// Sends server-to-client requests on behalf of the legacy adapter.
130pub(crate) trait Peer: Send + Sync {
131    /// Sends `method` with `params` to the client and waits for its result.
132    fn request<'a>(
133        &'a self,
134        method: &'a str,
135        params: &'a Value,
136    ) -> Pin<Box<dyn Future<Output = Result<Value, McpError>> + Send + 'a>>;
137}
138
139/// Server-to-client requests waiting for the client's answer. Ids are
140/// random (128 bits), so a client cannot guess another one's pending
141/// request, and each entry remembers which connection or session asked.
142#[derive(Debug, Default)]
143pub(crate) struct PendingRequests {
144    map: Mutex<HashMap<String, Pending>>,
145}
146
147#[derive(Debug)]
148struct Pending {
149    owner: u64,
150    reply: oneshot::Sender<Result<Value, Value>>,
151}
152
153impl PendingRequests {
154    fn register(&self, owner: u64) -> Option<(String, oneshot::Receiver<Result<Value, Value>>)> {
155        let mut bytes = [0u8; 16];
156        getrandom::fill(&mut bytes).ok()?;
157        let id: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
158        let id = format!("flymcprust-{id}");
159        let (reply, receiver) = oneshot::channel();
160        self.lock().insert(id.clone(), Pending { owner, reply });
161        Some((id, receiver))
162    }
163
164    fn forget(&self, id: &str) {
165        self.lock().remove(id);
166    }
167
168    /// Delivers the client's answer. `false` if no request of this owner has
169    /// that id.
170    pub(crate) fn complete(&self, response: ClientResponse, owner: u64) -> bool {
171        let RequestId::String(id) = &response.id else {
172            return false;
173        };
174        let mut map = self.lock();
175        match map.get(id) {
176            Some(pending) if pending.owner == owner => {}
177            _ => return false,
178        }
179        map.remove(id)
180            .is_some_and(|pending| pending.reply.send(response.outcome).is_ok())
181    }
182
183    fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Pending>> {
184        self.map.lock().unwrap_or_else(PoisonError::into_inner)
185    }
186}
187
188/// A [`Peer`] that writes requests to a channel (the stdio writer, or an
189/// HTTP response stream) and waits for answers routed by the transport.
190#[derive(Debug, Clone)]
191pub(crate) struct ChannelPeer {
192    pub(crate) pending: Arc<PendingRequests>,
193    pub(crate) owner: u64,
194    pub(crate) out: mpsc::Sender<Vec<u8>>,
195    pub(crate) timeout: Duration,
196}
197
198impl Peer for ChannelPeer {
199    fn request<'a>(
200        &'a self,
201        method: &'a str,
202        params: &'a Value,
203    ) -> Pin<Box<dyn Future<Output = Result<Value, McpError>> + Send + 'a>> {
204        Box::pin(async move {
205            let (id, receiver) = self
206                .pending
207                .register(self.owner)
208                .ok_or_else(McpError::internal)?;
209            let message = serde_json::json!({
210                "jsonrpc": "2.0", "id": id, "method": method, "params": params,
211            });
212            let bytes = serde_json::to_vec(&message).map_err(|_| McpError::internal())?;
213            // A client that stops reading must not hold the handler forever.
214            match tokio::time::timeout(self.timeout, self.out.send(bytes)).await {
215                Ok(Ok(())) => {}
216                Ok(Err(_)) | Err(_) => {
217                    self.pending.forget(&id);
218                    return Err(McpError::internal_with_message(
219                        "The connection to the client is closed or not reading.",
220                    ));
221                }
222            }
223            match tokio::time::timeout(self.timeout, receiver).await {
224                Ok(Ok(Ok(result))) => Ok(result),
225                Ok(Ok(Err(_error))) => Err(McpError::request_failed(format!(
226                    "The client returned an error for `{method}`."
227                ))),
228                Ok(Err(_)) | Err(_) => {
229                    self.pending.forget(&id);
230                    tracing::warn!(
231                        "a client did not answer a `{method}` request in time\n  = help: the \
232                         wait is bounded by ServerBuilder::tool_timeout(..)"
233                    );
234                    Err(McpError::internal_with_message(
235                        "The client did not answer in time.",
236                    ))
237                }
238            }
239        })
240    }
241}
242
243/// The legacy-protocol data a server needs, computed at build time.
244#[derive(Debug)]
245pub(crate) struct LegacyConfig {
246    pub(crate) enabled: bool,
247    pub(crate) session_ttl: Duration,
248    pub(crate) capabilities: Value,
249    pub(crate) server_info: Value,
250    pub(crate) instructions: Option<String>,
251}
252
253impl LegacyConfig {
254    pub(crate) fn new(
255        enabled: bool,
256        session_ttl: Duration,
257        capabilities: &spec::ServerCapabilities,
258        server_info: &spec::Implementation,
259        instructions: Option<&str>,
260    ) -> Self {
261        let mut capabilities = serde_json::to_value(capabilities).unwrap_or_default();
262        if let Value::Object(map) = &mut capabilities {
263            // Extensions are formalized in 2026-07-28, but MCP Apps hosts
264            // already negotiate `io.modelcontextprotocol/ui` with 2025-11-25.
265            let apps = map
266                .get("extensions")
267                .and_then(|e| e.get(crate::apps::EXTENSION_ID))
268                .cloned();
269            map.remove("extensions");
270            if let Some(settings) = apps {
271                let mut extensions = Map::new();
272                extensions.insert(crate::apps::EXTENSION_ID.to_owned(), settings);
273                map.insert("extensions".to_owned(), Value::Object(extensions));
274            }
275        }
276        Self {
277            enabled,
278            session_ttl,
279            capabilities,
280            server_info: serde_json::to_value(server_info).unwrap_or_default(),
281            instructions: instructions.map(str::to_owned),
282        }
283    }
284}
285
286#[derive(Deserialize)]
287struct InitializeParams {
288    #[serde(rename = "protocolVersion")]
289    protocol_version: String,
290    #[serde(default)]
291    capabilities: Value,
292    #[serde(rename = "clientInfo", default)]
293    client_info: Option<Value>,
294}
295
296fn to_raw(value: &Value) -> Result<Box<RawValue>, McpError> {
297    serde_json::value::to_raw_value(value).map_err(|_| McpError::internal())
298}
299
300impl Server {
301    /// Whether legacy (2025-11-25, 2025-06-18) clients are served.
302    #[must_use]
303    pub fn legacy_clients_enabled(&self) -> bool {
304        self.legacy().enabled
305    }
306
307    /// Answers `initialize`: negotiates the version and returns the
308    /// `InitializeResult` plus the session to remember.
309    pub(crate) fn legacy_initialize(
310        &self,
311        params: Option<&RawValue>,
312    ) -> Result<(Box<RawValue>, LegacySession), McpError> {
313        let config = self.legacy();
314        let params: InitializeParams = serde_json::from_str(params.map_or("{}", RawValue::get))
315            .map_err(|e| {
316                McpError::invalid_params(format!(
317                    "Invalid initialize params: {}",
318                    crate::error::sanitize(&e.to_string())
319                ))
320            })?;
321        let declared = serde_json::to_vec(&params.capabilities)
322            .map_or(usize::MAX, |c| c.len())
323            .saturating_add(
324                params
325                    .client_info
326                    .as_ref()
327                    .and_then(|i| serde_json::to_vec(i).ok())
328                    .map_or(0, |i| i.len()),
329            );
330        if declared > MAX_DECLARATION_BYTES {
331            return Err(McpError::invalid_params(
332                "Client capabilities and info are too large",
333            ));
334        }
335        let version = if is_legacy_version(&params.protocol_version) {
336            params.protocol_version
337        } else {
338            // The client decides whether it can work with our newest version.
339            LEGACY_PROTOCOL_VERSIONS
340                .first()
341                .copied()
342                .unwrap_or_default()
343                .to_owned()
344        };
345        let mut result = Map::new();
346        result.insert("protocolVersion".to_owned(), Value::from(version.clone()));
347        result.insert("capabilities".to_owned(), config.capabilities.clone());
348        result.insert("serverInfo".to_owned(), config.server_info.clone());
349        if let Some(instructions) = &config.instructions {
350            result.insert("instructions".to_owned(), Value::from(instructions.clone()));
351        }
352        let mut nonce = [0u8; 16];
353        getrandom::fill(&mut nonce).map_err(|_| McpError::internal())?;
354        let session = LegacySession {
355            version,
356            capabilities: params.capabilities,
357            client_info: params.client_info,
358            expires: now_secs().saturating_add(config.session_ttl.as_secs()),
359            nonce: nonce.iter().map(|b| format!("{b:02x}")).collect(),
360            log_level: None,
361            principal: None,
362        };
363        Ok((to_raw(&Value::Object(result))?, session))
364    }
365
366    /// Signs a session into an `MCP-Session-Id` value.
367    pub(crate) fn seal_session(&self, session: &LegacySession) -> Option<String> {
368        self.state_codec().seal_for(SESSION_CONTEXT, session)
369    }
370
371    /// Verifies an `MCP-Session-Id` value; `None` if invalid or expired.
372    pub(crate) fn open_session(&self, token: &str) -> Option<LegacySession> {
373        self.state_codec()
374            .open_for::<LegacySession>(SESSION_CONTEXT, token)
375            .filter(|s| s.expires >= now_secs() && is_legacy_version(&s.version))
376    }
377
378    /// Handles one request from a legacy client. Input requests of multi
379    /// round-trip handlers are sent to the client through `peer`.
380    pub(crate) async fn handle_legacy(
381        &self,
382        request: Request,
383        session: &LegacySession,
384        mut transport: TransportContext,
385        cancellation: CancellationToken,
386        peer: &dyn Peer,
387    ) -> Option<Response> {
388        let Request { id, method, params } = request;
389        match method.as_str() {
390            "initialize" => {
391                return Some(match self.legacy_initialize(params.as_deref()) {
392                    Ok((result, _)) => Response::Result { id, result },
393                    Err(error) => Response::error(Some(id), error),
394                });
395            }
396            // Legacy log level: kept per connection on stdio (which answers it
397            // before reaching here); HTTP sessions cannot change, so the
398            // request is acknowledged and every level stays enabled.
399            "logging/setLevel" | "ping" => {
400                return Some(match to_raw(&Value::Object(Map::new())) {
401                    Ok(result) => Response::Result { id, result },
402                    Err(error) => Response::error(Some(id), error),
403                });
404            }
405            _ => {}
406        }
407        transport.insert(session.clone());
408
409        let mut params = match params
410            .as_deref()
411            .map(|p| serde_json::from_str::<Value>(p.get()))
412        {
413            None => Map::new(),
414            Some(Ok(Value::Object(map))) => map,
415            Some(Ok(Value::Null)) => Map::new(),
416            Some(_) => {
417                return Some(Response::error(
418                    Some(id),
419                    McpError::invalid_params("params must be an object"),
420                ));
421            }
422        };
423        for _ in 0..MAX_INPUT_ROUNDS {
424            let raw = match to_raw(&Value::Object(params.clone())) {
425                Ok(raw) => raw,
426                Err(error) => return Some(Response::error(Some(id), error)),
427            };
428            let response = self
429                .handle(McpRequest {
430                    message: Message::Request(Request {
431                        id: id.clone(),
432                        method: method.clone(),
433                        params: Some(raw),
434                    }),
435                    transport: transport.clone(),
436                    cancellation: cancellation.clone(),
437                })
438                .await?;
439            let (id, result) = match response {
440                Response::Error { id, error } => {
441                    return Some(Response::Error {
442                        id,
443                        error: legacy_error(error),
444                    });
445                }
446                Response::Result { id, result } => (id, result),
447            };
448            let Ok(Value::Object(mut result)) = serde_json::from_str::<Value>(result.get()) else {
449                return Some(Response::error(Some(id), McpError::internal()));
450            };
451            if result.get("resultType").and_then(Value::as_str) != Some("input_required") {
452                for key in ["resultType", "ttlMs", "cacheScope"] {
453                    result.remove(key);
454                }
455                return Some(match to_raw(&Value::Object(result)) {
456                    Ok(result) => Response::Result { id, result },
457                    Err(error) => Response::error(Some(id), error),
458                });
459            }
460
461            // Ask the client, then rerun the handler with the answers.
462            let mut answers = Map::new();
463            if let Some(Value::Object(requests)) = result.remove("inputRequests") {
464                for (key, request) in requests {
465                    let method = request
466                        .get("method")
467                        .and_then(Value::as_str)
468                        .unwrap_or_default();
469                    let empty = Value::Object(Map::new());
470                    let request_params = request.get("params").unwrap_or(&empty);
471                    let answer = tokio::select! {
472                        biased;
473                        () = cancellation.cancelled() => return None,
474                        answer = peer.request(method, request_params) => answer,
475                    };
476                    match answer {
477                        Ok(answer) => {
478                            answers.insert(key, answer);
479                        }
480                        Err(error) => return Some(Response::error(Some(id), error)),
481                    }
482                }
483            }
484            params.insert("inputResponses".to_owned(), Value::Object(answers));
485            match result.remove("requestState") {
486                Some(state) => params.insert("requestState".to_owned(), state),
487                None => params.remove("requestState"),
488            };
489        }
490        tracing::warn!(
491            "a legacy request needed more than {MAX_INPUT_ROUNDS} input rounds\n  = help: a \
492             handler keeps asking for input; check that each `ctx.elicit` key is answered once"
493        );
494        Some(Response::error(
495            Some(id),
496            McpError::internal_with_message("Too many input rounds."),
497        ))
498    }
499}
500
501/// Adapts an error to the 2025-11-25 wire format.
502fn legacy_error(error: McpError) -> McpError {
503    let resource_not_found = error.code() == spec::error_codes::INVALID_PARAMS_ERROR
504        && error.message().starts_with("Resource not found")
505        && error.data().is_some_and(|d| d.get("uri").is_some());
506    if resource_not_found {
507        error.with_code(LEGACY_RESOURCE_NOT_FOUND)
508    } else {
509        error
510    }
511}
512
513#[cfg(test)]
514mod tests {
515    use super::*;
516
517    #[test]
518    fn versions() {
519        assert!(is_legacy_version("2025-11-25"));
520        assert!(is_legacy_version("2025-06-18"));
521        assert!(!is_legacy_version("2025-03-26"));
522        assert!(!is_legacy_version("2026-07-28"));
523    }
524
525    #[tokio::test]
526    async fn a_client_that_stops_reading_does_not_block_the_handler() {
527        let (out, _unread) = mpsc::channel(1);
528        out.send(b"queued".to_vec()).await.unwrap();
529        let peer = ChannelPeer {
530            pending: Arc::default(),
531            owner: 0,
532            out,
533            timeout: Duration::from_millis(50),
534        };
535        let started = std::time::Instant::now();
536        let params = Value::Object(Map::new());
537        assert!(peer.request("elicitation/create", &params).await.is_err());
538        assert!(started.elapsed() < Duration::from_secs(2));
539        assert!(peer.pending.lock().is_empty());
540    }
541
542    #[test]
543    fn legacy_capabilities_keep_only_the_apps_extension() {
544        let mut extensions = std::collections::BTreeMap::new();
545        extensions.insert(crate::apps::EXTENSION_ID.to_owned(), Map::new());
546        extensions.insert("com.example/other".to_owned(), Map::new());
547        let capabilities = spec::ServerCapabilities {
548            extensions: Some(extensions),
549            ..spec::ServerCapabilities::default()
550        };
551        let info = spec::Implementation::new("s".to_owned(), "1".to_owned());
552        let config = LegacyConfig::new(true, Duration::from_secs(60), &capabilities, &info, None);
553        assert_eq!(
554            config.capabilities["extensions"],
555            serde_json::json!({ "io.modelcontextprotocol/ui": {} })
556        );
557        let plain = LegacyConfig::new(
558            true,
559            Duration::from_secs(60),
560            &spec::ServerCapabilities::default(),
561            &info,
562            None,
563        );
564        assert!(plain.capabilities.get("extensions").is_none());
565    }
566
567    #[test]
568    fn resource_not_found_uses_the_legacy_code() {
569        let e = legacy_error(McpError::resource_not_found("x://y"));
570        assert_eq!(e.code(), -32002);
571        assert_eq!(legacy_error(McpError::invalid_params("bad")).code(), -32602);
572    }
573}