Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions docs/integrations/fastmcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,26 @@ def greet_person(person_name: str) -> str:
return f"Hello, {person_name}!"
```

Set `logging_turn_off_middleware=True` on the config to disable the per-MCP-message
access log middleware. Set `health_checks_enabled=False` to omit the health route.
## Logging

The per-MCP-message access log is **off by default**, matching FastAPI and Litestar. Turn it on
explicitly:

```python
FastMcpConfig(
service_name="microservice",
fastmcp_logging_middleware_enabled=True,
)
```

Enabled, each message is logged with its `method`, `source` and `type`, plus `duration` in
nanoseconds. A message that raises is logged at exception level and the exception is re-raised.

This replaces `logging_turn_off_middleware`, which has been removed. Setting it now raises
`TypeError`: the default flipped from on to off, so a service that configured the old field has to
decide again rather than upgrade past the change unnoticed.

Set `health_checks_enabled=False` to omit the health route.

Teardown is wired through FastMCP's provider lifecycle — `bootstrapper.teardown()`
runs automatically when the FastMCP server's ASGI lifespan shuts down (i.e. when
Expand Down
9 changes: 9 additions & 0 deletions docs/introduction/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,15 @@ FastAPI ships no access log of its own, so lite-bootstrap provides one. It is **
See [the FastAPI integration guide](../integrations/fastapi.md#logging) for what gets logged and why
it defaults to off.

### Structlog FastMCP

The per-MCP-message access log is **off by default**:

- `fastmcp_logging_middleware_enabled` - turn on the access log (default: `False`). Replaces
`logging_turn_off_middleware`, which has been removed.

See [the FastMCP integration guide](../integrations/fastmcp.md#logging) for what gets logged.

### Structlog FastStream

When using FastStream, the structlog logger is automatically injected into the broker so that all broker
Expand Down
4 changes: 2 additions & 2 deletions lite_bootstrap/bootstrappers/fastmcp_bootstrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ async def on_message(
@dataclasses.dataclass(kw_only=True, slots=True, frozen=True)
class FastMcpConfig(HealthChecksConfig, LoggingConfig, PrometheusConfig, PyroscopeConfig, SentryConfig):
application: "FastMCP[typing.Any]" = dataclasses.field(default_factory=_make_fastmcp)
logging_turn_off_middleware: bool = False
fastmcp_logging_middleware_enabled: bool = False


@dataclasses.dataclass(kw_only=True)
Expand Down Expand Up @@ -132,7 +132,7 @@ class FastMcpLoggingInstrument(LoggingInstrument):

def bootstrap(self) -> None:
super().bootstrap()
if self.bootstrap_config.logging_turn_off_middleware:
if not self.bootstrap_config.fastmcp_logging_middleware_enabled:
return
self.bootstrap_config.application.add_middleware(FastMcpLoggingMiddleware())

Expand Down
44 changes: 32 additions & 12 deletions tests/test_fastmcp_bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@
from lite_bootstrap import BootstrapperNotReadyError, FastMcpBootstrapper, FastMcpConfig
from lite_bootstrap.bootstrappers.fastmcp_bootstrapper import FastMcpLoggingMiddleware
from lite_bootstrap.exceptions import ConfigurationError
from tests.conftest import emulate_package_missing, emulate_package_missing_with_module_reload
from tests.conftest import (
emulate_package_missing,
emulate_package_missing_with_module_reload,
)


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


def test_fastmcp_logging_middleware_is_mounted_by_default() -> None:
config = _make_test_config()
def _mounted_logging_middleware(config: FastMcpConfig) -> list[FastMcpLoggingMiddleware]:
bootstrapper = FastMcpBootstrapper(bootstrap_config=config)
application = bootstrapper.bootstrap()
try:
assert len(_find_mcp_logging_middleware(application)) == 1
return _find_mcp_logging_middleware(application)
finally:
bootstrapper.teardown()


def test_fastmcp_logging_middleware_disabled_via_flag() -> None:
config = _make_test_config(logging_turn_off_middleware=True)
bootstrapper = FastMcpBootstrapper(bootstrap_config=config)
application = bootstrapper.bootstrap()
try:
assert _find_mcp_logging_middleware(application) == []
finally:
bootstrapper.teardown()
def test_fastmcp_logging_middleware_is_not_mounted_by_default() -> None:
assert _mounted_logging_middleware(_make_test_config()) == []


def test_fastmcp_logging_middleware_is_mounted_when_enabled() -> None:
config = _make_test_config(fastmcp_logging_middleware_enabled=True)
assert len(_mounted_logging_middleware(config)) == 1


def test_fastmcp_config_rejects_the_removed_turn_off_field() -> None:
"""The superseded name is gone: setting it fails at construction rather than silently.

The default flipped, so a shim could not have been silent anyway, and a service that set the old
field has to re-decide rather than upgrade past the change without noticing.
"""
with pytest.raises(TypeError, match="logging_turn_off_middleware"):
_make_test_config(logging_turn_off_middleware=True)


@pytest.mark.parametrize("overrides", [{}, {"fastmcp_logging_middleware_enabled": True}], ids=["default", "enabled"])
def test_fastmcp_logging_config_warns_nothing(overrides: dict[str, bool]) -> None:
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
_make_test_config(**overrides)

assert [str(one.message) for one in caught] == []


@pytest.mark.parametrize(
Expand Down
Loading