diff --git a/Cargo.lock b/Cargo.lock index c5409c4e14..a8d1de1820 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4258,7 +4258,6 @@ dependencies = [ "etcetera 0.11.0", "fs2", "futures", - "goose-mcp", "goose-test-support", "hf-hub", "ignore", diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index 080e8975e6..c42d81e6e6 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -6,9 +6,7 @@ use goose::config::Config; use goose::posthog::get_telemetry_choice; use goose::recipe::Recipe; use goose_mcp::mcp_server_runner::{serve, McpCommand}; -use goose_mcp::{ - AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer, -}; +use goose_mcp::{AutoVisualiserRouter, ComputerControllerServer, MemoryServer, TutorialServer}; use crate::commands::configure::{configure_telemetry_consent_dialog, handle_configure}; use crate::commands::info::handle_info; @@ -1018,7 +1016,6 @@ async fn handle_mcp_command(server: McpCommand) -> Result<()> { McpCommand::ComputerController => serve(ComputerControllerServer::new()).await?, McpCommand::Memory => serve(MemoryServer::new()).await?, McpCommand::Tutorial => serve(TutorialServer::new()).await?, - McpCommand::Developer => serve(DeveloperServer::new()).await?, } Ok(()) } diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs deleted file mode 100644 index d7bf0d1091..0000000000 --- a/crates/goose-mcp/src/developer/mod.rs +++ /dev/null @@ -1,164 +0,0 @@ -pub mod edit; -pub mod shell; -pub mod tree; - -use std::sync::Arc; - -use indoc::formatdoc; -use rmcp::{ - handler::server::{router::tool::ToolRouter, wrapper::Parameters}, - model::{CallToolResult, ErrorData, Implementation, ServerCapabilities, ServerInfo}, - tool, tool_handler, tool_router, ServerHandler, -}; - -use edit::{EditTools, FileEditParams, FileWriteParams}; -use shell::{ShellParams, ShellTool}; -use tree::{TreeParams, TreeTool}; - -pub struct DeveloperServer { - tool_router: ToolRouter, - instructions: String, - shell_tool: Arc, - edit_tools: Arc, - tree_tool: Arc, -} - -impl Clone for DeveloperServer { - fn clone(&self) -> Self { - Self { - tool_router: Self::tool_router(), - instructions: self.instructions.clone(), - shell_tool: Arc::clone(&self.shell_tool), - edit_tools: Arc::clone(&self.edit_tools), - tree_tool: Arc::clone(&self.tree_tool), - } - } -} - -impl Default for DeveloperServer { - fn default() -> Self { - Self::new() - } -} - -#[tool_router(router = tool_router)] -impl DeveloperServer { - pub fn new() -> Self { - let shell_tool = Arc::new(ShellTool::new()); - let edit_tools = Arc::new(EditTools::new()); - let tree_tool = Arc::new(TreeTool::new()); - - let cwd = std::env::current_dir() - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_else(|_| ".".to_string()); - let os = std::env::consts::OS; - - let instructions = formatdoc! {r#" - The developer extension provides tools for software development tasks. - - Capabilities: - - **Shell**: Execute shell commands with `shell` - - **Edit**: Create and modify files with `file_write` and `file_edit` - - **Explore**: Navigate codebases with `tree` - - `shell` output is limited to 2000 lines. When exceeded, the full output is saved - to a temp file and the response includes only the last 50 lines plus the file path. - You can read the temp file with shell commands like `head`, `tail`, or `sed -n '100,200p'` - up to 2000 lines at a time. For long-running commands, pass `timeout_secs`. - - operating system: {os} - current directory: {cwd} - "#, - os = os, - cwd = cwd, - }; - - Self { - tool_router: Self::tool_router(), - instructions, - shell_tool, - edit_tools, - tree_tool, - } - } - - #[tool( - name = "shell", - description = "Execute a shell command in the user's default shell in the current dir and return both stdout/stderr. The output is limited to up to 2000 lines, and longer outputs will be saved to a temporary file." - )] - pub async fn shell( - &self, - params: Parameters, - ) -> Result { - Ok(self.shell_tool.shell(params.0).await) - } - - #[tool( - name = "file_write", - description = "Create a new file or overwrite an existing file. Creates parent directories if needed." - )] - pub async fn file_write( - &self, - params: Parameters, - ) -> Result { - Ok(self.edit_tools.file_write(params.0)) - } - - #[tool( - name = "file_edit", - description = "Edit a file by finding and replacing text. The before text must match exactly and uniquely. Use empty after text to delete." - )] - pub async fn file_edit( - &self, - params: Parameters, - ) -> Result { - Ok(self.edit_tools.file_edit(params.0)) - } - - #[tool( - name = "tree", - description = "List a directory tree with line counts. Traversal respects .gitignore rules." - )] - pub async fn tree(&self, params: Parameters) -> Result { - Ok(self.tree_tool.tree(params.0)) - } -} - -#[tool_handler(router = self.tool_router)] -impl ServerHandler for DeveloperServer { - fn get_info(&self) -> ServerInfo { - ServerInfo { - server_info: Implementation { - name: "goose-developer".to_string(), - version: env!("CARGO_PKG_VERSION").to_owned(), - title: None, - description: None, - icons: None, - website_url: None, - }, - capabilities: ServerCapabilities::builder().enable_tools().build(), - instructions: Some(self.instructions.clone()), - ..Default::default() - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_server_creation() { - let server = DeveloperServer::new(); - assert!(!server.instructions.is_empty()); - } - - #[test] - fn test_get_info() { - let server = DeveloperServer::new(); - let info = server.get_info(); - - assert_eq!(info.server_info.name, "goose-developer"); - assert!(info.instructions.is_some()); - } -} diff --git a/crates/goose-mcp/src/lib.rs b/crates/goose-mcp/src/lib.rs index 63d50e65ab..24d9d37ca7 100644 --- a/crates/goose-mcp/src/lib.rs +++ b/crates/goose-mcp/src/lib.rs @@ -11,7 +11,6 @@ pub static APP_STRATEGY: Lazy = Lazy::new(|| AppStrategyArgs { pub mod autovisualiser; pub mod computercontroller; -pub mod developer; pub mod mcp_server_runner; mod memory; #[cfg(target_os = "macos")] @@ -21,8 +20,6 @@ pub mod tutorial; pub use autovisualiser::AutoVisualiserRouter; pub use computercontroller::ComputerControllerServer; -pub use developer::DeveloperServer; -pub const WORKING_DIR_PLACEHOLDER: &str = "{{WORKING_DIR}}"; pub use memory::MemoryServer; pub use tutorial::TutorialServer; @@ -57,7 +54,6 @@ macro_rules! builtin { pub static BUILTIN_EXTENSIONS: Lazy> = Lazy::new(|| { HashMap::from([ - builtin!(developer, DeveloperServer), builtin!(autovisualiser, AutoVisualiserRouter), builtin!(computercontroller, ComputerControllerServer), builtin!(memory, MemoryServer), diff --git a/crates/goose-mcp/src/mcp_server_runner.rs b/crates/goose-mcp/src/mcp_server_runner.rs index 9b978bc9a4..de62bb56da 100644 --- a/crates/goose-mcp/src/mcp_server_runner.rs +++ b/crates/goose-mcp/src/mcp_server_runner.rs @@ -7,7 +7,6 @@ use rmcp::{transport::stdio, ServiceExt}; pub enum McpCommand { AutoVisualiser, ComputerController, - Developer, Memory, Tutorial, } @@ -19,7 +18,6 @@ impl FromStr for McpCommand { match s.to_lowercase().replace(' ', "").as_str() { "autovisualiser" => Ok(McpCommand::AutoVisualiser), "computercontroller" => Ok(McpCommand::ComputerController), - "develop" | "developer" => Ok(McpCommand::Developer), "memory" => Ok(McpCommand::Memory), "tutorial" => Ok(McpCommand::Tutorial), _ => Err(format!("Invalid command: {}", s)), @@ -32,7 +30,6 @@ impl McpCommand { match self { McpCommand::AutoVisualiser => "autovisualiser", McpCommand::ComputerController => "computercontroller", - McpCommand::Developer => "developer", McpCommand::Memory => "memory", McpCommand::Tutorial => "tutorial", } diff --git a/crates/goose-server/src/main.rs b/crates/goose-server/src/main.rs index 4c48f2790d..25bf34246d 100644 --- a/crates/goose-server/src/main.rs +++ b/crates/goose-server/src/main.rs @@ -13,7 +13,7 @@ use clap::{Parser, Subcommand}; use goose::agents::validate_extensions; use goose_mcp::{ mcp_server_runner::{serve, McpCommand}, - AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer, + AutoVisualiserRouter, ComputerControllerServer, MemoryServer, TutorialServer, }; #[derive(Parser)] @@ -56,7 +56,6 @@ async fn main() -> anyhow::Result<()> { McpCommand::ComputerController => serve(ComputerControllerServer::new()).await?, McpCommand::Memory => serve(MemoryServer::new()).await?, McpCommand::Tutorial => serve(TutorialServer::new()).await?, - McpCommand::Developer => serve(DeveloperServer::new()).await?, } } Commands::ValidateExtensions { path } => { diff --git a/crates/goose/Cargo.toml b/crates/goose/Cargo.toml index a0de55ef8f..b51bd059f1 100644 --- a/crates/goose/Cargo.toml +++ b/crates/goose/Cargo.toml @@ -94,13 +94,12 @@ jsonwebtoken = { version = "10.3.0", features = ["aws_lc_rs"] } blake3 = "1.5" fs2 = { workspace = true } -tokio-stream = { workspace = true } +tokio-stream = { workspace = true, features = ["io-util"] } tempfile = { workspace = true } dashmap = "6.1" ahash = "0.8" tokio-util = { workspace = true, features = ["compat"] } unicode-normalization = "0.1" -goose-mcp = { path = "../goose-mcp" } # For local Whisper transcription candle-core = { version = "0.9", default-features = false } diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 61bfa88318..db66e4e9b3 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -805,8 +805,7 @@ impl ExtensionManager { .iter() .map(|(name, ext)| { let instructions = ext.get_instructions().unwrap_or_default(); - let instructions = - instructions.replace(goose_mcp::WORKING_DIR_PLACEHOLDER, &working_dir_str); + let instructions = instructions.replace("{{WORKING_DIR}}", &working_dir_str); ExtensionInfo::new(name, &instructions, ext.supports_resources()) }) .collect() diff --git a/crates/goose-mcp/src/developer/edit.rs b/crates/goose/src/agents/platform_extensions/developer/edit.rs similarity index 100% rename from crates/goose-mcp/src/developer/edit.rs rename to crates/goose/src/agents/platform_extensions/developer/edit.rs diff --git a/crates/goose/src/agents/platform_extensions/developer.rs b/crates/goose/src/agents/platform_extensions/developer/mod.rs similarity index 98% rename from crates/goose/src/agents/platform_extensions/developer.rs rename to crates/goose/src/agents/platform_extensions/developer/mod.rs index aa93f10926..deb97b04d3 100644 --- a/crates/goose/src/agents/platform_extensions/developer.rs +++ b/crates/goose/src/agents/platform_extensions/developer/mod.rs @@ -1,10 +1,12 @@ +pub mod edit; +pub mod shell; +pub mod tree; + use crate::agents::extension::PlatformExtensionContext; use crate::agents::mcp_client::{Error, McpClientTrait}; use anyhow::Result; use async_trait::async_trait; -use goose_mcp::developer::edit::{EditTools, FileEditParams, FileWriteParams}; -use goose_mcp::developer::shell::{ShellParams, ShellTool}; -use goose_mcp::developer::tree::{TreeParams, TreeTool}; +use edit::{EditTools, FileEditParams, FileWriteParams}; use indoc::indoc; use rmcp::model::{ CallToolResult, Content, Implementation, InitializeResult, JsonObject, ListToolsResult, @@ -12,9 +14,11 @@ use rmcp::model::{ }; use schemars::{schema_for, JsonSchema}; use serde_json::Value; +use shell::{ShellParams, ShellTool}; use std::path::Path; use std::sync::Arc; use tokio_util::sync::CancellationToken; +use tree::{TreeParams, TreeTool}; pub static EXTENSION_NAME: &str = "developer"; diff --git a/crates/goose-mcp/src/developer/shell.rs b/crates/goose/src/agents/platform_extensions/developer/shell.rs similarity index 94% rename from crates/goose-mcp/src/developer/shell.rs rename to crates/goose/src/agents/platform_extensions/developer/shell.rs index 3481e78802..fb6a1e3bd0 100644 --- a/crates/goose-mcp/src/developer/shell.rs +++ b/crates/goose/src/agents/platform_extensions/developer/shell.rs @@ -211,16 +211,13 @@ fn render_output(full_output: &str) -> Result { return Ok(full_output.to_string()); } - // Output exceeds limit — save full output to temp file and show tail preview let output_path = save_full_output(full_output)?; let preview_start = total_lines.saturating_sub(OUTPUT_PREVIEW_LINES); let preview = lines[preview_start..].join("\n"); let reason = if exceeded_lines { - format!( - "Output exceeded {OUTPUT_LIMIT_LINES} line limit ({total_lines} lines total)." - ) + format!("Output exceeded {OUTPUT_LIMIT_LINES} line limit ({total_lines} lines total).") } else { format!( "Output exceeded {} byte limit ({total_bytes} bytes total).", @@ -236,16 +233,14 @@ fn render_output(full_output: &str) -> Result { )) } -/// Returns the path to a single reusable temp file for shell output. -/// The file is created once and rewritten on each call with large output. fn output_buffer_path() -> Result { static PATH: Mutex> = Mutex::new(None); let mut guard = PATH.lock().map_err(|e| format!("Lock poisoned: {e}"))?; if let Some(path) = guard.as_ref() { return Ok(path.clone()); } - let temp_file = tempfile::NamedTempFile::new() - .map_err(|e| format!("Failed to create temp file: {e}"))?; + let temp_file = + tempfile::NamedTempFile::new().map_err(|e| format!("Failed to create temp file: {e}"))?; let (_, path) = temp_file .keep() .map_err(|e| format!("Failed to persist temp file: {}", e.error))?; @@ -255,8 +250,7 @@ fn output_buffer_path() -> Result { fn save_full_output(output: &str) -> Result { let path = output_buffer_path()?; - std::fs::write(&path, output) - .map_err(|e| format!("Failed to write output buffer: {e}"))?; + std::fs::write(&path, output).map_err(|e| format!("Failed to write output buffer: {e}"))?; Ok(path) } @@ -361,7 +355,6 @@ mod tests { #[test] fn render_output_truncates_when_bytes_exceeded() { - // Create output that is under line limit but over byte limit let long_line = "x".repeat(1000); let input = (0..100) .map(|_| long_line.clone()) diff --git a/crates/goose-mcp/src/developer/tree.rs b/crates/goose/src/agents/platform_extensions/developer/tree.rs similarity index 98% rename from crates/goose-mcp/src/developer/tree.rs rename to crates/goose/src/agents/platform_extensions/developer/tree.rs index 7172e0b24b..e6c68ff0b5 100644 --- a/crates/goose-mcp/src/developer/tree.rs +++ b/crates/goose/src/agents/platform_extensions/developer/tree.rs @@ -30,11 +30,7 @@ impl TreeTool { self.tree_at(root, params.depth) } - pub fn tree_with_cwd( - &self, - params: TreeParams, - working_dir: Option<&Path>, - ) -> CallToolResult { + pub fn tree_with_cwd(&self, params: TreeParams, working_dir: Option<&Path>) -> CallToolResult { let path = PathBuf::from(¶ms.path); let root = if path.is_absolute() { path