# Goose Tool Registry and Filtering Mechanism: Dynamic Extension Management

> Explore the Goose tool registry: discover how it dynamically aggregates tools from MCP extensions and offers filtering APIs for precise extension management. Learn about its design.

- Repository: [Block Open Source/goose](https://github.com/block/goose)
- Tags: deep-dive
- Published: 2026-04-05

---

**Goose implements a dynamic tool registry that aggregates tools from MCP extensions, applies extension-specific prefixes for global uniqueness, and provides filtering APIs to include or exclude specific extension tools.**

The `block/goose` repository defines a flexible architecture for managing AI tool capabilities. Rather than maintaining a static, monolithic tool list, Goose dynamically constructs its **tool registry** by aggregating capabilities from every loaded Model Context Protocol (MCP) extension, applying intelligent prefixing and ownership metadata to prevent naming collisions.

## Core Architecture of the Tool Registry

### Extension Manager and Discovery

The registry centers on `ExtensionManager` in [`crates/goose/src/agents/extension_manager.rs`](https://github.com/block/goose/blob/main/crates/goose/src/agents/extension_manager.rs). This component discovers, creates, caches, and invalidates the global tool list coming from all extensions. When an extension registers, Goose stores a `McpClientBox` capable of reporting its available tools via the MCP protocol.

### Dynamic Tool Aggregation

`ExtensionManager::fetch_all_tools` iterates over every loaded extension, calls its MCP `list_tools` RPC (handling pagination automatically), and rewrites each tool name to ensure global uniqueness:

```rust
let public_name = if expose_unprefixed {
    tool.name.to_string()
} else {
    format!("{}__{}", name, tool.name)   // <ext>__<tool>
};
tool.name = public_name.into();

```

Each tool is enriched with a meta field (`goose_extension` via `TOOL_EXTENSION_META_KEY`) that records its owning extension. The complete vector is stored in `tools_cache` alongside a version counter, allowing subsequent calls to hit the cache unless the registry has been invalidated.

## Tool Name Prefixing and Ownership

### The Prefixing Strategy

Unless an extension explicitly opts out via `expose_unprefixed`, Goose prefixes every tool with its extension name and a double underscore separator (`<ext>__`). This guarantees that tools with identical names from different extensions remain distinguishable in the global registry.

### Ownership Detection

When filtering tools, `ExtensionManager::filter_tools` canonicalizes names using the `name_to_key` helper (defined in the utils module) to handle case insensitivity and hyphen/underscore normalization. Ownership is determined by reading the `goose_extension` meta field, or inferred from the prefix if metadata is absent:

```rust
let tool_owner = get_tool_owner(tool)
    .map(|s| name_to_key(&s))
    .unwrap_or_else(|| tool.name.split("__").next().unwrap_or("").to_string());

```

## Filtering Mechanisms

### Extension-Based Filtering

The registry exposes three access patterns via `Agent::list_tools` in [`crates/goose/src/agents/agent.rs`](https://github.com/block/goose/blob/main/crates/goose/src/agents/agent.rs):

- **All tools**: `list_tools(session_id, None)` returns the complete prefixed set via `ExtensionManager::get_prefixed_tools`
- **Specific extension**: `list_tools(session_id, Some("extension_name"))` filters to tools owned by that extension
- **Excluding extensions**: `ExtensionManager::get_prefixed_tools_excluding(session_id, "extension_name")` returns all tools except those from the specified extension

The filtering logic applies inclusion or exclusion rules after normalizing both the tool owner and filter arguments:

```rust
if let Some(ref excluded) = exclude_normalized {
    if tool_owner == *excluded { return false; }
}
if let Some(ref name_filter) = extension_name_normalized {
    tool_owner == *name_filter
} else {
    true
}

```

### Hidden Extensions

Extensions whose names start with an underscore (`_`) are considered **hidden** and never exposed to end-users. The `is_hidden_extension` helper (lines 225-229 in [`extension_manager.rs`](https://github.com/block/goose/blob/main/extension_manager.rs)) filters these during tool enumeration.

## Caching and Invalidation

The registry maintains a **versioned cache** to optimize performance. When extensions are added or removed, `ExtensionManager::invalidate_tools_cache_and_bump_version` clears the cache and increments the version counter. This forces the next `fetch_all_tools` call to rebuild the registry from scratch, ensuring the tool list reflects the current extension state.

## Accessing the Registry Programmatically

List every tool available to a session:

```rust
let agent = GooseAgent::new(...).await?;
let tools = agent.list_tools("session-123", None).await;
for t in tools {
    println!("{} – {}", t.name, t.description);
}

```

Retrieve only tools from a specific extension:

```rust
let github_tools = agent.list_tools("session-123", Some("github".into())).await;

```

Exclude a specific extension when building prompts:

```rust
let all_tools = agent.extension_manager
    .get_prefixed_tools_excluding("session-123", "developer")
    .await?;
let tool_infos: Vec<ToolInfo> = all_tools.iter()
    .map(|t| ToolInfo::new(&t.name, &t.description, get_parameter_names(t), None))
    .collect();

```

Force a registry refresh after loading new extensions:

```rust
agent.extension_manager
    .invalidate_tools_cache_and_bump_version().await;
let refreshed = agent.list_tools("session-123", None).await;

```

## Summary

- Goose builds a **dynamic tool registry** by aggregating MCP extension capabilities at runtime rather than using static definitions
- **Extension-specific prefixes** (`<ext>__<tool>`) ensure global uniqueness and prevent naming collisions
- **Metadata tagging** (`goose_extension`) tracks tool ownership for reliable filtering and attribution
- **Normalized name matching** via `name_to_key` handles case insensitivity and punctuation variations consistently
- **Versioned caching** optimizes performance while allowing dynamic extension updates without restarts
- **Hidden extension support** (names starting with `_`) isolates internal tools from user-facing interfaces

## Frequently Asked Questions

### How does Goose prevent tool name collisions between extensions?

Goose automatically prefixes tool names with their extension name and a double underscore separator (`<ext>__<tool>`) unless the extension explicitly opts out via `expose_unprefixed`. This ensures every tool has a globally unique identifier within the registry, preventing collisions when multiple extensions define tools with the same base name.

### How does the filtering mechanism determine which extension owns a tool?

The system first checks the `goose_extension` meta field stored in the tool's metadata. If this field is absent, ownership is inferred by parsing the tool name itself—splitting on `__` and extracting the prefix. Both the extracted owner and user-provided filter arguments are normalized using `name_to_key` to handle case insensitivity and punctuation variations like hyphens versus underscores.

### Can I dynamically refresh the tool registry without restarting Goose?

Yes. Call `ExtensionManager::invalidate_tools_cache_and_bump_version()` to clear the internal cache and increment the version counter. The next call to `list_tools` or `fetch_all_tools` will automatically rebuild the registry, picking up any newly loaded extensions or removing tools from unloaded ones.

### What happens to extensions prefixed with an underscore?

Extensions whose names start with `_` are treated as **hidden extensions**. The `is_hidden_extension` helper function filters these out during tool enumeration, ensuring that internal, experimental, or system extensions never appear in user-facing tool lists or CLI completions.