> ## Documentation Index

> Fetch the complete documentation index at: /llms.txt

> Use this file to discover all available pages before exploring further.

Skip to main content
Working with Tools

# Component Visibility

Control which components are available to clients

New in 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.

    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.

    # 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.

    # 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.

    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.

    # 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.

    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

    # 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.

    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.

    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.

    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.

    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.

    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.

    @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

    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:

    # 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:

    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.

    # 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.

    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.
Namespace Transform
Previous
Resources as Tools
Next
Ctrl+I
Assistant
Responses are generated using AI and may contain mistakes.
\n\n
\n
\n \n
\n
","contacts":" \n
\n\n Prefab\n \n \n \n \n\n
\n
\n \n
\n
","dashboard":" \n
\n\n Prefab\n \n \n \n \n\n
\n
\n \n
\n
","data-table":" \n
\n\n Prefab\n \n \n \n \n\n
\n
\n \n
\n
","hitchhikers":" \n
\n\n Prefab Showcase\n \n \n \n \n\n
\n
\n \n
\n
","pie-chart":" \n
\n\n Prefab\n \n \n \n \n\n
\n
\n \n
\n
","reactive":" \n
\n\n Prefab\n \n \n \n \n\n
\n
\n \n
\n
","team-directory-reactive":" \n
\n\n Prefab\n \n \n \n \n\n
\n
\n \n
\n
","team-directory":" \n
\n\n Prefab\n \n \n \n \n\n
\n
\n \n
\n
"};