--- title: v3.0 Feature Tracking --- This document tracks major features in FastMCP v3.0 for release notes preparation. ## Provider-Based Architecture v3.0 introduces a provider-based component system that replaces v2's static-only registration ([#2622](https://github.com/jlowin/fastmcp/pull/2622)). Providers dynamically source tools, resources, templates, and prompts at runtime. **Core abstraction** (`src/fastmcp/server/providers/base.py`): ```python class Provider: async def list_tools(self) -> Sequence[Tool]: ... async def get_tool(self, name: str) -> Tool | None: ... async def list_resources(self) -> Sequence[Resource]: ... async def get_resource(self, uri: str) -> Resource | None: ... async def list_resource_templates(self) -> Sequence[ResourceTemplate]: ... async def get_resource_template(self, uri: str) -> ResourceTemplate | None: ... async def list_prompts(self) -> Sequence[Prompt]: ... async def get_prompt(self, name: str) -> Prompt | None: ... ``` Providers support: - **Lifecycle management**: `async def lifespan()` for setup/teardown - **Visibility control**: `enable()` / `disable()` with name, version, tags, components, and allowlist mode - **Transform stacking**: `provider.add_transform(Namespace(...))`, `provider.add_transform(ToolTransform(...))` ### LocalProvider `LocalProvider` (`src/fastmcp/server/providers/local_provider.py`) manages components registered via decorators. Can be used standalone and attached to multiple servers: ```python from fastmcp.server.providers import LocalProvider provider = LocalProvider() @provider.tool def greet(name: str) -> str: return f"Hello, {name}!" # Attach to multiple servers server1 = FastMCP("Server1", providers=[provider]) server2 = FastMCP("Server2", providers=[provider]) ``` ### ProxyProvider `ProxyProvider` (`src/fastmcp/server/providers/proxy.py`) proxies components from remote MCP servers via a client factory. Used by `create_proxy()` and `FastMCP.mount()` for remote server integration. ```python from fastmcp.server import create_proxy # Create proxy to remote server server = create_proxy("http://remote-server/mcp") ``` ### OpenAPIProvider `OpenAPIProvider` (`src/fastmcp/server/providers/openapi/provider.py`) creates MCP components from OpenAPI specifications. Routes map HTTP operations to tools, resources, or templates based on configurable rules. ```python from fastmcp.server.providers.openapi import OpenAPIProvider import httpx client = httpx.AsyncClient(base_url="https://api.example.com") provider = OpenAPIProvider(openapi_spec=spec, client=client) mcp = FastMCP("API Server", providers=[provider]) ``` Features: - Automatic route-to-component mapping (GET → resource, POST/PUT/DELETE → tool) - Custom route mappings via `route_maps` or `route_map_fn` - Component customization via `mcp_component_fn` - Name collision detection and handling ### FastMCPProvider `FastMCPProvider` (`src/fastmcp/server/providers/fastmcp_provider.py`) wraps a FastMCP server to enable mounting one server onto another. Components delegate execution through the wrapped server's middleware chain. ```python from fastmcp import FastMCP from fastmcp.server.providers import FastMCPProvider from fastmcp.server.transforms import Namespace main = FastMCP("Main") sub = FastMCP("Sub") @sub.tool def greet(name: str) -> str: return f"Hello, {name}!" # Mount with namespace provider = FastMCPProvider(sub) provider.add_transform(Namespace("sub")) main.add_provider(provider) # Tool accessible as "sub_greet" ``` ### Transforms Transforms modify components (tools, resources, prompts) as they flow from providers to clients ([#2836](https://github.com/jlowin/fastmcp/pull/2836)). They use a middleware pattern where each transform receives a `call_next` callable to continue the chain. **Built-in transforms** (`src/fastmcp/server/transforms/`): - `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) - `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 ```python from fastmcp.server.transforms import Namespace, ToolTransform from fastmcp.tools.tool_transform import ToolTransformConfig provider = SomeProvider() provider.add_transform(Namespace("api")) provider.add_transform(ToolTransform({ "api_verbose_tool_name": ToolTransformConfig(name="short") })) # Stacking composes transformations # "foo" → "api_foo" (namespace) → "short" (rename) ``` **Custom transforms** subclass `Transform` and override needed methods: ```python from collections.abc import Sequence from fastmcp.server.transforms import Transform, ListToolsNext, GetToolNext from fastmcp.tools import Tool class TagFilter(Transform): def __init__(self, required_tags: set[str]): self.required_tags = required_tags async def list_tools(self, call_next: ListToolsNext) -> Sequence[Tool]: tools = await call_next() # Get tools from downstream return [t for t in tools if t.tags & self.required_tags] async def get_tool(self, name: str, call_next: GetToolNext) -> Tool | None: tool = await call_next(name) return tool if tool and tool.tags & self.required_tags else None ``` 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/transforms/transforms.mdx`, `docs/servers/visibility.mdx` ### ResourcesAsTools and PromptsAsTools These transforms expose resources and prompts as tools for clients that only support the tools protocol. Each transform generates two tools that provide listing and access functionality. **ResourcesAsTools** generates `list_resources` and `read_resource` tools: ```python from fastmcp import FastMCP from fastmcp.server.transforms import ResourcesAsTools mcp = FastMCP("Server") @mcp.resource("data://config") def get_config() -> dict: return {"setting": "value"} mcp.add_transform(ResourcesAsTools(mcp)) # Now has list_resources and read_resource tools ``` The `list_resources` tool returns JSON with resource metadata. The `read_resource` tool accepts a URI and returns the resource content, preserving both text and binary data through base64 encoding. **PromptsAsTools** generates `list_prompts` and `get_prompt` tools: ```python from fastmcp import FastMCP from fastmcp.server.transforms import PromptsAsTools mcp = FastMCP("Server") @mcp.prompt def analyze_code(code: str, language: str = "python") -> str: return f"Analyze this {language} code:\n{code}" mcp.add_transform(PromptsAsTools(mcp)) # Now has list_prompts and get_prompt tools ``` The `list_prompts` tool returns JSON with prompt metadata including argument information. The `get_prompt` tool accepts a prompt name and optional arguments dict, returning the rendered prompt as a messages array. Non-text content (like embedded resources) is preserved as structured JSON. Both transforms: - Capture a provider reference at construction for deferred querying - Route through `FastMCP.read_resource()` / `FastMCP.render_prompt()` when the provider is FastMCP, ensuring middleware chains execute - Fall back to direct provider methods for plain providers - Return JSON for easy parsing by tool-only clients Documentation: `docs/servers/transforms/resources-as-tools.mdx`, `docs/servers/transforms/prompts-as-tools.mdx` --- ## Session-Scoped State v3.0 changes context state from request-scoped to session-scoped. State now persists across multiple tool calls within the same MCP session. ```python @mcp.tool async def increment_counter(ctx: Context) -> int: count = await ctx.get_state("counter") or 0 await ctx.set_state("counter", count + 1) return count + 1 ``` State is automatically keyed by session ID, ensuring isolation between different clients. The implementation uses [pykeyvalue](https://github.com/strawgate/py-key-value) for pluggable storage backends: ```python from key_value.aio.stores.redis import RedisStore # Use Redis for distributed deployments mcp = FastMCP("server", session_state_store=RedisStore(...)) ``` **Key details:** - Methods are now async: `await ctx.get_state()`, `await ctx.set_state()`, `await ctx.delete_state()` - State expires after 1 day (TTL) to prevent unbounded memory growth - Works during `on_initialize` middleware when using the same session object - For distributed HTTP, session identity comes from the `mcp-session-id` header Documentation: `docs/servers/context.mdx` --- ## 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. ```python mcp = FastMCP("Server") # Disable by name and component type mcp.disable(names={"dangerous_tool"}, components=["tool"]) # Disable by tag mcp.disable(tags={"admin"}) # Disable by version mcp.disable(names={"old_tool"}, version="1.0", components=["tool"]) # Allowlist mode - only show components with these tags mcp.enable(tags={"public"}, only=True) # Enable overrides earlier disable (later transform wins) mcp.disable(tags={"internal"}) mcp.enable(names={"safe_tool"}) # safe_tool is visible despite internal tag ``` Works at both server and provider level. Supports: - **Blocklist mode** (default): All components visible except explicitly disabled - **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**: Visibility transforms are injected at the point you call them, so component state is known ### Per-Session Visibility Server-level visibility changes affect all connected clients. For per-session control, use `Context` methods that apply rules only to the current session ([#2917](https://github.com/jlowin/fastmcp/pull/2917)): ```python from fastmcp import FastMCP from fastmcp.server.context import Context mcp = FastMCP("Server") @mcp.tool(tags={"premium"}) def premium_analysis(data: str) -> str: return f"Premium analysis of: {data}" @mcp.tool async def unlock_premium(ctx: Context) -> str: """Unlock premium features for this session only.""" await ctx.enable_components(tags={"premium"}) return "Premium features unlocked" @mcp.tool async def reset_features(ctx: Context) -> str: """Reset to default feature set.""" await ctx.reset_visibility() return "Features reset to defaults" # Globally disabled - sessions unlock individually 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_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. Documentation: `docs/servers/visibility.mdx` --- ## Component Versioning v3.0 introduces versioning support for tools, resources, and prompts. Components can declare a version, and when multiple versions of the same component exist, the highest version is automatically exposed to clients. **Declaring versions:** ```python @mcp.tool(version="1.0") def add(x: int, y: int) -> int: return x + y @mcp.tool(version="2.0") def add(x: int, y: int, z: int = 0) -> int: return x + y + z # Only v2.0 is exposed to clients via list_tools() # Calling "add" invokes the v2.0 implementation ``` **Version comparison:** - Uses PEP 440 semantic versioning (1.10 > 1.9 > 1.2) - Falls back to string comparison for non-PEP 440 versions (dates like `2025-01-15` work) - Unversioned components sort lower than any versioned component - The `v` prefix is normalized (`v1.0` equals `1.0`) **Version visibility in meta:** List operations expose all available versions in the component's `meta` field: ```python tools = await client.list_tools() # Each tool's meta includes: # - meta["fastmcp"]["version"]: the version of this component ("2.0") # - meta["fastmcp"]["versions"]: all available versions ["2.0", "1.0"] ``` **Retrieving and calling specific versions:** ```python # Get the highest version (default) tool = await server.get_tool("add") # Get a specific version tool_v1 = await server.get_tool("add", version="1.0") # Call a specific version result = await server.call_tool("add", {"x": 1, "y": 2}, version="1.0") ``` **Client version requests:** The FastMCP client supports version selection: ```python async with Client(server) as client: # Call specific tool version result = await client.call_tool("add", {"x": 1, "y": 2}, version="1.0") # Get specific prompt version prompt = await client.get_prompt("my_prompt", {"text": "..."}, version="2.0") ``` For generic MCP clients, pass version via `_meta` in arguments: ```json { "x": 1, "y": 2, "_meta": { "fastmcp": { "version": "1.0" } } } ``` **VersionFilter transform:** The `VersionFilter` transform enables serving different API versions from a single codebase: ```python from fastmcp import FastMCP from fastmcp.server.providers import LocalProvider from fastmcp.server.transforms import VersionFilter # Define components on a shared provider components = LocalProvider() @components.tool(version="1.0") def calculate(x: int, y: int) -> int: return x + y @components.tool(version="2.0") def calculate(x: int, y: int, z: int = 0) -> int: return x + y + z # Create servers that share the provider with different filters api_v1 = FastMCP("API v1", providers=[components]) api_v1.add_transform(VersionFilter(version_lt="2.0")) api_v2 = FastMCP("API v2", providers=[components]) api_v2.add_transform(VersionFilter(version_gte="2.0")) ``` Parameters mirror comparison operators: - `version_gte`: Versions >= this value pass through - `version_lt`: Versions < this value pass through **Key format:** Component keys now include a version suffix using `@` as a delimiter: - Versioned: `tool:add@1.0`, `resource:data://config@2.0` - Unversioned: `tool:add@`, `resource:data://config@` The `@` is always present (even for unversioned components) to enable unambiguous parsing of URIs that may contain `@`. --- ## Type-Safe Canonical Results v3.0 introduces type-safe result classes that provide explicit control over component responses while supporting MCP runtime metadata: `ToolResult` ([#2736](https://github.com/jlowin/fastmcp/pull/2736)), `ResourceResult` ([#2734](https://github.com/jlowin/fastmcp/pull/2734)), and `PromptResult` ([#2738](https://github.com/jlowin/fastmcp/pull/2738)). ### ToolResult `ToolResult` (`src/fastmcp/tools/tool.py:79`) provides structured tool responses: ```python from fastmcp.tools import ToolResult @mcp.tool def process(data: str) -> ToolResult: return ToolResult( content=[TextContent(type="text", text="Done")], structured_content={"status": "success", "count": 42}, meta={"processing_time_ms": 150} ) ``` Fields: - `content`: List of MCP ContentBlocks (text, images, etc.) - `structured_content`: Dict matching tool's output schema - `meta`: Runtime metadata passed to MCP as `_meta` ### ResourceResult `ResourceResult` (`src/fastmcp/resources/resource.py:117`) provides structured resource responses: ```python from fastmcp.resources import ResourceResult, ResourceContent @mcp.resource("data://items") def get_items() -> ResourceResult: return ResourceResult( contents=[ ResourceContent({"key": "value"}), # auto-serialized to JSON ResourceContent(b"binary data"), ], meta={"count": 2} ) ``` Accepts strings, bytes, or `list[ResourceContent]` for flexible content handling. ### PromptResult `PromptResult` (`src/fastmcp/prompts/prompt.py:109`) provides structured prompt responses: ```python from fastmcp.prompts import PromptResult, Message @mcp.prompt def conversation() -> PromptResult: return PromptResult( messages=[ Message("What's the weather?"), Message("It's sunny today.", role="assistant"), ], meta={"generated_at": "2024-01-01"} ) ``` --- ## Background Tasks (SEP-1686) v3.0 implements MCP SEP-1686 for background task execution via Docket integration. **Configuration** (`src/fastmcp/server/tasks/config.py`): ```python from fastmcp.server.tasks import TaskConfig @mcp.tool(task=TaskConfig(mode="required")) async def long_running_task(): # Must be executed as background task ... @mcp.tool(task=TaskConfig(mode="optional")) async def flexible_task(): # Supports both sync and task execution ... @mcp.tool(task=True) # Shorthand for mode="optional" async def simple_task(): ... ``` Task modes: - `"forbidden"`: Component does not support task execution (default) - `"optional"`: Supports both synchronous and task execution - `"required"`: Must be executed as background task Requires Docket server for task scheduling and result polling. --- ## Decorators Return Functions v3.0 changes what decorators (`@tool`, `@resource`, `@prompt`) return ([#2856](https://github.com/jlowin/fastmcp/pull/2856)). Decorators now return the original function unchanged, rather than transforming it into a component object. **v3 behavior (default):** ```python @mcp.tool def greet(name: str) -> str: return f"Hello, {name}!" # greet is still your function - call it directly greet("World") # "Hello, World!" ``` **Why this matters:** - Functions stay callable - useful for testing and reuse - Instance methods just work: `mcp.add_tool(obj.method)` - Matches how Flask, FastAPI, and Typer decorators behave **For v2 compatibility:** ```python import fastmcp # v2 behavior: decorators return FunctionTool/FunctionResource/FunctionPrompt objects fastmcp.settings.decorator_mode = "object" ``` Environment variable: `FASTMCP_DECORATOR_MODE=object` --- ## CLI Auto-Reload The `--reload` flag enables file watching with automatic server restarts for development ([#2816](https://github.com/jlowin/fastmcp/pull/2816)). ```bash # Watch for changes and restart fastmcp run server.py --reload # Watch specific directories fastmcp run server.py --reload --reload-dir ./src --reload-dir ./lib # Works with any transport fastmcp run server.py --reload --transport http --port 8080 ``` Implementation (`src/fastmcp/cli/run.py`): - Uses `watchfiles` for efficient file monitoring - Runs server as subprocess for clean restarts - Stateless mode for seamless reconnection after restart - stdio: Full MCP features including elicitation - HTTP: Limited bidirectional features during reload Also available with `fastmcp dev`: ```bash fastmcp dev server.py # Includes --reload by default ``` --- ## Component Authorization v3.0 introduces callable-based authorization for tools, resources, and prompts ([#2855](https://github.com/jlowin/fastmcp/pull/2855)). **Component-level auth**: ```python from fastmcp import FastMCP from fastmcp.server.auth import require_auth, require_scopes mcp = FastMCP() @mcp.tool(auth=require_auth) def protected_tool(): ... @mcp.resource("data://secret", auth=require_scopes("read")) def secret_data(): ... @mcp.prompt(auth=require_scopes("admin")) def admin_prompt(): ... ``` **Server-wide auth via middleware**: ```python from fastmcp.server.middleware import AuthMiddleware from fastmcp.server.auth import require_auth, restrict_tag # Require auth for all components mcp = FastMCP(middleware=[AuthMiddleware(auth=require_auth)]) # Tag-based restrictions mcp = FastMCP(middleware=[ AuthMiddleware(auth=restrict_tag("admin", scopes=["admin"])) ]) ``` Built-in checks: - `require_auth`: Requires any valid token - `require_scopes(*scopes)`: Requires specific OAuth scopes - `restrict_tag(tag, scopes)`: Requires scopes only for tagged components Custom checks receive `AuthContext` with `token` and `component`: ```python def custom_check(ctx: AuthContext) -> bool: return ctx.token is not None and "admin" in ctx.token.scopes ``` STDIO transport bypasses all auth checks (no OAuth concept). --- ## FileSystemProvider v3.0 introduces `FileSystemProvider`, a fundamentally different approach to organizing MCP servers. Instead of importing a server instance and decorating functions with `@server.tool`, you use standalone decorators in separate files and let the provider discover them. **The problem it solves**: Traditional servers require coordination between files—either tool files import the server (creating coupling) or the server imports all tool modules (creating a registry bottleneck). FileSystemProvider removes this coupling entirely. **Usage** ([#2823](https://github.com/jlowin/fastmcp/pull/2823)): ```python from fastmcp import FastMCP from fastmcp.server.providers import FileSystemProvider # Scans mcp/ directory for decorated functions mcp = FastMCP("server", providers=[FileSystemProvider("mcp/")]) ``` **Tool files are self-contained**: ```python # mcp/tools/greet.py from fastmcp.tools import tool @tool def greet(name: str) -> str: """Greet someone by name.""" return f"Hello, {name}!" ``` Features: - **Standalone decorators**: `@tool`, `@resource`, `@prompt` from `fastmcp.tools`, `fastmcp.resources`, `fastmcp.prompts` ([#2832](https://github.com/jlowin/fastmcp/pull/2832)) - **Reload mode**: `FileSystemProvider("mcp/", reload=True)` re-scans on every request for development - **Package support**: Directories with `__init__.py` support relative imports - **Warning deduplication**: Broken imports warn once per file modification Documentation: [FileSystemProvider](/servers/providers/filesystem) --- ## SkillsProvider v3.0 introduces `SkillsProvider` for exposing agent skills as MCP resources ([#2944](https://github.com/jlowin/fastmcp/pull/2944)). Skills are directories containing instructions and supporting files that teach AI assistants how to perform tasks—used by Claude Code, Cursor, VS Code Copilot, and other AI coding tools. **Usage**: ```python from pathlib import Path from fastmcp import FastMCP from fastmcp.server.providers.skills import SkillsDirectoryProvider mcp = FastMCP("Skills Server") mcp.add_provider(SkillsDirectoryProvider(roots=Path.home() / ".claude" / "skills")) ``` Each subdirectory with a `SKILL.md` file becomes a discoverable skill. Clients see: - `skill://{name}/SKILL.md` - Main instruction file - `skill://{name}/_manifest` - JSON listing of all files with sizes and hashes - `skill://{name}/{path}` - Supporting files (via template or resources) **Two-layer architecture**: - `SkillProvider` - Handles a single skill folder - `SkillsDirectoryProvider` - Scans directories, creates a `SkillProvider` per valid skill **Vendor providers** with locked default paths: | Provider | Directory | |----------|-----------| | `ClaudeSkillsProvider` | `~/.claude/skills/` | | `CursorSkillsProvider` | `~/.cursor/skills/` | | `VSCodeSkillsProvider` | `~/.copilot/skills/` | | `CodexSkillsProvider` | `/etc/codex/skills/`, `~/.codex/skills/` | | `GeminiSkillsProvider` | `~/.gemini/skills/` | | `GooseSkillsProvider` | `~/.config/agents/skills/` | | `CopilotSkillsProvider` | `~/.copilot/skills/` | | `OpenCodeSkillsProvider` | `~/.config/opencode/skills/` | **Progressive disclosure**: By default, supporting files are hidden from `list_resources()` and accessed via template. Set `supporting_files="resources"` for full enumeration. Documentation: [Skills Provider](/servers/providers/skills) --- ## OpenTelemetry Tracing v3.0 adds OpenTelemetry instrumentation for observability into server and client operations ([#2869](https://github.com/jlowin/fastmcp/pull/2869)). **Server spans**: Created for tool calls, resource reads, and prompt renders with attributes including component key, provider type, session ID, and auth context. **Client spans**: Wrap outgoing calls with W3C trace context propagation via request meta. ```python # Tracing is passive - configure an OTel SDK to export spans from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter provider = TracerProvider() provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter())) trace.set_tracer_provider(provider) # Use fastmcp normally - spans export to your configured backend ``` Components provide their own span attributes through a `get_span_attributes()` method that subclasses override—this lets LocalProvider, FastMCPProvider, and ProxyProvider each include relevant context (original names, backend URIs, etc.). Documentation: [Telemetry](/servers/telemetry) --- ## Pagination v3.0 adds pagination support for list operations when servers expose many components ([#2903](https://github.com/jlowin/fastmcp/pull/2903)). ```python from fastmcp import FastMCP # Enable pagination with 50 items per page server = FastMCP("ComponentRegistry", list_page_size=50) ``` When `list_page_size` is set, `tools/list`, `resources/list`, `resources/templates/list`, and `prompts/list` paginate responses with `nextCursor` for subsequent pages. **Client behavior**: The FastMCP Client fetches all pages automatically—`list_tools()` and similar methods return the complete list. For manual pagination (memory constraints, progress reporting), use `_mcp` variants: ```python async with Client(server) as client: result = await client.list_tools_mcp() while result.nextCursor: result = await client.list_tools_mcp(cursor=result.nextCursor) ``` Documentation: [Pagination](/servers/pagination) --- ## Composable Lifespans Lifespans can be combined with the `|` operator for modular setup/teardown ([#2828](https://github.com/jlowin/fastmcp/pull/2828)): ```python from fastmcp import FastMCP from fastmcp.server.lifespan import lifespan @lifespan async def db_lifespan(server): db = await connect_db() try: yield {"db": db} finally: await db.close() @lifespan async def cache_lifespan(server): cache = await connect_cache() try: yield {"cache": cache} finally: await cache.close() mcp = FastMCP("server", lifespan=db_lifespan | cache_lifespan) ``` Both enter lifespans in order and exit in reverse (LIFO). Context dicts are merged. Also adds `combine_lifespans()` utility for FastAPI integration: ```python from fastmcp.utilities.lifespan import combine_lifespans app = FastAPI(lifespan=combine_lifespans(app_lifespan, mcp_app.lifespan)) ``` Documentation: [Lifespan](/servers/lifespan) --- The features above were released in **v3.0.0b1**. --- ## v3.0 Beta 2 Features --- ## Auto-Docket Execution Components with Docket dependencies (like `Timeout`, `Retry`, `ExponentialRetry`) automatically route through Docket, ensuring consistent behavior for both foreground and background execution. ```python from datetime import timedelta from docket import Timeout, ExponentialRetry from fastmcp import FastMCP mcp = FastMCP("Server") @mcp.tool async def fetch_data( url: str, timeout: Timeout = Timeout(timedelta(seconds=30)), retry: ExponentialRetry = ExponentialRetry(max_attempts=3), ) -> dict: """Fetch with timeout and automatic retry.""" ... ``` When FastMCP detects these Docket dependencies in a component's function signature, it: 1. **Registers the component with Docket at startup** - even if `task=True` wasn't specified 2. **Routes foreground calls through Docket** - ensuring timeouts/retries work correctly 3. **Uses the same code path for background tasks** - unified execution model This replaces the previous `timeout` parameter on `@mcp.tool()`. The Docket approach has several advantages: - **Unified behavior**: Timeouts and retries work identically for foreground and background execution - **Battle-tested implementation**: Docket's timeout/retry logic handles edge cases properly - **Composable**: Multiple dependencies can be combined (timeout + retry + concurrency limits) - **Declarative**: Dependencies in the function signature clearly document requirements Supported Docket dependencies: - `Timeout` - Cancel execution after a duration - `Retry` / `ExponentialRetry` - Retry on failure with configurable backoff - `ConcurrencyLimit` - Limit concurrent executions - `CurrentDocket` / `CurrentWorker` / `CurrentExecution` - Access Docket runtime context --- ## PingMiddleware Sends periodic server-to-client pings to keep long-lived connections alive ([#2838](https://github.com/jlowin/fastmcp/pull/2838)): ```python from fastmcp import FastMCP from fastmcp.server.middleware import PingMiddleware mcp = FastMCP("server") mcp.add_middleware(PingMiddleware(interval_ms=5000)) ``` The middleware starts a background ping task on first message from each session, using the session's existing task group for automatic cleanup when the session ends. --- ## Context.transport Property Tools can detect which transport is active ([#2850](https://github.com/jlowin/fastmcp/pull/2850)): ```python from fastmcp import FastMCP, Context mcp = FastMCP("example") @mcp.tool def my_tool(ctx: Context) -> str: if ctx.transport == "stdio": return "short response" return "detailed response with more context" ``` Returns `Literal["stdio", "sse", "streamable-http"]` when running, or `None` outside a server context. --- ## Automatic Threadpool for Sync Functions Synchronous tools, resources, and prompts now automatically run in a threadpool, preventing event loop blocking during concurrent requests ([#2865](https://github.com/jlowin/fastmcp/pull/2865)): ```python import time @mcp.tool def slow_tool(): time.sleep(10) # No longer blocks other requests return "done" ``` Three concurrent calls now execute in parallel (~10s) rather than sequentially (30s). Uses `anyio.to_thread.run_sync()` which properly propagates contextvars, so `Context` and `Depends` continue to work. --- ## CLI Update Notifications The CLI notifies users when a newer FastMCP version is available on PyPI ([#2840](https://github.com/jlowin/fastmcp/pull/2840)). **Setting**: `FASTMCP_CHECK_FOR_UPDATES` - `"stable"` - Check for stable releases (default) - `"prerelease"` - Include alpha/beta/rc versions - `"off"` - Disable 12-hour cache, 2-second timeout, fails silently on network errors. --- ## Deprecated Features These emit deprecation warnings but continue to work. ### Mount Prefix Parameter The `prefix` parameter for `mount()` renamed to `namespace`: ```python # Deprecated main.mount(subserver, prefix="api") # New main.mount(subserver, namespace="api") ``` ### Tag Filtering Init Parameters `FastMCP(include_tags=..., exclude_tags=...)` deprecated. Use `enable()`/`disable()` methods: ```python # Deprecated mcp = FastMCP("server", exclude_tags={"internal"}) # New mcp = FastMCP("server") mcp.disable(tags={"internal"}) ``` ### Tool Serializer Parameter The `tool_serializer` parameter on `FastMCP` is deprecated. Return `ToolResult` for explicit serialization control. ### Tool Transformation Methods `add_tool_transformation()`, `remove_tool_transformation()`, and `tool_transformations` constructor parameter are deprecated. Use `add_transform(ToolTransform({...}))` instead: ```python # Deprecated mcp.add_tool_transformation("name", config) # New from fastmcp.server.transforms import ToolTransform mcp.add_transform(ToolTransform({"name": config})) ``` --- ## Breaking Changes ### WSTransport Removed The deprecated `WSTransport` client transport has been removed ([#2826](https://github.com/jlowin/fastmcp/pull/2826)). Use `StreamableHttpTransport` instead. ### Decorators Return Functions Decorators (`@tool`, `@resource`, `@prompt`) now return the original function instead of component objects. Code that treats the decorated function as a `FunctionTool`, `FunctionResource`, or `FunctionPrompt` will break. ```python # v2.x @mcp.tool def greet(name: str) -> str: return f"Hello, {name}!" isinstance(greet, FunctionTool) # True # v3.0 @mcp.tool def greet(name: str) -> str: return f"Hello, {name}!" isinstance(greet, FunctionTool) # False callable(greet) # True - it's still your function greet("World") # "Hello, World!" ``` Set `FASTMCP_DECORATOR_MODE=object` or `fastmcp.settings.decorator_mode = "object"` for v2 behavior. ### Component Enable/Disable Moved to Server/Provider The `enabled` field and `enable()`/`disable()` methods removed from component objects: ```python # v2.x tool = await server.get_tool("my_tool") tool.disable() # v3.0 server.disable(names={"my_tool"}, components=["tool"]) ``` ### Component Lookup Methods Server lookup and listing methods have updated signatures: - Parameter names: `get_tool(name=...)`, `get_resource(uri=...)`, etc. (was `key`) - Return types: `get_tools()`, `get_resources()`, etc. return lists instead of dicts ```python # v2.x tools = await server.get_tools() tool = tools["my_tool"] # v3.0 tools = await server.get_tools() tool = next((t for t in tools if t.name == "my_tool"), None) ``` ### Prompt Return Types Prompt functions now use `Message` instead of `mcp.types.PromptMessage`: ```python # v2.x from mcp.types import PromptMessage, TextContent @mcp.prompt def my_prompt() -> PromptMessage: return PromptMessage(role="user", content=TextContent(type="text", text="Hello")) # v3.0 from fastmcp.prompts import Message @mcp.prompt def my_prompt() -> Message: return Message("Hello") # role defaults to "user" ``` ### Auth Provider Environment Variables Removed Auth providers no longer auto-load from environment variables ([#2752](https://github.com/jlowin/fastmcp/pull/2752)): ```python # v2.x - auto-loaded from FASTMCP_SERVER_AUTH_GITHUB_* auth = GitHubProvider() # v3.0 - explicit configuration import os auth = GitHubProvider( client_id=os.environ["GITHUB_CLIENT_ID"], client_secret=os.environ["GITHUB_CLIENT_SECRET"], ) ``` See `docs/development/v3-notes/auth-provider-env-vars.mdx` for rationale. ### Server Banner Environment Variable `FASTMCP_SHOW_CLI_BANNER` → `FASTMCP_SHOW_SERVER_BANNER` ([#2771](https://github.com/jlowin/fastmcp/pull/2771)) Now applies to all server startup methods, not just the CLI. ### Context State Methods Are Async `ctx.set_state()` and `ctx.get_state()` are now async and session-scoped: ```python # v2.x ctx.set_state("key", "value") value = ctx.get_state("key") # v3.0 await ctx.set_state("key", "value") value = await ctx.get_state("key") ``` State now persists across requests within a session. See "Session-Scoped State" above.