local source change events to mcp notifs
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
from .mapping import map_event_to_notifications
|
||||
from .sink import (
|
||||
FastMcpContextNotificationSink,
|
||||
NotificationSink,
|
||||
RecordingNotificationSink,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"FastMcpContextNotificationSink",
|
||||
"NotificationSink",
|
||||
"RecordingNotificationSink",
|
||||
"map_event_to_notifications",
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import mcp.types as mcp_types
|
||||
|
||||
from wf_mcp.events import McpEvent
|
||||
|
||||
|
||||
def map_event_to_notifications(event: McpEvent) -> list[mcp_types.ServerNotification]:
|
||||
"""Project broker-local events into MCP server notifications.
|
||||
|
||||
The event bus is intentionally protocol-neutral. This function is the
|
||||
boundary where local capability changes become official MCP notification
|
||||
payloads that a FastMCP transport can later send to connected clients.
|
||||
"""
|
||||
notification = _list_changed_notification(event)
|
||||
if notification is None:
|
||||
return []
|
||||
return [mcp_types.ServerNotification(notification)]
|
||||
|
||||
|
||||
def _list_changed_notification(
|
||||
event: McpEvent,
|
||||
) -> (
|
||||
mcp_types.ToolListChangedNotification
|
||||
| mcp_types.ResourceListChangedNotification
|
||||
| mcp_types.PromptListChangedNotification
|
||||
| None
|
||||
):
|
||||
if event.kind == "tools_changed":
|
||||
return mcp_types.ToolListChangedNotification()
|
||||
if event.kind == "resources_changed":
|
||||
return mcp_types.ResourceListChangedNotification()
|
||||
if event.kind == "prompts_changed":
|
||||
return mcp_types.PromptListChangedNotification()
|
||||
return None
|
||||
@@ -0,0 +1,49 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
import mcp.types as mcp_types
|
||||
|
||||
from wf_mcp.events import McpEvent
|
||||
|
||||
from .mapping import map_event_to_notifications
|
||||
|
||||
|
||||
class NotificationSink(Protocol):
|
||||
"""Consumes local events and emits or records MCP notifications."""
|
||||
|
||||
def __call__(self, event: McpEvent) -> None: ...
|
||||
|
||||
|
||||
class RecordingNotificationSink:
|
||||
"""Test sink that records MCP notifications projected from local events."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._notifications: list[mcp_types.ServerNotification] = []
|
||||
|
||||
def __call__(self, event: McpEvent) -> None:
|
||||
self._notifications.extend(map_event_to_notifications(event))
|
||||
|
||||
def list_notifications(self) -> list[mcp_types.ServerNotification]:
|
||||
"""Return a defensive copy of projected notifications."""
|
||||
return list(self._notifications)
|
||||
|
||||
|
||||
class FastMcpNotificationContext(Protocol):
|
||||
"""Small protocol for the FastMCP context method we need."""
|
||||
|
||||
async def send_notification(
|
||||
self,
|
||||
notification: mcp_types.ServerNotificationType,
|
||||
) -> None: ...
|
||||
|
||||
|
||||
class FastMcpContextNotificationSink:
|
||||
"""Send projected local events through a request-scoped FastMCP context."""
|
||||
|
||||
def __init__(self, context: FastMcpNotificationContext) -> None:
|
||||
self._context = context
|
||||
|
||||
async def send_event(self, event: McpEvent) -> None:
|
||||
for notification in map_event_to_notifications(event):
|
||||
await self._context.send_notification(notification.root)
|
||||
@@ -2,9 +2,11 @@ from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp import Context, FastMCP
|
||||
|
||||
from ..admin_surface import ProxyAdminRuntime, TransparentAdminHandlers
|
||||
from ..events import make_event
|
||||
from ..notifications import FastMcpContextNotificationSink
|
||||
|
||||
|
||||
def create_proxy_admin_server(
|
||||
@@ -44,8 +46,10 @@ def create_proxy_admin_server(
|
||||
"Reload the config file and remount enabled upstream MCP connections."
|
||||
),
|
||||
)
|
||||
async def reload_config() -> dict[str, Any]:
|
||||
return handlers.reload_config()
|
||||
async def reload_config(ctx: Context) -> dict[str, Any]:
|
||||
result = handlers.reload_config()
|
||||
await _send_reload_notifications(ctx)
|
||||
return result
|
||||
|
||||
@admin.tool(
|
||||
title="List Proxy Tools",
|
||||
@@ -134,3 +138,11 @@ def create_proxy_admin_server(
|
||||
return handlers.remove_connection(connection_id)
|
||||
|
||||
return admin
|
||||
|
||||
|
||||
async def _send_reload_notifications(ctx: Context) -> None:
|
||||
"""Notify the current client that reload may have changed visible capabilities."""
|
||||
sink = FastMcpContextNotificationSink(ctx)
|
||||
await sink.send_event(make_event("tools_changed"))
|
||||
await sink.send_event(make_event("resources_changed"))
|
||||
await sink.send_event(make_event("prompts_changed"))
|
||||
|
||||
Reference in New Issue
Block a user