Skip to main content
Extensions bring external capabilities to Goose through the Model Context Protocol (MCP). They provide tools, resources, and prompts that the AI agent can use to interact with files, APIs, databases, and more.

What are Extensions?

Extensions in Goose are MCP servers that provide:
  • Tools: Functions the agent can call (read files, run commands, query APIs)
  • Resources: Context the agent can access (file contents, documentation)
  • Prompts: Reusable prompt templates
Each extension runs as an independent process, providing isolation and security.

Extension Types

Goose supports three types of extensions:

1. Built-in Extensions

Bundled with Goose, run in-process for performance:
extensions:
  - type: builtin
    name: developer
    description: File operations and shell commands
Available built-in extensions:

developer

File operations, shell commands, and development tools
  • Read/write files
  • Execute shell commands
  • Search codebases

memory

Persistent knowledge storage across sessions
  • Store facts and learnings
  • Retrieve relevant context
  • Build knowledge graphs

computercontroller

Desktop automation (macOS)
  • Control mouse and keyboard
  • Take screenshots
  • UI automation

autovisualiser

Chart and visualization generation
  • Create charts from data
  • Generate plots
  • Data visualization

2. Stdio Extensions

External processes communicating via stdin/stdout:
extensions:
  - type: stdio
    name: brave-search
    cmd: npx
    args: ["-y", "@modelcontextprotocol/server-brave-search"]
    env:
      BRAVE_API_KEY: "${BRAVE_API_KEY}"  # Environment variable expansion
Most third-party MCP servers use stdio transport.

3. SSE Extensions

HTTP-based extensions using Server-Sent Events:
extensions:
  - type: sse
    name: remote-tools
    url: https://mcp.company.internal/tools
    headers:
      Authorization: "Bearer ${API_TOKEN}"
Useful for:
  • Remote MCP servers
  • Cloud-hosted tools
  • Internal enterprise services

Extension Configuration

Basic Configuration

extensions:
  - type: stdio
    name: github
    cmd: uvx
    args: ["mcp-server-github"]
    env:
      GITHUB_TOKEN: "${GITHUB_TOKEN}"
    description: "GitHub API access"

Advanced Options

extensions:
  - type: stdio
    name: database-tools
    cmd: python
    args: ["/path/to/db_server.py"]
    
    # Environment variables
    env:
      DB_HOST: "localhost"
      DB_PORT: "5432"
      DB_PASSWORD: "${DB_PASSWORD}"  # From secrets
    
    # Working directory
    cwd: /opt/database-tools
    
    # Timeout (milliseconds)
    timeout: 30000
    
    # OAuth configuration
    oauth:
      client_id: "${OAUTH_CLIENT_ID}"
      client_secret: "${OAUTH_CLIENT_SECRET}"
      scopes: ["read:data", "write:data"]

Extension Manager

The Extension Manager handles the lifecycle of all extensions:
// From crates/goose/src/agents/extension_manager.rs
pub struct ExtensionManager {
    extensions: Mutex<HashMap<String, Extension>>,
    tools_cache: Mutex<Option<Arc<Vec<Tool>>>>,
    capabilities: ExtensionManagerCapabilities,
}

impl ExtensionManager {
    /// Load and initialize an extension
    pub async fn load_extension(
        &self,
        config: ExtensionConfig,
    ) -> Result<ExtensionInfo> {
        // 1. Validate configuration
        config.validate()?;
        
        // 2. Start MCP server process
        let client = self.create_mcp_client(&config).await?;
        
        // 3. Initialize MCP protocol
        let server_info = client.initialize().await?;
        
        // 4. Discover tools and resources
        let tools = client.list_tools().await?;
        
        // 5. Cache extension
        self.extensions.lock().await
            .insert(config.name.clone(), Extension {
                config,
                client,
                server_info,
            });
        
        Ok(ExtensionInfo { /* ... */ })
    }
    
    /// Call a tool from an extension
    pub async fn call_tool(
        &self,
        tool_name: &str,
        arguments: Value,
    ) -> Result<ToolResult> {
        // Find extension that provides this tool
        let extension = self.find_extension_for_tool(tool_name)?;
        
        // Call via MCP protocol
        let result = extension.client
            .call_tool(CallToolRequestParams {
                name: tool_name.to_string(),
                arguments: Some(arguments),
            })
            .await?;
        
        Ok(result)
    }
}

Creating Custom Extensions

MCP Server Basics

An MCP server exposes tools, resources, and prompts:
# example_server.py
from mcp.server import Server
from mcp.types import Tool, TextContent
import asyncio

# Create server
server = Server("example-tools")

# Define a tool
@server.tool()
async def search_database(query: str, limit: int = 10) -> list[TextContent]:
    """Search the company database.
    
    Args:
        query: Search query string
        limit: Maximum results to return
    """
    # Your implementation
    results = await db.search(query, limit=limit)
    
    return [TextContent(
        type="text",
        text=f"Found {len(results)} results: {results}"
    )]

# Define a resource
@server.resource("database://schema")
async def get_schema() -> str:
    """Return the database schema."""
    return await db.get_schema()

# Run server
if __name__ == "__main__":
    asyncio.run(server.run())

Tool Definition

Tools are defined with JSON Schema parameters:
@server.tool()
async def process_payment(
    amount: float,
    currency: str,
    customer_id: str,
) -> list[TextContent]:
    """Process a payment transaction.
    
    Args:
        amount: Payment amount
        currency: Three-letter currency code (USD, EUR, etc.)
        customer_id: Unique customer identifier
    """
    # Validation
    if amount <= 0:
        raise ValueError("Amount must be positive")
    
    # Process payment
    result = payment_processor.charge(
        amount=amount,
        currency=currency,
        customer=customer_id,
    )
    
    return [TextContent(
        type="text",
        text=f"Payment processed: {result.transaction_id}"
    )]
The MCP server automatically generates JSON Schema from Python type hints:
{
  "name": "process_payment",
  "description": "Process a payment transaction.",
  "inputSchema": {
    "type": "object",
    "properties": {
      "amount": {"type": "number"},
      "currency": {"type": "string"},
      "customer_id": {"type": "string"}
    },
    "required": ["amount", "currency", "customer_id"]
  }
}

Using the Extension

# payment-recipe.yaml
title: Payment Processor
extensions:
  - type: stdio
    name: payment-tools
    cmd: python
    args: ["example_server.py"]
    env:
      PAYMENT_API_KEY: "${PAYMENT_API_KEY}"

instructions: |
  You can process payments using the payment-tools extension.
  Always confirm amounts and customer IDs before processing.

Built-in Extension: Developer

The most commonly used extension provides file and shell tools:
// From crates/goose-mcp/src/computercontroller/developer.rs

// Available tools:
// - read_file: Read file contents
// - write_file: Write/create files  
// - edit_file: Apply diffs to files
// - list_directory: List directory contents
// - search_files: Search file contents
// - execute_command: Run shell commands
// - get_file_info: Get file metadata
Example usage by the agent:
// Agent calls read_file
{
  "name": "read_file",
  "arguments": {
    "path": "src/main.rs"
  }
}

// Returns file contents
{
  "content": [
    {
      "type": "text",
      "text": "fn main() {\n    println!(\"Hello, world!\");\n}"
    }
  ]
}

Extension Security

Environment Variable Protection

Goose blocks dangerous environment variables:
// From crates/goose/src/agents/extension.rs
impl Envs {
    const DISALLOWED_KEYS: [&'static str; 31] = [
        "PATH",              // Prevent binary hijacking
        "LD_LIBRARY_PATH",   // Prevent library injection
        "PYTHONPATH",        // Prevent module hijacking
        "NODE_OPTIONS",      // Prevent Node.js injection
        // ... 27 more dangerous variables
    ];
}
Attempting to set these variables will fail with an error.

Permission System

Tools can require user confirmation:
# ~/.config/goose/config.yaml
permissions:
  execute_command: confirm    # Always ask before running commands
  read_file: allow            # Auto-allow file reads
  delete_file: deny           # Never allow deletions

Malware Scanning

Goose scans downloaded extensions:
// From crates/goose/src/agents/extension_malware_check.rs

pub async fn check_extension_safety(
    extension_path: &Path,
) -> Result<SafetyCheckResult> {
    // Check for:
    // - Known malware signatures
    // - Suspicious patterns
    // - Dangerous system calls
    // - Network activity to unknown hosts
}

Process Isolation

Each extension runs in a separate process:
// stdio extensions run as child processes
let mut child = Command::new(&config.cmd)
    .args(&config.args)
    .env_clear()  // Start with clean environment
    .envs(&safe_env_vars)  // Only add safe variables
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .stderr(Stdio::piped())
    .spawn()?;

Resources (MCP)

Extensions can provide resources that the agent reads:
# Resource example
@server.resource("file:///{path}")
async def read_file_resource(path: str) -> str:
    """Read a file as a resource."""
    with open(path, 'r') as f:
        return f.read()

@server.resource("docs://api-reference")
async def get_api_docs() -> str:
    """Return API documentation."""
    return load_api_documentation()
The agent can access resources:
// Agent requests resource
let resource = extension_manager
    .read_resource("docs://api-reference")
    .await?;

// Include in context
system_prompt += format!("\n\nAPI Documentation:\n{}", resource.contents);

Prompts (MCP)

Extensions can provide reusable prompts:
@server.prompt("code-review")
async def code_review_prompt(file_path: str) -> str:
    """Generate a code review prompt."""
    code = await read_file(file_path)
    return f"""
Review this code for:
1. Security issues
2. Performance problems  
3. Code style

```{file_path}
{code}
"""

The agent or user can invoke prompts:

```rust
let prompt = extension_manager
    .get_prompt("code-review", args! {"file_path": "src/auth.rs"})
    .await?;

Extension Discovery

Goose discovers extensions from:
  1. Built-in: Bundled extensions
  2. Config file: ~/.config/goose/config.yaml
  3. Recipe: Defined in recipe YAML
  4. Runtime: Dynamically added via API
// Get all available tools
let tools = extension_manager.get_tools().await?;

// Tools are cached and versioned for performance
// Cache invalidated when extensions change

Dynamic Extension Management

Extensions can be loaded/unloaded at runtime:
// Load extension
let info = extension_manager.load_extension(ExtensionConfig {
    extension_type: ExtensionType::Stdio,
    name: "new-tool".to_string(),
    cmd: "python".to_string(),
    args: vec!["tool.py".to_string()],
    env: HashMap::new(),
    // ...
}).await?;

// Unload extension  
extension_manager.unload_extension("new-tool").await?;

// List loaded extensions
let extensions = extension_manager.list_extensions().await?;

OAuth Extensions

Extensions can use OAuth for authentication:
extensions:
  - type: stdio
    name: google-drive
    cmd: npx
    args: ["-y", "@modelcontextprotocol/server-gdrive"]
    oauth:
      client_id: "${GOOGLE_CLIENT_ID}"
      client_secret: "${GOOGLE_CLIENT_SECRET}"
      scopes:
        - "https://www.googleapis.com/auth/drive.readonly"
      auth_url: "https://accounts.google.com/o/oauth2/v2/auth"
      token_url: "https://oauth2.googleapis.com/token"
Goose handles the OAuth flow automatically:
  1. Opens browser for user authentication
  2. Exchanges code for tokens
  3. Stores tokens securely
  4. Refreshes tokens when expired

Best Practices

Each tool should do one thing well:
# Good: Focused tools
@server.tool()
async def read_file(path: str): ...

@server.tool()
async def write_file(path: str, content: str): ...

# Bad: Too many responsibilities
@server.tool()
async def file_operations(operation: str, path: str, content: str): ...
The AI uses descriptions to choose tools:
@server.tool()
async def search_code(query: str, language: str = "all") -> list[TextContent]:
    """Search code files for a pattern.
    
    Use this tool to find specific code patterns, function definitions,
    or variable usages across the codebase. Supports regex patterns.
    
    Args:
        query: Regex pattern to search for
        language: Filter by language (python, rust, javascript, or 'all')
    
    Returns:
        List of file paths and matching lines
    """
Return useful error messages:
@server.tool()
async def deploy_service(service_name: str) -> list[TextContent]:
    try:
        result = await deployer.deploy(service_name)
        return [TextContent(type="text", text=f"Deployed {service_name}: {result}")]
    except ServiceNotFound:
        return [TextContent(
            type="text",
            text=f"Error: Service '{service_name}' not found. Available services: {list_services()}"
        )]
    except DeploymentError as e:
        return [TextContent(
            type="text",
            text=f"Deployment failed: {e}. Check logs at /var/log/deploy/{service_name}.log"
        )]
Resources are more efficient than tools for static data:
# Good: Use resource for API docs
@server.resource("docs://api")
async def api_docs() -> str:
    return STATIC_API_DOCUMENTATION

# Bad: Tool for static data
@server.tool()
async def get_api_docs() -> list[TextContent]:
    return [TextContent(type="text", text=STATIC_API_DOCUMENTATION)]
Don’t trust AI-generated arguments:
@server.tool()
async def delete_files(pattern: str) -> list[TextContent]:
    # Validate dangerous operations
    if pattern in ["/", "/*", "*"]:
        raise ValueError("Refusing to delete all files")
    
    # Validate paths are within allowed directories
    if not is_safe_path(pattern):
        raise ValueError(f"Path {pattern} is outside allowed directories")
    
    # Proceed with operation
    deleted = delete_matching_files(pattern)
    return [TextContent(type="text", text=f"Deleted {len(deleted)} files")]

Troubleshooting

Common Issues

“Extension failed to initialize”
# Check extension logs
goose logs --extension developer

# Test extension directly
python your_server.py  # Should not crash
“Tool not found”
# List available tools
goose extensions list-tools

# Reload extension
goose extensions reload developer
“Permission denied”
# Check permission settings
permissions:
  tool_name: allow  # or confirm/deny
“Extension timeout”
# Increase timeout
extensions:
  - type: stdio
    name: slow-tool
    timeout: 60000  # 60 seconds

Next Steps

Recipes

Configure extensions in recipes

MCP Documentation

Learn the MCP specification

Example Extensions

Build your own MCP servers

Built-in Extensions

Reference for bundled extensions

Build docs developers (and LLMs) love