diff --git a/docs/changelog.mdx b/docs/changelog.mdx index 0e84bd4da8..eec90a81d5 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -1234,7 +1234,7 @@ This release introduces completely new tools for generating and customizing MCP ## [v2.4.0: Config and Conquer](https://github.com/jlowin/fastmcp/releases/tag/v2.4.0) -**Note**: this release includes a backwards-incompatible change to how resources are prefixed when mounted in composed servers. However, it is only backwards-incompatible if users were running tests or manually loading resources by prefixed key; LLMs should not have any issue discovering the new route. See [Resource Prefix Formats](https://gofastmcp.com/servers/composition#resource-prefix-formats) for more. +**Note**: this release includes a backwards-incompatible change to how resources are prefixed when mounted in composed servers. However, it is only backwards-incompatible if users were running tests or manually loading resources by prefixed key; LLMs should not have any issue discovering the new route. ### New Features 🎉 diff --git a/docs/docs.json b/docs/docs.json index e8af19b5e6..7e306b6423 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -356,7 +356,6 @@ "python-sdk/fastmcp-server-auth-providers-auth0", "python-sdk/fastmcp-server-auth-providers-aws", "python-sdk/fastmcp-server-auth-providers-azure", - "python-sdk/fastmcp-server-auth-providers-bearer", "python-sdk/fastmcp-server-auth-providers-debug", "python-sdk/fastmcp-server-auth-providers-descope", "python-sdk/fastmcp-server-auth-providers-discord", diff --git a/docs/python-sdk/fastmcp-cli-cli.mdx b/docs/python-sdk/fastmcp-cli-cli.mdx index 3b6f0bf3a6..79cb794db1 100644 --- a/docs/python-sdk/fastmcp-cli-cli.mdx +++ b/docs/python-sdk/fastmcp-cli-cli.mdx @@ -10,7 +10,7 @@ FastMCP CLI tools using Cyclopts. ## Functions -### `with_argv` +### `with_argv` ```python with_argv(args: list[str] | None) @@ -27,7 +27,7 @@ Args are provided without the script name, so we preserve sys.argv[0] and replace the rest. -### `version` +### `version` ```python version() @@ -37,7 +37,7 @@ version() Display version information and platform details. -### `dev` +### `dev` ```python dev(server_spec: str | None = None) -> None @@ -50,7 +50,7 @@ Run an MCP server with the MCP Inspector for development. - `server_spec`: Python file to run, optionally with \:object suffix, or None to auto-detect fastmcp.json -### `run` +### `run` ```python run(server_spec: str | None = None, *server_args: str) -> None @@ -74,7 +74,7 @@ fastmcp run server.py -- --config config.json --debug - `server_spec`: Python file, object specification (file\:obj), config file, URL, or None to auto-detect -### `inspect` +### `inspect` ```python inspect(server_spec: str | None = None) -> None @@ -105,7 +105,7 @@ fastmcp inspect # auto-detect fastmcp.json - `server_spec`: Python file to inspect, optionally with \:object suffix, or fastmcp.json -### `prepare` +### `prepare` ```python prepare(config_path: Annotated[str | None, cyclopts.Parameter(help='Path to fastmcp.json configuration file')] = None, output_dir: Annotated[str | None, cyclopts.Parameter(help='Directory to create the persistent environment in')] = None, skip_source: Annotated[bool, cyclopts.Parameter(help='Skip source preparation (e.g., git clone)')] = False) -> None diff --git a/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx b/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx deleted file mode 100644 index 3cfbf0239f..0000000000 --- a/docs/python-sdk/fastmcp-server-auth-providers-bearer.mdx +++ /dev/null @@ -1,13 +0,0 @@ ---- -title: bearer -sidebarTitle: bearer ---- - -# `fastmcp.server.auth.providers.bearer` - - -Backwards compatibility shim for BearerAuthProvider. - -The BearerAuthProvider class has been moved to fastmcp.server.auth.providers.jwt.JWTVerifier -for better organization. This module provides a backwards-compatible import. - diff --git a/docs/python-sdk/fastmcp-server-context.mdx b/docs/python-sdk/fastmcp-server-context.mdx index ca1f8f4c87..ee9772830d 100644 --- a/docs/python-sdk/fastmcp-server-context.mdx +++ b/docs/python-sdk/fastmcp-server-context.mdx @@ -7,7 +7,7 @@ sidebarTitle: context ## Functions -### `set_context` +### `set_context` ```python set_context(context: Context) -> Generator[Context, None, None] @@ -15,7 +15,7 @@ set_context(context: Context) -> Generator[Context, None, None] ## Classes -### `LogData` +### `LogData` Data object for passing log arguments to client-side handlers. @@ -24,7 +24,7 @@ This provides an interface to match the Python standard library logging, for compatibility with structured logging. -### `Context` +### `Context` Context object providing access to MCP capabilities. @@ -72,7 +72,7 @@ The context is optional - tools that don't need it can omit the parameter. **Methods:** -#### `fastmcp` +#### `fastmcp` ```python fastmcp(self) -> FastMCP @@ -81,7 +81,7 @@ fastmcp(self) -> FastMCP Get the FastMCP instance. -#### `request_context` +#### `request_context` ```python request_context(self) -> RequestContext[ServerSession, Any, Request] | None @@ -110,7 +110,7 @@ async def on_request(self, context, call_next): ``` -#### `report_progress` +#### `report_progress` ```python report_progress(self, progress: float, total: float | None = None, message: str | None = None) -> None @@ -123,7 +123,7 @@ Report progress for the current operation. - `total`: Optional total value e.g. 100 -#### `list_resources` +#### `list_resources` ```python list_resources(self) -> list[MCPResource] @@ -135,7 +135,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[MCPPrompt] @@ -147,7 +147,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 @@ -163,7 +163,7 @@ Get a prompt by name with optional arguments. - The prompt result -#### `read_resource` +#### `read_resource` ```python read_resource(self, uri: str | AnyUrl) -> list[ReadResourceContents] @@ -178,7 +178,7 @@ Read a resource by URI. - The resource content as either text or bytes -#### `log` +#### `log` ```python log(self, message: str, level: LoggingLevel | None = None, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None @@ -196,7 +196,7 @@ Messages sent to Clients are also logged to the `fastmcp.server.context.to_clien - `extra`: Optional mapping for additional arguments -#### `client_id` +#### `client_id` ```python client_id(self) -> str | None @@ -205,7 +205,7 @@ client_id(self) -> str | None Get the client ID if available. -#### `request_id` +#### `request_id` ```python request_id(self) -> str @@ -216,7 +216,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 @@ -233,7 +233,7 @@ the same client session. - for other transports. -#### `session` +#### `session` ```python session(self) -> ServerSession @@ -244,7 +244,7 @@ Access to the underlying session for advanced usage. Raises RuntimeError if MCP request context is not available. -#### `debug` +#### `debug` ```python debug(self, message: str, logger_name: str | None = None, extra: Mapping[str, Any] | None = None) -> None @@ -255,7 +255,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 @@ -266,7 +266,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 @@ -277,7 +277,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 @@ -288,7 +288,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] @@ -297,7 +297,7 @@ list_roots(self) -> list[Root] List the roots available to the server, as indicated by the client. -#### `send_tool_list_changed` +#### `send_tool_list_changed` ```python send_tool_list_changed(self) -> None @@ -306,7 +306,7 @@ send_tool_list_changed(self) -> None Send a tool list changed notification to the client. -#### `send_resource_list_changed` +#### `send_resource_list_changed` ```python send_resource_list_changed(self) -> None @@ -315,7 +315,7 @@ send_resource_list_changed(self) -> None Send a resource list changed notification to the client. -#### `send_prompt_list_changed` +#### `send_prompt_list_changed` ```python send_prompt_list_changed(self) -> None @@ -324,7 +324,7 @@ send_prompt_list_changed(self) -> None Send a prompt list changed notification to the client. -#### `sample` +#### `sample` ```python sample(self, messages: str | Sequence[str | SamplingMessage], system_prompt: str | None = None, include_context: IncludeContext | None = None, temperature: float | None = None, max_tokens: int | None = None, model_preferences: ModelPreferences | str | list[str] | None = None) -> TextContent | ImageContent | AudioContent @@ -337,25 +337,25 @@ completion from the client. The client must be appropriately configured, or the request will error. -#### `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: type[T] | list[str] | None = None) -> AcceptedElicitation[T] | AcceptedElicitation[dict[str, Any]] | AcceptedElicitation[str] | DeclinedElicitation | CancelledElicitation @@ -384,16 +384,7 @@ type or dataclass or BaseModel. If it is a primitive type, an object schema with a single "value" field will be generated. -#### `get_http_request` - -```python -get_http_request(self) -> Request -``` - -Get the active starlette request. - - -#### `set_state` +#### `set_state` ```python set_state(self, key: str, value: Any) -> None @@ -402,7 +393,7 @@ set_state(self, key: str, value: Any) -> None Set a value in the context state. -#### `get_state` +#### `get_state` ```python get_state(self, key: str) -> Any diff --git a/docs/python-sdk/fastmcp-server-proxy.mdx b/docs/python-sdk/fastmcp-server-proxy.mdx index 445fc1a688..da1447345e 100644 --- a/docs/python-sdk/fastmcp-server-proxy.mdx +++ b/docs/python-sdk/fastmcp-server-proxy.mdx @@ -7,7 +7,7 @@ sidebarTitle: proxy ## Functions -### `default_proxy_roots_handler` +### `default_proxy_roots_handler` ```python default_proxy_roots_handler(context: RequestContext[ClientSession, LifespanContextT]) -> RootsList @@ -19,13 +19,13 @@ A handler that forwards the list roots request from the remote server to the pro ## Classes -### `ProxyManagerMixin` +### `ProxyManagerMixin` A mixin for proxy managers to provide a unified client retrieval method. -### `ProxyToolManager` +### `ProxyToolManager` A ToolManager that sources its tools from a remote client in addition to local and mounted tools. @@ -33,7 +33,7 @@ A ToolManager that sources its tools from a remote client in addition to local a **Methods:** -#### `get_tools` +#### `get_tools` ```python get_tools(self) -> dict[str, Tool] @@ -42,7 +42,7 @@ get_tools(self) -> dict[str, Tool] Gets the unfiltered tool inventory including local, mounted, and proxy tools. -#### `list_tools` +#### `list_tools` ```python list_tools(self) -> list[Tool] @@ -51,7 +51,7 @@ list_tools(self) -> list[Tool] Gets the filtered list of tools including local, mounted, and proxy tools. -#### `call_tool` +#### `call_tool` ```python call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult @@ -60,7 +60,7 @@ call_tool(self, key: str, arguments: dict[str, Any]) -> ToolResult Calls a tool, trying local/mounted first, then proxy if not found. -### `ProxyResourceManager` +### `ProxyResourceManager` A ResourceManager that sources its resources from a remote client in addition to local and mounted resources. @@ -68,7 +68,7 @@ A ResourceManager that sources its resources from a remote client in addition to **Methods:** -#### `get_resources` +#### `get_resources` ```python get_resources(self) -> dict[str, Resource] @@ -77,7 +77,7 @@ get_resources(self) -> dict[str, Resource] Gets the unfiltered resource inventory including local, mounted, and proxy resources. -#### `get_resource_templates` +#### `get_resource_templates` ```python get_resource_templates(self) -> dict[str, ResourceTemplate] @@ -86,7 +86,7 @@ get_resource_templates(self) -> dict[str, ResourceTemplate] Gets the unfiltered template inventory including local, mounted, and proxy templates. -#### `list_resources` +#### `list_resources` ```python list_resources(self) -> list[Resource] @@ -95,7 +95,7 @@ list_resources(self) -> list[Resource] Gets the filtered list of resources including local, mounted, and proxy resources. -#### `list_resource_templates` +#### `list_resource_templates` ```python list_resource_templates(self) -> list[ResourceTemplate] @@ -104,7 +104,7 @@ list_resource_templates(self) -> list[ResourceTemplate] Gets the filtered list of templates including local, mounted, and proxy templates. -#### `read_resource` +#### `read_resource` ```python read_resource(self, uri: AnyUrl | str) -> str | bytes @@ -113,7 +113,7 @@ read_resource(self, uri: AnyUrl | str) -> str | bytes Reads a resource, trying local/mounted first, then proxy if not found. -### `ProxyPromptManager` +### `ProxyPromptManager` A PromptManager that sources its prompts from a remote client in addition to local and mounted prompts. @@ -121,7 +121,7 @@ A PromptManager that sources its prompts from a remote client in addition to loc **Methods:** -#### `get_prompts` +#### `get_prompts` ```python get_prompts(self) -> dict[str, Prompt] @@ -130,7 +130,7 @@ get_prompts(self) -> dict[str, Prompt] Gets the unfiltered prompt inventory including local, mounted, and proxy prompts. -#### `list_prompts` +#### `list_prompts` ```python list_prompts(self) -> list[Prompt] @@ -139,7 +139,7 @@ list_prompts(self) -> list[Prompt] Gets the filtered list of prompts including local, mounted, and proxy prompts. -#### `render_prompt` +#### `render_prompt` ```python render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPromptResult @@ -148,7 +148,7 @@ render_prompt(self, name: str, arguments: dict[str, Any] | None = None) -> GetPr Renders a prompt, trying local/mounted first, then proxy if not found. -### `ProxyTool` +### `ProxyTool` A Tool that represents and executes a tool on a remote server. @@ -156,7 +156,7 @@ A Tool that represents and executes a tool on a remote server. **Methods:** -#### `from_mcp_tool` +#### `from_mcp_tool` ```python from_mcp_tool(cls, client: Client, mcp_tool: mcp.types.Tool) -> ProxyTool @@ -165,7 +165,7 @@ from_mcp_tool(cls, client: Client, mcp_tool: mcp.types.Tool) -> ProxyTool Factory method to create a ProxyTool from a raw MCP tool schema. -#### `run` +#### `run` ```python run(self, arguments: dict[str, Any], context: Context | None = None) -> ToolResult @@ -174,7 +174,7 @@ run(self, arguments: dict[str, Any], context: Context | None = None) -> ToolResu Executes the tool by making a call through the client. -### `ProxyResource` +### `ProxyResource` A Resource that represents and reads a resource from a remote server. @@ -182,7 +182,7 @@ A Resource that represents and reads a resource from a remote server. **Methods:** -#### `from_mcp_resource` +#### `from_mcp_resource` ```python from_mcp_resource(cls, client: Client, mcp_resource: mcp.types.Resource) -> ProxyResource @@ -191,7 +191,7 @@ from_mcp_resource(cls, client: Client, mcp_resource: mcp.types.Resource) -> Prox Factory method to create a ProxyResource from a raw MCP resource schema. -#### `read` +#### `read` ```python read(self) -> str | bytes @@ -200,7 +200,7 @@ read(self) -> str | bytes Read the resource content from the remote server. -### `ProxyTemplate` +### `ProxyTemplate` A ResourceTemplate that represents and creates resources from a remote server template. @@ -208,7 +208,7 @@ A ResourceTemplate that represents and creates resources from a remote server te **Methods:** -#### `from_mcp_template` +#### `from_mcp_template` ```python from_mcp_template(cls, client: Client, mcp_template: mcp.types.ResourceTemplate) -> ProxyTemplate @@ -217,7 +217,7 @@ from_mcp_template(cls, client: Client, mcp_template: mcp.types.ResourceTemplate) Factory method to create a ProxyTemplate from a raw MCP template schema. -#### `create_resource` +#### `create_resource` ```python create_resource(self, uri: str, params: dict[str, Any], context: Context | None = None) -> ProxyResource @@ -226,7 +226,7 @@ create_resource(self, uri: str, params: dict[str, Any], context: Context | None Create a resource from the template by calling the remote server. -### `ProxyPrompt` +### `ProxyPrompt` A Prompt that represents and renders a prompt from a remote server. @@ -234,7 +234,7 @@ A Prompt that represents and renders a prompt from a remote server. **Methods:** -#### `from_mcp_prompt` +#### `from_mcp_prompt` ```python from_mcp_prompt(cls, client: Client, mcp_prompt: mcp.types.Prompt) -> ProxyPrompt @@ -243,7 +243,7 @@ from_mcp_prompt(cls, client: Client, mcp_prompt: mcp.types.Prompt) -> ProxyPromp Factory method to create a ProxyPrompt from a raw MCP prompt schema. -#### `render` +#### `render` ```python render(self, arguments: dict[str, Any]) -> list[PromptMessage] @@ -252,14 +252,14 @@ render(self, arguments: dict[str, Any]) -> list[PromptMessage] Render the prompt by making a call through the client. -### `FastMCPProxy` +### `FastMCPProxy` A FastMCP server that acts as a proxy to a remote MCP-compliant server. It uses specialized managers that fulfill requests via a client factory. -### `ProxyClient` +### `ProxyClient` A proxy client that forwards advanced interactions between a remote MCP server and the proxy's connected clients. @@ -268,7 +268,7 @@ Supports forwarding roots, sampling, elicitation, logging, and progress. **Methods:** -#### `default_sampling_handler` +#### `default_sampling_handler` ```python default_sampling_handler(cls, messages: list[mcp.types.SamplingMessage], params: mcp.types.CreateMessageRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> mcp.types.CreateMessageResult @@ -277,7 +277,7 @@ default_sampling_handler(cls, messages: list[mcp.types.SamplingMessage], params: A handler that forwards the sampling request from the remote server to the proxy's connected clients and relays the response back to the remote server. -#### `default_elicitation_handler` +#### `default_elicitation_handler` ```python default_elicitation_handler(cls, message: str, response_type: type, params: mcp.types.ElicitRequestParams, context: RequestContext[ClientSession, LifespanContextT]) -> ElicitResult @@ -286,7 +286,7 @@ default_elicitation_handler(cls, message: str, response_type: type, params: mcp. A handler that forwards the elicitation request from the remote server to the proxy's connected clients and relays the response back to the remote server. -#### `default_log_handler` +#### `default_log_handler` ```python default_log_handler(cls, message: LogMessage) -> None @@ -295,7 +295,7 @@ default_log_handler(cls, message: LogMessage) -> None A handler that forwards the log notification from the remote server to the proxy's connected clients. -#### `default_progress_handler` +#### `default_progress_handler` ```python default_progress_handler(cls, progress: float, total: float | None, message: str | None) -> None @@ -304,7 +304,7 @@ default_progress_handler(cls, progress: float, total: float | None, message: str A handler that forwards the progress notification from the remote server to the proxy's connected clients. -### `StatefulProxyClient` +### `StatefulProxyClient` A proxy client that provides a stateful client factory for the proxy server. @@ -318,7 +318,7 @@ Note that it is essential to ensure that the proxy server itself is also statefu **Methods:** -#### `clear` +#### `clear` ```python clear(self) @@ -327,7 +327,7 @@ clear(self) Clear all cached clients and force disconnect them. -#### `new_stateful` +#### `new_stateful` ```python new_stateful(self) -> Client[ClientTransportT] diff --git a/docs/python-sdk/fastmcp-server-server.mdx b/docs/python-sdk/fastmcp-server-server.mdx index 086f6072b5..186377e35b 100644 --- a/docs/python-sdk/fastmcp-server-server.mdx +++ b/docs/python-sdk/fastmcp-server-server.mdx @@ -10,7 +10,7 @@ FastMCP - A more ergonomic interface for MCP servers. ## Functions -### `default_lifespan` +### `default_lifespan` ```python default_lifespan(server: FastMCP[LifespanResultT]) -> AsyncIterator[Any] @@ -26,14 +26,14 @@ Default lifespan context manager that does nothing. - An empty dictionary as the lifespan result. -### `add_resource_prefix` +### `add_resource_prefix` ```python -add_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> str +add_resource_prefix(uri: str, prefix: str) -> str ``` -Add a prefix to a resource URI. +Add a prefix to a resource URI using path formatting (resource://prefix/path). **Args:** - `uri`: The original resource URI @@ -44,16 +44,10 @@ Add a prefix to a resource URI. **Examples:** -With new style: ```python add_resource_prefix("resource://path/to/resource", "prefix") "resource://prefix/path/to/resource" ``` -With legacy style: -```python -add_resource_prefix("resource://path/to/resource", "prefix") -"prefix+resource://path/to/resource" -``` With absolute path: ```python add_resource_prefix("resource:///absolute/path", "prefix") @@ -64,10 +58,10 @@ add_resource_prefix("resource:///absolute/path", "prefix") - `ValueError`: If the URI doesn't match the expected protocol\://path format -### `remove_resource_prefix` +### `remove_resource_prefix` ```python -remove_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> str +remove_resource_prefix(uri: str, prefix: str) -> str ``` @@ -76,23 +70,16 @@ Remove a prefix from a resource URI. **Args:** - `uri`: The resource URI with a prefix - `prefix`: The prefix to remove -- `prefix_format`: The format of the prefix to remove -Returns: - The resource URI with the prefix removed +**Returns:** +- The resource URI with the prefix removed **Examples:** -With new style: ```python remove_resource_prefix("resource://prefix/path/to/resource", "prefix") "resource://path/to/resource" ``` -With legacy style: -```python -remove_resource_prefix("prefix+resource://path/to/resource", "prefix") -"resource://path/to/resource" -``` With absolute path: ```python remove_resource_prefix("resource://prefix//absolute/path", "prefix") @@ -103,10 +90,10 @@ remove_resource_prefix("resource://prefix//absolute/path", "prefix") - `ValueError`: If the URI doesn't match the expected protocol\://path format -### `has_resource_prefix` +### `has_resource_prefix` ```python -has_resource_prefix(uri: str, prefix: str, prefix_format: Literal['protocol', 'path'] | None = None) -> bool +has_resource_prefix(uri: str, prefix: str) -> bool ``` @@ -121,16 +108,10 @@ Check if a resource URI has a specific prefix. **Examples:** -With new style: ```python has_resource_prefix("resource://prefix/path/to/resource", "prefix") True ``` -With legacy style: -```python -has_resource_prefix("prefix+resource://path/to/resource", "prefix") -True -``` With other path: ```python has_resource_prefix("resource://other/path/to/resource", "prefix") @@ -143,53 +124,53 @@ False ## Classes -### `FastMCP` +### `FastMCP` **Methods:** -#### `settings` +#### `settings` ```python settings(self) -> Settings ``` -#### `name` +#### `name` ```python name(self) -> str ``` -#### `instructions` +#### `instructions` ```python instructions(self) -> str | None ``` -#### `instructions` +#### `instructions` ```python instructions(self, value: str | None) -> None ``` -#### `version` +#### `version` ```python version(self) -> str | None ``` -#### `website_url` +#### `website_url` ```python website_url(self) -> str | None ``` -#### `icons` +#### `icons` ```python icons(self) -> list[mcp.types.Icon] ``` -#### `run_async` +#### `run_async` ```python run_async(self, transport: Transport | None = None, show_banner: bool = True, **transport_kwargs: Any) -> None @@ -201,7 +182,7 @@ Run the FastMCP server asynchronously. - `transport`: Transport protocol to use ("stdio", "sse", or "streamable-http") -#### `run` +#### `run` ```python run(self, transport: Transport | None = None, show_banner: bool = True, **transport_kwargs: Any) -> None @@ -213,13 +194,13 @@ Run the FastMCP server. Note this is a synchronous function. - `transport`: Transport protocol to use ("stdio", "sse", or "streamable-http") -#### `add_middleware` +#### `add_middleware` ```python add_middleware(self, middleware: Middleware) -> None ``` -#### `get_tools` +#### `get_tools` ```python get_tools(self) -> dict[str, Tool] @@ -228,13 +209,13 @@ get_tools(self) -> dict[str, Tool] Get all tools (unfiltered), including mounted servers, indexed by key. -#### `get_tool` +#### `get_tool` ```python get_tool(self, key: str) -> Tool ``` -#### `get_resources` +#### `get_resources` ```python get_resources(self) -> dict[str, Resource] @@ -243,13 +224,13 @@ get_resources(self) -> dict[str, Resource] Get all resources (unfiltered), including mounted servers, indexed by key. -#### `get_resource` +#### `get_resource` ```python get_resource(self, key: str) -> Resource ``` -#### `get_resource_templates` +#### `get_resource_templates` ```python get_resource_templates(self) -> dict[str, ResourceTemplate] @@ -258,7 +239,7 @@ get_resource_templates(self) -> dict[str, ResourceTemplate] Get all resource templates (unfiltered), including mounted servers, indexed by key. -#### `get_resource_template` +#### `get_resource_template` ```python get_resource_template(self, key: str) -> ResourceTemplate @@ -267,7 +248,7 @@ get_resource_template(self, key: str) -> ResourceTemplate Get a registered resource template by key. -#### `get_prompts` +#### `get_prompts` ```python get_prompts(self) -> dict[str, Prompt] @@ -276,13 +257,13 @@ get_prompts(self) -> dict[str, Prompt] Get all prompts (unfiltered), including mounted servers, indexed by key. -#### `get_prompt` +#### `get_prompt` ```python get_prompt(self, key: str) -> Prompt ``` -#### `custom_route` +#### `custom_route` ```python custom_route(self, path: str, methods: list[str], name: str | None = None, include_in_schema: bool = True) -> Callable[[Callable[[Request], Awaitable[Response]]], Callable[[Request], Awaitable[Response]]] @@ -303,7 +284,7 @@ Starlette's reverse URL lookup feature) - `include_in_schema`: Whether to include in OpenAPI schema, defaults to True -#### `add_tool` +#### `add_tool` ```python add_tool(self, tool: Tool) -> Tool @@ -321,7 +302,7 @@ with the Context type annotation. See the @tool decorator for examples. - The tool instance that was added to the server. -#### `remove_tool` +#### `remove_tool` ```python remove_tool(self, name: str) -> None @@ -336,7 +317,7 @@ Remove a tool from the server. - `NotFoundError`: If the tool is not found -#### `add_tool_transformation` +#### `add_tool_transformation` ```python add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfig) -> None @@ -345,7 +326,7 @@ add_tool_transformation(self, tool_name: str, transformation: ToolTransformConfi Add a tool transformation. -#### `remove_tool_transformation` +#### `remove_tool_transformation` ```python remove_tool_transformation(self, tool_name: str) -> None @@ -354,19 +335,19 @@ remove_tool_transformation(self, tool_name: str) -> None Remove a tool transformation. -#### `tool` +#### `tool` ```python tool(self, name_or_fn: AnyFunction) -> FunctionTool ``` -#### `tool` +#### `tool` ```python tool(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionTool] ``` -#### `tool` +#### `tool` ```python tool(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionTool] | FunctionTool @@ -424,7 +405,7 @@ server.tool(my_function, name="custom_name") ``` -#### `add_resource` +#### `add_resource` ```python add_resource(self, resource: Resource) -> Resource @@ -439,7 +420,7 @@ Add a resource to the server. - The resource instance that was added to the server. -#### `add_template` +#### `add_template` ```python add_template(self, template: ResourceTemplate) -> ResourceTemplate @@ -454,27 +435,7 @@ Add a resource template to the server. - The template instance that was added to the server. -#### `add_resource_fn` - -```python -add_resource_fn(self, fn: AnyFunction, uri: str, name: str | None = None, description: str | None = None, mime_type: str | None = None, tags: set[str] | None = None) -> None -``` - -Add a resource or template to the server from a function. - -If the URI contains parameters (e.g. "resource://{param}") or the function -has parameters, it will be registered as a template resource. - -**Args:** -- `fn`: The function to register as a resource -- `uri`: The URI for the resource -- `name`: Optional name for the resource -- `description`: Optional description of the resource -- `mime_type`: Optional MIME type for the resource -- `tags`: Optional set of tags for categorizing the resource - - -#### `resource` +#### `resource` ```python resource(self, uri: str) -> Callable[[AnyFunction], Resource | ResourceTemplate] @@ -534,7 +495,7 @@ async def get_weather(city: str) -> str: ``` -#### `add_prompt` +#### `add_prompt` ```python add_prompt(self, prompt: Prompt) -> Prompt @@ -549,19 +510,19 @@ Add a prompt to the server. - The prompt instance that was added to the server. -#### `prompt` +#### `prompt` ```python prompt(self, name_or_fn: AnyFunction) -> FunctionPrompt ``` -#### `prompt` +#### `prompt` ```python prompt(self, name_or_fn: str | None = None) -> Callable[[AnyFunction], FunctionPrompt] ``` -#### `prompt` +#### `prompt` ```python prompt(self, name_or_fn: str | AnyFunction | None = None) -> Callable[[AnyFunction], FunctionPrompt] | FunctionPrompt @@ -639,7 +600,7 @@ Decorator to register a prompt. ``` -#### `run_stdio_async` +#### `run_stdio_async` ```python run_stdio_async(self, show_banner: bool = True, log_level: str | None = None) -> None @@ -652,7 +613,7 @@ Run the server using stdio transport. - `log_level`: Log level for the server -#### `run_http_async` +#### `run_http_async` ```python run_http_async(self, show_banner: bool = True, transport: Literal['http', 'streamable-http', 'sse'] = 'http', host: str | None = None, port: int | None = None, log_level: str | None = None, path: str | None = None, uvicorn_config: dict[str, Any] | None = None, middleware: list[ASGIMiddleware] | None = None, json_response: bool | None = None, stateless_http: bool | None = None) -> None @@ -672,43 +633,7 @@ Run the server using HTTP transport. - `stateless_http`: Whether to use stateless HTTP (defaults to settings.stateless_http) -#### `run_sse_async` - -```python -run_sse_async(self, host: str | None = None, port: int | None = None, log_level: str | None = None, path: str | None = None, uvicorn_config: dict[str, Any] | None = None) -> None -``` - -Run the server using SSE transport. - - -#### `sse_app` - -```python -sse_app(self, path: str | None = None, message_path: str | None = None, middleware: list[ASGIMiddleware] | None = None) -> StarletteWithLifespan -``` - -Create a Starlette app for the SSE server. - -**Args:** -- `path`: The path to the SSE endpoint -- `message_path`: The path to the message endpoint -- `middleware`: A list of middleware to apply to the app - - -#### `streamable_http_app` - -```python -streamable_http_app(self, path: str | None = None, middleware: list[ASGIMiddleware] | None = None) -> StarletteWithLifespan -``` - -Create a Starlette app for the StreamableHTTP server. - -**Args:** -- `path`: The path to the StreamableHTTP endpoint -- `middleware`: A list of middleware to apply to the app - - -#### `http_app` +#### `http_app` ```python http_app(self, path: str | None = None, middleware: list[ASGIMiddleware] | None = None, json_response: bool | None = None, stateless_http: bool | None = None, transport: Literal['http', 'streamable-http', 'sse'] = 'http') -> StarletteWithLifespan @@ -725,13 +650,7 @@ Create a Starlette app using the specified HTTP transport. - A Starlette application configured with the specified transport -#### `run_streamable_http_async` - -```python -run_streamable_http_async(self, host: str | None = None, port: int | None = None, log_level: str | None = None, path: str | None = None, uvicorn_config: dict[str, Any] | None = None) -> None -``` - -#### `mount` +#### `mount` ```python mount(self, server: FastMCP[LifespanResultT], prefix: str | None = None, as_proxy: bool | None = None) -> None @@ -785,7 +704,7 @@ automatically determined based on whether the server has a custom lifespan - `prompt_separator`: Deprecated. Separator character for prompt names. -#### `import_server` +#### `import_server` ```python import_server(self, server: FastMCP[LifespanResultT], prefix: str | None = None, tool_separator: str | None = None, resource_separator: str | None = None, prompt_separator: str | None = None) -> None @@ -826,7 +745,7 @@ applied using the protocol\://prefix/path format - `prompt_separator`: Deprecated. Separator for prompt names. -#### `from_openapi` +#### `from_openapi` ```python from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, route_maps: list[RouteMap] | list[RouteMapNew] | None = None, route_map_fn: OpenAPIRouteMapFn | OpenAPIRouteMapFnNew | None = None, mcp_component_fn: OpenAPIComponentFn | OpenAPIComponentFnNew | None = None, mcp_names: dict[str, str] | None = None, tags: set[str] | None = None, **settings: Any) -> FastMCPOpenAPI | FastMCPOpenAPINew @@ -835,7 +754,7 @@ from_openapi(cls, openapi_spec: dict[str, Any], client: httpx.AsyncClient, route Create a FastMCP server from an OpenAPI specification. -#### `from_fastapi` +#### `from_fastapi` ```python from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] | list[RouteMapNew] | None = None, route_map_fn: OpenAPIRouteMapFn | OpenAPIRouteMapFnNew | None = None, mcp_component_fn: OpenAPIComponentFn | OpenAPIComponentFnNew | None = None, mcp_names: dict[str, str] | None = None, httpx_client_kwargs: dict[str, Any] | None = None, tags: set[str] | None = None, **settings: Any) -> FastMCPOpenAPI | FastMCPOpenAPINew @@ -844,7 +763,7 @@ from_fastapi(cls, app: Any, name: str | None = None, route_maps: list[RouteMap] Create a FastMCP server from a FastAPI application. -#### `as_proxy` +#### `as_proxy` ```python as_proxy(cls, backend: Client[ClientTransportT] | ClientTransport | FastMCP[Any] | FastMCP1Server | AnyUrl | Path | MCPConfig | dict[str, Any] | str, **settings: Any) -> FastMCPProxy @@ -858,19 +777,10 @@ instance or any value accepted as the `transport` argument of `fastmcp.client.Client` constructor. -#### `from_client` - -```python -from_client(cls, client: Client[ClientTransportT], **settings: Any) -> FastMCPProxy -``` - -Create a FastMCP proxy server from a FastMCP client. - - -#### `generate_name` +#### `generate_name` ```python generate_name(cls, name: str | None = None) -> str ``` -### `MountedServer` +### `MountedServer` diff --git a/docs/python-sdk/fastmcp-settings.mdx b/docs/python-sdk/fastmcp-settings.mdx index 5f9451a0d5..716534ea1c 100644 --- a/docs/python-sdk/fastmcp-settings.mdx +++ b/docs/python-sdk/fastmcp-settings.mdx @@ -7,27 +7,9 @@ sidebarTitle: settings ## Classes -### `ExtendedEnvSettingsSource` +### `ExperimentalSettings` - -A special EnvSettingsSource that allows for multiple env var prefixes to be used. - -Raises a deprecation warning if the old `FASTMCP_SERVER_` prefix is used. - - -**Methods:** - -#### `get_field_value` - -```python -get_field_value(self, field: FieldInfo, field_name: str) -> tuple[Any, str, bool] -``` - -### `ExtendedSettingsConfigDict` - -### `ExperimentalSettings` - -### `Settings` +### `Settings` FastMCP settings. @@ -35,7 +17,7 @@ FastMCP settings. **Methods:** -#### `get_setting` +#### `get_setting` ```python get_setting(self, attr: str) -> Any @@ -45,7 +27,7 @@ Get a setting. If the setting contains one or more `__`, it will be treated as a nested setting. -#### `set_setting` +#### `set_setting` ```python set_setting(self, attr: str, value: Any) -> None @@ -55,13 +37,7 @@ Set a setting. If the setting contains one or more `__`, it will be treated as a nested setting. -#### `settings_customise_sources` - -```python -settings_customise_sources(cls, settings_cls: type[BaseSettings], init_settings: PydanticBaseSettingsSource, env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource) -> tuple[PydanticBaseSettingsSource, ...] -``` - -#### `settings` +#### `settings` ```python settings(self) -> Self @@ -71,13 +47,13 @@ This property is for backwards compatibility with FastMCP < 2.8.0, which accessed fastmcp.settings.settings -#### `normalize_log_level` +#### `normalize_log_level` ```python normalize_log_level(cls, v) ``` -#### `server_auth_class` +#### `server_auth_class` ```python server_auth_class(self) -> AuthProvider | None diff --git a/docs/python-sdk/fastmcp-tools-tool.mdx b/docs/python-sdk/fastmcp-tools-tool.mdx index aa1f33624e..fff2e46575 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]] | CallToolResult ``` -### `Tool` +### `Tool` Internal tool registration info. @@ -33,19 +33,19 @@ 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 @@ -54,16 +54,16 @@ to_mcp_tool(self, **overrides: Any) -> MCPTool Convert the FastMCP tool to an MCP tool. -#### `from_function` +#### `from_function` ```python -from_function(fn: Callable[..., Any], name: str | None = None, title: str | None = None, description: str | None = None, icons: list[Icon] | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet, serializer: ToolResultSerializerType | 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, icons: list[Icon] | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | NotSetT | None = NotSet, serializer: ToolResultSerializerType | 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 @@ -78,26 +78,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, icons: list[Icon] | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet, serializer: ToolResultSerializerType | 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, icons: list[Icon] | None = None, tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, output_schema: dict[str, Any] | NotSetT | None = NotSet, serializer: ToolResultSerializerType | 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 @@ -106,11 +106,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 bccf670d4d..8586fb4fcc 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: Any) -> 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: Any) -> 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 | NotSetT | None = NotSet, description: str | NotSetT | None = NotSet, tags: set[str] | None = None, transform_fn: Callable[..., Any] | None = None, transform_args: dict[str, ArgTransform] | None = None, annotations: ToolAnnotations | NotSetT | None = NotSet, output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet, serializer: Callable[[Any], str] | NotSetT | None = NotSet, meta: dict[str, Any] | NotSetT | None = NotSet, enabled: bool | None = None) -> TransformedTool +from_tool(cls, tool: Tool, name: str | None = None, title: str | NotSetT | None = NotSet, description: str | NotSetT | None = NotSet, tags: set[str] | None = None, transform_fn: Callable[..., Any] | None = None, transform_args: dict[str, ArgTransform] | None = None, annotations: ToolAnnotations | NotSetT | None = NotSet, output_schema: dict[str, Any] | NotSetT | None = NotSet, serializer: Callable[[Any], str] | NotSetT | None = NotSet, meta: dict[str, Any] | NotSetT | None = 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/docs/servers/composition.mdx b/docs/servers/composition.mdx index d90bb47c76..6c614000c6 100644 --- a/docs/servers/composition.mdx +++ b/docs/servers/composition.mdx @@ -41,7 +41,7 @@ FastMCP supports [MCP proxying](/servers/proxy), which allows you to mirror a lo You can also create proxies from configuration dictionaries that follow the MCPConfig schema, which is useful for quickly connecting to one or more remote servers. See the [Proxy Servers documentation](/servers/proxy#configuration-based-proxies) for details on configuration-based proxying. Note that MCPConfig follows an emerging standard and its format may evolve over time. -Prefixing rules for tools, prompts, resources, and templates are identical across importing, mounting, and proxies. +Prefixing rules for tools, prompts, resources, and templates are identical across importing, mounting, and proxies. When prefixes are used, resource URIs are prefixed using path format (since 2.4.0): `resource://prefix/path/to/resource`. ## Importing (Static Composition) @@ -207,6 +207,10 @@ The same prefixing rules apply as with `import_server` for naming tools, resourc The `prefix` parameter is optional. If omitted, components are mounted without modification. + +When mounting servers, custom HTTP routes defined with `@server.custom_route()` are also forwarded to the parent server, making them accessible through the parent's HTTP application. + + #### Performance Considerations Due to the "live link", operations like `list_tools()` on the parent server will be impacted by the speed of the slowest mounted server. In particular, HTTP-based mounted servers can introduce significant latency (300-400ms vs 1-2ms for local tools), and this slowdown affects the whole server, not just interactions with the HTTP-proxied tools. If performance is important, importing tools via [`import_server()`](#importing-static-composition) may be a more appropriate solution as it copies components once at startup rather than delegating requests at runtime. @@ -320,62 +324,3 @@ This ensures that parent server tag policies act as a global policy for everythi This filtering applies to both **listing** (e.g., `list_tools()`) and **execution** (e.g., `call_tool()`). Filtered components are neither visible nor executable through the parent server. - -## Resource Prefix Formats - - - -When mounting or importing servers, resource URIs are usually prefixed to avoid naming conflicts. FastMCP supports two different formats for resource prefixes: - -### Path Format (Default) - -In path format, prefixes are added to the path component of the URI: - -``` -resource://prefix/path/to/resource -``` - -This is the default format since FastMCP 2.4. This format is recommended because it avoids issues with URI protocol restrictions (like underscores not being allowed in protocol names). - -### Protocol Format (Legacy) - -In protocol format, prefixes are added as part of the protocol: - -``` -prefix+resource://path/to/resource -``` - -This was the default format in FastMCP before 2.4. While still supported, it's not recommended for new code as it can cause problems with prefix names that aren't valid in URI protocols. - -### Configuring the Prefix Format - -You can configure the prefix format globally in code: - -```python -import fastmcp -fastmcp.settings.resource_prefix_format = "protocol" -``` - -Or via environment variable: - -```bash -FASTMCP_RESOURCE_PREFIX_FORMAT=protocol -``` - -Or per-server: - -```python -from fastmcp import FastMCP - -# Create a server that uses legacy protocol format -server = FastMCP("LegacyServer", resource_prefix_format="protocol") - -# Create a server that uses new path format -server = FastMCP("NewServer", resource_prefix_format="path") -``` - -When mounting or importing servers, the prefix format of the parent server is used. - - -When mounting servers, custom HTTP routes defined with `@server.custom_route()` are also forwarded to the parent server, making them accessible through the parent's HTTP application. - diff --git a/docs/servers/proxy.mdx b/docs/servers/proxy.mdx index 7cbb6d90cc..401005dea1 100644 --- a/docs/servers/proxy.mdx +++ b/docs/servers/proxy.mdx @@ -266,8 +266,6 @@ These rules apply uniformly whether you: - Create a multi-server proxy from an `MCPConfig` - Use `FastMCP.as_proxy()` directly -For resource URI prefix formats (path vs legacy protocol style) and configuration options, see Server Composition → Resource Prefix Formats. - ## Mirrored Components @@ -317,7 +315,6 @@ proxy = FastMCPProxy(client_factory=create_client) ### Parameters -- **`client`**: **[DEPRECATED]** A `Client` instance. Use `client_factory` instead for explicit session management. - **`client_factory`**: A callable that returns a `Client` instance when called. This gives you full control over session creation and reuse strategies. ### Explicit Session Management diff --git a/docs/servers/server.mdx b/docs/servers/server.mdx index f1f4ed6da5..8d8d362f84 100644 --- a/docs/servers/server.mdx +++ b/docs/servers/server.mdx @@ -357,7 +357,6 @@ import fastmcp # Access global settings print(fastmcp.settings.log_level) # Default: "INFO" print(fastmcp.settings.mask_error_details) # Default: False -print(fastmcp.settings.resource_prefix_format) # Default: "path" print(fastmcp.settings.strict_input_validation) # Default: False print(fastmcp.settings.include_fastmcp_meta) # Default: True ``` @@ -365,7 +364,6 @@ print(fastmcp.settings.include_fastmcp_meta) # Default: True Common global settings include: - **`log_level`**: Logging level ("DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"), set with `FASTMCP_LOG_LEVEL` - **`mask_error_details`**: Whether to hide detailed error information from clients, set with `FASTMCP_MASK_ERROR_DETAILS` -- **`resource_prefix_format`**: How to format resource prefixes ("path" or "protocol"), set with `FASTMCP_RESOURCE_PREFIX_FORMAT` - **`strict_input_validation`**: Controls tool input validation mode (default: False for flexible coercion), set with `FASTMCP_STRICT_INPUT_VALIDATION`. See [Input Validation Modes](/servers/tools#input-validation-modes) - **`include_fastmcp_meta`**: Whether to include FastMCP metadata in component responses (default: True), set with `FASTMCP_INCLUDE_FASTMCP_META` - **`env_file`**: Path to the environment file to load settings from (default: ".env"), set with `FASTMCP_ENV_FILE`. Useful when your project uses a `.env` file with syntax incompatible with python-dotenv @@ -399,7 +397,6 @@ Global FastMCP settings can be configured via environment variables (prefixed wi # Configure global FastMCP behavior export FASTMCP_LOG_LEVEL=DEBUG export FASTMCP_MASK_ERROR_DETAILS=True -export FASTMCP_RESOURCE_PREFIX_FORMAT=protocol export FASTMCP_STRICT_INPUT_VALIDATION=False export FASTMCP_INCLUDE_FASTMCP_META=False ``` diff --git a/examples/memory.py b/examples/memory.py index c5a3488a4b..792bd48c6c 100644 --- a/examples/memory.py +++ b/examples/memory.py @@ -34,15 +34,8 @@ DEFAULT_LLM_MODEL = "openai:gpt-4o" DEFAULT_EMBEDDING_MODEL = "text-embedding-3-small" -mcp = FastMCP( - "memory", - dependencies=[ - "pydantic-ai-slim[openai]", - "asyncpg", - "numpy", - "pgvector", - ], -) +# Dependencies are configured in memory.fastmcp.json +mcp = FastMCP("memory") DB_DSN = "postgresql://postgres:postgres@localhost:54320/memory_db" # reset memory by deleting the profile directory diff --git a/examples/screenshot.py b/examples/screenshot.py index 3322c03257..8661337335 100644 --- a/examples/screenshot.py +++ b/examples/screenshot.py @@ -1,3 +1,7 @@ +# /// script +# dependencies = ["pyautogui", "Pillow", "fastmcp"] +# /// + """ FastMCP Screenshot Example @@ -10,7 +14,8 @@ from fastmcp.utilities.types import Image # Create server -mcp = FastMCP("Screenshot Demo", dependencies=["pyautogui", "Pillow"]) +# Dependencies are configured in screenshot.fastmcp.json +mcp = FastMCP("Screenshot Demo") @mcp.tool diff --git a/examples/smart_home/src/smart_home/lights/server.py b/examples/smart_home/src/smart_home/lights/server.py index 87e8d4455d..aa559b68f1 100644 --- a/examples/smart_home/src/smart_home/lights/server.py +++ b/examples/smart_home/src/smart_home/lights/server.py @@ -1,3 +1,10 @@ +# /// script +# dependencies = [ +# "smart_home@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/smart_home", +# "fastmcp", +# ] +# /// + from typing import Annotated, Any, Literal, TypedDict from phue2.exceptions import PhueException @@ -35,12 +42,8 @@ class HueAttributes(TypedDict, total=False): transitiontime: NotRequired[Annotated[int, Field(description="deciseconds")]] -lights_mcp = FastMCP( - "Hue Lights Service (phue2)", - dependencies=[ - "smart_home@git+https://github.com/jlowin/fastmcp.git#subdirectory=examples/smart_home", - ], -) +# Dependencies are configured in lights.fastmcp.json +lights_mcp = FastMCP("Hue Lights Service (phue2)") @lights_mcp.tool diff --git a/src/fastmcp/__init__.py b/src/fastmcp/__init__.py index d47bf88c8f..99cce018d3 100644 --- a/src/fastmcp/__init__.py +++ b/src/fastmcp/__init__.py @@ -27,26 +27,6 @@ warnings.simplefilter("default", DeprecationWarning) -def __getattr__(name: str): - """ - Used to deprecate the module-level Image class; can be removed once it is no longer imported to root. - """ - if name == "Image": - # Deprecated in 2.8.1 - if settings.deprecation_warnings: - warnings.warn( - "The top-level `fastmcp.Image` import is deprecated " - "and will be removed in a future version. " - "Please use `fastmcp.utilities.types.Image` instead.", - DeprecationWarning, - stacklevel=2, - ) - from fastmcp.utilities.types import Image - - return Image - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") - - __all__ = [ "Client", "Context", diff --git a/src/fastmcp/cli/cli.py b/src/fastmcp/cli/cli.py index 17d7e1c995..3ed5a3c01e 100644 --- a/src/fastmcp/cli/cli.py +++ b/src/fastmcp/cli/cli.py @@ -19,7 +19,6 @@ import fastmcp from fastmcp.cli import run as run_module from fastmcp.cli.install import install_app -from fastmcp.server.server import FastMCP from fastmcp.utilities.cli import is_already_in_uv_subprocess, load_and_merge_config from fastmcp.utilities.inspect import ( InspectFormat, @@ -28,7 +27,6 @@ ) from fastmcp.utilities.logging import get_logger from fastmcp.utilities.mcp_server_config import MCPServerConfig -from fastmcp.utilities.mcp_server_config.v1.environments.uv import UVEnvironment logger = get_logger("cli") console = Console() @@ -224,29 +222,11 @@ async def dev( ) try: - # Load server to check for deprecated dependencies if not config: logger.error("No configuration available") sys.exit(1) assert config is not None # For type checker - server: FastMCP = await config.source.load_server() - if server.dependencies: - import warnings - - warnings.warn( - f"Server '{server.name}' uses deprecated 'dependencies' parameter (deprecated in FastMCP 2.11.4). " - "Please migrate to fastmcp.json configuration file. " - "See https://gofastmcp.com/docs/deployment/server-configuration for details.", - DeprecationWarning, - stacklevel=2, - ) - # Merge server dependencies with environment dependencies - env_deps = config.environment.dependencies or [] - all_deps = list(set(env_deps + server.dependencies)) - if not config.environment: - config.environment = UVEnvironment(dependencies=all_deps) - else: - config.environment.dependencies = all_deps + await config.source.load_server() env_vars = {} if ui_port: diff --git a/src/fastmcp/cli/install/shared.py b/src/fastmcp/cli/install/shared.py index 3544a2b6bb..8e183562a1 100644 --- a/src/fastmcp/cli/install/shared.py +++ b/src/fastmcp/cli/install/shared.py @@ -105,21 +105,6 @@ async def process_common_args( ) name = file.stem - # Get server dependencies if available - # TODO: Remove dependencies handling (deprecated in v2.11.4) - server_dependencies = getattr(server, "dependencies", []) if server else [] - if server_dependencies: - import warnings - - warnings.warn( - "Server uses deprecated 'dependencies' parameter (deprecated in FastMCP 2.11.4). " - "Please migrate to fastmcp.json configuration file. " - "See https://gofastmcp.com/docs/deployment/server-configuration for details.", - DeprecationWarning, - stacklevel=2, - ) - with_packages = list(set(with_packages + server_dependencies)) - # Process environment variables if provided env_dict: dict[str, str] | None = None if env_file or env_vars: diff --git a/src/fastmcp/contrib/component_manager/component_service.py b/src/fastmcp/contrib/component_manager/component_service.py index b23f96420c..9fb2e4fbae 100644 --- a/src/fastmcp/contrib/component_manager/component_service.py +++ b/src/fastmcp/contrib/component_manager/component_service.py @@ -105,16 +105,8 @@ async def _enable_resource(self, key: str) -> Resource | ResourceTemplate: # 2. Check mounted servers using the filtered protocol path. for mounted in reversed(self._server._mounted_servers): if mounted.prefix: - if has_resource_prefix( - key, - mounted.prefix, - mounted.resource_prefix_format, - ): - key = remove_resource_prefix( - key, - mounted.prefix, - mounted.resource_prefix_format, - ) + if has_resource_prefix(key, mounted.prefix): + key = remove_resource_prefix(key, mounted.prefix) mounted_service = ComponentService(mounted.server) mounted_resource: ( Resource | ResourceTemplate @@ -148,16 +140,8 @@ async def _disable_resource(self, key: str) -> Resource | ResourceTemplate: # 2. Check mounted servers using the filtered protocol path. for mounted in reversed(self._server._mounted_servers): if mounted.prefix: - if has_resource_prefix( - key, - mounted.prefix, - mounted.resource_prefix_format, - ): - key = remove_resource_prefix( - key, - mounted.prefix, - mounted.resource_prefix_format, - ) + if has_resource_prefix(key, mounted.prefix): + key = remove_resource_prefix(key, mounted.prefix) mounted_service = ComponentService(mounted.server) mounted_resource: ( Resource | ResourceTemplate diff --git a/src/fastmcp/server/auth/__init__.py b/src/fastmcp/server/auth/__init__.py index e33ad022a2..43f2f3f2e4 100644 --- a/src/fastmcp/server/auth/__init__.py +++ b/src/fastmcp/server/auth/__init__.py @@ -23,12 +23,3 @@ "StaticTokenVerifier", "TokenVerifier", ] - - -def __getattr__(name: str): - # Defer import because it raises a deprecation warning - if name == "BearerAuthProvider": - from .providers.bearer import BearerAuthProvider - - return BearerAuthProvider - raise AttributeError(f"module '{__name__}' has no attribute '{name}'") diff --git a/src/fastmcp/server/auth/providers/bearer.py b/src/fastmcp/server/auth/providers/bearer.py deleted file mode 100644 index 1482ab0f3d..0000000000 --- a/src/fastmcp/server/auth/providers/bearer.py +++ /dev/null @@ -1,25 +0,0 @@ -"""Backwards compatibility shim for BearerAuthProvider. - -The BearerAuthProvider class has been moved to fastmcp.server.auth.providers.jwt.JWTVerifier -for better organization. This module provides a backwards-compatible import. -""" - -import warnings - -import fastmcp -from fastmcp.server.auth.providers.jwt import JWKData, JWKSData, RSAKeyPair -from fastmcp.server.auth.providers.jwt import JWTVerifier as BearerAuthProvider - -# Re-export for backwards compatibility -__all__ = ["BearerAuthProvider", "JWKData", "JWKSData", "RSAKeyPair"] - -# Deprecated in 2.11 -if fastmcp.settings.deprecation_warnings: - warnings.warn( - "The `fastmcp.server.auth.providers.bearer` module is deprecated " - "and will be removed in a future version. " - "Please use `fastmcp.server.auth.providers.jwt.JWTVerifier` " - "instead of this module's BearerAuthProvider.", - DeprecationWarning, - stacklevel=2, - ) diff --git a/src/fastmcp/server/context.py b/src/fastmcp/server/context.py index d2124b7f1a..515fa84a5c 100644 --- a/src/fastmcp/server/context.py +++ b/src/fastmcp/server/context.py @@ -3,7 +3,6 @@ import copy import inspect import logging -import warnings import weakref from collections.abc import Generator, Mapping, Sequence from contextlib import contextmanager @@ -39,8 +38,6 @@ from starlette.requests import Request from typing_extensions import TypeVar -import fastmcp.server.dependencies -from fastmcp import settings from fastmcp.server.elicitation import ( AcceptedElicitation, CancelledElicitation, @@ -680,21 +677,6 @@ async def elicit( # This should never happen, but handle it just in case raise ValueError(f"Unexpected elicitation action: {result.action}") - def get_http_request(self) -> Request: - """Get the active starlette request.""" - - # Deprecated in 2.2.11 - if settings.deprecation_warnings: - warnings.warn( - "Context.get_http_request() is deprecated and will be removed in a future version. " - "Use get_http_request() from fastmcp.server.dependencies instead. " - "See https://gofastmcp.com/servers/context#http-requests for more details.", - DeprecationWarning, - stacklevel=2, - ) - - return fastmcp.server.dependencies.get_http_request() - def set_state(self, key: str, value: Any) -> None: """Set a value in the context state.""" self._state[key] = value diff --git a/src/fastmcp/server/proxy.py b/src/fastmcp/server/proxy.py index 1aeb02854f..90798a8c5b 100644 --- a/src/fastmcp/server/proxy.py +++ b/src/fastmcp/server/proxy.py @@ -1,7 +1,6 @@ from __future__ import annotations import inspect -import warnings from collections.abc import Awaitable, Callable from pathlib import Path from typing import TYPE_CHECKING, Any, cast @@ -20,7 +19,6 @@ ) from pydantic.networks import AnyUrl -import fastmcp from fastmcp.client.client import Client, FastMCP1Server from fastmcp.client.elicitation import ElicitResult from fastmcp.client.logging import LogMessage @@ -471,9 +469,8 @@ class FastMCPProxy(FastMCP): def __init__( self, - client: Client | None = None, *, - client_factory: ClientFactoryT | None = None, + client_factory: ClientFactoryT, **kwargs, ): """ @@ -483,9 +480,6 @@ def __init__( Use FastMCP.as_proxy() for convenience with automatic session strategy. Args: - client: [DEPRECATED] A Client instance. Use client_factory instead for explicit - session management. When provided, a client_factory will be automatically - created that provides session isolation for backwards compatibility. client_factory: A callable that returns a Client instance when called. This gives you full control over session creation and reuse. Can be either a synchronous or asynchronous function. @@ -494,29 +488,7 @@ def __init__( super().__init__(**kwargs) - # Handle client and client_factory parameters - if client is not None and client_factory is not None: - raise ValueError("Cannot specify both 'client' and 'client_factory'") - - if client is not None: - # Deprecated in 2.10.3 - if fastmcp.settings.deprecation_warnings: - warnings.warn( - "Passing 'client' to FastMCPProxy is deprecated. Use 'client_factory' instead for explicit session management. " - "For automatic session strategy, use FastMCP.as_proxy().", - DeprecationWarning, - stacklevel=2, - ) - - # Create a factory that provides session isolation for backwards compatibility - def deprecated_client_factory(): - return client.new() - - self.client_factory = deprecated_client_factory - elif client_factory is not None: - self.client_factory = client_factory - else: - raise ValueError("Must specify 'client_factory'") + self.client_factory = client_factory # Replace the default managers with our specialized proxy managers. self._tool_manager = ProxyToolManager( diff --git a/src/fastmcp/server/server.py b/src/fastmcp/server/server.py index 597c15dfab..fcacc3afd4 100644 --- a/src/fastmcp/server/server.py +++ b/src/fastmcp/server/server.py @@ -3,7 +3,6 @@ from __future__ import annotations import inspect -import json import re import secrets import warnings @@ -158,8 +157,6 @@ def __init__( auth: AuthProvider | NotSetT | None = NotSet, middleware: Sequence[Middleware] | None = None, lifespan: LifespanCallable | None = None, - dependencies: list[str] | None = None, - resource_prefix_format: Literal["protocol", "path"] | None = None, mask_error_details: bool | None = None, tools: Sequence[Tool | Callable[..., Any]] | None = None, tool_transformations: Mapping[str, ToolTransformConfig] | None = None, @@ -188,10 +185,6 @@ def __init__( sampling_handler: ServerSamplingHandler[LifespanResultT] | None = None, sampling_handler_behavior: Literal["always", "fallback"] | None = None, ): - self.resource_prefix_format: Literal["protocol", "path"] = ( - resource_prefix_format or fastmcp.settings.resource_prefix_format - ) - self._additional_http_routes: list[BaseRoute] = [] self._mounted_servers: list[MountedServer] = [] self._tool_manager: ToolManager = ToolManager( @@ -259,24 +252,6 @@ def __init__( # Set up MCP protocol handlers self._setup_handlers() - # Handle dependencies with deprecation warning - # TODO: Remove dependencies parameter (deprecated in v2.11.4) - if dependencies is not None: - import warnings - - warnings.warn( - "The 'dependencies' parameter is deprecated as of FastMCP 2.11.4 and will be removed in a future version. " - "Please specify dependencies in a fastmcp.json configuration file instead:\n" - '{\n "entrypoint": "your_server.py",\n "environment": {\n "dependencies": ' - f"{json.dumps(dependencies)}\n }}\n}}\n" - "See https://gofastmcp.com/docs/deployment/server-configuration for more information.", - DeprecationWarning, - stacklevel=2, - ) - self.dependencies: list[str] = ( - dependencies or fastmcp.settings.server_dependencies - ) # TODO: Remove (deprecated in v2.11.4) - self.sampling_handler: ServerSamplingHandler[LifespanResultT] | None = ( sampling_handler ) @@ -516,9 +491,7 @@ async def get_resources(self) -> dict[str, Resource]: child_resources = await mounted.server.get_resources() for key, resource in child_resources.items(): new_key = ( - add_resource_prefix( - key, mounted.prefix, mounted.resource_prefix_format - ) + add_resource_prefix(key, mounted.prefix) if mounted.prefix else key ) @@ -555,9 +528,7 @@ async def get_resource_templates(self) -> dict[str, ResourceTemplate]: child_templates = await mounted.server.get_resource_templates() for key, template in child_templates.items(): new_key = ( - add_resource_prefix( - key, mounted.prefix, mounted.resource_prefix_format - ) + add_resource_prefix(key, mounted.prefix) if mounted.prefix else key ) @@ -835,11 +806,7 @@ async def _list_resources( key = resource.key if mounted.prefix: - key = add_resource_prefix( - resource.key, - mounted.prefix, - mounted.resource_prefix_format, - ) + key = add_resource_prefix(resource.key, mounted.prefix) resource = resource.model_copy( key=key, update={"name": f"{mounted.prefix}_{resource.name}"}, @@ -931,11 +898,7 @@ async def _list_resource_templates( key = template.key if mounted.prefix: - key = add_resource_prefix( - template.key, - mounted.prefix, - mounted.resource_prefix_format, - ) + key = add_resource_prefix(template.key, mounted.prefix) template = template.model_copy( key=key, update={"name": f"{mounted.prefix}_{template.name}"}, @@ -1195,13 +1158,9 @@ async def _read_resource( for mounted in reversed(self._mounted_servers): key = uri_str if mounted.prefix: - if not has_resource_prefix( - key, mounted.prefix, mounted.resource_prefix_format - ): + if not has_resource_prefix(key, mounted.prefix): continue - key = remove_resource_prefix( - key, mounted.prefix, mounted.resource_prefix_format - ) + key = remove_resource_prefix(key, mounted.prefix) try: # First, get the resource to check if parent's filter allows it @@ -1580,44 +1539,6 @@ def add_template(self, template: ResourceTemplate) -> ResourceTemplate: return template - def add_resource_fn( - self, - fn: AnyFunction, - uri: str, - name: str | None = None, - description: str | None = None, - mime_type: str | None = None, - tags: set[str] | None = None, - ) -> None: - """Add a resource or template to the server from a function. - - If the URI contains parameters (e.g. "resource://{param}") or the function - has parameters, it will be registered as a template resource. - - Args: - fn: The function to register as a resource - uri: The URI for the resource - name: Optional name for the resource - description: Optional description of the resource - mime_type: Optional MIME type for the resource - tags: Optional set of tags for categorizing the resource - """ - # deprecated since 2.7.0 - if fastmcp.settings.deprecation_warnings: - warnings.warn( - "The add_resource_fn method is deprecated. Use the resource decorator instead.", - DeprecationWarning, - stacklevel=2, - ) - self._resource_manager.add_resource_or_template_from_fn( - fn=fn, - uri=uri, - name=name, - description=description, - mime_type=mime_type, - tags=tags, - ) - def resource( self, uri: str, @@ -2061,86 +1982,6 @@ async def run_http_async( await server.serve() - async def run_sse_async( - self, - host: str | None = None, - port: int | None = None, - log_level: str | None = None, - path: str | None = None, - uvicorn_config: dict[str, Any] | None = None, - ) -> None: - """Run the server using SSE transport.""" - - # Deprecated since 2.3.2 - if fastmcp.settings.deprecation_warnings: - warnings.warn( - "The run_sse_async method is deprecated (as of 2.3.2). Use run_http_async for a " - "modern (non-SSE) alternative, or create an SSE app with " - "`fastmcp.server.http.create_sse_app` and run it directly.", - DeprecationWarning, - stacklevel=2, - ) - await self.run_http_async( - transport="sse", - host=host, - port=port, - log_level=log_level, - path=path, - uvicorn_config=uvicorn_config, - ) - - def sse_app( - self, - path: str | None = None, - message_path: str | None = None, - middleware: list[ASGIMiddleware] | None = None, - ) -> StarletteWithLifespan: - """ - Create a Starlette app for the SSE server. - - Args: - path: The path to the SSE endpoint - message_path: The path to the message endpoint - middleware: A list of middleware to apply to the app - """ - # Deprecated since 2.3.2 - if fastmcp.settings.deprecation_warnings: - warnings.warn( - "The sse_app method is deprecated (as of 2.3.2). Use http_app as a modern (non-SSE) " - "alternative, or call `fastmcp.server.http.create_sse_app` directly.", - DeprecationWarning, - stacklevel=2, - ) - return create_sse_app( - server=self, - message_path=message_path or self._deprecated_settings.message_path, - sse_path=path or self._deprecated_settings.sse_path, - auth=self.auth, - debug=self._deprecated_settings.debug, - middleware=middleware, - ) - - def streamable_http_app( - self, - path: str | None = None, - middleware: list[ASGIMiddleware] | None = None, - ) -> StarletteWithLifespan: - """ - Create a Starlette app for the StreamableHTTP server. - - Args: - path: The path to the StreamableHTTP endpoint - middleware: A list of middleware to apply to the app - """ - # Deprecated since 2.3.2 - if fastmcp.settings.deprecation_warnings: - warnings.warn( - "The streamable_http_app method is deprecated (as of 2.3.2). Use http_app() instead.", - DeprecationWarning, - stacklevel=2, - ) - return self.http_app(path=path, middleware=middleware) - def http_app( self, path: str | None = None, @@ -2190,31 +2031,6 @@ def http_app( middleware=middleware, ) - async def run_streamable_http_async( - self, - host: str | None = None, - port: int | None = None, - log_level: str | None = None, - path: str | None = None, - uvicorn_config: dict[str, Any] | None = None, - ) -> None: - # Deprecated since 2.3.2 - if fastmcp.settings.deprecation_warnings: - warnings.warn( - "The run_streamable_http_async method is deprecated (as of 2.3.2). " - "Use run_http_async instead.", - DeprecationWarning, - stacklevel=2, - ) - await self.run_http_async( - transport="http", - host=host, - port=port, - log_level=log_level, - path=path, - uvicorn_config=uvicorn_config, - ) - def mount( self, server: FastMCP[LifespanResultT], @@ -2330,7 +2146,6 @@ def mount( mounted_server = MountedServer( prefix=prefix, server=server, - resource_prefix_format=self.resource_prefix_format, ) self._mounted_servers.append(mounted_server) @@ -2431,9 +2246,7 @@ async def import_server( # Import resources and templates from the server for key, resource in (await server.get_resources()).items(): if prefix: - resource_key = add_resource_prefix( - key, prefix, self.resource_prefix_format - ) + resource_key = add_resource_prefix(key, prefix) resource = resource.model_copy( update={"name": f"{prefix}_{resource.name}"}, key=resource_key ) @@ -2441,9 +2254,7 @@ async def import_server( for key, template in (await server.get_resource_templates()).items(): if prefix: - template_key = add_resource_prefix( - key, prefix, self.resource_prefix_format - ) + template_key = add_resource_prefix(key, prefix) template = template.model_copy( update={"name": f"{prefix}_{template.name}"}, key=template_key ) @@ -2643,23 +2454,6 @@ def proxy_client_factory(): return FastMCPProxy(client_factory=client_factory, **settings) - @classmethod - def from_client( - cls, client: Client[ClientTransportT], **settings: Any - ) -> FastMCPProxy: - """ - Create a FastMCP proxy server from a FastMCP client. - """ - # Deprecated since 2.3.5 - if fastmcp.settings.deprecation_warnings: - warnings.warn( - "FastMCP.from_client() is deprecated; use FastMCP.as_proxy() instead.", - DeprecationWarning, - stacklevel=2, - ) - - return cls.as_proxy(client, **settings) - def _should_enable_component( self, component: FastMCPComponent, @@ -2706,13 +2500,10 @@ def generate_name(cls, name: str | None = None) -> str: class MountedServer: prefix: str | None server: FastMCP[Any] - resource_prefix_format: Literal["protocol", "path"] | None = None -def add_resource_prefix( - uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None -) -> str: - """Add a prefix to a resource URI. +def add_resource_prefix(uri: str, prefix: str) -> str: + """Add a prefix to a resource URI using path formatting (resource://prefix/path). Args: uri: The original resource URI @@ -2722,16 +2513,10 @@ def add_resource_prefix( The resource URI with the prefix added Examples: - With new style: ```python add_resource_prefix("resource://path/to/resource", "prefix") "resource://prefix/path/to/resource" ``` - With legacy style: - ```python - add_resource_prefix("resource://path/to/resource", "prefix") - "prefix+resource://path/to/resource" - ``` With absolute path: ```python add_resource_prefix("resource:///absolute/path", "prefix") @@ -2744,54 +2529,32 @@ def add_resource_prefix( if not prefix: return uri - # Get the server settings to check for legacy format preference - - if prefix_format is None: - prefix_format = fastmcp.settings.resource_prefix_format - - if prefix_format == "protocol": - # Legacy style: prefix+protocol://path - return f"{prefix}+{uri}" - elif prefix_format == "path": - # New style: protocol://prefix/path - # Split the URI into protocol and path - match = URI_PATTERN.match(uri) - if not match: - raise ValueError( - f"Invalid URI format: {uri}. Expected protocol://path format." - ) + # Split the URI into protocol and path + match = URI_PATTERN.match(uri) + if not match: + raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.") - protocol, path = match.groups() + protocol, path = match.groups() - # Add the prefix to the path - return f"{protocol}{prefix}/{path}" - else: - raise ValueError(f"Invalid prefix format: {prefix_format}") + # Add the prefix to the path + return f"{protocol}{prefix}/{path}" -def remove_resource_prefix( - uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None -) -> str: +def remove_resource_prefix(uri: str, prefix: str) -> str: """Remove a prefix from a resource URI. Args: uri: The resource URI with a prefix prefix: The prefix to remove - prefix_format: The format of the prefix to remove + Returns: The resource URI with the prefix removed Examples: - With new style: ```python remove_resource_prefix("resource://prefix/path/to/resource", "prefix") "resource://path/to/resource" ``` - With legacy style: - ```python - remove_resource_prefix("prefix+resource://path/to/resource", "prefix") - "resource://path/to/resource" - ``` With absolute path: ```python remove_resource_prefix("resource://prefix//absolute/path", "prefix") @@ -2804,41 +2567,24 @@ def remove_resource_prefix( if not prefix: return uri - if prefix_format is None: - prefix_format = fastmcp.settings.resource_prefix_format - - if prefix_format == "protocol": - # Legacy style: prefix+protocol://path - legacy_prefix = f"{prefix}+" - if uri.startswith(legacy_prefix): - return uri[len(legacy_prefix) :] - return uri - elif prefix_format == "path": - # New style: protocol://prefix/path - # Split the URI into protocol and path - match = URI_PATTERN.match(uri) - if not match: - raise ValueError( - f"Invalid URI format: {uri}. Expected protocol://path format." - ) + # Split the URI into protocol and path + match = URI_PATTERN.match(uri) + if not match: + raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.") - protocol, path = match.groups() + protocol, path = match.groups() - # Check if the path starts with the prefix followed by a / - prefix_pattern = f"^{re.escape(prefix)}/(.*?)$" - path_match = re.match(prefix_pattern, path) - if not path_match: - return uri + # Check if the path starts with the prefix followed by a / + prefix_pattern = f"^{re.escape(prefix)}/(.*?)$" + path_match = re.match(prefix_pattern, path) + if not path_match: + return uri - # Return the URI without the prefix - return f"{protocol}{path_match.group(1)}" - else: - raise ValueError(f"Invalid prefix format: {prefix_format}") + # Return the URI without the prefix + return f"{protocol}{path_match.group(1)}" -def has_resource_prefix( - uri: str, prefix: str, prefix_format: Literal["protocol", "path"] | None = None -) -> bool: +def has_resource_prefix(uri: str, prefix: str) -> bool: """Check if a resource URI has a specific prefix. Args: @@ -2849,16 +2595,10 @@ def has_resource_prefix( True if the URI has the specified prefix, False otherwise Examples: - With new style: ```python has_resource_prefix("resource://prefix/path/to/resource", "prefix") True ``` - With legacy style: - ```python - has_resource_prefix("prefix+resource://path/to/resource", "prefix") - True - ``` With other path: ```python has_resource_prefix("resource://other/path/to/resource", "prefix") @@ -2871,28 +2611,13 @@ def has_resource_prefix( if not prefix: return False - # Get the server settings to check for legacy format preference - - if prefix_format is None: - prefix_format = fastmcp.settings.resource_prefix_format - - if prefix_format == "protocol": - # Legacy style: prefix+protocol://path - legacy_prefix = f"{prefix}+" - return uri.startswith(legacy_prefix) - elif prefix_format == "path": - # New style: protocol://prefix/path - # Split the URI into protocol and path - match = URI_PATTERN.match(uri) - if not match: - raise ValueError( - f"Invalid URI format: {uri}. Expected protocol://path format." - ) + # Split the URI into protocol and path + match = URI_PATTERN.match(uri) + if not match: + raise ValueError(f"Invalid URI format: {uri}. Expected protocol://path format.") - _, path = match.groups() + _, path = match.groups() - # Check if the path starts with the prefix followed by a / - prefix_pattern = f"^{re.escape(prefix)}/" - return bool(re.match(prefix_pattern, path)) - else: - raise ValueError(f"Invalid prefix format: {prefix_format}") + # Check if the path starts with the prefix followed by a / + prefix_pattern = f"^{re.escape(prefix)}/" + return bool(re.match(prefix_pattern, path)) diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index ca84cb5a21..3babc40448 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -8,11 +8,8 @@ from platformdirs import user_data_dir from pydantic import Field, ImportString, field_validator -from pydantic.fields import FieldInfo from pydantic_settings import ( BaseSettings, - EnvSettingsSource, - PydanticBaseSettingsSource, SettingsConfigDict, ) from typing_extensions import Self @@ -33,37 +30,6 @@ from fastmcp.server.auth.auth import AuthProvider -class ExtendedEnvSettingsSource(EnvSettingsSource): - """ - A special EnvSettingsSource that allows for multiple env var prefixes to be used. - - Raises a deprecation warning if the old `FASTMCP_SERVER_` prefix is used. - """ - - def get_field_value( - self, field: FieldInfo, field_name: str - ) -> tuple[Any, str, bool]: - if prefixes := self.config.get("env_prefixes"): - for prefix in prefixes: - self.env_prefix = prefix - env_val, field_key, value_is_complex = super().get_field_value( - field, field_name - ) - if env_val is not None: - if prefix == "FASTMCP_SERVER_": - # Deprecated in 2.8.0 - logger.warning( - "Using `FASTMCP_SERVER_` environment variables is deprecated. Use `FASTMCP_` instead.", - ) - return env_val, field_key, value_is_complex - - return super().get_field_value(field, field_name) - - -class ExtendedSettingsConfigDict(SettingsConfigDict, total=False): - env_prefixes: list[str] | None - - class ExperimentalSettings(BaseSettings): model_config = SettingsConfigDict( env_prefix="FASTMCP_EXPERIMENTAL_", @@ -86,8 +52,8 @@ class ExperimentalSettings(BaseSettings): class Settings(BaseSettings): """FastMCP settings.""" - model_config = ExtendedSettingsConfigDict( - env_prefixes=["FASTMCP_", "FASTMCP_SERVER_"], + model_config = SettingsConfigDict( + env_prefix="FASTMCP_", env_file=ENV_FILE, extra="ignore", env_nested_delimiter="__", @@ -121,24 +87,6 @@ def set_setting(self, attr: str, value: Any) -> None: settings = getattr(settings, parent_attr) setattr(settings, attr, value) - @classmethod - def settings_customise_sources( - cls, - settings_cls: type[BaseSettings], - init_settings: PydanticBaseSettingsSource, - env_settings: PydanticBaseSettingsSource, - dotenv_settings: PydanticBaseSettingsSource, - file_secret_settings: PydanticBaseSettingsSource, - ) -> tuple[PydanticBaseSettingsSource, ...]: - # can remove this classmethod after deprecated FASTMCP_SERVER_ prefix is - # removed - return ( - init_settings, - ExtendedEnvSettingsSource(settings_cls), - dotenv_settings, - file_secret_settings, - ) - @property def settings(self) -> Self: """ @@ -207,19 +155,6 @@ def normalize_log_level(cls, v): ), ] = True - resource_prefix_format: Annotated[ - Literal["protocol", "path"], - Field( - description=inspect.cleandoc( - """ - When perfixing a resource URI, either use path formatting (resource://prefix/path) - or protocol formatting (prefix+resource://path). Protocol formatting was the default in FastMCP < 2.4; - path formatting is current default. - """ - ), - ), - ] = "path" - client_init_timeout: Annotated[ float | None, Field( diff --git a/src/fastmcp/tools/tool.py b/src/fastmcp/tools/tool.py index 552c4d7299..71fae733ad 100644 --- a/src/fastmcp/tools/tool.py +++ b/src/fastmcp/tools/tool.py @@ -9,7 +9,6 @@ Annotated, Any, Generic, - Literal, TypeAlias, get_type_hints, ) @@ -184,7 +183,7 @@ def from_function( tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, - output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet, + output_schema: dict[str, Any] | NotSetT | None = NotSet, serializer: ToolResultSerializerType | None = None, meta: dict[str, Any] | None = None, enabled: bool | None = None, @@ -227,7 +226,7 @@ def from_tool( description: str | NotSetT | None = NotSet, tags: set[str] | None = None, annotations: ToolAnnotations | NotSetT | None = NotSet, - output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet, + output_schema: dict[str, Any] | NotSetT | None = NotSet, serializer: ToolResultSerializerType | None = None, meta: dict[str, Any] | NotSetT | None = NotSet, transform_args: dict[str, ArgTransform] | None = None, @@ -266,7 +265,7 @@ def from_function( tags: set[str] | None = None, annotations: ToolAnnotations | None = None, exclude_args: list[str] | None = None, - output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet, + output_schema: dict[str, Any] | NotSetT | None = NotSet, serializer: ToolResultSerializerType | None = None, meta: dict[str, Any] | None = None, enabled: bool | None = None, @@ -290,17 +289,8 @@ def from_function( if isinstance(output_schema, NotSetT): final_output_schema = parsed_fn.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: - # At this point output_schema is not NotSetT and not False, so it must be dict | None + # At this point output_schema is not NotSetT, so it must be dict | None final_output_schema = output_schema # Note: explicit schemas (dict) are used as-is without auto-wrapping diff --git a/src/fastmcp/tools/tool_transform.py b/src/fastmcp/tools/tool_transform.py index ded56a22e2..62ade0cbb1 100644 --- a/src/fastmcp/tools/tool_transform.py +++ b/src/fastmcp/tools/tool_transform.py @@ -1,7 +1,6 @@ from __future__ import annotations import inspect -import warnings from collections.abc import Callable from contextvars import ContextVar from copy import deepcopy @@ -14,7 +13,6 @@ 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 @@ -372,7 +370,7 @@ def from_tool( transform_fn: Callable[..., Any] | None = None, transform_args: dict[str, ArgTransform] | None = None, annotations: ToolAnnotations | NotSetT | None = NotSet, - output_schema: dict[str, Any] | Literal[False] | NotSetT | None = NotSet, + output_schema: dict[str, Any] | NotSetT | None = NotSet, serializer: Callable[[Any], str] | NotSetT | None = NotSet, meta: dict[str, Any] | NotSetT | None = NotSet, enabled: bool | None = None, @@ -487,15 +485,6 @@ async def custom_output(**kwargs) -> ToolResult: 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: final_output_schema = cast(dict | None, output_schema) diff --git a/tests/client/test_sse.py b/tests/client/test_sse.py index 7110d5cfd3..b0c9199b20 100644 --- a/tests/client/test_sse.py +++ b/tests/client/test_sse.py @@ -8,6 +8,7 @@ from fastmcp.client import Client from fastmcp.client.transports import SSETransport from fastmcp.server.dependencies import get_http_request +from fastmcp.server.http import create_sse_app from fastmcp.server.server import FastMCP from fastmcp.utilities.tests import run_server_async @@ -94,7 +95,6 @@ async def nested_sse_server(): from starlette.applications import Starlette from starlette.routing import Mount - from fastmcp.server.http import create_sse_app from fastmcp.utilities.http import find_available_port server = create_test_server() diff --git a/tests/deprecated/test_bearer_auth_provider.py b/tests/deprecated/test_bearer_auth_provider.py deleted file mode 100644 index 5e02677d13..0000000000 --- a/tests/deprecated/test_bearer_auth_provider.py +++ /dev/null @@ -1,13 +0,0 @@ -import pytest - -# reset deprecation warnings for this module -pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning") - - -def test_bearer_auth_provider_deprecated(): - """Test that BearerAuthProvider import shows deprecation warning.""" - with pytest.warns( - DeprecationWarning, - match="The `fastmcp.server.auth.providers.bearer` module is deprecated and will be removed in a future version. Please use `fastmcp.server.auth.providers.jwt.JWTVerifier` instead of this module's BearerAuthProvider.", - ): - from fastmcp.server.auth import BearerAuthProvider # noqa: F401 diff --git a/tests/deprecated/test_dependencies.py b/tests/deprecated/test_dependencies.py deleted file mode 100644 index b8dca71d85..0000000000 --- a/tests/deprecated/test_dependencies.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Tests for deprecated dependencies parameter. - -This entire file can be deleted when the dependencies parameter is removed (deprecated in v2.11.4). -""" - -import warnings - -import pytest - -from fastmcp import FastMCP - - -def test_dependencies_parameter_deprecated(): - """Test that using the dependencies parameter raises a deprecation warning.""" - - with pytest.warns(DeprecationWarning, match="deprecated as of FastMCP 2.11.4"): - server = FastMCP("Test Server", dependencies=["pandas", "numpy"]) - - # Should still work for backward compatibility - assert server.dependencies == ["pandas", "numpy"] - - -def test_no_warning_without_dependencies(): - """Test that no warning is raised when dependencies are not used.""" - - with warnings.catch_warnings(): - warnings.simplefilter("error") # Turn warnings into errors - server = FastMCP("Test Server") # Should not raise - - assert server.dependencies == [] # Should use default empty list diff --git a/tests/deprecated/test_deprecated.py b/tests/deprecated/test_deprecated.py index 92b28cda80..25a32aff66 100644 --- a/tests/deprecated/test_deprecated.py +++ b/tests/deprecated/test_deprecated.py @@ -1,10 +1,9 @@ import warnings -from unittest.mock import AsyncMock, patch import pytest from starlette.applications import Starlette -from fastmcp import Client, FastMCP +from fastmcp import FastMCP from fastmcp.utilities.tests import temporary_settings # reset deprecation warnings for this module @@ -33,61 +32,6 @@ def test_deprecation_warnings_setting_false(self): mcp.settings -def test_sse_app_deprecation_warning(): - """Test that sse_app raises a deprecation warning.""" - server = FastMCP("TestServer") - - with pytest.warns(DeprecationWarning, match="The sse_app method is deprecated"): - app = server.sse_app() - assert isinstance(app, Starlette) - - -def test_streamable_http_app_deprecation_warning(): - """Test that streamable_http_app raises a deprecation warning.""" - server = FastMCP("TestServer") - - with pytest.warns( - DeprecationWarning, match="The streamable_http_app method is deprecated" - ): - app = server.streamable_http_app() - assert isinstance(app, Starlette) - - -async def test_run_sse_async_deprecation_warning(): - """Test that run_sse_async raises a deprecation warning.""" - server = FastMCP("TestServer") - - # Use patch to avoid actually running the server - with patch.object(server, "run_http_async", new_callable=AsyncMock) as mock_run: - with pytest.warns( - DeprecationWarning, match="The run_sse_async method is deprecated" - ): - await server.run_sse_async() - - # Verify the mock was called with the right transport - mock_run.assert_called_once() - call_kwargs = mock_run.call_args.kwargs - assert call_kwargs.get("transport") == "sse" - - -async def test_run_streamable_http_async_deprecation_warning(): - """Test that run_streamable_http_async raises a deprecation warning.""" - server = FastMCP("TestServer") - - # Use patch to avoid actually running the server - with patch.object(server, "run_http_async", new_callable=AsyncMock) as mock_run: - with pytest.warns( - DeprecationWarning, - match="The run_streamable_http_async method is deprecated", - ): - await server.run_streamable_http_async() - - # Verify the mock was called with the right transport - mock_run.assert_called_once() - call_kwargs = mock_run.call_args.kwargs - assert call_kwargs.get("transport") == "http" - - def test_http_app_with_sse_transport(): """Test that http_app with SSE transport works (no warning).""" server = FastMCP("TestServer") @@ -102,10 +46,3 @@ def test_http_app_with_sse_transport(): w for w in recorded_warnings if issubclass(w.category, DeprecationWarning) ] assert len(deprecation_warnings) == 0 - - -def test_from_client_deprecation_warning(): - """Test that FastMCP.from_client raises a deprecation warning.""" - server = FastMCP("TestServer") - with pytest.warns(DeprecationWarning, match="from_client"): - FastMCP.from_client(Client(server)) diff --git a/tests/deprecated/test_output_schema_false.py b/tests/deprecated/test_output_schema_false.py deleted file mode 100644 index 0dcc20d06f..0000000000 --- a/tests/deprecated/test_output_schema_false.py +++ /dev/null @@ -1,139 +0,0 @@ -"""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} diff --git a/tests/deprecated/test_proxy_client.py b/tests/deprecated/test_proxy_client.py deleted file mode 100644 index f278c812c0..0000000000 --- a/tests/deprecated/test_proxy_client.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Tests for deprecated FastMCPProxy client parameter.""" - -import warnings - -import pytest - -from fastmcp import Client, FastMCP -from fastmcp.server.proxy import FastMCPProxy, ProxyClient - - -@pytest.fixture -def simple_server(): - """Create a simple FastMCP server for testing.""" - server = FastMCP("TestServer") - - @server.tool - def simple_tool() -> str: - return "test_result" - - return server - - -class TestDeprecatedClientParameter: - """Test the deprecated client parameter in FastMCPProxy.""" - - def test_client_parameter_deprecation_warning(self, simple_server): - """Test that using the client parameter raises a deprecation warning.""" - client = Client(simple_server) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") # Ensure all warnings are captured - - FastMCPProxy(client=client) - - # Verify a deprecation warning was raised - assert len(w) == 1 - assert issubclass(w[0].category, DeprecationWarning) - assert "client' to FastMCPProxy is deprecated" in str(w[0].message) - assert "client_factory" in str(w[0].message) - - def test_client_parameter_still_works(self, simple_server): - """Test that the deprecated client parameter still functions.""" - client = ProxyClient(simple_server) - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") # Suppress warnings for functionality test - - proxy = FastMCPProxy(client=client) - - # Verify the proxy was created successfully - assert proxy is not None - assert hasattr(proxy, "client_factory") - assert callable(proxy.client_factory) - - # Verify the factory returns a new client instance (session isolation for backwards compatibility) - returned_client = proxy.client_factory() - assert returned_client is not client - assert isinstance(returned_client, type(client)) - - def test_cannot_specify_both_client_and_factory(self, simple_server): - """Test that specifying both client and client_factory raises an error.""" - client = Client(simple_server) - - def factory(): - return Client(simple_server) - - with pytest.raises( - ValueError, match="Cannot specify both 'client' and 'client_factory'" - ): - FastMCPProxy(client=client, client_factory=factory) - - def test_must_specify_client_factory_when_no_client(self): - """Test that client_factory is required when client is not provided.""" - with pytest.raises(ValueError, match="Must specify 'client_factory'"): - FastMCPProxy() - - def test_client_factory_preferred_over_deprecated_client(self, simple_server): - """Test that the recommended client_factory approach works without warnings.""" - - def factory(): - return ProxyClient(simple_server) - - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - - proxy = FastMCPProxy(client_factory=factory) - - # Verify no warnings were raised - assert len(w) == 0 - - # Verify the proxy works correctly - assert proxy is not None - assert proxy.client_factory is factory - - async def test_deprecated_client_functional_test(self, simple_server): - """End-to-end test that deprecated client parameter still works functionally.""" - client = ProxyClient(simple_server) - - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - - proxy = FastMCPProxy(client=client) - - # Test that the proxy can actually handle requests - async with Client(proxy) as proxy_client: - result = await proxy_client.call_tool("simple_tool", {}) - assert result.data == "test_result" diff --git a/tests/deprecated/test_resource_prefixes.py b/tests/deprecated/test_resource_prefixes.py deleted file mode 100644 index 44390f80f3..0000000000 --- a/tests/deprecated/test_resource_prefixes.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Tests for legacy resource prefix behavior.""" - -import pytest - -from fastmcp import Client, FastMCP -from fastmcp.server.server import ( - add_resource_prefix, - has_resource_prefix, - remove_resource_prefix, -) -from fastmcp.utilities.tests import temporary_settings - -# reset deprecation warnings for this module -pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning") - - -class TestLegacyResourcePrefixes: - """Test the legacy resource prefix behavior.""" - - def test_add_resource_prefix_legacy(self): - """Test that add_resource_prefix uses the legacy format when resource_prefix_format is 'protocol'.""" - with temporary_settings(resource_prefix_format="protocol"): - result = add_resource_prefix("resource://path/to/resource", "prefix") - assert result == "prefix+resource://path/to/resource" - - # Empty prefix should return the original URI - result = add_resource_prefix("resource://path/to/resource", "") - assert result == "resource://path/to/resource" - - def test_remove_resource_prefix_legacy(self): - """Test that remove_resource_prefix uses the legacy format when resource_prefix_format is 'protocol'.""" - with temporary_settings(resource_prefix_format="protocol"): - result = remove_resource_prefix( - "prefix+resource://path/to/resource", "prefix" - ) - assert result == "resource://path/to/resource" - - # URI without the prefix should be returned as is - result = remove_resource_prefix("resource://path/to/resource", "prefix") - assert result == "resource://path/to/resource" - - # Empty prefix should return the original URI - result = remove_resource_prefix("resource://path/to/resource", "") - assert result == "resource://path/to/resource" - - def test_has_resource_prefix_legacy(self): - """Test that has_resource_prefix uses the legacy format when resource_prefix_format is 'protocol'.""" - with temporary_settings(resource_prefix_format="protocol"): - result = has_resource_prefix("prefix+resource://path/to/resource", "prefix") - assert result is True - - result = has_resource_prefix("resource://path/to/resource", "prefix") - assert result is False - - # Empty prefix should always return False - result = has_resource_prefix("resource://path/to/resource", "") - assert result is False - - -async def test_mount_with_legacy_prefixes(): - """Test mounting a server with legacy resource prefixes.""" - with temporary_settings(resource_prefix_format="protocol"): - main_server = FastMCP("MainServer") - sub_server = FastMCP("SubServer") - - @sub_server.resource("resource://test") - def get_test(): - return "test content" - - # Mount the server with a prefix (using old argument order for this legacy test) - with pytest.warns(DeprecationWarning, match="Mount prefixes are now optional"): - main_server.mount("sub", sub_server) # type: ignore[arg-type] - - # Check that the resource is prefixed using the legacy format - resources = await main_server.get_resources() - - # In legacy format, the key would be "sub+resource://test" - assert "sub+resource://test" in resources - - # Test accessing the resource through client - async with Client(main_server) as client: - result = await client.read_resource("sub+resource://test") - # Different content types might be returned, but we just want to verify we got something - assert len(result) > 0 - - -async def test_import_server_with_legacy_prefixes(): - """Test importing a server with legacy resource prefixes.""" - with temporary_settings(resource_prefix_format="protocol"): - main_server = FastMCP("MainServer") - sub_server = FastMCP("SubServer") - - @sub_server.resource("resource://test") - def get_test(): - return "test content" - - # Import the server with a prefix (using old argument order for this legacy test) - with pytest.warns(DeprecationWarning, match="Import prefixes are now optional"): - await main_server.import_server("sub", sub_server) # type: ignore[arg-type] - - # Check that the resource is prefixed using the legacy format - resources = await main_server.get_resources() - - # In legacy format, the key would be "sub+resource://test" - assert "sub+resource://test" in resources diff --git a/tests/deprecated/test_settings.py b/tests/deprecated/test_settings.py index ad6a29a671..c0692a14eb 100644 --- a/tests/deprecated/test_settings.py +++ b/tests/deprecated/test_settings.py @@ -1,4 +1,3 @@ -import os import warnings from unittest.mock import patch @@ -179,7 +178,6 @@ def test_non_deprecated_kwargs_no_warnings(self): on_duplicate_tools="warn", on_duplicate_resources="error", on_duplicate_prompts="replace", - resource_prefix_format="path", mask_error_details=True, ) @@ -304,39 +302,6 @@ def test_stacklevel_points_to_constructor_call(self): assert "server.py" in warning.filename -class TestDeprecatedEnvironmentVariables: - """Test deprecated environment variable prefixes.""" - - def test_fastmcp_server_env_var_deprecation_warning(self, caplog): - """Test that FASTMCP_SERVER_ environment variables emit deprecation warnings.""" - env_var_name = "FASTMCP_SERVER_HOST" - original_value = os.environ.get(env_var_name) - - try: - os.environ[env_var_name] = "192.168.1.1" - - with caplog_for_fastmcp(caplog): - settings = Settings() - - # Check that a warning was logged - assert any( - "Using `FASTMCP_SERVER_` environment variables is deprecated. Use `FASTMCP_` instead." - in record.message - for record in caplog.records - if record.levelname == "WARNING" - ) - - # Verify the setting is still applied - assert settings.host == "192.168.1.1" - - finally: - # Clean up environment variable - if original_value is not None: - os.environ[env_var_name] = original_value - else: - os.environ.pop(env_var_name, None) - - class TestDeprecatedSettingsProperty: """Test deprecated settings property access.""" diff --git a/tests/server/test_context.py b/tests/server/test_context.py index d87a30666e..f53797326c 100644 --- a/tests/server/test_context.py +++ b/tests/server/test_context.py @@ -1,9 +1,7 @@ -import warnings -from unittest.mock import MagicMock, patch +from unittest.mock import MagicMock import pytest from mcp.types import ModelPreferences -from starlette.requests import Request from fastmcp.server.context import ( Context, @@ -12,58 +10,6 @@ from fastmcp.server.server import FastMCP -class TestContextDeprecations: - def test_get_http_request_deprecation_warning(self): - """Test that using Context.get_http_request() raises a deprecation warning.""" - # Create a mock FastMCP instance - mock_fastmcp = MagicMock() - context = Context(fastmcp=mock_fastmcp) - - # Patch the dependency function to return a mock request - mock_request = MagicMock(spec=Request) - with patch( - "fastmcp.server.dependencies.get_http_request", return_value=mock_request - ): - # Check that the deprecation warning is raised - with pytest.warns( - DeprecationWarning, match="Context.get_http_request\\(\\) is deprecated" - ): - request = context.get_http_request() - - # Verify the function still works and returns the request - assert request is mock_request - - def test_get_http_request_deprecation_message(self): - """Test that the deprecation warning has the correct message with guidance.""" - # Create a mock FastMCP instance - mock_fastmcp = MagicMock() - context = Context(fastmcp=mock_fastmcp) - - # Patch the dependency function to return a mock request - mock_request = MagicMock(spec=Request) - with patch( - "fastmcp.server.dependencies.get_http_request", return_value=mock_request - ): - # Capture and check the specific warning message - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") - context.get_http_request() - - assert len(w) == 1 - warning = w[0] - assert issubclass(warning.category, DeprecationWarning) - assert "Context.get_http_request() is deprecated" in str( - warning.message - ) - assert ( - "Use get_http_request() from fastmcp.server.dependencies instead" - in str(warning.message) - ) - assert "https://gofastmcp.com/servers/context#http-requests" in str( - warning.message - ) - - @pytest.fixture def context(): return Context(fastmcp=FastMCP()) diff --git a/tests/server/test_import_server.py b/tests/server/test_import_server.py index 0d5661e17c..446dbfcc94 100644 --- a/tests/server/test_import_server.py +++ b/tests/server/test_import_server.py @@ -391,25 +391,6 @@ def create_user(name: str, email: str): assert content["email"] == "john@example.com" -async def test_import_invalid_resource_prefix(): - main_app = FastMCP("MainApp") - api_app = FastMCP("APIApp") - - # This test doesn't apply anymore with the new prefix format since we're not validating - # the protocol://prefix/path format - # Just import the server to maintain test coverage without deprecated parameters - await main_app.import_server(api_app, "api") - - -async def test_import_invalid_resource_separator(): - main_app = FastMCP("MainApp") - api_app = FastMCP("APIApp") - - # This test is for maintaining coverage for importing with prefixes - # We no longer pass the deprecated resource_separator parameter - await main_app.import_server(api_app, "api") - - async def test_import_with_no_prefix(): """Test importing a server without providing a prefix.""" main_app = FastMCP("MainApp") diff --git a/tests/server/test_mount.py b/tests/server/test_mount.py index d02e86b6cb..844fd24649 100644 --- a/tests/server/test_mount.py +++ b/tests/server/test_mount.py @@ -66,22 +66,6 @@ def greet(name: str) -> str: result = await client.call_tool("sub_greet", {"name": "World"}) assert result.data == "Hello, World!" - async def test_mount_invalid_resource_prefix(self): - main_app = FastMCP("MainApp") - api_app = FastMCP("APIApp") - - # This test doesn't apply anymore with the new prefix format - # just mount the server to maintain test coverage - main_app.mount(api_app, "api:sub") - - async def test_mount_invalid_resource_separator(self): - main_app = FastMCP("MainApp") - api_app = FastMCP("APIApp") - - # This test doesn't apply anymore with the new prefix format - # Mount without deprecated parameters - main_app.mount(api_app, "api") - @pytest.mark.parametrize("prefix", ["", None]) async def test_mount_with_no_prefix(self, prefix): main_app = FastMCP("MainApp") diff --git a/tests/server/test_resource_prefix_formats.py b/tests/server/test_resource_prefix_formats.py deleted file mode 100644 index c45731ccde..0000000000 --- a/tests/server/test_resource_prefix_formats.py +++ /dev/null @@ -1,65 +0,0 @@ -"""Tests for different resource prefix formats in server mounting and importing.""" - -from fastmcp import FastMCP - - -async def test_resource_prefix_format_in_constructor(): - """Test that the resource_prefix_format parameter is respected in the constructor.""" - server_path = FastMCP("PathFormat", resource_prefix_format="path") - server_protocol = FastMCP("ProtocolFormat", resource_prefix_format="protocol") - - # Check that the format is stored correctly - assert server_path.resource_prefix_format == "path" - assert server_protocol.resource_prefix_format == "protocol" - - # Register resources - @server_path.resource("resource://test") - def get_test_path(): - return "test content" - - @server_protocol.resource("resource://test") - def get_test_protocol(): - return "test content" - - # Create mount servers - main_server_path = FastMCP("MainPath", resource_prefix_format="path") - main_server_protocol = FastMCP("MainProtocol", resource_prefix_format="protocol") - - # Mount the servers - main_server_path.mount(server_path, "sub") - main_server_protocol.mount(server_protocol, "sub") - - # Check that the resources are prefixed correctly - path_resources = await main_server_path.get_resources() - protocol_resources = await main_server_protocol.get_resources() - - # Path format should be resource://sub/test - assert "resource://sub/test" in path_resources - # Protocol format should be sub+resource://test - assert "sub+resource://test" in protocol_resources - - -async def test_resource_prefix_format_in_import_server(): - """Test that the resource_prefix_format parameter is respected in import_server.""" - server = FastMCP("TestServer") - - @server.resource("resource://test") - def get_test(): - return "test content" - - # Import with path format - main_server_path = FastMCP("MainPath", resource_prefix_format="path") - await main_server_path.import_server(server, "sub") - - # Import with protocol format - main_server_protocol = FastMCP("MainProtocol", resource_prefix_format="protocol") - await main_server_protocol.import_server(server, "sub") - - # Check that the resources are prefixed correctly - path_resources = await main_server_path._resource_manager.get_resources() - protocol_resources = await main_server_protocol._resource_manager.get_resources() - - # Path format should be resource://sub/test - assert "resource://sub/test" in path_resources - # Protocol format should be sub+resource://test - assert "sub+resource://test" in protocol_resources diff --git a/tests/server/test_server.py b/tests/server/test_server.py index 38ec249981..22fcf5e590 100644 --- a/tests/server/test_server.py +++ b/tests/server/test_server.py @@ -1287,20 +1287,11 @@ async def test_mounted_server_matching_and_stripping( """Test that resource prefix utility functions correctly match and strip resource prefixes.""" from fastmcp.server.server import has_resource_prefix, remove_resource_prefix - # Create a basic server to get the default resource prefix format - server = FastMCP() - # Test matching - assert ( - has_resource_prefix(uri, prefix, server.resource_prefix_format) - == expected_match - ) + assert has_resource_prefix(uri, prefix) == expected_match # Test stripping - assert ( - remove_resource_prefix(uri, prefix, server.resource_prefix_format) - == expected_strip - ) + assert remove_resource_prefix(uri, prefix) == expected_strip async def test_import_server_with_new_prefix_format(self): """Test that import_server correctly uses the new prefix format."""