From e861613b96fca887600a7df139a4db18f17ab812 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 19 Jan 2026 20:10:52 -0500 Subject: [PATCH 1/3] Rename Enabled transform class to Visibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The class name `Enabled` was confusing because it described a state rather than the concept being controlled. `Visibility` better expresses what the transform does: control which components are visible to clients. - Class: Enabled → Visibility - File: enabled.py → visibility.py - Context method: reset_components() → reset_visibility() - Internal metadata key: "enabled" → "visibility" --- docs/development/v3-notes/v3-features.mdx | 10 +-- docs/docs.json | 4 +- docs/python-sdk/fastmcp-server-context.mdx | 8 +- .../fastmcp-server-providers-base.mdx | 10 +-- docs/python-sdk/fastmcp-server-server.mdx | 16 ++-- ... fastmcp-server-transforms-visibility.mdx} | 83 +++++++++---------- docs/servers/context.mdx | 4 +- docs/servers/prompts.mdx | 2 +- docs/servers/providers/custom.mdx | 2 +- docs/servers/providers/local.mdx | 4 +- docs/servers/providers/overview.mdx | 4 +- docs/servers/resources.mdx | 2 +- docs/servers/tools.mdx | 2 +- docs/servers/{enabled.mdx => visibility.mdx} | 40 ++++----- examples/namespace_activation/server.py | 2 +- src/fastmcp/server/context.py | 28 +++---- src/fastmcp/server/providers/base.py | 12 +-- src/fastmcp/server/server.py | 18 ++-- src/fastmcp/server/transforms/__init__.py | 4 +- .../transforms/{enabled.py => visibility.py} | 74 +++++++++-------- .../server/test_include_exclude_tags.py | 18 ++-- tests/server/providers/test_local_provider.py | 40 +++++---- tests/server/test_session_visibility.py | 8 +- .../{test_enabled.py => test_visibility.py} | 78 ++++++++--------- 24 files changed, 241 insertions(+), 232 deletions(-) rename docs/python-sdk/{fastmcp-server-transforms-enabled.mdx => fastmcp-server-transforms-visibility.mdx} (60%) rename docs/servers/{enabled.mdx => visibility.mdx} (87%) rename src/fastmcp/server/transforms/{enabled.py => visibility.py} (89%) rename tests/server/transforms/{test_enabled.py => test_visibility.py} (78%) diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/development/v3-notes/v3-features.mdx index 86a78afdd7..ce31447a09 100644 --- a/docs/development/v3-notes/v3-features.mdx +++ b/docs/development/v3-notes/v3-features.mdx @@ -106,7 +106,7 @@ Transforms modify components (tools, resources, prompts) as they flow from provi - `Namespace` - adds prefixes to names (`tool` → `api_tool`) and path segments to URIs (`data://x` → `data://api/x`) - `ToolTransform` - modifies tool schemas (rename, description, tags, argument transforms) -- `Enabled` - sets enabled state on components by key or tag (backs `enable()`/`disable()` API) +- `Visibility` - sets visibility state on components by key or tag (backs `enable()`/`disable()` API) - `VersionFilter` - filters components by version range (`version_gte`, `version_lt`) - `ResourcesAsTools` - exposes resources as tools for tool-only clients - `PromptsAsTools` - exposes prompts as tools for tool-only clients @@ -149,7 +149,7 @@ Transforms apply at two levels: - **Provider-level**: `provider.add_transform()` - affects only that provider's components - **Server-level**: `server.add_transform()` - affects all components from all providers -Documentation: `docs/servers/providers/transforms.mdx`, `docs/servers/enabled.mdx` +Documentation: `docs/servers/providers/transforms.mdx`, `docs/servers/visibility.mdx` ### ResourcesAsTools and PromptsAsTools @@ -234,7 +234,7 @@ Documentation: `docs/servers/context.mdx` ## Enabled System -Components can be enabled/disabled using the enabled system. Each `enable()` or `disable()` call adds a stateless Enabled transform that marks components via internal metadata. Later transforms override earlier ones. +Components can be enabled/disabled using the visibility system. Each `enable()` or `disable()` call adds a stateless Visibility transform that marks components via internal metadata. Later transforms override earlier ones. ```python mcp = FastMCP("Server") @@ -286,7 +286,7 @@ async def unlock_premium(ctx: Context) -> str: @mcp.tool async def reset_features(ctx: Context) -> str: """Reset to default feature set.""" - await ctx.reset_components() + await ctx.reset_visibility() return "Features reset to defaults" # Globally disabled - sessions unlock individually @@ -300,7 +300,7 @@ Session visibility methods: Session rules override global transforms. FastMCP automatically sends `ToolListChangedNotification` (and resource/prompt equivalents) to affected sessions when visibility changes. -Documentation: `docs/servers/enabled.mdx` +Documentation: `docs/servers/visibility.mdx` --- diff --git a/docs/docs.json b/docs/docs.json index 61259d7739..cfc5f54e8b 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -137,7 +137,7 @@ "servers/tasks", "servers/telemetry", "servers/versioning", - "servers/enabled" + "servers/visibility" ] }, { @@ -563,7 +563,7 @@ "group": "transforms", "pages": [ "python-sdk/fastmcp-server-transforms-__init__", - "python-sdk/fastmcp-server-transforms-enabled", + "python-sdk/fastmcp-server-transforms-visibility", "python-sdk/fastmcp-server-transforms-namespace", "python-sdk/fastmcp-server-transforms-prompts_as_tools", "python-sdk/fastmcp-server-transforms-resources_as_tools", diff --git a/docs/python-sdk/fastmcp-server-context.mdx b/docs/python-sdk/fastmcp-server-context.mdx index 85688d54fc..1edf918c8d 100644 --- a/docs/python-sdk/fastmcp-server-context.mdx +++ b/docs/python-sdk/fastmcp-server-context.mdx @@ -591,7 +591,7 @@ Enable components matching criteria for this session only. Session rules override global transforms. Rules accumulate - each call adds a new rule to the session. Later marks override earlier ones -(Enabled transform semantics). +(Visibility transform semantics). Sends notifications to this session only: ToolListChangedNotification, ResourceListChangedNotification, and PromptListChangedNotification. @@ -615,7 +615,7 @@ Disable components matching criteria for this session only. Session rules override global transforms. Rules accumulate - each call adds a new rule to the session. Later marks override earlier ones -(Enabled transform semantics). +(Visibility transform semantics). Sends notifications to this session only: ToolListChangedNotification, ResourceListChangedNotification, and PromptListChangedNotification. @@ -629,10 +629,10 @@ ResourceListChangedNotification, and PromptListChangedNotification. - `match_all`: If True, matches all components regardless of other criteria. -#### `reset_components` +#### `reset_visibility` ```python -reset_components(self) -> None +reset_visibility(self) -> None ``` Clear all session visibility rules. diff --git a/docs/python-sdk/fastmcp-server-providers-base.mdx b/docs/python-sdk/fastmcp-server-providers-base.mdx index 730fe973dc..ee80c310fd 100644 --- a/docs/python-sdk/fastmcp-server-providers-base.mdx +++ b/docs/python-sdk/fastmcp-server-providers-base.mdx @@ -121,7 +121,7 @@ get_tool(self, name: str, version: VersionSpec | None = None) -> Tool | None Get tool by transformed name with all transforms applied. Note: This method does NOT filter disabled components. The Server -(FastMCP) performs enabled filtering after all transforms complete, +(FastMCP) performs visibility filtering after all transforms complete, allowing session-level transforms to override provider-level disables. **Args:** @@ -152,7 +152,7 @@ get_resource(self, uri: str, version: VersionSpec | None = None) -> Resource | N Get resource by transformed URI with all transforms applied. Note: This method does NOT filter disabled components. The Server -(FastMCP) performs enabled filtering after all transforms complete. +(FastMCP) performs visibility filtering after all transforms complete. **Args:** - `uri`: The transformed resource URI to look up. @@ -182,7 +182,7 @@ get_resource_template(self, uri: str, version: VersionSpec | None = None) -> Res Get resource template by transformed URI with all transforms applied. Note: This method does NOT filter disabled components. The Server -(FastMCP) performs enabled filtering after all transforms complete. +(FastMCP) performs visibility filtering after all transforms complete. **Args:** - `uri`: The transformed template URI to look up. @@ -212,7 +212,7 @@ get_prompt(self, name: str, version: VersionSpec | None = None) -> Prompt | None Get prompt by transformed name with all transforms applied. Note: This method does NOT filter disabled components. The Server -(FastMCP) performs enabled filtering after all transforms complete. +(FastMCP) performs visibility filtering after all transforms complete. **Args:** - `name`: The transformed prompt name to look up. @@ -289,7 +289,7 @@ disable(self) -> Self Disable components matching all specified criteria. -Adds an enabled transform that marks matching components as disabled. +Adds a visibility transform that marks matching components as disabled. Components can be re-enabled by calling enable() with matching criteria (the later transform wins). diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx index 353023918d..21db24fadc 100644 --- a/docs/python-sdk/fastmcp-server-server.mdx +++ b/docs/python-sdk/fastmcp-server-server.mdx @@ -191,7 +191,7 @@ list_tools(self) -> Sequence[Tool] List all enabled tools from providers. -Overrides Provider.list_tools() to add enabled filtering, auth filtering, +Overrides Provider.list_tools() to add visibility filtering, auth filtering, and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. @@ -204,7 +204,7 @@ get_tool(self, name: str, version: VersionSpec | None = None) -> Tool | None Get a tool by name, filtering disabled tools. -Overrides Provider.get_tool() to add enabled filtering after all +Overrides Provider.get_tool() to add visibility filtering after all transforms (including session-level) have been applied. This ensures session transforms can override provider-level disables. @@ -224,7 +224,7 @@ list_resources(self) -> Sequence[Resource] List all enabled resources from providers. -Overrides Provider.list_resources() to add enabled filtering, auth filtering, +Overrides Provider.list_resources() to add visibility filtering, auth filtering, and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. @@ -237,7 +237,7 @@ get_resource(self, uri: str, version: VersionSpec | None = None) -> Resource | N Get a resource by URI, filtering disabled resources. -Overrides Provider.get_resource() to add enabled filtering after all +Overrides Provider.get_resource() to add visibility filtering after all transforms (including session-level) have been applied. **Args:** @@ -256,7 +256,7 @@ list_resource_templates(self) -> Sequence[ResourceTemplate] List all enabled resource templates from providers. -Overrides Provider.list_resource_templates() to add enabled filtering, +Overrides Provider.list_resource_templates() to add visibility filtering, auth filtering, and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. @@ -269,7 +269,7 @@ get_resource_template(self, uri: str, version: VersionSpec | None = None) -> Res Get a resource template by URI, filtering disabled templates. -Overrides Provider.get_resource_template() to add enabled filtering after +Overrides Provider.get_resource_template() to add visibility filtering after all transforms (including session-level) have been applied. **Args:** @@ -288,7 +288,7 @@ list_prompts(self) -> Sequence[Prompt] List all enabled prompts from providers. -Overrides Provider.list_prompts() to add enabled filtering, auth filtering, +Overrides Provider.list_prompts() to add visibility filtering, auth filtering, and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. @@ -301,7 +301,7 @@ get_prompt(self, name: str, version: VersionSpec | None = None) -> Prompt | None Get a prompt by name, filtering disabled prompts. -Overrides Provider.get_prompt() to add enabled filtering after all +Overrides Provider.get_prompt() to add visibility filtering after all transforms (including session-level) have been applied. **Args:** diff --git a/docs/python-sdk/fastmcp-server-transforms-enabled.mdx b/docs/python-sdk/fastmcp-server-transforms-visibility.mdx similarity index 60% rename from docs/python-sdk/fastmcp-server-transforms-enabled.mdx rename to docs/python-sdk/fastmcp-server-transforms-visibility.mdx index a3b3f3b8d0..383eb0cc4d 100644 --- a/docs/python-sdk/fastmcp-server-transforms-enabled.mdx +++ b/docs/python-sdk/fastmcp-server-transforms-visibility.mdx @@ -1,21 +1,21 @@ --- -title: enabled -sidebarTitle: enabled +title: visibility +sidebarTitle: visibility --- -# `fastmcp.server.transforms.enabled` +# `fastmcp.server.transforms.visibility` -Enabled transform for marking component enabled state. +Visibility transform for marking component visibility state. -Each Enabled instance marks components via internal metadata. Multiple -enabled transforms can be stacked - later transforms override earlier ones. +Each Visibility instance marks components via internal metadata. Multiple +visibility transforms can be stacked - later transforms override earlier ones. Final filtering happens at the Provider level. ## Functions -### `is_enabled` +### `is_enabled` ```python is_enabled(component: FastMCPComponent) -> bool @@ -25,10 +25,10 @@ is_enabled(component: FastMCPComponent) -> bool Check if component is enabled. Returns True if: -- No enabled mark exists (default is enabled) -- Enabled mark is True +- No visibility mark exists (default is enabled) +- Visibility mark is True -Returns False if enabled mark is False. +Returns False if visibility mark is False. **Args:** - `component`: Component to check. @@ -37,7 +37,7 @@ Returns False if enabled mark is False. - True if component should be enabled/visible to clients. -### `get_visibility_rules` +### `get_visibility_rules` ```python get_visibility_rules(context: Context) -> list[dict[str, Any]] @@ -47,7 +47,7 @@ get_visibility_rules(context: Context) -> list[dict[str, Any]] Load visibility rule dicts from session state. -### `save_visibility_rules` +### `save_visibility_rules` ```python save_visibility_rules(context: Context, rules: list[dict[str, Any]]) -> None @@ -64,27 +64,27 @@ If None, sends notifications for all types (safe default). If provided, only sends notifications for specified types. -### `create_enabled_transforms` +### `create_visibility_transforms` ```python -create_enabled_transforms(rules: list[dict[str, Any]]) -> list[Enabled] +create_visibility_transforms(rules: list[dict[str, Any]]) -> list[Visibility] ``` -Convert rule dicts to Enabled transforms. +Convert rule dicts to Visibility transforms. -### `get_session_transforms` +### `get_session_transforms` ```python -get_session_transforms(context: Context) -> list[Enabled] +get_session_transforms(context: Context) -> list[Visibility] ``` -Get session-specific Enabled transforms from state store. +Get session-specific Visibility transforms from state store. -### `enable_components` +### `enable_components` ```python enable_components(context: Context) -> None @@ -95,7 +95,7 @@ Enable components matching criteria for this session only. Session rules override global transforms. Rules accumulate - each call adds a new rule to the session. Later marks override earlier ones -(Enabled transform semantics). +(Visibility transform semantics). Sends notifications to this session only: ToolListChangedNotification, ResourceListChangedNotification, and PromptListChangedNotification. @@ -110,7 +110,7 @@ ResourceListChangedNotification, and PromptListChangedNotification. - `match_all`: If True, matches all components regardless of other criteria. -### `disable_components` +### `disable_components` ```python disable_components(context: Context) -> None @@ -121,7 +121,7 @@ Disable components matching criteria for this session only. Session rules override global transforms. Rules accumulate - each call adds a new rule to the session. Later marks override earlier ones -(Enabled transform semantics). +(Visibility transform semantics). Sends notifications to this session only: ToolListChangedNotification, ResourceListChangedNotification, and PromptListChangedNotification. @@ -136,10 +136,10 @@ ResourceListChangedNotification, and PromptListChangedNotification. - `match_all`: If True, matches all components regardless of other criteria. -### `reset_components` +### `reset_visibility` ```python -reset_components(context: Context) -> None +reset_visibility(context: Context) -> None ``` @@ -154,7 +154,7 @@ ResourceListChangedNotification, and PromptListChangedNotification. - `context`: The context for this session. -### `apply_session_transforms` +### `apply_session_transforms` ```python apply_session_transforms(components: Sequence[ComponentT]) -> Sequence[ComponentT] @@ -164,7 +164,7 @@ apply_session_transforms(components: Sequence[ComponentT]) -> Sequence[Component Apply session-specific visibility transforms to components. This helper applies session-level enable/disable rules by marking -components with their enabled state. Session transforms override +components with their visibility state. Session transforms override global transforms due to mark-based semantics (later marks win). **Args:** @@ -176,28 +176,28 @@ global transforms due to mark-based semantics (later marks win). ## Classes -### `Enabled` +### `Visibility` -Sets enabled state on matching components. +Sets visibility state on matching components. -Does NOT filter inline - just marks components with enabled state. +Does NOT filter inline - just marks components with visibility state. Later transforms in the chain can override earlier marks. Final filtering happens at the Provider level after all transforms run. **Methods:** -#### `list_tools` +#### `list_tools` ```python list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool] ``` -Mark tools by enabled state. +Mark tools by visibility state. -#### `get_tool` +#### `get_tool` ```python get_tool(self, name: str, call_next: GetToolNext) -> Tool | None @@ -206,16 +206,16 @@ get_tool(self, name: str, call_next: GetToolNext) -> Tool | None Mark tool if found. -#### `list_resources` +#### `list_resources` ```python list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource] ``` -Mark resources by enabled state. +Mark resources by visibility state. -#### `get_resource` +#### `get_resource` ```python get_resource(self, uri: str, call_next: GetResourceNext) -> Resource | None @@ -224,16 +224,16 @@ get_resource(self, uri: str, call_next: GetResourceNext) -> Resource | None Mark resource if found. -#### `list_resource_templates` +#### `list_resource_templates` ```python list_resource_templates(self, templates: Sequence[ResourceTemplate]) -> Sequence[ResourceTemplate] ``` -Mark resource templates by enabled state. +Mark resource templates by visibility state. -#### `get_resource_template` +#### `get_resource_template` ```python get_resource_template(self, uri: str, call_next: GetResourceTemplateNext) -> ResourceTemplate | None @@ -242,20 +242,19 @@ get_resource_template(self, uri: str, call_next: GetResourceTemplateNext) -> Res Mark resource template if found. -#### `list_prompts` +#### `list_prompts` ```python list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt] ``` -Mark prompts by enabled state. +Mark prompts by visibility state. -#### `get_prompt` +#### `get_prompt` ```python get_prompt(self, name: str, call_next: GetPromptNext) -> Prompt | None ``` Mark prompt if found. - diff --git a/docs/servers/context.mdx b/docs/servers/context.mdx index 5dcea7f67b..e020db32e3 100644 --- a/docs/servers/context.mdx +++ b/docs/servers/context.mdx @@ -23,7 +23,7 @@ The `Context` object provides a clean interface to access MCP features within yo - **LLM Sampling**: Request the client's LLM to generate text based on provided messages - **User Elicitation**: Request structured input from users during tool execution - **Session State**: Store data that persists across requests within an MCP session -- **Session Visibility**: [Control which components are visible](/servers/enabled#per-session-visibility) to the current session +- **Session Visibility**: [Control which components are visible](/servers/visibility#per-session-visibility) to the current session - **Request Information**: Access metadata about the current request - **Server Access**: When needed, access the underlying FastMCP server instance @@ -266,7 +266,7 @@ State set during `on_initialize` middleware persists to subsequent tool calls wh -Tools can customize which components are visible to their current session using `ctx.enable_components()`, `ctx.disable_components()`, and `ctx.reset_components()`. These methods apply visibility rules that affect only the calling session, leaving other sessions unchanged. See [Per-Session Visibility](/servers/enabled#per-session-visibility) for complete documentation, filter criteria, and patterns like namespace activation. +Tools can customize which components are visible to their current session using `ctx.enable_components()`, `ctx.disable_components()`, and `ctx.reset_visibility()`. These methods apply visibility rules that affect only the calling session, leaving other sessions unchanged. See [Per-Session Visibility](/servers/visibility#per-session-visibility) for complete documentation, filter criteria, and patterns like namespace activation. ### Change Notifications diff --git a/docs/servers/prompts.mdx b/docs/servers/prompts.mdx index 47e3c48729..95b1bb64a2 100644 --- a/docs/servers/prompts.mdx +++ b/docs/servers/prompts.mdx @@ -354,7 +354,7 @@ mcp.disable(tags={"internal"}) mcp.enable(tags={"public"}, only=True) ``` -See [Enabled](/servers/enabled) for the complete enabled control API including key formats, tag-based filtering, and provider-level control. +See [Visibility](/servers/visibility) for the complete visibility control API including key formats, tag-based filtering, and provider-level control. ### Async Prompts diff --git a/docs/servers/providers/custom.mdx b/docs/servers/providers/custom.mdx index 12daa4bdcf..248e32d20a 100644 --- a/docs/servers/providers/custom.mdx +++ b/docs/servers/providers/custom.mdx @@ -29,7 +29,7 @@ Both providers and [middleware](/servers/middleware) can influence what componen **Middleware** intercepts individual requests. It's well-suited for request-specific decisions like logging, rate limiting, or authentication. -You *could* use middleware to dynamically add tools based on request context. But it's often cleaner to have a provider source all possible tools, then use middleware or [enabled controls](/servers/enabled) to filter what each request can see. This separation makes it easier to reason about how components are sourced and how they interact with other server machinery. +You *could* use middleware to dynamically add tools based on request context. But it's often cleaner to have a provider source all possible tools, then use middleware or [visibility controls](/servers/visibility) to filter what each request can see. This separation makes it easier to reason about how components are sourced and how they interact with other server machinery. ## The Provider Interface diff --git a/docs/servers/providers/local.mdx b/docs/servers/providers/local.mdx index 9765c4111f..7cdefa5e61 100644 --- a/docs/servers/providers/local.mdx +++ b/docs/servers/providers/local.mdx @@ -126,7 +126,7 @@ mcp.disable(tags={"admin"}) mcp.enable(keys={"tool:get_status"}, only=True) ``` -See [Enabled](/servers/enabled) for the full documentation on keys, tags, allowlist mode, and provider-level control. +See [Visibility](/servers/visibility) for the full documentation on keys, tags, allowlist mode, and provider-level control. ## Standalone LocalProvider @@ -157,4 +157,4 @@ This is useful for: - Testing components in isolation - Building reusable component libraries -Standalone providers also support enabled control with `enable()` and `disable()`. See [Enabled](/servers/enabled) for details. +Standalone providers also support visibility control with `enable()` and `disable()`. See [Visibility](/servers/visibility) for details. diff --git a/docs/servers/providers/overview.mdx b/docs/servers/providers/overview.mdx index c291f2eccb..07b9e8bceb 100644 --- a/docs/servers/providers/overview.mdx +++ b/docs/servers/providers/overview.mdx @@ -67,7 +67,7 @@ When a client requests a tool, FastMCP queries providers in registration order. **Learn about providers when** you want to: - [Mount another server](/servers/providers/mounting) into yours - [Proxy a remote server](/servers/providers/proxy) through yours -- [Control enabled state](/servers/enabled) of components +- [Control visibility state](/servers/visibility) of components - [Build dynamic sources](/servers/providers/custom) like database-backed tools ## Next Steps @@ -76,5 +76,5 @@ When a client requests a tool, FastMCP queries providers in registration order. - [Mounting](/servers/providers/mounting) - Compose servers together - [Proxying](/servers/providers/proxy) - Connect to remote servers - [Transforms](/servers/providers/transforms) - Namespace, rename, and modify components -- [Enabled](/servers/enabled) - Control which components clients can access +- [Visibility](/servers/visibility) - Control which components clients can access - [Custom](/servers/providers/custom) - Build your own providers diff --git a/docs/servers/resources.mdx b/docs/servers/resources.mdx index b78b7568a0..1c24d524f4 100644 --- a/docs/servers/resources.mdx +++ b/docs/servers/resources.mdx @@ -240,7 +240,7 @@ mcp.disable(tags={"internal"}) mcp.enable(tags={"public"}, only=True) ``` -See [Enabled](/servers/enabled) for the complete enabled control API including key formats, tag-based filtering, and provider-level control. +See [Visibility](/servers/visibility) for the complete visibility control API including key formats, tag-based filtering, and provider-level control. ### Accessing MCP Context diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx index 483d0825b4..ce4cb9b888 100644 --- a/docs/servers/tools.mdx +++ b/docs/servers/tools.mdx @@ -862,7 +862,7 @@ mcp.disable(tags={"admin"}) mcp.enable(tags={"public"}, only=True) ``` -See [Enabled](/servers/enabled) for the complete enabled control API including key formats, tag-based filtering, and provider-level control. +See [Visibility](/servers/visibility) for the complete visibility control API including key formats, tag-based filtering, and provider-level control. ## MCP Annotations diff --git a/docs/servers/enabled.mdx b/docs/servers/visibility.mdx similarity index 87% rename from docs/servers/enabled.mdx rename to docs/servers/visibility.mdx index bd718aa149..ad378eacdc 100644 --- a/docs/servers/enabled.mdx +++ b/docs/servers/visibility.mdx @@ -59,7 +59,7 @@ mcp.enable(tags={"admin"}) ## Keys and Tags -Enabled filtering works with two identifiers: keys (for specific components) and tags (for groups). +Visibility filtering works with two identifiers: keys (for specific components) and tags (for groups). ### Component Keys @@ -123,7 +123,7 @@ mcp.disable(keys={"tool:debug_info"}, tags={"dangerous"}) ## Allowlist Mode -By default, enabled filtering uses blocklist mode: everything is enabled unless explicitly disabled. The `only=True` parameter switches to allowlist mode, where **only** specified components are enabled. +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 @@ -159,7 +159,7 @@ Allowlist mode is useful for restrictive environments where you want to explicit When you call `enable(only=True)`: -1. Default enabled state switches to "disabled" +1. Default visibility state switches to "disabled" 2. Previous allowlists are cleared 3. Only specified keys/tags become enabled @@ -186,11 +186,11 @@ You can always re-enable something that was disabled by adding another `enable() ## Server vs Provider -Enabled state operates at two levels: the server and individual providers. +Visibility state operates at two levels: the server and individual providers. ### Server-Level -Server-level enabled 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. +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 @@ -208,13 +208,13 @@ main.disable(tags={"internal"}) ### Provider-Level -Each provider can add its own enabled transforms. These run before server-level transforms, so the server can override provider-level disables. +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 enabled control +# Create provider with visibility control admin_tools = LocalProvider() @admin_tools.tool(tags={"admin"}) @@ -261,7 +261,7 @@ mcp.disable(tags={"beta"}) ## Dynamic Changes -Enabled state changes take effect immediately. You can adjust during request handling based on context. +Visibility state changes take effect immediately. You can adjust during request handling based on context. ```python from fastmcp import FastMCP @@ -312,7 +312,7 @@ async def unlock_premium(ctx: Context) -> str: @mcp.tool async def reset_features(ctx: Context) -> str: """Reset to default feature set.""" - await ctx.reset_components() + await ctx.reset_visibility() return "Features reset to defaults" # Premium tools are disabled globally by default @@ -419,7 +419,7 @@ async def activate_admin(ctx: Context) -> str: @server.tool async def deactivate_all(ctx: Context) -> str: - await ctx.reset_components() + await ctx.reset_visibility() return "All namespaces deactivated" # Disable namespace tools globally @@ -432,11 +432,11 @@ Sessions start seeing only the activation tools. Calling `activate_finance` reve - **`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_components() -> None`**: Clear all session rules, returning to global defaults +- **`await ctx.reset_visibility() -> None`**: Clear all session rules, returning to global defaults ## Client Notifications -When enabled state changes, FastMCP automatically notifies connected clients. Clients supporting the MCP notification protocol receive `list_changed` events and can refresh their component lists. +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. @@ -449,23 +449,23 @@ mcp.disable(tags={"maintenance"}) ## Filtering Logic -Understanding the filtering logic helps when debugging enabled state issues. +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.enabled = False`, it's disabled -2. If the component has `meta.fastmcp._internal.enabled = True`, it's enabled -3. If no enabled state is set, the component is enabled by default +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 Enabled Transform +## The Visibility Transform -Under the hood, `enable()` and `disable()` add `Enabled` transforms to the server or provider. The `Enabled` transform marks components with enabled metadata, and the server applies the final filter after all provider and server transforms complete. +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 Enabled +from fastmcp.server.transforms import Visibility mcp = FastMCP("Server") @@ -473,7 +473,7 @@ mcp = FastMCP("Server") mcp.disable(names={"secret_tool"}) # Equivalent to: -mcp.add_transform(Enabled(False, names={"secret_tool"})) +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. diff --git a/examples/namespace_activation/server.py b/examples/namespace_activation/server.py index 758790b0c9..f906d9f15c 100644 --- a/examples/namespace_activation/server.py +++ b/examples/namespace_activation/server.py @@ -61,7 +61,7 @@ async def activate_admin(ctx: Context) -> str: @server.tool async def deactivate_all(ctx: Context) -> str: """Deactivate all namespaces, returning to defaults.""" - await ctx.reset_components() + await ctx.reset_visibility() return "All namespaces deactivated" diff --git a/src/fastmcp/server/context.py b/src/fastmcp/server/context.py index 9dbc9376cf..0fe505b16b 100644 --- a/src/fastmcp/server/context.py +++ b/src/fastmcp/server/context.py @@ -39,23 +39,23 @@ sample_step_impl, ) from fastmcp.server.server import FastMCP, StateValue -from fastmcp.server.transforms.enabled import ( - Enabled, +from fastmcp.server.transforms.visibility import ( + Visibility, ) -from fastmcp.server.transforms.enabled import ( +from fastmcp.server.transforms.visibility import ( disable_components as _disable_components, ) -from fastmcp.server.transforms.enabled import ( +from fastmcp.server.transforms.visibility import ( enable_components as _enable_components, ) -from fastmcp.server.transforms.enabled import ( +from fastmcp.server.transforms.visibility import ( get_session_transforms as _get_session_transforms, ) -from fastmcp.server.transforms.enabled import ( +from fastmcp.server.transforms.visibility import ( get_visibility_rules as _get_visibility_rules, ) -from fastmcp.server.transforms.enabled import ( - reset_components as _reset_components, +from fastmcp.server.transforms.visibility import ( + reset_visibility as _reset_visibility, ) from fastmcp.utilities.logging import _clamp_logger, get_logger from fastmcp.utilities.versions import VersionSpec @@ -990,8 +990,8 @@ async def _get_visibility_rules(self) -> list[dict[str, Any]]: """Load visibility rule dicts from session state.""" return await _get_visibility_rules(self) - async def _get_session_transforms(self) -> list[Enabled]: - """Get session-specific Enabled transforms from state store.""" + async def _get_session_transforms(self) -> list[Visibility]: + """Get session-specific Visibility transforms from state store.""" return await _get_session_transforms(self) async def enable_components( @@ -1009,7 +1009,7 @@ async def enable_components( Session rules override global transforms. Rules accumulate - each call adds a new rule to the session. Later marks override earlier ones - (Enabled transform semantics). + (Visibility transform semantics). Sends notifications to this session only: ToolListChangedNotification, ResourceListChangedNotification, and PromptListChangedNotification. @@ -1047,7 +1047,7 @@ async def disable_components( Session rules override global transforms. Rules accumulate - each call adds a new rule to the session. Later marks override earlier ones - (Enabled transform semantics). + (Visibility transform semantics). Sends notifications to this session only: ToolListChangedNotification, ResourceListChangedNotification, and PromptListChangedNotification. @@ -1070,7 +1070,7 @@ async def disable_components( match_all=match_all, ) - async def reset_components(self) -> None: + async def reset_visibility(self) -> None: """Clear all session visibility rules. Use this to reset session visibility back to global defaults. @@ -1078,7 +1078,7 @@ async def reset_components(self) -> None: Sends notifications to this session only: ToolListChangedNotification, ResourceListChangedNotification, and PromptListChangedNotification. """ - await _reset_components(self) + await _reset_visibility(self) async def _log_to_server_and_client( diff --git a/src/fastmcp/server/providers/base.py b/src/fastmcp/server/providers/base.py index 03c00a8a25..ccd48765f7 100644 --- a/src/fastmcp/server/providers/base.py +++ b/src/fastmcp/server/providers/base.py @@ -38,7 +38,7 @@ async def _get_tool(self, name: str) -> Tool | None: from fastmcp.prompts.prompt import Prompt from fastmcp.resources.resource import Resource from fastmcp.resources.template import ResourceTemplate -from fastmcp.server.transforms.enabled import Enabled +from fastmcp.server.transforms.visibility import Visibility from fastmcp.tools.tool import Tool from fastmcp.utilities.async_utils import gather from fastmcp.utilities.components import FastMCPComponent @@ -502,7 +502,7 @@ def enable( ) -> Self: """Enable components matching all specified criteria. - Adds an enabled transform that marks matching components as enabled. + Adds a visibility transform that marks matching components as enabled. Later transforms override earlier ones, so enable after disable makes the component enabled. @@ -524,9 +524,9 @@ def enable( if only: # Allowlist: disable everything, then enable matching # The enable transform runs later on return path, so it overrides - self._transforms.append(Enabled(False, match_all=True)) + self._transforms.append(Visibility(False, match_all=True)) self._transforms.append( - Enabled( + Visibility( True, names=names, keys=keys, @@ -550,7 +550,7 @@ def disable( ) -> Self: """Disable components matching all specified criteria. - Adds an enabled transform that marks matching components as disabled. + Adds a visibility transform that marks matching components as disabled. Components can be re-enabled by calling enable() with matching criteria (the later transform wins). @@ -566,7 +566,7 @@ def disable( Self for method chaining. """ self._transforms.append( - Enabled( + Visibility( False, names=names, keys=keys, diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index a1c328a0c6..48a2fdf933 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -72,7 +72,7 @@ ToolTransform, Transform, ) -from fastmcp.server.transforms.enabled import apply_session_transforms, is_enabled +from fastmcp.server.transforms.visibility import apply_session_transforms, is_enabled from fastmcp.settings import DuplicateBehavior as DuplicateBehaviorSetting from fastmcp.settings import Settings from fastmcp.tools.function_tool import FunctionTool @@ -604,7 +604,7 @@ def remove_tool_transformation(self, _tool_name: str) -> None: async def list_tools(self, *, run_middleware: bool = True) -> Sequence[Tool]: """List all enabled tools from providers. - Overrides Provider.list_tools() to add enabled filtering, auth filtering, + Overrides Provider.list_tools() to add visibility filtering, auth filtering, and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. """ @@ -676,7 +676,7 @@ async def get_tool( ) -> Tool | None: """Get a tool by name, filtering disabled tools. - Overrides Provider.get_tool() to add enabled filtering after all + Overrides Provider.get_tool() to add visibility filtering after all transforms (including session-level) have been applied. This ensures session transforms can override provider-level disables. @@ -702,7 +702,7 @@ async def list_resources( ) -> Sequence[Resource]: """List all enabled resources from providers. - Overrides Provider.list_resources() to add enabled filtering, auth filtering, + Overrides Provider.list_resources() to add visibility filtering, auth filtering, and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. """ @@ -774,7 +774,7 @@ async def get_resource( ) -> Resource | None: """Get a resource by URI, filtering disabled resources. - Overrides Provider.get_resource() to add enabled filtering after all + Overrides Provider.get_resource() to add visibility filtering after all transforms (including session-level) have been applied. Args: @@ -799,7 +799,7 @@ async def list_resource_templates( ) -> Sequence[ResourceTemplate]: """List all enabled resource templates from providers. - Overrides Provider.list_resource_templates() to add enabled filtering, + Overrides Provider.list_resource_templates() to add visibility filtering, auth filtering, and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. """ @@ -873,7 +873,7 @@ async def get_resource_template( ) -> ResourceTemplate | None: """Get a resource template by URI, filtering disabled templates. - Overrides Provider.get_resource_template() to add enabled filtering after + Overrides Provider.get_resource_template() to add visibility filtering after all transforms (including session-level) have been applied. Args: @@ -896,7 +896,7 @@ async def get_resource_template( async def list_prompts(self, *, run_middleware: bool = True) -> Sequence[Prompt]: """List all enabled prompts from providers. - Overrides Provider.list_prompts() to add enabled filtering, auth filtering, + Overrides Provider.list_prompts() to add visibility filtering, auth filtering, and middleware execution. Returns all versions (no deduplication). Protocol handlers deduplicate for MCP wire format. """ @@ -968,7 +968,7 @@ async def get_prompt( ) -> Prompt | None: """Get a prompt by name, filtering disabled prompts. - Overrides Provider.get_prompt() to add enabled filtering after all + Overrides Provider.get_prompt() to add visibility filtering after all transforms (including session-level) have been applied. Args: diff --git a/src/fastmcp/server/transforms/__init__.py b/src/fastmcp/server/transforms/__init__.py index 63225854ae..9e55cc9035 100644 --- a/src/fastmcp/server/transforms/__init__.py +++ b/src/fastmcp/server/transforms/__init__.py @@ -220,7 +220,7 @@ async def get_prompt( # Re-export built-in transforms (must be after Transform class to avoid circular imports) -from fastmcp.server.transforms.enabled import Enabled, is_enabled # noqa: E402 +from fastmcp.server.transforms.visibility import Visibility, is_enabled # noqa: E402 from fastmcp.server.transforms.namespace import Namespace # noqa: E402 from fastmcp.server.transforms.prompts_as_tools import PromptsAsTools # noqa: E402 from fastmcp.server.transforms.resources_as_tools import ResourcesAsTools # noqa: E402 @@ -228,7 +228,6 @@ async def get_prompt( from fastmcp.server.transforms.version_filter import VersionFilter # noqa: E402 __all__ = [ - "Enabled", "GetPromptNext", "GetResourceNext", "GetResourceTemplateNext", @@ -240,5 +239,6 @@ async def get_prompt( "Transform", "VersionFilter", "VersionSpec", + "Visibility", "is_enabled", ] diff --git a/src/fastmcp/server/transforms/enabled.py b/src/fastmcp/server/transforms/visibility.py similarity index 89% rename from src/fastmcp/server/transforms/enabled.py rename to src/fastmcp/server/transforms/visibility.py index faf9aeaaad..41061a6510 100644 --- a/src/fastmcp/server/transforms/enabled.py +++ b/src/fastmcp/server/transforms/visibility.py @@ -1,7 +1,7 @@ -"""Enabled transform for marking component enabled state. +"""Visibility transform for marking component visibility state. -Each Enabled instance marks components via internal metadata. Multiple -enabled transforms can be stacked - later transforms override earlier ones. +Each Visibility instance marks components via internal metadata. Multiple +visibility transforms can be stacked - later transforms override earlier ones. Final filtering happens at the Provider level. """ @@ -31,29 +31,29 @@ T = TypeVar("T", bound="FastMCPComponent") -# Enabled state stored at meta["fastmcp"]["_internal"]["enabled"] +# Visibility state stored at meta["fastmcp"]["_internal"]["visibility"] _FASTMCP_KEY = "fastmcp" _INTERNAL_KEY = "_internal" -class Enabled(Transform): - """Sets enabled state on matching components. +class Visibility(Transform): + """Sets visibility state on matching components. - Does NOT filter inline - just marks components with enabled state. + Does NOT filter inline - just marks components with visibility state. Later transforms in the chain can override earlier marks. Final filtering happens at the Provider level after all transforms run. Example: ```python # Disable components tagged "internal" - Enabled(False, tags={"internal"}) + Visibility(False, tags={"internal"}) # Re-enable specific tool (override earlier disable) - Enabled(True, names={"safe_tool"}) + Visibility(True, names={"safe_tool"}) # Allowlist via composition: - Enabled(False, match_all=True) # disable everything - Enabled(True, tags={"public"}) # enable public + Visibility(False, match_all=True) # disable everything + Visibility(True, tags={"public"}) # enable public ``` """ @@ -69,7 +69,7 @@ def __init__( | None = None, match_all: bool = False, ) -> None: - """Initialize an enabled marker. + """Initialize a visibility marker. Args: enabled: If True, mark matching as enabled; if False, mark as disabled. @@ -92,7 +92,7 @@ def __init__( def __repr__(self) -> str: action = "enable" if self._enabled else "disable" if self.match_all: - return f"Enabled({self._enabled}, match_all=True)" + return f"Visibility({self._enabled}, match_all=True)" parts = [] if self.names: parts.append(f"names={set(self.names)}") @@ -105,8 +105,8 @@ def __repr__(self) -> str: if self.tags: parts.append(f"tags={set(self.tags)}") if parts: - return f"Enabled({action}, {', '.join(parts)})" - return f"Enabled({action})" + return f"Visibility({action}, {', '.join(parts)})" + return f"Visibility({action})" def _matches(self, component: FastMCPComponent) -> bool: """Check if this transform applies to the component. @@ -171,18 +171,20 @@ def _matches(self, component: FastMCPComponent) -> bool: return self.tags is None or bool(component.tags & self.tags) def _mark_component(self, component: T) -> T: - """Set enabled state in component metadata if rule matches.""" + """Set visibility state in component metadata if rule matches.""" if not self._matches(component): return component # Create new dicts to avoid mutating shared dicts # (e.g., when Tool.from_tool shares the meta dict between tools) if component.meta is None: - component.meta = {_FASTMCP_KEY: {_INTERNAL_KEY: {"enabled": self._enabled}}} + component.meta = { + _FASTMCP_KEY: {_INTERNAL_KEY: {"visibility": self._enabled}} + } else: old_fastmcp = component.meta.get(_FASTMCP_KEY, {}) old_internal = old_fastmcp.get(_INTERNAL_KEY, {}) - new_internal = {**old_internal, "enabled": self._enabled} + new_internal = {**old_internal, "visibility": self._enabled} new_fastmcp = {**old_fastmcp, _INTERNAL_KEY: new_internal} component.meta = {**component.meta, _FASTMCP_KEY: new_fastmcp} return component @@ -192,7 +194,7 @@ def _mark_component(self, component: T) -> T: # ------------------------------------------------------------------------- async def list_tools(self, tools: Sequence[Tool]) -> Sequence[Tool]: - """Mark tools by enabled state.""" + """Mark tools by visibility state.""" return [self._mark_component(t) for t in tools] async def get_tool( @@ -209,7 +211,7 @@ async def get_tool( # ------------------------------------------------------------------------- async def list_resources(self, resources: Sequence[Resource]) -> Sequence[Resource]: - """Mark resources by enabled state.""" + """Mark resources by visibility state.""" return [self._mark_component(r) for r in resources] async def get_resource( @@ -232,7 +234,7 @@ async def get_resource( async def list_resource_templates( self, templates: Sequence[ResourceTemplate] ) -> Sequence[ResourceTemplate]: - """Mark resource templates by enabled state.""" + """Mark resource templates by visibility state.""" return [self._mark_component(t) for t in templates] async def get_resource_template( @@ -253,7 +255,7 @@ async def get_resource_template( # ------------------------------------------------------------------------- async def list_prompts(self, prompts: Sequence[Prompt]) -> Sequence[Prompt]: - """Mark prompts by enabled state.""" + """Mark prompts by visibility state.""" return [self._mark_component(p) for p in prompts] async def get_prompt( @@ -270,10 +272,10 @@ def is_enabled(component: FastMCPComponent) -> bool: """Check if component is enabled. Returns True if: - - No enabled mark exists (default is enabled) - - Enabled mark is True + - No visibility mark exists (default is enabled) + - Visibility mark is True - Returns False if enabled mark is False. + Returns False if visibility mark is False. Args: component: Component to check. @@ -284,7 +286,7 @@ def is_enabled(component: FastMCPComponent) -> bool: meta = component.meta or {} fastmcp = meta.get(_FASTMCP_KEY, {}) internal = fastmcp.get(_INTERNAL_KEY, {}) - return internal.get("enabled", True) # Default True if not set + return internal.get("visibility", True) # Default True if not set # ------------------------------------------------------------------------- @@ -327,8 +329,8 @@ async def save_visibility_rules( await context.send_notification(mcp.types.PromptListChangedNotification()) -def create_enabled_transforms(rules: list[dict[str, Any]]) -> list[Enabled]: - """Convert rule dicts to Enabled transforms.""" +def create_visibility_transforms(rules: list[dict[str, Any]]) -> list[Visibility]: + """Convert rule dicts to Visibility transforms.""" transforms = [] for params in rules: version = None @@ -340,7 +342,7 @@ def create_enabled_transforms(rules: list[dict[str, Any]]) -> list[Enabled]: eq=version_dict.get("eq"), ) transforms.append( - Enabled( + Visibility( params["enabled"], names=set(params["names"]) if params.get("names") else None, keys=set(params["keys"]) if params.get("keys") else None, @@ -355,8 +357,8 @@ def create_enabled_transforms(rules: list[dict[str, Any]]) -> list[Enabled]: return transforms -async def get_session_transforms(context: Context) -> list[Enabled]: - """Get session-specific Enabled transforms from state store.""" +async def get_session_transforms(context: Context) -> list[Visibility]: + """Get session-specific Visibility transforms from state store.""" try: # Will raise RuntimeError if no session available _ = context.session_id @@ -364,7 +366,7 @@ async def get_session_transforms(context: Context) -> list[Enabled]: return [] rules = await get_visibility_rules(context) - return create_enabled_transforms(rules) + return create_visibility_transforms(rules) async def enable_components( @@ -381,7 +383,7 @@ async def enable_components( Session rules override global transforms. Rules accumulate - each call adds a new rule to the session. Later marks override earlier ones - (Enabled transform semantics). + (Visibility transform semantics). Sends notifications to this session only: ToolListChangedNotification, ResourceListChangedNotification, and PromptListChangedNotification. @@ -435,7 +437,7 @@ async def disable_components( Session rules override global transforms. Rules accumulate - each call adds a new rule to the session. Later marks override earlier ones - (Enabled transform semantics). + (Visibility transform semantics). Sends notifications to this session only: ToolListChangedNotification, ResourceListChangedNotification, and PromptListChangedNotification. @@ -475,7 +477,7 @@ async def disable_components( await save_visibility_rules(context, rules, components=components) -async def reset_components(context: Context) -> None: +async def reset_visibility(context: Context) -> None: """Clear all session visibility rules. Use this to reset session visibility back to global defaults. @@ -498,7 +500,7 @@ async def apply_session_transforms( """Apply session-specific visibility transforms to components. This helper applies session-level enable/disable rules by marking - components with their enabled state. Session transforms override + components with their visibility state. Session transforms override global transforms due to mark-based semantics (later marks win). Args: diff --git a/tests/deprecated/server/test_include_exclude_tags.py b/tests/deprecated/server/test_include_exclude_tags.py index 223f70048a..1bdf684324 100644 --- a/tests/deprecated/server/test_include_exclude_tags.py +++ b/tests/deprecated/server/test_include_exclude_tags.py @@ -3,7 +3,7 @@ import pytest from fastmcp import FastMCP -from fastmcp.server.transforms.enabled import Enabled +from fastmcp.server.transforms.visibility import Visibility class TestIncludeExcludeTagsDeprecation: @@ -20,28 +20,28 @@ def test_include_tags_emits_warning(self): FastMCP(include_tags={"public"}) def test_exclude_tags_still_works(self): - """exclude_tags adds an Enabled transform that disables matching tags.""" + """exclude_tags adds a Visibility transform that disables matching tags.""" with pytest.warns(DeprecationWarning): mcp = FastMCP(exclude_tags={"internal"}) - # Should have added an Enabled transform that disables the tag - enabled_transforms = [t for t in mcp._transforms if isinstance(t, Enabled)] + # Should have added a Visibility transform that disables the tag + enabled_transforms = [t for t in mcp._transforms if isinstance(t, Visibility)] assert len(enabled_transforms) == 1 e = enabled_transforms[0] assert e._enabled is False assert e.tags == {"internal"} def test_include_tags_still_works(self): - """include_tags adds Enabled transforms for allowlist mode.""" + """include_tags adds Visibility transforms for allowlist mode.""" with pytest.warns(DeprecationWarning): mcp = FastMCP(include_tags={"public"}) - # Should have added Enabled transforms for allowlist mode + # Should have added Visibility transforms for allowlist mode # (one to disable all, one to enable matching) - enabled_transforms = [t for t in mcp._transforms if isinstance(t, Enabled)] + enabled_transforms = [t for t in mcp._transforms if isinstance(t, Visibility)] assert len(enabled_transforms) == 2 - # First should disable all (Enabled.all(False)) + # First should disable all (Visibility.all(False)) disable_all_transform = enabled_transforms[0] assert disable_all_transform._enabled is False assert disable_all_transform.match_all is True @@ -59,7 +59,7 @@ def test_exclude_and_include_both_create_transforms(self): # Should have added transforms for both # include_tags creates 2 (disable all + enable matching) # exclude_tags creates 1 (disable matching) - enabled_transforms = [t for t in mcp._transforms if isinstance(t, Enabled)] + enabled_transforms = [t for t in mcp._transforms if isinstance(t, Visibility)] assert len(enabled_transforms) == 3 # Check we have both tag rules diff --git a/tests/server/providers/test_local_provider.py b/tests/server/providers/test_local_provider.py index d920333933..2db664338c 100644 --- a/tests/server/providers/test_local_provider.py +++ b/tests/server/providers/test_local_provider.py @@ -336,7 +336,7 @@ def my_tool(x: int) -> int: assert "tool:direct_tool@" in provider._components def test_tool_enabled_false(self): - """Tool with enabled=False should add an Enabled transform.""" + """Tool with enabled=False should add a Visibility transform.""" provider = LocalProvider() @provider.tool(enabled=False) @@ -344,10 +344,12 @@ def disabled_tool() -> str: return "should be disabled" assert "tool:disabled_tool@" in provider._components - # enabled=False adds an Enabled transform to disable the tool - from fastmcp.server.transforms.enabled import Enabled + # enabled=False adds a Visibility transform to disable the tool + from fastmcp.server.transforms.visibility import Visibility - enabled_transforms = [t for t in provider.transforms if isinstance(t, Enabled)] + enabled_transforms = [ + t for t in provider.transforms if isinstance(t, Visibility) + ] assert len(enabled_transforms) == 1 assert enabled_transforms[0]._enabled is False assert enabled_transforms[0].keys == {"tool:disabled_tool@"} @@ -425,7 +427,7 @@ def my_resource() -> str: assert provider._components["resource:resource://test@"].name == "custom_name" def test_resource_enabled_false(self): - """Resource with enabled=False should add an Enabled transform.""" + """Resource with enabled=False should add a Visibility transform.""" provider = LocalProvider() @provider.resource("resource://test", enabled=False) @@ -433,10 +435,12 @@ def disabled_resource() -> str: return "should be disabled" assert "resource:resource://test@" in provider._components - # enabled=False adds an Enabled transform to disable the resource - from fastmcp.server.transforms.enabled import Enabled + # enabled=False adds a Visibility transform to disable the resource + from fastmcp.server.transforms.visibility import Visibility - enabled_transforms = [t for t in provider.transforms if isinstance(t, Enabled)] + enabled_transforms = [ + t for t in provider.transforms if isinstance(t, Visibility) + ] assert len(enabled_transforms) == 1 assert enabled_transforms[0]._enabled is False assert enabled_transforms[0].keys == {"resource:resource://test@"} @@ -461,7 +465,7 @@ def enabled_resource() -> str: assert "resource://disabled" not in uris def test_template_enabled_false(self): - """Template with enabled=False should add an Enabled transform.""" + """Template with enabled=False should add a Visibility transform.""" provider = LocalProvider() @provider.resource("data://{id}", enabled=False) @@ -469,10 +473,12 @@ def disabled_template(id: str) -> str: return f"Data {id}" assert "template:data://{id}@" in provider._components - # enabled=False adds an Enabled transform to disable the template - from fastmcp.server.transforms.enabled import Enabled + # enabled=False adds a Visibility transform to disable the template + from fastmcp.server.transforms.visibility import Visibility - enabled_transforms = [t for t in provider.transforms if isinstance(t, Enabled)] + enabled_transforms = [ + t for t in provider.transforms if isinstance(t, Visibility) + ] assert len(enabled_transforms) == 1 assert enabled_transforms[0]._enabled is False assert enabled_transforms[0].keys == {"template:data://{id}@"} @@ -532,7 +538,7 @@ def my_prompt() -> str: assert "prompt:my_prompt@" not in provider._components def test_prompt_enabled_false(self): - """Prompt with enabled=False should add an Enabled transform.""" + """Prompt with enabled=False should add a Visibility transform.""" provider = LocalProvider() @provider.prompt(enabled=False) @@ -540,10 +546,12 @@ def disabled_prompt() -> str: return "should be disabled" assert "prompt:disabled_prompt@" in provider._components - # enabled=False adds an Enabled transform to disable the prompt - from fastmcp.server.transforms.enabled import Enabled + # enabled=False adds a Visibility transform to disable the prompt + from fastmcp.server.transforms.visibility import Visibility - enabled_transforms = [t for t in provider.transforms if isinstance(t, Enabled)] + enabled_transforms = [ + t for t in provider.transforms if isinstance(t, Visibility) + ] assert len(enabled_transforms) == 1 assert enabled_transforms[0]._enabled is False assert enabled_transforms[0].keys == {"prompt:disabled_prompt@"} diff --git a/tests/server/test_session_visibility.py b/tests/server/test_session_visibility.py index ca072c3f67..ae307dfc68 100644 --- a/tests/server/test_session_visibility.py +++ b/tests/server/test_session_visibility.py @@ -229,7 +229,7 @@ async def enable_v2_only(ctx: Context) -> str: assert not any(t.name == "old_tool" for t in tools) async def test_clear_visibility_rules(self): - """Test that reset_components removes all session rules.""" + """Test that reset_visibility removes all session rules.""" from fastmcp import Client mcp = FastMCP("test") @@ -245,7 +245,7 @@ async def activate_finance(ctx: Context) -> str: @mcp.tool async def clear_rules(ctx: Context) -> str: - await ctx.reset_components() + await ctx.reset_visibility() rules = await ctx._get_visibility_rules() assert len(rules) == 0 return "cleared" @@ -449,14 +449,14 @@ async def deactivate(ctx: Context) -> str: ) async def test_clear_visibility_rules_sends_notifications(self): - """Test that reset_components sends notifications.""" + """Test that reset_visibility sends notifications.""" from fastmcp import Client mcp = FastMCP("test") @mcp.tool async def clear(ctx: Context) -> str: - await ctx.reset_components() + await ctx.reset_visibility() return "cleared" handler = RecordingMessageHandler() diff --git a/tests/server/transforms/test_enabled.py b/tests/server/transforms/test_visibility.py similarity index 78% rename from tests/server/transforms/test_enabled.py rename to tests/server/transforms/test_visibility.py index fca9511d5c..cf7b9f1b7e 100644 --- a/tests/server/transforms/test_enabled.py +++ b/tests/server/transforms/test_visibility.py @@ -1,8 +1,8 @@ -"""Tests for Enabled transform.""" +"""Tests for Visibility transform.""" import pytest -from fastmcp.server.transforms.enabled import Enabled, is_enabled +from fastmcp.server.transforms.visibility import Visibility, is_enabled from fastmcp.tools.tool import Tool from fastmcp.utilities.versions import VersionSpec @@ -12,43 +12,43 @@ class TestMatching: def test_empty_criteria_matches_nothing(self): """Empty criteria is a safe default - matches nothing.""" - t = Enabled(False) + t = Visibility(False) assert t._matches(Tool(name="anything", parameters={})) is False def test_match_all_matches_everything(self): """match_all=True matches all components.""" - t = Enabled(False, match_all=True) + t = Visibility(False, match_all=True) assert t._matches(Tool(name="anything", parameters={})) is True def test_match_by_name(self): """Matches component by name.""" - t = Enabled(False, names={"foo"}) + t = Visibility(False, names={"foo"}) assert t._matches(Tool(name="foo", parameters={})) is True assert t._matches(Tool(name="bar", parameters={})) is False def test_match_by_version(self): """Matches component by version.""" - t = Enabled(False, version=VersionSpec(eq="v1")) + t = Visibility(False, version=VersionSpec(eq="v1")) assert t._matches(Tool(name="foo", version="v1", parameters={})) is True assert t._matches(Tool(name="foo", version="v2", parameters={})) is False def test_match_by_version_spec_exact(self): """VersionSpec(eq="v1") matches v1 only.""" - t = Enabled(False, version=VersionSpec(eq="v1")) + t = Visibility(False, version=VersionSpec(eq="v1")) assert t._matches(Tool(name="foo", version="v1", parameters={})) is True assert t._matches(Tool(name="foo", version="v2", parameters={})) is False assert t._matches(Tool(name="foo", version="v0", parameters={})) is False def test_match_by_version_spec_gte(self): """VersionSpec(gte="v2") matches v2, v3, but not v1.""" - t = Enabled(False, version=VersionSpec(gte="v2")) + t = Visibility(False, version=VersionSpec(gte="v2")) assert t._matches(Tool(name="foo", version="v1", parameters={})) is False assert t._matches(Tool(name="foo", version="v2", parameters={})) is True assert t._matches(Tool(name="foo", version="v3", parameters={})) is True def test_match_by_version_spec_range(self): """VersionSpec(gte="v1", lt="v3") matches v1, v2, but not v3.""" - t = Enabled(False, version=VersionSpec(gte="v1", lt="v3")) + t = Visibility(False, version=VersionSpec(gte="v1", lt="v3")) assert t._matches(Tool(name="foo", version="v0", parameters={})) is False assert t._matches(Tool(name="foo", version="v1", parameters={})) is True assert t._matches(Tool(name="foo", version="v2", parameters={})) is True @@ -57,27 +57,27 @@ def test_match_by_version_spec_range(self): def test_unversioned_does_not_match_version_spec(self): """Unversioned components (version=None) don't match a VersionSpec.""" - t = Enabled(False, version=VersionSpec(eq="v1")) + t = Visibility(False, version=VersionSpec(eq="v1")) assert t._matches(Tool(name="foo", parameters={})) is False - t2 = Enabled(False, version=VersionSpec(gte="v1")) + t2 = Visibility(False, version=VersionSpec(gte="v1")) assert t2._matches(Tool(name="foo", parameters={})) is False def test_match_by_tag(self): """Matches if component has any of the specified tags.""" - t = Enabled(False, tags=set({"internal", "deprecated"})) + t = Visibility(False, tags=set({"internal", "deprecated"})) assert t._matches(Tool(name="foo", parameters={}, tags={"internal"})) is True assert t._matches(Tool(name="foo", parameters={}, tags={"public"})) is False def test_match_by_component_type(self): """Only matches specified component types.""" - t = Enabled(False, names={"foo"}, components={"prompt"}) + t = Visibility(False, names={"foo"}, components={"prompt"}) # Tool has key "tool:foo@", not "prompt:foo@" assert t._matches(Tool(name="foo", parameters={})) is False def test_all_criteria_must_match(self): """Multiple criteria use AND logic - all must match.""" - t = Enabled( + t = Visibility( False, names={"foo"}, version=VersionSpec(eq="v1"), @@ -96,26 +96,26 @@ def test_all_criteria_must_match(self): class TestMarking: - """Test enabled state marking.""" + """Test visibility state marking.""" def test_disable_marks_as_disabled(self): - """Enabled(False, ...) marks matching components as disabled.""" + """Visibility(False, ...) marks matching components as disabled.""" tool = Tool(name="foo", parameters={}) - Enabled(False, names={"foo"})._mark_component(tool) + Visibility(False, names={"foo"})._mark_component(tool) assert is_enabled(tool) is False def test_enable_marks_as_enabled(self): - """Enabled(True, ...) marks matching components as enabled.""" + """Visibility(True, ...) marks matching components as enabled.""" tool = Tool(name="foo", parameters={}) - Enabled(True, names={"foo"})._mark_component(tool) + Visibility(True, names={"foo"})._mark_component(tool) assert is_enabled(tool) is True assert tool.meta is not None - assert tool.meta["fastmcp"]["_internal"]["enabled"] is True + assert tool.meta["fastmcp"]["_internal"]["visibility"] is True def test_non_matching_unchanged(self): """Non-matching components are not modified.""" tool = Tool(name="bar", parameters={}) - Enabled(False, names={"foo"})._mark_component(tool) + Visibility(False, names={"foo"})._mark_component(tool) # No _internal key added assert tool.meta is None or "_internal" not in tool.meta.get("fastmcp", {}) assert is_enabled(tool) is True @@ -123,13 +123,13 @@ def test_non_matching_unchanged(self): def test_mutates_in_place(self): """Marking mutates the component in place.""" tool = Tool(name="foo", parameters={}) - result = Enabled(False, names={"foo"})._mark_component(tool) + result = Visibility(False, names={"foo"})._mark_component(tool) assert result is tool def test_disable_all(self): """match_all=True disables all components.""" tool = Tool(name="anything", parameters={}) - Enabled(False, match_all=True)._mark_component(tool) + Visibility(False, match_all=True)._mark_component(tool) assert is_enabled(tool) is False @@ -139,19 +139,19 @@ class TestOverride: def test_enable_overrides_disable(self): """An enable after disable results in enabled.""" tool = Tool(name="foo", parameters={}) - Enabled(False, names={"foo"})._mark_component(tool) + Visibility(False, names={"foo"})._mark_component(tool) assert is_enabled(tool) is False - Enabled(True, names={"foo"})._mark_component(tool) + Visibility(True, names={"foo"})._mark_component(tool) assert is_enabled(tool) is True def test_disable_overrides_enable(self): """A disable after enable results in disabled.""" tool = Tool(name="foo", parameters={}) - Enabled(True, names={"foo"})._mark_component(tool) + Visibility(True, names={"foo"})._mark_component(tool) assert is_enabled(tool) is True - Enabled(False, names={"foo"})._mark_component(tool) + Visibility(False, names={"foo"})._mark_component(tool) assert is_enabled(tool) is False @@ -169,7 +169,7 @@ def test_filtering_pattern(self): Tool(name="enabled", parameters={}), Tool(name="disabled", parameters={}), ] - Enabled(False, names={"disabled"})._mark_component(tools[1]) + Visibility(False, names={"disabled"})._mark_component(tools[1]) visible = [t for t in tools if is_enabled(t)] assert [t.name for t in visible] == ["enabled"] @@ -181,7 +181,7 @@ class TestMetadata: def test_internal_metadata_stripped_by_get_meta(self): """Internal metadata is stripped when calling get_meta().""" tool = Tool(name="foo", parameters={}) - Enabled(True, names={"foo"})._mark_component(tool) + Visibility(True, names={"foo"})._mark_component(tool) # Raw meta has _internal assert tool.meta is not None @@ -194,7 +194,7 @@ def test_internal_metadata_stripped_by_get_meta(self): def test_user_metadata_preserved(self): """User-provided metadata is not affected.""" tool = Tool(name="foo", parameters={}, meta={"custom": "value"}) - marked = Enabled(False, names={"foo"})._mark_component(tool) + marked = Visibility(False, names={"foo"})._mark_component(tool) assert marked.meta is not None assert marked.meta["custom"] == "value" @@ -205,24 +205,24 @@ class TestRepr: def test_repr_disable(self): """Repr shows disable action and criteria.""" - t = Enabled(False, names={"foo"}) + t = Visibility(False, names={"foo"}) r = repr(t) assert "disable" in r assert "foo" in r def test_repr_enable(self): """Repr shows enable action.""" - t = Enabled(True, names={"foo"}) + t = Visibility(True, names={"foo"}) assert "enable" in repr(t) def test_repr_match_all(self): """Repr shows match_all.""" - t = Enabled(False, match_all=True) + t = Visibility(False, match_all=True) assert "match_all=True" in repr(t) class TestTransformChain: - """Test Enabled in async transform chains.""" + """Test Visibility in async transform chains.""" @pytest.fixture def tools(self): @@ -234,7 +234,7 @@ def tools(self): async def test_list_tools_marks_matching(self, tools): """list_tools applies marks to matching components.""" - disable_internal = Enabled(False, tags=set({"internal"})) + disable_internal = Visibility(False, tags=set({"internal"})) result = await disable_internal.list_tools(tools) @@ -245,8 +245,8 @@ async def test_list_tools_marks_matching(self, tools): async def test_later_transform_overrides(self, tools): """Later transforms in chain override earlier ones.""" - disable_internal = Enabled(False, tags=set({"internal"})) - enable_safe = Enabled(True, tags=set({"safe"})) + disable_internal = Visibility(False, tags=set({"internal"})) + enable_safe = Visibility(True, tags=set({"safe"})) # Apply transforms sequentially after_disable = await disable_internal.list_tools(tools) @@ -260,8 +260,8 @@ async def test_later_transform_overrides(self, tools): async def test_allowlist_pattern(self, tools): """Disable all, then enable specific = allowlist.""" - disable_all = Enabled(False, match_all=True) - enable_public = Enabled(True, tags=set({"public"})) + disable_all = Visibility(False, match_all=True) + enable_public = Visibility(True, tags=set({"public"})) # Apply transforms sequentially after_disable = await disable_all.list_tools(tools) From 3ea9289ea7eb24ab8ac5e36c270e8c0f842d38cf Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 19 Jan 2026 20:19:51 -0500 Subject: [PATCH 2/3] Update v3-features doc to use Visibility terminology --- docs/development/v3-notes/v3-features.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/development/v3-notes/v3-features.mdx index ce31447a09..19930109c0 100644 --- a/docs/development/v3-notes/v3-features.mdx +++ b/docs/development/v3-notes/v3-features.mdx @@ -23,7 +23,7 @@ class Provider: Providers support: - **Lifecycle management**: `async def lifespan()` for setup/teardown -- **Enabled control**: `enable()` / `disable()` with name, version, tags, components, and allowlist mode +- **Visibility control**: `enable()` / `disable()` with name, version, tags, components, and allowlist mode - **Transform stacking**: `provider.add_transform(Namespace(...))`, `provider.add_transform(ToolTransform(...))` ### LocalProvider @@ -232,7 +232,7 @@ Documentation: `docs/servers/context.mdx` --- -## Enabled System +## Visibility System Components can be enabled/disabled using the visibility system. Each `enable()` or `disable()` call adds a stateless Visibility transform that marks components via internal metadata. Later transforms override earlier ones. @@ -261,7 +261,7 @@ Works at both server and provider level. Supports: - **Allowlist mode** (`only=True`): Only explicitly enabled components visible - **Tag-based filtering**: Enable/disable groups of components by tag - **Override semantics**: Later transforms override earlier marks (enable after disable = enabled) -- **Transform ordering**: Enabled transforms are injected at the point you call them, so component state is known +- **Transform ordering**: Visibility transforms are injected at the point you call them, so component state is known ### Per-Session Visibility From 081223fdf13a678943507d4eeb73c85770fff6a7 Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 19 Jan 2026 20:43:58 -0500 Subject: [PATCH 3/3] Fix missed reset_components reference in docs --- docs/development/v3-notes/v3-features.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/v3-notes/v3-features.mdx b/docs/development/v3-notes/v3-features.mdx index 19930109c0..be92595eef 100644 --- a/docs/development/v3-notes/v3-features.mdx +++ b/docs/development/v3-notes/v3-features.mdx @@ -296,7 +296,7 @@ mcp.disable(tags={"premium"}) Session visibility methods: - `await ctx.enable_components(...)`: Enable components for this session - `await ctx.disable_components(...)`: Disable components for this session -- `await ctx.reset_components()`: Clear session rules, return to global defaults +- `await ctx.reset_visibility()`: Clear session rules, return to global defaults Session rules override global transforms. FastMCP automatically sends `ToolListChangedNotification` (and resource/prompt equivalents) to affected sessions when visibility changes.