mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-24 06:24:18 +02:00
* Consolidate tool transformation logic into TransformingProvider
Tool transformations were previously scattered across LocalProvider,
ProxyProvider, and MCPConfig. This consolidates all transformation
logic into TransformingProvider via with_transforms(tool_transforms={...}).
- Add tool_transforms parameter to TransformingProvider
- Add tool_transforms to Provider.with_transforms()
- Remove transformation storage from LocalProvider and ProxyProvider
- Remove add_tool_transformation() and remove_tool_transformation() from FastMCP
- Add tool_transforms parameter to factory methods (from_openapi, from_fastapi, create_proxy)
- Update tests to use new patterns
* Fix: reject tool lookups by pre-transform name
* Add collision validation for tool_transforms and fix docstring examples
- Validate duplicate target names in tool_transforms raise ValueError
- Fix docstring examples to use arguments/ArgTransformConfig (not args/ArgTransform)
- Add test for collision validation
* Add server-level tool transform APIs and fix task registration
- Add AggregateProvider to present multiple providers as one
- Add _get_root_provider() to apply server-level transforms uniformly
- Fix _docket_lifespan to use root provider (ensures renamed tools
register with correct keys for background execution)
- Add tool_transforms kwarg to __init__ (non-deprecated)
- Add add_tool_transform(), remove_tool_transform(), tool_transforms property
- Deprecate old API names (tool_transformations, add_tool_transformation, etc.)
- Update tests to use new API
* Add graceful degradation for provider errors in AggregateProvider
* Match original behavior: parallel queries with DEBUG logging
* Refactor transforms to middleware-style call_next pattern
Replaces the ad-hoc transformation system with a unified Transform
abstraction using the same call_next pattern as server middleware.
Key changes:
- New src/fastmcp/server/transforms/ module with Transform base class
- Namespace, ToolTransform, Visibility all implement the same interface
- Transforms compose via functools.partial chain building
- Visibility is now just the first transform in provider._transforms
- Server-level transforms apply after provider aggregation
- Task registration now applies full transform chain
Removes TransformingProvider, _BoundTransform, ComponentSource protocol.
User-facing API unchanged: mount(), add_transform(), enable/disable all
work as before.
* Add comprehensive transforms and visibility documentation
New docs/servers/providers/transforms.mdx covering:
- Mental model for middleware-style transform pattern
- Built-in transforms (Namespace, ToolTransform)
- Server vs provider-level transforms and ordering
- Tool modification (immediate vs deferred)
- Custom transform creation
New docs/servers/visibility.mdx covering:
- Enable/disable API for runtime visibility control
- Keys and tags for targeting components
- Allowlist mode with only=True
- Server vs provider visibility layering
Updates existing docs to reference new pages and simplifies
redundant content. Visibility is documented as a user feature,
not as an implementation detail.
* Restructure transforms docs and delete tool-transformation pattern
* Cleanup: simplify get_tasks and remove unused Provider.get_component
* Update loq
* Update loq limits and add loq note to AGENTS.md
* Deprecate add_tool_transformation and tool_transformations param
* Address PR review feedback: remove redundant imports, fix path reference
* Add missing imports to code examples in v3-features.mdx
315 lines
8.3 KiB
Text
315 lines
8.3 KiB
Text
---
|
|
title: Visibility
|
|
sidebarTitle: Visibility
|
|
description: Control which components are visible to clients
|
|
icon: eye
|
|
---
|
|
|
|
import { VersionBadge } from '/snippets/version-badge.mdx'
|
|
|
|
<VersionBadge 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.
|
|
|
|
```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"
|
|
|
|
# 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.
|
|
|
|
```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 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.
|
|
|
|
```python
|
|
# 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.
|
|
|
|
```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 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
|
|
|
|
```python
|
|
# 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.
|
|
|
|
```python
|
|
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.
|
|
|
|
```python
|
|
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.
|
|
|
|
```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"
|
|
|
|
# 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.
|
|
|
|
```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 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.
|
|
|
|
```python
|
|
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"
|
|
```
|
|
|
|
<Warning>
|
|
Dynamic visibility affects all connected clients. For per-user visibility, consider using separate server instances or implementing authorization in the tools themselves.
|
|
</Warning>
|
|
|
|
## 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.
|
|
|
|
```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 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.
|