Skip to content

Commit 5e96b5c

Browse files
authored
feat: make the FastMCP access log opt-in (#243)
* feat: make the FastMCP access log opt-in Closes #240 * review: warn at config construction, per branch, and on the contradiction * feat: remove logging_turn_off_middleware instead of shimming it
1 parent a18f6f0 commit 5e96b5c

4 files changed

Lines changed: 63 additions & 16 deletions

File tree

‎docs/integrations/fastmcp.md‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,26 @@ def greet_person(person_name: str) -> str:
4747
return f"Hello, {person_name}!"
4848
```
4949

50-
Set `logging_turn_off_middleware=True` on the config to disable the per-MCP-message
51-
access log middleware. Set `health_checks_enabled=False` to omit the health route.
50+
## Logging
51+
52+
The per-MCP-message access log is **off by default**, matching FastAPI and Litestar. Turn it on
53+
explicitly:
54+
55+
```python
56+
FastMcpConfig(
57+
service_name="microservice",
58+
fastmcp_logging_middleware_enabled=True,
59+
)
60+
```
61+
62+
Enabled, each message is logged with its `method`, `source` and `type`, plus `duration` in
63+
nanoseconds. A message that raises is logged at exception level and the exception is re-raised.
64+
65+
This replaces `logging_turn_off_middleware`, which has been removed. Setting it now raises
66+
`TypeError`: the default flipped from on to off, so a service that configured the old field has to
67+
decide again rather than upgrade past the change unnoticed.
68+
69+
Set `health_checks_enabled=False` to omit the health route.
5270

5371
Teardown is wired through FastMCP's provider lifecycle — `bootstrapper.teardown()`
5472
runs automatically when the FastMCP server's ASGI lifespan shuts down (i.e. when

‎docs/introduction/configuration.md‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,15 @@ FastAPI ships no access log of its own, so lite-bootstrap provides one. It is **
255255
See [the FastAPI integration guide](../integrations/fastapi.md#logging) for what gets logged and why
256256
it defaults to off.
257257

258+
### Structlog FastMCP
259+
260+
The per-MCP-message access log is **off by default**:
261+
262+
- `fastmcp_logging_middleware_enabled` - turn on the access log (default: `False`). Replaces
263+
`logging_turn_off_middleware`, which has been removed.
264+
265+
See [the FastMCP integration guide](../integrations/fastmcp.md#logging) for what gets logged.
266+
258267
### Structlog FastStream
259268

260269
When using FastStream, the structlog logger is automatically injected into the broker so that all broker

‎lite_bootstrap/bootstrappers/fastmcp_bootstrapper.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ async def on_message(
8181
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
8282
class FastMcpConfig(HealthChecksConfig, LoggingConfig, PrometheusConfig, PyroscopeConfig, SentryConfig):
8383
application: "FastMCP[typing.Any]" = dataclasses.field(default_factory=_make_fastmcp)
84-
logging_turn_off_middleware: bool = False
84+
fastmcp_logging_middleware_enabled: bool = False
8585

8686

8787
@dataclasses.dataclass(kw_only=True)
@@ -132,7 +132,7 @@ class FastMcpLoggingInstrument(LoggingInstrument):
132132

133133
def bootstrap(self) -> None:
134134
super().bootstrap()
135-
if self.bootstrap_config.logging_turn_off_middleware:
135+
if not self.bootstrap_config.fastmcp_logging_middleware_enabled:
136136
return
137137
self.bootstrap_config.application.add_middleware(FastMcpLoggingMiddleware())
138138

‎tests/test_fastmcp_bootstrap.py‎

Lines changed: 32 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@
1313
from lite_bootstrap import BootstrapperNotReadyError, FastMcpBootstrapper, FastMcpConfig
1414
from lite_bootstrap.bootstrappers.fastmcp_bootstrapper import FastMcpLoggingMiddleware
1515
from lite_bootstrap.exceptions import ConfigurationError
16-
from tests.conftest import emulate_package_missing, emulate_package_missing_with_module_reload
16+
from tests.conftest import (
17+
emulate_package_missing,
18+
emulate_package_missing_with_module_reload,
19+
)
1720

1821

1922
def test_fastmcp_config_default_application() -> None:
@@ -221,24 +224,41 @@ def _find_mcp_logging_middleware(application: "FastMCP") -> list[FastMcpLoggingM
221224
return [m for m in application.middleware if isinstance(m, FastMcpLoggingMiddleware)]
222225

223226

224-
def test_fastmcp_logging_middleware_is_mounted_by_default() -> None:
225-
config = _make_test_config()
227+
def _mounted_logging_middleware(config: FastMcpConfig) -> list[FastMcpLoggingMiddleware]:
226228
bootstrapper = FastMcpBootstrapper(bootstrap_config=config)
227229
application = bootstrapper.bootstrap()
228230
try:
229-
assert len(_find_mcp_logging_middleware(application)) == 1
231+
return _find_mcp_logging_middleware(application)
230232
finally:
231233
bootstrapper.teardown()
232234

233235

234-
def test_fastmcp_logging_middleware_disabled_via_flag() -> None:
235-
config = _make_test_config(logging_turn_off_middleware=True)
236-
bootstrapper = FastMcpBootstrapper(bootstrap_config=config)
237-
application = bootstrapper.bootstrap()
238-
try:
239-
assert _find_mcp_logging_middleware(application) == []
240-
finally:
241-
bootstrapper.teardown()
236+
def test_fastmcp_logging_middleware_is_not_mounted_by_default() -> None:
237+
assert _mounted_logging_middleware(_make_test_config()) == []
238+
239+
240+
def test_fastmcp_logging_middleware_is_mounted_when_enabled() -> None:
241+
config = _make_test_config(fastmcp_logging_middleware_enabled=True)
242+
assert len(_mounted_logging_middleware(config)) == 1
243+
244+
245+
def test_fastmcp_config_rejects_the_removed_turn_off_field() -> None:
246+
"""The superseded name is gone: setting it fails at construction rather than silently.
247+
248+
The default flipped, so a shim could not have been silent anyway, and a service that set the old
249+
field has to re-decide rather than upgrade past the change without noticing.
250+
"""
251+
with pytest.raises(TypeError, match="logging_turn_off_middleware"):
252+
_make_test_config(logging_turn_off_middleware=True)
253+
254+
255+
@pytest.mark.parametrize("overrides", [{}, {"fastmcp_logging_middleware_enabled": True}], ids=["default", "enabled"])
256+
def test_fastmcp_logging_config_warns_nothing(overrides: dict[str, bool]) -> None:
257+
with warnings.catch_warnings(record=True) as caught:
258+
warnings.simplefilter("always")
259+
_make_test_config(**overrides)
260+
261+
assert [str(one.message) for one in caught] == []
242262

243263

244264
@pytest.mark.parametrize(

0 commit comments

Comments
 (0)