Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ resolver = "2"

[workspace.package]
edition = "2021"
version = "1.14.0"
version = "1.15.0"
authors = ["Block <[email protected]>"]
license = "Apache-2.0"
repository = "https://github.com/block/goose"
Expand Down
28 changes: 19 additions & 9 deletions crates/goose-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ use anyhow::Result;
use clap::{Args, Parser, Subcommand};

use goose::config::{Config, ExtensionConfig};
use goose_mcp::mcp_server_runner::{serve, McpCommand};
use goose_mcp::{
AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer,
};

use crate::commands::acp::run_acp_agent;
use crate::commands::bench::agent_generator;
Expand Down Expand Up @@ -420,7 +424,10 @@ enum Command {

/// Manage system prompts and behaviors
#[command(about = "Run one of the mcp servers bundled with goose")]
Mcp { name: String },
Mcp {
#[arg(value_parser = clap::value_parser!(McpCommand))]
server: McpCommand,
},

/// Run goose as an ACP (Agent Client Protocol) agent
#[command(about = "Run goose as an ACP agent server on stdio")]
Expand Down Expand Up @@ -877,15 +884,18 @@ pub async fn cli() -> anyhow::Result<()> {
);

match cli.command {
Some(Command::Configure {}) => {
handle_configure().await?;
}
Some(Command::Info { verbose }) => {
handle_info(verbose)?;
}
Some(Command::Mcp { name }) => {
Some(Command::Configure {}) => handle_configure().await?,
Some(Command::Info { verbose }) => handle_info(verbose)?,
Some(Command::Mcp { server }) => {
let name = server.name();
crate::logging::setup_logging(Some(&format!("mcp-{name}")), None)?;
goose_mcp::mcp_server_runner::run_mcp_server(&name).await?;
match server {
McpCommand::AutoVisualiser => serve(AutoVisualiserRouter::new()).await?,
McpCommand::ComputerController => serve(ComputerControllerServer::new()).await?,
McpCommand::Memory => serve(MemoryServer::new()).await?,
McpCommand::Tutorial => serve(TutorialServer::new()).await?,
McpCommand::Developer => serve(DeveloperServer::new()).await?,
}
}
Some(Command::Acp {}) => {
run_acp_agent().await?;
Expand Down
3 changes: 1 addition & 2 deletions crates/goose-mcp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ description.workspace = true
workspace = true

[dependencies]
goose = { path = "../goose" }
rmcp = { version = "0.8.1", features = ["server", "client", "transport-io", "macros"] }
anyhow = "1.0.94"
tokio = { version = "1", features = ["full"] }
Expand Down Expand Up @@ -77,13 +76,13 @@ libc = "0.2"
# ~1000 downloads). Pinned to exact version to prevent supply chain attacks.
mpatch = "=0.2.0"
tokio-util = "0.7.16"
clap = { version = "4", features = ["derive"] }


[dev-dependencies]
serial_test = "3.0.0"
sysinfo = "0.32.1"
temp-env = "0.3.6"
clap = { version = "4", features = ["derive"] }
colored = "2"

[features]
Expand Down
10 changes: 9 additions & 1 deletion crates/goose-mcp/src/developer/analyze/formatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@ use crate::developer::analyze::types::{
AnalysisMode, AnalysisResult, CallChain, EntryType, FocusedAnalysisData,
};
use crate::developer::lang;
use goose::utils::safe_truncate;
use rmcp::model::{Content, Role};
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};

fn safe_truncate(s: &str, max_chars: usize) -> String {
if s.chars().count() <= max_chars {
s.to_string()
} else {
let truncated: String = s.chars().take(max_chars.saturating_sub(3)).collect();
format!("{}...", truncated)
}
}

pub struct Formatter;

impl Formatter {
Expand Down
1 change: 1 addition & 0 deletions crates/goose-mcp/src/developer/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
pub mod analyze;
mod editor_models;
mod lang;
pub mod paths;
mod shell;
mod text_editor;

Expand Down
112 changes: 112 additions & 0 deletions crates/goose-mcp/src/developer/paths.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
use anyhow::Result;
use std::env;
use std::path::PathBuf;
use tokio::process::Command;
use tokio::sync::OnceCell;

static SHELL_PATH_DIRS: OnceCell<Result<Vec<PathBuf>, anyhow::Error>> = OnceCell::const_new();

pub async fn get_shell_path_dirs() -> Result<&'static Vec<PathBuf>> {
let result = SHELL_PATH_DIRS
.get_or_init(|| async {
get_shell_path_async()
.await
.map(|path| env::split_paths(&path).collect())
})
.await;

match result {
Ok(dirs) => Ok(dirs),
Err(e) => Err(anyhow::anyhow!(
"Failed to get shell PATH directories: {}",
e
)),
}
}

async fn get_shell_path_async() -> Result<String> {
let shell = env::var("SHELL").unwrap_or_else(|_| {
if cfg!(windows) {
"cmd".to_string()
} else {
"/bin/bash".to_string()
}
});

if cfg!(windows) {
get_windows_path_async(&shell).await
} else {
get_unix_path_async(&shell).await
}
.or_else(|e| {
tracing::warn!(
"Failed to get PATH from shell ({}), falling back to current PATH",
e
);
env::var("PATH").map_err(|_| anyhow::anyhow!("No PATH variable available"))
})
}

async fn get_unix_path_async(shell: &str) -> Result<String> {
let output = Command::new(shell)
.args(["-l", "-i", "-c", "echo $PATH"])
.output()
.await
.map_err(|e| anyhow::anyhow!("Failed to execute shell command: {}", e))?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow::anyhow!("Shell command failed: {}", stderr));
}

let path = String::from_utf8(output.stdout)
.map_err(|e| anyhow::anyhow!("Invalid UTF-8 in shell output: {}", e))?
.trim()
.to_string();

if path.is_empty() {
return Err(anyhow::anyhow!("Shell returned empty PATH"));
}

Ok(path)
}

async fn get_windows_path_async(shell: &str) -> Result<String> {
let shell_name = std::path::Path::new(shell)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("cmd");

let output = match shell_name {
"pwsh" | "powershell" => {
Command::new(shell)
.args(["-NoLogo", "-Command", "$env:PATH"])
.output()
.await
}
_ => {
Command::new(shell)
.args(["/c", "echo %PATH%"])
.output()
.await
}
};

let output = output.map_err(|e| anyhow::anyhow!("Failed to execute shell command: {}", e))?;

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow::anyhow!("Shell command failed: {}", stderr));
}

let path = String::from_utf8(output.stdout)
.map_err(|e| anyhow::anyhow!("Invalid UTF-8 in shell output: {}", e))?
.trim()
.to_string();

if path.is_empty() {
return Err(anyhow::anyhow!("Shell returned empty PATH"));
}

Ok(path)
}
53 changes: 47 additions & 6 deletions crates/goose-mcp/src/developer/rmcp_developer.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use anyhow::anyhow;
use base64::Engine;
use ignore::gitignore::{Gitignore, GitignoreBuilder};
use include_dir::{include_dir, Dir};
Expand All @@ -17,6 +18,8 @@ use rmcp::{
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
env::join_paths,
ffi::OsString,
future::Future,
io::Cursor,
path::{Path, PathBuf},
Expand All @@ -31,11 +34,11 @@ use tokio::{
use tokio_stream::{wrappers::SplitStream, StreamExt as _};
use tokio_util::sync::CancellationToken;

use crate::developer::{paths::get_shell_path_dirs, shell::ShellConfig};

use super::analyze::{types::AnalyzeParams, CodeAnalyzer};
use super::editor_models::{create_editor_model, EditorModel};
use super::shell::{
configure_shell_command, expand_path, get_shell_config, is_absolute_path, kill_process_group,
};
use super::shell::{configure_shell_command, expand_path, is_absolute_path, kill_process_group};
use super::text_editor::{
text_editor_insert, text_editor_replace, text_editor_undo, text_editor_view, text_editor_write,
};
Expand Down Expand Up @@ -179,6 +182,8 @@ pub struct DeveloperServer {
pub running_processes: Arc<RwLock<HashMap<String, CancellationToken>>>,
#[cfg(not(test))]
running_processes: Arc<RwLock<HashMap<String, CancellationToken>>>,
bash_env_file: Option<PathBuf>,
extend_path_with_shell: bool,
}

#[tool_handler(router = self.tool_router)]
Expand Down Expand Up @@ -549,9 +554,21 @@ impl DeveloperServer {
prompts: load_prompt_files(),
code_analyzer: CodeAnalyzer::new(),
running_processes: Arc::new(RwLock::new(HashMap::new())),
extend_path_with_shell: false,
bash_env_file: None,
}
}

pub fn extend_path_with_shell(mut self, value: bool) -> Self {
self.extend_path_with_shell = value;
self
}

pub fn bash_env_file(mut self, value: Option<PathBuf>) -> Self {
self.bash_env_file = value;
self
}

/// List all available windows that can be used with screen_capture.
/// Returns a list of window titles that can be used with the window_title parameter
/// of the screen_capture tool.
Expand Down Expand Up @@ -942,10 +959,34 @@ impl DeveloperServer {
peer: &rmcp::service::Peer<RoleServer>,
cancellation_token: CancellationToken,
) -> Result<String, ErrorData> {
// Get platform-specific shell configuration
let shell_config = get_shell_config();
let mut shell_config = ShellConfig::default();
let shell_name = std::path::Path::new(&shell_config.executable)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("bash");

if let Some(ref env_file) = self.bash_env_file {
if shell_name == "bash" {
shell_config.envs.push((
OsString::from("BASH_ENV"),
env_file.clone().into_os_string(),
))
}
}

let mut command = configure_shell_command(&shell_config, command);

if self.extend_path_with_shell {
if let Err(e) = get_shell_path_dirs()
.await
.and_then(|dirs| join_paths(dirs).map_err(|e| anyhow!(e)))
.map(|path| command.env("PATH", path))
{
tracing::error!("Failed to extend PATH with shell directories: {}", e)
}
}

let mut child = configure_shell_command(&shell_config, command)
let mut child = command
.spawn()
.map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?;

Expand Down
Loading
Loading