mcp nice loading

frdel committed Jun 4, 2025 at 17:24 UTC 01d1002dfbdae8a914c42441e0bc7658dd16ebd6
4 files changed +61 -36
python/api/mcp_servers_apply.py
+5 -1
@@ -1,9 +1,10 @@
1 +import time
2 from python.helpers.api import ApiHandler
3 from flask import Request, Response
4
5 from typing import Any
6
6 -# from python.helpers.mcp_handler import MCPConfig
7 +from python.helpers.mcp_handler import MCPConfig
8 from python.helpers.settings import set_settings_delta
9
10
@@ -14,6 +15,9 @@ class McpServersApply(ApiHandler):
15 # MCPConfig.update(mcp_servers) # done in settings automatically
16 set_settings_delta({"mcp_servers": "[]"}) # to force reinitialization
17 set_settings_delta({"mcp_servers": mcp_servers})
18 +
19 + time.sleep(1) # wait at least a second
20 + MCPConfig.wait_for_lock() # wait until config lock is released
21
22 except Exception as e:
23 return {"success": False, "error": str(e)}
python/helpers/mcp_handler.py
+5
@@ -377,6 +377,11 @@ class MCPConfig(BaseModel):
377 cls.__instance = cls(servers_list=[])
378 return cls.__instance
379
380 + @classmethod
381 + def wait_for_lock(cls):
382 + with cls.__lock:
383 + return
384 +
385 @classmethod
386 def update(cls, config_str: str) -> Any:
387 with cls.__lock:
webui/components/settings/mcp/client/mcp-servers-store.js
+9 -3
@@ -6,7 +6,7 @@ import * as API from "/js/api.js";
6 const model = {
7 editor: null,
8 servers: [],
9 - loading: false,
9 + loading: true,
10 statusCheck: false,
11 serverLog: "",
12
@@ -72,9 +72,14 @@ const model = {
72
73 async startStatusCheck() {
74 this.statusCheck = true;
75 + const firstLoad = true;
76
77 while (this.statusCheck) {
78 await this._statusCheck();
79 + if (firstLoad) {
80 + this.loading = false;
81 + firstLoad = false;
82 + }
83 await sleep(3000);
84 }
85 },
@@ -99,8 +104,9 @@ const model = {
104 await API.callJsonApi("mcp_servers_apply", {
105 mcp_servers: this.getEditorValue(),
106 });
102 - await sleep(5000); // just to prevent user from clicking apply multiple times
103 - // scrollModal("mcp-servers-status");
107 + // await sleep(5000); // just to prevent user from clicking apply multiple times
108 + // api now waits for config locks automatically
109 + scrollModal("mcp-servers-status");
110 } catch (error) {
111 console.error("Failed to apply MCP servers:", error);
112 alert("Failed to apply MCP servers: " + error.message);
webui/components/settings/mcp/client/mcp-servers.html
+42 -32
@@ -14,7 +14,8 @@
14 <div x-init="$store.mcpServersStore.initialize()" x-destroy="$store.mcpServersStore.onClose()">
15
16 <h3>MCP Servers Configuration JSON
17 - <button class="btn slim" style="margin-left: 0.5em;" onclick="openModal('settings/mcp/client/example.html')">Examples</button>
17 + <button class="btn slim" style="margin-left: 0.5em;"
18 + onclick="openModal('settings/mcp/client/example.html')">Examples</button>
19 <button class="btn slim" style="margin-left: 0.5em;"
20 @click="$store.mcpServersStore.formatJson()">Reformat</button>
21 <button class="btn slim primary" :disabled="$store.mcpServersStore.loading"
@@ -24,57 +25,64 @@
25
26 <h3 id="mcp-servers-status">Servers status (refreshing automatically)</h3>
27
27 - <div class="server-list">
28 +
29 + <div class="server-list" x-show="!$store.mcpServersStore.loading">
30 <template x-for="server in $store.mcpServersStore.servers" :key="server.name">
31 <div class="server-item">
32 <div class="server-main-row">
33 <!-- Status indicator -->
34 <div class="status-dot" x-data="{ connected: server.connected }">
35 <svg viewBox="0 0 16 16" width="12" height="12">
34 - <circle cx="8" cy="8" r="6"
35 - x-bind:fill="server.connected
36 + <circle cx="8" cy="8" r="6" x-bind:fill="server.connected
37 ? (server.error ? '#e40138' : (server.tool_count > 0 ? '#00c340' : '#e40138'))
37 - : 'none'"
38 - x-bind:opacity="server.connected ? 1 : 0" />
39 - <circle cx="8" cy="8" r="6" fill="none"
40 - stroke="#e40138" stroke-width="2"
38 + : 'none'" x-bind:opacity="server.connected ? 1 : 0" />
39 + <circle cx="8" cy="8" r="6" fill="none" stroke="#e40138" stroke-width="2"
40 x-bind:opacity="server.connected ? 0 : 1" />
41 </svg>
42 </div>
44 -
43 +
44 <!-- Server name -->
45 <span class="server-name" x-text="server.name"></span>
47 -
46 +
47 <!-- Tool count (clickable if > 0, only for connected servers without errors) -->
49 - <span class="tool-count"
50 - x-show="server.tool_count > 0"
51 - @click="$store.mcpServersStore.onToolCountClick && $store.mcpServersStore.onToolCountClick(server.name)"
52 - x-text="server.tool_count + ' tools'"></span>
53 -
48 + <span class="tool-count" x-show="server.tool_count > 0"
49 + @click="$store.mcpServersStore.onToolCountClick && $store.mcpServersStore.onToolCountClick(server.name)"
50 + x-text="server.tool_count + ' tools'"></span>
51 +
52 <!-- Disabled in config message -->
53 <span class="config-status" x-show="server.disabled_in_config">Disabled in config</span>
56 -
54 +
55 <!-- Log button (only shown if has_log is true) -->
58 - <span class="log-btn" x-show="server.has_log" @click="$store.mcpServersStore.getServerLog(server.name)">Log</span>
56 + <span class="log-btn" x-show="server.has_log"
57 + @click="$store.mcpServersStore.getServerLog(server.name)">Log</span>
58 </div>
60 -
59 +
60 <!-- Error message (if any) -->
61 <div class="server-error-row" x-show="server.error">
62 <span class="server-error" x-text="server.error"></span>
63 </div>
64 </div>
65 </template>
67 - <div x-show="$store.mcpServersStore.servers.length === 0" class="no-servers">
66 + <div x-show="$store.mcpServersStore.servers.length === 0" class="mcp-servers-loading">
67 No servers
68 </div>
69 </div>
70
72 -
71 + <div x-show="$store.mcpServersStore.loading" class="mcp-servers-loading">
72 + Loading servers status...
73 + </div>
74 </div>
75 </template>
76 </div>
77
78 <style>
79 +
80 + .mcp-servers-loading {
81 + width: 100%;
82 + text-align: center;
83 + margin-top: 2rem;
84 + margin-bottom: 2rem;
85 + }
86 #mcp-servers-config-json {
87 width: 100%;
88 height: 40em;
@@ -90,8 +98,10 @@
98 flex-direction: column;
99 padding: 0.5em 0.7em;
100 margin-bottom: 0.4em;
93 - min-height: 2.2em; /* Ensure consistent height even without errors */
94 - border: 1px solid rgba(192, 192, 192, 0.161); /* Silver with 30% opacity */
101 + min-height: 2.2em;
102 + /* Ensure consistent height even without errors */
103 + border: 1px solid rgba(192, 192, 192, 0.161);
104 + /* Silver with 30% opacity */
105 border-radius: 4px;
106 }
107
@@ -102,7 +112,7 @@
112 flex-direction: column;
113 gap: 0.2em;
114 }
105 -
115 +
116 .server-main-row {
117 display: flex;
118 align-items: center;
@@ -115,33 +125,33 @@
125 align-items: center;
126 justify-content: center;
127 }
118 -
128 +
129 .server-name {
130 font-weight: 600;
131 min-width: 12em;
132 }
123 -
133 +
134 .tool-count {
135 color: var(--c-fg2);
136 font-size: 0.9em;
137 user-select: none;
138 }
129 -
139 +
140 .tool-count {
141 cursor: default;
142 }
133 -
143 +
144 .tool-count:hover {
145 opacity: 0.8;
146 cursor: pointer;
147 }
138 -
148 +
149 .config-status {
150 color: #e40138;
151 font-size: 0.85em;
152 opacity: 0.8;
153 }
144 -
154 +
155 .log-btn {
156 margin-left: auto;
157 font-size: 0.9em;
@@ -149,11 +159,11 @@
159 text-decoration: none;
160 opacity: 0.85;
161 }
152 -
162 +
163 .log-btn:hover {
164 opacity: 1;
165 }
156 -
166 +
167 .server-error-row {
168 margin-left: 1.8em;
169 margin-top: 0.1em;
@@ -162,7 +172,7 @@
172 opacity: 0.85;
173 line-height: 1.2;
174 }
165 -
175 +
176 .no-servers {
177 padding: 0.5em;
178 color: var(--c-fg2);