Publish FastMCP 4.0.0b1 docs to gofastmcp.com (#4695)

This commit is contained in:
Jeremiah Lowin 2026-07-28 17:26:15 -04:00 committed by GitHub
commit 0747ce0bc1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
220 changed files with 12093 additions and 8314 deletions

View file

@ -0,0 +1,73 @@
---
title: Auth Provider Environment Variables
---
## Decision: Remove automatic environment variable loading from auth providers
You can still use environment variables for configuration - you just read them yourself with `os.environ` instead of relying on FastMCP's automatic loading.
**Status:** Implemented in v3.0.0
### Background
Auth providers in v2.x used `pydantic-settings` to automatically load configuration from environment variables with a `FASTMCP_SERVER_AUTH_<PROVIDER>_` prefix. For example, `GitHubProvider` would read from:
- `FASTMCP_SERVER_AUTH_GITHUB_CLIENT_ID`
- `FASTMCP_SERVER_AUTH_GITHUB_CLIENT_SECRET`
- `FASTMCP_SERVER_AUTH_GITHUB_BASE_URL`
- etc.
This was implemented via a `*ProviderSettings(BaseSettings)` class in each provider, combined with a `NotSet` sentinel pattern to distinguish between "not provided" and `None`.
### Why remove it
1. **Maintenance burden**: Every new provider needed to implement the settings class, validators, and the `NotSet` merging logic. This was ~50-100 lines of boilerplate per provider.
2. **Documentation complexity**: Each provider needed documentation explaining both the parameter and the corresponding environment variable. This doubled the surface area to document and maintain.
3. **Contributor friction**: New contributors adding providers had to understand and replicate this pattern, which was a source of inconsistency and bugs.
4. **Marginal user value**: Python developers are comfortable with `os.environ["VAR"]` or `os.environ.get("VAR", default)`. The automatic loading saved a single line of code per parameter while adding significant complexity.
5. **Implicit behavior**: Magic environment variable loading makes it harder to understand where values come from. Explicit `os.environ` calls are more traceable.
### Migration path
The migration is trivial - users add explicit environment variable reads:
```python
# Before (v2.x)
auth = GitHubProvider() # Relied on env vars
# After (v3.0)
import os
auth = GitHubProvider(
client_id=os.environ["GITHUB_CLIENT_ID"],
client_secret=os.environ["GITHUB_CLIENT_SECRET"],
base_url=os.environ["MY_BASE_URL"],
)
```
Users can also use `os.environ.get()` with defaults, or any other configuration library they prefer (dotenv, dynaconf, etc.).
### Backwards compatibility
We chose not to provide backwards compatibility because:
1. This is a major version bump (v3.0), which is the appropriate time for breaking changes
2. The migration is straightforward (add `os.environ` calls)
3. Maintaining compatibility would require keeping all the boilerplate we're trying to remove
4. The pattern was likely not heavily used - most production deployments pass secrets explicitly rather than relying on magic prefixes
### What was removed
- `*ProviderSettings(BaseSettings)` classes from all auth providers
- `NotSet` sentinel usage in provider constructors
- `pydantic-settings` dependency for auth providers
- Environment variable documentation from provider docs
- Related test cases for env var loading
### Result
Provider constructors are now simple and explicit. Required parameters are actually required (Python raises `TypeError` if missing), and optional parameters have clear defaults. The code is more readable and easier to maintain.

View file

@ -0,0 +1,61 @@
# Consolidating Discovery Methods
This document captures the design decisions around component listing methods in FastMCP 3.0.
## Problem
The server had parallel implementations for listing components:
- `get_tools()` / `_list_tools()`
- `get_resources()` / `_list_resources()`
- `get_prompts()` / `_list_prompts()`
- `get_resource_templates()` / `_list_resource_templates()`
These were nearly identical but with subtle differences in dedup keys, logging, and return types. The `_list_*` methods were internal and used by the MCP protocol handlers, while `get_*` methods were the public API.
## Solution
The duplicate methods were consolidated into a single set of `list_*` methods. The old `get_*` plural methods and `_list_*` internal methods were both removed.
This happened in two phases:
1. **Consolidation** (Dec 2025): Merged `get_*` and `_list_*` into a single `get_*` method with an `apply_middleware` parameter.
2. **Rename** (Jan 2026): When `FastMCP` was refactored to inherit from `Provider`, the methods were renamed to `list_*` to align with the `Provider` interface. The `apply_middleware` parameter was renamed to `run_middleware` with a default of `True`.
```python
async def list_tools(self, *, run_middleware: bool = True) -> Sequence[Tool]:
"""Canonical method for listing tools."""
...
```
## Key Changes
### Return Type: dict → list
The dict return type was removed because the key was redundant—components already have `.name` or `.uri` attributes.
```python
# Before (v2.x)
tools = await server.get_tools()
tool = tools["my_tool"]
# After (v3.0)
tools = await server.list_tools()
tool = next(t for t in tools if t.name == "my_tool")
```
### Middleware via Parameter
The `run_middleware=True` parameter (default) applies the middleware chain. This replaces the separate `_list_*_middleware()` methods.
## Benefits
1. **Single source of truth** - One method, not two
2. **Consistent behavior** - Same dedup key, same visibility filtering
3. **Clearer API** - Public method with explicit middleware opt-in
4. **Provider alignment** - `FastMCP.list_tools()` overrides `Provider.list_tools()`
5. **Less code** - Deleted ~200 lines of duplicate implementation
## Implementation Files
- `src/fastmcp/server/server.py` - Canonical `list_*` methods
- `src/fastmcp/server/providers/` - Provider base class defines the interface

View file

@ -0,0 +1,121 @@
# Prompt Internal Types - Message and PromptResult
**Version:** 3.0.0
**Impact:** Breaking change for prompts returning `mcp.types.PromptMessage`
## Summary
Prompts now use FastMCP's `Message` and `PromptResult` types internally, following the same pattern as resources (#2734). MCP SDK types are only used at the protocol boundary.
## What Changed
### Before (v2.x)
```python
from mcp.types import PromptMessage, TextContent
@mcp.prompt
def my_prompt() -> PromptMessage:
return PromptMessage(
role="user",
content=TextContent(type="text", text="Hello")
)
```
### After (v3.0)
```python
from fastmcp.prompts import Message
@mcp.prompt
def my_prompt() -> Message:
return Message("Hello") # role defaults to "user"
```
## Type Constraints
### Prompt Function Return Types
```python
str | list[Message | str] | PromptResult
```
**Valid:**
- `return "Hello"` → wrapped as single user Message
- `return [Message("Hi"), Message("Response", role="assistant")]`
- `return ["Hi", "Response"]` → strings auto-wrapped as user Messages
- `return PromptResult(messages=[...], meta={...})`
**Invalid (now raises error):**
- `return PromptMessage(...)` → Use `Message` instead
- `return Message(...)` as single value → Use `PromptResult([Message(...)])` or return a list
### Message Class
```python
Message(
content: Any, # Auto-serializes non-str to JSON
role: Literal["user", "assistant"] = "user"
)
```
**Auto-Serialization:**
- `str` → passes through as TextContent
- `dict` → JSON-serialized to text
- `list` → JSON-serialized to text
- `BaseModel` → JSON-serialized to text
- `TextContent` / `EmbeddedResource` → passes through directly
### PromptResult Class
```python
PromptResult(
messages: str | list[Message], # str wrapped as single Message
description: str | None = None,
meta: dict[str, Any] | None = None
)
```
## Why This Change?
1. **Simpler API** - `Message("Hello")` vs `PromptMessage(role="user", content=TextContent(type="text", text="Hello"))`
2. **Auto-serialization** - Dicts/lists/models automatically become JSON
3. **Consistent with resources** - Same pattern as `ResourceContent`/`ResourceResult`
4. **Type safety** - Strict typing catches errors at development time
## Migration Guide
### Simple Message
```python
# Before
from mcp.types import PromptMessage, TextContent
return PromptMessage(role="user", content=TextContent(type="text", text="Hello"))
# After
from fastmcp.prompts import Message
return Message("Hello")
```
### Conversation
```python
# Before
return [
PromptMessage(role="user", content=TextContent(type="text", text="Hi")),
PromptMessage(role="assistant", content=TextContent(type="text", text="Hello!")),
]
# After
return [
Message("Hi"),
Message("Hello!", role="assistant"),
]
```
### With Metadata
```python
from fastmcp.prompts import Message, PromptResult
return PromptResult(
messages=[Message("Analyze this")],
meta={"priority": "high"}
)
```
## PR
- #2738 - Introduce Message and PromptResult as canonical prompt types

View file

@ -0,0 +1,116 @@
# Provider Architecture: FastMCPProvider + TransformingProvider
**Version:** 3.0.0
**Impact:** Breaking change - `MountedProvider` removed
## Summary
The monolithic `MountedProvider` was split into two focused, composable components:
- **`FastMCPProvider`**: Wraps a FastMCP server, exposing its components through the Provider interface
- **`TransformingProvider`**: Wraps any provider to apply namespace prefixes and tool renames
## Why the Split?
`MountedProvider` was doing two things:
1. Wrapping a FastMCP server as a provider
2. Transforming component names with prefixes
Separating these concerns enables:
- Reusing transformations on any provider (not just FastMCP servers)
- Stacking transformations via composition
- Clearer mental model
## New API
### FastMCPProvider
Wraps a FastMCP server to expose it through the Provider interface:
```python
from fastmcp.server.providers import FastMCPProvider
sub_server = FastMCP("Sub")
@sub_server.tool
def greet(name: str) -> str:
return f"Hello, {name}!"
# Wrap as provider
provider = FastMCPProvider(sub_server)
main_server.add_provider(provider)
```
### TransformingProvider
Wraps any provider to apply transformations:
```python
# Apply namespace to all components
provider = FastMCPProvider(server).with_namespace("api")
# "my_tool" → "api_my_tool"
# "resource://data" → "resource://api/data"
# Rename specific tools (bypasses namespace)
provider = FastMCPProvider(server).with_transforms(
namespace="api",
tool_renames={"verbose_tool_name": "short"}
)
# "verbose_tool_name" → "short"
# "other_tool" → "api_other_tool"
```
### Stacking Transformations
Transformations compose via stacking:
```python
provider = (
FastMCPProvider(server)
.with_namespace("inner")
.with_namespace("outer")
)
# "tool" → "outer_inner_tool"
```
## mount() Uses This Internally
`FastMCP.mount()` now creates a `FastMCPProvider` + `TransformingProvider` internally:
```python
main.mount(sub, namespace="api")
# Equivalent to:
main.add_provider(
FastMCPProvider(sub).with_namespace("api")
)
```
## Breaking Changes
### MountedProvider Removed
```python
# Before (2.x)
from fastmcp.server.providers import MountedProvider
provider = MountedProvider(server, prefix="api")
# After (3.x)
from fastmcp.server.providers import FastMCPProvider
provider = FastMCPProvider(server).with_namespace("api")
```
### prefix → namespace
```python
# Before (deprecated)
main.mount(sub, prefix="api")
# After
main.mount(sub, namespace="api")
```
## Implementation PRs
- #2653 - Split MountedProvider into FastMCPProvider + TransformingProvider
- #2635 - Initial MountedProvider (superseded by #2653)

View file

@ -0,0 +1,60 @@
# Provider Tests: Direct Server Calls
This document captures the design decision to test providers via direct server method calls rather than wrapping in a Client.
## Problem
Provider tests were using the Client pattern:
```python
async with Client(mcp) as client:
result = await client.call_tool("add", {"x": 1, "y": 2})
assert result.data == 3
```
This conflated two concerns:
1. Does the provider/server work correctly?
2. Does the Client-Server interaction work correctly?
Additionally, ~1,200 lines of tests in `test_server_interactions.py` duplicated provider tests.
## Solution
Provider tests now call server methods directly:
```python
result = await mcp.call_tool("add", {"x": 1, "y": 2})
assert result.structured_content == {"result": 3}
```
This establishes clear test ownership:
- **Provider tests** → verify server functionality
- **Integration tests** → verify Client-Server interaction
## Result Access Patterns
Direct server calls return canonical FastMCP types, not MCP protocol types:
| Component | Access Pattern |
|-----------|----------------|
| Tool | `result.structured_content` or `result.text` |
| Resource | `result.contents[0].content` |
| Prompt | `result.messages[0].content.text` |
## Error Types
Direct calls raise FastMCP exceptions:
- `NotFoundError` - component not found
- `DisabledError` - component disabled by visibility
Client calls raise MCP protocol errors (wrapped in `McpError`).
## Implementation
- Consolidated duplicate tests from `test_server_interactions.py` into provider test files
- Reduced `test_server_interactions.py` from 1,455 → 179 lines
- Only `TestMeta` tests remain in interactions file (require Client for context injection)
## PR
- #2748 - Convert provider tests to use direct server calls

View file

@ -0,0 +1,196 @@
# Resource Internal Types - Strict Typing for Type Safety
**Version:** 3.0.0
**Impact:** Breaking change for resources returning dict/list
## Summary
ResourceResult now enforces strict typing to catch errors at development time (via type checker) rather than at runtime (when a client reads a resource).
## What Changed
### Before (v2.x)
```python
@mcp.resource("data://config")
def get_config() -> dict: # Auto-serialized to JSON
return {"key": "value"}
@mcp.resource("data://items")
def get_items() -> list: # Each item auto-wrapped
return ["item1", "item2"]
ResourceResult({"key": "value"}) # Dict auto-converted
ResourceResult(["a", "b"]) # List split into items
```
### After (v3.0)
```python
@mcp.resource("data://config")
def get_config() -> str: # Explicit JSON serialization
import json
return json.dumps({"key": "value"})
@mcp.resource("data://items")
def get_items() -> ResourceResult: # Explicit multi-item response
return ResourceResult([
ResourceContent("item1"),
ResourceContent("item2"),
])
ResourceResult([ResourceContent(...)]) # Explicit list wrapping
# Dict/list raises TypeError
```
## Type Constraints
### Resource.read() Return Type
```python
str | bytes | ResourceResult
```
**Valid:**
- `return "text content"`
- `return b"binary data"`
- `return ResourceResult([ResourceContent(...)])`
**Invalid (now raises TypeError):**
- `return {"key": "value"}` → Use `json.dumps()` instead
- `return ["item1", "item2"]` → Use `ResourceResult([ResourceContent(...)])`
- `return ResourceContent(...)` → Use `ResourceResult([ResourceContent(...)])`
### ResourceResult Type Signature
```python
ResourceResult(
contents: str | bytes | list[ResourceContent],
meta: dict[str, Any] | None = None
)
```
**Valid:**
- `ResourceResult("plain text")`
- `ResourceResult(b"binary")`
- `ResourceResult([ResourceContent(...), ResourceContent(...)])`
**Invalid (now raises TypeError):**
- `ResourceResult({"key": "value"})` → Dict not supported
- `ResourceResult(["a", "b"])` → Bare list not supported (must be list[ResourceContent])
- `ResourceResult(resource_content_obj)` → Single item must be in list
### ResourceContent Type Signature
```python
ResourceContent(
content: Any, # Auto-serializes non-str/bytes to JSON
mime_type: str | None = None,
meta: dict[str, Any] | None = None
)
```
**Auto-Serialization in ResourceContent.__init__:**
- `str` → passes through (mime_type defaults to "text/plain")
- `bytes` → passes through (mime_type defaults to "application/octet-stream")
- `dict` → JSON-serialized string (mime_type defaults to "application/json")
- `list` → JSON-serialized string (mime_type defaults to "application/json")
- `BaseModel` → JSON-serialized string (mime_type defaults to "application/json")
## Why This Change?
The old auto-conversion behavior was convenient but hid errors:
```python
# Old behavior - silent failure
return ["item1", "item2"] # Client sees 2 items OR JSON array?
# Ambiguous! Users would discover issues only when client reads resource
# New behavior - caught at dev time
return ["item1", "item2"] # Type checker error immediately
# Must explicitly write:
return json.dumps(["item1", "item2"]) # Clear intent
# OR:
return ResourceResult([ResourceContent("item1"), ResourceContent("item2")])
```
Type checkers now catch return type mismatches during development rather than at runtime.
## Migration Guide
### Returning JSON Data
**Before:**
```python
def get_config() -> dict:
return {"key": "value", "nested": {"a": 1}}
```
**After:**
```python
import json
def get_config() -> str:
return json.dumps({"key": "value", "nested": {"a": 1}})
```
### Returning Multiple Items
**Before:**
```python
def get_items() -> list:
return ["user1", "user2", "user3"]
```
**After (Option 1: Single JSON array):**
```python
import json
def get_items() -> str:
return json.dumps(["user1", "user2", "user3"])
```
**After (Option 2: Multiple content items):**
```python
from fastmcp.resources import ResourceContent, ResourceResult
def get_items() -> ResourceResult:
return ResourceResult([
ResourceContent("user1"),
ResourceContent("user2"),
ResourceContent("user3"),
])
```
### Returning Structured Data with Custom MIME Types
**Before:**
```python
def get_html() -> dict:
return {"html": "<div>content</div>"}
```
**After:**
```python
from fastmcp.resources import ResourceContent, ResourceResult
def get_html() -> ResourceResult:
return ResourceResult([
ResourceContent(
content="<div>content</div>",
mime_type="text/html"
)
])
```
## Type Checking
Your type checker will now catch these errors:
```python
@mcp.resource("data://test")
def bad_resource() -> dict: # ← Type error: should be str | bytes | ResourceResult
return {"key": "value"}
```
This is intentional. The type system enforces correct typing at development time.
## Backward Compatibility
**This is a breaking change.** Code that returns dict or list from resources will:
1. **Pass type checking**: If you ignore type warnings
2. **Fail at runtime**: Raises `TypeError` when client reads the resource
Migrate to explicit JSON serialization or ResourceResult.

View file

@ -0,0 +1,109 @@
# Explicit task_meta Parameter for Background Tasks
This document captures the design decision to add explicit `task_meta` parameters to component execution methods, replacing context variable-based task routing.
## Problem
Background task execution used context variables (`_task_metadata`, `_docket_fn_key`) to pass task metadata through the call stack. This was implicit and had several issues:
1. **Hidden state** - Task metadata flowed through context vars, making it hard to trace
2. **Fragile enrichment** - `fn_key` was enriched in 9 different places (component methods + provider wrappers)
3. **Testing difficulty** - Required setting context vars to test background behavior
4. **No programmatic API** - Users couldn't explicitly request background execution via `call_tool()`
## Solution
Add explicit `task_meta: TaskMeta | None` parameters to:
- `FastMCP.call_tool()`, `FastMCP.read_resource()`, `FastMCP.render_prompt()`
- Component methods: `Tool._run()`, `Resource._read()`, `Prompt._render()`, `ResourceTemplate._read()`
```python
from fastmcp.server.tasks import TaskMeta
# Explicit background execution
result = await server.call_tool("my_tool", {"arg": "value"}, task_meta=TaskMeta(ttl=300))
# Returns CreateTaskResult for background, ToolResult for sync
```
## fn_key Enrichment Centralization
Previously, `fn_key` (the Docket registry key) was set in 9 places:
**Component methods (5):**
- `Tool._run()`
- `Resource._read()`
- `ResourceTemplate._read()` (2 places)
- `Prompt._render()`
**Provider wrappers (4):**
- `FastMCPProviderTool._run()`
- `FastMCPProviderResource._read()`
- `FastMCPProviderPrompt._render()`
- `FastMCPProviderResourceTemplate._read()`
Now, `fn_key` is set in **3 places** (server methods only):
```python
# In call_tool(), after finding the tool:
if task_meta is not None and task_meta.fn_key is None:
task_meta = replace(task_meta, fn_key=tool.key)
# In read_resource(), after finding resource or template:
if task_meta is not None and task_meta.fn_key is None:
task_meta = replace(task_meta, fn_key=resource.key) # or template.key
# In render_prompt(), after finding the prompt:
if task_meta is not None and task_meta.fn_key is None:
task_meta = replace(task_meta, fn_key=prompt.key)
```
## Why This Works for Mounted Servers
For mounted servers, `provider.get_tool(name)` returns a `FastMCPProviderTool` whose `.key` is already namespaced (e.g., `"tool:child_multiply"`). So setting `fn_key = tool.key` in the parent server gives the correct namespaced key.
When the provider wrapper delegates to the child server, `fn_key` is already set, so the child server won't override it.
## Type-Safe Overloads
Each method uses `@overload` to provide correct return types:
```python
@overload
async def call_tool(
self, name: str, arguments: dict[str, Any], *, task_meta: None = None
) -> ToolResult: ...
@overload
async def call_tool(
self, name: str, arguments: dict[str, Any], *, task_meta: TaskMeta
) -> ToolResult | mcp.types.CreateTaskResult: ...
```
## Middleware Runs Before Docket
A key fix from #2663: background tasks now properly pass through all middleware stacks before being submitted to Docket. Previously, background task submission bypassed middleware entirely.
The flow is now:
1. MCP handler extracts task metadata from request
2. Server method (`call_tool`, etc.) finds component via provider
3. Server enriches `task_meta.fn_key` with component key
4. Component's `_run()`/`_read()`/`_render()` is called
5. Middleware runs (logging, auth, rate limiting, etc.)
6. `check_background_task()` submits to Docket if task_meta present
For mounted servers, the wrapper components delegate to the child server, which runs the child's middleware before the actual execution or Docket submission.
## Removed Dead Code
- `_task_metadata` context variable
- `_docket_fn_key` context variable
- `get_task_metadata()` function
- `key` parameter in `check_background_task()` (backwards compat fallback)
## Implementation PRs
- #2663 - Components own execution; middleware runs before Docket
- #2749 - `task_meta` for `call_tool()`
- #2750 - `task_meta` for `read_resource()`
- #2751 - `task_meta` for `render_prompt()` + fn_key centralization

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,113 @@
# Visibility & Enable/Disable Design
This document captures the design decisions for the enable/disable system in FastMCP 3.0.
## Core Principle
**Components describe capabilities. Servers and providers control availability.**
Previously, each component had an `enabled` field that users could mutate directly. This caused a fundamental problem: when components pass through providers (especially TransformingProvider), you receive copies—and mutating a copy doesn't affect the original.
## Solution: Hierarchical Visibility
Both servers and providers maintain their own `VisibilityFilter`. If a component is disabled at any level, it's disabled up the chain.
```
Provider A (filters) → Provider B (filters) → Server (filters) → Client sees only enabled components
```
## VisibilityFilter
The `VisibilityFilter` class (`src/fastmcp/utilities/visibility.py`) provides:
### Blocklist (disable)
```python
server.disable(keys=["tool:my_tool"]) # Hide specific component
server.disable(tags={"internal"}) # Hide all components with tag
```
### Allowlist (enable with only=True)
```python
server.enable(tags={"public"}, only=True) # Show ONLY components with tag
```
### Blocklist Wins
If a component is in both blocklist and allowlist, blocklist wins. This ensures you can always hide something regardless of other filters.
### Change Detection
The `VisibilityFilter` only sends notifications when visibility actually changes:
- Disabling an already-disabled component: no notification
- Enabling an already-enabled component: no notification
- Actual state change: notification sent
## Vocabulary
Consistent verbs throughout the codebase:
- `enable()` / `disable()` - methods on servers and providers
- `is_enabled()` - check if component is visible
- `_disabled_keys`, `_disabled_tags` - blocklist state
- `_enabled_keys`, `_enabled_tags` - allowlist state
- `_default_enabled` - True unless `only=True` was used
## Notifications
`VisibilityFilter` handles notifications directly via `_send_notification()`. This:
1. Gets the current request context (if any)
2. Queues the appropriate list-changed notification
3. No-ops gracefully outside request context
This simplifies the code—no callback wiring needed between VisibilityFilter and its owners.
## Migration from 2.x
### Component enable/disable removed
```python
# Before (2.x) - BROKEN: mutates a copy
tool.disable()
# After (3.x)
server.disable(keys=["tool:my_tool"])
```
### enabled field removed
```python
# Before (2.x)
@mcp.tool(enabled=False)
def my_tool(): ...
# After (3.x)
@mcp.tool
def my_tool(): ...
mcp.disable(keys=["tool:my_tool"])
```
### include_tags/exclude_tags deprecated
```python
# Before (deprecated)
mcp = FastMCP("server", exclude_tags={"internal"})
# After
mcp = FastMCP("server")
mcp.disable(tags={"internal"})
```
## Component Keys
Components use prefixed keys for enable/disable:
- Tools: `"tool:function_name"`
- Prompts: `"prompt:prompt_name"`
- Resources: `"resource:resource://uri"`
- Templates: `"template:resource://{param}/path"`
Use `component.key` to get the correct key format.
## Implementation Files
- `src/fastmcp/utilities/visibility.py` - VisibilityFilter class
- `src/fastmcp/server/providers/base.py` - Provider.enable/disable
- `src/fastmcp/server/server.py` - FastMCP.enable/disable
- `src/fastmcp/utilities/components.py` - Component.enable/disable raise NotImplementedError