Skip to main content

ai_agent/utils/plugins/
mcp_plugin_integration.rs

1// Source: ~/claudecode/openclaudecode/src/utils/plugins/mcpPluginIntegration.rs
2#![allow(dead_code)]
3
4use std::collections::HashMap;
5use std::path::Path;
6
7use super::mcpb_handler::{McpServerConfig, is_mcpb_source, load_mcpb_file};
8use super::plugin_directories::get_plugin_data_dir;
9use super::plugin_options_storage::{
10    get_plugin_storage_id, load_plugin_options, substitute_plugin_variables,
11    substitute_user_config_variables,
12};
13use crate::plugin::types::{LoadedPlugin, PluginError};
14
15/// Scoped MCP server configuration.
16#[derive(Clone, Debug)]
17pub struct ScopedMcpServerConfig {
18    pub config: McpServerConfig,
19    pub scope: String,
20    pub plugin_source: String,
21}
22
23/// Load MCP servers from a plugin's manifest.
24pub async fn load_plugin_mcp_servers(
25    plugin: &LoadedPlugin,
26    errors: &mut Vec<PluginError>,
27) -> Result<Option<HashMap<String, McpServerConfig>>, String> {
28    let mut servers: HashMap<String, McpServerConfig> = HashMap::new();
29
30    // Check for .mcp.json in plugin directory first
31    let default_mcp_path = Path::new(&plugin.path).join(".mcp.json");
32    if default_mcp_path.exists() {
33        if let Ok(Some(default_servers)) =
34            load_mcp_servers_from_file(&plugin.path, ".mcp.json").await
35        {
36            servers.extend(default_servers);
37        }
38    }
39
40    // Handle manifest mcp_servers if present
41    if let Some(ref mcp_servers_spec) = plugin.manifest.mcp_servers {
42        if let Some(s) = mcp_servers_spec.as_str() {
43            if is_mcpb_source(s) {
44                if let Ok(Some(mcpb_servers)) = load_mcp_servers_from_mcpb(plugin, s, errors).await
45                {
46                    servers.extend(mcpb_servers);
47                }
48            } else {
49                if let Ok(Some(json_servers)) = load_mcp_servers_from_file(&plugin.path, s).await {
50                    servers.extend(json_servers);
51                }
52            }
53        } else if let Some(arr) = mcp_servers_spec.as_array() {
54            for spec in arr {
55                if let Some(s) = spec.as_str() {
56                    if is_mcpb_source(s) {
57                        if let Ok(Some(mcpb_servers)) =
58                            load_mcp_servers_from_mcpb(plugin, s, errors).await
59                        {
60                            servers.extend(mcpb_servers);
61                        }
62                    } else if let Ok(Some(json_servers)) =
63                        load_mcp_servers_from_file(&plugin.path, s).await
64                    {
65                        servers.extend(json_servers);
66                    }
67                } else if let Some(_obj) = spec.as_object() {
68                    // Inline MCP server configs - stub
69                }
70            }
71        } else if let Some(_obj) = mcp_servers_spec.as_object() {
72            // Direct MCP server configs - stub
73        }
74    }
75
76    if servers.is_empty() {
77        Ok(None)
78    } else {
79        Ok(Some(servers))
80    }
81}
82
83/// Load MCP servers from an MCPB file.
84async fn load_mcp_servers_from_mcpb(
85    plugin: &LoadedPlugin,
86    mcpb_path: &str,
87    errors: &mut Vec<PluginError>,
88) -> Result<Option<HashMap<String, McpServerConfig>>, String> {
89    log::debug!("Loading MCP servers from MCPB: {}", mcpb_path);
90
91    let plugin_id = &plugin.source;
92
93    match load_mcpb_file(mcpb_path, Path::new(&plugin.path), plugin_id).await {
94        Ok(Ok(result)) => {
95            let server_name = result.manifest.name;
96            log::debug!("Loaded MCP server \"{}\" from MCPB", server_name);
97            Ok(Some(result.mcp_config))
98        }
99        Ok(Err(_needs_config)) => {
100            log::debug!("MCPB {} requires user configuration", mcpb_path);
101            Ok(None)
102        }
103        Err(e) => {
104            log::debug!("Failed to load MCPB {}: {}", mcpb_path, e);
105            errors.push(PluginError::McpbExtractFailed {
106                source: plugin_id.clone(),
107                plugin: plugin.name.clone(),
108                mcpb_path: mcpb_path.to_string(),
109                reason: e.to_string(),
110            });
111            Ok(None)
112        }
113    }
114}
115
116/// Load MCP servers from a JSON file within a plugin.
117async fn load_mcp_servers_from_file(
118    plugin_path: &str,
119    relative_path: &str,
120) -> Result<Option<HashMap<String, McpServerConfig>>, String> {
121    let file_path = Path::new(plugin_path).join(relative_path);
122
123    let content = match tokio::fs::read_to_string(&file_path).await {
124        Ok(c) => c,
125        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
126        Err(e) => {
127            log::debug!("Failed to load MCP servers from {:?}: {}", file_path, e);
128            return Ok(None);
129        }
130    };
131
132    match serde_json::from_str::<HashMap<String, McpServerConfig>>(&content) {
133        Ok(servers) => Ok(Some(servers)),
134        Err(e) => {
135            log::debug!("Invalid MCP server config in {:?}: {}", file_path, e);
136            Ok(None)
137        }
138    }
139}
140
141/// Add plugin scope to MCP server configs.
142pub fn add_plugin_scope_to_servers(
143    servers: HashMap<String, McpServerConfig>,
144    plugin_name: &str,
145    plugin_source: &str,
146) -> HashMap<String, ScopedMcpServerConfig> {
147    servers
148        .into_iter()
149        .map(|(name, config)| {
150            let scoped_name = format!("plugin:{}:{}", plugin_name, name);
151            (
152                scoped_name,
153                ScopedMcpServerConfig {
154                    config,
155                    scope: "dynamic".to_string(),
156                    plugin_source: plugin_source.to_string(),
157                },
158            )
159        })
160        .collect()
161}
162
163/// Resolve environment variables for plugin MCP servers.
164pub fn resolve_plugin_mcp_environment(
165    config: &McpServerConfig,
166    plugin: &LoadedPlugin,
167    user_config: Option<&HashMap<String, serde_json::Value>>,
168) -> McpServerConfig {
169    let resolve_value = |value: &str| -> String {
170        let mut resolved = substitute_plugin_variables(value, &plugin.path, &plugin.source);
171
172        if let Some(uc) = user_config {
173            resolved = substitute_user_config_variables(&resolved, uc);
174        }
175
176        expand_env_vars_in_string(&resolved)
177    };
178
179    let mut resolved = config.clone();
180
181    if let Some(ref cmd) = config.command {
182        resolved.command = Some(resolve_value(cmd));
183    }
184
185    if let Some(ref args) = config.args {
186        resolved.args = Some(args.iter().map(|a| resolve_value(a)).collect());
187    }
188
189    let mut resolved_env: HashMap<String, String> = HashMap::new();
190    resolved_env.insert("CLAUDE_PLUGIN_ROOT".to_string(), plugin.path.clone());
191    resolved_env.insert(
192        "CLAUDE_PLUGIN_DATA".to_string(),
193        get_plugin_data_dir(&plugin.source),
194    );
195
196    if let Some(ref env) = config.env {
197        for (key, value) in env {
198            if key != "CLAUDE_PLUGIN_ROOT" && key != "CLAUDE_PLUGIN_DATA" {
199                resolved_env.insert(key.clone(), resolve_value(value));
200            }
201        }
202    }
203    resolved.env = Some(resolved_env);
204
205    resolved
206}
207
208fn expand_env_vars_in_string(value: &str) -> String {
209    let mut result = value.to_string();
210    let mut start = 0;
211    while let Some(pos) = result[start..].find("${") {
212        let abs_pos = start + pos;
213        if let Some(end) = result[abs_pos + 2..].find('}') {
214            let var_name = &result[abs_pos + 2..abs_pos + 2 + end];
215            if let Ok(env_value) = std::env::var(var_name) {
216                result.replace_range(abs_pos..abs_pos + 2 + end + 1, &env_value);
217                start = abs_pos + env_value.len();
218                continue;
219            }
220        }
221        start = abs_pos + 2;
222    }
223    result
224}
225
226/// Extract all MCP servers from loaded plugins.
227pub async fn extract_mcp_servers_from_plugins(
228    plugins: &[LoadedPlugin],
229    errors: &mut Vec<PluginError>,
230) -> HashMap<String, ScopedMcpServerConfig> {
231    let mut all_servers = HashMap::new();
232
233    for plugin in plugins {
234        if !plugin.enabled.unwrap_or(true) {
235            continue;
236        }
237
238        match load_plugin_mcp_servers(plugin, errors).await {
239            Ok(Some(servers)) => {
240                let scoped_servers =
241                    add_plugin_scope_to_servers(servers, &plugin.name, &plugin.source);
242                log::debug!(
243                    "Loaded {} MCP servers from plugin {}",
244                    scoped_servers.len(),
245                    plugin.name
246                );
247                all_servers.extend(scoped_servers);
248            }
249            _ => {}
250        }
251    }
252
253    all_servers
254}