Compare commits

...

1 commit

Author SHA1 Message Date
William Easton
577d23d5c3
Fix Server Creation with Auth Providers 2025-09-13 15:23:48 -05:00
5 changed files with 84 additions and 27 deletions

View file

@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any
from mcp import GetPromptResult from mcp import GetPromptResult
from fastmcp import settings import fastmcp
from fastmcp.exceptions import NotFoundError, PromptError from fastmcp.exceptions import NotFoundError, PromptError
from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
from fastmcp.settings import DuplicateBehavior from fastmcp.settings import DuplicateBehavior
@ -28,7 +28,9 @@ class PromptManager:
): ):
self._prompts: dict[str, Prompt] = {} self._prompts: dict[str, Prompt] = {}
self._mounted_servers: list[MountedServer] = [] 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 # Default to "warn" if None is provided
if duplicate_behavior is None: if duplicate_behavior is None:
@ -80,7 +82,7 @@ class PromptManager:
logger.warning( logger.warning(
f"Failed to get prompts from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}" 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 raise
continue continue
@ -122,7 +124,7 @@ class PromptManager:
) -> FunctionPrompt: ) -> FunctionPrompt:
"""Create a prompt from a function.""" """Create a prompt from a function."""
# deprecated in 2.7.0 # deprecated in 2.7.0
if settings.deprecation_warnings: if fastmcp.settings.deprecation_warnings:
warnings.warn( warnings.warn(
"PromptManager.add_prompt_from_fn() is deprecated. Use Prompt.from_function() and call add_prompt() instead.", "PromptManager.add_prompt_from_fn() is deprecated. Use Prompt.from_function() and call add_prompt() instead.",
DeprecationWarning, DeprecationWarning,

View file

@ -9,7 +9,7 @@ from typing import TYPE_CHECKING, Any
from pydantic import AnyUrl from pydantic import AnyUrl
from fastmcp import settings import fastmcp
from fastmcp.exceptions import NotFoundError, ResourceError from fastmcp.exceptions import NotFoundError, ResourceError
from fastmcp.resources.resource import Resource from fastmcp.resources.resource import Resource
from fastmcp.resources.template import ( from fastmcp.resources.template import (
@ -44,7 +44,9 @@ class ResourceManager:
self._resources: dict[str, Resource] = {} self._resources: dict[str, Resource] = {}
self._templates: dict[str, ResourceTemplate] = {} self._templates: dict[str, ResourceTemplate] = {}
self._mounted_servers: list[MountedServer] = [] 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 # Default to "warn" if None is provided
if duplicate_behavior is None: if duplicate_behavior is None:
@ -114,7 +116,7 @@ class ResourceManager:
logger.warning( logger.warning(
f"Failed to get resources from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}" 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 raise
continue continue
@ -167,7 +169,7 @@ class ResourceManager:
logger.warning( logger.warning(
f"Failed to get templates from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}" 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 raise
continue continue
@ -261,7 +263,7 @@ class ResourceManager:
returns the existing resource. returns the existing resource.
""" """
# deprecated in 2.7.0 # deprecated in 2.7.0
if settings.deprecation_warnings: if fastmcp.settings.deprecation_warnings:
warnings.warn( warnings.warn(
"add_resource_from_fn is deprecated. Use Resource.from_function() and call add_resource() instead.", "add_resource_from_fn is deprecated. Use Resource.from_function() and call add_resource() instead.",
DeprecationWarning, DeprecationWarning,
@ -310,7 +312,7 @@ class ResourceManager:
) -> ResourceTemplate: ) -> ResourceTemplate:
"""Create a template from a function.""" """Create a template from a function."""
# deprecated in 2.7.0 # deprecated in 2.7.0
if settings.deprecation_warnings: if fastmcp.settings.deprecation_warnings:
warnings.warn( warnings.warn(
"add_template_from_fn is deprecated. Use ResourceTemplate.from_function() and call add_template() instead.", "add_template_from_fn is deprecated. Use ResourceTemplate.from_function() and call add_template() instead.",
DeprecationWarning, DeprecationWarning,

View file

@ -86,31 +86,38 @@ class Settings(BaseSettings):
validate_assignment=True, 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: def get_setting(self, attr: str) -> Any:
""" """
Get a setting. If the setting contains one or more `__`, it will be Get a setting. If the setting contains one or more `__`, it will be
treated as a nested setting. treated as a nested setting.
""" """
settings = self settings_level = self._find_setting_level(attr)
while "__" in attr: return getattr(settings_level, 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)
def set_setting(self, attr: str, value: Any) -> None: def set_setting(self, attr: str, value: Any) -> None:
""" """
Set a setting. If the setting contains one or more `__`, it will be Set a setting. If the setting contains one or more `__`, it will be
treated as a nested setting. treated as a nested setting.
""" """
settings = self settings_level = self._find_setting_level(attr)
while "__" in attr: setattr(settings_level, attr, value)
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)
@classmethod @classmethod
def settings_customise_sources( def settings_customise_sources(

View file

@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any
from mcp.types import ToolAnnotations from mcp.types import ToolAnnotations
from fastmcp import settings import fastmcp
from fastmcp.exceptions import NotFoundError, ToolError from fastmcp.exceptions import NotFoundError, ToolError
from fastmcp.settings import DuplicateBehavior from fastmcp.settings import DuplicateBehavior
from fastmcp.tools.tool import Tool, ToolResult from fastmcp.tools.tool import Tool, ToolResult
@ -33,7 +33,9 @@ class ToolManager:
): ):
self._tools: dict[str, Tool] = {} self._tools: dict[str, Tool] = {}
self._mounted_servers: list[MountedServer] = [] 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 {} self.transformations = transformations or {}
# Default to "warn" if None is provided # Default to "warn" if None is provided
@ -86,7 +88,7 @@ class ToolManager:
logger.warning( logger.warning(
f"Failed to get tools from server: {mounted.server.name!r}, mounted at: {mounted.prefix!r}: {e}" 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 raise
continue continue
@ -146,7 +148,7 @@ class ToolManager:
) -> Tool: ) -> Tool:
"""Add a tool to the server.""" """Add a tool to the server."""
# deprecated in 2.7.0 # deprecated in 2.7.0
if settings.deprecation_warnings: if fastmcp.settings.deprecation_warnings:
warnings.warn( warnings.warn(
"ToolManager.add_tool_from_fn() is deprecated. Use Tool.from_function() and call add_tool() instead.", "ToolManager.add_tool_from_fn() is deprecated. Use Tool.from_function() and call add_tool() instead.",
DeprecationWarning, DeprecationWarning,

View file

@ -1,10 +1,15 @@
import os import os
import warnings import warnings
from pathlib import Path
from tempfile import TemporaryDirectory
from textwrap import dedent
from unittest.mock import patch from unittest.mock import patch
import pytest import pytest
from fastmcp import FastMCP from fastmcp import FastMCP
from fastmcp.client import Client
from fastmcp.client.transports import PythonStdioTransport
from fastmcp.settings import Settings from fastmcp.settings import Settings
from fastmcp.utilities.tests import caplog_for_fastmcp 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") 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: class TestDeprecatedServerInitKwargs:
"""Test deprecated server initialization keyword arguments.""" """Test deprecated server initialization keyword arguments."""