structify dicts

This commit is contained in:
lda
2026-05-13 23:37:47 +07:00 Verified
parent 11afed99ab
commit ed239b5fd8
5 changed files with 72 additions and 17 deletions
+2
View File
@@ -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
+4 -3
View File
@@ -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)
+33 -4
View File
@@ -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)
+8 -10
View File
@@ -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
+25
View File
@@ -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