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