diff --git a/docs/docs.json b/docs/docs.json index 4c3e5e153..c8d1ea394 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -73,7 +73,10 @@ { "group": "Essentials", "icon": "cube", - "pages": ["servers/server", "deployment/running-server"] + "pages": [ + "servers/server", + "deployment/running-server" + ] }, { "group": "Core Components", @@ -116,7 +119,10 @@ { "group": "Essentials", "icon": "cube", - "pages": ["clients/client", "clients/transports"] + "pages": [ + "clients/client", + "clients/transports" + ] }, { "group": "Core Operations", @@ -142,7 +148,10 @@ { "group": "Authentication", "icon": "user-shield", - "pages": ["clients/auth/oauth", "clients/auth/bearer"] + "pages": [ + "clients/auth/oauth", + "clients/auth/bearer" + ] } ] }, @@ -188,12 +197,17 @@ }, { "anchor": "What's New", - "pages": ["updates", "changelog"] + "pages": [ + "updates", + "changelog" + ] }, { "anchor": "Community", "icon": "users", - "pages": ["community/showcase"] + "pages": [ + "community/showcase" + ] } ] }, diff --git a/docs/python-sdk/fastmcp-client-transports.mdx b/docs/python-sdk/fastmcp-client-transports.mdx index 13d6a3818..e7fb245f8 100644 --- a/docs/python-sdk/fastmcp-client-transports.mdx +++ b/docs/python-sdk/fastmcp-client-transports.mdx @@ -7,7 +7,7 @@ sidebarTitle: transports ## Functions -### `infer_transport` +### `infer_transport` ```python infer_transport(transport: ClientTransport | FastMCP | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str) -> ClientTransport @@ -161,61 +161,61 @@ transports like Python, Node, Uvx, etc. connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession] ``` -#### `connect` +#### `connect` ```python connect(self, **session_kwargs: Unpack[SessionKwargs]) -> ClientSession | None ``` -#### `disconnect` +#### `disconnect` ```python disconnect(self) ``` -#### `close` +#### `close` ```python close(self) ``` -### `PythonStdioTransport` +### `PythonStdioTransport` Transport for running Python scripts. -### `FastMCPStdioTransport` +### `FastMCPStdioTransport` Transport for running FastMCP servers using the FastMCP CLI. -### `NodeStdioTransport` +### `NodeStdioTransport` Transport for running Node.js scripts. -### `UvStdioTransport` +### `UvStdioTransport` Transport for running commands via the uv tool. -### `UvxStdioTransport` +### `UvxStdioTransport` Transport for running commands via the uvx tool. -### `NpxStdioTransport` +### `NpxStdioTransport` Transport for running commands via the npx tool. -### `FastMCPTransport` +### `FastMCPTransport` In-memory transport for FastMCP servers. @@ -228,13 +228,13 @@ tests or scenarios where client and server run in the same runtime. **Methods:** -#### `connect_session` +#### `connect_session` ```python connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession] ``` -### `MCPConfigTransport` +### `MCPConfigTransport` Transport for connecting to one or more MCP servers defined in an MCPConfig. @@ -287,7 +287,7 @@ async with client: **Methods:** -#### `connect_session` +#### `connect_session` ```python connect_session(self, **session_kwargs: Unpack[SessionKwargs]) -> AsyncIterator[ClientSession] diff --git a/docs/python-sdk/fastmcp-tools-tool.mdx b/docs/python-sdk/fastmcp-tools-tool.mdx index 2aaf97cc3..f7d1ff55a 100644 --- a/docs/python-sdk/fastmcp-tools-tool.mdx +++ b/docs/python-sdk/fastmcp-tools-tool.mdx @@ -7,7 +7,7 @@ sidebarTitle: tool ## Functions -### `default_serializer` +### `default_serializer` ```python default_serializer(data: Any) -> str @@ -15,17 +15,17 @@ default_serializer(data: Any) -> str ## Classes -### `ToolResult` +### `ToolResult` **Methods:** -#### `to_mcp_result` +#### `to_mcp_result` ```python to_mcp_result(self) -> list[ContentBlock] | tuple[list[ContentBlock], dict[str, Any]] ``` -### `Tool` +### `Tool` Internal tool registration info. @@ -33,34 +33,34 @@ Internal tool registration info. **Methods:** -#### `enable` +#### `enable` ```python enable(self) -> None ``` -#### `disable` +#### `disable` ```python disable(self) -> None ``` -#### `to_mcp_tool` +#### `to_mcp_tool` ```python to_mcp_tool(self, **overrides: Any) -> MCPTool ``` -#### `from_function` +#### `from_function` ```python -from_function(fn: Callable[..., Any], name: str | None = None, title: str | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | None | NotSetT = NotSet, serializer: Callable[[Any], str] | None = None, meta: dict[str, Any] | None = None, enabled: bool | None = None) -> FunctionTool +from_function(fn: Callable[..., Any], name: str | None = None, title: str | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet, serializer: Callable[[Any], str] | None = None, meta: dict[str, Any] | None = None, enabled: bool | None = None) -> FunctionTool ``` Create a Tool from a function. -#### `run` +#### `run` ```python run(self, arguments: dict[str, Any]) -> ToolResult @@ -75,26 +75,26 @@ implemented by subclasses. (list of ContentBlocks, dict of structured output). -#### `from_tool` +#### `from_tool` ```python from_tool(cls, tool: Tool) -> TransformedTool ``` -### `FunctionTool` +### `FunctionTool` **Methods:** -#### `from_function` +#### `from_function` ```python -from_function(cls, fn: Callable[..., Any], name: str | None = None, title: str | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | None | NotSetT = NotSet, serializer: Callable[[Any], str] | None = None, meta: dict[str, Any] | None = None, enabled: bool | None = None) -> FunctionTool +from_function(cls, fn: Callable[..., Any], name: str | None = None, title: str | None = None, description: str | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet, serializer: Callable[[Any], str] | None = None, meta: dict[str, Any] | None = None, enabled: bool | None = None) -> FunctionTool ``` Create a Tool from a function. -#### `run` +#### `run` ```python run(self, arguments: dict[str, Any]) -> ToolResult @@ -103,11 +103,11 @@ run(self, arguments: dict[str, Any]) -> ToolResult Run the tool with arguments. -### `ParsedFunction` +### `ParsedFunction` **Methods:** -#### `from_function` +#### `from_function` ```python from_function(cls, fn: Callable[..., Any], exclude_args: list[str] | None = None, validate: bool = True, wrap_non_object_output_schema: bool = True) -> ParsedFunction diff --git a/docs/python-sdk/fastmcp-tools-tool_transform.mdx b/docs/python-sdk/fastmcp-tools-tool_transform.mdx index b0a891fc2..5cfed788b 100644 --- a/docs/python-sdk/fastmcp-tools-tool_transform.mdx +++ b/docs/python-sdk/fastmcp-tools-tool_transform.mdx @@ -7,7 +7,7 @@ sidebarTitle: tool_transform ## Functions -### `forward` +### `forward` ```python forward(**kwargs) -> ToolResult @@ -36,7 +36,7 @@ tool has args `a` and `b`, and an `transform_args` was provided that maps `x` to - `TypeError`: If provided arguments don't match the transformed schema. -### `forward_raw` +### `forward_raw` ```python forward_raw(**kwargs) -> ToolResult @@ -62,7 +62,7 @@ y=2)` will call the parent tool with `x=1` and `y=2`. - `RuntimeError`: If called outside a transformed tool context. -### `apply_transformations_to_tools` +### `apply_transformations_to_tools` ```python apply_transformations_to_tools(tools: dict[str, Tool], transformations: dict[str, ToolTransformConfig]) -> dict[str, Tool] @@ -75,7 +75,7 @@ are left unchanged. ## Classes -### `ArgTransform` +### `ArgTransform` Configuration for transforming a parent tool's argument. @@ -137,7 +137,7 @@ ArgTransform(name="new_name", description="New desc", default=None, type=int) ``` -### `ArgTransformConfig` +### `ArgTransformConfig` A model for requesting a single argument transform. @@ -145,7 +145,7 @@ A model for requesting a single argument transform. **Methods:** -#### `to_arg_transform` +#### `to_arg_transform` ```python to_arg_transform(self) -> ArgTransform @@ -154,7 +154,7 @@ to_arg_transform(self) -> ArgTransform Convert the argument transform to a FastMCP argument transform. -### `TransformedTool` +### `TransformedTool` A tool that is transformed from another tool. @@ -171,7 +171,7 @@ inherited from the parent tool but can be overridden or disabled. **Methods:** -#### `run` +#### `run` ```python run(self, arguments: dict[str, Any]) -> ToolResult @@ -190,10 +190,10 @@ functions. - ToolResult object containing content and optional structured output. -#### `from_tool` +#### `from_tool` ```python -from_tool(cls, tool: Tool, name: str | None = None, title: str | None | NotSetT = NotSet, description: str | None | NotSetT = NotSet, tags: set[str] | None = None, transform_fn: Callable[..., Any] | None = None, transform_args: dict[str, ArgTransform] | None = None, annotations: ToolAnnotations | None | NotSetT = NotSet, output_schema: dict[str, Any] | None | NotSetT = NotSet, serializer: Callable[[Any], str] | None | NotSetT = NotSet, meta: dict[str, Any] | None | NotSetT = NotSet, enabled: bool | None = None) -> TransformedTool +from_tool(cls, tool: Tool, name: str | None = None, title: str | None | NotSetT = NotSet, description: str | None | NotSetT = NotSet, tags: set[str] | None = None, transform_fn: Callable[..., Any] | None = None, transform_args: dict[str, ArgTransform] | None = None, annotations: ToolAnnotations | None | NotSetT = NotSet, output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet, serializer: Callable[[Any], str] | None | NotSetT = NotSet, meta: dict[str, Any] | None | NotSetT = NotSet, enabled: bool | None = None) -> TransformedTool ``` Create a transformed tool from a parent tool. @@ -272,7 +272,7 @@ async def custom_output(**kwargs) -> ToolResult: ``` -### `ToolTransformConfig` +### `ToolTransformConfig` Provides a way to transform a tool. @@ -280,7 +280,7 @@ Provides a way to transform a tool. **Methods:** -#### `apply` +#### `apply` ```python apply(self, tool: Tool) -> TransformedTool diff --git a/src/fastmcp/client/transports.py b/src/fastmcp/client/transports.py index b334baa73..2d1c212b2 100644 --- a/src/fastmcp/client/transports.py +++ b/src/fastmcp/client/transports.py @@ -346,8 +346,7 @@ class StdioTransport(ClientTransport): ) -> AsyncIterator[ClientSession]: try: await self.connect(**session_kwargs) - assert self._session is not None - yield self._session + yield cast(ClientSession, self._session) finally: if not self.keep_alive: await self.disconnect() diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py index bb0c8afd5..fcbb3a5ce 100644 --- a/src/fastmcp/tools/tool.py +++ b/src/fastmcp/tools/tool.py @@ -1,6 +1,7 @@ from __future__ import annotations import inspect +import warnings from collections.abc import Callable from dataclasses import dataclass from typing import ( @@ -8,6 +9,7 @@ from typing import ( Annotated, Any, Generic, + Literal, TypeVar, get_type_hints, ) @@ -18,6 +20,7 @@ from mcp.types import ContentBlock, TextContent, ToolAnnotations from mcp.types import Tool as MCPTool from pydantic import Field, PydanticSchemaGenerationError +import fastmcp from fastmcp.server.dependencies import get_context from fastmcp.utilities.components import FastMCPComponent from fastmcp.utilities.json_schema import compress_schema @@ -162,7 +165,7 @@ class Tool(FastMCPComponent): tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, - output_schema: dict[str, Any] | None | NotSetT = NotSet, + output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet, serializer: Callable[[Any], str] | None = None, meta: dict[str, Any] | None = None, enabled: bool | None = None, @@ -204,7 +207,7 @@ class Tool(FastMCPComponent): description: str | None | NotSetT = NotSet, tags: set[str] | None = None, annotations: ToolAnnotations | None | NotSetT = NotSet, - output_schema: dict[str, Any] | None | NotSetT = NotSet, + output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet, serializer: Callable[[Any], str] | None = None, meta: dict[str, Any] | None | NotSetT = NotSet, transform_args: dict[str, ArgTransform] | None = None, @@ -242,7 +245,7 @@ class FunctionTool(Tool): tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, - output_schema: dict[str, Any] | None | NotSetT = NotSet, + output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet, serializer: Callable[[Any], str] | None = None, meta: dict[str, Any] | None = None, enabled: bool | None = None, @@ -255,16 +258,26 @@ class FunctionTool(Tool): raise ValueError("You must provide a name for lambda functions") if isinstance(output_schema, NotSetT): - output_schema = parsed_fn.output_schema + final_output_schema = parsed_fn.output_schema elif output_schema is False: - output_schema = None + # Handle False as deprecated synonym for None (deprecated in 2.11.4) + if fastmcp.settings.deprecation_warnings: + warnings.warn( + "Passing output_schema=False is deprecated. Use output_schema=None instead.", + DeprecationWarning, + stacklevel=2, + ) + final_output_schema = None + else: + # At this point output_schema is not NotSetT and not False, so it must be dict | None + final_output_schema = output_schema # Note: explicit schemas (dict) are used as-is without auto-wrapping # Validate that explicit schemas are object type for structured content - if output_schema is not None and isinstance(output_schema, dict): - if output_schema.get("type") != "object": + if final_output_schema is not None and isinstance(final_output_schema, dict): + if final_output_schema.get("type") != "object": raise ValueError( - f'Output schemas must have "type" set to "object" due to MCP spec limitations. Received: {output_schema!r}' + f'Output schemas must have "type" set to "object" due to MCP spec limitations. Received: {final_output_schema!r}' ) return cls( @@ -273,7 +286,7 @@ class FunctionTool(Tool): title=title, description=description or parsed_fn.description, parameters=parsed_fn.input_schema, - output_schema=output_schema, + output_schema=final_output_schema, annotations=annotations, tags=tags or set(), serializer=serializer, diff --git a/src/fastmcp/tools/tool_transform.py b/src/fastmcp/tools/tool_transform.py index 7a34efa60..e4f867ab2 100644 --- a/src/fastmcp/tools/tool_transform.py +++ b/src/fastmcp/tools/tool_transform.py @@ -1,10 +1,11 @@ from __future__ import annotations import inspect +import warnings from collections.abc import Callable from contextvars import ContextVar from dataclasses import dataclass -from typing import Annotated, Any, Literal +from typing import Annotated, Any, Literal, cast import pydantic_core from mcp.types import ToolAnnotations @@ -12,6 +13,7 @@ from pydantic import ConfigDict from pydantic.fields import Field from pydantic.functional_validators import BeforeValidator +import fastmcp from fastmcp.tools.tool import ParsedFunction, Tool, ToolResult, _convert_to_content from fastmcp.utilities.components import _convert_set_default_none from fastmcp.utilities.json_schema import compress_schema @@ -369,7 +371,7 @@ class TransformedTool(Tool): transform_fn: Callable[..., Any] | None = None, transform_args: dict[str, ArgTransform] | None = None, annotations: ToolAnnotations | None | NotSetT = NotSet, - output_schema: dict[str, Any] | None | NotSetT = NotSet, + output_schema: dict[str, Any] | None | NotSetT | Literal[False] = NotSet, serializer: Callable[[Any], str] | None | NotSetT = NotSet, meta: dict[str, Any] | None | NotSetT = NotSet, enabled: bool | None = None, @@ -471,8 +473,8 @@ class TransformedTool(Tool): if output_schema is NotSet: # Use smart fallback: try custom function, then parent if transform_fn is not None: - assert parsed_fn is not None - final_output_schema = parsed_fn.output_schema + # parsed fn is not none here + final_output_schema = cast(ParsedFunction, parsed_fn).output_schema if final_output_schema is None: # Check if function returns ToolResult - if so, don't fall back to parent return_annotation = inspect.signature( @@ -484,16 +486,25 @@ class TransformedTool(Tool): final_output_schema = tool.output_schema else: final_output_schema = tool.output_schema + elif output_schema is False: + # Handle False as deprecated synonym for None (deprecated in 2.11.4) + if fastmcp.settings.deprecation_warnings: + warnings.warn( + "Passing output_schema=False is deprecated. Use output_schema=None instead.", + DeprecationWarning, + stacklevel=2, + ) + final_output_schema = None else: - assert isinstance(output_schema, dict | None) - final_output_schema = output_schema + final_output_schema = cast(dict | None, output_schema) if transform_fn is None: # User wants pure transformation - use forwarding_fn as the main function final_fn = forwarding_fn final_schema = schema else: - assert parsed_fn is not None + # parsed fn is not none here + parsed_fn = cast(ParsedFunction, parsed_fn) # User provided custom function - merge schemas final_fn = transform_fn diff --git a/tests/deprecated/test_output_schema_false.py b/tests/deprecated/test_output_schema_false.py new file mode 100644 index 000000000..0dcc20d06 --- /dev/null +++ b/tests/deprecated/test_output_schema_false.py @@ -0,0 +1,139 @@ +"""Test deprecated output_schema=False behavior (deprecated in 2.11.4).""" + +import warnings + +import pytest + +from fastmcp import FastMCP +from fastmcp.tools import Tool + + +class TestDeprecatedOutputSchemaFalse: + """Test that output_schema=False is deprecated but still works.""" + + async def test_tool_decorator_output_schema_false_deprecated(self): + """Test that @mcp.tool(output_schema=False) shows deprecation warning.""" + mcp = FastMCP() + + with pytest.warns( + DeprecationWarning, match="output_schema=False is deprecated" + ): + + @mcp.tool(output_schema=False) # type: ignore[arg-type] + def simple_tool() -> int: + """A simple tool.""" + return 42 + + # Verify the tool was created with None as output_schema + tool = mcp._tool_manager._tools["simple_tool"] + assert tool.output_schema is None + + async def test_tool_from_function_output_schema_false_deprecated(self): + """Test that Tool.from_function(output_schema=False) shows deprecation warning.""" + + def my_function() -> str: + """A simple function.""" + return "hello" + + with pytest.warns( + DeprecationWarning, match="output_schema=False is deprecated" + ): + tool = Tool.from_function(my_function, output_schema=False) # type: ignore[arg-type] + + # Verify the tool was created with None as output_schema + assert tool.output_schema is None + + async def test_tool_from_tool_output_schema_false_deprecated(self): + """Test that Tool.from_tool(output_schema=False) shows deprecation warning.""" + + # Create a parent tool + def parent_function() -> dict[str, str]: + """A parent function.""" + return {"status": "ok"} + + parent_tool = Tool.from_function(parent_function) + + with pytest.warns( + DeprecationWarning, match="output_schema=False is deprecated" + ): + transformed_tool = Tool.from_tool(parent_tool, output_schema=False) # type: ignore[arg-type] + + # Verify the tool was created with None as output_schema + assert transformed_tool.output_schema is None + + async def test_output_schema_false_functionality_preserved(self): + """Test that output_schema=False still works functionally like output_schema=None.""" + mcp = FastMCP() + + # Create two tools - one with False, one with None + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + + @mcp.tool(output_schema=False) # type: ignore[arg-type] + def tool_with_false() -> dict[str, str]: + """Tool with output_schema=False.""" + return {"result": "false"} + + @mcp.tool(output_schema=None) + def tool_with_none() -> dict[str, str]: + """Tool with output_schema=None.""" + return {"result": "none"} + + # Both should have None as output_schema + assert mcp._tool_manager._tools["tool_with_false"].output_schema is None + assert mcp._tool_manager._tools["tool_with_none"].output_schema is None + + # Both should work the same way + result_false = await mcp._tool_manager._tools["tool_with_false"].run({}) + result_none = await mcp._tool_manager._tools["tool_with_none"].run({}) + + # Both should return structured content for dict-like objects + assert result_false.structured_content == {"result": "false"} + assert result_none.structured_content == {"result": "none"} + + async def test_output_schema_false_with_scalar_return(self): + """Test that output_schema=False works with scalar returns (no structured content).""" + mcp = FastMCP() + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + + @mcp.tool(output_schema=False) # type: ignore[arg-type] + def scalar_tool() -> int: + """Tool returning a scalar.""" + return 42 + + tool = mcp._tool_manager._tools["scalar_tool"] + assert tool.output_schema is None + + result = await tool.run({}) + # Scalar values don't produce structured content + assert result.structured_content is None + assert len(result.content) == 1 + assert result.content[0].text == "42" # type: ignore[attr-defined] + + async def test_transform_with_output_schema_false(self): + """Test that transformation with output_schema=False still works.""" + + # Create a parent tool + def parent_function(x: int) -> dict[str, int]: + """A parent function.""" + return {"value": x * 2} + + parent_tool = Tool.from_function(parent_function) + + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + + # Transform with output_schema=False + transformed = Tool.from_tool( + parent_tool, + name="doubled", + output_schema=False, # type: ignore[arg-type] + ) + + assert transformed.output_schema is None + + # Tool should still work + result = await transformed.run({"x": 5}) + assert result.structured_content == {"value": 10}