1use std::{collections::HashMap, sync::Arc};
2
3use schemars::{JsonSchema, Schema, schema_for};
4
5use rig_core::{
6 memory::ConversationMemory,
7 message::ToolChoice,
8 vector_store::{VectorSearchRequest, VectorStoreIndexDyn},
9};
10
11use crate::{
12 agent::hook::{
13 AgentHook, CompletionCall, CompletionCallAction, HookContext, HookStack, RequestPatch,
14 },
15 completion::{CompletionModel, Document},
16 tool::{
17 DynamicTool, PortableDynamicTool, Tool, ToolSet,
18 server::{ToolServer, ToolServerHandle},
19 },
20};
21
22#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
23#[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
24use crate::tool::rmcp::McpTool as RmcpTool;
25
26use super::{Agent, OutputMode};
27
28struct DynamicContext<I> {
29 samples: usize,
30 index: I,
31}
32
33impl<I> AgentHook for DynamicContext<I>
34where
35 I: VectorStoreIndexDyn,
36{
37 async fn on_completion_call(
38 &self,
39 _ctx: &HookContext,
40 event: CompletionCall<'_>,
41 ) -> CompletionCallAction {
42 let query = event.prompt.rag_text().or_else(|| {
43 event
44 .history
45 .iter()
46 .rev()
47 .find_map(|message| message.rag_text())
48 });
49 let Some(query) = query else {
50 return CompletionCallAction::continue_run();
51 };
52
53 let request = VectorSearchRequest::builder()
54 .query(query)
55 .samples(self.samples as u64)
56 .build();
57 match self.index.top_n(request).await {
58 Ok(results) => CompletionCallAction::patch(RequestPatch::new().extra_context(
59 results.into_iter().map(|(_, id, value)| Document {
60 id,
61 text:
62 serde_json::to_string_pretty(&value).unwrap_or_else(|_| value.to_string()),
63 additional_props: Default::default(),
64 }),
65 )),
66 Err(error) => {
67 CompletionCallAction::stop(format!("failed to retrieve dynamic context: {error}"))
68 }
69 }
70 }
71}
72
73#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
77fn build_rmcp_tools(
78 tools: Vec<rmcp::model::Tool>,
79 client: rmcp::service::ServerSink,
80 timeout: Option<std::time::Duration>,
81) -> Vec<(String, RmcpTool)> {
82 tools
83 .into_iter()
84 .map(|tool| {
85 let name = tool.name.to_string();
86 let rmcp_tool = RmcpTool::from_mcp_server(tool, client.clone()).with_timeout(timeout);
87 (name, rmcp_tool)
88 })
89 .collect()
90}
91
92#[derive(Default)]
101pub struct NoToolConfig;
102
103pub struct WithToolServerHandle {
108 handle: ToolServerHandle,
109}
110
111pub struct WithBuilderTools {
118 tools: ToolSet,
119 retrieval_indexes: Vec<(usize, Arc<dyn VectorStoreIndexDyn + Send + Sync>)>,
120}
121
122pub struct AgentBuilder<M, ToolState = NoToolConfig>
149where
150 M: CompletionModel,
151{
152 name: Option<String>,
154 description: Option<String>,
156 model: M,
158 preamble: Option<String>,
160 static_context: Vec<Document>,
162 additional_params: Option<serde_json::Value>,
164 record_telemetry_content: bool,
166 max_tokens: Option<u64>,
168 temperature: Option<f64>,
170 tool_choice: Option<ToolChoice>,
172 default_max_turns: Option<usize>,
174 tool_state: ToolState,
176 hooks: HookStack,
178 output_schema: Option<schemars::Schema>,
180 output_mode: OutputMode,
182 memory: Option<Arc<dyn ConversationMemory>>,
184 default_conversation_id: Option<String>,
186}
187
188impl<M, ToolState> AgentBuilder<M, ToolState>
189where
190 M: CompletionModel,
191{
192 pub fn name(mut self, name: &str) -> Self {
194 self.name = Some(name.into());
195 self
196 }
197
198 pub fn description(mut self, description: &str) -> Self {
200 self.description = Some(description.into());
201 self
202 }
203
204 pub fn preamble(mut self, preamble: &str) -> Self {
206 self.preamble = Some(preamble.into());
207 self
208 }
209
210 pub fn without_preamble(mut self) -> Self {
212 self.preamble = None;
213 self
214 }
215
216 pub fn append_preamble(mut self, doc: &str) -> Self {
218 self.preamble = Some(format!("{}\n{}", self.preamble.unwrap_or_default(), doc));
219 self
220 }
221
222 pub fn context(mut self, doc: &str) -> Self {
224 self.static_context.push(Document {
225 id: format!("static_doc_{}", self.static_context.len()),
226 text: doc.into(),
227 additional_props: HashMap::new(),
228 });
229 self
230 }
231
232 pub fn dynamic_context<I>(self, samples: usize, index: I) -> Self
242 where
243 I: VectorStoreIndexDyn + 'static,
244 {
245 self.add_hook(DynamicContext { samples, index })
246 }
247
248 pub fn tool_choice(mut self, tool_choice: ToolChoice) -> Self {
250 self.tool_choice = Some(tool_choice);
251 self
252 }
253
254 pub fn default_max_turns(mut self, default_max_turns: usize) -> Self {
257 self.default_max_turns = Some(default_max_turns);
258 self
259 }
260
261 pub fn temperature(mut self, temperature: f64) -> Self {
263 self.temperature = Some(temperature);
264 self
265 }
266
267 pub fn max_tokens(mut self, max_tokens: u64) -> Self {
269 self.max_tokens = Some(max_tokens);
270 self
271 }
272
273 pub fn additional_params(mut self, params: serde_json::Value) -> Self {
275 self.additional_params = Some(params);
276 self
277 }
278
279 pub fn record_content_telemetry(mut self, enabled: bool) -> Self {
289 self.record_telemetry_content = enabled;
290 self
291 }
292
293 pub fn output_schema<T>(mut self) -> Self
296 where
297 T: JsonSchema,
298 {
299 self.output_schema = Some(schema_for!(T));
300 self
301 }
302
303 pub fn output_schema_raw(mut self, schema: Schema) -> Self {
305 self.output_schema = Some(schema);
306 self
307 }
308
309 pub fn output_mode(mut self, mode: OutputMode) -> Self {
314 self.output_mode = mode;
315 self
316 }
317
318 pub fn memory<B>(mut self, memory: B) -> Self
326 where
327 B: ConversationMemory + 'static,
328 {
329 self.memory = Some(Arc::new(memory));
330 self
331 }
332
333 pub fn conversation(mut self, id: impl Into<String>) -> Self {
338 self.default_conversation_id = Some(id.into());
339 self
340 }
341
342 pub fn add_hook<H>(mut self, hook: H) -> Self
350 where
351 H: AgentHook + 'static,
352 {
353 self.hooks.push(hook);
354 self
355 }
356}
357
358impl<M> AgentBuilder<M, NoToolConfig>
359where
360 M: CompletionModel,
361{
362 pub fn new(model: M) -> Self {
364 Self {
365 name: None,
366 description: None,
367 model,
368 preamble: None,
369 static_context: vec![],
370 temperature: None,
371 max_tokens: None,
372 additional_params: None,
373 record_telemetry_content: false,
374 tool_choice: None,
375 default_max_turns: None,
376 tool_state: NoToolConfig,
377 hooks: HookStack::new(),
378 output_schema: None,
379 output_mode: OutputMode::default(),
380 memory: None,
381 default_conversation_id: None,
382 }
383 }
384}
385
386impl<M> AgentBuilder<M, NoToolConfig>
387where
388 M: CompletionModel,
389{
390 pub fn tool_server_handle(
396 self,
397 handle: ToolServerHandle,
398 ) -> AgentBuilder<M, WithToolServerHandle> {
399 AgentBuilder {
400 name: self.name,
401 description: self.description,
402 model: self.model,
403 preamble: self.preamble,
404 static_context: self.static_context,
405 additional_params: self.additional_params,
406 record_telemetry_content: self.record_telemetry_content,
407 max_tokens: self.max_tokens,
408 temperature: self.temperature,
409 tool_choice: self.tool_choice,
410 default_max_turns: self.default_max_turns,
411 tool_state: WithToolServerHandle { handle },
412 hooks: self.hooks,
413 output_schema: self.output_schema,
414 output_mode: self.output_mode,
415 memory: self.memory,
416 default_conversation_id: self.default_conversation_id,
417 }
418 }
419
420 pub fn tool<T>(self, tool: T) -> AgentBuilder<M, WithBuilderTools>
425 where
426 T: Tool + 'static,
427 {
428 let mut tools = ToolSet::default();
429 tools.add_tool(tool);
430 AgentBuilder {
431 name: self.name,
432 description: self.description,
433 model: self.model,
434 preamble: self.preamble,
435 static_context: self.static_context,
436 additional_params: self.additional_params,
437 record_telemetry_content: self.record_telemetry_content,
438 max_tokens: self.max_tokens,
439 temperature: self.temperature,
440 tool_choice: self.tool_choice,
441 default_max_turns: self.default_max_turns,
442 tool_state: WithBuilderTools {
443 tools,
444 retrieval_indexes: vec![],
445 },
446 hooks: self.hooks,
447 output_schema: self.output_schema,
448 output_mode: self.output_mode,
449 memory: self.memory,
450 default_conversation_id: self.default_conversation_id,
451 }
452 }
453
454 pub fn dynamic_tool(self, tool: DynamicTool) -> AgentBuilder<M, WithBuilderTools> {
456 self.dynamic_tools(vec![tool])
457 }
458
459 pub fn portable_dynamic_tool(
461 self,
462 tool: PortableDynamicTool,
463 ) -> AgentBuilder<M, WithBuilderTools> {
464 self.dynamic_tool(DynamicTool::from_portable(tool))
465 }
466
467 pub fn dynamic_tools(self, tools: Vec<DynamicTool>) -> AgentBuilder<M, WithBuilderTools> {
472 let tools = ToolSet::from_dynamic_tools(tools);
473
474 AgentBuilder {
475 name: self.name,
476 description: self.description,
477 model: self.model,
478 preamble: self.preamble,
479 static_context: self.static_context,
480 additional_params: self.additional_params,
481 record_telemetry_content: self.record_telemetry_content,
482 max_tokens: self.max_tokens,
483 temperature: self.temperature,
484 tool_choice: self.tool_choice,
485 default_max_turns: self.default_max_turns,
486 hooks: self.hooks,
487 output_schema: self.output_schema,
488 output_mode: self.output_mode,
489 memory: self.memory,
490 default_conversation_id: self.default_conversation_id,
491 tool_state: WithBuilderTools {
492 tools,
493 retrieval_indexes: vec![],
494 },
495 }
496 }
497
498 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
505 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
506 pub fn rmcp_tool(
507 self,
508 tool: rmcp::model::Tool,
509 client: rmcp::service::ServerSink,
510 ) -> AgentBuilder<M, WithBuilderTools> {
511 self.rmcp_tool_with_timeout(tool, client, crate::tool::rmcp::DEFAULT_MCP_TOOL_TIMEOUT)
512 }
513
514 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
521 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
522 pub fn rmcp_tool_with_timeout(
523 self,
524 tool: rmcp::model::Tool,
525 client: rmcp::service::ServerSink,
526 timeout: impl Into<Option<std::time::Duration>>,
527 ) -> AgentBuilder<M, WithBuilderTools> {
528 self.with_rmcp_toolset(build_rmcp_tools(vec![tool], client, timeout.into()))
529 }
530
531 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
538 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
539 pub fn rmcp_tools(
540 self,
541 tools: Vec<rmcp::model::Tool>,
542 client: rmcp::service::ServerSink,
543 ) -> AgentBuilder<M, WithBuilderTools> {
544 self.rmcp_tools_with_timeout(tools, client, crate::tool::rmcp::DEFAULT_MCP_TOOL_TIMEOUT)
545 }
546
547 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
555 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
556 pub fn rmcp_tools_with_timeout(
557 self,
558 tools: Vec<rmcp::model::Tool>,
559 client: rmcp::service::ServerSink,
560 timeout: impl Into<Option<std::time::Duration>>,
561 ) -> AgentBuilder<M, WithBuilderTools> {
562 self.with_rmcp_toolset(build_rmcp_tools(tools, client, timeout.into()))
563 }
564
565 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
568 fn with_rmcp_toolset(
569 self,
570 built: Vec<(String, RmcpTool)>,
571 ) -> AgentBuilder<M, WithBuilderTools> {
572 AgentBuilder {
573 name: self.name,
574 description: self.description,
575 model: self.model,
576 preamble: self.preamble,
577 static_context: self.static_context,
578 additional_params: self.additional_params,
579 record_telemetry_content: self.record_telemetry_content,
580 max_tokens: self.max_tokens,
581 temperature: self.temperature,
582 tool_choice: self.tool_choice,
583 default_max_turns: self.default_max_turns,
584 hooks: self.hooks,
585 output_schema: self.output_schema,
586 output_mode: self.output_mode,
587 memory: self.memory,
588 default_conversation_id: self.default_conversation_id,
589 tool_state: WithBuilderTools {
590 tools: {
591 let mut set = ToolSet::default();
592 for (_, tool) in built {
593 set.add_erased(std::sync::Arc::new(tool));
594 }
595 set
596 },
597 retrieval_indexes: vec![],
598 },
599 }
600 }
601
602 pub fn retrieved_tools(
606 self,
607 sample: usize,
608 index: impl VectorStoreIndexDyn + Send + Sync + 'static,
609 toolset: ToolSet,
610 ) -> AgentBuilder<M, WithBuilderTools> {
611 let mut tools = ToolSet::default();
612 tools.add_retrievable_tools(toolset);
613 AgentBuilder {
614 name: self.name,
615 description: self.description,
616 model: self.model,
617 preamble: self.preamble,
618 static_context: self.static_context,
619 additional_params: self.additional_params,
620 record_telemetry_content: self.record_telemetry_content,
621 max_tokens: self.max_tokens,
622 temperature: self.temperature,
623 tool_choice: self.tool_choice,
624 default_max_turns: self.default_max_turns,
625 hooks: self.hooks,
626 output_schema: self.output_schema,
627 output_mode: self.output_mode,
628 memory: self.memory,
629 default_conversation_id: self.default_conversation_id,
630 tool_state: WithBuilderTools {
631 tools,
632 retrieval_indexes: vec![(sample, Arc::new(index))],
633 },
634 }
635 }
636
637 pub fn build(self) -> Agent<M> {
641 let tool_server_handle = ToolServer::new().run();
642
643 Agent {
644 name: self.name,
645 description: self.description,
646 model: Arc::new(self.model),
647 preamble: self.preamble,
648 static_context: self.static_context,
649 temperature: self.temperature,
650 max_tokens: self.max_tokens,
651 additional_params: self.additional_params,
652 record_telemetry_content: self.record_telemetry_content,
653 tool_choice: self.tool_choice,
654 tool_server_handle,
655 default_max_turns: self.default_max_turns,
656 hooks: self.hooks,
657 output_schema: self.output_schema,
658 output_mode: self.output_mode,
659 memory: self.memory,
660 default_conversation_id: self.default_conversation_id,
661 }
662 }
663}
664
665impl<M> AgentBuilder<M, WithToolServerHandle>
666where
667 M: CompletionModel,
668{
669 pub fn build(self) -> Agent<M> {
671 Agent {
672 name: self.name,
673 description: self.description,
674 model: Arc::new(self.model),
675 preamble: self.preamble,
676 static_context: self.static_context,
677 temperature: self.temperature,
678 max_tokens: self.max_tokens,
679 additional_params: self.additional_params,
680 record_telemetry_content: self.record_telemetry_content,
681 tool_choice: self.tool_choice,
682 tool_server_handle: self.tool_state.handle,
683 default_max_turns: self.default_max_turns,
684 hooks: self.hooks,
685 output_schema: self.output_schema,
686 output_mode: self.output_mode,
687 memory: self.memory,
688 default_conversation_id: self.default_conversation_id,
689 }
690 }
691}
692
693impl<M> AgentBuilder<M, WithBuilderTools>
694where
695 M: CompletionModel,
696{
697 pub fn tool<T>(mut self, tool: T) -> Self
699 where
700 T: Tool + 'static,
701 {
702 self.tool_state.tools.add_tool(tool);
703 self
704 }
705
706 pub fn dynamic_tool(mut self, tool: DynamicTool) -> Self {
708 self.tool_state.tools.add_dynamic_tool(tool);
709 self
710 }
711
712 pub fn portable_dynamic_tool(mut self, tool: PortableDynamicTool) -> Self {
714 self.tool_state.tools.add_portable_dynamic_tool(tool);
715 self
716 }
717
718 pub fn dynamic_tools(mut self, tools: Vec<DynamicTool>) -> Self {
720 let tools = ToolSet::from_dynamic_tools(tools);
721 self.tool_state.tools.add_tools(tools);
722 self
723 }
724
725 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
730 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
731 pub fn rmcp_tools(
732 self,
733 tools: Vec<rmcp::model::Tool>,
734 client: rmcp::service::ServerSink,
735 ) -> Self {
736 self.rmcp_tools_with_timeout(tools, client, crate::tool::rmcp::DEFAULT_MCP_TOOL_TIMEOUT)
737 }
738
739 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
746 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
747 pub fn rmcp_tools_with_timeout(
748 self,
749 tools: Vec<rmcp::model::Tool>,
750 client: rmcp::service::ServerSink,
751 timeout: impl Into<Option<std::time::Duration>>,
752 ) -> Self {
753 self.add_rmcp_tools(build_rmcp_tools(tools, client, timeout.into()))
754 }
755
756 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
757 fn add_rmcp_tools(mut self, built: Vec<(String, RmcpTool)>) -> Self {
758 for (_, tool) in built {
759 self.tool_state.tools.add_erased(std::sync::Arc::new(tool));
760 }
761
762 self
763 }
764
765 pub fn retrieved_tools(
767 mut self,
768 sample: usize,
769 index: impl VectorStoreIndexDyn + Send + Sync + 'static,
770 toolset: ToolSet,
771 ) -> Self {
772 self.tool_state
773 .retrieval_indexes
774 .push((sample, Arc::new(index)));
775 self.tool_state.tools.add_retrievable_tools(toolset);
776 self
777 }
778
779 pub fn build(self) -> Agent<M> {
785 let tool_server_handle = ToolServer::new()
786 .add_tools(self.tool_state.tools)
787 .add_retrieval_indexes(self.tool_state.retrieval_indexes)
788 .run();
789
790 Agent {
791 name: self.name,
792 description: self.description,
793 model: Arc::new(self.model),
794 preamble: self.preamble,
795 static_context: self.static_context,
796 temperature: self.temperature,
797 max_tokens: self.max_tokens,
798 additional_params: self.additional_params,
799 record_telemetry_content: self.record_telemetry_content,
800 tool_choice: self.tool_choice,
801 tool_server_handle,
802 default_max_turns: self.default_max_turns,
803 hooks: self.hooks,
804 output_schema: self.output_schema,
805 output_mode: self.output_mode,
806 memory: self.memory,
807 default_conversation_id: self.default_conversation_id,
808 }
809 }
810}
811#[cfg(test)]
812mod tests {
813 use super::*;
814 use crate::test_utils::{MockAddTool, MockCompletionModel, MockSubtractTool, MockToolIndex};
815 use crate::tool::{ToolContext, ToolExecutionError};
816
817 #[derive(Clone)]
818 struct BuilderHook;
819
820 impl AgentHook for BuilderHook {}
821
822 #[test]
823 fn hook_can_be_set_after_tool_configuration() {
824 let _agent = AgentBuilder::new(MockCompletionModel::text("ok"))
825 .tool(MockAddTool)
826 .add_hook(BuilderHook)
827 .build();
828 }
829
830 struct NamedTool;
831
832 impl NamedTool {
833 fn new() -> Self {
834 Self
835 }
836 }
837
838 impl Tool for NamedTool {
839 const NAME: &'static str = "registered_named";
840 type Error = rig::tool::ToolExecutionError;
841 type Args = serde_json::Value;
842 type Output = String;
843
844 fn description(&self) -> String {
845 "uses its canonical name".to_string()
846 }
847
848 fn parameters(&self) -> serde_json::Value {
849 serde_json::json!({"type": "object", "properties": {}})
850 }
851
852 async fn call(
853 &self,
854 _context: &mut ToolContext,
855 _args: Self::Args,
856 ) -> Result<Self::Output, ToolExecutionError> {
857 Ok("ok".to_string())
858 }
859 }
860
861 #[tokio::test]
862 async fn typed_tool_builder_paths_advertise_canonical_name() {
863 for agent in [
864 AgentBuilder::new(MockCompletionModel::text("ok"))
865 .tool(NamedTool::new())
866 .build(),
867 AgentBuilder::new(MockCompletionModel::text("ok"))
868 .tool(MockAddTool)
869 .tool(NamedTool::new())
870 .build(),
871 ] {
872 let definitions = agent.tool_server_handle.get_tool_defs(None).await.unwrap();
873 assert!(
874 definitions
875 .iter()
876 .any(|definition| definition.name == NamedTool::NAME),
877 "the provider definitions dropped the canonical tool name"
878 );
879
880 let mut context = ToolContext::new();
881 let result = agent
882 .tool_server_handle
883 .execute(NamedTool::NAME, "{}", &mut context)
884 .await;
885 assert!(result.is_success());
886 assert_eq!(result.output().as_text(), Some("ok"));
887 }
888 }
889
890 #[tokio::test]
891 async fn retrieved_tools_are_exposed_only_for_prompted_retrieval() {
892 let retrieval_only = AgentBuilder::new(MockCompletionModel::text("ok"))
893 .retrieved_tools(
894 1,
895 MockToolIndex::new(["add"]),
896 ToolSet::from_tools(vec![MockAddTool]),
897 )
898 .build();
899 assert!(
900 retrieval_only
901 .tool_server_handle
902 .get_tool_defs(None)
903 .await
904 .unwrap()
905 .is_empty()
906 );
907
908 let agent = AgentBuilder::new(MockCompletionModel::text("ok"))
909 .tool(MockSubtractTool)
910 .retrieved_tools(
911 1,
912 MockToolIndex::new(["add"]),
913 ToolSet::from_tools(vec![MockAddTool]),
914 )
915 .build();
916
917 let always = agent.tool_server_handle.get_tool_defs(None).await.unwrap();
918 assert_eq!(
919 always
920 .iter()
921 .map(|definition| definition.name.as_str())
922 .collect::<Vec<_>>(),
923 vec!["subtract"]
924 );
925
926 let with_retrieval = agent
927 .tool_server_handle
928 .get_tool_defs(Some("add two numbers".to_string()))
929 .await
930 .unwrap();
931 assert_eq!(
932 with_retrieval
933 .iter()
934 .map(|definition| definition.name.as_str())
935 .collect::<Vec<_>>(),
936 vec!["add", "subtract"]
937 );
938 }
939
940 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
945 #[tokio::test]
946 async fn build_rmcp_tools_threads_timeout_into_built_tools() {
947 use crate::tool::rmcp::DEFAULT_MCP_TOOL_TIMEOUT;
948 use crate::tool::{ToolContext, ToolErrorKind, server::ToolServer};
949 use rmcp::model::{
950 CallToolRequestParams, CallToolResult, ClientInfo, ErrorData, Implementation,
951 ProtocolVersion, ServerCapabilities, ServerInfo, Tool,
952 };
953 use rmcp::service::RequestContext;
954 use rmcp::{RoleServer, ServerHandler, ServiceExt};
955 use std::sync::Arc;
956 use std::time::Duration;
957
958 #[derive(Clone)]
959 struct HangingServer;
960 impl ServerHandler for HangingServer {
961 fn get_info(&self) -> ServerInfo {
962 ServerInfo::new(ServerCapabilities::builder().enable_tools().build())
963 .with_protocol_version(ProtocolVersion::LATEST)
964 .with_server_info(Implementation::new("builder-timeout-test", "0.1.0"))
965 }
966 async fn call_tool(
967 &self,
968 _request: CallToolRequestParams,
969 _context: RequestContext<RoleServer>,
970 ) -> Result<CallToolResult, ErrorData> {
971 std::future::pending::<Result<CallToolResult, ErrorData>>().await
972 }
973 }
974
975 fn tool(name: &str) -> Tool {
976 Tool::new(
977 name.to_string(),
978 String::new(),
979 Arc::new(serde_json::Map::new()),
980 )
981 }
982
983 let (c2s, sfc) = tokio::io::duplex(8192);
984 let (s2c, cfs) = tokio::io::duplex(8192);
985 let server_task = tokio::spawn(async move {
986 let running = HangingServer.serve((sfc, s2c)).await.expect("server start");
987 running.waiting().await.expect("server error");
988 });
989 let client = ClientInfo::default()
990 .serve((cfs, c2s))
991 .await
992 .expect("client connect");
993 let peer = client.peer().clone();
994
995 let built_default = build_rmcp_tools(
998 vec![tool("a")],
999 peer.clone(),
1000 Some(DEFAULT_MCP_TOOL_TIMEOUT),
1001 );
1002 assert_eq!(built_default[0].1.timeout(), Some(DEFAULT_MCP_TOOL_TIMEOUT));
1003 let built_none = build_rmcp_tools(vec![tool("b")], peer.clone(), None);
1004 assert_eq!(built_none[0].1.timeout(), None);
1005
1006 let built = build_rmcp_tools(
1008 vec![tool("hang_forever")],
1009 peer,
1010 Some(Duration::from_millis(200)),
1011 );
1012 assert_eq!(built.len(), 1);
1013 assert_eq!(built[0].0, "hang_forever");
1014 let handle = ToolServer::new().run();
1015 handle
1016 .add_erased_tool(Arc::new(built.into_iter().next().unwrap().1))
1017 .await;
1018 let timed = tokio::time::timeout(Duration::from_secs(5), async {
1019 let mut context = ToolContext::new();
1020 handle.execute("hang_forever", "{}", &mut context).await
1021 })
1022 .await;
1023 let result = timed.expect("built tool hung past the safety timeout");
1024 assert!(result.is_error_kind(ToolErrorKind::Timeout));
1025 assert!(result.output().render().contains("timed out"));
1026
1027 drop(client);
1028 server_task.abort();
1029 }
1030}