Skip to main content
Extensions add capabilities to Goose through tools, resources, and prompts. They are implemented as MCP (Model Context Protocol) servers.

What are Extensions?

Extensions are MCP servers that provide:
  • Tools - Functions the agent can call (e.g., read files, search web, query databases)
  • Resources - Data sources the agent can access
  • Prompts - Reusable prompt templates
Goose includes several builtin extensions and supports custom external extensions.

Builtin Extensions

Builtin extensions are implemented in crates/goose-mcp/src/:
  • autovisualiser - Automatic visualization generation
  • computercontroller - Desktop automation (keyboard, mouse)
  • memory - Long-term memory storage
  • tutorial - Interactive tutorials
These run as in-process MCP servers.

Extension Architecture

Extensions communicate with Goose using the MCP protocol over stdio. Each extension:
  1. Implements the MCP server specification
  2. Exposes tools/resources/prompts via MCP methods
  3. Receives requests from the agent
  4. Returns results in MCP format
Goose Agent
    ↓ MCP Protocol (stdio)
Extension Manager

Extension (MCP Server)

Your Extension Logic

Creating a Builtin Extension

1. Define Your Extension

Create a new module in crates/goose-mcp/src/:
// crates/goose-mcp/src/myextension/mod.rs
use rmcp::{ServerHandler, model::*};
use async_trait::async_trait;

pub struct MyExtensionServer;

impl MyExtensionServer {
    pub fn new() -> Self {
        Self
    }
}

#[async_trait]
impl ServerHandler for MyExtensionServer {
    async fn list_tools(
        &self,
        _params: ListToolsRequestParams,
    ) -> rmcp::Result<ListToolsResult> {
        Ok(ListToolsResult {
            tools: vec![
                Tool {
                    name: "my_tool".into(),
                    description: Some("Does something useful".to_string()),
                    input_schema: serde_json::json!({
                        "type": "object",
                        "properties": {
                            "input": {
                                "type": "string",
                                "description": "Input text"
                            }
                        },
                        "required": ["input"]
                    }),
                },
            ],
        })
    }
    
    async fn call_tool(
        &self,
        params: CallToolRequestParams,
    ) -> rmcp::Result<CallToolResult> {
        match params.name.as_str() {
            "my_tool" => {
                let input = params.arguments
                    .get("input")
                    .and_then(|v| v.as_str())
                    .unwrap_or("");
                    
                let result = format!("Processed: {}", input);
                
                Ok(CallToolResult {
                    content: vec![ToolResponseContent::text(result)],
                    is_error: Some(false),
                })
            }
            _ => Err(rmcp::Error::method_not_found("Unknown tool")),
        }
    }
}

2. Register the Extension

Add to crates/goose-mcp/src/lib.rs:
pub mod myextension;
pub use myextension::MyExtensionServer;

pub static BUILTIN_EXTENSIONS: Lazy<HashMap<&'static str, SpawnServerFn>> = 
    Lazy::new(|| {
        HashMap::from([
            builtin!(autovisualiser, AutoVisualiserRouter),
            builtin!(computercontroller, ComputerControllerServer),
            builtin!(memory, MemoryServer),
            builtin!(tutorial, TutorialServer),
            builtin!(myextension, MyExtensionServer),  // Add this
        ])
    });

3. Use the Extension

In your goose configuration or code:
use goose::agents::extension::ExtensionConfig;

let config = ExtensionConfig::builtin("myextension");

Creating an External Extension

External extensions are standalone executables that implement the MCP server protocol.

Using Python (FastMCP)

FastMCP makes it easy to build MCP servers in Python:
from fastmcp import FastMCP

mcp = FastMCP("My Extension")

@mcp.tool()
def process_text(text: str) -> str:
    """Process input text.
    
    Args:
        text: The text to process
        
    Returns:
        Processed text
    """
    return f"Processed: {text}"

if __name__ == "__main__":
    mcp.run()
Save as my_extension.py and configure in Goose:
{
  "name": "myextension",
  "module": "my_extension",
  "type": "stdio"
}

Using TypeScript (MCP SDK)

import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

const server = new Server(
  {
    name: "myextension",
    version: "1.0.0",
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: "process_text",
      description: "Process input text",
      inputSchema: {
        type: "object",
        properties: {
          text: {
            type: "string",
            description: "Text to process",
          },
        },
        required: ["text"],
      },
    },
  ],
}));

server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === "process_text") {
    return {
      content: [
        {
          type: "text",
          text: `Processed: ${request.params.arguments.text}`,
        },
      ],
    };
  }
  throw new Error("Unknown tool");
});

const transport = new StdioServerTransport();
await server.connect(transport);

Using Rust (RMCP)

Create a standalone Rust binary:
use rmcp::{ServerHandler, ServiceExt};
use async_trait::async_trait;

struct MyExtension;

#[async_trait]
impl ServerHandler for MyExtension {
    // Implement list_tools, call_tool, etc.
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let server = MyExtension;
    let stdin = tokio::io::stdin();
    let stdout = tokio::io::stdout();
    
    server.serve((stdin, stdout)).await?.waiting().await;
    Ok(())
}

Tool Design Best Practices

1. Clear Descriptions

Provide clear, concise descriptions:
Tool {
    name: "search_files".into(),
    description: Some(
        "Search for files matching a pattern in a directory"
        .to_string()
    ),
    // ...
}

2. Well-Defined Schemas

Use JSON Schema to define parameters:
input_schema: serde_json::json!({
    "type": "object",
    "properties": {
        "pattern": {
            "type": "string",
            "description": "Glob pattern to match"
        },
        "directory": {
            "type": "string",
            "description": "Directory to search in"
        }
    },
    "required": ["pattern", "directory"]
})

3. Error Handling

Return clear error messages:
match perform_operation() {
    Ok(result) => Ok(CallToolResult {
        content: vec![ToolResponseContent::text(result)],
        is_error: Some(false),
    }),
    Err(e) => Ok(CallToolResult {
        content: vec![ToolResponseContent::text(
            format!("Error: {}", e)
        )],
        is_error: Some(true),
    }),
}

4. Atomic Operations

Each tool should do one thing well. Split complex operations into multiple tools.

5. Idempotency

When possible, make tools idempotent - they can be called multiple times with the same result.

Testing Extensions

Using MCP Inspector

Test your extension with the MCP Inspector:
npx @modelcontextprotocol/inspector cargo run -p goose-mcp --example mcp
Or for Python:
npx @modelcontextprotocol/inspector python my_extension.py

Integration Tests

Write integration tests:
#[tokio::test]
async fn test_my_extension() {
    let config = ExtensionConfig::builtin("myextension");
    let manager = ExtensionManager::new(
        vec![config],
        CancellationToken::new(),
    ).await.unwrap();
    
    let tools = manager.list_tools().await.unwrap();
    assert!(!tools.is_empty());
    
    let result = manager.call_tool(
        "my_tool",
        serde_json::json!({"input": "test"}),
    ).await.unwrap();
    
    assert!(!result.is_error.unwrap_or(false));
}

Extension Configuration

Extensions can be configured via:

Environment Variables

let api_key = std::env::var("MY_EXTENSION_API_KEY")?;

Extension Config

pub struct MyExtensionServer {
    config: HashMap<String, String>,
}

impl MyExtensionServer {
    pub fn new_with_config(config: HashMap<String, String>) -> Self {
        Self { config }
    }
}

Resources and Prompts

Implementing Resources

async fn list_resources(
    &self,
    _params: ListResourcesRequestParams,
) -> rmcp::Result<ListResourcesResult> {
    Ok(ListResourcesResult {
        resources: vec![
            Resource {
                uri: "myext://data/users".to_string(),
                name: "User Data".to_string(),
                description: Some("List of users".to_string()),
                mime_type: Some("application/json".to_string()),
            },
        ],
    })
}

async fn read_resource(
    &self,
    params: ReadResourceRequestParams,
) -> rmcp::Result<ReadResourceResult> {
    match params.uri.as_str() {
        "myext://data/users" => {
            let data = get_user_data();
            Ok(ReadResourceResult {
                contents: vec![ResourceContents {
                    uri: params.uri,
                    mime_type: Some("application/json".to_string()),
                    text: Some(serde_json::to_string(&data)?),
                    blob: None,
                }],
            })
        }
        _ => Err(rmcp::Error::invalid_params("Unknown resource")),
    }
}

Implementing Prompts

async fn list_prompts(
    &self,
    _params: ListPromptsRequestParams,
) -> rmcp::Result<ListPromptsResult> {
    Ok(ListPromptsResult {
        prompts: vec![
            Prompt {
                name: "analyze_code".to_string(),
                description: Some("Analyze code quality".to_string()),
                arguments: Some(vec![
                    PromptArgument {
                        name: "language".to_string(),
                        description: Some("Programming language".to_string()),
                        required: Some(true),
                    },
                ]),
            },
        ],
    })
}

Next Steps

Build docs developers (and LLMs) love