# Source: https://gofastmcp.com/servers/visibility.md

> ## Documentation Index
> Fetch the complete documentation index at: https://gofastmcp.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Component Visibility

> Control which components are available to clients

export const VersionBadge = ({version}) => {
  return <Badge stroke size="lg" icon="gift" iconType="regular" className="version-badge">
            New in version <code>{version}</code>
        </Badge>;
};

<VersionBadge version="3.0.0" />

Components can be dynamically enabled or disabled at runtime. A disabled tool disappears from listings and cannot be called. This enables runtime access control, feature flags, and context-aware component exposure.

## Component Visibility

Every FastMCP server provides `enable()` and `disable()` methods for controlling component availability.

### Disabling Components

The `disable()` method marks components as disabled. Disabled components are filtered out from all client queries.

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
from fastmcp import FastMCP

mcp = FastMCP("Server")

@mcp.tool(tags={"admin"})
def delete_everything() -> str:
    """Delete all data."""
    return "Deleted"

@mcp.tool(tags={"admin"})
def reset_system() -> str:
    """Reset the system."""
    return "Reset"

@mcp.tool
def get_status() -> str:
    """Get system status."""
    return "OK"

# Disable admin tools
mcp.disable(tags={"admin"})

# Clients only see: get_status
```

### Enabling Components

The `enable()` method re-enables previously disabled components.

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
# Re-enable admin tools
mcp.enable(tags={"admin"})

# Clients now see all three tools
```

## Keys and Tags

Visibility filtering works with two identifiers: keys (for specific components) and tags (for groups).

### Component Keys

Every component has a unique key in the format `{type}:{identifier}`.

| Component | Key Format       | Example                  |
| --------- | ---------------- | ------------------------ |
| Tool      | `tool:{name}`    | `tool:delete_everything` |
| Resource  | `resource:{uri}` | `resource:data://config` |
| Template  | `template:{uri}` | `template:file://{path}` |
| Prompt    | `prompt:{name}`  | `prompt:analyze`         |

Use keys to target specific components.

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
# Disable a specific tool
mcp.disable(keys={"tool:delete_everything"})

# Disable multiple specific components
mcp.disable(keys={"tool:reset_system", "resource:data://secrets"})
```

### Tags

Tags group components for bulk operations. Define tags when creating components, then filter by them.

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
from fastmcp import FastMCP

mcp = FastMCP("Server")

@mcp.tool(tags={"public", "read"})
def get_data() -> str:
    return "data"

@mcp.tool(tags={"admin", "write"})
def set_data(value: str) -> str:
    return f"Set: {value}"

@mcp.tool(tags={"admin", "dangerous"})
def delete_data() -> str:
    return "Deleted"

# Disable all admin tools
mcp.disable(tags={"admin"})

# Disable all dangerous tools (some overlap with admin)
mcp.disable(tags={"dangerous"})
```

A component is disabled if it has **any** of the disabled tags. The component doesn't need all the tags; one match is enough.

### Combining Keys and Tags

You can specify both keys and tags in a single call. The filters combine additively.

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
# Disable specific tools AND all dangerous-tagged components
mcp.disable(keys={"tool:debug_info"}, tags={"dangerous"})
```

## Allowlist Mode

By default, visibility filtering uses blocklist mode: everything is enabled unless explicitly disabled. The `only=True` parameter switches to allowlist mode, where **only** specified components are enabled.

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
from fastmcp import FastMCP

mcp = FastMCP("Server")

@mcp.tool(tags={"safe"})
def read_only_operation() -> str:
    return "Read"

@mcp.tool(tags={"safe"})
def list_items() -> list[str]:
    return ["a", "b", "c"]

@mcp.tool(tags={"dangerous"})
def delete_all() -> str:
    return "Deleted"

@mcp.tool
def untagged_tool() -> str:
    return "Untagged"

# Only enable safe tools - everything else is disabled
mcp.enable(tags={"safe"}, only=True)

# Clients see: read_only_operation, list_items
# Disabled: delete_all, untagged_tool
```

Allowlist mode is useful for restrictive environments where you want to explicitly opt-in components rather than opt-out.

### Allowlist Behavior

When you call `enable(only=True)`:

1. Default visibility state switches to "disabled"
2. Previous allowlists are cleared
3. Only specified keys/tags become enabled

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
# Start fresh - only enable these specific tools
mcp.enable(keys={"tool:safe_read", "tool:safe_write"}, only=True)

# Later, switch to a different allowlist
mcp.enable(tags={"production"}, only=True)
```

### Ordering and Overrides

Later `enable()` and `disable()` calls override earlier ones. This lets you create broad rules with specific exceptions.

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
mcp.enable(tags={"api"}, only=True)  # Allow all api-tagged
mcp.disable(keys={"tool:api_admin"})  # Later disable overrides for this tool

# api_admin is disabled because the later disable() overrides the allowlist
```

You can always re-enable something that was disabled by adding another `enable()` call after it.

## Server vs Provider

Visibility state operates at two levels: the server and individual providers.

### Server-Level

Server-level visibility state applies to all components from all providers. When you call `mcp.enable()` or `mcp.disable()`, you're filtering the final view that clients see.

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
from fastmcp import FastMCP

main = FastMCP("Main")
main.mount(sub_server, namespace="api")

@main.tool(tags={"internal"})
def local_debug() -> str:
    return "Debug"

# Disable internal tools from ALL sources
main.disable(tags={"internal"})
```

### Provider-Level

Each provider can add its own visibility transforms. These run before server-level transforms, so the server can override provider-level disables.

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
from fastmcp import FastMCP
from fastmcp.server.providers import LocalProvider

# Create provider with visibility control
admin_tools = LocalProvider()

@admin_tools.tool(tags={"admin"})
def admin_action() -> str:
    return "Admin"

@admin_tools.tool
def regular_action() -> str:
    return "Regular"

# Disable at provider level
admin_tools.disable(tags={"admin"})

# Server can override if needed
mcp = FastMCP("Server", providers=[admin_tools])
mcp.enable(names={"admin_action"})  # Re-enables despite provider disable
```

Provider-level transforms are useful for setting default visibility that servers can selectively override.

### Layered Transforms

Provider transforms run first, then server transforms. Later transforms override earlier ones, so the server has final say.

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
from fastmcp import FastMCP
from fastmcp.server.providers import LocalProvider

provider = LocalProvider()

@provider.tool(tags={"feature", "beta"})
def new_feature() -> str:
    return "New"

# Provider enables feature-tagged
provider.enable(tags={"feature"}, only=True)

# Server disables beta-tagged (runs after provider)
mcp = FastMCP("Server", providers=[provider])
mcp.disable(tags={"beta"})

# new_feature is disabled (server's later disable overrides provider's enable)
```

## Per-Session Visibility

Server-level visibility changes affect all connected clients simultaneously. When you need different clients to see different components, use per-session visibility instead.

Session visibility lets individual sessions customize their view of available components. When a tool calls `ctx.enable_components()` or `ctx.disable_components()`, those rules apply only to the current session. Other sessions continue to see the global defaults. This enables patterns like progressive disclosure, role-based access, and on-demand feature activation.

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
from fastmcp import FastMCP
from fastmcp.server.context import Context

mcp = FastMCP("Session-Aware Server")

@mcp.tool(tags={"premium"})
def premium_analysis(data: str) -> str:
    """Advanced analysis available to premium users."""
    return f"Premium analysis of: {data}"

@mcp.tool
async def unlock_premium(ctx: Context) -> str:
    """Unlock premium features for this session."""
    await ctx.enable_components(tags={"premium"})
    return "Premium features unlocked"

@mcp.tool
async def reset_features(ctx: Context) -> str:
    """Reset to default feature set."""
    await ctx.reset_visibility()
    return "Features reset to defaults"

# Premium tools are disabled globally by default
mcp.disable(tags={"premium"})
```

All sessions start with `premium_analysis` hidden. When a session calls `unlock_premium`, that session gains access to premium tools while other sessions remain unaffected. Calling `reset_features` returns the session to the global defaults.

### How Session Rules Work

Session rules override global transforms. When listing components, FastMCP first applies global enable/disable rules, then applies session-specific rules on top. Rules within a session accumulate, and later rules override earlier ones for the same component.

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
@mcp.tool
async def customize_session(ctx: Context) -> str:
    # Enable finance tools for this session
    await ctx.enable_components(tags={"finance"})

    # Also enable admin tools
    await ctx.enable_components(tags={"admin"})

    # Later: disable a specific admin tool
    await ctx.disable_components(names={"dangerous_admin_tool"})

    return "Session customized"
```

Each call adds a rule to the session. The `dangerous_admin_tool` ends up disabled because its disable rule was added after the admin enable rule.

### Filter Criteria

The session visibility methods accept the same filter criteria as `server.enable()` and `server.disable()`:

| Parameter    | Description                                                                |
| ------------ | -------------------------------------------------------------------------- |
| `names`      | Component names or URIs to match                                           |
| `keys`       | Component keys (e.g., `{"tool:my_tool"}`)                                  |
| `tags`       | Tags to match (component must have at least one)                           |
| `version`    | Version specification to match                                             |
| `components` | Component types (`{"tool"}`, `{"resource"}`, `{"prompt"}`, `{"template"}`) |
| `match_all`  | If `True`, matches all components regardless of other criteria             |

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
from fastmcp.utilities.versions import VersionSpec

@mcp.tool
async def enable_recent_tools(ctx: Context) -> str:
    """Enable only tools from version 2.0.0 or later."""
    await ctx.enable_components(
        version=VersionSpec(gte="2.0.0"),
        components={"tool"}
    )
    return "Recent tools enabled"
```

### Automatic Notifications

When session visibility changes, FastMCP automatically sends notifications to that session. Clients receive `ToolListChangedNotification`, `ResourceListChangedNotification`, and `PromptListChangedNotification` so they can refresh their component lists. These notifications go only to the affected session.

When you specify the `components` parameter, FastMCP optimizes by sending only the relevant notifications:

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
# Only sends ToolListChangedNotification
await ctx.enable_components(tags={"finance"}, components={"tool"})

# Sends all three notifications (no components filter)
await ctx.enable_components(tags={"finance"})
```

### Namespace Activation Pattern

A common pattern organizes tools into namespaces using tag prefixes, disables them globally, then provides activation tools that unlock namespaces on demand:

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
from fastmcp import FastMCP
from fastmcp.server.context import Context

server = FastMCP("Multi-Domain Assistant")

# Finance namespace
@server.tool(tags={"namespace:finance"})
def analyze_portfolio(symbols: list[str]) -> str:
    return f"Analysis for: {', '.join(symbols)}"

@server.tool(tags={"namespace:finance"})
def get_market_data(symbol: str) -> dict:
    return {"symbol": symbol, "price": 150.25}

# Admin namespace
@server.tool(tags={"namespace:admin"})
def list_users() -> list[str]:
    return ["alice", "bob", "charlie"]

# Activation tools - always visible
@server.tool
async def activate_finance(ctx: Context) -> str:
    await ctx.enable_components(tags={"namespace:finance"})
    return "Finance tools activated"

@server.tool
async def activate_admin(ctx: Context) -> str:
    await ctx.enable_components(tags={"namespace:admin"})
    return "Admin tools activated"

@server.tool
async def deactivate_all(ctx: Context) -> str:
    await ctx.reset_visibility()
    return "All namespaces deactivated"

# Disable namespace tools globally
server.disable(tags={"namespace:finance", "namespace:admin"})
```

Sessions start seeing only the activation tools. Calling `activate_finance` reveals finance tools for that session only. Multiple namespaces can be activated independently, and `deactivate_all` returns to the initial state.

### Method Reference

* **`await ctx.enable_components(...) -> None`**: Enable matching components for this session
* **`await ctx.disable_components(...) -> None`**: Disable matching components for this session
* **`await ctx.reset_visibility() -> None`**: Clear all session rules, returning to global defaults

## Client Notifications

When visibility state changes, FastMCP automatically notifies connected clients. Clients supporting the MCP notification protocol receive `list_changed` events and can refresh their component lists.

This happens automatically. You don't need to trigger notifications manually.

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
# This automatically notifies clients
mcp.disable(tags={"maintenance"})

# Clients receive: tools/list_changed, resources/list_changed, etc.
```

## Filtering Logic

Understanding the filtering logic helps when debugging visibility state issues.

The `is_enabled()` function checks a component's internal metadata:

1. If the component has `meta.fastmcp._internal.visibility = False`, it's disabled
2. If the component has `meta.fastmcp._internal.visibility = True`, it's enabled
3. If no visibility state is set, the component is enabled by default

When multiple `enable()` and `disable()` calls are made, transforms are applied in order. **Later transforms override earlier ones**, so the last matching transform wins.

## The Visibility Transform

Under the hood, `enable()` and `disable()` add `Visibility` transforms to the server or provider. The `Visibility` transform marks components with visibility metadata, and the server applies the final filter after all provider and server transforms complete.

```python  theme={"theme":{"light":"snazzy-light","dark":"dark-plus"}}
from fastmcp import FastMCP
from fastmcp.server.transforms import Visibility

mcp = FastMCP("Server")

# Using the convenience method (recommended)
mcp.disable(names={"secret_tool"})

# Equivalent to:
mcp.add_transform(Visibility(False, names={"secret_tool"}))
```

Server-level transforms override provider-level transforms. If a component is disabled at the provider level but enabled at the server level, the server-level `enable()` can re-enable it.