1use std::{collections::BTreeSet, sync::Arc};
2
3#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
4use std::collections::HashMap;
5
6use indexmap::IndexMap;
7use tokio::sync::RwLock;
8
9#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
10use crate::tool::ErasedTool;
11
12use crate::{
13 completion::{CompletionError, ToolDefinition},
14 tool::{
15 DynamicTool, PortableDynamicTool, RegisteredTool, Tool, ToolContext, ToolDispatch,
16 ToolResult, ToolSet, dispatch_tool,
17 },
18};
19use rig_core::vector_store::{
20 VectorSearchRequest, VectorStoreError, VectorStoreIndexDyn, request::Filter,
21};
22
23#[derive(Clone)]
29pub(crate) struct ToolRegistrySnapshot {
30 definitions: Vec<ToolDefinition>,
31 tools: IndexMap<String, RegisteredTool>,
32}
33
34impl ToolRegistrySnapshot {
35 fn new(tools: IndexMap<String, RegisteredTool>) -> Self {
36 let definitions = tools
37 .iter()
38 .map(|(name, tool)| tool.definition_with_name(name.clone()))
39 .collect();
40 Self { definitions, tools }
41 }
42
43 pub(crate) fn definitions(&self) -> &[ToolDefinition] {
45 &self.definitions
46 }
47
48 pub(crate) fn retain_names(&mut self, names: &BTreeSet<String>) {
50 self.definitions
51 .retain(|definition| names.contains(&definition.name));
52 self.tools.retain(|name, _| names.contains(name));
53 }
54
55 pub(crate) async fn dispatch(
57 &self,
58 tool_name: &str,
59 args: &str,
60 context: &ToolContext,
61 ) -> ToolDispatch {
62 let tool = self.tools.get(tool_name).cloned();
63 dispatch_tool(tool_name, args.to_string(), tool, context).await
64 }
65}
66
67struct ToolServerState {
69 retrieval_indexes: Vec<(usize, Arc<dyn VectorStoreIndexDyn + Send + Sync>)>,
71 toolset: ToolSet,
73 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
77 managed_generations: HashMap<String, ManagedToolToken>,
78}
79
80#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
81impl ToolServerState {
82 fn retire_disconnected_tools(&mut self) {
86 let disconnected = self
87 .toolset
88 .tools
89 .keys()
90 .filter(|name| self.toolset.get(name).is_none_or(|tool| !tool.is_live()))
91 .cloned()
92 .collect::<Vec<_>>();
93
94 for name in disconnected {
95 self.toolset.delete_tool(&name);
96 self.managed_generations.remove(&name);
97 tracing::debug!(tool_name = %name, "retired disconnected MCP tool registration");
98 }
99 }
100}
101
102#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
104#[derive(Clone, Debug)]
105pub(crate) struct ManagedToolToken(Arc<()>);
106
107#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
108impl ManagedToolToken {
109 fn new() -> Self {
110 Self(Arc::new(()))
111 }
112}
113
114#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
115impl PartialEq for ManagedToolToken {
116 fn eq(&self, other: &Self) -> bool {
117 Arc::ptr_eq(&self.0, &other.0)
118 }
119}
120
121#[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
122impl Eq for ManagedToolToken {}
123
124pub struct ToolServer {
129 retrieval_indexes: Vec<(usize, Arc<dyn VectorStoreIndexDyn + Send + Sync>)>,
130 toolset: ToolSet,
131}
132
133impl Default for ToolServer {
134 fn default() -> Self {
135 Self::new()
136 }
137}
138
139impl ToolServer {
140 pub fn new() -> Self {
141 Self {
142 retrieval_indexes: Vec::new(),
143 toolset: ToolSet::default(),
144 }
145 }
146
147 pub fn tool(mut self, tool: impl Tool + 'static) -> Self {
150 self.toolset.add_tool(tool);
151 self
152 }
153
154 pub fn dynamic_tool(mut self, tool: DynamicTool) -> Self {
157 self.toolset.add_dynamic_tool(tool);
158 self
159 }
160
161 pub fn dynamic_tools(self, tools: Vec<DynamicTool>) -> Self {
163 tools.into_iter().fold(self, Self::dynamic_tool)
164 }
165
166 pub fn portable_dynamic_tool(mut self, tool: PortableDynamicTool) -> Self {
168 self.toolset.add_portable_dynamic_tool(tool);
169 self
170 }
171
172 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
177 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
178 pub fn rmcp_tool(self, tool: rmcp::model::Tool, client: rmcp::service::ServerSink) -> Self {
179 self.rmcp_tool_with_timeout(tool, client, crate::tool::rmcp::DEFAULT_MCP_TOOL_TIMEOUT)
180 }
181
182 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
187 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
188 pub fn rmcp_tool_with_timeout(
189 mut self,
190 tool: rmcp::model::Tool,
191 client: rmcp::service::ServerSink,
192 timeout: impl Into<Option<std::time::Duration>>,
193 ) -> Self {
194 use crate::tool::rmcp::McpTool;
195 self.toolset.add_erased(Arc::new(
196 McpTool::from_mcp_server(tool, client).with_timeout(timeout),
197 ));
198 self
199 }
200
201 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
206 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
207 pub fn rmcp_tools(
208 self,
209 tools: Vec<rmcp::model::Tool>,
210 client: rmcp::service::ServerSink,
211 ) -> Self {
212 self.rmcp_tools_with_timeout(tools, client, crate::tool::rmcp::DEFAULT_MCP_TOOL_TIMEOUT)
213 }
214
215 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
221 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
222 pub fn rmcp_tools_with_timeout(
223 self,
224 tools: Vec<rmcp::model::Tool>,
225 client: rmcp::service::ServerSink,
226 timeout: impl Into<Option<std::time::Duration>>,
227 ) -> Self {
228 let timeout = timeout.into();
229 tools.into_iter().fold(self, |server, tool| {
230 server.rmcp_tool_with_timeout(tool, client.clone(), timeout)
231 })
232 }
233
234 pub fn retrieved_tools(
236 mut self,
237 sample: usize,
238 index: impl VectorStoreIndexDyn + Send + Sync + 'static,
239 toolset: ToolSet,
240 ) -> Self {
241 self.retrieval_indexes.push((sample, Arc::new(index)));
242 self.toolset.add_retrievable_tools(toolset);
243 self
244 }
245
246 pub fn run(self) -> ToolServerHandle {
248 ToolServerHandle(Arc::new(RwLock::new(ToolServerState {
249 retrieval_indexes: self.retrieval_indexes,
250 toolset: self.toolset,
251 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
252 managed_generations: HashMap::new(),
253 })))
254 }
255}
256
257#[derive(Clone)]
263pub struct ToolServerHandle(Arc<RwLock<ToolServerState>>);
264
265impl ToolServerHandle {
266 async fn register(&self, add: impl FnOnce(&mut ToolSet) -> String) {
269 let mut state = self.0.write().await;
270 let _name = add(&mut state.toolset);
271 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
272 state.managed_generations.remove(&_name);
273 }
274
275 pub async fn add_tool<T>(&self, tool: T)
278 where
279 T: Tool + 'static,
280 {
281 self.register(|toolset| toolset.add_tool(tool)).await
282 }
283
284 pub async fn add_dynamic_tool(&self, tool: DynamicTool) {
286 self.register(|toolset| toolset.add_dynamic_tool(tool))
287 .await
288 }
289
290 pub async fn add_portable_dynamic_tool(&self, tool: PortableDynamicTool) {
292 self.register(|toolset| toolset.add_portable_dynamic_tool(tool))
293 .await
294 }
295
296 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
299 pub(crate) async fn add_managed_erased_tools(
300 &self,
301 tools: Vec<Arc<dyn ErasedTool>>,
302 ) -> HashMap<String, ManagedToolToken> {
303 let mut state = self.0.write().await;
304 let mut managed = HashMap::with_capacity(tools.len());
305
306 for tool in tools {
307 if !tool.is_live() {
310 tracing::debug!(
311 tool_name = %tool.name(),
312 "ignored initial registration from disconnected MCP owner"
313 );
314 continue;
315 }
316
317 let name = state.toolset.add_erased(tool);
318 let token = ManagedToolToken::new();
319 state
320 .managed_generations
321 .insert(name.clone(), token.clone());
322 managed.insert(name, token);
323 }
324
325 managed
326 }
327
328 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
332 pub(crate) async fn reconcile_managed_erased_tools(
333 &self,
334 mut expected: HashMap<String, ManagedToolToken>,
335 tools: Vec<Arc<dyn ErasedTool>>,
336 ) -> HashMap<String, ManagedToolToken> {
337 let mut state = self.0.write().await;
338 let mut refreshed = HashMap::with_capacity(tools.len());
339 let mut managed_order = Vec::with_capacity(tools.len());
340 let mut seen = std::collections::HashSet::with_capacity(tools.len());
341
342 state.retire_disconnected_tools();
348
349 for tool in tools {
350 if !tool.is_live() {
354 tracing::debug!(
355 tool_name = %tool.name(),
356 "ignored registration from disconnected MCP owner"
357 );
358 continue;
359 }
360
361 let name = tool.name();
362 if !seen.insert(name.clone()) {
363 tracing::warn!(tool_name = %name, "ignoring duplicate MCP tool definition");
364 continue;
365 }
366 let present = state.toolset.contains(&name);
367 let may_register = match expected.remove(&name) {
368 Some(token) if present => state.managed_generations.get(&name) == Some(&token),
369 Some(_) => true,
373 None => !present,
374 };
375
376 if may_register {
377 state.toolset.add_erased(tool);
378 let token = ManagedToolToken::new();
379 state
380 .managed_generations
381 .insert(name.clone(), token.clone());
382 refreshed.insert(name.clone(), token);
383 managed_order.push(name);
384 } else {
385 tracing::debug!(
386 tool_name = name,
387 "MCP refresh left a newer same-name registration intact"
388 );
389 }
390 }
391
392 for (name, token) in expected {
393 if state.managed_generations.get(&name) == Some(&token) {
394 state.toolset.delete_tool(&name);
395 state.managed_generations.remove(&name);
396 }
397 }
398
399 let mut ordered_entries = Vec::with_capacity(managed_order.len());
403 for name in managed_order {
404 if let Some(entry) = state.toolset.tools.shift_remove_entry(&name) {
405 ordered_entries.push(entry);
406 }
407 }
408 for (name, registration) in ordered_entries {
409 state.toolset.tools.insert(name, registration);
410 }
411
412 refreshed
413 }
414
415 pub async fn append_toolset(&self, toolset: ToolSet) {
418 let mut state = self.0.write().await;
419 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
420 let names = toolset.tools.keys().cloned().collect::<Vec<_>>();
421 state.toolset.add_tools(toolset);
422 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
423 for name in names {
424 state.managed_generations.remove(&name);
425 }
426 }
427
428 pub async fn remove_tool(&self, tool_name: &str) {
430 let mut state = self.0.write().await;
431 state.toolset.delete_tool(tool_name);
432 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
433 state.managed_generations.remove(tool_name);
434 }
435
436 pub async fn execute(
444 &self,
445 tool_name: &str,
446 args: &str,
447 context: &mut ToolContext,
448 ) -> ToolResult {
449 context.clear_dispatch_result();
450 let dispatch = self.dispatch(tool_name, args, context).await;
451 dispatch.publish_to(context)
452 }
453
454 async fn with_registry<R>(&self, f: impl FnOnce(&ToolServerState) -> R) -> R {
457 #[cfg(all(feature = "rmcp", not(target_family = "wasm")))]
458 {
459 let mut state = self.0.write().await;
460 state.retire_disconnected_tools();
461 f(&state)
462 }
463 #[cfg(not(all(feature = "rmcp", not(target_family = "wasm"))))]
464 {
465 let state = self.0.read().await;
466 f(&state)
467 }
468 }
469
470 pub(crate) async fn dispatch(
472 &self,
473 tool_name: &str,
474 args: &str,
475 context: &ToolContext,
476 ) -> ToolDispatch {
477 let tool = self
478 .with_registry(|state| state.toolset.get(tool_name).cloned())
479 .await;
480 dispatch_tool(tool_name, args.to_string(), tool, context).await
481 }
482
483 pub async fn get_tool_defs(
486 &self,
487 prompt: Option<String>,
488 ) -> Result<Vec<ToolDefinition>, ToolServerError> {
489 Ok(self.snapshot_tool_defs(prompt).await?.definitions.clone())
490 }
491
492 pub(crate) async fn snapshot_tool_defs(
499 &self,
500 prompt: Option<String>,
501 ) -> Result<ToolRegistrySnapshot, ToolServerError> {
502 let retrieval_indexes = {
503 let state = self.0.read().await;
504 state.retrieval_indexes.clone()
505 };
506
507 let dynamic_tool_ids = if let Some(ref text) = prompt {
508 let search_futures = retrieval_indexes.iter().map(|(num_sample, index)| {
510 let text = text.clone();
511 let num_sample = *num_sample;
512 let index = index.clone();
513
514 async move {
515 let req = VectorSearchRequest::builder()
516 .query(text)
517 .samples(num_sample as u64)
518 .build();
519
520 let ids = index
521 .as_ref()
522 .top_n_ids(req.map_filter(Filter::interpret))
523 .await?
524 .into_iter()
525 .map(|(_, id)| id)
526 .collect::<Vec<String>>();
527
528 Ok::<_, VectorStoreError>(ids)
529 }
530 });
531
532 futures::future::try_join_all(search_futures)
534 .await
535 .map_err(|e| {
536 ToolServerError::DefinitionError(CompletionError::RequestError(Box::new(e)))
537 })?
538 .into_iter()
539 .flatten()
540 .collect::<Vec<String>>()
541 } else {
542 Vec::new()
543 };
544
545 let tools = self
546 .with_registry(|state| snapshot_registered_tools(state, dynamic_tool_ids))
547 .await;
548
549 Ok(ToolRegistrySnapshot::new(tools))
550 }
551}
552
553fn snapshot_registered_tools(
554 state: &ToolServerState,
555 dynamic_tool_ids: Vec<String>,
556) -> IndexMap<String, RegisteredTool> {
557 let mut tools = IndexMap::new();
558 let insert = |tools: &mut IndexMap<String, RegisteredTool>, name: &str, warn_missing| {
559 if tools.contains_key(name) {
560 tracing::debug!(
561 tool_name = %name,
562 "dropping duplicate tool definition from the request"
563 );
564 return;
565 }
566 match state.toolset.get(name).cloned() {
567 Some(tool) => {
568 tools.insert(name.to_string(), tool);
569 }
570 None if warn_missing => {
573 tracing::warn!("Tool implementation not found in toolset: {name}");
574 }
575 None => {}
576 }
577 };
578
579 for name in &dynamic_tool_ids {
582 insert(&mut tools, name, true);
583 }
584 for name in state.toolset.always_exposed_names() {
585 insert(&mut tools, name, false);
586 }
587 tools
588}
589
590#[derive(Debug, thiserror::Error)]
591pub enum ToolServerError {
592 #[error("Failed to retrieve tool definitions: {0}")]
593 DefinitionError(CompletionError),
594}
595#[cfg(test)]
596mod tests {
597 use std::{
598 future::{Future, pending, poll_fn},
599 sync::{
600 Arc,
601 atomic::{AtomicBool, AtomicUsize, Ordering},
602 },
603 task::Poll,
604 time::Duration,
605 };
606
607 use crate::{
608 test_utils::{
609 BarrierMockToolIndex, MockAddTool, MockBarrierTool, MockControlledTool,
610 MockSubtractTool, MockToolIndex,
611 },
612 tool::{
613 Tool, ToolContext, ToolEmbedding, ToolExecutionError, ToolSet,
614 server::{ToolServer, ToolServerHandle},
615 },
616 };
617
618 async fn execute_tool(
619 handle: &ToolServerHandle,
620 name: &str,
621 args: &str,
622 ) -> Result<String, ToolExecutionError> {
623 execute_tool_with_context(handle, name, args, &mut ToolContext::new()).await
624 }
625
626 async fn execute_tool_with_context(
627 handle: &ToolServerHandle,
628 name: &str,
629 args: &str,
630 context: &mut ToolContext,
631 ) -> Result<String, ToolExecutionError> {
632 let result = handle.execute(name, args, context).await;
633 match result.error() {
634 Some(error) => Err(error.clone()),
635 None => Ok(result.output().render()),
636 }
637 }
638
639 struct NamedTool;
640
641 impl NamedTool {
642 fn new() -> Self {
643 Self
644 }
645 }
646
647 impl Tool for NamedTool {
648 const NAME: &'static str = "registered_named";
649 type Error = rig::tool::ToolExecutionError;
650 type Args = serde_json::Value;
651 type Output = String;
652
653 fn description(&self) -> String {
654 "uses its canonical name".to_string()
655 }
656
657 fn parameters(&self) -> serde_json::Value {
658 serde_json::json!({"type": "object", "properties": {}})
659 }
660
661 async fn call(
662 &self,
663 _context: &mut crate::tool::ToolContext,
664 _args: Self::Args,
665 ) -> Result<Self::Output, crate::tool::ToolExecutionError> {
666 Ok("ok".to_string())
667 }
668 }
669
670 struct ReplacementTool {
671 description: &'static str,
672 output: &'static str,
673 }
674
675 impl Tool for ReplacementTool {
676 const NAME: &'static str = "replacement";
677 type Error = rig::tool::ToolExecutionError;
678 type Args = serde_json::Value;
679 type Output = String;
680
681 fn description(&self) -> String {
682 self.description.to_string()
683 }
684
685 fn parameters(&self) -> serde_json::Value {
686 serde_json::json!({"type": "object", "properties": {}})
687 }
688
689 async fn call(
690 &self,
691 _context: &mut ToolContext,
692 _args: Self::Args,
693 ) -> Result<Self::Output, ToolExecutionError> {
694 Ok(self.output.to_string())
695 }
696 }
697
698 #[derive(Debug, thiserror::Error)]
699 #[error("init error")]
700 struct InitError;
701
702 impl ToolEmbedding for NamedTool {
703 type InitError = InitError;
704 type Context = ();
705 type State = ();
706
707 fn embedding_docs(&self) -> Vec<String> {
708 vec!["named retrieved tool".to_string()]
709 }
710
711 fn context(&self) -> Self::Context {}
712
713 fn init(_state: Self::State, _context: Self::Context) -> Result<Self, Self::InitError> {
714 Ok(Self::new())
715 }
716 }
717
718 #[tokio::test]
719 pub async fn test_toolserver() {
720 let server = ToolServer::new();
721
722 let handle = server.run();
723
724 handle.add_tool(MockAddTool).await;
725 let res = handle.get_tool_defs(None).await.unwrap();
726
727 assert_eq!(res.len(), 1);
728
729 let json_args_as_string =
730 serde_json::to_string(&serde_json::json!({"x": 2, "y": 5})).unwrap();
731 let res = execute_tool(&handle, "add", &json_args_as_string)
732 .await
733 .unwrap();
734 assert_eq!(res, "7");
735
736 handle.remove_tool("add").await;
737 let res = handle.get_tool_defs(None).await.unwrap();
738
739 assert_eq!(res.len(), 0);
740 }
741
742 #[tokio::test]
743 async fn definition_snapshot_pins_the_exact_tool_registration() {
744 let handle = ToolServer::new()
745 .tool(ReplacementTool {
746 description: "first schema",
747 output: "first implementation",
748 })
749 .run();
750 let snapshot = handle.snapshot_tool_defs(None).await.unwrap();
751
752 handle
753 .add_tool(ReplacementTool {
754 description: "second schema",
755 output: "second implementation",
756 })
757 .await;
758
759 assert_eq!(snapshot.definitions()[0].description, "first schema");
760 let dispatch = snapshot
761 .dispatch(ReplacementTool::NAME, "{}", &ToolContext::new())
762 .await;
763 assert_eq!(dispatch.result.output().render(), "first implementation");
764
765 let live = handle
766 .dispatch(ReplacementTool::NAME, "{}", &ToolContext::new())
767 .await;
768 assert_eq!(live.result.output().render(), "second implementation");
769
770 let next_snapshot = handle.snapshot_tool_defs(None).await.unwrap();
771 assert_eq!(next_snapshot.definitions()[0].description, "second schema");
772 let dispatch = next_snapshot
773 .dispatch(ReplacementTool::NAME, "{}", &ToolContext::new())
774 .await;
775 assert_eq!(dispatch.result.output().render(), "second implementation");
776 }
777
778 #[tokio::test]
779 pub async fn test_toolserver_append_toolset_matches_add_tool() {
780 let mut via_add_tool = {
781 let handle = ToolServer::new().run();
782 handle.add_tool(MockAddTool).await;
783 handle.add_tool(MockSubtractTool).await;
784 handle.get_tool_defs(None).await.unwrap()
785 };
786 via_add_tool.sort_by(|a, b| a.name.cmp(&b.name));
787
788 let mut via_append_toolset = {
789 let handle = ToolServer::new().run();
790 let mut toolset = ToolSet::default();
791 toolset.add_tool(MockAddTool);
792 toolset.add_tool(MockSubtractTool);
793 handle.append_toolset(toolset).await;
794 handle.get_tool_defs(None).await.unwrap()
795 };
796 via_append_toolset.sort_by(|a, b| a.name.cmp(&b.name));
797
798 assert_eq!(via_add_tool.len(), via_append_toolset.len());
799 assert!(
800 via_add_tool
801 .iter()
802 .zip(via_append_toolset.iter())
803 .all(|(a, b)| a.name == b.name),
804 "append_toolset must surface the same LLM-visible tools as add_tool",
805 );
806 }
807
808 #[tokio::test]
809 pub async fn builder_tool_uses_canonical_static_name() {
810 let handle = ToolServer::new().tool(NamedTool::new()).run();
811
812 let defs = handle.get_tool_defs(None).await.unwrap();
813 assert_eq!(defs.len(), 1);
814 assert_eq!(defs[0].name, NamedTool::NAME);
815 }
816
817 #[tokio::test]
818 pub async fn handle_add_tool_uses_canonical_static_name() {
819 let handle = ToolServer::new().run();
820 handle.add_tool(NamedTool::new()).await;
821
822 let defs = handle.get_tool_defs(None).await.unwrap();
823 assert_eq!(defs.len(), 1);
824 assert_eq!(defs[0].name, NamedTool::NAME);
825 }
826
827 #[tokio::test]
828 pub async fn retrieval_resolves_canonical_key() {
829 let mut toolset = ToolSet::default();
830 toolset.add_retrieved_tool(NamedTool::new());
831 let handle = ToolServer::new()
832 .retrieved_tools(1, MockToolIndex::new([NamedTool::NAME]), toolset)
833 .run();
834
835 let defs = handle
836 .get_tool_defs(Some("use the changing tool".to_string()))
837 .await
838 .unwrap();
839 assert_eq!(defs.len(), 1);
840 assert_eq!(defs[0].name, NamedTool::NAME);
841 }
842
843 #[tokio::test]
844 pub async fn get_tool_defs_preserves_static_registration_order() {
845 let handle = ToolServer::new().run();
846 handle.add_tool(MockSubtractTool).await;
847 handle.add_tool(MockAddTool).await;
848
849 let defs = handle.get_tool_defs(None).await.unwrap();
850 assert_eq!(
851 defs.iter().map(|def| def.name.as_str()).collect::<Vec<_>>(),
852 vec!["subtract", "add"]
853 );
854 }
855
856 #[tokio::test]
857 pub async fn get_tool_defs_dedupes_dynamic_and_static_overlap() {
858 let handle = ToolServer::new()
861 .tool(MockAddTool)
862 .retrieved_tools(1, MockToolIndex::new(["add"]), ToolSet::default())
863 .run();
864
865 let defs = handle
866 .get_tool_defs(Some("add two numbers".to_string()))
867 .await
868 .unwrap();
869 assert_eq!(
870 defs.len(),
871 1,
872 "dynamic/static name overlap must not produce duplicate declarations: {:?}",
873 defs.iter().map(|def| def.name.as_str()).collect::<Vec<_>>()
874 );
875 assert_eq!(defs[0].name, "add");
876 }
877
878 #[tokio::test]
879 async fn retrieval_registration_preserves_existing_always_exposure() {
880 let handle = ToolServer::new()
881 .tool(MockAddTool)
882 .retrieved_tools(
883 1,
884 MockToolIndex::new(["add"]),
885 ToolSet::from_tools(vec![MockAddTool]),
886 )
887 .run();
888
889 let defs = handle.get_tool_defs(None).await.unwrap();
890 assert_eq!(
891 defs.iter()
892 .map(|definition| definition.name.as_str())
893 .collect::<Vec<_>>(),
894 vec!["add"],
895 "merging a retrieval implementation must not demote an always-exposed registration"
896 );
897 }
898
899 #[tokio::test]
900 pub async fn duplicate_registration_advertises_one_definition() {
901 let handle = ToolServer::new().tool(MockAddTool).run();
902 handle.add_tool(MockAddTool).await;
903
904 let mut toolset = ToolSet::default();
905 toolset.add_tool(MockAddTool);
906 handle.append_toolset(toolset).await;
907
908 let defs = handle.get_tool_defs(None).await.unwrap();
909 assert_eq!(
910 defs.len(),
911 1,
912 "re-registering a name must not advertise duplicate declarations"
913 );
914 assert_eq!(defs[0].name, "add");
915 }
916
917 #[tokio::test]
918 pub async fn test_toolserver_retrieved_tools() {
919 let mut toolset = ToolSet::default();
921 toolset.add_tool(MockAddTool);
922 toolset.add_tool(MockSubtractTool);
923
924 let mock_index = MockToolIndex::new(["subtract"]);
926
927 let server = ToolServer::new().tool(MockAddTool).retrieved_tools(
929 1,
930 mock_index,
931 ToolSet::from_tools(vec![MockSubtractTool]),
932 );
933
934 let handle = server.run();
935
936 let res = handle.get_tool_defs(None).await.unwrap();
938 assert_eq!(res.len(), 1);
939 assert_eq!(res[0].name, "add");
940
941 let res = handle
943 .get_tool_defs(Some("calculate difference".to_string()))
944 .await
945 .unwrap();
946 assert_eq!(res.len(), 2);
947
948 let tool_names: Vec<&str> = res.iter().map(|t| t.name.as_str()).collect();
950 assert!(tool_names.contains(&"add"));
951 assert!(tool_names.contains(&"subtract"));
952 }
953
954 #[tokio::test]
955 pub async fn test_toolserver_retrieved_tools_missing_implementation() {
956 let mock_index = MockToolIndex::new(["nonexistent_tool"]);
958
959 let server =
961 ToolServer::new()
962 .tool(MockAddTool)
963 .retrieved_tools(1, mock_index, ToolSet::default());
964
965 let handle = server.run();
966
967 let res = handle
969 .get_tool_defs(Some("some query".to_string()))
970 .await
971 .unwrap();
972 assert_eq!(res.len(), 1);
973 assert_eq!(res[0].name, "add");
974 }
975
976 #[tokio::test]
977 pub async fn test_toolserver_concurrent_tool_execution() {
978 let num_calls = 3;
979 let barrier = Arc::new(tokio::sync::Barrier::new(num_calls));
980
981 let server = ToolServer::new().tool(MockBarrierTool::new(barrier.clone()));
982 let handle = server.run();
983
984 let futures: Vec<_> = (0..num_calls)
986 .map(|_| execute_tool(&handle, "barrier_tool", "{}"))
987 .collect();
988
989 let result =
992 tokio::time::timeout(Duration::from_secs(1), futures::future::join_all(futures)).await;
993
994 assert!(
995 result.is_ok(),
996 "Tool execution deadlocked! Tools are executing sequentially instead of concurrently."
997 );
998
999 for res in result.unwrap() {
1001 assert!(res.is_ok(), "Tool call failed: {:?}", res);
1002 assert_eq!(res.unwrap(), "done");
1003 }
1004 }
1005
1006 #[tokio::test]
1007 pub async fn test_toolserver_write_while_tool_running() {
1008 let started = Arc::new(tokio::sync::Notify::new());
1009 let allow_finish = Arc::new(tokio::sync::Notify::new());
1010
1011 let tool = MockControlledTool::new(started.clone(), allow_finish.clone());
1013
1014 let server = ToolServer::new().tool(tool);
1015 let handle = server.run();
1016
1017 let handle_clone = handle.clone();
1019 let call_task =
1020 tokio::spawn(async move { execute_tool(&handle_clone, "controlled", "{}").await });
1021
1022 started.notified().await;
1024
1025 let add_result =
1028 tokio::time::timeout(Duration::from_secs(1), handle.add_tool(MockAddTool)).await;
1029
1030 assert!(
1031 add_result.is_ok(),
1032 "Writing to ToolServer deadlocked! The read lock is being held across tool execution."
1033 );
1034
1035 allow_finish.notify_one();
1037 let call_result = call_task.await.unwrap();
1038 assert_eq!(call_result.unwrap(), "42");
1039 }
1040
1041 #[tokio::test]
1042 pub async fn test_toolserver_parallel_retrieval() {
1043 let barrier = Arc::new(tokio::sync::Barrier::new(2));
1045
1046 let index1 = BarrierMockToolIndex::new(barrier.clone(), "add");
1047 let index2 = BarrierMockToolIndex::new(barrier.clone(), "subtract");
1048
1049 let mut toolset = ToolSet::default();
1051 toolset.add_tool(MockAddTool);
1052 toolset.add_tool(MockSubtractTool);
1053
1054 let server = ToolServer::new()
1055 .retrieved_tools(1, index1, ToolSet::default())
1056 .retrieved_tools(1, index2, toolset);
1057
1058 let handle = server.run();
1059
1060 let get_defs = tokio::time::timeout(
1063 std::time::Duration::from_secs(1),
1064 handle.get_tool_defs(Some("do math".to_string())),
1065 )
1066 .await;
1067
1068 assert!(
1069 get_defs.is_ok(),
1070 "Dynamic tools were fetched sequentially! The first query deadlocked waiting for the second query to start."
1071 );
1072
1073 let defs = get_defs.unwrap().unwrap();
1074 assert_eq!(defs.len(), 2);
1075
1076 let tool_names: Vec<&str> = defs.iter().map(|t| t.name.as_str()).collect();
1077 assert!(tool_names.contains(&"add"));
1078 assert!(tool_names.contains(&"subtract"));
1079 }
1080
1081 #[derive(Clone)]
1082 struct SessionId(String);
1083
1084 struct CloneTrackedContext {
1085 clones: Arc<AtomicUsize>,
1086 value: usize,
1087 }
1088
1089 impl Clone for CloneTrackedContext {
1090 fn clone(&self) -> Self {
1091 self.clones.fetch_add(1, Ordering::SeqCst);
1092 Self {
1093 clones: self.clones.clone(),
1094 value: self.value,
1095 }
1096 }
1097 }
1098
1099 #[derive(serde::Deserialize, serde::Serialize)]
1100 struct ContextReader;
1101
1102 impl crate::tool::Tool for ContextReader {
1103 const NAME: &'static str = "context_reader";
1104 type Error = rig::tool::ToolExecutionError;
1105 type Args = serde_json::Value;
1106 type Output = String;
1107
1108 fn description(&self) -> String {
1109 "Reads SessionId from context".to_string()
1110 }
1111
1112 fn parameters(&self) -> serde_json::Value {
1113 serde_json::json!({"type": "object", "properties": {}})
1114 }
1115
1116 async fn call(
1117 &self,
1118 context: &mut ToolContext,
1119 _args: Self::Args,
1120 ) -> Result<Self::Output, ToolExecutionError> {
1121 if let Some(value) = context.get_mut::<CloneTrackedContext>() {
1122 value.value += 1;
1123 let result_value = value.value;
1124 context.insert_result(result_value);
1125 }
1126 Ok(context
1127 .get::<SessionId>()
1128 .map(|session| format!("session:{}", session.0))
1129 .unwrap_or_else(|| "no session".to_string()))
1130 }
1131 }
1132
1133 #[tokio::test]
1134 async fn context_reaches_the_single_execute_path() {
1135 let handle = ToolServer::new().tool(ContextReader).run();
1136 let mut context = ToolContext::new();
1137 context.insert(SessionId("abc-123".to_string()));
1138 let result = execute_tool_with_context(&handle, "context_reader", "{}", &mut context)
1139 .await
1140 .unwrap();
1141 assert_eq!(result, "session:abc-123");
1142 }
1143
1144 #[tokio::test]
1145 async fn server_dispatch_snapshot_clones_once_and_only_publishes_result_metadata() {
1146 let handle = ToolServer::new().tool(ContextReader).run();
1147 let clones = Arc::new(AtomicUsize::new(0));
1148 let mut context = ToolContext::new();
1149 context.insert(CloneTrackedContext {
1150 clones: clones.clone(),
1151 value: 0,
1152 });
1153
1154 let result = execute_tool_with_context(&handle, "context_reader", "{}", &mut context)
1155 .await
1156 .unwrap();
1157
1158 assert_eq!(result, "no session");
1159 assert_eq!(clones.load(Ordering::SeqCst), 1);
1160 assert_eq!(
1161 context
1162 .get::<CloneTrackedContext>()
1163 .map(|value| value.value),
1164 Some(0),
1165 "tool-local inbound mutations must not change the caller's context"
1166 );
1167 assert_eq!(context.result::<usize>(), Some(&1));
1168 }
1169
1170 struct PendingTool(Arc<AtomicBool>);
1171
1172 impl Tool for PendingTool {
1173 const NAME: &'static str = "pending";
1174 type Error = rig::tool::ToolExecutionError;
1175 type Args = ();
1176 type Output = ();
1177
1178 fn description(&self) -> String {
1179 "never completes".into()
1180 }
1181
1182 fn parameters(&self) -> serde_json::Value {
1183 serde_json::json!({"type": "object"})
1184 }
1185
1186 async fn call(
1187 &self,
1188 context: &mut ToolContext,
1189 _args: Self::Args,
1190 ) -> Result<Self::Output, ToolExecutionError> {
1191 context.insert_result("unpublished".to_string());
1192 self.0.store(true, Ordering::SeqCst);
1193 pending().await
1194 }
1195 }
1196
1197 #[tokio::test]
1198 async fn cancelled_server_dispatch_does_not_retain_stale_result_metadata() {
1199 let started = Arc::new(AtomicBool::new(false));
1200 let handle = ToolServer::new().tool(PendingTool(started.clone())).run();
1201 let mut context = ToolContext::new();
1202 context.insert_result("stale".to_string());
1203
1204 let mut execution = Box::pin(handle.execute(PendingTool::NAME, "null", &mut context));
1205 tokio::time::timeout(
1206 Duration::from_secs(1),
1207 poll_fn(|cx| {
1208 assert!(execution.as_mut().poll(cx).is_pending());
1209 started.load(Ordering::SeqCst).then_some(()).map_or_else(
1210 || {
1211 cx.waker().wake_by_ref();
1212 Poll::Pending
1213 },
1214 Poll::Ready,
1215 )
1216 }),
1217 )
1218 .await
1219 .expect("pending tool did not start");
1220 drop(execution);
1221
1222 assert!(context.result::<String>().is_none());
1223 }
1224
1225 #[tokio::test]
1226 async fn empty_tool_context_uses_default() {
1227 let handle = ToolServer::new().tool(ContextReader).run();
1228 let result = execute_tool(&handle, "context_reader", "{}").await.unwrap();
1229
1230 assert_eq!(result, "no session");
1231 }
1232
1233 #[tokio::test]
1234 async fn tool_ignoring_context_still_works() {
1235 let handle = ToolServer::new().tool(MockAddTool).run();
1236 let mut context = ToolContext::new();
1237 context.insert(SessionId("ignored".to_string()));
1238 let args = serde_json::to_string(&serde_json::json!({"x": 3, "y": 7})).unwrap();
1239 let result = execute_tool_with_context(&handle, "add", &args, &mut context)
1240 .await
1241 .unwrap();
1242
1243 assert_eq!(result, "10");
1244 }
1245
1246 #[tokio::test]
1247 async fn execute_classifies_a_missing_tool_as_not_found() {
1248 let handle = ToolServer::new().tool(MockAddTool).run();
1249 let error = execute_tool(&handle, "does_not_exist", "{}")
1250 .await
1251 .unwrap_err();
1252 assert_eq!(error.kind(), crate::tool::ToolErrorKind::NotFound);
1253 assert!(
1254 error
1255 .model_feedback()
1256 .is_some_and(|feedback| feedback.contains("does_not_exist"))
1257 );
1258 }
1259}