Notification middleware does not work
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
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:
- Route outgoing notifications through the middleware pipeline
- Distinguish between incoming (client→server) and outgoing (server→client) notifications in middleware hooks
- Potentially add new hooks like
on_outgoing_notificationto handle server-initiated notifications
Findings
Current Architecture
Incoming Notifications (Client → Server)
- Flow through middleware via
MiddlewareContextwithtype="notification" - Trigger the
on_notification()middleware hook - This works as expected
Outgoing Notifications (Server → Client)
- Sent via
Contextmethods: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_notificationmiddleware 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 toMiddlewarebase class - Update
MiddlewareContextto supportsource="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_notificationfires 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 |