--- title: Component Visibility sidebarTitle: Visibility description: Control which components are available to clients icon: toggle-on tag: NEW --- import { VersionBadge } from '/snippets/version-badge.mdx' 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 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 # 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 # 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 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 # 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 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 # 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 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 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 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 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 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 @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 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 # 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 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 # 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 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.