"""Ping middleware for keeping client connections alive.""" from typing import Any import anyio from .middleware import CallNext, Middleware, MiddlewareContext class PingMiddleware(Middleware): """Middleware that sends periodic pings to keep client connections alive. Starts a background ping task on first message from each session. The task sends server-to-client pings at the configured interval until the session ends. Example: ```python from fastmcp import FastMCP from fastmcp.server.middleware import PingMiddleware mcp = FastMCP("MyServer") mcp.add_middleware(PingMiddleware(interval_ms=5000)) ``` """ def __init__(self, interval_ms: int = 30000): """Initialize ping middleware. Args: interval_ms: Interval between pings in milliseconds (default: 30000) Raises: ValueError: If interval_ms is not positive """ if interval_ms <= 0: raise ValueError("interval_ms must be positive") self.interval_ms = interval_ms self._active_sessions: set[int] = set() self._lock = anyio.Lock() async def on_message(self, context: MiddlewareContext, call_next: CallNext) -> Any: """Start ping task on first message from a session.""" if ( context.fastmcp_context is None or context.fastmcp_context.request_context is None ): return await call_next(context) session = context.fastmcp_context.session session_id = id(session) async with self._lock: if session_id not in self._active_sessions: # _subscription_task_group is added by MiddlewareServerSession tg = session._subscription_task_group # type: ignore[attr-defined] # ty:ignore[unresolved-attribute] if tg is not None: self._active_sessions.add(session_id) tg.start_soon(self._ping_loop, session, session_id) return await call_next(context) async def _ping_loop(self, session: Any, session_id: int) -> None: """Send periodic pings until session ends.""" try: while True: await anyio.sleep(self.interval_ms / 1000) try: await session.send_ping() except anyio.ClosedResourceError: return finally: self._active_sessions.discard(session_id)