admin subserver survives reloading
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user