refactor: move developer tool implementations from goose-mcp into goose crate

The developer extension is a platform extension that runs in-process,
not an MCP server. Move edit, shell, and tree tool implementations
from goose-mcp/src/developer/ into goose/src/agents/platform_extensions/developer/
where they belong.

This removes the goose -> goose-mcp dependency and consolidates the
developer extension code in a single crate.
This commit is contained in:
Bradley Axen
2026-02-24 10:25:33 -08:00
parent 84650ca3af
commit a7f8ca48ef
12 changed files with 16 additions and 201 deletions
Generated
-1
View File
@@ -4258,7 +4258,6 @@ dependencies = [
"etcetera 0.11.0",
"fs2",
"futures",
"goose-mcp",
"goose-test-support",
"hf-hub",
"ignore",
+1 -4
View File
@@ -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(())
}
-164
View File
@@ -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<Self>,
instructions: String,
shell_tool: Arc<ShellTool>,
edit_tools: Arc<EditTools>,
tree_tool: Arc<TreeTool>,
}
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<ShellParams>,
) -> Result<CallToolResult, ErrorData> {
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<FileWriteParams>,
) -> Result<CallToolResult, ErrorData> {
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<FileEditParams>,
) -> Result<CallToolResult, ErrorData> {
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<TreeParams>) -> Result<CallToolResult, ErrorData> {
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());
}
}
-4
View File
@@ -11,7 +11,6 @@ pub static APP_STRATEGY: Lazy<AppStrategyArgs> = 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<HashMap<&'static str, SpawnServerFn>> = Lazy::new(|| {
HashMap::from([
builtin!(developer, DeveloperServer),
builtin!(autovisualiser, AutoVisualiserRouter),
builtin!(computercontroller, ComputerControllerServer),
builtin!(memory, MemoryServer),
@@ -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",
}
+1 -2
View File
@@ -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 } => {
+1 -2
View File
@@ -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 }
+1 -2
View File
@@ -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()
@@ -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";
@@ -211,16 +211,13 @@ fn render_output(full_output: &str) -> Result<String, String> {
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<String, String> {
))
}
/// 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<PathBuf, String> {
static PATH: Mutex<Option<PathBuf>> = 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<PathBuf, String> {
fn save_full_output(output: &str) -> Result<PathBuf, String> {
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())
@@ -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(&params.path);
let root = if path.is_absolute() {
path