clay-server 2.28.0-beta.1 → 2.28.0-beta.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -227,6 +227,210 @@
227
227
  margin: -8px 0 20px;
228
228
  }
229
229
 
230
+ /* MCP Servers Modal */
231
+ #mcp-modal { position: fixed; inset: 0; z-index: 300; display: flex; align-items: center; justify-content: center; }
232
+ #mcp-modal.hidden { display: none; }
233
+ .mcp-dialog {
234
+ width: 480px;
235
+ max-width: 94vw;
236
+ max-height: 70vh;
237
+ display: flex;
238
+ flex-direction: column;
239
+ padding: 0;
240
+ overflow: hidden;
241
+ }
242
+ .mcp-header {
243
+ display: flex;
244
+ align-items: center;
245
+ justify-content: space-between;
246
+ padding: 16px 20px;
247
+ border-bottom: 1px solid var(--border);
248
+ }
249
+ .mcp-title {
250
+ display: flex;
251
+ align-items: center;
252
+ gap: 8px;
253
+ font-size: 16px;
254
+ font-weight: 700;
255
+ color: var(--text);
256
+ }
257
+ .mcp-title .lucide { width: 18px; height: 18px; }
258
+ .mcp-content {
259
+ padding: 16px 20px;
260
+ overflow-y: auto;
261
+ flex: 1;
262
+ }
263
+ .mcp-desc {
264
+ font-size: 13px;
265
+ color: var(--text-muted);
266
+ margin: 0 0 16px;
267
+ line-height: 1.5;
268
+ }
269
+ .mcp-empty {
270
+ text-align: center;
271
+ padding: 32px 16px;
272
+ color: var(--text-muted);
273
+ }
274
+ .mcp-empty .lucide { width: 32px; height: 32px; margin-bottom: 12px; opacity: 0.4; }
275
+ .mcp-empty-title {
276
+ font-size: 15px;
277
+ font-weight: 600;
278
+ color: var(--text-secondary);
279
+ margin: 0 0 6px;
280
+ }
281
+ .mcp-empty-desc {
282
+ font-size: 13px;
283
+ color: var(--text-muted);
284
+ margin: 0;
285
+ line-height: 1.5;
286
+ }
287
+ .mcp-ext-setup-btn {
288
+ display: inline-flex;
289
+ align-items: center;
290
+ gap: 6px;
291
+ padding: 7px 14px;
292
+ margin-top: 14px;
293
+ font-size: 13px;
294
+ font-weight: 500;
295
+ color: var(--text);
296
+ background: var(--bg-hover, rgba(255,255,255,0.06));
297
+ border: 1px solid var(--border);
298
+ border-radius: 8px;
299
+ cursor: pointer;
300
+ transition: background 0.15s;
301
+ }
302
+ .mcp-ext-setup-btn:hover {
303
+ background: var(--bg-active, rgba(255,255,255,0.1));
304
+ }
305
+ .mcp-ext-setup-btn .lucide {
306
+ width: 14px;
307
+ height: 14px;
308
+ }
309
+ .mcp-install-cmd-row {
310
+ display: flex;
311
+ align-items: center;
312
+ gap: 6px;
313
+ margin-top: 12px;
314
+ max-width: 100%;
315
+ }
316
+ .mcp-install-cmd {
317
+ flex: 1;
318
+ padding: 8px 10px;
319
+ background: var(--bg-deep, rgba(0,0,0,0.25));
320
+ border: 1px solid var(--border);
321
+ border-radius: 6px;
322
+ font-family: "SF Mono", Monaco, "Cascadia Code", monospace;
323
+ font-size: 12px;
324
+ color: var(--text);
325
+ overflow-x: auto;
326
+ white-space: nowrap;
327
+ line-height: 1.4;
328
+ }
329
+ .mcp-install-copy-btn {
330
+ flex-shrink: 0;
331
+ padding: 6px 8px;
332
+ background: var(--bg-hover, rgba(255,255,255,0.06));
333
+ border: 1px solid var(--border);
334
+ border-radius: 6px;
335
+ color: var(--text-muted);
336
+ cursor: pointer;
337
+ transition: background 0.15s;
338
+ }
339
+ .mcp-install-copy-btn:hover {
340
+ background: var(--bg-active, rgba(255,255,255,0.1));
341
+ }
342
+ .mcp-install-copy-btn .lucide {
343
+ width: 14px;
344
+ height: 14px;
345
+ }
346
+ .mcp-steps {
347
+ display: flex;
348
+ flex-direction: column;
349
+ gap: 2px;
350
+ }
351
+ .mcp-step {
352
+ display: flex;
353
+ gap: 12px;
354
+ padding: 12px 14px;
355
+ border-radius: 8px;
356
+ transition: opacity 0.15s;
357
+ }
358
+ .mcp-step.disabled {
359
+ opacity: 0.4;
360
+ pointer-events: none;
361
+ }
362
+ .mcp-step-icon {
363
+ flex-shrink: 0;
364
+ width: 22px;
365
+ height: 22px;
366
+ margin-top: 1px;
367
+ color: var(--text-muted);
368
+ }
369
+ .mcp-step-icon.done {
370
+ color: var(--green, #2ecc71);
371
+ }
372
+ .mcp-step-icon .lucide {
373
+ width: 20px;
374
+ height: 20px;
375
+ }
376
+ .mcp-step-body {
377
+ flex: 1;
378
+ min-width: 0;
379
+ }
380
+ .mcp-step-title {
381
+ font-size: 13px;
382
+ font-weight: 600;
383
+ color: var(--text);
384
+ margin-bottom: 2px;
385
+ }
386
+ .mcp-step-desc {
387
+ font-size: 12px;
388
+ color: var(--text-muted);
389
+ line-height: 1.4;
390
+ }
391
+ .mcp-step.done .mcp-step-desc {
392
+ color: var(--green, #2ecc71);
393
+ }
394
+ .mcp-divider {
395
+ height: 1px;
396
+ background: var(--border);
397
+ margin: 12px 0;
398
+ }
399
+ .mcp-server-row {
400
+ display: flex;
401
+ align-items: center;
402
+ gap: 12px;
403
+ padding: 10px 0;
404
+ border-bottom: 1px solid var(--border);
405
+ cursor: pointer;
406
+ transition: background 0.1s;
407
+ }
408
+ .mcp-server-row:last-child { border-bottom: none; }
409
+ .mcp-server-row:hover { background: var(--bg-dim); margin: 0 -20px; padding: 10px 20px; }
410
+ .mcp-server-row input[type="checkbox"] {
411
+ width: 16px;
412
+ height: 16px;
413
+ margin: 0;
414
+ cursor: pointer;
415
+ flex-shrink: 0;
416
+ }
417
+ .mcp-server-info {
418
+ display: flex;
419
+ flex-direction: column;
420
+ gap: 2px;
421
+ flex: 1;
422
+ min-width: 0;
423
+ }
424
+ .mcp-server-name {
425
+ font-size: 14px;
426
+ font-weight: 500;
427
+ color: var(--text);
428
+ }
429
+ .mcp-server-meta {
430
+ font-size: 12px;
431
+ color: var(--text-muted);
432
+ }
433
+
230
434
  /* Beta toggle row inside settings card */
231
435
  .ps-beta-toggle-row,
232
436
  .ss-beta-toggle-row {
@@ -191,6 +191,7 @@
191
191
  <button id="file-browser-btn"><i data-lucide="folder-tree"></i> <span>File browser</span></button>
192
192
  <button id="terminal-sidebar-btn"><i data-lucide="square-terminal"></i> <span>Terminal</span><span id="terminal-sidebar-count" class="sidebar-badge hidden"></span></button>
193
193
  <button id="sticky-notes-sidebar-btn"><i data-lucide="sticky-note"></i> <span>Sticky Notes</span><span id="sticky-notes-sidebar-count" class="sidebar-badge hidden"></span></button>
194
+ <button id="mcp-btn"><i data-lucide="cable"></i> <span>MCP Servers</span><span id="mcp-sidebar-count" class="sidebar-badge hidden"></span></button>
194
195
  <button id="skills-btn"><i data-lucide="puzzle"></i> <span>Skills</span></button>
195
196
  <button id="scheduler-btn"><i data-lucide="calendar-clock"></i> <span>Scheduled Tasks</span></button>
196
197
  </div>
@@ -255,6 +256,7 @@
255
256
  <button id="mate-memory-btn"><i data-lucide="brain"></i> <span>Memory</span><span id="mate-memory-count" class="sidebar-badge hidden"></span></button>
256
257
  <button id="mate-knowledge-btn"><i data-lucide="book-open"></i> <span>Knowledge</span><span id="mate-knowledge-count" class="sidebar-badge hidden"></span></button>
257
258
  <button id="mate-sticky-notes-btn"><i data-lucide="sticky-note"></i> <span>Sticky Notes</span></button>
259
+ <button id="mate-mcp-btn"><i data-lucide="cable"></i> <span>MCP Servers</span><span id="mate-mcp-sidebar-count" class="sidebar-badge hidden"></span></button>
258
260
  <button id="mate-skills-btn"><i data-lucide="puzzle"></i> <span>Skills</span></button>
259
261
  <button id="mate-scheduler-btn"><i data-lucide="calendar-clock"></i> <span>Scheduled Tasks</span></button>
260
262
  </div>
@@ -1507,6 +1509,20 @@
1507
1509
  </div>
1508
1510
  </div>
1509
1511
 
1512
+ <!-- MCP Servers Modal -->
1513
+ <div id="mcp-modal" class="hidden">
1514
+ <div class="confirm-backdrop"></div>
1515
+ <div class="confirm-dialog mcp-dialog">
1516
+ <div class="mcp-header">
1517
+ <span class="mcp-title"><i data-lucide="cable"></i> MCP Servers</span>
1518
+ <button class="skills-close" id="mcp-modal-close"><i data-lucide="x"></i></button>
1519
+ </div>
1520
+ <div class="mcp-content" id="mcp-content">
1521
+ <p class="mcp-no-servers">No MCP servers detected. Configure in Clay Chrome Extension.</p>
1522
+ </div>
1523
+ </div>
1524
+ </div>
1525
+
1510
1526
  <!-- Debate Modal (Wizard) -->
1511
1527
  <div id="debate-modal" class="hidden">
1512
1528
  <div class="debate-modal-backdrop"></div>
@@ -31,7 +31,8 @@ import { handleSkillInstalled, handleSkillUninstalled } from './skills.js';
31
31
  import { showRewindModal, onRewindComplete, setRewindMode, onRewindError, clearPendingRewindUuid, addRewindButton } from './rewind.js';
32
32
  import { checkAdminAccess } from './admin.js';
33
33
  import { mateAvatarUrl } from './avatar.js';
34
- import { showImageModal, sendExtensionCommand } from './app-misc.js';
34
+ import { showImageModal, sendExtensionCommand, handleMcpToolCallMessage } from './app-misc.js';
35
+ import { handleMcpServersState } from './mcp-ui.js';
35
36
  import { handleLoopRegistryUpdated, handleScheduleRunStarted, handleScheduleRunFinished, handleLoopScheduled, isSchedulerOpen, enterCraftingMode, exitCraftingMode, handleLoopRegistryFiles } from './scheduler.js';
36
37
 
37
38
  // --- App module imports ---
@@ -999,6 +1000,14 @@ export function processMessage(msg) {
999
1000
  sendExtensionCommand(msg.command, msg.args, msg.requestId);
1000
1001
  break;
1001
1002
 
1003
+ case "mcp_tool_call":
1004
+ handleMcpToolCallMessage(msg);
1005
+ break;
1006
+
1007
+ case "mcp_servers_state":
1008
+ handleMcpServersState(msg);
1009
+ break;
1010
+
1002
1011
  case "term_created":
1003
1012
  handleTermCreated(msg);
1004
1013
  if (store.getState().pendingTermCommand) {
@@ -5,6 +5,7 @@ import { refreshIcons, iconHtml } from './icons.js';
5
5
  import { escapeHtml, copyToClipboard } from './utils.js';
6
6
  import { getWs } from './ws-ref.js';
7
7
  import { updateBrowserTabList } from './context-sources.js';
8
+ import { setExtensionConnected } from './mcp-ui.js';
8
9
 
9
10
  // --- Module-owned state ---
10
11
  var confirmCallback = null;
@@ -95,6 +96,7 @@ export function initMisc() {
95
96
  var msg = event.data.payload;
96
97
 
97
98
  if (msg.type === "clay_ext_tab_list") {
99
+ setExtensionConnected(true);
98
100
  updateBrowserTabList(msg.tabs);
99
101
  // Also inform server about tab list
100
102
  var ws = getWs();
@@ -108,9 +110,111 @@ export function initMisc() {
108
110
  if (msg.type === "clay_ext_result") {
109
111
  handleExtensionResult(msg.requestId, msg.result);
110
112
  }
113
+ if (msg.type === "clay_ext_disconnected") {
114
+ setExtensionConnected(false);
115
+ }
116
+
117
+ // MCP bridge: extension reports available MCP servers
118
+ if (msg.type === "mcp_servers_available") {
119
+ var ws2 = getWs();
120
+ if (ws2 && ws2.readyState === 1) {
121
+ ws2.send(JSON.stringify({
122
+ type: "mcp_servers_available",
123
+ servers: msg.servers,
124
+ hostConnected: msg.hostConnected
125
+ }));
126
+ }
127
+ }
128
+
129
+ // MCP bridge: tool result from extension
130
+ if (msg.type === "mcp_tool_result") {
131
+ var ws3 = getWs();
132
+ if (ws3 && ws3.readyState === 1) {
133
+ ws3.send(JSON.stringify({
134
+ type: msg.error ? "mcp_tool_error" : "mcp_tool_result",
135
+ callId: msg.callId,
136
+ result: msg.result || null,
137
+ error: msg.error || null
138
+ }));
139
+ }
140
+ }
111
141
  });
112
142
  }
113
143
 
144
+ // Forward an MCP tool call from the server to the Chrome extension
145
+ export function forwardMcpToolCall(msg) {
146
+ window.postMessage({
147
+ source: "clay-page",
148
+ payload: {
149
+ type: "clay_mcp_tool_call",
150
+ callId: msg.callId,
151
+ server: msg.server,
152
+ method: msg.method,
153
+ params: msg.params,
154
+ }
155
+ }, "*");
156
+ }
157
+
158
+ // Forward an MCP tool call directly via HTTP for HTTP-transport servers
159
+ var _httpMcpServers = {}; // name -> url
160
+ export function setHttpMcpServers(servers) {
161
+ _httpMcpServers = {};
162
+ for (var i = 0; i < servers.length; i++) {
163
+ if (servers[i].transport === "http" && servers[i].url) {
164
+ _httpMcpServers[servers[i].name] = servers[i].url;
165
+ }
166
+ }
167
+ }
168
+
169
+ export function handleMcpToolCallMessage(msg) {
170
+ var httpUrl = _httpMcpServers[msg.server];
171
+ if (httpUrl) {
172
+ // HTTP transport: call directly via fetch
173
+ fetch(httpUrl, {
174
+ method: "POST",
175
+ headers: { "Content-Type": "application/json" },
176
+ body: JSON.stringify({
177
+ jsonrpc: "2.0",
178
+ id: msg.callId,
179
+ method: msg.method,
180
+ params: msg.params,
181
+ }),
182
+ })
183
+ .then(function (resp) { return resp.json(); })
184
+ .then(function (data) {
185
+ var ws = getWs();
186
+ if (ws && ws.readyState === 1) {
187
+ if (data.error) {
188
+ ws.send(JSON.stringify({
189
+ type: "mcp_tool_error",
190
+ callId: msg.callId,
191
+ error: data.error.message || JSON.stringify(data.error),
192
+ }));
193
+ } else {
194
+ ws.send(JSON.stringify({
195
+ type: "mcp_tool_result",
196
+ callId: msg.callId,
197
+ result: data.result,
198
+ }));
199
+ }
200
+ }
201
+ })
202
+ .catch(function (err) {
203
+ var ws = getWs();
204
+ if (ws && ws.readyState === 1) {
205
+ ws.send(JSON.stringify({
206
+ type: "mcp_tool_error",
207
+ callId: msg.callId,
208
+ error: "HTTP MCP fetch failed: " + err.message,
209
+ }));
210
+ }
211
+ });
212
+ } else {
213
+ // stdio transport: forward to extension
214
+ forwardMcpToolCall(msg);
215
+ }
216
+ }
217
+
114
218
  export function showImageModal(src) {
115
219
  var modal = document.getElementById("image-modal");
116
220
  var img = document.getElementById("image-modal-img");
@@ -0,0 +1,295 @@
1
+ // mcp-ui.js - MCP Servers modal (sidebar button + panel)
2
+ // Renders available MCP servers with per-project toggle checkboxes.
3
+
4
+ import { getWs } from './ws-ref.js';
5
+ import { refreshIcons } from './icons.js';
6
+ import { setHttpMcpServers } from './app-misc.js';
7
+
8
+ var modal = null;
9
+ var contentEl = null;
10
+ var _mcpServers = []; // { name, transport, toolCount, extensionEnabled, projectEnabled }
11
+ var _extensionConnected = false;
12
+ var _nativeHostConnected = false;
13
+ var _extensionId = null;
14
+
15
+ export function initMcp() {
16
+ modal = document.getElementById("mcp-modal");
17
+ contentEl = document.getElementById("mcp-content");
18
+
19
+ var btn = document.getElementById("mcp-btn");
20
+ var mateBtn = document.getElementById("mate-mcp-btn");
21
+ var closeBtn = document.getElementById("mcp-modal-close");
22
+ var backdrop = modal ? modal.querySelector(".confirm-backdrop") : null;
23
+
24
+ if (btn) btn.addEventListener("click", openMcpModal);
25
+ if (mateBtn) mateBtn.addEventListener("click", openMcpModal);
26
+ if (closeBtn) closeBtn.addEventListener("click", closeMcpModal);
27
+ if (backdrop) backdrop.addEventListener("click", closeMcpModal);
28
+
29
+ // ESC to close
30
+ document.addEventListener("keydown", function (e) {
31
+ if (e.key === "Escape" && modal && !modal.classList.contains("hidden")) {
32
+ closeMcpModal();
33
+ }
34
+ });
35
+ }
36
+
37
+ export function handleMcpServersState(msg) {
38
+ _mcpServers = msg.servers || [];
39
+ _extensionConnected = true;
40
+ if (msg.hostConnected !== undefined) _nativeHostConnected = msg.hostConnected;
41
+ if (msg.extensionId) _extensionId = msg.extensionId;
42
+
43
+ // Update HTTP MCP server registry for direct fetch calls
44
+ setHttpMcpServers(_mcpServers);
45
+
46
+ // Update sidebar badge
47
+ updateBadge();
48
+
49
+ // Re-render if modal is open (skip during toggle cooldown)
50
+ if (modal && !modal.classList.contains("hidden") && !_toggleCooldown) {
51
+ renderMcpServerList();
52
+ }
53
+ }
54
+
55
+ export function setExtensionConnected(connected) {
56
+ _extensionConnected = connected;
57
+ }
58
+
59
+ export function getMcpServers() {
60
+ return _mcpServers;
61
+ }
62
+
63
+ function openMcpModal() {
64
+ if (!modal) return;
65
+ modal.classList.remove("hidden");
66
+ refreshIcons(modal);
67
+ renderMcpServerList();
68
+ }
69
+
70
+ function closeMcpModal() {
71
+ if (!modal) return;
72
+ modal.classList.add("hidden");
73
+ }
74
+
75
+ function updateBadge() {
76
+ var enabled = 0;
77
+ for (var i = 0; i < _mcpServers.length; i++) {
78
+ if (_mcpServers[i].extensionEnabled && _mcpServers[i].projectEnabled) enabled++;
79
+ }
80
+
81
+ var badges = [
82
+ document.getElementById("mcp-sidebar-count"),
83
+ document.getElementById("mate-mcp-sidebar-count"),
84
+ ];
85
+ for (var j = 0; j < badges.length; j++) {
86
+ var badge = badges[j];
87
+ if (!badge) continue;
88
+ if (enabled > 0) {
89
+ badge.textContent = String(enabled);
90
+ badge.classList.remove("hidden");
91
+ } else {
92
+ badge.classList.add("hidden");
93
+ }
94
+ }
95
+ }
96
+
97
+ function renderMcpServerList() {
98
+ if (!contentEl) return;
99
+ contentEl.innerHTML = "";
100
+
101
+ var available = _mcpServers.filter(function (s) { return s.extensionEnabled; });
102
+ var hasServers = available.length > 0;
103
+ var allDone = _extensionConnected && _nativeHostConnected && hasServers;
104
+
105
+ // --- All setup complete: skip wizard, show server list only ---
106
+ if (allDone) {
107
+ var desc = document.createElement("p");
108
+ desc.className = "mcp-desc";
109
+ desc.textContent = "Toggle which MCP servers this project can use.";
110
+ contentEl.appendChild(desc);
111
+
112
+ for (var i = 0; i < available.length; i++) {
113
+ var server = available[i];
114
+ var row = document.createElement("label");
115
+ row.className = "mcp-server-row";
116
+
117
+ var cb = document.createElement("input");
118
+ cb.type = "checkbox";
119
+ cb.checked = server.projectEnabled;
120
+ cb.dataset.serverName = server.name;
121
+ cb.addEventListener("change", onToggle);
122
+
123
+ var info = document.createElement("div");
124
+ info.className = "mcp-server-info";
125
+
126
+ var nameSpan = document.createElement("span");
127
+ nameSpan.className = "mcp-server-name";
128
+ nameSpan.textContent = server.name;
129
+
130
+ var meta = document.createElement("span");
131
+ meta.className = "mcp-server-meta";
132
+ meta.textContent = server.toolCount + " tool" + (server.toolCount === 1 ? "" : "s");
133
+ if (server.transport === "http") meta.textContent += " \u00B7 HTTP";
134
+
135
+ info.appendChild(nameSpan);
136
+ info.appendChild(meta);
137
+
138
+ row.appendChild(cb);
139
+ row.appendChild(info);
140
+ contentEl.appendChild(row);
141
+ }
142
+ refreshIcons(contentEl);
143
+ return;
144
+ }
145
+
146
+ // --- Setup incomplete: show step wizard ---
147
+ var steps = document.createElement("div");
148
+ steps.className = "mcp-steps";
149
+
150
+ // Step 1: Chrome Extension
151
+ var step1Done = _extensionConnected;
152
+ steps.appendChild(renderStep({
153
+ num: 1,
154
+ done: step1Done,
155
+ title: "Install Chrome Extension",
156
+ desc: step1Done
157
+ ? "Connected"
158
+ : "Required to bridge your browser with Clay.",
159
+ action: step1Done ? null : {
160
+ label: "Setup Extension",
161
+ icon: "puzzle",
162
+ onClick: function () {
163
+ closeMcpModal();
164
+ setTimeout(function () {
165
+ var extPill = document.getElementById("ext-pill");
166
+ if (extPill) extPill.click();
167
+ }, 100);
168
+ }
169
+ }
170
+ }));
171
+
172
+ // Step 2: Native Host (only needed for remote users)
173
+ var step2Done = _extensionConnected && _nativeHostConnected;
174
+ var installCmd = _extensionId
175
+ ? "npx clay-mcp-bridge install " + _extensionId
176
+ : "npx clay-mcp-bridge install <extension-id>";
177
+ steps.appendChild(renderStep({
178
+ num: 2,
179
+ done: step2Done,
180
+ title: "Install MCP Bridge",
181
+ desc: step2Done
182
+ ? "Connected"
183
+ : "Run in your terminal, then restart your browser.",
184
+ disabled: !step1Done,
185
+ copyCmd: (!step2Done && step1Done) ? installCmd : null
186
+ }));
187
+
188
+ // Step 3: Configure MCP Servers
189
+ var step3Done = hasServers;
190
+ var step3Desc = "";
191
+ if (step3Done) {
192
+ step3Desc = available.length + " server" + (available.length === 1 ? "" : "s") + " enabled";
193
+ } else if (step2Done) {
194
+ step3Desc = "Add servers from the Clay Chrome Extension popup using the + button, or import an existing config file.";
195
+ } else {
196
+ step3Desc = "Configure after installing the bridge.";
197
+ }
198
+ steps.appendChild(renderStep({
199
+ num: 3,
200
+ done: step3Done,
201
+ title: "Add MCP Servers",
202
+ desc: step3Desc,
203
+ disabled: !step2Done,
204
+ action: (!step3Done && step2Done) ? {
205
+ label: "Open Extension popup to add servers",
206
+ icon: "puzzle",
207
+ onClick: function () {
208
+ closeMcpModal();
209
+ setTimeout(function () {
210
+ var extPill = document.getElementById("ext-pill");
211
+ if (extPill) extPill.click();
212
+ }, 100);
213
+ }
214
+ } : null
215
+ }));
216
+
217
+ contentEl.appendChild(steps);
218
+ refreshIcons(contentEl);
219
+ }
220
+
221
+ function renderStep(opts) {
222
+ var el = document.createElement("div");
223
+ el.className = "mcp-step" + (opts.done ? " done" : "") + (opts.disabled ? " disabled" : "");
224
+
225
+ var icon = opts.done ? "check-circle-2" : "circle";
226
+ var iconClass = opts.done ? "mcp-step-icon done" : "mcp-step-icon";
227
+
228
+ var html = '<div class="' + iconClass + '"><i data-lucide="' + icon + '"></i></div>'
229
+ + '<div class="mcp-step-body">'
230
+ + '<div class="mcp-step-title">' + opts.title + '</div>'
231
+ + '<div class="mcp-step-desc">' + opts.desc + '</div>';
232
+
233
+ if (opts.copyCmd) {
234
+ html += '<div class="mcp-install-cmd-row">'
235
+ + '<code class="mcp-install-cmd">' + opts.copyCmd + '</code>'
236
+ + '<button class="mcp-install-copy-btn" type="button"><i data-lucide="copy"></i></button>'
237
+ + '</div>';
238
+ }
239
+
240
+ html += '</div>';
241
+ el.innerHTML = html;
242
+
243
+ if (opts.action && !opts.disabled) {
244
+ var btn = document.createElement("button");
245
+ btn.className = "mcp-ext-setup-btn";
246
+ btn.type = "button";
247
+ btn.innerHTML = '<i data-lucide="' + opts.action.icon + '"></i> ' + opts.action.label;
248
+ btn.addEventListener("click", opts.action.onClick);
249
+ el.querySelector(".mcp-step-body").appendChild(btn);
250
+ }
251
+
252
+ if (opts.copyCmd) {
253
+ var copyBtn = el.querySelector(".mcp-install-copy-btn");
254
+ copyBtn.addEventListener("click", function () {
255
+ navigator.clipboard.writeText(opts.copyCmd).then(function () {
256
+ copyBtn.innerHTML = '<i data-lucide="check"></i>';
257
+ refreshIcons(copyBtn);
258
+ setTimeout(function () {
259
+ copyBtn.innerHTML = '<i data-lucide="copy"></i>';
260
+ refreshIcons(copyBtn);
261
+ }, 1500);
262
+ });
263
+ });
264
+ }
265
+
266
+ return el;
267
+ }
268
+
269
+ var _toggleCooldown = false;
270
+
271
+ function onToggle(e) {
272
+ var name = e.target.dataset.serverName;
273
+ var enabled = e.target.checked;
274
+
275
+ // Optimistic update: apply locally so incoming broadcasts don't revert
276
+ for (var i = 0; i < _mcpServers.length; i++) {
277
+ if (_mcpServers[i].name === name) {
278
+ _mcpServers[i].projectEnabled = enabled;
279
+ break;
280
+ }
281
+ }
282
+
283
+ // Suppress re-renders from broadcasts for a short window
284
+ _toggleCooldown = true;
285
+ setTimeout(function () { _toggleCooldown = false; }, 1000);
286
+
287
+ var ws = getWs();
288
+ if (ws && ws.readyState === 1) {
289
+ ws.send(JSON.stringify({
290
+ type: "mcp_toggle_server",
291
+ name: name,
292
+ enabled: enabled,
293
+ }));
294
+ }
295
+ }