Skip to main content

llm_kernel/mcp/
server.rs

1//! MCP server core — tool registration, initialization, and dispatch logic.
2
3use std::collections::HashMap;
4use std::future::Future;
5use std::sync::Arc;
6
7use async_trait::async_trait;
8
9use crate::mcp::auth::BearerAuth;
10use crate::mcp::schema::{PromptDescription, ResourceDescription, ToolDescription};
11
12/// MCP protocol versions this server understands, newest first.
13///
14/// `2026-07-28` is a *modern* (stateless, per-request `_meta`) revision served
15/// alongside the *legacy* (initialize-handshake) revisions in
16/// [`LEGACY_PROTOCOL_VERSIONS`] — this is a dual-era server. A request carrying
17/// `_meta["io.modelcontextprotocol/protocolVersion"]` is served statelessly;
18/// an `initialize` request selects legacy semantics.
19pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] =
20    &["2026-07-28", "2025-06-18", "2025-03-26", "2024-11-05"];
21
22/// The legacy (initialize-handshake) revisions this server implements, newest
23/// first — [`SUPPORTED_PROTOCOL_VERSIONS`] minus its modern head. The `initialize`
24/// handshake negotiates among these only.
25pub const LEGACY_PROTOCOL_VERSIONS: &[&str] = &["2025-06-18", "2025-03-26", "2024-11-05"];
26
27/// The newest MCP protocol revision this server implements (modern, stateless).
28pub const LATEST_PROTOCOL_VERSION: &str = "2026-07-28";
29
30/// The newest *legacy* (initialize-handshake) revision this server implements.
31///
32/// Used as the fallback in `initialize` responses: a legacy client that asked
33/// for an unknown version must not be handed `2026-07-28`, which postdates its
34/// handshake-based world.
35pub const LEGACY_LATEST_PROTOCOL_VERSION: &str = "2025-06-18";
36
37/// The `_meta` key carrying a modern request's protocol version.
38pub const META_PROTOCOL_VERSION: &str = "io.modelcontextprotocol/protocolVersion";
39
40/// The `_meta` key carrying the server identity in results (`server/discover`).
41pub const META_SERVER_INFO: &str = "io.modelcontextprotocol/serverInfo";
42
43/// The `_meta` key tagging subscription-stream messages with the JSON-RPC id
44/// of the `subscriptions/listen` request that opened them.
45pub const META_SUBSCRIPTION_ID: &str = "io.modelcontextprotocol/subscriptionId";
46
47/// Extract the modern-era protocol version from a request's
48/// `params._meta`, if present. `None` means the request speaks legacy.
49pub fn request_protocol_version(req: &serde_json::Value) -> Option<&str> {
50    req.pointer("/params/_meta")
51        .and_then(|m| m.get(META_PROTOCOL_VERSION))
52        .and_then(|v| v.as_str())
53}
54
55/// Methods whose results are cacheable and therefore require `ttlMs` and
56/// `cacheScope` (the `CacheableResult` interface of the 2026-07-28 revision).
57fn is_cacheable_method(method: &str) -> bool {
58    matches!(
59        method,
60        "server/discover"
61            | "tools/list"
62            | "resources/list"
63            | "resources/templates/list"
64            | "prompts/list"
65            | "resources/read"
66    )
67}
68
69/// Stamp a modern (2026-07-28) result with the fields the revision requires:
70/// `resultType: "complete"` on every result, plus `ttlMs`/`cacheScope` on
71/// cacheable ones. Results for other revisions are returned untouched.
72pub(crate) fn shape_modern_result(
73    protocol_version: &str,
74    method: &str,
75    result: &mut serde_json::Value,
76) {
77    if protocol_version != LATEST_PROTOCOL_VERSION {
78        return;
79    }
80    if let Some(obj) = result.as_object_mut() {
81        obj.entry("resultType")
82            .or_insert_with(|| serde_json::json!("complete"));
83        if is_cacheable_method(method) {
84            obj.entry("ttlMs")
85                .or_insert_with(|| serde_json::json!(3_600_000));
86            obj.entry("cacheScope")
87                .or_insert_with(|| serde_json::json!("private"));
88        }
89    }
90}
91
92/// Handler function type for MCP tool calls (synchronous).
93pub type Handler =
94    Box<dyn Fn(serde_json::Value) -> crate::error::Result<serde_json::Value> + Send + Sync>;
95
96/// Async tool-handler trait — the async counterpart to the synchronous [`Handler`].
97///
98/// Object-safe via `async_trait`, so an [`McpServer`] can store
99/// `Arc<dyn AsyncToolHandler>` and await it from an async transport
100/// (e.g. the Streamable HTTP transport).
101#[async_trait]
102pub trait AsyncToolHandler: Send + Sync {
103    /// Invoke the handler with the tool call parameters.
104    async fn call(&self, params: serde_json::Value) -> crate::error::Result<serde_json::Value>;
105}
106
107/// Adapts an async closure `Fn(Value) -> Future<Output = Result<Value>>` into an
108/// [`AsyncToolHandler`], so [`McpServer::set_async_handler`] accepts a plain
109/// async closure.
110struct AsyncHandlerFn<F>(F);
111
112#[async_trait]
113impl<F, Fut> AsyncToolHandler for AsyncHandlerFn<F>
114where
115    F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
116    Fut: Future<Output = crate::error::Result<serde_json::Value>> + Send,
117{
118    async fn call(&self, params: serde_json::Value) -> crate::error::Result<serde_json::Value> {
119        (self.0)(params).await
120    }
121}
122
123/// An MCP server that manages tools, resources, prompts, and dispatches calls.
124pub struct McpServer {
125    server_name: String,
126    server_version: String,
127    tools: Vec<ToolDescription>,
128    resources: Vec<ResourceDescription>,
129    prompts: Vec<PromptDescription>,
130    handlers: HashMap<String, Handler>,
131    async_handlers: HashMap<String, Arc<dyn AsyncToolHandler>>,
132    resource_handlers: HashMap<String, Handler>,
133    prompt_handlers: HashMap<String, Handler>,
134    auth: Option<BearerAuth>,
135}
136
137impl McpServer {
138    /// Create a new MCP server with no authentication.
139    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
140        Self {
141            server_name: name.into(),
142            server_version: version.into(),
143            tools: Vec::new(),
144            resources: Vec::new(),
145            prompts: Vec::new(),
146            handlers: HashMap::new(),
147            async_handlers: HashMap::new(),
148            resource_handlers: HashMap::new(),
149            prompt_handlers: HashMap::new(),
150            auth: None,
151        }
152    }
153
154    /// Require bearer token authentication for all requests.
155    pub fn with_bearer_auth(mut self, token: impl Into<String>) -> Self {
156        self.auth = Some(BearerAuth::new(token));
157        self
158    }
159
160    /// Generate and attach a random bearer token.
161    ///
162    /// Returns the generated token so the caller can distribute it.
163    pub fn with_generated_auth(mut self) -> (Self, String) {
164        let bearer = BearerAuth::generate();
165        let token = bearer.token().to_string();
166        self.auth = Some(bearer);
167        (self, token)
168    }
169
170    /// Validate an `Authorization` header value. Always returns `true` when no auth is configured.
171    pub fn check_auth(&self, authorization_header: &str) -> bool {
172        match &self.auth {
173            None => true,
174            Some(bearer) => bearer.validate(authorization_header),
175        }
176    }
177
178    /// Returns `true` if bearer authentication is enabled on this server.
179    pub fn auth_enabled(&self) -> bool {
180        self.auth.is_some()
181    }
182
183    /// Register a tool with the server.
184    pub fn register_tool(&mut self, tool: ToolDescription) {
185        self.tools.push(tool);
186    }
187
188    /// Register a resource with the server.
189    pub fn register_resource(&mut self, resource: ResourceDescription) {
190        self.resources.push(resource);
191    }
192
193    /// Set the handler for a tool by name.
194    pub fn set_handler(
195        &mut self,
196        tool_name: &str,
197        handler: impl Fn(serde_json::Value) -> crate::error::Result<serde_json::Value>
198        + Send
199        + Sync
200        + 'static,
201    ) {
202        self.handlers
203            .insert(tool_name.to_string(), Box::new(handler));
204    }
205
206    /// Register an async handler for a tool by name.
207    ///
208    /// `handler` is a closure returning a future (typically `async move { … }`).
209    /// Async handlers take precedence over sync handlers registered with
210    /// [`Self::set_handler`] when resolved via [`Self::call_tool_async`].
211    pub fn set_async_handler<F, Fut>(&mut self, tool_name: &str, handler: F)
212    where
213        F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
214        Fut: Future<Output = crate::error::Result<serde_json::Value>> + Send,
215    {
216        self.async_handlers
217            .insert(tool_name.to_string(), Arc::new(AsyncHandlerFn(handler)));
218    }
219
220    /// Get the server name.
221    pub fn name(&self) -> &str {
222        &self.server_name
223    }
224
225    /// Get the server version.
226    pub fn version(&self) -> &str {
227        &self.server_version
228    }
229
230    /// List all registered tools.
231    pub fn tools(&self) -> &[ToolDescription] {
232        &self.tools
233    }
234
235    /// List all registered resources.
236    pub fn resources(&self) -> &[ResourceDescription] {
237        &self.resources
238    }
239
240    /// Set the handler for a resource by URI.
241    pub fn set_resource_handler(
242        &mut self,
243        uri: &str,
244        handler: impl Fn(serde_json::Value) -> crate::error::Result<serde_json::Value>
245        + Send
246        + Sync
247        + 'static,
248    ) {
249        self.resource_handlers
250            .insert(uri.to_string(), Box::new(handler));
251    }
252
253    /// Read a resource by URI with the given parameters.
254    pub fn read_resource(
255        &self,
256        uri: &str,
257        params: serde_json::Value,
258    ) -> crate::error::Result<serde_json::Value> {
259        let handler = self
260            .resource_handlers
261            .get(uri)
262            .ok_or_else(|| crate::error::KernelError::Config(format!("unknown resource: {uri}")))?;
263        handler(params)
264    }
265
266    /// Register a prompt with the server.
267    pub fn register_prompt(&mut self, prompt: PromptDescription) {
268        self.prompts.push(prompt);
269    }
270
271    /// Set the handler for a prompt by name.
272    ///
273    /// The handler receives the `prompts/get` arguments object and returns the
274    /// result value — typically `{ "description": ..., "messages": [...] }`.
275    pub fn set_prompt_handler(
276        &mut self,
277        prompt_name: &str,
278        handler: impl Fn(serde_json::Value) -> crate::error::Result<serde_json::Value>
279        + Send
280        + Sync
281        + 'static,
282    ) {
283        self.prompt_handlers
284            .insert(prompt_name.to_string(), Box::new(handler));
285    }
286
287    /// List all registered prompts.
288    pub fn prompts(&self) -> &[PromptDescription] {
289        &self.prompts
290    }
291
292    /// Render a prompt by name with the given arguments.
293    pub fn get_prompt(
294        &self,
295        name: &str,
296        params: serde_json::Value,
297    ) -> crate::error::Result<serde_json::Value> {
298        let handler = self
299            .prompt_handlers
300            .get(name)
301            .ok_or_else(|| crate::error::KernelError::Config(format!("unknown prompt: {name}")))?;
302        handler(params)
303    }
304
305    /// Whether a tool with `name` is registered (has a sync or async handler).
306    ///
307    /// Lets a transport distinguish an *unknown tool* (a protocol-level invalid
308    /// params error) from a tool that ran and *failed* (reported in-band with
309    /// `isError: true`).
310    pub fn has_tool(&self, name: &str) -> bool {
311        self.handlers.contains_key(name) || self.async_handlers.contains_key(name)
312    }
313
314    /// Check `arguments` against the tool's advertised `input_schema`.
315    ///
316    /// Only the two constraints a caller can already see in `tools/list` are
317    /// enforced: an `"object"` schema needs an object, and every name under
318    /// `"required"` must be present. Without this a request that violates the
319    /// advertised contract reaches the handler, where `params["x"].as_str()
320    /// .unwrap_or_default()` turns a protocol error into a silently wrong
321    /// result (an empty-string search, a note titled "untitled").
322    ///
323    /// Returns the offending message; `Ok(())` when the tool is unknown here
324    /// (the caller reports unknown tools) or declares no constraints.
325    pub fn validate_tool_args(
326        &self,
327        name: &str,
328        arguments: &serde_json::Value,
329    ) -> Result<(), String> {
330        let Some(tool) = self.tools.iter().find(|t| t.name == name) else {
331            return Ok(());
332        };
333        if tool.input_schema.get("type").and_then(|t| t.as_str()) != Some("object") {
334            return Ok(());
335        }
336        // `null` stands for "no arguments given" on both transports.
337        let obj = match arguments {
338            serde_json::Value::Null => None,
339            serde_json::Value::Object(o) => Some(o),
340            _ => return Err(format!("tool '{name}' expects an object for arguments")),
341        };
342        let Some(required) = tool.input_schema.get("required").and_then(|r| r.as_array()) else {
343            return Ok(());
344        };
345        for field in required.iter().filter_map(|f| f.as_str()) {
346            if !obj.is_some_and(|o| o.contains_key(field)) {
347                return Err(format!(
348                    "tool '{name}' is missing required argument '{field}'"
349                ));
350            }
351        }
352        Ok(())
353    }
354
355    /// Names of tools that have ONLY an async handler — these cannot run on
356    /// the synchronous transport path. Sorted for stable reporting.
357    pub fn async_only_tools(&self) -> Vec<&str> {
358        let mut names: Vec<&str> = self
359            .async_handlers
360            .keys()
361            .filter(|n| !self.handlers.contains_key(*n))
362            .map(String::as_str)
363            .collect();
364        names.sort_unstable();
365        names
366    }
367
368    /// Call a tool by name with the given parameters.
369    ///
370    /// Synchronous handlers only. A tool registered via
371    /// [`McpServer::set_async_handler`] cannot run here — use
372    /// [`McpServer::call_tool_async`] (or an async transport entry point such
373    /// as `JsonRpcDispatcher::dispatch_async`).
374    pub fn call_tool(
375        &self,
376        name: &str,
377        params: serde_json::Value,
378    ) -> crate::error::Result<serde_json::Value> {
379        let handler = self.handlers.get(name).ok_or_else(|| {
380            // Distinguish "no such tool" from "registered, but async-only" —
381            // reporting the latter as unknown sends callers hunting a
382            // registration bug that isn't there.
383            if self.async_handlers.contains_key(name) {
384                crate::error::KernelError::Config(format!(
385                    "tool '{name}' has only an async handler; use call_tool_async"
386                ))
387            } else {
388                crate::error::KernelError::Config(format!("unknown tool: {name}"))
389            }
390        })?;
391        handler(params)
392    }
393
394    /// Call a tool by name, awaiting an async handler if one is registered and
395    /// otherwise falling back to the synchronous handler. Errors if the tool is
396    /// unknown. This is the entry point used by async transports (e.g. Streamable HTTP).
397    pub async fn call_tool_async(
398        &self,
399        name: &str,
400        params: serde_json::Value,
401    ) -> crate::error::Result<serde_json::Value> {
402        if let Some(handler) = self.async_handlers.get(name) {
403            return handler.call(params).await;
404        }
405        if let Some(handler) = self.handlers.get(name) {
406            return handler(params);
407        }
408        Err(crate::error::KernelError::Config(format!(
409            "unknown tool: {name}"
410        )))
411    }
412
413    /// Resolve the protocol version to report in `initialize`.
414    ///
415    /// `initialize` selects legacy (handshake) semantics, so only legacy
416    /// revisions may be negotiated here: echo `requested` when it is a legacy
417    /// version from [`SUPPORTED_PROTOCOL_VERSIONS`], otherwise fall back to
418    /// [`LEGACY_LATEST_PROTOCOL_VERSION`]. A handshake client must never be
419    /// handed a modern revision — statelessness would contradict the
420    /// handshake it just performed.
421    pub fn negotiate_protocol_version(&self, requested: Option<&str>) -> &'static str {
422        match requested {
423            Some(v) => LEGACY_PROTOCOL_VERSIONS
424                .iter()
425                .find(|&&s| s == v)
426                .copied()
427                .unwrap_or(LEGACY_LATEST_PROTOCOL_VERSION),
428            None => LEGACY_LATEST_PROTOCOL_VERSION,
429        }
430    }
431
432    /// The capabilities object shared by `initialize` and `server/discover`.
433    fn capabilities(&self) -> serde_json::Value {
434        let mut capabilities = serde_json::json!({
435            "tools": { "listChanged": false },
436            "resources": { "subscribe": false, "listChanged": false },
437        });
438        if !self.prompts.is_empty() {
439            capabilities["prompts"] = serde_json::json!({ "listChanged": false });
440        }
441        capabilities
442    }
443
444    /// Build the `server/discover` response (modern revisions): supported
445    /// versions, capabilities, and identity, cacheable per the
446    /// `CacheableResult` interface.
447    pub fn discover_response(&self) -> serde_json::Value {
448        serde_json::json!({
449            "supportedVersions": SUPPORTED_PROTOCOL_VERSIONS,
450            "capabilities": self.capabilities(),
451            "_meta": {
452                META_SERVER_INFO: {
453                    "name": self.server_name,
454                    "version": self.server_version,
455                }
456            },
457            "ttlMs": 3_600_000,
458            "cacheScope": "private",
459        })
460    }
461
462    /// The two messages that answer a `subscriptions/listen` request: the
463    /// mandatory acknowledgment, then the graceful-closure result.
464    ///
465    /// This server advertises `listChanged: false` everywhere and never emits
466    /// change notifications, so the agreed notification subset is empty and
467    /// the subscription is closed immediately on the spec's graceful-closure
468    /// path (server-initiated end, signaled by the empty result).
469    pub fn subscription_ack_and_close(
470        &self,
471        id: &serde_json::Value,
472    ) -> (serde_json::Value, serde_json::Value) {
473        let subscription_tag = serde_json::json!({ META_SUBSCRIPTION_ID: id });
474        let ack = serde_json::json!({
475            "jsonrpc": "2.0",
476            "method": "notifications/subscriptions/acknowledged",
477            "params": {
478                "_meta": subscription_tag,
479                "notifications": {},
480            }
481        });
482        let mut close = serde_json::json!({ "_meta": subscription_tag });
483        shape_modern_result(LATEST_PROTOCOL_VERSION, "subscriptions/listen", &mut close);
484        (ack, close)
485    }
486
487    /// Build the `initialize` response, negotiating the protocol version against
488    /// the client's requested version.
489    ///
490    /// The advertised capabilities reflect what the server actually supports:
491    /// `tools` and `resources` are always present; `prompts` is included only
492    /// when at least one prompt is registered.
493    pub fn initialize_response(&self, requested_version: Option<&str>) -> serde_json::Value {
494        serde_json::json!({
495            "protocolVersion": self.negotiate_protocol_version(requested_version),
496            "capabilities": self.capabilities(),
497            "serverInfo": {
498                "name": self.server_name,
499                "version": self.server_version,
500            }
501        })
502    }
503}
504
505#[cfg(test)]
506mod tests {
507    use super::*;
508
509    #[test]
510    fn register_and_call_tool() {
511        let mut server = McpServer::new("test", "0.1.0");
512        server.register_tool(ToolDescription {
513            name: "echo".into(),
514            description: "Echo input".into(),
515            input_schema: serde_json::json!({"type": "object"}),
516        });
517        server.set_handler("echo", Ok);
518
519        let result = server
520            .call_tool("echo", serde_json::json!({"msg": "hi"}))
521            .unwrap();
522        assert_eq!(result["msg"], "hi");
523    }
524
525    #[test]
526    fn unknown_tool_returns_error() {
527        let server = McpServer::new("test", "0.1.0");
528        let result = server.call_tool("missing", serde_json::json!(null));
529        assert!(result.is_err());
530    }
531
532    #[test]
533    fn initialize_response_shape() {
534        let server = McpServer::new("my-server", "2.0.0");
535        let resp = server.initialize_response(None);
536        assert_eq!(resp["serverInfo"]["name"], "my-server");
537        // The handshake path proposes the newest LEGACY version — a modern
538        // revision is meaningless to an initialize-speaking client.
539        assert_eq!(resp["protocolVersion"], LEGACY_LATEST_PROTOCOL_VERSION);
540        // No prompts registered → no prompts capability advertised.
541        assert!(resp["capabilities"].get("prompts").is_none());
542    }
543
544    #[test]
545    fn initialize_negotiates_supported_version() {
546        let server = McpServer::new("s", "1.0");
547        // A supported version the client asked for is echoed back.
548        assert_eq!(
549            server.initialize_response(Some("2024-11-05"))["protocolVersion"],
550            "2024-11-05"
551        );
552        // A modern revision is NOT negotiable over the handshake path —
553        // initialize selects legacy semantics, so it falls back.
554        assert_eq!(
555            server.initialize_response(Some("2026-07-28"))["protocolVersion"],
556            LEGACY_LATEST_PROTOCOL_VERSION
557        );
558        // An unknown version also falls back to the newest LEGACY version.
559        assert_eq!(
560            server.initialize_response(Some("1999-01-01"))["protocolVersion"],
561            LEGACY_LATEST_PROTOCOL_VERSION
562        );
563    }
564
565    #[test]
566    fn discover_response_lists_versions_and_identity() {
567        let mut server = McpServer::new("my-server", "2.0.0");
568        server.register_prompt(PromptDescription {
569            name: "greet".into(),
570            description: None,
571            arguments: Vec::new(),
572        });
573        let resp = server.discover_response();
574        assert_eq!(
575            resp["supportedVersions"][0], LATEST_PROTOCOL_VERSION,
576            "newest first: {resp}"
577        );
578        assert!(resp["capabilities"]["prompts"].is_object());
579        assert_eq!(resp["_meta"][META_SERVER_INFO]["name"], "my-server");
580        assert!(resp["ttlMs"].is_u64(), "cacheable result: {resp}");
581        assert_eq!(resp["cacheScope"], "private");
582    }
583
584    #[test]
585    fn shape_modern_result_adds_required_fields() {
586        // Cacheable method → resultType + ttlMs + cacheScope.
587        let mut list = serde_json::json!({ "tools": [] });
588        shape_modern_result(LATEST_PROTOCOL_VERSION, "tools/list", &mut list);
589        assert_eq!(list["resultType"], "complete");
590        assert!(list["ttlMs"].is_u64());
591        assert_eq!(list["cacheScope"], "private");
592
593        // Non-cacheable method → resultType only.
594        let mut call = serde_json::json!({ "content": [], "isError": false });
595        shape_modern_result(LATEST_PROTOCOL_VERSION, "tools/call", &mut call);
596        assert_eq!(call["resultType"], "complete");
597        assert!(call.get("ttlMs").is_none());
598
599        // Legacy version → untouched.
600        let mut legacy = serde_json::json!({ "tools": [] });
601        shape_modern_result("2025-06-18", "tools/list", &mut legacy);
602        assert!(legacy.get("resultType").is_none());
603    }
604
605    #[test]
606    fn initialize_advertises_prompts_when_registered() {
607        let mut server = McpServer::new("s", "1.0");
608        server.register_prompt(PromptDescription {
609            name: "greet".into(),
610            description: None,
611            arguments: Vec::new(),
612        });
613        let resp = server.initialize_response(None);
614        assert!(resp["capabilities"]["prompts"].is_object());
615    }
616
617    #[test]
618    fn register_and_get_prompt() {
619        let mut server = McpServer::new("s", "1.0");
620        server.register_prompt(PromptDescription {
621            name: "greet".into(),
622            description: Some("Greet someone".into()),
623            arguments: vec![crate::mcp::schema::PromptArgument {
624                name: "name".into(),
625                description: None,
626                required: true,
627                arg_type: None,
628            }],
629        });
630        server.set_prompt_handler("greet", |params| {
631            let who = params
632                .get("name")
633                .and_then(|v| v.as_str())
634                .unwrap_or("world");
635            Ok(serde_json::json!({
636                "messages": [{
637                    "role": "user",
638                    "content": { "type": "text", "text": format!("Hello, {who}!") }
639                }]
640            }))
641        });
642        assert_eq!(server.prompts().len(), 1);
643        let result = server
644            .get_prompt("greet", serde_json::json!({ "name": "Ada" }))
645            .unwrap();
646        assert_eq!(result["messages"][0]["content"]["text"], "Hello, Ada!");
647    }
648
649    #[test]
650    fn unknown_prompt_returns_error() {
651        let server = McpServer::new("s", "1.0");
652        assert!(server.get_prompt("missing", serde_json::json!({})).is_err());
653    }
654
655    #[test]
656    fn has_tool_reports_registration() {
657        let mut server = McpServer::new("s", "1.0");
658        server.set_handler("echo", Ok);
659        assert!(server.has_tool("echo"));
660        assert!(!server.has_tool("nope"));
661    }
662
663    #[test]
664    fn list_tools() {
665        let mut server = McpServer::new("test", "0.1.0");
666        server.register_tool(ToolDescription {
667            name: "a".into(),
668            description: "Tool A".into(),
669            input_schema: serde_json::json!({}),
670        });
671        server.register_tool(ToolDescription {
672            name: "b".into(),
673            description: "Tool B".into(),
674            input_schema: serde_json::json!({}),
675        });
676        assert_eq!(server.tools().len(), 2);
677    }
678
679    #[test]
680    fn read_resource() {
681        let mut server = McpServer::new("test", "0.1.0");
682        server.register_resource(ResourceDescription {
683            uri: "docs://readme".into(),
684            name: "README".into(),
685            description: Some("Project readme".into()),
686            mime_type: Some("text/markdown".into()),
687        });
688        server.set_resource_handler("docs://readme", |_params| {
689            Ok(serde_json::json!("# Hello World"))
690        });
691
692        let result = server
693            .read_resource("docs://readme", serde_json::json!({}))
694            .unwrap();
695        assert_eq!(result, serde_json::json!("# Hello World"));
696    }
697
698    #[test]
699    fn unknown_resource_returns_error() {
700        let server = McpServer::new("test", "0.1.0");
701        let result = server.read_resource("missing://uri", serde_json::json!({}));
702        assert!(result.is_err());
703    }
704
705    #[test]
706    fn no_auth_by_default() {
707        let server = McpServer::new("test", "0.1.0");
708        assert!(!server.auth_enabled());
709        assert!(server.check_auth(""));
710        assert!(server.check_auth("Bearer whatever"));
711    }
712
713    #[test]
714    fn with_bearer_auth_validates_correctly() {
715        let server = McpServer::new("test", "0.1.0").with_bearer_auth("my-token");
716        assert!(server.auth_enabled());
717        assert!(server.check_auth("Bearer my-token"));
718        assert!(!server.check_auth("Bearer wrong"));
719        assert!(!server.check_auth(""));
720    }
721
722    #[test]
723    fn with_generated_auth_returns_token() {
724        let (server, token) = McpServer::new("test", "0.1.0").with_generated_auth();
725        assert!(server.auth_enabled());
726        assert_eq!(token.len(), 32);
727        assert!(server.check_auth(&format!("Bearer {token}")));
728        assert!(!server.check_auth("Bearer bad"));
729    }
730
731    /// AC3: an async-registered tool resolves via `call_tool_async` and is awaited.
732    #[tokio::test]
733    async fn async_handler_is_awaited() {
734        let mut server = McpServer::new("test", "0.1.0");
735        server.register_tool(ToolDescription {
736            name: "async-echo".into(),
737            description: "Echo input asynchronously".into(),
738            input_schema: serde_json::json!({"type": "object"}),
739        });
740        server.set_async_handler("async-echo", |params| async move { Ok(params) });
741
742        let result = server
743            .call_tool_async("async-echo", serde_json::json!({"msg": "hi"}))
744            .await
745            .unwrap();
746        assert_eq!(result["msg"], "hi");
747    }
748
749    /// AC3: `call_tool_async` falls back to a sync handler when no async one is set.
750    #[tokio::test]
751    async fn async_dispatch_falls_back_to_sync() {
752        let mut server = McpServer::new("test", "0.1.0");
753        server.register_tool(ToolDescription {
754            name: "sync-echo".into(),
755            description: "Echo input synchronously".into(),
756            input_schema: serde_json::json!({"type": "object"}),
757        });
758        server.set_handler("sync-echo", Ok);
759
760        let result = server
761            .call_tool_async("sync-echo", serde_json::json!({"x": 1}))
762            .await
763            .unwrap();
764        assert_eq!(result["x"], 1);
765    }
766
767    #[tokio::test]
768    async fn call_tool_async_unknown_tool_errors() {
769        let server = McpServer::new("test", "0.1.0");
770        assert!(
771            server
772                .call_tool_async("missing", serde_json::json!(null))
773                .await
774                .is_err()
775        );
776    }
777}