Remove 3.x-era compatibility shims (#4661)

* Remove 3.x-era compatibility shims

* Require response_type in ctx.elicit()

* Name the utilities path for the two non-re-exported auth helpers

* Point sampling handler migration at its submodule
This commit is contained in:
Jeremiah Lowin 2026-07-27 14:59:43 -04:00 committed by GitHub
commit ea7fb8cb2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 150 additions and 285 deletions

View file

@ -567,6 +567,10 @@ The `_REMOVED_KWARGS` constructor shim (which raises helpful `TypeError`s for kw
- **Tool-level `serializer` parameter** — removed from `@tool` / `mcp.tool()`, `Tool.from_function`, `Tool.from_tool`, `TransformedTool.from_tool`, the OpenAPI `OpenAPITool`, and the `mcp_mixin` tool decorator. Return a `ToolResult` from your tool for full control over serialization instead (see [Custom Serialization](https://gofastmcp.com/servers/tools#custom-serialization)). The server-level `tool_serializer` constructor kwarg was already removed in 3.0. - **Tool-level `serializer` parameter** — removed from `@tool` / `mcp.tool()`, `Tool.from_function`, `Tool.from_tool`, `TransformedTool.from_tool`, the OpenAPI `OpenAPITool`, and the `mcp_mixin` tool decorator. Return a `ToolResult` from your tool for full control over serialization instead (see [Custom Serialization](https://gofastmcp.com/servers/tools#custom-serialization)). The server-level `tool_serializer` constructor kwarg was already removed in 3.0.
- **Tool `exclude_args` parameter** — removed from the tool decorator and its plumbing (`ParsedFunction.from_function`, `Tool.from_function`, `mcp.tool()`). Use dependency injection with `Depends()` to hide parameters from the tool schema instead. - **Tool `exclude_args` parameter** — removed from the tool decorator and its plumbing (`ParsedFunction.from_function`, `Tool.from_function`, `mcp.tool()`). Use dependency injection with `Depends()` to hide parameters from the tool schema instead.
- **`decorator_mode` setting** (`FASTMCP_DECORATOR_MODE`) and its `"object"` mode — removed. Decorators always return the original function with metadata attached; the object-returning machinery is gone. Access component objects through the server (e.g. `await mcp.get_tool("name")`) rather than the decorated function. - **`decorator_mode` setting** (`FASTMCP_DECORATOR_MODE`) and its `"object"` mode — removed. Decorators always return the original function with metadata attached; the object-returning machinery is gone. Access component objects through the server (e.g. `await mcp.get_tool("name")`) rather than the decorated function.
- **Component-import compatibility shims** — the `__getattr__` shims that re-exported `FunctionTool` / `ParsedFunction` / `tool` from `fastmcp.tools.tool`, `FunctionResource` / `resource` from `fastmcp.resources.resource`, and `FunctionPrompt` / `prompt` from `fastmcp.prompts.prompt` are removed. Import these from their canonical modules (`fastmcp.tools.function_tool`, `fastmcp.resources.function_resource`, `fastmcp.prompts.function_prompt`) instead. - **Component-import compatibility shims** — Breaking. `fastmcp.tools.tool`, `fastmcp.resources.resource`, and `fastmcp.prompts.prompt` no longer exist as modules. Two separate mechanisms kept them alive and both are now gone: the `__getattr__` shims that re-exported `FunctionTool` / `ParsedFunction` / `tool`, `FunctionResource` / `resource`, and `FunctionPrompt` / `prompt`; and the `sys.modules` aliases that pointed each old module name at its renamed `base.py`. Import the component types from the package itself — `from fastmcp.tools import Tool, ToolResult` — and the function-backed classes from their canonical modules (`fastmcp.tools.function_tool`, `fastmcp.resources.function_resource`, `fastmcp.prompts.function_prompt`).
- **`fastmcp.experimental.sampling`** and **`fastmcp.experimental.sampling.handlers`** (2.x-era re-export shims) — Breaking. These aliased the client-side sampling handlers without warning. Import from `fastmcp.client.sampling.handlers.openai` instead. Note this is unrelated to the SEP-2577 removal of *server-initiated* sampling: a FastMCP client still answers a legacy-era server's sampling requests, so `Client(sampling_handler=...)` and the Anthropic / OpenAI / Google GenAI handlers under `fastmcp.client.sampling.handlers` remain fully supported.
- **`fastmcp.server.auth.authorization`** (3.0-era re-export shim) — Breaking. The module was a pass-through sitting between the `fastmcp.server.auth` package and the real implementation in `fastmcp.utilities.authorization`, and FastMCP's own middleware and local-provider decorators imported through it. Everything internal now imports from `fastmcp.utilities.authorization` directly. The documented public path is unchanged: `from fastmcp.server.auth import require_scopes, require_roles, restrict_tag, run_auth_checks, AuthCheck, AuthContext`. Two names the old module also exported — `run_auth_checks_with_shortfall` and `scope_requirements` — are *not* re-exported from `fastmcp.server.auth` and must be imported from `fastmcp.utilities.authorization`. They are middleware plumbing with no documented user-facing use, so they were deliberately not widened onto the auth package's surface; the upgrade guide names the utilities path for them explicitly.
- **`SkillsProvider`** (3.0-era rename alias) — Breaking. Use `SkillsDirectoryProvider` from `fastmcp.server.providers.skills`. The alias was also re-exported from `fastmcp.server.providers`; both are gone.
- **`ctx.elicit()` without `response_type`** (deprecated 3.2, warned through 3.4.4) — Breaking. The parameter is now required, and passing `None` explicitly raises `TypeError`. The empty-object schema it produced was ambiguous under the MCP spec and left some clients (e.g. VS Code) rendering an empty, non-functional form. Pass a type describing the data you expect back; `bool` covers confirmations. This is the server-authoring API only — the *client* elicitation handler still receives `response_type=None` for URL requests and for empty schemas sent by other servers, which is unchanged.
*Verify:* deletions of `fastmcp_slim/fastmcp/server/proxy.py`, `fastmcp_slim/fastmcp/server/openapi/`, `fastmcp_slim/fastmcp/experimental/server/openapi/`, `fastmcp_slim/fastmcp/experimental/utilities/openapi/`, `fastmcp_slim/fastmcp/server/apps.py`, `fastmcp_slim/fastmcp/server/app.py`; the removed classes in `fastmcp_slim/fastmcp/server/middleware/tool_injection.py`; the removed parameter in `fastmcp_slim/fastmcp/client/transports/http.py`; `fastmcp_slim/fastmcp/server/server.py`; `fastmcp_slim/fastmcp/tools/base.py`, `tools/function_tool.py`, `tools/tool_transform.py`, `tools/function_parsing.py`; `fastmcp_slim/fastmcp/settings.py`, `resources/function_resource.py`, `prompts/function_prompt.py`, and the local-provider decorators; `resources/base.py`, `prompts/base.py`. *Verify:* deletions of `fastmcp_slim/fastmcp/server/proxy.py`, `fastmcp_slim/fastmcp/server/openapi/`, `fastmcp_slim/fastmcp/experimental/server/openapi/`, `fastmcp_slim/fastmcp/experimental/utilities/openapi/`, `fastmcp_slim/fastmcp/server/apps.py`, `fastmcp_slim/fastmcp/server/app.py`; the removed classes in `fastmcp_slim/fastmcp/server/middleware/tool_injection.py`; the removed parameter in `fastmcp_slim/fastmcp/client/transports/http.py`; `fastmcp_slim/fastmcp/server/server.py`; `fastmcp_slim/fastmcp/tools/base.py`, `tools/function_tool.py`, `tools/tool_transform.py`, `tools/function_parsing.py`; `fastmcp_slim/fastmcp/settings.py`, `resources/function_resource.py`, `prompts/function_prompt.py`, and the local-provider decorators; `resources/base.py`, `prompts/base.py`.

View file

@ -178,9 +178,16 @@ The proxy, OpenAPI, and app integrations moved to their permanent homes, and the
| `fastmcp.experimental.server.openapi` | `fastmcp.server.providers.openapi` | | `fastmcp.experimental.server.openapi` | `fastmcp.server.providers.openapi` |
| `fastmcp.experimental.utilities.openapi` | `fastmcp.utilities.openapi` | | `fastmcp.experimental.utilities.openapi` | `fastmcp.utilities.openapi` |
| `fastmcp.server.apps`, `fastmcp.server.app` | `fastmcp.apps` (e.g. `AppConfig`) or `fastmcp` (`FastMCPApp`) | | `fastmcp.server.apps`, `fastmcp.server.app` | `fastmcp.apps` (e.g. `AppConfig`) or `fastmcp` (`FastMCPApp`) |
| `Tool` / `ToolResult` from `fastmcp.tools.tool` | `fastmcp.tools` |
| `Resource` from `fastmcp.resources.resource` | `fastmcp.resources` |
| `Prompt` / `Message` from `fastmcp.prompts.prompt` | `fastmcp.prompts` |
| `FunctionTool` / `ParsedFunction` / `tool` from `fastmcp.tools.tool` | `fastmcp.tools.function_tool` | | `FunctionTool` / `ParsedFunction` / `tool` from `fastmcp.tools.tool` | `fastmcp.tools.function_tool` |
| `FunctionResource` / `resource` from `fastmcp.resources.resource` | `fastmcp.resources.function_resource` | | `FunctionResource` / `resource` from `fastmcp.resources.resource` | `fastmcp.resources.function_resource` |
| `FunctionPrompt` / `prompt` from `fastmcp.prompts.prompt` | `fastmcp.prompts.function_prompt` | | `FunctionPrompt` / `prompt` from `fastmcp.prompts.prompt` | `fastmcp.prompts.function_prompt` |
| `OpenAISamplingHandler` from `fastmcp.experimental.sampling.handlers` | `fastmcp.client.sampling.handlers.openai` |
| `AuthCheck` / `AuthContext` / `require_scopes` / `require_roles` / `restrict_tag` / `run_auth_checks` from `fastmcp.server.auth.authorization` | `fastmcp.server.auth` |
| `run_auth_checks_with_shortfall` / `scope_requirements` from `fastmcp.server.auth.authorization` | `fastmcp.utilities.authorization` |
| `SkillsProvider` | `SkillsDirectoryProvider` from `fastmcp.server.providers.skills` |
Two renames in the same family are worth calling out because they have no compatibility alias. The response-caching wrapper models lost a spelling typo — `CachableToolResult`, `CachablePromptResult`, and their siblings became `CacheableToolResult`, `CacheablePromptResult`, etc. — so an import of the old spelling from `fastmcp.server.middleware.caching` raises `ImportError`. And `PromptToolMiddleware` / `ResourceToolMiddleware` are gone in favor of the `PromptsAsTools` / `ResourcesAsTools` transforms from `fastmcp.server.transforms` (the `ToolInjectionMiddleware` base class is retained). Two renames in the same family are worth calling out because they have no compatibility alias. The response-caching wrapper models lost a spelling typo — `CachableToolResult`, `CachablePromptResult`, and their siblings became `CacheableToolResult`, `CacheablePromptResult`, etc. — so an import of the old spelling from `fastmcp.server.middleware.caching` raises `ImportError`. And `PromptToolMiddleware` / `ResourceToolMiddleware` are gone in favor of the `PromptsAsTools` / `ResourcesAsTools` transforms from `fastmcp.server.transforms` (the `ToolInjectionMiddleware` base class is retained).
@ -204,14 +211,25 @@ Two of these replacements are not exact behavioral swaps. `create_proxy` takes i
`import_server` → `mount` is the one row here that is not a mechanical swap, because the two never had the same semantics. `import_server` took a **one-time static snapshot** — it copied the child's tools, resources, and prompts at call time, with no live link, and did not run the child's lifespan or middleware. `mount` is a **live composition** — it holds a live link to the child and runs the child's lifespan and middleware. After switching, later changes to the child become visible through the parent, the child's lifespan runs with the parent's (entered when the server starts, held until it stops — not per request), and the child's middleware runs on the operations delegated to it. If you depended on the frozen-copy behavior (a stable snapshot, no child lifecycle), there is no drop-in replacement: register the child's components on the parent directly instead of composing the two servers. `import_server` → `mount` is the one row here that is not a mechanical swap, because the two never had the same semantics. `import_server` took a **one-time static snapshot** — it copied the child's tools, resources, and prompts at call time, with no live link, and did not run the child's lifespan or middleware. `mount` is a **live composition** — it holds a live link to the child and runs the child's lifespan and middleware. After switching, later changes to the child become visible through the parent, the child's lifespan runs with the parent's (entered when the server starts, held until it stops — not per request), and the child's middleware runs on the operations delegated to it. If you depended on the frozen-copy behavior (a stable snapshot, no child lifecycle), there is no drop-in replacement: register the child's components on the parent directly instead of composing the two servers.
### Removed tool and decorator parameters ### Removed parameters and settings
Two `@tool` parameters and two settings are gone: Several parameters and settings that warned in 3.x are gone:
- **Tool `serializer=`** is removed from `@tool` / `mcp.tool()`, `Tool.from_function`, `Tool.from_tool`, and the OpenAPI tool. Return a `ToolResult` from your tool for full control over serialization instead. - **Tool `serializer=`** is removed from `@tool` / `mcp.tool()`, `Tool.from_function`, `Tool.from_tool`, and the OpenAPI tool. Return a `ToolResult` from your tool for full control over serialization instead.
- **Tool `exclude_args=`** is removed. Hide a parameter from the tool schema by injecting it instead: give it a `Depends(factory)` default (from `fastmcp.dependencies`), where `factory` is a callable returning the value the argument used to carry. An injected parameter never appears in the tool's schema, which is what `exclude_args` was for. - **Tool `exclude_args=`** is removed. Hide a parameter from the tool schema by injecting it instead: give it a `Depends(factory)` default (from `fastmcp.dependencies`), where `factory` is a callable returning the value the argument used to carry. An injected parameter never appears in the tool's schema, which is what `exclude_args` was for.
- **The `decorator_mode` setting** (`FASTMCP_DECORATOR_MODE`) and its `"object"` mode are removed. Decorators always return your original function with metadata attached; reach the component object through the server (`await mcp.get_tool("name")`) rather than off the decorated function. - **The `decorator_mode` setting** (`FASTMCP_DECORATOR_MODE`) and its `"object"` mode are removed. Decorators always return your original function with metadata attached; reach the component object through the server (`await mcp.get_tool("name")`) rather than off the decorated function.
- **`StreamableHttpTransport(sse_read_timeout=...)`** is removed — it was a no-op under the SDK v2 client. Set the read timeout through the public `Client(transport, timeout=...)` (a `timedelta` or float seconds), or reach for a custom `httpx_client_factory` when you need finer control. (`SSETransport` still accepts `sse_read_timeout`.) - **`StreamableHttpTransport(sse_read_timeout=...)`** is removed — it was a no-op under the SDK v2 client. Set the read timeout through the public `Client(transport, timeout=...)` (a `timedelta` or float seconds), or reach for a custom `httpx_client_factory` when you need finer control. (`SSETransport` still accepts `sse_read_timeout`.)
- **`ctx.elicit()` now requires `response_type`.** Omitting it (or passing `None`) has warned since 3.2 and now raises `TypeError`. The empty-object schema it produced gave clients nothing to render, and some showed an empty, non-functional form. Pass a type describing what you expect back — `bool` is the right answer for a confirmation:
```python
# Before
result = await ctx.elicit("Approve this action?")
# After
result = await ctx.elicit("Approve this action?", response_type=bool)
```
This is the server-authoring API only. Client elicitation handlers still receive `response_type=None` for URL requests and for empty schemas sent by other servers — that contract is unchanged.
## Behavior changes to verify ## Behavior changes to verify

View file

@ -187,9 +187,9 @@ async def confirm_purchase(ctx: Context) -> str:
These arguments only apply when FastMCP is adding the wrapper. For structured responses (`BaseModel`, dataclass, `TypedDict`), set the metadata on the individual fields via `Field(title=..., description=...)` — passing `response_title` or `response_description` alongside a model type raises `TypeError`. These arguments only apply when FastMCP is adding the wrapper. For structured responses (`BaseModel`, dataclass, `TypedDict`), set the metadata on the individual fields via `Field(title=..., description=...)` — passing `response_title` or `response_description` alongside a model type raises `TypeError`.
### Empty Responses ### Confirmations
Passing `None` as the response type creates an empty-object schema and returns an accepted result with `data == {}`. This form is deprecated because some clients render empty forms poorly; prefer an explicit response type such as `bool` for confirmations. `response_type` is required. When all you want is a yes/no answer, ask for a `bool` rather than an empty schema — an empty schema gives the client nothing to render, and some clients show an empty, non-functional form.
```python ```python
@mcp.tool @mcp.tool

View file

@ -722,7 +722,7 @@ Schema generation works for most common types including basic types, collections
For complete control over tool responses, return a `ToolResult` object. This gives you explicit control over all aspects of the tool's output: traditional content, structured data, and metadata. For complete control over tool responses, return a `ToolResult` object. This gives you explicit control over all aspects of the tool's output: traditional content, structured data, and metadata.
```python ```python
from fastmcp.tools.tool import ToolResult from fastmcp.tools import ToolResult
from mcp_types import TextContent from mcp_types import TextContent
@mcp.tool @mcp.tool
@ -788,7 +788,7 @@ When you need custom serialization (like YAML, Markdown tables, or specialized f
```python ```python
import yaml import yaml
from fastmcp import FastMCP from fastmcp import FastMCP
from fastmcp.tools.tool import ToolResult from fastmcp.tools import ToolResult
mcp = FastMCP("MyServer") mcp = FastMCP("MyServer")

View file

@ -250,7 +250,7 @@ Here's a minimal example:
from fastmcp.experimental.transforms.code_mode import CodeMode from fastmcp.experimental.transforms.code_mode import CodeMode
from fastmcp.experimental.transforms.code_mode import GetToolCatalog, GetSchemas from fastmcp.experimental.transforms.code_mode import GetToolCatalog, GetSchemas
from fastmcp.server.context import Context from fastmcp.server.context import Context
from fastmcp.tools.tool import Tool from fastmcp.tools import Tool
def list_all_tools(get_catalog: GetToolCatalog) -> Tool: def list_all_tools(get_catalog: GetToolCatalog) -> Tool:
async def list_tools(ctx: Context) -> str: async def list_tools(ctx: Context) -> str:

View file

@ -118,7 +118,7 @@ Create custom transforms by subclassing `Transform` and overriding the methods y
```python ```python
from collections.abc import Sequence from collections.abc import Sequence
from fastmcp.server.transforms import Transform, GetToolNext from fastmcp.server.transforms import Transform, GetToolNext
from fastmcp.tools.tool import Tool from fastmcp.tools import Tool
class TagFilter(Transform): class TagFilter(Transform):
"""Filter tools to only those with specific tags.""" """Filter tools to only those with specific tags."""

View file

@ -1,7 +1,7 @@
"""Example: Downloading skills from an MCP server. """Example: Downloading skills from an MCP server.
This example shows how to use the skills client utilities to discover This example shows how to use the skills client utilities to discover
and download skills from any MCP server that exposes them via SkillsProvider. and download skills from any MCP server that exposes them via a skills provider.
Run this script: Run this script:
uv run python examples/skills/download_skills.py uv run python examples/skills/download_skills.py

View file

@ -1,5 +0,0 @@
# Re-export for backwards compatibility
# The canonical location is now fastmcp.client.sampling.handlers
from fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler
__all__ = ["OpenAISamplingHandler"]

View file

@ -1,5 +0,0 @@
# Re-export for backwards compatibility
# The canonical location is now fastmcp.client.sampling.handlers.openai
from fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler
__all__ = ["OpenAISamplingHandler"]

View file

@ -1,14 +1,6 @@
import sys
from .function_prompt import FunctionPrompt, prompt from .function_prompt import FunctionPrompt, prompt
from .base import Message, Prompt, PromptArgument, PromptMessage, PromptResult from .base import Message, Prompt, PromptArgument, PromptMessage, PromptResult
# Backward compat: prompt.py was renamed to base.py to stop Pyright from resolving
# `from fastmcp.prompts import prompt` as the submodule instead of the decorator function.
# This shim keeps `from fastmcp.prompts.prompt import Prompt` working at runtime.
# Safe to remove once we're confident no external code imports from the old path.
sys.modules[f"{__name__}.prompt"] = sys.modules[f"{__name__}.base"]
__all__ = [ __all__ = [
"FunctionPrompt", "FunctionPrompt",
"Message", "Message",

View file

@ -1,5 +1,3 @@
import sys
from .function_resource import FunctionResource, resource from .function_resource import FunctionResource, resource
from .base import Resource, ResourceContent, ResourceResult from .base import Resource, ResourceContent, ResourceResult
from .security import ResourceSecurity from .security import ResourceSecurity
@ -26,9 +24,3 @@ __all__ = [
"TextResource", "TextResource",
"resource", "resource",
] ]
# Backward compat: resource.py was renamed to base.py to stop Pyright from resolving
# `from fastmcp.resources import resource` as the submodule instead of the decorator function.
# This shim keeps `from fastmcp.resources.resource import Resource` working at runtime.
# Safe to remove once we're confident no external code imports from the old path.
sys.modules[f"{__name__}.resource"] = sys.modules[f"{__name__}.base"]

View file

@ -8,7 +8,7 @@ from .auth import (
AccessToken, AccessToken,
AuthProvider, AuthProvider,
) )
from .authorization import ( from fastmcp.utilities.authorization import (
AuthCheck, AuthCheck,
AuthContext, AuthContext,
require_roles, require_roles,

View file

@ -1,23 +0,0 @@
"""Backward-compatible exports for component authorization primitives."""
from fastmcp.utilities.authorization import (
AuthCheck,
AuthContext,
require_roles,
require_scopes,
restrict_tag,
run_auth_checks,
run_auth_checks_with_shortfall,
scope_requirements,
)
__all__ = [
"AuthCheck",
"AuthContext",
"require_roles",
"require_scopes",
"restrict_tag",
"run_auth_checks",
"run_auth_checks_with_shortfall",
"scope_requirements",
]

View file

@ -1,7 +1,6 @@
from __future__ import annotations from __future__ import annotations
import logging import logging
import warnings
import weakref import weakref
from collections.abc import Callable, Generator, Mapping from collections.abc import Callable, Generator, Mapping
from contextlib import contextmanager from contextlib import contextmanager
@ -24,11 +23,7 @@ from pydantic.networks import AnyUrl
from typing_extensions import TypeVar from typing_extensions import TypeVar
from uncalled_for import SharedContext from uncalled_for import SharedContext
import fastmcp from fastmcp.exceptions import ToolError
from fastmcp.exceptions import (
FastMCPDeprecationWarning,
ToolError,
)
from fastmcp.resources.base import ResourceResult from fastmcp.resources.base import ResourceResult
from fastmcp.server.dependencies import FastMCPRequestContext, fastmcp_request_ctx from fastmcp.server.dependencies import FastMCPRequestContext, fastmcp_request_ctx
from fastmcp.server.elicitation import ( from fastmcp.server.elicitation import (
@ -952,21 +947,6 @@ class Context:
return False return False
return rc.protocol_version in MODERN_PROTOCOL_VERSIONS return rc.protocol_version in MODERN_PROTOCOL_VERSIONS
@overload
async def elicit(
self,
message: str,
response_type: None,
*,
response_title: str | None = None,
response_description: str | None = None,
) -> (
AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation
): ...
"""When response_type is None, the accepted elicitation will contain an
empty dict"""
@overload @overload
async def elicit( async def elicit(
self, self,
@ -977,8 +957,7 @@ class Context:
response_description: str | None = None, response_description: str | None = None,
) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation: ... ) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation: ...
"""When response_type is not None, the accepted elicitation will contain the """The accepted elicitation will contain the response data"""
response data"""
@overload @overload
async def elicit( async def elicit(
@ -1044,8 +1023,7 @@ class Context:
| list[str] | list[str]
| dict[str, dict[str, str]] | dict[str, dict[str, str]]
| list[list[str]] | list[list[str]]
| list[dict[str, dict[str, str]]] | list[dict[str, dict[str, str]]],
| None = None,
*, *,
response_title: str | None = None, response_title: str | None = None,
response_description: str | None = None, response_description: str | None = None,
@ -1070,11 +1048,9 @@ class Context:
"value" field will be generated for the MCP interaction and "value" field will be generated for the MCP interaction and
automatically deconstructed into the primitive type upon response. automatically deconstructed into the primitive type upon response.
Passing ``response_type=None`` (or omitting it) is deprecated and will ``response_type`` is required. Pass ``bool`` when all you need is a
be removed in a future version. The resulting empty-schema form-mode confirmation; an empty schema leaves some clients rendering an empty,
request is ambiguous and causes some clients (e.g. VS Code) to hang on non-functional form.
an empty form. Pass an explicit ``response_type`` describing the data
you want back.
Args: Args:
message: A human-readable message explaining what information is needed message: A human-readable message explaining what information is needed
@ -1096,17 +1072,6 @@ class Context:
the guard pattern: return an ``InputRequiredResult`` and read the guard pattern: return an ``InputRequiredResult`` and read
``ctx.input_responses`` / ``ctx.request_state`` when the task re-runs. ``ctx.input_responses`` / ``ctx.request_state`` when the task re-runs.
""" """
if response_type is None and fastmcp.settings.deprecation_warnings:
warnings.warn(
"Calling ctx.elicit() without a response_type is deprecated "
"and will be removed in a future version. The empty-schema "
"form-mode request is ambiguous under the current MCP spec "
"and causes some clients (e.g. VS Code) to render an empty, "
"non-functional form. Pass an explicit response_type "
"describing the data you expect back.",
FastMCPDeprecationWarning,
stacklevel=2,
)
config = parse_elicit_response_type( config = parse_elicit_response_type(
response_type, response_type,
response_title=response_title, response_title=response_title,

View file

@ -129,6 +129,17 @@ class ElicitConfig:
is_raw: bool is_raw: bool
#: Raised when a response type is missing. The empty-object schema this used to
#: produce was ambiguous under the MCP spec and left some clients (e.g. VS Code)
#: rendering an empty, non-functional form. Deprecated in 3.2, removed in 4.0.
_NONE_RESPONSE_TYPE_ERROR = (
"ctx.elicit() requires a response_type. The empty-schema form-mode request "
"produced by response_type=None was ambiguous under the MCP spec and caused "
"some clients to render an empty, non-functional form. Pass a type "
"describing the data you expect back — use `bool` for a confirmation."
)
def parse_elicit_response_type( def parse_elicit_response_type(
response_type: Any, response_type: Any,
response_title: str | None = None, response_title: str | None = None,
@ -136,8 +147,8 @@ def parse_elicit_response_type(
) -> ElicitConfig: ) -> ElicitConfig:
"""Parse response_type into schema and handling configuration. """Parse response_type into schema and handling configuration.
Supports multiple syntaxes: A response type is required; ``None`` raises ``TypeError``. Supports
- None: Empty object schema, expect empty response multiple syntaxes:
- dict: `{"low": {"title": "..."}}` -> single-select titled enum - dict: `{"low": {"title": "..."}}` -> single-select titled enum
- list patterns: - list patterns:
- `[["a", "b"]]` -> multi-select untitled - `[["a", "b"]]` -> multi-select untitled
@ -150,26 +161,16 @@ def parse_elicit_response_type(
The ``response_title`` and ``response_description`` arguments customize the The ``response_title`` and ``response_description`` arguments customize the
label and description of the wrapped ``value`` property for the scalar/dict/list label and description of the wrapped ``value`` property for the scalar/dict/list
shorthand forms. They are only valid when FastMCP is wrapping the response shorthand forms. They are only valid when FastMCP is wrapping the response
type; passing them with a full BaseModel/dataclass (or ``None``) raises type; passing them with a full BaseModel/dataclass raises ``TypeError``,
``TypeError``, because in those cases the user already controls field because in those cases the user already controls field metadata via
metadata via ``Field(title=..., description=...)``. ``Field(title=..., description=...)``.
""" """
has_response_metadata = ( has_response_metadata = (
response_title is not None or response_description is not None response_title is not None or response_description is not None
) )
if response_type is None: if response_type is None:
if has_response_metadata: raise TypeError(_NONE_RESPONSE_TYPE_ERROR)
raise TypeError(
"response_title and response_description are not supported when "
"response_type is None, because the elicitation schema has no "
"fields to label."
)
return ElicitConfig(
schema={"type": "object", "properties": {}},
response_type=None,
is_raw=False,
)
if isinstance(response_type, dict): if isinstance(response_type, dict):
config = _parse_dict_syntax(response_type) config = _parse_dict_syntax(response_type)

View file

@ -33,13 +33,6 @@ from fastmcp.exceptions import AuthorizationError, InsufficientScopeError
from fastmcp.prompts.base import Prompt, PromptResult from fastmcp.prompts.base import Prompt, PromptResult
from fastmcp.resources.base import Resource, ResourceResult from fastmcp.resources.base import Resource, ResourceResult
from fastmcp.resources.template import ResourceTemplate from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.auth.authorization import (
AuthCheck,
AuthContext,
run_auth_checks,
run_auth_checks_with_shortfall,
scope_requirements,
)
from fastmcp.server.dependencies import get_access_token from fastmcp.server.dependencies import get_access_token
from fastmcp.server.middleware.middleware import ( from fastmcp.server.middleware.middleware import (
CallNext, CallNext,
@ -47,6 +40,13 @@ from fastmcp.server.middleware.middleware import (
MiddlewareContext, MiddlewareContext,
) )
from fastmcp.tools.base import Tool, ToolResult from fastmcp.tools.base import Tool, ToolResult
from fastmcp.utilities.authorization import (
AuthCheck,
AuthContext,
run_auth_checks,
run_auth_checks_with_shortfall,
scope_requirements,
)
from fastmcp.utilities.versions import VersionSpec from fastmcp.utilities.versions import VersionSpec
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View file

@ -36,7 +36,6 @@ from fastmcp.server.providers.skills import (
ClaudeSkillsProvider, ClaudeSkillsProvider,
SkillProvider, SkillProvider,
SkillsDirectoryProvider, SkillsDirectoryProvider,
SkillsProvider,
) )
if TYPE_CHECKING: if TYPE_CHECKING:
@ -54,7 +53,6 @@ __all__ = [
"ProxyProvider", "ProxyProvider",
"SkillProvider", "SkillProvider",
"SkillsDirectoryProvider", "SkillsDirectoryProvider",
"SkillsProvider", # Backwards compatibility alias for SkillsDirectoryProvider
] ]

View file

@ -15,7 +15,7 @@ import mcp_types
from fastmcp.prompts.base import Prompt from fastmcp.prompts.base import Prompt
from fastmcp.prompts.function_prompt import FunctionPrompt from fastmcp.prompts.function_prompt import FunctionPrompt
from fastmcp.server.auth.authorization import AuthCheck from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.types import AnyFunction from fastmcp.utilities.types import AnyFunction
if TYPE_CHECKING: if TYPE_CHECKING:

View file

@ -20,7 +20,7 @@ from fastmcp.resources.security import (
ResourceSecurity, ResourceSecurity,
) )
from fastmcp.resources.template import ResourceTemplate from fastmcp.resources.template import ResourceTemplate
from fastmcp.server.auth.authorization import AuthCheck from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.types import AnyFunction from fastmcp.utilities.types import AnyFunction
if TYPE_CHECKING: if TYPE_CHECKING:

View file

@ -25,9 +25,9 @@ from typing import (
import mcp_types import mcp_types
from mcp_types import ToolAnnotations from mcp_types import ToolAnnotations
from fastmcp.server.auth.authorization import AuthCheck
from fastmcp.tools.base import Tool from fastmcp.tools.base import Tool
from fastmcp.tools.function_tool import FunctionTool from fastmcp.tools.function_tool import FunctionTool
from fastmcp.utilities.authorization import AuthCheck
from fastmcp.utilities.tasks import TaskConfig from fastmcp.utilities.tasks import TaskConfig
from fastmcp.utilities.types import AnyFunction, NotSet, NotSetT from fastmcp.utilities.types import AnyFunction, NotSet, NotSetT

View file

@ -40,10 +40,6 @@ from fastmcp.server.providers.skills.vendor_providers import (
) )
# Backwards compatibility alias
SkillsProvider = SkillsDirectoryProvider
__all__ = [ __all__ = [
"ClaudeSkillsProvider", "ClaudeSkillsProvider",
"CodexSkillsProvider", "CodexSkillsProvider",
@ -54,6 +50,5 @@ __all__ = [
"OpenCodeSkillsProvider", "OpenCodeSkillsProvider",
"SkillProvider", "SkillProvider",
"SkillsDirectoryProvider", "SkillsDirectoryProvider",
"SkillsProvider", # Backwards compatibility alias
"VSCodeSkillsProvider", "VSCodeSkillsProvider",
] ]

View file

@ -1,15 +1,7 @@
import sys
from .function_tool import FunctionTool, tool from .function_tool import FunctionTool, tool
from .base import InputRequiredToolResult, Tool, ToolResult from .base import InputRequiredToolResult, Tool, ToolResult
from .tool_transform import forward, forward_raw from .tool_transform import forward, forward_raw
# Backward compat: tool.py was renamed to base.py to stop Pyright from resolving
# `from fastmcp.tools import tool` as the submodule instead of the decorator function.
# This shim keeps `from fastmcp.tools.tool import Tool` working at runtime.
# Safe to remove once we're confident no external code imports from the old path.
sys.modules[f"{__name__}.tool"] = sys.modules[f"{__name__}.base"]
__all__ = [ __all__ = [
"FunctionTool", "FunctionTool",
"InputRequiredToolResult", "InputRequiredToolResult",

View file

@ -3,7 +3,6 @@ from enum import Enum
from typing import Any, Literal, cast from typing import Any, Literal, cast
import pytest import pytest
from mcp_types import ElicitRequestFormParams, ElicitRequestParams
from pydantic import BaseModel from pydantic import BaseModel
from typing_extensions import TypedDict from typing_extensions import TypedDict
@ -229,29 +228,6 @@ async def test_elicitation_response_title_rejected_for_basemodel():
await client.call_tool("ask", {}) await client.call_tool("ask", {})
async def test_elicitation_response_title_rejected_for_none():
"""response_title raises TypeError when response_type is None."""
mcp = FastMCP("TestServer")
@mcp.tool
async def ask(context: Context) -> str:
await context.elicit(
message="Confirm?",
response_type=None,
response_title="Not allowed",
)
return "done"
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="accept", content={})
# Not pinned: response_title is validated locally before any request is
# dispatched, so this raises identically on every era.
async with Client(mcp, elicitation_handler=elicitation_handler) as client:
with pytest.raises(ToolError, match="response_title"):
await client.call_tool("ask", {})
async def test_elicitation_cancel_action(): async def test_elicitation_cancel_action():
"""Test user canceling elicitation request.""" """Test user canceling elicitation request."""
mcp = FastMCP("TestServer") mcp = FastMCP("TestServer")
@ -301,79 +277,6 @@ class TestScalarResponseTypes:
result = await client.call_tool("my_tool", {}) result = await client.call_tool("my_tool", {})
assert result.data == "Alice" assert result.data == "Alice"
async def test_elicitation_no_response(self):
"""Test elicitation with no response type."""
mcp = FastMCP("TestServer")
@mcp.tool
async def my_tool(context: Context) -> dict[str, Any]:
result = await context.elicit(message="", response_type=None)
assert isinstance(result, AcceptedElicitation)
assert isinstance(result.data, dict)
return cast(dict[str, Any], result.data)
async def elicitation_handler(
message, response_type, params: ElicitRequestParams, ctx
):
assert isinstance(params, ElicitRequestFormParams)
assert params.requested_schema == {"type": "object", "properties": {}}
assert response_type is None
return ElicitResult(action="accept")
async with Client(
mcp, mode="legacy", elicitation_handler=elicitation_handler
) as client:
result = await client.call_tool("my_tool", {})
assert result.data is None
async def test_elicitation_empty_response(self):
"""Test elicitation with empty response type."""
mcp = FastMCP("TestServer")
@mcp.tool
async def my_tool(context: Context) -> dict[str, Any]:
result = await context.elicit(message="", response_type=None)
assert result.action == "accept"
assert isinstance(result, AcceptedElicitation)
accepted = result
assert isinstance(accepted.data, dict)
return accepted.data
async def elicitation_handler(
message, response_type, params: ElicitRequestParams, ctx
):
return ElicitResult(action="accept", content={})
async with Client(
mcp, mode="legacy", elicitation_handler=elicitation_handler
) as client:
result = await client.call_tool("my_tool", {})
assert result.data is None
async def test_elicitation_response_when_no_response_requested(self):
"""Test elicitation with no response type."""
mcp = FastMCP("TestServer")
@mcp.tool
async def my_tool(context: Context) -> dict[str, Any]:
result = await context.elicit(message="", response_type=None)
assert result.action == "accept"
assert isinstance(result, AcceptedElicitation)
accepted = result
assert isinstance(accepted.data, dict)
return accepted.data
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="accept", content={"value": "hello"})
async with Client(
mcp, mode="legacy", elicitation_handler=elicitation_handler
) as client:
with pytest.raises(
ToolError, match="Elicitation expected an empty response"
):
await client.call_tool("my_tool", {})
async def test_elicitation_str_response(self): async def test_elicitation_str_response(self):
"""Test elicitation with string schema.""" """Test elicitation with string schema."""
mcp = FastMCP("TestServer") mcp = FastMCP("TestServer")
@ -733,6 +636,28 @@ async def test_all_primitive_field_types():
} }
class TestResponseTypeRequired:
"""`response_type` is required — the empty-schema form was removed in 4.0."""
async def test_explicit_none_raises_type_error(self):
mcp = FastMCP("TestServer")
@mcp.tool
async def my_tool(context: Context) -> str:
await context.elicit(message="Approve?", response_type=None) # ty: ignore[no-matching-overload]
return "unreachable"
async with Client(mcp, mode="legacy") as client:
with pytest.raises(ToolError, match="requires a response_type"):
await client.call_tool("my_tool", {})
async def test_omitting_response_type_raises_type_error(self):
ctx = Context(fastmcp=FastMCP("TestServer"))
with pytest.raises(TypeError, match="response_type"):
await ctx.elicit("Approve?") # ty: ignore[no-matching-overload]
class TestValidation: class TestValidation:
async def test_schema_validation_rejects_non_object(self): async def test_schema_validation_rejects_non_object(self):
"""Test that non-object schemas are rejected.""" """Test that non-object schemas are rejected."""

View file

@ -1,33 +0,0 @@
"""Tests for deprecated elicitation behavior."""
from typing import Any, cast
import pytest
from fastmcp import Context, FastMCP
from fastmcp.client.client import Client
from fastmcp.client.elicitation import ElicitResult
from fastmcp.exceptions import FastMCPDeprecationWarning
from fastmcp.server.elicitation import AcceptedElicitation
async def test_elicitation_none_response_type_warns_deprecation():
"""Passing response_type=None is deprecated — warn at call time."""
mcp = FastMCP("TestServer")
@mcp.tool
async def my_tool(context: Context) -> dict[str, Any]:
with pytest.warns(FastMCPDeprecationWarning, match="response_type"):
result = await context.elicit(message="", response_type=None)
assert isinstance(result, AcceptedElicitation)
return cast(dict[str, Any], result.data)
async def elicitation_handler(message, response_type, params, ctx):
return ElicitResult(action="accept", content={})
# `ctx.elicit` sends a server-initiated request down the client's
# back-channel, which only the older protocol has, so this pins that era.
async with Client(
mcp, mode="legacy", elicitation_handler=elicitation_handler
) as client:
await client.call_tool("my_tool", {})

View file

@ -12,7 +12,6 @@ from fastmcp.server.providers.skills import (
ClaudeSkillsProvider, ClaudeSkillsProvider,
SkillProvider, SkillProvider,
SkillsDirectoryProvider, SkillsDirectoryProvider,
SkillsProvider,
) )
from fastmcp.server.providers.skills._common import parse_frontmatter from fastmcp.server.providers.skills._common import parse_frontmatter
from fastmcp.server.providers.skills.skill_provider import SkillFileResource from fastmcp.server.providers.skills.skill_provider import SkillFileResource
@ -724,13 +723,6 @@ description: Second occurrence
assert resources == [] assert resources == []
class TestSkillsProviderAlias:
"""Test that SkillsProvider is a backwards-compatible alias."""
def test_skills_provider_is_alias(self):
assert SkillsProvider is SkillsDirectoryProvider
class TestClaudeSkillsProvider: class TestClaudeSkillsProvider:
def test_default_root_is_claude_skills_dir(self, tmp_path: Path, monkeypatch): def test_default_root_is_claude_skills_dir(self, tmp_path: Path, monkeypatch):
# Mock Path.home() to return a temp path (use tmp_path for cross-platform compatibility) # Mock Path.home() to return a temp path (use tmp_path for cross-platform compatibility)

View file

@ -34,18 +34,35 @@ from fastmcp import Client, FastMCP, settings
# longer resolves. `create_proxy`, `settings`, `McpError`, and # longer resolves. `create_proxy`, `settings`, `McpError`, and
# `CacheableToolResult` above are part of the same set. # `CacheableToolResult` above are part of the same set.
from fastmcp.apps import AppConfig from fastmcp.apps import AppConfig
from fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler
from fastmcp.client.transports import StreamableHttpTransport from fastmcp.client.transports import StreamableHttpTransport
from fastmcp.dependencies import Depends from fastmcp.dependencies import Depends
from fastmcp.exceptions import McpError from fastmcp.exceptions import McpError
from fastmcp.prompts.function_prompt import FunctionPrompt from fastmcp.prompts.function_prompt import FunctionPrompt
from fastmcp.resources.function_resource import FunctionResource from fastmcp.resources.function_resource import FunctionResource
from fastmcp.server import create_proxy from fastmcp.server import create_proxy
from fastmcp.server.auth import (
AuthCheck,
AuthContext,
require_roles,
require_scopes,
restrict_tag,
run_auth_checks,
)
from fastmcp.server.middleware.caching import CacheableToolResult from fastmcp.server.middleware.caching import CacheableToolResult
from fastmcp.server.providers.openapi import OpenAPIProvider from fastmcp.server.providers.openapi import OpenAPIProvider
from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient
from fastmcp.server.transforms import PromptsAsTools, ResourcesAsTools, ToolTransform from fastmcp.server.transforms import PromptsAsTools, ResourcesAsTools, ToolTransform
from fastmcp.tools.function_tool import FunctionTool from fastmcp.tools.function_tool import FunctionTool
# The two authorization names the removed shim exported that `fastmcp.server.auth`
# deliberately does not re-export (middleware plumbing, no documented user-facing
# use). The upgrade guide sends them here instead, so pin that path too.
from fastmcp.utilities.authorization import (
run_auth_checks_with_shortfall,
scope_requirements,
)
class TestCommonServersUpgradeCleanly: class TestCommonServersUpgradeCleanly:
"""Servers written against the 3.x API run unchanged on v4 defaults.""" """Servers written against the 3.x API run unchanged on v4 defaults."""
@ -152,6 +169,38 @@ class TestCanonicalReplacementsResolve:
) )
assert all(sym is not None for sym in symbols) assert all(sym is not None for sym in symbols)
def test_authorization_symbols_resolve_from_their_documented_paths(self):
# The removed `fastmcp.server.auth.authorization` shim exported eight
# names, and the upgrade guide splits them across two replacements. Pin
# both halves: the checks users write against reach the auth package,
# while the two middleware helpers stay on the utilities module.
from_auth_package = (
AuthCheck,
AuthContext,
require_roles,
require_scopes,
restrict_tag,
run_auth_checks,
)
from_utilities = (run_auth_checks_with_shortfall, scope_requirements)
assert all(sym is not None for sym in from_auth_package + from_utilities)
import fastmcp.server.auth as auth_package
for name in ("run_auth_checks_with_shortfall", "scope_requirements"):
assert not hasattr(auth_package, name)
def test_sampling_handler_resolves_from_its_submodule(self):
# The removed shim re-exported `OpenAISamplingHandler` from its package
# `__init__`. The canonical package keeps its `__init__` empty so that
# touching it never pulls in a vendor SDK, so the guide must name the
# submodule — pin both halves of that.
assert OpenAISamplingHandler is not None
import fastmcp.client.sampling.handlers as handlers_package
assert not hasattr(handlers_package, "OpenAISamplingHandler")
# --- Hard removals: modules that no longer exist --- # --- Hard removals: modules that no longer exist ---
@ -163,6 +212,15 @@ REMOVED_MODULES = [
"fastmcp.server.apps", # -> fastmcp.apps "fastmcp.server.apps", # -> fastmcp.apps
"fastmcp.server.app", # -> fastmcp.apps / fastmcp "fastmcp.server.app", # -> fastmcp.apps / fastmcp
"mcp.types", # -> mcp_types "mcp.types", # -> mcp_types
# The pre-rename component modules. `tool.py`/`resource.py`/`prompt.py` are
# now `base.py`; import the types from the package itself (`from
# fastmcp.tools import Tool`) rather than naming the private module.
"fastmcp.tools.tool", # -> fastmcp.tools
"fastmcp.resources.resource", # -> fastmcp.resources
"fastmcp.prompts.prompt", # -> fastmcp.prompts
"fastmcp.experimental.sampling", # -> fastmcp.client.sampling
"fastmcp.experimental.sampling.handlers", # -> fastmcp.client.sampling.handlers
"fastmcp.server.auth.authorization", # -> fastmcp.server.auth / fastmcp.utilities.authorization
] ]
# Names that were re-export shims and are gone; import them from the canonical # Names that were re-export shims and are gone; import them from the canonical
@ -174,10 +232,9 @@ REMOVED_NAMES = [
# old misspelled names renamed to Cacheable* (no alias) codespell:ignore # old misspelled names renamed to Cacheable* (no alias) codespell:ignore
("fastmcp.server.middleware.caching", "CachableToolResult"), # codespell:ignore ("fastmcp.server.middleware.caching", "CachableToolResult"), # codespell:ignore
("fastmcp.server.middleware.caching", "CachablePromptResult"), # codespell:ignore ("fastmcp.server.middleware.caching", "CachablePromptResult"), # codespell:ignore
# component-import shims -> fastmcp.tools.function_tool, etc. # 3.0-era rename alias -> SkillsDirectoryProvider
("fastmcp.tools.tool", "FunctionTool"), ("fastmcp.server.providers.skills", "SkillsProvider"),
("fastmcp.resources.resource", "FunctionResource"), ("fastmcp.server.providers", "SkillsProvider"),
("fastmcp.prompts.prompt", "FunctionPrompt"),
] ]

View file

@ -25,7 +25,7 @@ from fastmcp.tools.function_tool import DecoratedTool, FunctionTool, ToolMeta
"from fastmcp.resources import Resource, resource", "from fastmcp.resources import Resource, resource",
"from fastmcp.prompts import Prompt, prompt", "from fastmcp.prompts import Prompt, prompt",
"import sys; import fastmcp.apps.config; assert 'fastmcp.tools.function_tool' not in sys.modules", "import sys; import fastmcp.apps.config; assert 'fastmcp.tools.function_tool' not in sys.modules",
"from fastmcp.server.auth.authorization import AuthCheck", "from fastmcp.server.auth import AuthCheck",
"from fastmcp.server import Context, FastMCP, create_proxy", "from fastmcp.server import Context, FastMCP, create_proxy",
], ],
) )