fastmcp icon indicating copy to clipboard operation
fastmcp copied to clipboard

Notification middleware does not work

Open strawgate opened this issue 2 months ago • 1 comments

Description

It looks a bit like notification middleware does not work, in my response caching middleware #1845 i cannot get on_notification to fire when sending notifications via context

Example Code


Version Information

latest

strawgate avatar Oct 16 '25 18:10 strawgate

Claude finished @strawgate's task —— View job


Investigation Complete ✓

Recommendation

The reported issue is correct: server-side notification middleware does not work for outgoing notifications sent via context.

The on_notification hook in middleware is designed to handle incoming notifications from clients, not outgoing notifications to clients. When notifications are sent via context.send_tool_list_changed() (or similar methods), they bypass middleware entirely by calling session.send_*_list_changed() directly at the MCP SDK level.

This is a confirmed architectural limitation rather than a bug in PR #1845. To properly support notification middleware for cache invalidation and similar use cases, FastMCP would need to:

  1. Route outgoing notifications through the middleware pipeline
  2. Distinguish between incoming (client→server) and outgoing (server→client) notifications in middleware hooks
  3. Potentially add new hooks like on_outgoing_notification to handle server-initiated notifications
Findings

Current Architecture

Incoming Notifications (Client → Server)

  • Flow through middleware via MiddlewareContext with type="notification"
  • Trigger the on_notification() middleware hook
  • This works as expected

Outgoing Notifications (Server → Client)

  • Sent via Context methods: send_tool_list_changed(), send_resource_list_changed(), send_prompt_list_changed()
  • These call session.send_*_list_changed() directly (src/fastmcp/server/context.py:393-403, 672-676)
  • The session methods are from the MCP SDK and bypass FastMCP middleware entirely
  • Result: on_notification middleware hooks never fire

Key Code Locations

Middleware definition (src/fastmcp/server/middleware/middleware.py:119-148):

match context.type:
    case "request":
        handler = partial(self.on_request, call_next=handler)
    case "notification":  # Only handles INCOMING notifications
        handler = partial(self.on_notification, call_next=handler)

Context sending notifications (src/fastmcp/server/context.py:393-403):

async def send_tool_list_changed(self) -> None:
    """Send a tool list changed notification to the client."""
    await self.session.send_tool_list_changed()  # Bypasses middleware!

Why This Matters for PR #1845

The response caching middleware in PR #1845 wants to use on_notification to bust caches when list-changed notifications are sent. However, since outgoing notifications bypass middleware, cache invalidation triggered by context.send_*_list_changed() calls won't work.

Detailed Action Plan

To fix this issue, a FastMCP maintainer would need to:

1. Add Outgoing Notification Support to Middleware System

Create new middleware hooks:

  • Add on_outgoing_notification() hook to Middleware base class
  • Update MiddlewareContext to support source="server" for outgoing notifications
  • Consider adding method-specific hooks like on_send_tool_list_changed()

2. Route Outgoing Notifications Through Middleware

Modify Context methods (src/fastmcp/server/context.py:393-403):

async def send_tool_list_changed(self) -> None:
    """Send a tool list changed notification to the client."""
    # Create middleware context for outgoing notification
    notification = types.ToolListChangedNotification()
    mw_context = MiddlewareContext(
        message=notification,
        source="server",  # NEW: distinguish outgoing
        type="notification",
        method="notifications/tools/list_changed",
        fastmcp_context=self,
    )
    
    # Apply middleware before sending
    async def send_notification(ctx: MiddlewareContext) -> None:
        await self.session.send_tool_list_changed()
    
    await self.fastmcp._apply_middleware(mw_context, send_notification)

Repeat for:

  • send_resource_list_changed()
  • send_prompt_list_changed()
  • _flush_notifications() (src/fastmcp/server/context.py:664-680)

3. Update Middleware Base Class

Add new hook (src/fastmcp/server/middleware/middleware.py):

async def on_outgoing_notification(
    self,
    context: MiddlewareContext[mt.Notification[Any, Any]],
    call_next: CallNext[mt.Notification[Any, Any], None],
) -> None:
    return await call_next(context)

Update dispatcher (src/fastmcp/server/middleware/middleware.py:95-127):

match context.type:
    case "request":
        handler = partial(self.on_request, call_next=handler)
    case "notification":
        if context.source == "client":
            handler = partial(self.on_notification, call_next=handler)
        elif context.source == "server":
            handler = partial(self.on_outgoing_notification, call_next=handler)

4. Add Tests

Create tests/server/middleware/test_outgoing_notifications.py:

  • Test that on_outgoing_notification fires when sending notifications via context
  • Test that middleware can intercept and modify outgoing notifications
  • Test that cache invalidation works in response caching middleware
  • Test error handling when middleware fails during notification send

5. Update Documentation

  • Document the distinction between incoming and outgoing notifications
  • Add examples showing cache invalidation on notification send
  • Update middleware guide (docs/servers/middleware.mdx)
Related Items
Repository Issue or PR Relevance
jlowin/fastmcp Add Response Caching Middleware #1845 This PR needs outgoing notification middleware to properly invalidate caches when list-changed notifications are sent
jlowin/fastmcp Support initialize requests in middleware #1546 Precedent for extending middleware to support additional message types
jlowin/fastmcp Internal refactor of MCP handlers #2005 Recent refactoring of handler architecture that would be relevant for this work
Related Files
Repository File Relevance Sections
jlowin/fastmcp middleware.py Critical - Middleware base class and dispatcher 119-148 - Notification routing
jlowin/fastmcp context.py Critical - Where notifications are sent 393-403 - Send methods, 664-680 - Flush notifications
jlowin/fastmcp server.py High - Middleware application logic 429-438 - _apply_middleware method
jlowin/fastmcp test_notifications.py Medium - Existing notification tests Entire file - Patterns for testing notifications