fix(webui): surface MCP runtime connection failures (#5331)
This commit is contained in:
@@ -46,6 +46,10 @@ async def connect_mcp(state: Any, tools: ToolRegistry) -> None:
|
||||
await mcp_tools.connect_missing_servers(state, tools)
|
||||
|
||||
|
||||
def mcp_runtime_status(state: Any) -> dict[str, mcp_tools.MCPRuntimeStatus]:
|
||||
return mcp_tools.runtime_status(state)
|
||||
|
||||
|
||||
async def close_mcp(state: Any) -> None:
|
||||
await mcp_tools.close_mcp_servers(state)
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ from nanobot.utils.runtime import (
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from nanobot.agent.tools.mcp import MCPConnection
|
||||
from nanobot.agent.tools.mcp import MCPConnection, MCPRuntimeStatus
|
||||
from nanobot.config.schema import (
|
||||
ChannelsConfig,
|
||||
Config,
|
||||
@@ -401,6 +401,7 @@ class AgentLoop:
|
||||
self._running = False
|
||||
self._mcp_servers = mcp_servers or {}
|
||||
self._mcp_stacks: dict[str, MCPConnection] = {}
|
||||
self._mcp_runtime_statuses: dict[str, MCPRuntimeStatus] = {}
|
||||
self._mcp_connecting = False
|
||||
self._runtime_context_providers: list[RuntimeContextProvider] = []
|
||||
self._active_tasks: dict[str, set[asyncio.Task[Any]]] = {}
|
||||
@@ -646,6 +647,10 @@ class AgentLoop:
|
||||
"""Connect configured MCP servers."""
|
||||
await agent_context.connect_mcp(self, self.tools)
|
||||
|
||||
def mcp_runtime_status(self) -> dict[str, MCPRuntimeStatus]:
|
||||
"""Return connection state learned from real MCP runtime attempts."""
|
||||
return agent_context.mcp_runtime_status(self)
|
||||
|
||||
def register_runtime_context_provider(
|
||||
self,
|
||||
provider: RuntimeContextProvider,
|
||||
|
||||
@@ -9,7 +9,7 @@ import shutil
|
||||
import urllib.parse
|
||||
from collections.abc import AsyncIterator, Awaitable, Callable
|
||||
from contextlib import AsyncExitStack, suppress
|
||||
from typing import TYPE_CHECKING, Any, Mapping, Protocol, cast
|
||||
from typing import TYPE_CHECKING, Any, Literal, Mapping, Protocol, cast
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
import httpx
|
||||
@@ -62,6 +62,10 @@ _WINDOWS_SHELL_LAUNCHERS: frozenset[str] = frozenset(("npx", "npm", "pnpm", "yar
|
||||
_SANITIZE_RE = re.compile(r"_+")
|
||||
_RELOAD_LOCKS: WeakKeyDictionary[Any, asyncio.Lock] = WeakKeyDictionary()
|
||||
_ReconnectCallback = Callable[[str, str, Tool], Awaitable[Tool | None]]
|
||||
MCPRuntimeStatus = Literal["connecting", "connected", "failed"]
|
||||
_MCP_RUNTIME_STATUSES: frozenset[MCPRuntimeStatus] = frozenset(
|
||||
("connecting", "connected", "failed")
|
||||
)
|
||||
|
||||
|
||||
class MCPConnection(Protocol):
|
||||
@@ -1297,17 +1301,92 @@ def session_extra(metadata: Mapping[str, Any] | None) -> dict[str, Any]:
|
||||
return {"mcp_presets": mcp_presets} if isinstance(mcp_presets, list) and mcp_presets else {}
|
||||
|
||||
|
||||
def _runtime_status_store(
|
||||
state: Any,
|
||||
*,
|
||||
create: bool = False,
|
||||
) -> dict[str, MCPRuntimeStatus] | None:
|
||||
raw_statuses: object = getattr(state, "_mcp_runtime_statuses", None)
|
||||
if isinstance(raw_statuses, dict):
|
||||
return cast(dict[str, MCPRuntimeStatus], raw_statuses)
|
||||
if not create:
|
||||
return None
|
||||
statuses: dict[str, MCPRuntimeStatus] = {}
|
||||
state._mcp_runtime_statuses = statuses
|
||||
return statuses
|
||||
|
||||
|
||||
def runtime_status(state: Any) -> dict[str, MCPRuntimeStatus]:
|
||||
"""Return the latest connection-attempt result for configured MCP servers."""
|
||||
statuses = _runtime_status_store(state)
|
||||
raw_configured: object = getattr(state, "_mcp_servers", None)
|
||||
if statuses is None or not isinstance(raw_configured, dict):
|
||||
return {}
|
||||
configured = cast(dict[str, Any], raw_configured)
|
||||
return {
|
||||
name: status
|
||||
for name, status in statuses.items()
|
||||
if name in configured and status in _MCP_RUNTIME_STATUSES
|
||||
}
|
||||
|
||||
|
||||
def _set_runtime_status(
|
||||
state: Any,
|
||||
server_names: Mapping[str, Any] | set[str] | list[str] | tuple[str, ...],
|
||||
status: MCPRuntimeStatus,
|
||||
) -> None:
|
||||
statuses = _runtime_status_store(state, create=True)
|
||||
assert statuses is not None
|
||||
for name in server_names:
|
||||
statuses[name] = status
|
||||
|
||||
|
||||
def _record_connection_result(
|
||||
state: Any,
|
||||
attempted: Mapping[str, Any] | set[str] | list[str] | tuple[str, ...],
|
||||
connected: Mapping[str, Any] | set[str] | list[str] | tuple[str, ...],
|
||||
) -> None:
|
||||
attempted_names = set(attempted)
|
||||
connected_names = set(connected)
|
||||
_set_runtime_status(state, connected_names, "connected")
|
||||
_set_runtime_status(state, attempted_names - connected_names, "failed")
|
||||
|
||||
|
||||
async def connect_missing_servers(state: Any, registry: ToolRegistry) -> None:
|
||||
"""Connect configured MCP servers that are not currently live."""
|
||||
async with _reload_lock(state):
|
||||
if getattr(state, "_mcp_closing", False):
|
||||
return
|
||||
missing_servers = {
|
||||
configured_missing = {
|
||||
name: cfg for name, cfg in state._mcp_servers.items() if name not in state._mcp_stacks
|
||||
}
|
||||
oauth_servers = {
|
||||
name: cfg
|
||||
for name, cfg in configured_missing.items()
|
||||
if getattr(cfg, "auth", None) == "oauth"
|
||||
}
|
||||
authorization_pending: set[str] = set()
|
||||
if oauth_servers:
|
||||
from nanobot.agent.tools.mcp_oauth import mcp_oauth_has_credentials
|
||||
|
||||
authorization_pending = {
|
||||
name
|
||||
for name, cfg in oauth_servers.items()
|
||||
if not mcp_oauth_has_credentials(name, cfg.url)
|
||||
}
|
||||
statuses = _runtime_status_store(state)
|
||||
if statuses is not None:
|
||||
for name in authorization_pending:
|
||||
statuses.pop(name, None)
|
||||
missing_servers = {
|
||||
name: cfg
|
||||
for name, cfg in configured_missing.items()
|
||||
if name not in authorization_pending
|
||||
}
|
||||
if state._mcp_connecting or not missing_servers:
|
||||
return
|
||||
state._mcp_connecting = True
|
||||
_set_runtime_status(state, missing_servers, "connecting")
|
||||
try:
|
||||
connected = await connect_mcp_servers(missing_servers, registry)
|
||||
if getattr(state, "_mcp_closing", False):
|
||||
@@ -1315,6 +1394,7 @@ async def connect_missing_servers(state: Any, registry: ToolRegistry) -> None:
|
||||
await connection.aclose()
|
||||
return
|
||||
state._mcp_stacks.update(connected)
|
||||
_record_connection_result(state, missing_servers, connected)
|
||||
_attach_reconnect_handlers(state, registry, connected)
|
||||
if connected:
|
||||
logger.info("MCP connected servers: {}", sorted(connected))
|
||||
@@ -1323,8 +1403,10 @@ async def connect_missing_servers(state: Any, registry: ToolRegistry) -> None:
|
||||
except asyncio.CancelledError:
|
||||
if task_is_cancelling():
|
||||
raise
|
||||
_set_runtime_status(state, missing_servers, "failed")
|
||||
logger.warning("MCP connection cancelled (will retry next message)")
|
||||
except BaseException as e:
|
||||
_set_runtime_status(state, missing_servers, "failed")
|
||||
logger.warning("Failed to connect MCP servers (will retry next message): {}", e)
|
||||
finally:
|
||||
state._mcp_connecting = False
|
||||
@@ -1380,6 +1462,11 @@ async def reload_servers(state: Any, registry: ToolRegistry) -> dict[str, Any]:
|
||||
tools_removed += _unregister_server_tools(registry, name)
|
||||
await _close_server(state, name)
|
||||
|
||||
runtime_statuses = _runtime_status_store(state)
|
||||
if runtime_statuses is not None:
|
||||
for name in [*removed, *authorization_pending]:
|
||||
runtime_statuses.pop(name, None)
|
||||
|
||||
state._mcp_servers = next_servers
|
||||
retry_missing = sorted(
|
||||
name
|
||||
@@ -1394,6 +1481,7 @@ async def reload_servers(state: Any, registry: ToolRegistry) -> dict[str, Any]:
|
||||
to_connect = {name: next_servers[name] for name in to_connect_names}
|
||||
connected: dict[str, MCPConnection] = {}
|
||||
if to_connect:
|
||||
_set_runtime_status(state, to_connect, "connecting")
|
||||
connected = await connect_mcp_servers(to_connect, registry)
|
||||
if getattr(state, "_mcp_closing", False):
|
||||
for connection in connected.values():
|
||||
@@ -1404,6 +1492,7 @@ async def reload_servers(state: Any, registry: ToolRegistry) -> dict[str, Any]:
|
||||
"requires_restart": True,
|
||||
}
|
||||
state._mcp_stacks.update(connected)
|
||||
_record_connection_result(state, to_connect, connected)
|
||||
_attach_reconnect_handlers(state, registry, connected)
|
||||
|
||||
failed = sorted(set(to_connect) - set(connected))
|
||||
@@ -1562,12 +1651,14 @@ async def _refresh_terminated_server(
|
||||
_unregister_server_tools(registry, server_name)
|
||||
await _close_server(state, server_name)
|
||||
|
||||
_set_runtime_status(state, {server_name}, "connecting")
|
||||
connected = await connect_mcp_servers({server_name: cfg}, registry)
|
||||
if getattr(state, "_mcp_closing", False):
|
||||
for connection in connected.values():
|
||||
await connection.aclose()
|
||||
return None
|
||||
state._mcp_stacks.update(connected)
|
||||
_record_connection_result(state, {server_name}, connected)
|
||||
_attach_reconnect_handlers(state, registry, connected)
|
||||
if server_name not in connected:
|
||||
logger.warning("MCP server '{}' reconnect failed after session termination", server_name)
|
||||
@@ -1621,6 +1712,9 @@ async def close_mcp_servers(state: Any) -> None:
|
||||
async with _reload_lock(state):
|
||||
connections = list(state._mcp_stacks.items())
|
||||
state._mcp_stacks.clear()
|
||||
statuses = _runtime_status_store(state)
|
||||
if statuses is not None:
|
||||
statuses.clear()
|
||||
for name, connection in connections:
|
||||
try:
|
||||
await connection.aclose()
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import hashlib
|
||||
import inspect
|
||||
from collections.abc import Callable, Iterable
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from contextlib import suppress
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
@@ -100,6 +100,7 @@ class ChannelManager:
|
||||
webui_static_dist: bool = True,
|
||||
webui_runtime_surface: str = "browser",
|
||||
webui_runtime_capabilities: dict[str, Any] | None = None,
|
||||
webui_mcp_runtime_status: Callable[[], Mapping[str, str]] | None = None,
|
||||
webui_skill_state_action: Callable[[set[str]], None] | None = None,
|
||||
config_path: Path | None = None,
|
||||
):
|
||||
@@ -119,6 +120,7 @@ class ChannelManager:
|
||||
self._webui_static_dist = webui_static_dist
|
||||
self._webui_runtime_surface = webui_runtime_surface
|
||||
self._webui_runtime_capabilities = dict(webui_runtime_capabilities or {})
|
||||
self._webui_mcp_runtime_status = webui_mcp_runtime_status
|
||||
self._webui_skill_state_action = webui_skill_state_action
|
||||
self.channels: dict[str, BaseChannel] = {}
|
||||
self._channel_owners: dict[str, str] = {}
|
||||
@@ -187,6 +189,7 @@ class ChannelManager:
|
||||
local_trigger_pending_ids=self._webui_local_trigger_pending_ids,
|
||||
channel_feature_action=self.apply_channel_feature_action,
|
||||
channel_runtime_status=self.get_status,
|
||||
mcp_runtime_status=self._webui_mcp_runtime_status,
|
||||
skill_state_action=self._webui_skill_state_action,
|
||||
logger=logger,
|
||||
)
|
||||
|
||||
@@ -668,6 +668,7 @@ def _run_gateway(
|
||||
webui_static_dist=webui_static_dist,
|
||||
webui_runtime_surface=webui_runtime_surface,
|
||||
webui_runtime_capabilities=webui_runtime_capabilities,
|
||||
webui_mcp_runtime_status=agent.mcp_runtime_status,
|
||||
webui_skill_state_action=_webui_skill_state_action,
|
||||
config_path=Path(config_path),
|
||||
)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Callable
|
||||
@@ -64,6 +65,7 @@ def build_gateway_services(
|
||||
local_trigger_pending_ids: Callable[[str], set[str]] | None = None,
|
||||
channel_feature_action: Callable[..., Any] | None = None,
|
||||
channel_runtime_status: Callable[[], dict[str, Any]] | None = None,
|
||||
mcp_runtime_status: Callable[[], Mapping[str, str]] | None = None,
|
||||
skill_state_action: Callable[[set[str]], None] | None = None,
|
||||
logger: Any = default_logger,
|
||||
) -> GatewayServices:
|
||||
@@ -116,6 +118,7 @@ def build_gateway_services(
|
||||
local_trigger_pending_ids=local_trigger_pending_ids,
|
||||
channel_feature_action=channel_feature_action,
|
||||
channel_runtime_status=channel_runtime_status,
|
||||
mcp_runtime_status=mcp_runtime_status,
|
||||
skill_state_action=skill_state_action,
|
||||
log=logger,
|
||||
)
|
||||
|
||||
@@ -60,7 +60,10 @@ _MAX_TEST_TOOLS = 16
|
||||
_DEFAULT_TEST_TIMEOUT = 20
|
||||
_DEFAULT_CUSTOM_TIMEOUT = 30
|
||||
_CUSTOM_ACTIONS = {"custom", "import", "import-cursor", "tools"}
|
||||
_MCP_RUNTIME_STATUSES = {"connecting", "connected", "failed"}
|
||||
|
||||
McpReload = Callable[[], Awaitable[dict[str, Any]]]
|
||||
McpRuntimeStatus = Callable[[], Mapping[str, str]]
|
||||
|
||||
|
||||
class McpPresetError(Exception):
|
||||
@@ -943,6 +946,7 @@ def mcp_presets_payload(
|
||||
*,
|
||||
last_action: dict[str, Any] | None = None,
|
||||
tool_preview: Mapping[str, list[str]] | None = None,
|
||||
runtime_status: Mapping[str, str] | None = None,
|
||||
config_path: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
config = load_config(config_path) if config_path is not None else load_config()
|
||||
@@ -970,7 +974,38 @@ def mcp_presets_payload(
|
||||
}
|
||||
if last_action is not None:
|
||||
payload["last_action"] = last_action
|
||||
return payload
|
||||
return attach_mcp_runtime_status(payload, runtime_status)
|
||||
|
||||
|
||||
def attach_mcp_runtime_status(
|
||||
payload: dict[str, Any],
|
||||
runtime_status: Mapping[str, str] | None,
|
||||
) -> dict[str, Any]:
|
||||
"""Project safe, connection-attempt state onto configured MCP rows."""
|
||||
if runtime_status is None:
|
||||
return payload
|
||||
projected = dict(payload)
|
||||
raw_rows: object = payload.get("presets", [])
|
||||
preset_rows = cast(list[object], raw_rows) if isinstance(raw_rows, list) else []
|
||||
rows: list[Any] = []
|
||||
for raw_row in preset_rows:
|
||||
if not isinstance(raw_row, dict):
|
||||
rows.append(raw_row)
|
||||
continue
|
||||
row = dict(cast(dict[str, Any], raw_row))
|
||||
name = row.get("name")
|
||||
status = runtime_status.get(name) if isinstance(name, str) else None
|
||||
if (
|
||||
status in _MCP_RUNTIME_STATUSES
|
||||
and row.get("installed") is True
|
||||
and row.get("configured") is True
|
||||
):
|
||||
row["runtime_status"] = status
|
||||
else:
|
||||
row.pop("runtime_status", None)
|
||||
rows.append(row)
|
||||
projected["presets"] = rows
|
||||
return projected
|
||||
|
||||
|
||||
def _display_name_for(name: str, preset: McpPreset | None = None) -> str:
|
||||
@@ -1003,6 +1038,7 @@ def _server_action_message(action: str, name: str, *, ok: bool = True) -> dict[s
|
||||
"import-cursor": "Imported",
|
||||
"tools": "Updated tools for",
|
||||
"remove": "Removed",
|
||||
"reconnect": "Retried connection for",
|
||||
}.get(action, "Updated")
|
||||
payload: dict[str, Any] = {
|
||||
"ok": ok,
|
||||
@@ -1017,6 +1053,24 @@ def _server_action_message(action: str, name: str, *, ok: bool = True) -> dict[s
|
||||
return payload
|
||||
|
||||
|
||||
def mcp_reconnect_action(
|
||||
query: QueryParams,
|
||||
*,
|
||||
config_path: Path | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Validate a configured server before asking the live runtime to retry it."""
|
||||
name = _validated_server_name((_query_first(query, "name") or "").strip())
|
||||
config = load_config(config_path) if config_path is not None else load_config()
|
||||
if name not in config.tools.mcp_servers:
|
||||
raise McpPresetError("unknown MCP server", status=404)
|
||||
payload = mcp_presets_payload(
|
||||
last_action=_server_action_message("reconnect", name),
|
||||
config_path=config_path,
|
||||
)
|
||||
payload["requires_restart"] = True
|
||||
return payload
|
||||
|
||||
|
||||
def _scrub_test_error(text: str) -> str:
|
||||
scrubbed = _SECRET_QUERY_RE.sub(r"\1<redacted>", text.strip())
|
||||
scrubbed = _SECRET_ASSIGNMENT_RE.sub(r"\1<redacted>", scrubbed)
|
||||
@@ -1571,12 +1625,16 @@ async def mcp_presets_settings_action(
|
||||
query: QueryParams,
|
||||
*,
|
||||
reload_mcp: McpReload | None = None,
|
||||
mcp_runtime_status: McpRuntimeStatus | None = None,
|
||||
config: WebUISettingsConfig | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Run a WebUI MCP preset action and hot-reload the agent when config changes."""
|
||||
config_path = config.path if config is not None else None
|
||||
if action is None:
|
||||
return mcp_presets_payload(config_path=config_path)
|
||||
return mcp_presets_payload(
|
||||
runtime_status=mcp_runtime_status() if mcp_runtime_status is not None else None,
|
||||
config_path=config_path,
|
||||
)
|
||||
name = (_query_first(query, "name") or "").strip()
|
||||
if name.startswith("plugin-"):
|
||||
plugin_config = load_config(config_path) if config_path is not None else load_config()
|
||||
@@ -1601,8 +1659,18 @@ async def mcp_presets_settings_action(
|
||||
payload = attach_mcp_hot_reload_result(payload, await reload_mcp())
|
||||
return payload
|
||||
if action == "test":
|
||||
return await mcp_presets_test_action(query, config_path=config_path)
|
||||
if config is not None:
|
||||
payload = await mcp_presets_test_action(query, config_path=config_path)
|
||||
return attach_mcp_runtime_status(
|
||||
payload,
|
||||
mcp_runtime_status() if mcp_runtime_status is not None else None,
|
||||
)
|
||||
if action == "reconnect":
|
||||
payload = await asyncio.to_thread(
|
||||
mcp_reconnect_action,
|
||||
query,
|
||||
config_path=config_path,
|
||||
)
|
||||
elif config is not None:
|
||||
operation = custom_mcp_action if action in _CUSTOM_ACTIONS else mcp_presets_action
|
||||
payload = await asyncio.to_thread(
|
||||
config.run_serialized,
|
||||
@@ -1614,4 +1682,7 @@ async def mcp_presets_settings_action(
|
||||
payload = await asyncio.to_thread(mcp_presets_action, action, query)
|
||||
if reload_mcp is not None:
|
||||
payload = attach_mcp_hot_reload_result(payload, await reload_mcp())
|
||||
return payload
|
||||
return attach_mcp_runtime_status(
|
||||
payload,
|
||||
mcp_runtime_status() if mcp_runtime_status is not None else None,
|
||||
)
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import html
|
||||
import json
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Mapping
|
||||
from typing import Any, cast
|
||||
|
||||
from websockets.http11 import Request as WsRequest
|
||||
@@ -94,6 +94,7 @@ _MCP_PRESET_ACTIONS_BY_PATH = {
|
||||
"/api/settings/mcp-presets/disable": "disable",
|
||||
"/api/settings/mcp-presets/remove": "remove",
|
||||
"/api/settings/mcp-presets/test": "test",
|
||||
"/api/settings/mcp-presets/reconnect": "reconnect",
|
||||
"/api/settings/mcp-presets/custom": "custom",
|
||||
"/api/settings/mcp-presets/import": "import",
|
||||
"/api/settings/mcp-presets/import-cursor": "import-cursor",
|
||||
@@ -225,6 +226,7 @@ class WebUISettingsRouter:
|
||||
runtime_capabilities: dict[str, Any],
|
||||
channel_feature_action: Callable[..., Any] | None = None,
|
||||
channel_runtime_status: Callable[[], dict[str, Any]] | None = None,
|
||||
mcp_runtime_status: Callable[[], Mapping[str, str]] | None = None,
|
||||
mcp_oauth_redirect_uri: Callable[[WsRequest], str] | None = None,
|
||||
) -> None:
|
||||
self.settings = settings
|
||||
@@ -238,6 +240,7 @@ class WebUISettingsRouter:
|
||||
self._runtime_capabilities = runtime_capabilities
|
||||
self._channel_feature_action = channel_feature_action
|
||||
self._channel_runtime_status = channel_runtime_status
|
||||
self._mcp_runtime_status = mcp_runtime_status
|
||||
self._mcp_oauth_redirect_uri = mcp_oauth_redirect_uri
|
||||
self._mcp_oauth = McpOAuthManager()
|
||||
self._restart_sections: set[str] = set()
|
||||
@@ -470,6 +473,7 @@ class WebUISettingsRouter:
|
||||
deny_code=deny_code,
|
||||
mcp_presets_action=mcp_presets_settings_action,
|
||||
reload_mcp=lambda: request_mcp_reload(self.bus),
|
||||
mcp_runtime_status=self._mcp_runtime_status,
|
||||
check_for_update=check_for_update,
|
||||
channel_feature_action=self._channel_feature_action,
|
||||
channel_runtime_status=self._channel_runtime_status,
|
||||
|
||||
@@ -6,7 +6,7 @@ import asyncio
|
||||
import inspect
|
||||
import re
|
||||
import time
|
||||
from collections.abc import Callable, Iterable
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, TypedDict, cast
|
||||
@@ -55,6 +55,7 @@ class SystemSettingsOperations:
|
||||
deny_code: SettingsOperation
|
||||
mcp_presets_action: SettingsOperation
|
||||
reload_mcp: SettingsOperation
|
||||
mcp_runtime_status: Callable[[], Mapping[str, str]] | None
|
||||
check_for_update: SettingsOperation
|
||||
channel_feature_action: SettingsOperation | None = None
|
||||
channel_runtime_status: Callable[[], dict[str, Any]] | None = None
|
||||
@@ -928,6 +929,7 @@ class SystemSettingsHandler:
|
||||
action,
|
||||
request.query,
|
||||
reload_mcp=operations.reload_mcp,
|
||||
mcp_runtime_status=operations.mcp_runtime_status,
|
||||
config=self.settings.config,
|
||||
)
|
||||
except Exception as exc:
|
||||
|
||||
@@ -14,7 +14,7 @@ import json
|
||||
import mimetypes
|
||||
import re
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Mapping
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
from urllib.parse import quote, unquote, urlsplit, urlunsplit
|
||||
@@ -163,6 +163,7 @@ _WEBUI_MUTATION_PATHS = {
|
||||
"settings.mcp.disable": "/api/settings/mcp-presets/disable",
|
||||
"settings.mcp.remove": "/api/settings/mcp-presets/remove",
|
||||
"settings.mcp.test": "/api/settings/mcp-presets/test",
|
||||
"settings.mcp.reconnect": "/api/settings/mcp-presets/reconnect",
|
||||
"settings.mcp.custom": "/api/settings/mcp-presets/custom",
|
||||
"settings.mcp.import": "/api/settings/mcp-presets/import",
|
||||
"settings.mcp.import_cursor": "/api/settings/mcp-presets/import-cursor",
|
||||
@@ -306,6 +307,7 @@ class GatewayHTTPHandler:
|
||||
local_trigger_pending_ids: Callable[[str], set[str]] | None = None,
|
||||
channel_feature_action: Callable[..., Any] | None = None,
|
||||
channel_runtime_status: Callable[[], dict[str, Any]] | None = None,
|
||||
mcp_runtime_status: Callable[[], Mapping[str, str]] | None = None,
|
||||
skill_state_action: Callable[[set[str]], None] | None = None,
|
||||
log: Any = logger,
|
||||
) -> None:
|
||||
@@ -348,6 +350,7 @@ class GatewayHTTPHandler:
|
||||
runtime_capabilities=self._capabilities,
|
||||
channel_feature_action=channel_feature_action,
|
||||
channel_runtime_status=channel_runtime_status,
|
||||
mcp_runtime_status=mcp_runtime_status,
|
||||
mcp_oauth_redirect_uri=self._mcp_oauth_redirect_uri,
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user