structify dicts
This commit is contained in:
@@ -70,6 +70,8 @@ event bus is supplied. The admin MCP tool projects the same event kinds into
|
||||
MCP list-changed notifications for the current client session. Config mutation
|
||||
tools still only stage changes and return `requires_reload`; they do not emit
|
||||
list-changed notifications until reload remounts the visible capability set.
|
||||
Internally, reload metadata uses `ProxyReloadResult`; MCP tools serialize that
|
||||
typed result to a plain payload at the boundary.
|
||||
|
||||
Do not memoize mounted proxies or clients without an explicit lifecycle design.
|
||||
The tempting implementation is a dictionary keyed by connection id around
|
||||
|
||||
@@ -6,7 +6,7 @@ from fastmcp import Context, FastMCP
|
||||
|
||||
from ..admin_surface import ProxyAdminRuntime, TransparentAdminHandlers
|
||||
from ..notifications import FastMcpContextNotificationSink
|
||||
from .reload_events import reload_change_events
|
||||
from .reload_events import ProxyReloadResult, reload_change_events
|
||||
|
||||
|
||||
def create_proxy_admin_server(
|
||||
@@ -48,7 +48,7 @@ def create_proxy_admin_server(
|
||||
)
|
||||
async def reload_config(ctx: Context) -> dict[str, Any]:
|
||||
result = handlers.reload_config()
|
||||
await _send_reload_notifications(ctx, result)
|
||||
await _send_reload_notifications(ctx, ProxyReloadResult.from_payload(result))
|
||||
return result
|
||||
|
||||
@admin.tool(
|
||||
@@ -140,8 +140,9 @@ def create_proxy_admin_server(
|
||||
return admin
|
||||
|
||||
|
||||
async def _send_reload_notifications(ctx: Context, result: dict[str, Any]) -> None:
|
||||
async def _send_reload_notifications(ctx: Context, result: ProxyReloadResult) -> None:
|
||||
"""Notify the current client that reload may have changed visible capabilities."""
|
||||
sink = FastMcpContextNotificationSink(ctx)
|
||||
for event in reload_change_events(result):
|
||||
await sink.send_event(event)
|
||||
|
||||
|
||||
@@ -1,17 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from ..events import McpEvent, make_event
|
||||
|
||||
|
||||
def reload_change_events(result: dict[str, Any]) -> list[McpEvent]:
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ProxyReloadResult:
|
||||
"""Typed result for a successful proxy remount before MCP serialization."""
|
||||
|
||||
mounted_connections: list[str]
|
||||
connection_count: int
|
||||
enabled_connection_count: int
|
||||
|
||||
def to_payload(self) -> dict[str, Any]:
|
||||
"""Serialize the reload result for MCP tool responses."""
|
||||
return {
|
||||
"ok": True,
|
||||
"reloaded": True,
|
||||
"mounted_connections": self.mounted_connections,
|
||||
"connection_count": self.connection_count,
|
||||
"enabled_connection_count": self.enabled_connection_count,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_payload(cls, payload: dict[str, Any]) -> ProxyReloadResult:
|
||||
"""Rehydrate the typed result from an MCP/admin payload."""
|
||||
return cls(
|
||||
mounted_connections=list(payload["mounted_connections"]),
|
||||
connection_count=int(payload["connection_count"]),
|
||||
enabled_connection_count=int(payload["enabled_connection_count"]),
|
||||
)
|
||||
|
||||
|
||||
def reload_change_events(result: ProxyReloadResult) -> list[McpEvent]:
|
||||
"""Build local change events for a successful proxy runtime reload."""
|
||||
event_payload = {
|
||||
"reason": "transparent_reload",
|
||||
"mounted_connections": result["mounted_connections"],
|
||||
"connection_count": result["connection_count"],
|
||||
"enabled_connection_count": result["enabled_connection_count"],
|
||||
"mounted_connections": result.mounted_connections,
|
||||
"connection_count": result.connection_count,
|
||||
"enabled_connection_count": result.enabled_connection_count,
|
||||
}
|
||||
return [
|
||||
make_event(kind, payload=event_payload)
|
||||
|
||||
@@ -23,7 +23,7 @@ from .tools import (
|
||||
filter_proxy_tools,
|
||||
proxy_tools_page,
|
||||
)
|
||||
from .reload_events import reload_change_events
|
||||
from .reload_events import ProxyReloadResult, reload_change_events
|
||||
|
||||
_ADMIN_TOOL_NAMES = [
|
||||
f"{ADMIN_NAMESPACE}.list_connections",
|
||||
@@ -117,17 +117,15 @@ class ProxyRuntime:
|
||||
self.server.mount(proxy)
|
||||
mounted_connections.append(connection.id)
|
||||
|
||||
result = {
|
||||
"ok": True,
|
||||
"reloaded": True,
|
||||
"mounted_connections": mounted_connections,
|
||||
"connection_count": len(config.connections),
|
||||
"enabled_connection_count": len(mounted_connections),
|
||||
}
|
||||
result = ProxyReloadResult(
|
||||
mounted_connections=mounted_connections,
|
||||
connection_count=len(config.connections),
|
||||
enabled_connection_count=len(mounted_connections),
|
||||
)
|
||||
self._publish_reload_events(result)
|
||||
return result
|
||||
return result.to_payload()
|
||||
|
||||
def _publish_reload_events(self, result: dict[str, Any]) -> None:
|
||||
def _publish_reload_events(self, result: ProxyReloadResult) -> None:
|
||||
"""Publish local change events after a successful best-effort remount."""
|
||||
if self.event_bus is None:
|
||||
return
|
||||
|
||||
@@ -12,6 +12,10 @@ from wf_mcp.events import EventBus, InMemoryEventSink
|
||||
from wf_mcp.models import BrokerConfig, ConnectionConfig
|
||||
from wf_mcp.proxy_validation import ProxyConfigError, validate_transparent_proxy_config
|
||||
from wf_mcp.transparent_proxy import ProxyRuntime, create_transparent_proxy_client
|
||||
from wf_mcp.transparent_proxy.reload_events import (
|
||||
ProxyReloadResult,
|
||||
reload_change_events,
|
||||
)
|
||||
from wf_mcp.broker import load_broker_config
|
||||
|
||||
from .test_support import fixture_server_path, local_temp_root
|
||||
@@ -556,3 +560,24 @@ def test_transparent_proxy_runtime_reload_publishes_local_change_events() -> Non
|
||||
assert "resources_changed" in event_kinds
|
||||
assert "prompts_changed" in event_kinds
|
||||
assert catalog_changed[0].payload["reason"] == "transparent_reload"
|
||||
|
||||
|
||||
def test_proxy_reload_result_serializes_and_drives_reload_events() -> None:
|
||||
result = ProxyReloadResult(
|
||||
mounted_connections=["fixture.personal"],
|
||||
connection_count=2,
|
||||
enabled_connection_count=1,
|
||||
)
|
||||
|
||||
payload = result.to_payload()
|
||||
rehydrated = ProxyReloadResult.from_payload(payload)
|
||||
events = reload_change_events(result)
|
||||
|
||||
assert payload["ok"] is True
|
||||
assert payload["reloaded"] is True
|
||||
assert payload["mounted_connections"] == ["fixture.personal"]
|
||||
assert payload["connection_count"] == 2
|
||||
assert payload["enabled_connection_count"] == 1
|
||||
assert rehydrated == result
|
||||
assert events[0].payload["mounted_connections"] == ["fixture.personal"]
|
||||
assert events[0].payload["enabled_connection_count"] == 1
|
||||
|
||||
Reference in New Issue
Block a user