Spaces:
Running
Running
Merge pull request #750 from jlowin/settings
Browse filesRemove open-ended and server-specific settings
- docs/servers/composition.mdx +1 -1
- pyproject.toml +2 -0
- src/fastmcp/__init__.py +4 -1
- src/fastmcp/cli/cli.py +3 -2
- src/fastmcp/client/auth/oauth.py +1 -1
- src/fastmcp/client/client.py +1 -1
- src/fastmcp/prompts/prompt_manager.py +3 -2
- src/fastmcp/resources/resource_manager.py +3 -2
- src/fastmcp/server/server.py +98 -42
- src/fastmcp/settings.py +73 -40
- src/fastmcp/tools/tool.py +1 -1
- src/fastmcp/tools/tool_manager.py +3 -2
- src/fastmcp/utilities/exceptions.py +1 -1
- src/fastmcp/utilities/tests.py +3 -3
- tests/auth/providers/test_bearer_env.py +33 -24
- tests/deprecated/test_settings.py +351 -0
- tests/utilities/test_tests.py +3 -3
docs/servers/composition.mdx
CHANGED
|
@@ -215,7 +215,7 @@ You can configure the prefix format globally in code:
|
|
| 215 |
|
| 216 |
```python
|
| 217 |
import fastmcp
|
| 218 |
-
fastmcp.settings.
|
| 219 |
```
|
| 220 |
|
| 221 |
Or via environment variable:
|
|
|
|
| 215 |
|
| 216 |
```python
|
| 217 |
import fastmcp
|
| 218 |
+
fastmcp.settings.resource_prefix_format = "protocol"
|
| 219 |
```
|
| 220 |
|
| 221 |
Or via environment variable:
|
pyproject.toml
CHANGED
|
@@ -114,3 +114,5 @@ extend-select = ["I", "UP"]
|
|
| 114 |
|
| 115 |
[tool.ruff.lint.per-file-ignores]
|
| 116 |
"__init__.py" = ["F401", "I001", "RUF013"]
|
|
|
|
|
|
|
|
|
| 114 |
|
| 115 |
[tool.ruff.lint.per-file-ignores]
|
| 116 |
"__init__.py" = ["F401", "I001", "RUF013"]
|
| 117 |
+
# allow imports not at the top of the file
|
| 118 |
+
"src/fastmcp/__init__.py" = ["E402"]
|
src/fastmcp/__init__.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
| 1 |
"""FastMCP - An ergonomic MCP interface."""
|
| 2 |
|
| 3 |
from importlib.metadata import version
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
from fastmcp.server.server import FastMCP
|
| 6 |
from fastmcp.server.context import Context
|
|
@@ -8,7 +11,7 @@ import fastmcp.server
|
|
| 8 |
|
| 9 |
from fastmcp.client import Client
|
| 10 |
from fastmcp.utilities.types import Image
|
| 11 |
-
from . import client
|
| 12 |
|
| 13 |
__version__ = version("fastmcp")
|
| 14 |
__all__ = [
|
|
|
|
| 1 |
"""FastMCP - An ergonomic MCP interface."""
|
| 2 |
|
| 3 |
from importlib.metadata import version
|
| 4 |
+
from fastmcp.settings import Settings
|
| 5 |
+
|
| 6 |
+
settings = Settings()
|
| 7 |
|
| 8 |
from fastmcp.server.server import FastMCP
|
| 9 |
from fastmcp.server.context import Context
|
|
|
|
| 11 |
|
| 12 |
from fastmcp.client import Client
|
| 13 |
from fastmcp.utilities.types import Image
|
| 14 |
+
from . import client
|
| 15 |
|
| 16 |
__version__ = version("fastmcp")
|
| 17 |
__all__ = [
|
src/fastmcp/cli/cli.py
CHANGED
|
@@ -18,6 +18,7 @@ from typer import Context, Exit
|
|
| 18 |
import fastmcp
|
| 19 |
from fastmcp.cli import claude
|
| 20 |
from fastmcp.cli import run as run_module
|
|
|
|
| 21 |
from fastmcp.utilities.logging import get_logger
|
| 22 |
|
| 23 |
logger = get_logger("cli")
|
|
@@ -165,8 +166,8 @@ def dev(
|
|
| 165 |
|
| 166 |
try:
|
| 167 |
# Import server to get dependencies
|
| 168 |
-
server = run_module.import_server(file, server_object)
|
| 169 |
-
if
|
| 170 |
with_packages = list(set(with_packages + server.dependencies))
|
| 171 |
|
| 172 |
env_vars = {}
|
|
|
|
| 18 |
import fastmcp
|
| 19 |
from fastmcp.cli import claude
|
| 20 |
from fastmcp.cli import run as run_module
|
| 21 |
+
from fastmcp.server.server import FastMCP
|
| 22 |
from fastmcp.utilities.logging import get_logger
|
| 23 |
|
| 24 |
logger = get_logger("cli")
|
|
|
|
| 166 |
|
| 167 |
try:
|
| 168 |
# Import server to get dependencies
|
| 169 |
+
server: FastMCP = run_module.import_server(file, server_object)
|
| 170 |
+
if server.dependencies is not None:
|
| 171 |
with_packages = list(set(with_packages + server.dependencies))
|
| 172 |
|
| 173 |
env_vars = {}
|
src/fastmcp/client/auth/oauth.py
CHANGED
|
@@ -23,10 +23,10 @@ from mcp.shared.auth import (
|
|
| 23 |
)
|
| 24 |
from pydantic import AnyHttpUrl, ValidationError
|
| 25 |
|
|
|
|
| 26 |
from fastmcp.client.oauth_callback import (
|
| 27 |
create_oauth_callback_server,
|
| 28 |
)
|
| 29 |
-
from fastmcp.settings import settings as fastmcp_global_settings
|
| 30 |
from fastmcp.utilities.http import find_available_port
|
| 31 |
from fastmcp.utilities.logging import get_logger
|
| 32 |
|
|
|
|
| 23 |
)
|
| 24 |
from pydantic import AnyHttpUrl, ValidationError
|
| 25 |
|
| 26 |
+
from fastmcp import settings as fastmcp_global_settings
|
| 27 |
from fastmcp.client.oauth_callback import (
|
| 28 |
create_oauth_callback_server,
|
| 29 |
)
|
|
|
|
| 30 |
from fastmcp.utilities.http import find_available_port
|
| 31 |
from fastmcp.utilities.logging import get_logger
|
| 32 |
|
src/fastmcp/client/client.py
CHANGED
|
@@ -166,7 +166,7 @@ class Client(Generic[ClientTransportT]):
|
|
| 166 |
|
| 167 |
# handle init handshake timeout
|
| 168 |
if init_timeout is None:
|
| 169 |
-
init_timeout = fastmcp.settings.
|
| 170 |
if isinstance(init_timeout, datetime.timedelta):
|
| 171 |
init_timeout = init_timeout.total_seconds()
|
| 172 |
elif not init_timeout:
|
|
|
|
| 166 |
|
| 167 |
# handle init handshake timeout
|
| 168 |
if init_timeout is None:
|
| 169 |
+
init_timeout = fastmcp.settings.client_init_timeout
|
| 170 |
if isinstance(init_timeout, datetime.timedelta):
|
| 171 |
init_timeout = init_timeout.total_seconds()
|
| 172 |
elif not init_timeout:
|
src/fastmcp/prompts/prompt_manager.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any
|
|
| 6 |
|
| 7 |
from mcp import GetPromptResult
|
| 8 |
|
|
|
|
| 9 |
from fastmcp.exceptions import NotFoundError, PromptError
|
| 10 |
from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
|
| 11 |
from fastmcp.settings import DuplicateBehavior
|
|
@@ -23,10 +24,10 @@ class PromptManager:
|
|
| 23 |
def __init__(
|
| 24 |
self,
|
| 25 |
duplicate_behavior: DuplicateBehavior | None = None,
|
| 26 |
-
mask_error_details: bool =
|
| 27 |
):
|
| 28 |
self._prompts: dict[str, Prompt] = {}
|
| 29 |
-
self.mask_error_details = mask_error_details
|
| 30 |
|
| 31 |
# Default to "warn" if None is provided
|
| 32 |
if duplicate_behavior is None:
|
|
|
|
| 6 |
|
| 7 |
from mcp import GetPromptResult
|
| 8 |
|
| 9 |
+
from fastmcp import settings
|
| 10 |
from fastmcp.exceptions import NotFoundError, PromptError
|
| 11 |
from fastmcp.prompts.prompt import FunctionPrompt, Prompt, PromptResult
|
| 12 |
from fastmcp.settings import DuplicateBehavior
|
|
|
|
| 24 |
def __init__(
|
| 25 |
self,
|
| 26 |
duplicate_behavior: DuplicateBehavior | None = None,
|
| 27 |
+
mask_error_details: bool | None = None,
|
| 28 |
):
|
| 29 |
self._prompts: dict[str, Prompt] = {}
|
| 30 |
+
self.mask_error_details = mask_error_details or settings.mask_error_details
|
| 31 |
|
| 32 |
# Default to "warn" if None is provided
|
| 33 |
if duplicate_behavior is None:
|
src/fastmcp/resources/resource_manager.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
| 7 |
|
| 8 |
from pydantic import AnyUrl
|
| 9 |
|
|
|
|
| 10 |
from fastmcp.exceptions import NotFoundError, ResourceError
|
| 11 |
from fastmcp.resources.resource import Resource
|
| 12 |
from fastmcp.resources.template import (
|
|
@@ -25,7 +26,7 @@ class ResourceManager:
|
|
| 25 |
def __init__(
|
| 26 |
self,
|
| 27 |
duplicate_behavior: DuplicateBehavior | None = None,
|
| 28 |
-
mask_error_details: bool =
|
| 29 |
):
|
| 30 |
"""Initialize the ResourceManager.
|
| 31 |
|
|
@@ -37,7 +38,7 @@ class ResourceManager:
|
|
| 37 |
"""
|
| 38 |
self._resources: dict[str, Resource] = {}
|
| 39 |
self._templates: dict[str, ResourceTemplate] = {}
|
| 40 |
-
self.mask_error_details = mask_error_details
|
| 41 |
|
| 42 |
# Default to "warn" if None is provided
|
| 43 |
if duplicate_behavior is None:
|
|
|
|
| 7 |
|
| 8 |
from pydantic import AnyUrl
|
| 9 |
|
| 10 |
+
from fastmcp import settings
|
| 11 |
from fastmcp.exceptions import NotFoundError, ResourceError
|
| 12 |
from fastmcp.resources.resource import Resource
|
| 13 |
from fastmcp.resources.template import (
|
|
|
|
| 26 |
def __init__(
|
| 27 |
self,
|
| 28 |
duplicate_behavior: DuplicateBehavior | None = None,
|
| 29 |
+
mask_error_details: bool | None = None,
|
| 30 |
):
|
| 31 |
"""Initialize the ResourceManager.
|
| 32 |
|
|
|
|
| 38 |
"""
|
| 39 |
self._resources: dict[str, Resource] = {}
|
| 40 |
self._templates: dict[str, ResourceTemplate] = {}
|
| 41 |
+
self.mask_error_details = mask_error_details or settings.mask_error_details
|
| 42 |
|
| 43 |
# Default to "warn" if None is provided
|
| 44 |
if duplicate_behavior is None:
|
src/fastmcp/server/server.py
CHANGED
|
@@ -43,7 +43,6 @@ from starlette.routing import BaseRoute, Route
|
|
| 43 |
|
| 44 |
import fastmcp
|
| 45 |
import fastmcp.server
|
| 46 |
-
import fastmcp.settings
|
| 47 |
from fastmcp.exceptions import DisabledError, NotFoundError
|
| 48 |
from fastmcp.prompts import Prompt, PromptManager
|
| 49 |
from fastmcp.prompts.prompt import FunctionPrompt
|
|
@@ -56,6 +55,7 @@ from fastmcp.server.http import (
|
|
| 56 |
create_sse_app,
|
| 57 |
create_streamable_http_app,
|
| 58 |
)
|
|
|
|
| 59 |
from fastmcp.tools import ToolManager
|
| 60 |
from fastmcp.tools.tool import FunctionTool, Tool
|
| 61 |
from fastmcp.utilities.cache import TimedCache
|
|
@@ -121,7 +121,6 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 121 |
| None
|
| 122 |
) = None,
|
| 123 |
tags: set[str] | None = None,
|
| 124 |
-
dependencies: list[str] | None = None,
|
| 125 |
tool_serializer: Callable[[Any], str] | None = None,
|
| 126 |
cache_expiration_seconds: float | None = None,
|
| 127 |
on_duplicate_tools: DuplicateBehavior | None = None,
|
|
@@ -130,44 +129,44 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 130 |
resource_prefix_format: Literal["protocol", "path"] | None = None,
|
| 131 |
mask_error_details: bool | None = None,
|
| 132 |
tools: list[Tool | Callable[..., Any]] | None = None,
|
| 133 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
):
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
# If mask_error_details is provided, override the settings value
|
| 140 |
-
if mask_error_details is not None:
|
| 141 |
-
self.settings.mask_error_details = mask_error_details
|
| 142 |
-
|
| 143 |
-
self.resource_prefix_format: Literal["protocol", "path"]
|
| 144 |
-
if resource_prefix_format is None:
|
| 145 |
-
self.resource_prefix_format = (
|
| 146 |
-
fastmcp.settings.settings.resource_prefix_format
|
| 147 |
-
)
|
| 148 |
-
else:
|
| 149 |
-
self.resource_prefix_format = resource_prefix_format
|
| 150 |
|
| 151 |
self.tags: set[str] = tags or set()
|
| 152 |
-
|
| 153 |
self._cache = TimedCache(
|
| 154 |
-
expiration=datetime.timedelta(
|
| 155 |
-
seconds=self.settings.cache_expiration_seconds
|
| 156 |
-
)
|
| 157 |
)
|
| 158 |
self._mounted_servers: dict[str, MountedServer] = {}
|
| 159 |
self._additional_http_routes: list[BaseRoute] = []
|
| 160 |
self._tool_manager = ToolManager(
|
| 161 |
duplicate_behavior=on_duplicate_tools,
|
| 162 |
-
mask_error_details=
|
| 163 |
)
|
| 164 |
self._resource_manager = ResourceManager(
|
| 165 |
duplicate_behavior=on_duplicate_resources,
|
| 166 |
-
mask_error_details=
|
| 167 |
)
|
| 168 |
self._prompt_manager = PromptManager(
|
| 169 |
duplicate_behavior=on_duplicate_prompts,
|
| 170 |
-
mask_error_details=
|
| 171 |
)
|
| 172 |
self._tool_serializer = tool_serializer
|
| 173 |
|
|
@@ -182,7 +181,7 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 182 |
lifespan=_lifespan_wrapper(self, lifespan),
|
| 183 |
)
|
| 184 |
|
| 185 |
-
if auth is None and
|
| 186 |
auth = EnvBearerAuthProvider()
|
| 187 |
self.auth = auth
|
| 188 |
|
|
@@ -194,10 +193,62 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 194 |
|
| 195 |
# Set up MCP protocol handlers
|
| 196 |
self._setup_handlers()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 197 |
|
| 198 |
def __repr__(self) -> str:
|
| 199 |
return f"{type(self).__name__}({self.name!r})"
|
| 200 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
@property
|
| 202 |
def name(self) -> str:
|
| 203 |
return self._mcp_server.name
|
|
@@ -1127,9 +1178,11 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1127 |
path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
|
| 1128 |
uvicorn_config: Additional configuration for the Uvicorn server
|
| 1129 |
"""
|
| 1130 |
-
host = host or self.
|
| 1131 |
-
port = port or self.
|
| 1132 |
-
default_log_level_to_use = (
|
|
|
|
|
|
|
| 1133 |
|
| 1134 |
app = self.http_app(path=path, transport=transport, middleware=middleware)
|
| 1135 |
|
|
@@ -1203,10 +1256,10 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1203 |
)
|
| 1204 |
return create_sse_app(
|
| 1205 |
server=self,
|
| 1206 |
-
message_path=message_path or self.
|
| 1207 |
-
sse_path=path or self.
|
| 1208 |
auth=self.auth,
|
| 1209 |
-
debug=self.
|
| 1210 |
middleware=middleware,
|
| 1211 |
)
|
| 1212 |
|
|
@@ -1234,6 +1287,8 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1234 |
self,
|
| 1235 |
path: str | None = None,
|
| 1236 |
middleware: list[Middleware] | None = None,
|
|
|
|
|
|
|
| 1237 |
transport: Literal["streamable-http", "sse"] = "streamable-http",
|
| 1238 |
) -> StarletteWithLifespan:
|
| 1239 |
"""Create a Starlette app using the specified HTTP transport.
|
|
@@ -1250,21 +1305,22 @@ class FastMCP(Generic[LifespanResultT]):
|
|
| 1250 |
if transport == "streamable-http":
|
| 1251 |
return create_streamable_http_app(
|
| 1252 |
server=self,
|
| 1253 |
-
streamable_http_path=path
|
|
|
|
| 1254 |
event_store=None,
|
| 1255 |
auth=self.auth,
|
| 1256 |
-
json_response=self.
|
| 1257 |
-
stateless_http=self.
|
| 1258 |
-
debug=self.
|
| 1259 |
middleware=middleware,
|
| 1260 |
)
|
| 1261 |
elif transport == "sse":
|
| 1262 |
return create_sse_app(
|
| 1263 |
server=self,
|
| 1264 |
-
message_path=self.
|
| 1265 |
-
sse_path=path or self.
|
| 1266 |
auth=self.auth,
|
| 1267 |
-
debug=self.
|
| 1268 |
middleware=middleware,
|
| 1269 |
)
|
| 1270 |
|
|
@@ -1716,7 +1772,7 @@ def add_resource_prefix(
|
|
| 1716 |
# Get the server settings to check for legacy format preference
|
| 1717 |
|
| 1718 |
if prefix_format is None:
|
| 1719 |
-
prefix_format = fastmcp.settings.
|
| 1720 |
|
| 1721 |
if prefix_format == "protocol":
|
| 1722 |
# Legacy style: prefix+protocol://path
|
|
@@ -1765,7 +1821,7 @@ def remove_resource_prefix(
|
|
| 1765 |
return uri
|
| 1766 |
|
| 1767 |
if prefix_format is None:
|
| 1768 |
-
prefix_format = fastmcp.settings.
|
| 1769 |
|
| 1770 |
if prefix_format == "protocol":
|
| 1771 |
# Legacy style: prefix+protocol://path
|
|
@@ -1825,7 +1881,7 @@ def has_resource_prefix(
|
|
| 1825 |
# Get the server settings to check for legacy format preference
|
| 1826 |
|
| 1827 |
if prefix_format is None:
|
| 1828 |
-
prefix_format = fastmcp.settings.
|
| 1829 |
|
| 1830 |
if prefix_format == "protocol":
|
| 1831 |
# Legacy style: prefix+protocol://path
|
|
|
|
| 43 |
|
| 44 |
import fastmcp
|
| 45 |
import fastmcp.server
|
|
|
|
| 46 |
from fastmcp.exceptions import DisabledError, NotFoundError
|
| 47 |
from fastmcp.prompts import Prompt, PromptManager
|
| 48 |
from fastmcp.prompts.prompt import FunctionPrompt
|
|
|
|
| 55 |
create_sse_app,
|
| 56 |
create_streamable_http_app,
|
| 57 |
)
|
| 58 |
+
from fastmcp.settings import Settings
|
| 59 |
from fastmcp.tools import ToolManager
|
| 60 |
from fastmcp.tools.tool import FunctionTool, Tool
|
| 61 |
from fastmcp.utilities.cache import TimedCache
|
|
|
|
| 121 |
| None
|
| 122 |
) = None,
|
| 123 |
tags: set[str] | None = None,
|
|
|
|
| 124 |
tool_serializer: Callable[[Any], str] | None = None,
|
| 125 |
cache_expiration_seconds: float | None = None,
|
| 126 |
on_duplicate_tools: DuplicateBehavior | None = None,
|
|
|
|
| 129 |
resource_prefix_format: Literal["protocol", "path"] | None = None,
|
| 130 |
mask_error_details: bool | None = None,
|
| 131 |
tools: list[Tool | Callable[..., Any]] | None = None,
|
| 132 |
+
dependencies: list[str] | None = None,
|
| 133 |
+
# ---
|
| 134 |
+
# ---
|
| 135 |
+
# --- The following arguments are DEPRECATED ---
|
| 136 |
+
# ---
|
| 137 |
+
# ---
|
| 138 |
+
log_level: str | None = None,
|
| 139 |
+
debug: bool | None = None,
|
| 140 |
+
host: str | None = None,
|
| 141 |
+
port: int | None = None,
|
| 142 |
+
sse_path: str | None = None,
|
| 143 |
+
message_path: str | None = None,
|
| 144 |
+
streamable_http_path: str | None = None,
|
| 145 |
+
json_response: bool | None = None,
|
| 146 |
+
stateless_http: bool | None = None,
|
| 147 |
):
|
| 148 |
+
self.resource_prefix_format: Literal["protocol", "path"] = (
|
| 149 |
+
resource_prefix_format or fastmcp.settings.resource_prefix_format
|
| 150 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
|
| 152 |
self.tags: set[str] = tags or set()
|
| 153 |
+
|
| 154 |
self._cache = TimedCache(
|
| 155 |
+
expiration=datetime.timedelta(seconds=cache_expiration_seconds or 0)
|
|
|
|
|
|
|
| 156 |
)
|
| 157 |
self._mounted_servers: dict[str, MountedServer] = {}
|
| 158 |
self._additional_http_routes: list[BaseRoute] = []
|
| 159 |
self._tool_manager = ToolManager(
|
| 160 |
duplicate_behavior=on_duplicate_tools,
|
| 161 |
+
mask_error_details=mask_error_details,
|
| 162 |
)
|
| 163 |
self._resource_manager = ResourceManager(
|
| 164 |
duplicate_behavior=on_duplicate_resources,
|
| 165 |
+
mask_error_details=mask_error_details,
|
| 166 |
)
|
| 167 |
self._prompt_manager = PromptManager(
|
| 168 |
duplicate_behavior=on_duplicate_prompts,
|
| 169 |
+
mask_error_details=mask_error_details,
|
| 170 |
)
|
| 171 |
self._tool_serializer = tool_serializer
|
| 172 |
|
|
|
|
| 181 |
lifespan=_lifespan_wrapper(self, lifespan),
|
| 182 |
)
|
| 183 |
|
| 184 |
+
if auth is None and fastmcp.settings.default_auth_provider == "bearer_env":
|
| 185 |
auth = EnvBearerAuthProvider()
|
| 186 |
self.auth = auth
|
| 187 |
|
|
|
|
| 193 |
|
| 194 |
# Set up MCP protocol handlers
|
| 195 |
self._setup_handlers()
|
| 196 |
+
self.dependencies = dependencies or fastmcp.settings.server_dependencies
|
| 197 |
+
|
| 198 |
+
# handle deprecated settings
|
| 199 |
+
self._handle_deprecated_settings(
|
| 200 |
+
log_level=log_level,
|
| 201 |
+
debug=debug,
|
| 202 |
+
host=host,
|
| 203 |
+
port=port,
|
| 204 |
+
sse_path=sse_path,
|
| 205 |
+
message_path=message_path,
|
| 206 |
+
streamable_http_path=streamable_http_path,
|
| 207 |
+
json_response=json_response,
|
| 208 |
+
stateless_http=stateless_http,
|
| 209 |
+
)
|
| 210 |
|
| 211 |
def __repr__(self) -> str:
|
| 212 |
return f"{type(self).__name__}({self.name!r})"
|
| 213 |
|
| 214 |
+
def _handle_deprecated_settings(
|
| 215 |
+
self,
|
| 216 |
+
log_level: str | None,
|
| 217 |
+
debug: bool | None,
|
| 218 |
+
host: str | None,
|
| 219 |
+
port: int | None,
|
| 220 |
+
sse_path: str | None,
|
| 221 |
+
message_path: str | None,
|
| 222 |
+
streamable_http_path: str | None,
|
| 223 |
+
json_response: bool | None,
|
| 224 |
+
stateless_http: bool | None,
|
| 225 |
+
) -> None:
|
| 226 |
+
"""Handle deprecated settings. Deprecated in 2.8.0."""
|
| 227 |
+
deprecated_settings: dict[str, Any] = {}
|
| 228 |
+
|
| 229 |
+
for name, arg in [
|
| 230 |
+
("log_level", log_level),
|
| 231 |
+
("debug", debug),
|
| 232 |
+
("host", host),
|
| 233 |
+
("port", port),
|
| 234 |
+
("sse_path", sse_path),
|
| 235 |
+
("message_path", message_path),
|
| 236 |
+
("streamable_http_path", streamable_http_path),
|
| 237 |
+
("json_response", json_response),
|
| 238 |
+
("stateless_http", stateless_http),
|
| 239 |
+
]:
|
| 240 |
+
if arg is not None:
|
| 241 |
+
# Deprecated in 2.8.0
|
| 242 |
+
warnings.warn(
|
| 243 |
+
f"Providing `{name}` when creating a server is deprecated. Provide it when calling `run` or as a global setting instead.",
|
| 244 |
+
DeprecationWarning,
|
| 245 |
+
stacklevel=2,
|
| 246 |
+
)
|
| 247 |
+
deprecated_settings[name] = arg
|
| 248 |
+
|
| 249 |
+
combined_settings = fastmcp.settings.model_dump() | deprecated_settings
|
| 250 |
+
self._deprecated_settings = Settings(**combined_settings)
|
| 251 |
+
|
| 252 |
@property
|
| 253 |
def name(self) -> str:
|
| 254 |
return self._mcp_server.name
|
|
|
|
| 1178 |
path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
|
| 1179 |
uvicorn_config: Additional configuration for the Uvicorn server
|
| 1180 |
"""
|
| 1181 |
+
host = host or self._deprecated_settings.host
|
| 1182 |
+
port = port or self._deprecated_settings.port
|
| 1183 |
+
default_log_level_to_use = (
|
| 1184 |
+
log_level or self._deprecated_settings.log_level
|
| 1185 |
+
).lower()
|
| 1186 |
|
| 1187 |
app = self.http_app(path=path, transport=transport, middleware=middleware)
|
| 1188 |
|
|
|
|
| 1256 |
)
|
| 1257 |
return create_sse_app(
|
| 1258 |
server=self,
|
| 1259 |
+
message_path=message_path or self._deprecated_settings.message_path,
|
| 1260 |
+
sse_path=path or self._deprecated_settings.sse_path,
|
| 1261 |
auth=self.auth,
|
| 1262 |
+
debug=self._deprecated_settings.debug,
|
| 1263 |
middleware=middleware,
|
| 1264 |
)
|
| 1265 |
|
|
|
|
| 1287 |
self,
|
| 1288 |
path: str | None = None,
|
| 1289 |
middleware: list[Middleware] | None = None,
|
| 1290 |
+
json_response: bool | None = None,
|
| 1291 |
+
stateless_http: bool | None = None,
|
| 1292 |
transport: Literal["streamable-http", "sse"] = "streamable-http",
|
| 1293 |
) -> StarletteWithLifespan:
|
| 1294 |
"""Create a Starlette app using the specified HTTP transport.
|
|
|
|
| 1305 |
if transport == "streamable-http":
|
| 1306 |
return create_streamable_http_app(
|
| 1307 |
server=self,
|
| 1308 |
+
streamable_http_path=path
|
| 1309 |
+
or self._deprecated_settings.streamable_http_path,
|
| 1310 |
event_store=None,
|
| 1311 |
auth=self.auth,
|
| 1312 |
+
json_response=self._deprecated_settings.json_response,
|
| 1313 |
+
stateless_http=self._deprecated_settings.stateless_http,
|
| 1314 |
+
debug=self._deprecated_settings.debug,
|
| 1315 |
middleware=middleware,
|
| 1316 |
)
|
| 1317 |
elif transport == "sse":
|
| 1318 |
return create_sse_app(
|
| 1319 |
server=self,
|
| 1320 |
+
message_path=self._deprecated_settings.message_path,
|
| 1321 |
+
sse_path=path or self._deprecated_settings.sse_path,
|
| 1322 |
auth=self.auth,
|
| 1323 |
+
debug=self._deprecated_settings.debug,
|
| 1324 |
middleware=middleware,
|
| 1325 |
)
|
| 1326 |
|
|
|
|
| 1772 |
# Get the server settings to check for legacy format preference
|
| 1773 |
|
| 1774 |
if prefix_format is None:
|
| 1775 |
+
prefix_format = fastmcp.settings.resource_prefix_format
|
| 1776 |
|
| 1777 |
if prefix_format == "protocol":
|
| 1778 |
# Legacy style: prefix+protocol://path
|
|
|
|
| 1821 |
return uri
|
| 1822 |
|
| 1823 |
if prefix_format is None:
|
| 1824 |
+
prefix_format = fastmcp.settings.resource_prefix_format
|
| 1825 |
|
| 1826 |
if prefix_format == "protocol":
|
| 1827 |
# Legacy style: prefix+protocol://path
|
|
|
|
| 1881 |
# Get the server settings to check for legacy format preference
|
| 1882 |
|
| 1883 |
if prefix_format is None:
|
| 1884 |
+
prefix_format = fastmcp.settings.resource_prefix_format
|
| 1885 |
|
| 1886 |
if prefix_format == "protocol":
|
| 1887 |
# Legacy style: prefix+protocol://path
|
src/fastmcp/settings.py
CHANGED
|
@@ -1,12 +1,16 @@
|
|
| 1 |
from __future__ import annotations as _annotations
|
| 2 |
|
| 3 |
import inspect
|
|
|
|
| 4 |
from pathlib import Path
|
| 5 |
-
from typing import Annotated, Literal
|
| 6 |
|
| 7 |
from pydantic import Field, model_validator
|
|
|
|
| 8 |
from pydantic_settings import (
|
| 9 |
BaseSettings,
|
|
|
|
|
|
|
| 10 |
SettingsConfigDict,
|
| 11 |
)
|
| 12 |
from typing_extensions import Self
|
|
@@ -16,17 +20,82 @@ LOG_LEVEL = Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"]
|
|
| 16 |
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
| 17 |
|
| 18 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
class Settings(BaseSettings):
|
| 20 |
"""FastMCP settings."""
|
| 21 |
|
| 22 |
-
model_config =
|
| 23 |
-
|
| 24 |
env_file=".env",
|
| 25 |
extra="ignore",
|
| 26 |
env_nested_delimiter="__",
|
| 27 |
nested_model_default_partial_update=True,
|
| 28 |
)
|
| 29 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
home: Path = Path.home() / ".fastmcp"
|
| 31 |
|
| 32 |
test_mode: bool = False
|
|
@@ -107,27 +176,6 @@ class Settings(BaseSettings):
|
|
| 107 |
|
| 108 |
return self
|
| 109 |
|
| 110 |
-
|
| 111 |
-
class ServerSettings(BaseSettings):
|
| 112 |
-
"""FastMCP server settings.
|
| 113 |
-
|
| 114 |
-
All settings can be configured via environment variables with the prefix FASTMCP_.
|
| 115 |
-
For example, FASTMCP_DEBUG=true will set debug=True.
|
| 116 |
-
"""
|
| 117 |
-
|
| 118 |
-
model_config = SettingsConfigDict(
|
| 119 |
-
env_prefix="FASTMCP_SERVER_",
|
| 120 |
-
env_file=".env",
|
| 121 |
-
extra="ignore",
|
| 122 |
-
env_nested_delimiter="__",
|
| 123 |
-
nested_model_default_partial_update=True,
|
| 124 |
-
)
|
| 125 |
-
|
| 126 |
-
log_level: Annotated[
|
| 127 |
-
LOG_LEVEL,
|
| 128 |
-
Field(default_factory=lambda: Settings().log_level),
|
| 129 |
-
]
|
| 130 |
-
|
| 131 |
# HTTP settings
|
| 132 |
host: str = "127.0.0.1"
|
| 133 |
port: int = 8000
|
|
@@ -136,15 +184,6 @@ class ServerSettings(BaseSettings):
|
|
| 136 |
streamable_http_path: str = "/mcp"
|
| 137 |
debug: bool = False
|
| 138 |
|
| 139 |
-
# resource settings
|
| 140 |
-
on_duplicate_resources: DuplicateBehavior = "warn"
|
| 141 |
-
|
| 142 |
-
# tool settings
|
| 143 |
-
on_duplicate_tools: DuplicateBehavior = "warn"
|
| 144 |
-
|
| 145 |
-
# prompt settings
|
| 146 |
-
on_duplicate_prompts: DuplicateBehavior = "warn"
|
| 147 |
-
|
| 148 |
# error handling
|
| 149 |
mask_error_details: Annotated[
|
| 150 |
bool,
|
|
@@ -162,7 +201,7 @@ class ServerSettings(BaseSettings):
|
|
| 162 |
),
|
| 163 |
] = False
|
| 164 |
|
| 165 |
-
|
| 166 |
list[str],
|
| 167 |
Field(
|
| 168 |
default_factory=list,
|
|
@@ -170,9 +209,6 @@ class ServerSettings(BaseSettings):
|
|
| 170 |
),
|
| 171 |
] = []
|
| 172 |
|
| 173 |
-
# cache settings (for getting attributes from servers, used to avoid repeated calls)
|
| 174 |
-
cache_expiration_seconds: float = 0
|
| 175 |
-
|
| 176 |
# StreamableHTTP settings
|
| 177 |
json_response: bool = False
|
| 178 |
stateless_http: bool = (
|
|
@@ -197,6 +233,3 @@ class ServerSettings(BaseSettings):
|
|
| 197 |
),
|
| 198 |
),
|
| 199 |
] = None
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
settings = Settings()
|
|
|
|
| 1 |
from __future__ import annotations as _annotations
|
| 2 |
|
| 3 |
import inspect
|
| 4 |
+
import warnings
|
| 5 |
from pathlib import Path
|
| 6 |
+
from typing import Annotated, Any, Literal
|
| 7 |
|
| 8 |
from pydantic import Field, model_validator
|
| 9 |
+
from pydantic.fields import FieldInfo
|
| 10 |
from pydantic_settings import (
|
| 11 |
BaseSettings,
|
| 12 |
+
EnvSettingsSource,
|
| 13 |
+
PydanticBaseSettingsSource,
|
| 14 |
SettingsConfigDict,
|
| 15 |
)
|
| 16 |
from typing_extensions import Self
|
|
|
|
| 20 |
DuplicateBehavior = Literal["warn", "error", "replace", "ignore"]
|
| 21 |
|
| 22 |
|
| 23 |
+
class ExtendedEnvSettingsSource(EnvSettingsSource):
|
| 24 |
+
"""
|
| 25 |
+
A special EnvSettingsSource that allows for multiple env var prefixes to be used.
|
| 26 |
+
|
| 27 |
+
Raises a deprecation warning if the old `FASTMCP_SERVER_` prefix is used.
|
| 28 |
+
"""
|
| 29 |
+
|
| 30 |
+
def get_field_value(
|
| 31 |
+
self, field: FieldInfo, field_name: str
|
| 32 |
+
) -> tuple[Any, str, bool]:
|
| 33 |
+
if prefixes := self.config.get("env_prefixes"):
|
| 34 |
+
for prefix in prefixes:
|
| 35 |
+
self.env_prefix = prefix
|
| 36 |
+
env_val, field_key, value_is_complex = super().get_field_value(
|
| 37 |
+
field, field_name
|
| 38 |
+
)
|
| 39 |
+
if env_val is not None:
|
| 40 |
+
if prefix == "FASTMCP_SERVER_":
|
| 41 |
+
# Deprecated in 2.8.0
|
| 42 |
+
warnings.warn(
|
| 43 |
+
"Using `FASTMCP_SERVER_` environment variables is deprecated. Use `FASTMCP_` instead.",
|
| 44 |
+
DeprecationWarning,
|
| 45 |
+
stacklevel=2,
|
| 46 |
+
)
|
| 47 |
+
return env_val, field_key, value_is_complex
|
| 48 |
+
|
| 49 |
+
return super().get_field_value(field, field_name)
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class ExtendedSettingsConfigDict(SettingsConfigDict, total=False):
|
| 53 |
+
env_prefixes: list[str] | None
|
| 54 |
+
|
| 55 |
+
|
| 56 |
class Settings(BaseSettings):
|
| 57 |
"""FastMCP settings."""
|
| 58 |
|
| 59 |
+
model_config = ExtendedSettingsConfigDict(
|
| 60 |
+
env_prefixes=["FASTMCP_", "FASTMCP_SERVER_"],
|
| 61 |
env_file=".env",
|
| 62 |
extra="ignore",
|
| 63 |
env_nested_delimiter="__",
|
| 64 |
nested_model_default_partial_update=True,
|
| 65 |
)
|
| 66 |
|
| 67 |
+
@classmethod
|
| 68 |
+
def settings_customise_sources(
|
| 69 |
+
cls,
|
| 70 |
+
settings_cls: type[BaseSettings],
|
| 71 |
+
init_settings: PydanticBaseSettingsSource,
|
| 72 |
+
env_settings: PydanticBaseSettingsSource,
|
| 73 |
+
dotenv_settings: PydanticBaseSettingsSource,
|
| 74 |
+
file_secret_settings: PydanticBaseSettingsSource,
|
| 75 |
+
) -> tuple[PydanticBaseSettingsSource, ...]:
|
| 76 |
+
# can remove this classmethod after deprecated FASTMCP_SERVER_ prefix is
|
| 77 |
+
# removed
|
| 78 |
+
return (
|
| 79 |
+
init_settings,
|
| 80 |
+
ExtendedEnvSettingsSource(settings_cls),
|
| 81 |
+
dotenv_settings,
|
| 82 |
+
file_secret_settings,
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
@property
|
| 86 |
+
def settings(self) -> Self:
|
| 87 |
+
"""
|
| 88 |
+
This property is for backwards compatibility with FastMCP < 2.8.0,
|
| 89 |
+
which accessed fastmcp.settings.settings
|
| 90 |
+
"""
|
| 91 |
+
# Deprecated in 2.8.0
|
| 92 |
+
warnings.warn(
|
| 93 |
+
"Using fastmcp.settings.settings is deprecated. Use fastmcp.settings instead.",
|
| 94 |
+
DeprecationWarning,
|
| 95 |
+
stacklevel=2,
|
| 96 |
+
)
|
| 97 |
+
return self
|
| 98 |
+
|
| 99 |
home: Path = Path.home() / ".fastmcp"
|
| 100 |
|
| 101 |
test_mode: bool = False
|
|
|
|
| 176 |
|
| 177 |
return self
|
| 178 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
# HTTP settings
|
| 180 |
host: str = "127.0.0.1"
|
| 181 |
port: int = 8000
|
|
|
|
| 184 |
streamable_http_path: str = "/mcp"
|
| 185 |
debug: bool = False
|
| 186 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
# error handling
|
| 188 |
mask_error_details: Annotated[
|
| 189 |
bool,
|
|
|
|
| 201 |
),
|
| 202 |
] = False
|
| 203 |
|
| 204 |
+
server_dependencies: Annotated[
|
| 205 |
list[str],
|
| 206 |
Field(
|
| 207 |
default_factory=list,
|
|
|
|
| 209 |
),
|
| 210 |
] = []
|
| 211 |
|
|
|
|
|
|
|
|
|
|
| 212 |
# StreamableHTTP settings
|
| 213 |
json_response: bool = False
|
| 214 |
stateless_http: bool = (
|
|
|
|
| 233 |
),
|
| 234 |
),
|
| 235 |
] = None
|
|
|
|
|
|
|
|
|
src/fastmcp/tools/tool.py
CHANGED
|
@@ -156,7 +156,7 @@ class FunctionTool(Tool):
|
|
| 156 |
if context_kwarg and context_kwarg not in arguments:
|
| 157 |
arguments[context_kwarg] = get_context()
|
| 158 |
|
| 159 |
-
if fastmcp.settings.
|
| 160 |
# Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
|
| 161 |
# being passed in as JSON inside a string rather than an actual list.
|
| 162 |
#
|
|
|
|
| 156 |
if context_kwarg and context_kwarg not in arguments:
|
| 157 |
arguments[context_kwarg] = get_context()
|
| 158 |
|
| 159 |
+
if fastmcp.settings.tool_attempt_parse_json_args:
|
| 160 |
# Pre-parse data from JSON in order to handle cases like `["a", "b", "c"]`
|
| 161 |
# being passed in as JSON inside a string rather than an actual list.
|
| 162 |
#
|
src/fastmcp/tools/tool_manager.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, Any
|
|
| 6 |
|
| 7 |
from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
|
| 8 |
|
|
|
|
| 9 |
from fastmcp.exceptions import NotFoundError, ToolError
|
| 10 |
from fastmcp.settings import DuplicateBehavior
|
| 11 |
from fastmcp.tools.tool import Tool
|
|
@@ -23,10 +24,10 @@ class ToolManager:
|
|
| 23 |
def __init__(
|
| 24 |
self,
|
| 25 |
duplicate_behavior: DuplicateBehavior | None = None,
|
| 26 |
-
mask_error_details: bool =
|
| 27 |
):
|
| 28 |
self._tools: dict[str, Tool] = {}
|
| 29 |
-
self.mask_error_details = mask_error_details
|
| 30 |
|
| 31 |
# Default to "warn" if None is provided
|
| 32 |
if duplicate_behavior is None:
|
|
|
|
| 6 |
|
| 7 |
from mcp.types import EmbeddedResource, ImageContent, TextContent, ToolAnnotations
|
| 8 |
|
| 9 |
+
from fastmcp import settings
|
| 10 |
from fastmcp.exceptions import NotFoundError, ToolError
|
| 11 |
from fastmcp.settings import DuplicateBehavior
|
| 12 |
from fastmcp.tools.tool import Tool
|
|
|
|
| 24 |
def __init__(
|
| 25 |
self,
|
| 26 |
duplicate_behavior: DuplicateBehavior | None = None,
|
| 27 |
+
mask_error_details: bool | None = None,
|
| 28 |
):
|
| 29 |
self._tools: dict[str, Tool] = {}
|
| 30 |
+
self.mask_error_details = mask_error_details or settings.mask_error_details
|
| 31 |
|
| 32 |
# Default to "warn" if None is provided
|
| 33 |
if duplicate_behavior is None:
|
src/fastmcp/utilities/exceptions.py
CHANGED
|
@@ -43,7 +43,7 @@ def get_catch_handlers() -> Mapping[
|
|
| 43 |
type[BaseException] | Iterable[type[BaseException]],
|
| 44 |
Callable[[BaseExceptionGroup[Any]], Any],
|
| 45 |
]:
|
| 46 |
-
if fastmcp.settings.
|
| 47 |
return _catch_handlers
|
| 48 |
else:
|
| 49 |
return {}
|
|
|
|
| 43 |
type[BaseException] | Iterable[type[BaseException]],
|
| 44 |
Callable[[BaseExceptionGroup[Any]], Any],
|
| 45 |
]:
|
| 46 |
+
if fastmcp.settings.client_raise_first_exceptiongroup_error:
|
| 47 |
return _catch_handlers
|
| 48 |
else:
|
| 49 |
return {}
|
src/fastmcp/utilities/tests.py
CHANGED
|
@@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Any, Literal
|
|
| 10 |
|
| 11 |
import uvicorn
|
| 12 |
|
| 13 |
-
from fastmcp
|
| 14 |
from fastmcp.utilities.http import find_available_port
|
| 15 |
|
| 16 |
if TYPE_CHECKING:
|
|
@@ -32,8 +32,8 @@ def temporary_settings(**kwargs: Any):
|
|
| 32 |
from fastmcp.utilities.tests import temporary_settings
|
| 33 |
|
| 34 |
with temporary_settings(log_level='DEBUG'):
|
| 35 |
-
assert fastmcp.settings.
|
| 36 |
-
assert fastmcp.settings.
|
| 37 |
```
|
| 38 |
"""
|
| 39 |
old_settings = copy.deepcopy(settings.model_dump())
|
|
|
|
| 10 |
|
| 11 |
import uvicorn
|
| 12 |
|
| 13 |
+
from fastmcp import settings
|
| 14 |
from fastmcp.utilities.http import find_available_port
|
| 15 |
|
| 16 |
if TYPE_CHECKING:
|
|
|
|
| 32 |
from fastmcp.utilities.tests import temporary_settings
|
| 33 |
|
| 34 |
with temporary_settings(log_level='DEBUG'):
|
| 35 |
+
assert fastmcp.settings.log_level == 'DEBUG'
|
| 36 |
+
assert fastmcp.settings.log_level == 'INFO'
|
| 37 |
```
|
| 38 |
"""
|
| 39 |
old_settings = copy.deepcopy(settings.model_dump())
|
tests/auth/providers/test_bearer_env.py
CHANGED
|
@@ -4,16 +4,19 @@ from pydantic import AnyHttpUrl, ValidationError
|
|
| 4 |
from fastmcp import FastMCP
|
| 5 |
from fastmcp.server.auth.providers.bearer import BearerAuthProvider
|
| 6 |
from fastmcp.server.auth.providers.bearer_env import EnvBearerAuthProvider
|
|
|
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
def test_load_bearer_env_from_env_var(monkeypatch):
|
| 10 |
mcp = FastMCP()
|
| 11 |
assert mcp.auth is None
|
| 12 |
|
| 13 |
-
monkeypatch.setenv("
|
| 14 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
|
| 15 |
|
| 16 |
-
|
|
|
|
| 17 |
assert isinstance(mcp_with_auth.auth, EnvBearerAuthProvider)
|
| 18 |
|
| 19 |
|
|
@@ -21,16 +24,17 @@ def test_load_bearer_env_from_env_var_requires_public_key_or_jwks_uri(monkeypatc
|
|
| 21 |
mcp = FastMCP()
|
| 22 |
assert mcp.auth is None
|
| 23 |
|
| 24 |
-
monkeypatch.setenv("
|
| 25 |
|
| 26 |
-
with
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
|
|
|
| 30 |
|
| 31 |
|
| 32 |
def test_configure_bearer_env_from_env_var(monkeypatch):
|
| 33 |
-
monkeypatch.setenv("
|
| 34 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
|
| 35 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_ISSUER", "http://test-issuer")
|
| 36 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_AUDIENCE", "test-audience")
|
|
@@ -38,7 +42,8 @@ def test_configure_bearer_env_from_env_var(monkeypatch):
|
|
| 38 |
"FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", '["test-scope1", "test-scope2"]'
|
| 39 |
)
|
| 40 |
|
| 41 |
-
|
|
|
|
| 42 |
assert isinstance(mcp.auth, EnvBearerAuthProvider)
|
| 43 |
assert mcp.auth.public_key == "test-public-key"
|
| 44 |
assert mcp.auth.issuer_url == AnyHttpUrl("http://test-issuer")
|
|
@@ -47,36 +52,40 @@ def test_configure_bearer_env_from_env_var(monkeypatch):
|
|
| 47 |
|
| 48 |
|
| 49 |
def test_list_of_scopes_must_be_a_list(monkeypatch):
|
| 50 |
-
monkeypatch.setenv("
|
| 51 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", "test-scope1")
|
| 52 |
|
| 53 |
-
with
|
| 54 |
-
|
|
|
|
| 55 |
|
| 56 |
|
| 57 |
def test_configure_bearer_env_jwks_uri_from_env_var(monkeypatch):
|
| 58 |
-
monkeypatch.setenv("
|
| 59 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
|
| 60 |
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
|
|
|
| 64 |
|
| 65 |
|
| 66 |
def test_configure_bearer_env_public_key_and_jwks_uri_error(monkeypatch):
|
| 67 |
-
monkeypatch.setenv("
|
| 68 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
|
| 69 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
|
| 70 |
|
| 71 |
-
with
|
| 72 |
-
|
|
|
|
| 73 |
|
| 74 |
|
| 75 |
def test_provided_auth_takes_precedence_over_env_vars(monkeypatch):
|
| 76 |
-
monkeypatch.setenv("
|
| 77 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
|
| 78 |
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
|
|
|
|
|
| 4 |
from fastmcp import FastMCP
|
| 5 |
from fastmcp.server.auth.providers.bearer import BearerAuthProvider
|
| 6 |
from fastmcp.server.auth.providers.bearer_env import EnvBearerAuthProvider
|
| 7 |
+
from fastmcp.settings import Settings
|
| 8 |
+
from fastmcp.utilities.tests import temporary_settings
|
| 9 |
|
| 10 |
|
| 11 |
def test_load_bearer_env_from_env_var(monkeypatch):
|
| 12 |
mcp = FastMCP()
|
| 13 |
assert mcp.auth is None
|
| 14 |
|
| 15 |
+
monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "bearer_env")
|
| 16 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
|
| 17 |
|
| 18 |
+
with temporary_settings(**Settings().model_dump()):
|
| 19 |
+
mcp_with_auth = FastMCP()
|
| 20 |
assert isinstance(mcp_with_auth.auth, EnvBearerAuthProvider)
|
| 21 |
|
| 22 |
|
|
|
|
| 24 |
mcp = FastMCP()
|
| 25 |
assert mcp.auth is None
|
| 26 |
|
| 27 |
+
monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "bearer_env")
|
| 28 |
|
| 29 |
+
with temporary_settings(**Settings().model_dump()):
|
| 30 |
+
with pytest.raises(
|
| 31 |
+
ValueError, match="Either public_key or jwks_uri must be provided"
|
| 32 |
+
):
|
| 33 |
+
FastMCP()
|
| 34 |
|
| 35 |
|
| 36 |
def test_configure_bearer_env_from_env_var(monkeypatch):
|
| 37 |
+
monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "bearer_env")
|
| 38 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
|
| 39 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_ISSUER", "http://test-issuer")
|
| 40 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_AUDIENCE", "test-audience")
|
|
|
|
| 42 |
"FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", '["test-scope1", "test-scope2"]'
|
| 43 |
)
|
| 44 |
|
| 45 |
+
with temporary_settings(**Settings().model_dump()):
|
| 46 |
+
mcp = FastMCP()
|
| 47 |
assert isinstance(mcp.auth, EnvBearerAuthProvider)
|
| 48 |
assert mcp.auth.public_key == "test-public-key"
|
| 49 |
assert mcp.auth.issuer_url == AnyHttpUrl("http://test-issuer")
|
|
|
|
| 52 |
|
| 53 |
|
| 54 |
def test_list_of_scopes_must_be_a_list(monkeypatch):
|
| 55 |
+
monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "bearer_env")
|
| 56 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_REQUIRED_SCOPES", "test-scope1")
|
| 57 |
|
| 58 |
+
with temporary_settings(**Settings().model_dump()):
|
| 59 |
+
with pytest.raises(ValidationError, match="Input should be a valid list"):
|
| 60 |
+
FastMCP()
|
| 61 |
|
| 62 |
|
| 63 |
def test_configure_bearer_env_jwks_uri_from_env_var(monkeypatch):
|
| 64 |
+
monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "bearer_env")
|
| 65 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
|
| 66 |
|
| 67 |
+
with temporary_settings(**Settings().model_dump()):
|
| 68 |
+
mcp = FastMCP()
|
| 69 |
+
assert isinstance(mcp.auth, EnvBearerAuthProvider)
|
| 70 |
+
assert mcp.auth.jwks_uri == "test-jwks-uri"
|
| 71 |
|
| 72 |
|
| 73 |
def test_configure_bearer_env_public_key_and_jwks_uri_error(monkeypatch):
|
| 74 |
+
monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "bearer_env")
|
| 75 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
|
| 76 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_JWKS_URI", "test-jwks-uri")
|
| 77 |
|
| 78 |
+
with temporary_settings(**Settings().model_dump()):
|
| 79 |
+
with pytest.raises(ValueError, match="Provide either public_key or jwks_uri"):
|
| 80 |
+
FastMCP()
|
| 81 |
|
| 82 |
|
| 83 |
def test_provided_auth_takes_precedence_over_env_vars(monkeypatch):
|
| 84 |
+
monkeypatch.setenv("FASTMCP_DEFAULT_AUTH_PROVIDER", "bearer_env")
|
| 85 |
monkeypatch.setenv("FASTMCP_AUTH_BEARER_PUBLIC_KEY", "test-public-key")
|
| 86 |
|
| 87 |
+
with temporary_settings(**Settings().model_dump()):
|
| 88 |
+
mcp = FastMCP(auth=BearerAuthProvider(public_key="test-public-key-2"))
|
| 89 |
+
assert isinstance(mcp.auth, BearerAuthProvider)
|
| 90 |
+
assert not isinstance(mcp.auth, EnvBearerAuthProvider)
|
| 91 |
+
assert mcp.auth.public_key == "test-public-key-2"
|
tests/deprecated/test_settings.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import warnings
|
| 3 |
+
from unittest.mock import patch
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
|
| 7 |
+
from fastmcp import FastMCP
|
| 8 |
+
from fastmcp.settings import Settings
|
| 9 |
+
|
| 10 |
+
# reset deprecation warnings for this module
|
| 11 |
+
pytestmark = pytest.mark.filterwarnings("default::DeprecationWarning")
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class TestDeprecatedServerInitKwargs:
|
| 15 |
+
"""Test deprecated server initialization keyword arguments."""
|
| 16 |
+
|
| 17 |
+
def test_log_level_deprecation_warning(self):
|
| 18 |
+
"""Test that log_level raises a deprecation warning."""
|
| 19 |
+
with pytest.warns(
|
| 20 |
+
DeprecationWarning,
|
| 21 |
+
match=r"Providing `log_level` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
|
| 22 |
+
):
|
| 23 |
+
server = FastMCP("TestServer", log_level="DEBUG")
|
| 24 |
+
|
| 25 |
+
# Verify the setting is still applied
|
| 26 |
+
assert server._deprecated_settings.log_level == "DEBUG"
|
| 27 |
+
|
| 28 |
+
def test_debug_deprecation_warning(self):
|
| 29 |
+
"""Test that debug raises a deprecation warning."""
|
| 30 |
+
with pytest.warns(
|
| 31 |
+
DeprecationWarning,
|
| 32 |
+
match=r"Providing `debug` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
|
| 33 |
+
):
|
| 34 |
+
server = FastMCP("TestServer", debug=True)
|
| 35 |
+
|
| 36 |
+
# Verify the setting is still applied
|
| 37 |
+
assert server._deprecated_settings.debug is True
|
| 38 |
+
|
| 39 |
+
def test_host_deprecation_warning(self):
|
| 40 |
+
"""Test that host raises a deprecation warning."""
|
| 41 |
+
with pytest.warns(
|
| 42 |
+
DeprecationWarning,
|
| 43 |
+
match=r"Providing `host` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
|
| 44 |
+
):
|
| 45 |
+
server = FastMCP("TestServer", host="0.0.0.0")
|
| 46 |
+
|
| 47 |
+
# Verify the setting is still applied
|
| 48 |
+
assert server._deprecated_settings.host == "0.0.0.0"
|
| 49 |
+
|
| 50 |
+
def test_port_deprecation_warning(self):
|
| 51 |
+
"""Test that port raises a deprecation warning."""
|
| 52 |
+
with pytest.warns(
|
| 53 |
+
DeprecationWarning,
|
| 54 |
+
match=r"Providing `port` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
|
| 55 |
+
):
|
| 56 |
+
server = FastMCP("TestServer", port=8080)
|
| 57 |
+
|
| 58 |
+
# Verify the setting is still applied
|
| 59 |
+
assert server._deprecated_settings.port == 8080
|
| 60 |
+
|
| 61 |
+
def test_sse_path_deprecation_warning(self):
|
| 62 |
+
"""Test that sse_path raises a deprecation warning."""
|
| 63 |
+
with pytest.warns(
|
| 64 |
+
DeprecationWarning,
|
| 65 |
+
match=r"Providing `sse_path` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
|
| 66 |
+
):
|
| 67 |
+
server = FastMCP("TestServer", sse_path="/custom-sse")
|
| 68 |
+
|
| 69 |
+
# Verify the setting is still applied
|
| 70 |
+
assert server._deprecated_settings.sse_path == "/custom-sse"
|
| 71 |
+
|
| 72 |
+
def test_message_path_deprecation_warning(self):
|
| 73 |
+
"""Test that message_path raises a deprecation warning."""
|
| 74 |
+
with pytest.warns(
|
| 75 |
+
DeprecationWarning,
|
| 76 |
+
match=r"Providing `message_path` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
|
| 77 |
+
):
|
| 78 |
+
server = FastMCP("TestServer", message_path="/custom-message")
|
| 79 |
+
|
| 80 |
+
# Verify the setting is still applied
|
| 81 |
+
assert server._deprecated_settings.message_path == "/custom-message"
|
| 82 |
+
|
| 83 |
+
def test_streamable_http_path_deprecation_warning(self):
|
| 84 |
+
"""Test that streamable_http_path raises a deprecation warning."""
|
| 85 |
+
with pytest.warns(
|
| 86 |
+
DeprecationWarning,
|
| 87 |
+
match=r"Providing `streamable_http_path` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
|
| 88 |
+
):
|
| 89 |
+
server = FastMCP("TestServer", streamable_http_path="/custom-http")
|
| 90 |
+
|
| 91 |
+
# Verify the setting is still applied
|
| 92 |
+
assert server._deprecated_settings.streamable_http_path == "/custom-http"
|
| 93 |
+
|
| 94 |
+
def test_json_response_deprecation_warning(self):
|
| 95 |
+
"""Test that json_response raises a deprecation warning."""
|
| 96 |
+
with pytest.warns(
|
| 97 |
+
DeprecationWarning,
|
| 98 |
+
match=r"Providing `json_response` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
|
| 99 |
+
):
|
| 100 |
+
server = FastMCP("TestServer", json_response=True)
|
| 101 |
+
|
| 102 |
+
# Verify the setting is still applied
|
| 103 |
+
assert server._deprecated_settings.json_response is True
|
| 104 |
+
|
| 105 |
+
def test_stateless_http_deprecation_warning(self):
|
| 106 |
+
"""Test that stateless_http raises a deprecation warning."""
|
| 107 |
+
with pytest.warns(
|
| 108 |
+
DeprecationWarning,
|
| 109 |
+
match=r"Providing `stateless_http` when creating a server is deprecated\. Provide it when calling `run` or as a global setting instead\.",
|
| 110 |
+
):
|
| 111 |
+
server = FastMCP("TestServer", stateless_http=True)
|
| 112 |
+
|
| 113 |
+
# Verify the setting is still applied
|
| 114 |
+
assert server._deprecated_settings.stateless_http is True
|
| 115 |
+
|
| 116 |
+
def test_multiple_deprecated_kwargs_warnings(self):
|
| 117 |
+
"""Test that multiple deprecated kwargs each raise their own warning."""
|
| 118 |
+
with warnings.catch_warnings(record=True) as recorded_warnings:
|
| 119 |
+
warnings.simplefilter("always")
|
| 120 |
+
server = FastMCP(
|
| 121 |
+
"TestServer",
|
| 122 |
+
log_level="INFO",
|
| 123 |
+
debug=False,
|
| 124 |
+
host="127.0.0.1",
|
| 125 |
+
port=9999,
|
| 126 |
+
sse_path="/sse",
|
| 127 |
+
message_path="/msg",
|
| 128 |
+
streamable_http_path="/http",
|
| 129 |
+
json_response=False,
|
| 130 |
+
stateless_http=False,
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
# Should have 9 deprecation warnings (one for each deprecated parameter)
|
| 134 |
+
deprecation_warnings = [
|
| 135 |
+
w for w in recorded_warnings if issubclass(w.category, DeprecationWarning)
|
| 136 |
+
]
|
| 137 |
+
assert len(deprecation_warnings) == 9
|
| 138 |
+
|
| 139 |
+
# Verify all expected parameters are mentioned in warnings
|
| 140 |
+
expected_params = {
|
| 141 |
+
"log_level",
|
| 142 |
+
"debug",
|
| 143 |
+
"host",
|
| 144 |
+
"port",
|
| 145 |
+
"sse_path",
|
| 146 |
+
"message_path",
|
| 147 |
+
"streamable_http_path",
|
| 148 |
+
"json_response",
|
| 149 |
+
"stateless_http",
|
| 150 |
+
}
|
| 151 |
+
mentioned_params = set()
|
| 152 |
+
for warning in deprecation_warnings:
|
| 153 |
+
message = str(warning.message)
|
| 154 |
+
for param in expected_params:
|
| 155 |
+
if f"Providing `{param}`" in message:
|
| 156 |
+
mentioned_params.add(param)
|
| 157 |
+
|
| 158 |
+
assert mentioned_params == expected_params
|
| 159 |
+
|
| 160 |
+
# Verify all settings are still applied
|
| 161 |
+
assert server._deprecated_settings.log_level == "INFO"
|
| 162 |
+
assert server._deprecated_settings.debug is False
|
| 163 |
+
assert server._deprecated_settings.host == "127.0.0.1"
|
| 164 |
+
assert server._deprecated_settings.port == 9999
|
| 165 |
+
assert server._deprecated_settings.sse_path == "/sse"
|
| 166 |
+
assert server._deprecated_settings.message_path == "/msg"
|
| 167 |
+
assert server._deprecated_settings.streamable_http_path == "/http"
|
| 168 |
+
assert server._deprecated_settings.json_response is False
|
| 169 |
+
assert server._deprecated_settings.stateless_http is False
|
| 170 |
+
|
| 171 |
+
def test_non_deprecated_kwargs_no_warnings(self):
|
| 172 |
+
"""Test that non-deprecated kwargs don't raise warnings."""
|
| 173 |
+
with warnings.catch_warnings(record=True) as recorded_warnings:
|
| 174 |
+
warnings.simplefilter("always")
|
| 175 |
+
server = FastMCP(
|
| 176 |
+
name="TestServer",
|
| 177 |
+
instructions="Test instructions",
|
| 178 |
+
tags={"test", "server"},
|
| 179 |
+
cache_expiration_seconds=60.0,
|
| 180 |
+
on_duplicate_tools="warn",
|
| 181 |
+
on_duplicate_resources="error",
|
| 182 |
+
on_duplicate_prompts="replace",
|
| 183 |
+
resource_prefix_format="path",
|
| 184 |
+
mask_error_details=True,
|
| 185 |
+
)
|
| 186 |
+
|
| 187 |
+
# Should have no deprecation warnings
|
| 188 |
+
deprecation_warnings = [
|
| 189 |
+
w for w in recorded_warnings if issubclass(w.category, DeprecationWarning)
|
| 190 |
+
]
|
| 191 |
+
assert len(deprecation_warnings) == 0
|
| 192 |
+
|
| 193 |
+
# Verify server was created successfully
|
| 194 |
+
assert server.name == "TestServer"
|
| 195 |
+
assert server.instructions == "Test instructions"
|
| 196 |
+
assert server.tags == {"test", "server"}
|
| 197 |
+
|
| 198 |
+
def test_none_values_no_warnings(self):
|
| 199 |
+
"""Test that None values for deprecated kwargs don't raise warnings."""
|
| 200 |
+
with warnings.catch_warnings(record=True) as recorded_warnings:
|
| 201 |
+
warnings.simplefilter("always")
|
| 202 |
+
FastMCP(
|
| 203 |
+
"TestServer",
|
| 204 |
+
log_level=None,
|
| 205 |
+
debug=None,
|
| 206 |
+
host=None,
|
| 207 |
+
port=None,
|
| 208 |
+
sse_path=None,
|
| 209 |
+
message_path=None,
|
| 210 |
+
streamable_http_path=None,
|
| 211 |
+
json_response=None,
|
| 212 |
+
stateless_http=None,
|
| 213 |
+
)
|
| 214 |
+
|
| 215 |
+
# Should have no deprecation warnings for None values
|
| 216 |
+
deprecation_warnings = [
|
| 217 |
+
w for w in recorded_warnings if issubclass(w.category, DeprecationWarning)
|
| 218 |
+
]
|
| 219 |
+
assert len(deprecation_warnings) == 0
|
| 220 |
+
|
| 221 |
+
def test_deprecated_settings_inheritance_from_global(self):
|
| 222 |
+
"""Test that deprecated settings inherit from global settings when not provided."""
|
| 223 |
+
# Mock fastmcp.settings to test inheritance
|
| 224 |
+
with patch("fastmcp.settings") as mock_settings:
|
| 225 |
+
mock_settings.model_dump.return_value = {
|
| 226 |
+
"log_level": "WARNING",
|
| 227 |
+
"debug": True,
|
| 228 |
+
"host": "0.0.0.0",
|
| 229 |
+
"port": 3000,
|
| 230 |
+
"sse_path": "/events",
|
| 231 |
+
"message_path": "/messages",
|
| 232 |
+
"streamable_http_path": "/stream",
|
| 233 |
+
"json_response": True,
|
| 234 |
+
"stateless_http": True,
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
server = FastMCP("TestServer")
|
| 238 |
+
|
| 239 |
+
# Verify settings are inherited from global settings
|
| 240 |
+
assert server._deprecated_settings.log_level == "WARNING"
|
| 241 |
+
assert server._deprecated_settings.debug is True
|
| 242 |
+
assert server._deprecated_settings.host == "0.0.0.0"
|
| 243 |
+
assert server._deprecated_settings.port == 3000
|
| 244 |
+
assert server._deprecated_settings.sse_path == "/events"
|
| 245 |
+
assert server._deprecated_settings.message_path == "/messages"
|
| 246 |
+
assert server._deprecated_settings.streamable_http_path == "/stream"
|
| 247 |
+
assert server._deprecated_settings.json_response is True
|
| 248 |
+
assert server._deprecated_settings.stateless_http is True
|
| 249 |
+
|
| 250 |
+
def test_deprecated_settings_override_global(self):
|
| 251 |
+
"""Test that deprecated settings override global settings when provided."""
|
| 252 |
+
# Mock fastmcp.settings to test override behavior
|
| 253 |
+
with patch("fastmcp.settings") as mock_settings:
|
| 254 |
+
mock_settings.model_dump.return_value = {
|
| 255 |
+
"log_level": "WARNING",
|
| 256 |
+
"debug": True,
|
| 257 |
+
"host": "0.0.0.0",
|
| 258 |
+
"port": 3000,
|
| 259 |
+
"sse_path": "/events",
|
| 260 |
+
"message_path": "/messages",
|
| 261 |
+
"streamable_http_path": "/stream",
|
| 262 |
+
"json_response": True,
|
| 263 |
+
"stateless_http": True,
|
| 264 |
+
}
|
| 265 |
+
|
| 266 |
+
with warnings.catch_warnings():
|
| 267 |
+
warnings.simplefilter("ignore") # Ignore warnings for this test
|
| 268 |
+
server = FastMCP(
|
| 269 |
+
"TestServer",
|
| 270 |
+
log_level="ERROR",
|
| 271 |
+
debug=False,
|
| 272 |
+
host="127.0.0.1",
|
| 273 |
+
port=8080,
|
| 274 |
+
)
|
| 275 |
+
|
| 276 |
+
# Verify provided settings override global settings
|
| 277 |
+
assert server._deprecated_settings.log_level == "ERROR"
|
| 278 |
+
assert server._deprecated_settings.debug is False
|
| 279 |
+
assert server._deprecated_settings.host == "127.0.0.1"
|
| 280 |
+
assert server._deprecated_settings.port == 8080
|
| 281 |
+
# Non-overridden settings should still come from global
|
| 282 |
+
assert server._deprecated_settings.sse_path == "/events"
|
| 283 |
+
assert server._deprecated_settings.message_path == "/messages"
|
| 284 |
+
assert server._deprecated_settings.streamable_http_path == "/stream"
|
| 285 |
+
assert server._deprecated_settings.json_response is True
|
| 286 |
+
assert server._deprecated_settings.stateless_http is True
|
| 287 |
+
|
| 288 |
+
def test_stacklevel_points_to_constructor_call(self):
|
| 289 |
+
"""Test that deprecation warnings point to the FastMCP constructor call."""
|
| 290 |
+
with warnings.catch_warnings(record=True) as recorded_warnings:
|
| 291 |
+
warnings.simplefilter("always")
|
| 292 |
+
|
| 293 |
+
FastMCP("TestServer", log_level="DEBUG")
|
| 294 |
+
|
| 295 |
+
# Should have exactly one deprecation warning
|
| 296 |
+
deprecation_warnings = [
|
| 297 |
+
w for w in recorded_warnings if issubclass(w.category, DeprecationWarning)
|
| 298 |
+
]
|
| 299 |
+
assert len(deprecation_warnings) == 1
|
| 300 |
+
|
| 301 |
+
# The warning should point to the server.py file where FastMCP.__init__ is called
|
| 302 |
+
# This verifies the stacklevel is working as intended (pointing to constructor)
|
| 303 |
+
warning = deprecation_warnings[0]
|
| 304 |
+
assert "server.py" in warning.filename
|
| 305 |
+
|
| 306 |
+
|
| 307 |
+
class TestDeprecatedEnvironmentVariables:
|
| 308 |
+
"""Test deprecated environment variable prefixes."""
|
| 309 |
+
|
| 310 |
+
def test_fastmcp_server_env_var_deprecation_warning(self):
|
| 311 |
+
"""Test that FASTMCP_SERVER_ environment variables emit deprecation warnings."""
|
| 312 |
+
env_var_name = "FASTMCP_SERVER_HOST"
|
| 313 |
+
original_value = os.environ.get(env_var_name)
|
| 314 |
+
|
| 315 |
+
try:
|
| 316 |
+
os.environ[env_var_name] = "192.168.1.1"
|
| 317 |
+
|
| 318 |
+
with pytest.warns(
|
| 319 |
+
DeprecationWarning,
|
| 320 |
+
match=r"Using `FASTMCP_SERVER_` environment variables is deprecated\. Use `FASTMCP_` instead\.",
|
| 321 |
+
):
|
| 322 |
+
settings = Settings()
|
| 323 |
+
|
| 324 |
+
# Verify the setting is still applied
|
| 325 |
+
assert settings.host == "192.168.1.1"
|
| 326 |
+
|
| 327 |
+
finally:
|
| 328 |
+
# Clean up environment variable
|
| 329 |
+
if original_value is not None:
|
| 330 |
+
os.environ[env_var_name] = original_value
|
| 331 |
+
else:
|
| 332 |
+
os.environ.pop(env_var_name, None)
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
class TestDeprecatedSettingsProperty:
|
| 336 |
+
"""Test deprecated settings property access."""
|
| 337 |
+
|
| 338 |
+
def test_settings_property_deprecation_warning(self):
|
| 339 |
+
"""Test that accessing fastmcp.settings.settings raises a deprecation warning."""
|
| 340 |
+
from fastmcp import settings
|
| 341 |
+
|
| 342 |
+
with pytest.warns(
|
| 343 |
+
DeprecationWarning,
|
| 344 |
+
match=r"Using fastmcp\.settings\.settings is deprecated\. Use fastmcp\.settings instead\.",
|
| 345 |
+
):
|
| 346 |
+
# Access the deprecated property
|
| 347 |
+
deprecated_settings = settings.settings
|
| 348 |
+
|
| 349 |
+
# Verify it still returns the same settings object
|
| 350 |
+
assert deprecated_settings is settings
|
| 351 |
+
assert isinstance(deprecated_settings, Settings)
|
tests/utilities/test_tests.py
CHANGED
|
@@ -4,7 +4,7 @@ from fastmcp.utilities.tests import temporary_settings
|
|
| 4 |
|
| 5 |
class TestTemporarySettings:
|
| 6 |
def test_temporary_settings(self):
|
| 7 |
-
assert fastmcp.settings.
|
| 8 |
with temporary_settings(log_level="ERROR"):
|
| 9 |
-
assert fastmcp.settings.
|
| 10 |
-
assert fastmcp.settings.
|
|
|
|
| 4 |
|
| 5 |
class TestTemporarySettings:
|
| 6 |
def test_temporary_settings(self):
|
| 7 |
+
assert fastmcp.settings.log_level == "DEBUG"
|
| 8 |
with temporary_settings(log_level="ERROR"):
|
| 9 |
+
assert fastmcp.settings.log_level == "ERROR"
|
| 10 |
+
assert fastmcp.settings.log_level == "DEBUG"
|