mirror of
https://github.com/PrefectHQ/fastmcp.git
synced 2026-08-09 15:19:10 +02:00
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:
parent
1550eea886
commit
ea7fb8cb2e
28 changed files with 150 additions and 285 deletions
|
|
@ -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`.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
|
||||||
|
|
@ -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"]
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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",
|
|
||||||
]
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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."""
|
||||||
|
|
|
||||||
|
|
@ -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", {})
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue