vtcode_core/mcp/
plugin_providers.rs1use hashbrown::HashMap;
2use std::path::{Path, PathBuf};
3use tracing::warn;
4
5use vtcode_agent_plugins::{LoadedPlugin, plugin_roots_for};
6use vtcode_config::mcp::McpProviderConfig;
7
8pub fn discover_plugin_mcp_providers(workspace_root: &Path) -> Vec<McpProviderConfig> {
10 discover_plugin_mcp_providers_in_roots(&plugin_roots_for(workspace_root))
11}
12
13pub(crate) fn discover_plugin_mcp_providers_in_roots(roots: &[PathBuf]) -> Vec<McpProviderConfig> {
19 let mut providers = Vec::new();
20
21 let mut seen = std::collections::HashSet::new();
24 let mut unique_roots = Vec::new();
25 for root in roots {
26 if seen.insert(root.clone()) {
27 unique_roots.push(root.clone());
28 }
29 }
30
31 for root in unique_roots {
32 if !root.is_dir() {
33 continue;
34 }
35 let entries = match std::fs::read_dir(&root) {
36 Ok(entries) => entries,
37 Err(e) => {
38 tracing::debug!(root = %root.display(), error = %e, "failed to read plugin root directory");
39 continue;
40 }
41 };
42 for entry in entries {
43 let Ok(entry) = entry else { continue };
44 let path = entry.path();
45 if !path.is_dir() {
46 continue;
47 }
48 match LoadedPlugin::load_from_dir(&path) {
49 Ok(loaded) => {
50 if let Some(mcp_config) = loaded.mcp {
51 for (server_name, server) in mcp_config.servers {
52 match map_server_to_provider(&loaded.manifest.name, &server_name, server, &loaded.root) {
53 Ok(provider) => providers.push(provider),
54 Err(e) => {
55 warn!(plugin = %loaded.manifest.name, server = %server_name, error = %e, "skipping plugin MCP server")
56 }
57 }
58 }
59 }
60 }
61 Err(e) => {
62 warn!(root = %path.display(), error = %e, "failed to load agent plugin for MCP discovery");
63 }
64 }
65 }
66 }
67
68 providers
69}
70
71fn map_server_to_provider(
72 plugin_name: &str,
73 server_name: &str,
74 server: vtcode_agent_plugins::ServerConfig,
75 plugin_root: &Path,
76) -> Result<McpProviderConfig, vtcode_agent_plugins::PluginError> {
77 let provider_name = format!("{}.{}", plugin_name, server_name);
78
79 match server {
80 vtcode_agent_plugins::ServerConfig::Stdio(stdio) => {
81 let plugin_data = vtcode_agent_plugins::default_plugin_data_dir(plugin_name);
86 if let Err(e) = std::fs::create_dir_all(&plugin_data) {
87 return Err(vtcode_agent_plugins::PluginError::InvalidMcp(format!(
88 "server '{server_name}' cannot use plugin data dir {}: {e}",
89 plugin_data.display()
90 )));
91 }
92
93 for key in stdio.env.keys() {
96 if vtcode_agent_plugins::is_reserved_env_key(key) {
97 return Err(vtcode_agent_plugins::PluginError::InvalidMcp(format!(
98 "server '{server_name}' env must not set reserved '{key}'"
99 )));
100 }
101 }
102 if let Err(reason) = vtcode_agent_plugins::validate_command_token(&stdio.command) {
103 return Err(vtcode_agent_plugins::PluginError::InvalidMcp(format!(
104 "server '{server_name}' has invalid command: {reason}"
105 )));
106 }
107 if let Some(ref raw_cwd) = stdio.cwd {
108 if let Err(reason) = vtcode_agent_plugins::validate_cwd_form(raw_cwd) {
109 return Err(vtcode_agent_plugins::PluginError::InvalidMcp(format!(
110 "server '{server_name}' has invalid cwd: {reason}"
111 )));
112 }
113 }
114
115 let command = if stdio.command.starts_with("./") {
120 vtcode_agent_plugins::validate_plugin_relative(&stdio.command, plugin_root)
121 .map_err(|e| vtcode_agent_plugins::PluginError::PathEscape(e.to_string()))?
122 .to_string_lossy()
123 .to_string()
124 } else {
125 stdio.command
126 };
127
128 let canonical_root =
130 vtcode_commons::canonicalize(plugin_root).unwrap_or_else(|_| plugin_root.to_path_buf());
131 let canonical_data = vtcode_commons::canonicalize(&plugin_data).unwrap_or_else(|_| plugin_data.clone());
132
133 let mut env = stdio
134 .env
135 .into_iter()
136 .map(|(k, v)| (k, vtcode_agent_plugins::expand_placeholders(&v, &canonical_root, &canonical_data)))
137 .collect::<HashMap<String, String>>();
138 #[cfg(windows)]
141 {
142 env.retain(|k, _| k.to_ascii_lowercase() != "plugin_root" && k.to_ascii_lowercase() != "plugin_data");
143 }
144 env.insert("PLUGIN_ROOT".into(), canonical_root.to_string_lossy().to_string());
145 env.insert("PLUGIN_DATA".into(), canonical_data.to_string_lossy().to_string());
146
147 let resolved_cwd =
148 vtcode_agent_plugins::resolve_cwd(stdio.cwd.as_deref(), &canonical_root, &canonical_data)
149 .map_err(|e| vtcode_agent_plugins::PluginError::PathEscape(e.to_string()))?;
150 let cwd = resolved_cwd.to_string_lossy().to_string();
151
152 let args = stdio
153 .args
154 .iter()
155 .map(|a| vtcode_agent_plugins::expand_placeholders(a, &canonical_root, &canonical_data))
156 .collect();
157
158 Ok(McpProviderConfig {
159 name: provider_name,
160 transport: vtcode_config::mcp::McpTransportConfig::Stdio(vtcode_config::mcp::McpStdioServerConfig {
161 command,
162 args,
163 working_directory: Some(cwd),
164 }),
165 env,
166 ..McpProviderConfig::default()
167 })
168 }
169 vtcode_agent_plugins::ServerConfig::StreamableHttp(http) => Ok(McpProviderConfig {
170 name: provider_name,
171 transport: vtcode_config::mcp::McpTransportConfig::Http(vtcode_config::mcp::McpHttpServerConfig {
172 endpoint: http.url,
173 api_key_env: None,
174 oauth: None,
175 protocol_version: vtcode_config::mcp::MCP_STABLE_PROTOCOL_VERSION.into(),
176 handshake: vtcode_config::mcp::McpHttpHandshakeMode::Legacy,
177 http_headers: http.headers.into_iter().collect(),
178 env_http_headers: HashMap::new(),
179 }),
180 ..McpProviderConfig::default()
181 }),
182 vtcode_agent_plugins::ServerConfig::Sse(_) => {
183 Err(vtcode_agent_plugins::PluginError::InvalidMcp("SSE transport is not supported by VT Code".into()))
184 }
185 }
186}