@auggieteo/dsh-mcp-adapter 0.1.0

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.
package/lib/client.js ADDED
@@ -0,0 +1,1200 @@
1
+ window.__ModuleLoader__.load({
2
+ id: "@auggieteo/dsh-mcp-adapter",
3
+ factory: (require) => {
4
+ var module = { exports: {} };
5
+ var exports = module.exports;
6
+ var __create = Object.create;
7
+ var __defProp = Object.defineProperty;
8
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9
+ var __getOwnPropNames = Object.getOwnPropertyNames;
10
+ var __getProtoOf = Object.getPrototypeOf;
11
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
12
+ var __export = (target, all) => {
13
+ for (var name in all)
14
+ __defProp(target, name, { get: all[name], enumerable: true });
15
+ };
16
+ var __copyProps = (to, from, except, desc) => {
17
+ if (from && typeof from === "object" || typeof from === "function") {
18
+ for (let key of __getOwnPropNames(from))
19
+ if (!__hasOwnProp.call(to, key) && key !== except)
20
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
21
+ }
22
+ return to;
23
+ };
24
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
25
+ // If the importer is in node compatibility mode or this is not an ESM
26
+ // file that has been converted to a CommonJS file using a Babel-
27
+ // compatible transform (i.e. "__esModule" has not been set), then set
28
+ // "default" to the CommonJS "module.exports" for node compatibility.
29
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
30
+ mod
31
+ ));
32
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
33
+
34
+ // src/client/index.jsx
35
+ var index_exports = {};
36
+ __export(index_exports, {
37
+ apply: () => apply,
38
+ inject: () => inject
39
+ });
40
+ module.exports = __toCommonJS(index_exports);
41
+
42
+ // src/client/McpSettingsPage.jsx
43
+ var import_react = __toESM(require("react"), 1);
44
+
45
+ // src/client/settings-controller.js
46
+ var MCP_SETTINGS_NAMESPACE = "mcp";
47
+ var MCP_RPC_CHANNEL = "/mcp-adapter";
48
+ var SERVER_FIELDS = /* @__PURE__ */ new Set([
49
+ "command",
50
+ "args",
51
+ "env",
52
+ "url",
53
+ "headers",
54
+ "disabled",
55
+ "autoAllow",
56
+ "lifecycle",
57
+ "idleTimeoutMinutes",
58
+ "promotedTools"
59
+ ]);
60
+ function isRecord(value) {
61
+ return typeof value === "object" && value !== null && !Array.isArray(value);
62
+ }
63
+ function messageOf(error) {
64
+ return error instanceof Error ? error.message : String(error);
65
+ }
66
+ function requireStringRecord(value, label) {
67
+ if (value === void 0) return {};
68
+ if (!isRecord(value)) throw new Error(`${label} must be an object of string values`);
69
+ for (const [key, entry] of Object.entries(value)) {
70
+ if (key.trim() === "" || typeof entry !== "string") {
71
+ throw new Error(`${label} must contain non-empty keys and string values`);
72
+ }
73
+ }
74
+ return value;
75
+ }
76
+ function normalizeServerConfig(name, input) {
77
+ if (typeof name !== "string" || name.trim() === "") {
78
+ throw new Error("Server name cannot be empty");
79
+ }
80
+ if (!isRecord(input)) throw new Error(`Server ${JSON.stringify(name)} must be an object`);
81
+ for (const key of Object.keys(input)) {
82
+ if (!SERVER_FIELDS.has(key)) {
83
+ throw new Error(`Server ${JSON.stringify(name)} has unknown field ${JSON.stringify(key)}`);
84
+ }
85
+ }
86
+ const hasCommand = typeof input.command === "string";
87
+ const hasUrl = typeof input.url === "string";
88
+ if (hasCommand === hasUrl) {
89
+ throw new Error(`Server ${JSON.stringify(name)} needs exactly one of command or url`);
90
+ }
91
+ if (hasCommand && input.command.trim() === "") {
92
+ throw new Error(`Server ${JSON.stringify(name)} command cannot be empty`);
93
+ }
94
+ if (hasUrl) {
95
+ let url;
96
+ try {
97
+ url = new URL(input.url);
98
+ } catch {
99
+ throw new Error(`Server ${JSON.stringify(name)} URL must be absolute`);
100
+ }
101
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
102
+ throw new Error(`Server ${JSON.stringify(name)} URL must use HTTP or HTTPS`);
103
+ }
104
+ }
105
+ const args = input.args ?? [];
106
+ if (!Array.isArray(args) || args.some((entry) => typeof entry !== "string")) {
107
+ throw new Error(`Server ${JSON.stringify(name)} args must be an array of strings`);
108
+ }
109
+ const env = requireStringRecord(input.env, `Server ${JSON.stringify(name)} env`);
110
+ const headers = requireStringRecord(input.headers, `Server ${JSON.stringify(name)} headers`);
111
+ if (hasCommand && Object.keys(headers).length > 0) {
112
+ throw new Error(`Server ${JSON.stringify(name)} headers require HTTP transport`);
113
+ }
114
+ if (hasUrl && (args.length > 0 || Object.keys(env).length > 0)) {
115
+ throw new Error(`Server ${JSON.stringify(name)} args and env require stdio transport`);
116
+ }
117
+ const idleTimeoutMinutes = input.idleTimeoutMinutes ?? 10;
118
+ if (!Number.isFinite(idleTimeoutMinutes) || idleTimeoutMinutes <= 0) {
119
+ throw new Error(`Server ${JSON.stringify(name)} idle timeout must be positive`);
120
+ }
121
+ const promotedTools = input.promotedTools ?? [];
122
+ if (!Array.isArray(promotedTools) || promotedTools.some((entry) => typeof entry !== "string" || entry.trim() === "") || new Set(promotedTools).size !== promotedTools.length) {
123
+ throw new Error(`Server ${JSON.stringify(name)} promotedTools must contain unique names`);
124
+ }
125
+ return {
126
+ ...hasCommand ? { command: input.command.trim(), args, env } : {
127
+ url: input.url.trim(),
128
+ headers
129
+ },
130
+ disabled: input.disabled === true,
131
+ autoAllow: input.autoAllow === true,
132
+ lifecycle: "lazy",
133
+ idleTimeoutMinutes,
134
+ promotedTools
135
+ };
136
+ }
137
+ function parseMcpImport(text) {
138
+ let parsed;
139
+ try {
140
+ parsed = JSON.parse(text);
141
+ } catch (error) {
142
+ throw new Error(`Invalid JSON: ${messageOf(error)}`);
143
+ }
144
+ if (!isRecord(parsed) || !isRecord(parsed.mcpServers)) {
145
+ throw new Error("Import JSON must contain an mcpServers object");
146
+ }
147
+ const servers = {};
148
+ for (const [name, config] of Object.entries(parsed.mcpServers)) {
149
+ servers[name] = normalizeServerConfig(name, config);
150
+ }
151
+ if (Object.keys(servers).length === 0) {
152
+ throw new Error("Import JSON contains no Servers");
153
+ }
154
+ return servers;
155
+ }
156
+ function parseArgs(text) {
157
+ let parsed;
158
+ try {
159
+ parsed = JSON.parse(text);
160
+ } catch (error) {
161
+ throw new Error(`Args must be a JSON array: ${messageOf(error)}`);
162
+ }
163
+ if (!Array.isArray(parsed) || parsed.some((entry) => typeof entry !== "string")) {
164
+ throw new Error("Args must be a JSON array of strings");
165
+ }
166
+ return parsed;
167
+ }
168
+ function secretKeysFromView(view, serverName, field) {
169
+ const namespace = view?.namespaces?.find((entry) => entry.ns === MCP_SETTINGS_NAMESPACE);
170
+ if (namespace === void 0) return [];
171
+ return namespace.secrets.filter(
172
+ (secret) => secret.set === true && secret.path.length === 4 && secret.path[0] === "mcpServers" && secret.path[1] === serverName && secret.path[2] === field
173
+ ).map((secret) => secret.path[3]).sort();
174
+ }
175
+ function secretRowOps(serverName, field, rows, existingKeys) {
176
+ const unsetKeys = /* @__PURE__ */ new Set();
177
+ const sets = [];
178
+ const retained = /* @__PURE__ */ new Set();
179
+ for (const row of rows) {
180
+ const originalKey = typeof row.originalKey === "string" ? row.originalKey : void 0;
181
+ const key = typeof row.key === "string" ? row.key.trim() : "";
182
+ const value = typeof row.value === "string" ? row.value : "";
183
+ if (row.removed) {
184
+ if (originalKey !== void 0) unsetKeys.add(originalKey);
185
+ continue;
186
+ }
187
+ if (key === "") throw new Error(`${field} keys cannot be empty`);
188
+ if (retained.has(key)) throw new Error(`${field} key ${JSON.stringify(key)} is duplicated`);
189
+ retained.add(key);
190
+ if (originalKey !== void 0 && originalKey !== key) {
191
+ unsetKeys.add(originalKey);
192
+ if (value === "") {
193
+ throw new Error(`Enter a new value when renaming secret key ${JSON.stringify(originalKey)}`);
194
+ }
195
+ }
196
+ if (value !== "") {
197
+ sets.push({ op: "set", path: ["mcpServers", serverName, field, key], value });
198
+ } else if (originalKey === void 0 && !existingKeys.includes(key)) {
199
+ throw new Error(`Enter a value for new ${field} key ${JSON.stringify(key)}`);
200
+ }
201
+ }
202
+ for (const key of existingKeys) {
203
+ if (!rows.some((row) => row.originalKey === key || !row.removed && row.key === key)) {
204
+ unsetKeys.add(key);
205
+ }
206
+ }
207
+ return [
208
+ ...[...unsetKeys].map((key) => ({
209
+ op: "unset",
210
+ path: ["mcpServers", serverName, field, key]
211
+ })),
212
+ ...sets
213
+ ];
214
+ }
215
+ function overviewEmpty() {
216
+ return { status: { servers: [] }, catalog: { servers: [] } };
217
+ }
218
+ var McpSettingsController = class {
219
+ constructor({ scope, describe, settingsApi, rpc, pollIntervalMs = 3e3 }) {
220
+ this.scope = scope;
221
+ this.describe = describe;
222
+ this.settingsApi = settingsApi;
223
+ this.rpc = rpc;
224
+ this.pollIntervalMs = pollIntervalMs;
225
+ this.listeners = /* @__PURE__ */ new Set();
226
+ this.overview = overviewEmpty();
227
+ this.actionError = void 0;
228
+ this.overviewError = void 0;
229
+ this.pendingWrites = 0;
230
+ this.mounted = 0;
231
+ this.stopPoll = void 0;
232
+ this.loadingOverview = void 0;
233
+ this.tail = Promise.resolve();
234
+ this.disposed = false;
235
+ this.unsubscribeScope = scope.subscribe(() => {
236
+ this.publish();
237
+ if (this.mounted > 0) void this.loadOverview();
238
+ });
239
+ this.unsubscribeDescribe = describe.subscribe(() => this.publish());
240
+ this.snapshot = this.projection();
241
+ this.subscribe = (listener) => {
242
+ this.listeners.add(listener);
243
+ return () => this.listeners.delete(listener);
244
+ };
245
+ this.getSnapshot = () => this.snapshot;
246
+ }
247
+ projection() {
248
+ return {
249
+ settings: this.scope.getSnapshot(),
250
+ settingsDocument: this.describe.getSnapshot(),
251
+ overview: this.overview,
252
+ error: this.actionError ?? this.overviewError,
253
+ busy: this.pendingWrites > 0
254
+ };
255
+ }
256
+ publish() {
257
+ this.snapshot = this.projection();
258
+ for (const listener of this.listeners) listener();
259
+ }
260
+ mount() {
261
+ if (this.disposed) return () => {
262
+ };
263
+ this.mounted += 1;
264
+ if (this.mounted === 1) {
265
+ void this.describe.ensure().catch((error) => {
266
+ this.actionError = `Could not load MCP Settings: ${messageOf(error)}`;
267
+ this.publish();
268
+ });
269
+ void this.loadOverview();
270
+ const id = setInterval(() => void this.loadOverview(), this.pollIntervalMs);
271
+ this.stopPoll = () => clearInterval(id);
272
+ }
273
+ return () => {
274
+ this.mounted -= 1;
275
+ if (this.mounted === 0) {
276
+ this.stopPoll?.();
277
+ this.stopPoll = void 0;
278
+ }
279
+ };
280
+ }
281
+ async loadOverview() {
282
+ if (this.disposed) return;
283
+ if (this.loadingOverview !== void 0) return this.loadingOverview;
284
+ const request = (async () => {
285
+ try {
286
+ const result = await this.rpc("overview", {});
287
+ if (!result.ok) throw new Error(result.error.message);
288
+ this.overview = result.value;
289
+ this.overviewError = void 0;
290
+ } catch (error) {
291
+ this.overviewError = `Could not load MCP status: ${messageOf(error)}`;
292
+ } finally {
293
+ this.loadingOverview = void 0;
294
+ this.publish();
295
+ }
296
+ })();
297
+ this.loadingOverview = request;
298
+ return request;
299
+ }
300
+ enqueue(label, operation) {
301
+ this.pendingWrites += 1;
302
+ this.actionError = void 0;
303
+ this.publish();
304
+ const run = this.tail.then(async () => {
305
+ try {
306
+ await operation();
307
+ await this.loadOverview();
308
+ return true;
309
+ } catch (error) {
310
+ this.actionError = `${label}: ${messageOf(error)}`;
311
+ return false;
312
+ } finally {
313
+ this.pendingWrites -= 1;
314
+ this.publish();
315
+ }
316
+ });
317
+ this.tail = run.then(() => void 0);
318
+ return run;
319
+ }
320
+ async settingsWrite(method, input) {
321
+ const revision = this.scope.getSnapshot().revision;
322
+ if (revision === void 0) throw new Error("Settings have not loaded yet");
323
+ const response = await this.settingsApi[method]({
324
+ ns: MCP_SETTINGS_NAMESPACE,
325
+ ...input,
326
+ expectedRevision: revision
327
+ });
328
+ if (!response.result.ok) throw new Error(response.result.error.message);
329
+ this.describe.acceptView(response.result.value);
330
+ }
331
+ addServer(name, config) {
332
+ return this.enqueue("Could not add Server", async () => {
333
+ const normalizedName = name.trim();
334
+ const current = this.scope.getSnapshot().value?.mcpServers ?? {};
335
+ if (Object.hasOwn(current, normalizedName)) {
336
+ throw new Error(`Server ${JSON.stringify(normalizedName)} already exists`);
337
+ }
338
+ const server = normalizeServerConfig(normalizedName, config);
339
+ await this.settingsWrite("update", {
340
+ patch: { mcpServers: { [normalizedName]: server } }
341
+ });
342
+ });
343
+ }
344
+ importJson(text) {
345
+ return this.enqueue("Could not import Servers", async () => {
346
+ const servers = parseMcpImport(text);
347
+ const current = this.scope.getSnapshot().value?.mcpServers ?? {};
348
+ const replacesExisting = Object.keys(servers).some((name) => Object.hasOwn(current, name));
349
+ if (!replacesExisting) {
350
+ await this.settingsWrite("update", { patch: { mcpServers: servers } });
351
+ return;
352
+ }
353
+ await this.settingsWrite("mutate", {
354
+ ops: Object.entries(servers).map(([name, server]) => ({
355
+ op: "set",
356
+ path: ["mcpServers", name],
357
+ value: server
358
+ }))
359
+ });
360
+ });
361
+ }
362
+ deleteServer(name) {
363
+ return this.enqueue(
364
+ "Could not delete Server",
365
+ () => this.settingsWrite("mutate", {
366
+ ops: [{ op: "unset", path: ["mcpServers", name] }]
367
+ })
368
+ );
369
+ }
370
+ setServerField(name, field, value) {
371
+ return this.enqueue(
372
+ `Could not update ${field}`,
373
+ () => this.settingsWrite("mutate", {
374
+ ops: [{ op: "set", path: ["mcpServers", name, field], value }]
375
+ })
376
+ );
377
+ }
378
+ saveServer(name, draft, secretRows2) {
379
+ return this.enqueue("Could not save Server", async () => {
380
+ const existing = this.scope.getSnapshot().value?.mcpServers?.[name];
381
+ if (existing === void 0) throw new Error(`Server ${JSON.stringify(name)} no longer exists`);
382
+ const args = draft.transport === "stdio" ? parseArgs(draft.argsText) : [];
383
+ const candidate = normalizeServerConfig(name, {
384
+ ...draft.transport === "stdio" ? { command: draft.command, args, env: {} } : { url: draft.url, headers: {} },
385
+ disabled: draft.disabled,
386
+ autoAllow: draft.autoAllow,
387
+ idleTimeoutMinutes: Number(draft.idleTimeoutMinutes),
388
+ promotedTools: existing.promotedTools
389
+ });
390
+ const ops = [
391
+ { op: "set", path: ["mcpServers", name, "disabled"], value: candidate.disabled },
392
+ { op: "set", path: ["mcpServers", name, "autoAllow"], value: candidate.autoAllow },
393
+ {
394
+ op: "set",
395
+ path: ["mcpServers", name, "idleTimeoutMinutes"],
396
+ value: candidate.idleTimeoutMinutes
397
+ }
398
+ ];
399
+ if (draft.transport === "stdio") {
400
+ ops.push(
401
+ { op: "set", path: ["mcpServers", name, "command"], value: candidate.command },
402
+ { op: "set", path: ["mcpServers", name, "args"], value: candidate.args },
403
+ { op: "unset", path: ["mcpServers", name, "url"] },
404
+ { op: "unset", path: ["mcpServers", name, "headers"] },
405
+ ...secretRowOps(name, "env", secretRows2, secretKeysFromView(
406
+ this.describe.getSnapshot().view,
407
+ name,
408
+ "env"
409
+ ))
410
+ );
411
+ } else {
412
+ ops.push(
413
+ { op: "set", path: ["mcpServers", name, "url"], value: candidate.url },
414
+ { op: "unset", path: ["mcpServers", name, "command"] },
415
+ { op: "unset", path: ["mcpServers", name, "args"] },
416
+ { op: "unset", path: ["mcpServers", name, "env"] },
417
+ ...secretRowOps(name, "headers", secretRows2, secretKeysFromView(
418
+ this.describe.getSnapshot().view,
419
+ name,
420
+ "headers"
421
+ ))
422
+ );
423
+ }
424
+ await this.settingsWrite("mutate", { ops });
425
+ });
426
+ }
427
+ togglePromotion(name, toolName) {
428
+ const tools = this.scope.getSnapshot().value?.mcpServers?.[name]?.promotedTools ?? [];
429
+ const next = tools.includes(toolName) ? tools.filter((entry) => entry !== toolName) : [...tools, toolName];
430
+ return this.setServerField(name, "promotedTools", next);
431
+ }
432
+ reconnect(name) {
433
+ return this.enqueue("Could not reconnect Server", async () => {
434
+ const result = await this.rpc("reconnect", { server: name });
435
+ if (!result.ok) {
436
+ await this.loadOverview();
437
+ throw new Error(result.error.message);
438
+ }
439
+ this.overview = result.value;
440
+ });
441
+ }
442
+ async dispose() {
443
+ if (this.disposed) return;
444
+ this.disposed = true;
445
+ this.stopPoll?.();
446
+ this.unsubscribeScope?.();
447
+ this.unsubscribeDescribe?.();
448
+ await this.tail;
449
+ this.listeners.clear();
450
+ }
451
+ };
452
+
453
+ // src/client/McpSettingsPage.jsx
454
+ var import_jsx_runtime = require("react/jsx-runtime");
455
+ function statusFor(overview, name) {
456
+ return overview.status.servers.find((entry) => entry.name === name) ?? {
457
+ name,
458
+ state: "disconnected",
459
+ toolCount: 0
460
+ };
461
+ }
462
+ function catalogFor(overview, name) {
463
+ return overview.catalog.servers.find((entry) => entry.name === name)?.tools ?? [];
464
+ }
465
+ function statusClass(state) {
466
+ if (state === "connected") return "mcp-status-connected";
467
+ if (state === "connecting") return "mcp-status-connecting";
468
+ if (state === "error") return "mcp-status-error";
469
+ if (state === "disabled") return "mcp-status-disabled";
470
+ return "";
471
+ }
472
+ function secretRows(keys) {
473
+ return keys.map((key) => ({ originalKey: key, key, value: "" }));
474
+ }
475
+ function SecretEditor({ field, rows, onChange, disabled }) {
476
+ const label = field === "env" ? "Environment variables" : "HTTP headers";
477
+ const add = () => onChange([...rows, { key: "", value: "" }]);
478
+ const update = (index, patch) => {
479
+ onChange(rows.map((row, position) => position === index ? { ...row, ...patch } : row));
480
+ };
481
+ const remove = (index) => onChange(rows.filter((_, position) => position !== index));
482
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-section", children: [
483
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
484
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h4", { className: "mcp-section-title", children: label }),
485
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mcp-muted", children: "Existing secret values stay saved when their value field remains blank." })
486
+ ] }),
487
+ rows.map((row, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-secret-row", children: [
488
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
489
+ "input",
490
+ {
491
+ className: "mcp-input",
492
+ "aria-label": `${label} key ${index + 1}`,
493
+ placeholder: field === "env" ? "API_TOKEN" : "Authorization",
494
+ value: row.key,
495
+ disabled,
496
+ onChange: (event) => update(index, { key: event.target.value })
497
+ }
498
+ ),
499
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
500
+ "input",
501
+ {
502
+ className: "mcp-input",
503
+ "aria-label": `${label} value ${index + 1}`,
504
+ type: "password",
505
+ autoComplete: "off",
506
+ placeholder: row.originalKey === void 0 ? "Secret value" : "Saved value",
507
+ value: row.value,
508
+ disabled,
509
+ onChange: (event) => update(index, { value: event.target.value })
510
+ }
511
+ ),
512
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
513
+ "button",
514
+ {
515
+ type: "button",
516
+ className: "mcp-button mcp-button-danger",
517
+ disabled,
518
+ onClick: () => remove(index),
519
+ children: "Remove"
520
+ }
521
+ )
522
+ ] }, `${row.originalKey ?? "new"}-${index}`)),
523
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", { type: "button", className: "mcp-button", disabled, onClick: add, children: [
524
+ "Add ",
525
+ field === "env" ? "variable" : "header"
526
+ ] }) })
527
+ ] });
528
+ }
529
+ function AddServerDialog({ controller, busy, onClose }) {
530
+ const [name, setName] = (0, import_react.useState)("");
531
+ const [transport, setTransport] = (0, import_react.useState)("stdio");
532
+ const [command, setCommand] = (0, import_react.useState)("");
533
+ const [url, setUrl] = (0, import_react.useState)("");
534
+ const [argsText, setArgsText] = (0, import_react.useState)("[]");
535
+ const [error, setError] = (0, import_react.useState)();
536
+ const submit = async (event) => {
537
+ event.preventDefault();
538
+ setError(void 0);
539
+ let config;
540
+ try {
541
+ config = normalizeServerConfig(
542
+ name,
543
+ transport === "stdio" ? { command, args: parseArgs(argsText) } : { url }
544
+ );
545
+ } catch (nextError) {
546
+ setError(nextError instanceof Error ? nextError.message : String(nextError));
547
+ return;
548
+ }
549
+ if (await controller.addServer(name, config)) onClose();
550
+ };
551
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-overlay", role: "presentation", onMouseDown: (event) => {
552
+ if (event.target === event.currentTarget) onClose();
553
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: "mcp-modal-card", "aria-label": "Add MCP Server", onSubmit: submit, children: [
554
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-card-head", children: [
555
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
556
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "mcp-modal-title", children: "Add Server" }),
557
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mcp-muted", children: "Configure one stdio command or one HTTP URL." })
558
+ ] }),
559
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "mcp-button", onClick: onClose, children: "Close" })
560
+ ] }),
561
+ error !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-error", role: "alert", children: error }),
562
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
563
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Name" }),
564
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
565
+ "input",
566
+ {
567
+ autoFocus: true,
568
+ className: "mcp-input",
569
+ value: name,
570
+ disabled: busy,
571
+ onChange: (event) => setName(event.target.value),
572
+ placeholder: "github"
573
+ }
574
+ )
575
+ ] }),
576
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
577
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Transport" }),
578
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
579
+ "select",
580
+ {
581
+ className: "mcp-select",
582
+ value: transport,
583
+ disabled: busy,
584
+ onChange: (event) => setTransport(event.target.value),
585
+ children: [
586
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "stdio", children: "stdio" }),
587
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "http", children: "Streamable HTTP" })
588
+ ]
589
+ }
590
+ )
591
+ ] }),
592
+ transport === "stdio" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
593
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
594
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Command" }),
595
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
596
+ "input",
597
+ {
598
+ className: "mcp-input",
599
+ value: command,
600
+ disabled: busy,
601
+ onChange: (event) => setCommand(event.target.value),
602
+ placeholder: "npx"
603
+ }
604
+ )
605
+ ] }),
606
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
607
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Args as a JSON array" }),
608
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
609
+ "textarea",
610
+ {
611
+ className: "mcp-textarea",
612
+ value: argsText,
613
+ disabled: busy,
614
+ onChange: (event) => setArgsText(event.target.value),
615
+ spellCheck: false
616
+ }
617
+ )
618
+ ] })
619
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
620
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "URL" }),
621
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
622
+ "input",
623
+ {
624
+ className: "mcp-input",
625
+ value: url,
626
+ disabled: busy,
627
+ onChange: (event) => setUrl(event.target.value),
628
+ placeholder: "https://example.com/mcp"
629
+ }
630
+ )
631
+ ] }),
632
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-actions", children: [
633
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", className: "mcp-button mcp-button-primary", disabled: busy, children: busy ? "Adding\u2026" : "Add Server" }),
634
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "mcp-button", disabled: busy, onClick: onClose, children: "Cancel" })
635
+ ] })
636
+ ] }) });
637
+ }
638
+ function ImportDialog({ controller, busy, onClose }) {
639
+ const [text, setText] = (0, import_react.useState)('{\n "mcpServers": {\n \n }\n}');
640
+ const [error, setError] = (0, import_react.useState)();
641
+ const submit = async (event) => {
642
+ event.preventDefault();
643
+ setError(void 0);
644
+ try {
645
+ parseMcpImport(text);
646
+ } catch (nextError) {
647
+ setError(nextError instanceof Error ? nextError.message : String(nextError));
648
+ return;
649
+ }
650
+ const result = await controller.importJson(text);
651
+ if (result) onClose();
652
+ else setError("The Host rejected this import. Review the page error and retry.");
653
+ };
654
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-overlay", role: "presentation", onMouseDown: (event) => {
655
+ if (event.target === event.currentTarget) onClose();
656
+ }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: "mcp-modal-card", "aria-label": "Import MCP Servers", onSubmit: submit, children: [
657
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-card-head", children: [
658
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
659
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "mcp-modal-title", children: "Import JSON" }),
660
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mcp-muted", children: "Paste a standard object with an mcpServers property." })
661
+ ] }),
662
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "mcp-button", onClick: onClose, children: "Close" })
663
+ ] }),
664
+ error !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-error", role: "alert", children: error }),
665
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
666
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "MCP Config JSON" }),
667
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
668
+ "textarea",
669
+ {
670
+ autoFocus: true,
671
+ className: "mcp-textarea",
672
+ style: { minHeight: 280 },
673
+ value: text,
674
+ disabled: busy,
675
+ spellCheck: false,
676
+ onChange: (event) => setText(event.target.value)
677
+ }
678
+ )
679
+ ] }),
680
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-actions", children: [
681
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "submit", className: "mcp-button mcp-button-primary", disabled: busy, children: busy ? "Importing\u2026" : "Import Servers" }),
682
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "mcp-button", disabled: busy, onClick: onClose, children: "Cancel" })
683
+ ] })
684
+ ] }) });
685
+ }
686
+ function ServerDetail({ controller, snapshot, name, server }) {
687
+ const status = statusFor(snapshot.overview, name);
688
+ const tools = catalogFor(snapshot.overview, name);
689
+ const transport = typeof server.command === "string" ? "stdio" : "http";
690
+ const [draft, setDraft] = (0, import_react.useState)(() => ({
691
+ transport,
692
+ command: server.command ?? "",
693
+ url: server.url ?? "",
694
+ argsText: JSON.stringify(server.args ?? [], null, 2),
695
+ disabled: server.disabled === true,
696
+ autoAllow: server.autoAllow === true,
697
+ idleTimeoutMinutes: String(server.idleTimeoutMinutes ?? 10)
698
+ }));
699
+ const [rowSets, setRowSets] = (0, import_react.useState)(() => ({
700
+ env: secretRows(secretKeysFromView(snapshot.settingsDocument.view, name, "env")),
701
+ headers: secretRows(secretKeysFromView(snapshot.settingsDocument.view, name, "headers"))
702
+ }));
703
+ const activeSecretField = draft.transport === "stdio" ? "env" : "headers";
704
+ const rows = rowSets[activeSecretField];
705
+ const [dirty, setDirty] = (0, import_react.useState)(false);
706
+ const [formError, setFormError] = (0, import_react.useState)();
707
+ const [confirmDelete, setConfirmDelete] = (0, import_react.useState)(false);
708
+ (0, import_react.useEffect)(() => {
709
+ if (dirty) return;
710
+ const nextTransport = typeof server.command === "string" ? "stdio" : "http";
711
+ setDraft({
712
+ transport: nextTransport,
713
+ command: server.command ?? "",
714
+ url: server.url ?? "",
715
+ argsText: JSON.stringify(server.args ?? [], null, 2),
716
+ disabled: server.disabled === true,
717
+ autoAllow: server.autoAllow === true,
718
+ idleTimeoutMinutes: String(server.idleTimeoutMinutes ?? 10)
719
+ });
720
+ setRowSets({
721
+ env: secretRows(secretKeysFromView(snapshot.settingsDocument.view, name, "env")),
722
+ headers: secretRows(secretKeysFromView(snapshot.settingsDocument.view, name, "headers"))
723
+ });
724
+ }, [dirty, name, server, snapshot.settingsDocument.view]);
725
+ const changeDraft = (patch) => {
726
+ setDirty(true);
727
+ setDraft((current) => ({ ...current, ...patch }));
728
+ };
729
+ const changeRows = (next) => {
730
+ setDirty(true);
731
+ setRowSets((current) => ({ ...current, [activeSecretField]: next }));
732
+ };
733
+ const save = async (event) => {
734
+ event.preventDefault();
735
+ setFormError(void 0);
736
+ try {
737
+ const nextTransport = draft.transport;
738
+ normalizeServerConfig(name, {
739
+ ...nextTransport === "stdio" ? { command: draft.command, args: parseArgs(draft.argsText) } : { url: draft.url },
740
+ disabled: draft.disabled,
741
+ autoAllow: draft.autoAllow,
742
+ idleTimeoutMinutes: Number(draft.idleTimeoutMinutes),
743
+ promotedTools: server.promotedTools
744
+ });
745
+ const field = nextTransport === "stdio" ? "env" : "headers";
746
+ secretRowOps(
747
+ name,
748
+ field,
749
+ rows,
750
+ secretKeysFromView(snapshot.settingsDocument.view, name, field)
751
+ );
752
+ } catch (nextError) {
753
+ setFormError(nextError instanceof Error ? nextError.message : String(nextError));
754
+ return;
755
+ }
756
+ if (await controller.saveServer(name, draft, rows)) {
757
+ setRowSets((current) => ({
758
+ ...current,
759
+ [activeSecretField]: rows.map((row) => ({
760
+ originalKey: row.key.trim(),
761
+ key: row.key.trim(),
762
+ value: ""
763
+ }))
764
+ }));
765
+ setDirty(false);
766
+ }
767
+ };
768
+ const remove = async () => {
769
+ if (await controller.deleteServer(name)) setConfirmDelete(false);
770
+ };
771
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-card", children: [
772
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-card-head", children: [
773
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
774
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-row", children: [
775
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `mcp-status-dot ${statusClass(status.state)}` }),
776
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "mcp-card-title", children: name }),
777
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-badge", children: status.state })
778
+ ] }),
779
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "mcp-muted", children: [
780
+ status.toolCount,
781
+ " ",
782
+ status.toolCount === 1 ? "tool" : "tools",
783
+ " cached",
784
+ status.transport === void 0 ? "" : ` \xB7 ${status.transport}`
785
+ ] })
786
+ ] }),
787
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-actions", children: [
788
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
789
+ "button",
790
+ {
791
+ type: "button",
792
+ className: "mcp-button",
793
+ disabled: snapshot.busy || draft.disabled,
794
+ onClick: () => void controller.reconnect(name),
795
+ children: "Reconnect"
796
+ }
797
+ ),
798
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
799
+ "button",
800
+ {
801
+ type: "button",
802
+ className: "mcp-button mcp-button-danger",
803
+ disabled: snapshot.busy,
804
+ onClick: () => setConfirmDelete(true),
805
+ children: "Delete"
806
+ }
807
+ )
808
+ ] })
809
+ ] }),
810
+ status.message !== void 0 && status.state === "error" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-error", role: "alert", children: status.message }),
811
+ confirmDelete && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-confirm", children: [
812
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("strong", { children: [
813
+ "Delete ",
814
+ name,
815
+ "?"
816
+ ] }),
817
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-muted", children: "This removes its Config and disconnects the Server." }),
818
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-actions", children: [
819
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
820
+ "button",
821
+ {
822
+ type: "button",
823
+ className: "mcp-button mcp-button-danger",
824
+ disabled: snapshot.busy,
825
+ onClick: () => void remove(),
826
+ children: "Confirm delete"
827
+ }
828
+ ),
829
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", { type: "button", className: "mcp-button", onClick: () => setConfirmDelete(false), children: "Cancel" })
830
+ ] })
831
+ ] }),
832
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("form", { className: "mcp-form", onSubmit: save, children: [
833
+ formError !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-error", role: "alert", children: formError }),
834
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-field-row", children: [
835
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
836
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Name" }),
837
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", { className: "mcp-input", value: name, readOnly: true })
838
+ ] }),
839
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
840
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Transport" }),
841
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
842
+ "select",
843
+ {
844
+ className: "mcp-select",
845
+ value: draft.transport,
846
+ disabled: snapshot.busy,
847
+ onChange: (event) => changeDraft({ transport: event.target.value }),
848
+ children: [
849
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "stdio", children: "stdio" }),
850
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "http", children: "Streamable HTTP" })
851
+ ]
852
+ }
853
+ )
854
+ ] })
855
+ ] }),
856
+ draft.transport === "stdio" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
857
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
858
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Command" }),
859
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
860
+ "input",
861
+ {
862
+ className: "mcp-input",
863
+ value: draft.command,
864
+ disabled: snapshot.busy,
865
+ onChange: (event) => changeDraft({ command: event.target.value })
866
+ }
867
+ )
868
+ ] }),
869
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
870
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Args as a JSON array" }),
871
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
872
+ "textarea",
873
+ {
874
+ className: "mcp-textarea",
875
+ value: draft.argsText,
876
+ disabled: snapshot.busy,
877
+ spellCheck: false,
878
+ onChange: (event) => changeDraft({ argsText: event.target.value })
879
+ }
880
+ )
881
+ ] }),
882
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SecretEditor, { field: "env", rows, onChange: changeRows, disabled: snapshot.busy })
883
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
884
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
885
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "URL" }),
886
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
887
+ "input",
888
+ {
889
+ className: "mcp-input",
890
+ value: draft.url,
891
+ disabled: snapshot.busy,
892
+ onChange: (event) => changeDraft({ url: event.target.value })
893
+ }
894
+ )
895
+ ] }),
896
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SecretEditor, { field: "headers", rows, onChange: changeRows, disabled: snapshot.busy })
897
+ ] }),
898
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-field-row", children: [
899
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-field", children: [
900
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Idle timeout in minutes" }),
901
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
902
+ "input",
903
+ {
904
+ className: "mcp-input",
905
+ type: "number",
906
+ min: "0.1",
907
+ step: "0.1",
908
+ value: draft.idleTimeoutMinutes,
909
+ disabled: snapshot.busy,
910
+ onChange: (event) => changeDraft({ idleTimeoutMinutes: event.target.value })
911
+ }
912
+ )
913
+ ] }),
914
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-field", children: [
915
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-label", children: "Call policy" }),
916
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-checkbox", children: [
917
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
918
+ "input",
919
+ {
920
+ type: "checkbox",
921
+ checked: draft.autoAllow,
922
+ disabled: snapshot.busy,
923
+ onChange: (event) => changeDraft({ autoAllow: event.target.checked })
924
+ }
925
+ ),
926
+ "Auto-allow calls from this Server"
927
+ ] })
928
+ ] })
929
+ ] }),
930
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-checkbox", children: [
931
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
932
+ "input",
933
+ {
934
+ type: "checkbox",
935
+ checked: draft.disabled,
936
+ disabled: snapshot.busy,
937
+ onChange: (event) => changeDraft({ disabled: event.target.checked })
938
+ }
939
+ ),
940
+ "Disable this Server"
941
+ ] }),
942
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-actions", children: [
943
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
944
+ "button",
945
+ {
946
+ type: "submit",
947
+ className: "mcp-button mcp-button-primary",
948
+ disabled: snapshot.busy || !dirty,
949
+ children: snapshot.busy ? "Saving\u2026" : "Save Server"
950
+ }
951
+ ),
952
+ dirty && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-muted", children: "Unsaved changes" })
953
+ ] })
954
+ ] }),
955
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-section", children: [
956
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
957
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h3", { className: "mcp-section-title", children: "Promotions" }),
958
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "mcp-muted", children: [
959
+ "Promote selected MCP tools as native tools named ",
960
+ name,
961
+ "__tool."
962
+ ] })
963
+ ] }),
964
+ tools.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-warning", children: "No cached tools are available. Reconnect to discover this Server." }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-promotion-list", children: tools.map((tool) => {
965
+ const checked = (server.promotedTools ?? []).includes(tool.name);
966
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { className: "mcp-promotion", children: [
967
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
968
+ "input",
969
+ {
970
+ type: "checkbox",
971
+ checked,
972
+ disabled: snapshot.busy,
973
+ onChange: () => void controller.togglePromotion(name, tool.name)
974
+ }
975
+ ),
976
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { children: [
977
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-promotion-name", children: tool.name }),
978
+ tool.description !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-promotion-description", children: tool.description })
979
+ ] })
980
+ ] }, tool.name);
981
+ }) })
982
+ ] })
983
+ ] });
984
+ }
985
+ function McpSettingsPage({ controller }) {
986
+ const snapshot = (0, import_react.useSyncExternalStore)(controller.subscribe, controller.getSnapshot);
987
+ const [selected, setSelected] = (0, import_react.useState)();
988
+ const [dialog, setDialog] = (0, import_react.useState)();
989
+ const settings = snapshot.settings;
990
+ const servers = settings.value?.mcpServers ?? {};
991
+ const names = (0, import_react.useMemo)(() => Object.keys(servers).sort(), [servers]);
992
+ (0, import_react.useEffect)(() => controller.mount(), [controller]);
993
+ (0, import_react.useEffect)(() => {
994
+ if (selected === void 0 || !Object.hasOwn(servers, selected)) {
995
+ setSelected(names[0]);
996
+ }
997
+ }, [names, selected, servers]);
998
+ if (settings.status === "loading") {
999
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("section", { className: "mcp-settings", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mcp-muted", children: "Loading MCP Settings\u2026" }) });
1000
+ }
1001
+ if (settings.status !== "ready") {
1002
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "mcp-settings", children: [
1003
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: "mcp-title", children: "MCP" }),
1004
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-error", role: "alert", children: settings.error?.message ?? "MCP Settings are unavailable." })
1005
+ ] });
1006
+ }
1007
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { className: "mcp-settings", children: [
1008
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-toolbar", children: [
1009
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [
1010
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", { className: "mcp-title", children: "MCP" }),
1011
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mcp-intro", children: "Connect MCP Servers and choose how their tools reach the Agent." })
1012
+ ] }),
1013
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-actions", children: [
1014
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1015
+ "button",
1016
+ {
1017
+ type: "button",
1018
+ className: "mcp-button",
1019
+ disabled: snapshot.busy || !settings.writable,
1020
+ onClick: () => setDialog("import"),
1021
+ children: "Import JSON"
1022
+ }
1023
+ ),
1024
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1025
+ "button",
1026
+ {
1027
+ type: "button",
1028
+ className: "mcp-button mcp-button-primary",
1029
+ disabled: snapshot.busy || !settings.writable,
1030
+ onClick: () => setDialog("add"),
1031
+ children: "Add Server"
1032
+ }
1033
+ )
1034
+ ] })
1035
+ ] }),
1036
+ !settings.writable && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-warning", children: "The active Settings Provider is read-only." }),
1037
+ snapshot.error !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "mcp-error", role: "alert", children: snapshot.error }),
1038
+ names.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-empty", children: [
1039
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", { className: "mcp-card-title", children: "No MCP Servers" }),
1040
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { className: "mcp-muted", children: "Add a Server or import an existing mcpServers Config." }),
1041
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-actions", children: [
1042
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1043
+ "button",
1044
+ {
1045
+ type: "button",
1046
+ className: "mcp-button mcp-button-primary",
1047
+ disabled: !settings.writable,
1048
+ onClick: () => setDialog("add"),
1049
+ children: "Add Server"
1050
+ }
1051
+ ),
1052
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1053
+ "button",
1054
+ {
1055
+ type: "button",
1056
+ className: "mcp-button",
1057
+ disabled: !settings.writable,
1058
+ onClick: () => setDialog("import"),
1059
+ children: "Import JSON"
1060
+ }
1061
+ )
1062
+ ] })
1063
+ ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mcp-layout", children: [
1064
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("nav", { className: "mcp-sidebar", "aria-label": "MCP Servers", children: names.map((name) => {
1065
+ const status = statusFor(snapshot.overview, name);
1066
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1067
+ "button",
1068
+ {
1069
+ type: "button",
1070
+ className: "mcp-server-button",
1071
+ "aria-current": name === selected,
1072
+ onClick: () => setSelected(name),
1073
+ children: [
1074
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `mcp-status-dot ${statusClass(status.state)}` }),
1075
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-server-name", children: name }),
1076
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "mcp-server-count", children: status.toolCount })
1077
+ ]
1078
+ },
1079
+ name
1080
+ );
1081
+ }) }),
1082
+ selected !== void 0 && servers[selected] !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1083
+ ServerDetail,
1084
+ {
1085
+ controller,
1086
+ snapshot,
1087
+ name: selected,
1088
+ server: servers[selected]
1089
+ },
1090
+ selected
1091
+ )
1092
+ ] }),
1093
+ dialog === "add" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AddServerDialog, { controller, busy: snapshot.busy, onClose: () => setDialog() }),
1094
+ dialog === "import" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ImportDialog, { controller, busy: snapshot.busy, onClose: () => setDialog() })
1095
+ ] });
1096
+ }
1097
+
1098
+ // src/client/styles.js
1099
+ var MCP_SETTINGS_CSS = `
1100
+ .mcp-settings{max-width:980px;color:var(--dsw-alias-label-primary);display:flex;flex-direction:column;gap:16px}
1101
+ .mcp-settings *{box-sizing:border-box}
1102
+ .mcp-title{margin:0;font-size:18px;line-height:26px;font-weight:600}
1103
+ .mcp-intro,.mcp-muted{margin:0;color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px}
1104
+ .mcp-toolbar,.mcp-actions,.mcp-row,.mcp-card-head{display:flex;align-items:center;gap:8px}
1105
+ .mcp-toolbar{justify-content:space-between;flex-wrap:wrap}
1106
+ .mcp-actions{flex-wrap:wrap}
1107
+ .mcp-button{height:34px;border:1px solid var(--dsw-alias-border-l2);border-radius:17px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);padding:0 13px;font:inherit;font-size:13px;cursor:pointer}
1108
+ .mcp-button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
1109
+ .mcp-button:disabled{cursor:not-allowed;opacity:.5}
1110
+ .mcp-button-primary{border-color:transparent;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}
1111
+ .mcp-button-primary:hover:not(:disabled){background:var(--dsw-alias-button-primary-fill)}
1112
+ .mcp-button-danger{color:var(--dsw-alias-state-error-primary)}
1113
+ .mcp-error,.mcp-warning{border-radius:8px;padding:9px 11px;font-size:12px;line-height:18px}
1114
+ .mcp-error{background:color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);color:var(--dsw-alias-state-error-primary)}
1115
+ .mcp-warning{background:var(--dsw-alias-state-warn-tertiary);color:var(--dsw-alias-state-warn-label)}
1116
+ .mcp-layout{display:grid;grid-template-columns:240px minmax(0,1fr);gap:12px;align-items:start}
1117
+ .mcp-sidebar,.mcp-card,.mcp-empty,.mcp-modal-card{border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-base)}
1118
+ .mcp-sidebar{padding:6px;display:flex;flex-direction:column;gap:3px}
1119
+ .mcp-server-button{width:100%;border:0;border-radius:8px;background:transparent;color:var(--dsw-alias-label-primary);padding:9px 10px;display:grid;grid-template-columns:auto minmax(0,1fr) auto;gap:8px;align-items:center;text-align:left;cursor:pointer;font:inherit}
1120
+ .mcp-server-button:hover{background:var(--dsw-alias-interactive-bg-hover)}
1121
+ .mcp-server-button[aria-current=true]{background:var(--dsw-alias-button-ghost-active-fill)}
1122
+ .mcp-server-name{min-width:0;overflow:hidden;text-overflow:ellipsis;font-size:13px;font-weight:500}
1123
+ .mcp-server-count{color:var(--dsw-alias-label-tertiary);font-size:11px}
1124
+ .mcp-status-dot{width:8px;height:8px;border-radius:50%;background:var(--dsw-alias-label-quaternary)}
1125
+ .mcp-status-connected{background:var(--dsw-alias-state-success-primary)}
1126
+ .mcp-status-connecting{background:var(--dsw-alias-state-warn-primary)}
1127
+ .mcp-status-error{background:var(--dsw-alias-state-error-primary)}
1128
+ .mcp-status-disabled{background:var(--dsw-alias-label-quaternary)}
1129
+ .mcp-card{padding:16px;display:flex;flex-direction:column;gap:16px;min-width:0}
1130
+ .mcp-card-head{align-items:flex-start}
1131
+ .mcp-card-title{margin:0;font-size:16px;line-height:24px;font-weight:600;overflow-wrap:anywhere}
1132
+ .mcp-card-head .mcp-actions{margin-left:auto;justify-content:flex-end}
1133
+ .mcp-badge{border:1px solid var(--dsw-alias-border-l3);border-radius:5px;padding:2px 7px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:16px}
1134
+ .mcp-form{display:flex;flex-direction:column;gap:13px}
1135
+ .mcp-field{display:flex;flex-direction:column;gap:5px;min-width:0}
1136
+ .mcp-field-row{display:grid;grid-template-columns:1fr 1fr;gap:10px}
1137
+ .mcp-label{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500;line-height:18px}
1138
+ .mcp-input,.mcp-select,.mcp-textarea{width:100%;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;outline:none}
1139
+ .mcp-input,.mcp-select{height:36px;padding:0 10px}
1140
+ .mcp-textarea{min-height:82px;padding:9px 10px;resize:vertical;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;line-height:19px}
1141
+ .mcp-input:focus,.mcp-select:focus,.mcp-textarea:focus{border-color:var(--dsw-alias-state-business-primary)}
1142
+ .mcp-checkbox{display:flex;align-items:flex-start;gap:8px;color:var(--dsw-alias-label-secondary);font-size:13px;line-height:20px}
1143
+ .mcp-checkbox input{margin-top:3px}
1144
+ .mcp-section{border-top:1px solid var(--dsw-alias-border-l3);padding-top:14px;display:flex;flex-direction:column;gap:10px}
1145
+ .mcp-section-title{margin:0;font-size:13px;font-weight:600;line-height:20px}
1146
+ .mcp-secret-row{display:grid;grid-template-columns:minmax(100px,.8fr) minmax(140px,1.2fr) auto;gap:7px;align-items:center}
1147
+ .mcp-promotion-list{display:flex;flex-direction:column;gap:6px;max-height:260px;overflow:auto}
1148
+ .mcp-promotion{border:1px solid var(--dsw-alias-border-l3);border-radius:8px;padding:8px 10px;display:grid;grid-template-columns:auto minmax(0,1fr);gap:9px;align-items:start}
1149
+ .mcp-promotion-name{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px;overflow-wrap:anywhere}
1150
+ .mcp-promotion-description{display:block;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
1151
+ .mcp-empty{padding:34px 20px;text-align:center;display:flex;align-items:center;flex-direction:column;gap:10px}
1152
+ .mcp-overlay{position:fixed;inset:0;background:rgba(0,0,0,.28);display:flex;align-items:center;justify-content:center;padding:20px;z-index:1000}
1153
+ .mcp-modal-card{width:min(620px,100%);max-height:min(760px,90vh);overflow:auto;padding:18px;display:flex;flex-direction:column;gap:14px;box-shadow:0 18px 60px rgba(0,0,0,.24)}
1154
+ .mcp-modal-title{margin:0;font-size:16px;line-height:24px}
1155
+ .mcp-confirm{border:1px solid var(--dsw-alias-state-error-primary);border-radius:8px;padding:10px;display:flex;flex-direction:column;gap:8px}
1156
+ @media(max-width:760px){.mcp-layout{grid-template-columns:1fr}.mcp-sidebar{max-height:190px;overflow:auto}.mcp-field-row{grid-template-columns:1fr}.mcp-card-head{flex-direction:column}.mcp-card-head .mcp-actions{margin-left:0}.mcp-secret-row{grid-template-columns:1fr}.mcp-toolbar{align-items:flex-start}}
1157
+ `;
1158
+ function installMcpSettingsStyles() {
1159
+ const id = "dsh-mcp-adapter-settings";
1160
+ const existing = document.querySelector(`style[data-plugin-css="${id}"]`);
1161
+ if (existing !== null) return () => {
1162
+ };
1163
+ const style = document.createElement("style");
1164
+ style.dataset.pluginCss = id;
1165
+ style.textContent = MCP_SETTINGS_CSS;
1166
+ document.head.appendChild(style);
1167
+ return () => style.remove();
1168
+ }
1169
+
1170
+ // src/client/index.jsx
1171
+ var inject = ["slots", "settingsScope", "connection"];
1172
+ function apply(ctx) {
1173
+ const scope = ctx.settingsScope.bind({ namespace: MCP_SETTINGS_NAMESPACE });
1174
+ const describe = ctx.settingsScope.describe();
1175
+ const controller = new McpSettingsController({
1176
+ scope,
1177
+ describe,
1178
+ settingsApi: ctx.connection.api.settings,
1179
+ rpc: (endpoint, payload, signal) => ctx.connection.rpc.call(MCP_RPC_CHANNEL, endpoint, payload, signal)
1180
+ });
1181
+ ctx.effect(() => installMcpSettingsStyles(), "mcp-adapter: Settings styles");
1182
+ ctx.effect(() => () => controller.dispose(), "mcp-adapter: Settings controller");
1183
+ ctx.slots.inject(
1184
+ "settings.section",
1185
+ () => ctx.slots.register(
1186
+ {
1187
+ name: "settings.section",
1188
+ id: "mcp",
1189
+ order: 30,
1190
+ label: "MCP",
1191
+ inject: () => ({ controller })
1192
+ },
1193
+ McpSettingsPage
1194
+ )
1195
+ );
1196
+ }
1197
+ return module.exports;
1198
+ }
1199
+ });
1200
+ //# sourceMappingURL=client.js.map