Skip to main content

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