feat(code-mode): use server names for MCP extensions (#6284)

Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
Adrian Cole
2025-12-30 13:08:38 +08:00
committed by GitHub
parent 454e98bb74
commit d1f95757cf
19 changed files with 2142 additions and 1662 deletions
+67 -8
View File
@@ -3,6 +3,7 @@ use axum::http::{HeaderMap, HeaderName};
use chrono::{DateTime, Utc};
use futures::stream::{FuturesUnordered, StreamExt};
use futures::{future, FutureExt};
use rand::{distributions::Alphanumeric, Rng};
use rmcp::service::{ClientInitializeError, ServiceError};
use rmcp::transport::streamable_http_client::{
AuthRequiredError, StreamableHttpClientTransportConfig, StreamableHttpError,
@@ -145,6 +146,31 @@ fn normalize(input: String) -> String {
result.to_lowercase()
}
/// Generates extension name from server info; adds random suffix on collision.
fn generate_extension_name(
server_info: Option<&ServerInfo>,
name_exists: impl Fn(&str) -> bool,
) -> String {
let base = server_info
.and_then(|info| {
let name = info.server_info.name.as_str();
(!name.is_empty()).then(|| normalize(name.to_string()))
})
.unwrap_or_else(|| "unnamed".to_string());
if !name_exists(&base) {
return base;
}
let suffix: String = rand::thread_rng()
.sample_iter(Alphanumeric)
.take(6)
.map(char::from)
.collect();
format!("{base}_{suffix}")
}
fn resolve_command(cmd: &str) -> PathBuf {
SearchPaths::builder()
.with_npm()
@@ -574,14 +600,18 @@ impl ExtensionManager {
};
let server_info = client.get_info().cloned();
self.add_client(
sanitized_name,
config,
Arc::new(Mutex::new(client)),
server_info,
temp_dir,
)
.await;
// Only generate name from server info when config has no name (e.g., CLI --with-*-extension args)
let mut extensions = self.extensions.lock().await;
let final_name = if sanitized_name.is_empty() {
generate_extension_name(server_info.as_ref(), |n| extensions.contains_key(n))
} else {
sanitized_name
};
extensions.insert(
final_name,
Extension::new(config, Arc::new(Mutex::new(client)), server_info, temp_dir),
);
Ok(())
}
@@ -1781,4 +1811,33 @@ mod tests {
);
assert_eq!(result, "Authorization: Bearer secret123 and API key456");
}
mod generate_extension_name_tests {
use super::*;
use rmcp::model::Implementation;
use test_case::test_case;
fn make_info(name: &str) -> ServerInfo {
ServerInfo {
server_info: Implementation {
name: name.into(),
..Default::default()
},
..Default::default()
}
}
#[test_case(Some("kiwi-mcp-server"), None, "^kiwi-mcp-server$" ; "already normalized server name")]
#[test_case(Some("Context7"), None, "^context7$" ; "mixed case normalized")]
#[test_case(Some("@huggingface/mcp-services"), None, "^_huggingface_mcp-services$" ; "special chars normalized")]
#[test_case(None, None, "^unnamed$" ; "no server info falls back")]
#[test_case(Some(""), None, "^unnamed$" ; "empty server name falls back")]
#[test_case(Some("github-mcp-server"), Some("github-mcp-server"), r"^github-mcp-server_[A-Za-z0-9]{6}$" ; "duplicate adds suffix")]
fn test_generate_name(server_name: Option<&str>, collision: Option<&str>, expected: &str) {
let info = server_name.map(make_info);
let result = generate_extension_name(info.as_ref(), |n| collision == Some(n));
let re = regex::Regex::new(expected).unwrap();
assert!(re.is_match(&result));
}
}
}