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;
95use std::sync::mpsc::{self, SyncSender};
96use std::sync::{Arc, Mutex, RwLock};
97use std::time::Duration;
98
99use crate::app::App;
100use crate::application::Application;
101use crate::core::New;
102use crate::header::Header;
103use crate::mime_type::MimeType;
104use crate::range::Range;
105use crate::request::Request;
106use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
107use crate::server::ConnectionInfo;
108
109const PROTOCOL_VERSION: &str = "2024-11-05";
110
111// ── public content types ──────────────────────────────────────────────────────
112
113/// Content returned by tool and resource handlers.
114///
115/// Create with [`McpContent::text`] (plain text or JSON strings),
116/// [`McpContent::json`] (marks MIME type as `application/json`),
117/// [`McpContent::image`] (base64-encoded binary image data), or
118/// [`McpContent::embedded`] (a resource embedded inline in a tool response).
119#[derive(Clone, Debug)]
120pub struct McpContent {
121    /// `"text"`, `"image"`, or `"resource"`.
122    pub kind: &'static str,
123    /// The content string — text for `"text"`, base64 data for `"image"`,
124    /// or the embedded resource's text for `"resource"`.
125    pub text: String,
126    /// Optional MIME type override (default `"text/plain"` for `"text"`;
127    /// required in practice for `"image"`/`"resource"`, set by their
128    /// constructors).
129    pub mime_type: Option<String>,
130    /// The resource URI — only set (and only serialized) for `"resource"`.
131    pub uri: Option<String>,
132}
133
134impl McpContent {
135    /// Plain-text content.
136    pub fn text(s: impl Into<String>) -> Self {
137        McpContent { kind: "text", text: s.into(), mime_type: None, uri: None }
138    }
139
140    /// JSON content — sets `mimeType` to `application/json`.
141    pub fn json(s: impl Into<String>) -> Self {
142        McpContent { kind: "text", text: s.into(), mime_type: Some("application/json".to_string()), uri: None }
143    }
144
145    /// Image content (screenshot, chart, generated art) — `data` is base64-encoded
146    /// binary and `mime_type` is e.g. `"image/png"`.
147    pub fn image(data: impl Into<String>, mime_type: impl Into<String>) -> Self {
148        McpContent { kind: "image", text: data.into(), mime_type: Some(mime_type.into()), uri: None }
149    }
150
151    /// A resource embedded inline in a tool response, as opposed to one a
152    /// client fetches separately via `resources/read`.
153    pub fn embedded(uri: impl Into<String>, text: impl Into<String>, mime_type: impl Into<String>) -> Self {
154        McpContent { kind: "resource", text: text.into(), mime_type: Some(mime_type.into()), uri: Some(uri.into()) }
155    }
156
157    fn to_content_json(&self) -> String {
158        match self.kind {
159            "image" => format!(
160                r#"{{"type":"image","data":"{}","mimeType":"{}"}}"#,
161                json_escape(&self.text),
162                json_escape(self.mime_type.as_deref().unwrap_or("application/octet-stream")),
163            ),
164            "resource" => format!(
165                r#"{{"type":"resource","resource":{{"uri":"{}","mimeType":"{}","text":"{}"}}}}"#,
166                json_escape(self.uri.as_deref().unwrap_or("")),
167                json_escape(self.mime_type.as_deref().unwrap_or("text/plain")),
168                json_escape(&self.text),
169            ),
170            _ => format!(r#"{{"type":"text","text":"{}"}}"#, json_escape(&self.text)),
171        }
172    }
173
174    fn mime(&self) -> &str {
175        self.mime_type.as_deref().unwrap_or("text/plain")
176    }
177}
178
179/// A single message in a prompt response.
180#[derive(Clone, Debug)]
181pub struct PromptMessage {
182    /// `"user"` or `"assistant"`.
183    pub role: &'static str,
184    /// The message content.
185    pub content: McpContent,
186}
187
188impl PromptMessage {
189    /// Build a user-role message.
190    pub fn user(text: impl Into<String>) -> Self {
191        PromptMessage { role: "user", content: McpContent::text(text) }
192    }
193
194    /// Build an assistant-role message.
195    pub fn assistant(text: impl Into<String>) -> Self {
196        PromptMessage { role: "assistant", content: McpContent::text(text) }
197    }
198
199    fn to_json(&self) -> String {
200        format!(
201            r#"{{"role":"{}","content":{}}}"#,
202            self.role,
203            self.content.to_content_json(),
204        )
205    }
206}
207
208/// Argument definition for a prompt template.
209#[derive(Clone)]
210pub struct PromptArgDef {
211    pub name: String,
212    pub description: String,
213    pub required: bool,
214}
215
216impl PromptArgDef {
217    pub fn required(name: impl Into<String>, description: impl Into<String>) -> Self {
218        PromptArgDef { name: name.into(), description: description.into(), required: true }
219    }
220
221    pub fn optional(name: impl Into<String>, description: impl Into<String>) -> Self {
222        PromptArgDef { name: name.into(), description: description.into(), required: false }
223    }
224}
225
226// ── McpContext ────────────────────────────────────────────────────────────────
227
228/// Per-request context passed to tool handlers registered via
229/// [`McpServer::tool_with_context`] — caller identity and session info that a
230/// plain `Fn(&str) -> ...` tool handler has no way to see.
231///
232/// Constructed in [`McpServer::execute`] from the current request's headers
233/// plus whatever `clientInfo` was recorded for this session at `initialize`
234/// time (see [`McpServer::handle_request_with_context`]).
235#[derive(Debug, Clone, Default)]
236pub struct McpContext {
237    /// `clientInfo.name` sent in this session's `initialize` call, if the
238    /// client sent one and this request carries a recognized `Mcp-Session-Id`.
239    pub client_name: Option<String>,
240    /// `clientInfo.version` sent in this session's `initialize` call, under
241    /// the same conditions as `client_name`.
242    pub client_version: Option<String>,
243    /// The `Mcp-Session-Id` header on this request, if present — the value
244    /// the server minted and returned in the `initialize` response header
245    /// for this session (see the module docs' Sessions section).
246    pub session_id: Option<String>,
247    /// Verified JWT claims as a JSON string. Not populated by anything in
248    /// this crate yet — reserved for a future JWT-auth integration
249    /// (MCP_TODO.md TODO-11/TODO-13); always `None` today.
250    pub auth_claims: Option<String>,
251    /// The raw JSON value of `params._meta.progressToken` from the
252    /// triggering `tools/call` request, if the client sent one — a spec
253    /// `string | number`, so this is stored pre-rendered (already correctly
254    /// quoted if it's a string) rather than decoded, and spliced back
255    /// verbatim by [`Self::report_progress`]. `None` for anything other than
256    /// a `tools/call` whose caller asked for progress updates.
257    pub progress_token: Option<String>,
258    /// Shared handle back to the owning [`McpServer`]'s SSE broadcast list,
259    /// used by [`Self::report_progress`]. Not `pub` — this is plumbing, not
260    /// part of the context data a handler reads. `None` for a context built
261    /// by hand (e.g. via [`McpServer::handle_request_with_context`] in a
262    /// test) rather than through [`McpServer::execute`], in which case
263    /// `report_progress` silently no-ops — there's no live server to
264    /// broadcast through.
265    sse_clients: Option<Arc<Mutex<Vec<SseClient>>>>,
266    /// Shared flag flipped by a `notifications/cancelled` referencing this
267    /// `tools/call`'s request id, checked by [`Self::is_cancelled`]. Not
268    /// `pub` — see that method. `None` for anything other than a live
269    /// `tools/call` dispatched through [`McpServer::execute`]/`handle_request_with_context`.
270    cancellation: Option<Arc<std::sync::atomic::AtomicBool>>,
271}
272
273impl McpContext {
274    /// Push a `notifications/progress` event over the SSE channel for this
275    /// request's `progressToken`, if the client asked for progress updates
276    /// (`params._meta.progressToken` on the triggering `tools/call`) and
277    /// this context was built through a live [`McpServer`] (via `execute()`,
278    /// not a bare `McpContext { .. }` — see the `sse_clients` field doc).
279    ///
280    /// Silently does nothing in either case — a handler doesn't need to
281    /// branch on whether progress reporting is actually wired up before
282    /// calling this; it's always safe to call.
283    ///
284    /// `total` and `message` are both optional, matching the spec's
285    /// `notifications/progress` shape: `{"progressToken":...,"progress":...,
286    /// "total":...,"message":"..."}` (with `total`/`message` omitted when not
287    /// given here).
288    ///
289    /// ```rust,no_run
290    /// use rust_web_server::mcp::{McpContent, McpServer};
291    ///
292    /// let server = McpServer::new("my-server", "1.0")
293    ///     .tool_with_context("long_job", "Do something slow", "{}", |ctx, _args| {
294    ///         ctx.report_progress(0.0, Some(100.0), Some("starting"));
295    ///         // ... do work ...
296    ///         ctx.report_progress(100.0, Some(100.0), Some("done"));
297    ///         Ok(McpContent::text("done"))
298    ///     });
299    /// ```
300    pub fn report_progress(&self, progress: f64, total: Option<f64>, message: Option<&str>) {
301        let (Some(token), Some(sse_clients)) = (&self.progress_token, &self.sse_clients) else {
302            return;
303        };
304
305        let total_field = match total {
306            Some(t) => format!(r#","total":{t}"#),
307            None => String::new(),
308        };
309        let message_field = match message {
310            Some(m) => format!(r#","message":"{}""#, json_escape(m)),
311            None => String::new(),
312        };
313        let params = format!(
314            r#"{{"progressToken":{token},"progress":{progress}{total_field}{message_field}}}"#
315        );
316        let json = render_notification("notifications/progress", Some(&params));
317        broadcast_sse_to(sse_clients, &json);
318    }
319
320    /// `true` if the client sent `notifications/cancelled` referencing this
321    /// `tools/call`'s request id.
322    ///
323    /// Rust cannot forcibly interrupt a running synchronous closure — same
324    /// limitation [`crate::timeout::with_timeout`] documents — so this is
325    /// *cooperative* cancellation: nothing happens automatically. A handler
326    /// that does its own iterative work (processing a batch of items, say)
327    /// can check this between steps and return early; a handler that never
328    /// checks it runs to completion regardless, exactly as before this
329    /// existed.
330    ///
331    /// Always `false` if the client never sent a cancellation, if this
332    /// wasn't dispatched as a `tools/call` (the only method cancellation
333    /// applies to), or if `ctx` was built without a live server behind it.
334    ///
335    /// ```rust,no_run
336    /// use rust_web_server::mcp::{McpContent, McpServer};
337    ///
338    /// let server = McpServer::new("my-server", "1.0")
339    ///     .tool_with_context("process_batch", "Process many items", "{}", |ctx, _args| {
340    ///         for i in 0..1_000_000 {
341    ///             if ctx.is_cancelled() {
342    ///                 return Err("cancelled by client".to_string());
343    ///             }
344    ///             // ... process item i ...
345    ///         }
346    ///         Ok(McpContent::text("done"))
347    ///     });
348    /// ```
349    pub fn is_cancelled(&self) -> bool {
350        self.cancellation.as_ref().is_some_and(|c| c.load(std::sync::atomic::Ordering::Relaxed))
351    }
352}
353
354/// `clientInfo` recorded for one session at `initialize` time, looked up by
355/// `Mcp-Session-Id` for later requests in the same session. See
356/// `McpServer`'s `sessions` field doc comment for the unbounded-growth caveat.
357#[derive(Clone, Default)]
358struct StoredClientInfo {
359    name: Option<String>,
360    version: Option<String>,
361}
362
363// ── ToolAnnotations ───────────────────────────────────────────────────────────
364
365/// Behavioral hints for a tool, per the MCP 2025-03-26 spec's tool
366/// annotations. Clients (Claude Desktop and others) use these to decide
367/// whether to warn or ask for confirmation before calling a tool — e.g. skip
368/// confirmation for a read-only tool, or warn before a destructive one.
369///
370/// Every field is a *hint*, not something this server enforces or verifies —
371/// nothing stops a handler registered with `read_only_hint: Some(true)` from
372/// actually writing to disk. A well-behaved server sets these accurately;
373/// a client is free to ignore them or ask for confirmation anyway.
374///
375/// Register with [`McpServer::tool_annotated`]. Build one with plain struct
376/// syntax — every field defaults to `None` (no hint given, the client's own
377/// default applies):
378///
379/// ```rust
380/// use rust_web_server::mcp::ToolAnnotations;
381///
382/// let destructive = ToolAnnotations {
383///     destructive_hint: Some(true),
384///     read_only_hint: Some(false),
385///     ..Default::default()
386/// };
387/// ```
388#[derive(Debug, Clone, Copy, Default)]
389pub struct ToolAnnotations {
390    /// The tool does not modify its environment.
391    pub read_only_hint: Option<bool>,
392    /// The tool may perform destructive updates (only meaningful when
393    /// `read_only_hint` is not `Some(true)`).
394    pub destructive_hint: Option<bool>,
395    /// Calling the tool repeatedly with the same arguments has no additional
396    /// effect beyond the first call.
397    pub idempotent_hint: Option<bool>,
398    /// The tool may interact with an open-ended set of external entities
399    /// (e.g. web search), as opposed to a fixed, closed set.
400    pub open_world_hint: Option<bool>,
401}
402
403impl ToolAnnotations {
404    /// Render as a JSON object containing only the hints that are `Some`,
405    /// using the spec's camelCase key names. Returns `"{}"` if every field
406    /// is `None`.
407    fn to_json(self) -> String {
408        let mut fields = Vec::with_capacity(4);
409        if let Some(v) = self.read_only_hint {
410            fields.push(format!(r#""readOnlyHint":{v}"#));
411        }
412        if let Some(v) = self.destructive_hint {
413            fields.push(format!(r#""destructiveHint":{v}"#));
414        }
415        if let Some(v) = self.idempotent_hint {
416            fields.push(format!(r#""idempotentHint":{v}"#));
417        }
418        if let Some(v) = self.open_world_hint {
419            fields.push(format!(r#""openWorldHint":{v}"#));
420        }
421        format!("{{{}}}", fields.join(","))
422    }
423}
424
425// ── LogLevel ──────────────────────────────────────────────────────────────────
426
427/// RFC 5424 syslog severity levels, as used by the MCP `logging/setLevel`
428/// request and `notifications/message` log entries — ordered from most to
429/// least verbose so `level < min_level` comparisons work directly (this
430/// relies on declaration order matching severity order; don't reorder the
431/// variants).
432///
433/// ```rust
434/// use rust_web_server::mcp::LogLevel;
435///
436/// assert!(LogLevel::Debug < LogLevel::Warning);
437/// assert!(LogLevel::Emergency > LogLevel::Error);
438/// assert_eq!(LogLevel::parse("warning"), Some(LogLevel::Warning));
439/// assert_eq!(LogLevel::Warning.as_str(), "warning");
440/// ```
441#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
442pub enum LogLevel {
443    Debug,
444    Info,
445    Notice,
446    Warning,
447    Error,
448    Critical,
449    Alert,
450    Emergency,
451}
452
453impl LogLevel {
454    /// Parse the MCP spec's lowercase level name. Returns `None` for
455    /// anything that isn't one of the eight recognized levels.
456    pub fn parse(s: &str) -> Option<Self> {
457        match s {
458            "debug"     => Some(LogLevel::Debug),
459            "info"      => Some(LogLevel::Info),
460            "notice"    => Some(LogLevel::Notice),
461            "warning"   => Some(LogLevel::Warning),
462            "error"     => Some(LogLevel::Error),
463            "critical"  => Some(LogLevel::Critical),
464            "alert"     => Some(LogLevel::Alert),
465            "emergency" => Some(LogLevel::Emergency),
466            _           => None,
467        }
468    }
469
470    /// The MCP spec's lowercase level name, e.g. `"warning"`.
471    pub fn as_str(self) -> &'static str {
472        match self {
473            LogLevel::Debug     => "debug",
474            LogLevel::Info      => "info",
475            LogLevel::Notice    => "notice",
476            LogLevel::Warning   => "warning",
477            LogLevel::Error     => "error",
478            LogLevel::Critical  => "critical",
479            LogLevel::Alert     => "alert",
480            LogLevel::Emergency => "emergency",
481        }
482    }
483}
484
485// ── internal handler registrations ───────────────────────────────────────────
486
487type ToolFn     = Arc<dyn Fn(McpContext, &str) -> Result<McpContent, String>    + Send + Sync>;
488type ResourceFn = Arc<dyn Fn(&str) -> Result<McpContent, String>    + Send + Sync>;
489type PromptFn   = Arc<dyn Fn(&str) -> Result<Vec<PromptMessage>, String> + Send + Sync>;
490/// `Fn(argument_name, partial_value) -> candidate completion strings`.
491type CompletionFn = Arc<dyn Fn(&str, &str) -> Result<Vec<String>, String> + Send + Sync>;
492
493#[derive(Clone)]
494struct ToolDef {
495    name: String,
496    description: String,
497    input_schema: String,
498    annotations: Option<ToolAnnotations>,
499    handler: ToolFn,
500}
501
502#[derive(Clone)]
503struct ResourceDef {
504    uri_template: String,
505    name: String,
506    description: String,
507    handler: ResourceFn,
508}
509
510#[derive(Clone)]
511struct PromptDef {
512    name: String,
513    description: String,
514    arguments: Vec<PromptArgDef>,
515    handler: PromptFn,
516}
517
518/// One `.completion()` registration — completion candidates for a single
519/// named argument of a single tool or prompt. `ref_type` is the short form
520/// passed to `.completion()` (e.g. `"tool"`, `"prompt"`), matched against the
521/// request's `ref.type` (e.g. `"ref/tool"`) with the `"ref/"` prefix
522/// stripped, not the raw wire value.
523#[derive(Clone)]
524struct CompletionDef {
525    ref_type: String,
526    ref_name: String,
527    handler: CompletionFn,
528}
529
530// ── McpServer ─────────────────────────────────────────────────────────────────
531
532/// An HTTP server that implements the MCP 2024-11-05 protocol.
533///
534/// Register tools, resources, and prompts with the builder methods, then pass
535/// the server to [`Server::run`] (or [`Server::run_tls`]) as an [`Application`].
536/// Requests that do not match the MCP endpoint fall through to the built-in
537/// [`App`] controller chain.
538#[derive(Clone)]
539pub struct McpServer {
540    server_name: String,
541    server_version: String,
542    path: String,
543    /// `Arc<RwLock<_>>` (not a plain `Vec`) so a running server's tool list
544    /// can be mutated at runtime — see [`Self::register_tool`]/[`Self::remove_tool`]
545    /// — and every clone of this `McpServer` (each connection thread gets
546    /// one) sees the same live list.
547    tools: Arc<RwLock<Vec<ToolDef>>>,
548    resources: Arc<RwLock<Vec<ResourceDef>>>,
549    prompts: Arc<RwLock<Vec<PromptDef>>>,
550    /// Argument completion providers registered via [`Self::completion`].
551    /// `initialize` advertises the `completions` capability iff this is
552    /// non-empty at that moment.
553    completions: Arc<RwLock<Vec<CompletionDef>>>,
554    fallback: Option<Arc<dyn Application + Send + Sync>>,
555    auth_token: Option<String>,
556    /// Max items per page for `tools/list`/`resources/list`/`prompts/list`,
557    /// set via [`Self::page_size`]. `None` (the default) means no pagination
558    /// — every item comes back in one response, same as before pagination
559    /// existed.
560    page_size: Option<usize>,
561    /// `clientInfo` recorded per `Mcp-Session-Id`, minted at `initialize` time.
562    /// `Arc<Mutex<_>>` so every clone of this `McpServer` shares one map.
563    ///
564    /// This map only grows — nothing ever removes an entry, since there's no
565    /// session-termination signal in the MCP Streamable HTTP transport to key
566    /// eviction off of. Fine for the expected usage (a modest, roughly-stable
567    /// set of long-lived AI-agent clients); a public-internet-facing server
568    /// churning through unbounded distinct clients would leak memory here
569    /// with no built-in reaping mechanism.
570    sessions: Arc<Mutex<HashMap<String, StoredClientInfo>>>,
571    /// Senders for every currently-connected `GET /mcp` SSE client, pushed to
572    /// by [`Self::notify`]. `Arc<Mutex<_>>` so every clone of this `McpServer`
573    /// broadcasts to the same set of listeners.
574    ///
575    /// Entries for clients that disconnected (or whose buffer filled up) are
576    /// only pruned lazily, the next time [`Self::notify`] is called and its
577    /// `try_send` fails — not proactively, since nothing else observes the
578    /// underlying `Receiver` closing. A server that never calls `notify`
579    /// after clients disconnect will accumulate dead entries here.
580    sse_clients: Arc<Mutex<Vec<SseClient>>>,
581    /// Whether `initialize`'s advertised `capabilities` includes `"logging":{}`.
582    /// Set via [`Self::logging_enabled`]. This only controls what's
583    /// advertised — [`Self::log`] works regardless, same as [`Self::notify`]
584    /// does; a spec-honest client just wouldn't call `logging/setLevel` in
585    /// the first place if the capability was never advertised.
586    logging_enabled: bool,
587    /// The minimum [`LogLevel`] that [`Self::log`] will actually push,
588    /// settable at runtime by a client's `logging/setLevel` request. Starts
589    /// at [`LogLevel::Debug`] (the least restrictive level, i.e. nothing is
590    /// filtered) until a client requests otherwise.
591    min_log_level: Arc<Mutex<LogLevel>>,
592    /// In-flight `tools/call` cancellation flags, keyed by the request's raw
593    /// `id` JSON token. Populated just before dispatching a `tools/call` and
594    /// removed again once it returns (success, error, or the flag was never
595    /// checked) — unlike `sessions`/`sse_clients`, this map's entries are
596    /// always short-lived by construction, not merely "not evicted."
597    /// `notifications/cancelled` looks up `params.requestId` here and flips
598    /// the flag; see [`McpContext::is_cancelled`] for how (and whether) a
599    /// handler ever observes it.
600    cancellations: Arc<Mutex<HashMap<String, Arc<std::sync::atomic::AtomicBool>>>>,
601    /// Resource URI -> session ids subscribed to it via
602    /// `resources/subscribe`, consulted by [`Self::notify_resource_updated`].
603    /// A session id is only meaningful if the client also opened `GET /mcp`
604    /// with the same `Mcp-Session-Id` header (see [`SseClient`]) — a session
605    /// that only ever calls `resources/subscribe` over POST with no matching
606    /// SSE connection is subscribed in name only, since there's no channel
607    /// to push the update over. Entries are removed by explicit
608    /// `resources/unsubscribe` calls (URIs with zero subscribers are pruned
609    /// entirely); a session that disconnects without unsubscribing leaves a
610    /// harmless stale session id behind, same "no proactive eviction"
611    /// tradeoff already documented for `sessions`.
612    subscriptions: Arc<Mutex<HashMap<String, Vec<String>>>>,
613}
614
615/// One `GET /mcp` SSE client: its outbound channel plus the
616/// `Mcp-Session-Id` it connected with, if any. Broadcast notifications
617/// (`.notify()`, `.log()`, `list_changed`) go to every client regardless of
618/// `session_id`; [`McpServer::notify_resource_updated`] targets only
619/// clients whose `session_id` is subscribed to the changed URI.
620#[derive(Debug)]
621struct SseClient {
622    session_id: Option<String>,
623    sender: SseSender,
624}
625
626/// One `GET /mcp` SSE client's outbound channel. Bounded so a slow or stuck
627/// client can't grow memory without limit; [`McpServer::notify`] uses
628/// `try_send` (never blocks) and drops any client whose buffer is full.
629type SseSender = SyncSender<Vec<u8>>;
630
631/// Max buffered-but-unread SSE frames per client before it's treated as dead.
632const SSE_CHANNEL_CAPACITY: usize = 32;
633
634/// How often an idle SSE connection gets a `: keep-alive` comment.
635const SSE_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(15);
636
637/// Max `completion/complete` values returned in one response, per spec
638/// guidance that servers SHOULD NOT return more than 100. A handler
639/// returning more has the rest reported via `hasMore`/`total` rather than
640/// silently included.
641const MAX_COMPLETION_VALUES: usize = 100;
642
643impl McpServer {
644    /// Create a new `McpServer`.  The default MCP endpoint is `POST /mcp`.
645    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
646        McpServer {
647            server_name: name.into(),
648            server_version: version.into(),
649            path: "/mcp".to_string(),
650            tools: Arc::new(RwLock::new(Vec::new())),
651            resources: Arc::new(RwLock::new(Vec::new())),
652            prompts: Arc::new(RwLock::new(Vec::new())),
653            completions: Arc::new(RwLock::new(Vec::new())),
654            fallback: None,
655            auth_token: None,
656            page_size: None,
657            sessions: Arc::new(Mutex::new(HashMap::new())),
658            sse_clients: Arc::new(Mutex::new(Vec::new())),
659            logging_enabled: false,
660            min_log_level: Arc::new(Mutex::new(LogLevel::Debug)),
661            cancellations: Arc::new(Mutex::new(HashMap::new())),
662            subscriptions: Arc::new(Mutex::new(HashMap::new())),
663        }
664    }
665
666    /// Cap `tools/list`, `resources/list`, and `prompts/list` to at most `n`
667    /// items per response, enabling cursor-based pagination: a response with
668    /// more items remaining includes `"nextCursor"`, an opaque string the
669    /// client echoes back as `params.cursor` on its next call to get the next
670    /// page. `n` is clamped to a minimum of `1`.
671    ///
672    /// Without calling this, every registered tool/resource/prompt is
673    /// returned in a single response — the default, and the only behavior
674    /// before pagination existed.
675    ///
676    /// ```rust
677    /// use rust_web_server::mcp::McpServer;
678    ///
679    /// let server = McpServer::new("my-server", "1.0").page_size(50);
680    /// ```
681    pub fn page_size(mut self, n: usize) -> Self {
682        self.page_size = Some(n.max(1));
683        self
684    }
685
686    /// Push a JSON-RPC notification (no `id` — fire-and-forget, per the
687    /// spec) to every client currently connected to the `GET /mcp` SSE
688    /// stream, framed as an SSE `data:` event.
689    ///
690    /// `params_json`, if given, must already be a valid JSON value (usually
691    /// an object) — it's spliced in verbatim, not escaped or re-serialized.
692    ///
693    /// Never blocks: a client whose event buffer is full (not reading fast
694    /// enough) is treated the same as a disconnected one and dropped from
695    /// the broadcast list, same as `notify` would drop it anyway.
696    ///
697    /// ```rust
698    /// use rust_web_server::mcp::McpServer;
699    ///
700    /// let server = McpServer::new("my-server", "1.0");
701    /// server.notify("notifications/message", Some(r#"{"level":"info","data":"hello"}"#));
702    /// ```
703    pub fn notify(&self, method: &str, params_json: Option<&str>) {
704        let json = render_notification(method, params_json);
705        broadcast_sse_to(&self.sse_clients, &json);
706    }
707
708    /// Push `notifications/resources/updated` for `uri` to every session
709    /// that called `resources/subscribe` for it — the mechanism behind
710    /// live-updating resource panels (e.g. Claude Desktop watching a
711    /// config file or a dashboard resource for changes). Call this from
712    /// wherever your application actually changes the underlying data a
713    /// resource represents (a file watcher, a webhook handler, a database
714    /// trigger poll, ...).
715    ///
716    /// A no-op if nobody has subscribed to `uri` — including if every
717    /// subscriber's `GET /mcp` SSE connection has since disconnected
718    /// (pruned the same way [`Self::notify`] prunes dead broadcast clients,
719    /// but the `subscriptions` bookkeeping for `uri` itself is untouched
720    /// either way; only [`Self::do_resource_unsubscribe`] removes that).
721    ///
722    /// ```rust
723    /// use rust_web_server::mcp::McpServer;
724    ///
725    /// let server = McpServer::new("my-server", "1.0");
726    /// // Elsewhere, e.g. after reloading a watched config file:
727    /// server.notify_resource_updated("config://main");
728    /// ```
729    pub fn notify_resource_updated(&self, uri: &str) {
730        let session_ids = match self.subscriptions.lock().unwrap().get(uri) {
731            Some(ids) if !ids.is_empty() => ids.clone(),
732            _ => return,
733        };
734        let params = format!(r#"{{"uri":"{}"}}"#, json_escape(uri));
735        let json = render_notification("notifications/resources/updated", Some(&params));
736        send_sse_to_sessions(&self.sse_clients, &session_ids, &json);
737    }
738
739    /// Handle `GET /mcp`: register a new SSE client and return a
740    /// `text/event-stream` response that streams from its channel until the
741    /// connection closes. See the module docs' SSE section for the wire
742    /// details (keep-alive interval, backpressure behavior).
743    ///
744    /// Tags the client with this request's `Mcp-Session-Id` header, if
745    /// present, so [`Self::notify_resource_updated`] can route
746    /// `notifications/resources/updated` to just the sessions that
747    /// subscribed via `resources/subscribe` — a client that connects
748    /// without the header can still receive broadcasts (`.notify()`,
749    /// `.log()`, `list_changed`) but can never be a subscription target.
750    fn start_sse_stream(&self, request: &Request) -> Response {
751        let session_id = request.get_header("Mcp-Session-Id".to_string()).map(|h| h.value.clone());
752        let (tx, rx) = mpsc::sync_channel::<Vec<u8>>(SSE_CHANNEL_CAPACITY);
753        self.sse_clients.lock().unwrap().push(SseClient { session_id, sender: tx });
754
755        let mut response = Response::new();
756        response.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
757        response.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
758        response.headers.push(Header {
759            name: Header::_CONTENT_TYPE.to_string(),
760            value: "text/event-stream".to_string(),
761        });
762        response.headers.push(Header {
763            name: Header::_CACHE_CONTROL.to_string(),
764            value: "no-cache".to_string(),
765        });
766        response.headers.push(Header {
767            name: "X-Accel-Buffering".to_string(),
768            value: "no".to_string(),
769        });
770        response.stream_pipe = Some(Box::new(SseChannelReader::new(rx)));
771        response
772    }
773
774    /// Advertise the `logging` capability (`"logging":{}`) in `initialize`'s
775    /// response, so clients know they can call `logging/setLevel` and expect
776    /// `notifications/message` log entries over the `GET /mcp` SSE stream.
777    ///
778    /// This only changes what's *advertised* — [`Self::log`] pushes log
779    /// entries regardless of whether this was called, same as [`Self::notify`]
780    /// works unconditionally. A spec-honest client simply wouldn't send
781    /// `logging/setLevel` in the first place without seeing the capability.
782    ///
783    /// ```rust
784    /// use rust_web_server::mcp::McpServer;
785    ///
786    /// let server = McpServer::new("my-server", "1.0").logging_enabled();
787    /// ```
788    pub fn logging_enabled(mut self) -> Self {
789        self.logging_enabled = true;
790        self
791    }
792
793    /// Push a `notifications/message` log entry to every client connected to
794    /// the `GET /mcp` SSE stream, if `level` is at or above the level most
795    /// recently set via a client's `logging/setLevel` request (or
796    /// [`LogLevel::Debug`] — i.e. every level — if none has been set yet).
797    ///
798    /// `data_json` must already be a valid JSON value (the spec allows any
799    /// type here, not just an object — a plain string is fine) — it's
800    /// spliced in verbatim, not escaped or re-serialized. `logger`, if
801    /// given, identifies the log's source (e.g. a module or subsystem name)
802    /// and is escaped automatically.
803    ///
804    /// ```rust
805    /// use rust_web_server::mcp::{LogLevel, McpServer};
806    ///
807    /// let server = McpServer::new("my-server", "1.0").logging_enabled();
808    /// server.log(LogLevel::Warning, Some("database"), r#""connection pool exhausted""#);
809    /// ```
810    pub fn log(&self, level: LogLevel, logger: Option<&str>, data_json: &str) {
811        if level < *self.min_log_level.lock().unwrap() {
812            return;
813        }
814        let logger_field = match logger {
815            Some(l) => format!(r#","logger":"{}""#, json_escape(l)),
816            None => String::new(),
817        };
818        let params = format!(r#"{{"level":"{}"{logger_field},"data":{data_json}}}"#, level.as_str());
819        self.notify("notifications/message", Some(&params));
820    }
821
822    /// Handle `logging/setLevel`: store the requested minimum level so
823    /// subsequent [`Self::log`] calls filter against it. Returns
824    /// `INVALID_PARAMS` for a missing or unrecognized `params.level`.
825    fn do_set_log_level(&self, body: &str) -> Result<String, (i32, String)> {
826        let params = json_rpc::extract_raw(body, "params")
827            .ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
828        let level_str = json_rpc::extract_str(&params, "level")
829            .ok_or((json_rpc::INVALID_PARAMS, "Missing level".to_string()))?;
830        let level = LogLevel::parse(&level_str)
831            .ok_or_else(|| (json_rpc::INVALID_PARAMS, format!("Unknown log level: {level_str}")))?;
832        *self.min_log_level.lock().unwrap() = level;
833        Ok("{}".to_string())
834    }
835
836    // ── dynamic registration ──────────────────────────────────────────────────
837    //
838    // Unlike `.tool()`/`.resource()`/`.prompt()` (consuming builders, called
839    // before the server starts serving requests), these take `&self` and can
840    // be called at any time from any thread holding a clone of this
841    // `McpServer` — e.g. after discovering a plugin, connecting to a
842    // database, or reacting to a hot-reloaded config file. Every clone
843    // shares the same underlying `Arc<RwLock<Vec<_>>>`, so a mutation made
844    // through one clone is immediately visible to every other clone,
845    // including the ones handling concurrent requests on other threads.
846    //
847    // Each registration/removal pushes the corresponding
848    // `notifications/{tools,resources,prompts}/list_changed` event (no
849    // params, per spec) to every `GET /mcp` SSE client via `.notify()`.
850
851    /// Register a callable tool at runtime, exactly like [`Self::tool`] but
852    /// without needing to own the server (and usable after it's already
853    /// serving requests). Pushes `notifications/tools/list_changed`.
854    ///
855    /// ```rust
856    /// use rust_web_server::mcp::{McpContent, McpServer};
857    ///
858    /// let server = McpServer::new("my-server", "1.0");
859    ///
860    /// // Later, from any thread holding a clone of `server`:
861    /// server.register_tool("refresh_cache", "Reload the in-memory cache", "{}", |_args| {
862    ///     Ok(McpContent::text("cache refreshed"))
863    /// });
864    /// let existed = server.remove_tool("refresh_cache");
865    /// assert!(existed);
866    /// ```
867    pub fn register_tool<F>(&self, name: &str, description: &str, input_schema: &str, handler: F)
868    where
869        F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
870    {
871        self.tools.write().unwrap().push(ToolDef {
872            name: name.to_string(),
873            description: description.to_string(),
874            input_schema: input_schema.to_string(),
875            annotations: None,
876            handler: Arc::new(move |_ctx: McpContext, args: &str| handler(args)),
877        });
878        self.notify("notifications/tools/list_changed", None);
879    }
880
881    /// Remove a previously-registered tool by name. Returns `true` if a tool
882    /// with that name existed and was removed. Pushes
883    /// `notifications/tools/list_changed` only when something was actually
884    /// removed.
885    pub fn remove_tool(&self, name: &str) -> bool {
886        let removed = {
887            let mut tools = self.tools.write().unwrap();
888            let before = tools.len();
889            tools.retain(|t| t.name != name);
890            tools.len() != before
891        };
892        if removed {
893            self.notify("notifications/tools/list_changed", None);
894        }
895        removed
896    }
897
898    /// Register a readable resource at runtime, exactly like [`Self::resource`].
899    /// Pushes `notifications/resources/list_changed`.
900    pub fn register_resource<F>(&self, uri_template: &str, name: &str, description: &str, handler: F)
901    where
902        F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
903    {
904        self.resources.write().unwrap().push(ResourceDef {
905            uri_template: uri_template.to_string(),
906            name: name.to_string(),
907            description: description.to_string(),
908            handler: Arc::new(handler),
909        });
910        self.notify("notifications/resources/list_changed", None);
911    }
912
913    /// Remove a previously-registered resource by its exact `uri_template`
914    /// (the same string passed to [`Self::register_resource`]/[`Self::resource`],
915    /// not a concrete URI). Returns `true` if it existed. Pushes
916    /// `notifications/resources/list_changed` only when something was removed.
917    pub fn remove_resource(&self, uri_template: &str) -> bool {
918        let removed = {
919            let mut resources = self.resources.write().unwrap();
920            let before = resources.len();
921            resources.retain(|r| r.uri_template != uri_template);
922            resources.len() != before
923        };
924        if removed {
925            self.notify("notifications/resources/list_changed", None);
926        }
927        removed
928    }
929
930    /// Register a prompt template at runtime, exactly like [`Self::prompt`]
931    /// (no argument definitions — use [`Self::remove_prompt`] +
932    /// [`Self::register_prompt`] if you need to change a prompt's arguments
933    /// later; there is no dynamic equivalent of [`Self::prompt_with_args`]).
934    /// Pushes `notifications/prompts/list_changed`.
935    pub fn register_prompt<F>(&self, name: &str, description: &str, handler: F)
936    where
937        F: Fn(&str) -> Result<Vec<PromptMessage>, String> + Send + Sync + 'static,
938    {
939        self.prompts.write().unwrap().push(PromptDef {
940            name: name.to_string(),
941            description: description.to_string(),
942            arguments: vec![],
943            handler: Arc::new(handler),
944        });
945        self.notify("notifications/prompts/list_changed", None);
946    }
947
948    /// Remove a previously-registered prompt by name. Returns `true` if it
949    /// existed. Pushes `notifications/prompts/list_changed` only when
950    /// something was removed.
951    pub fn remove_prompt(&self, name: &str) -> bool {
952        let removed = {
953            let mut prompts = self.prompts.write().unwrap();
954            let before = prompts.len();
955            prompts.retain(|p| p.name != name);
956            prompts.len() != before
957        };
958        if removed {
959            self.notify("notifications/prompts/list_changed", None);
960        }
961        removed
962    }
963
964    /// Require a bearer token on every request to the MCP endpoint.
965    ///
966    /// The client must send `Authorization: Bearer <token>`. Requests with a
967    /// missing or wrong token receive `401 Unauthorized` before any JSON-RPC
968    /// processing occurs.
969    ///
970    /// Store the token in an environment variable — never hard-code it:
971    ///
972    /// ```rust,no_run
973    /// use rust_web_server::app::App;
974    /// use rust_web_server::core::New;
975    ///
976    /// let app = App::new()
977    ///     .mcp("my-server", "1.0")
978    ///     .require_bearer(std::env::var("MCP_TOKEN").expect("MCP_TOKEN not set"));
979    /// ```
980    ///
981    /// Claude Desktop config:
982    /// ```json
983    /// { "mcpServers": { "my-server": {
984    ///     "url": "http://localhost:7878/mcp",
985    ///     "headers": { "Authorization": "Bearer <token>" }
986    /// }}}
987    /// ```
988    pub fn require_bearer(mut self, token: impl Into<String>) -> Self {
989        self.auth_token = Some(token.into());
990        self
991    }
992
993    /// Wrap an existing [`Application`] so that non-MCP requests are forwarded
994    /// to it instead of the built-in [`App`].
995    ///
996    /// Use this when your existing server has custom routes, state, or
997    /// middleware that you want to keep alongside the MCP endpoint:
998    ///
999    /// ```rust,no_run
1000    /// use rust_web_server::app::App;
1001    /// use rust_web_server::mcp::{McpServer, McpContent};
1002    /// use rust_web_server::response::{Response, STATUS_CODE_REASON_PHRASE};
1003    /// use rust_web_server::test_client::TestClient;
1004    ///
1005    /// let existing_app = App::with_state(42u32)
1006    ///     .get("/api/hello", |_req, _params, _conn, _state| {
1007    ///         Response::get_response(&STATUS_CODE_REASON_PHRASE.n200_ok, None, None)
1008    ///     });
1009    ///
1010    /// let server = McpServer::new("my-app", "1.0")
1011    ///     .tool("ping", "Ping", "{}", |_| Ok(McpContent::text("pong")))
1012    ///     .wrap(existing_app);
1013    ///
1014    /// // Both /mcp and /api/hello are now handled by the same server.
1015    /// let client = TestClient::new(server);
1016    /// ```
1017    pub fn wrap(mut self, app: impl Application + Send + Sync + 'static) -> Self {
1018        self.fallback = Some(Arc::new(app));
1019        self
1020    }
1021
1022    /// Override the HTTP path for the MCP endpoint (default `"/mcp"`).
1023    pub fn at(mut self, path: impl Into<String>) -> Self {
1024        self.path = path.into();
1025        self
1026    }
1027
1028    /// Register a callable tool.
1029    ///
1030    /// - `name` — tool identifier (snake_case recommended)
1031    /// - `description` — human-readable description shown to the AI
1032    /// - `input_schema` — JSON Schema object for the tool's arguments
1033    /// - `handler` — closure receiving the raw `arguments` JSON string
1034    ///
1035    /// The handler returns [`McpContent`] on success or an error string.  An
1036    /// error is returned to the client as `isError: true` (not a protocol error).
1037    ///
1038    /// Use [`Self::tool_with_context`] instead if the handler needs the
1039    /// caller's identity, session, or headers.
1040    pub fn tool<F>(self, name: &str, description: &str, input_schema: &str, handler: F) -> Self
1041    where
1042        F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
1043    {
1044        self.tools.write().unwrap().push(ToolDef {
1045            name: name.to_string(),
1046            description: description.to_string(),
1047            input_schema: input_schema.to_string(),
1048            annotations: None,
1049            handler: Arc::new(move |_ctx: McpContext, args: &str| handler(args)),
1050        });
1051        self
1052    }
1053
1054    /// Register a callable tool with [`ToolAnnotations`] — behavioral hints
1055    /// (read-only, destructive, idempotent, open-world) that MCP clients use
1056    /// to decide whether to warn or confirm before calling it. Otherwise
1057    /// identical to [`Self::tool`] — the handler still only receives
1058    /// `arguments`, not [`McpContext`] (there is currently no single builder
1059    /// combining annotations with per-request context; call [`Self::tool_with_context`]
1060    /// instead if you need context and don't need annotations).
1061    ///
1062    /// ```rust,no_run
1063    /// use rust_web_server::mcp::{McpContent, McpServer, ToolAnnotations};
1064    ///
1065    /// let server = McpServer::new("my-server", "1.0")
1066    ///     .tool_annotated(
1067    ///         "delete_file",
1068    ///         "Delete a file from disk",
1069    ///         r#"{"type":"object","properties":{"path":{"type":"string"}},"required":["path"]}"#,
1070    ///         ToolAnnotations {
1071    ///             destructive_hint: Some(true),
1072    ///             read_only_hint: Some(false),
1073    ///             idempotent_hint: Some(true), // deleting twice = deleting once
1074    ///             ..Default::default()
1075    ///         },
1076    ///         |_args| Ok(McpContent::text("deleted")),
1077    ///     );
1078    /// ```
1079    pub fn tool_annotated<F>(
1080        self,
1081        name: &str,
1082        description: &str,
1083        input_schema: &str,
1084        annotations: ToolAnnotations,
1085        handler: F,
1086    ) -> Self
1087    where
1088        F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
1089    {
1090        self.tools.write().unwrap().push(ToolDef {
1091            name: name.to_string(),
1092            description: description.to_string(),
1093            input_schema: input_schema.to_string(),
1094            annotations: Some(annotations),
1095            handler: Arc::new(move |_ctx: McpContext, args: &str| handler(args)),
1096        });
1097        self
1098    }
1099
1100    /// Register a callable tool whose handler also receives [`McpContext`] —
1101    /// caller identity/session info derived from this request's headers and
1102    /// whatever `clientInfo` this session sent at `initialize` time.
1103    ///
1104    /// Same `name`/`description`/`input_schema` semantics as [`Self::tool`];
1105    /// the only difference is the handler's first parameter.
1106    ///
1107    /// ```rust,no_run
1108    /// use rust_web_server::mcp::{McpContent, McpServer};
1109    ///
1110    /// let server = McpServer::new("my-server", "1.0")
1111    ///     .tool_with_context(
1112    ///         "whoami",
1113    ///         "Report the caller's client info",
1114    ///         "{}",
1115    ///         |ctx, _args| {
1116    ///             let name = ctx.client_name.as_deref().unwrap_or("unknown client");
1117    ///             Ok(McpContent::text(format!("Called by {name}")))
1118    ///         },
1119    ///     );
1120    /// ```
1121    pub fn tool_with_context<F>(self, name: &str, description: &str, input_schema: &str, handler: F) -> Self
1122    where
1123        F: Fn(McpContext, &str) -> Result<McpContent, String> + Send + Sync + 'static,
1124    {
1125        self.tools.write().unwrap().push(ToolDef {
1126            name: name.to_string(),
1127            description: description.to_string(),
1128            input_schema: input_schema.to_string(),
1129            annotations: None,
1130            handler: Arc::new(handler),
1131        });
1132        self
1133    }
1134
1135    /// Register a readable resource.
1136    ///
1137    /// `uri_template` uses `{param}` placeholders, e.g. `"user://{id}"`.
1138    /// The handler receives the full concrete URI string.
1139    pub fn resource<F>(self, uri_template: &str, name: &str, description: &str, handler: F) -> Self
1140    where
1141        F: Fn(&str) -> Result<McpContent, String> + Send + Sync + 'static,
1142    {
1143        self.resources.write().unwrap().push(ResourceDef {
1144            uri_template: uri_template.to_string(),
1145            name: name.to_string(),
1146            description: description.to_string(),
1147            handler: Arc::new(handler),
1148        });
1149        self
1150    }
1151
1152    /// Register a prompt template.
1153    ///
1154    /// The handler receives the raw `arguments` JSON string and returns a
1155    /// list of [`PromptMessage`] values.
1156    pub fn prompt<F>(self, name: &str, description: &str, handler: F) -> Self
1157    where
1158        F: Fn(&str) -> Result<Vec<PromptMessage>, String> + Send + Sync + 'static,
1159    {
1160        self.prompts.write().unwrap().push(PromptDef {
1161            name: name.to_string(),
1162            description: description.to_string(),
1163            arguments: vec![],
1164            handler: Arc::new(handler),
1165        });
1166        self
1167    }
1168
1169    /// Register a prompt template with explicit argument definitions.
1170    pub fn prompt_with_args<F>(
1171        self,
1172        name: &str,
1173        description: &str,
1174        args: Vec<PromptArgDef>,
1175        handler: F,
1176    ) -> Self
1177    where
1178        F: Fn(&str) -> Result<Vec<PromptMessage>, String> + Send + Sync + 'static,
1179    {
1180        self.prompts.write().unwrap().push(PromptDef {
1181            name: name.to_string(),
1182            description: description.to_string(),
1183            arguments: args,
1184            handler: Arc::new(handler),
1185        });
1186        self
1187    }
1188
1189    /// Register an argument-completion provider for one named argument of a
1190    /// tool or prompt, so clients like Cursor and VS Code can offer
1191    /// autocomplete while the user fills in that argument.
1192    ///
1193    /// `ref_type` is `"tool"` or `"prompt"` — matched against the incoming
1194    /// `completion/complete` request's `ref.type` (`"ref/tool"`/`"ref/prompt"`
1195    /// on the wire) with the `"ref/"` prefix stripped. `ref_name` is the
1196    /// tool or prompt name this applies to. The handler receives the
1197    /// argument's name and whatever partial value the user has typed so
1198    /// far, and returns candidate completion strings (or an error, mapped
1199    /// to a JSON-RPC `INVALID_PARAMS` response).
1200    ///
1201    /// `initialize` advertises the `completions` capability automatically
1202    /// once at least one `.completion()` has been registered — there's no
1203    /// separate opt-in flag to remember.
1204    ///
1205    /// ```rust
1206    /// use rust_web_server::mcp::McpServer;
1207    ///
1208    /// let server = McpServer::new("my-server", "1.0")
1209    ///     .completion("tool", "deploy", |arg_name, _partial| {
1210    ///         match arg_name {
1211    ///             "region" => Ok(vec!["us-east-1".to_string(), "eu-west-1".to_string()]),
1212    ///             _ => Ok(vec![]),
1213    ///         }
1214    ///     });
1215    /// ```
1216    pub fn completion<F>(self, ref_type: &str, ref_name: &str, handler: F) -> Self
1217    where
1218        F: Fn(&str, &str) -> Result<Vec<String>, String> + Send + Sync + 'static,
1219    {
1220        self.completions.write().unwrap().push(CompletionDef {
1221            ref_type: ref_type.to_string(),
1222            ref_name: ref_name.to_string(),
1223            handler: Arc::new(handler),
1224        });
1225        self
1226    }
1227
1228    // ── request dispatch ──────────────────────────────────────────────────────
1229
1230    /// Process a raw JSON-RPC body and return an HTTP response.
1231    ///
1232    /// Equivalent to [`Self::handle_request_with_context`] with an empty
1233    /// [`McpContext`] — tool handlers registered via
1234    /// [`Self::tool_with_context`] will see every field as `None`. Prefer
1235    /// calling through [`Application::execute`] (i.e. actually serving HTTP
1236    /// requests) when you need real per-request context; this method exists
1237    /// for calling the JSON-RPC layer directly, e.g. in tests.
1238    pub fn handle_request(&self, body: &str) -> Response {
1239        self.handle_request_with_context(body, McpContext::default())
1240    }
1241
1242    /// Process a raw JSON-RPC body with an explicit [`McpContext`] and return
1243    /// an HTTP response. [`Self::execute`] calls this with a context built
1244    /// from the request's headers and this session's stored `clientInfo`;
1245    /// [`Self::handle_request`] calls this with an empty context.
1246    ///
1247    /// On a successful `initialize`, this mints a new session id (reusing
1248    /// [`crate::request_id::generate_request_id`]'s ID generator), records
1249    /// `params.clientInfo` under it, and returns the id in an
1250    /// `Mcp-Session-Id` response header — the client is expected to echo that
1251    /// header back on subsequent requests so later `tools/call`s in the same
1252    /// session can look their `clientInfo` back up.
1253    pub fn handle_request_with_context(&self, body: &str, ctx: McpContext) -> Response {
1254        let trimmed = body.trim_start();
1255        if trimmed.starts_with('[') {
1256            return self.handle_batch(trimmed, ctx);
1257        }
1258
1259        let method = match json_rpc::extract_str(body, "method") {
1260            Some(m) => m,
1261            None => return rpc_error(None, json_rpc::INVALID_REQUEST, "Missing method"),
1262        };
1263
1264        let id = json_rpc::extract_id(body);
1265
1266        if method == "notifications/cancelled" {
1267            self.handle_cancellation(body);
1268            return no_content();
1269        }
1270
1271        // Notifications have no `id` — acknowledge with 202 and no body.
1272        if method == "notifications/initialized" || (id.is_none() && method != "ping") {
1273            return no_content();
1274        }
1275
1276        let result = self.dispatch_with_cancellation(&method, body, ctx, &id);
1277        let id_str = id.as_deref().unwrap_or("null");
1278        let is_ok = result.is_ok();
1279
1280        let mut response = json_response(&Self::format_result(id_str, &result));
1281
1282        if method == "initialize" && is_ok {
1283            self.start_session(body, &mut response);
1284        }
1285
1286        response
1287    }
1288
1289    /// Process a JSON-RPC 2.0 batch request — a top-level JSON array of
1290    /// request objects sent in a single `POST /mcp` body, per the JSON-RPC
1291    /// batch spec that MCP inherits. Each element is dispatched exactly as
1292    /// [`Self::handle_request_with_context`] would dispatch it standalone;
1293    /// notifications (no `id`) contribute no entry to the response array,
1294    /// same as they'd get no response body outside a batch.
1295    ///
1296    /// An empty array (`[]`) is itself an invalid request per the JSON-RPC
1297    /// spec, so it gets one `Invalid Request` error object back rather than
1298    /// an empty array. A batch made up entirely of notifications produces no
1299    /// response body at all (`202 Accepted`), matching a single notification.
1300    ///
1301    /// If the batch contains a successful `initialize` call, the *first* one
1302    /// mints a session and attaches `Mcp-Session-Id` to the overall response,
1303    /// same as a standalone `initialize` would — sending more than one
1304    /// `initialize` in a batch is unusual and only the first is honored for
1305    /// session purposes, since one HTTP response can only carry one session id.
1306    fn handle_batch(&self, array_body: &str, ctx: McpContext) -> Response {
1307        let elements = json_rpc::split_array_elements(array_body);
1308        if elements.is_empty() {
1309            return rpc_error(None, json_rpc::INVALID_REQUEST, "Invalid Request");
1310        }
1311
1312        let mut parts: Vec<String> = Vec::new();
1313        let mut session_init_body: Option<String> = None;
1314
1315        for elem in &elements {
1316            let method = match json_rpc::extract_str(elem, "method") {
1317                Some(m) => m,
1318                None => {
1319                    parts.push(Self::format_result(
1320                        "null",
1321                        &Err((json_rpc::INVALID_REQUEST, "Missing method".to_string())),
1322                    ));
1323                    continue;
1324                }
1325            };
1326
1327            let id = json_rpc::extract_id(elem);
1328
1329            if method == "notifications/cancelled" {
1330                self.handle_cancellation(elem);
1331                continue;
1332            }
1333
1334            if method == "notifications/initialized" || (id.is_none() && method != "ping") {
1335                continue;
1336            }
1337
1338            let result = self.dispatch_with_cancellation(&method, elem, ctx.clone(), &id);
1339            let id_str = id.as_deref().unwrap_or("null");
1340            let is_ok = result.is_ok();
1341
1342            if method == "initialize" && is_ok && session_init_body.is_none() {
1343                session_init_body = Some(elem.clone());
1344            }
1345
1346            parts.push(Self::format_result(id_str, &result));
1347        }
1348
1349        if parts.is_empty() {
1350            // Every element was a notification — no response body, same as a
1351            // single standalone notification.
1352            return no_content();
1353        }
1354
1355        let mut response = json_response(&format!("[{}]", parts.join(",")));
1356        if let Some(init_body) = session_init_body {
1357            self.start_session(&init_body, &mut response);
1358        }
1359        response
1360    }
1361
1362    /// Dispatch one already-parsed JSON-RPC `method` against `body` (the raw
1363    /// single-object message, whether it arrived standalone or as one element
1364    /// of a batch) and return the JSON-RPC `result` payload or an error.
1365    /// Shared by [`Self::handle_request_with_context`] and [`Self::handle_batch`]
1366    /// so the method table exists in exactly one place.
1367    fn dispatch(&self, method: &str, body: &str, ctx: McpContext) -> Result<String, (i32, String)> {
1368        match method {
1369            "initialize"     => self.do_initialize(body),
1370            "ping"           => Ok("{}".to_string()),
1371            "tools/list"     => self.do_tools_list(body),
1372            "tools/call"     => self.do_tools_call(body, ctx),
1373            "resources/list" => self.do_resources_list(body),
1374            "resources/read" => self.do_resources_read(body),
1375            "resources/subscribe"   => self.do_resource_subscribe(body, ctx.session_id),
1376            "resources/unsubscribe" => self.do_resource_unsubscribe(body, ctx.session_id),
1377            "prompts/list"   => self.do_prompts_list(body),
1378            "prompts/get"    => self.do_prompts_get(body),
1379            "logging/setLevel" => self.do_set_log_level(body),
1380            "completion/complete" => self.do_completion(body),
1381            _                => Err((json_rpc::METHOD_NOT_FOUND, format!("Unknown method: {method}"))),
1382        }
1383    }
1384
1385    /// Wraps [`Self::dispatch`] for one request, additionally registering a
1386    /// cancellation flag for `tools/call` (keyed by `id`, guaranteed `Some`
1387    /// for `tools/call` — a notification-shaped `tools/call` with no `id`
1388    /// never reaches this far, see the notification checks in
1389    /// [`Self::handle_request_with_context`]/[`Self::handle_batch`]) so a
1390    /// later `notifications/cancelled` referencing this exact request can
1391    /// flip it. The entry is removed again once dispatch returns, whether
1392    /// the handler checked the flag or not — this map never accumulates
1393    /// stale entries the way `sessions`/`sse_clients` can.
1394    fn dispatch_with_cancellation(
1395        &self,
1396        method: &str,
1397        body: &str,
1398        ctx: McpContext,
1399        id: &Option<String>,
1400    ) -> Result<String, (i32, String)> {
1401        let Some(id_str) = (method == "tools/call").then_some(id.as_ref()).flatten() else {
1402            return self.dispatch(method, body, ctx);
1403        };
1404
1405        let flag = Arc::new(std::sync::atomic::AtomicBool::new(false));
1406        self.cancellations.lock().unwrap().insert(id_str.clone(), flag.clone());
1407        let ctx = McpContext { cancellation: Some(flag), ..ctx };
1408        let result = self.dispatch(method, body, ctx);
1409        self.cancellations.lock().unwrap().remove(id_str);
1410        result
1411    }
1412
1413    /// Handle `notifications/cancelled`: look up `params.requestId` (the
1414    /// spec allows `string | number`, so this is matched as a raw JSON
1415    /// token, the same way [`McpContext::progress_token`] is) in
1416    /// `cancellations` and flip its flag if the request is still in flight.
1417    /// Silently does nothing for an unknown/already-finished request id —
1418    /// the target call may simply have already completed naturally, which
1419    /// isn't an error.
1420    fn handle_cancellation(&self, body: &str) {
1421        let Some(params) = json_rpc::extract_raw(body, "params") else { return };
1422        let Some(request_id) = json_rpc::extract_raw(&params, "requestId") else { return };
1423        if let Some(flag) = self.cancellations.lock().unwrap().get(&request_id) {
1424            flag.store(true, std::sync::atomic::Ordering::Relaxed);
1425        }
1426    }
1427
1428    /// Render one JSON-RPC 2.0 response object — `{"jsonrpc":"2.0","result":...,"id":...}`
1429    /// or the `error` shape — from a dispatch result and its request's `id` (already
1430    /// rendered as a raw JSON token, e.g. `"1"`, `"\"abc\""`, or `"null"`).
1431    fn format_result(id_str: &str, result: &Result<String, (i32, String)>) -> String {
1432        match result {
1433            Ok(result_json) => format!(
1434                r#"{{"jsonrpc":"2.0","result":{result_json},"id":{id_str}}}"#
1435            ),
1436            Err((code, msg)) => {
1437                let escaped = json_escape(msg);
1438                format!(
1439                    r#"{{"jsonrpc":"2.0","error":{{"code":{code},"message":"{escaped}"}},"id":{id_str}}}"#
1440                )
1441            }
1442        }
1443    }
1444
1445    /// Mint a new session id, record `body`'s `params.clientInfo` under it
1446    /// (logging the caller's identity), and attach the id to `response` as
1447    /// an `Mcp-Session-Id` header. Called once, from
1448    /// [`Self::handle_request_with_context`], right after a successful
1449    /// `initialize`.
1450    fn start_session(&self, body: &str, response: &mut Response) {
1451        let client_info = json_rpc::extract_raw(body, "params")
1452            .and_then(|p| json_rpc::extract_raw(&p, "clientInfo"));
1453        let (name, version) = match &client_info {
1454            Some(info) => (
1455                json_rpc::extract_str(info, "name"),
1456                json_rpc::extract_str(info, "version"),
1457            ),
1458            None => (None, None),
1459        };
1460
1461        eprintln!(
1462            "[mcp] initialize from client {} v{}",
1463            name.as_deref().unwrap_or("unknown"),
1464            version.as_deref().unwrap_or("unknown"),
1465        );
1466
1467        let session_id = crate::request_id::generate_request_id();
1468        self.sessions
1469            .lock()
1470            .unwrap()
1471            .insert(session_id.clone(), StoredClientInfo { name, version });
1472
1473        response.headers.push(Header {
1474            name: "Mcp-Session-Id".to_string(),
1475            value: session_id,
1476        });
1477    }
1478
1479    // ── method handlers ───────────────────────────────────────────────────────
1480
1481    /// Handle `initialize`. Per spec, the server must inspect the client's
1482    /// requested `protocolVersion` and respond with the version it actually
1483    /// supports — allowing the client to abort the session if incompatible —
1484    /// rather than blindly echoing `PROTOCOL_VERSION` regardless of what was
1485    /// asked for.
1486    ///
1487    /// This server implements exactly one protocol version, so "negotiation"
1488    /// here means: if the client asked for that same version, confirm it;
1489    /// otherwise tell the client the version we actually speak (older *or*
1490    /// newer than what was requested), which is always the lower of the two
1491    /// — version strings are `YYYY-MM-DD` dates, so a plain string comparison
1492    /// already orders them correctly with no date parsing needed.
1493    ///
1494    /// `clientInfo` is *not* handled here — [`Self::handle_request_with_context`]
1495    /// extracts and stores it (under a freshly minted session id) after this
1496    /// returns, so it's only ever parsed out of `body` once per call.
1497    fn do_initialize(&self, body: &str) -> Result<String, (i32, String)> {
1498        let params = json_rpc::extract_raw(body, "params");
1499
1500        let client_version = params.as_deref().and_then(|p| json_rpc::extract_str(p, "protocolVersion"));
1501
1502        let negotiated_version: &str = match client_version.as_deref() {
1503            Some(v) if v < PROTOCOL_VERSION => v,
1504            _ => PROTOCOL_VERSION,
1505        };
1506
1507        let logging_cap = if self.logging_enabled { r#","logging":{}"# } else { "" };
1508        // completions is advertised iff at least one .completion() has been registered —
1509        // no separate opt-in flag needed, unlike logging: if nothing was registered,
1510        // completion/complete would just return empty results for everything anyway.
1511        let completions_cap = if self.completions.read().unwrap().is_empty() { "" } else { r#","completions":{}"# };
1512        // listChanged is always true: register_tool/remove_tool (and the resource/prompt
1513        // equivalents) are always available, unlike logging which is opt-in via
1514        // .logging_enabled(). resources.subscribe is also always true now that
1515        // resources/subscribe and resources/unsubscribe are implemented (MCP_TODO.md's TODO-14).
1516        let caps = format!(
1517            r#"{{"tools":{{"listChanged":true}},"resources":{{"subscribe":true,"listChanged":true}},"prompts":{{"listChanged":true}}{logging_cap}{completions_cap}}}"#
1518        );
1519        Ok(format!(
1520            r#"{{"protocolVersion":"{}","capabilities":{caps},"serverInfo":{{"name":"{}","version":"{}"}}}}"#,
1521            json_escape(negotiated_version),
1522            json_escape(&self.server_name),
1523            json_escape(&self.server_version),
1524        ))
1525    }
1526
1527    fn do_tools_list(&self, body: &str) -> Result<String, (i32, String)> {
1528        let items: Vec<String> = self.tools.read().unwrap().iter().map(|t| {
1529            let annotations = match t.annotations {
1530                Some(a) => format!(r#","annotations":{}"#, a.to_json()),
1531                None => String::new(),
1532            };
1533            format!(
1534                r#"{{"name":"{}","description":"{}","inputSchema":{}{}}}"#,
1535                json_escape(&t.name),
1536                json_escape(&t.description),
1537                t.input_schema,
1538                annotations,
1539            )
1540        }).collect();
1541        let (page, next_cursor) = self.paginate(&items, body)?;
1542        Ok(format!(r#"{{"tools":[{}]{}}}"#, page.join(","), next_cursor_json(&next_cursor)))
1543    }
1544
1545    fn do_tools_call(&self, body: &str, ctx: McpContext) -> Result<String, (i32, String)> {
1546        let params = json_rpc::extract_raw(body, "params")
1547            .ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
1548        let name = json_rpc::extract_str(&params, "name")
1549            .ok_or((json_rpc::INVALID_PARAMS, "Missing tool name".to_string()))?;
1550        let args = json_rpc::extract_raw(&params, "arguments")
1551            .unwrap_or_else(|| "{}".to_string());
1552
1553        // `_meta.progressToken` (string or number, per spec) — stored raw so
1554        // `McpContext::report_progress` can splice it back verbatim.
1555        let progress_token = json_rpc::extract_raw(&params, "_meta")
1556            .and_then(|meta| json_rpc::extract_raw(&meta, "progressToken"));
1557        let ctx = McpContext { progress_token, ..ctx };
1558
1559        let handler = {
1560            let tools = self.tools.read().unwrap();
1561            tools.iter().find(|t| t.name == name).map(|t| t.handler.clone())
1562        }.ok_or_else(|| (json_rpc::INVALID_PARAMS, format!("Unknown tool: {name}")))?;
1563
1564        match handler(ctx, &args) {
1565            Ok(c) => Ok(format!(
1566                r#"{{"content":[{}],"isError":false}}"#,
1567                c.to_content_json(),
1568            )),
1569            Err(e) => {
1570                let escaped = json_escape(&e);
1571                Ok(format!(
1572                    r#"{{"content":[{{"type":"text","text":"{escaped}"}}],"isError":true}}"#
1573                ))
1574            }
1575        }
1576    }
1577
1578    fn do_resources_list(&self, body: &str) -> Result<String, (i32, String)> {
1579        let items: Vec<String> = self.resources.read().unwrap().iter().map(|r| {
1580            format!(
1581                r#"{{"uri":"{}","name":"{}","description":"{}","mimeType":"text/plain"}}"#,
1582                json_escape(&r.uri_template),
1583                json_escape(&r.name),
1584                json_escape(&r.description),
1585            )
1586        }).collect();
1587        let (page, next_cursor) = self.paginate(&items, body)?;
1588        Ok(format!(r#"{{"resources":[{}]{}}}"#, page.join(","), next_cursor_json(&next_cursor)))
1589    }
1590
1591    fn do_resources_read(&self, body: &str) -> Result<String, (i32, String)> {
1592        let params = json_rpc::extract_raw(body, "params")
1593            .ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
1594        let uri = json_rpc::extract_str(&params, "uri")
1595            .ok_or((json_rpc::INVALID_PARAMS, "Missing uri".to_string()))?;
1596
1597        let handler = {
1598            let resources = self.resources.read().unwrap();
1599            resources.iter().find(|r| uri_matches(&r.uri_template, &uri)).map(|r| r.handler.clone())
1600        }.ok_or_else(|| (json_rpc::INVALID_PARAMS, format!("Resource not found: {uri}")))?;
1601
1602        match handler(&uri) {
1603            Ok(c) => {
1604                let text_esc = json_escape(&c.text);
1605                let uri_esc  = json_escape(&uri);
1606                Ok(format!(
1607                    r#"{{"contents":[{{"uri":"{uri_esc}","mimeType":"{}","text":"{text_esc}"}}]}}"#,
1608                    c.mime(),
1609                ))
1610            }
1611            Err(e) => Err((json_rpc::INVALID_PARAMS, e)),
1612        }
1613    }
1614
1615    /// Handle `resources/subscribe`: record that `session_id` (this
1616    /// request's `Mcp-Session-Id`) wants `notifications/resources/updated`
1617    /// pushed whenever [`Self::notify_resource_updated`] is called for
1618    /// `params.uri`. Requires a session id — without one there's no way to
1619    /// later match this subscription to a specific `GET /mcp` SSE
1620    /// connection, so a client that never sent `Mcp-Session-Id` gets
1621    /// `INVALID_PARAMS` rather than a subscription that can never fire.
1622    fn do_resource_subscribe(&self, body: &str, session_id: Option<String>) -> Result<String, (i32, String)> {
1623        let params = json_rpc::extract_raw(body, "params")
1624            .ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
1625        let uri = json_rpc::extract_str(&params, "uri")
1626            .ok_or((json_rpc::INVALID_PARAMS, "Missing uri".to_string()))?;
1627        let session_id = session_id.ok_or((
1628            json_rpc::INVALID_PARAMS,
1629            "resources/subscribe requires an Mcp-Session-Id header (call initialize first)".to_string(),
1630        ))?;
1631
1632        let mut subs = self.subscriptions.lock().unwrap();
1633        let subscribers = subs.entry(uri).or_default();
1634        if !subscribers.contains(&session_id) {
1635            subscribers.push(session_id);
1636        }
1637        Ok("{}".to_string())
1638    }
1639
1640    /// Handle `resources/unsubscribe`: the inverse of
1641    /// [`Self::do_resource_subscribe`]. Removing the last subscriber for a
1642    /// URI drops the URI's entry entirely rather than leaving an empty
1643    /// `Vec` behind.
1644    fn do_resource_unsubscribe(&self, body: &str, session_id: Option<String>) -> Result<String, (i32, String)> {
1645        let params = json_rpc::extract_raw(body, "params")
1646            .ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
1647        let uri = json_rpc::extract_str(&params, "uri")
1648            .ok_or((json_rpc::INVALID_PARAMS, "Missing uri".to_string()))?;
1649        let session_id = session_id.ok_or((
1650            json_rpc::INVALID_PARAMS,
1651            "resources/unsubscribe requires an Mcp-Session-Id header".to_string(),
1652        ))?;
1653
1654        let mut subs = self.subscriptions.lock().unwrap();
1655        if let Some(subscribers) = subs.get_mut(&uri) {
1656            subscribers.retain(|s| s != &session_id);
1657            if subscribers.is_empty() {
1658                subs.remove(&uri);
1659            }
1660        }
1661        Ok("{}".to_string())
1662    }
1663
1664    fn do_prompts_list(&self, body: &str) -> Result<String, (i32, String)> {
1665        let items: Vec<String> = self.prompts.read().unwrap().iter().map(|p| {
1666            let arg_defs: Vec<String> = p.arguments.iter().map(|a| {
1667                format!(
1668                    r#"{{"name":"{}","description":"{}","required":{}}}"#,
1669                    json_escape(&a.name),
1670                    json_escape(&a.description),
1671                    a.required,
1672                )
1673            }).collect();
1674            format!(
1675                r#"{{"name":"{}","description":"{}","arguments":[{}]}}"#,
1676                json_escape(&p.name),
1677                json_escape(&p.description),
1678                arg_defs.join(","),
1679            )
1680        }).collect();
1681        let (page, next_cursor) = self.paginate(&items, body)?;
1682        Ok(format!(r#"{{"prompts":[{}]{}}}"#, page.join(","), next_cursor_json(&next_cursor)))
1683    }
1684
1685    /// Slice `items` (already-rendered JSON object strings for one
1686    /// `*/list` response) according to [`Self::page_size`] and this
1687    /// request's `params.cursor`, returning the page and — if more items
1688    /// remain — the opaque `nextCursor` to embed in the response.
1689    ///
1690    /// Without a configured `page_size`, always returns every item and no
1691    /// cursor, i.e. pagination is fully opt-in.
1692    fn paginate<'a>(&self, items: &'a [String], body: &str) -> Result<(&'a [String], Option<String>), (i32, String)> {
1693        let page_size = match self.page_size {
1694            Some(n) => n,
1695            None => return Ok((items, None)),
1696        };
1697
1698        let cursor = json_rpc::extract_raw(body, "params")
1699            .and_then(|p| json_rpc::extract_str(&p, "cursor"));
1700
1701        let offset = match cursor {
1702            Some(c) => decode_cursor(&c)
1703                .ok_or((json_rpc::INVALID_PARAMS, "Invalid cursor".to_string()))?,
1704            None => 0,
1705        };
1706
1707        if offset >= items.len() {
1708            return Ok((&[], None));
1709        }
1710
1711        let end = (offset + page_size).min(items.len());
1712        let next_cursor = if end < items.len() { Some(encode_cursor(end)) } else { None };
1713        Ok((&items[offset..end], next_cursor))
1714    }
1715
1716    fn do_prompts_get(&self, body: &str) -> Result<String, (i32, String)> {
1717        let params = json_rpc::extract_raw(body, "params")
1718            .ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
1719        let name = json_rpc::extract_str(&params, "name")
1720            .ok_or((json_rpc::INVALID_PARAMS, "Missing prompt name".to_string()))?;
1721        let args = json_rpc::extract_raw(&params, "arguments")
1722            .unwrap_or_else(|| "{}".to_string());
1723
1724        let (description, handler) = {
1725            let prompts = self.prompts.read().unwrap();
1726            prompts.iter().find(|p| p.name == name).map(|p| (p.description.clone(), p.handler.clone()))
1727        }.ok_or_else(|| (json_rpc::INVALID_PARAMS, format!("Unknown prompt: {name}")))?;
1728
1729        match handler(&args) {
1730            Ok(msgs) => {
1731                let msg_jsons: Vec<String> = msgs.iter().map(|m| m.to_json()).collect();
1732                Ok(format!(
1733                    r#"{{"description":"{}","messages":[{}]}}"#,
1734                    json_escape(&description),
1735                    msg_jsons.join(","),
1736                ))
1737            }
1738            Err(e) => Err((json_rpc::INVALID_PARAMS, e)),
1739        }
1740    }
1741
1742    /// Handle `completion/complete`: look up the registered
1743    /// [`CompletionDef`] matching `params.ref.type`/`params.ref.name`, call
1744    /// its handler with `params.argument.name`/`params.argument.value`, and
1745    /// render the spec's `{"completion":{"values":[...],"hasMore":...,
1746    /// "total":...}}` shape. No registered provider for the given ref/name
1747    /// (or an unrecognized `ref.type` not stripped of `"ref/"`) returns an
1748    /// empty `values` list rather than an error — matching the spec's own
1749    /// framing of completion as a best-effort hint, not a required capability
1750    /// per tool/prompt.
1751    fn do_completion(&self, body: &str) -> Result<String, (i32, String)> {
1752        let params = json_rpc::extract_raw(body, "params")
1753            .ok_or((json_rpc::INVALID_PARAMS, "Missing params".to_string()))?;
1754        let reference = json_rpc::extract_raw(&params, "ref")
1755            .ok_or((json_rpc::INVALID_PARAMS, "Missing ref".to_string()))?;
1756        let ref_type_raw = json_rpc::extract_str(&reference, "type")
1757            .ok_or((json_rpc::INVALID_PARAMS, "Missing ref.type".to_string()))?;
1758        let ref_type = ref_type_raw.strip_prefix("ref/").unwrap_or(&ref_type_raw);
1759        let ref_name = json_rpc::extract_str(&reference, "name")
1760            .ok_or((json_rpc::INVALID_PARAMS, "Missing ref.name".to_string()))?;
1761
1762        let argument = json_rpc::extract_raw(&params, "argument")
1763            .ok_or((json_rpc::INVALID_PARAMS, "Missing argument".to_string()))?;
1764        let arg_name = json_rpc::extract_str(&argument, "name")
1765            .ok_or((json_rpc::INVALID_PARAMS, "Missing argument.name".to_string()))?;
1766        let partial = json_rpc::extract_str(&argument, "value").unwrap_or_default();
1767
1768        let handler = {
1769            let completions = self.completions.read().unwrap();
1770            completions.iter()
1771                .find(|c| c.ref_type == ref_type && c.ref_name == ref_name)
1772                .map(|c| c.handler.clone())
1773        };
1774
1775        let values = match handler {
1776            Some(h) => h(&arg_name, &partial).map_err(|e| (json_rpc::INVALID_PARAMS, e))?,
1777            None => vec![],
1778        };
1779
1780        let total = values.len();
1781        let has_more = total > MAX_COMPLETION_VALUES;
1782        let page = if has_more { &values[..MAX_COMPLETION_VALUES] } else { &values[..] };
1783        let values_json: Vec<String> = page.iter().map(|v| format!(r#""{}""#, json_escape(v))).collect();
1784
1785        Ok(format!(
1786            r#"{{"completion":{{"values":[{}],"hasMore":{has_more},"total":{total}}}}}"#,
1787            values_json.join(","),
1788        ))
1789    }
1790
1791    /// Build the [`McpContext`] for an incoming request: the `Mcp-Session-Id`
1792    /// header, if present, plus whatever `clientInfo` was recorded for that
1793    /// session at `initialize` time (if this session is recognized).
1794    fn context_for(&self, request: &Request) -> McpContext {
1795        let session_id = request
1796            .get_header("Mcp-Session-Id".to_string())
1797            .map(|h| h.value.clone());
1798
1799        let (client_name, client_version) = match &session_id {
1800            Some(sid) => match self.sessions.lock().unwrap().get(sid) {
1801                Some(info) => (info.name.clone(), info.version.clone()),
1802                None => (None, None),
1803            },
1804            None => (None, None),
1805        };
1806
1807        McpContext {
1808            client_name,
1809            client_version,
1810            session_id,
1811            auth_claims: None,
1812            progress_token: None,
1813            sse_clients: Some(self.sse_clients.clone()),
1814            cancellation: None,
1815        }
1816    }
1817}
1818
1819/// Render one JSON-RPC 2.0 notification (no `id` — fire-and-forget, per
1820/// spec) as an SSE `data:`-ready message body. Shared by [`McpServer::notify`]
1821/// and [`McpContext::report_progress`].
1822fn render_notification(method: &str, params_json: Option<&str>) -> String {
1823    let params_field = match params_json {
1824        Some(p) => format!(r#","params":{p}"#),
1825        None => String::new(),
1826    };
1827    format!(r#"{{"jsonrpc":"2.0","method":"{}"{}}}"#, json_escape(method), params_field)
1828}
1829
1830/// Send a raw pre-built JSON-RPC message to every client in `clients`,
1831/// pruning any whose channel is full or disconnected. Shared by
1832/// [`McpServer::notify`] and [`McpContext::report_progress`] — the latter
1833/// only has a clone of the broadcast list, not a whole `McpServer`.
1834fn broadcast_sse_to(clients: &Arc<Mutex<Vec<SseClient>>>, json: &str) {
1835    let frame = format!("data: {json}\n\n").into_bytes();
1836    let mut clients = clients.lock().unwrap();
1837    clients.retain(|c| c.sender.try_send(frame.clone()).is_ok());
1838}
1839
1840/// Send a raw pre-built JSON-RPC message only to clients whose `session_id`
1841/// is in `session_ids`, pruning any of *those* whose channel is full or
1842/// disconnected. A client with no `session_id`, or one not in
1843/// `session_ids`, is left untouched (kept, not sent to) — this is a
1844/// targeted send for [`McpServer::notify_resource_updated`], not a
1845/// broadcast like [`broadcast_sse_to`].
1846fn send_sse_to_sessions(clients: &Arc<Mutex<Vec<SseClient>>>, session_ids: &[String], json: &str) {
1847    let frame = format!("data: {json}\n\n").into_bytes();
1848    let mut clients = clients.lock().unwrap();
1849    clients.retain(|c| {
1850        let is_target = c.session_id.as_deref().is_some_and(|sid| session_ids.iter().any(|s| s == sid));
1851        if is_target {
1852            c.sender.try_send(frame.clone()).is_ok()
1853        } else {
1854            true
1855        }
1856    });
1857}
1858
1859// ── SSE channel reader ────────────────────────────────────────────────────────
1860
1861/// Adapts an `mpsc::Receiver<Vec<u8>>` of pre-framed SSE bytes into a
1862/// blocking [`std::io::Read`], so `Server::pipe_stream` (already written for
1863/// proxy passthrough streaming) can drive a `GET /mcp` SSE connection with no
1864/// changes to the server's write loop.
1865///
1866/// Blocks in [`Self::read`] until either a frame arrives, the sender side is
1867/// dropped (all `McpServer` clones gone — EOF, closing the connection), or
1868/// [`SSE_KEEPALIVE_INTERVAL`] elapses with nothing to send (writes a `:
1869/// keep-alive` comment instead, both to satisfy proxies that time out
1870/// silent connections and to surface a dead peer on the next write attempt).
1871struct SseChannelReader {
1872    rx: mpsc::Receiver<Vec<u8>>,
1873    leftover: Vec<u8>,
1874}
1875
1876impl SseChannelReader {
1877    fn new(rx: mpsc::Receiver<Vec<u8>>) -> Self {
1878        SseChannelReader { rx, leftover: Vec::new() }
1879    }
1880}
1881
1882impl std::io::Read for SseChannelReader {
1883    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
1884        if self.leftover.is_empty() {
1885            loop {
1886                match self.rx.recv_timeout(SSE_KEEPALIVE_INTERVAL) {
1887                    Ok(frame) => { self.leftover = frame; break; }
1888                    Err(mpsc::RecvTimeoutError::Timeout) => {
1889                        self.leftover = b": keep-alive\n\n".to_vec();
1890                        break;
1891                    }
1892                    Err(mpsc::RecvTimeoutError::Disconnected) => return Ok(0),
1893                }
1894            }
1895        }
1896
1897        let n = self.leftover.len().min(buf.len());
1898        buf[..n].copy_from_slice(&self.leftover[..n]);
1899        self.leftover.drain(..n);
1900        Ok(n)
1901    }
1902}
1903
1904// ── Application ───────────────────────────────────────────────────────────────
1905
1906impl Application for McpServer {
1907    fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
1908        if request.request_uri == self.path {
1909            // Check bearer token before processing any MCP request.
1910            if let Some(expected) = &self.auth_token {
1911                let provided = request.headers.iter()
1912                    .find(|h| h.name.eq_ignore_ascii_case("authorization"))
1913                    .map(|h| h.value.as_str())
1914                    .unwrap_or("");
1915                let bearer = provided.strip_prefix("Bearer ").unwrap_or("");
1916                if bearer != expected.as_str() {
1917                    return Ok(unauthorized());
1918                }
1919            }
1920
1921            return Ok(match request.method.as_str() {
1922                "POST" => {
1923                    let body = std::str::from_utf8(&request.body).unwrap_or("");
1924                    let ctx = self.context_for(request);
1925                    self.handle_request_with_context(body, ctx)
1926                }
1927                "GET" => self.start_sse_stream(request),
1928                "OPTIONS" => {
1929                    // CORS preflight for browser-based MCP clients
1930                    let mut r = Response::new();
1931                    r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
1932                    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
1933                    r.headers.push(Header {
1934                        name: "Allow".to_string(),
1935                        value: "GET, POST, OPTIONS".to_string(),
1936                    });
1937                    r
1938                }
1939                _ => {
1940                    let mut r = Response::new();
1941                    r.status_code = *STATUS_CODE_REASON_PHRASE.n405_method_not_allowed.status_code;
1942                    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n405_method_not_allowed.reason_phrase.to_string();
1943                    r.headers.push(Header {
1944                        name: "Allow".to_string(),
1945                        value: "GET, POST, OPTIONS".to_string(),
1946                    });
1947                    r.content_range_list = vec![Range::get_content_range(
1948                        b"MCP endpoint only accepts GET (SSE) or POST".to_vec(),
1949                        MimeType::TEXT_PLAIN.to_string(),
1950                    )];
1951                    r
1952                }
1953            });
1954        }
1955
1956        // Not an MCP path — fall through to the wrapped app (or built-in App).
1957        match &self.fallback {
1958            Some(app) => app.execute(request, connection),
1959            None      => App::new().execute(request, connection),
1960        }
1961    }
1962}
1963
1964// ── public helper ─────────────────────────────────────────────────────────────
1965
1966/// Extract a string argument from a tool/prompt `arguments` JSON object.
1967///
1968/// ```rust
1969/// use rust_web_server::mcp::extract_arg;
1970/// assert_eq!(extract_arg(r#"{"text":"hello"}"#, "text").as_deref(), Some("hello"));
1971/// assert_eq!(extract_arg(r#"{}"#, "missing"), None);
1972/// ```
1973pub fn extract_arg(arguments: &str, name: &str) -> Option<String> {
1974    json_rpc::extract_str(arguments, name)
1975}
1976
1977// ── internal helpers ──────────────────────────────────────────────────────────
1978
1979fn json_response(body: &str) -> Response {
1980    let mut r = Response::new();
1981    r.status_code = *STATUS_CODE_REASON_PHRASE.n200_ok.status_code;
1982    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n200_ok.reason_phrase.to_string();
1983    r.content_range_list = vec![Range::get_content_range(
1984        body.as_bytes().to_vec(),
1985        MimeType::APPLICATION_JSON.to_string(),
1986    )];
1987    r
1988}
1989
1990fn no_content() -> Response {
1991    let mut r = Response::new();
1992    r.status_code = *STATUS_CODE_REASON_PHRASE.n202_accepted.status_code;
1993    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n202_accepted.reason_phrase.to_string();
1994    r
1995}
1996
1997fn unauthorized() -> Response {
1998    let mut r = Response::new();
1999    r.status_code = *STATUS_CODE_REASON_PHRASE.n401_unauthorized.status_code;
2000    r.reason_phrase = STATUS_CODE_REASON_PHRASE.n401_unauthorized.reason_phrase.to_string();
2001    r.headers.push(Header {
2002        name: "WWW-Authenticate".to_string(),
2003        value: "Bearer".to_string(),
2004    });
2005    r.content_range_list = vec![Range::get_content_range(
2006        b"Unauthorized".to_vec(),
2007        MimeType::TEXT_PLAIN.to_string(),
2008    )];
2009    r
2010}
2011
2012fn rpc_error(id: Option<&str>, code: i32, message: &str) -> Response {
2013    let id_str  = id.unwrap_or("null");
2014    let escaped = json_escape(message);
2015    json_response(&format!(
2016        r#"{{"jsonrpc":"2.0","error":{{"code":{code},"message":"{escaped}"}},"id":{id_str}}}"#
2017    ))
2018}
2019
2020pub(crate) fn json_escape(s: &str) -> String {
2021    let mut out = String::with_capacity(s.len() + 4);
2022    for ch in s.chars() {
2023        match ch {
2024            '"'  => out.push_str("\\\""),
2025            '\\' => out.push_str("\\\\"),
2026            '\n' => out.push_str("\\n"),
2027            '\r' => out.push_str("\\r"),
2028            '\t' => out.push_str("\\t"),
2029            c if (c as u32) < 0x20 => { let _ = std::fmt::Write::write_fmt(&mut out, format_args!("\\u{:04x}", c as u32)); }
2030            c    => out.push(c),
2031        }
2032    }
2033    out
2034}
2035
2036// ── pagination cursors ─────────────────────────────────────────────────────────
2037
2038/// Render `,"nextCursor":"..."` for a `*/list` response, or `""` if there's
2039/// no next page — spliced directly after the closing `]` of the items array.
2040fn next_cursor_json(next_cursor: &Option<String>) -> String {
2041    match next_cursor {
2042        Some(c) => format!(r#","nextCursor":"{}""#, json_escape(c)),
2043        None => String::new(),
2044    }
2045}
2046
2047/// Encode a `tools/list`/`resources/list`/`prompts/list` offset as the
2048/// opaque `nextCursor`/`params.cursor` string the MCP spec expects — just
2049/// base64 of the decimal offset, e.g. `50` → `"NTA="`. Callers only ever
2050/// treat this as opaque; the encoding is a private implementation detail of
2051/// this module, not a client-facing contract.
2052fn encode_cursor(offset: usize) -> String {
2053    base64_encode(offset.to_string().as_bytes())
2054}
2055
2056/// Decode a cursor produced by [`encode_cursor`]. Returns `None` for
2057/// anything that isn't valid base64 of a decimal `usize` — a malformed or
2058/// tampered cursor, not a crash.
2059fn decode_cursor(cursor: &str) -> Option<usize> {
2060    String::from_utf8(base64_decode(cursor)?).ok()?.parse().ok()
2061}
2062
2063const BASE64_TABLE: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
2064
2065fn base64_encode(data: &[u8]) -> String {
2066    let mut out = String::with_capacity((data.len() + 2) / 3 * 4);
2067    for chunk in data.chunks(3) {
2068        let b0 = chunk[0] as u32;
2069        let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
2070        let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
2071        let n = (b0 << 16) | (b1 << 8) | b2;
2072        out.push(BASE64_TABLE[((n >> 18) & 0x3F) as usize] as char);
2073        out.push(BASE64_TABLE[((n >> 12) & 0x3F) as usize] as char);
2074        out.push(if chunk.len() > 1 { BASE64_TABLE[((n >> 6) & 0x3F) as usize] as char } else { '=' });
2075        out.push(if chunk.len() > 2 { BASE64_TABLE[(n & 0x3F) as usize] as char } else { '=' });
2076    }
2077    out
2078}
2079
2080fn base64_decode(s: &str) -> Option<Vec<u8>> {
2081    fn sextet(c: u8) -> Option<u32> {
2082        match c {
2083            b'A'..=b'Z' => Some((c - b'A') as u32),
2084            b'a'..=b'z' => Some((c - b'a' + 26) as u32),
2085            b'0'..=b'9' => Some((c - b'0' + 52) as u32),
2086            b'+' => Some(62),
2087            b'/' => Some(63),
2088            _ => None,
2089        }
2090    }
2091
2092    let trimmed = s.trim_end_matches('=');
2093    let bytes = trimmed.as_bytes();
2094    let mut out = Vec::with_capacity(bytes.len() * 3 / 4 + 3);
2095    for chunk in bytes.chunks(4) {
2096        if chunk.len() == 1 {
2097            return None; // not a valid base64 length
2098        }
2099        let vals: Vec<u32> = chunk.iter().map(|&b| sextet(b)).collect::<Option<Vec<_>>>()?;
2100        let n = vals.iter().enumerate().fold(0u32, |acc, (i, &v)| acc | (v << (18 - 6 * i)));
2101        out.push(((n >> 16) & 0xFF) as u8);
2102        if vals.len() > 2 { out.push(((n >> 8) & 0xFF) as u8); }
2103        if vals.len() > 3 { out.push((n & 0xFF) as u8); }
2104    }
2105    Some(out)
2106}
2107
2108fn uri_matches(template: &str, uri: &str) -> bool {
2109    // Template `"user://{id}"` matches any URI starting with `"user://"`.
2110    match template.find('{') {
2111        Some(pos) => uri.starts_with(&template[..pos]),
2112        None      => template == uri,
2113    }
2114}