Polish MCP server removal flow

Use the shared inline confirmation helper for configured MCP server removal and keep the delete icon neutral until confirmation.\n\nApply confirmed removals immediately, refresh server status from the apply response, guard against stale/double actions, and document the new MCP manager behavior.

Alessandro committed Jun 11, 2026 at 03:04 UTC da4fb47d0b8cade0d1ce0c1b6784dee3aed5dde2
3 files changed +36 -10
webui/components/settings/AGENTS.md
+1
@@ -16,6 +16,7 @@
16 - Do not store secrets in localStorage, URLs, or console output.
17 - Preserve Store Gating and modal footer conventions in settings components.
18 - MCP manager tool toggles write `disabled_tools` into the draft JSON and require Apply before changing the running MCP tool set.
19 +- Confirmed MCP server removals apply immediately and refresh server status; other MCP manager draft edits still require Apply.
20
21 ## Work Guidance
22
webui/components/settings/mcp/client/mcp-servers-store.js
+29 -7
@@ -865,15 +865,33 @@ const model = {
865 requestAnimationFrame(() => globalThis.scrollModal?.("mcp-add-server"));
866 },
867
868 - removeConfigServer(name) {
868 + async removeConfigServer(name) {
869 try {
870 const config = this.getConfigObject();
871 + const normalized = normalizeName(name);
872 + let removed = false;
873 +
874 if (Array.isArray(config.mcpServers)) {
872 - config.mcpServers = config.mcpServers.filter((server) => normalizeName(server?.name || "") !== normalizeName(name));
873 - } else {
874 - delete config.mcpServers[name];
875 + const nextServers = config.mcpServers.filter((server) => normalizeName(server?.name || "") !== normalized);
876 + removed = nextServers.length !== config.mcpServers.length;
877 + config.mcpServers = nextServers;
878 + } else if (config.mcpServers && typeof config.mcpServers === "object") {
879 + const key = Object.keys(config.mcpServers).find((serverName) => normalizeName(serverName) === normalized);
880 + if (key) {
881 + delete config.mcpServers[key];
882 + removed = true;
883 + }
884 }
885 +
886 + if (!removed) {
887 + void toastFrontendWarning("MCP server is no longer in this config.", "MCP Servers");
888 + await this.loadStatus({ silent: true });
889 + return;
890 + }
891 +
892 this.setEditorValue(stringifyConfig(config));
893 + if (normalizeName(this.serverForm.name) === normalized) this.resetForm();
894 + await this.applyNow({ successMessage: "MCP server removed" });
895 } catch (error) {
896 void toastFrontendError(`Failed to remove MCP server: ${error.message || error}`, "MCP Servers");
897 }
@@ -995,7 +1013,7 @@ const model = {
1013 this.statusCheck = false;
1014 },
1015
998 - async applyNow() {
1016 + async applyNow(options = {}) {
1017 if (this.applying) return;
1018 try {
1019 const formatted = stringifyConfig(this.getConfigObject());
@@ -1012,9 +1030,13 @@ const model = {
1030 this.setScopeConfigJson(resp.mcp_servers || this.getEditorValue());
1031 this.servers = resp.status || [];
1032 this.servers.sort((a, b) => String(a.name || "").localeCompare(String(b.name || "")));
1015 - void toastFrontendSuccess("MCP servers applied", "MCP Servers");
1033 + if (options.successMessage !== false) {
1034 + void toastFrontendSuccess(options.successMessage || "MCP servers applied", "MCP Servers");
1035 + }
1036 await sleep(100);
1017 - if (globalThis.scrollModal) globalThis.scrollModal("mcp-servers-status");
1037 + if (options.scrollToStatus !== false && globalThis.scrollModal) {
1038 + globalThis.scrollModal("mcp-servers-status");
1039 + }
1040 } catch (error) {
1041 console.error("Failed to apply MCP servers:", error);
1042 void toastFrontendError(`Failed to apply MCP servers: ${error.message || error}`, "MCP Servers");
webui/components/settings/mcp/client/mcp-servers.html
+6 -3
@@ -94,7 +94,9 @@
94 </label>
95 <span class="plugin-status-text" x-text="entry.config.disabled ? 'OFF' : 'ON'"></span>
96 </div>
97 - <button type="button" class="mcp-icon-button danger" title="Remove" @click="$store.mcpServersStore.removeConfigServer(entry.name)">
97 + <button type="button" class="mcp-icon-button" title="Remove"
98 + :disabled="$store.mcpServersStore.applying"
99 + @click="$confirmClick($event, () => $store.mcpServersStore.removeConfigServer(entry.name))">
100 <span class="material-symbols-outlined" aria-hidden="true">delete</span>
101 </button>
102 </div>
@@ -704,8 +706,9 @@
706 cursor: pointer;
707 }
708
707 - .mcp-icon-button.danger {
708 - color: var(--color-error-text);
709 + .mcp-icon-button:disabled {
710 + opacity: 0.55;
711 + cursor: not-allowed;
712 }
713
714 .mcp-raw-toolbar {