Skip to main content

rust_web_server/mcp/
modern.rs

1//! MCP 2026-07-28 ("modern") requests, served beside the earlier versions
2//! on the same endpoint. A modern request is self-contained: no
3//! `initialize`, no session — its protocol version, client capabilities
4//! and identity ride in `params._meta`, mirrored in HTTP headers
5//! (`MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name`) that must agree with
6//! the body. A request is modern when its `_meta` names a protocol version
7//! or its `MCP-Protocol-Version` header isn't one of the earlier versions;
8//! anything else (an `initialize`, a session's requests) keeps the earlier
9//! behaviour.
10//!
11//! What changes on the modern path, beside validation (`-32602` for missing
12//! `_meta` fields, `-32020` for headers that disagree, `-32022` for a
13//! version this server doesn't speak — all `400`):
14//!
15//! - `server/discover` answers the versions, capabilities and identity;
16//!   `initialize`, `ping`, `logging/setLevel`, `resources/(un)subscribe`
17//!   and unknown methods are `404` with `-32601`.
18//! - Every result carries `resultType` and `_meta.serverInfo`; cacheable
19//!   ones `ttlMs` and `cacheScope` ([`McpServer::cache_hints`]).
20//! - Handlers ask the client for input by returning [`InputRequired`]
21//!   (multi round-trip requests) instead of sending it requests; the
22//!   client retries with `inputResponses` and the `requestState`, which
23//!   is HMAC-signed here — bound to the method, the tool or prompt, the
24//!   caller and ten minutes — and refused (`-32602`) when altered.
25//! - `subscriptions/listen` replaces the `GET` stream: an acknowledgment
26//!   first, then only the notification types asked for, each tagged with
27//!   the subscription's id.
28//! - Notifications about a request (progress, log messages — the latter
29//!   only when its `_meta` sets a `logLevel`) go on that request's own
30//!   response stream.
31
32use std::sync::mpsc::{self, SyncSender, TrySendError};
33use std::sync::Arc;
34
35use super::{
36    base64_decode, base64_encode, icons_json, json_escape, json_rpc, LogLevel, McpContext, McpServer,
37    PromptMessage, Response, SamplingRequest, SUPPORTED_PROTOCOL_VERSIONS,
38};
39use crate::core::New;
40use crate::header::Header;
41use crate::request::Request;
42use crate::response::STATUS_CODE_REASON_PHRASE;
43
44/// The protocol versions served statelessly.
45pub(crate) const MODERN_VERSIONS: [&str; 1] = ["2026-07-28"];
46
47/// The `_meta` keys of a modern request.
48const META_VERSION: &str = "io.modelcontextprotocol/protocolVersion";
49const META_CAPABILITIES: &str = "io.modelcontextprotocol/clientCapabilities";
50const META_LOG_LEVEL: &str = "io.modelcontextprotocol/logLevel";
51
52const HEADER_MISMATCH: i32 = -32020;
53const MISSING_CLIENT_CAPABILITY: i32 = -32021;
54const UNSUPPORTED_PROTOCOL_VERSION: i32 = -32022;
55
56/// How long a `requestState` stays valid.
57const STATE_TTL_SECS: u64 = 600;
58
59/// What a modern request carries that a handler may read (through
60/// [`McpContext`]).
61#[derive(Debug, Default)]
62pub(crate) struct ModernRequest {
63    /// `_meta.io.modelcontextprotocol/clientCapabilities`, raw JSON.
64    client_capabilities: String,
65    /// `params.inputResponses`, raw JSON.
66    input_responses: Option<String>,
67    /// The `requestState` this server issued, verified.
68    request_state: Option<String>,
69    /// `_meta.io.modelcontextprotocol/logLevel`: no log messages without it.
70    log_level: Option<LogLevel>,
71    /// The client declared the tasks extension.
72    tasks_extension: bool,
73}
74
75/// A handler's request for input from the client (MCP 2026-07-28 multi
76/// round-trip requests): the client answers each request and calls again,
77/// the answers then in [`McpContext::input_response`] under the same keys,
78/// and [`state`](Self::state) in [`McpContext::request_state`].
79///
80/// ```rust
81/// use rust_web_server::mcp::{InputRequired, McpContent, McpServer};
82///
83/// let server = McpServer::new("greeter", "1.0").tool_with_context("greet", "Greets you", "{}", |ctx, _args| {
84///     match ctx.input_response("name") {
85///         Some(answer) => Ok(McpContent::text(format!("Hello! (you said {answer})"))),
86///         None => Ok(McpContent::input_required(InputRequired::new().elicit(
87///             "name",
88///             "What is your name?",
89///             r#"{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]}"#,
90///         ))),
91///     }
92/// });
93/// ```
94#[derive(Debug, Clone, Default)]
95pub struct InputRequired {
96    /// `(key, method, params JSON)`.
97    requests: Vec<(String, &'static str, String)>,
98    state: Option<String>,
99}
100
101impl InputRequired {
102    pub fn new() -> Self {
103        Self::default()
104    }
105
106    /// Ask the user to fill in a form: `requested_schema` is a JSON Schema
107    /// object of primitive properties (`elicitation/create`, form mode).
108    /// Needs the client's `elicitation` capability.
109    pub fn elicit(mut self, key: &str, message: &str, requested_schema: &str) -> Self {
110        let params = format!(r#"{{"mode":"form","message":"{}","requestedSchema":{}}}"#, json_escape(message), requested_schema);
111        self.requests.push((key.to_string(), "elicitation/create", params));
112        self
113    }
114
115    /// Ask the client's LLM (`sampling/createMessage`). Needs the client's
116    /// `sampling` capability.
117    pub fn sample(mut self, key: &str, request: &SamplingRequest) -> Self {
118        let messages: Vec<String> = request.messages.iter().map(PromptMessage::to_json).collect();
119        let mut params = format!(r#"{{"messages":[{}],"maxTokens":{}"#, messages.join(","), request.max_tokens);
120        if let Some(system) = &request.system_prompt {
121            params.push_str(&format!(r#","systemPrompt":"{}""#, json_escape(system)));
122        }
123        params.push('}');
124        self.requests.push((key.to_string(), "sampling/createMessage", params));
125        self
126    }
127
128    /// Ask for the client's roots (`roots/list`). Needs the client's
129    /// `roots` capability.
130    pub fn list_roots(mut self, key: &str) -> Self {
131        self.requests.push((key.to_string(), "roots/list", "{}".to_string()));
132        self
133    }
134
135    /// Opaque state for the retry, echoed back by the client: signed by
136    /// the server, so the handler gets back exactly what it set.
137    pub fn state(mut self, state: impl Into<String>) -> Self {
138        self.state = Some(state.into());
139        self
140    }
141
142    /// The requests as `(key, {"method":...,"params":...})`.
143    pub(crate) fn requests_json(&self) -> Vec<(String, String)> {
144        self.requests.iter().map(|(key, method, params)| (key.clone(), format!(r#"{{"method":"{method}","params":{params}}}"#))).collect()
145    }
146
147    /// `{"inputRequests":{...},"requestState":"<unsigned>"}` — how it rides
148    /// inside an [`McpContent`](super::McpContent) to the server.
149    pub(crate) fn to_json(&self) -> String {
150        let requests: Vec<String> = self
151            .requests
152            .iter()
153            .map(|(key, method, params)| format!(r#""{}":{{"method":"{method}","params":{params}}}"#, json_escape(key)))
154            .collect();
155        let mut json = format!(r#"{{"inputRequests":{{{}}}"#, requests.join(","));
156        if let Some(state) = &self.state {
157            json.push_str(&format!(r#","requestState":"{}""#, json_escape(state)));
158        }
159        json.push('}');
160        json
161    }
162}
163
164/// What a prompt handler registered with
165/// [`McpServer::prompt_with_context`] answers.
166#[derive(Debug, Clone)]
167pub enum PromptReply {
168    Messages(Vec<PromptMessage>),
169    /// Input from the client first (MCP 2026-07-28).
170    InputRequired(InputRequired),
171}
172
173impl From<Vec<PromptMessage>> for PromptReply {
174    fn from(messages: Vec<PromptMessage>) -> Self {
175        PromptReply::Messages(messages)
176    }
177}
178
179impl McpContext {
180    /// The client's answer to the input request `key` (MCP 2026-07-28), as
181    /// raw JSON — an `ElicitResult` (`{"action":"accept","content":{...}}`),
182    /// a `CreateMessageResult` or a `ListRootsResult`. `None` on the first
183    /// call, for a key the client didn't answer, and on earlier versions.
184    pub fn input_response(&self, key: &str) -> Option<String> {
185        let responses = self.modern.as_ref()?.input_responses.as_deref()?;
186        member(&members(responses), key).map(str::to_string)
187    }
188
189    /// What the user submitted for the elicitation `key`, as a raw JSON
190    /// object — when they accepted it; `None` when they declined,
191    /// cancelled, or haven't answered.
192    pub fn elicitation_content(&self, key: &str) -> Option<String> {
193        let answer = members(&self.input_response(key)?);
194        (member_str(&answer, "action").as_deref() == Some("accept")).then(|| member(&answer, "content").unwrap_or("{}").to_string())
195    }
196
197    /// The state the handler set on its [`InputRequired`], when the client
198    /// retried with it — verified, so exactly what was set.
199    pub fn request_state(&self) -> Option<&str> {
200        self.modern.as_ref()?.request_state.as_deref()
201    }
202
203    /// Whether the client declared `capability` (`"elicitation"`,
204    /// `"sampling"`, `"roots"`, ...): from this request's `_meta` (MCP
205    /// 2026-07-28), else from the session's `initialize` (`sampling` and
206    /// `roots` only).
207    pub fn client_supports(&self, capability: &str) -> bool {
208        match &self.modern {
209            Some(modern) => member(&members(&modern.client_capabilities), capability).is_some(),
210            None => match capability {
211                "sampling" => self.sampling_supported,
212                "roots" => self.roots_supported,
213                _ => false,
214            },
215        }
216    }
217
218    /// Whether this request uses MCP 2026-07-28 (stateless; input through
219    /// [`InputRequired`]).
220    pub fn is_modern(&self) -> bool {
221        self.modern.is_some()
222    }
223
224    /// Send a `notifications/message` about this request on its response
225    /// stream (MCP 2026-07-28): only when the request's `_meta` set a
226    /// `logLevel` at or below `level` and the client accepts a stream.
227    /// `data_json` is any JSON value. On earlier versions use
228    /// [`McpServer::log`].
229    pub fn log(&self, level: LogLevel, data_json: &str) {
230        let (Some(modern), Some(stream)) = (&self.modern, &self.request_stream) else { return };
231        if modern.log_level.is_some_and(|min| level >= min) {
232            let params = format!(r#"{{"level":"{}","data":{data_json}}}"#, level.as_str());
233            let _ = stream.try_send(format!("data: {}\n\n", super::render_notification("notifications/message", Some(&params))).into_bytes());
234        }
235    }
236}
237
238// ── request parsing ───────────────────────────────────────────────────────────
239
240fn members(json: &str) -> Vec<(String, String)> {
241    json_rpc::object_members(json).unwrap_or_default()
242}
243
244fn member<'a>(members: &'a [(String, String)], key: &str) -> Option<&'a str> {
245    members.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str())
246}
247
248/// A JSON string member's value, unescaped.
249fn member_str(members: &[(String, String)], key: &str) -> Option<String> {
250    member(members, key).filter(|v| v.starts_with('"')).and_then(|v| json_rpc::extract_str(&format!(r#"{{"v":{v}}}"#), "v"))
251}
252
253fn header(request: &Request, name: &str) -> Option<String> {
254    request.headers.iter().find(|h| h.name.eq_ignore_ascii_case(name)).map(|h| h.value.to_string())
255}
256
257/// `params._meta`'s protocol version, if any.
258fn meta_version(body: &str) -> Option<String> {
259    let body = members(body);
260    let params = members(member(&body, "params")?);
261    member_str(&members(member(&params, "_meta")?), META_VERSION)
262}
263
264/// Whether `request` is an MCP 2026-07-28 (or later) request: its `_meta`
265/// names a protocol version, or its `MCP-Protocol-Version` header is not
266/// one of the session-based versions.
267pub(crate) fn is_modern(request: &Request, body: &str) -> bool {
268    let header_version = header(request, "mcp-protocol-version").map(|v| v.trim().to_string());
269    if header_version.as_deref().is_some_and(|v| !SUPPORTED_PROTOCOL_VERSIONS.contains(&v)) {
270        return true;
271    }
272    header_version.is_none() && meta_version(body).is_some()
273}
274
275/// A header value as sent, or decoded from `=?base64?...?=`; `None` when
276/// it has characters a header value can't, or isn't canonical base64
277/// (padding included).
278fn header_value(raw: &str) -> Option<String> {
279    if raw.bytes().any(|b| !(b == b'\t' || (0x20..=0x7e).contains(&b))) {
280        return None;
281    }
282    match raw.strip_prefix("=?base64?").and_then(|rest| rest.strip_suffix("?=")) {
283        Some(encoded) => {
284            let decoded = base64_decode(encoded).filter(|bytes| base64_encode(bytes) == encoded)?;
285            String::from_utf8(decoded).ok()
286        }
287        None => Some(raw.to_string()),
288    }
289}
290
291/// The parameters of a tool's input schema mirrored into headers
292/// (`x-mcp-header`): the property path and the header name's suffix. Only
293/// properties reached through `properties` keys count.
294pub(crate) fn header_params(schema: &str) -> Vec<(Vec<String>, String)> {
295    let mut found = Vec::new();
296    // (path so far, schema object), depth-bounded.
297    let mut pending: Vec<(Vec<String>, String)> = vec![(Vec::new(), schema.to_string())];
298    while let Some((path, object)) = pending.pop() {
299        if path.len() > 32 {
300            continue;
301        }
302        let Some(properties) = member(&members(&object), "properties").map(members) else { continue };
303        for (key, sub) in properties {
304            let mut at = path.clone();
305            at.push(key);
306            let sub_members = members(&sub);
307            if let Some(name) = member_str(&sub_members, "x-mcp-header") {
308                found.push((at.clone(), name));
309            }
310            pending.push((at, sub));
311        }
312    }
313    found
314}
315
316/// The raw JSON value at `path` in `arguments`.
317fn value_at(arguments: &str, path: &[String]) -> Option<String> {
318    let mut value = arguments.to_string();
319    for key in path {
320        value = member(&members(&value), key)?.to_string();
321    }
322    Some(value)
323}
324
325/// Whether the header text `header` stands for the JSON value `raw`:
326/// strings as is, booleans as `true`/`false`, numbers compared as numbers.
327fn header_matches(raw: &str, header: &str) -> bool {
328    if raw.starts_with('"') {
329        return json_rpc::extract_str(&format!(r#"{{"v":{raw}}}"#), "v").as_deref() == Some(header);
330    }
331    if raw == "true" || raw == "false" {
332        return raw == header;
333    }
334    matches!((raw.parse::<f64>(), header.parse::<f64>()), (Ok(a), Ok(b)) if a == b)
335}
336
337/// `tools/call`: every `Mcp-Param-{name}` header a tool's schema asks for
338/// must be present when its argument has a value, and match it.
339fn check_param_headers(request: &Request, schema: &str, arguments: &str) -> Result<(), ModernError> {
340    for (path, name) in header_params(schema) {
341        let header_name = format!("Mcp-Param-{name}");
342        let sent = match header(request, &header_name) {
343            Some(raw) => Some(header_value(&raw).ok_or_else(|| {
344                ModernError::bad_request(HEADER_MISMATCH, format!("Header mismatch: {header_name} isn't a valid header value (or valid =?base64?...?=)"))
345            })?),
346            None => None,
347        };
348        match (value_at(arguments, &path).filter(|v| v != "null"), sent) {
349            (Some(_), None) => {
350                return Err(ModernError::bad_request(HEADER_MISMATCH, format!("Header mismatch: {header_name} is required, the argument {} has a value", path.join("."))))
351            }
352            (Some(value), Some(sent)) if !header_matches(&value, &sent) => {
353                return Err(ModernError::bad_request(HEADER_MISMATCH, format!("Header mismatch: {header_name} {sent:?} doesn't match the argument {}", path.join("."))))
354            }
355            _ => {}
356        }
357    }
358    Ok(())
359}
360
361/// An error answered on the modern path: a JSON-RPC error with `data`, and
362/// the HTTP status the transport prescribes.
363struct ModernError {
364    code: i32,
365    message: String,
366    data: Option<String>,
367    status: u16,
368}
369
370impl ModernError {
371    fn new(code: i32, message: impl Into<String>) -> Self {
372        ModernError { code, message: message.into(), data: None, status: 200 }
373    }
374
375    fn bad_request(code: i32, message: impl Into<String>) -> Self {
376        ModernError { status: 400, ..Self::new(code, message) }
377    }
378
379    fn data(mut self, data: String) -> Self {
380        self.data = Some(data);
381        self
382    }
383}
384
385impl From<(i32, String)> for ModernError {
386    fn from((code, message): (i32, String)) -> Self {
387        ModernError::new(code, message)
388    }
389}
390
391/// A modern result: JSON to decorate, and whether it is cacheable.
392enum Answer {
393    Complete { result: String, cacheable: bool },
394    InputRequired(String),
395    /// A task was started: its id.
396    Task(String),
397}
398
399/// `-32021` naming the tasks extension.
400fn tasks_extension_required() -> ModernError {
401    ModernError::bad_request(MISSING_CLIENT_CAPABILITY, "Missing required client capability: the io.modelcontextprotocol/tasks extension")
402        .data(format!(r#"{{"requiredCapabilities":{{"extensions":{{"{}":{{}}}}}}}}"#, super::tasks::EXTENSION))
403}
404
405/// One `subscriptions/listen` stream.
406pub(crate) struct Listener {
407    /// The listen request's JSON-RPC id, raw: its notifications' tag.
408    id: String,
409    tools: bool,
410    prompts: bool,
411    resources: bool,
412    uris: Vec<String>,
413    /// Tasks whose status changes it gets (`notifications/tasks`).
414    task_ids: Vec<String>,
415    tx: SyncSender<Vec<u8>>,
416}
417
418/// Insert `fields` (already rendered, comma-separated) at the start of the
419/// JSON object `object`.
420fn with_fields(object: &str, fields: &str) -> String {
421    let inner = object.trim().strip_prefix('{').unwrap_or("}").trim_start();
422    if inner.starts_with('}') {
423        format!("{{{fields}}}")
424    } else {
425        format!("{{{fields},{inner}")
426    }
427}
428
429fn status_response(status: u16, body: String) -> Response {
430    let mut response = super::json_response(&body);
431    if status == 400 {
432        response.status_code = *STATUS_CODE_REASON_PHRASE.n400_bad_request.status_code;
433        response.reason_phrase = STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase.to_string();
434    } else if status == 404 {
435        response.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
436        response.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
437    }
438    response
439}
440
441fn error_json(id: &str, error: &ModernError) -> String {
442    let data = error.data.as_deref().map(|d| format!(r#","data":{d}"#)).unwrap_or_default();
443    format!(r#"{{"jsonrpc":"2.0","id":{id},"error":{{"code":{},"message":"{}"{data}}}}}"#, error.code, json_escape(&error.message))
444}
445
446/// 32 random bytes: `/dev/urandom`, else hashed time and counters.
447pub(crate) fn random_key() -> Vec<u8> {
448    use std::hash::{BuildHasher, Hasher};
449    use std::io::Read;
450    let mut key = vec![0u8; 32];
451    if std::fs::File::open("/dev/urandom").and_then(|mut f| f.read_exact(&mut key)).is_ok() {
452        return key;
453    }
454    let keys = std::collections::hash_map::RandomState::new();
455    let nanos = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_nanos());
456    for (i, chunk) in key.chunks_mut(8).enumerate() {
457        let mut hasher = keys.build_hasher();
458        hasher.write_usize(i);
459        hasher.write_u128(nanos);
460        chunk.copy_from_slice(&hasher.finish().to_le_bytes());
461    }
462    key
463}
464
465fn unix_now() -> u64 {
466    std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs())
467}
468
469impl McpServer {
470    /// The key that signs `requestState` (MCP 2026-07-28 multi round-trip
471    /// requests). Random per process by default; give replicas behind a
472    /// load balancer the same key, so a retry may land on any of them.
473    pub fn request_state_key(mut self, key: impl AsRef<[u8]>) -> Self {
474        self.state_key = Arc::new(key.as_ref().to_vec());
475        self
476    }
477
478    /// The caching hints on cacheable results (MCP 2026-07-28: discovery,
479    /// lists, `resources/read`): fresh for `ttl_ms`, shareable between
480    /// users (`public`) or not. Default `0` and private — nothing cached;
481    /// raise it when the lists don't vary per caller.
482    pub fn cache_hints(mut self, ttl_ms: u64, public: bool) -> Self {
483        self.cache_ttl_ms = ttl_ms;
484        self.cache_scope = if public { "public" } else { "private" };
485        self
486    }
487
488    fn server_info_json(&self) -> String {
489        format!(
490            r#"{{"name":"{}"{}"version":"{}"{}}}"#,
491            json_escape(&self.server_name),
492            self.server_title.as_ref().map(|t| format!(r#","title":"{}","#, json_escape(t))).unwrap_or_else(|| ",".to_string()),
493            json_escape(&self.server_version),
494            icons_json(&self.server_icons),
495        )
496    }
497
498    fn capabilities_json(&self) -> String {
499        let logging = if self.logging_enabled { r#","logging":{}"# } else { "" };
500        let completions = if self.completions.read().unwrap().is_empty() { "" } else { r#","completions":{}"# };
501        format!(
502            r#"{{"tools":{{"listChanged":true}},"resources":{{"subscribe":true,"listChanged":true}},"prompts":{{"listChanged":true}}{logging}{completions},"extensions":{{"{}":{{}}}}}}"#,
503            super::tasks::EXTENSION
504        )
505    }
506
507    /// Every version this endpoint serves: modern ones statelessly, the
508    /// earlier ones through `initialize`.
509    fn supported_versions_json() -> String {
510        let all: Vec<String> = MODERN_VERSIONS.iter().chain(SUPPORTED_PROTOCOL_VERSIONS.iter().rev()).map(|v| format!(r#""{v}""#)).collect();
511        format!("[{}]", all.join(","))
512    }
513
514    /// Sign `state` for the retry of `method` on `name` by `principal`.
515    fn sign_state(&self, method: &str, name: &str, principal: &str, state: &str) -> String {
516        let payload = format!("{}\n{method}\n{name}\n{principal}\n{state}", unix_now() + STATE_TTL_SECS);
517        let mac = crate::core::sha256::hmac_sha256(&self.state_key, payload.as_bytes());
518        format!("{}.{}", base64_encode(payload.as_bytes()), base64_encode(&mac))
519    }
520
521    /// The state inside `token`, if this server signed it for this method,
522    /// name and caller and it hasn't expired.
523    fn verify_state(&self, token: &str, method: &str, name: &str, principal: &str) -> Option<String> {
524        let (payload, mac) = token.split_once('.')?;
525        let payload = base64_decode(payload)?;
526        if !crate::core::sha256::verify(&self.state_key, &payload, &base64_decode(mac)?) {
527            return None;
528        }
529        let payload = String::from_utf8(payload).ok()?;
530        let mut parts = payload.splitn(5, '\n');
531        let expires: u64 = parts.next()?.parse().ok()?;
532        let fits = parts.next()? == method && parts.next()? == name && parts.next()? == principal;
533        (fits && unix_now() <= expires).then(|| parts.next().unwrap_or("").to_string())
534    }
535
536    /// Serve one MCP 2026-07-28 request.
537    pub(crate) fn handle_modern(&self, request: &Request, body: &str, mut ctx: McpContext) -> Response {
538        let body_members = match json_rpc::object_members(body) {
539            Some(members) => members,
540            None => {
541                let error = ModernError::bad_request(json_rpc::INVALID_REQUEST, "The body must be one JSON-RPC request or notification");
542                return status_response(400, error_json("null", &error));
543            }
544        };
545        let id = member(&body_members, "id").map(str::to_string);
546        let id_json = id.clone().unwrap_or_else(|| "null".to_string());
547        let method = match member_str(&body_members, "method") {
548            Some(method) => method,
549            None => return status_response(400, error_json(&id_json, &ModernError::bad_request(json_rpc::INVALID_REQUEST, "Missing method"))),
550        };
551        let params = member(&body_members, "params").unwrap_or("{}").to_string();
552        let modern = match self.validate_modern(request, &method, &params) {
553            Ok(modern) => modern,
554            Err(error) => return status_response(error.status, error_json(&id_json, &error)),
555        };
556        // A notification: nothing in this revision needs one over HTTP.
557        let Some(id) = id else {
558            let mut accepted = super::no_content();
559            accepted.status_code = 202;
560            accepted.reason_phrase = "Accepted".to_string();
561            return accepted;
562        };
563
564        if method == "subscriptions/listen" {
565            return self.listen(&id, &params, modern.tasks_extension);
566        }
567        if method == "tools/call" {
568            if let Err(error) = self.check_tool_headers(request, &params) {
569                return status_response(error.status, error_json(&id, &error));
570            }
571            let tool = member_str(&members(&params), "name").unwrap_or_default();
572            if !modern.tasks_extension && self.is_task_required(&tool) {
573                return status_response(400, error_json(&id, &tasks_extension_required()));
574            }
575        }
576
577        let principal = ctx.auth_claims.as_deref().and_then(|claims| json_rpc::extract_str(claims, "sub")).unwrap_or_default();
578        let target = {
579            let params = members(&params);
580            member_str(&params, "name").or_else(|| member_str(&params, "uri")).unwrap_or_default()
581        };
582        let mut modern = modern;
583        if modern.tasks_extension {
584            ctx.tasks = Some(super::tasks::TaskHandle { server: self.clone(), principal: principal.clone() });
585        }
586        if let Some(token) = member_str(&members(&params), "requestState") {
587            match self.verify_state(&token, &method, &target, &principal) {
588                Some(state) => modern.request_state = Some(state),
589                None => {
590                    let error = ModernError::new(json_rpc::INVALID_PARAMS, "requestState failed verification (altered, expired, or for another request)");
591                    return status_response(200, error_json(&id, &error));
592                }
593            }
594        }
595        if let Some(responses) = member(&members(&params), "inputResponses") {
596            if !json_rpc::object_members(responses).is_some_and(|answers| answers.iter().all(|(_, v)| v.trim_start().starts_with('{'))) {
597                let error = ModernError::new(json_rpc::INVALID_PARAMS, "inputResponses must be an object of result objects");
598                return status_response(200, error_json(&id, &error));
599            }
600            modern.input_responses = Some(responses.to_string());
601        }
602        let wants_stream = modern.log_level.is_some() || member(&members(&params), "_meta").is_some_and(|meta| member(&members(meta), "progressToken").is_some());
603        let accepts_stream = header(request, "accept").is_some_and(|a| a.to_ascii_lowercase().contains("text/event-stream"));
604        ctx.modern = Some(Arc::new(modern));
605        let body = body.to_string();
606
607        if wants_stream && accepts_stream && matches!(method.as_str(), "tools/call" | "prompts/get" | "resources/read") {
608            // Progress and log messages first, then the result, on this
609            // request's own stream.
610            let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(32);
611            ctx.request_stream = Some(tx.clone());
612            let server = self.clone();
613            std::thread::spawn(move || {
614                let (_, json) = server.answer(&id, &method, &body, ctx, &target, &principal);
615                let _ = tx.send(format!("data: {json}\n\n").into_bytes());
616            });
617            let mut response = Response::new();
618            response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
619            response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
620            response.headers.push(Header { name: Header::_CONTENT_TYPE.into(), value: "text/event-stream".into() });
621            response.headers.push(Header { name: Header::_CACHE_CONTROL.into(), value: "no-cache".into() });
622            response.headers.push(Header { name: "X-Accel-Buffering".into(), value: "no".into() });
623            response.stream_pipe = Some(Box::new(crate::sse::SseReader::new(rx, super::SSE_KEEPALIVE_INTERVAL)));
624            return response;
625        }
626        let (status, json) = self.answer(&id, &method, &body, ctx, &target, &principal);
627        status_response(status, json)
628    }
629
630    /// `Mcp-Param-*` headers against the arguments of the tool called.
631    fn check_tool_headers(&self, request: &Request, params: &str) -> Result<(), ModernError> {
632        let params = members(params);
633        let Some(name) = member_str(&params, "name") else { return Ok(()) };
634        let schema = self.tools.read().unwrap().iter().find(|t| t.name == name).map(|t| t.input_schema.clone());
635        #[cfg(feature = "http2")]
636        let schema = schema.or_else(|| self.async_tools.read().unwrap().iter().find(|t| t.name == name).map(|t| t.input_schema.clone()));
637        match schema {
638            Some(schema) if schema.contains("x-mcp-header") => check_param_headers(request, &schema, member(&params, "arguments").unwrap_or("{}")),
639            _ => Ok(()),
640        }
641    }
642
643    /// The `_meta` and header checks every modern request passes first.
644    fn validate_modern(&self, request: &Request, method: &str, params: &str) -> Result<ModernRequest, ModernError> {
645        let params = members(params);
646        let meta = member(&params, "_meta").map(members).ok_or_else(|| {
647            ModernError::bad_request(json_rpc::INVALID_PARAMS, "params._meta is required (protocol version and client capabilities)")
648        })?;
649        let version = member_str(&meta, META_VERSION)
650            .ok_or_else(|| ModernError::bad_request(json_rpc::INVALID_PARAMS, format!("params._meta[\"{META_VERSION}\"] is required")))?;
651        let capabilities = member(&meta, META_CAPABILITIES)
652            .filter(|c| c.trim_start().starts_with('{'))
653            .ok_or_else(|| ModernError::bad_request(json_rpc::INVALID_PARAMS, format!("params._meta[\"{META_CAPABILITIES}\"] is required")))?;
654        match header(request, "mcp-protocol-version").map(|v| v.trim().to_string()) {
655            Some(header_version) if header_version == version => {}
656            Some(header_version) => {
657                return Err(ModernError::bad_request(
658                    HEADER_MISMATCH,
659                    format!("Header mismatch: MCP-Protocol-Version {header_version:?} doesn't match _meta's {version:?}"),
660                ))
661            }
662            None => return Err(ModernError::bad_request(HEADER_MISMATCH, "The MCP-Protocol-Version header is required")),
663        }
664        if !MODERN_VERSIONS.contains(&version.as_str()) {
665            return Err(ModernError::bad_request(UNSUPPORTED_PROTOCOL_VERSION, "Unsupported protocol version")
666                .data(format!(r#"{{"supported":{},"requested":"{}"}}"#, Self::supported_versions_json(), json_escape(&version))));
667        }
668        match header(request, "mcp-method").and_then(|h| header_value(&h)) {
669            Some(header_method) if header_method == method => {}
670            Some(header_method) => {
671                return Err(ModernError::bad_request(HEADER_MISMATCH, format!("Header mismatch: Mcp-Method {header_method:?} doesn't match the body's {method:?}")))
672            }
673            None => return Err(ModernError::bad_request(HEADER_MISMATCH, "The Mcp-Method header is required (and must be a valid header value)")),
674        }
675        let named = match method {
676            "tools/call" | "prompts/get" => member_str(&params, "name"),
677            "resources/read" => member_str(&params, "uri"),
678            "tasks/get" | "tasks/update" | "tasks/cancel" => member_str(&params, "taskId"),
679            _ => None,
680        };
681        if let Some(body_name) = named {
682            match header(request, "mcp-name").and_then(|h| header_value(&h)) {
683                Some(header_name) if header_name == body_name => {}
684                Some(header_name) => {
685                    return Err(ModernError::bad_request(HEADER_MISMATCH, format!("Header mismatch: Mcp-Name {header_name:?} doesn't match the body's {body_name:?}")))
686                }
687                None => return Err(ModernError::bad_request(HEADER_MISMATCH, "The Mcp-Name header is required for this method")),
688            }
689        }
690        let tasks_extension = member(&members(capabilities), "extensions").is_some_and(|ext| member(&members(ext), super::tasks::EXTENSION).is_some());
691        Ok(ModernRequest {
692            client_capabilities: capabilities.to_string(),
693            log_level: member_str(&meta, META_LOG_LEVEL).and_then(|level| LogLevel::parse(&level)),
694            tasks_extension,
695            ..ModernRequest::default()
696        })
697    }
698
699    /// Dispatch and render: the HTTP status and the JSON-RPC response.
700    fn answer(&self, id: &str, method: &str, body: &str, ctx: McpContext, target: &str, principal: &str) -> (u16, String) {
701        let capabilities = ctx.modern.as_ref().map(|m| m.client_capabilities.clone()).unwrap_or_default();
702        let retry = ctx.modern.as_ref().is_some_and(|m| m.input_responses.is_some() || m.request_state.is_some());
703        let server_info = format!(r#""_meta":{{"io.modelcontextprotocol/serverInfo":{}}}"#, self.server_info_json());
704        let answer = self.dispatch_modern(method, body, ctx);
705        let result = match answer {
706            Ok(Answer::Complete { result, cacheable }) => {
707                let mut fields = format!(r#""resultType":"complete",{server_info}"#);
708                // A retry's result depends on its inputs: never cached.
709                if cacheable && !retry {
710                    fields.push_str(&format!(r#","ttlMs":{},"cacheScope":"{}""#, self.cache_ttl_ms, self.cache_scope));
711                }
712                Ok(with_fields(&result, &fields))
713            }
714            Ok(Answer::InputRequired(input)) => self.input_required_result(&input, &capabilities, method, target, principal, &server_info),
715            Ok(Answer::Task(task_id)) => match self.create_task_fields(&task_id, principal) {
716                Some(fields) => Ok(format!(r#"{{"resultType":"task",{server_info},{fields}}}"#)),
717                None => Err(ModernError::new(json_rpc::INTERNAL_ERROR, "the task disappeared")),
718            },
719            Err(error) => Err(error),
720        };
721        match result {
722            Ok(result) => (200, format!(r#"{{"jsonrpc":"2.0","id":{id},"result":{result}}}"#)),
723            Err(error) => (error.status, error_json(id, &error)),
724        }
725    }
726
727    /// An `InputRequiredResult` from a handler's [`InputRequired`]: refused
728    /// with `-32021` when it asks for something the client didn't declare;
729    /// its state signed.
730    fn input_required_result(&self, input: &str, capabilities: &str, method: &str, target: &str, principal: &str, server_info: &str) -> Result<String, ModernError> {
731        let input = members(input);
732        let declared = members(capabilities);
733        let requests = member(&input, "inputRequests").unwrap_or("{}");
734        let mut missing: Vec<&str> = Vec::new();
735        for (_, request) in members(requests) {
736            let needs = match member_str(&members(&request), "method").as_deref() {
737                Some("elicitation/create") => "elicitation",
738                Some("sampling/createMessage") => "sampling",
739                Some("roots/list") => "roots",
740                _ => continue,
741            };
742            if member(&declared, needs).is_none() && !missing.contains(&needs) {
743                missing.push(needs);
744            }
745        }
746        if !missing.is_empty() {
747            let required: Vec<String> = missing.iter().map(|c| format!(r#""{c}":{{}}"#)).collect();
748            return Err(ModernError::bad_request(MISSING_CLIENT_CAPABILITY, format!("This request needs the client capabilities: {}", missing.join(", ")))
749                .data(format!(r#"{{"requiredCapabilities":{{{}}}}}"#, required.join(","))));
750        }
751        let mut fields = format!(r#""resultType":"input_required",{server_info}"#);
752        if requests.trim() != "{}" {
753            fields.push_str(&format!(r#","inputRequests":{requests}"#));
754        }
755        if let Some(state) = member_str(&input, "requestState") {
756            fields.push_str(&format!(r#","requestState":"{}""#, json_escape(&self.sign_state(method, target, principal, &state))));
757        }
758        Ok(format!("{{{fields}}}"))
759    }
760
761    fn dispatch_modern(&self, method: &str, body: &str, ctx: McpContext) -> Result<Answer, ModernError> {
762        let complete = |result: String, cacheable: bool| Answer::Complete { result, cacheable };
763        Ok(match method {
764            "server/discover" => complete(
765                format!(r#"{{"supportedVersions":{},"capabilities":{}}}"#, Self::supported_versions_json(), self.capabilities_json()),
766                true,
767            ),
768            "tools/list" => complete(self.do_tools_list(body, &ctx)?, true),
769            "tools/call" => match self.call_tool(body, ctx)? {
770                Ok(content) if content.kind == "input_required" => Answer::InputRequired(content.text),
771                Ok(content) if content.kind == "task" => Answer::Task(content.text),
772                other => complete(Self::format_tool_result(other), false),
773            },
774            "resources/list" => complete(self.do_resources_list(body, &ctx)?, true),
775            "resources/templates/list" => complete(self.do_resource_templates_list(body, &ctx)?, true),
776            "resources/read" => match self.do_resources_read(body, &ctx) {
777                Ok(result) => complete(result, true),
778                Err((code, message)) if message.starts_with("Resource not found") => {
779                    let uri = json_rpc::extract_raw(body, "params").and_then(|p| json_rpc::extract_str(&p, "uri")).unwrap_or_default();
780                    return Err(ModernError::new(code, "Resource not found").data(format!(r#"{{"uri":"{}"}}"#, json_escape(&uri))));
781                }
782                Err(error) => return Err(error.into()),
783            },
784            "prompts/list" => complete(self.do_prompts_list(body, &ctx)?, true),
785            "prompts/get" => match self.get_prompt(body, ctx)? {
786                (_, super::PromptReply::InputRequired(input)) => Answer::InputRequired(input.to_json()),
787                (description, super::PromptReply::Messages(messages)) => {
788                    let messages: Vec<String> = messages.iter().map(PromptMessage::to_json).collect();
789                    complete(format!(r#"{{"description":"{}","messages":[{}]}}"#, json_escape(&description), messages.join(",")), false)
790                }
791            },
792            "completion/complete" => complete(self.do_completion(body)?, false),
793            "tasks/get" | "tasks/update" | "tasks/cancel" => {
794                let principal = ctx.tasks.as_ref().map(|handle| handle.principal.clone()).ok_or_else(tasks_extension_required)?;
795                let params = members(&json_rpc::object_members(body).and_then(|m| m.into_iter().find(|(k, _)| k == "params").map(|(_, v)| v)).unwrap_or_default());
796                let task_id = member_str(&params, "taskId").ok_or_else(|| ModernError::new(json_rpc::INVALID_PARAMS, "taskId is required"))?;
797                match method {
798                    "tasks/get" => complete(format!("{{{}}}", self.task_get(&task_id, &principal)?), false),
799                    "tasks/update" => {
800                        self.task_update(&task_id, &principal, member(&params, "inputResponses").unwrap_or("{}"))?;
801                        complete("{}".to_string(), false)
802                    }
803                    _ => {
804                        self.task_cancel(&task_id, &principal)?;
805                        complete("{}".to_string(), false)
806                    }
807                }
808            }
809            _ => {
810                let mut error = ModernError::new(json_rpc::METHOD_NOT_FOUND, format!("Method not found: {method}"));
811                error.status = 404;
812                return Err(error);
813            }
814        })
815    }
816
817    /// `subscriptions/listen`: an SSE stream, the acknowledgment first.
818    pub(crate) fn listen(&self, id: &str, params: &str, tasks_extension: bool) -> Response {
819        let filter = member(&members(params), "notifications").map(members).unwrap_or_default();
820        let flag = |key: &str| member(&filter, key) == Some("true");
821        let uris: Vec<String> = member(&filter, "resourceSubscriptions")
822            .map(|list| json_rpc::split_array_elements(list).iter().filter_map(|u| json_rpc::extract_str(&format!(r#"{{"v":{u}}}"#), "v")).collect())
823            .unwrap_or_default();
824        let (tools, prompts, resources) = (flag("toolsListChanged"), flag("promptsListChanged"), flag("resourcesListChanged"));
825        let task_ids: Vec<String> = member(&filter, "taskIds")
826            .map(|list| json_rpc::split_array_elements(list).iter().filter_map(|t| json_rpc::extract_str(&format!(r#"{{"v":{t}}}"#), "v")).collect())
827            .unwrap_or_default();
828        if !task_ids.is_empty() && !tasks_extension {
829            return status_response(400, error_json(id, &tasks_extension_required()));
830        }
831        let mut honoured: Vec<String> = Vec::new();
832        for (on, key) in [(tools, "toolsListChanged"), (prompts, "promptsListChanged"), (resources, "resourcesListChanged")] {
833            if on {
834                honoured.push(format!(r#""{key}":true"#));
835            }
836        }
837        if !uris.is_empty() {
838            let list: Vec<String> = uris.iter().map(|u| format!(r#""{}""#, json_escape(u))).collect();
839            honoured.push(format!(r#""resourceSubscriptions":[{}]"#, list.join(",")));
840        }
841        if !task_ids.is_empty() {
842            let list: Vec<String> = task_ids.iter().map(|t| format!(r#""{}""#, json_escape(t))).collect();
843            honoured.push(format!(r#""taskIds":[{}]"#, list.join(",")));
844        }
845        let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(super::SSE_CHANNEL_CAPACITY);
846        let ack = format!(
847            r#"{{"jsonrpc":"2.0","method":"notifications/subscriptions/acknowledged","params":{{"_meta":{{"io.modelcontextprotocol/subscriptionId":{id}}},"notifications":{{{}}}}}}}"#,
848            honoured.join(",")
849        );
850        let _ = tx.try_send(format!("data: {ack}\n\n").into_bytes());
851        self.listeners.lock().unwrap().push(Listener { id: id.to_string(), tools, prompts, resources, uris, task_ids, tx });
852        let mut response = Response::new();
853        response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
854        response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
855        response.headers.push(Header { name: Header::_CONTENT_TYPE.into(), value: "text/event-stream".into() });
856        response.headers.push(Header { name: Header::_CACHE_CONTROL.into(), value: "no-cache".into() });
857        response.headers.push(Header { name: "X-Accel-Buffering".into(), value: "no".into() });
858        response.stream_pipe = Some(Box::new(crate::sse::SseReader::new(rx, super::SSE_KEEPALIVE_INTERVAL)));
859        response
860    }
861
862    /// `notifications/tasks` for the task `task_id` to the streams listening
863    /// for it.
864    pub(crate) fn deliver_task_notification(&self, task_id: &str, params_json: &str) {
865        let mut listeners = self.listeners.lock().unwrap();
866        listeners.retain(|listener| {
867            if !listener.task_ids.iter().any(|t| t == task_id) {
868                return true;
869            }
870            let tag = format!(r#""_meta":{{"io.modelcontextprotocol/subscriptionId":{}}}"#, listener.id);
871            let frame = format!("data: {}\n\n", super::render_notification("notifications/tasks", Some(&with_fields(params_json, &tag))));
872            !matches!(listener.tx.try_send(frame.into_bytes()), Err(TrySendError::Disconnected(_)))
873        });
874    }
875
876    /// Send a change notification to the `subscriptions/listen` streams that
877    /// asked for its type, tagged with each one's id; drops closed ones.
878    pub(crate) fn deliver_to_listeners(&self, method: &str, params_json: Option<&str>, uri: Option<&str>) {
879        let mut listeners = self.listeners.lock().unwrap();
880        if listeners.is_empty() {
881            return;
882        }
883        listeners.retain(|listener| {
884            let wanted = match method {
885                "notifications/tools/list_changed" => listener.tools,
886                "notifications/prompts/list_changed" => listener.prompts,
887                "notifications/resources/list_changed" => listener.resources,
888                "notifications/resources/updated" => uri.is_some_and(|uri| listener.uris.iter().any(|u| u == uri)),
889                _ => false,
890            };
891            if !wanted {
892                return true;
893            }
894            let tag = format!(r#""_meta":{{"io.modelcontextprotocol/subscriptionId":{}}}"#, listener.id);
895            let params = with_fields(params_json.unwrap_or("{}"), &tag);
896            let frame = format!("data: {}\n\n", super::render_notification(method, Some(&params)));
897            !matches!(listener.tx.try_send(frame.into_bytes()), Err(TrySendError::Disconnected(_)))
898        });
899    }
900}
901
902#[cfg(test)]
903mod tests;