mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
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:
Generated
-1
@@ -4258,7 +4258,6 @@ dependencies = [
|
||||
"etcetera 0.11.0",
|
||||
"fs2",
|
||||
"futures",
|
||||
"goose-mcp",
|
||||
"goose-test-support",
|
||||
"hf-hub",
|
||||
"ignore",
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
}
|
||||
|
||||
@@ -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 } => {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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()
|
||||
|
||||
+7
-3
@@ -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";
|
||||
|
||||
+4
-11
@@ -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())
|
||||
+1
-5
@@ -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
|
||||
Reference in New Issue
Block a user