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/// During `initialize` the server echoes the client's requested version when it
15/// appears here, otherwise it falls back to [`LATEST_PROTOCOL_VERSION`].
16pub const SUPPORTED_PROTOCOL_VERSIONS: &[&str] = &["2025-06-18", "2025-03-26", "2024-11-05"];
17
18/// The newest MCP protocol version this server implements.
19pub const LATEST_PROTOCOL_VERSION: &str = "2025-06-18";
20
21/// Handler function type for MCP tool calls (synchronous).
22pub type Handler =
23    Box<dyn Fn(serde_json::Value) -> crate::error::Result<serde_json::Value> + Send + Sync>;
24
25/// Async tool-handler trait — the async counterpart to the synchronous [`Handler`].
26///
27/// Object-safe via `async_trait`, so an [`McpServer`] can store
28/// `Arc<dyn AsyncToolHandler>` and await it from an async transport
29/// (e.g. the HTTP/SSE transport).
30#[async_trait]
31pub trait AsyncToolHandler: Send + Sync {
32    /// Invoke the handler with the tool call parameters.
33    async fn call(&self, params: serde_json::Value) -> crate::error::Result<serde_json::Value>;
34}
35
36/// Adapts an async closure `Fn(Value) -> Future<Output = Result<Value>>` into an
37/// [`AsyncToolHandler`], so [`McpServer::set_async_handler`] accepts a plain
38/// async closure.
39struct AsyncHandlerFn<F>(F);
40
41#[async_trait]
42impl<F, Fut> AsyncToolHandler for AsyncHandlerFn<F>
43where
44    F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
45    Fut: Future<Output = crate::error::Result<serde_json::Value>> + Send,
46{
47    async fn call(&self, params: serde_json::Value) -> crate::error::Result<serde_json::Value> {
48        (self.0)(params).await
49    }
50}
51
52/// An MCP server that manages tools, resources, prompts, and dispatches calls.
53pub struct McpServer {
54    server_name: String,
55    server_version: String,
56    tools: Vec<ToolDescription>,
57    resources: Vec<ResourceDescription>,
58    prompts: Vec<PromptDescription>,
59    handlers: HashMap<String, Handler>,
60    async_handlers: HashMap<String, Arc<dyn AsyncToolHandler>>,
61    resource_handlers: HashMap<String, Handler>,
62    prompt_handlers: HashMap<String, Handler>,
63    auth: Option<BearerAuth>,
64}
65
66impl McpServer {
67    /// Create a new MCP server with no authentication.
68    pub fn new(name: impl Into<String>, version: impl Into<String>) -> Self {
69        Self {
70            server_name: name.into(),
71            server_version: version.into(),
72            tools: Vec::new(),
73            resources: Vec::new(),
74            prompts: Vec::new(),
75            handlers: HashMap::new(),
76            async_handlers: HashMap::new(),
77            resource_handlers: HashMap::new(),
78            prompt_handlers: HashMap::new(),
79            auth: None,
80        }
81    }
82
83    /// Require bearer token authentication for all requests.
84    pub fn with_bearer_auth(mut self, token: impl Into<String>) -> Self {
85        self.auth = Some(BearerAuth::new(token));
86        self
87    }
88
89    /// Generate and attach a random bearer token.
90    ///
91    /// Returns the generated token so the caller can distribute it.
92    pub fn with_generated_auth(mut self) -> (Self, String) {
93        let bearer = BearerAuth::generate();
94        let token = bearer.token().to_string();
95        self.auth = Some(bearer);
96        (self, token)
97    }
98
99    /// Validate an `Authorization` header value. Always returns `true` when no auth is configured.
100    pub fn check_auth(&self, authorization_header: &str) -> bool {
101        match &self.auth {
102            None => true,
103            Some(bearer) => bearer.validate(authorization_header),
104        }
105    }
106
107    /// Returns `true` if bearer authentication is enabled on this server.
108    pub fn auth_enabled(&self) -> bool {
109        self.auth.is_some()
110    }
111
112    /// Register a tool with the server.
113    pub fn register_tool(&mut self, tool: ToolDescription) {
114        self.tools.push(tool);
115    }
116
117    /// Register a resource with the server.
118    pub fn register_resource(&mut self, resource: ResourceDescription) {
119        self.resources.push(resource);
120    }
121
122    /// Set the handler for a tool by name.
123    pub fn set_handler(
124        &mut self,
125        tool_name: &str,
126        handler: impl Fn(serde_json::Value) -> crate::error::Result<serde_json::Value>
127        + Send
128        + Sync
129        + 'static,
130    ) {
131        self.handlers
132            .insert(tool_name.to_string(), Box::new(handler));
133    }
134
135    /// Register an async handler for a tool by name.
136    ///
137    /// `handler` is a closure returning a future (typically `async move { … }`).
138    /// Async handlers take precedence over sync handlers registered with
139    /// [`Self::set_handler`] when resolved via [`Self::call_tool_async`].
140    pub fn set_async_handler<F, Fut>(&mut self, tool_name: &str, handler: F)
141    where
142        F: Fn(serde_json::Value) -> Fut + Send + Sync + 'static,
143        Fut: Future<Output = crate::error::Result<serde_json::Value>> + Send,
144    {
145        self.async_handlers
146            .insert(tool_name.to_string(), Arc::new(AsyncHandlerFn(handler)));
147    }
148
149    /// Get the server name.
150    pub fn name(&self) -> &str {
151        &self.server_name
152    }
153
154    /// Get the server version.
155    pub fn version(&self) -> &str {
156        &self.server_version
157    }
158
159    /// List all registered tools.
160    pub fn tools(&self) -> &[ToolDescription] {
161        &self.tools
162    }
163
164    /// List all registered resources.
165    pub fn resources(&self) -> &[ResourceDescription] {
166        &self.resources
167    }
168
169    /// Set the handler for a resource by URI.
170    pub fn set_resource_handler(
171        &mut self,
172        uri: &str,
173        handler: impl Fn(serde_json::Value) -> crate::error::Result<serde_json::Value>
174        + Send
175        + Sync
176        + 'static,
177    ) {
178        self.resource_handlers
179            .insert(uri.to_string(), Box::new(handler));
180    }
181
182    /// Read a resource by URI with the given parameters.
183    pub fn read_resource(
184        &self,
185        uri: &str,
186        params: serde_json::Value,
187    ) -> crate::error::Result<serde_json::Value> {
188        let handler = self
189            .resource_handlers
190            .get(uri)
191            .ok_or_else(|| crate::error::KernelError::Config(format!("unknown resource: {uri}")))?;
192        handler(params)
193    }
194
195    /// Register a prompt with the server.
196    pub fn register_prompt(&mut self, prompt: PromptDescription) {
197        self.prompts.push(prompt);
198    }
199
200    /// Set the handler for a prompt by name.
201    ///
202    /// The handler receives the `prompts/get` arguments object and returns the
203    /// result value — typically `{ "description": ..., "messages": [...] }`.
204    pub fn set_prompt_handler(
205        &mut self,
206        prompt_name: &str,
207        handler: impl Fn(serde_json::Value) -> crate::error::Result<serde_json::Value>
208        + Send
209        + Sync
210        + 'static,
211    ) {
212        self.prompt_handlers
213            .insert(prompt_name.to_string(), Box::new(handler));
214    }
215
216    /// List all registered prompts.
217    pub fn prompts(&self) -> &[PromptDescription] {
218        &self.prompts
219    }
220
221    /// Render a prompt by name with the given arguments.
222    pub fn get_prompt(
223        &self,
224        name: &str,
225        params: serde_json::Value,
226    ) -> crate::error::Result<serde_json::Value> {
227        let handler = self
228            .prompt_handlers
229            .get(name)
230            .ok_or_else(|| crate::error::KernelError::Config(format!("unknown prompt: {name}")))?;
231        handler(params)
232    }
233
234    /// Whether a tool with `name` is registered (has a sync or async handler).
235    ///
236    /// Lets a transport distinguish an *unknown tool* (a protocol-level invalid
237    /// params error) from a tool that ran and *failed* (reported in-band with
238    /// `isError: true`).
239    pub fn has_tool(&self, name: &str) -> bool {
240        self.handlers.contains_key(name) || self.async_handlers.contains_key(name)
241    }
242
243    /// Check `arguments` against the tool's advertised `input_schema`.
244    ///
245    /// Only the two constraints a caller can already see in `tools/list` are
246    /// enforced: an `"object"` schema needs an object, and every name under
247    /// `"required"` must be present. Without this a request that violates the
248    /// advertised contract reaches the handler, where `params["x"].as_str()
249    /// .unwrap_or_default()` turns a protocol error into a silently wrong
250    /// result (an empty-string search, a note titled "untitled").
251    ///
252    /// Returns the offending message; `Ok(())` when the tool is unknown here
253    /// (the caller reports unknown tools) or declares no constraints.
254    pub fn validate_tool_args(
255        &self,
256        name: &str,
257        arguments: &serde_json::Value,
258    ) -> Result<(), String> {
259        let Some(tool) = self.tools.iter().find(|t| t.name == name) else {
260            return Ok(());
261        };
262        if tool.input_schema.get("type").and_then(|t| t.as_str()) != Some("object") {
263            return Ok(());
264        }
265        // `null` stands for "no arguments given" on both transports.
266        let obj = match arguments {
267            serde_json::Value::Null => None,
268            serde_json::Value::Object(o) => Some(o),
269            _ => return Err(format!("tool '{name}' expects an object for arguments")),
270        };
271        let Some(required) = tool.input_schema.get("required").and_then(|r| r.as_array()) else {
272            return Ok(());
273        };
274        for field in required.iter().filter_map(|f| f.as_str()) {
275            if !obj.is_some_and(|o| o.contains_key(field)) {
276                return Err(format!(
277                    "tool '{name}' is missing required argument '{field}'"
278                ));
279            }
280        }
281        Ok(())
282    }
283
284    /// Names of tools that have ONLY an async handler — these cannot run on
285    /// the synchronous transport path. Sorted for stable reporting.
286    pub fn async_only_tools(&self) -> Vec<&str> {
287        let mut names: Vec<&str> = self
288            .async_handlers
289            .keys()
290            .filter(|n| !self.handlers.contains_key(*n))
291            .map(String::as_str)
292            .collect();
293        names.sort_unstable();
294        names
295    }
296
297    /// Call a tool by name with the given parameters.
298    ///
299    /// Synchronous handlers only. A tool registered via
300    /// [`McpServer::set_async_handler`] cannot run here — use
301    /// [`McpServer::call_tool_async`] (or an async transport entry point such
302    /// as `JsonRpcDispatcher::dispatch_async`).
303    pub fn call_tool(
304        &self,
305        name: &str,
306        params: serde_json::Value,
307    ) -> crate::error::Result<serde_json::Value> {
308        let handler = self.handlers.get(name).ok_or_else(|| {
309            // Distinguish "no such tool" from "registered, but async-only" —
310            // reporting the latter as unknown sends callers hunting a
311            // registration bug that isn't there.
312            if self.async_handlers.contains_key(name) {
313                crate::error::KernelError::Config(format!(
314                    "tool '{name}' has only an async handler; use call_tool_async"
315                ))
316            } else {
317                crate::error::KernelError::Config(format!("unknown tool: {name}"))
318            }
319        })?;
320        handler(params)
321    }
322
323    /// Call a tool by name, awaiting an async handler if one is registered and
324    /// otherwise falling back to the synchronous handler. Errors if the tool is
325    /// unknown. This is the entry point used by async transports (e.g. HTTP/SSE).
326    pub async fn call_tool_async(
327        &self,
328        name: &str,
329        params: serde_json::Value,
330    ) -> crate::error::Result<serde_json::Value> {
331        if let Some(handler) = self.async_handlers.get(name) {
332            return handler.call(params).await;
333        }
334        if let Some(handler) = self.handlers.get(name) {
335            return handler(params);
336        }
337        Err(crate::error::KernelError::Config(format!(
338            "unknown tool: {name}"
339        )))
340    }
341
342    /// Resolve the protocol version to report in `initialize`.
343    ///
344    /// Echoes `requested` when it is one of [`SUPPORTED_PROTOCOL_VERSIONS`];
345    /// otherwise returns [`LATEST_PROTOCOL_VERSION`] (per the MCP spec, the
346    /// server proposes its own latest when it cannot honor the client's).
347    pub fn negotiate_protocol_version(&self, requested: Option<&str>) -> &'static str {
348        match requested {
349            Some(v) => SUPPORTED_PROTOCOL_VERSIONS
350                .iter()
351                .find(|&&s| s == v)
352                .copied()
353                .unwrap_or(LATEST_PROTOCOL_VERSION),
354            None => LATEST_PROTOCOL_VERSION,
355        }
356    }
357
358    /// Build the `initialize` response, negotiating the protocol version against
359    /// the client's requested version.
360    ///
361    /// The advertised capabilities reflect what the server actually supports:
362    /// `tools` and `resources` are always present; `prompts` is included only
363    /// when at least one prompt is registered.
364    pub fn initialize_response(&self, requested_version: Option<&str>) -> serde_json::Value {
365        let mut capabilities = serde_json::json!({
366            "tools": { "listChanged": false },
367            "resources": { "subscribe": false, "listChanged": false },
368        });
369        if !self.prompts.is_empty() {
370            capabilities["prompts"] = serde_json::json!({ "listChanged": false });
371        }
372        serde_json::json!({
373            "protocolVersion": self.negotiate_protocol_version(requested_version),
374            "capabilities": capabilities,
375            "serverInfo": {
376                "name": self.server_name,
377                "version": self.server_version,
378            }
379        })
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386
387    #[test]
388    fn register_and_call_tool() {
389        let mut server = McpServer::new("test", "0.1.0");
390        server.register_tool(ToolDescription {
391            name: "echo".into(),
392            description: "Echo input".into(),
393            input_schema: serde_json::json!({"type": "object"}),
394        });
395        server.set_handler("echo", Ok);
396
397        let result = server
398            .call_tool("echo", serde_json::json!({"msg": "hi"}))
399            .unwrap();
400        assert_eq!(result["msg"], "hi");
401    }
402
403    #[test]
404    fn unknown_tool_returns_error() {
405        let server = McpServer::new("test", "0.1.0");
406        let result = server.call_tool("missing", serde_json::json!(null));
407        assert!(result.is_err());
408    }
409
410    #[test]
411    fn initialize_response_shape() {
412        let server = McpServer::new("my-server", "2.0.0");
413        let resp = server.initialize_response(None);
414        assert_eq!(resp["serverInfo"]["name"], "my-server");
415        assert_eq!(resp["protocolVersion"], LATEST_PROTOCOL_VERSION);
416        // No prompts registered → no prompts capability advertised.
417        assert!(resp["capabilities"].get("prompts").is_none());
418    }
419
420    #[test]
421    fn initialize_negotiates_supported_version() {
422        let server = McpServer::new("s", "1.0");
423        // A supported version the client asked for is echoed back.
424        assert_eq!(
425            server.initialize_response(Some("2024-11-05"))["protocolVersion"],
426            "2024-11-05"
427        );
428        // An unsupported version falls back to the server's latest.
429        assert_eq!(
430            server.initialize_response(Some("1999-01-01"))["protocolVersion"],
431            LATEST_PROTOCOL_VERSION
432        );
433    }
434
435    #[test]
436    fn initialize_advertises_prompts_when_registered() {
437        let mut server = McpServer::new("s", "1.0");
438        server.register_prompt(PromptDescription {
439            name: "greet".into(),
440            description: None,
441            arguments: Vec::new(),
442        });
443        let resp = server.initialize_response(None);
444        assert!(resp["capabilities"]["prompts"].is_object());
445    }
446
447    #[test]
448    fn register_and_get_prompt() {
449        let mut server = McpServer::new("s", "1.0");
450        server.register_prompt(PromptDescription {
451            name: "greet".into(),
452            description: Some("Greet someone".into()),
453            arguments: vec![crate::mcp::schema::PromptArgument {
454                name: "name".into(),
455                description: None,
456                required: true,
457            }],
458        });
459        server.set_prompt_handler("greet", |params| {
460            let who = params
461                .get("name")
462                .and_then(|v| v.as_str())
463                .unwrap_or("world");
464            Ok(serde_json::json!({
465                "messages": [{
466                    "role": "user",
467                    "content": { "type": "text", "text": format!("Hello, {who}!") }
468                }]
469            }))
470        });
471        assert_eq!(server.prompts().len(), 1);
472        let result = server
473            .get_prompt("greet", serde_json::json!({ "name": "Ada" }))
474            .unwrap();
475        assert_eq!(result["messages"][0]["content"]["text"], "Hello, Ada!");
476    }
477
478    #[test]
479    fn unknown_prompt_returns_error() {
480        let server = McpServer::new("s", "1.0");
481        assert!(server.get_prompt("missing", serde_json::json!({})).is_err());
482    }
483
484    #[test]
485    fn has_tool_reports_registration() {
486        let mut server = McpServer::new("s", "1.0");
487        server.set_handler("echo", Ok);
488        assert!(server.has_tool("echo"));
489        assert!(!server.has_tool("nope"));
490    }
491
492    #[test]
493    fn list_tools() {
494        let mut server = McpServer::new("test", "0.1.0");
495        server.register_tool(ToolDescription {
496            name: "a".into(),
497            description: "Tool A".into(),
498            input_schema: serde_json::json!({}),
499        });
500        server.register_tool(ToolDescription {
501            name: "b".into(),
502            description: "Tool B".into(),
503            input_schema: serde_json::json!({}),
504        });
505        assert_eq!(server.tools().len(), 2);
506    }
507
508    #[test]
509    fn read_resource() {
510        let mut server = McpServer::new("test", "0.1.0");
511        server.register_resource(ResourceDescription {
512            uri: "docs://readme".into(),
513            name: "README".into(),
514            description: Some("Project readme".into()),
515            mime_type: Some("text/markdown".into()),
516        });
517        server.set_resource_handler("docs://readme", |_params| {
518            Ok(serde_json::json!("# Hello World"))
519        });
520
521        let result = server
522            .read_resource("docs://readme", serde_json::json!({}))
523            .unwrap();
524        assert_eq!(result, serde_json::json!("# Hello World"));
525    }
526
527    #[test]
528    fn unknown_resource_returns_error() {
529        let server = McpServer::new("test", "0.1.0");
530        let result = server.read_resource("missing://uri", serde_json::json!({}));
531        assert!(result.is_err());
532    }
533
534    #[test]
535    fn no_auth_by_default() {
536        let server = McpServer::new("test", "0.1.0");
537        assert!(!server.auth_enabled());
538        assert!(server.check_auth(""));
539        assert!(server.check_auth("Bearer whatever"));
540    }
541
542    #[test]
543    fn with_bearer_auth_validates_correctly() {
544        let server = McpServer::new("test", "0.1.0").with_bearer_auth("my-token");
545        assert!(server.auth_enabled());
546        assert!(server.check_auth("Bearer my-token"));
547        assert!(!server.check_auth("Bearer wrong"));
548        assert!(!server.check_auth(""));
549    }
550
551    #[test]
552    fn with_generated_auth_returns_token() {
553        let (server, token) = McpServer::new("test", "0.1.0").with_generated_auth();
554        assert!(server.auth_enabled());
555        assert_eq!(token.len(), 32);
556        assert!(server.check_auth(&format!("Bearer {token}")));
557        assert!(!server.check_auth("Bearer bad"));
558    }
559
560    /// AC3: an async-registered tool resolves via `call_tool_async` and is awaited.
561    #[tokio::test]
562    async fn async_handler_is_awaited() {
563        let mut server = McpServer::new("test", "0.1.0");
564        server.register_tool(ToolDescription {
565            name: "async-echo".into(),
566            description: "Echo input asynchronously".into(),
567            input_schema: serde_json::json!({"type": "object"}),
568        });
569        server.set_async_handler("async-echo", |params| async move { Ok(params) });
570
571        let result = server
572            .call_tool_async("async-echo", serde_json::json!({"msg": "hi"}))
573            .await
574            .unwrap();
575        assert_eq!(result["msg"], "hi");
576    }
577
578    /// AC3: `call_tool_async` falls back to a sync handler when no async one is set.
579    #[tokio::test]
580    async fn async_dispatch_falls_back_to_sync() {
581        let mut server = McpServer::new("test", "0.1.0");
582        server.register_tool(ToolDescription {
583            name: "sync-echo".into(),
584            description: "Echo input synchronously".into(),
585            input_schema: serde_json::json!({"type": "object"}),
586        });
587        server.set_handler("sync-echo", Ok);
588
589        let result = server
590            .call_tool_async("sync-echo", serde_json::json!({"x": 1}))
591            .await
592            .unwrap();
593        assert_eq!(result["x"], 1);
594    }
595
596    #[tokio::test]
597    async fn call_tool_async_unknown_tool_errors() {
598        let server = McpServer::new("test", "0.1.0");
599        assert!(
600            server
601                .call_tool_async("missing", serde_json::json!(null))
602                .await
603                .is_err()
604        );
605    }
606}