Add web UI for model and MCP server configuration#11
Conversation
- Settings modal (gear icon) in the web channel: change the runtime model and enable/disable MCP servers with live toggle switches - Slash-command autocomplete palette: typing / in the input lists all registered commands with descriptions (arrow keys, Tab/Enter, click) - New REST endpoints: GET /api/commands, GET|POST /api/model, GET|POST /api/mcp/servers - MCPManager: runtime enable_server()/disable_server() that connect or disconnect clients, update the tool catalog, and persist the disabled flag back to mcp_servers.json - New /mcp enable <server> and /mcp disable <server> subcommands available on all channels - WebChannel.set_command_registry() wired in bg_server so the UI can list the BackgroundAgent's commands https://claude.ai/code/session_01BY92TUwnhiWT5pMoZC6Hu2
There was a problem hiding this comment.
Code Review
This pull request introduces runtime enabling and disabling of MCP servers, a settings panel in the web UI for toggling servers and changing the active model, and a slash-command autocomplete palette. The feedback highlights a critical concurrency issue in MCPManager where concurrent calls to enable_server and disable_server can cause race conditions. It is recommended to use an asyncio.Lock to serialize these operations and ensure consistency between the in-memory state and the persistent configuration file.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| self._clients: dict[str, Client] = {} | ||
| self._specs: dict[str, dict] = {} | ||
| self._server_configs: dict[str, dict] = {} | ||
| self._config_path: Path | None = None |
There was a problem hiding this comment.
Initialize an asyncio.Lock to serialize enable_server and disable_server operations, preventing race conditions and inconsistent states between the in-memory configuration and the persistent file.
| self._config_path: Path | None = None | |
| self._config_path: Path | None = None | |
| self._lock = asyncio.Lock() |
| async def enable_server(self, name: str) -> dict: | ||
| """Connect a configured server at runtime and clear its disabled flag.""" | ||
| cfg = self._server_configs.get(name) | ||
| if cfg is None: | ||
| raise ValueError(f"Unknown MCP server '{name}'") | ||
| cfg.pop("disabled", None) | ||
| if name not in self._clients: | ||
| await self._connect_server(name, cfg) | ||
| self._persist_disabled(name, False) | ||
| return self._status_for(name) | ||
|
|
||
| async def disable_server(self, name: str) -> dict: | ||
| """Disconnect a server at runtime, drop its tools, and set its disabled flag.""" | ||
| cfg = self._server_configs.get(name) | ||
| if cfg is None: | ||
| raise ValueError(f"Unknown MCP server '{name}'") | ||
| client = self._clients.pop(name, None) | ||
| if client is not None: | ||
| try: | ||
| await client.__aexit__(None, None, None) | ||
| except Exception as e: | ||
| log.warning(f"MCP server '{name}': error during disconnect — {e}") | ||
| prefix = f"{name}{self._SEP}" | ||
| for key in [k for k in self._specs if k.startswith(prefix)]: | ||
| del self._specs[key] | ||
| cfg["disabled"] = True | ||
| self._persist_disabled(name, True) | ||
| return self._status_for(name) |
There was a problem hiding this comment.
There is a potential race condition when enable_server and disable_server are called concurrently for the same server. Because both methods contain await points (await self._connect_server and await client.__aexit__) before modifying the in-memory configuration and persisting it to the file, concurrent execution can lead to inconsistent states (e.g., the in-memory config and the persistent file state getting out of sync).
To prevent this, serialize these operations using async with self._lock: (using the lock initialized in __init__).
async def enable_server(self, name: str) -> dict:
"""Connect a configured server at runtime and clear its disabled flag."""
async with self._lock:
cfg = self._server_configs.get(name)
if cfg is None:
raise ValueError(f"Unknown MCP server '{name}'")
cfg.pop("disabled", None)
if name not in self._clients:
await self._connect_server(name, cfg)
self._persist_disabled(name, False)
return self._status_for(name)
async def disable_server(self, name: str) -> dict:
"""Disconnect a server at runtime, drop its tools, and set its disabled flag."""
async with self._lock:
cfg = self._server_configs.get(name)
if cfg is None:
raise ValueError(f"Unknown MCP server '{name}'")
client = self._clients.pop(name, None)
if client is not None:
try:
await client.__aexit__(None, None, None)
except Exception as e:
log.warning(f"MCP server '{name}': error during disconnect — {e}")
prefix = f"{name}{self._SEP}"
for key in [k for k in self._specs if k.startswith(prefix)]:
del self._specs[key]
cfg["disabled"] = True
self._persist_disabled(name, True)
return self._status_for(name)There was a problem hiding this comment.
Pull request overview
This PR adds a web UI and supporting APIs to configure the runtime model and toggle MCP servers on/off, and extends the /mcp command to support runtime enable/disable across channels. It also wires the web channel to the BackgroundAgent’s command registry so the browser can show slash-command autocomplete.
Changes:
- Added REST endpoints to the web channel for commands listing, model get/set, and MCP server status/toggling.
- Implemented runtime
MCPManager.enable_server()/disable_server()with persistence back tomcp_servers.json. - Added a slash-command autocomplete palette and a settings modal (model + MCP toggles) to the web UI.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/test_web_channel.py | Adds tests for new web REST endpoints and verifies settings/palette UI elements and JS wiring. |
| tests/test_mcp_manager.py | Adds unit tests for MCPManager enable/disable behavior, persistence, and status reporting. |
| tests/test_main.py | Updates MCP initialization test to assert config_path is passed into mcp_manager.initialize(). |
| tests/test_commands.py | Adds tests for /mcp enable and /mcp disable command behavior and error messaging. |
| README.md | Documents the new slash-command palette and settings panel, plus runtime MCP toggling behavior. |
| CLAUDE.md | Updates architecture notes to include runtime MCP toggling and new WebChannel endpoints/UI. |
| app/main.py | Passes config_path into MCP initialization so runtime toggles can persist to the same file. |
| app/core/mcp_manager.py | Adds enable/disable operations, status helper, and persistence of the disabled flag. |
| app/core/background_agent.py | Updates the /mcp command description to reflect new subcommands. |
| app/channels/web_channel.py | Adds settings/palette markup, new REST endpoints, and a command-registry attachment hook. |
| app/channels/static/web_channel.js | Implements slash-command palette UX and settings modal behavior (model + MCP toggles). |
| app/channels/static/web_channel.css | Styles the new palette and settings modal UI elements. |
| app/channels/commands.py | Extends /mcp to support enable/disable subcommands in addition to status/tools. |
| app/bg_server.py | Wires WebChannel to the BackgroundAgent registry so /api/commands can list commands. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if subcmd in ("enable", "disable"): | ||
| if not subargs: | ||
| return f"Usage: /mcp {subcmd} <server>" | ||
| try: | ||
| if subcmd == "enable": | ||
| status = await mcp_manager.enable_server(subargs) | ||
| else: | ||
| status = await mcp_manager.disable_server(subargs) | ||
| except ValueError as e: | ||
| configured = [s["name"] for s in mcp_manager.get_server_status()] | ||
| return f"{e}. Configured: {', '.join(configured) or 'none'}" | ||
| if status["disabled"]: | ||
| state = "disabled" | ||
| elif status["connected"]: | ||
| state = f"enabled and connected ({status['tool_count']} tool(s))" | ||
| else: | ||
| state = "enabled but failed to connect (check logs)" | ||
| return f"MCP server '{subargs}' is now {state}." |
| with open(self._config_path, "w", encoding="utf-8") as f: | ||
| json.dump(data, f, indent=2) | ||
| f.write("\n") |
model and enable/disable MCP servers with live toggle switches
registered commands with descriptions (arrow keys, Tab/Enter, click)
GET|POST /api/mcp/servers
disconnect clients, update the tool catalog, and persist the disabled
flag back to mcp_servers.json
available on all channels
list the BackgroundAgent's commands
https://claude.ai/code/session_01BY92TUwnhiWT5pMoZC6Hu2