mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-22 13:34:17 +02:00
Publish FastMCP 4.0.0b1 docs to gofastmcp.com (#4695)
This commit is contained in:
parent
8cf4506aa9
commit
0747ce0bc1
220 changed files with 12093 additions and 8314 deletions
73
dev-docs/v3-notes/auth-provider-env-vars.md
Normal file
73
dev-docs/v3-notes/auth-provider-env-vars.md
Normal 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.
|
||||
61
dev-docs/v3-notes/get-methods-consolidation.md
Normal file
61
dev-docs/v3-notes/get-methods-consolidation.md
Normal 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
|
||||
121
dev-docs/v3-notes/prompt-internal-types.md
Normal file
121
dev-docs/v3-notes/prompt-internal-types.md
Normal 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
|
||||
116
dev-docs/v3-notes/provider-architecture.md
Normal file
116
dev-docs/v3-notes/provider-architecture.md
Normal 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)
|
||||
60
dev-docs/v3-notes/provider-test-pattern.md
Normal file
60
dev-docs/v3-notes/provider-test-pattern.md
Normal 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
|
||||
196
dev-docs/v3-notes/resource-internal-types.md
Normal file
196
dev-docs/v3-notes/resource-internal-types.md
Normal 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.
|
||||
109
dev-docs/v3-notes/task-meta-parameter.md
Normal file
109
dev-docs/v3-notes/task-meta-parameter.md
Normal 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
|
||||
1481
dev-docs/v3-notes/v3-features.md
Normal file
1481
dev-docs/v3-notes/v3-features.md
Normal file
File diff suppressed because it is too large
Load diff
113
dev-docs/v3-notes/visibility.md
Normal file
113
dev-docs/v3-notes/visibility.md
Normal 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue