diff --git a/docs/guides/mcp-integration.md b/docs/guides/mcp-integration.md index 4f2c92809..b242dc0c5 100644 --- a/docs/guides/mcp-integration.md +++ b/docs/guides/mcp-integration.md @@ -34,11 +34,22 @@ allowlists control tool access, and how to troubleshoot common configuration iss command = "uvx" args = ["mcp-server-time"] max_concurrent_requests = 2 + + [[mcp.providers]] + name = "context7" + enabled = true + command = "npx" + args = ["-y", "@upstash/context7-mcp@latest"] + auth = { api_key_env = "CONTEXT7_API_KEY", arg = "--api-key" } ``` For HTTP transports, specify the endpoint and headers in place of the stdio fields. The configuration loader automatically deserializes either transport variant. + The optional `auth` table lets you forward API keys from environment variables to stdio + transports. VT Code reads the `api_key_env` variable at runtime and appends the resulting + value with the specified `arg`. When omitted, the flag defaults to `--api-key`. + ## Allowlist Behaviour MCP access is gated by pattern-based allowlists. The defaults apply to every provider unless the diff --git a/vtcode-core/src/config/constants.rs b/vtcode-core/src/config/constants.rs index fdf51dd09..0491381c6 100644 --- a/vtcode-core/src/config/constants.rs +++ b/vtcode-core/src/config/constants.rs @@ -180,6 +180,21 @@ pub mod models { pub const DEEPSEEK_REASONER: &str = deepseek::DEEPSEEK_REASONER; } +/// MCP-related constants to avoid scattering provider identifiers +pub mod mcp { + pub mod providers { + pub const CONTEXT7: &str = "context7"; + } + + pub mod env { + pub const CONTEXT7_API_KEY: &str = "CONTEXT7_API_KEY"; + } + + pub mod auth { + pub const DEFAULT_API_KEY_FLAG: &str = "--api-key"; + } +} + /// Prompt caching defaults shared across features and providers pub mod prompt_cache { pub const DEFAULT_ENABLED: bool = true; diff --git a/vtcode-core/src/config/mcp.rs b/vtcode-core/src/config/mcp.rs index 91ba84633..90ec85d8d 100644 --- a/vtcode-core/src/config/mcp.rs +++ b/vtcode-core/src/config/mcp.rs @@ -1,3 +1,4 @@ +use crate::config::constants::mcp::auth::DEFAULT_API_KEY_FLAG; use regex::Regex; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; @@ -118,6 +119,10 @@ pub struct McpProviderConfig { #[serde(default)] pub env: HashMap, + /// Authentication configuration for the provider + #[serde(default)] + pub auth: Option, + /// Whether this provider is enabled #[serde(default = "default_provider_enabled")] pub enabled: bool, @@ -133,12 +138,38 @@ impl Default for McpProviderConfig { name: String::new(), transport: McpTransportConfig::Stdio(McpStdioServerConfig::default()), env: HashMap::new(), + auth: None, enabled: default_provider_enabled(), max_concurrent_requests: default_provider_max_concurrent(), } } } +/// Authentication configuration for MCP providers +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct McpProviderAuthConfig { + /// Environment variable containing the API key + #[serde(default)] + pub api_key_env: Option, + + /// CLI flag to use when passing the API key to stdio transports + #[serde(default = "default_auth_arg")] + pub arg: String, +} + +impl Default for McpProviderAuthConfig { + fn default() -> Self { + Self { + api_key_env: None, + arg: default_auth_arg(), + } + } +} + +fn default_auth_arg() -> String { + DEFAULT_API_KEY_FLAG.to_string() +} + /// Allow list configuration for MCP providers #[derive(Debug, Clone, Deserialize, Serialize)] pub struct McpAllowListConfig { diff --git a/vtcode-core/src/config/mod.rs b/vtcode-core/src/config/mod.rs index 38a4a2651..da2ac61f7 100644 --- a/vtcode-core/src/config/mod.rs +++ b/vtcode-core/src/config/mod.rs @@ -197,8 +197,9 @@ pub use core::{ pub use defaults::{ContextStoreDefaults, PerformanceDefaults, ScenarioDefaults}; pub use loader::{ConfigManager, VTCodeConfig}; pub use mcp::{ - McpAllowListConfig, McpAllowListRules, McpClientConfig, McpHttpServerConfig, McpProviderConfig, - McpStdioServerConfig, McpTransportConfig, McpUiConfig, McpUiMode, + McpAllowListConfig, McpAllowListRules, McpClientConfig, McpHttpServerConfig, + McpProviderAuthConfig, McpProviderConfig, McpStdioServerConfig, McpTransportConfig, + McpUiConfig, McpUiMode, }; pub use router::{ComplexityModelMap, ResourceBudget, RouterConfig}; pub use telemetry::TelemetryConfig; diff --git a/vtcode-core/src/core/agent/runner.rs b/vtcode-core/src/core/agent/runner.rs index 217b8d938..9881c3268 100644 --- a/vtcode-core/src/core/agent/runner.rs +++ b/vtcode-core/src/core/agent/runner.rs @@ -344,10 +344,7 @@ impl AgentRunner { parallel_tool_config: Some( crate::llm::provider::ParallelToolConfig::anthropic_optimized(), ), - reasoning_effort: if self - .provider_client - .supports_reasoning_effort(&self.model) - { + reasoning_effort: if self.provider_client.supports_reasoning_effort(&self.model) { self.reasoning_effort } else { None diff --git a/vtcode-core/src/mcp_client.rs b/vtcode-core/src/mcp_client.rs index 2393ea1fa..9865fdc8a 100644 --- a/vtcode-core/src/mcp_client.rs +++ b/vtcode-core/src/mcp_client.rs @@ -1625,6 +1625,27 @@ impl McpProvider { command.envs(&self.config.env); } + if let Some(auth) = &self.config.auth { + if let Some(api_key_env) = &auth.api_key_env { + match std::env::var(api_key_env) { + Ok(api_key) => { + debug!( + "Injecting API key for provider '{}' using flag '{}'", + provider_name, auth.arg + ); + command.arg(&auth.arg); + command.arg(api_key); + } + Err(error) => { + warn!( + "API key environment variable '{}' not available for provider '{}': {}", + api_key_env, provider_name, error + ); + } + } + } + } + // Create new process group to ensure proper cleanup (Unix only) #[cfg(unix)] { @@ -1908,6 +1929,7 @@ mod tests { working_directory: None, }), env: HashMap::new(), + auth: None, enabled: true, max_concurrent_requests: 3, }; diff --git a/vtcode-core/tests/mcp_basic_test.rs b/vtcode-core/tests/mcp_basic_test.rs index 7ec8d8823..af5659bd1 100644 --- a/vtcode-core/tests/mcp_basic_test.rs +++ b/vtcode-core/tests/mcp_basic_test.rs @@ -3,9 +3,11 @@ //! These tests verify that MCP configuration and basic functionality work correctly. use std::collections::HashMap; +use vtcode_core::config::constants::mcp::auth::DEFAULT_API_KEY_FLAG; +use vtcode_core::config::constants::mcp::env::CONTEXT7_API_KEY; use vtcode_core::config::mcp::{ - McpClientConfig, McpProviderConfig, McpStdioServerConfig, McpTransportConfig, McpUiConfig, - McpUiMode, + McpClientConfig, McpProviderAuthConfig, McpProviderConfig, McpStdioServerConfig, + McpTransportConfig, McpUiConfig, McpUiMode, }; use vtcode_core::mcp_client::McpClient; @@ -107,6 +109,7 @@ mod tests { working_directory: None, }), env: env_vars, + auth: None, enabled: true, max_concurrent_requests: 1, }; @@ -119,6 +122,39 @@ mod tests { assert_eq!(provider_config.env.get("DEBUG"), Some(&"true".to_string())); } + #[test] + fn test_provider_auth_config_defaults() { + let auth = McpProviderAuthConfig::default(); + assert!(auth.api_key_env.is_none()); + assert_eq!(auth.arg, DEFAULT_API_KEY_FLAG); + } + + #[test] + fn test_provider_auth_config_from_toml() { + let toml_content = format!( + r#" +[mcp] +enabled = true + +[[mcp.providers]] +name = "context7" +command = "npx" +args = ["-y", "@upstash/context7-mcp@latest"] +auth = {{ api_key_env = "{}" }} + "#, + CONTEXT7_API_KEY + ); + + let config: vtcode_core::config::VTCodeConfig = toml::from_str(&toml_content).unwrap(); + let provider = &config.mcp.providers[0]; + let auth = provider + .auth + .as_ref() + .expect("auth config should be present"); + assert_eq!(auth.api_key_env.as_deref(), Some(CONTEXT7_API_KEY)); + assert_eq!(auth.arg, DEFAULT_API_KEY_FLAG); + } + #[tokio::test] async fn test_mcp_client_initialization() { let config = McpClientConfig { diff --git a/vtcode-core/tests/mcp_context7_manual.rs b/vtcode-core/tests/mcp_context7_manual.rs index a9d2713c0..3b06b0077 100644 --- a/vtcode-core/tests/mcp_context7_manual.rs +++ b/vtcode-core/tests/mcp_context7_manual.rs @@ -16,6 +16,7 @@ async fn context7_list_tools_smoke() { working_directory: None, }), env: HashMap::new(), + auth: None, enabled: true, max_concurrent_requests: 1, }; diff --git a/vtcode-core/tests/mcp_integration_e2e.rs b/vtcode-core/tests/mcp_integration_e2e.rs index 1c4fa8524..1ae4af030 100644 --- a/vtcode-core/tests/mcp_integration_e2e.rs +++ b/vtcode-core/tests/mcp_integration_e2e.rs @@ -42,6 +42,7 @@ mod tests { working_directory: Some(workspace.to_string_lossy().to_string()), }), env: HashMap::new(), + auth: None, enabled: true, max_concurrent_requests: 3, }; @@ -245,6 +246,7 @@ max_concurrent_requests = 1 working_directory: None, }), env: env_vars, + auth: None, enabled: true, max_concurrent_requests: 1, }; diff --git a/vtcode-core/tests/mcp_integration_test.rs b/vtcode-core/tests/mcp_integration_test.rs index 8195cdaa4..7bdf14a58 100644 --- a/vtcode-core/tests/mcp_integration_test.rs +++ b/vtcode-core/tests/mcp_integration_test.rs @@ -100,6 +100,7 @@ max_concurrent_requests = 1 name: "context7".to_string(), transport: McpTransportConfig::Stdio(stdio_config), env: HashMap::new(), + auth: None, enabled: true, max_concurrent_requests: 2, }; @@ -341,6 +342,7 @@ max_concurrent_requests = 1 working_directory: None, }), env: env_vars, + auth: None, enabled: true, max_concurrent_requests: 1, }; diff --git a/vtcode.toml b/vtcode.toml index 1752f2ec4..a716eea2a 100644 --- a/vtcode.toml +++ b/vtcode.toml @@ -139,6 +139,7 @@ name = "context7" enabled = true command = "npx" args = ["-y", "@upstash/context7-mcp@latest"] +auth = { api_key_env = "CONTEXT7_API_KEY", arg = "--api-key" } max_concurrent_requests = 3 [[mcp.providers]]