Skip to main content
New in version 3.0.0 Visibility control lets you dynamically show or hide components from clients. A disabled tool disappears from listings and cannot be called. This enables runtime access control, feature flags, and context-aware component exposure.

Enable and Disable

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

Disabling Components

The disable() method adds components to a blocklist. Blocked components are hidden 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"

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

# Clients only see: get_status

Enabling Components

The enable() method removes components from the blocklist, making them visible again.
# 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 hidden 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 uses blocklist mode: everything is visible unless explicitly disabled. The only=True parameter switches to allowlist mode, where only specified components are visible.
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 show safe tools - everything else is hidden
mcp.enable(tags={"safe"}, only=True)

# Clients see: read_only_operation, list_items
# Hidden: 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 switches to “hidden”
  2. Previous allowlists are cleared
  3. Only specified keys/tags become visible
# Start fresh - only show 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)

Blocklist Precedence

Even in allowlist mode, the blocklist takes precedence. A component that’s both allowlisted and blocklisted remains hidden.
mcp.enable(tags={"api"}, only=True)  # Allow all api-tagged
mcp.disable(keys=["tool:api_admin"])  # But block this specific one

# api_admin is hidden despite having the "api" tag
This lets you create broad allowlists with specific exceptions.

Server vs Provider Visibility

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

Server-Level Visibility

Server visibility 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"

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

Provider-Level Visibility

Each provider maintains its own visibility state. Provider visibility filters components before they reach the server.
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"

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

# Server receives only regular_action
mcp = FastMCP("Server", providers=[admin_tools])
Provider-level visibility is useful when different servers should see different subsets of the same provider’s components.

Layered Filtering

When both server and provider have visibility rules, they stack. A component must pass both filters to be visible.
from fastmcp import FastMCP
from fastmcp.server.providers import LocalProvider

provider = LocalProvider()

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

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

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

# new_feature is hidden (blocked at server level)

Dynamic Visibility

Visibility changes take effect immediately. You can adjust visibility during request handling based on context.
from fastmcp import FastMCP
from fastmcp.server import Context

mcp = FastMCP("Server")

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

@mcp.tool
def check_permissions(ctx: Context) -> str:
    """Check if admin tools should be available."""
    user = ctx.request_context.get_user()

    if user and user.is_admin:
        mcp.enable(tags={"admin"})
        return "Admin tools enabled"
    else:
        mcp.disable(tags={"admin"})
        return "Admin tools disabled"
Dynamic visibility affects all connected clients. For per-user visibility, consider using separate server instances or implementing authorization in the tools themselves.

Client Notifications

When visibility 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 issues. The rules evaluate in this order:
  1. Blocklist by key: If the component’s key is in _disabled_keys, it’s hidden
  2. Blocklist by tag: If any of the component’s tags are in _disabled_tags, it’s hidden
  3. Allowlist check: If default visibility is off (allowlist mode) and the component isn’t in the allowlist, it’s hidden
  4. Default: Otherwise, the component is visible
The blocklist always wins over the allowlist. A component that matches both is hidden.