Skip to main content

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.

What is a Tool?

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.

Tool Anatomy

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()
)

Input Schema

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"]
}

Implementing Tool Logic

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),
    })
}

Parameter Extraction Helpers

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(&params.arguments, "path")?;
let encoding = get_optional_string_param(&params.arguments, "encoding")
    .unwrap_or_else(|| "utf-8".to_string());
let recursive = get_bool_param(&params.arguments, "recursive", false);

Tool Design Patterns

1. Read-Only Tools

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"]
    }),
}

2. Write/Modify Tools

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"]
    }),
}

3. Query Tools

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"]
    }),
}

4. Action Tools

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

Informative Error Messages

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),
    }),
}

5. Input Validation

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()));

Testing Tools

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