Skip to main content
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}.
ComponentKey FormatExample
Tooltool:{name}tool:delete_everything
Resourceresource:{uri}resource:data://config
Templatetemplate:{uri}template:file://{path}
Promptprompt:{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():
ParameterDescription
namesComponent names or URIs to match
keysComponent keys (e.g., {"tool:my_tool"})
tagsTags to match (component must have at least one)
versionVersion specification to match
componentsComponent types ({"tool"}, {"resource"}, {"prompt"}, {"template"})
match_allIf 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.