mcp_utils/client/
mcp_snapshot.rs1use super::{
2 McpClient, McpError, ToolCatalog, ToolRoute,
3 naming::{create_namespaced_tool_name, split_on_server_name},
4};
5use llm::ToolDefinition;
6use rmcp::{RoleClient, model::CallToolRequestParams, service::RunningService};
7use serde_json::{Map, Value};
8use std::{collections::HashMap, fmt, sync::Arc};
9
10#[derive(Clone, Default)]
11pub struct McpSnapshot {
12 catalog: Arc<ToolCatalog>,
13 clients: Arc<HashMap<String, Arc<RunningService<RoleClient, McpClient>>>>,
14}
15
16impl fmt::Debug for McpSnapshot {
17 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
18 formatter
19 .debug_struct("McpSnapshot")
20 .field("catalog", &self.catalog)
21 .field("connected_servers", &self.clients.keys().collect::<Vec<_>>())
22 .finish()
23 }
24}
25
26impl McpSnapshot {
27 pub fn new(
28 catalog: Arc<ToolCatalog>,
29 clients: Arc<HashMap<String, Arc<RunningService<RoleClient, McpClient>>>>,
30 ) -> Self {
31 Self { catalog, clients }
32 }
33
34 pub fn catalog(&self) -> &Arc<ToolCatalog> {
35 &self.catalog
36 }
37
38 pub fn tool_definitions(&self) -> Vec<ToolDefinition> {
39 self.catalog.tools().model_visible.into_iter().map(|tool| tool.definition().clone()).collect()
40 }
41
42 pub fn model_instructions(&self) -> std::collections::BTreeMap<String, String> {
43 self.catalog.model_instructions()
44 }
45
46 pub fn server_statuses(&self) -> Vec<crate::status::McpServerStatusEntry> {
47 self.catalog.server_statuses()
48 }
49
50 pub fn resolve(
51 &self,
52 route: ToolRoute,
53 arguments: Map<String, Value>,
54 ) -> super::Result<(Arc<RunningService<RoleClient, McpClient>>, CallToolRequestParams)> {
55 if let ToolRoute::ModelVisible { namespaced_name } = &route {
56 split_on_server_name(namespaced_name)
57 .ok_or_else(|| McpError::InvalidToolNameFormat(namespaced_name.clone()))?;
58 }
59 if !self.catalog.route_permitted(&route) {
60 let (tool_name, namespaced_name) = match &route {
61 ToolRoute::ModelVisible { namespaced_name } => (namespaced_name.clone(), namespaced_name.clone()),
62 ToolRoute::Deferred { server, tool } => (tool.clone(), create_namespaced_tool_name(server, tool)),
63 };
64 if matches!(route, ToolRoute::Deferred { .. })
65 && self.catalog.route_permitted(&ToolRoute::ModelVisible { namespaced_name: namespaced_name.clone() })
66 {
67 return Err(McpError::DirectToolRequiresDirectRoute { tool_name, direct_name: namespaced_name });
68 }
69 return Err(McpError::ToolNotFound(namespaced_name));
70 }
71 let (server, tool) = match route {
72 ToolRoute::ModelVisible { namespaced_name } => {
73 let (server, tool) =
74 split_on_server_name(&namespaced_name).expect("model-visible route was validated above");
75 (server.to_string(), tool.to_string())
76 }
77 ToolRoute::Deferred { server, tool } => (server, tool),
78 };
79 let client = self.clients.get(&server).cloned().ok_or_else(|| McpError::ServerNotFound(server.clone()))?;
80 Ok((client, CallToolRequestParams::new(tool).with_arguments(arguments)))
81 }
82
83 pub fn clients_with_prompts(&self) -> Vec<(String, Arc<RunningService<RoleClient, McpClient>>)> {
84 self.clients
85 .iter()
86 .filter(|(_, client)| client.peer_info().is_some_and(|info| info.capabilities.prompts.is_some()))
87 .map(|(name, client)| (name.clone(), Arc::clone(client)))
88 .collect()
89 }
90
91 pub fn client_for_prompt(
92 &self,
93 namespaced_name: &str,
94 ) -> super::Result<(String, Arc<RunningService<RoleClient, McpClient>>)> {
95 let (server, prompt) = split_on_server_name(namespaced_name)
96 .ok_or_else(|| McpError::InvalidToolNameFormat(namespaced_name.to_string()))?;
97 let client = self.clients.get(server).cloned().ok_or_else(|| McpError::ServerNotFound(server.to_string()))?;
98 Ok((prompt.to_string(), client))
99 }
100}