nate nowack commited on
Commit
a7c6f7e
·
unverified ·
2 Parent(s): 7b7ee7e731e83f

Merge pull request #504 from jlowin/uvicorn-config

Browse files
src/fastmcp/server/server.py CHANGED
@@ -760,24 +760,24 @@ class FastMCP(Generic[LifespanResultT]):
760
  path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
761
  uvicorn_config: Additional configuration for the Uvicorn server
762
  """
763
- uvicorn_config = uvicorn_config or {}
764
- uvicorn_config.setdefault("timeout_graceful_shutdown", 0)
765
- # lifespan is required for streamable http
766
- uvicorn_config["lifespan"] = "on"
767
-
768
  host = host or self.settings.host
769
  port = port or self.settings.port
770
- log_level = log_level or self.settings.log_level.lower()
771
 
772
  app = self.http_app(path=path, transport=transport, middleware=middleware)
773
 
774
- config = uvicorn.Config(
775
- app,
776
- host=host,
777
- port=port,
778
- log_level=log_level,
779
- **uvicorn_config,
780
- )
 
 
 
 
 
781
  server = uvicorn.Server(config)
782
  path = app.state.path.lstrip("/") # type: ignore
783
  logger.info(
@@ -1040,9 +1040,6 @@ class FastMCP(Generic[LifespanResultT]):
1040
  - The prompts are imported with prefixed names using the
1041
  prompt_separator Example: If server has a prompt named
1042
  "weather_prompt", it will be available as "weather_weather_prompt"
1043
- - The mounted server's lifespan will be executed when the parent
1044
- server's lifespan runs, ensuring that any setup needed by the mounted
1045
- server is performed
1046
 
1047
  Args:
1048
  prefix: The prefix to use for the mounted server server: The FastMCP
 
760
  path: Path for the endpoint (defaults to settings.streamable_http_path or settings.sse_path)
761
  uvicorn_config: Additional configuration for the Uvicorn server
762
  """
 
 
 
 
 
763
  host = host or self.settings.host
764
  port = port or self.settings.port
765
+ default_log_level_to_use = log_level or self.settings.log_level.lower()
766
 
767
  app = self.http_app(path=path, transport=transport, middleware=middleware)
768
 
769
+ _uvicorn_config_from_user = uvicorn_config or {}
770
+
771
+ config_kwargs: dict[str, Any] = {
772
+ "timeout_graceful_shutdown": 0,
773
+ "lifespan": "on",
774
+ }
775
+ config_kwargs.update(_uvicorn_config_from_user)
776
+
777
+ if "log_config" not in config_kwargs and "log_level" not in config_kwargs:
778
+ config_kwargs["log_level"] = default_log_level_to_use
779
+
780
+ config = uvicorn.Config(app, host=host, port=port, **config_kwargs)
781
  server = uvicorn.Server(config)
782
  path = app.state.path.lstrip("/") # type: ignore
783
  logger.info(
 
1040
  - The prompts are imported with prefixed names using the
1041
  prompt_separator Example: If server has a prompt named
1042
  "weather_prompt", it will be available as "weather_weather_prompt"
 
 
 
1043
 
1044
  Args:
1045
  prefix: The prefix to use for the mounted server server: The FastMCP
tests/server/test_logging.py ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import logging
3
+ from unittest.mock import AsyncMock, Mock, patch
4
+
5
+ import pytest
6
+
7
+ from fastmcp.server.server import FastMCP
8
+
9
+
10
+ class CustomLogFormatterForTest(logging.Formatter):
11
+ def format(self, record: logging.LogRecord) -> str:
12
+ return f"TEST_FORMAT::{record.levelname}::{record.name}::{record.getMessage()}"
13
+
14
+
15
+ @pytest.fixture
16
+ def mcp_server() -> FastMCP:
17
+ return FastMCP(name="TestLogServer")
18
+
19
+
20
+ @patch("fastmcp.server.server.uvicorn.Server")
21
+ @patch("fastmcp.server.server.uvicorn.Config")
22
+ async def test_uvicorn_logging_default_level(
23
+ mock_uvicorn_config_constructor: Mock,
24
+ mock_uvicorn_server_constructor: Mock,
25
+ mcp_server: FastMCP,
26
+ ):
27
+ """Tests that FastMCP passes log_level to uvicorn.Config if no log_config is given."""
28
+ mock_server_instance = AsyncMock()
29
+ mock_uvicorn_server_constructor.return_value = mock_server_instance
30
+ serve_finished_event = asyncio.Event()
31
+ mock_server_instance.serve.side_effect = serve_finished_event.wait
32
+
33
+ test_log_level = "warning"
34
+
35
+ server_task = asyncio.create_task(
36
+ mcp_server.run_http_async(log_level=test_log_level, port=8003)
37
+ )
38
+ await asyncio.sleep(0.01)
39
+
40
+ mock_uvicorn_config_constructor.assert_called_once()
41
+ _, kwargs_config = mock_uvicorn_config_constructor.call_args
42
+
43
+ assert kwargs_config.get("log_level") == test_log_level.lower()
44
+ assert "log_config" not in kwargs_config
45
+
46
+ mock_uvicorn_server_constructor.assert_called_once_with(
47
+ mock_uvicorn_config_constructor.return_value
48
+ )
49
+ mock_server_instance.serve.assert_awaited_once()
50
+
51
+ server_task.cancel()
52
+ with pytest.raises(asyncio.CancelledError):
53
+ await server_task
54
+
55
+
56
+ @patch("fastmcp.server.server.uvicorn.Server")
57
+ @patch("fastmcp.server.server.uvicorn.Config")
58
+ async def test_uvicorn_logging_with_custom_log_config(
59
+ mock_uvicorn_config_constructor: Mock,
60
+ mock_uvicorn_server_constructor: Mock,
61
+ mcp_server: FastMCP,
62
+ ):
63
+ """Tests that FastMCP passes log_config to uvicorn.Config and not log_level."""
64
+ mock_server_instance = AsyncMock()
65
+ mock_uvicorn_server_constructor.return_value = mock_server_instance
66
+ serve_finished_event = asyncio.Event()
67
+ mock_server_instance.serve.side_effect = serve_finished_event.wait
68
+
69
+ sample_log_config = {
70
+ "version": 1,
71
+ "disable_existing_loggers": False,
72
+ "formatters": {
73
+ "test_formatter": {
74
+ "()": "tests.server.test_logging.CustomLogFormatterForTest"
75
+ }
76
+ },
77
+ "handlers": {
78
+ "test_handler": {
79
+ "formatter": "test_formatter",
80
+ "class": "logging.StreamHandler",
81
+ "stream": "ext://sys.stdout",
82
+ }
83
+ },
84
+ "loggers": {
85
+ "uvicorn.error": {
86
+ "handlers": ["test_handler"],
87
+ "level": "INFO",
88
+ "propagate": False,
89
+ }
90
+ },
91
+ }
92
+
93
+ server_task = asyncio.create_task(
94
+ mcp_server.run_http_async(
95
+ uvicorn_config={"log_config": sample_log_config}, port=8004
96
+ )
97
+ )
98
+ await asyncio.sleep(0.01)
99
+
100
+ mock_uvicorn_config_constructor.assert_called_once()
101
+ _, kwargs_config = mock_uvicorn_config_constructor.call_args
102
+
103
+ assert kwargs_config.get("log_config") == sample_log_config
104
+ assert "log_level" not in kwargs_config
105
+
106
+ mock_uvicorn_server_constructor.assert_called_once_with(
107
+ mock_uvicorn_config_constructor.return_value
108
+ )
109
+ mock_server_instance.serve.assert_awaited_once()
110
+
111
+ server_task.cancel()
112
+ with pytest.raises(asyncio.CancelledError):
113
+ await server_task
114
+
115
+
116
+ @patch("fastmcp.server.server.uvicorn.Server")
117
+ @patch("fastmcp.server.server.uvicorn.Config")
118
+ async def test_uvicorn_logging_custom_log_config_overrides_log_level_param(
119
+ mock_uvicorn_config_constructor: Mock,
120
+ mock_uvicorn_server_constructor: Mock,
121
+ mcp_server: FastMCP,
122
+ ):
123
+ """Tests log_config precedence if log_level is also passed to run_http_async."""
124
+ mock_server_instance = AsyncMock()
125
+ mock_uvicorn_server_constructor.return_value = mock_server_instance
126
+ serve_finished_event = asyncio.Event()
127
+ mock_server_instance.serve.side_effect = serve_finished_event.wait
128
+
129
+ sample_log_config = {
130
+ "version": 1,
131
+ "disable_existing_loggers": False,
132
+ "formatters": {
133
+ "test_formatter": {
134
+ "()": "tests.server.test_logging.CustomLogFormatterForTest"
135
+ }
136
+ },
137
+ "handlers": {
138
+ "test_handler": {
139
+ "formatter": "test_formatter",
140
+ "class": "logging.StreamHandler",
141
+ "stream": "ext://sys.stdout",
142
+ }
143
+ },
144
+ "loggers": {
145
+ "uvicorn.error": {
146
+ "handlers": ["test_handler"],
147
+ "level": "INFO",
148
+ "propagate": False,
149
+ }
150
+ },
151
+ }
152
+ explicit_log_level = "debug"
153
+
154
+ server_task = asyncio.create_task(
155
+ mcp_server.run_http_async(
156
+ log_level=explicit_log_level,
157
+ uvicorn_config={"log_config": sample_log_config},
158
+ port=8005,
159
+ )
160
+ )
161
+ await asyncio.sleep(0.01)
162
+
163
+ mock_uvicorn_config_constructor.assert_called_once()
164
+ _, kwargs_config = mock_uvicorn_config_constructor.call_args
165
+
166
+ assert kwargs_config.get("log_config") == sample_log_config
167
+ assert "log_level" not in kwargs_config
168
+
169
+ mock_uvicorn_server_constructor.assert_called_once_with(
170
+ mock_uvicorn_config_constructor.return_value
171
+ )
172
+ mock_server_instance.serve.assert_awaited_once()
173
+
174
+ server_task.cancel()
175
+ with pytest.raises(asyncio.CancelledError):
176
+ await server_task
tests/server/test_proxy.py CHANGED
@@ -74,7 +74,7 @@ async def test_create_proxy(fastmcp_server):
74
  # Create a client
75
  client = Client(transport=FastMCPTransport(fastmcp_server))
76
 
77
- server = FastMCPProxy.from_client(client)
78
 
79
  assert isinstance(server, FastMCPProxy)
80
  assert isinstance(server, FastMCP)
 
74
  # Create a client
75
  client = Client(transport=FastMCPTransport(fastmcp_server))
76
 
77
+ server = FastMCPProxy.as_proxy(client)
78
 
79
  assert isinstance(server, FastMCPProxy)
80
  assert isinstance(server, FastMCP)