Skip to main content

rust_web_server/mcp/
mod.rs

1//! Model Context Protocol (MCP) server — HTTP Streamable HTTP transport.
2//!
3//! [`McpServer`] implements [`Application`] so it can be passed directly to
4//! [`Server::run`]. Unmatched requests fall through to the built-in [`App`]
5//! controller chain (static files, health probes, etc.).
6//!
7//! # Quick start
8//!
9//! ```rust,no_run
10//! use rust_web_server::server::Server;
11//! use rust_web_server::mcp::{McpServer, McpContent, PromptMessage};
12//! # fn main() {
13//! let mcp = McpServer::new("my-server", "1.0")
14//!     // A tool: callable by the AI, like a function
15//!     .tool(
16//!         "echo",
17//!         "Echo text back",
18//!         r#"{"type":"object","properties":{"text":{"type":"string"}},"required":["text"]}"#,
19//!         |args| {
20//!             let text = rust_web_server::mcp::extract_arg(args, "text")
21//!                 .unwrap_or_else(|| "(nothing)".to_string());
22//!             Ok(McpContent::text(text))
23//!         },
24//!     )
25//!     // A resource: data the AI can read by URI
26//!     .resource(
27//!         "docs://{topic}",
28//!         "Documentation",
29//!         "Return documentation for a topic",
30//!         |uri| Ok(McpContent::text(format!("Documentation for: {uri}"))),
31//!     )
32//!     // A prompt template: reusable message structures
33//!     .prompt(
34//!         "summarize",
35//!         "Summarize the given text",
36//!         |args| {
37//!             let text = rust_web_server::mcp::extract_arg(args, "text")
38//!                 .unwrap_or_else(|| "some text".to_string());
39//!             Ok(vec![PromptMessage::user(format!("Please summarize: {text}"))])
40//!         },
41//!     );
42//!
43//! // let (listener, pool) = Server::setup().unwrap();
44//! // Server::run(listener, pool, mcp);
45//! # }
46//! ```
47//!
48//! # MCP endpoint
49//!
50//! All JSON-RPC messages are sent as `POST /mcp` (override with [`.at()`](McpServer::at)).
51//! The server implements the [MCP specification](https://modelcontextprotocol.io/specification/2025-11-25)
52//! up to 2025-11-25, negotiating 2024-11-05, 2025-03-26, 2025-06-18 or 2025-11-25.
53//!
54//! `GET /mcp` opens a Server-Sent Events stream for server → client push —
55//! see [`McpServer::notify`] and the module docs' SSE section below.
56//!
57//! # SSE streaming transport
58//!
59//! A client that sends `GET /mcp` (instead of `POST`) gets back a
60//! `text/event-stream` response that stays open indefinitely. Call
61//! [`McpServer::notify`] from anywhere (a background thread, another request's
62//! handler, ...) to push a JSON-RPC notification to every currently-connected
63//! SSE client:
64//!
65//! ```rust,no_run
66//! use rust_web_server::mcp::McpServer;
67//!
68//! let server = McpServer::new("my-server", "1.0");
69//! // Elsewhere, e.g. after some background job finishes:
70//! server.notify("notifications/message", Some(r#"{"level":"info","data":"job done"}"#));
71//! ```
72//!
73//! Idle connections receive a `: keep-alive` SSE comment every 15 seconds so
74//! intermediate proxies don't time them out; this doubles as the mechanism
75//! that detects a client has disconnected (the next write attempt fails and
76//! the connection is dropped). A client whose event buffer fills up (32
77//! pending frames, unconsumed) is treated the same as a disconnected one and
78//! dropped from the broadcast list — [`McpServer::notify`] never blocks the
79//! calling thread waiting on a slow reader.
80//!
81//! This transport is only wired up for the plain HTTP/1.1 path
82//! (`Server::run`/`Server::process`) — same scope as `Response::stream_pipe`
83//! generally, which the HTTP/2 (`h2_server`) and HTTP/3 (`h3_handler`)
84//! handlers don't yet support for *any* response, not just this one.
85//!
86//! # Environment variables
87//!
88//! None — configure the server programmatically via the builder.
89
90pub(crate) mod json_rpc;
91pub mod gateway;
92
93#[cfg(test)]
94mod tests;
95
96use std::collections::HashMap;
97#[cfg(feature = "http2")]
98use std::future::Future;
99#[cfg(feature = "http2")]
100use std::pin::Pin;
101use std::sync::mpsc::{self, SyncSender};
102use std::sync::{Arc, Mutex, RwLock};
103use std::time::Duration;
104
105use crate::app::App;
106use crate::application::Application;
107use crate::core::New;
108use crate::header::Header;
109use crate::mime_type::MimeType;
110use crate::range::Range;
111use crate::request::Request;
112use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
113use crate::server::ConnectionInfo;
114
115/// The newest MCP protocol version this server speaks (#3).
116pub(crate) const PROTOCOL_VERSION: &str = "2025-11-25";
117
118/// Every protocol version this server speaks, oldest first.
119pub(crate) const SUPPORTED_PROTOCOL_VERSIONS: [&str; 4] = ["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"];
120
121/// The version a request without `MCP-Protocol-Version` is taken to use
122/// (Streamable HTTP transport, 2025-06-18).
123const ASSUMED_PROTOCOL_VERSION: &str = "2025-03-26";
124
125/// The version to answer an `initialize` asking for `requested` with: that
126/// version when this server speaks it, else the newest (the client then
127/// decides whether it can continue).
128pub(crate) fn negotiate_version(requested: Option<&str>) -> &'static str {
129    requested
130        .and_then(|v| SUPPORTED_PROTOCOL_VERSIONS.iter().find(|s| **s == v).copied())
131        .unwrap_or(PROTOCOL_VERSION)
132}
133
134// ── public content types ──────────────────────────────────────────────────────
135
136/// Content returned by tool and resource handlers.
137///
138/// Create with [`McpContent::text`] (plain text or JSON strings),
139/// [`McpContent::json`] (marks MIME type as `application/json`),
140/// [`McpContent::image`] (base64-encoded binary image data), or
141/// [`McpContent::embedded`] (a resource embedded inline in a tool response).
142#[derive(Clone, Debug)]
143pub struct McpContent {
144    /// `"text"`, `"image"`, `"audio"`, `"resource"`, `"blob"`, `"blocks"`
145    /// or `"structured"`.
146    pub kind: &'static str,
147    /// The content string — text for `"text"`, base64 data for `"image"`,
148    /// or the embedded resource's text for `"resource"`.
149    pub text: String,
150    /// Optional MIME type override (default `"text/plain"` for `"text"`;
151    /// required in practice for `"image"`/`"resource"`, set by their
152    /// constructors).
153    pub mime_type: Option<String>,
154    /// The resource URI — only set (and only serialized) for `"resource"`.
155    pub uri: Option<String>,
156}
157
158impl McpContent {
159    /// Plain-text content.
160    pub fn text(s: impl Into<String>) -> Self {
161        McpContent { kind: "text", text: s.into(), mime_type: None, uri: None }
162    }
163
164    /// JSON content — sets `mimeType` to `application/json`.
165    pub fn json(s: impl Into<String>) -> Self {
166        McpContent { kind: "text", text: s.into(), mime_type: Some("application/json".to_string()), uri: None }
167    }
168
169    /// Structured content (MCP 2025-06-18): `json` is a JSON value — usually an
170    /// object matching the tool's [`McpServer::tool_output_schema`] — sent as
171    /// the result's `structuredContent`, and as text for older clients.
172    pub fn structured(json: impl Into<String>) -> Self {
173        McpContent { kind: "structured", text: json.into(), mime_type: Some("application/json".to_string()), uri: None }
174    }
175
176    /// Audio content — `data` is base64-encoded audio and `mime_type` e.g.
177    /// `"audio/wav"` (MCP 2025-03-26).
178    pub fn audio(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
179        McpContent { kind: "audio", text: data.into(), mime_type: Some(mime_type.into()), uri: None }
180    }
181
182    /// A binary resource for `resources/read` — `data` is base64-encoded and
183    /// sent as the contents' `blob` (with `mime_type`) instead of `text`.
184    pub fn blob(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
185        McpContent { kind: "blob", text: data.into(), mime_type: Some(mime_type.into()), uri: None }
186    }
187
188    /// Several content items in one tool result — e.g. a text summary, an
189    /// image and an embedded resource. Each item renders as it would alone.
190    pub fn blocks(items: impl IntoIterator<Item = McpContent>) -> Self {
191        let rendered: Vec<String> = items.into_iter().map(|item| item.to_content_json()).collect();
192        McpContent { kind: "blocks", text: rendered.join(","), mime_type: None, uri: None }
193    }
194
195    /// A complete `tools/call` result (`{"content":[...],...}`), sent as is —
196    /// how the gateway passes an upstream server's result through.
197    pub(crate) fn raw_result(json: impl Into<String>) -> Self {
198        McpContent { kind: "result", text: json.into(), mime_type: Some("application/json".to_string()), uri: None }
199    }
200
201    /// Image content (screenshot, chart, generated art) — `data` is base64-encoded
202    /// binary and `mime_type` is e.g. `"image/png"`.
203    pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
204        McpContent { kind: "image", text: data.into(), mime_type: Some(mime_type.into()), uri: None }
205    }
206
207    /// A resource embedded inline in a tool response, as opposed to one a
208    /// client fetches separately via `resources/read`.
209    pub fn embedded(uri: impl Into<String>, text: impl Into<String>, mime_type: impl Into<String>) -> Self {
210        McpContent { kind: "resource", text: text.into(), mime_type: Some(mime_type.into()), uri: Some(uri.into()) }
211    }
212
213    fn to_content_json(&self) -> String {
214        match self.kind {
215            "image" | "audio" => format!(
216                r#"{{"type":"{}","data":"{}","mimeType":"{}"}}"#,
217                self.kind,
218                json_escape(&self.text),
219                json_escape(self.mime_type.as_deref().unwrap_or("application/octet-stream")),
220            ),
221            "blob" => format!(
222                r#"{{"type":"resource","resource":{{"uri":"{}","mimeType":"{}","blob":"{}"}}}}"#,
223                json_escape(self.uri.as_deref().unwrap_or("")),
224                json_escape(self.mime()),
225                json_escape(&self.text),
226            ),
227            "blocks" => self.text.clone(),
228            "resource" => format!(
229                r#"{{"type":"resource","resource":{{"uri":"{}","mimeType":"{}","text":"{}"}}}}"#,
230                json_escape(self.uri.as_deref().unwrap_or("")),
231                json_escape(self.mime_type.as_deref().unwrap_or("text/plain")),
232                json_escape(&self.text),
233            ),
234            _ => format!(r#"{{"type":"text","text":"{}"}}"#, json_escape(&self.text)),
235        }
236    }
237
238    fn mime(&self) -> &str {
239        self.mime_type.as_deref().unwrap_or("text/plain")
240    }
241}
242
243/// A single message in a prompt response.
244#[derive(Clone, Debug)]
245pub struct PromptMessage {
246    /// `"user"` or `"assistant"`.
247    pub role: &'static str,
248    /// The message content.
249    pub content: McpContent,
250}
251
252impl PromptMessage {
253    /// Build a user-role message.
254    pub fn user(text: impl Into<String>) -> Self {
255        PromptMessage { role: "user", content: McpContent::text(text) }
256    }
257
258    /// Build an assistant-role message.
259    pub fn assistant(text: impl Into<String>) -> Self {
260        PromptMessage { role: "assistant", content: McpContent::text(text) }
261    }
262
263    fn to_json(&self) -> String {
264        format!(
265            r#"{{"role":"{}","content":{}}}"#,
266            self.role,
267            self.content.to_content_json(),
268        )
269    }
270}
271
272/// Argument definition for a prompt template.
273#[derive(Clone)]
274pub struct PromptArgDef {
275    pub name: String,
276    pub description: String,
277    pub required: bool,
278}
279
280impl PromptArgDef {
281    pub fn required(name: impl Into<String>, description: impl Into<String>) -> Self {
282        PromptArgDef { name: name.into(), description: description.into(), required: true }
283    }
284
285    pub fn optional(name: impl Into<String>, description: impl Into<String>) -> Self {
286        PromptArgDef { name: name.into(), description: description.into(), required: false }
287    }
288}
289
290// ── McpContext ────────────────────────────────────────────────────────────────
291
292/// Per-request context passed to tool handlers registered via
293/// [`McpServer::tool_with_context`] — caller identity and session info that a
294/// plain `Fn(&str) -> ...` tool handler has no way to see.
295///
296/// Constructed in [`McpServer::execute`] from the current request's headers
297/// plus whatever `clientInfo` was recorded for this session at `initialize`
298/// time (see [`McpServer::handle_request_with_context`]).
299#[derive(Debug, Clone, Default)]
300pub struct McpContext {
301    /// `clientInfo.name` sent in this session's `initialize` call, if the
302    /// client sent one and this request carries a recognized `Mcp-Session-Id`.
303    pub client_name: Option<String>,
304    /// `clientInfo.version` sent in this session's `initialize` call, under
305    /// the same conditions as `client_name`.
306    pub client_version: Option<String>,
307    /// The `Mcp-Session-Id` header on this request, if present — the value
308    /// the server minted and returned in the `initialize` response header
309    /// for this session (see the module docs' Sessions section).
310    pub session_id: Option<String>,
311    /// The verified caller's claims as a JSON string: the serialized
312    /// [`crate::sso::OidcClaims`] when [`McpServer::require_oauth`] (`sso`
313    /// feature) is configured and this request's bearer token verified, or
314    /// `{"sub":"<identity>"}` for a token from
315    /// [`McpServer::require_bearer_tokens`]. `None` if OAuth isn't configured, or for a
316    /// hand-built context (e.g. `handle_request` in tests) that bypassed
317    /// `McpServer::execute`'s auth check entirely.
318    pub auth_claims: Option<String>,
319    /// The raw JSON value of `params._meta.progressToken` from the
320    /// triggering `tools/call` request, if the client sent one — a spec
321    /// `string | number`, so this is stored pre-rendered (already correctly
322    /// quoted if it's a string) rather than decoded, and spliced back
323    /// verbatim by [`Self::report_progress`]. `None` for anything other than
324    /// a `tools/call` whose caller asked for progress updates.
325    pub progress_token: Option<String>,
326    /// Shared handle back to the owning [`McpServer`]'s SSE broadcast list,
327    /// used by [`Self::report_progress`]. Not `pub` — this is plumbing, not
328    /// part of the context data a handler reads. `None` for a context built
329    /// by hand (e.g. via [`McpServer::handle_request_with_context`] in a
330    /// test) rather than through [`McpServer::execute`], in which case
331    /// `report_progress` silently no-ops — there's no live server to
332    /// broadcast through.
333    sse_clients: Option<Arc<Mutex<Vec<SseClient>>>>,
334    /// Shared flag flipped by a `notifications/cancelled` referencing this
335    /// `tools/call`'s request id, checked by [`Self::is_cancelled`]. Not
336    /// `pub` — see that method. `None` for anything other than a live
337    /// `tools/call` dispatched through [`McpServer::execute`]/`handle_request_with_context`.
338    cancellation: Option<Arc<std::sync::atomic::AtomicBool>>,
339    /// Whether this session's `initialize` call declared
340    /// `params.capabilities.sampling`, checked by [`Self::sample`] before
341    /// sending a request the client never said it could answer.
342    sampling_supported: bool,
343    /// Shared handle to the owning [`McpServer`]'s in-flight
344    /// server-initiated-request registry (`sampling/createMessage`,
345    /// `roots/list`), used by [`Self::sample`]/[`Self::list_roots`]. Not
346    /// `pub` — see `sse_clients` for the same "plumbing, not context data"
347    /// reasoning; `None` under the same conditions.
348    pending_replies: Option<Arc<Mutex<HashMap<String, mpsc::Sender<Result<String, String>>>>>>,
349    /// Whether this session's `initialize` call declared
350    /// `params.capabilities.roots`, checked by [`Self::list_roots`] before
351    /// sending a request the client never said it could answer.
352    roots_supported: bool,
353    /// Shared handle to the owning [`McpServer`]'s session registry, used
354    /// by [`Self::list_roots`] to read/write this session's cached roots
355    /// list. Not `pub` — plumbing, not context data.
356    sessions: Option<Arc<Mutex<HashMap<String, StoredClientInfo>>>>,
357    /// The SSE response of the `tools/call` POST this context serves, when
358    /// the client asked for progress and accepts `text/event-stream`:
359    /// progress notifications go there, ahead of the result.
360    request_stream: Option<SseSender>,
361}
362
363impl McpContext {
364    /// Push a `notifications/progress` event over the SSE channel for this
365    /// request's `progressToken`, if the client asked for progress updates
366    /// (`params._meta.progressToken` on the triggering `tools/call`) and
367    /// this context was built through a live [`McpServer`] (via `execute()`,
368    /// not a bare `McpContext { .. }` — see the `sse_clients` field doc).
369    ///
370    /// Silently does nothing in either case — a handler doesn't need to
371    /// branch on whether progress reporting is actually wired up before
372    /// calling this; it's always safe to call.
373    ///
374    /// `total` and `message` are both optional, matching the spec's
375    /// `notifications/progress` shape: `{"progressToken":...,"progress":...,
376    /// "total":...,"message":"..."}` (with `total`/`message` omitted when not
377    /// given here).
378    ///
379    /// ```rust,no_run
380    /// use rust_web_server::mcp::{McpContent, McpServer};
381    ///
382    /// let server = McpServer::new("my-server", "1.0")
383    ///     .tool_with_context("long_job", "Do something slow", "{}", |ctx, _args| {
384    ///         ctx.report_progress(0.0, Some(100.0), Some("starting"));
385    ///         // ... do work ...
386    ///         ctx.report_progress(100.0, Some(100.0), Some("done"));
387    ///         Ok(McpContent::text("done"))
388    ///     });
389    /// ```
390    pub fn report_progress(&self, progress: f64, total: Option<f64>, message: Option<&str>) {
391        let (Some(token), Some(sse_clients)) = (&self.progress_token, &self.sse_clients) else {
392            return;
393        };
394
395        let total_field = match total {
396            Some(t) => format!(r#","total":{t}"#),
397            None => String::new(),
398        };
399        let message_field = match message {
400            Some(m) => format!(r#","message":"{}""#, json_escape(m)),
401            None => String::new(),
402        };
403        let params = format!(
404            r#"{{"progressToken":{token},"progress":{progress}{total_field}{message_field}}}"#
405        );
406        let json = render_notification("notifications/progress", Some(&params));
407        if let Some(stream) = &self.request_stream {
408            let _ = stream.send(format!("data: {json}\n\n").into_bytes());
409        } else if let Some(session) = &self.session_id {
410            send_sse_to_sessions(sse_clients, std::slice::from_ref(session), &json);
411        } else {
412            broadcast_sse_to(sse_clients, &json);
413        }
414    }
415
416    /// `true` if the client sent `notifications/cancelled` referencing this
417    /// `tools/call`'s request id.
418    ///
419    /// Rust cannot forcibly interrupt a running synchronous closure — same
420    /// limitation [`crate::timeout::with_timeout`] documents — so this is
421    /// *cooperative* cancellation: nothing happens automatically. A handler
422    /// that does its own iterative work (processing a batch of items, say)
423    /// can check this between steps and return early; a handler that never
424    /// checks it runs to completion regardless, exactly as before this
425    /// existed.
426    ///
427    /// Always `false` if the client never sent a cancellation, if this
428    /// wasn't dispatched as a `tools/call` (the only method cancellation
429    /// applies to), or if `ctx` was built without a live server behind it.
430    ///
431    /// ```rust,no_run
432    /// use rust_web_server::mcp::{McpContent, McpServer};
433    ///
434    /// let server = McpServer::new("my-server", "1.0")
435    ///     .tool_with_context("process_batch", "Process many items", "{}", |ctx, _args| {
436    ///         for i in 0..1_000_000 {
437    ///             if ctx.is_cancelled() {
438    ///                 return Err("cancelled by client".to_string());
439    ///             }
440    ///             // ... process item i ...
441    ///         }
442    ///         Ok(McpContent::text("done"))
443    ///     });
444    /// ```
445    pub fn is_cancelled(&self) -> bool {
446        self.cancellation.as_ref().is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
447    }
448
449    /// **Deprecated by MCP 2026-07-28** (SEP-2577): Sampling keeps working with
450    /// clients that use it, for at least twelve months, but new servers shouldn't
451    /// rely on it.
452    ///
453    /// Ask the connected client to run LLM inference ("sampling") and block
454    /// until it replies or `timeout` elapses — the server-initiated half of
455    /// `sampling/createMessage`. This reverses the usual request direction:
456    /// the server sends a JSON-RPC *request* to the client over the `GET
457    /// /mcp` SSE stream, and the client answers with its own `POST /mcp`
458    /// carrying a JSON-RPC *response* (no `method`) that
459    /// [`McpServer::handle_request_with_context`] recognizes and routes
460    /// back here instead of treating it as an invalid request.
461    ///
462    /// Blocking (not `async fn`) is deliberate: tool handlers in this crate
463    /// are plain synchronous closures — there is no async tool handler
464    /// support yet (MCP_TODO.md's TODO-17) for this to `.await` inside of.
465    /// The calling thread parks on the response channel for up to
466    /// `timeout`; on a thread-pool server this ties up one worker for that
467    /// long, same tradeoff `crate::timeout::with_timeout` already accepts
468    /// for bounding a slow handler's *caller*.
469    ///
470    /// Fails fast, before sending anything, if: the client's `initialize`
471    /// call never declared `capabilities.sampling` (it wouldn't know what
472    /// to do with the request); this request has no session id (nothing to
473    /// address the request to); or `ctx` has no live server behind it.
474    /// Otherwise the request goes out on the session's `GET /mcp` SSE stream
475    /// — waited for, within `timeout`, if the client hasn't opened it yet
476    /// (clients often call a tool right after `initialize`, before their
477    /// stream is up) — and it fails with a timeout error if the stream never
478    /// opens or the client never responds.
479    ///
480    /// ```rust,no_run
481    /// use rust_web_server::mcp::{McpServer, PromptMessage, SamplingRequest};
482    /// use std::time::Duration;
483    ///
484    /// let server = McpServer::new("my-server", "1.0")
485    ///     .tool_with_context("ask_llm", "Ask the connected client's model a question", "{}", |ctx, _args| {
486    ///         let response = ctx.sample(
487    ///             SamplingRequest {
488    ///                 messages: vec![PromptMessage::user("What is 2+2?")],
489    ///                 max_tokens: 100,
490    ///                 system_prompt: None,
491    ///             },
492    ///             Duration::from_secs(30),
493    ///         )?;
494    ///         Ok(response.content)
495    ///     });
496    /// ```
497    pub fn sample(&self, request: SamplingRequest, timeout: Duration) -> Result<SamplingResponse, String> {
498        if !self.sampling_supported {
499            return Err("the connected client did not declare sampling support in its initialize capabilities".to_string());
500        }
501
502        let messages_json: Vec<String> = request.messages.iter().map(|m| m.to_json()).collect();
503        let system_prompt_field = match &request.system_prompt {
504            Some(s) => format!(r#","systemPrompt":"{}""#, json_escape(s)),
505            None => String::new(),
506        };
507        let params = format!(
508            r#"{{"messages":[{}],"maxTokens":{}{system_prompt_field}}}"#,
509            messages_json.join(","), request.max_tokens,
510        );
511
512        let result_json = self.send_and_wait("sampling/createMessage", Some(&params), timeout)?;
513        parse_sampling_response(&result_json)
514    }
515
516    /// **Deprecated by MCP 2026-07-28** (SEP-2577): Roots keeps working with
517    /// clients that use it, for at least twelve months, but new servers shouldn't
518    /// rely on it.
519    ///
520    /// Ask the connected client which filesystem roots (workspace
521    /// directories, mounted volumes, ...) it has access to — useful for a
522    /// file-system-aware tool that should only operate within the client's
523    /// declared workspace rather than the whole filesystem.
524    ///
525    /// Like [`Self::sample`], this reverses the usual request direction (a
526    /// server-initiated `roots/list` request over SSE, answered by the
527    /// client's own `POST /mcp`) and blocks the calling thread for up to
528    /// `timeout` — see `sample`'s docs for why blocking is deliberate here.
529    ///
530    /// The result is cached per session: the first call after `initialize`
531    /// (or after the client sends `notifications/roots/list_changed`, which
532    /// invalidates the cache) does a live round trip; later calls in the
533    /// same session return the cached list without sending anything. A tool
534    /// handler can call this on every invocation without worrying about
535    /// spamming the client with redundant `roots/list` requests.
536    ///
537    /// Fails fast, before sending anything, under the same conditions as
538    /// [`Self::sample`] (translated to roots): the client never declared
539    /// `capabilities.roots`, this request has no session id, or `ctx` has
540    /// no live server behind it.
541    ///
542    /// ```rust,no_run
543    /// use rust_web_server::mcp::McpServer;
544    /// use std::time::Duration;
545    ///
546    /// let server = McpServer::new("my-server", "1.0")
547    ///     .tool_with_context("list_workspace_files", "List files in the client's workspace", "{}", |ctx, _args| {
548    ///         let roots = ctx.list_roots(Duration::from_secs(10))?;
549    ///         let names: Vec<String> = roots.iter().map(|r| r.uri.clone()).collect();
550    ///         Ok(rust_web_server::mcp::McpContent::text(names.join(", ")))
551    ///     });
552    /// ```
553    pub fn list_roots(&self, timeout: Duration) -> Result<Vec<McpRoot>, String> {
554        if !self.roots_supported {
555            return Err("the connected client did not declare roots support in its initialize capabilities".to_string());
556        }
557
558        if let (Some(session_id), Some(sessions)) = (&self.session_id, &self.sessions) {
559            if let Some(cached) = sessions.lock().unwrap().get(session_id).and_then(|info| info.roots.clone()) {
560                return Ok(cached);
561            }
562        }
563
564        let result_json = self.send_and_wait("roots/list", None, timeout)?;
565        let roots = parse_roots_response(&result_json)?;
566
567        if let (Some(session_id), Some(sessions)) = (&self.session_id, &self.sessions) {
568            if let Some(info) = sessions.lock().unwrap().get_mut(session_id) {
569                info.roots = Some(roots.clone());
570            }
571        }
572
573        Ok(roots)
574    }
575
576    /// Send a JSON-RPC request to the client for this context's session and
577    /// block for the reply — the shared send-and-correlate mechanic behind
578    /// [`Self::sample`] and [`Self::list_roots`]. Returns the raw `result`
579    /// JSON string (not yet parsed into either method's own response type)
580    /// on success.
581    fn send_and_wait(&self, method: &str, params_json: Option<&str>, timeout: Duration) -> Result<String, String> {
582        let session_id = self.session_id.clone()
583            .ok_or_else(|| format!("{method} requires a session (Mcp-Session-Id)"))?;
584        let sse_clients = self.sse_clients.as_ref()
585            .ok_or_else(|| format!("{method} requires a live server (this context has none)"))?;
586        let pending = self.pending_replies.as_ref()
587            .ok_or_else(|| format!("{method} requires a live server (this context has none)"))?;
588
589        let request_id = format!("\"{}\"", crate::request_id::generate_request_id());
590        let (tx, rx) = mpsc::channel::<Result<String, String>>();
591        pending.lock().unwrap().insert(request_id.clone(), tx);
592
593        let params_field = match params_json {
594            Some(p) => format!(r#","params":{p}"#),
595            None => String::new(),
596        };
597        let rpc = format!(r#"{{"jsonrpc":"2.0","id":{request_id},"method":"{method}"{params_field}}}"#);
598
599        // A client may call a tool before its `GET /mcp` stream is open (the
600        // SDK clients open it in the background after `initialize`): wait for
601        // the stream, within the same timeout, instead of sending into nothing.
602        let deadline = std::time::Instant::now() + timeout;
603        let outcome = loop {
604            if send_sse_to_sessions(sse_clients, std::slice::from_ref(&session_id), &rpc) > 0 {
605                break rx.recv_timeout(deadline.saturating_duration_since(std::time::Instant::now()))
606                    .map_err(|_| format!("{method} timed out waiting for the client's response"));
607            }
608            let left = deadline.saturating_duration_since(std::time::Instant::now());
609            if left.is_zero() {
610                break Err(format!("{method} timed out: the client has no GET /mcp stream open for this session"));
611            }
612            std::thread::sleep(left.min(SSE_STREAM_WAIT_POLL));
613        };
614        pending.lock().unwrap().remove(&request_id);
615
616        outcome.and_then(|reply| reply)
617    }
618}
619
620/// A request to ask the connected MCP client to run LLM inference
621/// ("sampling") — build one and pass it to [`McpContext::sample`].
622///
623/// `messages` reuses [`PromptMessage`] rather than introducing a near-identical
624/// type — the spec's sampling message shape (`{"role":...,"content":{"type":"text",...}}`)
625/// is exactly what `PromptMessage` already models.
626pub struct SamplingRequest {
627    pub messages: Vec<PromptMessage>,
628    pub max_tokens: u32,
629    /// An optional system prompt to steer the client's model, per the
630    /// spec's `params.systemPrompt`.
631    pub system_prompt: Option<String>,
632}
633
634/// The client's answer to a [`SamplingRequest`], returned by
635/// [`McpContext::sample`].
636pub struct SamplingResponse {
637    /// Almost always `"assistant"`, per spec.
638    pub role: String,
639    pub content: McpContent,
640    /// The model the client actually used to generate this response, e.g.
641    /// `"claude-3-sonnet-20240307"`.
642    pub model: String,
643    /// Why the model stopped, e.g. `"endTurn"`, `"maxTokens"`, `"stopSequence"`.
644    /// `None` if the client didn't report one.
645    pub stop_reason: Option<String>,
646}
647
648/// Parse a `sampling/createMessage` response's `result` object (already
649/// extracted from the enclosing JSON-RPC envelope) into a [`SamplingResponse`].
650fn parse_sampling_response(result_json: &str) -> Result<SamplingResponse, String> {
651    let role = json_rpc::extract_str(result_json, "role").unwrap_or_else(|| "assistant".to_string());
652    let content_json = json_rpc::extract_raw(result_json, "content")
653        .ok_or_else(|| "sampling response missing content".to_string())?;
654    let kind = json_rpc::extract_str(&content_json, "type").unwrap_or_else(|| "text".to_string());
655    let content = match kind.as_str() {
656        "image" => McpContent::image(
657            json_rpc::extract_str(&content_json, "data").unwrap_or_default(),
658            json_rpc::extract_str(&content_json, "mimeType").unwrap_or_default(),
659        ),
660        _ => McpContent::text(json_rpc::extract_str(&content_json, "text").unwrap_or_default()),
661    };
662    let model = json_rpc::extract_str(result_json, "model").unwrap_or_default();
663    let stop_reason = json_rpc::extract_str(result_json, "stopReason");
664    Ok(SamplingResponse { role, content, model, stop_reason })
665}
666
667/// One filesystem root a client has access to, returned by
668/// [`McpContext::list_roots`] — a workspace directory, a mounted volume,
669/// or similar. `uri` is typically a `file://` URI per spec.
670#[derive(Debug, Clone)]
671pub struct McpRoot {
672    pub uri: String,
673    /// A human-readable label for the root, if the client provided one.
674    pub name: Option<String>,
675}
676
677/// Parse a `roots/list` response's `result` object (already extracted from
678/// the enclosing JSON-RPC envelope) into a `Vec<McpRoot>`.
679fn parse_roots_response(result_json: &str) -> Result<Vec<McpRoot>, String> {
680    let roots_array = json_rpc::extract_raw(result_json, "roots")
681        .ok_or_else(|| "roots/list response missing roots".to_string())?;
682    let roots = json_rpc::split_array_elements(&roots_array).iter().map(|elem| {
683        McpRoot {
684            uri: json_rpc::extract_str(elem, "uri").unwrap_or_default(),
685            name: json_rpc::extract_str(elem, "name"),
686        }
687    }).collect();
688    Ok(roots)
689}
690
691/// Configuration installed by [`McpServer::require_oauth`] (`sso` feature).
692/// Reuses [`crate::sso::jwks::JwksCache`] and [`crate::sso::OidcProvider`]
693/// directly rather than re-implementing JWKS fetch/verify — see
694/// `require_oauth`'s doc comment for the full rationale.
695#[cfg(feature = "sso")]
696struct OAuthConfig {
697    jwks: crate::sso::jwks::JwksCache,
698    provider: crate::sso::OidcProvider,
699    audience: String,
700}
701
702/// `clientInfo` recorded for one session at `initialize` time, looked up by
703/// `Mcp-Session-Id` for later requests in the same session. See
704/// `McpServer`'s `sessions` field doc comment for the unbounded-growth caveat.
705#[derive(Debug, Clone)]
706struct StoredClientInfo {
707    /// Last request in this session; idle past the server's session TTL, the
708    /// session is evicted (#4).
709    last_seen: std::time::Instant,
710    /// The protocol version `initialize` settled on.
711    protocol_version: &'static str,
712    name: Option<String>,
713    version: Option<String>,
714    /// Whether this session's `initialize` call declared `params.capabilities.sampling`
715    /// (spec: the client, not the server, declares sampling support — the
716    /// presence of the key is what matters, not its contents, which are
717    /// normally an empty object). Checked by [`McpContext::sample`] before
718    /// sending a `sampling/createMessage` request that the client would
719    /// have no idea how to answer.
720    supports_sampling: bool,
721    /// Whether this session's `initialize` call declared `params.capabilities.roots`,
722    /// checked by [`McpContext::list_roots`] the same way `supports_sampling` gates `sample`.
723    supports_roots: bool,
724    /// Cached result of this session's last `roots/list` round trip, read
725    /// and written by [`McpContext::list_roots`]. `None` means "never
726    /// fetched, or invalidated" — set back to `None` when this session
727    /// sends `notifications/roots/list_changed`, so the next `list_roots`
728    /// call does a fresh round trip instead of serving stale data.
729    roots: Option<Vec<McpRoot>>,
730}
731
732// ── ToolAnnotations ───────────────────────────────────────────────────────────
733
734/// Behavioral hints for a tool, per the MCP 2025-03-26 spec's tool
735/// annotations. Clients (Claude Desktop and others) use these to decide
736/// whether to warn or ask for confirmation before calling a tool — e.g. skip
737/// confirmation for a read-only tool, or warn before a destructive one.
738///
739/// Every field is a *hint*, not something this server enforces or verifies —
740/// nothing stops a handler registered with `read_only_hint: Some(true)` from
741/// actually writing to disk. A well-behaved server sets these accurately;
742/// a client is free to ignore them or ask for confirmation anyway.
743///
744/// Register with [`McpServer::tool_annotated`]. Build one with plain struct
745/// syntax — every field defaults to `None` (no hint given, the client's own
746/// default applies):
747///
748/// ```rust
749/// use rust_web_server::mcp::ToolAnnotations;
750///
751/// let destructive = ToolAnnotations {
752///     destructive_hint: Some(true),
753///     read_only_hint: Some(false),
754///     ..Default::default()
755/// };
756/// ```
757#[derive(Debug, Clone, Copy, Default)]
758pub struct ToolAnnotations {
759    /// The tool does not modify its environment.
760    pub read_only_hint: Option<bool>,
761    /// The tool may perform destructive updates (only meaningful when
762    /// `read_only_hint` is not `Some(true)`).
763    pub destructive_hint: Option<bool>,
764    /// Calling the tool repeatedly with the same arguments has no additional
765    /// effect beyond the first call.
766    pub idempotent_hint: Option<bool>,
767    /// The tool may interact with an open-ended set of external entities
768    /// (e.g. web search), as opposed to a fixed, closed set.
769    pub open_world_hint: Option<bool>,
770}
771
772impl ToolAnnotations {
773    /// Render as a JSON object containing only the hints that are `Some`,
774    /// using the spec's camelCase key names. Returns `"{}"` if every field
775    /// is `None`.
776    fn to_json(self) -> String {
777        let mut fields = Vec::with_capacity(4);
778        if let Some(v) = self.read_only_hint {
779            fields.push(format!(r#""readOnlyHint":{v}"#));
780        }
781        if let Some(v) = self.destructive_hint {
782            fields.push(format!(r#""destructiveHint":{v}"#));
783        }
784        if let Some(v) = self.idempotent_hint {
785            fields.push(format!(r#""idempotentHint":{v}"#));
786        }
787        if let Some(v) = self.open_world_hint {
788            fields.push(format!(r#""openWorldHint":{v}"#));
789        }
790        format!("{{{}}}", fields.join(","))
791    }
792}
793
794// ── LogLevel ──────────────────────────────────────────────────────────────────
795
796/// RFC 5424 syslog severity levels, as used by the MCP `logging/setLevel`
797/// request and `notifications/message` log entries — ordered from most to
798/// least verbose so `level < min_level` comparisons work directly (this
799/// relies on declaration order matching severity order; don't reorder the
800/// variants).
801///
802/// ```rust
803/// use rust_web_server::mcp::LogLevel;
804///
805/// assert!(LogLevel::Debug < LogLevel::Warning);
806/// assert!(LogLevel::Emergency > LogLevel::Error);
807/// assert_eq!(LogLevel::parse("warning"), Some(LogLevel::Warning));
808/// assert_eq!(LogLevel::Warning.as_str(), "warning");
809/// ```
810#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
811pub enum LogLevel {
812    Debug,
813    Info,
814    Notice,
815    Warning,
816    Error,
817    Critical,
818    Alert,
819    Emergency,
820}
821
822impl LogLevel {
823    /// Parse the MCP spec's lowercase level name. Returns `None` for
824    /// anything that isn't one of the eight recognized levels.
825    pub fn parse(s: &str) -> Option<Self> {
826        match s {
827            "debug"     => Some(LogLevel::Debug),
828            "info"      => Some(LogLevel::Info),
829            "notice"    => Some(LogLevel::Notice),
830            "warning"   => Some(LogLevel::Warning),
831            "error"     => Some(LogLevel::Error),
832            "critical"  => Some(LogLevel::Critical),
833            "alert"     => Some(LogLevel::Alert),
834            "emergency" => Some(LogLevel::Emergency),
835            _           => None,
836        }
837    }
838
839    /// The MCP spec's lowercase level name, e.g. `"warning"`.
840    pub fn as_str(self) -> &'static str {
841        match self {
842            LogLevel::Debug     => "debug",
843            LogLevel::Info      => "info",
844            LogLevel::Notice    => "notice",
845            LogLevel::Warning   => "warning",
846            LogLevel::Error     => "error",
847            LogLevel::Critical  => "critical",
848            LogLevel::Alert     => "alert",
849            LogLevel::Emergency => "emergency",
850        }
851    }
852}
853
854// ── internal handler registrations ───────────────────────────────────────────
855
856type ToolFn     = Arc<dyn Fn(McpContext, &str) -> Result<McpContent, String>    + Send + Sync>;
857type ResourceFn = Arc<dyn Fn(&str) -> Result<McpContent, String>    + Send + Sync>;
858type PromptFn   = Arc<dyn Fn(&str) -> Result<Vec<PromptMessage>, String> + Send + Sync>;
859/// `Fn(argument_name, partial_value) -> candidate completion strings`.
860type CompletionFn = Arc<dyn Fn(&str, &str) -> Result<Vec<String>, String> + Send + Sync>;
861type ToolFilter = Arc<dyn Fn(&McpContext, &str) -> bool + Send + Sync>;
862/// An async tool handler, boxed so `AsyncToolDef` can store handlers with
863/// different concrete `Future` types uniformly. The `Future` itself doesn't
864/// need `Send` for [`crate::async_bridge::block_on_isolated`] (which drives
865/// it to completion without ever moving it across a thread boundary), but
866/// it's required here anyway for consistency with [`crate::async_state`]'s
867/// equivalent handler type — the common case in practice, and one less
868/// surprise for anyone switching between the two async handler styles.
869#[cfg(feature = "http2")]
870type AsyncToolFn = Arc<dyn Fn(String) -> Pin<Box<dyn Future<Output = Result<McpContent, String>> + Send>> + Send + Sync>;
871
872#[derive(Clone)]
873struct ToolDef {
874    name: String,
875    description: String,
876    input_schema: String,
877    annotations: Option<ToolAnnotations>,
878    meta: ToolMeta,
879    handler: ToolFn,
880}
881
882/// Optional `tools/list` fields set after registration, by tool name:
883/// [`McpServer::tool_title`], [`McpServer::tool_icon`],
884/// [`McpServer::tool_output_schema`].
885#[derive(Clone, Default)]
886struct ToolMeta {
887    title: Option<String>,
888    icons: Vec<McpIcon>,
889    output_schema: Option<String>,
890}
891
892/// An icon for the server, a tool, a resource or a prompt (MCP 2025-11-25):
893/// `src` is an `https:` or `data:` URL.
894#[derive(Clone, Debug, PartialEq, Eq)]
895pub struct McpIcon {
896    pub src: String,
897    pub mime_type: Option<String>,
898    /// e.g. `["48x48"]`, `["any"]` for SVG.
899    pub sizes: Vec<String>,
900}
901
902impl McpIcon {
903    /// An icon at `src`, with no MIME type or sizes.
904    pub fn new(src: impl Into<String>) -> Self {
905        McpIcon { src: src.into(), mime_type: None, sizes: Vec::new() }
906    }
907
908    /// Set the MIME type (`image/png`, `image/svg+xml`, …).
909    pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
910        self.mime_type = Some(mime_type.into());
911        self
912    }
913
914    /// Add a size (`48x48`, or `any` for scalable formats).
915    pub fn size(mut self, size: impl Into<String>) -> Self {
916        self.sizes.push(size.into());
917        self
918    }
919
920    fn to_json(&self) -> String {
921        let mut out = format!(r#"{{"src":"{}""#, json_escape(&self.src));
922        if let Some(mime) = &self.mime_type {
923            out.push_str(&format!(r#","mimeType":"{}""#, json_escape(mime)));
924        }
925        if !self.sizes.is_empty() {
926            let sizes: Vec<String> = self.sizes.iter().map(|s| format!(r#""{}""#, json_escape(s))).collect();
927            out.push_str(&format!(r#","sizes":[{}]"#, sizes.join(",")));
928        }
929        out.push('}');
930        out
931    }
932}
933
934fn icons_json(icons: &[McpIcon]) -> String {
935    if icons.is_empty() {
936        return String::new();
937    }
938    let icons: Vec<String> = icons.iter().map(McpIcon::to_json).collect();
939    format!(r#","icons":[{}]"#, icons.join(","))
940}
941
942/// A tool registered via [`McpServer::async_tool`]/[`McpServer::register_async_tool`]
943/// — stored separately from [`ToolDef`] rather than unifying the two behind
944/// a handler enum, since sync and async tools are otherwise identical and
945/// keeping them apart touches far less of the existing (already
946/// substantial) tool-handling code. `tools/list` and `tools/call` merge
947/// both collections transparently; from the client's perspective there is
948/// no distinction.
949#[cfg(feature = "http2")]
950#[derive(Clone)]
951struct AsyncToolDef {
952    name: String,
953    description: String,
954    input_schema: String,
955    annotations: Option<ToolAnnotations>,
956    meta: ToolMeta,
957    handler: AsyncToolFn,
958}
959
960#[derive(Clone)]
961struct ResourceDef {
962    uri_template: String,
963    name: String,
964    description: String,
965    handler: ResourceFn,
966}
967
968#[derive(Clone)]
969struct PromptDef {
970    name: String,
971    description: String,
972    arguments: Vec<PromptArgDef>,
973    handler: PromptFn,
974}
975
976/// One `.completion()` registration — completion candidates for a single
977/// named argument of a single tool or prompt. `ref_type` is the short form
978/// passed to `.completion()` (e.g. `"tool"`, `"prompt"`), matched against the
979/// request's `ref.type` (e.g. `"ref/tool"`) with the `"ref/"` prefix
980/// stripped, not the raw wire value.
981#[derive(Clone)]
982struct CompletionDef {
983    ref_type: String,
984    ref_name: String,
985    handler: CompletionFn,
986}
987
988// ── McpServer ─────────────────────────────────────────────────────────────────
989
990/// An HTTP server that implements the MCP protocol (2024-11-05 to 2025-11-25).
991///
992/// Register tools, resources, and prompts with the builder methods, then pass
993/// the server to [`Server::run`] (or [`Server::run_tls`]) as an [`Application`].
994/// Requests that do not match the MCP endpoint fall through to the built-in
995/// [`App`] controller chain.
996#[derive(Clone)]
997pub struct McpServer {
998    server_name: String,
999    server_version: String,
1000    path: String,
1001    /// `Arc<RwLock<_>>` (not a plain `Vec`) so a running server's tool list
1002    /// can be mutated at runtime — see [`Self::register_tool`]/[`Self::remove_tool`]
1003    /// — and every clone of this `McpServer` (each connection thread gets
1004    /// one) sees the same live list.
1005    tools: Arc<RwLock<Vec<ToolDef>>>,
1006    /// Tools registered via [`Self::async_tool`]/[`Self::register_async_tool`],
1007    /// bridged into synchronous [`Application::execute`] via
1008    /// [`crate::async_bridge::block_on_isolated`] at call time. Kept
1009    /// separate from `tools` rather than merged into one collection — see
1010    /// [`AsyncToolDef`]'s doc comment.
1011    #[cfg(feature = "http2")]
1012    async_tools: Arc<RwLock<Vec<AsyncToolDef>>>,
1013    resources: Arc<RwLock<Vec<ResourceDef>>>,
1014    prompts: Arc<RwLock<Vec<PromptDef>>>,
1015    /// Argument completion providers registered via [`Self::completion`].
1016    /// `initialize` advertises the `completions` capability iff this is
1017    /// non-empty at that moment.
1018    completions: Arc<RwLock<Vec<CompletionDef>>>,
1019    fallback: Option<Arc<dyn Application + Send + Sync>>,
1020    auth_token: Option<String>,
1021    /// `(token, identity)` pairs from [`Self::require_bearer_tokens`].
1022    bearer_tokens: Vec<(String, String)>,
1023    /// Which tools a caller may see and call ([`Self::tool_filter`]).
1024    tool_filter: Option<ToolFilter>,
1025    /// Origins a browser may call the endpoint from (see
1026    /// [`Self::allowed_origins`]); a request with any other `Origin` is
1027    /// refused.
1028    allowed_origins: Vec<String>,
1029    /// Installed by [`Self::require_oauth`] (`sso` feature) — an alternative
1030    /// to `auth_token` that verifies a client-supplied OAuth 2.0 / OIDC
1031    /// bearer JWT via JWKS instead of comparing against one static shared
1032    /// secret. If both are somehow configured, OAuth verification takes
1033    /// precedence and `auth_token` is ignored (see [`Self::require_oauth`]).
1034    #[cfg(feature = "sso")]
1035    oauth: Option<Arc<OAuthConfig>>,
1036    /// Max items per page for `tools/list`/`resources/list`/`prompts/list`,
1037    /// set via [`Self::page_size`]. `None` (the default) means no pagination
1038    /// — every item comes back in one response, same as before pagination
1039    /// existed.
1040    page_size: Option<usize>,
1041    /// `clientInfo` recorded per `Mcp-Session-Id`, minted at `initialize` time.
1042    /// `Arc<Mutex<_>>` so every clone of this `McpServer` shares one map.
1043    /// Bounded (#4): a session idle for `session_ttl` is evicted when next
1044    /// seen or when a new one is created, `max_sessions` caps the map
1045    /// (oldest evicted first), and `DELETE /mcp` ends one. An evicted
1046    /// session's subscriptions and SSE streams go with it.
1047    sessions: Arc<Mutex<HashMap<String, StoredClientInfo>>>,
1048    /// Idle time after which a session is evicted (default 1 hour).
1049    session_ttl: Duration,
1050    /// Most sessions kept (default 10 000).
1051    max_sessions: usize,
1052    /// Most open `GET /mcp` SSE streams (default 1000); past it `503`.
1053    max_sse_clients: usize,
1054    /// `serverInfo.title` and `serverInfo.icons`.
1055    server_title: Option<String>,
1056    server_icons: Vec<McpIcon>,
1057    /// Senders for every currently-connected `GET /mcp` SSE client, pushed to
1058    /// by [`Self::notify`]. `Arc<Mutex<_>>` so every clone of this `McpServer`
1059    /// broadcasts to the same set of listeners.
1060    ///
1061    /// Entries for clients that disconnected (or whose buffer filled up) are
1062    /// only pruned lazily, the next time [`Self::notify`] is called and its
1063    /// `try_send` fails — not proactively, since nothing else observes the
1064    /// underlying `Receiver` closing. A server that never calls `notify`
1065    /// after clients disconnect will accumulate dead entries here.
1066    sse_clients: Arc<Mutex<Vec<SseClient>>>,
1067    /// Whether `initialize`'s advertised `capabilities` includes `"logging":{}`.
1068    /// Set via [`Self::logging_enabled`]. This only controls what's
1069    /// advertised — [`Self::log`] works regardless, same as [`Self::notify`]
1070    /// does; a spec-honest client just wouldn't call `logging/setLevel` in
1071    /// the first place if the capability was never advertised.
1072    logging_enabled: bool,
1073    /// The minimum [`LogLevel`] that [`Self::log`] will actually push,
1074    /// settable at runtime by a client's `logging/setLevel` request. Starts
1075    /// at [`LogLevel::Debug`] (the least restrictive level, i.e. nothing is
1076    /// filtered) until a client requests otherwise.
1077    min_log_level: Arc<Mutex<LogLevel>>,
1078    /// In-flight `tools/call` cancellation flags, keyed by the request's raw
1079    /// `id` JSON token. Populated just before dispatching a `tools/call` and
1080    /// removed again once it returns (success, error, or the flag was never
1081    /// checked) — unlike `sessions`/`sse_clients`, this map's entries are
1082    /// always short-lived by construction, not merely "not evicted."
1083    /// `notifications/cancelled` looks up `params.requestId` here and flips
1084    /// the flag; see [`McpContext::is_cancelled`] for how (and whether) a
1085    /// handler ever observes it.
1086    cancellations: Arc<Mutex<HashMap<String, Arc<std::sync::atomic::AtomicBool>>>>,
1087    /// Resource URI -> session ids subscribed to it via
1088    /// `resources/subscribe`, consulted by [`Self::notify_resource_updated`].
1089    /// A session id is only meaningful if the client also opened `GET /mcp`
1090    /// with the same `Mcp-Session-Id` header (see [`SseClient`]) — a session
1091    /// that only ever calls `resources/subscribe` over POST with no matching
1092    /// SSE connection is subscribed in name only, since there's no channel
1093    /// to push the update over. Entries are removed by explicit
1094    /// `resources/unsubscribe` calls (URIs with zero subscribers are pruned
1095    /// entirely); a session that disconnects without unsubscribing leaves a
1096    /// harmless stale session id behind, same "no proactive eviction"
1097    /// tradeoff already documented for `sessions`.
1098    subscriptions: Arc<Mutex<HashMap<String, Vec<String>>>>,
1099    /// In-flight `sampling/createMessage` requests this server has sent to
1100    /// a client, keyed by the raw (already-quoted) request id token this
1101    /// server minted. A JSON-RPC *response* has no `method` field, so
1102    /// [`Self::handle_request_with_context`]/[`Self::handle_batch`]
1103    /// special-case a method-less body with a recognized `id` as a
1104    /// sampling reply (via [`Self::try_deliver_sampling_response`]) rather
1105    /// than an invalid request. See [`McpContext::sample`] for the
1106    /// send-and-block side.
1107    pending_replies: Arc<Mutex<HashMap<String, mpsc::Sender<Result<String, String>>>>>,
1108}
1109
1110/// One `GET /mcp` SSE client: its outbound channel plus the
1111/// `Mcp-Session-Id` it connected with, if any. Broadcast notifications
1112/// (`.notify()`, `.log()`, `list_changed`) go to every client regardless of
1113/// `session_id`; [`McpServer::notify_resource_updated`] targets only
1114/// clients whose `session_id` is subscribed to the changed URI.
1115#[derive(Debug)]
1116struct SseClient {
1117    session_id: Option<String>,
1118    sender: SseSender,
1119}
1120
1121/// One `GET /mcp` SSE client's outbound channel. Bounded so a slow or stuck
1122/// client can't grow memory without limit; [`McpServer::notify`] uses
1123/// `try_send` (never blocks) and drops any client whose buffer is full.
1124type SseSender = SyncSender<Vec<u8>>;
1125
1126/// Max buffered-but-unread SSE frames per client before it's treated as dead.
1127const SSE_CHANNEL_CAPACITY: usize = 32;
1128
1129/// Default idle time after which an MCP session is evicted.
1130pub const DEFAULT_SESSION_TTL: Duration = Duration::from_secs(3600);
1131/// Default cap on MCP sessions.
1132pub const DEFAULT_MAX_SESSIONS: usize = 10_000;
1133/// Default cap on open `GET /mcp` SSE streams.
1134pub const DEFAULT_MAX_SSE_CLIENTS: usize = 1000;
1135
1136/// How often an idle SSE connection gets a `: keep-alive` comment.
1137const SSE_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(15);
1138
1139/// Max `completion/complete` values returned in one response, per spec
1140/// guidance that servers SHOULD NOT return more than 100. A handler
1141/// returning more has the rest reported via `hasMore`/`total` rather than
1142/// silently included.
1143const MAX_COMPLETION_VALUES: usize = 100;
1144
1145impl McpServer {
1146    /// Create a new `McpServer`.  The default MCP endpoint is `POST /mcp`.
1147    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
1148        McpServer {
1149            server_name: name.into(),
1150            server_version: version.into(),
1151            path: "/mcp".to_string(),
1152            tools: Arc::new(RwLock::new(Vec::new())),
1153            #[cfg(feature = "http2")]
1154            async_tools: Arc::new(RwLock::new(Vec::new())),
1155            resources: Arc::new(RwLock::new(Vec::new())),
1156            prompts: Arc::new(RwLock::new(Vec::new())),
1157            completions: Arc::new(RwLock::new(Vec::new())),
1158            fallback: None,
1159            auth_token: None,
1160            bearer_tokens: Vec::new(),
1161            tool_filter: None,
1162            allowed_origins: Vec::new(),
1163            #[cfg(feature = "sso")]
1164            oauth: None,
1165            page_size: None,
1166            sessions: Arc::new(Mutex::new(HashMap::new())),
1167            session_ttl: DEFAULT_SESSION_TTL,
1168            max_sessions: DEFAULT_MAX_SESSIONS,
1169            max_sse_clients: DEFAULT_MAX_SSE_CLIENTS,
1170            server_title: None,
1171            server_icons: Vec::new(),
1172            sse_clients: Arc::new(Mutex::new(Vec::new())),
1173            logging_enabled: false,
1174            min_log_level: Arc::new(Mutex::new(LogLevel::Debug)),
1175            cancellations: Arc::new(Mutex::new(HashMap::new())),
1176            subscriptions: Arc::new(Mutex::new(HashMap::new())),
1177            pending_replies: Arc::new(Mutex::new(HashMap::new())),
1178        }
1179    }
1180
1181    /// Evict a session after it has been idle this long (default 1 hour,
1182    /// [`DEFAULT_SESSION_TTL`]). A request with an evicted session's
1183    /// `Mcp-Session-Id` gets `404`, which tells the client to initialize a
1184    /// new session (MCP Streamable HTTP transport).
1185    pub fn session_ttl(mut self, ttl: Duration) -> Self {
1186        self.session_ttl = ttl;
1187        self
1188    }
1189
1190    /// Keep at most `n` sessions (default [`DEFAULT_MAX_SESSIONS`]); a new
1191    /// one past that evicts the least recently used.
1192    pub fn max_sessions(mut self, n: usize) -> Self {
1193        self.max_sessions = n.max(1);
1194        self
1195    }
1196
1197    /// Accept at most `n` open `GET /mcp` SSE streams (default
1198    /// [`DEFAULT_MAX_SSE_CLIENTS`]); another gets `503 Service Unavailable`.
1199    pub fn max_sse_clients(mut self, n: usize) -> Self {
1200        self.max_sse_clients = n;
1201        self
1202    }
1203
1204    /// A human-readable title for tool `name` (`title` in `tools/list`,
1205    /// MCP 2025-06-18); `name` stays the identifier. No effect for an
1206    /// unknown name — register the tool first.
1207    pub fn tool_title(self, name: &str, title: impl Into<String>) -> Self {
1208        let title = title.into();
1209        self.with_tool_meta(name, |meta| meta.title = Some(title.clone()));
1210        self
1211    }
1212
1213    /// Add an icon to tool `name` (`icons` in `tools/list`, MCP 2025-11-25).
1214    pub fn tool_icon(self, name: &str, icon: McpIcon) -> Self {
1215        self.with_tool_meta(name, |meta| meta.icons.push(icon.clone()));
1216        self
1217    }
1218
1219    /// Declare tool `name`'s result shape: a JSON Schema object (an `object`
1220    /// type at the top). Clients may validate the `structuredContent` the
1221    /// handler returns with [`McpContent::structured`] against it.
1222    pub fn tool_output_schema(self, name: &str, schema: &str) -> Self {
1223        let schema = schema.to_string();
1224        self.with_tool_meta(name, |meta| meta.output_schema = Some(schema.clone()));
1225        self
1226    }
1227
1228    /// Set (or replace) the behaviour hints of tool `name` — e.g. read-only,
1229    /// or destructive — for a tool registered without them.
1230    pub fn tool_annotations(self, name: &str, annotations: ToolAnnotations) -> Self {
1231        for tool in self.tools.write().unwrap().iter_mut().filter(|t| t.name == name) {
1232            tool.annotations = Some(annotations.clone());
1233        }
1234        #[cfg(feature = "http2")]
1235        for tool in self.async_tools.write().unwrap().iter_mut().filter(|t| t.name == name) {
1236            tool.annotations = Some(annotations.clone());
1237        }
1238        self
1239    }
1240
1241    fn with_tool_meta(&self, name: &str, mut change: impl FnMut(&mut ToolMeta)) {
1242        for tool in self.tools.write().unwrap().iter_mut().filter(|t| t.name == name) {
1243            change(&mut tool.meta);
1244        }
1245        #[cfg(feature = "http2")]
1246        for tool in self.async_tools.write().unwrap().iter_mut().filter(|t| t.name == name) {
1247            change(&mut tool.meta);
1248        }
1249    }
1250
1251    /// A human-readable server title (`serverInfo.title`, MCP 2025-06-18).
1252    pub fn title(mut self, title: impl Into<String>) -> Self {
1253        self.server_title = Some(title.into());
1254        self
1255    }
1256
1257    /// Add a server icon (`serverInfo.icons`, MCP 2025-11-25).
1258    pub fn icon(mut self, icon: McpIcon) -> Self {
1259        self.server_icons.push(icon);
1260        self
1261    }
1262
1263    /// Mark a session as used now. `false` when it doesn't exist or has been
1264    /// idle past the TTL (then it's evicted).
1265    fn touch_session(&self, session_id: &str) -> bool {
1266        let expired = {
1267            let mut sessions = self.sessions.lock().unwrap();
1268            match sessions.get_mut(session_id) {
1269                None => return false,
1270                Some(info) if info.last_seen.elapsed() > self.session_ttl => {
1271                    sessions.remove(session_id);
1272                    true
1273                }
1274                Some(info) => {
1275                    info.last_seen = std::time::Instant::now();
1276                    false
1277                }
1278            }
1279        };
1280        if expired {
1281            self.forget_sessions(&[session_id.to_string()]);
1282            return false;
1283        }
1284        true
1285    }
1286
1287    /// Before a new session: drop expired ones, then the least recently used
1288    /// until there's room under `max_sessions`.
1289    fn make_room_for_a_session(&self) {
1290        let evicted: Vec<String> = {
1291            let mut sessions = self.sessions.lock().unwrap();
1292            let ttl = self.session_ttl;
1293            let mut evicted: Vec<String> = sessions.iter()
1294                .filter(|(_, info)| info.last_seen.elapsed() > ttl)
1295                .map(|(id, _)| id.clone())
1296                .collect();
1297            for id in &evicted {
1298                sessions.remove(id);
1299            }
1300            if sessions.len() >= self.max_sessions {
1301                let mut by_age: Vec<(std::time::Instant, String)> =
1302                    sessions.iter().map(|(id, info)| (info.last_seen, id.clone())).collect();
1303                by_age.sort();
1304                for (_, id) in by_age.into_iter().take(sessions.len() + 1 - self.max_sessions) {
1305                    sessions.remove(&id);
1306                    evicted.push(id);
1307                }
1308            }
1309            evicted
1310        };
1311        if !evicted.is_empty() {
1312            self.forget_sessions(&evicted);
1313        }
1314    }
1315
1316    /// The rest of an evicted or ended session: its resource subscriptions
1317    /// and its SSE streams (dropping the sender ends the stream).
1318    fn forget_sessions(&self, ids: &[String]) {
1319        let mut subscriptions = self.subscriptions.lock().unwrap();
1320        for subscribers in subscriptions.values_mut() {
1321            subscribers.retain(|sid| !ids.contains(sid));
1322        }
1323        subscriptions.retain(|_, subscribers| !subscribers.is_empty());
1324        drop(subscriptions);
1325        self.sse_clients.lock().unwrap()
1326            .retain(|client| client.session_id.as_ref().is_none_or(|sid| !ids.contains(sid)));
1327    }
1328
1329    /// Number of live sessions (for metrics and tests).
1330    pub fn session_count(&self) -> usize {
1331        self.sessions.lock().unwrap().len()
1332    }
1333
1334    /// Cap `tools/list`, `resources/list`, and `prompts/list` to at most `n`
1335    /// items per response, enabling cursor-based pagination: a response with
1336    /// more items remaining includes `"nextCursor"`, an opaque string the
1337    /// client echoes back as `params.cursor` on its next call to get the next
1338    /// page. `n` is clamped to a minimum of `1`.
1339    ///
1340    /// Without calling this, every registered tool/resource/prompt is
1341    /// returned in a single response — the default, and the only behavior
1342    /// before pagination existed.
1343    ///
1344    /// ```rust
1345    /// use rust_web_server::mcp::McpServer;
1346    ///
1347    /// let server = McpServer::new("my-server", "1.0").page_size(50);
1348    /// ```
1349    pub fn page_size(mut self, n: usize) -> Self {
1350        self.page_size = Some(n.max(1));
1351        self
1352    }
1353
1354    /// Push a JSON-RPC notification (no `id` — fire-and-forget, per the
1355    /// spec) to every client currently connected to the `GET /mcp` SSE
1356    /// stream, framed as an SSE `data:` event.
1357    ///
1358    /// `params_json`, if given, must already be a valid JSON value (usually
1359    /// an object) — it's spliced in verbatim, not escaped or re-serialized.
1360    ///
1361    /// Never blocks: a client whose event buffer is full (not reading fast
1362    /// enough) is treated the same as a disconnected one and dropped from
1363    /// the broadcast list, same as `notify` would drop it anyway.
1364    ///
1365    /// ```rust
1366    /// use rust_web_server::mcp::McpServer;
1367    ///
1368    /// let server = McpServer::new("my-server", "1.0");
1369    /// server.notify("notifications/message", Some(r#"{"level":"info","data":"hello"}"#));
1370    /// ```
1371    pub fn notify(&self, method: &str, params_json: Option<&str>) {
1372        let json = render_notification(method, params_json);
1373        broadcast_sse_to(&self.sse_clients, &json);
1374    }
1375
1376    /// Push `notifications/resources/updated` for `uri` to every session
1377    /// that called `resources/subscribe` for it — the mechanism behind
1378    /// live-updating resource panels (e.g. Claude Desktop watching a
1379    /// config file or a dashboard resource for changes). Call this from
1380    /// wherever your application actually changes the underlying data a
1381    /// resource represents (a file watcher, a webhook handler, a database
1382    /// trigger poll, ...).
1383    ///
1384    /// A no-op if nobody has subscribed to `uri` — including if every
1385    /// subscriber's `GET /mcp` SSE connection has since disconnected
1386    /// (pruned the same way [`Self::notify`] prunes dead broadcast clients,
1387    /// but the `subscriptions` bookkeeping for `uri` itself is untouched
1388    /// either way; only [`Self::do_resource_unsubscribe`] removes that).
1389    ///
1390    /// ```rust
1391    /// use rust_web_server::mcp::McpServer;
1392    ///
1393    /// let server = McpServer::new("my-server", "1.0");
1394    /// // Elsewhere, e.g. after reloading a watched config file:
1395    /// server.notify_resource_updated("config://main");
1396    /// ```
1397    pub fn notify_resource_updated(&self, uri: &str) {
1398        let session_ids = match self.subscriptions.lock().unwrap().get(uri) {
1399            Some(ids) if !ids.is_empty() => ids.clone(),
1400            _ => return,
1401        };
1402        let params = format!(r#"{{"uri":"{}"}}"#, json_escape(uri));
1403        let json = render_notification("notifications/resources/updated", Some(&params));
1404        send_sse_to_sessions(&self.sse_clients, &session_ids, &json);
1405    }
1406
1407    /// Handle `GET /mcp`: register a new SSE client and return a
1408    /// `text/event-stream` response that streams from its channel until the
1409    /// connection closes. See the module docs' SSE section for the wire
1410    /// details (keep-alive interval, backpressure behavior).
1411    ///
1412    /// Tags the client with this request's `Mcp-Session-Id` header, if
1413    /// present, so [`Self::notify_resource_updated`] can route
1414    /// `notifications/resources/updated` to just the sessions that
1415    /// subscribed via `resources/subscribe` — a client that connects
1416    /// without the header can still receive broadcasts (`.notify()`,
1417    /// `.log()`, `list_changed`) but can never be a subscription target.
1418    fn start_sse_stream(&self, request: &Request) -> Response {
1419        let session_id = request.get_header("Mcp-Session-Id".to_string()).map(|h| h.value.clone());
1420        let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(SSE_CHANNEL_CAPACITY);
1421        {
1422            let mut clients = self.sse_clients.lock().unwrap();
1423            if clients.len() >= self.max_sse_clients {
1424                // Disconnected clients are otherwise only noticed on the next
1425                // broadcast: a comment frame finds them now (#4).
1426                clients.retain(|client| !matches!(client.sender.try_send(b": ping\n\n".to_vec()), Err(mpsc::TrySendError::Disconnected(_))));
1427            }
1428            if clients.len() >= self.max_sse_clients {
1429                let mut busy = Response::new();
1430                busy.status_code = *STATUS_CODE_REASON_PHRASE.n503_service_unavailable.status_code;
1431                busy.reason_phrase = STATUS_CODE_REASON_PHRASE.n503_service_unavailable.reason_phrase.to_string();
1432                busy.headers.push(Header { name: "Retry-After".into(), value: "30".into() });
1433                return busy;
1434            }
1435            clients.push(SseClient { session_id: session_id.map(|v| v.into_owned()), sender: tx });
1436        }
1437
1438        let mut response = Response::new();
1439        response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
1440        response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
1441        response.headers.push(Header {
1442            name: Header::_CONTENT_TYPE.into(),
1443            value: "text/event-stream".into(),
1444        });
1445        response.headers.push(Header {
1446            name: Header::_CACHE_CONTROL.into(),
1447            value: "no-cache".into(),
1448        });
1449        response.headers.push(Header {
1450            name: "X-Accel-Buffering".into(),
1451            value: "no".into(),
1452        });
1453        response.stream_pipe = Some(Box::new(SseChannelReader::new(rx)));
1454        response
1455    }
1456
1457    /// **Deprecated by MCP 2026-07-28** (SEP-2577): Logging keeps working with
1458    /// clients that use it, for at least twelve months, but new servers shouldn't
1459    /// rely on it.
1460    ///
1461    /// Advertise the `logging` capability (`"logging":{}`) in `initialize`'s
1462    /// response, so clients know they can call `logging/setLevel` and expect
1463    /// `notifications/message` log entries over the `GET /mcp` SSE stream.
1464    ///
1465    /// This only changes what's *advertised* — [`Self::log`] pushes log
1466    /// entries regardless of whether this was called, same as [`Self::notify`]
1467    /// works unconditionally. A spec-honest client simply wouldn't send
1468    /// `logging/setLevel` in the first place without seeing the capability.
1469    ///
1470    /// ```rust
1471    /// use rust_web_server::mcp::McpServer;
1472    ///
1473    /// let server = McpServer::new("my-server", "1.0").logging_enabled();
1474    /// ```
1475    pub fn logging_enabled(mut self) -> Self {
1476        self.logging_enabled = true;
1477        self
1478    }
1479
1480    /// **Deprecated by MCP 2026-07-28** (SEP-2577): Logging keeps working with
1481    /// clients that use it, for at least twelve months, but new servers shouldn't
1482    /// rely on it.
1483    ///
1484    /// Push a `notifications/message` log entry to every client connected to
1485    /// the `GET /mcp` SSE stream, if `level` is at or above the level most
1486    /// recently set via a client's `logging/setLevel` request (or
1487    /// [`LogLevel::Debug`] — i.e. every level — if none has been set yet).
1488    ///
1489    /// `data_json` must already be a valid JSON value (the spec allows any
1490    /// type here, not just an object — a plain string is fine) — it's
1491    /// spliced in verbatim, not escaped or re-serialized. `logger`, if
1492    /// given, identifies the log's source (e.g. a module or subsystem name)
1493    /// and is escaped automatically.
1494    ///
1495    /// ```rust
1496    /// use rust_web_server::mcp::{LogLevel, McpServer};
1497    ///
1498    /// let server = McpServer::new("my-server", "1.0").logging_enabled();
1499    /// server.log(LogLevel::Warning, Some("database"), r#""connection pool exhausted""#);
1500    /// ```
1501    pub fn log(&self, level: LogLevel, logger: Option<&str>, data_json: &str) {
1502        if level < *self.min_log_level.lock().unwrap() {
1503            return;
1504        }
1505        let logger_field = match logger {
1506            Some(l) => format!(r#","logger":"{}""#, json_escape(l)),
1507            None => String::new(),
1508        };
1509        let params = format!(r#"{{"level":"{}"{logger_field},"data":{data_json}}}"#, level.as_str());
1510        self.notify("notifications/message", Some(&params));
1511    }
1512
1513    /// Handle `logging/setLevel`: store the requested minimum level so
1514    /// subsequent [`Self::log`] calls filter against it. Returns
1515    /// `INVALID_PARAMS` for a missing or unrecognized `params.level`.
1516    fn do_set_log_level(&self, body: &str) -> Result<String, (i32, String)> {
1517        let params = json_rpc::extract_raw(body, "params")
1518            .ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
1519        let level_str = json_rpc::extract_str(&params, "level")
1520            .ok_or((json_rpc::INVALID_PARAMS, "Missing level".to_string()))?;
1521        let level = LogLevel::parse(&level_str)
1522            .ok_or_else(|| (json_rpc::INVALID_PARAMS, format!("Unknown log level: {level_str}")))?;
1523        *self.min_log_level.lock().unwrap() = level;
1524        Ok("{}".to_string())
1525    }
1526
1527    // ── dynamic registration ──────────────────────────────────────────────────
1528    //
1529    // Unlike `.tool()`/`.resource()`/`.prompt()` (consuming builders, called
1530    // before the server starts serving requests), these take `&self` and can
1531    // be called at any time from any thread holding a clone of this
1532    // `McpServer` — e.g. after discovering a plugin, connecting to a
1533    // database, or reacting to a hot-reloaded config file. Every clone
1534    // shares the same underlying `Arc<RwLock<Vec<_>>>`, so a mutation made
1535    // through one clone is immediately visible to every other clone,
1536    // including the ones handling concurrent requests on other threads.
1537    //
1538    // Each registration/removal pushes the corresponding
1539    // `notifications/{tools,resources,prompts}/list_changed` event (no
1540    // params, per spec) to every `GET /mcp` SSE client via `.notify()`.
1541
1542    /// Register a callable tool at runtime, exactly like [`Self::tool`] but
1543    /// without needing to own the server (and usable after it's already
1544    /// serving requests). Pushes `notifications/tools/list_changed`.
1545    ///
1546    /// ```rust
1547    /// use rust_web_server::mcp::{McpContent, McpServer};
1548    ///
1549    /// let server = McpServer::new("my-server", "1.0");
1550    ///
1551    /// // Later, from any thread holding a clone of `server`:
1552    /// server.register_tool("refresh_cache", "Reload the in-memory cache", "{}", |_args| {
1553    ///     Ok(McpContent::text("cache refreshed"))
1554    /// });
1555    /// let existed = server.remove_tool("refresh_cache");
1556    /// assert!(existed);
1557    /// ```
1558    pub fn register_tool<F>(&self, name: &str, description: &str, input_schema: &str, handler: F)
1559    where
1560        F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
1561    {
1562        self.tools.write().unwrap().push(ToolDef {
1563            name: name.to_string(),
1564            description: description.to_string(),
1565            input_schema: input_schema.to_string(),
1566            annotations: None,
1567            meta: ToolMeta::default(),
1568            handler: Arc::new(move |_ctx: McpContext, args: &str| handler(args)),
1569        });
1570        self.notify("notifications/tools/list_changed", None);
1571    }
1572
1573    /// Register an `async fn` tool handler at runtime, exactly like
1574    /// [`Self::async_tool`] but usable after the server is already serving
1575    /// requests. Requires the `http2` feature — see `async_tool`'s docs for
1576    /// the async-to-sync bridging details.
1577    #[cfg(feature = "http2")]
1578    pub fn register_async_tool<F, Fut>(&self, name: &str, description: &str, input_schema: &str, handler: F)
1579    where
1580        F: Fn(&str) -> Fut + Send + Sync + 'static,
1581        Fut: Future<Output = Result<McpContent, String>> + Send + 'static,
1582    {
1583        self.async_tools.write().unwrap().push(AsyncToolDef {
1584            name: name.to_string(),
1585            description: description.to_string(),
1586            input_schema: input_schema.to_string(),
1587            annotations: None,
1588            meta: ToolMeta::default(),
1589            handler: Arc::new(move |args: String| Box::pin(handler(&args))),
1590        });
1591        self.notify("notifications/tools/list_changed", None);
1592    }
1593
1594    /// Remove a previously-registered tool by name — a sync tool registered
1595    /// via [`Self::tool`]/[`Self::register_tool`], or (with the `http2`
1596    /// feature) an async one via [`Self::async_tool`]/[`Self::register_async_tool`];
1597    /// both collections are checked. Returns `true` if a tool with that name
1598    /// existed in either and was removed. Pushes `notifications/tools/list_changed`
1599    /// only when something was actually removed.
1600    pub fn remove_tool(&self, name: &str) -> bool {
1601        #[cfg_attr(not(feature = "http2"), allow(unused_mut))]
1602        let mut removed = {
1603            let mut tools = self.tools.write().unwrap();
1604            let before = tools.len();
1605            tools.retain(|t| t.name != name);
1606            tools.len() != before
1607        };
1608
1609        #[cfg(feature = "http2")]
1610        {
1611            let mut async_tools = self.async_tools.write().unwrap();
1612            let before = async_tools.len();
1613            async_tools.retain(|t| t.name != name);
1614            removed = removed || async_tools.len() != before;
1615        }
1616
1617        if removed {
1618            self.notify("notifications/tools/list_changed", None);
1619        }
1620        removed
1621    }
1622
1623    /// Register a readable resource at runtime, exactly like [`Self::resource`].
1624    /// Pushes `notifications/resources/list_changed`.
1625    pub fn register_resource<F>(&self, uri_template: &str, name: &str, description: &str, handler: F)
1626    where
1627        F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
1628    {
1629        self.resources.write().unwrap().push(ResourceDef {
1630            uri_template: uri_template.to_string(),
1631            name: name.to_string(),
1632            description: description.to_string(),
1633            handler: Arc::new(handler),
1634        });
1635        self.notify("notifications/resources/list_changed", None);
1636    }
1637
1638    /// Remove a previously-registered resource by its exact `uri_template`
1639    /// (the same string passed to [`Self::register_resource`]/[`Self::resource`],
1640    /// not a concrete URI). Returns `true` if it existed. Pushes
1641    /// `notifications/resources/list_changed` only when something was removed.
1642    pub fn remove_resource(&self, uri_template: &str) -> bool {
1643        let removed = {
1644            let mut resources = self.resources.write().unwrap();
1645            let before = resources.len();
1646            resources.retain(|r| r.uri_template != uri_template);
1647            resources.len() != before
1648        };
1649        if removed {
1650            self.notify("notifications/resources/list_changed", None);
1651        }
1652        removed
1653    }
1654
1655    /// Register a prompt template at runtime, exactly like [`Self::prompt`]
1656    /// (no argument definitions — use [`Self::remove_prompt`] +
1657    /// [`Self::register_prompt`] if you need to change a prompt's arguments
1658    /// later; there is no dynamic equivalent of [`Self::prompt_with_args`]).
1659    /// Pushes `notifications/prompts/list_changed`.
1660    pub fn register_prompt<F>(&self, name: &str, description: &str, handler: F)
1661    where
1662        F: Fn(&str) -> Result<Vec<PromptMessage>, String> + Send + Sync + 'static,
1663    {
1664        self.prompts.write().unwrap().push(PromptDef {
1665            name: name.to_string(),
1666            description: description.to_string(),
1667            arguments: vec![],
1668            handler: Arc::new(handler),
1669        });
1670        self.notify("notifications/prompts/list_changed", None);
1671    }
1672
1673    /// Remove a previously-registered prompt by name. Returns `true` if it
1674    /// existed. Pushes `notifications/prompts/list_changed` only when
1675    /// something was removed.
1676    pub fn remove_prompt(&self, name: &str) -> bool {
1677        let removed = {
1678            let mut prompts = self.prompts.write().unwrap();
1679            let before = prompts.len();
1680            prompts.retain(|p| p.name != name);
1681            prompts.len() != before
1682        };
1683        if removed {
1684            self.notify("notifications/prompts/list_changed", None);
1685        }
1686        removed
1687    }
1688
1689    /// Require a bearer token on every request to the MCP endpoint.
1690    ///
1691    /// The client must send `Authorization: Bearer <token>`. Requests with a
1692    /// missing or wrong token receive `401 Unauthorized` before any JSON-RPC
1693    /// processing occurs.
1694    ///
1695    /// Store the token in an environment variable — never hard-code it:
1696    ///
1697    /// ```rust,no_run
1698    /// use rust_web_server::app::App;
1699    /// use rust_web_server::core::New;
1700    ///
1701    /// let app = App::new()
1702    ///     .mcp("my-server", "1.0")
1703    ///     .require_bearer(std::env::var("MCP_TOKEN").expect("MCP_TOKEN not set"));
1704    /// ```
1705    ///
1706    /// Claude Desktop config:
1707    /// ```json
1708    /// { "mcpServers": { "my-server": {
1709    ///     "url": "http://localhost:7878/mcp",
1710    ///     "headers": { "Authorization": "Bearer <token>" }
1711    /// }}}
1712    /// ```
1713    ///
1714    /// An empty token refuses every request (fails closed) rather than
1715    /// accepting an empty `Authorization: Bearer` (since 17.113.1). Tokens
1716    /// are compared in constant time.
1717    pub fn require_bearer(mut self, token: impl Into<String>) -> Self {
1718        self.auth_token = Some(token.into());
1719        self
1720    }
1721
1722    /// Accept several bearer tokens, each naming a caller: a request with
1723    /// `Authorization: Bearer <token>` is that caller, and handlers see
1724    /// `{"sub":"<identity>"}` as [`McpContext::auth_claims`] — what
1725    /// [`Self::tool_filter`] and the [gateway](gateway::McpGateway) decide
1726    /// by. Any other token gets `401`. Combines with
1727    /// [`Self::require_bearer`] (whose token carries no identity); OAuth
1728    /// ([`Self::require_oauth`]) replaces both. Empty tokens never match;
1729    /// tokens are compared in constant time.
1730    ///
1731    /// ```rust
1732    /// use rust_web_server::mcp::McpServer;
1733    ///
1734    /// let server = McpServer::new("my-server", "1.0")
1735    ///     .require_bearer_tokens([("token-for-alice", "alice"), ("token-for-ci", "ci")]);
1736    /// ```
1737    pub fn require_bearer_tokens<I, T, N>(mut self, tokens: I) -> Self
1738    where
1739        I: IntoIterator<Item = (T, N)>,
1740        T: Into<String>,
1741        N: Into<String>,
1742    {
1743        self.bearer_tokens.extend(tokens.into_iter().map(|(token, identity)| (token.into(), identity.into())));
1744        self
1745    }
1746
1747    /// Show and allow only the tools `allow(ctx, tool_name)` accepts for a
1748    /// request: `tools/list` leaves the others out, and `tools/call` answers
1749    /// them as it would an unknown tool. `ctx.auth_claims` identifies the
1750    /// caller ([`Self::require_bearer_tokens`], [`Self::require_oauth`]).
1751    ///
1752    /// ```rust
1753    /// use rust_web_server::mcp::McpServer;
1754    ///
1755    /// let server = McpServer::new("my-server", "1.0")
1756    ///     .tool_filter(|ctx, tool| !tool.starts_with("admin_") || ctx.auth_claims.as_deref() == Some(r#"{"sub":"root"}"#));
1757    /// ```
1758    pub fn tool_filter<F>(mut self, allow: F) -> Self
1759    where
1760        F: Fn(&McpContext, &str) -> bool + Send + Sync + 'static,
1761    {
1762        self.tool_filter = Some(Arc::new(allow));
1763        self
1764    }
1765
1766    fn tool_allowed(&self, ctx: &McpContext, name: &str) -> bool {
1767        self.tool_filter.as_ref().is_none_or(|allow| allow(ctx, name))
1768    }
1769
1770    /// Checks a request's bearer token against [`Self::require_bearer`] and
1771    /// [`Self::require_bearer_tokens`]: `Ok(claims)` (`Some` for a named
1772    /// token), or the `401` to send.
1773    fn check_bearer(&self, request: &Request) -> Result<Option<String>, Response> {
1774        if self.auth_token.is_none() && self.bearer_tokens.is_empty() {
1775            return Ok(None);
1776        }
1777        let provided = request.headers.iter()
1778            .find(|h| h.name.eq_ignore_ascii_case("authorization"))
1779            .map(|h| h.value.as_ref())
1780            .unwrap_or("");
1781        let bearer = provided.strip_prefix("Bearer ").unwrap_or("");
1782        // Compare against every token so the time taken doesn't tell which one matched.
1783        let mut identity = None;
1784        for (token, name) in &self.bearer_tokens {
1785            if bearer_matches(token, bearer) && identity.is_none() {
1786                identity = Some(name);
1787            }
1788        }
1789        if let Some(identity) = identity {
1790            return Ok(Some(format!(r#"{{"sub":"{}"}}"#, json_escape(identity))));
1791        }
1792        match &self.auth_token {
1793            Some(expected) if bearer_matches(expected, bearer) => Ok(None),
1794            _ => Err(unauthorized()),
1795        }
1796    }
1797
1798    /// Browser origins allowed to call the MCP endpoint, e.g.
1799    /// `["https://inspector.example.com"]`. A request carrying an `Origin`
1800    /// header that isn't listed gets `403` (the MCP transport spec requires
1801    /// validating `Origin`: without it a web page — or a DNS-rebinding
1802    /// attack — can drive a local MCP server from the visitor's browser).
1803    /// Requests without `Origin` (CLI and agent clients) are unaffected.
1804    /// Empty by default: no browser origin is allowed (since 17.113.1).
1805    pub fn allowed_origins<I, S>(mut self, origins: I) -> Self
1806    where
1807        I: IntoIterator<Item = S>,
1808        S: Into<String>,
1809    {
1810        self.allowed_origins = origins.into_iter().map(Into::into).collect();
1811        self
1812    }
1813
1814    /// Require an OAuth 2.0 / OIDC bearer JWT — verified against a live
1815    /// JWKS endpoint — on every request to the MCP endpoint (MCP
1816    /// 2025-03-26's OAuth 2.0 authorization requirement), instead of one
1817    /// static shared secret. Requires the `sso` feature.
1818    ///
1819    /// Reuses [`crate::sso::jwks::JwksCache`] directly — the "leverage
1820    /// point" this feature was scoped around — rather than reimplementing
1821    /// JWT/JWKS verification. `provider` supplies the issuer, JWKS URI, and
1822    /// (for `GET /.well-known/oauth-protected-resource`, RFC 9728) the
1823    /// authorization server's issuer; `audience` is the value tokens must
1824    /// carry in `aud` — ideally this server's canonical URL
1825    /// (`https://mcp.example.com/mcp`, RFC 8707), which is then also the
1826    /// metadata's `resource`; use a preset
1827    /// (`OidcProvider::google()`, etc.) or `OidcProvider::discover(issuer)?`.
1828    ///
1829    /// A request with a missing, malformed, or invalid-signature bearer
1830    /// token gets `401` with `WWW-Authenticate: Bearer` before any JSON-RPC
1831    /// processing occurs — matching [`Self::require_bearer`]'s behavior. On
1832    /// success, the verified claims are available to `.tool_with_context()`
1833    /// handlers as JSON via [`McpContext::auth_claims`].
1834    ///
1835    /// If both this and [`Self::require_bearer`] are configured, OAuth
1836    /// verification takes precedence and the static token is never checked
1837    /// — configuring both is not a supported combination, just an
1838    /// unambiguous tie-break for an unusual configuration.
1839    ///
1840    /// ```rust,no_run
1841    /// use rust_web_server::app::App;
1842    /// use rust_web_server::core::New;
1843    /// use rust_web_server::sso::OidcProvider;
1844    ///
1845    /// let app = App::new()
1846    ///     .mcp("my-server", "1.0")
1847    ///     .require_oauth(OidcProvider::google(), "my-mcp-client-id");
1848    /// ```
1849    #[cfg(feature = "sso")]
1850    pub fn require_oauth(mut self, provider: crate::sso::OidcProvider, audience: impl Into<String>) -> Self {
1851        self.oauth = Some(Arc::new(OAuthConfig {
1852            jwks: crate::sso::jwks::JwksCache::new(&provider.jwks_uri),
1853            provider,
1854            audience: audience.into(),
1855        }));
1856        self
1857    }
1858
1859    /// Wrap an existing [`Application`] so that non-MCP requests are forwarded
1860    /// to it instead of the built-in [`App`].
1861    ///
1862    /// Use this when your existing server has custom routes, state, or
1863    /// middleware that you want to keep alongside the MCP endpoint:
1864    ///
1865    /// ```rust,no_run
1866    /// use rust_web_server::app::App;
1867    /// use rust_web_server::mcp::{McpServer, McpContent};
1868    /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
1869    /// use rust_web_server::test_client::TestClient;
1870    ///
1871    /// let existing_app = App::with_state(42u32)
1872    ///     .get("/api/hello", |_req, _params, _conn, _state| {
1873    ///         Response::get_response(&STATUS_CODE_REASON_PHRASE.n200_ok, None, None)
1874    ///     });
1875    ///
1876    /// let server = McpServer::new("my-app", "1.0")
1877    ///     .tool("ping", "Ping", "{}", |_| Ok(McpContent::text("pong")))
1878    ///     .wrap(existing_app);
1879    ///
1880    /// // Both /mcp and /api/hello are now handled by the same server.
1881    /// let client = TestClient::new(server);
1882    /// ```
1883    pub fn wrap(mut self, app: impl Application + Send + Sync + 'static) -> Self {
1884        self.fallback = Some(Arc::new(app));
1885        self
1886    }
1887
1888    /// Override the HTTP path for the MCP endpoint (default `"/mcp"`).
1889    pub fn at(mut self, path: impl Into<String>) -> Self {
1890        self.path = path.into();
1891        self
1892    }
1893
1894    /// Register a callable tool.
1895    ///
1896    /// - `name` — tool identifier (snake_case recommended)
1897    /// - `description` — human-readable description shown to the AI
1898    /// - `input_schema` — JSON Schema object for the tool's arguments
1899    /// - `handler` — closure receiving the raw `arguments` JSON string
1900    ///
1901    /// The handler returns [`McpContent`] on success or an error string.  An
1902    /// error is returned to the client as `isError: true` (not a protocol error).
1903    ///
1904    /// Use [`Self::tool_with_context`] instead if the handler needs the
1905    /// caller's identity, session, or headers.
1906    pub fn tool<F>(self, name: &str, description: &str, input_schema: &str, handler: F) -> Self
1907    where
1908        F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
1909    {
1910        self.tools.write().unwrap().push(ToolDef {
1911            name: name.to_string(),
1912            description: description.to_string(),
1913            input_schema: input_schema.to_string(),
1914            annotations: None,
1915            meta: ToolMeta::default(),
1916            handler: Arc::new(move |_ctx: McpContext, args: &str| handler(args)),
1917        });
1918        self
1919    }
1920
1921    /// Register an `async fn` tool handler — for a tool whose work is
1922    /// naturally async (an `AsyncClient` HTTP call, an async database
1923    /// query, awaiting another future) rather than forcing it through a
1924    /// blocking call inside a plain [`Self::tool`] handler.
1925    ///
1926    /// Requires the `http2` feature (tokio). `tools/call` bridges into the
1927    /// handler's future via [`crate::async_bridge::block_on_isolated`] —
1928    /// the same mechanism [`crate::proxy::H2ReverseProxy`] and
1929    /// [`crate::async_state::AsyncAppWithState`] already use to call async
1930    /// code from a synchronous [`Application::execute`] — rather than
1931    /// `tokio::task::block_in_place`, which panics outside a `multi_thread`
1932    /// runtime; `block_on_isolated` works under any runtime flavor,
1933    /// including `current_thread`.
1934    ///
1935    /// Like [`Self::tool`] (not [`Self::tool_with_context`]), the handler
1936    /// only receives `arguments` — there is no async equivalent of
1937    /// `tool_with_context` or `tool_annotated` yet.
1938    ///
1939    /// ```rust,no_run
1940    /// # #[cfg(feature = "http2")]
1941    /// # {
1942    /// use rust_web_server::mcp::{McpContent, McpServer};
1943    ///
1944    /// let server = McpServer::new("my-server", "1.0")
1945    ///     .async_tool("call_api", "Call an external API", "{}", |_args: &str| async move {
1946    ///         Ok(McpContent::text("response"))
1947    ///     });
1948    /// # }
1949    /// ```
1950    #[cfg(feature = "http2")]
1951    pub fn async_tool<F, Fut>(self, name: &str, description: &str, input_schema: &str, handler: F) -> Self
1952    where
1953        F: Fn(&str) -> Fut + Send + Sync + 'static,
1954        Fut: Future<Output = Result<McpContent, String>> + Send + 'static,
1955    {
1956        self.async_tools.write().unwrap().push(AsyncToolDef {
1957            name: name.to_string(),
1958            description: description.to_string(),
1959            input_schema: input_schema.to_string(),
1960            annotations: None,
1961            meta: ToolMeta::default(),
1962            handler: Arc::new(move |args: String| Box::pin(handler(&args))),
1963        });
1964        self
1965    }
1966
1967    /// Register a callable tool with [`ToolAnnotations`] — behavioral hints
1968    /// (read-only, destructive, idempotent, open-world) that MCP clients use
1969    /// to decide whether to warn or confirm before calling it. Otherwise
1970    /// identical to [`Self::tool`] — the handler still only receives
1971    /// `arguments`, not [`McpContext`] (there is currently no single builder
1972    /// combining annotations with per-request context; call [`Self::tool_with_context`]
1973    /// instead if you need context and don't need annotations).
1974    ///
1975    /// ```rust,no_run
1976    /// use rust_web_server::mcp::{McpContent, McpServer, ToolAnnotations};
1977    ///
1978    /// let server = McpServer::new("my-server", "1.0")
1979    ///     .tool_annotated(
1980    ///         "delete_file",
1981    ///         "Delete a file from disk",
1982    ///         r#"{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}"#,
1983    ///         ToolAnnotations {
1984    ///             destructive_hint: Some(true),
1985    ///             read_only_hint: Some(false),
1986    ///             idempotent_hint: Some(true), // deleting twice = deleting once
1987    ///             ..Default::default()
1988    ///         },
1989    ///         |_args| Ok(McpContent::text("deleted")),
1990    ///     );
1991    /// ```
1992    pub fn tool_annotated<F>(
1993        self,
1994        name: &str,
1995        description: &str,
1996        input_schema: &str,
1997        annotations: ToolAnnotations,
1998        handler: F,
1999    ) -> Self
2000    where
2001        F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
2002    {
2003        self.tools.write().unwrap().push(ToolDef {
2004            name: name.to_string(),
2005            description: description.to_string(),
2006            input_schema: input_schema.to_string(),
2007            annotations: Some(annotations),
2008            meta: ToolMeta::default(),
2009            handler: Arc::new(move |_ctx: McpContext, args: &str| handler(args)),
2010        });
2011        self
2012    }
2013
2014    /// Register a callable tool whose handler also receives [`McpContext`] —
2015    /// caller identity/session info derived from this request's headers and
2016    /// whatever `clientInfo` this session sent at `initialize` time.
2017    ///
2018    /// Same `name`/`description`/`input_schema` semantics as [`Self::tool`];
2019    /// the only difference is the handler's first parameter.
2020    ///
2021    /// ```rust,no_run
2022    /// use rust_web_server::mcp::{McpContent, McpServer};
2023    ///
2024    /// let server = McpServer::new("my-server", "1.0")
2025    ///     .tool_with_context(
2026    ///         "whoami",
2027    ///         "Report the caller's client info",
2028    ///         "{}",
2029    ///         |ctx, _args| {
2030    ///             let name = ctx.client_name.as_deref().unwrap_or("unknown client");
2031    ///             Ok(McpContent::text(format!("Called by {name}")))
2032    ///         },
2033    ///     );
2034    /// ```
2035    pub fn tool_with_context<F>(self, name: &str, description: &str, input_schema: &str, handler: F) -> Self
2036    where
2037        F: Fn(McpContext, &str) -> Result<McpContent, String> + Send + Sync + 'static,
2038    {
2039        self.tools.write().unwrap().push(ToolDef {
2040            name: name.to_string(),
2041            description: description.to_string(),
2042            input_schema: input_schema.to_string(),
2043            annotations: None,
2044            meta: ToolMeta::default(),
2045            handler: Arc::new(handler),
2046        });
2047        self
2048    }
2049
2050    /// Register a tool whose arguments and result are Rust types: the
2051    /// `inputSchema` comes from `In` and the `outputSchema` from `Out`
2052    /// ([`JsonSchema`](crate::schema::JsonSchema), usually
2053    /// `#[derive(JsonSchema)]`), the arguments are deserialized into `In`, and
2054    /// the handler's `Out` is sent as `structuredContent` (and as JSON text
2055    /// for older clients). Requires the `serde` feature.
2056    ///
2057    /// Arguments that don't deserialize come back as an `isError` result
2058    /// naming the problem, as do the handler's errors. MCP requires an
2059    /// `outputSchema` to describe an object, so an `Out` that isn't a struct
2060    /// is sent as JSON text without one.
2061    ///
2062    /// ```rust,ignore
2063    /// use rust_web_server::JsonSchema;
2064    /// use serde::{Deserialize, Serialize};
2065    ///
2066    /// #[derive(Deserialize, JsonSchema)]
2067    /// struct Lookup { sku: String }
2068    ///
2069    /// #[derive(Serialize, JsonSchema)]
2070    /// struct Stock { sku: String, available: u32 }
2071    ///
2072    /// let server = McpServer::new("shop", "1.0")
2073    ///     .typed_tool("stock", "Units available for a SKU", |_ctx, args: Lookup| {
2074    ///         Ok(Stock { available: 3, sku: args.sku })
2075    ///     });
2076    /// ```
2077    #[cfg(feature = "serde")]
2078    pub fn typed_tool<In, Out, F>(self, name: &str, description: &str, handler: F) -> Self
2079    where
2080        In: serde::de::DeserializeOwned + crate::schema::JsonSchema,
2081        Out: serde::Serialize + crate::schema::JsonSchema,
2082        F: Fn(McpContext, In) -> Result<Out, String> + Send + Sync + 'static,
2083    {
2084        let output_schema = Out::json_schema();
2085        let structured = crate::schema::is_object_schema(&output_schema);
2086        let server = self.tool_with_context(name, description, &In::json_schema(), move |ctx, args| {
2087            let args = if args.trim().is_empty() { "{}" } else { args };
2088            let input: In = serde_json::from_str(args).map_err(|e| format!("Invalid arguments: {e}"))?;
2089            let output = handler(ctx, input)?;
2090            let json = serde_json::to_string(&output).map_err(|e| format!("Could not serialize the result: {e}"))?;
2091            Ok(if structured { McpContent::structured(json) } else { McpContent::json(json) })
2092        });
2093        if structured {
2094            server.tool_output_schema(name, &output_schema)
2095        } else {
2096            server
2097        }
2098    }
2099
2100    /// Register a readable resource.
2101    ///
2102    /// `uri_template` uses `{param}` placeholders, e.g. `"user://{id}"`.
2103    /// The handler receives the full concrete URI string.
2104    pub fn resource<F>(self, uri_template: &str, name: &str, description: &str, handler: F) -> Self
2105    where
2106        F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
2107    {
2108        self.resources.write().unwrap().push(ResourceDef {
2109            uri_template: uri_template.to_string(),
2110            name: name.to_string(),
2111            description: description.to_string(),
2112            handler: Arc::new(handler),
2113        });
2114        self
2115    }
2116
2117    /// Register a prompt template.
2118    ///
2119    /// The handler receives the raw `arguments` JSON string and returns a
2120    /// list of [`PromptMessage`] values.
2121    pub fn prompt<F>(self, name: &str, description: &str, handler: F) -> Self
2122    where
2123        F: Fn(&str) -> Result<Vec<PromptMessage>, String> + Send + Sync + 'static,
2124    {
2125        self.prompts.write().unwrap().push(PromptDef {
2126            name: name.to_string(),
2127            description: description.to_string(),
2128            arguments: vec![],
2129            handler: Arc::new(handler),
2130        });
2131        self
2132    }
2133
2134    /// Register a prompt template with explicit argument definitions.
2135    pub fn prompt_with_args<F>(
2136        self,
2137        name: &str,
2138        description: &str,
2139        args: Vec<PromptArgDef>,
2140        handler: F,
2141    ) -> Self
2142    where
2143        F: Fn(&str) -> Result<Vec<PromptMessage>, String> + Send + Sync + 'static,
2144    {
2145        self.prompts.write().unwrap().push(PromptDef {
2146            name: name.to_string(),
2147            description: description.to_string(),
2148            arguments: args,
2149            handler: Arc::new(handler),
2150        });
2151        self
2152    }
2153
2154    /// Register an argument-completion provider for one named argument of a
2155    /// tool or prompt, so clients like Cursor and VS Code can offer
2156    /// autocomplete while the user fills in that argument.
2157    ///
2158    /// `ref_type` is `"tool"` or `"prompt"` — matched against the incoming
2159    /// `completion/complete` request's `ref.type` (`"ref/tool"`/`"ref/prompt"`
2160    /// on the wire) with the `"ref/"` prefix stripped. `ref_name` is the
2161    /// tool or prompt name this applies to. The handler receives the
2162    /// argument's name and whatever partial value the user has typed so
2163    /// far, and returns candidate completion strings (or an error, mapped
2164    /// to a JSON-RPC `INVALID_PARAMS` response).
2165    ///
2166    /// `initialize` advertises the `completions` capability automatically
2167    /// once at least one `.completion()` has been registered — there's no
2168    /// separate opt-in flag to remember.
2169    ///
2170    /// ```rust
2171    /// use rust_web_server::mcp::McpServer;
2172    ///
2173    /// let server = McpServer::new("my-server", "1.0")
2174    ///     .completion("tool", "deploy", |arg_name, _partial| {
2175    ///         match arg_name {
2176    ///             "region" => Ok(vec!["us-east-1".to_string(), "eu-west-1".to_string()]),
2177    ///             _ => Ok(vec![]),
2178    ///         }
2179    ///     });
2180    /// ```
2181    pub fn completion<F>(self, ref_type: &str, ref_name: &str, handler: F) -> Self
2182    where
2183        F: Fn(&str, &str) -> Result<Vec<String>, String> + Send + Sync + 'static,
2184    {
2185        self.completions.write().unwrap().push(CompletionDef {
2186            ref_type: ref_type.to_string(),
2187            ref_name: ref_name.to_string(),
2188            handler: Arc::new(handler),
2189        });
2190        self
2191    }
2192
2193    // ── request dispatch ──────────────────────────────────────────────────────
2194
2195    /// Process a raw JSON-RPC body and return an HTTP response.
2196    ///
2197    /// Equivalent to [`Self::handle_request_with_context`] with an empty
2198    /// [`McpContext`] — tool handlers registered via
2199    /// [`Self::tool_with_context`] will see every field as `None`. Prefer
2200    /// calling through [`Application::execute`] (i.e. actually serving HTTP
2201    /// requests) when you need real per-request context; this method exists
2202    /// for calling the JSON-RPC layer directly, e.g. in tests.
2203    pub fn handle_request(&self, body: &str) -> Response {
2204        self.handle_request_with_context(body, McpContext::default())
2205    }
2206
2207    /// Process a raw JSON-RPC body with an explicit [`McpContext`] and return
2208    /// an HTTP response. [`Self::execute`] calls this with a context built
2209    /// from the request's headers and this session's stored `clientInfo`;
2210    /// [`Self::handle_request`] calls this with an empty context.
2211    ///
2212    /// On a successful `initialize`, this mints a new session id (reusing
2213    /// [`crate::request_id::generate_request_id`]'s ID generator), records
2214    /// `params.clientInfo` under it, and returns the id in an
2215    /// `Mcp-Session-Id` response header — the client is expected to echo that
2216    /// header back on subsequent requests so later `tools/call`s in the same
2217    /// session can look their `clientInfo` back up.
2218    pub fn handle_request_with_context(&self, body: &str, ctx: McpContext) -> Response {
2219        let trimmed = body.trim_start();
2220        if trimmed.starts_with('[') {
2221            return self.handle_batch(trimmed, ctx);
2222        }
2223
2224        let id = json_rpc::extract_id(body);
2225
2226        let method = match json_rpc::extract_str(body, "method") {
2227            Some(m) => m,
2228            None => {
2229                // No `method` — either malformed, or a JSON-RPC *response*
2230                // to a sampling/createMessage request this server sent
2231                // earlier (responses never carry `method`).
2232                if let Some(id) = &id {
2233                    if self.try_deliver_sampling_response(id, body) {
2234                        return no_content();
2235                    }
2236                }
2237                return rpc_error(None, json_rpc::INVALID_REQUEST, "Missing method");
2238            }
2239        };
2240
2241        if method == "notifications/cancelled" {
2242            self.handle_cancellation(body);
2243            return no_content();
2244        }
2245
2246        if method == "notifications/roots/list_changed" {
2247            self.invalidate_roots_cache(&ctx.session_id);
2248            return no_content();
2249        }
2250
2251        // Notifications have no `id` — acknowledge with 202 and no body.
2252        if method == "notifications/initialized" || (id.is_none() && method != "ping") {
2253            return no_content();
2254        }
2255
2256        let result = self.dispatch_with_cancellation(&method, body, ctx, &id);
2257        let id_str = id.as_deref().unwrap_or("null");
2258        let is_ok = result.is_ok();
2259
2260        let mut response = json_response(&Self::format_result(id_str, &result));
2261
2262        if method == "initialize" && is_ok {
2263            self.start_session(body, &mut response);
2264        }
2265
2266        response
2267    }
2268
2269    /// Process a JSON-RPC 2.0 batch request — a top-level JSON array of
2270    /// request objects sent in a single `POST /mcp` body, per the JSON-RPC
2271    /// batch spec that MCP inherits. Each element is dispatched exactly as
2272    /// [`Self::handle_request_with_context`] would dispatch it standalone;
2273    /// notifications (no `id`) contribute no entry to the response array,
2274    /// same as they'd get no response body outside a batch.
2275    ///
2276    /// An empty array (`[]`) is itself an invalid request per the JSON-RPC
2277    /// spec, so it gets one `Invalid Request` error object back rather than
2278    /// an empty array. A batch made up entirely of notifications produces no
2279    /// response body at all (`202 Accepted`), matching a single notification.
2280    ///
2281    /// If the batch contains a successful `initialize` call, the *first* one
2282    /// mints a session and attaches `Mcp-Session-Id` to the overall response,
2283    /// same as a standalone `initialize` would — sending more than one
2284    /// `initialize` in a batch is unusual and only the first is honored for
2285    /// session purposes, since one HTTP response can only carry one session id.
2286    fn handle_batch(&self, array_body: &str, ctx: McpContext) -> Response {
2287        let elements = json_rpc::split_array_elements(array_body);
2288        if elements.is_empty() {
2289            return rpc_error(None, json_rpc::INVALID_REQUEST, "Invalid Request");
2290        }
2291
2292        let mut parts: Vec<String> = Vec::new();
2293        let mut session_init_body: Option<String> = None;
2294
2295        for elem in &elements {
2296            let id = json_rpc::extract_id(elem);
2297
2298            let method = match json_rpc::extract_str(elem, "method") {
2299                Some(m) => m,
2300                None => {
2301                    if let Some(id) = &id {
2302                        if self.try_deliver_sampling_response(id, elem) {
2303                            continue; // delivered; no response entry, same as any notification
2304                        }
2305                    }
2306                    parts.push(Self::format_result(
2307                        "null",
2308                        &Err((json_rpc::INVALID_REQUEST, "Missing method".to_string())),
2309                    ));
2310                    continue;
2311                }
2312            };
2313
2314            if method == "notifications/cancelled" {
2315                self.handle_cancellation(elem);
2316                continue;
2317            }
2318
2319            if method == "notifications/roots/list_changed" {
2320                self.invalidate_roots_cache(&ctx.session_id);
2321                continue;
2322            }
2323
2324            if method == "notifications/initialized" || (id.is_none() && method != "ping") {
2325                continue;
2326            }
2327
2328            let result = self.dispatch_with_cancellation(&method, elem, ctx.clone(), &id);
2329            let id_str = id.as_deref().unwrap_or("null");
2330            let is_ok = result.is_ok();
2331
2332            if method == "initialize" && is_ok && session_init_body.is_none() {
2333                session_init_body = Some(elem.clone());
2334            }
2335
2336            parts.push(Self::format_result(id_str, &result));
2337        }
2338
2339        if parts.is_empty() {
2340            // Every element was a notification — no response body, same as a
2341            // single standalone notification.
2342            return no_content();
2343        }
2344
2345        let mut response = json_response(&format!("[{}]", parts.join(",")));
2346        if let Some(init_body) = session_init_body {
2347            self.start_session(&init_body, &mut response);
2348        }
2349        response
2350    }
2351
2352    /// Dispatch one already-parsed JSON-RPC `method` against `body` (the raw
2353    /// single-object message, whether it arrived standalone or as one element
2354    /// of a batch) and return the JSON-RPC `result` payload or an error.
2355    /// Shared by [`Self::handle_request_with_context`] and [`Self::handle_batch`]
2356    /// so the method table exists in exactly one place.
2357    fn dispatch(&self, method: &str, body: &str, ctx: McpContext) -> Result<String, (i32, String)> {
2358        match method {
2359            "initialize"     => self.do_initialize(body),
2360            "ping"           => Ok("{}".to_string()),
2361            "tools/list"     => self.do_tools_list(body, &ctx),
2362            "tools/call"     => self.do_tools_call(body, ctx),
2363            "resources/list" => self.do_resources_list(body),
2364            "resources/read" => self.do_resources_read(body),
2365            "resources/subscribe"   => self.do_resource_subscribe(body, ctx.session_id),
2366            "resources/unsubscribe" => self.do_resource_unsubscribe(body, ctx.session_id),
2367            "prompts/list"   => self.do_prompts_list(body),
2368            "prompts/get"    => self.do_prompts_get(body),
2369            "logging/setLevel" => self.do_set_log_level(body),
2370            "completion/complete" => self.do_completion(body),
2371            _                => Err((json_rpc::METHOD_NOT_FOUND, format!("Unknown method: {method}"))),
2372        }
2373    }
2374
2375    /// Wraps [`Self::dispatch`] for one request, additionally registering a
2376    /// cancellation flag for `tools/call` (keyed by `id`, guaranteed `Some`
2377    /// for `tools/call` — a notification-shaped `tools/call` with no `id`
2378    /// never reaches this far, see the notification checks in
2379    /// [`Self::handle_request_with_context`]/[`Self::handle_batch`]) so a
2380    /// later `notifications/cancelled` referencing this exact request can
2381    /// flip it. The entry is removed again once dispatch returns, whether
2382    /// the handler checked the flag or not — this map never accumulates
2383    /// stale entries the way `sessions`/`sse_clients` can.
2384    fn dispatch_with_cancellation(
2385        &self,
2386        method: &str,
2387        body: &str,
2388        ctx: McpContext,
2389        id: &Option<String>,
2390    ) -> Result<String, (i32, String)> {
2391        let Some(id_str) = (method == "tools/call").then_some(id.as_ref()).flatten() else {
2392            return self.dispatch(method, body, ctx);
2393        };
2394
2395        let flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
2396        self.cancellations.lock().unwrap().insert(id_str.clone(), flag.clone());
2397        let ctx = McpContext { cancellation: Some(flag), ..ctx };
2398        let result = self.dispatch(method, body, ctx);
2399        self.cancellations.lock().unwrap().remove(id_str);
2400        result
2401    }
2402
2403    /// Handle `notifications/cancelled`: look up `params.requestId` (the
2404    /// spec allows `string | number`, so this is matched as a raw JSON
2405    /// token, the same way [`McpContext::progress_token`] is) in
2406    /// `cancellations` and flip its flag if the request is still in flight.
2407    /// Silently does nothing for an unknown/already-finished request id —
2408    /// the target call may simply have already completed naturally, which
2409    /// isn't an error.
2410    fn handle_cancellation(&self, body: &str) {
2411        let Some(params) = json_rpc::extract_raw(body, "params") else { return };
2412        let Some(request_id) = json_rpc::extract_raw(&params, "requestId") else { return };
2413        if let Some(flag) = self.cancellations.lock().unwrap().get(&request_id) {
2414            flag.store(true, std::sync::atomic::Ordering::Relaxed);
2415        }
2416    }
2417
2418    /// Handle `notifications/roots/list_changed`: clear this session's
2419    /// cached `roots/list` result (if any), so the next
2420    /// [`McpContext::list_roots`] call in this session does a fresh round
2421    /// trip instead of serving stale data. Correlated purely by this
2422    /// request's own `Mcp-Session-Id` (`session_id`) — unlike
2423    /// `notifications/cancelled`, this notification carries no params of
2424    /// its own to key off of; it's about "your cache for *this connection*
2425    /// is stale," not any other request.
2426    fn invalidate_roots_cache(&self, session_id: &Option<String>) {
2427        let Some(sid) = session_id else { return };
2428        if let Some(info) = self.sessions.lock().unwrap().get_mut(sid) {
2429            info.roots = None;
2430        }
2431    }
2432
2433    /// If `body` is a JSON-RPC *response* (no `method`, by definition — see
2434    /// [`Self::handle_request_with_context`]) to a `sampling/createMessage`
2435    /// request this server sent and is still waiting on (`id` found in
2436    /// `pending_replies`), deliver it to the blocked [`McpContext::sample`]
2437    /// call and return `true`. Returns `false` for any `id` this server
2438    /// isn't tracking — the caller still needs to treat that as a genuine
2439    /// malformed/unrecognized message.
2440    fn try_deliver_sampling_response(&self, id: &str, body: &str) -> bool {
2441        let sender = match self.pending_replies.lock().unwrap().remove(id) {
2442            Some(s) => s,
2443            None => return false,
2444        };
2445        let payload = match json_rpc::extract_raw(body, "result") {
2446            Some(result) => Ok(result),
2447            None => {
2448                let message = json_rpc::extract_raw(body, "error")
2449                    .and_then(|e| json_rpc::extract_str(&e, "message"))
2450                    .unwrap_or_else(|| "sampling/createMessage request failed".to_string());
2451                Err(message)
2452            }
2453        };
2454        let _ = sender.send(payload); // the waiter may have already timed out and dropped rx
2455        true
2456    }
2457
2458    /// Render one JSON-RPC 2.0 response object — `{"jsonrpc":"2.0","result":...,"id":...}`
2459    /// or the `error` shape — from a dispatch result and its request's `id` (already
2460    /// rendered as a raw JSON token, e.g. `"1"`, `"\"abc\""`, or `"null"`).
2461    fn format_result(id_str: &str, result: &Result<String, (i32, String)>) -> String {
2462        match result {
2463            Ok(result_json) => format!(
2464                r#"{{"jsonrpc":"2.0","result":{result_json},"id":{id_str}}}"#
2465            ),
2466            Err((code, msg)) => {
2467                let escaped = json_escape(msg);
2468                format!(
2469                    r#"{{"jsonrpc":"2.0","error":{{"code":{code},"message":"{escaped}"}},"id":{id_str}}}"#
2470                )
2471            }
2472        }
2473    }
2474
2475    /// Mint a new session id, record `body`'s `params.clientInfo` under it
2476    /// (logging the caller's identity), and attach the id to `response` as
2477    /// an `Mcp-Session-Id` header. Called once, from
2478    /// [`Self::handle_request_with_context`], right after a successful
2479    /// `initialize`.
2480    fn start_session(&self, body: &str, response: &mut Response) {
2481        let params = json_rpc::extract_raw(body, "params");
2482        let client_info = params.as_deref().and_then(|p| json_rpc::extract_raw(p, "clientInfo"));
2483        let (name, version) = match &client_info {
2484            Some(info) => (
2485                json_rpc::extract_str(info, "name"),
2486                json_rpc::extract_str(info, "version"),
2487            ),
2488            None => (None, None),
2489        };
2490        let capabilities = params.as_deref().and_then(|p| json_rpc::extract_raw(p, "capabilities"));
2491        let supports_sampling = capabilities.as_deref()
2492            .and_then(|c| json_rpc::extract_raw(c, "sampling"))
2493            .is_some();
2494        let supports_roots = capabilities.as_deref()
2495            .and_then(|c| json_rpc::extract_raw(c, "roots"))
2496            .is_some();
2497
2498        eprintln!(
2499            "[mcp] initialize from client {} v{}",
2500            name.as_deref().unwrap_or("unknown"),
2501            version.as_deref().unwrap_or("unknown"),
2502        );
2503
2504        let session_id = crate::request_id::generate_request_id();
2505        self.make_room_for_a_session();
2506        self.sessions
2507            .lock()
2508            .unwrap()
2509            .insert(session_id.clone(), StoredClientInfo { last_seen: std::time::Instant::now(), protocol_version: negotiate_version(
2510                params.as_deref().and_then(|p| json_rpc::extract_str(p, "protocolVersion")).as_deref()), name, version, supports_sampling, supports_roots, roots: None });
2511
2512        response.headers.push(Header {
2513            name: "Mcp-Session-Id".into(),
2514            value: session_id.into(),
2515        });
2516    }
2517
2518    // ── method handlers ───────────────────────────────────────────────────────
2519
2520    /// Handle `initialize`. Per spec, the server must inspect the client's
2521    /// requested `protocolVersion` and respond with the version it actually
2522    /// supports — allowing the client to abort the session if incompatible —
2523    /// rather than blindly echoing `PROTOCOL_VERSION` regardless of what was
2524    /// asked for.
2525    ///
2526    /// A version in [`SUPPORTED_PROTOCOL_VERSIONS`] is confirmed; any other
2527    /// (older, newer, malformed) gets [`PROTOCOL_VERSION`], the newest. The
2528    /// session remembers the result, which decides e.g. whether JSON-RPC
2529    /// batches are accepted (only up to 2025-03-26).
2530    ///
2531    /// `clientInfo` is *not* handled here — [`Self::handle_request_with_context`]
2532    /// extracts and stores it (under a freshly minted session id) after this
2533    /// returns, so it's only ever parsed out of `body` once per call.
2534    fn do_initialize(&self, body: &str) -> Result<String, (i32, String)> {
2535        let params = json_rpc::extract_raw(body, "params");
2536
2537        let client_version = params.as_deref().and_then(|p| json_rpc::extract_str(p, "protocolVersion"));
2538
2539        let negotiated_version = negotiate_version(client_version.as_deref());
2540
2541        let logging_cap = if self.logging_enabled { r#","logging":{}"# } else { "" };
2542        // completions is advertised iff at least one .completion() has been registered —
2543        // no separate opt-in flag needed, unlike logging: if nothing was registered,
2544        // completion/complete would just return empty results for everything anyway.
2545        let completions_cap = if self.completions.read().unwrap().is_empty() { "" } else { r#","completions":{}"# };
2546        // listChanged is always true: register_tool/remove_tool (and the resource/prompt
2547        // equivalents) are always available, unlike logging which is opt-in via
2548        // .logging_enabled(). resources.subscribe is also always true now that
2549        // resources/subscribe and resources/unsubscribe are implemented (MCP_TODO.md's TODO-14).
2550        let caps = format!(
2551            r#"{{"tools":{{"listChanged":true}},"resources":{{"subscribe":true,"listChanged":true}},"prompts":{{"listChanged":true}}{logging_cap}{completions_cap}}}"#
2552        );
2553        Ok(format!(
2554            r#"{{"protocolVersion":"{}","capabilities":{caps},"serverInfo":{{"name":"{}"{}"version":"{}"{}}}}}"#,
2555            json_escape(negotiated_version),
2556            json_escape(&self.server_name),
2557            self.server_title.as_ref().map(|t| format!(r#","title":"{}","#, json_escape(t))).unwrap_or_else(|| ",".to_string()),
2558            json_escape(&self.server_version),
2559            icons_json(&self.server_icons),
2560        ))
2561    }
2562
2563    fn do_tools_list(&self, body: &str, ctx: &McpContext) -> Result<String, (i32, String)> {
2564        #[cfg_attr(not(feature = "http2"), allow(unused_mut))]
2565        let mut items: Vec<String> = self.tools.read().unwrap().iter()
2566            .filter(|t| self.tool_allowed(ctx, &t.name))
2567            .map(|t| Self::render_tool_list_entry(&t.name, &t.description, &t.input_schema, t.annotations, &t.meta))
2568            .collect();
2569
2570        #[cfg(feature = "http2")]
2571        items.extend(self.async_tools.read().unwrap().iter()
2572            .filter(|t| self.tool_allowed(ctx, &t.name))
2573            .map(|t| Self::render_tool_list_entry(&t.name, &t.description, &t.input_schema, t.annotations, &t.meta)));
2574
2575        let (page, next_cursor) = self.paginate(&items, body)?;
2576        Ok(format!(r#"{{"tools":[{}]{}}}"#, page.join(","), next_cursor_json(&next_cursor)))
2577    }
2578
2579    /// Render one `tools/list` entry — shared by the sync (`tools`) and
2580    /// (with the `http2` feature) async (`async_tools`) collections, which
2581    /// are otherwise identical from the client's point of view.
2582    fn render_tool_list_entry(name: &str, description: &str, input_schema: &str, annotations: Option<ToolAnnotations>, meta: &ToolMeta) -> String {
2583        let annotations_field = match annotations {
2584            Some(a) => format!(r#","annotations":{}"#, a.to_json()),
2585            None => String::new(),
2586        };
2587        let title = meta.title.as_ref().map(|t| format!(r#","title":"{}""#, json_escape(t))).unwrap_or_default();
2588        let output_schema = meta.output_schema.as_ref().map(|schema| format!(r#","outputSchema":{schema}"#)).unwrap_or_default();
2589        format!(
2590            r#"{{"name":"{}"{title},"description":"{}","inputSchema":{}{output_schema}{}{}}}"#,
2591            json_escape(name), json_escape(description), input_schema, annotations_field, icons_json(&meta.icons),
2592        )
2593    }
2594
2595    /// Check a `tools/call`'s `arguments` against the tool's input schema:
2596    /// an object, every `required` property present, and each declared
2597    /// property of the declared `type`. Not a full JSON Schema validator —
2598    /// the checks a model's mistakes actually trip. A failure becomes a tool
2599    /// result with `isError: true` (MCP 2025-11-25, SEP-1303), so the model
2600    /// sees why and can retry, instead of a protocol error it never reads.
2601    pub(crate) fn validate_arguments(schema: &str, args: &str) -> Result<(), String> {
2602        let Some(schema) = json_rpc::object_members(schema) else { return Ok(()) };
2603        let field = |name: &str| schema.iter().find(|(k, _)| k == name).map(|(_, v)| v.as_str());
2604        let Some(arguments) = json_rpc::object_members(args) else {
2605            return Err("Invalid arguments: expected a JSON object".to_string());
2606        };
2607        if let Some(required) = field("required") {
2608            for name in json_rpc::split_array_elements(required) {
2609                let name = name.trim().trim_matches('"');
2610                if !arguments.iter().any(|(k, _)| k == name) {
2611                    return Err(format!("Invalid arguments: missing required property \"{name}\""));
2612                }
2613            }
2614        }
2615        let Some(properties) = field("properties").and_then(json_rpc::object_members) else { return Ok(()) };
2616        for (name, value) in &arguments {
2617            let Some((_, property)) = properties.iter().find(|(k, _)| k == name) else { continue };
2618            let Some(declared) = json_rpc::object_members(property)
2619                .and_then(|p| p.into_iter().find(|(k, _)| k == "type").map(|(_, v)| v))
2620            else { continue };
2621            let allowed: Vec<String> = if declared.starts_with('[') {
2622                json_rpc::split_array_elements(&declared).iter().map(|t| t.trim().trim_matches('"').to_string()).collect()
2623            } else {
2624                vec![declared.trim_matches('"').to_string()]
2625            };
2626            let actual = json_type(value);
2627            let fits = allowed.iter().any(|t| t == actual || (t == "number" && actual == "integer"));
2628            if !fits {
2629                return Err(format!("Invalid arguments: property \"{name}\" must be {}, got {actual}", allowed.join(" or ")));
2630            }
2631        }
2632        Ok(())
2633    }
2634
2635    fn do_tools_call(&self, body: &str, ctx: McpContext) -> Result<String, (i32, String)> {
2636        let params = json_rpc::extract_raw(body, "params")
2637            .ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
2638        let name = json_rpc::extract_str(&params, "name")
2639            .ok_or((json_rpc::INVALID_PARAMS, "Missing tool name".to_string()))?;
2640        let args = json_rpc::extract_raw(&params, "arguments")
2641            .unwrap_or_else(|| "{}".to_string());
2642        if !self.tool_allowed(&ctx, &name) {
2643            return Err((json_rpc::INVALID_PARAMS, format!("Unknown tool: {name}")));
2644        }
2645
2646        // `_meta.progressToken` (string or number, per spec) — stored raw so
2647        // `McpContext::report_progress` can splice it back verbatim.
2648        let progress_token = json_rpc::extract_raw(&params, "_meta")
2649            .and_then(|meta| json_rpc::extract_raw(&meta, "progressToken"));
2650        let ctx = McpContext { progress_token, ..ctx };
2651
2652        let sync_handler = {
2653            let tools = self.tools.read().unwrap();
2654            tools.iter().find(|t| t.name == name).map(|t| (t.handler.clone(), t.input_schema.clone()))
2655        };
2656        if let Some((handler, schema)) = sync_handler {
2657            if let Err(invalid) = Self::validate_arguments(&schema, &args) {
2658                return Ok(Self::format_tool_result(Err(invalid)));
2659            }
2660            return Ok(Self::format_tool_result(handler(ctx, &args)));
2661        }
2662
2663        #[cfg(feature = "http2")]
2664        {
2665            let async_handler = {
2666                let async_tools = self.async_tools.read().unwrap();
2667                async_tools.iter().find(|t| t.name == name).map(|t| (t.handler.clone(), t.input_schema.clone()))
2668            };
2669            if let Some((handler, schema)) = async_handler {
2670                if let Err(invalid) = Self::validate_arguments(&schema, &args) {
2671                    return Ok(Self::format_tool_result(Err(invalid)));
2672                }
2673                let args_owned = args.clone();
2674                let result = crate::async_bridge::block_on_isolated(move || handler(args_owned));
2675                return Ok(Self::format_tool_result(result));
2676            }
2677        }
2678
2679        Err((json_rpc::INVALID_PARAMS, format!("Unknown tool: {name}")))
2680    }
2681
2682    /// Render a tool handler's result as the spec's `tools/call` shape —
2683    /// `{"content":[...],"isError":false}` on success, or `isError:true`
2684    /// with the error message as a text content block. Shared by the sync
2685    /// and (with `http2`) async `tools/call` dispatch paths.
2686    fn format_tool_result(result: Result<McpContent, String>) -> String {
2687        match result {
2688            Ok(c) if c.kind == "result" => c.text,
2689            Ok(c) if c.kind == "blocks" => format!(r#"{{"content":[{}],"isError":false}}"#, c.text),
2690            Ok(c) if c.kind == "structured" => format!(
2691                r#"{{"content":[{{"type":"text","text":"{}"}}],"structuredContent":{},"isError":false}}"#,
2692                json_escape(&c.text),
2693                c.text,
2694            ),
2695            Ok(c) => format!(
2696                r#"{{"content":[{}],"isError":false}}"#,
2697                c.to_content_json(),
2698            ),
2699            Err(e) => {
2700                let escaped = json_escape(&e);
2701                format!(
2702                    r#"{{"content":[{{"type":"text","text":"{escaped}"}}],"isError":true}}"#
2703                )
2704            }
2705        }
2706    }
2707
2708    fn do_resources_list(&self, body: &str) -> Result<String, (i32, String)> {
2709        let items: Vec<String> = self.resources.read().unwrap().iter().map(|r| {
2710            format!(
2711                r#"{{"uri":"{}","name":"{}","description":"{}","mimeType":"text/plain"}}"#,
2712                json_escape(&r.uri_template),
2713                json_escape(&r.name),
2714                json_escape(&r.description),
2715            )
2716        }).collect();
2717        let (page, next_cursor) = self.paginate(&items, body)?;
2718        Ok(format!(r#"{{"resources":[{}]{}}}"#, page.join(","), next_cursor_json(&next_cursor)))
2719    }
2720
2721    fn do_resources_read(&self, body: &str) -> Result<String, (i32, String)> {
2722        let params = json_rpc::extract_raw(body, "params")
2723            .ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
2724        let uri = json_rpc::extract_str(&params, "uri")
2725            .ok_or((json_rpc::INVALID_PARAMS, "Missing uri".to_string()))?;
2726
2727        let handler = {
2728            let resources = self.resources.read().unwrap();
2729            resources.iter().find(|r| uri_matches(&r.uri_template, &uri)).map(|r| r.handler.clone())
2730        }.ok_or_else(|| (json_rpc::INVALID_PARAMS, format!("Resource not found: {uri}")))?;
2731
2732        match handler(&uri) {
2733            Ok(c) => {
2734                let text_esc = json_escape(&c.text);
2735                let uri_esc  = json_escape(&uri);
2736                let field = if c.kind == "blob" { "blob" } else { "text" };
2737                Ok(format!(
2738                    r#"{{"contents":[{{"uri":"{uri_esc}","mimeType":"{}","{field}":"{text_esc}"}}]}}"#,
2739                    json_escape(c.mime()),
2740                ))
2741            }
2742            Err(e) => Err((json_rpc::INVALID_PARAMS, e)),
2743        }
2744    }
2745
2746    /// Handle `resources/subscribe`: record that `session_id` (this
2747    /// request's `Mcp-Session-Id`) wants `notifications/resources/updated`
2748    /// pushed whenever [`Self::notify_resource_updated`] is called for
2749    /// `params.uri`. Requires a session id — without one there's no way to
2750    /// later match this subscription to a specific `GET /mcp` SSE
2751    /// connection, so a client that never sent `Mcp-Session-Id` gets
2752    /// `INVALID_PARAMS` rather than a subscription that can never fire.
2753    fn do_resource_subscribe(&self, body: &str, session_id: Option<String>) -> Result<String, (i32, String)> {
2754        let params = json_rpc::extract_raw(body, "params")
2755            .ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
2756        let uri = json_rpc::extract_str(&params, "uri")
2757            .ok_or((json_rpc::INVALID_PARAMS, "Missing uri".to_string()))?;
2758        let session_id = session_id.ok_or((
2759            json_rpc::INVALID_PARAMS,
2760            "resources/subscribe requires an Mcp-Session-Id header (call initialize first)".to_string(),
2761        ))?;
2762
2763        let mut subs = self.subscriptions.lock().unwrap();
2764        let subscribers = subs.entry(uri).or_default();
2765        if !subscribers.contains(&session_id) {
2766            subscribers.push(session_id);
2767        }
2768        Ok("{}".to_string())
2769    }
2770
2771    /// Handle `resources/unsubscribe`: the inverse of
2772    /// [`Self::do_resource_subscribe`]. Removing the last subscriber for a
2773    /// URI drops the URI's entry entirely rather than leaving an empty
2774    /// `Vec` behind.
2775    fn do_resource_unsubscribe(&self, body: &str, session_id: Option<String>) -> Result<String, (i32, String)> {
2776        let params = json_rpc::extract_raw(body, "params")
2777            .ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
2778        let uri = json_rpc::extract_str(&params, "uri")
2779            .ok_or((json_rpc::INVALID_PARAMS, "Missing uri".to_string()))?;
2780        let session_id = session_id.ok_or((
2781            json_rpc::INVALID_PARAMS,
2782            "resources/unsubscribe requires an Mcp-Session-Id header".to_string(),
2783        ))?;
2784
2785        let mut subs = self.subscriptions.lock().unwrap();
2786        if let Some(subscribers) = subs.get_mut(&uri) {
2787            subscribers.retain(|s| s != &session_id);
2788            if subscribers.is_empty() {
2789                subs.remove(&uri);
2790            }
2791        }
2792        Ok("{}".to_string())
2793    }
2794
2795    fn do_prompts_list(&self, body: &str) -> Result<String, (i32, String)> {
2796        let items: Vec<String> = self.prompts.read().unwrap().iter().map(|p| {
2797            let arg_defs: Vec<String> = p.arguments.iter().map(|a| {
2798                format!(
2799                    r#"{{"name":"{}","description":"{}","required":{}}}"#,
2800                    json_escape(&a.name),
2801                    json_escape(&a.description),
2802                    a.required,
2803                )
2804            }).collect();
2805            format!(
2806                r#"{{"name":"{}","description":"{}","arguments":[{}]}}"#,
2807                json_escape(&p.name),
2808                json_escape(&p.description),
2809                arg_defs.join(","),
2810            )
2811        }).collect();
2812        let (page, next_cursor) = self.paginate(&items, body)?;
2813        Ok(format!(r#"{{"prompts":[{}]{}}}"#, page.join(","), next_cursor_json(&next_cursor)))
2814    }
2815
2816    /// Slice `items` (already-rendered JSON object strings for one
2817    /// `*/list` response) according to [`Self::page_size`] and this
2818    /// request's `params.cursor`, returning the page and — if more items
2819    /// remain — the opaque `nextCursor` to embed in the response.
2820    ///
2821    /// Without a configured `page_size`, always returns every item and no
2822    /// cursor, i.e. pagination is fully opt-in.
2823    fn paginate<'a>(&self, items: &'a [String], body: &str) -> Result<(&'a [String], Option<String>), (i32, String)> {
2824        let page_size = match self.page_size {
2825            Some(n) => n,
2826            None => return Ok((items, None)),
2827        };
2828
2829        let cursor = json_rpc::extract_raw(body, "params")
2830            .and_then(|p| json_rpc::extract_str(&p, "cursor"));
2831
2832        let offset = match cursor {
2833            Some(c) => decode_cursor(&c)
2834                .ok_or((json_rpc::INVALID_PARAMS, "Invalid cursor".to_string()))?,
2835            None => 0,
2836        };
2837
2838        if offset >= items.len() {
2839            return Ok((&[], None));
2840        }
2841
2842        let end = (offset + page_size).min(items.len());
2843        let next_cursor = if end < items.len() { Some(encode_cursor(end)) } else { None };
2844        Ok((&items[offset..end], next_cursor))
2845    }
2846
2847    fn do_prompts_get(&self, body: &str) -> Result<String, (i32, String)> {
2848        let params = json_rpc::extract_raw(body, "params")
2849            .ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
2850        let name = json_rpc::extract_str(&params, "name")
2851            .ok_or((json_rpc::INVALID_PARAMS, "Missing prompt name".to_string()))?;
2852        let args = json_rpc::extract_raw(&params, "arguments")
2853            .unwrap_or_else(|| "{}".to_string());
2854
2855        let (description, handler) = {
2856            let prompts = self.prompts.read().unwrap();
2857            prompts.iter().find(|p| p.name == name).map(|p| (p.description.clone(), p.handler.clone()))
2858        }.ok_or_else(|| (json_rpc::INVALID_PARAMS, format!("Unknown prompt: {name}")))?;
2859
2860        match handler(&args) {
2861            Ok(msgs) => {
2862                let msg_jsons: Vec<String> = msgs.iter().map(|m| m.to_json()).collect();
2863                Ok(format!(
2864                    r#"{{"description":"{}","messages":[{}]}}"#,
2865                    json_escape(&description),
2866                    msg_jsons.join(","),
2867                ))
2868            }
2869            Err(e) => Err((json_rpc::INVALID_PARAMS, e)),
2870        }
2871    }
2872
2873    /// Handle `completion/complete`: look up the registered
2874    /// [`CompletionDef`] matching `params.ref.type`/`params.ref.name`, call
2875    /// its handler with `params.argument.name`/`params.argument.value`, and
2876    /// render the spec's `{"completion":{"values":[...],"hasMore":...,
2877    /// "total":...}}` shape. No registered provider for the given ref/name
2878    /// (or an unrecognized `ref.type` not stripped of `"ref/"`) returns an
2879    /// empty `values` list rather than an error — matching the spec's own
2880    /// framing of completion as a best-effort hint, not a required capability
2881    /// per tool/prompt.
2882    fn do_completion(&self, body: &str) -> Result<String, (i32, String)> {
2883        let params = json_rpc::extract_raw(body, "params")
2884            .ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
2885        let reference = json_rpc::extract_raw(&params, "ref")
2886            .ok_or((json_rpc::INVALID_PARAMS, "Missing ref".to_string()))?;
2887        let ref_type_raw = json_rpc::extract_str(&reference, "type")
2888            .ok_or((json_rpc::INVALID_PARAMS, "Missing ref.type".to_string()))?;
2889        let ref_type = ref_type_raw.strip_prefix("ref/").unwrap_or(&ref_type_raw);
2890        let ref_name = json_rpc::extract_str(&reference, "name")
2891            .ok_or((json_rpc::INVALID_PARAMS, "Missing ref.name".to_string()))?;
2892
2893        let argument = json_rpc::extract_raw(&params, "argument")
2894            .ok_or((json_rpc::INVALID_PARAMS, "Missing argument".to_string()))?;
2895        let arg_name = json_rpc::extract_str(&argument, "name")
2896            .ok_or((json_rpc::INVALID_PARAMS, "Missing argument.name".to_string()))?;
2897        let partial = json_rpc::extract_str(&argument, "value").unwrap_or_default();
2898
2899        let handler = {
2900            let completions = self.completions.read().unwrap();
2901            completions.iter()
2902                .find(|c| c.ref_type == ref_type && c.ref_name == ref_name)
2903                .map(|c| c.handler.clone())
2904        };
2905
2906        let values = match handler {
2907            Some(h) => h(&arg_name, &partial).map_err(|e| (json_rpc::INVALID_PARAMS, e))?,
2908            None => vec![],
2909        };
2910
2911        let total = values.len();
2912        let has_more = total > MAX_COMPLETION_VALUES;
2913        let page = if has_more { &values[..MAX_COMPLETION_VALUES] } else { &values[..] };
2914        let values_json: Vec<String> = page.iter().map(|v| format!(r#""{}""#, json_escape(v))).collect();
2915
2916        Ok(format!(
2917            r#"{{"completion":{{"values":[{}],"hasMore":{has_more},"total":{total}}}}}"#,
2918            values_json.join(","),
2919        ))
2920    }
2921
2922    /// Answers a `tools/call` as an SSE stream (Streamable HTTP): the
2923    /// handler runs on its own thread, its progress notifications go out as
2924    /// they happen, then the JSON-RPC response, and the stream ends.
2925    fn stream_tool_call(&self, body: &str, mut ctx: McpContext) -> Response {
2926        let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(32);
2927        ctx.request_stream = Some(tx.clone());
2928        let server = self.clone();
2929        let body = body.to_string();
2930        std::thread::spawn(move || {
2931            let response = server.handle_request_with_context(&body, ctx);
2932            let json = response.content_range_list.first().map(|part| String::from_utf8_lossy(&part.body).into_owned()).unwrap_or_default();
2933            let _ = tx.send(format!("data: {json}\n\n").into_bytes());
2934        });
2935        let mut response = Response::new();
2936        response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
2937        response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
2938        response.headers.push(Header { name: Header::_CONTENT_TYPE.into(), value: "text/event-stream".into() });
2939        response.headers.push(Header { name: Header::_CACHE_CONTROL.into(), value: "no-cache".into() });
2940        response.headers.push(Header { name: "X-Accel-Buffering".into(), value: "no".into() });
2941        response.stream_pipe = Some(Box::new(SseChannelReader::new(rx)));
2942        response
2943    }
2944
2945    /// Build the [`McpContext`] for an incoming request: the `Mcp-Session-Id`
2946    /// header, if present, plus whatever `clientInfo` was recorded for that
2947    /// session at `initialize` time (if this session is recognized).
2948    fn context_for(&self, request: &Request) -> McpContext {
2949        let session_id = request
2950            .get_header("Mcp-Session-Id".to_string())
2951            .map(|h| h.value.to_string());
2952
2953        let (client_name, client_version, sampling_supported, roots_supported) = match &session_id {
2954            Some(sid) => match self.sessions.lock().unwrap().get(sid) {
2955                Some(info) => (info.name.clone(), info.version.clone(), info.supports_sampling, info.supports_roots),
2956                None => (None, None, false, false),
2957            },
2958            None => (None, None, false, false),
2959        };
2960
2961        McpContext {
2962            client_name,
2963            client_version,
2964            session_id,
2965            auth_claims: None,
2966            progress_token: None,
2967            sse_clients: Some(self.sse_clients.clone()),
2968            cancellation: None,
2969            sampling_supported,
2970            pending_replies: Some(self.pending_replies.clone()),
2971            roots_supported,
2972            sessions: Some(self.sessions.clone()),
2973            request_stream: None,
2974        }
2975    }
2976}
2977
2978/// Render one JSON-RPC 2.0 notification (no `id` — fire-and-forget, per
2979/// spec) as an SSE `data:`-ready message body. Shared by [`McpServer::notify`]
2980/// and [`McpContext::report_progress`].
2981/// A single `tools/call` POST that asks for progress (`_meta.progressToken`)
2982/// from a client that accepts an SSE response.
2983fn wants_progress_stream(request: &Request, body: &str) -> bool {
2984    let accepts_sse = request.headers.iter()
2985        .filter(|h| h.name.eq_ignore_ascii_case("accept"))
2986        .any(|h| h.value.to_ascii_lowercase().contains("text/event-stream"));
2987    if !accepts_sse || body.trim_start().starts_with('[') {
2988        return false;
2989    }
2990    let Some(members) = json_rpc::object_members(body) else { return false };
2991    let member = |key: &str| members.iter().find(|(k, _)| k == key).map(|(_, v)| v.as_str());
2992    member("method") == Some(r#""tools/call""#)
2993        && member("id").is_some()
2994        && member("params")
2995            .and_then(json_rpc::object_members)
2996            .and_then(|params| params.into_iter().find(|(k, _)| k == "_meta"))
2997            .and_then(|(_, meta)| json_rpc::object_members(&meta))
2998            .is_some_and(|meta| meta.iter().any(|(k, _)| k == "progressToken"))
2999}
3000
3001fn render_notification(method: &str, params_json: Option<&str>) -> String {
3002    let params_field = match params_json {
3003        Some(p) => format!(r#","params":{p}"#),
3004        None => String::new(),
3005    };
3006    format!(r#"{{"jsonrpc":"2.0","method":"{}"{}}}"#, json_escape(method), params_field)
3007}
3008
3009/// Send a raw pre-built JSON-RPC message to every client in `clients`,
3010/// pruning any whose channel is full or disconnected. Shared by
3011/// [`McpServer::notify`] and [`McpContext::report_progress`] — the latter
3012/// only has a clone of the broadcast list, not a whole `McpServer`.
3013fn broadcast_sse_to(clients: &Arc<Mutex<Vec<SseClient>>>, json: &str) {
3014    let frame = format!("data: {json}\n\n").into_bytes();
3015    let mut clients = clients.lock().unwrap();
3016    clients.retain(|c| c.sender.try_send(frame.clone()).is_ok());
3017}
3018
3019/// Send a raw pre-built JSON-RPC message only to clients whose `session_id`
3020/// is in `session_ids`, pruning any of *those* whose channel is full or
3021/// disconnected. A client with no `session_id`, or one not in
3022/// `session_ids`, is left untouched (kept, not sent to) — this is a
3023/// targeted send for [`McpServer::notify_resource_updated`], not a
3024/// broadcast like [`broadcast_sse_to`].
3025/// Sends `json` to every SSE stream of `session_ids`; returns how many took it.
3026fn send_sse_to_sessions(clients: &Arc<Mutex<Vec<SseClient>>>, session_ids: &[String], json: &str) -> usize {
3027    let frame = format!("data: {json}\n\n").into_bytes();
3028    let mut clients = clients.lock().unwrap();
3029    let mut delivered = 0;
3030    clients.retain(|c| {
3031        let is_target = c.session_id.as_deref().is_some_and(|sid| session_ids.iter().any(|s| s == sid));
3032        if is_target {
3033            let sent = c.sender.try_send(frame.clone()).is_ok();
3034            delivered += sent as usize;
3035            sent
3036        } else {
3037            true
3038        }
3039    });
3040    delivered
3041}
3042
3043/// How often a server-to-client request checks for the session's SSE stream
3044/// while waiting for the client to open it.
3045const SSE_STREAM_WAIT_POLL: Duration = Duration::from_millis(20);
3046
3047// ── SSE channel reader ────────────────────────────────────────────────────────
3048
3049/// Adapts an `mpsc::Receiver<Vec<u8>>` of pre-framed SSE bytes into a
3050/// blocking [`std::io::Read`], so `Server::pipe_stream` (already written for
3051/// proxy passthrough streaming) can drive a `GET /mcp` SSE connection with no
3052/// changes to the server's write loop.
3053///
3054/// Blocks in [`Self::read`] until either a frame arrives, the sender side is
3055/// dropped (all `McpServer` clones gone — EOF, closing the connection), or
3056/// [`SSE_KEEPALIVE_INTERVAL`] elapses with nothing to send (writes a `:
3057/// keep-alive` comment instead, both to satisfy proxies that time out
3058/// silent connections and to surface a dead peer on the next write attempt).
3059struct SseChannelReader {
3060    rx: mpsc::Receiver<Vec<u8>>,
3061    leftover: Vec<u8>,
3062}
3063
3064impl SseChannelReader {
3065    fn new(rx: mpsc::Receiver<Vec<u8>>) -> Self {
3066        SseChannelReader { rx, leftover: Vec::new() }
3067    }
3068}
3069
3070impl std::io::Read for SseChannelReader {
3071    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
3072        if self.leftover.is_empty() {
3073            loop {
3074                match self.rx.recv_timeout(SSE_KEEPALIVE_INTERVAL) {
3075                    Ok(frame) => { self.leftover = frame; break; }
3076                    Err(mpsc::RecvTimeoutError::Timeout) => {
3077                        self.leftover = b": keep-alive\n\n".to_vec();
3078                        break;
3079                    }
3080                    Err(mpsc::RecvTimeoutError::Disconnected) => return Ok(0),
3081                }
3082            }
3083        }
3084
3085        let n = self.leftover.len().min(buf.len());
3086        buf[..n].copy_from_slice(&self.leftover[..n]);
3087        self.leftover.drain(..n);
3088        Ok(n)
3089    }
3090}
3091
3092// ── Application ───────────────────────────────────────────────────────────────
3093
3094impl Application for McpServer {
3095    fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
3096        // RFC 9728 protected-resource metadata (MCP 2025-06-18), at the
3097        // well-known path and at it suffixed with the endpoint's path.
3098        #[cfg(feature = "sso")]
3099        if request.method == "GET" {
3100            if let Some(oauth) = &self.oauth {
3101                let target = request.request_uri.split('?').next().unwrap_or("");
3102                let suffixed = format!("{}{}", PROTECTED_RESOURCE_PATH, self.path);
3103                if target == PROTECTED_RESOURCE_PATH || target == suffixed {
3104                    return Ok(protected_resource_document(oauth, request, &self.path));
3105                }
3106            }
3107        }
3108
3109        if request.request_uri == self.path {
3110            // Browser requests (they carry `Origin`) only from listed origins.
3111            if let Some(origin) = request.headers.iter().find(|h| h.name.eq_ignore_ascii_case("origin")) {
3112                let origin = origin.value.trim();
3113                if !self.allowed_origins.iter().any(|o| o.trim_end_matches('/').eq_ignore_ascii_case(origin.trim_end_matches('/'))) {
3114                    return Ok(forbidden_origin());
3115                }
3116            }
3117            // JSON-RPC bodies only: a form or text/plain POST is what a web
3118            // page can send cross-site without a CORS preflight.
3119            if request.method == "POST" {
3120                let content_type = request.headers.iter()
3121                    .find(|h| h.name.eq_ignore_ascii_case("content-type"))
3122                    .map(|h| h.value.as_ref())
3123                    .unwrap_or("");
3124                let media_type = content_type.split(';').next().unwrap_or("").trim();
3125                if !media_type.eq_ignore_ascii_case("application/json") {
3126                    return Ok(unsupported_media_type());
3127                }
3128            }
3129
3130            #[cfg_attr(not(feature = "sso"), allow(unused_mut))]
3131            let mut auth_claims_json: Option<String> = None;
3132            #[cfg(feature = "sso")]
3133            let oauth_configured = self.oauth.is_some();
3134            #[cfg(not(feature = "sso"))]
3135            let oauth_configured = false;
3136
3137            // OAuth (sso feature) takes precedence over bearer tokens if both
3138            // are somehow configured — see require_oauth's doc comment.
3139            #[cfg(feature = "sso")]
3140            if let Some(oauth) = &self.oauth {
3141                let provided = request.headers.iter()
3142                    .find(|h| h.name.eq_ignore_ascii_case("authorization"))
3143                    .map(|h| h.value.as_ref())
3144                    .unwrap_or("");
3145                let bearer = provided.strip_prefix("Bearer ").unwrap_or("");
3146                if bearer.is_empty() {
3147                    return Ok(unauthorized_with_metadata(oauth, request, &self.path));
3148                }
3149                let opts = crate::sso::jwks::VerifyOptions {
3150                    audience: &oauth.audience,
3151                    issuer: &oauth.provider.issuer,
3152                    leeway_secs: 60,
3153                };
3154                match oauth.jwks.verify_jwt(bearer, &opts) {
3155                    Ok(claims) => auth_claims_json = serde_json::to_string(&claims).ok(),
3156                    Err(_) => return Ok(unauthorized_with_metadata(oauth, request, &self.path)),
3157                }
3158            }
3159            if !oauth_configured {
3160                match self.check_bearer(request) {
3161                    Ok(claims) => auth_claims_json = claims,
3162                    Err(response) => return Ok(response),
3163                }
3164            }
3165
3166            // A session id this server doesn't know — never minted, ended,
3167            // or evicted — is 404: the client must initialize again
3168            // (Streamable HTTP transport). An `initialize` POST may carry a
3169            // stale one.
3170            let session_id = request.headers.iter()
3171                .find(|h| h.name.eq_ignore_ascii_case("mcp-session-id"))
3172                .map(|h| h.value.to_string());
3173            if let Some(sid) = &session_id {
3174                let initializing = request.method == "POST"
3175                    && json_rpc::extract_str(std::str::from_utf8(&request.body).unwrap_or(""), "method").as_deref() == Some("initialize");
3176                if request.method != "OPTIONS" && !initializing && !self.touch_session(sid) {
3177                    return Ok(session_not_found());
3178                }
3179            }
3180            // The protocol version in effect: the header (2025-06-18+
3181            // clients send it on every request after initialize), else the
3182            // session's, else 2025-03-26. A version this server doesn't speak
3183            // is 400 (Streamable HTTP transport).
3184            let header_version = request.headers.iter()
3185                .find(|h| h.name.eq_ignore_ascii_case("mcp-protocol-version"))
3186                .map(|h| h.value.trim().to_string());
3187            if let Some(v) = &header_version {
3188                if !SUPPORTED_PROTOCOL_VERSIONS.contains(&v.as_str()) {
3189                    return Ok(bad_protocol_version(v));
3190                }
3191            }
3192            if request.method == "POST" && request.body.iter().find(|b| !b.is_ascii_whitespace()) == Some(&b'[') {
3193                let version = header_version.clone()
3194                    .or_else(|| session_id.as_ref().and_then(|sid| self.sessions.lock().unwrap().get(sid).map(|s| s.protocol_version.to_string())))
3195                    .unwrap_or_else(|| ASSUMED_PROTOCOL_VERSION.to_string());
3196                // JSON-RPC batching was removed in 2025-06-18.
3197                if version.as_str() >= "2025-06-18" {
3198                    return Ok(rpc_error(None, json_rpc::INVALID_REQUEST,
3199                        &format!("JSON-RPC batches are not supported in protocol version {version}")));
3200                }
3201            }
3202            return Ok(match request.method.as_str() {
3203                "DELETE" => match session_id {
3204                    // The client ends its session.
3205                    Some(sid) => {
3206                        self.sessions.lock().unwrap().remove(&sid);
3207                        self.forget_sessions(&[sid]);
3208                        let mut r = Response::new();
3209                        r.status_code = *STATUS_CODE_REASON_PHRASE.n204_no_content.status_code;
3210                        r.reason_phrase = STATUS_CODE_REASON_PHRASE.n204_no_content.reason_phrase.to_string();
3211                        r
3212                    }
3213                    None => {
3214                        let mut r = Response::new();
3215                        r.status_code = *STATUS_CODE_REASON_PHRASE.n400_bad_request.status_code;
3216                        r.reason_phrase = STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase.to_string();
3217                        r.content_range_list = vec![Range::get_content_range(
3218                            b"DELETE /mcp needs an Mcp-Session-Id".to_vec(),
3219                            MimeType::TEXT_PLAIN.to_string(),
3220                        )];
3221                        r
3222                    }
3223                },
3224                "POST" => {
3225                    let body = std::str::from_utf8(&request.body).unwrap_or("");
3226                    let mut ctx = self.context_for(request);
3227                    ctx.auth_claims = auth_claims_json;
3228                    if wants_progress_stream(request, body) {
3229                        self.stream_tool_call(body, ctx)
3230                    } else {
3231                        self.handle_request_with_context(body, ctx)
3232                    }
3233                }
3234                "GET" => self.start_sse_stream(request),
3235                "OPTIONS" => {
3236                    // CORS preflight for browser-based MCP clients
3237                    let mut r = Response::new();
3238                    r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
3239                    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
3240                    r.headers.push(Header {
3241                        name: "Allow".into(),
3242                        value: "GET, POST, DELETE, OPTIONS".into(),
3243                    });
3244                    r
3245                }
3246                _ => {
3247                    let mut r = Response::new();
3248                    r.status_code = *STATUS_CODE_REASON_PHRASE.n405_method_not_allowed.status_code;
3249                    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n405_method_not_allowed.reason_phrase.to_string();
3250                    r.headers.push(Header {
3251                        name: "Allow".into(),
3252                        value: "GET, POST, DELETE, OPTIONS".into(),
3253                    });
3254                    r.content_range_list = vec![Range::get_content_range(
3255                        b"MCP endpoint only accepts GET (SSE), POST or DELETE".to_vec(),
3256                        MimeType::TEXT_PLAIN.to_string(),
3257                    )];
3258                    r
3259                }
3260            });
3261        }
3262
3263        // Not an MCP path — fall through to the wrapped app (or built-in App).
3264        match &self.fallback {
3265            Some(app) => app.execute(request, connection),
3266            None      => App::new().execute(request, connection),
3267        }
3268    }
3269
3270    /// MCP calls run tool handlers (user code): never inline. Other requests
3271    /// go to the fallback app, which decides.
3272    fn runs_inline(&self, request: &Request, connection: &ConnectionInfo) -> bool {
3273        if request.request_uri == self.path || request.request_uri.starts_with("/.well-known/") {
3274            return false;
3275        }
3276        match &self.fallback {
3277            Some(app) => app.runs_inline(request, connection),
3278            None => App::is_inline_request(request),
3279        }
3280    }
3281
3282    /// MCP requests are JSON-RPC read whole; other paths: the wrapped app.
3283    fn streams_body(&self, request: &Request, connection: &ConnectionInfo) -> bool {
3284        if request.request_uri == self.path || request.request_uri.starts_with("/.well-known/") {
3285            return false;
3286        }
3287        self.fallback.as_ref().is_some_and(|app| app.streams_body(request, connection))
3288    }
3289
3290    #[cfg(feature = "http2")]
3291    fn execute_async<'a>(&'a self, request: &'a Request, connection: &'a ConnectionInfo) -> Option<crate::application::AsyncResponse<'a>> {
3292        if request.request_uri == self.path || request.request_uri.starts_with("/.well-known/") {
3293            return None;
3294        }
3295        self.fallback.as_ref()?.execute_async(request, connection)
3296    }
3297}
3298
3299// ── public helper ─────────────────────────────────────────────────────────────
3300
3301/// Extract an argument from a tool/prompt `arguments` JSON object: a
3302/// string's value, or a number's or boolean's literal text (`"3"`,
3303/// `"true"`). Objects, arrays and `null` are `None`. Numbers and booleans
3304/// used to be `None` too, so a tool reading `"enabled": true` saw nothing.
3305///
3306/// ```rust
3307/// use rust_web_server::mcp::extract_arg;
3308/// assert_eq!(extract_arg(r#"{"text":"hello"}"#, "text").as_deref(), Some("hello"));
3309/// assert_eq!(extract_arg(r#"{"enabled":true,"n":3}"#, "enabled").as_deref(), Some("true"));
3310/// assert_eq!(extract_arg(r#"{"enabled":true,"n":3}"#, "n").as_deref(), Some("3"));
3311/// assert_eq!(extract_arg(r#"{}"#, "missing"), None);
3312/// ```
3313pub fn extract_arg(arguments: &str, name: &str) -> Option<String> {
3314    if let Some(members) = json_rpc::object_members(arguments) {
3315        let (_, raw) = members.into_iter().find(|(key, _)| key == name)?;
3316        return match raw.as_bytes().first() {
3317            Some(b'"') => json_rpc::extract_str(&format!(r#"{{"v":{raw}}}"#), "v"),
3318            Some(b'{' | b'[') | None => None,
3319            _ if raw == "null" => None,
3320            _ => Some(raw),
3321        };
3322    }
3323    json_rpc::extract_str(arguments, name)
3324}
3325
3326// ── internal helpers ──────────────────────────────────────────────────────────
3327
3328fn json_response(body: &str) -> Response {
3329    let mut r = Response::new();
3330    r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
3331    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
3332    r.content_range_list = vec![Range::get_content_range(
3333        body.as_bytes().to_vec(),
3334        MimeType::APPLICATION_JSON.to_string(),
3335    )];
3336    r
3337}
3338
3339fn no_content() -> Response {
3340    let mut r = Response::new();
3341    r.status_code = *STATUS_CODE_REASON_PHRASE.n202_accepted.status_code;
3342    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n202_accepted.reason_phrase.to_string();
3343    r
3344}
3345
3346/// A configured static token against the presented one: never matches an
3347/// empty token (fails closed), compared in constant time.
3348fn bearer_matches(expected: &str, provided: &str) -> bool {
3349    !expected.is_empty() && crate::proxy_config::constant_time_eq(expected.as_bytes(), provided.as_bytes())
3350}
3351
3352/// `403` for a browser `Origin` not in [`McpServer::allowed_origins`].
3353fn forbidden_origin() -> Response {
3354    let mut r = Response::new();
3355    r.status_code = *STATUS_CODE_REASON_PHRASE.n403_forbidden.status_code;
3356    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n403_forbidden.reason_phrase.to_string();
3357    r.content_range_list = vec![Range::get_content_range(
3358        b"Origin not allowed".to_vec(),
3359        MimeType::TEXT_PLAIN.to_string(),
3360    )];
3361    r
3362}
3363
3364/// `415` for a POST whose body isn't `application/json`.
3365fn unsupported_media_type() -> Response {
3366    let mut r = Response::new();
3367    r.status_code = *STATUS_CODE_REASON_PHRASE.n415_unsupported_media_type.status_code;
3368    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n415_unsupported_media_type.reason_phrase.to_string();
3369    r.content_range_list = vec![Range::get_content_range(
3370        b"MCP requests must be application/json".to_vec(),
3371        MimeType::TEXT_PLAIN.to_string(),
3372    )];
3373    r
3374}
3375
3376/// `400` for an `MCP-Protocol-Version` this server doesn't speak.
3377fn bad_protocol_version(version: &str) -> Response {
3378    let mut r = Response::new();
3379    r.status_code = *STATUS_CODE_REASON_PHRASE.n400_bad_request.status_code;
3380    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n400_bad_request.reason_phrase.to_string();
3381    r.content_range_list = vec![Range::get_content_range(
3382        format!("Unsupported MCP-Protocol-Version {:?}; supported: {}", version, SUPPORTED_PROTOCOL_VERSIONS.join(", ")).into_bytes(),
3383        MimeType::TEXT_PLAIN.to_string(),
3384    )];
3385    r
3386}
3387
3388/// `404` for a request in a session this server doesn't know (anymore).
3389fn session_not_found() -> Response {
3390    let mut r = Response::new();
3391    r.status_code = *STATUS_CODE_REASON_PHRASE.n404_not_found.status_code;
3392    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n404_not_found.reason_phrase.to_string();
3393    r.content_range_list = vec![Range::get_content_range(
3394        br#"{"jsonrpc":"2.0","error":{"code":-32001,"message":"Session not found: initialize a new one"},"id":null}"#.to_vec(),
3395        MimeType::APPLICATION_JSON.to_string(),
3396    )];
3397    r
3398}
3399
3400fn unauthorized() -> Response {
3401    let mut r = Response::new();
3402    r.status_code = *STATUS_CODE_REASON_PHRASE.n401_unauthorized.status_code;
3403    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n401_unauthorized.reason_phrase.to_string();
3404    r.headers.push(Header {
3405        name: "WWW-Authenticate".into(),
3406        value: "Bearer".into(),
3407    });
3408    r.content_range_list = vec![Range::get_content_range(
3409        b"Unauthorized".to_vec(),
3410        MimeType::TEXT_PLAIN.to_string(),
3411    )];
3412    r
3413}
3414
3415/// RFC 9728's well-known path for protected-resource metadata.
3416#[cfg(feature = "sso")]
3417const PROTECTED_RESOURCE_PATH: &str = "/.well-known/oauth-protected-resource";
3418
3419/// The protected resource's identifier: the configured audience when it is
3420/// an absolute URL (RFC 8707 — the value tokens are minted for), else this
3421/// server's origin from the request plus the MCP path.
3422#[cfg(feature = "sso")]
3423fn resource_identifier(oauth: &OAuthConfig, request: &Request, path: &str) -> String {
3424    if oauth.audience.starts_with("https://") || oauth.audience.starts_with("http://") {
3425        return oauth.audience.clone();
3426    }
3427    format!("{}{}", request_origin(request), path)
3428}
3429
3430/// `scheme://host` this request was sent to (`Host`; `https` unless the
3431/// host is a loopback name).
3432#[cfg(feature = "sso")]
3433fn request_origin(request: &Request) -> String {
3434    let host = request.headers.iter()
3435        .find(|h| h.name.eq_ignore_ascii_case("host"))
3436        .map(|h| h.value.to_string())
3437        .unwrap_or_else(|| "localhost".to_string());
3438    let local = host.starts_with("localhost") || host.starts_with("127.") || host.starts_with("[::1]");
3439    format!("{}://{}", if local { "http" } else { "https" }, host)
3440}
3441
3442/// The metadata document's URL, for `WWW-Authenticate`.
3443#[cfg(feature = "sso")]
3444fn metadata_url(oauth: &OAuthConfig, request: &Request, path: &str) -> String {
3445    let resource = resource_identifier(oauth, request, path);
3446    let origin = match resource.find("://").and_then(|i| resource[i + 3..].find('/').map(|j| i + 3 + j)) {
3447        Some(end) => resource[..end].to_string(),
3448        None => resource,
3449    };
3450    format!("{origin}{PROTECTED_RESOURCE_PATH}")
3451}
3452
3453/// `GET /.well-known/oauth-protected-resource` (RFC 9728): which
3454/// authorization server issues tokens for this MCP server, and the resource
3455/// identifier a client asks it for (RFC 8707). Served whenever
3456/// [`McpServer::require_oauth`] is configured; it replaced the
3457/// authorization-server-style document earlier versions served.
3458#[cfg(feature = "sso")]
3459fn protected_resource_document(oauth: &OAuthConfig, request: &Request, path: &str) -> Response {
3460    let body = format!(
3461        r#"{{"resource":"{}","authorization_servers":["{}"],"bearer_methods_supported":["header"]}}"#,
3462        json_escape(&resource_identifier(oauth, request, path)),
3463        json_escape(&oauth.provider.issuer),
3464    );
3465    let mut r = Response::new();
3466    r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
3467    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
3468    r.content_range_list = vec![Range::get_content_range(
3469        body.into_bytes(),
3470        MimeType::APPLICATION_JSON.to_string(),
3471    )];
3472    r
3473}
3474
3475/// `401` whose `WWW-Authenticate` points at the protected-resource
3476/// metadata, so a client can discover the authorization server.
3477#[cfg(feature = "sso")]
3478fn unauthorized_with_metadata(oauth: &OAuthConfig, request: &Request, path: &str) -> Response {
3479    let mut r = unauthorized();
3480    if let Some(header) = r.headers.iter_mut().find(|h| h.name.eq_ignore_ascii_case("www-authenticate")) {
3481        header.value = format!(r#"Bearer resource_metadata="{}""#, metadata_url(oauth, request, path)).into();
3482    }
3483    r
3484}
3485
3486fn rpc_error(id: Option<&str>, code: i32, message: &str) -> Response {
3487    let id_str  = id.unwrap_or("null");
3488    let escaped = json_escape(message);
3489    json_response(&format!(
3490        r#"{{"jsonrpc":"2.0","error":{{"code":{code},"message":"{escaped}"}},"id":{id_str}}}"#
3491    ))
3492}
3493
3494/// The JSON Schema type of a raw JSON value (`integer` for whole numbers).
3495fn json_type(raw: &str) -> &'static str {
3496    let raw = raw.trim();
3497    match raw.as_bytes().first() {
3498        Some(b'"') => "string",
3499        Some(b'{') => "object",
3500        Some(b'[') => "array",
3501        Some(b't' | b'f') => "boolean",
3502        Some(b'n') => "null",
3503        _ if raw.bytes().all(|b| b.is_ascii_digit() || b == b'-') => "integer",
3504        _ => "number",
3505    }
3506}
3507
3508pub(crate) fn json_escape(s: &str) -> String {
3509    let mut out = String::with_capacity(s.len() + 4);
3510    for ch in s.chars() {
3511        match ch {
3512            '"'  => out.push_str("\\\""),
3513            '\\' => out.push_str("\\\\"),
3514            '\n' => out.push_str("\\n"),
3515            '\r' => out.push_str("\\r"),
3516            '\t' => out.push_str("\\t"),
3517            c if (c as u32) < 0x20 => { let _ = std::fmt::Write::write_fmt(&mut out, format_args!("\\u{:04x}", c as u32)); }
3518            c    => out.push(c),
3519        }
3520    }
3521    out
3522}
3523
3524// ── pagination cursors ─────────────────────────────────────────────────────────
3525
3526/// Render `,"nextCursor":"..."` for a `*/list` response, or `""` if there's
3527/// no next page — spliced directly after the closing `]` of the items array.
3528fn next_cursor_json(next_cursor: &Option<String>) -> String {
3529    match next_cursor {
3530        Some(c) => format!(r#","nextCursor":"{}""#, json_escape(c)),
3531        None => String::new(),
3532    }
3533}
3534
3535/// Encode a `tools/list`/`resources/list`/`prompts/list` offset as the
3536/// opaque `nextCursor`/`params.cursor` string the MCP spec expects — just
3537/// base64 of the decimal offset, e.g. `50` → `"NTA="`. Callers only ever
3538/// treat this as opaque; the encoding is a private implementation detail of
3539/// this module, not a client-facing contract.
3540fn encode_cursor(offset: usize) -> String {
3541    base64_encode(offset.to_string().as_bytes())
3542}
3543
3544/// Decode a cursor produced by [`encode_cursor`]. Returns `None` for
3545/// anything that isn't valid base64 of a decimal `usize` — a malformed or
3546/// tampered cursor, not a crash.
3547fn decode_cursor(cursor: &str) -> Option<usize> {
3548    String::from_utf8(base64_decode(cursor)?).ok()?.parse().ok()
3549}
3550
3551const BASE64_TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
3552
3553fn base64_encode(data: &[u8]) -> String {
3554    let mut out = String::with_capacity((data.len() + 2) / 3 * 4);
3555    for chunk in data.chunks(3) {
3556        let b0 = chunk[0] as u32;
3557        let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
3558        let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
3559        let n = (b0 << 16) | (b1 << 8) | b2;
3560        out.push(BASE64_TABLE[((n >> 18) & 0x3F) as usize] as char);
3561        out.push(BASE64_TABLE[((n >> 12) & 0x3F) as usize] as char);
3562        out.push(if chunk.len() > 1 { BASE64_TABLE[((n >> 6) & 0x3F) as usize] as char } else { '=' });
3563        out.push(if chunk.len() > 2 { BASE64_TABLE[(n & 0x3F) as usize] as char } else { '=' });
3564    }
3565    out
3566}
3567
3568fn base64_decode(s: &str) -> Option<Vec<u8>> {
3569    fn sextet(c: u8) -> Option<u32> {
3570        match c {
3571            b'A'..=b'Z' => Some((c - b'A') as u32),
3572            b'a'..=b'z' => Some((c - b'a' + 26) as u32),
3573            b'0'..=b'9' => Some((c - b'0' + 52) as u32),
3574            b'+' => Some(62),
3575            b'/' => Some(63),
3576            _ => None,
3577        }
3578    }
3579
3580    let trimmed = s.trim_end_matches('=');
3581    let bytes = trimmed.as_bytes();
3582    let mut out = Vec::with_capacity(bytes.len() * 3 / 4 + 3);
3583    for chunk in bytes.chunks(4) {
3584        if chunk.len() == 1 {
3585            return None; // not a valid base64 length
3586        }
3587        let vals: Vec<u32> = chunk.iter().map(|&b| sextet(b)).collect::<Option<Vec<_>>>()?;
3588        let n = vals.iter().enumerate().fold(0u32, |acc, (i, &v)| acc | (v << (18 - 6 * i)));
3589        out.push(((n >> 16) & 0xFF) as u8);
3590        if vals.len() > 2 { out.push(((n >> 8) & 0xFF) as u8); }
3591        if vals.len() > 3 { out.push((n & 0xFF) as u8); }
3592    }
3593    Some(out)
3594}
3595
3596fn uri_matches(template: &str, uri: &str) -> bool {
3597    // Template `"user://{id}"` matches any URI starting with `"user://"`.
3598    match template.find('{') {
3599        Some(pos) => uri.starts_with(&template[..pos]),
3600        None      => template == uri,
3601    }
3602}