Skip to main content

component_shape_mcp/
server.rs

1use super::*;
2
3/// A clonable collection of MCP tool definitions and in-process handlers.
4///
5/// Resources, prompts, server identity, and transport lifecycle remain owned
6/// by [`McpServer`].
7/// Output schemas are compiled at registration and shared by cloned registries.
8/// Successful structured results are checked against the advertised schema on
9/// direct, async, and protocol calls; handler error results bypass this check.
10/// See [`tool_definition`] for dialect, format, and reference behavior.
11#[derive(Clone, Default)]
12pub struct McpToolRegistry {
13    tools: BTreeMap<String, Arc<dyn ToolExecutor>>,
14}
15
16impl McpToolRegistry {
17    /// Create an empty tool registry.
18    #[must_use]
19    pub fn new() -> Self {
20        Self::default()
21    }
22
23    /// Register a synchronous MCP tool handler.
24    ///
25    /// # Errors
26    ///
27    /// Returns [`McpToolError`] when `definition` is invalid or a tool with the
28    /// same name is already registered.
29    pub fn add_tool<Call>(
30        &mut self,
31        definition: ToolDefinition,
32        call: Call,
33    ) -> Result<(), McpToolError>
34    where
35        Call: Fn(McpToolCall) -> ToolCallResult + Send + Sync + 'static,
36    {
37        let name = definition.name.to_string();
38        let output_validator =
39            definitions::validate_tool_definition_with_output(&definition)?.map(Arc::new);
40        if self.tools.contains_key(&name) {
41            return Err(McpToolError::duplicate_tool(name));
42        }
43
44        self.tools.insert(
45            name,
46            Arc::new(RegisteredTool {
47                definition,
48                output_validator,
49                call: Arc::new(move |arguments| Box::pin(std::future::ready(call(arguments)))),
50            }),
51        );
52        Ok(())
53    }
54
55    /// Register a synchronous typed MCP tool handler.
56    ///
57    /// # Errors
58    ///
59    /// Returns [`McpToolError`] when `definition` is invalid or a tool with the
60    /// same name is already registered.
61    pub fn add_typed_tool<Input, Call>(
62        &mut self,
63        definition: McpTypedTool<Input>,
64        call: Call,
65    ) -> Result<(), McpToolError>
66    where
67        Input: McpToolInput,
68        Call: Fn(Input) -> ToolCallResult + Send + Sync + 'static,
69    {
70        self.add_tool(definition.into_definition(), move |tool_call| {
71            let input = match Input::from_tool_call(tool_call) {
72                Ok(input) => input,
73                Err(error) => return tool_error_result_for(error),
74            };
75            call(input)
76        })
77    }
78
79    /// Register a typed handler with canonical Koruma domain validation.
80    ///
81    /// Strict decoding runs first, then [`validate_koruma`]. A failing value
82    /// returns structured issues without invoking the handler. Successful
83    /// results still pass the advertised output-schema checks.
84    ///
85    /// ```
86    /// use koruma_collection::numeric::RangeValidation;
87    ///
88    /// #[derive(component_shape_mcp::McpToolInput, koruma::Koruma)]
89    /// # #[mcp(crate = component_shape_mcp)]
90    /// struct BatchArgs {
91    ///     #[koruma(RangeValidation::<_>.min(1).max(5))]
92    ///     count: u32,
93    /// }
94    /// # fn main() -> Result<(), component_shape_mcp::McpToolError> {
95    /// let mut tools = component_shape_mcp::McpToolRegistry::new();
96    /// let definition = component_shape_mcp::tool_definition_for_input::<BatchArgs>(
97    ///     "batch", None, None, None,
98    /// )?;
99    /// tools.add_koruma_tool(definition, |args: BatchArgs| {
100    ///     component_shape_mcp::tool_structured_result(
101    ///         component_shape_mcp::serde_json::json!({ "count": args.count }),
102    ///     )
103    /// })?;
104    /// assert_eq!(tools.call_tool("batch", Some(
105    ///     component_shape_mcp::serde_json::json!({ "count": 0 }),
106    /// )).is_error, Some(true));
107    /// # Ok(())
108    /// # }
109    /// ```
110    ///
111    /// # Errors
112    ///
113    /// Returns [`McpToolError`] for an invalid or duplicate tool definition.
114    pub fn add_koruma_tool<Input, Call>(
115        &mut self,
116        definition: McpTypedTool<Input>,
117        call: Call,
118    ) -> Result<(), McpToolError>
119    where
120        Input: McpToolInput + koruma_core::ValidateExt,
121        Input::Error: koruma_core::ValidationIssues,
122        Call: Fn(Input) -> ToolCallResult + Send + Sync + 'static,
123    {
124        self.add_typed_tool(definition, move |input| match validate_koruma(&input) {
125            Ok(()) => call(input),
126            Err(error) => tool_error_result_for(error),
127        })
128    }
129
130    /// Register an async MCP tool handler.
131    ///
132    /// # Errors
133    ///
134    /// Returns [`McpToolError`] when `definition` is invalid or a tool with the
135    /// same name is already registered.
136    pub fn add_tool_async<Call, Fut>(
137        &mut self,
138        definition: ToolDefinition,
139        call: Call,
140    ) -> Result<(), McpToolError>
141    where
142        Call: Fn(McpToolCall) -> Fut + Send + Sync + 'static,
143        Fut: Future<Output = ToolCallResult> + Send + 'static,
144    {
145        let name = definition.name.to_string();
146        let output_validator =
147            definitions::validate_tool_definition_with_output(&definition)?.map(Arc::new);
148        if self.tools.contains_key(&name) {
149            return Err(McpToolError::duplicate_tool(name));
150        }
151
152        self.tools.insert(
153            name,
154            Arc::new(RegisteredTool {
155                definition,
156                output_validator,
157                call: Arc::new(move |arguments| Box::pin(call(arguments))),
158            }),
159        );
160        Ok(())
161    }
162
163    /// Register an async typed MCP tool handler.
164    ///
165    /// # Errors
166    ///
167    /// Returns [`McpToolError`] when `definition` is invalid or a tool with the
168    /// same name is already registered.
169    pub fn add_typed_tool_async<Input, Call, Fut>(
170        &mut self,
171        definition: McpTypedTool<Input>,
172        call: Call,
173    ) -> Result<(), McpToolError>
174    where
175        Input: McpToolInput,
176        Call: Fn(Input) -> Fut + Send + Sync + 'static,
177        Fut: Future<Output = ToolCallResult> + Send + 'static,
178    {
179        self.add_tool_async(definition.into_definition(), move |tool_call| {
180            let input = Input::from_tool_call(tool_call);
181            let future = match input {
182                Ok(input) => call(input),
183                Err(error) => {
184                    return Box::pin(std::future::ready(tool_error_result_for(error)))
185                        as ToolFuture;
186                },
187            };
188            Box::pin(future) as ToolFuture
189        })
190    }
191
192    /// Register an async typed handler with Koruma domain validation.
193    ///
194    /// Decoding and validation run before the handler creates its future.
195    /// See [`Self::add_koruma_tool`] for issue and output-schema behavior.
196    ///
197    /// # Errors
198    ///
199    /// Returns [`McpToolError`] for an invalid or duplicate tool definition.
200    pub fn add_koruma_tool_async<Input, Call, Fut>(
201        &mut self,
202        definition: McpTypedTool<Input>,
203        call: Call,
204    ) -> Result<(), McpToolError>
205    where
206        Input: McpToolInput + koruma_core::ValidateExt,
207        Input::Error: koruma_core::ValidationIssues,
208        Call: Fn(Input) -> Fut + Send + Sync + 'static,
209        Fut: Future<Output = ToolCallResult> + Send + 'static,
210    {
211        self.add_typed_tool_async(definition, move |input| match validate_koruma(&input) {
212            Ok(()) => Box::pin(call(input)) as ToolFuture,
213            Err(error) => Box::pin(std::future::ready(tool_error_result_for(error))) as ToolFuture,
214        })
215    }
216
217    /// Return registered MCP tool definitions.
218    #[must_use]
219    pub fn list_tools(&self) -> Vec<ToolDefinition> {
220        self.tools
221            .values()
222            .map(|executor| executor.definition())
223            .collect()
224    }
225
226    /// Whether a tool name is already registered.
227    #[must_use]
228    pub fn contains_tool(&self, name: &str) -> bool {
229        self.tools.contains_key(name)
230    }
231
232    /// Number of registered tools.
233    #[must_use]
234    pub fn tool_count(&self) -> usize {
235        self.tools.len()
236    }
237
238    /// Calls a registered tool and converts validation or handler failures into
239    /// a protocol-level tool result.
240    #[must_use]
241    pub fn call_tool(&self, name: &str, arguments: Option<Value>) -> ToolCallResult {
242        let call = match McpToolCall::from_value(arguments) {
243            Ok(call) => call,
244            Err(error) => return tool_error_result_for(error),
245        };
246        let call = match self.resolve_tool(name, call) {
247            Ok(resolved) => resolved,
248            Err(error) => return tool_error_result_for(error),
249        };
250        block_on_tool_future(call)
251    }
252
253    /// Asynchronously calls a registered tool and converts validation or
254    /// handler failures into a protocol-level tool result.
255    pub async fn call_tool_async(&self, name: &str, arguments: Option<Value>) -> ToolCallResult {
256        let call = match McpToolCall::from_value(arguments) {
257            Ok(call) => call,
258            Err(error) => return tool_error_result_for(error),
259        };
260        let call = match self.resolve_tool(name, call) {
261            Ok(resolved) => resolved,
262            Err(error) => return tool_error_result_for(error),
263        };
264        call.await
265    }
266
267    fn definition(&self, name: &str) -> Option<ToolDefinition> {
268        self.tools.get(name).map(|executor| executor.definition())
269    }
270
271    fn resolve_tool(&self, name: &str, call: McpToolCall) -> Result<ToolFuture, McpToolError> {
272        match self.tools.get(name) {
273            Some(executor) => Ok(executor.call(call)),
274            None => Err(McpToolError::UnknownTool {
275                name: name.to_string(),
276            }),
277        }
278    }
279}
280
281/// In-process MCP server that owns registered tools, resources, and prompts.
282#[derive(Clone)]
283pub struct McpServer {
284    pub(crate) server_name: Cow<'static, str>,
285    pub(crate) server_version: Cow<'static, str>,
286    tools: McpToolRegistry,
287    resources: BTreeMap<String, Arc<dyn ResourceReader>>,
288    resource_templates: Vec<ResourceTemplate>,
289    prompts: BTreeMap<String, Arc<dyn PromptExecutor>>,
290}
291
292impl McpServer {
293    /// Create a dynamic MCP tool server with the advertised metadata.
294    pub fn new(
295        server_name: impl Into<Cow<'static, str>>,
296        server_version: impl Into<Cow<'static, str>>,
297    ) -> Self {
298        Self {
299            server_name: server_name.into(),
300            server_version: server_version.into(),
301            tools: McpToolRegistry::new(),
302            resources: BTreeMap::new(),
303            resource_templates: Vec::new(),
304            prompts: BTreeMap::new(),
305        }
306    }
307
308    /// Start building a dynamic MCP tool server with generated registrars.
309    pub fn builder(
310        server_name: impl Into<Cow<'static, str>>,
311        server_version: impl Into<Cow<'static, str>>,
312    ) -> McpServerBuilder {
313        McpServerBuilder::new(server_name, server_version)
314    }
315
316    /// Create a dynamic MCP server from an existing tool registry.
317    #[must_use]
318    pub fn from_tool_registry(
319        server_name: impl Into<Cow<'static, str>>,
320        server_version: impl Into<Cow<'static, str>>,
321        tools: McpToolRegistry,
322    ) -> Self {
323        Self {
324            tools,
325            ..Self::new(server_name, server_version)
326        }
327    }
328
329    /// Borrow the shared tool registry.
330    #[must_use]
331    pub fn tool_registry(&self) -> &McpToolRegistry {
332        &self.tools
333    }
334
335    /// Mutably borrow the shared tool registry.
336    pub fn tool_registry_mut(&mut self) -> &mut McpToolRegistry {
337        &mut self.tools
338    }
339
340    /// Consume the server and return its tool registry.
341    #[must_use]
342    pub fn into_tool_registry(self) -> McpToolRegistry {
343        self.tools
344    }
345
346    /// Register a synchronous MCP tool handler.
347    ///
348    /// # Errors
349    ///
350    /// Returns [`McpToolError`] when `definition` is invalid or a tool with the
351    /// same name is already registered.
352    pub fn add_tool<Call>(
353        &mut self,
354        definition: ToolDefinition,
355        call: Call,
356    ) -> Result<(), McpToolError>
357    where
358        Call: Fn(McpToolCall) -> ToolCallResult + Send + Sync + 'static,
359    {
360        self.tools.add_tool(definition, call)
361    }
362
363    /// Register a synchronous typed MCP tool handler.
364    ///
365    /// # Errors
366    ///
367    /// Returns [`McpToolError`] when `definition` is invalid or a tool with the
368    /// same name is already registered.
369    pub fn add_typed_tool<Input, Call>(
370        &mut self,
371        definition: McpTypedTool<Input>,
372        call: Call,
373    ) -> Result<(), McpToolError>
374    where
375        Input: McpToolInput,
376        Call: Fn(Input) -> ToolCallResult + Send + Sync + 'static,
377    {
378        self.tools.add_typed_tool(definition, call)
379    }
380
381    /// Register a typed handler with Koruma validation before dispatch.
382    ///
383    /// # Errors
384    ///
385    /// Returns [`McpToolError`] for an invalid or duplicate tool definition.
386    pub fn add_koruma_tool<Input, Call>(
387        &mut self,
388        definition: McpTypedTool<Input>,
389        call: Call,
390    ) -> Result<(), McpToolError>
391    where
392        Input: McpToolInput + koruma_core::ValidateExt,
393        Input::Error: koruma_core::ValidationIssues,
394        Call: Fn(Input) -> ToolCallResult + Send + Sync + 'static,
395    {
396        self.tools.add_koruma_tool(definition, call)
397    }
398
399    /// Register an async MCP tool handler.
400    ///
401    /// # Errors
402    ///
403    /// Returns [`McpToolError`] when `definition` is invalid or a tool with the
404    /// same name is already registered.
405    pub fn add_tool_async<Call, Fut>(
406        &mut self,
407        definition: ToolDefinition,
408        call: Call,
409    ) -> Result<(), McpToolError>
410    where
411        Call: Fn(McpToolCall) -> Fut + Send + Sync + 'static,
412        Fut: Future<Output = ToolCallResult> + Send + 'static,
413    {
414        self.tools.add_tool_async(definition, call)
415    }
416
417    /// Register an async typed MCP tool handler.
418    ///
419    /// # Errors
420    ///
421    /// Returns [`McpToolError`] when `definition` is invalid or a tool with the
422    /// same name is already registered.
423    pub fn add_typed_tool_async<Input, Call, Fut>(
424        &mut self,
425        definition: McpTypedTool<Input>,
426        call: Call,
427    ) -> Result<(), McpToolError>
428    where
429        Input: McpToolInput,
430        Call: Fn(Input) -> Fut + Send + Sync + 'static,
431        Fut: Future<Output = ToolCallResult> + Send + 'static,
432    {
433        self.tools.add_typed_tool_async(definition, call)
434    }
435
436    /// Register an async typed handler with Koruma validation before dispatch.
437    ///
438    /// # Errors
439    ///
440    /// Returns [`McpToolError`] for an invalid or duplicate tool definition.
441    pub fn add_koruma_tool_async<Input, Call, Fut>(
442        &mut self,
443        definition: McpTypedTool<Input>,
444        call: Call,
445    ) -> Result<(), McpToolError>
446    where
447        Input: McpToolInput + koruma_core::ValidateExt,
448        Input::Error: koruma_core::ValidationIssues,
449        Call: Fn(Input) -> Fut + Send + Sync + 'static,
450        Fut: Future<Output = ToolCallResult> + Send + 'static,
451    {
452        self.tools.add_koruma_tool_async(definition, call)
453    }
454
455    /// Return registered MCP tool definitions.
456    #[must_use]
457    pub fn list_tools(&self) -> Vec<ToolDefinition> {
458        self.tools.list_tools()
459    }
460
461    /// Whether a tool name is already registered.
462    #[must_use]
463    pub fn contains_tool(&self, name: &str) -> bool {
464        self.tools.contains_tool(name)
465    }
466
467    /// Number of registered tools.
468    #[must_use]
469    pub fn tool_count(&self) -> usize {
470        self.tools.tool_count()
471    }
472
473    /// Register a static MCP resource reader.
474    ///
475    /// # Errors
476    ///
477    /// Returns [`McpToolError`] when `definition` is invalid or a resource with
478    /// the same URI is already registered.
479    pub fn add_resource<Read>(
480        &mut self,
481        definition: ResourceDefinition,
482        read: Read,
483    ) -> Result<(), McpToolError>
484    where
485        Read: Fn() -> ReadResourceResult + Send + Sync + 'static,
486    {
487        self.add_resource_async(definition, move || {
488            let result = read();
489            std::future::ready(Ok(result))
490        })
491    }
492
493    /// Register an async MCP resource reader.
494    ///
495    /// # Errors
496    ///
497    /// Returns [`McpToolError`] when `definition` is invalid or a resource with
498    /// the same URI is already registered.
499    pub fn add_resource_async<Read, Fut>(
500        &mut self,
501        definition: ResourceDefinition,
502        read: Read,
503    ) -> Result<(), McpToolError>
504    where
505        Read: Fn() -> Fut + Send + Sync + 'static,
506        Fut: Future<Output = Result<ReadResourceResult, ErrorData>> + Send + 'static,
507    {
508        let uri = definition.uri.clone();
509        validate_resource_definition(&definition)?;
510        if self.resources.contains_key(&uri) {
511            return Err(McpToolError::duplicate_resource(uri));
512        }
513
514        self.resources.insert(
515            uri,
516            Arc::new(RegisteredResource {
517                definition,
518                read: Arc::new(move || Box::pin(read())),
519            }),
520        );
521        Ok(())
522    }
523
524    /// Register an MCP resource template advertised by `resources/templates/list`.
525    ///
526    /// # Errors
527    ///
528    /// Returns [`McpToolError`] when `definition` is invalid.
529    pub fn add_resource_template(
530        &mut self,
531        definition: ResourceTemplateDefinition,
532    ) -> Result<(), McpToolError> {
533        validate_resource_template(&definition)?;
534        self.resource_templates.push(definition);
535        Ok(())
536    }
537
538    /// Return registered MCP resource definitions.
539    pub fn list_resources(&self) -> Vec<ResourceDefinition> {
540        self.resources
541            .values()
542            .map(|resource| resource.definition())
543            .collect()
544    }
545
546    /// Return registered MCP resource template definitions.
547    pub fn list_resource_templates(&self) -> Vec<ResourceTemplateDefinition> {
548        self.resource_templates.clone()
549    }
550
551    /// Whether a resource URI is already registered.
552    pub fn contains_resource(&self, uri: &str) -> bool {
553        self.resources.contains_key(uri)
554    }
555
556    /// Number of registered concrete resources.
557    pub fn resource_count(&self) -> usize {
558        self.resources.len()
559    }
560
561    /// Register a static MCP prompt.
562    ///
563    /// # Errors
564    ///
565    /// Returns [`McpToolError`] when `definition` is invalid or a prompt with
566    /// the same name is already registered.
567    pub fn add_prompt<Get>(
568        &mut self,
569        definition: PromptDefinition,
570        get: Get,
571    ) -> Result<(), McpToolError>
572    where
573        Get: Fn(Option<JsonObject>) -> GetPromptResult + Send + Sync + 'static,
574    {
575        self.add_prompt_async(definition, move |arguments| {
576            let result = get(arguments);
577            std::future::ready(Ok(result))
578        })
579    }
580
581    /// Register an async MCP prompt.
582    ///
583    /// # Errors
584    ///
585    /// Returns [`McpToolError`] when `definition` is invalid or a prompt with
586    /// the same name is already registered.
587    pub fn add_prompt_async<Get, Fut>(
588        &mut self,
589        definition: PromptDefinition,
590        get: Get,
591    ) -> Result<(), McpToolError>
592    where
593        Get: Fn(Option<JsonObject>) -> Fut + Send + Sync + 'static,
594        Fut: Future<Output = Result<GetPromptResult, ErrorData>> + Send + 'static,
595    {
596        let name = definition.name.clone();
597        validate_prompt_definition(&definition)?;
598        if self.prompts.contains_key(&name) {
599            return Err(McpToolError::duplicate_prompt(name));
600        }
601
602        self.prompts.insert(
603            name,
604            Arc::new(RegisteredPrompt {
605                definition,
606                get: Arc::new(move |arguments| Box::pin(get(arguments))),
607            }),
608        );
609        Ok(())
610    }
611
612    /// Return registered MCP prompt definitions.
613    pub fn list_prompts(&self) -> Vec<PromptDefinition> {
614        self.prompts
615            .values()
616            .map(|prompt| prompt.definition())
617            .collect()
618    }
619
620    /// Whether a prompt name is already registered.
621    pub fn contains_prompt(&self, name: &str) -> bool {
622        self.prompts.contains_key(name)
623    }
624
625    /// Number of registered prompts.
626    pub fn prompt_count(&self) -> usize {
627        self.prompts.len()
628    }
629
630    /// Calls a registered tool and converts validation or handler failures into
631    /// a protocol-level tool result.
632    #[must_use]
633    pub fn call_tool(&self, name: &str, arguments: Option<Value>) -> ToolCallResult {
634        self.tools.call_tool(name, arguments)
635    }
636
637    /// Asynchronously calls a registered tool and converts validation or
638    /// handler failures into a protocol-level tool result.
639    pub async fn call_tool_async(&self, name: &str, arguments: Option<Value>) -> ToolCallResult {
640        self.tools.call_tool_async(name, arguments).await
641    }
642
643    /// Serve this server over stdin/stdout using the MCP stdio transport.
644    ///
645    /// # Errors
646    ///
647    /// Returns an error when stdio serving fails or the service task fails.
648    pub async fn serve_stdio(self) -> ServeStdioResult {
649        let service = self.serve(stdio()).await?;
650        service.waiting().await?;
651        Ok(())
652    }
653
654    /// Serve this server over stdin/stdout on a new Tokio runtime.
655    ///
656    /// # Errors
657    ///
658    /// Returns an error when the Tokio runtime cannot be created, stdio serving
659    /// fails, or the service task fails.
660    pub fn serve_stdio_blocking(self) -> ServeStdioResult {
661        let runtime = tokio::runtime::Builder::new_multi_thread()
662            .enable_all()
663            .build()?;
664        runtime.block_on(self.serve_stdio())
665    }
666
667    fn result_meta(&self) -> MetaObject {
668        let mut meta = MetaObject::default();
669        meta.0.insert(
670            "io.modelcontextprotocol/serverInfo".to_string(),
671            json!({
672                "name": self.server_name,
673                "version": self.server_version,
674            }),
675        );
676        meta
677    }
678
679    fn prepare_cacheable_result(
680        &self,
681        protocol_version: Option<&ProtocolVersion>,
682        ttl_ms: &mut Option<u64>,
683        cache_scope: &mut Option<CacheScope>,
684        meta: &mut Option<MetaObject>,
685    ) {
686        if protocol_version.is_some_and(|version| version >= &ProtocolVersion::V_2026_07_28) {
687            *ttl_ms = Some(0);
688            *cache_scope = Some(CacheScope::Private);
689        }
690        *meta = Some(self.result_meta());
691    }
692}
693
694/// Builder for composing generated MCP server registrars.
695#[derive(Clone)]
696pub struct McpServerBuilder {
697    server: Result<McpServer, McpToolError>,
698}
699
700impl McpServerBuilder {
701    /// Create a builder with advertised server metadata.
702    pub fn new(
703        server_name: impl Into<Cow<'static, str>>,
704        server_version: impl Into<Cow<'static, str>>,
705    ) -> Self {
706        Self {
707            server: Ok(McpServer::new(server_name, server_version)),
708        }
709    }
710
711    /// Runs a registrar against the server being built.
712    pub fn register<Register>(mut self, register: Register) -> Self
713    where
714        Register: FnOnce(&mut McpServer) -> Result<(), McpToolError>,
715    {
716        if let Ok(server) = self.server.as_mut()
717            && let Err(error) = register(server)
718        {
719            self.server = Err(error);
720        }
721        self
722    }
723
724    /// Add a synchronous MCP tool to the server being built.
725    pub fn tool<Call>(self, definition: ToolDefinition, call: Call) -> Self
726    where
727        Call: Fn(McpToolCall) -> ToolCallResult + Send + Sync + 'static,
728    {
729        self.register(move |server| server.add_tool(definition, call))
730    }
731
732    /// Add a synchronous typed MCP tool to the server being built.
733    pub fn typed_tool<Input, Call>(self, definition: McpTypedTool<Input>, call: Call) -> Self
734    where
735        Input: McpToolInput,
736        Call: Fn(Input) -> ToolCallResult + Send + Sync + 'static,
737    {
738        self.register(move |server| server.add_typed_tool(definition, call))
739    }
740
741    /// Add a typed tool with Koruma domain validation before its handler.
742    pub fn koruma_tool<Input, Call>(self, definition: McpTypedTool<Input>, call: Call) -> Self
743    where
744        Input: McpToolInput + koruma_core::ValidateExt,
745        Input::Error: koruma_core::ValidationIssues,
746        Call: Fn(Input) -> ToolCallResult + Send + Sync + 'static,
747    {
748        self.register(move |server| server.add_koruma_tool(definition, call))
749    }
750
751    /// Add an async MCP tool to the server being built.
752    pub fn tool_async<Call, Fut>(self, definition: ToolDefinition, call: Call) -> Self
753    where
754        Call: Fn(McpToolCall) -> Fut + Send + Sync + 'static,
755        Fut: Future<Output = ToolCallResult> + Send + 'static,
756    {
757        self.register(move |server| server.add_tool_async(definition, call))
758    }
759
760    /// Add an async typed MCP tool to the server being built.
761    pub fn typed_tool_async<Input, Call, Fut>(
762        self,
763        definition: McpTypedTool<Input>,
764        call: Call,
765    ) -> Self
766    where
767        Input: McpToolInput,
768        Call: Fn(Input) -> Fut + Send + Sync + 'static,
769        Fut: Future<Output = ToolCallResult> + Send + 'static,
770    {
771        self.register(move |server| server.add_typed_tool_async(definition, call))
772    }
773
774    /// Add an async typed tool with Koruma validation before creating its future.
775    pub fn koruma_tool_async<Input, Call, Fut>(
776        self,
777        definition: McpTypedTool<Input>,
778        call: Call,
779    ) -> Self
780    where
781        Input: McpToolInput + koruma_core::ValidateExt,
782        Input::Error: koruma_core::ValidationIssues,
783        Call: Fn(Input) -> Fut + Send + Sync + 'static,
784        Fut: Future<Output = ToolCallResult> + Send + 'static,
785    {
786        self.register(move |server| server.add_koruma_tool_async(definition, call))
787    }
788
789    /// Add a static MCP resource to the server being built.
790    pub fn resource<Read>(self, definition: ResourceDefinition, read: Read) -> Self
791    where
792        Read: Fn() -> ReadResourceResult + Send + Sync + 'static,
793    {
794        self.register(move |server| server.add_resource(definition, read))
795    }
796
797    /// Add an async MCP resource to the server being built.
798    pub fn resource_async<Read, Fut>(self, definition: ResourceDefinition, read: Read) -> Self
799    where
800        Read: Fn() -> Fut + Send + Sync + 'static,
801        Fut: Future<Output = Result<ReadResourceResult, ErrorData>> + Send + 'static,
802    {
803        self.register(move |server| server.add_resource_async(definition, read))
804    }
805
806    /// Add an MCP resource template to the server being built.
807    pub fn resource_template(self, definition: ResourceTemplateDefinition) -> Self {
808        self.register(move |server| server.add_resource_template(definition))
809    }
810
811    /// Add a static MCP prompt to the server being built.
812    pub fn prompt<Get>(self, definition: PromptDefinition, get: Get) -> Self
813    where
814        Get: Fn(Option<JsonObject>) -> GetPromptResult + Send + Sync + 'static,
815    {
816        self.register(move |server| server.add_prompt(definition, get))
817    }
818
819    /// Add an async MCP prompt to the server being built.
820    pub fn prompt_async<Get, Fut>(self, definition: PromptDefinition, get: Get) -> Self
821    where
822        Get: Fn(Option<JsonObject>) -> Fut + Send + Sync + 'static,
823        Fut: Future<Output = Result<GetPromptResult, ErrorData>> + Send + 'static,
824    {
825        self.register(move |server| server.add_prompt_async(definition, get))
826    }
827
828    /// Finish the builder and return the composed server.
829    ///
830    /// # Errors
831    ///
832    /// Returns the first [`McpToolError`] produced by a builder registrar.
833    pub fn build(self) -> Result<McpServer, McpToolError> {
834        self.server
835    }
836
837    /// Build and serve this server over stdin/stdout using the MCP stdio transport.
838    ///
839    /// # Errors
840    ///
841    /// Returns an error when a builder registrar fails, stdio serving fails, or
842    /// the service task fails.
843    pub async fn serve_stdio(self) -> ServeStdioResult {
844        self.build()?.serve_stdio().await
845    }
846
847    /// Build and serve this server over stdin/stdout on a new Tokio runtime.
848    ///
849    /// # Errors
850    ///
851    /// Returns an error when a builder registrar fails, the Tokio runtime cannot
852    /// be created, stdio serving fails, or the service task fails.
853    pub fn serve_stdio_blocking(self) -> ServeStdioResult {
854        self.build()?.serve_stdio_blocking()
855    }
856}
857
858impl ServerHandler for McpServer {
859    fn get_info(&self) -> ServerConfig {
860        let mut capabilities = ServerCapabilities::builder().enable_tools().build();
861        capabilities.resources = (!self.resources.is_empty()
862            || !self.resource_templates.is_empty())
863        .then(Default::default);
864        capabilities.prompts = (!self.prompts.is_empty()).then(Default::default);
865        ServerConfig::new(capabilities)
866            .with_protocol_version(ProtocolVersion::V_2026_07_28)
867            .with_server_info(Implementation::new(
868                self.server_name.clone(),
869                self.server_version.clone(),
870            ))
871    }
872
873    fn list_tools(
874        &self,
875        _request: Option<PaginatedRequestParams>,
876        context: RequestContext<RoleServer>,
877    ) -> impl Future<Output = Result<ListToolsResult, ErrorData>> + MaybeSendFuture + '_ {
878        let mut result = ListToolsResult::with_all_items(self.list_tools());
879        self.prepare_cacheable_result(
880            context.protocol_version().as_ref(),
881            &mut result.ttl_ms,
882            &mut result.cache_scope,
883            &mut result.meta,
884        );
885        std::future::ready(Ok(result))
886    }
887
888    fn get_tool(&self, name: &str) -> Option<Tool> {
889        self.tools.definition(name)
890    }
891
892    fn call_tool(
893        &self,
894        request: CallToolRequestParams,
895        _context: RequestContext<RoleServer>,
896    ) -> impl Future<Output = Result<CallToolResponse, ErrorData>> + MaybeSendFuture + '_ {
897        let name = request.name.to_string();
898        let call = McpToolCall::new(request.arguments.unwrap_or_default());
899        let call = match self.tools.resolve_tool(&name, call) {
900            Ok(call) => call,
901            Err(error) => {
902                let result = tool_error_result_for(error);
903                Box::pin(std::future::ready(result))
904            },
905        };
906        let result_meta = self.result_meta();
907        async move {
908            let mut result = call.await;
909            result.meta = Some(result_meta);
910            Ok(result.into())
911        }
912    }
913
914    fn list_resources(
915        &self,
916        _request: Option<PaginatedRequestParams>,
917        context: RequestContext<RoleServer>,
918    ) -> impl Future<Output = Result<ListResourcesResult, ErrorData>> + MaybeSendFuture + '_ {
919        let mut result = ListResourcesResult::with_all_items(self.list_resources());
920        self.prepare_cacheable_result(
921            context.protocol_version().as_ref(),
922            &mut result.ttl_ms,
923            &mut result.cache_scope,
924            &mut result.meta,
925        );
926        std::future::ready(Ok(result))
927    }
928
929    fn list_resource_templates(
930        &self,
931        _request: Option<PaginatedRequestParams>,
932        context: RequestContext<RoleServer>,
933    ) -> impl Future<Output = Result<ListResourceTemplatesResult, ErrorData>> + MaybeSendFuture + '_
934    {
935        let mut result =
936            ListResourceTemplatesResult::with_all_items(self.list_resource_templates());
937        self.prepare_cacheable_result(
938            context.protocol_version().as_ref(),
939            &mut result.ttl_ms,
940            &mut result.cache_scope,
941            &mut result.meta,
942        );
943        std::future::ready(Ok(result))
944    }
945
946    fn read_resource(
947        &self,
948        request: ReadResourceRequestParams,
949        context: RequestContext<RoleServer>,
950    ) -> impl Future<Output = Result<ReadResourceResponse, ErrorData>> + MaybeSendFuture + '_ {
951        let uri = request.uri;
952        let read = self.resources.get(&uri).map(|resource| resource.read());
953        let protocol_version = context.protocol_version();
954        let result_meta = self.result_meta();
955        async move {
956            match read {
957                Some(read) => {
958                    let mut result = read.await?;
959                    if protocol_version
960                        .as_ref()
961                        .is_some_and(|version| version >= &ProtocolVersion::V_2026_07_28)
962                    {
963                        result.ttl_ms = Some(0);
964                        result.cache_scope = Some(CacheScope::Private);
965                    }
966                    result.meta = Some(result_meta);
967                    Ok(result.into())
968                },
969                None => Err(ErrorData::resource_not_found(
970                    format!("resource `{uri}` not found"),
971                    Some(McpToolError::unknown_resource(uri).to_structured_value()),
972                )),
973            }
974        }
975    }
976
977    fn list_prompts(
978        &self,
979        _request: Option<PaginatedRequestParams>,
980        context: RequestContext<RoleServer>,
981    ) -> impl Future<Output = Result<ListPromptsResult, ErrorData>> + MaybeSendFuture + '_ {
982        let mut result = ListPromptsResult::with_all_items(self.list_prompts());
983        self.prepare_cacheable_result(
984            context.protocol_version().as_ref(),
985            &mut result.ttl_ms,
986            &mut result.cache_scope,
987            &mut result.meta,
988        );
989        std::future::ready(Ok(result))
990    }
991
992    fn get_prompt(
993        &self,
994        request: GetPromptRequestParams,
995        _context: RequestContext<RoleServer>,
996    ) -> impl Future<Output = Result<GetPromptResponse, ErrorData>> + MaybeSendFuture + '_ {
997        let name = request.name;
998        let get = self
999            .prompts
1000            .get(&name)
1001            .map(|prompt| prompt.get(request.arguments));
1002        let result_meta = self.result_meta();
1003        async move {
1004            match get {
1005                Some(get) => {
1006                    let mut result = get.await?;
1007                    result.meta = Some(result_meta);
1008                    Ok(result.into())
1009                },
1010                None => Err(ErrorData::invalid_params(
1011                    format!("prompt `{name}` not found"),
1012                    Some(McpToolError::unknown_prompt(name).to_structured_value()),
1013                )),
1014            }
1015        }
1016    }
1017}
1018
1019trait ToolExecutor: Send + Sync {
1020    fn definition(&self) -> ToolDefinition;
1021    fn call(&self, call: McpToolCall) -> ToolFuture;
1022}
1023
1024struct RegisteredTool {
1025    definition: ToolDefinition,
1026    output_validator: Option<Arc<jsonschema::Validator>>,
1027    call: Arc<dyn Fn(McpToolCall) -> ToolFuture + Send + Sync>,
1028}
1029
1030impl ToolExecutor for RegisteredTool {
1031    fn definition(&self) -> ToolDefinition {
1032        self.definition.clone()
1033    }
1034
1035    fn call(&self, call: McpToolCall) -> ToolFuture {
1036        let future = (self.call)(call);
1037        let Some(validator) = self.output_validator.clone() else {
1038            return future;
1039        };
1040        let name = self.definition.name.clone();
1041        Box::pin(async move { validate_tool_call_result(&name, &validator, future.await) })
1042    }
1043}
1044
1045trait ResourceReader: Send + Sync {
1046    fn definition(&self) -> ResourceDefinition;
1047    fn read(&self) -> ResourceFuture;
1048}
1049
1050struct RegisteredResource {
1051    definition: ResourceDefinition,
1052    read: Arc<dyn Fn() -> ResourceFuture + Send + Sync>,
1053}
1054
1055impl ResourceReader for RegisteredResource {
1056    fn definition(&self) -> ResourceDefinition {
1057        self.definition.clone()
1058    }
1059
1060    fn read(&self) -> ResourceFuture {
1061        (self.read)()
1062    }
1063}
1064
1065trait PromptExecutor: Send + Sync {
1066    fn definition(&self) -> PromptDefinition;
1067    fn get(&self, arguments: Option<JsonObject>) -> PromptFuture;
1068}
1069
1070struct RegisteredPrompt {
1071    definition: PromptDefinition,
1072    get: Arc<dyn Fn(Option<JsonObject>) -> PromptFuture + Send + Sync>,
1073}
1074
1075impl PromptExecutor for RegisteredPrompt {
1076    fn definition(&self) -> PromptDefinition {
1077        self.definition.clone()
1078    }
1079
1080    fn get(&self, arguments: Option<JsonObject>) -> PromptFuture {
1081        (self.get)(arguments)
1082    }
1083}
1084
1085fn validate_tool_call_result(
1086    tool_name: &str,
1087    output_validator: &jsonschema::Validator,
1088    result: ToolCallResult,
1089) -> ToolCallResult {
1090    if result.is_error == Some(true) {
1091        return result;
1092    }
1093
1094    let Some(structured_content) = result.structured_content.as_ref() else {
1095        return tool_error_result_for(McpToolError::invalid_tool_output(
1096            tool_name,
1097            "tool declares output_schema but returned no structured_content",
1098        ));
1099    };
1100
1101    if let Err(error) = output_validator.validate(structured_content) {
1102        return tool_error_result_for(McpToolError::invalid_tool_output(
1103            tool_name,
1104            format!(
1105                "structured_content{}: {}",
1106                error.instance_path(),
1107                error.masked()
1108            ),
1109        ));
1110    }
1111
1112    result
1113}
1114
1115fn block_on_tool_future(future: ToolFuture) -> ToolCallResult {
1116    let join = std::thread::spawn(move || {
1117        let runtime = tokio::runtime::Builder::new_current_thread()
1118            .enable_all()
1119            .build()?;
1120        Ok::<_, Box<dyn std::error::Error + Send + Sync>>(runtime.block_on(future))
1121    })
1122    .join();
1123
1124    match join {
1125        Ok(Ok(result)) => result,
1126        Ok(Err(error)) => tool_error_result_for(McpToolError::handler(format!(
1127            "failed to run async tool handler: {error}"
1128        ))),
1129        Err(_) => {
1130            tool_error_result_for(McpToolError::handler("async tool handler runtime panicked"))
1131        },
1132    }
1133}