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.
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:
- Implements the MCP server specification
- Exposes tools/resources/prompts via MCP methods
- Receives requests from the agent
- 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(())
}
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