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
+14 -4
View File
@@ -450,7 +450,17 @@ enum Command {
/// Run goose as an ACP (Agent Client Protocol) agent
#[command(about = "Run goose as an ACP agent server on stdio")]
Acp {},
Acp {
/// Add builtin extensions by name
#[arg(
long = "with-builtin",
value_name = "NAME",
help = "Add builtin extensions by name (e.g., 'developer' or multiple: 'developer,github')",
long_help = "Add one or more builtin extensions that are bundled with goose by specifying their names, comma-separated",
value_delimiter = ','
)]
builtins: Vec<String>,
},
/// Start or resume interactive chat sessions
#[command(
@@ -961,7 +971,7 @@ pub async fn cli() -> anyhow::Result<()> {
Some(Command::Configure {}) => "configure",
Some(Command::Info { .. }) => "info",
Some(Command::Mcp { .. }) => "mcp",
Some(Command::Acp {}) => "acp",
Some(Command::Acp { .. }) => "acp",
Some(Command::Session { .. }) => "session",
Some(Command::Project {}) => "project",
Some(Command::Projects) => "projects",
@@ -995,8 +1005,8 @@ pub async fn cli() -> anyhow::Result<()> {
McpCommand::Developer => serve(DeveloperServer::new()).await?,
}
}
Some(Command::Acp {}) => {
run_acp_agent().await?;
Some(Command::Acp { builtins }) => {
run_acp_agent(builtins).await?;
}
Some(Command::Session {
command,
+44 -6
View File
@@ -1,5 +1,5 @@
use anyhow::Result;
use goose::agents::extension::Envs;
use goose::agents::extension::{Envs, PlatformExtensionContext, PLATFORM_EXTENSIONS};
use goose::agents::{Agent, ExtensionConfig, SessionConfig};
use goose::config::{get_all_extensions, Config};
use goose::conversation::message::{ActionRequiredData, Message, MessageContent};
@@ -246,8 +246,34 @@ fn format_tool_name(tool_name: &str) -> String {
}
}
async fn add_builtins(agent: &Agent, builtins: Vec<String>) {
for builtin in builtins {
let config = if PLATFORM_EXTENSIONS.contains_key(builtin.as_str()) {
ExtensionConfig::Platform {
name: builtin.clone(),
bundled: None,
description: builtin.clone(),
available_tools: Vec::new(),
}
} else {
ExtensionConfig::Builtin {
name: builtin.clone(),
display_name: None,
timeout: None,
bundled: None,
description: builtin.clone(),
available_tools: Vec::new(),
}
};
match agent.add_extension(config).await {
Ok(_) => info!(extension = %builtin, "builtin extension loaded"),
Err(e) => warn!(extension = %builtin, error = %e, "builtin extension load failed"),
}
}
}
impl GooseAcpAgent {
async fn new() -> Result<Self> {
async fn new(builtins: Vec<String>) -> Result<Self> {
let config = Config::global();
let provider_name: String = config
@@ -286,6 +312,16 @@ impl GooseAcpAgent {
.collect();
let agent_ptr = Arc::new(agent);
// ACP loads the same default extensions as CLI
agent_ptr
.extension_manager
.set_context(PlatformExtensionContext {
session_id: Some(session.id.clone()),
extension_manager: Some(Arc::downgrade(&agent_ptr.extension_manager)),
})
.await;
let mut set = JoinSet::new();
let mut waiting_on = HashSet::new();
@@ -316,6 +352,8 @@ impl GooseAcpAgent {
}
}
add_builtins(&agent_ptr, builtins).await;
Ok(Self {
sessions: Arc::new(Mutex::new(HashMap::new())),
agent: agent_ptr,
@@ -584,7 +622,7 @@ impl GooseAcpAgent {
};
cx.send_request(permission_request)
.await_when_result_received(move |result| async move {
.on_receiving_result(move |result| async move {
match result {
Ok(response) => {
agent
@@ -1029,7 +1067,7 @@ struct GooseAcpHandler {
}
impl JrMessageHandler for GooseAcpHandler {
type Role = AgentToClient;
type Link = AgentToClient;
fn describe_chain(&self) -> impl std::fmt::Debug {
"goose-acp"
@@ -1097,13 +1135,13 @@ impl JrMessageHandler for GooseAcpHandler {
}
}
pub async fn run_acp_agent() -> Result<()> {
pub async fn run_acp_agent(builtins: Vec<String>) -> Result<()> {
info!("listening on stdio");
let outgoing = tokio::io::stdout().compat_write();
let incoming = tokio::io::stdin().compat();
let agent = Arc::new(GooseAcpAgent::new().await?);
let agent = Arc::new(GooseAcpAgent::new(builtins).await?);
let handler = GooseAcpHandler { agent };
AgentToClient::builder()
+3 -35
View File
@@ -41,7 +41,6 @@ use rmcp::model::{ErrorCode, ErrorData};
use goose::config::paths::Paths;
use goose::conversation::message::{ActionRequiredData, Message, MessageContent};
use rand::{distributions::Alphanumeric, Rng};
use rustyline::EditMode;
use serde::{Deserialize, Serialize};
use serde_json::Value;
@@ -171,32 +170,6 @@ pub async fn classify_planner_response(
}
}
fn generate_extension_name(extension_command: &str) -> String {
let cmd_name: String = extension_command
.split([' ', '/'])
.next_back()
.unwrap_or("")
.chars()
.filter(|c| c.is_alphanumeric())
.collect();
let prefix: String = cmd_name.chars().take(16).collect();
let random_suffix: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from)
.collect();
let name = format!("{}_{}", prefix, random_suffix);
if name.chars().next().is_none_or(|c| !c.is_alphabetic()) {
format!("g{}", name)
} else {
name
}
}
impl CliSession {
#[allow(clippy::too_many_arguments)]
pub async fn new(
@@ -256,10 +229,9 @@ impl CliSession {
}
let cmd = parts.remove(0).to_string();
let name = generate_extension_name(&extension_command);
let config = ExtensionConfig::Stdio {
name,
name: String::new(),
cmd,
args: parts.iter().map(|s| s.to_string()).collect(),
envs: Envs::new(envs),
@@ -287,10 +259,8 @@ impl CliSession {
/// # Arguments
/// * `extension_url` - URL of the server
pub async fn add_remote_extension(&mut self, extension_url: String) -> Result<()> {
let name = generate_extension_name(&extension_url);
let config = ExtensionConfig::Sse {
name,
name: String::new(),
uri: extension_url,
envs: Envs::new(HashMap::new()),
env_keys: Vec::new(),
@@ -317,10 +287,8 @@ impl CliSession {
/// # Arguments
/// * `extension_url` - URL of the server
pub async fn add_streamable_http_extension(&mut self, extension_url: String) -> Result<()> {
let name = generate_extension_name(&extension_url);
let config = ExtensionConfig::StreamableHttp {
name,
name: String::new(),
uri: extension_url,
envs: Envs::new(HashMap::new()),
env_keys: Vec::new(),