1use dashmap::DashMap;
2use rmcp::{
3 RoleClient,
4 model::{CallToolRequestParam, CallToolResult},
5 service::{DynService, RunningService},
6};
7use serde_json::Value;
8use std::borrow::Cow;
9use std::sync::{
10 Arc,
11 atomic::{AtomicUsize, Ordering},
12};
13
14use crate::Tool;
15
16struct InternalPegBoard {
20 tools: DashMap<String, Tool>,
23
24 services: DashMap<
27 usize,
28 (
29 String,
30 RunningService<RoleClient, Box<dyn DynService<RoleClient>>>,
31 ),
32 >,
33
34 next_service_idx: AtomicUsize,
37
38 tool_routing: DashMap<String, ToolRoute>,
42
43 namespace_tools: DashMap<String, Vec<String>>,
45}
46
47#[derive(Debug, Clone)]
49struct ToolRoute {
50 service_index: usize,
52 original_name: String,
54}
55
56fn prefix_tool_name(namespace: &str, tool_name: &str) -> String {
58 format!("{}-{}", namespace, tool_name)
59}
60
61impl InternalPegBoard {
62 fn new() -> Self {
64 Self {
65 tools: DashMap::new(),
66 services: DashMap::new(),
67 next_service_idx: AtomicUsize::new(0),
68 tool_routing: DashMap::new(),
69 namespace_tools: DashMap::new(),
70 }
71 }
72
73 pub async fn add_service(
98 &self,
99 namespace: Option<String>,
100 service: RunningService<RoleClient, Box<dyn DynService<RoleClient>>>,
101 ) -> Result<(), PegBoardError> {
102 let namespace_str = namespace.unwrap_or_default();
104 let has_namespace = !namespace_str.is_empty();
105
106 if has_namespace && self.namespace_tools.contains_key(&namespace_str) {
108 return Err(PegBoardError::NamespaceAlreadyExists(namespace_str));
109 }
110
111 let service_idx = self.next_service_idx.fetch_add(1, Ordering::SeqCst);
114
115 let tools_response = service
117 .list_tools(None) .await
119 .map_err(|e| PegBoardError::ServiceError(format!("Failed to list tools: {:?}", e)))?;
120
121 let tools_list: Vec<Tool> = tools_response
123 .tools
124 .into_iter()
125 .map(|rmcp_tool| Tool {
126 name: rmcp_tool.name,
127 description: rmcp_tool.description.map(Cow::from),
128 input_schema: serde_json::Value::Object((*rmcp_tool.input_schema).clone()),
129 })
130 .collect();
131
132 self.services
134 .insert(service_idx, (namespace_str.clone(), service));
135
136 let mut registered_tool_names = Vec::new();
138
139 for original_tool in tools_list {
141 let original_name = original_tool.name.to_string();
142
143 let final_name = if has_namespace {
145 prefix_tool_name(&namespace_str, &original_name)
146 } else {
147 original_name.clone()
148 };
149
150 if self.tools.contains_key(&final_name) {
152 return Err(PegBoardError::ToolAlreadyExists(final_name));
153 }
154
155 let mut final_tool = original_tool.clone();
157 final_tool.name = Cow::Owned(final_name.clone());
158
159 self.tools.insert(final_name.clone(), final_tool);
161
162 self.tool_routing.insert(
164 final_name.clone(),
165 ToolRoute {
166 service_index: service_idx,
167 original_name,
168 },
169 );
170
171 registered_tool_names.push(final_name);
172 }
173
174 if has_namespace {
176 self.namespace_tools
177 .insert(namespace_str, registered_tool_names);
178 }
179
180 Ok(())
181 }
182
183 pub fn register_tool(
188 &self,
189 namespace: Option<&str>,
190 tool: Tool,
191 service_idx: usize,
192 ) -> Result<(), PegBoardError> {
193 if !self.services.contains_key(&service_idx) {
195 return Err(PegBoardError::InvalidServiceIndex {
196 index: service_idx,
197 max: self.services.len(),
198 });
199 }
200
201 let original_name = tool.name.to_string();
202 let namespace_str = namespace.unwrap_or("");
203 let has_namespace = !namespace_str.is_empty();
204
205 let final_name = if has_namespace {
207 prefix_tool_name(namespace_str, &original_name)
208 } else {
209 original_name.clone()
210 };
211
212 if self.tools.contains_key(&final_name) {
214 return Err(PegBoardError::ToolAlreadyExists(final_name));
215 }
216
217 let mut final_tool = tool;
219 final_tool.name = Cow::Owned(final_name.clone());
220
221 self.tools.insert(final_name.clone(), final_tool);
223 self.tool_routing.insert(
224 final_name.clone(),
225 ToolRoute {
226 service_index: service_idx,
227 original_name,
228 },
229 );
230
231 if has_namespace {
233 self.namespace_tools
234 .entry(namespace_str.to_string())
235 .or_default()
236 .push(final_name);
237 }
238
239 Ok(())
240 }
241
242 pub fn get_tool(&self, tool_name: &str) -> Option<Tool> {
245 self.tools.get(tool_name).map(|entry| entry.value().clone())
246 }
247
248 pub fn select_tools(&self, tool_names: &[&str]) -> Option<Vec<Tool>> {
269 let mut result = Vec::with_capacity(tool_names.len());
270
271 for &tool_name in tool_names {
272 let tool = self.get_tool(tool_name)?;
273 result.push(tool);
274 }
275
276 Some(result)
277 }
278
279 pub fn get_tool_route(&self, tool_name: &str) -> Option<(usize, String)> {
282 self.tool_routing.get(tool_name).map(|entry| {
283 let route = entry.value();
284 (route.service_index, route.original_name.clone())
285 })
286 }
287
288 pub fn list_tools_in_namespace(&self, namespace: &str) -> Vec<String> {
290 self.namespace_tools
291 .get(namespace)
292 .map(|entry| entry.value().clone())
293 .unwrap_or_default()
294 }
295
296 pub fn get_tools_in_namespace(&self, namespace: &str) -> Vec<Tool> {
298 self.list_tools_in_namespace(namespace)
299 .iter()
300 .filter_map(|tool_name| self.get_tool(tool_name))
301 .collect()
302 }
303
304 pub fn list_all_tools(&self) -> Vec<String> {
307 self.tools.iter().map(|entry| entry.key().clone()).collect()
308 }
309
310 pub fn get_all_tools(&self) -> Vec<Tool> {
313 self.tools
314 .iter()
315 .map(|entry| entry.value().clone())
316 .collect()
317 }
318
319 pub fn list_namespaces(&self) -> Vec<String> {
321 self.namespace_tools
322 .iter()
323 .map(|entry| entry.key().clone())
324 .collect()
325 }
326
327 pub fn unregister_tool(&self, prefixed_name: &str) -> Result<(), PegBoardError> {
329 if self.tools.remove(prefixed_name).is_none() {
330 return Err(PegBoardError::ToolNotFound(prefixed_name.to_string()));
331 }
332
333 self.tool_routing.remove(prefixed_name);
334
335 for mut namespace_entry in self.namespace_tools.iter_mut() {
338 namespace_entry.value_mut().retain(|n| n != prefixed_name);
339 }
340
341 Ok(())
342 }
343
344 pub fn unregister_namespace(&self, namespace: &str) -> Result<usize, PegBoardError> {
346 let prefixed_names = self.list_tools_in_namespace(namespace);
347 let count = prefixed_names.len();
348
349 for prefixed_name in prefixed_names {
351 self.tools.remove(&prefixed_name);
352 self.tool_routing.remove(&prefixed_name);
353 }
354
355 let service_idx_to_remove = self
357 .services
358 .iter()
359 .find(|entry| entry.value().0 == namespace)
360 .map(|entry| *entry.key());
361
362 if let Some(idx) = service_idx_to_remove {
363 self.services.remove(&idx);
364 }
365
366 self.namespace_tools.remove(namespace);
367 Ok(count)
368 }
369
370 pub fn unregister_service(&self, service_idx: usize) -> Result<usize, PegBoardError> {
372 let namespace = self
374 .services
375 .get(&service_idx)
376 .map(|entry| entry.value().0.clone())
377 .ok_or(PegBoardError::InvalidServiceIndex {
378 index: service_idx,
379 max: self.next_service_idx.load(Ordering::SeqCst),
380 })?;
381
382 let tools_to_remove: Vec<String> = self
384 .tool_routing
385 .iter()
386 .filter(|entry| entry.value().service_index == service_idx)
387 .map(|entry| entry.key().clone())
388 .collect();
389
390 let count = tools_to_remove.len();
391 for tool_name in tools_to_remove {
392 self.tools.remove(&tool_name);
393 self.tool_routing.remove(&tool_name);
394 }
395
396 self.services.remove(&service_idx);
398
399 if !namespace.is_empty() {
401 self.namespace_tools.remove(&namespace);
402 }
403
404 Ok(count)
405 }
406
407 pub fn tool_count(&self) -> usize {
409 self.tools.len()
410 }
411
412 pub fn service_count(&self) -> usize {
414 self.services.len()
415 }
416
417 pub fn namespace_count(&self) -> usize {
419 self.namespace_tools.len()
420 }
421
422 pub async fn call_tool(
449 &self,
450 tool_name: &str,
451 arguments: Value,
452 ) -> Result<CallToolResult, PegBoardError> {
453 let (service_idx, original_name) = self
455 .get_tool_route(tool_name)
456 .ok_or_else(|| PegBoardError::ToolNotFound(tool_name.to_string()))?;
457
458 let service_entry =
460 self.services
461 .get(&service_idx)
462 .ok_or(PegBoardError::InvalidServiceIndex {
463 index: service_idx,
464 max: self.services.len(),
465 })?;
466 let (_namespace, service) = service_entry.value();
467
468 let arguments_obj = match arguments {
470 Value::Object(obj) => Some(obj),
471 Value::Null => None,
472 _ => {
473 return Err(PegBoardError::ServiceError(
474 "Tool arguments must be a JSON object or null".to_string(),
475 ));
476 }
477 };
478
479 let param = CallToolRequestParam {
481 name: Cow::from(original_name),
482 arguments: arguments_obj,
483 };
484
485 service
486 .call_tool(param)
487 .await
488 .map_err(|e| PegBoardError::ServiceError(format!("Tool call failed: {:?}", e)))
489 }
490}
491
492#[derive(Clone)]
520pub struct PegBoard {
521 inner: Arc<InternalPegBoard>,
522}
523
524impl PegBoard {
525 pub fn new() -> Self {
527 Self {
528 inner: Arc::new(InternalPegBoard::new()),
529 }
530 }
531
532 pub async fn add_service(
536 &self,
537 namespace: Option<String>,
538 service: RunningService<RoleClient, Box<dyn DynService<RoleClient>>>,
539 ) -> Result<(), PegBoardError> {
540 self.inner.add_service(namespace, service).await
541 }
542
543 pub fn register_tool(
545 &self,
546 namespace: Option<&str>,
547 tool: Tool,
548 service_idx: usize,
549 ) -> Result<(), PegBoardError> {
550 self.inner.register_tool(namespace, tool, service_idx)
551 }
552
553 pub fn get_tool(&self, tool_name: &str) -> Option<Tool> {
555 self.inner.get_tool(tool_name)
556 }
557
558 pub fn select_tools(&self, tool_names: &[&str]) -> Option<Vec<Tool>> {
560 self.inner.select_tools(tool_names)
561 }
562
563 pub fn get_tool_route(&self, tool_name: &str) -> Option<(usize, String)> {
565 self.inner.get_tool_route(tool_name)
566 }
567
568 pub fn list_tools_in_namespace(&self, namespace: &str) -> Vec<String> {
570 self.inner.list_tools_in_namespace(namespace)
571 }
572
573 pub fn get_tools_in_namespace(&self, namespace: &str) -> Vec<Tool> {
575 self.inner.get_tools_in_namespace(namespace)
576 }
577
578 pub fn list_all_tools(&self) -> Vec<String> {
580 self.inner.list_all_tools()
581 }
582
583 pub fn get_all_tools(&self) -> Vec<Tool> {
585 self.inner.get_all_tools()
586 }
587
588 pub fn list_namespaces(&self) -> Vec<String> {
590 self.inner.list_namespaces()
591 }
592
593 pub fn unregister_tool(&self, prefixed_name: &str) -> Result<(), PegBoardError> {
595 self.inner.unregister_tool(prefixed_name)
596 }
597
598 pub fn unregister_namespace(&self, namespace: &str) -> Result<usize, PegBoardError> {
600 self.inner.unregister_namespace(namespace)
601 }
602
603 pub fn unregister_service(&self, service_idx: usize) -> Result<usize, PegBoardError> {
605 self.inner.unregister_service(service_idx)
606 }
607
608 pub fn tool_count(&self) -> usize {
610 self.inner.tool_count()
611 }
612
613 pub fn service_count(&self) -> usize {
615 self.inner.service_count()
616 }
617
618 pub fn namespace_count(&self) -> usize {
620 self.inner.namespace_count()
621 }
622
623 pub async fn call_tool(
625 &self,
626 tool_name: &str,
627 arguments: Value,
628 ) -> Result<CallToolResult, PegBoardError> {
629 self.inner.call_tool(tool_name, arguments).await
630 }
631}
632
633impl Default for PegBoard {
634 fn default() -> Self {
635 Self::new()
636 }
637}
638
639#[derive(Debug, thiserror::Error)]
641pub enum PegBoardError {
642 #[error("Tool '{0}' already exists")]
643 ToolAlreadyExists(String),
644
645 #[error("Tool '{0}' not found")]
646 ToolNotFound(String),
647
648 #[error("Invalid service index {index}, max is {max}")]
649 InvalidServiceIndex { index: usize, max: usize },
650
651 #[error("Namespace '{0}' already exists")]
652 NamespaceAlreadyExists(String),
653
654 #[error("Service error: {0}")]
655 ServiceError(String),
656}
657
658#[cfg(test)]
659mod tests {
660 use super::*;
661 use schemars::JsonSchema;
662
663 #[derive(JsonSchema)]
664 #[allow(dead_code)]
665 struct TestParams {
666 value: String,
667 }
668
669 #[test]
670 fn test_prefix_tool_name() {
671 let prefixed = prefix_tool_name("web_search", "search");
672 assert_eq!(prefixed, "web_search-search");
673
674 let prefixed2 = prefix_tool_name("fs", "read_file");
675 assert_eq!(prefixed2, "fs-read_file");
676 }
677
678 #[test]
679 fn test_pegboard_register_and_get() {
680 let pegboard = PegBoard::new();
681 let tool = crate::get_tool::<TestParams, _, _>("search", Some("Search tool")).unwrap();
682
683 assert_eq!(pegboard.tool_count(), 0);
685 assert_eq!(pegboard.namespace_count(), 0);
686
687 assert!(
689 pegboard
690 .register_tool(Some("web"), tool.clone(), 0)
691 .is_err()
692 );
693
694 assert!(pegboard.register_tool(None, tool.clone(), 0).is_err());
696 }
697
698 #[test]
699 fn test_pegboard_tool_name_prefixing() {
700 let original_tool =
702 crate::get_tool::<TestParams, _, _>("search", Some("Search tool")).unwrap();
703 assert_eq!(original_tool.name, "search");
704
705 }
709
710 #[test]
711 fn test_pegboard_namespace_operations() {
712 let pegboard = PegBoard::new();
713
714 assert_eq!(pegboard.list_namespaces().len(), 0);
716 assert_eq!(pegboard.list_all_tools().len(), 0);
717 assert_eq!(pegboard.get_all_tools().len(), 0);
718
719 assert_eq!(pegboard.list_tools_in_namespace("nonexistent").len(), 0);
721 assert_eq!(pegboard.get_tools_in_namespace("nonexistent").len(), 0);
722 }
723
724 #[test]
725 fn test_pegboard_get_tool_methods() {
726 let pegboard = PegBoard::new();
727
728 assert!(pegboard.get_tool("web-search").is_none());
730 assert!(pegboard.get_tool_route("web-search").is_none());
731 }
732
733 #[test]
734 fn test_pegboard_unregister() {
735 let pegboard = PegBoard::new();
736
737 assert!(pegboard.unregister_tool("web-nonexistent").is_err());
739
740 let result = pegboard.unregister_namespace("nonexistent");
742 assert!(result.is_ok());
743 assert_eq!(result.unwrap(), 0); }
745
746 #[test]
747 fn test_tool_route_structure() {
748 let route = ToolRoute {
749 service_index: 0,
750 original_name: "search".to_string(),
751 };
752
753 assert_eq!(route.service_index, 0);
754 assert_eq!(route.original_name, "search");
755 }
756
757 #[test]
758 fn test_optional_namespace() {
759 let namespace_none: Option<String> = None;
761 let namespace_empty = Some(String::new());
762
763 let ns1 = namespace_none.unwrap_or_default();
765 let ns2 = namespace_empty.unwrap_or_default();
766
767 assert_eq!(ns1, "");
768 assert_eq!(ns2, "");
769 assert!(!ns1.is_empty() == false);
770 assert!(!ns2.is_empty() == false);
771 }
772
773 #[test]
774 fn test_prefix_only_when_namespace_provided() {
775 let original_name = "search";
776
777 let with_ns = prefix_tool_name("web", original_name);
779 assert_eq!(with_ns, "web-search");
780
781 }
784
785 }