Skip to main content

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