feat(code-mode): use server names for MCP extensions (#6284)
Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user