use std::sync::Arc;
use tokio::sync::Mutex;
use super::{ExitWatch, McpClient, McpToolAdapter, ServerNotifications};
use crate::tool_source::ToolSource;
use crate::types::AgentTool;
pub struct McpToolSource {
client: Arc<Mutex<McpClient>>,
notifications: ServerNotifications,
exit: Option<ExitWatch>,
exit_logged: std::sync::atomic::AtomicBool,
prefix: Option<String>,
last: std::sync::Mutex<Vec<Arc<dyn AgentTool>>>,
}
impl McpToolSource {
pub fn new(client: McpClient) -> Self {
if !client.reports_notifications() {
tracing::warn!(
"MCP tool source: the client's transport does not report server \
notifications; its tools are listed once per run, never mid-run"
);
}
let notifications = client.notifications();
let exit = client.exit_watch();
Self {
client: Arc::new(Mutex::new(client)),
notifications,
exit,
exit_logged: std::sync::atomic::AtomicBool::new(false),
prefix: None,
last: std::sync::Mutex::new(Vec::new()),
}
}
pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
self.prefix = Some(prefix.into());
self
}
fn exited(&self) -> Option<String> {
self.exit.as_ref()?.exited()
}
pub fn client(&self) -> &Arc<Mutex<McpClient>> {
&self.client
}
}
#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
impl ToolSource for McpToolSource {
async fn tools(&self) -> Vec<Arc<dyn AgentTool>> {
if let Some(why) = self.exited() {
if !self
.exit_logged
.swap(true, std::sync::atomic::Ordering::SeqCst)
{
let server = self
.client
.lock()
.await
.server_info()
.map(|info| info.name.clone());
tracing::warn!(
server = server.as_deref().unwrap_or("?"),
prefix = self.prefix.as_deref().unwrap_or(""),
"MCP tool source: the server has gone; its tools are withdrawn: {why}"
);
}
self.last.lock().unwrap_or_else(|p| p.into_inner()).clear();
return Vec::new();
}
let (listed, timeout) = {
let client = self.client.lock().await;
let timeout = client.call_timeout();
let listed = match timeout {
Some(limit) => crate::rt::timeout(limit, client.list_tools())
.await
.unwrap_or_else(|_| {
Err(super::McpError::Transport(format!(
"tools/list did not answer within {limit:?}"
)))
}),
None => client.list_tools().await,
};
(listed, timeout)
};
let tools: Vec<Arc<dyn AgentTool>> = match listed {
Ok(tools) => tools
.into_iter()
.map(|tool| {
let adapter =
McpToolAdapter::new(self.client.clone(), tool).with_call_timeout(timeout);
let adapter = match &self.prefix {
Some(prefix) => adapter.with_prefix(prefix.clone()),
None => adapter,
};
Arc::new(adapter) as Arc<dyn AgentTool>
})
.collect(),
Err(e) => {
let last = self.last.lock().unwrap_or_else(|p| p.into_inner()).clone();
tracing::warn!(
kept = last.len(),
"MCP tool source: tools/list failed; keeping the last list that succeeded: {e}"
);
return last;
}
};
*self.last.lock().unwrap_or_else(|p| p.into_inner()) = tools.clone();
tools
}
fn generation(&self) -> u64 {
self.notifications.tools_list_changed() + u64::from(self.exited().is_some())
}
}