Skip to main content

rust_web_server/mcp/
mod.rs

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