---
title: v3.0 Feature Tracking
---

This document tracks major features in FastMCP v3.0 for release notes preparation.

## 3.0.0beta2

### Expanded CLI Auto-Reload

The `--reload` flag now watches a variety of file types beyond Python files ([#3028](https://github.com/jlowin/fastmcp/pull/3028)), enabling seamless development for MCP apps that include frontend bundles. The implementation automatically detects changes to JavaScript, TypeScript, CSS, HTML, and other common web development file types, triggering server restarts when needed.

---

## 3.0.0beta1

### 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).

---

### MCP Apps (SDK Compatibility)

v3.0 adds Phase 1 support for [MCP Apps](https://modelcontextprotocol.io/specification/2025-06-18/server/apps) — the spec extension that lets MCP servers deliver interactive UIs via sandboxed iframes. Phase 1 is SDK compatibility only: extension negotiation, typed UI metadata on tools and resources, and the `ui://` resource scheme. No component DSL, renderer, or `FastMCPApp` class yet — those are future phases.

**Registering tools with UI metadata:**

```python
from fastmcp import FastMCP
from fastmcp.server.apps import ToolUI, ResourceUI, UI_MIME_TYPE

mcp = FastMCP("My Server")

# Register the HTML bundle as a ui:// resource
@mcp.resource("ui://my-app/view.html")
def app_html() -> str:
    from pathlib import Path
    return Path("./dist/index.html").read_text()

# Tool with UI — clients render an iframe alongside the result
@mcp.tool(ui=ToolUI(resource_uri="ui://my-app/view.html"))
async def list_users() -> list[dict]:
    return [{"id": "1", "name": "Alice"}]

# App-only tool — visible to the UI but hidden from the model
@mcp.tool(ui=ToolUI(resource_uri="ui://my-app/view.html", visibility=["app"]))
async def delete_user(id: str) -> dict:
    return {"deleted": True}
```

The `ui=` parameter accepts either a typed model (`ToolUI`, `ResourceUI`) or a raw dict for forward compatibility. It merges into `meta["ui"]` — alongside any other metadata you set.

**`ui://` resources** automatically get the correct MIME type (`text/html;profile=mcp-app`) unless you override it explicitly.

**Extension negotiation**: The server advertises `io.modelcontextprotocol/ui` in `capabilities.extensions`. UI metadata (`_meta.ui`) always flows through to clients — the MCP Apps spec assigns visibility enforcement to the host, not the server. Tools can check whether the connected client supports a given extension at runtime via `ctx.client_supports_extension()`:

```python
from fastmcp import Context
from fastmcp.server.apps import ToolUI, UI_EXTENSION_ID

@mcp.tool(ui=ToolUI(resource_uri="ui://dashboard"))
async def dashboard(ctx: Context) -> dict:
    data = compute_dashboard()
    if ctx.client_supports_extension(UI_EXTENSION_ID):
        # Client will render the iframe with structured data
        return data
    # Fallback: text-only summary
    return {"summary": format_text(data)}
```

**Key details:**
- `ToolUI` fields: `resource_uri`, `visibility`, `csp`, `permissions`, `domain`, `prefers_border` (all optional except for typical usage of `resource_uri`)
- `ResourceUI` fields: `csp`, `permissions`, `domain`, `prefers_border` — metadata for the resource itself when it's a UI bundle
- Models use Pydantic aliases for wire format (`resourceUri`, `prefersBorder`)
- `ctx.client_supports_extension(id)` is a general-purpose method — works for any extension, not just MCP Apps
- `structuredContent` in tool results already works via `ToolResult` — MCP Apps clients use this to pass data into the iframe
- Text content fallback already works — tools return both `content` and `structured_content`
- The server does not strip `_meta.ui` for non-UI clients; per the spec, visibility enforcement is the host's responsibility

**Future phases** will add a component DSL for building UIs declaratively, an in-repo renderer, and a `FastMCPApp` class.

Implementation: `src/fastmcp/server/apps.py` (models and constants), with integration points in `server.py` (decorator parameters), `low_level.py` (extension advertisement), and `context.py` (`client_supports_extension` method).

---

### 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)

---

### Tool Timeout

Tools can limit foreground execution time with a `timeout` parameter ([#2872](https://github.com/jlowin/fastmcp/pull/2872)):

```python
@mcp.tool(timeout=30.0)
async def fetch_data(url: str) -> dict:
    """Fetch with 30-second timeout."""
    ...
```

When exceeded, clients receive MCP error code `-32000`. Both sync and async tools are supported—sync functions run in thread pools so the timeout applies regardless of execution model.

Note: This timeout applies to foreground execution only. Background tasks (`task=True`) execute in Docket workers where this timeout isn't enforced.

---

### 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.
