diff --git a/docs/python-sdk/fastmcp-server-context.mdx b/docs/python-sdk/fastmcp-server-context.mdx index 2873100b7d..a87096234b 100644 --- a/docs/python-sdk/fastmcp-server-context.mdx +++ b/docs/python-sdk/fastmcp-server-context.mdx @@ -170,6 +170,10 @@ Returns the context dict yielded by the server's lifespan function. Returns an empty dict if no lifespan was configured or if the MCP session is not yet established. +In background tasks (Docket workers), where request_context is not +available, falls back to reading from the FastMCP server's lifespan +result directly. + Example: ```python @server.tool @@ -181,7 +185,7 @@ def my_tool(ctx: Context) -> str: ``` -#### `report_progress` +#### `report_progress` ```python report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None @@ -198,7 +202,7 @@ Works in both foreground (MCP progress notifications) and background - `message`: Optional status message describing current progress -#### `list_resources` +#### `list_resources` ```python list_resources(self) -> list[SDKResource] @@ -210,7 +214,7 @@ List all available resources from the server. - List of Resource objects available on the server -#### `list_prompts` +#### `list_prompts` ```python list_prompts(self) -> list[SDKPrompt] @@ -222,7 +226,7 @@ List all available prompts from the server. - List of Prompt objects available on the server -#### `get_prompt` +#### `get_prompt` ```python get_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult @@ -238,7 +242,7 @@ Get a prompt by name with optional arguments. - The prompt result -#### `read_resource` +#### `read_resource` ```python read_resource(self, uri: str | AnyUrl) -> ResourceResult @@ -253,7 +257,7 @@ Read a resource by URI. - ResourceResult with contents -#### `log` +#### `log` ```python log(self, message: str, level: LoggingLevel | None = None, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None @@ -271,7 +275,7 @@ Messages sent to Clients are also logged to the `fastmcp.server.context.to_clien - `extra`: Optional mapping for additional arguments -#### `transport` +#### `transport` ```python transport(self) -> TransportType | None @@ -283,7 +287,7 @@ Returns the transport type used to run this server: "stdio", "sse", or "streamable-http". Returns None if called outside of a server context. -#### `client_supports_extension` +#### `client_supports_extension` ```python client_supports_extension(self, extension_id: str) -> bool @@ -308,7 +312,7 @@ Example:: return "text-only client" -#### `client_id` +#### `client_id` ```python client_id(self) -> str | None @@ -317,7 +321,7 @@ client_id(self) -> str | None Get the client ID if available. -#### `request_id` +#### `request_id` ```python request_id(self) -> str @@ -328,7 +332,7 @@ Get the unique ID for this request. Raises RuntimeError if MCP request context is not available. -#### `session_id` +#### `session_id` ```python session_id(self) -> str @@ -345,7 +349,7 @@ the same client session. - for other transports. -#### `session` +#### `session` ```python session(self) -> ServerSession @@ -359,7 +363,7 @@ In background task mode: Returns the session stored at Context creation. Raises RuntimeError if no session is available. -#### `debug` +#### `debug` ```python debug(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None @@ -370,7 +374,7 @@ Send a `DEBUG`-level message to the connected MCP Client. Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`. -#### `info` +#### `info` ```python info(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None @@ -381,7 +385,7 @@ Send a `INFO`-level message to the connected MCP Client. Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`. -#### `warning` +#### `warning` ```python warning(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None @@ -392,7 +396,7 @@ Send a `WARNING`-level message to the connected MCP Client. Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`. -#### `error` +#### `error` ```python error(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None @@ -403,7 +407,7 @@ Send a `ERROR`-level message to the connected MCP Client. Messages sent to Clients are also logged to the `fastmcp.server.context.to_client` logger with a level of `DEBUG`. -#### `list_roots` +#### `list_roots` ```python list_roots(self) -> list[Root] @@ -412,7 +416,7 @@ list_roots(self) -> list[Root] List the roots available to the server, as indicated by the client. -#### `send_notification` +#### `send_notification` ```python send_notification(self, notification: mcp.types.ServerNotificationType) -> None @@ -424,7 +428,7 @@ Send a notification to the client immediately. - `notification`: An MCP notification instance (e.g., ToolListChangedNotification()) -#### `close_sse_stream` +#### `close_sse_stream` ```python close_sse_stream(self) -> None @@ -442,7 +446,7 @@ Instead of holding a connection open for minutes, you can periodically close and let the client reconnect. -#### `sample_step` +#### `sample_step` ```python sample_step(self, messages: str | Sequence[str | SamplingMessage]) -> SampleStep @@ -485,7 +489,7 @@ regardless of this setting. - - .text: The text content (if any) -#### `sample` +#### `sample` ```python sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT] @@ -494,7 +498,7 @@ sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ Overload: With result_type, returns SamplingResult[ResultT]. -#### `sample` +#### `sample` ```python sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[str] @@ -503,7 +507,7 @@ sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ Overload: Without result_type, returns SamplingResult[str]. -#### `sample` +#### `sample` ```python sample(self, messages: str | Sequence[str | SamplingMessage]) -> SamplingResult[ResultT] | SamplingResult[str] @@ -551,43 +555,43 @@ regardless of this setting. - - .history: All messages exchanged during sampling -#### `elicit` +#### `elicit` ```python elicit(self, message: str, response_type: None) -> AcceptedElicitation[dict[str, Any]] | DeclinedElicitation | CancelledElicitation ``` -#### `elicit` +#### `elicit` ```python elicit(self, message: str, response_type: type[T]) -> AcceptedElicitation[T] | DeclinedElicitation | CancelledElicitation ``` -#### `elicit` +#### `elicit` ```python elicit(self, message: str, response_type: list[str]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation ``` -#### `elicit` +#### `elicit` ```python elicit(self, message: str, response_type: dict[str, dict[str, str]]) -> AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation ``` -#### `elicit` +#### `elicit` ```python elicit(self, message: str, response_type: list[list[str]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation ``` -#### `elicit` +#### `elicit` ```python elicit(self, message: str, response_type: list[dict[str, dict[str, str]]]) -> AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation ``` -#### `elicit` +#### `elicit` ```python elicit(self, message: str, response_type: type[T] | list[str] | dict[str, dict[str, str]] | list[list[str]] | list[dict[str, dict[str, str]]] | None = None) -> AcceptedElicitation[T] | AcceptedElicitation[dict[str, Any]] | AcceptedElicitation[str] | AcceptedElicitation[list[str]] | DeclinedElicitation | CancelledElicitation @@ -616,7 +620,7 @@ type or dataclass or BaseModel. If it is a primitive type, an object schema with a single "value" field will be generated. -#### `set_state` +#### `set_state` ```python set_state(self, key: str, value: Any) -> None @@ -629,7 +633,7 @@ The key is automatically prefixed with the session identifier. State expires after 1 day to prevent unbounded memory growth. -#### `get_state` +#### `get_state` ```python get_state(self, key: str) -> Any @@ -640,7 +644,7 @@ Get a value from the session-scoped state store. Returns None if the key is not found. -#### `delete_state` +#### `delete_state` ```python delete_state(self, key: str) -> None @@ -649,7 +653,7 @@ delete_state(self, key: str) -> None Delete a value from the session-scoped state store. -#### `enable_components` +#### `enable_components` ```python enable_components(self) -> None @@ -673,7 +677,7 @@ ResourceListChangedNotification, and PromptListChangedNotification. - `match_all`: If True, matches all components regardless of other criteria. -#### `disable_components` +#### `disable_components` ```python disable_components(self) -> None @@ -697,7 +701,7 @@ ResourceListChangedNotification, and PromptListChangedNotification. - `match_all`: If True, matches all components regardless of other criteria. -#### `reset_visibility` +#### `reset_visibility` ```python reset_visibility(self) -> None diff --git a/docs/python-sdk/fastmcp-server-dependencies.mdx b/docs/python-sdk/fastmcp-server-dependencies.mdx index 496a19e8c5..5660ac13af 100644 --- a/docs/python-sdk/fastmcp-server-dependencies.mdx +++ b/docs/python-sdk/fastmcp-server-dependencies.mdx @@ -15,7 +15,7 @@ CurrentWorker) and background task execution require fastmcp[tasks]. ## Functions -### `get_task_context` +### `get_task_context` ```python get_task_context() -> TaskContextInfo | None @@ -31,7 +31,7 @@ Returns None if not running in a task context (e.g., foreground execution). - TaskContextInfo with task_id and session_id, or None if not in a task. -### `register_task_session` +### `register_task_session` ```python register_task_session(session_id: str, session: ServerSession) -> None @@ -49,7 +49,7 @@ client disconnects. - `session`: The ServerSession instance -### `get_task_session` +### `get_task_session` ```python get_task_session(session_id: str) -> ServerSession | None @@ -65,7 +65,7 @@ Get a registered session by ID if still alive. - The ServerSession if found and alive, None otherwise -### `is_docket_available` +### `is_docket_available` ```python is_docket_available() -> bool @@ -75,7 +75,7 @@ is_docket_available() -> bool Check if pydocket is installed. -### `require_docket` +### `require_docket` ```python require_docket(feature: str) -> None @@ -89,7 +89,7 @@ Raise ImportError with install instructions if docket not available. "CurrentDocket()"). Will be included in the error message. -### `transform_context_annotations` +### `transform_context_annotations` ```python transform_context_annotations(fn: Callable[..., Any]) -> Callable[..., Any] @@ -115,7 +115,7 @@ allows them to have defaults in any order. - Function with modified signature (same function object, updated __signature__) -### `get_context` +### `get_context` ```python get_context() -> Context @@ -125,7 +125,7 @@ get_context() -> Context Get the current FastMCP Context instance directly. -### `get_server` +### `get_server` ```python get_server() -> FastMCP @@ -141,7 +141,7 @@ Get the current FastMCP server instance directly. - `RuntimeError`: If no server in context -### `get_http_request` +### `get_http_request` ```python get_http_request() -> Request @@ -153,7 +153,7 @@ Get the current HTTP request. Tries MCP SDK's request_ctx first, then falls back to FastMCP's HTTP context. -### `get_http_headers` +### `get_http_headers` ```python get_http_headers(include_all: bool = False) -> dict[str, str] @@ -169,7 +169,7 @@ By default, strips problematic headers like `content-length` that cause issues if forwarded to downstream clients. If `include_all` is True, all headers are returned. -### `get_access_token` +### `get_access_token` ```python get_access_token() -> AccessToken | None @@ -181,13 +181,14 @@ Get the FastMCP access token from the current context. This function first tries to get the token from the current HTTP request's scope, which is more reliable for long-lived connections where the SDK's auth_context_var may become stale after token refresh. Falls back to the SDK's context var if no -request is available. +request is available. In background tasks (Docket workers), falls back to the +token snapshot stored in Redis at task submission time. **Returns:** - The access token if an authenticated user is available, None otherwise. -### `without_injected_parameters` +### `without_injected_parameters` ```python without_injected_parameters(fn: Callable[..., Any]) -> Callable[..., Any] @@ -212,7 +213,7 @@ Handles: - Async wrapper function without injected parameters -### `resolve_dependencies` +### `resolve_dependencies` ```python resolve_dependencies(fn: Callable[..., Any], arguments: dict[str, Any]) -> AsyncGenerator[dict[str, Any], None] @@ -238,7 +239,7 @@ time, so all injection goes through the unified DI system. which will be filtered out) -### `CurrentContext` +### `CurrentContext` ```python CurrentContext() -> Context @@ -257,7 +258,7 @@ current MCP operation (tool/resource/prompt call). - `RuntimeError`: If no active context found (during resolution) -### `CurrentDocket` +### `CurrentDocket` ```python CurrentDocket() -> Docket @@ -277,7 +278,7 @@ automatically creates for background task scheduling. - `ImportError`: If fastmcp[tasks] not installed -### `CurrentWorker` +### `CurrentWorker` ```python CurrentWorker() -> Worker @@ -297,7 +298,7 @@ automatically creates for background task processing. - `ImportError`: If fastmcp[tasks] not installed -### `CurrentFastMCP` +### `CurrentFastMCP` ```python CurrentFastMCP() -> FastMCP @@ -315,7 +316,7 @@ This dependency provides access to the active FastMCP server. - `RuntimeError`: If no server in context (during resolution) -### `CurrentRequest` +### `CurrentRequest` ```python CurrentRequest() -> Request @@ -335,7 +336,7 @@ current HTTP request. Only available when running over HTTP transports - `RuntimeError`: If no HTTP request in context (e.g., STDIO transport) -### `CurrentHeaders` +### `CurrentHeaders` ```python CurrentHeaders() -> dict[str, str] @@ -352,7 +353,7 @@ safe to use in code that might run over any transport. - A dependency that resolves to a dictionary of header name -> value -### `CurrentAccessToken` +### `CurrentAccessToken` ```python CurrentAccessToken() -> AccessToken @@ -371,7 +372,7 @@ authenticated request. Raises an error if no authentication is present. - `RuntimeError`: If no authenticated user (use get_access_token() for optional) -### `TokenClaim` +### `TokenClaim` ```python TokenClaim(name: str) -> str @@ -396,7 +397,7 @@ without needing the full token object. ## Classes -### `TaskContextInfo` +### `TaskContextInfo` Information about the current background task context. @@ -405,7 +406,7 @@ Returned by ``get_task_context()`` when running inside a Docket worker. Contains identifiers needed to communicate with the MCP session. -### `ProgressLike` +### `ProgressLike` Protocol for progress tracking interface. @@ -416,7 +417,7 @@ and Docket's Progress (worker context). **Methods:** -#### `current` +#### `current` ```python current(self) -> int | None @@ -425,7 +426,7 @@ current(self) -> int | None Current progress value. -#### `total` +#### `total` ```python total(self) -> int @@ -434,7 +435,7 @@ total(self) -> int Total/target progress value. -#### `message` +#### `message` ```python message(self) -> str | None @@ -443,7 +444,7 @@ message(self) -> str | None Current progress message. -#### `set_total` +#### `set_total` ```python set_total(self, total: int) -> None @@ -452,7 +453,7 @@ set_total(self, total: int) -> None Set the total/target value for progress tracking. -#### `increment` +#### `increment` ```python increment(self, amount: int = 1) -> None @@ -461,7 +462,7 @@ increment(self, amount: int = 1) -> None Atomically increment the current progress value. -#### `set_message` +#### `set_message` ```python set_message(self, message: str | None) -> None @@ -470,7 +471,7 @@ set_message(self, message: str | None) -> None Update the progress status message. -### `InMemoryProgress` +### `InMemoryProgress` In-memory progress tracker for immediate tool execution. @@ -482,25 +483,25 @@ progress doesn't need to be observable across processes. **Methods:** -#### `current` +#### `current` ```python current(self) -> int | None ``` -#### `total` +#### `total` ```python total(self) -> int ``` -#### `message` +#### `message` ```python message(self) -> str | None ``` -#### `set_total` +#### `set_total` ```python set_total(self, total: int) -> None @@ -509,7 +510,7 @@ set_total(self, total: int) -> None Set the total/target value for progress tracking. -#### `increment` +#### `increment` ```python increment(self, amount: int = 1) -> None @@ -518,7 +519,7 @@ increment(self, amount: int = 1) -> None Atomically increment the current progress value. -#### `set_message` +#### `set_message` ```python set_message(self, message: str | None) -> None @@ -527,7 +528,7 @@ set_message(self, message: str | None) -> None Update the progress status message. -### `Progress` +### `Progress` FastMCP Progress dependency that works in both server and worker contexts. diff --git a/docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx b/docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx index 69c2b0dae8..ba3d7918bf 100644 --- a/docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx +++ b/docs/python-sdk/fastmcp-server-providers-openapi-provider.mdx @@ -10,7 +10,7 @@ OpenAPIProvider for creating MCP components from OpenAPI specifications. ## Classes -### `OpenAPIProvider` +### `OpenAPIProvider` Provider that creates MCP components from an OpenAPI specification. @@ -21,7 +21,7 @@ spec. Each component makes HTTP calls to the described API endpoints. **Methods:** -#### `lifespan` +#### `lifespan` ```python lifespan(self) -> AsyncIterator[None] @@ -30,7 +30,7 @@ lifespan(self) -> AsyncIterator[None] Manage the lifecycle of the auto-created httpx client. -#### `get_tasks` +#### `get_tasks` ```python get_tasks(self) -> Sequence[FastMCPComponent] diff --git a/docs/python-sdk/fastmcp-utilities-openapi-formatters.mdx b/docs/python-sdk/fastmcp-utilities-openapi-formatters.mdx index 1b725fc5a3..0b9d123693 100644 --- a/docs/python-sdk/fastmcp-utilities-openapi-formatters.mdx +++ b/docs/python-sdk/fastmcp-utilities-openapi-formatters.mdx @@ -72,26 +72,7 @@ format_json_for_description(data: Any, indent: int = 2) -> str Formats Python data as a JSON string block for Markdown. -### `format_simple_description` - -```python -format_simple_description(base_description: str, parameters: list[ParameterInfo] | None = None, request_body: RequestBodyInfo | None = None) -> str -``` - - -Formats a simple description for MCP objects (tools, resources, prompts). -Excludes response details, examples, and verbose status codes. - -**Args:** -- `base_description`: The initial description to be formatted. -- `parameters`: A list of parameter information. -- `request_body`: Information about the request body. - -**Returns:** -- The formatted description string with minimal details. - - -### `format_description_with_responses` +### `format_description_with_responses` ```python format_description_with_responses(base_description: str, responses: dict[str, Any], parameters: list[ParameterInfo] | None = None, request_body: RequestBodyInfo | None = None) -> str diff --git a/src/fastmcp/experimental/utilities/openapi/__init__.py b/src/fastmcp/experimental/utilities/openapi/__init__.py index 51c947bf17..0c8dd15e51 100644 --- a/src/fastmcp/experimental/utilities/openapi/__init__.py +++ b/src/fastmcp/experimental/utilities/openapi/__init__.py @@ -10,7 +10,6 @@ RequestBodyInfo, ResponseInfo, extract_output_schema_from_responses, - format_simple_description, parse_openapi_to_http_routes, _combine_schemas, ) @@ -32,6 +31,5 @@ "ResponseInfo", "_combine_schemas", "extract_output_schema_from_responses", - "format_simple_description", "parse_openapi_to_http_routes", ] diff --git a/src/fastmcp/server/providers/openapi/provider.py b/src/fastmcp/server/providers/openapi/provider.py index 68979c0c1d..975e0228b2 100644 --- a/src/fastmcp/server/providers/openapi/provider.py +++ b/src/fastmcp/server/providers/openapi/provider.py @@ -34,7 +34,6 @@ from fastmcp.utilities.openapi import ( HTTPRoute, extract_output_schema_from_responses, - format_simple_description, parse_openapi_to_http_routes, ) from fastmcp.utilities.openapi.director import RequestDirector @@ -255,18 +254,13 @@ def _create_openapi_tool( or route.summary or f"Executes {route.method} {route.path}" ) - enhanced_description = format_simple_description( - base_description=base_description, - parameters=route.parameters, - request_body=route.request_body, - ) tool = OpenAPITool( client=self._client, route=route, director=self._director, name=tool_name, - description=enhanced_description, + description=base_description, parameters=combined_schema, output_schema=output_schema, tags=set(route.tags or []) | tags, @@ -293,11 +287,6 @@ def _create_openapi_resource( base_description = ( route.description or route.summary or f"Represents {route.path}" ) - enhanced_description = format_simple_description( - base_description=base_description, - parameters=route.parameters, - request_body=route.request_body, - ) resource = OpenAPIResource( client=self._client, @@ -305,7 +294,7 @@ def _create_openapi_resource( director=self._director, uri=resource_uri, name=resource_name, - description=enhanced_description, + description=base_description, mime_type=_extract_mime_type_from_route(route), tags=set(route.tags or []) | tags, ) @@ -338,11 +327,6 @@ def _create_openapi_template( base_description = ( route.description or route.summary or f"Template for {route.path}" ) - enhanced_description = format_simple_description( - base_description=base_description, - parameters=route.parameters, - request_body=route.request_body, - ) template_params_schema = { "type": "object", @@ -372,7 +356,7 @@ def _create_openapi_template( director=self._director, uri_template=uri_template_str, name=template_name, - description=enhanced_description, + description=base_description, parameters=template_params_schema, tags=set(route.tags or []) | tags, mime_type=_extract_mime_type_from_route(route), diff --git a/src/fastmcp/utilities/openapi/__init__.py b/src/fastmcp/utilities/openapi/__init__.py index f71bc7a6a7..eb25666d1d 100644 --- a/src/fastmcp/utilities/openapi/__init__.py +++ b/src/fastmcp/utilities/openapi/__init__.py @@ -20,7 +20,6 @@ format_deep_object_parameter, format_description_with_responses, format_json_for_description, - format_simple_description, generate_example_from_schema, ) @@ -57,7 +56,6 @@ "format_deep_object_parameter", "format_description_with_responses", "format_json_for_description", - "format_simple_description", "generate_example_from_schema", "parse_openapi_to_http_routes", ] diff --git a/src/fastmcp/utilities/openapi/formatters.py b/src/fastmcp/utilities/openapi/formatters.py index 27580fcdd5..a0bd75bef2 100644 --- a/src/fastmcp/utilities/openapi/formatters.py +++ b/src/fastmcp/utilities/openapi/formatters.py @@ -189,39 +189,6 @@ def format_json_for_description(data: Any, indent: int = 2) -> str: return f"```\nCould not serialize to JSON: {data}\n```" -def format_simple_description( - base_description: str, - parameters: list[ParameterInfo] | None = None, - request_body: RequestBodyInfo | None = None, -) -> str: - """ - Formats a simple description for MCP objects (tools, resources, prompts). - Excludes response details, examples, and verbose status codes. - - Args: - base_description (str): The initial description to be formatted. - parameters (list[ParameterInfo] | None, optional): A list of parameter information. - request_body (RequestBodyInfo | None, optional): Information about the request body. - - Returns: - str: The formatted description string with minimal details. - """ - desc_parts = [base_description] - - # Only add critical parameter information if they have descriptions - if parameters: - path_params = [p for p in parameters if p.location == "path" and p.description] - if path_params: - desc_parts.append("\n\n**Path Parameters:**") - for param in path_params: - desc_parts.append(f"\n- **{param.name}**: {param.description}") - - # Skip query parameters, request body details, and all response information - # These are already captured in the inputSchema - - return "\n".join(desc_parts) - - def format_description_with_responses( base_description: str, responses: dict[ @@ -384,6 +351,5 @@ def format_description_with_responses( "format_deep_object_parameter", "format_description_with_responses", "format_json_for_description", - "format_simple_description", "generate_example_from_schema", ] diff --git a/tests/server/test_tool_transformation.py b/tests/server/test_tool_transformation.py index 4f9833bdf0..4cf9388229 100644 --- a/tests/server/test_tool_transformation.py +++ b/tests/server/test_tool_transformation.py @@ -1,6 +1,12 @@ +import httpx + from fastmcp import FastMCP +from fastmcp.client import Client from fastmcp.server.transforms import ToolTransform -from fastmcp.tools.tool_transform import ToolTransformConfig +from fastmcp.tools.tool_transform import ( + ArgTransformConfig, + ToolTransformConfig, +) async def test_tool_transformation_via_layer(): @@ -207,3 +213,73 @@ def my_tool() -> str: # Tool should now be visible assert "my_tool" in tool_names + + +async def test_openapi_path_params_not_duplicated_in_description(): + """Path parameter details should live in inputSchema, not the description. + + Regression test for https://github.com/jlowin/fastmcp/issues/3130 — hiding + a path param via ToolTransform left stale references in the description + because the description was generated before transforms ran. The fix is to + keep parameter docs in inputSchema only, where transforms can control them. + """ + spec = { + "openapi": "3.1.0", + "info": {"title": "Test", "version": "0.1.0"}, + "paths": { + "/api/{version}/users/{user_id}": { + "get": { + "operationId": "my_endpoint", + "summary": "My endpoint", + "parameters": [ + { + "name": "version", + "in": "path", + "required": True, + "description": "API version", + "schema": {"type": "string"}, + }, + { + "name": "user_id", + "in": "path", + "required": True, + "description": "The user ID", + "schema": {"type": "string"}, + }, + ], + "responses": {"200": {"description": "OK"}}, + }, + }, + }, + } + + async with httpx.AsyncClient(base_url="http://localhost") as http_client: + mcp = FastMCP.from_openapi(openapi_spec=spec, client=http_client) + + # Hide one of the two path params + mcp.add_transform( + ToolTransform( + { + "my_endpoint": ToolTransformConfig( + arguments={ + "version": ArgTransformConfig(hide=True, default="v1"), + } + ) + } + ) + ) + + async with Client(mcp) as client: + tools = await client.list_tools() + tool = tools[0] + + # Description should be the summary only — no parameter details + assert tool.description == "My endpoint" + + # Hidden param gone from schema, visible param still present + assert "version" not in tool.inputSchema.get("properties", {}) + assert "user_id" in tool.inputSchema["properties"] + assert ( + tool.inputSchema["properties"]["user_id"]["description"] + == "The user ID" + )