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(
101 &self,
102 namespace: Option<String>,
103 service: RunningService<RoleClient, Box<dyn DynService<RoleClient>>>,
104 ) -> Result<usize, PegBoardError> {
105 let namespace_str = namespace.unwrap_or_default();
107 let has_namespace = !namespace_str.is_empty();
108
109 if has_namespace && self.namespace_tools.contains_key(&namespace_str) {
111 return Err(PegBoardError::NamespaceAlreadyExists(namespace_str));
112 }
113
114 let service_idx = self.next_service_idx.fetch_add(1, Ordering::SeqCst);
117
118 let tools_response = service
120 .list_tools(None) .await
122 .map_err(|e| PegBoardError::ServiceError(format!("Failed to list tools: {:?}", e)))?;
123
124 let tools_list: Vec<Tool> = tools_response
126 .tools
127 .into_iter()
128 .map(|rmcp_tool| Tool {
129 name: rmcp_tool.name,
130 description: rmcp_tool.description.map(Cow::from),
131 input_schema: serde_json::Value::Object((*rmcp_tool.input_schema).clone()),
132 })
133 .collect();
134
135 self.services
137 .insert(service_idx, (namespace_str.clone(), service));
138
139 let mut registered_tool_names = Vec::new();
141
142 for original_tool in tools_list {
144 let original_name = original_tool.name.to_string();
145
146 let final_name = if has_namespace {
148 prefix_tool_name(&namespace_str, &original_name)
149 } else {
150 original_name.clone()
151 };
152
153 if self.tools.contains_key(&final_name) {
155 return Err(PegBoardError::ToolAlreadyExists(final_name));
156 }
157
158 let mut final_tool = original_tool.clone();
160 final_tool.name = Cow::Owned(final_name.clone());
161
162 self.tools.insert(final_name.clone(), final_tool);
164
165 self.tool_routing.insert(
167 final_name.clone(),
168 ToolRoute {
169 service_index: service_idx,
170 original_name,
171 },
172 );
173
174 registered_tool_names.push(final_name);
175 }
176
177 let tool_count = registered_tool_names.len();
178
179 if has_namespace {
181 self.namespace_tools
182 .insert(namespace_str, registered_tool_names);
183 }
184
185 Ok(tool_count)
186 }
187
188 pub fn register_tool(
193 &self,
194 namespace: Option<&str>,
195 tool: Tool,
196 service_idx: usize,
197 ) -> Result<(), PegBoardError> {
198 if !self.services.contains_key(&service_idx) {
200 return Err(PegBoardError::InvalidServiceIndex {
201 index: service_idx,
202 max: self.services.len(),
203 });
204 }
205
206 let original_name = tool.name.to_string();
207 let namespace_str = namespace.unwrap_or("");
208 let has_namespace = !namespace_str.is_empty();
209
210 let final_name = if has_namespace {
212 prefix_tool_name(namespace_str, &original_name)
213 } else {
214 original_name.clone()
215 };
216
217 if self.tools.contains_key(&final_name) {
219 return Err(PegBoardError::ToolAlreadyExists(final_name));
220 }
221
222 let mut final_tool = tool;
224 final_tool.name = Cow::Owned(final_name.clone());
225
226 self.tools.insert(final_name.clone(), final_tool);
228 self.tool_routing.insert(
229 final_name.clone(),
230 ToolRoute {
231 service_index: service_idx,
232 original_name,
233 },
234 );
235
236 if has_namespace {
238 self.namespace_tools
239 .entry(namespace_str.to_string())
240 .or_default()
241 .push(final_name);
242 }
243
244 Ok(())
245 }
246
247 pub fn get_tool(&self, tool_name: &str) -> Option<Tool> {
250 self.tools.get(tool_name).map(|entry| entry.value().clone())
251 }
252
253 pub fn select_tools(&self, tool_names: &[&str]) -> Option<Vec<Tool>> {
274 let mut result = Vec::with_capacity(tool_names.len());
275
276 for &tool_name in tool_names {
277 let tool = self.get_tool(tool_name)?;
278 result.push(tool);
279 }
280
281 Some(result)
282 }
283
284 pub fn get_tool_route(&self, tool_name: &str) -> Option<(usize, String)> {
287 self.tool_routing.get(tool_name).map(|entry| {
288 let route = entry.value();
289 (route.service_index, route.original_name.clone())
290 })
291 }
292
293 pub fn list_tools_in_namespace(&self, namespace: &str) -> Vec<String> {
295 self.namespace_tools
296 .get(namespace)
297 .map(|entry| entry.value().clone())
298 .unwrap_or_default()
299 }
300
301 pub fn get_tools_in_namespace(&self, namespace: &str) -> Vec<Tool> {
303 self.list_tools_in_namespace(namespace)
304 .iter()
305 .filter_map(|tool_name| self.get_tool(tool_name))
306 .collect()
307 }
308
309 pub fn list_all_tools(&self) -> Vec<String> {
312 self.tools.iter().map(|entry| entry.key().clone()).collect()
313 }
314
315 pub fn get_all_tools(&self) -> Vec<Tool> {
318 self.tools
319 .iter()
320 .map(|entry| entry.value().clone())
321 .collect()
322 }
323
324 pub fn list_namespaces(&self) -> Vec<String> {
326 self.namespace_tools
327 .iter()
328 .map(|entry| entry.key().clone())
329 .collect()
330 }
331
332 pub fn unregister_tool(&self, prefixed_name: &str) -> Result<(), PegBoardError> {
334 if self.tools.remove(prefixed_name).is_none() {
335 return Err(PegBoardError::ToolNotFound(prefixed_name.to_string()));
336 }
337
338 self.tool_routing.remove(prefixed_name);
339
340 for mut namespace_entry in self.namespace_tools.iter_mut() {
343 namespace_entry.value_mut().retain(|n| n != prefixed_name);
344 }
345
346 Ok(())
347 }
348
349 pub fn unregister_namespace(&self, namespace: &str) -> Result<usize, PegBoardError> {
351 let prefixed_names = self.list_tools_in_namespace(namespace);
352 let count = prefixed_names.len();
353
354 for prefixed_name in prefixed_names {
356 self.tools.remove(&prefixed_name);
357 self.tool_routing.remove(&prefixed_name);
358 }
359
360 let service_idx_to_remove = self
362 .services
363 .iter()
364 .find(|entry| entry.value().0 == namespace)
365 .map(|entry| *entry.key());
366
367 if let Some(idx) = service_idx_to_remove {
368 self.services.remove(&idx);
369 }
370
371 self.namespace_tools.remove(namespace);
372 Ok(count)
373 }
374
375 pub fn unregister_service(&self, service_idx: usize) -> Result<usize, PegBoardError> {
377 let namespace = self
379 .services
380 .get(&service_idx)
381 .map(|entry| entry.value().0.clone())
382 .ok_or(PegBoardError::InvalidServiceIndex {
383 index: service_idx,
384 max: self.next_service_idx.load(Ordering::SeqCst),
385 })?;
386
387 let tools_to_remove: Vec<String> = self
389 .tool_routing
390 .iter()
391 .filter(|entry| entry.value().service_index == service_idx)
392 .map(|entry| entry.key().clone())
393 .collect();
394
395 let count = tools_to_remove.len();
396 for tool_name in tools_to_remove {
397 self.tools.remove(&tool_name);
398 self.tool_routing.remove(&tool_name);
399 }
400
401 self.services.remove(&service_idx);
403
404 if !namespace.is_empty() {
406 self.namespace_tools.remove(&namespace);
407 }
408
409 Ok(count)
410 }
411
412 pub fn tool_count(&self) -> usize {
414 self.tools.len()
415 }
416
417 pub fn service_count(&self) -> usize {
419 self.services.len()
420 }
421
422 pub fn namespace_count(&self) -> usize {
424 self.namespace_tools.len()
425 }
426
427 pub async fn call_tool(
454 &self,
455 tool_name: &str,
456 arguments: Value,
457 ) -> Result<CallToolResult, PegBoardError> {
458 let (service_idx, original_name) = self
460 .get_tool_route(tool_name)
461 .ok_or_else(|| PegBoardError::ToolNotFound(tool_name.to_string()))?;
462
463 let service_entry =
465 self.services
466 .get(&service_idx)
467 .ok_or(PegBoardError::InvalidServiceIndex {
468 index: service_idx,
469 max: self.services.len(),
470 })?;
471 let (_namespace, service) = service_entry.value();
472
473 let arguments_obj = match arguments {
475 Value::Object(obj) => Some(obj),
476 Value::Null => None,
477 _ => {
478 return Err(PegBoardError::ServiceError(
479 "Tool arguments must be a JSON object or null".to_string(),
480 ));
481 }
482 };
483
484 let param = CallToolRequestParam {
486 name: Cow::from(original_name),
487 arguments: arguments_obj,
488 };
489
490 service
491 .call_tool(param)
492 .await
493 .map_err(|e| PegBoardError::ServiceError(format!("Tool call failed: {:?}", e)))
494 }
495}
496
497#[derive(Clone)]
525pub struct PegBoard {
526 inner: Arc<InternalPegBoard>,
527}
528
529impl PegBoard {
530 pub fn new() -> Self {
532 Self {
533 inner: Arc::new(InternalPegBoard::new()),
534 }
535 }
536
537 pub async fn add_service(
541 &self,
542 namespace: Option<String>,
543 service: RunningService<RoleClient, Box<dyn DynService<RoleClient>>>,
544 ) -> Result<usize, PegBoardError> {
545 self.inner.add_service(namespace, service).await
546 }
547
548 pub fn register_tool(
550 &self,
551 namespace: Option<&str>,
552 tool: Tool,
553 service_idx: usize,
554 ) -> Result<(), PegBoardError> {
555 self.inner.register_tool(namespace, tool, service_idx)
556 }
557
558 pub fn get_tool(&self, tool_name: &str) -> Option<Tool> {
560 self.inner.get_tool(tool_name)
561 }
562
563 pub fn select_tools(&self, tool_names: &[&str]) -> Option<Vec<Tool>> {
565 self.inner.select_tools(tool_names)
566 }
567
568 pub fn get_tool_route(&self, tool_name: &str) -> Option<(usize, String)> {
570 self.inner.get_tool_route(tool_name)
571 }
572
573 pub fn list_tools_in_namespace(&self, namespace: &str) -> Vec<String> {
575 self.inner.list_tools_in_namespace(namespace)
576 }
577
578 pub fn get_tools_in_namespace(&self, namespace: &str) -> Vec<Tool> {
580 self.inner.get_tools_in_namespace(namespace)
581 }
582
583 pub fn list_all_tools(&self) -> Vec<String> {
585 self.inner.list_all_tools()
586 }
587
588 pub fn get_all_tools(&self) -> Vec<Tool> {
590 self.inner.get_all_tools()
591 }
592
593 pub fn list_namespaces(&self) -> Vec<String> {
595 self.inner.list_namespaces()
596 }
597
598 pub fn unregister_tool(&self, prefixed_name: &str) -> Result<(), PegBoardError> {
600 self.inner.unregister_tool(prefixed_name)
601 }
602
603 pub fn unregister_namespace(&self, namespace: &str) -> Result<usize, PegBoardError> {
605 self.inner.unregister_namespace(namespace)
606 }
607
608 pub fn unregister_service(&self, service_idx: usize) -> Result<usize, PegBoardError> {
610 self.inner.unregister_service(service_idx)
611 }
612
613 pub fn tool_count(&self) -> usize {
615 self.inner.tool_count()
616 }
617
618 pub fn service_count(&self) -> usize {
620 self.inner.service_count()
621 }
622
623 pub fn namespace_count(&self) -> usize {
625 self.inner.namespace_count()
626 }
627
628 pub async fn call_tool(
630 &self,
631 tool_name: &str,
632 arguments: Value,
633 ) -> Result<CallToolResult, PegBoardError> {
634 self.inner.call_tool(tool_name, arguments).await
635 }
636}
637
638impl Default for PegBoard {
639 fn default() -> Self {
640 Self::new()
641 }
642}
643
644#[derive(Debug, thiserror::Error)]
646pub enum PegBoardError {
647 #[error("Tool '{0}' already exists")]
648 ToolAlreadyExists(String),
649
650 #[error("Tool '{0}' not found")]
651 ToolNotFound(String),
652
653 #[error("Invalid service index {index}, max is {max}")]
654 InvalidServiceIndex { index: usize, max: usize },
655
656 #[error("Namespace '{0}' already exists")]
657 NamespaceAlreadyExists(String),
658
659 #[error("Service error: {0}")]
660 ServiceError(String),
661}
662
663#[cfg(test)]
664mod tests {
665 use super::*;
666 use schemars::JsonSchema;
667
668 #[derive(JsonSchema)]
669 #[allow(dead_code)]
670 struct TestParams {
671 value: String,
672 }
673
674 #[test]
675 fn test_prefix_tool_name() {
676 let prefixed = prefix_tool_name("web_search", "search");
677 assert_eq!(prefixed, "web_search-search");
678
679 let prefixed2 = prefix_tool_name("fs", "read_file");
680 assert_eq!(prefixed2, "fs-read_file");
681 }
682
683 #[test]
684 fn test_pegboard_register_and_get() {
685 let pegboard = PegBoard::new();
686 let tool = crate::get_tool::<TestParams, _, _>("search", Some("Search tool")).unwrap();
687
688 assert_eq!(pegboard.tool_count(), 0);
690 assert_eq!(pegboard.namespace_count(), 0);
691
692 assert!(
694 pegboard
695 .register_tool(Some("web"), tool.clone(), 0)
696 .is_err()
697 );
698
699 assert!(pegboard.register_tool(None, tool.clone(), 0).is_err());
701 }
702
703 #[test]
704 fn test_pegboard_tool_name_prefixing() {
705 let original_tool =
707 crate::get_tool::<TestParams, _, _>("search", Some("Search tool")).unwrap();
708 assert_eq!(original_tool.name, "search");
709
710 }
714
715 #[test]
716 fn test_pegboard_namespace_operations() {
717 let pegboard = PegBoard::new();
718
719 assert_eq!(pegboard.list_namespaces().len(), 0);
721 assert_eq!(pegboard.list_all_tools().len(), 0);
722 assert_eq!(pegboard.get_all_tools().len(), 0);
723
724 assert_eq!(pegboard.list_tools_in_namespace("nonexistent").len(), 0);
726 assert_eq!(pegboard.get_tools_in_namespace("nonexistent").len(), 0);
727 }
728
729 #[test]
730 fn test_pegboard_get_tool_methods() {
731 let pegboard = PegBoard::new();
732
733 assert!(pegboard.get_tool("web-search").is_none());
735 assert!(pegboard.get_tool_route("web-search").is_none());
736 }
737
738 #[test]
739 fn test_pegboard_unregister() {
740 let pegboard = PegBoard::new();
741
742 assert!(pegboard.unregister_tool("web-nonexistent").is_err());
744
745 let result = pegboard.unregister_namespace("nonexistent");
747 assert!(result.is_ok());
748 assert_eq!(result.unwrap(), 0); }
750
751 #[test]
752 fn test_tool_route_structure() {
753 let route = ToolRoute {
754 service_index: 0,
755 original_name: "search".to_string(),
756 };
757
758 assert_eq!(route.service_index, 0);
759 assert_eq!(route.original_name, "search");
760 }
761
762 #[test]
763 fn test_optional_namespace() {
764 let namespace_none: Option<String> = None;
766 let namespace_empty = Some(String::new());
767
768 let ns1 = namespace_none.unwrap_or_default();
770 let ns2 = namespace_empty.unwrap_or_default();
771
772 assert_eq!(ns1, "");
773 assert_eq!(ns2, "");
774 assert!(!ns1.is_empty() == false);
775 assert!(!ns2.is_empty() == false);
776 }
777
778 #[test]
779 fn test_prefix_only_when_namespace_provided() {
780 let original_name = "search";
781
782 let with_ns = prefix_tool_name("web", original_name);
784 assert_eq!(with_ns, "web-search");
785
786 }
789
790 }