diff --git a/src/fastmcp/prompts/prompt_manager.py b/src/fastmcp/prompts/prompt_manager.py index ab1a944e2..59ed7cbf4 100644 --- a/src/fastmcp/prompts/prompt_manager.py +++ b/src/fastmcp/prompts/prompt_manager.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any from mcp import GetPromptResult -from fastmcp import settings +import fastmcp from fastmcp.exceptions import NotFoundError, PromptError from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult from fastmcp.settings import DuplicateBehavior @@ -28,7 +28,9 @@ class PromptManager: ): self._prompts: dict[str, Prompt] = {} self._mounted_servers: list[MountedServer] = [] - self.mask_error_details = mask_error_details or settings.mask_error_details + self.mask_error_details = ( + mask_error_details or fastmcp.settings.mask_error_details + ) # Default to "warn" if None is provided if duplicate_behavior is None: @@ -80,7 +82,7 @@ class PromptManager: logger.warning( f"Failed to get prompts from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}" ) - if settings.mounted_components_raise_on_load_error: + if fastmcp.settings.mounted_components_raise_on_load_error: raise continue @@ -122,7 +124,7 @@ class PromptManager: ) -> FunctionPrompt: """Create a prompt from a function.""" # deprecated in 2.7.0 - if settings.deprecation_warnings: + if fastmcp.settings.deprecation_warnings: warnings.warn( "PromptManager.add_prompt_from_fn() is deprecated. Use Prompt.from_function() and call add_prompt() instead.", DeprecationWarning, diff --git a/src/fastmcp/resources/resource_manager.py b/src/fastmcp/resources/resource_manager.py index c646c71aa..703b55358 100644 --- a/src/fastmcp/resources/resource_manager.py +++ b/src/fastmcp/resources/resource_manager.py @@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any from pydantic import AnyUrl -from fastmcp import settings +import fastmcp from fastmcp.exceptions import NotFoundError, ResourceError from fastmcp.resources.resource import Resource from fastmcp.resources.template import ( @@ -44,7 +44,9 @@ class ResourceManager: self._resources: dict[str, Resource] = {} self._templates: dict[str, ResourceTemplate] = {} self._mounted_servers: list[MountedServer] = [] - self.mask_error_details = mask_error_details or settings.mask_error_details + self.mask_error_details = ( + mask_error_details or fastmcp.settings.mask_error_details + ) # Default to "warn" if None is provided if duplicate_behavior is None: @@ -114,7 +116,7 @@ class ResourceManager: logger.warning( f"Failed to get resources from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}" ) - if settings.mounted_components_raise_on_load_error: + if fastmcp.settings.mounted_components_raise_on_load_error: raise continue @@ -167,7 +169,7 @@ class ResourceManager: logger.warning( f"Failed to get templates from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}" ) - if settings.mounted_components_raise_on_load_error: + if fastmcp.settings.mounted_components_raise_on_load_error: raise continue @@ -261,7 +263,7 @@ class ResourceManager: returns the existing resource. """ # deprecated in 2.7.0 - if settings.deprecation_warnings: + if fastmcp.settings.deprecation_warnings: warnings.warn( "add_resource_from_fn is deprecated. Use Resource.from_function() and call add_resource() instead.", DeprecationWarning, @@ -310,7 +312,7 @@ class ResourceManager: ) -> ResourceTemplate: """Create a template from a function.""" # deprecated in 2.7.0 - if settings.deprecation_warnings: + if fastmcp.settings.deprecation_warnings: warnings.warn( "add_template_from_fn is deprecated. Use ResourceTemplate.from_function() and call add_template() instead.", DeprecationWarning, diff --git a/src/fastmcp/settings.py b/src/fastmcp/settings.py index 431b65236..47eccc8db 100644 --- a/src/fastmcp/settings.py +++ b/src/fastmcp/settings.py @@ -86,31 +86,38 @@ class Settings(BaseSettings): validate_assignment=True, ) + def _find_setting_level(self, attr: str) -> Any: + """ + Get the parent of a setting. If the setting contains one or more `__`, it will be + treated as a nested setting. + """ + current_settings_level = self + + while "__" in attr: + parent_attr, attr = attr.split("__", 1) + if not hasattr(current_settings_level, parent_attr): + raise AttributeError(f"Setting {parent_attr} does not exist.") + + # Go one level deeper into the nested settings + current_settings_level = getattr(current_settings_level, parent_attr) + + return current_settings_level + def get_setting(self, attr: str) -> Any: """ Get a setting. If the setting contains one or more `__`, it will be treated as a nested setting. """ - settings = self - while "__" in attr: - parent_attr, attr = attr.split("__", 1) - if not hasattr(settings, parent_attr): - raise AttributeError(f"Setting {parent_attr} does not exist.") - settings = getattr(settings, parent_attr) - return getattr(settings, attr) + settings_level = self._find_setting_level(attr) + return getattr(settings_level, attr) def set_setting(self, attr: str, value: Any) -> None: """ Set a setting. If the setting contains one or more `__`, it will be treated as a nested setting. """ - settings = self - while "__" in attr: - parent_attr, attr = attr.split("__", 1) - if not hasattr(settings, parent_attr): - raise AttributeError(f"Setting {parent_attr} does not exist.") - settings = getattr(settings, parent_attr) - setattr(settings, attr, value) + settings_level = self._find_setting_level(attr) + setattr(settings_level, attr, value) @classmethod def settings_customise_sources( diff --git a/src/fastmcp/tools/tool_manager.py b/src/fastmcp/tools/tool_manager.py index a40a9877c..29f8980ef 100644 --- a/src/fastmcp/tools/tool_manager.py +++ b/src/fastmcp/tools/tool_manager.py @@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any from mcp.types import ToolAnnotations -from fastmcp import settings +import fastmcp from fastmcp.exceptions import NotFoundError, ToolError from fastmcp.settings import DuplicateBehavior from fastmcp.tools.tool import Tool, ToolResult @@ -33,7 +33,9 @@ class ToolManager: ): self._tools: dict[str, Tool] = {} self._mounted_servers: list[MountedServer] = [] - self.mask_error_details = mask_error_details or settings.mask_error_details + self.mask_error_details = ( + mask_error_details or fastmcp.settings.mask_error_details + ) self.transformations = transformations or {} # Default to "warn" if None is provided @@ -86,7 +88,7 @@ class ToolManager: logger.warning( f"Failed to get tools from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}" ) - if settings.mounted_components_raise_on_load_error: + if fastmcp.settings.mounted_components_raise_on_load_error: raise continue @@ -146,7 +148,7 @@ class ToolManager: ) -> Tool: """Add a tool to the server.""" # deprecated in 2.7.0 - if settings.deprecation_warnings: + if fastmcp.settings.deprecation_warnings: warnings.warn( "ToolManager.add_tool_from_fn() is deprecated. Use Tool.from_function() and call add_tool() instead.", DeprecationWarning, diff --git a/tests/deprecated/test_settings.py b/tests/deprecated/test_settings.py index ad6a29a67..8698654a6 100644 --- a/tests/deprecated/test_settings.py +++ b/tests/deprecated/test_settings.py @@ -1,10 +1,15 @@ import os import warnings +from pathlib import Path +from tempfile import TemporaryDirectory +from textwrap import dedent from unittest.mock import patch import pytest from fastmcp import FastMCP +from fastmcp.client import Client +from fastmcp.client.transports import PythonStdioTransport from fastmcp.settings import Settings from fastmcp.utilities.tests import caplog_for_fastmcp @@ -12,6 +17,45 @@ from fastmcp.utilities.tests import caplog_for_fastmcp pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning") +class TestSettingsImport: + """Test settings import.""" + + async def test_settings_from_environment_issue_1749(self): + """Test that when auth is enabled, the server starts.""" + + script = dedent(""" + import os + + os.environ["FASTMCP_SERVER_AUTH"] = "fastmcp.server.auth.providers.azure.AzureProvider" + + os.environ["FASTMCP_SERVER_AUTH_AZURE_TENANT_ID"] = "A_Valid_Value" + os.environ["FASTMCP_SERVER_AUTH_AZURE_CLIENT_ID"] = "A_Valid_Value" + os.environ["FASTMCP_SERVER_AUTH_AZURE_CLIENT_SECRET"] = "A_Valid_Value" + os.environ["FASTMCP_SERVER_AUTH_AZURE_REDIRECT_PATH"] = "/auth/callback" + os.environ["FASTMCP_SERVER_AUTH_AZURE_BASE_URL"] = "http://localhost:8000" + os.environ["FASTMCP_SERVER_AUTH_AZURE_REQUIRED_SCOPES"] = "User.Read,email,profile" + + import fastmcp + + mcp = fastmcp.FastMCP("TestServer") + + mcp.run() + """) + + with TemporaryDirectory() as temp_dir: + server_file = Path(temp_dir) / "server.py" + server_file.write_text(script) + + transport: PythonStdioTransport = PythonStdioTransport( + script_path=server_file + ) + + async with Client[PythonStdioTransport](transport=transport) as client: + tools = await client.list_tools() + + assert tools == [] + + class TestDeprecatedServerInitKwargs: """Test deprecated server initialization keyword arguments."""