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"
+ )