vtcode_mcp/client/
status.rs1use super::*;
2
3impl McpClient {
4 pub fn get_status(&self) -> McpClientStatus {
6 let state = self.state.read();
7 let providers = &state.providers;
8 let mut configured_providers: Vec<String> = providers.keys().cloned().collect();
10 configured_providers.sort_unstable();
11 McpClientStatus {
12 enabled: self.config.enabled,
13 provider_count: providers.len(),
14 active_connections: providers.len(),
15 configured_providers,
16 }
17 }
18
19 pub async fn list_servers(&self) -> Vec<Value> {
25 let live: Vec<(McpProviderConfig, Option<Arc<McpProvider>>)> = {
26 let state = self.state.read();
27 self.config
28 .providers
29 .iter()
30 .map(|provider_config| (provider_config.clone(), state.providers.get(&provider_config.name).cloned()))
31 .collect()
32 };
33 let mut servers = Vec::with_capacity(live.len());
34 for (provider_config, provider) in live {
35 let negotiated = match &provider {
36 Some(provider) => provider.negotiated_protocol_version().await,
37 None => None,
38 };
39 let connected = provider.is_some();
40 let (transport, target) = match &provider_config.transport {
41 McpTransportConfig::Stdio(stdio) => ("stdio", Value::String(stdio.command.clone())),
42 McpTransportConfig::Http(http) => ("http", Value::String(http.endpoint.clone())),
43 };
44
45 servers.push(json!({
46 "name": provider_config.name,
47 "enabled": provider_config.enabled,
48 "connected": connected,
49 "connection_state": if connected { "connected" } else { "disconnected" },
50 "transport": transport,
51 "target": target,
52 "negotiated_protocol_version": negotiated.map(Value::String).unwrap_or(Value::Null),
53 }));
54 }
55 servers
56 }
57
58 pub fn allow_model_lifecycle_control(&self) -> bool {
60 self.config.lifecycle.allow_model_control
61 }
62
63 pub async fn connect_server(&self, server_name: &str) -> Result<()> {
65 if !self.config.enabled {
66 bail!("MCP support is disabled in the current configuration");
67 }
68 reject_unsupported_ui_apps_tier(&self.config.ui)?;
69
70 if self.state.read().providers.contains_key(server_name) {
71 return Ok(());
72 }
73
74 let provider_config = self
75 .config
76 .providers
77 .iter()
78 .find(|provider| provider.name == server_name)
79 .cloned()
80 .ok_or_else(|| anyhow!("MCP server '{server_name}' is not configured"))?;
81
82 if !provider_config.enabled {
83 bail!("MCP server '{server_name}' is configured but disabled");
84 }
85
86 if let Some(reason) = self.requirement_mismatch_reason(&provider_config) {
87 bail!("Cannot connect MCP server '{}': {}", provider_config.name, reason);
88 }
89
90 if matches!(provider_config.transport, McpTransportConfig::Http(_)) && !self.config.experimental_use_rmcp_client
91 {
92 bail!(
93 "Cannot connect MCP HTTP server '{}' while experimental_use_rmcp_client is disabled",
94 provider_config.name
95 );
96 }
97
98 let allowlist_snapshot = self.state.read().allowlist.clone();
99 let tool_timeout = self.tool_timeout();
100 let provider = self
101 .connect_and_initialize_provider(&provider_config, &allowlist_snapshot, tool_timeout)
102 .await?;
103
104 if let Err(err) = provider.cached_tools_or_refresh_shared(&allowlist_snapshot, tool_timeout).await {
105 warn!("Connected MCP server '{}' but failed to refresh tools: {err}", server_name);
106 } else if let Some(cache) = provider.cached_tools_shared().await {
107 self.record_tool_provider(&provider.name, &cache);
108 }
109
110 drop(self.state.write().providers.insert(provider.name.clone(), Arc::new(provider)));
111 Ok(())
112 }
113
114 pub async fn disconnect_server(&self, server_name: &str) -> Result<()> {
116 let provider = {
117 let mut state = self.state.write();
118 let provider = state
119 .providers
120 .remove(server_name)
121 .ok_or_else(|| anyhow!("MCP server '{server_name}' is not connected"))?;
122 state
123 .tool_provider_index
124 .retain(|_, provider_name| provider_name != server_name);
125 state
126 .resource_provider_index
127 .retain(|_, provider_name| provider_name != server_name);
128 state
129 .prompt_provider_index
130 .retain(|_, provider_name| provider_name != server_name);
131 provider
132 };
133
134 provider.shutdown().await?;
135 Ok(())
136 }
137
138 pub async fn sync_tools_to_files(&self, workspace_root: &Path) -> Result<(PathBuf, usize)> {
147 let tools = self.list_tools().await?;
148 let mcp_dir = workspace_root.join(".vtcode").join("mcp");
149 let tools_dir = mcp_dir.join("tools");
150
151 ensure_dir_exists(&tools_dir)
153 .await
154 .with_context(|| format!("Failed to create MCP tools directory: {}", tools_dir.display()))?;
155
156 let mut by_provider: BTreeMap<String, Vec<&McpToolInfo>> = BTreeMap::new();
158 for tool in &tools {
159 by_provider.entry(tool.provider.clone()).or_default().push(tool);
160 }
161
162 for (provider, provider_tools) in &by_provider {
164 let provider_dir = tools_dir.join(sanitize_filename(provider));
165 ensure_dir_exists(&provider_dir)
166 .await
167 .with_context(|| format!("Failed to create provider directory: {}", provider_dir.display()))?;
168
169 for tool in provider_tools {
170 let tool_content = format_tool_markdown(tool);
171 let tool_path = provider_dir.join(format!("{}.md", sanitize_filename(&tool.name)));
172 write_file_with_context(&tool_path, &tool_content, "MCP tool file")
173 .await
174 .with_context(|| format!("Failed to write tool file: {}", tool_path.display()))?;
175 }
176 }
177
178 let index_content = self.generate_tools_index(&tools, &by_provider);
180 let index_path = tools_dir.join("INDEX.md");
181 write_file_with_context(&index_path, &index_content, "MCP tools index")
182 .await
183 .with_context(|| format!("Failed to write MCP tools index: {}", index_path.display()))?;
184
185 let status = self.generate_status_json();
187 let status_path = mcp_dir.join("status.json");
188 let status_json = serde_json::to_string_pretty(&status)?;
189 write_file_with_context(&status_path, &status_json, "MCP status")
190 .await
191 .with_context(|| format!("Failed to write MCP status: {}", status_path.display()))?;
192
193 info!(
194 tools = tools.len(),
195 providers = by_provider.len(),
196 index = %index_path.display(),
197 "Synced MCP tool descriptions to files"
198 );
199
200 Ok((index_path, tools.len()))
201 }
202
203 fn generate_tools_index(&self, tools: &[McpToolInfo], by_provider: &BTreeMap<String, Vec<&McpToolInfo>>) -> String {
205 let mut content = String::new();
206 content.push_str("# MCP Tools Index\n\n");
207 content.push_str("This file lists all available MCP tools for dynamic discovery.\n");
208 content.push_str("Use `read_file` on individual tool files for full schema details.\n\n");
209
210 if tools.is_empty() {
211 content.push_str("*No MCP tools available.*\n\n");
212 content.push_str("Configure MCP servers in `vtcode.toml` or `.mcp.json`.\n");
213 } else {
214 content.push_str(&format!("**Total Tools**: {}\n\n", tools.len()));
215
216 content.push_str("## Quick Reference\n\n");
218 content.push_str("| Provider | Tool | Description |\n");
219 content.push_str("|----------|------|-------------|\n");
220
221 for tool in tools {
222 let desc = tool.description.lines().next().unwrap_or(&tool.description);
223 let desc_truncated = vtcode_commons::formatting::truncate_byte_budget(desc, 57, "...");
224 content.push_str(&format!(
225 "| {} | `{}` | {} |\n",
226 tool.provider,
227 tool.name,
228 desc_truncated.replace('|', "\\|")
229 ));
230 }
231
232 content.push_str("\n## Tools by Provider\n\n");
234 for (provider, provider_tools) in by_provider {
235 content.push_str(&format!("### {provider}\n\n"));
236 for tool in provider_tools {
237 content.push_str(&format!(
238 "- **{}**: {}\n - Path: `.vtcode/mcp/tools/{}/{}.md`\n",
239 tool.name,
240 tool.description.lines().next().unwrap_or(&tool.description),
241 sanitize_filename(provider),
242 sanitize_filename(&tool.name)
243 ));
244 }
245 content.push('\n');
246 }
247 }
248
249 content.push_str("\n---\n");
250 content.push_str("*Generated automatically. Do not edit manually.*\n");
251
252 content
253 }
254
255 fn generate_status_json(&self) -> Value {
257 let status = self.get_status();
258 json!({
259 "enabled": status.enabled,
260 "provider_count": status.provider_count,
261 "active_connections": status.active_connections,
262 "configured_providers": status.configured_providers,
263 "last_updated": Utc::now().to_rfc3339(),
264 })
265 }
266}