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