Documentation Index
Fetch the complete documentation index at: https://mintlify.com/block/goose/llms.txt
Use this file to discover all available pages before exploring further.
Tools are the primary way extensions provide functionality to Goose. This guide covers best practices for designing and implementing effective tools.
A tool is a function that:
- Has a well-defined interface (name, parameters, schema)
- Performs a specific operation
- Returns a result to the agent
- Can be called by the LLM during agent execution
Tools are exposed via MCP and called by the agent when needed.
Every tool consists of:
Tool {
name: "tool_name".into(),
description: Some("What this tool does".to_string()),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"param_name": {
"type": "string",
"description": "Parameter description"
}
},
"required": ["param_name"]
}),
}
Name
- Use
snake_case
- Be descriptive but concise
- Prefix with domain if needed (e.g.,
file_read, db_query)
Good:
read_file
search_code
execute_command
Bad:
rf (too short)
readTheContentsOfAFile (too verbose)
do_stuff (not descriptive)
Description
Provide a clear description that explains:
- What the tool does
- When to use it
- Important limitations or requirements
description: Some(
"Read the contents of a file from the filesystem. \
Returns the file contents as text. \
Fails if the file doesn't exist or is not readable by the current user."
.to_string()
)
Define parameters using JSON Schema:
{
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the file to read"
},
"encoding": {
"type": "string",
"description": "File encoding (default: utf-8)",
"enum": ["utf-8", "ascii", "latin1"]
}
},
"required": ["path"]
}
Basic Implementation
async fn call_tool(
&self,
params: CallToolRequestParams,
) -> rmcp::Result<CallToolResult> {
match params.name.as_str() {
"read_file" => self.read_file_tool(params).await,
"write_file" => self.write_file_tool(params).await,
_ => Err(rmcp::Error::method_not_found(
format!("Unknown tool: {}", params.name)
)),
}
}
async fn read_file_tool(
&self,
params: CallToolRequestParams,
) -> rmcp::Result<CallToolResult> {
// 1. Extract and validate parameters
let path = params.arguments
.get("path")
.and_then(|v| v.as_str())
.ok_or_else(|| rmcp::Error::invalid_params(
"Missing required parameter: path"
))?;
// 2. Perform operation
let contents = match std::fs::read_to_string(path) {
Ok(contents) => contents,
Err(e) => {
return Ok(CallToolResult {
content: vec![ToolResponseContent::text(
format!("Failed to read file: {}", e)
)],
is_error: Some(true),
});
}
};
// 3. Return result
Ok(CallToolResult {
content: vec![ToolResponseContent::text(contents)],
is_error: Some(false),
})
}
Create helper functions for common parameter types:
fn get_string_param(
params: &serde_json::Map<String, serde_json::Value>,
name: &str,
) -> rmcp::Result<String> {
params
.get(name)
.and_then(|v| v.as_str())
.map(|s| s.to_string())
.ok_or_else(|| rmcp::Error::invalid_params(
format!("Missing or invalid parameter: {}", name)
))
}
fn get_optional_string_param(
params: &serde_json::Map<String, serde_json::Value>,
name: &str,
) -> Option<String> {
params.get(name).and_then(|v| v.as_str()).map(|s| s.to_string())
}
fn get_bool_param(
params: &serde_json::Map<String, serde_json::Value>,
name: &str,
default: bool,
) -> bool {
params.get(name).and_then(|v| v.as_bool()).unwrap_or(default)
}
Usage:
let path = get_string_param(¶ms.arguments, "path")?;
let encoding = get_optional_string_param(¶ms.arguments, "encoding")
.unwrap_or_else(|| "utf-8".to_string());
let recursive = get_bool_param(¶ms.arguments, "recursive", false);
Tools that only read data, never modify:
Tool {
name: "list_files".into(),
description: Some("List files in a directory".to_string()),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"path": {"type": "string"},
"pattern": {
"type": "string",
"description": "Glob pattern to filter files"
}
},
"required": ["path"]
}),
}
Tools that modify state:
Tool {
name: "write_file".into(),
description: Some(
"Write content to a file. Creates parent directories if needed. \
WARNING: Overwrites existing files.".to_string()
),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"path": {"type": "string"},
"content": {"type": "string"},
"append": {
"type": "boolean",
"description": "Append instead of overwrite",
"default": false
}
},
"required": ["path", "content"]
}),
}
Tools that search or filter:
Tool {
name: "search_code".into(),
description: Some(
"Search for code patterns using regex. \
Returns file paths and line numbers.".to_string()
),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"pattern": {
"type": "string",
"description": "Regex pattern to search for"
},
"directory": {
"type": "string",
"description": "Directory to search in"
},
"file_pattern": {
"type": "string",
"description": "File glob pattern (e.g., '*.rs')"
}
},
"required": ["pattern", "directory"]
}),
}
Tools that perform actions:
Tool {
name: "execute_command".into(),
description: Some(
"Execute a shell command. \
Returns stdout, stderr, and exit code. \
WARNING: Commands run with current user permissions.".to_string()
),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"command": {"type": "string"},
"working_dir": {
"type": "string",
"description": "Working directory (default: current)"
},
"timeout_seconds": {
"type": "integer",
"description": "Max execution time",
"default": 30
}
},
"required": ["command"]
}),
}
Error Handling
Provide context in error messages:
match std::fs::read_to_string(&path) {
Ok(contents) => Ok(CallToolResult {
content: vec![ToolResponseContent::text(contents)],
is_error: Some(false),
}),
Err(e) => Ok(CallToolResult {
content: vec![ToolResponseContent::text(
format!("Failed to read file '{}': {}", path, e)
)],
is_error: Some(true),
}),
}
Validation Errors
if !path.exists() {
return Ok(CallToolResult {
content: vec![ToolResponseContent::text(
format!("File not found: {}", path.display())
)],
is_error: Some(true),
});
}
if !path.is_file() {
return Ok(CallToolResult {
content: vec![ToolResponseContent::text(
format!("Path is not a file: {}", path.display())
)],
is_error: Some(true),
});
}
Permission Errors
match std::fs::metadata(&path) {
Ok(metadata) => {
if metadata.permissions().readonly() {
return Ok(CallToolResult {
content: vec![ToolResponseContent::text(
"Cannot write to read-only file".to_string()
)],
is_error: Some(true),
});
}
}
Err(e) => {
return Ok(CallToolResult {
content: vec![ToolResponseContent::text(
format!("Permission denied: {}", e)
)],
is_error: Some(true),
});
}
}
Return Types
Text Results
Most common return type:
Ok(CallToolResult {
content: vec![ToolResponseContent::text(
"Operation completed successfully".to_string()
)],
is_error: Some(false),
})
Structured Data
Return JSON for structured data:
let result = serde_json::json!({
"files": [
{"name": "file1.txt", "size": 1024},
{"name": "file2.txt", "size": 2048}
],
"total": 2
});
Ok(CallToolResult {
content: vec![ToolResponseContent::text(
serde_json::to_string_pretty(&result)?
)],
is_error: Some(false),
})
Multiple Content Items
Return multiple pieces of content:
Ok(CallToolResult {
content: vec![
ToolResponseContent::text("Summary: 5 files found".to_string()),
ToolResponseContent::text(
serde_json::to_string_pretty(&files)?
),
],
is_error: Some(false),
})
Best Practices
1. Single Responsibility
Each tool should do one thing well:
Good:
read_file - reads a file
write_file - writes a file
list_files - lists files
Bad:
file_operations - does everything
2. Idempotency
When possible, make tools idempotent:
// Good: Creating directory is idempotent
if !path.exists() {
std::fs::create_dir_all(&path)?;
}
// Returns success whether dir was created or already existed
3. Safe Defaults
Choose safe defaults for optional parameters:
input_schema: serde_json::json!({
"properties": {
"overwrite": {
"type": "boolean",
"description": "Overwrite existing file",
"default": false // Safe default
},
"recursive": {
"type": "boolean",
"description": "Delete recursively",
"default": false // Safe default
}
}
})
4. Timeouts
Set reasonable timeouts for long operations:
use tokio::time::timeout;
use std::time::Duration;
let result = timeout(
Duration::from_secs(30),
perform_operation()
).await;
match result {
Ok(Ok(value)) => Ok(CallToolResult {
content: vec![ToolResponseContent::text(value)],
is_error: Some(false),
}),
Ok(Err(e)) => Ok(CallToolResult {
content: vec![ToolResponseContent::text(
format!("Operation failed: {}", e)
)],
is_error: Some(true),
}),
Err(_) => Ok(CallToolResult {
content: vec![ToolResponseContent::text(
"Operation timed out after 30 seconds".to_string()
)],
is_error: Some(true),
}),
}
Validate inputs thoroughly:
// Path validation
let path = PathBuf::from(path_str);
if path.is_absolute() {
return Err(rmcp::Error::invalid_params(
"Absolute paths not allowed"
));
}
// Pattern validation
let regex = regex::Regex::new(pattern)
.map_err(|e| rmcp::Error::invalid_params(
format!("Invalid regex pattern: {}", e)
))?;
// Range validation
if timeout_seconds > 300 {
return Err(rmcp::Error::invalid_params(
"Timeout cannot exceed 300 seconds"
));
}
6. Progress for Long Operations
For operations that may take time, provide feedback:
let mut result = String::new();
result.push_str("Processing files...\n");
for (i, file) in files.iter().enumerate() {
process_file(file)?;
if i % 10 == 0 {
result.push_str(&format!("Processed {}/{}\n", i, files.len()));
}
}
result.push_str(&format!("Complete: {} files processed\n", files.len()));
Unit Tests
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_read_file_tool() {
let server = MyExtension::new();
let params = CallToolRequestParams {
name: "read_file".into(),
arguments: serde_json::json!({
"path": "test.txt"
}).as_object().unwrap().clone(),
};
let result = server.call_tool(params).await.unwrap();
assert!(!result.is_error.unwrap_or(false));
}
#[tokio::test]
async fn test_read_file_not_found() {
let server = MyExtension::new();
let params = CallToolRequestParams {
name: "read_file".into(),
arguments: serde_json::json!({
"path": "nonexistent.txt"
}).as_object().unwrap().clone(),
};
let result = server.call_tool(params).await.unwrap();
assert!(result.is_error.unwrap_or(false));
}
}
Integration Tests
Test tools through the extension manager:
#[tokio::test]
async fn test_tool_integration() {
let config = ExtensionConfig::builtin("myextension");
let manager = ExtensionManager::new(
vec![config],
CancellationToken::new(),
).await.unwrap();
let result = manager.call_tool(
"read_file",
serde_json::json!({"path": "test.txt"}),
).await.unwrap();
assert!(!result.is_error.unwrap_or(false));
}
Examples from Goose
See real tool implementations in:
crates/goose-mcp/src/memory/ - Memory storage tools
crates/goose-mcp/src/computercontroller/ - Desktop automation
crates/goose-mcp/src/autovisualiser/ - Visualization generation
Next Steps