pub mod factory;
pub mod meta_tool;
pub mod slot;
pub use factory::{AnyToolFactory, ToolFactoryRegistration};
pub use slot::{AnyToolSlot, TypedSlot};
use std::{
collections::HashMap,
sync::{Arc, OnceLock, RwLock},
};
use futures::future::BoxFuture;
use rmcp::{
ErrorData,
model::{CallToolRequestParams, CallToolResult, Content, Tool},
service::{Peer, RequestContext},
};
use schemars::JsonSchema;
use serde::{Serialize, de::DeserializeOwned};
use tracing::{debug, instrument, warn};
use crate::{plugin::ElicitPlugin, rmcp::RoleServer, traits::Elicitation};
pub struct DynamicToolDescriptor {
pub name: String,
pub description: String,
pub schema: serde_json::Value,
pub handler: Arc<
dyn Fn(serde_json::Value) -> BoxFuture<'static, Result<CallToolResult, ErrorData>>
+ Send
+ Sync,
>,
}
impl std::fmt::Debug for DynamicToolDescriptor {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DynamicToolDescriptor")
.field("name", &self.name)
.field("description", &self.description)
.finish()
}
}
impl DynamicToolDescriptor {
pub fn as_tool(&self) -> Tool {
let schema_obj = match &self.schema {
serde_json::Value::Object(m) => Arc::new(m.clone()),
_ => Arc::new(Default::default()),
};
Tool::new(self.name.clone(), self.description.clone(), schema_obj)
}
}
#[derive(Clone)]
pub struct DynamicToolRegistry {
factories: Vec<&'static dyn AnyToolFactory>,
slots: Arc<RwLock<HashMap<String, Box<dyn AnyToolSlot>>>>,
dynamic_tools: Arc<RwLock<Vec<DynamicToolDescriptor>>>,
peer: Arc<OnceLock<Peer<RoleServer>>>,
}
impl DynamicToolRegistry {
#[instrument]
pub fn new() -> Self {
let factories: Vec<&'static dyn AnyToolFactory> =
inventory::iter::<ToolFactoryRegistration>
.into_iter()
.map(|r| r.factory)
.collect();
debug!(
count = factories.len(),
"Collected tool factories from inventory"
);
Self {
factories,
slots: Arc::new(RwLock::new(HashMap::new())),
dynamic_tools: Arc::new(RwLock::new(Vec::new())),
peer: Arc::new(OnceLock::new()),
}
}
#[instrument(skip(self), fields(prefix))]
pub fn register_type<T>(self, prefix: impl Into<String>) -> Self
where
T: Serialize + DeserializeOwned + JsonSchema + Elicitation + Send + Sync + 'static,
{
let prefix = prefix.into();
tracing::Span::current().record("prefix", prefix.as_str());
let mut slots = self.slots.write().expect("slots lock poisoned");
assert!(
!slots.contains_key(&prefix),
"prefix `{prefix}` already registered in DynamicToolRegistry"
);
let slot: Box<dyn AnyToolSlot> = Box::new(TypedSlot::<T>::new(prefix.clone()));
slots.insert(prefix.clone(), slot);
debug!(%prefix, type_name = std::any::type_name::<T>(), "Registered type slot");
drop(slots);
self
}
#[instrument(skip(self), fields(tool_name))]
pub fn register_convert<T, U>(self) -> Self
where
T: Serialize + DeserializeOwned + JsonSchema + Send + Sync + 'static,
U: Serialize + DeserializeOwned + JsonSchema + Send + Sync + 'static,
{
let t_seg = type_leaf_snake::<T>();
let u_seg = type_leaf_snake::<U>();
let tool_name = format!("convert__{t_seg}__to__{u_seg}");
tracing::Span::current().record("tool_name", tool_name.as_str());
let schema = serde_json::to_value(schemars::schema_for!(T)).unwrap_or_default();
let t_name = std::any::type_name::<T>();
let u_name = std::any::type_name::<U>();
let description =
format!("Convert a `{t_name}` value to `{u_name}` via serde structural mapping.");
let handler: Arc<
dyn Fn(serde_json::Value) -> BoxFuture<'static, Result<CallToolResult, ErrorData>>
+ Send
+ Sync,
> = Arc::new(move |params| {
Box::pin(async move {
let t: T = serde_json::from_value(params).map_err(|e| {
ErrorData::invalid_params(format!("failed to deserialize {t_name}: {e}"), None)
})?;
let intermediate = serde_json::to_value(&t).map_err(|e| {
ErrorData::internal_error(format!("failed to serialize {t_name}: {e}"), None)
})?;
let u: U = serde_json::from_value(intermediate).map_err(|e| {
ErrorData::invalid_params(
format!("conversion from {t_name} to {u_name} failed: {e}"),
None,
)
})?;
let text = serde_json::to_string(&u).map_err(|e| {
ErrorData::internal_error(format!("failed to serialize {u_name}: {e}"), None)
})?;
Ok(CallToolResult::success(vec![Content::text(text)]))
})
});
let descriptor = DynamicToolDescriptor {
name: tool_name.clone(),
description,
schema,
handler,
};
let mut tools = self
.dynamic_tools
.write()
.expect("dynamic_tools lock poisoned");
assert!(
!tools.iter().any(|d| d.name == tool_name),
"convert tool `{tool_name}` already registered"
);
tools.push(descriptor);
debug!(tool_name, "Registered convert tool");
drop(tools);
self
}
pub fn set_peer(&self, peer: Peer<RoleServer>) {
let _ = self.peer.set(peer);
}
#[instrument(skip(self))]
pub async fn instantiate(
&self,
trait_name: &str,
prefix: &str,
) -> Result<CallToolResult, ErrorData> {
self.instantiate_for(trait_name, prefix).await
}
#[instrument(skip(self))]
async fn instantiate_for(
&self,
trait_name: &str,
prefix: &str,
) -> Result<CallToolResult, ErrorData> {
let factory = self
.factories
.iter()
.find(|f| f.trait_name() == trait_name)
.ok_or_else(|| {
ErrorData::invalid_params(
format!("no factory registered for trait `{trait_name}`"),
None,
)
})?;
let (new_descriptors, new_names) = {
let slots = self.slots.read().expect("slots lock poisoned");
let slot = slots.get(prefix).ok_or_else(|| {
ErrorData::invalid_params(
format!(
"no type registered under prefix `{prefix}`. \
Call register_type::<T>(\"{prefix}\") at startup."
),
None,
)
})?;
let descriptors = factory.instantiate(slot.as_ref())?;
let names: Vec<String> = descriptors.iter().map(|d| d.name.clone()).collect();
(descriptors, names)
};
debug!(
trait_name,
prefix,
count = new_descriptors.len(),
"Instantiated dynamic tools"
);
{
let mut tools = self
.dynamic_tools
.write()
.expect("dynamic_tools lock poisoned");
tools.retain(|d| !d.name.starts_with(&format!("{prefix}__")));
tools.extend(new_descriptors);
}
if let Some(peer) = self.peer.get() {
if let Err(e) = peer.notify_tool_list_changed().await {
warn!(error = ?e, "Failed to send notify_tool_list_changed");
}
} else {
debug!("No peer set — agent must re-call list_tools manually");
}
let summary = format!(
"Instantiated {} tools for `{prefix}`: {}",
new_names.len(),
new_names.join(", ")
);
Ok(CallToolResult::success(vec![Content::text(summary)]))
}
pub async fn invoke_dynamic(
&self,
name: &str,
args: serde_json::Value,
) -> Option<Result<CallToolResult, ErrorData>> {
let handler = {
let tools = self
.dynamic_tools
.read()
.expect("dynamic_tools lock poisoned");
tools.iter().find(|d| d.name == name)?.handler.clone()
};
Some(handler(args).await)
}
fn factory_meta_tools(&self) -> Vec<Tool> {
self.factories
.iter()
.map(|f| meta_tool::make_meta_tool(*f))
.collect()
}
}
impl Default for DynamicToolRegistry {
fn default() -> Self {
Self::new()
}
}
impl ElicitPlugin for DynamicToolRegistry {
fn name(&self) -> &'static str {
"dynamic"
}
fn list_tools(&self) -> Vec<Tool> {
let mut tools = self.factory_meta_tools();
let dynamic = self
.dynamic_tools
.read()
.expect("dynamic_tools lock poisoned");
tools.extend(dynamic.iter().map(|d| d.as_tool()));
tools
}
#[instrument(skip(self, _ctx), fields(tool = %params.name))]
fn call_tool<'a>(
&'a self,
params: CallToolRequestParams,
_ctx: RequestContext<RoleServer>,
) -> BoxFuture<'a, Result<CallToolResult, ErrorData>> {
let tool_name = params.name.to_string();
let args = params.arguments.clone().unwrap_or_default();
Box::pin(async move {
if let Some(trait_name) = self
.factories
.iter()
.find(|f| meta_tool::meta_tool_name(f.trait_name()) == tool_name)
.map(|f| f.trait_name())
{
let prefix = args
.get("prefix")
.and_then(|v| v.as_str())
.ok_or_else(|| ErrorData::invalid_params("missing `prefix` argument", None))?
.to_string();
return self.instantiate_for(trait_name, &prefix).await;
}
let handler = {
let tools = self
.dynamic_tools
.read()
.expect("dynamic_tools lock poisoned");
tools
.iter()
.find(|d| d.name == tool_name)
.ok_or_else(|| {
ErrorData::invalid_params(
format!("tool `{tool_name}` not found in dynamic registry"),
None,
)
})?
.handler
.clone()
};
let value = serde_json::Value::Object(args);
handler(value).await
})
}
}
fn type_leaf_snake<T: 'static>() -> String {
let full = std::any::type_name::<T>();
let without_generics = full.split('<').next().unwrap_or(full);
let leaf = without_generics
.rsplit("::")
.next()
.unwrap_or(without_generics);
camel_to_snake_rt(leaf)
}
fn camel_to_snake_rt(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 4);
for (i, ch) in s.chars().enumerate() {
if ch.is_uppercase() && i != 0 {
out.push('_');
}
out.extend(ch.to_lowercase());
}
out
}