From ea7fb8cb2e71e90e9a83ba424f8afa3886ee828f Mon Sep 17 00:00:00 2001 From: Jeremiah Lowin <153965+jlowin@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:59:43 -0400 Subject: [PATCH] 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 --- dev-docs/v4-notes/change-register.md | 6 +- .../upgrading/from-fastmcp-3.mdx | 22 +++- docs/servers/elicitation.mdx | 4 +- docs/servers/tools.mdx | 4 +- docs/servers/transforms/code-mode.mdx | 2 +- docs/servers/transforms/transforms.mdx | 2 +- examples/skills/download_skills.py | 2 +- .../fastmcp/experimental/sampling/__init__.py | 0 .../sampling/handlers/__init__.py | 5 - .../experimental/sampling/handlers/openai.py | 5 - fastmcp_slim/fastmcp/prompts/__init__.py | 8 -- fastmcp_slim/fastmcp/resources/__init__.py | 8 -- fastmcp_slim/fastmcp/server/auth/__init__.py | 2 +- .../fastmcp/server/auth/authorization.py | 23 ---- fastmcp_slim/fastmcp/server/context.py | 47 +------ fastmcp_slim/fastmcp/server/elicitation.py | 33 ++--- .../server/middleware/authorization.py | 14 +-- .../fastmcp/server/providers/__init__.py | 2 - .../local_provider/decorators/prompts.py | 2 +- .../local_provider/decorators/resources.py | 2 +- .../local_provider/decorators/tools.py | 2 +- .../server/providers/skills/__init__.py | 5 - fastmcp_slim/fastmcp/tools/__init__.py | 8 -- tests/client/test_elicitation.py | 119 ++++-------------- tests/deprecated/test_elicitation.py | 33 ----- .../server/providers/test_skills_provider.py | 8 -- tests/test_upgrade_from_v3.py | 65 +++++++++- tests/tools/test_standalone_decorator.py | 2 +- 28 files changed, 150 insertions(+), 285 deletions(-) delete mode 100644 fastmcp_slim/fastmcp/experimental/sampling/__init__.py delete mode 100644 fastmcp_slim/fastmcp/experimental/sampling/handlers/__init__.py delete mode 100644 fastmcp_slim/fastmcp/experimental/sampling/handlers/openai.py delete mode 100644 fastmcp_slim/fastmcp/server/auth/authorization.py delete mode 100644 tests/deprecated/test_elicitation.py diff --git a/dev-docs/v4-notes/change-register.md b/dev-docs/v4-notes/change-register.md index 39ecabdbd..771a2234c 100644 --- a/dev-docs/v4-notes/change-register.md +++ b/dev-docs/v4-notes/change-register.md @@ -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 `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. -- **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`. diff --git a/docs/getting-started/upgrading/from-fastmcp-3.mdx b/docs/getting-started/upgrading/from-fastmcp-3.mdx index eb9f0a0f0..010718fc9 100644 --- a/docs/getting-started/upgrading/from-fastmcp-3.mdx +++ b/docs/getting-started/upgrading/from-fastmcp-3.mdx @@ -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.utilities.openapi` | `fastmcp.utilities.openapi` | | `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` | | `FunctionResource` / `resource` from `fastmcp.resources.resource` | `fastmcp.resources.function_resource` | | `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). @@ -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. -### 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 `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. - **`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 diff --git a/docs/servers/elicitation.mdx b/docs/servers/elicitation.mdx index 5584ba78b..3798d6e7c 100644 --- a/docs/servers/elicitation.mdx +++ b/docs/servers/elicitation.mdx @@ -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`. -### 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 @mcp.tool diff --git a/docs/servers/tools.mdx b/docs/servers/tools.mdx index 42fd0fb17..c9c4e9b01 100644 --- a/docs/servers/tools.mdx +++ b/docs/servers/tools.mdx @@ -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. ```python -from fastmcp.tools.tool import ToolResult +from fastmcp.tools import ToolResult from mcp_types import TextContent @mcp.tool @@ -788,7 +788,7 @@ When you need custom serialization (like YAML, Markdown tables, or specialized f ```python import yaml from fastmcp import FastMCP -from fastmcp.tools.tool import ToolResult +from fastmcp.tools import ToolResult mcp = FastMCP("MyServer") diff --git a/docs/servers/transforms/code-mode.mdx b/docs/servers/transforms/code-mode.mdx index c7ef55bf0..5b04fcab5 100644 --- a/docs/servers/transforms/code-mode.mdx +++ b/docs/servers/transforms/code-mode.mdx @@ -250,7 +250,7 @@ Here's a minimal example: from fastmcp.experimental.transforms.code_mode import CodeMode from fastmcp.experimental.transforms.code_mode import GetToolCatalog, GetSchemas 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: async def list_tools(ctx: Context) -> str: diff --git a/docs/servers/transforms/transforms.mdx b/docs/servers/transforms/transforms.mdx index 4347b2f18..fc8f0ceda 100644 --- a/docs/servers/transforms/transforms.mdx +++ b/docs/servers/transforms/transforms.mdx @@ -118,7 +118,7 @@ Create custom transforms by subclassing `Transform` and overriding the methods y ```python from collections.abc import Sequence from fastmcp.server.transforms import Transform, GetToolNext -from fastmcp.tools.tool import Tool +from fastmcp.tools import Tool class TagFilter(Transform): """Filter tools to only those with specific tags.""" diff --git a/examples/skills/download_skills.py b/examples/skills/download_skills.py index 69b8d0373..c6e571c57 100644 --- a/examples/skills/download_skills.py +++ b/examples/skills/download_skills.py @@ -1,7 +1,7 @@ """Example: Downloading skills from an MCP server. 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: uv run python examples/skills/download_skills.py diff --git a/fastmcp_slim/fastmcp/experimental/sampling/__init__.py b/fastmcp_slim/fastmcp/experimental/sampling/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/fastmcp_slim/fastmcp/experimental/sampling/handlers/__init__.py b/fastmcp_slim/fastmcp/experimental/sampling/handlers/__init__.py deleted file mode 100644 index 627dfd011..000000000 --- a/fastmcp_slim/fastmcp/experimental/sampling/handlers/__init__.py +++ /dev/null @@ -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"] diff --git a/fastmcp_slim/fastmcp/experimental/sampling/handlers/openai.py b/fastmcp_slim/fastmcp/experimental/sampling/handlers/openai.py deleted file mode 100644 index b466f7a77..000000000 --- a/fastmcp_slim/fastmcp/experimental/sampling/handlers/openai.py +++ /dev/null @@ -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"] diff --git a/fastmcp_slim/fastmcp/prompts/__init__.py b/fastmcp_slim/fastmcp/prompts/__init__.py index d1b866075..b94b5952d 100644 --- a/fastmcp_slim/fastmcp/prompts/__init__.py +++ b/fastmcp_slim/fastmcp/prompts/__init__.py @@ -1,14 +1,6 @@ -import sys - from .function_prompt import FunctionPrompt, prompt 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__ = [ "FunctionPrompt", "Message", diff --git a/fastmcp_slim/fastmcp/resources/__init__.py b/fastmcp_slim/fastmcp/resources/__init__.py index cbe819c95..b0e5b4524 100644 --- a/fastmcp_slim/fastmcp/resources/__init__.py +++ b/fastmcp_slim/fastmcp/resources/__init__.py @@ -1,5 +1,3 @@ -import sys - from .function_resource import FunctionResource, resource from .base import Resource, ResourceContent, ResourceResult from .security import ResourceSecurity @@ -26,9 +24,3 @@ __all__ = [ "TextResource", "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"] diff --git a/fastmcp_slim/fastmcp/server/auth/__init__.py b/fastmcp_slim/fastmcp/server/auth/__init__.py index d0a2953e9..8c1e2631d 100644 --- a/fastmcp_slim/fastmcp/server/auth/__init__.py +++ b/fastmcp_slim/fastmcp/server/auth/__init__.py @@ -8,7 +8,7 @@ from .auth import ( AccessToken, AuthProvider, ) -from .authorization import ( +from fastmcp.utilities.authorization import ( AuthCheck, AuthContext, require_roles, diff --git a/fastmcp_slim/fastmcp/server/auth/authorization.py b/fastmcp_slim/fastmcp/server/auth/authorization.py deleted file mode 100644 index b6e731a2e..000000000 --- a/fastmcp_slim/fastmcp/server/auth/authorization.py +++ /dev/null @@ -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", -] diff --git a/fastmcp_slim/fastmcp/server/context.py b/fastmcp_slim/fastmcp/server/context.py index df43d32f4..4b5a88082 100644 --- a/fastmcp_slim/fastmcp/server/context.py +++ b/fastmcp_slim/fastmcp/server/context.py @@ -1,7 +1,6 @@ from __future__ import annotations import logging -import warnings import weakref from collections.abc import Callable, Generator, Mapping from contextlib import contextmanager @@ -24,11 +23,7 @@ from pydantic.networks import AnyUrl from typing_extensions import TypeVar from uncalled_for import SharedContext -import fastmcp -from fastmcp.exceptions import ( - FastMCPDeprecationWarning, - ToolError, -) +from fastmcp.exceptions import ToolError from fastmcp.resources.base import ResourceResult from fastmcp.server.dependencies import FastMCPRequestContext, fastmcp_request_ctx from fastmcp.server.elicitation import ( @@ -952,21 +947,6 @@ class Context: return False 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 async def elicit( self, @@ -977,8 +957,7 @@ class Context: response_description: str | None = None, ) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation: ... - """When response_type is not None, the accepted elicitation will contain the - response data""" + """The accepted elicitation will contain the response data""" @overload async def elicit( @@ -1044,8 +1023,7 @@ class Context: | list[str] | dict[str, dict[str, str]] | list[list[str]] - | list[dict[str, dict[str, str]]] - | None = None, + | list[dict[str, dict[str, str]]], *, response_title: str | None = None, response_description: str | None = None, @@ -1070,11 +1048,9 @@ class Context: "value" field will be generated for the MCP interaction and automatically deconstructed into the primitive type upon response. - Passing ``response_type=None`` (or omitting it) is deprecated and will - be removed in a future version. The resulting empty-schema form-mode - request is ambiguous and causes some clients (e.g. VS Code) to hang on - an empty form. Pass an explicit ``response_type`` describing the data - you want back. + ``response_type`` is required. Pass ``bool`` when all you need is a + confirmation; an empty schema leaves some clients rendering an empty, + non-functional form. Args: message: A human-readable message explaining what information is needed @@ -1096,17 +1072,6 @@ class Context: the guard pattern: return an ``InputRequiredResult`` and read ``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( response_type, response_title=response_title, diff --git a/fastmcp_slim/fastmcp/server/elicitation.py b/fastmcp_slim/fastmcp/server/elicitation.py index 8a3fe0a5e..5eaccf399 100644 --- a/fastmcp_slim/fastmcp/server/elicitation.py +++ b/fastmcp_slim/fastmcp/server/elicitation.py @@ -129,6 +129,17 @@ class ElicitConfig: 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( response_type: Any, response_title: str | None = None, @@ -136,8 +147,8 @@ def parse_elicit_response_type( ) -> ElicitConfig: """Parse response_type into schema and handling configuration. - Supports multiple syntaxes: - - None: Empty object schema, expect empty response + A response type is required; ``None`` raises ``TypeError``. Supports + multiple syntaxes: - dict: `{"low": {"title": "..."}}` -> single-select titled enum - list patterns: - `[["a", "b"]]` -> multi-select untitled @@ -150,26 +161,16 @@ def parse_elicit_response_type( The ``response_title`` and ``response_description`` arguments customize the 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 - type; passing them with a full BaseModel/dataclass (or ``None``) raises - ``TypeError``, because in those cases the user already controls field - metadata via ``Field(title=..., description=...)``. + type; passing them with a full BaseModel/dataclass raises ``TypeError``, + because in those cases the user already controls field metadata via + ``Field(title=..., description=...)``. """ has_response_metadata = ( response_title is not None or response_description is not None ) if response_type is None: - if has_response_metadata: - 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, - ) + raise TypeError(_NONE_RESPONSE_TYPE_ERROR) if isinstance(response_type, dict): config = _parse_dict_syntax(response_type) diff --git a/fastmcp_slim/fastmcp/server/middleware/authorization.py b/fastmcp_slim/fastmcp/server/middleware/authorization.py index 163571f25..42f3cad81 100644 --- a/fastmcp_slim/fastmcp/server/middleware/authorization.py +++ b/fastmcp_slim/fastmcp/server/middleware/authorization.py @@ -33,13 +33,6 @@ from fastmcp.exceptions import AuthorizationError, InsufficientScopeError from fastmcp.prompts.base import Prompt, PromptResult from fastmcp.resources.base import Resource, ResourceResult 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.middleware.middleware import ( CallNext, @@ -47,6 +40,13 @@ from fastmcp.server.middleware.middleware import ( MiddlewareContext, ) 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 logger = logging.getLogger(__name__) diff --git a/fastmcp_slim/fastmcp/server/providers/__init__.py b/fastmcp_slim/fastmcp/server/providers/__init__.py index fe0c248f9..f138404f2 100644 --- a/fastmcp_slim/fastmcp/server/providers/__init__.py +++ b/fastmcp_slim/fastmcp/server/providers/__init__.py @@ -36,7 +36,6 @@ from fastmcp.server.providers.skills import ( ClaudeSkillsProvider, SkillProvider, SkillsDirectoryProvider, - SkillsProvider, ) if TYPE_CHECKING: @@ -54,7 +53,6 @@ __all__ = [ "ProxyProvider", "SkillProvider", "SkillsDirectoryProvider", - "SkillsProvider", # Backwards compatibility alias for SkillsDirectoryProvider ] diff --git a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/prompts.py b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/prompts.py index 53e4a7080..d0816325d 100644 --- a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/prompts.py +++ b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/prompts.py @@ -15,7 +15,7 @@ import mcp_types from fastmcp.prompts.base import Prompt 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 if TYPE_CHECKING: diff --git a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/resources.py b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/resources.py index 477833c11..f1f7b106b 100644 --- a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/resources.py +++ b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/resources.py @@ -20,7 +20,7 @@ from fastmcp.resources.security import ( ResourceSecurity, ) 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 if TYPE_CHECKING: diff --git a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py index cc82b7dec..77081165a 100644 --- a/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py +++ b/fastmcp_slim/fastmcp/server/providers/local_provider/decorators/tools.py @@ -25,9 +25,9 @@ from typing import ( import mcp_types from mcp_types import ToolAnnotations -from fastmcp.server.auth.authorization import AuthCheck from fastmcp.tools.base import Tool from fastmcp.tools.function_tool import FunctionTool +from fastmcp.utilities.authorization import AuthCheck from fastmcp.utilities.tasks import TaskConfig from fastmcp.utilities.types import AnyFunction, NotSet, NotSetT diff --git a/fastmcp_slim/fastmcp/server/providers/skills/__init__.py b/fastmcp_slim/fastmcp/server/providers/skills/__init__.py index b15c1c636..5945944b5 100644 --- a/fastmcp_slim/fastmcp/server/providers/skills/__init__.py +++ b/fastmcp_slim/fastmcp/server/providers/skills/__init__.py @@ -40,10 +40,6 @@ from fastmcp.server.providers.skills.vendor_providers import ( ) -# Backwards compatibility alias -SkillsProvider = SkillsDirectoryProvider - - __all__ = [ "ClaudeSkillsProvider", "CodexSkillsProvider", @@ -54,6 +50,5 @@ __all__ = [ "OpenCodeSkillsProvider", "SkillProvider", "SkillsDirectoryProvider", - "SkillsProvider", # Backwards compatibility alias "VSCodeSkillsProvider", ] diff --git a/fastmcp_slim/fastmcp/tools/__init__.py b/fastmcp_slim/fastmcp/tools/__init__.py index 45a3f74ad..d3f7303fc 100644 --- a/fastmcp_slim/fastmcp/tools/__init__.py +++ b/fastmcp_slim/fastmcp/tools/__init__.py @@ -1,15 +1,7 @@ -import sys - from .function_tool import FunctionTool, tool from .base import InputRequiredToolResult, Tool, ToolResult 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__ = [ "FunctionTool", "InputRequiredToolResult", diff --git a/tests/client/test_elicitation.py b/tests/client/test_elicitation.py index d98c4199d..87c89e3a1 100644 --- a/tests/client/test_elicitation.py +++ b/tests/client/test_elicitation.py @@ -3,7 +3,6 @@ from enum import Enum from typing import Any, Literal, cast import pytest -from mcp_types import ElicitRequestFormParams, ElicitRequestParams from pydantic import BaseModel from typing_extensions import TypedDict @@ -229,29 +228,6 @@ async def test_elicitation_response_title_rejected_for_basemodel(): 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(): """Test user canceling elicitation request.""" mcp = FastMCP("TestServer") @@ -301,79 +277,6 @@ class TestScalarResponseTypes: result = await client.call_tool("my_tool", {}) 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): """Test elicitation with string schema.""" 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: async def test_schema_validation_rejects_non_object(self): """Test that non-object schemas are rejected.""" diff --git a/tests/deprecated/test_elicitation.py b/tests/deprecated/test_elicitation.py deleted file mode 100644 index 187c5404d..000000000 --- a/tests/deprecated/test_elicitation.py +++ /dev/null @@ -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", {}) diff --git a/tests/server/providers/test_skills_provider.py b/tests/server/providers/test_skills_provider.py index 051f68fdb..cfc03e7d2 100644 --- a/tests/server/providers/test_skills_provider.py +++ b/tests/server/providers/test_skills_provider.py @@ -12,7 +12,6 @@ from fastmcp.server.providers.skills import ( ClaudeSkillsProvider, SkillProvider, SkillsDirectoryProvider, - SkillsProvider, ) from fastmcp.server.providers.skills._common import parse_frontmatter from fastmcp.server.providers.skills.skill_provider import SkillFileResource @@ -724,13 +723,6 @@ description: Second occurrence 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: 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) diff --git a/tests/test_upgrade_from_v3.py b/tests/test_upgrade_from_v3.py index f324241ba..bc5444e91 100644 --- a/tests/test_upgrade_from_v3.py +++ b/tests/test_upgrade_from_v3.py @@ -34,18 +34,35 @@ from fastmcp import Client, FastMCP, settings # longer resolves. `create_proxy`, `settings`, `McpError`, and # `CacheableToolResult` above are part of the same set. from fastmcp.apps import AppConfig +from fastmcp.client.sampling.handlers.openai import OpenAISamplingHandler from fastmcp.client.transports import StreamableHttpTransport from fastmcp.dependencies import Depends from fastmcp.exceptions import McpError from fastmcp.prompts.function_prompt import FunctionPrompt from fastmcp.resources.function_resource import FunctionResource 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.providers.openapi import OpenAPIProvider from fastmcp.server.providers.proxy import FastMCPProxy, ProxyClient from fastmcp.server.transforms import PromptsAsTools, ResourcesAsTools, ToolTransform 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: """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) + 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 --- @@ -163,6 +212,15 @@ REMOVED_MODULES = [ "fastmcp.server.apps", # -> fastmcp.apps "fastmcp.server.app", # -> fastmcp.apps / fastmcp "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 @@ -174,10 +232,9 @@ REMOVED_NAMES = [ # old misspelled names renamed to Cacheable* (no alias) codespell:ignore ("fastmcp.server.middleware.caching", "CachableToolResult"), # codespell:ignore ("fastmcp.server.middleware.caching", "CachablePromptResult"), # codespell:ignore - # component-import shims -> fastmcp.tools.function_tool, etc. - ("fastmcp.tools.tool", "FunctionTool"), - ("fastmcp.resources.resource", "FunctionResource"), - ("fastmcp.prompts.prompt", "FunctionPrompt"), + # 3.0-era rename alias -> SkillsDirectoryProvider + ("fastmcp.server.providers.skills", "SkillsProvider"), + ("fastmcp.server.providers", "SkillsProvider"), ] diff --git a/tests/tools/test_standalone_decorator.py b/tests/tools/test_standalone_decorator.py index 6b79001cc..fc90ae13f 100644 --- a/tests/tools/test_standalone_decorator.py +++ b/tests/tools/test_standalone_decorator.py @@ -25,7 +25,7 @@ from fastmcp.tools.function_tool import DecoratedTool, FunctionTool, ToolMeta "from fastmcp.resources import Resource, resource", "from fastmcp.prompts import Prompt, prompt", "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", ], )