use dashmap::DashMap;
use rmcp::{
RoleClient,
model::{CallToolRequestParam, CallToolResult},
service::{DynService, RunningService},
};
use serde_json::Value;
use std::borrow::Cow;
use crate::Tool;
pub struct PegBoard {
tools: DashMap<String, Tool>,
services: Vec<(
String,
RunningService<RoleClient, Box<dyn DynService<RoleClient>>>,
)>,
tool_routing: DashMap<String, ToolRoute>,
namespace_tools: DashMap<String, Vec<String>>,
}
#[derive(Debug, Clone)]
struct ToolRoute {
service_index: usize,
original_name: String,
}
fn prefix_tool_name(namespace: &str, tool_name: &str) -> String {
format!("{}-{}", namespace, tool_name)
}
impl PegBoard {
pub fn new() -> Self {
Self {
tools: DashMap::new(),
services: Vec::new(),
tool_routing: DashMap::new(),
namespace_tools: DashMap::new(),
}
}
pub async fn add_service(
&mut self,
namespace: Option<String>,
service: RunningService<RoleClient, Box<dyn DynService<RoleClient>>>,
) -> Result<usize, PegBoardError> {
let namespace_str = namespace.unwrap_or_default();
let has_namespace = !namespace_str.is_empty();
if has_namespace && self.namespace_tools.contains_key(&namespace_str) {
return Err(PegBoardError::NamespaceAlreadyExists(namespace_str));
}
let service_idx = self.services.len();
let tools_response = service
.list_tools(None) .await
.map_err(|e| PegBoardError::ServiceError(format!("Failed to list tools: {:?}", e)))?;
let tools_list: Vec<Tool> = tools_response
.tools
.into_iter()
.map(|rmcp_tool| Tool {
name: rmcp_tool.name,
description: rmcp_tool.description.map(Cow::from),
input_schema: serde_json::Value::Object((*rmcp_tool.input_schema).clone()),
})
.collect();
self.services.push((namespace_str.clone(), service));
let mut registered_tool_names = Vec::new();
for original_tool in tools_list {
let original_name = original_tool.name.to_string();
let final_name = if has_namespace {
prefix_tool_name(&namespace_str, &original_name)
} else {
original_name.clone()
};
if self.tools.contains_key(&final_name) {
return Err(PegBoardError::ToolAlreadyExists(final_name));
}
let mut final_tool = original_tool.clone();
final_tool.name = Cow::Owned(final_name.clone());
self.tools.insert(final_name.clone(), final_tool);
self.tool_routing.insert(
final_name.clone(),
ToolRoute {
service_index: service_idx,
original_name,
},
);
registered_tool_names.push(final_name);
}
let tool_count = registered_tool_names.len();
if has_namespace {
self.namespace_tools
.insert(namespace_str, registered_tool_names);
}
Ok(tool_count)
}
pub fn register_tool(
&self,
namespace: Option<&str>,
tool: Tool,
service_idx: usize,
) -> Result<(), PegBoardError> {
if service_idx >= self.services.len() {
return Err(PegBoardError::InvalidServiceIndex {
index: service_idx,
max: self.services.len(),
});
}
let original_name = tool.name.to_string();
let namespace_str = namespace.unwrap_or("");
let has_namespace = !namespace_str.is_empty();
let final_name = if has_namespace {
prefix_tool_name(namespace_str, &original_name)
} else {
original_name.clone()
};
if self.tools.contains_key(&final_name) {
return Err(PegBoardError::ToolAlreadyExists(final_name));
}
let mut final_tool = tool;
final_tool.name = Cow::Owned(final_name.clone());
self.tools.insert(final_name.clone(), final_tool);
self.tool_routing.insert(
final_name.clone(),
ToolRoute {
service_index: service_idx,
original_name,
},
);
if has_namespace {
self.namespace_tools
.entry(namespace_str.to_string())
.or_default()
.push(final_name);
}
Ok(())
}
pub fn get_tool(&self, tool_name: &str) -> Option<Tool> {
self.tools.get(tool_name).map(|entry| entry.value().clone())
}
pub fn select_tools(&self, tool_names: &[&str]) -> Option<Vec<Tool>> {
let mut result = Vec::with_capacity(tool_names.len());
for &tool_name in tool_names {
let tool = self.get_tool(tool_name)?;
result.push(tool);
}
Some(result)
}
pub fn get_tool_route(&self, tool_name: &str) -> Option<(usize, String)> {
self.tool_routing.get(tool_name).map(|entry| {
let route = entry.value();
(route.service_index, route.original_name.clone())
})
}
pub fn list_tools_in_namespace(&self, namespace: &str) -> Vec<String> {
self.namespace_tools
.get(namespace)
.map(|entry| entry.value().clone())
.unwrap_or_default()
}
pub fn get_tools_in_namespace(&self, namespace: &str) -> Vec<Tool> {
self.list_tools_in_namespace(namespace)
.iter()
.filter_map(|tool_name| self.get_tool(tool_name))
.collect()
}
pub fn list_all_tools(&self) -> Vec<String> {
self.tools.iter().map(|entry| entry.key().clone()).collect()
}
pub fn get_all_tools(&self) -> Vec<Tool> {
self.tools
.iter()
.map(|entry| entry.value().clone())
.collect()
}
pub fn list_namespaces(&self) -> Vec<String> {
self.namespace_tools
.iter()
.map(|entry| entry.key().clone())
.collect()
}
pub fn unregister_tool(&self, prefixed_name: &str) -> Result<(), PegBoardError> {
if self.tools.remove(prefixed_name).is_none() {
return Err(PegBoardError::ToolNotFound(prefixed_name.to_string()));
}
self.tool_routing.remove(prefixed_name);
for mut namespace_entry in self.namespace_tools.iter_mut() {
namespace_entry.value_mut().retain(|n| n != prefixed_name);
}
Ok(())
}
pub fn unregister_namespace(&self, namespace: &str) -> Result<usize, PegBoardError> {
let prefixed_names = self.list_tools_in_namespace(namespace);
let count = prefixed_names.len();
for prefixed_name in prefixed_names {
self.tools.remove(&prefixed_name);
self.tool_routing.remove(&prefixed_name);
}
self.namespace_tools.remove(namespace);
Ok(count)
}
pub fn tool_count(&self) -> usize {
self.tools.len()
}
pub fn service_count(&self) -> usize {
self.services.len()
}
pub fn namespace_count(&self) -> usize {
self.namespace_tools.len()
}
pub async fn call_tool(
&self,
tool_name: &str,
arguments: Value,
) -> Result<CallToolResult, PegBoardError> {
let (service_idx, original_name) = self
.get_tool_route(tool_name)
.ok_or_else(|| PegBoardError::ToolNotFound(tool_name.to_string()))?;
let (_namespace, service) =
self.services
.get(service_idx)
.ok_or(PegBoardError::InvalidServiceIndex {
index: service_idx,
max: self.services.len(),
})?;
let arguments_obj = match arguments {
Value::Object(obj) => Some(obj),
Value::Null => None,
_ => {
return Err(PegBoardError::ServiceError(
"Tool arguments must be a JSON object or null".to_string(),
));
}
};
let param = CallToolRequestParam {
name: Cow::from(original_name),
arguments: arguments_obj,
};
service
.call_tool(param)
.await
.map_err(|e| PegBoardError::ServiceError(format!("Tool call failed: {:?}", e)))
}
}
impl Default for PegBoard {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, thiserror::Error)]
pub enum PegBoardError {
#[error("Tool '{0}' already exists")]
ToolAlreadyExists(String),
#[error("Tool '{0}' not found")]
ToolNotFound(String),
#[error("Invalid service index {index}, max is {max}")]
InvalidServiceIndex { index: usize, max: usize },
#[error("Namespace '{0}' already exists")]
NamespaceAlreadyExists(String),
#[error("Service error: {0}")]
ServiceError(String),
}
#[cfg(test)]
mod tests {
use super::*;
use schemars::JsonSchema;
#[derive(JsonSchema)]
#[allow(dead_code)]
struct TestParams {
value: String,
}
#[test]
fn test_prefix_tool_name() {
let prefixed = prefix_tool_name("web_search", "search");
assert_eq!(prefixed, "web_search-search");
let prefixed2 = prefix_tool_name("fs", "read_file");
assert_eq!(prefixed2, "fs-read_file");
}
#[test]
fn test_pegboard_register_and_get() {
let pegboard = PegBoard::new();
let tool = crate::get_tool::<TestParams, _, _>("search", Some("Search tool")).unwrap();
assert_eq!(pegboard.tool_count(), 0);
assert_eq!(pegboard.namespace_count(), 0);
assert!(
pegboard
.register_tool(Some("web"), tool.clone(), 0)
.is_err()
);
assert!(pegboard.register_tool(None, tool.clone(), 0).is_err());
}
#[test]
fn test_pegboard_tool_name_prefixing() {
let original_tool =
crate::get_tool::<TestParams, _, _>("search", Some("Search tool")).unwrap();
assert_eq!(original_tool.name, "search");
}
#[test]
fn test_pegboard_namespace_operations() {
let pegboard = PegBoard::new();
assert_eq!(pegboard.list_namespaces().len(), 0);
assert_eq!(pegboard.list_all_tools().len(), 0);
assert_eq!(pegboard.get_all_tools().len(), 0);
assert_eq!(pegboard.list_tools_in_namespace("nonexistent").len(), 0);
assert_eq!(pegboard.get_tools_in_namespace("nonexistent").len(), 0);
}
#[test]
fn test_pegboard_get_tool_methods() {
let pegboard = PegBoard::new();
assert!(pegboard.get_tool("web-search").is_none());
assert!(pegboard.get_tool_route("web-search").is_none());
}
#[test]
fn test_pegboard_unregister() {
let pegboard = PegBoard::new();
assert!(pegboard.unregister_tool("web-nonexistent").is_err());
let result = pegboard.unregister_namespace("nonexistent");
assert!(result.is_ok());
assert_eq!(result.unwrap(), 0); }
#[test]
fn test_tool_route_structure() {
let route = ToolRoute {
service_index: 0,
original_name: "search".to_string(),
};
assert_eq!(route.service_index, 0);
assert_eq!(route.original_name, "search");
}
#[test]
fn test_optional_namespace() {
let namespace_none: Option<String> = None;
let namespace_empty = Some(String::new());
let ns1 = namespace_none.unwrap_or_default();
let ns2 = namespace_empty.unwrap_or_default();
assert_eq!(ns1, "");
assert_eq!(ns2, "");
assert!(!ns1.is_empty() == false);
assert!(!ns2.is_empty() == false);
}
#[test]
fn test_prefix_only_when_namespace_provided() {
let original_name = "search";
let with_ns = prefix_tool_name("web", original_name);
assert_eq!(with_ns, "web-search");
}
}