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
|
MCP list-changed notifications for the current client session. Config mutation
|
||||||
tools still only stage changes and return `requires_reload`; they do not emit
|
tools still only stage changes and return `requires_reload`; they do not emit
|
||||||
list-changed notifications until reload remounts the visible capability set.
|
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.
|
Do not memoize mounted proxies or clients without an explicit lifecycle design.
|
||||||
The tempting implementation is a dictionary keyed by connection id around
|
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 ..admin_surface import ProxyAdminRuntime, TransparentAdminHandlers
|
||||||
from ..notifications import FastMcpContextNotificationSink
|
from ..notifications import FastMcpContextNotificationSink
|
||||||
from .reload_events import reload_change_events
|
from .reload_events import ProxyReloadResult, reload_change_events
|
||||||
|
|
||||||
|
|
||||||
def create_proxy_admin_server(
|
def create_proxy_admin_server(
|
||||||
@@ -48,7 +48,7 @@ def create_proxy_admin_server(
|
|||||||
)
|
)
|
||||||
async def reload_config(ctx: Context) -> dict[str, Any]:
|
async def reload_config(ctx: Context) -> dict[str, Any]:
|
||||||
result = handlers.reload_config()
|
result = handlers.reload_config()
|
||||||
await _send_reload_notifications(ctx, result)
|
await _send_reload_notifications(ctx, ProxyReloadResult.from_payload(result))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@admin.tool(
|
@admin.tool(
|
||||||
@@ -140,8 +140,9 @@ def create_proxy_admin_server(
|
|||||||
return admin
|
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."""
|
"""Notify the current client that reload may have changed visible capabilities."""
|
||||||
sink = FastMcpContextNotificationSink(ctx)
|
sink = FastMcpContextNotificationSink(ctx)
|
||||||
for event in reload_change_events(result):
|
for event in reload_change_events(result):
|
||||||
await sink.send_event(event)
|
await sink.send_event(event)
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,46 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from ..events import McpEvent, make_event
|
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."""
|
"""Build local change events for a successful proxy runtime reload."""
|
||||||
event_payload = {
|
event_payload = {
|
||||||
"reason": "transparent_reload",
|
"reason": "transparent_reload",
|
||||||
"mounted_connections": result["mounted_connections"],
|
"mounted_connections": result.mounted_connections,
|
||||||
"connection_count": result["connection_count"],
|
"connection_count": result.connection_count,
|
||||||
"enabled_connection_count": result["enabled_connection_count"],
|
"enabled_connection_count": result.enabled_connection_count,
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
make_event(kind, payload=event_payload)
|
make_event(kind, payload=event_payload)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from .tools import (
|
|||||||
filter_proxy_tools,
|
filter_proxy_tools,
|
||||||
proxy_tools_page,
|
proxy_tools_page,
|
||||||
)
|
)
|
||||||
from .reload_events import reload_change_events
|
from .reload_events import ProxyReloadResult, reload_change_events
|
||||||
|
|
||||||
_ADMIN_TOOL_NAMES = [
|
_ADMIN_TOOL_NAMES = [
|
||||||
f"{ADMIN_NAMESPACE}.list_connections",
|
f"{ADMIN_NAMESPACE}.list_connections",
|
||||||
@@ -117,17 +117,15 @@ class ProxyRuntime:
|
|||||||
self.server.mount(proxy)
|
self.server.mount(proxy)
|
||||||
mounted_connections.append(connection.id)
|
mounted_connections.append(connection.id)
|
||||||
|
|
||||||
result = {
|
result = ProxyReloadResult(
|
||||||
"ok": True,
|
mounted_connections=mounted_connections,
|
||||||
"reloaded": True,
|
connection_count=len(config.connections),
|
||||||
"mounted_connections": mounted_connections,
|
enabled_connection_count=len(mounted_connections),
|
||||||
"connection_count": len(config.connections),
|
)
|
||||||
"enabled_connection_count": len(mounted_connections),
|
|
||||||
}
|
|
||||||
self._publish_reload_events(result)
|
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."""
|
"""Publish local change events after a successful best-effort remount."""
|
||||||
if self.event_bus is None:
|
if self.event_bus is None:
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ from wf_mcp.events import EventBus, InMemoryEventSink
|
|||||||
from wf_mcp.models import BrokerConfig, ConnectionConfig
|
from wf_mcp.models import BrokerConfig, ConnectionConfig
|
||||||
from wf_mcp.proxy_validation import ProxyConfigError, validate_transparent_proxy_config
|
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 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 wf_mcp.broker import load_broker_config
|
||||||
|
|
||||||
from .test_support import fixture_server_path, local_temp_root
|
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 "resources_changed" in event_kinds
|
||||||
assert "prompts_changed" in event_kinds
|
assert "prompts_changed" in event_kinds
|
||||||
assert catalog_changed[0].payload["reason"] == "transparent_reload"
|
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