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);