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 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,

View file

@ -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,

View file

@ -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(

View file

@ -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,

View file

@ -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."""