use std::collections::HashMap;
use std::sync::Mutex;
use crate::session_history::get_claude_ai_oauth_tokens;
use crate::utils::http::get_user_agent;
const FETCH_TIMEOUT_MS: u64 = 5000;
const MCP_SERVERS_BETA_HEADER: &str = "mcp-servers-2025-12-04";
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClaudeAiMcpServer {
#[serde(rename = "type")]
pub server_type: String,
pub id: String,
pub display_name: String,
pub url: String,
pub created_at: String,
}
#[derive(Debug, Clone, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClaudeAiMcpServersResponse {
pub data: Vec<ClaudeAiMcpServer>,
pub has_more: bool,
#[serde(default)]
pub next_page: Option<String>,
}
static CONFIGS_CACHE: Mutex<
Option<HashMap<String, crate::services::mcp::types::ScopedMcpServerConfig>>,
> = Mutex::new(None);
fn is_env_defined_falsy(env_var: Option<String>) -> bool {
match env_var {
None => false,
Some(val) => {
let trimmed = val.trim().to_lowercase();
trimmed.is_empty() || ["0", "false", "no", "off"].contains(&trimmed.as_str())
}
}
}
fn is_enabled_via_env() -> bool {
!is_env_defined_falsy(std::env::var("AI_CODE_ENABLE_CLAUDEAI_MCP_SERVERS").ok())
}
fn has_required_oauth_scope() -> bool {
if let Some(tokens) = get_claude_ai_oauth_tokens() {
tokens.scopes.iter().any(|s| s == "user:mcp_servers")
} else {
false
}
}
fn get_base_api_url() -> String {
std::env::var("AI_CODE_BASE_API_URL")
.unwrap_or_else(|_| "https://api.anthropic.com".to_string())
}
pub async fn fetch_claudeai_mcp_configs_if_eligible()
-> HashMap<String, crate::services::mcp::types::ScopedMcpServerConfig> {
if is_env_defined_falsy(std::env::var("AI_CODE_ENABLE_CLAUDEAI_MCP_SERVERS").ok()) {
log::debug!("[claudeai-mcp] Disabled via env var");
return HashMap::new();
}
if !has_required_oauth_scope() {
log::debug!("[claudeai-mcp] No access token or missing scope");
return HashMap::new();
}
{
let cache = CONFIGS_CACHE.lock().unwrap();
if let Some(ref configs) = *cache {
return configs.clone();
}
}
let base_url = get_base_api_url();
let url = format!("{}/v1/mcp_servers?limit=1000", base_url);
log::debug!("[claudeai-mcp] Fetching from {}", url);
let client = reqwest::Client::builder()
.timeout(std::time::Duration::from_millis(FETCH_TIMEOUT_MS))
.build();
let Ok(client) = client else {
return HashMap::new();
};
let access_token = get_claude_ai_oauth_tokens()
.map(|t| t.access_token)
.unwrap_or_default();
let result = client
.get(&url)
.header("Authorization", format!("Bearer {}", access_token))
.header("Content-Type", "application/json")
.header("anthropic-beta", MCP_SERVERS_BETA_HEADER)
.header("anthropic-version", "2023-06-01")
.header("User-Agent", get_user_agent())
.send()
.await;
match result {
Ok(response) => {
if let Ok(data) = response.json::<ClaudeAiMcpServersResponse>().await {
let configs = convert_to_scoped_configs(&data);
let mut cache = CONFIGS_CACHE.lock().unwrap();
*cache = Some(configs.clone());
return configs;
}
}
Err(e) => {
log::debug!("[claudeai-mcp] Failed to fetch: {}", e);
}
}
HashMap::new()
}
fn convert_to_scoped_configs(
response: &ClaudeAiMcpServersResponse,
) -> HashMap<String, crate::services::mcp::types::ScopedMcpServerConfig> {
use crate::services::mcp::normalize_name_for_mcp;
use crate::services::mcp::types::*;
let mut configs = HashMap::new();
let mut used_normalized_names: HashSet<String> = HashSet::new();
for server in &response.data {
let base_name = format!("claude.ai {}", server.display_name);
let mut final_name = base_name.clone();
let mut final_normalized = normalize_name_for_mcp(&final_name);
let mut count = 1;
while used_normalized_names.contains(&final_normalized) {
count += 1;
final_name = format!("{} ({})", base_name, count);
final_normalized = normalize_name_for_mcp(&final_name);
}
used_normalized_names.insert(final_normalized.clone());
let config = ScopedMcpServerConfig {
config: McpServerConfig::ClaudeAiProxy(McpClaudeAiProxyServerConfig {
config_type: "claudeai-proxy".to_string(),
url: server.url.clone(),
id: server.id.clone(),
}),
scope: ConfigScope::ClaudeAi,
plugin_source: Some(format!("claude.ai:{}", server.id)),
};
configs.insert(final_name, config);
}
configs
}
pub fn clear_configs_cache() {
let mut cache = CONFIGS_CACHE.lock().unwrap();
*cache = None;
}
use std::collections::HashSet;