1use std::sync::Arc;
2
3use tokio::sync::RwLock;
4
5use crate::{
6 completion::{CompletionError, ToolDefinition},
7 tool::{
8 Tool, ToolCallExtensions, ToolDyn, ToolExecutionResult, ToolFailure, ToolSet, ToolSetError,
9 },
10 vector_store::{VectorSearchRequest, VectorStoreError, VectorStoreIndexDyn, request::Filter},
11};
12
13fn push_unique_name(names: &mut Vec<String>, name: String) {
19 if !names.contains(&name) {
20 names.push(name);
21 }
22}
23
24struct ToolServerState {
26 static_tool_names: Vec<String>,
28 dynamic_tools: Vec<(usize, Arc<dyn VectorStoreIndexDyn + Send + Sync>)>,
30 toolset: ToolSet,
32}
33
34pub struct ToolServer {
39 static_tool_names: Vec<String>,
40 dynamic_tools: Vec<(usize, Arc<dyn VectorStoreIndexDyn + Send + Sync>)>,
41 toolset: ToolSet,
42}
43
44impl Default for ToolServer {
45 fn default() -> Self {
46 Self::new()
47 }
48}
49
50impl ToolServer {
51 pub fn new() -> Self {
52 Self {
53 static_tool_names: Vec::new(),
54 dynamic_tools: Vec::new(),
55 toolset: ToolSet::default(),
56 }
57 }
58
59 pub(crate) fn static_tool_names(mut self, names: Vec<String>) -> Self {
60 self.static_tool_names = Vec::with_capacity(names.len());
64 for name in names {
65 push_unique_name(&mut self.static_tool_names, name);
66 }
67 self
68 }
69
70 pub(crate) fn add_tools(mut self, tools: ToolSet) -> Self {
71 self.toolset = tools;
72 self
73 }
74
75 pub(crate) fn add_dynamic_tools(
76 mut self,
77 dyn_tools: Vec<(usize, Arc<dyn VectorStoreIndexDyn + Send + Sync>)>,
78 ) -> Self {
79 self.dynamic_tools = dyn_tools;
80 self
81 }
82
83 pub fn tool(mut self, tool: impl Tool + 'static) -> Self {
86 let toolname = self.toolset.add_tool(tool);
87 push_unique_name(&mut self.static_tool_names, toolname);
88 self
89 }
90
91 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
96 #[cfg(feature = "rmcp")]
97 pub fn rmcp_tool(self, tool: rmcp::model::Tool, client: rmcp::service::ServerSink) -> Self {
98 self.rmcp_tool_with_timeout(tool, client, crate::tool::rmcp::DEFAULT_MCP_TOOL_TIMEOUT)
99 }
100
101 #[cfg_attr(docsrs, doc(cfg(feature = "rmcp")))]
106 #[cfg(feature = "rmcp")]
107 pub fn rmcp_tool_with_timeout(
108 mut self,
109 tool: rmcp::model::Tool,
110 client: rmcp::service::ServerSink,
111 timeout: impl Into<Option<std::time::Duration>>,
112 ) -> Self {
113 use crate::tool::rmcp::McpTool;
114 let toolname = self
115 .toolset
116 .add_tool(McpTool::from_mcp_server(tool, client).with_timeout(timeout));
117 push_unique_name(&mut self.static_tool_names, toolname);
118 self
119 }
120
121 pub fn dynamic_tools(
124 mut self,
125 sample: usize,
126 dynamic_tools: impl VectorStoreIndexDyn + Send + Sync + 'static,
127 toolset: ToolSet,
128 ) -> Self {
129 self.dynamic_tools.push((sample, Arc::new(dynamic_tools)));
130 self.toolset.add_tools(toolset);
131 self
132 }
133
134 pub fn run(self) -> ToolServerHandle {
136 ToolServerHandle(Arc::new(RwLock::new(ToolServerState {
137 static_tool_names: self.static_tool_names,
138 dynamic_tools: self.dynamic_tools,
139 toolset: self.toolset,
140 })))
141 }
142}
143
144#[derive(Clone)]
150pub struct ToolServerHandle(Arc<RwLock<ToolServerState>>);
151
152impl ToolServerHandle {
153 pub async fn add_tool(&self, tool: impl ToolDyn + 'static) -> Result<(), ToolServerError> {
156 let mut state = self.0.write().await;
157 let toolname = state.toolset.add_tool_boxed(Box::new(tool));
158 push_unique_name(&mut state.static_tool_names, toolname);
159 Ok(())
160 }
161
162 pub async fn append_toolset(&self, toolset: ToolSet) -> Result<(), ToolServerError> {
168 let mut state = self.0.write().await;
169 for name in toolset.ordered_names() {
170 push_unique_name(&mut state.static_tool_names, name.clone());
171 }
172 state.toolset.add_tools(toolset);
173 Ok(())
174 }
175
176 pub async fn remove_tool(&self, tool_name: &str) -> Result<(), ToolServerError> {
178 let mut state = self.0.write().await;
179 state.static_tool_names.retain(|x| *x != tool_name);
180 state.toolset.delete_tool(tool_name);
181 Ok(())
182 }
183
184 pub async fn call_tool(&self, tool_name: &str, args: &str) -> Result<String, ToolServerError> {
189 self.call_tool_with_extensions(tool_name, args, &ToolCallExtensions::EMPTY)
190 .await
191 }
192
193 pub async fn call_tool_with_extensions(
200 &self,
201 tool_name: &str,
202 args: &str,
203 extensions: &ToolCallExtensions,
204 ) -> Result<String, ToolServerError> {
205 let tool = {
206 let state = self.0.read().await;
207 state.toolset.get(tool_name).cloned()
208 };
209
210 match tool {
211 Some(tool) => {
212 tracing::debug!(target: "rig",
213 "Calling tool {tool_name} with args:\n{}",
214 serde_json::to_string_pretty(&args).unwrap_or_default()
215 );
216 tool.call_with_extensions(args.to_string(), extensions)
217 .await
218 .map_err(|e| ToolSetError::ToolCallError(e).into())
219 }
220 None => Err(ToolServerError::ToolsetError(
221 ToolSetError::ToolNotFoundError(tool_name.to_string()),
222 )),
223 }
224 }
225
226 pub async fn call_tool_structured(
237 &self,
238 tool_name: &str,
239 args: &str,
240 extensions: &ToolCallExtensions,
241 ) -> ToolExecutionResult {
242 let tool = {
243 let state = self.0.read().await;
244 state.toolset.get(tool_name).cloned()
245 };
246
247 match tool {
248 Some(tool) => {
249 tracing::debug!(target: "rig",
250 "Calling tool {tool_name} with args:\n{}",
251 serde_json::to_string_pretty(&args).unwrap_or_default()
252 );
253 tool.call_structured(args.to_string(), extensions).await
254 }
255 None => ToolExecutionResult::failed(
256 format!("tool `{tool_name}` not found"),
257 ToolFailure::not_found(format!("no tool named `{tool_name}` is registered")),
258 ),
259 }
260 }
261
262 pub async fn get_tool_defs(
265 &self,
266 prompt: Option<String>,
267 ) -> Result<Vec<ToolDefinition>, ToolServerError> {
268 let (static_tool_names, dynamic_tools) = {
270 let state = self.0.read().await;
271 (state.static_tool_names.clone(), state.dynamic_tools.clone())
272 };
273
274 let mut tools = if let Some(ref text) = prompt {
275 let search_futures = dynamic_tools.iter().map(|(num_sample, index)| {
277 let text = text.clone();
278 let num_sample = *num_sample;
279 let index = index.clone();
280
281 async move {
282 let req = VectorSearchRequest::builder()
283 .query(text)
284 .samples(num_sample as u64)
285 .build();
286
287 let ids = index
288 .as_ref()
289 .top_n_ids(req.map_filter(Filter::interpret))
290 .await?
291 .into_iter()
292 .map(|(_, id)| id)
293 .collect::<Vec<String>>();
294
295 Ok::<_, VectorStoreError>(ids)
296 }
297 });
298
299 let dynamic_tool_ids: Vec<String> = futures::future::try_join_all(search_futures)
301 .await
302 .map_err(|e| {
303 ToolServerError::DefinitionError(CompletionError::RequestError(Box::new(e)))
304 })?
305 .into_iter()
306 .flatten()
307 .collect();
308
309 let dynamic_tool_handles: Vec<_> = {
310 let state = self.0.read().await;
311 dynamic_tool_ids
312 .iter()
313 .filter_map(|doc| {
314 let handle = state.toolset.get(doc).cloned();
315 if handle.is_none() {
316 tracing::warn!("Tool implementation not found in toolset: {}", doc);
317 }
318 handle.map(|handle| (doc.clone(), handle))
319 })
320 .collect()
321 };
322
323 dynamic_tool_handles
324 .into_iter()
325 .map(|(name, tool)| tool.definition_with_name(name))
326 .collect()
327 } else {
328 Vec::new()
329 };
330
331 let static_tool_handles: Vec<_> = {
332 let state = self.0.read().await;
333 static_tool_names
334 .iter()
335 .filter_map(|toolname| {
336 let handle = state.toolset.get(toolname).cloned();
337 if handle.is_none() {
338 tracing::warn!("Tool implementation not found in toolset: {}", toolname);
339 }
340 handle.map(|handle| (toolname.clone(), handle))
341 })
342 .collect()
343 };
344
345 for (name, tool) in static_tool_handles {
346 tools.push(tool.definition_with_name(name));
347 }
348
349 let mut seen = std::collections::HashSet::new();
354 tools.retain(|def| {
355 let fresh = seen.insert(def.name.clone());
356 if !fresh {
357 tracing::debug!(
358 tool_name = %def.name,
359 "dropping duplicate tool definition from the request"
360 );
361 }
362 fresh
363 });
364
365 Ok(tools)
366 }
367}
368
369#[derive(Debug, thiserror::Error)]
370pub enum ToolServerError {
371 #[error("Toolset error: {0}")]
372 ToolsetError(#[from] ToolSetError),
373 #[error("Failed to retrieve tool definitions: {0}")]
374 DefinitionError(CompletionError),
375}
376
377#[cfg(test)]
378mod tests {
379 use std::{
380 sync::{
381 Arc,
382 atomic::{AtomicUsize, Ordering},
383 },
384 time::Duration,
385 };
386
387 use crate::{
388 test_utils::{
389 BarrierMockToolIndex, MockAddTool, MockBarrierTool, MockControlledTool,
390 MockSubtractTool, MockToolError, MockToolIndex,
391 },
392 tool::{Tool, ToolEmbedding, ToolSet, server::ToolServer},
393 };
394
395 struct ChangingNameTool {
396 calls: AtomicUsize,
397 }
398
399 impl ChangingNameTool {
400 fn new() -> Self {
401 Self {
402 calls: AtomicUsize::new(0),
403 }
404 }
405 }
406
407 impl Tool for ChangingNameTool {
408 const NAME: &'static str = "unused";
409 type Error = MockToolError;
410 type Args = serde_json::Value;
411 type Output = String;
412
413 fn name(&self) -> String {
414 match self.calls.fetch_add(1, Ordering::SeqCst) {
415 0 => "registered_changing".to_string(),
416 _ => "changed_after_registration".to_string(),
417 }
418 }
419
420 fn description(&self) -> String {
421 "changes name after registration".to_string()
422 }
423
424 fn parameters(&self) -> serde_json::Value {
425 serde_json::json!({"type": "object", "properties": {}})
426 }
427
428 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
429 Ok("ok".to_string())
430 }
431 }
432
433 #[derive(Debug, thiserror::Error)]
434 #[error("init error")]
435 struct InitError;
436
437 impl ToolEmbedding for ChangingNameTool {
438 type InitError = InitError;
439 type Context = ();
440 type State = ();
441
442 fn embedding_docs(&self) -> Vec<String> {
443 vec!["changing dynamic tool".to_string()]
444 }
445
446 fn context(&self) -> Self::Context {}
447
448 fn init(_state: Self::State, _context: Self::Context) -> Result<Self, Self::InitError> {
449 Ok(Self::new())
450 }
451 }
452
453 #[tokio::test]
454 pub async fn test_toolserver() {
455 let server = ToolServer::new();
456
457 let handle = server.run();
458
459 handle.add_tool(MockAddTool).await.unwrap();
460 let res = handle.get_tool_defs(None).await.unwrap();
461
462 assert_eq!(res.len(), 1);
463
464 let json_args_as_string =
465 serde_json::to_string(&serde_json::json!({"x": 2, "y": 5})).unwrap();
466 let res = handle.call_tool("add", &json_args_as_string).await.unwrap();
467 assert_eq!(res, "7");
468
469 handle.remove_tool("add").await.unwrap();
470 let res = handle.get_tool_defs(None).await.unwrap();
471
472 assert_eq!(res.len(), 0);
473 }
474
475 #[tokio::test]
476 pub async fn test_toolserver_append_toolset_matches_add_tool() {
477 let mut via_add_tool = {
478 let handle = ToolServer::new().run();
479 handle.add_tool(MockAddTool).await.unwrap();
480 handle.add_tool(MockSubtractTool).await.unwrap();
481 handle.get_tool_defs(None).await.unwrap()
482 };
483 via_add_tool.sort_by(|a, b| a.name.cmp(&b.name));
484
485 let mut via_append_toolset = {
486 let handle = ToolServer::new().run();
487 let mut toolset = ToolSet::default();
488 toolset.add_tool(MockAddTool);
489 toolset.add_tool(MockSubtractTool);
490 handle.append_toolset(toolset).await.unwrap();
491 handle.get_tool_defs(None).await.unwrap()
492 };
493 via_append_toolset.sort_by(|a, b| a.name.cmp(&b.name));
494
495 assert_eq!(via_add_tool.len(), via_append_toolset.len());
496 assert!(
497 via_add_tool
498 .iter()
499 .zip(via_append_toolset.iter())
500 .all(|(a, b)| a.name == b.name),
501 "append_toolset must surface the same LLM-visible tools as add_tool",
502 );
503 }
504
505 #[tokio::test]
506 pub async fn builder_tool_uses_registered_key_for_static_names() {
507 let handle = ToolServer::new().tool(ChangingNameTool::new()).run();
508
509 let defs = handle.get_tool_defs(None).await.unwrap();
510 assert_eq!(defs.len(), 1);
511 assert_eq!(defs[0].name, "registered_changing");
512 }
513
514 #[tokio::test]
515 pub async fn handle_add_tool_uses_registered_key_for_static_names() {
516 let handle = ToolServer::new().run();
517 handle.add_tool(ChangingNameTool::new()).await.unwrap();
518
519 let defs = handle.get_tool_defs(None).await.unwrap();
520 assert_eq!(defs.len(), 1);
521 assert_eq!(defs[0].name, "registered_changing");
522 }
523
524 #[tokio::test]
525 pub async fn dynamic_retrieval_resolves_registered_key() {
526 let toolset = ToolSet::builder()
527 .dynamic_tool(ChangingNameTool::new())
528 .build();
529 let handle = ToolServer::new()
530 .dynamic_tools(1, MockToolIndex::new(["registered_changing"]), toolset)
531 .run();
532
533 let defs = handle
534 .get_tool_defs(Some("use the changing tool".to_string()))
535 .await
536 .unwrap();
537 assert_eq!(defs.len(), 1);
538 assert_eq!(defs[0].name, "registered_changing");
539 }
540
541 #[tokio::test]
542 pub async fn get_tool_defs_preserves_static_registration_order() {
543 let handle = ToolServer::new().run();
544 handle.add_tool(MockSubtractTool).await.unwrap();
545 handle.add_tool(MockAddTool).await.unwrap();
546
547 let defs = handle.get_tool_defs(None).await.unwrap();
548 assert_eq!(
549 defs.iter().map(|def| def.name.as_str()).collect::<Vec<_>>(),
550 vec!["subtract", "add"]
551 );
552 }
553
554 #[tokio::test]
555 pub async fn get_tool_defs_dedupes_dynamic_and_static_overlap() {
556 let handle = ToolServer::new()
559 .tool(MockAddTool)
560 .dynamic_tools(1, MockToolIndex::new(["add"]), ToolSet::default())
561 .run();
562
563 let defs = handle
564 .get_tool_defs(Some("add two numbers".to_string()))
565 .await
566 .unwrap();
567 assert_eq!(
568 defs.len(),
569 1,
570 "dynamic/static name overlap must not produce duplicate declarations: {:?}",
571 defs.iter().map(|def| def.name.as_str()).collect::<Vec<_>>()
572 );
573 assert_eq!(defs[0].name, "add");
574 }
575
576 #[tokio::test]
577 pub async fn duplicate_registration_advertises_one_definition() {
578 let handle = ToolServer::new().tool(MockAddTool).run();
579 handle.add_tool(MockAddTool).await.unwrap();
580
581 let mut toolset = ToolSet::default();
582 toolset.add_tool(MockAddTool);
583 handle.append_toolset(toolset).await.unwrap();
584
585 let defs = handle.get_tool_defs(None).await.unwrap();
586 assert_eq!(
587 defs.len(),
588 1,
589 "re-registering a name must not advertise duplicate declarations"
590 );
591 assert_eq!(defs[0].name, "add");
592 }
593
594 #[tokio::test]
595 pub async fn test_toolserver_dynamic_tools() {
596 let mut toolset = ToolSet::default();
598 toolset.add_tool(MockAddTool);
599 toolset.add_tool(MockSubtractTool);
600
601 let mock_index = MockToolIndex::new(["subtract"]);
603
604 let server = ToolServer::new().tool(MockAddTool).dynamic_tools(
606 1,
607 mock_index,
608 ToolSet::from_tools(vec![MockSubtractTool]),
609 );
610
611 let handle = server.run();
612
613 let res = handle.get_tool_defs(None).await.unwrap();
615 assert_eq!(res.len(), 1);
616 assert_eq!(res[0].name, "add");
617
618 let res = handle
620 .get_tool_defs(Some("calculate difference".to_string()))
621 .await
622 .unwrap();
623 assert_eq!(res.len(), 2);
624
625 let tool_names: Vec<&str> = res.iter().map(|t| t.name.as_str()).collect();
627 assert!(tool_names.contains(&"add"));
628 assert!(tool_names.contains(&"subtract"));
629 }
630
631 #[tokio::test]
632 pub async fn test_toolserver_dynamic_tools_missing_implementation() {
633 let mock_index = MockToolIndex::new(["nonexistent_tool"]);
635
636 let server =
638 ToolServer::new()
639 .tool(MockAddTool)
640 .dynamic_tools(1, mock_index, ToolSet::default());
641
642 let handle = server.run();
643
644 let res = handle
646 .get_tool_defs(Some("some query".to_string()))
647 .await
648 .unwrap();
649 assert_eq!(res.len(), 1);
650 assert_eq!(res[0].name, "add");
651 }
652
653 #[tokio::test]
654 pub async fn test_toolserver_concurrent_tool_execution() {
655 let num_calls = 3;
656 let barrier = Arc::new(tokio::sync::Barrier::new(num_calls));
657
658 let server = ToolServer::new().tool(MockBarrierTool::new(barrier.clone()));
659 let handle = server.run();
660
661 let futures: Vec<_> = (0..num_calls)
663 .map(|_| handle.call_tool("barrier_tool", "{}"))
664 .collect();
665
666 let result =
669 tokio::time::timeout(Duration::from_secs(1), futures::future::join_all(futures)).await;
670
671 assert!(
672 result.is_ok(),
673 "Tool execution deadlocked! Tools are executing sequentially instead of concurrently."
674 );
675
676 for res in result.unwrap() {
678 assert!(res.is_ok(), "Tool call failed: {:?}", res);
679 assert_eq!(res.unwrap(), "done");
680 }
681 }
682
683 #[tokio::test]
684 pub async fn test_toolserver_write_while_tool_running() {
685 let started = Arc::new(tokio::sync::Notify::new());
686 let allow_finish = Arc::new(tokio::sync::Notify::new());
687
688 let tool = MockControlledTool::new(started.clone(), allow_finish.clone());
690
691 let server = ToolServer::new().tool(tool);
692 let handle = server.run();
693
694 let handle_clone = handle.clone();
696 let call_task =
697 tokio::spawn(async move { handle_clone.call_tool("controlled", "{}").await });
698
699 started.notified().await;
701
702 let add_result =
705 tokio::time::timeout(Duration::from_secs(1), handle.add_tool(MockAddTool)).await;
706
707 assert!(
708 add_result.is_ok(),
709 "Writing to ToolServer deadlocked! The read lock is being held across tool execution."
710 );
711 assert!(add_result.unwrap().is_ok());
712
713 allow_finish.notify_one();
715 let call_result = call_task.await.unwrap();
716 assert_eq!(call_result.unwrap(), "42");
717 }
718
719 #[tokio::test]
720 pub async fn test_toolserver_parallel_dynamic_tool_fetching() {
721 let barrier = Arc::new(tokio::sync::Barrier::new(2));
723
724 let index1 = BarrierMockToolIndex::new(barrier.clone(), "add");
725 let index2 = BarrierMockToolIndex::new(barrier.clone(), "subtract");
726
727 let mut toolset = ToolSet::default();
729 toolset.add_tool(MockAddTool);
730 toolset.add_tool(MockSubtractTool);
731
732 let server = ToolServer::new()
733 .dynamic_tools(1, index1, ToolSet::default())
734 .dynamic_tools(1, index2, toolset);
735
736 let handle = server.run();
737
738 let get_defs = tokio::time::timeout(
741 std::time::Duration::from_secs(1),
742 handle.get_tool_defs(Some("do math".to_string())),
743 )
744 .await;
745
746 assert!(
747 get_defs.is_ok(),
748 "Dynamic tools were fetched sequentially! The first query deadlocked waiting for the second query to start."
749 );
750
751 let defs = get_defs.unwrap().unwrap();
752 assert_eq!(defs.len(), 2);
753
754 let tool_names: Vec<&str> = defs.iter().map(|t| t.name.as_str()).collect();
755 assert!(tool_names.contains(&"add"));
756 assert!(tool_names.contains(&"subtract"));
757 }
758
759 #[derive(Clone)]
762 struct SessionId(String);
763
764 #[derive(serde::Deserialize, serde::Serialize)]
765 struct ExtensionsReader;
766
767 #[derive(Debug, thiserror::Error)]
768 #[error("context reader error")]
769 struct ExtensionsReaderError;
770
771 impl crate::tool::Tool for ExtensionsReader {
772 const NAME: &'static str = "context_reader";
773 type Error = ExtensionsReaderError;
774 type Args = serde_json::Value;
775 type Output = String;
776
777 fn description(&self) -> String {
778 "Reads SessionId from context".to_string()
779 }
780
781 fn parameters(&self) -> serde_json::Value {
782 serde_json::json!({"type": "object", "properties": {}})
783 }
784
785 async fn call(&self, _args: Self::Args) -> Result<Self::Output, Self::Error> {
786 Ok("no context".to_string())
787 }
788
789 async fn call_with_extensions(
790 &self,
791 _args: Self::Args,
792 extensions: &crate::tool::ToolCallExtensions,
793 ) -> Result<Self::Output, Self::Error> {
794 match extensions.get::<SessionId>() {
795 Some(session) => Ok(format!("session:{}", session.0)),
796 None => Ok("no session".to_string()),
797 }
798 }
799 }
800
801 #[tokio::test]
802 async fn test_call_tool_with_extensions_reaches_tool() {
803 let server = ToolServer::new().tool(ExtensionsReader);
804 let handle = server.run();
805
806 let mut extensions = crate::tool::ToolCallExtensions::new();
807 extensions.insert(SessionId("abc-123".to_string()));
808
809 let result = handle
810 .call_tool_with_extensions("context_reader", "{}", &extensions)
811 .await
812 .unwrap();
813
814 assert_eq!(result, "session:abc-123");
815 }
816
817 #[tokio::test]
818 async fn test_call_tool_without_extensions_uses_default() {
819 let server = ToolServer::new().tool(ExtensionsReader);
820 let handle = server.run();
821
822 let result = handle.call_tool("context_reader", "{}").await.unwrap();
823 assert_eq!(result, "no session");
824 }
825
826 #[tokio::test]
827 async fn test_tool_ignoring_extensions_still_works() {
828 let server = ToolServer::new().tool(MockAddTool);
829 let handle = server.run();
830
831 let mut extensions = crate::tool::ToolCallExtensions::new();
832 extensions.insert(SessionId("ignored".to_string()));
833
834 let args = serde_json::to_string(&serde_json::json!({"x": 3, "y": 7})).unwrap();
835 let result = handle
836 .call_tool_with_extensions("add", &args, &extensions)
837 .await
838 .unwrap();
839
840 assert_eq!(result, "10");
841 }
842
843 #[tokio::test]
844 async fn call_tool_structured_returns_success_for_a_known_tool() {
845 use crate::tool::{ToolCallExtensions, ToolOutcome};
846
847 let handle = ToolServer::new().tool(MockAddTool).run();
848 let args = serde_json::to_string(&serde_json::json!({"x": 2, "y": 5})).unwrap();
849 let result = handle
850 .call_tool_structured("add", &args, &ToolCallExtensions::EMPTY)
851 .await;
852
853 assert!(matches!(result.outcome, ToolOutcome::Success));
854 assert_eq!(result.model_output, "7");
855 }
856
857 #[tokio::test]
858 async fn call_tool_structured_classifies_a_missing_tool_as_not_found() {
859 use crate::tool::{ToolCallExtensions, ToolFailureKind, ToolOutcome};
860
861 let handle = ToolServer::new().tool(MockAddTool).run();
862 let result = handle
863 .call_tool_structured("does_not_exist", "{}", &ToolCallExtensions::EMPTY)
864 .await;
865
866 match result.outcome {
867 ToolOutcome::Error(failure) => assert_eq!(failure.kind, ToolFailureKind::NotFound),
868 other => panic!("expected a NotFound error outcome, got {other:?}"),
869 }
870 assert!(result.model_output.contains("does_not_exist"));
871 }
872}