admin subserver survives reloading

This commit is contained in:
lda
2026-05-17 00:06:56 +07:00 Verified
parent dbf7c0b2a9
commit b5c896b77b
5 changed files with 51 additions and 34 deletions
+3 -2
View File
@@ -64,8 +64,9 @@ Unified mode currently reuses `ProxyRuntime` as its proxy mounting engine. The
the place where configured upstream MCP connections become mounted FastMCP the place where configured upstream MCP connections become mounted FastMCP
providers. `TransparentProxyRuntime` remains a compatibility alias. providers. `TransparentProxyRuntime` remains a compatibility alias.
`ProxyRuntime` now owns a small `ProxyMountRegistry`. Reload still clears the `ProxyRuntime` now owns a small `ProxyMountRegistry`. Proxy/admin tools are
visible mounted provider list and rebuilds it from current config, but unchanged registered once on the top-level local provider; reload only clears the visible
mounted upstream provider list and rebuilds it from current config. Unchanged
enabled connections reuse their cached proxy mount instead of recreating a new enabled connections reuse their cached proxy mount instead of recreating a new
client/proxy pair every time. Disabled or removed connections are no longer client/proxy pair every time. Disabled or removed connections are no longer
mounted after reload, but their cached mounts are only *retired* internally for mounted after reload, but their cached mounts are only *retired* internally for
+2 -2
View File
@@ -1,4 +1,4 @@
from .admin import create_proxy_admin_server from .admin import register_proxy_admin_tools
from .runtime import ( from .runtime import (
ProxyRuntime, ProxyRuntime,
TransparentProxyRuntime, TransparentProxyRuntime,
@@ -9,7 +9,7 @@ from .runtime import (
__all__ = [ __all__ = [
"ProxyRuntime", "ProxyRuntime",
"TransparentProxyRuntime", "TransparentProxyRuntime",
"create_proxy_admin_server", "register_proxy_admin_tools",
"create_transparent_proxy_client", "create_transparent_proxy_client",
"create_transparent_proxy_server", "create_transparent_proxy_server",
] ]
+27 -23
View File
@@ -9,49 +9,49 @@ from ..notifications import FastMcpContextNotificationSink
from .reload_events import ProxyReloadResult, reload_change_events from .reload_events import ProxyReloadResult, reload_change_events
def create_proxy_admin_server( def register_proxy_admin_tools(
server: FastMCP[Any],
runtime: ProxyAdminRuntime, runtime: ProxyAdminRuntime,
) -> FastMCP[Any]: ) -> None:
"""Create the admin MCP server mounted under the broker namespace.""" """Register proxy-runtime admin tools directly on the local provider."""
admin = FastMCP(
"wf-mcp-admin",
instructions="Administrative tools for this wf-mcp proxy instance.",
)
handlers = TransparentAdminHandlers(runtime) handlers = TransparentAdminHandlers(runtime)
@admin.tool( @server.tool(
name="wf.admin.list_connections",
title="List Connections", title="List Connections",
description="List configured MCP connections known to this proxy instance.", description="List configured MCP connections known to this proxy instance.",
) )
async def list_connections() -> list[dict[str, Any]]: async def list_connections() -> list[dict[str, Any]]:
return handlers.list_connections() return handlers.list_connections()
@admin.tool( @server.tool(
name="wf.admin.get_connection_statuses",
title="Get Connection Statuses", title="Get Connection Statuses",
description="Show configured MCP connection status and basic catalog counts.", description="Show configured MCP connection status and basic catalog counts.",
) )
async def get_connection_statuses() -> list[dict[str, Any]]: async def get_connection_statuses() -> list[dict[str, Any]]:
return handlers.get_connection_statuses() return handlers.get_connection_statuses()
@admin.tool( @server.tool(
name="wf.admin.get_config",
title="Get Config", title="Get Config",
description="Return the current proxy configuration payload.", description="Return the current proxy configuration payload.",
) )
async def get_config() -> dict[str, Any]: async def get_config() -> dict[str, Any]:
return handlers.get_config() return handlers.get_config()
@admin.tool( @server.tool(
name="wf.admin.reload_config",
title="Reload Config", title="Reload Config",
description=( description="Reload the config file and remount enabled upstream MCP connections.",
"Reload the config file and remount enabled upstream MCP connections."
),
) )
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, ProxyReloadResult.from_payload(result)) await _send_reload_notifications(ctx, ProxyReloadResult.from_payload(result))
return result return result
@admin.tool( @server.tool(
name="wf.admin.list_proxy_tools",
title="List Proxy Tools", title="List Proxy Tools",
description=( description=(
"List upstream tools projected through this proxy, with optional " "List upstream tools projected through this proxy, with optional "
@@ -71,14 +71,16 @@ def create_proxy_admin_server(
cursor=cursor, cursor=cursor,
) )
@admin.tool( @server.tool(
name="wf.admin.get_proxy_tool",
title="Get Proxy Tool", title="Get Proxy Tool",
description="Return admin metadata and schema for one projected proxy tool.", description="Return admin metadata and schema for one projected proxy tool.",
) )
async def get_proxy_tool(proxy_name: str) -> dict[str, Any]: async def get_proxy_tool(proxy_name: str) -> dict[str, Any]:
return await handlers.get_proxy_tool(proxy_name) return await handlers.get_proxy_tool(proxy_name)
@admin.tool( @server.tool(
name="wf.admin.add_connection",
title="Add Connection", title="Add Connection",
description="Add a new MCP connection to the file-backed proxy config.", description="Add a new MCP connection to the file-backed proxy config.",
) )
@@ -97,7 +99,8 @@ def create_proxy_admin_server(
enabled=enabled, enabled=enabled,
) )
@admin.tool( @server.tool(
name="wf.admin.update_connection",
title="Update Connection", title="Update Connection",
description="Update server, account, metadata, or enabled state for a connection.", description="Update server, account, metadata, or enabled state for a connection.",
) )
@@ -116,29 +119,30 @@ def create_proxy_admin_server(
enabled=enabled, enabled=enabled,
) )
@admin.tool( @server.tool(
name="wf.admin.enable_connection",
title="Enable Connection", title="Enable Connection",
description="Mark a configured MCP connection as enabled.", description="Mark a configured MCP connection as enabled.",
) )
async def enable_connection(connection_id: str) -> dict[str, Any]: async def enable_connection(connection_id: str) -> dict[str, Any]:
return handlers.enable_connection(connection_id) return handlers.enable_connection(connection_id)
@admin.tool( @server.tool(
name="wf.admin.disable_connection",
title="Disable Connection", title="Disable Connection",
description="Mark a configured MCP connection as disabled.", description="Mark a configured MCP connection as disabled.",
) )
async def disable_connection(connection_id: str) -> dict[str, Any]: async def disable_connection(connection_id: str) -> dict[str, Any]:
return handlers.disable_connection(connection_id) return handlers.disable_connection(connection_id)
@admin.tool( @server.tool(
name="wf.admin.remove_connection",
title="Remove Connection", title="Remove Connection",
description="Remove a configured MCP connection from the file-backed config.", description="Remove a configured MCP connection from the file-backed config.",
) )
async def remove_connection(connection_id: str) -> dict[str, Any]: async def remove_connection(connection_id: str) -> dict[str, Any]:
return handlers.remove_connection(connection_id) return handlers.remove_connection(connection_id)
return admin
async def _send_reload_notifications(ctx: Context, result: ProxyReloadResult) -> 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."""
+4 -7
View File
@@ -12,9 +12,9 @@ from fastmcp.server.transforms.search import BM25SearchTransform
from ..control import BrokerConfigManager, ConfigMutationError from ..control import BrokerConfigManager, ConfigMutationError
from ..events import EventBus from ..events import EventBus
from ..models import BrokerConfig from ..models import BrokerConfig
from ..shared.names import ADMIN_NAMESPACE, LdaNamespace from ..shared.names import ADMIN_NAMESPACE
from ..proxy_validation import validate_transparent_proxy_config from ..proxy_validation import validate_transparent_proxy_config
from .admin import create_proxy_admin_server from .admin import register_proxy_admin_tools
from .mounts import ProxyMountRegistry, create_proxy_mount from .mounts import ProxyMountRegistry, create_proxy_mount
from .tools import ( from .tools import (
ProxyToolPayload, ProxyToolPayload,
@@ -72,6 +72,8 @@ class ProxyRuntime:
self.mounts: ProxyMountRegistry[FastMCP[Any]] = ProxyMountRegistry( self.mounts: ProxyMountRegistry[FastMCP[Any]] = ProxyMountRegistry(
create_proxy_mount create_proxy_mount
) )
if self.admin_tools:
register_proxy_admin_tools(self.server, self)
self.reload() self.reload()
if resources_as_tools: if resources_as_tools:
self.server.add_transform(ResourcesAsTools(self.server)) self.server.add_transform(ResourcesAsTools(self.server))
@@ -100,11 +102,6 @@ class ProxyRuntime:
validate_transparent_proxy_config(config) validate_transparent_proxy_config(config)
self.server.providers[:] = [self.server.local_provider] self.server.providers[:] = [self.server.local_provider]
if self.admin_tools:
admin = create_proxy_admin_server(self)
admin.add_transform(LdaNamespace(ADMIN_NAMESPACE))
self.server.mount(admin)
mounts = self.mounts.active_mounts_for(config) mounts = self.mounts.active_mounts_for(config)
for mount in mounts: for mount in mounts:
self.server.mount(mount.proxy) self.server.mount(mount.proxy)
+15
View File
@@ -106,6 +106,21 @@ def test_transparent_proxy_lists_and_calls_upstream_tools() -> None:
asyncio.run(run_proxy()) asyncio.run(run_proxy())
def test_transparent_proxy_registers_admin_tools_on_local_provider() -> None:
config = BrokerConfig(
store_root=local_temp_root() / "transparent_proxy_local_admin_store",
connections=[],
)
runtime = ProxyRuntime(config)
assert len(runtime.server.providers) == 1
tools = asyncio.run(runtime.server.local_provider.list_tools())
names = {tool.name for tool in tools}
assert "wf.admin.list_connections" in names
assert "wf.admin.reload_config" in names
def test_transparent_proxy_rewrites_resource_links_returned_by_tools() -> None: def test_transparent_proxy_rewrites_resource_links_returned_by_tools() -> None:
config = BrokerConfig( config = BrokerConfig(
store_root=local_temp_root() / "transparent_proxy_resource_link_store", store_root=local_temp_root() / "transparent_proxy_resource_link_store",