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    /// Call a tool by name with the given parameters.
244    pub fn call_tool(
245        &self,
246        name: &str,
247        params: serde_json::Value,
248    ) -> crate::error::Result<serde_json::Value> {
249        let handler = self
250            .handlers
251            .get(name)
252            .ok_or_else(|| crate::error::KernelError::Config(format!("unknown tool: {name}")))?;
253        handler(params)
254    }
255
256    /// Call a tool by name, awaiting an async handler if one is registered and
257    /// otherwise falling back to the synchronous handler. Errors if the tool is
258    /// unknown. This is the entry point used by async transports (e.g. HTTP/SSE).
259    pub async fn call_tool_async(
260        &self,
261        name: &str,
262        params: serde_json::Value,
263    ) -> crate::error::Result<serde_json::Value> {
264        if let Some(handler) = self.async_handlers.get(name) {
265            return handler.call(params).await;
266        }
267        if let Some(handler) = self.handlers.get(name) {
268            return handler(params);
269        }
270        Err(crate::error::KernelError::Config(format!(
271            "unknown tool: {name}"
272        )))
273    }
274
275    /// Resolve the protocol version to report in `initialize`.
276    ///
277    /// Echoes `requested` when it is one of [`SUPPORTED_PROTOCOL_VERSIONS`];
278    /// otherwise returns [`LATEST_PROTOCOL_VERSION`] (per the MCP spec, the
279    /// server proposes its own latest when it cannot honor the client's).
280    pub fn negotiate_protocol_version(&self, requested: Option<&str>) -> &'static str {
281        match requested {
282            Some(v) => SUPPORTED_PROTOCOL_VERSIONS
283                .iter()
284                .find(|&&s| s == v)
285                .copied()
286                .unwrap_or(LATEST_PROTOCOL_VERSION),
287            None => LATEST_PROTOCOL_VERSION,
288        }
289    }
290
291    /// Build the `initialize` response, negotiating the protocol version against
292    /// the client's requested version.
293    ///
294    /// The advertised capabilities reflect what the server actually supports:
295    /// `tools` and `resources` are always present; `prompts` is included only
296    /// when at least one prompt is registered.
297    pub fn initialize_response(&self, requested_version: Option<&str>) -> serde_json::Value {
298        let mut capabilities = serde_json::json!({
299            "tools": { "listChanged": false },
300            "resources": { "subscribe": false, "listChanged": false },
301        });
302        if !self.prompts.is_empty() {
303            capabilities["prompts"] = serde_json::json!({ "listChanged": false });
304        }
305        serde_json::json!({
306            "protocolVersion": self.negotiate_protocol_version(requested_version),
307            "capabilities": capabilities,
308            "serverInfo": {
309                "name": self.server_name,
310                "version": self.server_version,
311            }
312        })
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319
320    #[test]
321    fn register_and_call_tool() {
322        let mut server = McpServer::new("test", "0.1.0");
323        server.register_tool(ToolDescription {
324            name: "echo".into(),
325            description: "Echo input".into(),
326            input_schema: serde_json::json!({"type": "object"}),
327        });
328        server.set_handler("echo", Ok);
329
330        let result = server
331            .call_tool("echo", serde_json::json!({"msg": "hi"}))
332            .unwrap();
333        assert_eq!(result["msg"], "hi");
334    }
335
336    #[test]
337    fn unknown_tool_returns_error() {
338        let server = McpServer::new("test", "0.1.0");
339        let result = server.call_tool("missing", serde_json::json!(null));
340        assert!(result.is_err());
341    }
342
343    #[test]
344    fn initialize_response_shape() {
345        let server = McpServer::new("my-server", "2.0.0");
346        let resp = server.initialize_response(None);
347        assert_eq!(resp["serverInfo"]["name"], "my-server");
348        assert_eq!(resp["protocolVersion"], LATEST_PROTOCOL_VERSION);
349        // No prompts registered → no prompts capability advertised.
350        assert!(resp["capabilities"].get("prompts").is_none());
351    }
352
353    #[test]
354    fn initialize_negotiates_supported_version() {
355        let server = McpServer::new("s", "1.0");
356        // A supported version the client asked for is echoed back.
357        assert_eq!(
358            server.initialize_response(Some("2024-11-05"))["protocolVersion"],
359            "2024-11-05"
360        );
361        // An unsupported version falls back to the server's latest.
362        assert_eq!(
363            server.initialize_response(Some("1999-01-01"))["protocolVersion"],
364            LATEST_PROTOCOL_VERSION
365        );
366    }
367
368    #[test]
369    fn initialize_advertises_prompts_when_registered() {
370        let mut server = McpServer::new("s", "1.0");
371        server.register_prompt(PromptDescription {
372            name: "greet".into(),
373            description: None,
374            arguments: Vec::new(),
375        });
376        let resp = server.initialize_response(None);
377        assert!(resp["capabilities"]["prompts"].is_object());
378    }
379
380    #[test]
381    fn register_and_get_prompt() {
382        let mut server = McpServer::new("s", "1.0");
383        server.register_prompt(PromptDescription {
384            name: "greet".into(),
385            description: Some("Greet someone".into()),
386            arguments: vec![crate::mcp::schema::PromptArgument {
387                name: "name".into(),
388                description: None,
389                required: true,
390            }],
391        });
392        server.set_prompt_handler("greet", |params| {
393            let who = params
394                .get("name")
395                .and_then(|v| v.as_str())
396                .unwrap_or("world");
397            Ok(serde_json::json!({
398                "messages": [{
399                    "role": "user",
400                    "content": { "type": "text", "text": format!("Hello, {who}!") }
401                }]
402            }))
403        });
404        assert_eq!(server.prompts().len(), 1);
405        let result = server
406            .get_prompt("greet", serde_json::json!({ "name": "Ada" }))
407            .unwrap();
408        assert_eq!(result["messages"][0]["content"]["text"], "Hello, Ada!");
409    }
410
411    #[test]
412    fn unknown_prompt_returns_error() {
413        let server = McpServer::new("s", "1.0");
414        assert!(server.get_prompt("missing", serde_json::json!({})).is_err());
415    }
416
417    #[test]
418    fn has_tool_reports_registration() {
419        let mut server = McpServer::new("s", "1.0");
420        server.set_handler("echo", Ok);
421        assert!(server.has_tool("echo"));
422        assert!(!server.has_tool("nope"));
423    }
424
425    #[test]
426    fn list_tools() {
427        let mut server = McpServer::new("test", "0.1.0");
428        server.register_tool(ToolDescription {
429            name: "a".into(),
430            description: "Tool A".into(),
431            input_schema: serde_json::json!({}),
432        });
433        server.register_tool(ToolDescription {
434            name: "b".into(),
435            description: "Tool B".into(),
436            input_schema: serde_json::json!({}),
437        });
438        assert_eq!(server.tools().len(), 2);
439    }
440
441    #[test]
442    fn read_resource() {
443        let mut server = McpServer::new("test", "0.1.0");
444        server.register_resource(ResourceDescription {
445            uri: "docs://readme".into(),
446            name: "README".into(),
447            description: Some("Project readme".into()),
448            mime_type: Some("text/markdown".into()),
449        });
450        server.set_resource_handler("docs://readme", |_params| {
451            Ok(serde_json::json!("# Hello World"))
452        });
453
454        let result = server
455            .read_resource("docs://readme", serde_json::json!({}))
456            .unwrap();
457        assert_eq!(result, serde_json::json!("# Hello World"));
458    }
459
460    #[test]
461    fn unknown_resource_returns_error() {
462        let server = McpServer::new("test", "0.1.0");
463        let result = server.read_resource("missing://uri", serde_json::json!({}));
464        assert!(result.is_err());
465    }
466
467    #[test]
468    fn no_auth_by_default() {
469        let server = McpServer::new("test", "0.1.0");
470        assert!(!server.auth_enabled());
471        assert!(server.check_auth(""));
472        assert!(server.check_auth("Bearer whatever"));
473    }
474
475    #[test]
476    fn with_bearer_auth_validates_correctly() {
477        let server = McpServer::new("test", "0.1.0").with_bearer_auth("my-token");
478        assert!(server.auth_enabled());
479        assert!(server.check_auth("Bearer my-token"));
480        assert!(!server.check_auth("Bearer wrong"));
481        assert!(!server.check_auth(""));
482    }
483
484    #[test]
485    fn with_generated_auth_returns_token() {
486        let (server, token) = McpServer::new("test", "0.1.0").with_generated_auth();
487        assert!(server.auth_enabled());
488        assert_eq!(token.len(), 32);
489        assert!(server.check_auth(&format!("Bearer {token}")));
490        assert!(!server.check_auth("Bearer bad"));
491    }
492
493    /// AC3: an async-registered tool resolves via `call_tool_async` and is awaited.
494    #[tokio::test]
495    async fn async_handler_is_awaited() {
496        let mut server = McpServer::new("test", "0.1.0");
497        server.register_tool(ToolDescription {
498            name: "async-echo".into(),
499            description: "Echo input asynchronously".into(),
500            input_schema: serde_json::json!({"type": "object"}),
501        });
502        server.set_async_handler("async-echo", |params| async move { Ok(params) });
503
504        let result = server
505            .call_tool_async("async-echo", serde_json::json!({"msg": "hi"}))
506            .await
507            .unwrap();
508        assert_eq!(result["msg"], "hi");
509    }
510
511    /// AC3: `call_tool_async` falls back to a sync handler when no async one is set.
512    #[tokio::test]
513    async fn async_dispatch_falls_back_to_sync() {
514        let mut server = McpServer::new("test", "0.1.0");
515        server.register_tool(ToolDescription {
516            name: "sync-echo".into(),
517            description: "Echo input synchronously".into(),
518            input_schema: serde_json::json!({"type": "object"}),
519        });
520        server.set_handler("sync-echo", Ok);
521
522        let result = server
523            .call_tool_async("sync-echo", serde_json::json!({"x": 1}))
524            .await
525            .unwrap();
526        assert_eq!(result["x"], 1);
527    }
528
529    #[tokio::test]
530    async fn call_tool_async_unknown_tool_errors() {
531        let server = McpServer::new("test", "0.1.0");
532        assert!(
533            server
534                .call_tool_async("missing", serde_json::json!(null))
535                .await
536                .is_err()
537        );
538    }
539}