Skip to main content
Extensions are how you add custom functionality to goose. By implementing a Model Context Protocol (MCP) server, you can provide tools, resources, and prompts that agents can discover and use.

Extension Types

Goose supports several types of extensions:

Tools

Functions the agent can call (e.g., API integrations, file operations)

Resources

Data sources the agent can read (e.g., databases, documentation)

Prompts

Pre-defined prompt templates for common tasks

OAuth Apps

Extensions with authentication flows for third-party services

Quick Start: Python MCP Server

The fastest way to create an extension is using FastMCP, a Python framework for MCP servers.

Installation

pip install fastmcp

Basic Server

Create my_tools.py:
from fastmcp import FastMCP

# Initialize MCP server
mcp = FastMCP("My Custom Tools")

@mcp.tool()
def calculate_fibonacci(n: int) -> int:
    """Calculate the nth Fibonacci number"""
    if n <= 1:
        return n
    a, b = 0, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b

@mcp.tool()
def reverse_string(text: str) -> str:
    """Reverse a string"""
    return text[::-1]

if __name__ == "__main__":
    mcp.run()

Register with Goose

Add to ~/.config/goose/config.yaml:
extensions:
  - name: "my-tools"
    enabled: true
    transport:
      type: "stdio"
      command: "python"
      args: ["-m", "my_tools"]

Test the Extension

goose session
In the session:
you: Calculate the 10th Fibonacci number
goose: [Calling calculate_fibonacci(10)]
goose: The 10th Fibonacci number is 55.

Tool Design Best Practices

1. Clear Descriptions

Provide detailed descriptions so the agent understands when to use your tool:
@mcp.tool()
def search_database(
    query: str,
    filters: dict = None,
    limit: int = 10
) -> list:
    """
    Search the product database using natural language.
    
    Use this tool when the user asks about products, inventory,
    or wants to find items matching certain criteria.
    
    Args:
        query: Natural language search query
        filters: Optional filters (e.g., {"category": "electronics", "price_max": 100})
        limit: Maximum number of results to return (default 10)
    
    Returns:
        List of matching products with name, price, and availability
    """
    # Implementation
Good descriptions improve agent accuracy. Include:
  • What the tool does
  • When to use it
  • What parameters mean
  • What the return value represents

2. Type Safety

Use type hints for automatic schema generation:
from typing import Literal, Optional
from pydantic import BaseModel

class SearchResult(BaseModel):
    name: str
    price: float
    in_stock: bool

@mcp.tool()
def search_products(
    category: Literal["electronics", "clothing", "books"],
    max_price: Optional[float] = None
) -> list[SearchResult]:
    """Search products by category and price"""
    # Implementation

3. Error Handling

Provide clear error messages:
@mcp.tool()
def fetch_weather(city: str) -> dict:
    """Get current weather for a city"""
    try:
        response = requests.get(f"https://api.weather.com/v1/weather?city={city}")
        response.raise_for_status()
        return response.json()
    except requests.HTTPError as e:
        if e.response.status_code == 404:
            return {"error": f"City '{city}' not found. Check the spelling or try a nearby city."}
        elif e.response.status_code == 429:
            return {"error": "API rate limit exceeded. Please try again in a few minutes."}
        else:
            return {"error": f"Weather API error: {str(e)}"}
    except Exception as e:
        return {"error": f"Unexpected error fetching weather: {str(e)}"}

4. Idempotency

Make tools safe to retry:
@mcp.tool()
def create_user(email: str, name: str) -> dict:
    """Create a new user account (idempotent)"""
    # Check if user exists first
    existing = db.query(User).filter_by(email=email).first()
    if existing:
        return {"id": existing.id, "message": "User already exists"}
    
    # Create new user
    user = User(email=email, name=name)
    db.add(user)
    db.commit()
    return {"id": user.id, "message": "User created"}

Resources

Resources provide read access to data:
@mcp.resource("docs://api-reference")
def get_api_docs() -> str:
    """API documentation for internal services"""
    with open("docs/api.md", "r") as f:
        return f.read()

@mcp.resource("database://users/{user_id}")
def get_user_profile(user_id: str) -> dict:
    """Get user profile data"""
    user = db.query(User).filter_by(id=user_id).first()
    if not user:
        raise ValueError(f"User {user_id} not found")
    return {
        "id": user.id,
        "name": user.name,
        "email": user.email,
        "created_at": user.created_at.isoformat()
    }
Resources use URI patterns for identification. The agent can discover and read them as needed.

Prompts

Prompts are reusable templates:
@mcp.prompt()
def code_review_prompt(language: str, code: str) -> str:
    """Generate a code review prompt"""
    return f"""
Review this {language} code for:
- Code quality and style
- Potential bugs
- Performance issues
- Security vulnerabilities

Code:
```{language}
{code}
Provide specific, actionable feedback. """

Agents can use prompts to structure their requests to the LLM.

## TypeScript MCP Server

For Node.js/TypeScript:

```typescript
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';

const server = new Server(
  {
    name: 'my-tools',
    version: '1.0.0',
  },
  {
    capabilities: {
      tools: {},
    },
  }
);

// List available tools
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: 'calculate_sum',
      description: 'Add two numbers together',
      inputSchema: {
        type: 'object',
        properties: {
          a: { type: 'number', description: 'First number' },
          b: { type: 'number', description: 'Second number' },
        },
        required: ['a', 'b'],
      },
    },
  ],
}));

// Handle tool calls
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  if (request.params.name === 'calculate_sum') {
    const { a, b } = request.params.arguments as { a: number; b: number };
    return {
      content: [
        {
          type: 'text',
          text: String(a + b),
        },
      ],
    };
  }
  throw new Error(`Unknown tool: ${request.params.name}`);
});

// Start server
const transport = new StdioServerTransport();
await server.connect(transport);
Save as my-tools.ts and register:
extensions:
  - name: "my-tools"
    transport:
      type: "stdio"
      command: "npx"
      args: ["tsx", "my-tools.ts"]

Rust MCP Server

For performance-critical extensions in Rust:
use goose_mcp::{mcp_server_runner::serve, Memory};
use rmcp::{
    model::{Content, TextContent, ToolCallContent, ToolInfo},
    ServerHandler,
};
use anyhow::Result;

struct MyTools;

#[async_trait]
impl ServerHandler for MyTools {
    async fn list_tools(&self) -> Result<Vec<ToolInfo>> {
        Ok(vec![
            ToolInfo {
                name: "reverse_string".to_string(),
                description: Some("Reverse a string".to_string()),
                input_schema: serde_json::json!({
                    "type": "object",
                    "properties": {
                        "text": {
                            "type": "string",
                            "description": "String to reverse"
                        }
                    },
                    "required": ["text"]
                }),
            },
        ])
    }

    async fn call_tool(&self, tool_call: ToolCallContent) -> Result<Vec<Content>> {
        match tool_call.name.as_str() {
            "reverse_string" => {
                let text = tool_call.arguments["text"]
                    .as_str()
                    .ok_or_else(|| anyhow::anyhow!("Missing 'text' parameter"))?;
                let reversed = text.chars().rev().collect::<String>();
                Ok(vec![Content::text(reversed)])
            }
            _ => Err(anyhow::anyhow!("Unknown tool: {}", tool_call.name)),
        }
    }
}

#[tokio::main]
async fn main() -> Result<()> {
    serve(MyTools).await
}
See the Tool Creation guide for more Rust examples.

Testing Your Extension

Manual Testing

Test your MCP server standalone:
python -m my_tools
Then send JSON-RPC messages via stdin:
{"jsonrpc":"2.0","id":1,"method":"tools/list"}

MCP Inspector

Use the MCP Inspector for interactive testing:
npm install -g @modelcontextprotocol/inspector
mcp-inspector python -m my_tools
This opens a web UI where you can:
  • View available tools
  • Test tool calls
  • Inspect request/response formats

Integration Testing

Test with goose:
goose session
# Ask the agent to use your tool
Enable debug logging to see tool calls:
goose session --log-level debug

Distribution

PyPI Package

Publish your Python extension:
# pyproject.toml
[project]
name = "my-mcp-tools"
version = "1.0.0"
dependencies = ["fastmcp"]

[project.scripts]
my-mcp-tools = "my_tools:main"
Users can install:
pip install my-mcp-tools
And configure:
extensions:
  - name: "my-tools"
    transport:
      type: "stdio"
      command: "my-mcp-tools"

npm Package

For TypeScript extensions:
{
  "name": "my-mcp-tools",
  "version": "1.0.0",
  "bin": {
    "my-mcp-tools": "./dist/index.js"
  },
  "dependencies": {
    "@modelcontextprotocol/sdk": "^0.5.0"
  }
}
Users install:
npm install -g my-mcp-tools

Next Steps

Extension Development

In-depth extension development guide

MCP Protocol

Learn the Model Context Protocol specification

Tool Creation

Best practices for designing tools

Built-in Extensions

Study built-in extension implementations

Build docs developers (and LLMs) love