@modelcontextprotocol/inspector 1.0.0 → 2.0.0-rc.1

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.
Files changed (48) hide show
  1. package/README.md +199 -351
  2. package/clients/cli/build/index.js +13218 -0
  3. package/clients/launcher/build/index.js +90 -0
  4. package/clients/launcher/build/parse-launcher-argv.js +29 -0
  5. package/clients/tui/build/chunk-MCKGQKYU.js +15 -0
  6. package/clients/tui/build/index.js +14573 -0
  7. package/clients/tui/build/undici-HXPKCIY3.js +25789 -0
  8. package/clients/web/build/chunk-MCKGQKYU.js +15 -0
  9. package/clients/web/build/index.js +4322 -0
  10. package/clients/web/build/undici-HXPKCIY3.js +25789 -0
  11. package/clients/web/dist/assets/css-DfBGKsUM.js +1 -0
  12. package/clients/web/dist/assets/index-Cbtmmw0V.js +300 -0
  13. package/clients/web/dist/assets/index-CiUpWldH.css +1 -0
  14. package/clients/web/dist/assets/json-rfaH9Mx5.js +1 -0
  15. package/clients/web/dist/assets/markdown-PVXc4EJ0.js +1 -0
  16. package/clients/web/dist/assets/markup-Bk4keGDK.js +1 -0
  17. package/clients/web/dist/assets/markup-DPsXihXF.js +1 -0
  18. package/clients/web/dist/assets/prism-light-B8tIwqDr.js +6 -0
  19. package/clients/web/dist/assets/tomorrow-COrD2Qnh.js +1 -0
  20. package/clients/web/dist/assets/yaml-B78V9E0m.js +1 -0
  21. package/clients/web/dist/icons.svg +24 -0
  22. package/clients/web/dist/index.html +17 -0
  23. package/package.json +96 -80
  24. package/scripts/install-clients.mjs +65 -0
  25. package/LICENSE +0 -216
  26. package/cli/build/cli.js +0 -277
  27. package/cli/build/client/connection.js +0 -38
  28. package/cli/build/client/index.js +0 -6
  29. package/cli/build/client/prompts.js +0 -43
  30. package/cli/build/client/resources.js +0 -36
  31. package/cli/build/client/tools.js +0 -87
  32. package/cli/build/client/types.js +0 -1
  33. package/cli/build/error-handler.js +0 -18
  34. package/cli/build/index.js +0 -257
  35. package/cli/build/transport.js +0 -65
  36. package/cli/build/utils/awaitable-log.js +0 -7
  37. package/client/bin/client.js +0 -62
  38. package/client/bin/start.js +0 -350
  39. package/client/dist/assets/OAuthCallback-D56s1eS_.js +0 -55
  40. package/client/dist/assets/OAuthDebugCallback-CdGJkhXe.js +0 -64
  41. package/client/dist/assets/index-BlUSNvoM.css +0 -2738
  42. package/client/dist/assets/index-DjSLdS7f.js +0 -66698
  43. package/client/dist/index.html +0 -14
  44. package/server/build/index.js +0 -886
  45. package/server/build/mcpProxy.js +0 -121
  46. package/server/build/static/sandbox_proxy.html +0 -197
  47. package/server/static/sandbox_proxy.html +0 -197
  48. /package/{client/dist/mcp.svg → clients/web/dist/assets/MCP-CWGQJYyH.svg} +0 -0
@@ -0,0 +1,4322 @@
1
+ #!/usr/bin/env node
2
+ import "./chunk-MCKGQKYU.js";
3
+
4
+ // src/index.ts
5
+ import { resolve as resolve5 } from "path";
6
+ import { fileURLToPath as fileURLToPath7 } from "url";
7
+
8
+ // server/run-web.ts
9
+ import { resolve as resolve4, join as join9, dirname as dirname7 } from "path";
10
+ import { fileURLToPath as fileURLToPath6 } from "url";
11
+ import { Command } from "commander";
12
+
13
+ // ../../core/mcp/node/config.ts
14
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
15
+ import { dirname as dirname2, resolve as resolve2 } from "path";
16
+
17
+ // ../../core/storage/store-io.ts
18
+ import * as path from "path";
19
+ import * as fs from "fs/promises";
20
+ import { readFile, writeFile } from "atomically";
21
+
22
+ // ../../core/storage/store-id.ts
23
+ function validateStoreId(storeId) {
24
+ return /^[a-zA-Z0-9_-]+$/.test(storeId) && storeId.length > 0;
25
+ }
26
+
27
+ // ../../core/storage/store-serialize.ts
28
+ function serializeStore(data) {
29
+ return JSON.stringify(data, null, 2);
30
+ }
31
+ function parseStore(raw) {
32
+ return JSON.parse(raw);
33
+ }
34
+
35
+ // ../../core/storage/store-io.ts
36
+ function getDefaultStorageDir() {
37
+ const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
38
+ return path.join(homeDir, ".mcp-inspector", "storage");
39
+ }
40
+ function getDefaultMcpConfigPath() {
41
+ const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
42
+ return path.join(homeDir, ".mcp-inspector", "mcp.json");
43
+ }
44
+ function getStoreFilePath(storageDir, storeId) {
45
+ return path.join(storageDir, `${storeId}.json`);
46
+ }
47
+ async function readStoreFile(filePath) {
48
+ try {
49
+ const data = await readFile(filePath, { encoding: "utf-8" });
50
+ return data;
51
+ } catch (error) {
52
+ const err = error;
53
+ if (err.code === "ENOENT") {
54
+ return null;
55
+ }
56
+ throw error;
57
+ }
58
+ }
59
+ var pendingWrites = /* @__PURE__ */ new Map();
60
+ async function writeStoreFile(filePath, data) {
61
+ const key = path.resolve(filePath);
62
+ const run = async () => {
63
+ const dir = path.dirname(filePath);
64
+ await fs.mkdir(dir, { recursive: true });
65
+ await writeFile(filePath, data, {
66
+ encoding: "utf-8",
67
+ mode: 384
68
+ });
69
+ };
70
+ const prior = pendingWrites.get(key);
71
+ const tracked = (prior ?? Promise.resolve()).catch(() => {
72
+ }).then(run);
73
+ pendingWrites.set(key, tracked);
74
+ try {
75
+ await tracked;
76
+ } finally {
77
+ if (pendingWrites.get(key) === tracked) {
78
+ pendingWrites.delete(key);
79
+ }
80
+ }
81
+ }
82
+ async function deleteStoreFile(filePath) {
83
+ try {
84
+ await fs.unlink(filePath);
85
+ } catch (error) {
86
+ const err = error;
87
+ if (err.code !== "ENOENT") {
88
+ throw error;
89
+ }
90
+ }
91
+ }
92
+
93
+ // ../../core/mcp/types.ts
94
+ var DEFAULT_TASK_TTL_MS = 6e4;
95
+ var DEFAULT_MAX_FETCH_REQUESTS = 1e3;
96
+ var DEFAULT_PROTOCOL_ERA = "legacy";
97
+ var DEFAULT_MODERN_LOG_LEVEL = "debug";
98
+ var MODERN_LOG_LEVELS = [
99
+ "off",
100
+ "debug",
101
+ "info",
102
+ "notice",
103
+ "warning",
104
+ "error",
105
+ "critical",
106
+ "alert",
107
+ "emergency"
108
+ ];
109
+ function isModernLogLevel(value) {
110
+ return typeof value === "string" && MODERN_LOG_LEVELS.includes(value);
111
+ }
112
+
113
+ // ../../core/auth/secret-fields.ts
114
+ var SECRET_FIELD_OAUTH_CLIENT_SECRET = "oauth-client-secret";
115
+ var SECRET_FIELD_IDP_CLIENT_SECRET = "idp-client-secret";
116
+ var envSecretField = (envKey) => `env:${envKey}`;
117
+
118
+ // ../../core/json/jsonUtils.ts
119
+ function toRecord(value) {
120
+ return value;
121
+ }
122
+
123
+ // ../../core/mcp/serverList.ts
124
+ var VALID_SERVER_TYPES = /* @__PURE__ */ new Set([
125
+ "stdio",
126
+ "sse",
127
+ "streamable-http"
128
+ ]);
129
+ var VALID_PROTOCOL_ERAS = /* @__PURE__ */ new Set([
130
+ "legacy",
131
+ "auto",
132
+ "modern"
133
+ ]);
134
+ function isProtocolEra(value) {
135
+ return typeof value === "string" && VALID_PROTOCOL_ERAS.has(value);
136
+ }
137
+ function normalizeServerType(config) {
138
+ const type = config.type;
139
+ let normalizedType;
140
+ if (typeof type !== "string") {
141
+ normalizedType = "stdio";
142
+ } else if (type === "http") {
143
+ normalizedType = "streamable-http";
144
+ } else if (VALID_SERVER_TYPES.has(type)) {
145
+ normalizedType = type;
146
+ } else {
147
+ normalizedType = "stdio";
148
+ }
149
+ return { ...config, type: normalizedType };
150
+ }
151
+ function cleanRoots(roots) {
152
+ if (!Array.isArray(roots)) {
153
+ console.warn("Ignoring `roots`: expected an array, got", typeof roots);
154
+ return [];
155
+ }
156
+ return roots.filter((r) => {
157
+ if (typeof r?.uri !== "string") {
158
+ console.warn("Dropping root without a string `uri`:", r);
159
+ return false;
160
+ }
161
+ return r.uri.trim() !== "";
162
+ }).map((r) => {
163
+ const rawName = r.name;
164
+ if (rawName !== void 0 && typeof rawName !== "string") {
165
+ console.warn("Dropping non-string `name` on root:", r);
166
+ }
167
+ const trimmedName = typeof rawName === "string" ? rawName.trim() : void 0;
168
+ const { name: _name, ...rest } = r;
169
+ return trimmedName ? { ...rest, name: trimmedName } : rest;
170
+ });
171
+ }
172
+ function envRecordToPairs(env) {
173
+ return env ? Object.entries(env).map(([key, value]) => ({ key, value })) : [];
174
+ }
175
+ function envPairsToRecord(pairs) {
176
+ const out = {};
177
+ for (const { key, value } of pairs) {
178
+ if (key.trim() === "") continue;
179
+ out[key] = value;
180
+ }
181
+ return out;
182
+ }
183
+ function storedFieldsToInspectorSettings(stored) {
184
+ const hasAny = stored.headers !== void 0 || stored.metadata !== void 0 || stored.connectionTimeout !== void 0 || stored.requestTimeout !== void 0 || stored.taskTtl !== void 0 || stored.autoRefreshOnListChanged !== void 0 || stored.paginatedLists !== void 0 || stored.advertisedExtensions !== void 0 || stored.maxFetchRequests !== void 0 || stored.oauth !== void 0 || stored.roots !== void 0 || stored.protocolEra !== void 0 || stored.modernLogLevel !== void 0 || stored.env !== void 0 || stored.cwd !== void 0;
185
+ if (!hasAny) return void 0;
186
+ const headersPairs = stored.headers ? Object.entries(stored.headers).map(([key, value]) => ({ key, value })) : [];
187
+ const settings = {
188
+ headers: headersPairs,
189
+ env: envRecordToPairs(stored.env),
190
+ metadata: stored.metadata ?? [],
191
+ connectionTimeout: stored.connectionTimeout ?? 0,
192
+ requestTimeout: stored.requestTimeout ?? 0,
193
+ // Unlike the timeouts (0 = "SDK default"), task TTL has a concrete product
194
+ // default so the form shows it and "Run as task" has a value to send.
195
+ taskTtl: stored.taskTtl ?? DEFAULT_TASK_TTL_MS,
196
+ autoRefreshOnListChanged: stored.autoRefreshOnListChanged ?? false,
197
+ paginatedLists: stored.paginatedLists ?? false,
198
+ // Concrete default like taskTtl (not a 0-sentinel): the form needs a value
199
+ // to render and the log state needs one to size its buffer. An absent
200
+ // on-disk field reads back as the default, which the write side then omits.
201
+ maxFetchRequests: stored.maxFetchRequests ?? DEFAULT_MAX_FETCH_REQUESTS,
202
+ // Defaults to an empty list so the form always has a concrete array to
203
+ // render controlled rows from. An absent on-disk `roots` reads back as
204
+ // `[]`, which `inspectorSettingsToStoredFields` then omits on write.
205
+ roots: stored.roots ?? []
206
+ };
207
+ if (isProtocolEra(stored.protocolEra)) {
208
+ settings.protocolEra = stored.protocolEra;
209
+ }
210
+ if (isModernLogLevel(stored.modernLogLevel)) {
211
+ settings.modernLogLevel = stored.modernLogLevel;
212
+ }
213
+ if (stored.advertisedExtensions && Object.keys(stored.advertisedExtensions).length > 0) {
214
+ settings.advertisedExtensions = { ...stored.advertisedExtensions };
215
+ }
216
+ if (stored.oauth?.clientId) settings.oauthClientId = stored.oauth.clientId;
217
+ if (stored.oauth?.clientSecret)
218
+ settings.oauthClientSecret = stored.oauth.clientSecret;
219
+ if (stored.oauth?.scopes) settings.oauthScopes = stored.oauth.scopes;
220
+ if (stored.oauth?.onInsufficientScope) {
221
+ settings.oauthOnInsufficientScope = stored.oauth.onInsufficientScope;
222
+ }
223
+ if (stored.oauth?.enterpriseManaged === true) {
224
+ settings.enterpriseManaged = true;
225
+ }
226
+ if (stored.cwd) settings.cwd = stored.cwd;
227
+ return settings;
228
+ }
229
+ function inspectorSettingsToStoredFields(settings) {
230
+ const out = {};
231
+ const headersRecord = {};
232
+ for (const { key, value } of settings.headers) {
233
+ if (key.trim() === "") continue;
234
+ headersRecord[key] = value;
235
+ }
236
+ if (Object.keys(headersRecord).length > 0) {
237
+ out.headers = headersRecord;
238
+ }
239
+ const metadataFiltered = settings.metadata.filter((m) => m.key.trim() !== "");
240
+ if (metadataFiltered.length > 0) {
241
+ out.metadata = metadataFiltered;
242
+ }
243
+ if (settings.connectionTimeout > 0) {
244
+ out.connectionTimeout = settings.connectionTimeout;
245
+ }
246
+ if (settings.requestTimeout > 0) {
247
+ out.requestTimeout = settings.requestTimeout;
248
+ }
249
+ if (settings.taskTtl > 0 && settings.taskTtl !== DEFAULT_TASK_TTL_MS) {
250
+ out.taskTtl = settings.taskTtl;
251
+ }
252
+ if (settings.autoRefreshOnListChanged) {
253
+ out.autoRefreshOnListChanged = true;
254
+ }
255
+ if (settings.paginatedLists) {
256
+ out.paginatedLists = true;
257
+ }
258
+ if (settings.advertisedExtensions && Object.keys(settings.advertisedExtensions).length > 0) {
259
+ out.advertisedExtensions = { ...settings.advertisedExtensions };
260
+ }
261
+ if (settings.protocolEra !== void 0 && settings.protocolEra !== DEFAULT_PROTOCOL_ERA) {
262
+ out.protocolEra = settings.protocolEra;
263
+ }
264
+ if (settings.modernLogLevel !== void 0 && settings.modernLogLevel !== DEFAULT_MODERN_LOG_LEVEL) {
265
+ out.modernLogLevel = settings.modernLogLevel;
266
+ }
267
+ if (settings.maxFetchRequests !== DEFAULT_MAX_FETCH_REQUESTS) {
268
+ out.maxFetchRequests = settings.maxFetchRequests;
269
+ }
270
+ const oauthFields = {};
271
+ if (settings.oauthClientId) oauthFields.clientId = settings.oauthClientId;
272
+ if (settings.oauthClientSecret)
273
+ oauthFields.clientSecret = settings.oauthClientSecret;
274
+ if (settings.oauthScopes) oauthFields.scopes = settings.oauthScopes;
275
+ if (settings.oauthOnInsufficientScope) {
276
+ oauthFields.onInsufficientScope = settings.oauthOnInsufficientScope;
277
+ }
278
+ if (settings.enterpriseManaged === true) {
279
+ oauthFields.enterpriseManaged = true;
280
+ }
281
+ if (Object.keys(oauthFields).length > 0) {
282
+ out.oauth = oauthFields;
283
+ }
284
+ const rootsFiltered = cleanRoots(settings.roots);
285
+ if (rootsFiltered.length > 0) {
286
+ out.roots = rootsFiltered;
287
+ }
288
+ return out;
289
+ }
290
+ var INSPECTOR_FIELD_KEY_MAP = {
291
+ headers: true,
292
+ metadata: true,
293
+ protocolEra: true,
294
+ modernLogLevel: true,
295
+ connectionTimeout: true,
296
+ requestTimeout: true,
297
+ taskTtl: true,
298
+ autoRefreshOnListChanged: true,
299
+ paginatedLists: true,
300
+ advertisedExtensions: true,
301
+ maxFetchRequests: true,
302
+ oauth: true,
303
+ roots: true
304
+ };
305
+ var INSPECTOR_FIELD_KEYS = new Set(
306
+ Object.keys(INSPECTOR_FIELD_KEY_MAP)
307
+ );
308
+ function stripInspectorFields(stored) {
309
+ const out = { ...stored };
310
+ for (const key of INSPECTOR_FIELD_KEYS) {
311
+ delete out[key];
312
+ }
313
+ return out;
314
+ }
315
+ var isStdioStored = (stored) => stored.type === "stdio" || stored.type === void 0;
316
+ function extractSecretsFromStored(stored) {
317
+ const secrets = {};
318
+ const stripped = { ...stored };
319
+ if (stored.oauth?.clientSecret) {
320
+ secrets[SECRET_FIELD_OAUTH_CLIENT_SECRET] = stored.oauth.clientSecret;
321
+ const restOauth = {};
322
+ if (stored.oauth.clientId !== void 0)
323
+ restOauth.clientId = stored.oauth.clientId;
324
+ if (stored.oauth.scopes !== void 0)
325
+ restOauth.scopes = stored.oauth.scopes;
326
+ if (stored.oauth.enterpriseManaged === true) {
327
+ restOauth.enterpriseManaged = true;
328
+ }
329
+ if (Object.keys(restOauth).length > 0) {
330
+ stripped.oauth = restOauth;
331
+ } else {
332
+ delete stripped.oauth;
333
+ }
334
+ }
335
+ if (isStdioStored(stripped)) {
336
+ const env = stripped.env;
337
+ if (env) {
338
+ const newEnv = {};
339
+ for (const [k, v] of Object.entries(env)) {
340
+ if (typeof v === "string" && v.length > 0) {
341
+ secrets[envSecretField(k)] = v;
342
+ }
343
+ newEnv[k] = "";
344
+ }
345
+ stripped.env = newEnv;
346
+ }
347
+ }
348
+ return { stripped, secrets };
349
+ }
350
+ function mergeSecretsIntoStored(stored, secrets) {
351
+ const out = { ...stored };
352
+ const oauthSecret = secrets[SECRET_FIELD_OAUTH_CLIENT_SECRET];
353
+ if (oauthSecret) {
354
+ out.oauth = { ...out.oauth ?? {}, clientSecret: oauthSecret };
355
+ }
356
+ if (isStdioStored(out) && out.env) {
357
+ const newEnv = { ...out.env };
358
+ let mutated = false;
359
+ for (const k of Object.keys(out.env)) {
360
+ const val = secrets[envSecretField(k)];
361
+ if (val !== void 0) {
362
+ newEnv[k] = val;
363
+ mutated = true;
364
+ }
365
+ }
366
+ if (mutated) {
367
+ out.env = newEnv;
368
+ }
369
+ }
370
+ return out;
371
+ }
372
+ function expectedSecretFields(stored) {
373
+ const fields = [];
374
+ fields.push(SECRET_FIELD_OAUTH_CLIENT_SECRET);
375
+ if (isStdioStored(stored) && stored.env) {
376
+ for (const k of Object.keys(stored.env)) {
377
+ fields.push(envSecretField(k));
378
+ }
379
+ }
380
+ return fields;
381
+ }
382
+ var DEFAULT_SEED_CONFIG = {
383
+ mcpServers: {
384
+ "filesystem-server-default": {
385
+ type: "stdio",
386
+ command: "npx",
387
+ args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
388
+ },
389
+ "everything-server-default": {
390
+ type: "stdio",
391
+ command: "npx",
392
+ args: ["-y", "@modelcontextprotocol/server-everything"]
393
+ }
394
+ }
395
+ };
396
+
397
+ // ../../core/mcp/node/config.ts
398
+ function parseKeyValuePair(value, previous = {}) {
399
+ const parts = value.split("=");
400
+ const key = parts[0] ?? "";
401
+ const val = parts.slice(1).join("=");
402
+ if (!key || val === void 0 || val === "") {
403
+ throw new Error(
404
+ `Invalid parameter format: ${value}. Use key=value format.`
405
+ );
406
+ }
407
+ return { ...previous, [key]: val };
408
+ }
409
+ function parseHeaderPair(value, previous = {}) {
410
+ const colonIndex = value.indexOf(":");
411
+ if (colonIndex === -1) {
412
+ throw new Error(
413
+ `Invalid header format: ${value}. Use "HeaderName: Value" format.`
414
+ );
415
+ }
416
+ const key = value.slice(0, colonIndex).trim();
417
+ const val = value.slice(colonIndex + 1).trim();
418
+ if (key === "" || val === "") {
419
+ throw new Error(
420
+ `Invalid header format: ${value}. Use "HeaderName: Value" format.`
421
+ );
422
+ }
423
+ return { ...previous, [key]: val };
424
+ }
425
+ var EMPTY_CATALOG_CONTENT = `${JSON.stringify({ mcpServers: {} }, null, 2)}
426
+ `;
427
+ function seedEmptyCatalog(resolvedPath) {
428
+ mkdirSync(dirname2(resolvedPath), { recursive: true });
429
+ writeFileSync(resolvedPath, EMPTY_CATALOG_CONTENT, { mode: 384 });
430
+ }
431
+ function readServerListContent(configPath, writable) {
432
+ const resolvedPath = resolve2(process.cwd(), configPath);
433
+ if (!existsSync(resolvedPath)) {
434
+ if (writable) {
435
+ seedEmptyCatalog(resolvedPath);
436
+ return EMPTY_CATALOG_CONTENT;
437
+ }
438
+ throw new Error(`Config file not found: ${resolvedPath}`);
439
+ }
440
+ return readFileSync(resolvedPath, "utf-8");
441
+ }
442
+ function loadMcpServersConfig(configPath, writable) {
443
+ try {
444
+ const configContent = readServerListContent(configPath, writable);
445
+ const config = JSON.parse(configContent);
446
+ if (!config.mcpServers) {
447
+ throw new Error("Configuration file must contain an mcpServers element");
448
+ }
449
+ const normalizedServers = {};
450
+ for (const [name, raw] of Object.entries(config.mcpServers)) {
451
+ normalizedServers[name] = normalizeServerType(toRecord(raw));
452
+ }
453
+ return { ...config, mcpServers: normalizedServers };
454
+ } catch (error) {
455
+ if (error instanceof Error) {
456
+ throw new Error(`Error loading configuration: ${error.message}`);
457
+ }
458
+ throw new Error("Error loading configuration: Unknown error");
459
+ }
460
+ }
461
+ function loadServerFromConfig(configPath, serverName, writable) {
462
+ const config = loadMcpServersConfig(configPath, writable);
463
+ if (!config.mcpServers[serverName]) {
464
+ const available = Object.keys(config.mcpServers).join(", ");
465
+ throw new Error(
466
+ `Server '${serverName}' not found in config file. Available servers: ${available}`
467
+ );
468
+ }
469
+ return config.mcpServers[serverName];
470
+ }
471
+ function buildConfigFromOptions(options) {
472
+ const target = options.target ?? [];
473
+ const first = target[0];
474
+ const rest = target.slice(1);
475
+ const urlFromTarget = first && (first.startsWith("http://") || first.startsWith("https://")) ? first : null;
476
+ const url = urlFromTarget ?? options.serverUrl ?? null;
477
+ if (url) {
478
+ if (rest.length > 0 && urlFromTarget) {
479
+ throw new Error("Arguments cannot be passed to a URL-based MCP server.");
480
+ }
481
+ let transportType;
482
+ const t = options.transport === "http" ? "streamable-http" : options.transport;
483
+ if (t === "sse" || t === "streamable-http") {
484
+ transportType = t;
485
+ } else {
486
+ const u = new URL(url);
487
+ if (u.pathname.endsWith("/mcp")) {
488
+ transportType = "streamable-http";
489
+ } else if (u.pathname.endsWith("/sse")) {
490
+ transportType = "sse";
491
+ } else {
492
+ throw new Error(
493
+ `Transport type not specified and could not be determined from URL: ${url}.`
494
+ );
495
+ }
496
+ }
497
+ if (transportType === "sse") {
498
+ const config3 = { type: "sse", url };
499
+ return config3;
500
+ }
501
+ const config2 = { type: "streamable-http", url };
502
+ return config2;
503
+ }
504
+ if (target.length === 0 || !first) {
505
+ throw new Error(
506
+ "Target is required. Specify a URL or a command to execute."
507
+ );
508
+ }
509
+ if (options.transport && options.transport !== "stdio") {
510
+ throw new Error("Only stdio transport can be used with local commands.");
511
+ }
512
+ const config = { type: "stdio", command: first };
513
+ if (rest.length > 0) config.args = rest;
514
+ if (options.env && Object.keys(options.env).length > 0)
515
+ config.env = options.env;
516
+ if (options.cwd?.trim()) config.cwd = options.cwd.trim();
517
+ return config;
518
+ }
519
+ function applyOverrides(config, overrides) {
520
+ if (config.type === "stdio") {
521
+ const c = { ...config };
522
+ if (overrides.env && Object.keys(overrides.env).length > 0) {
523
+ c.env = { ...c.env ?? {}, ...overrides.env };
524
+ }
525
+ if (overrides.cwd?.trim()) c.cwd = overrides.cwd.trim();
526
+ return c;
527
+ }
528
+ return config;
529
+ }
530
+ function hasAdHocServerOptions(options) {
531
+ return options.target != null && options.target.length > 0 || Boolean(options.transport) || Boolean(options.serverUrl?.trim());
532
+ }
533
+ function resolveServerSource(options) {
534
+ if (options.catalogPath?.trim()) {
535
+ return { path: options.catalogPath, writable: true };
536
+ }
537
+ if (options.configPath?.trim()) {
538
+ return { path: options.configPath, writable: false };
539
+ }
540
+ return null;
541
+ }
542
+ function resolveServerConfigs(options, mode) {
543
+ const source = resolveServerSource(options);
544
+ const hasAdHoc = hasAdHocServerOptions(options);
545
+ if (mode === "single") {
546
+ if (source && options.serverName) {
547
+ const config = loadServerFromConfig(
548
+ source.path,
549
+ options.serverName,
550
+ source.writable
551
+ );
552
+ return [
553
+ applyOverrides(config, {
554
+ env: options.env,
555
+ cwd: options.cwd
556
+ })
557
+ ];
558
+ }
559
+ if (source && !options.serverName) {
560
+ const mcpConfig = loadMcpServersConfig(source.path, source.writable);
561
+ const servers = Object.keys(mcpConfig.mcpServers);
562
+ if (servers.length === 0)
563
+ throw new Error("No servers found in config file");
564
+ if (servers.length > 1) {
565
+ throw new Error(
566
+ `Multiple servers found in config file. Please specify one with --server. Available servers: ${servers.join(", ")}`
567
+ );
568
+ }
569
+ const serverName = servers[0];
570
+ if (!serverName) throw new Error("No servers found in config file");
571
+ const config = loadServerFromConfig(
572
+ source.path,
573
+ serverName,
574
+ source.writable
575
+ );
576
+ return [
577
+ applyOverrides(config, {
578
+ env: options.env,
579
+ cwd: options.cwd
580
+ })
581
+ ];
582
+ }
583
+ return [buildConfigFromOptions(options)];
584
+ }
585
+ if (mode === "multi") {
586
+ if (source && hasAdHoc) {
587
+ throw new Error(
588
+ "In multi-server mode with a config file, do not pass --transport, --server-url, or positional command/URL. Use only --config with optional -e, --cwd."
589
+ );
590
+ }
591
+ if (source) {
592
+ const mcpConfig = loadMcpServersConfig(source.path, source.writable);
593
+ const configs = Object.values(mcpConfig.mcpServers).map(
594
+ (c) => applyOverrides({ ...c }, {
595
+ env: options.env,
596
+ cwd: options.cwd
597
+ })
598
+ );
599
+ return configs;
600
+ }
601
+ return [buildConfigFromOptions(options)];
602
+ }
603
+ return [];
604
+ }
605
+
606
+ // server/web-server-config.ts
607
+ import pino from "pino";
608
+
609
+ // ../../core/mcp/remote/constants.ts
610
+ var LEGACY_AUTH_TOKEN_ENV = "MCP_PROXY_AUTH_TOKEN";
611
+ var API_SERVER_ENV_VARS = {
612
+ /**
613
+ * Auth token for authenticating requests to the remote API server.
614
+ * Used by the x-mcp-remote-auth header (or Authorization header if changed).
615
+ */
616
+ AUTH_TOKEN: "MCP_INSPECTOR_API_TOKEN"
617
+ };
618
+ var INSPECTOR_API_TOKEN_GLOBAL = "__INSPECTOR_API_TOKEN__";
619
+
620
+ // ../../core/node/version.ts
621
+ import { readFileSync as readFileSync2 } from "fs";
622
+ import { dirname as dirname3, join as join2, parse } from "path";
623
+ import { fileURLToPath } from "url";
624
+ var ROOT_PACKAGE_NAME = "@modelcontextprotocol/inspector";
625
+ function readInspectorVersion(callerUrl) {
626
+ const startDir = dirname3(fileURLToPath(callerUrl));
627
+ const { root } = parse(startDir);
628
+ let dir = startDir;
629
+ for (; ; ) {
630
+ let manifest;
631
+ try {
632
+ manifest = JSON.parse(
633
+ readFileSync2(join2(dir, "package.json"), "utf-8")
634
+ );
635
+ } catch {
636
+ }
637
+ if (manifest?.name === ROOT_PACKAGE_NAME && manifest.version) {
638
+ return manifest.version;
639
+ }
640
+ if (dir === root) break;
641
+ dir = dirname3(dir);
642
+ }
643
+ throw new Error(
644
+ `Could not locate the ${ROOT_PACKAGE_NAME} root package.json above ${startDir}`
645
+ );
646
+ }
647
+ function readInspectorVersionSafe(callerUrl) {
648
+ try {
649
+ return readInspectorVersion(callerUrl);
650
+ } catch {
651
+ return void 0;
652
+ }
653
+ }
654
+
655
+ // server/sandbox-controller.ts
656
+ import { createServer } from "http";
657
+ import { readFileSync as readFileSync3 } from "fs";
658
+ import { dirname as dirname4, join as join3 } from "path";
659
+ import { fileURLToPath as fileURLToPath2 } from "url";
660
+
661
+ // ../../core/node/hostUrl.ts
662
+ import { isIPv6 } from "net";
663
+ function stripBrackets(host) {
664
+ return host.replace(/^\[(.*)\]$/, "$1");
665
+ }
666
+ function formatHostForUrl(host) {
667
+ const h = host.trim();
668
+ const bare = stripBrackets(h).split("%")[0];
669
+ return isIPv6(bare) ? `[${bare}]` : h;
670
+ }
671
+ function unmapIpv4MappedHost(host) {
672
+ const m = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(
673
+ stripBrackets(host)
674
+ );
675
+ if (!m) return host;
676
+ const hi = parseInt(m[1], 16);
677
+ const lo = parseInt(m[2], 16);
678
+ return `${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`;
679
+ }
680
+ function canonicalUrlHost(host) {
681
+ const formatted = formatHostForUrl(host.trim().toLowerCase());
682
+ try {
683
+ return unmapIpv4MappedHost(new URL(`http://${formatted}`).hostname);
684
+ } catch {
685
+ return formatted;
686
+ }
687
+ }
688
+ var ALL_INTERFACES_LITERALS = /* @__PURE__ */ new Set(["", "0.0.0.0", "::", "::ffff:0:0"]);
689
+ function canonicalizeIpv6(value) {
690
+ if (!isIPv6(value)) return value;
691
+ const [address] = value.split("%");
692
+ return new URL(`http://[${address}]`).hostname.slice(1, -1);
693
+ }
694
+ function parseAddressPart(part) {
695
+ if (/^0x[0-9a-f]+$/.test(part)) return parseInt(part, 16);
696
+ if (/^[0-9]+$/.test(part)) return parseInt(part, 10);
697
+ return NaN;
698
+ }
699
+ function isAllZeroIpv4(value) {
700
+ const parts = value.split(".");
701
+ if (parts.length > 4) return false;
702
+ return parts.every((part) => parseAddressPart(part) === 0);
703
+ }
704
+ function isAllInterfacesHost(host) {
705
+ const normalized = canonicalizeIpv6(stripBrackets(canonicalUrlHost(host)));
706
+ return ALL_INTERFACES_LITERALS.has(normalized) || isAllZeroIpv4(normalized);
707
+ }
708
+
709
+ // server/sandbox-controller.ts
710
+ var __dirname = dirname4(fileURLToPath2(import.meta.url));
711
+ var LOOPBACK_FRAME_ANCESTORS = ["http://127.0.0.1:*", "http://localhost:*"];
712
+ var CSP_HOST_SOURCE = /^[a-z][a-z0-9+.-]*:\/\/[^\s;,'"[\]*]+$/i;
713
+ function sandboxFrameAncestors(allowedOrigins) {
714
+ const valid = (allowedOrigins ?? []).filter((o) => CSP_HOST_SOURCE.test(o));
715
+ const sources = valid.length > 0 ? valid : LOOPBACK_FRAME_ANCESTORS;
716
+ return `frame-ancestors ${sources.join(" ")}`;
717
+ }
718
+ function parseListenPort(raw) {
719
+ const v = raw?.trim();
720
+ if (!v || !/^\d+$/.test(v)) return void 0;
721
+ const n = parseInt(v, 10);
722
+ return n <= 65535 ? n : void 0;
723
+ }
724
+ function resolveSandboxPort() {
725
+ const fromSandbox = parseListenPort(process.env.MCP_SANDBOX_PORT);
726
+ if (fromSandbox === void 0 && process.env.MCP_SANDBOX_PORT?.trim()) {
727
+ console.warn(
728
+ `Ignoring invalid MCP_SANDBOX_PORT="${process.env.MCP_SANDBOX_PORT}" (need an integer 0\u201365535); falling back.`
729
+ );
730
+ }
731
+ return fromSandbox ?? parseListenPort(process.env.SERVER_PORT) ?? 0;
732
+ }
733
+ function createSandboxController(options) {
734
+ const { port, host = "localhost", allowedOrigins } = options;
735
+ let server = null;
736
+ let sandboxUrl = null;
737
+ const SANDBOX_PROXY_CSP = sandboxFrameAncestors(allowedOrigins);
738
+ let sandboxHtml;
739
+ try {
740
+ const sandboxHtmlPath = join3(__dirname, "../static/sandbox_proxy.html");
741
+ sandboxHtml = readFileSync3(sandboxHtmlPath, "utf-8");
742
+ } catch (e) {
743
+ sandboxHtml = "<!DOCTYPE html><html><body>Sandbox not loaded: " + String(e.message) + "</body></html>";
744
+ }
745
+ return {
746
+ async start() {
747
+ if (server && sandboxUrl) {
748
+ const p = parseInt(new URL(sandboxUrl).port, 10);
749
+ return { port: p, url: sandboxUrl };
750
+ }
751
+ return new Promise((resolve6) => {
752
+ let settled = false;
753
+ const settle = (value) => {
754
+ if (settled) return;
755
+ settled = true;
756
+ resolve6(value);
757
+ };
758
+ server = createServer((req, res) => {
759
+ if (req.method !== "GET" || req.url !== "/sandbox" && req.url !== "/sandbox/") {
760
+ res.writeHead(404, { "Content-Type": "text/plain" });
761
+ res.end("Not Found");
762
+ return;
763
+ }
764
+ res.writeHead(200, {
765
+ "Content-Type": "text/html; charset=utf-8",
766
+ "Cache-Control": "no-store, no-cache, must-revalidate",
767
+ Pragma: "no-cache",
768
+ "Content-Security-Policy": SANDBOX_PROXY_CSP
769
+ });
770
+ res.end(sandboxHtml);
771
+ });
772
+ server.on("error", (err) => {
773
+ if (err.code === "EADDRINUSE") {
774
+ console.error(
775
+ `Sandbox: port ${port || "dynamic"} in use. MCP Apps tab may not work.`
776
+ );
777
+ } else {
778
+ console.error("Sandbox server error:", err);
779
+ }
780
+ server = null;
781
+ settle({ port: 0, url: "" });
782
+ });
783
+ server.listen(port, host, () => {
784
+ const addr = server.address();
785
+ const actualPort = typeof addr === "object" && addr !== null && "port" in addr ? addr.port : (
786
+ /* v8 ignore next -- unreachable for a TCP listen: `address()`
787
+ always returns an `AddressInfo` object with `port`. The
788
+ string form only occurs for unix-socket/pipe listens, which
789
+ this controller never performs. */
790
+ addr
791
+ );
792
+ const canonicalHost = canonicalUrlHost(host);
793
+ const urlHost = isAllInterfacesHost(canonicalHost) ? "localhost" : canonicalHost;
794
+ sandboxUrl = `http://${urlHost}:${actualPort}/sandbox`;
795
+ settle({ port: actualPort, url: sandboxUrl });
796
+ });
797
+ });
798
+ },
799
+ async close() {
800
+ if (!server) return;
801
+ return new Promise((resolve6) => {
802
+ server.close(() => {
803
+ server = null;
804
+ sandboxUrl = null;
805
+ resolve6();
806
+ });
807
+ });
808
+ },
809
+ getUrl() {
810
+ return sandboxUrl;
811
+ }
812
+ };
813
+ }
814
+
815
+ // server/resolve-bind-host.ts
816
+ var BIND_ALL_INTERFACES_ENV = "DANGEROUSLY_BIND_ALL_INTERFACES";
817
+ function isEnabled(value) {
818
+ const v = value?.trim().toLowerCase();
819
+ return v === "true" || v === "1";
820
+ }
821
+ function resolveBindHostname(env = process.env) {
822
+ const host = (env.HOST ?? "localhost").trim();
823
+ if (isAllInterfacesHost(host) && !isEnabled(env[BIND_ALL_INTERFACES_ENV])) {
824
+ const resolved = canonicalUrlHost(host);
825
+ const shown = stripBrackets(resolved) === stripBrackets(host.toLowerCase()) ? `HOST="${host}"` : `HOST="${host}" (resolves to ${resolved})`;
826
+ throw new Error(
827
+ `Refusing to bind ${shown}: this exposes the MCP Inspector to your entire network, and its backend can spawn local processes and connect to MCP servers on your behalf \u2014 the exposure DNS-rebinding attacks target. Bind a loopback host (localhost / 127.0.0.1) instead. To override \u2014 only inside an isolated container or trusted network \u2014 set ${BIND_ALL_INTERFACES_ENV}=true.`
828
+ );
829
+ }
830
+ return stripBrackets(host);
831
+ }
832
+
833
+ // server/web-server-config.ts
834
+ var inspectorVersion = readInspectorVersionSafe(import.meta.url);
835
+ function defaultEnvironmentFromProcess(extra) {
836
+ const keys = process.platform === "win32" ? [
837
+ "APPDATA",
838
+ "HOMEDRIVE",
839
+ "HOMEPATH",
840
+ "LOCALAPPDATA",
841
+ "PATH",
842
+ "PROCESSOR_ARCHITECTURE",
843
+ "SYSTEMDRIVE",
844
+ "SYSTEMROOT",
845
+ "TEMP",
846
+ "USERNAME",
847
+ "USERPROFILE",
848
+ "PROGRAMFILES"
849
+ ] : ["HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER"];
850
+ const out = {};
851
+ for (const key of keys) {
852
+ const value = process.env[key];
853
+ if (value && !value.startsWith("()")) {
854
+ out[key] = value;
855
+ }
856
+ }
857
+ if (extra) {
858
+ Object.assign(out, extra);
859
+ }
860
+ return out;
861
+ }
862
+ function webServerConfigToInitialPayload(config) {
863
+ return { ...transportDefaults(config), version: inspectorVersion };
864
+ }
865
+ function transportDefaults(config) {
866
+ const mc = config.initialMcpConfig;
867
+ const defaultEnvironment = defaultEnvironmentFromProcess(
868
+ mc && "env" in mc && mc.env ? mc.env : void 0
869
+ );
870
+ if (!mc) {
871
+ return { defaultEnvironment };
872
+ }
873
+ if (mc.type === "stdio" || mc.type === void 0) {
874
+ return {
875
+ defaultCommand: mc.command,
876
+ defaultArgs: mc.args ?? [],
877
+ defaultTransport: "stdio",
878
+ defaultCwd: mc.cwd,
879
+ defaultEnvironment
880
+ };
881
+ }
882
+ if (mc.type === "sse") {
883
+ return {
884
+ defaultTransport: "sse",
885
+ defaultServerUrl: mc.url,
886
+ defaultEnvironment
887
+ };
888
+ }
889
+ if (mc.type === "streamable-http") {
890
+ return {
891
+ defaultTransport: "streamable-http",
892
+ defaultServerUrl: mc.url,
893
+ defaultEnvironment
894
+ };
895
+ }
896
+ const c = mc;
897
+ return {
898
+ defaultTransport: "streamable-http",
899
+ defaultServerUrl: c.url,
900
+ defaultEnvironment
901
+ };
902
+ }
903
+ function printServerBanner(config, actualPort, resolvedToken, sandboxUrl) {
904
+ const h = canonicalUrlHost(config.hostname);
905
+ const bannerHost = isAllInterfacesHost(h) ? "localhost" : h;
906
+ const baseUrl = httpOrigin(bannerHost, actualPort);
907
+ const url = config.dangerouslyOmitAuth || !resolvedToken ? baseUrl : `${baseUrl}?${API_SERVER_ENV_VARS.AUTH_TOKEN}=${resolvedToken}`;
908
+ console.log(`
909
+ MCP Inspector Web is up and running at:
910
+ ${url}
911
+ `);
912
+ if (sandboxUrl) {
913
+ console.log(` Sandbox (MCP Apps): ${sandboxUrl}
914
+ `);
915
+ }
916
+ if (config.dangerouslyOmitAuth) {
917
+ console.log(" Auth: disabled (DANGEROUSLY_OMIT_AUTH)\n");
918
+ } else {
919
+ console.log(` Auth token: ${resolvedToken}
920
+ `);
921
+ }
922
+ if (config.autoOpen) {
923
+ console.log("Opening browser...");
924
+ }
925
+ return url;
926
+ }
927
+ var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
928
+ function httpOrigin(host, port) {
929
+ return port === 80 ? `http://${host}` : `http://${host}:${port}`;
930
+ }
931
+ function loopbackOrigins(port) {
932
+ return [
933
+ httpOrigin("localhost", port),
934
+ httpOrigin("127.0.0.1", port),
935
+ httpOrigin("[::1]", port)
936
+ ];
937
+ }
938
+ function defaultAllowedOrigins(hostname, port) {
939
+ const h = canonicalUrlHost(hostname);
940
+ if (LOOPBACK_HOSTNAMES.has(h)) {
941
+ return loopbackOrigins(port);
942
+ }
943
+ if (isAllInterfacesHost(h)) {
944
+ return [
945
+ ...loopbackOrigins(port),
946
+ httpOrigin("0.0.0.0", port),
947
+ httpOrigin("[::]", port)
948
+ ];
949
+ }
950
+ return [httpOrigin(h, port)];
951
+ }
952
+ function buildWebServerConfig(options = {}) {
953
+ const {
954
+ initialMcpConfig = null,
955
+ mcpConfigPath,
956
+ writable = true,
957
+ initialServers = null
958
+ } = options;
959
+ const rawPort = process.env.CLIENT_PORT?.trim() || "6274";
960
+ const port = parseInt(rawPort, 10);
961
+ if (!/^\d+$/.test(rawPort) || port < 1 || port > 65535) {
962
+ throw new Error(
963
+ `Invalid CLIENT_PORT="${process.env.CLIENT_PORT}": the web server needs a fixed port in 1\u201365535 (0 / dynamic is unsupported \u2014 the origin allow-list and sandbox CSP are derived from it).`
964
+ );
965
+ }
966
+ const hostname = resolveBindHostname();
967
+ const dangerouslyOmitAuth = !!process.env.DANGEROUSLY_OMIT_AUTH;
968
+ const authToken = dangerouslyOmitAuth ? "" : process.env[API_SERVER_ENV_VARS.AUTH_TOKEN] ?? process.env[LEGACY_AUTH_TOKEN_ENV] ?? "";
969
+ const sandboxPort = resolveSandboxPort();
970
+ let logger;
971
+ if (process.env.MCP_LOG_FILE) {
972
+ logger = pino(
973
+ { level: "info" },
974
+ pino.destination({
975
+ dest: process.env.MCP_LOG_FILE,
976
+ append: true,
977
+ mkdir: true
978
+ })
979
+ );
980
+ }
981
+ const configuredOrigins = process.env.ALLOWED_ORIGINS?.split(",").map((o) => o.trim()).filter(Boolean).map((o) => {
982
+ try {
983
+ const { origin } = new URL(o);
984
+ if (origin === "null") throw new Error("opaque origin");
985
+ if (origin.includes("*")) throw new Error("wildcard origin");
986
+ return origin;
987
+ } catch {
988
+ console.warn(`Ignoring invalid ALLOWED_ORIGINS entry: ${o}`);
989
+ return null;
990
+ }
991
+ }).filter((o) => o !== null);
992
+ return {
993
+ port,
994
+ hostname,
995
+ authToken,
996
+ dangerouslyOmitAuth,
997
+ initialMcpConfig,
998
+ mcpConfigPath,
999
+ writable,
1000
+ initialServers,
1001
+ storageDir: process.env.MCP_STORAGE_DIR,
1002
+ allowedOrigins: configuredOrigins?.length ? configuredOrigins : defaultAllowedOrigins(hostname, port),
1003
+ sandboxPort,
1004
+ sandboxHost: hostname,
1005
+ logger,
1006
+ autoOpen: resolveAutoOpen()
1007
+ };
1008
+ }
1009
+ function resolveAutoOpen() {
1010
+ const flag = process.env.MCP_AUTO_OPEN_ENABLED;
1011
+ if (flag === "true") return true;
1012
+ if (flag === "false") return false;
1013
+ return !process.env.VITEST;
1014
+ }
1015
+
1016
+ // server/start-vite-dev-server.ts
1017
+ import { join as join6, dirname as dirname5, resolve as resolve3 } from "path";
1018
+ import { fileURLToPath as fileURLToPath4 } from "url";
1019
+ import { createServer as createServer2 } from "vite";
1020
+ import react from "@vitejs/plugin-react";
1021
+
1022
+ // server/vite-hono-plugin.ts
1023
+ import open from "open";
1024
+
1025
+ // ../../core/mcp/remote/node/server.ts
1026
+ import { randomBytes, timingSafeEqual } from "crypto";
1027
+ import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
1028
+ import { stat as fsStat } from "fs/promises";
1029
+ import { homedir } from "os";
1030
+ import { Hono } from "hono";
1031
+ import { streamSSE } from "hono/streaming";
1032
+ import { watch as chokidarWatch } from "chokidar";
1033
+
1034
+ // ../../core/mcp/config.ts
1035
+ function getServerType(config) {
1036
+ if (!("type" in config) || config.type === void 0) {
1037
+ return "stdio";
1038
+ }
1039
+ const type = config.type;
1040
+ if (type === "stdio") {
1041
+ return "stdio";
1042
+ }
1043
+ if (type === "sse") {
1044
+ return "sse";
1045
+ }
1046
+ if (type === "streamable-http") {
1047
+ return "streamable-http";
1048
+ }
1049
+ throw new Error(
1050
+ `Invalid server type: ${type}. Valid types are: stdio, sse, streamable-http`
1051
+ );
1052
+ }
1053
+
1054
+ // ../../core/mcp/node/transport.ts
1055
+ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
1056
+ import { SSEClientTransport } from "@modelcontextprotocol/client";
1057
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
1058
+
1059
+ // ../../core/mcp/fetchTracking.ts
1060
+ var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
1061
+ "authorization",
1062
+ "cookie",
1063
+ "set-cookie",
1064
+ "proxy-authorization",
1065
+ "x-api-key",
1066
+ // The inspector backend's own bearer (createRemoteFetch stamps this on every
1067
+ // proxied request); same exposure as Authorization.
1068
+ "x-mcp-remote-auth"
1069
+ ]);
1070
+ var REDACTED_HEADER_VALUE = "[REDACTED]";
1071
+ var SENSITIVE_BODY_FIELDS = /* @__PURE__ */ new Set([
1072
+ "client_secret",
1073
+ "code",
1074
+ "refresh_token",
1075
+ "access_token",
1076
+ "id_token",
1077
+ "code_verifier",
1078
+ "client_assertion",
1079
+ "assertion",
1080
+ "password",
1081
+ "token"
1082
+ ]);
1083
+ var REDACTED_VALUE = "[REDACTED]";
1084
+ function isSensitiveField(name) {
1085
+ return SENSITIVE_BODY_FIELDS.has(name.toLowerCase());
1086
+ }
1087
+ function redactSensitiveHeaders(headers) {
1088
+ const out = {};
1089
+ for (const [key, value] of Object.entries(headers)) {
1090
+ out[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? REDACTED_HEADER_VALUE : value;
1091
+ }
1092
+ return out;
1093
+ }
1094
+ function redactUrlQuery(url) {
1095
+ const queryStart = url.indexOf("?");
1096
+ if (queryStart === -1) return url;
1097
+ const base = url.slice(0, queryStart);
1098
+ const afterQuery = url.slice(queryStart + 1);
1099
+ const hashStart = afterQuery.indexOf("#");
1100
+ const query = hashStart === -1 ? afterQuery : afterQuery.slice(0, hashStart);
1101
+ const fragment = hashStart === -1 ? "" : afterQuery.slice(hashStart);
1102
+ try {
1103
+ const params = new URLSearchParams(query);
1104
+ let changed = false;
1105
+ for (const key of new Set(params.keys())) {
1106
+ if (isSensitiveField(key)) {
1107
+ changed = true;
1108
+ params.set(key, REDACTED_VALUE);
1109
+ }
1110
+ }
1111
+ if (!changed) return url;
1112
+ return `${base}?${params.toString()}${fragment}`;
1113
+ } catch {
1114
+ return url;
1115
+ }
1116
+ }
1117
+ function redactJsonValue(value) {
1118
+ if (Array.isArray(value)) {
1119
+ return value.map(redactJsonValue);
1120
+ }
1121
+ if (value !== null && typeof value === "object") {
1122
+ const out = {};
1123
+ for (const [key, val] of Object.entries(value)) {
1124
+ out[key] = isSensitiveField(key) && typeof val === "string" ? REDACTED_VALUE : redactJsonValue(val);
1125
+ }
1126
+ return out;
1127
+ }
1128
+ return value;
1129
+ }
1130
+ function redactBody(body, contentType) {
1131
+ if (!body) return body;
1132
+ const type = (contentType ?? "").toLowerCase();
1133
+ if (type.includes("application/x-www-form-urlencoded")) {
1134
+ try {
1135
+ const params = new URLSearchParams(body);
1136
+ let changed = false;
1137
+ for (const key of new Set(params.keys())) {
1138
+ if (isSensitiveField(key)) {
1139
+ changed = true;
1140
+ params.set(key, REDACTED_VALUE);
1141
+ }
1142
+ }
1143
+ return changed ? params.toString() : body;
1144
+ } catch {
1145
+ return body;
1146
+ }
1147
+ }
1148
+ try {
1149
+ const parsed = JSON.parse(body);
1150
+ if (parsed !== null && typeof parsed === "object") {
1151
+ return JSON.stringify(redactJsonValue(parsed));
1152
+ }
1153
+ } catch {
1154
+ }
1155
+ return body;
1156
+ }
1157
+ function findHeader(headers, name) {
1158
+ const target = name.toLowerCase();
1159
+ for (const [key, value] of Object.entries(headers)) {
1160
+ if (key.toLowerCase() === target) return value;
1161
+ }
1162
+ return void 0;
1163
+ }
1164
+ function isLongLivedStreamResponse(method, contentType) {
1165
+ if (method !== "GET") return false;
1166
+ if (!contentType) return false;
1167
+ return contentType.includes("text/event-stream") || contentType.includes("application/x-ndjson");
1168
+ }
1169
+ function createFetchTracker(baseFetch, callbacks) {
1170
+ return async (input, init) => {
1171
+ const startTime = Date.now();
1172
+ const timestamp = /* @__PURE__ */ new Date();
1173
+ const id = `${timestamp.getTime()}-${Math.random().toString(36).slice(2, 11)}`;
1174
+ const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
1175
+ const method = init?.method || "GET";
1176
+ const rawRequestHeaders = {};
1177
+ if (input instanceof Request) {
1178
+ input.headers.forEach((value, key) => {
1179
+ rawRequestHeaders[key] = value;
1180
+ });
1181
+ }
1182
+ if (init?.headers) {
1183
+ const headers = new Headers(init.headers);
1184
+ headers.forEach((value, key) => {
1185
+ rawRequestHeaders[key] = value;
1186
+ });
1187
+ }
1188
+ const requestHeaders = redactSensitiveHeaders(rawRequestHeaders);
1189
+ const requestContentType = findHeader(rawRequestHeaders, "content-type");
1190
+ const redactedUrl = redactUrlQuery(url);
1191
+ let requestBody;
1192
+ if (init?.body) {
1193
+ if (typeof init.body === "string") {
1194
+ requestBody = init.body;
1195
+ } else {
1196
+ try {
1197
+ requestBody = String(init.body);
1198
+ } catch {
1199
+ requestBody = void 0;
1200
+ }
1201
+ }
1202
+ } else if (input instanceof Request && input.body) {
1203
+ try {
1204
+ const cloned = input.clone();
1205
+ requestBody = await cloned.text();
1206
+ } catch {
1207
+ requestBody = void 0;
1208
+ }
1209
+ }
1210
+ const redactedRequestBody = redactBody(requestBody, requestContentType);
1211
+ let response;
1212
+ let error;
1213
+ try {
1214
+ response = await baseFetch(input, init);
1215
+ } catch (err) {
1216
+ error = err instanceof Error ? err.message : String(err);
1217
+ const entry2 = {
1218
+ id,
1219
+ timestamp,
1220
+ method,
1221
+ url: redactedUrl,
1222
+ requestHeaders,
1223
+ requestBody: redactedRequestBody,
1224
+ error,
1225
+ duration: Date.now() - startTime
1226
+ };
1227
+ callbacks.trackRequest?.(entry2);
1228
+ throw err;
1229
+ }
1230
+ const responseStatus = response.status;
1231
+ const responseStatusText = response.statusText;
1232
+ const rawResponseHeaders = {};
1233
+ response.headers.forEach((value, key) => {
1234
+ rawResponseHeaders[key] = value;
1235
+ });
1236
+ const responseHeaders = redactSensitiveHeaders(rawResponseHeaders);
1237
+ const isLongLivedStream = isLongLivedStreamResponse(
1238
+ method,
1239
+ response.headers.get("content-type")
1240
+ );
1241
+ const duration = Date.now() - startTime;
1242
+ const entry = {
1243
+ id,
1244
+ timestamp,
1245
+ method,
1246
+ url: redactedUrl,
1247
+ requestHeaders,
1248
+ requestBody: redactedRequestBody,
1249
+ responseStatus,
1250
+ responseStatusText,
1251
+ responseHeaders,
1252
+ responseBody: void 0,
1253
+ duration
1254
+ };
1255
+ callbacks.trackRequest?.(entry);
1256
+ if (!isLongLivedStream && response.body && !response.bodyUsed) {
1257
+ const responseContentType = response.headers.get("content-type");
1258
+ try {
1259
+ const cloned = response.clone();
1260
+ cloned.text().then((body) => {
1261
+ callbacks.updateResponseBody?.(
1262
+ id,
1263
+ redactBody(body, responseContentType) ?? body
1264
+ );
1265
+ }).catch(() => {
1266
+ });
1267
+ } catch {
1268
+ }
1269
+ }
1270
+ return response;
1271
+ };
1272
+ }
1273
+
1274
+ // ../../core/auth/utils.ts
1275
+ import { UnauthorizedError } from "@modelcontextprotocol/client";
1276
+ import { ZodError } from "zod";
1277
+
1278
+ // ../../core/auth/challenge.ts
1279
+ var EMA_STEP_UP_PENDING_URL = new URL("mcp-inspector:ema-step-up");
1280
+ var AuthChallengeError = class extends Error {
1281
+ authChallenge;
1282
+ status;
1283
+ constructor(authChallenge, status, message) {
1284
+ super(message ?? `Auth challenge: ${authChallenge.reason}`);
1285
+ this.name = "AuthChallengeError";
1286
+ this.authChallenge = authChallenge;
1287
+ this.status = status;
1288
+ }
1289
+ };
1290
+ function parseWwwAuthenticateBearer(header) {
1291
+ const match = header.match(/Bearer\s+(.+)/i);
1292
+ if (!match) {
1293
+ return {};
1294
+ }
1295
+ const params = {};
1296
+ const paramRegex = /([\w-]+)(?:="([^"]*)"|=([^\s,]+))?/g;
1297
+ let m;
1298
+ while ((m = paramRegex.exec(match[1])) !== null) {
1299
+ const value = m[2] ?? m[3];
1300
+ if (value !== void 0) {
1301
+ params[m[1].toLowerCase()] = value;
1302
+ }
1303
+ }
1304
+ return {
1305
+ error: params.error,
1306
+ scope: params.scope,
1307
+ resourceMetadata: params.resource_metadata,
1308
+ errorDescription: params.error_description
1309
+ };
1310
+ }
1311
+ function parseScopeString(scope) {
1312
+ if (!scope?.trim()) {
1313
+ return [];
1314
+ }
1315
+ return scope.trim().split(/\s+/).filter(Boolean);
1316
+ }
1317
+ function reasonFromHttpResponse(status, bearer) {
1318
+ if (status === 403) {
1319
+ if (bearer.error === "insufficient_scope") {
1320
+ return "insufficient_scope";
1321
+ }
1322
+ return "unauthorized";
1323
+ }
1324
+ if (bearer.error === "insufficient_scope") {
1325
+ return "insufficient_scope";
1326
+ }
1327
+ if (bearer.error === "invalid_token") {
1328
+ return "invalid_token";
1329
+ }
1330
+ if (status === 401) {
1331
+ return "token_expired";
1332
+ }
1333
+ return "unauthorized";
1334
+ }
1335
+ function parseAuthChallengeFromResponse(response, context) {
1336
+ const status = response.status;
1337
+ if (status !== 401 && status !== 403) {
1338
+ return void 0;
1339
+ }
1340
+ const wwwAuthenticate = response.headers.get("WWW-Authenticate") ?? void 0;
1341
+ const bearer = wwwAuthenticate ? parseWwwAuthenticateBearer(wwwAuthenticate) : {};
1342
+ const requiredScopes = parseScopeString(bearer.scope);
1343
+ return {
1344
+ reason: reasonFromHttpResponse(status, bearer),
1345
+ ...requiredScopes.length > 0 ? { requiredScopes } : {},
1346
+ ...bearer.errorDescription ? { message: bearer.errorDescription } : {},
1347
+ ...context ? { context } : {},
1348
+ raw: {
1349
+ httpStatus: status,
1350
+ ...wwwAuthenticate ? { wwwAuthenticate } : {}
1351
+ }
1352
+ };
1353
+ }
1354
+
1355
+ // ../../core/mcp/node/authChallengeFetch.ts
1356
+ function createAuthChallengeInterceptFetch(baseFetch) {
1357
+ return async (input, init) => {
1358
+ const response = await baseFetch(input, init);
1359
+ if (response.status !== 401 && response.status !== 403) {
1360
+ return response;
1361
+ }
1362
+ const challenge = parseAuthChallengeFromResponse(response);
1363
+ if (!challenge) {
1364
+ return response;
1365
+ }
1366
+ await response.body?.cancel().catch(() => {
1367
+ });
1368
+ throw new AuthChallengeError(
1369
+ challenge,
1370
+ response.status,
1371
+ `MCP auth challenge (${response.status})`
1372
+ );
1373
+ };
1374
+ }
1375
+
1376
+ // ../../core/mcp/node/transport.ts
1377
+ var PROXY_ENV_VARS = [
1378
+ "HTTPS_PROXY",
1379
+ "https_proxy",
1380
+ "HTTP_PROXY",
1381
+ "http_proxy"
1382
+ ];
1383
+ function readProxyEnv() {
1384
+ for (const name of PROXY_ENV_VARS) {
1385
+ const value = process.env[name];
1386
+ if (value && value.trim() !== "") return value;
1387
+ }
1388
+ return void 0;
1389
+ }
1390
+ function withProxyDispatcher(baseFetch) {
1391
+ if (readProxyEnv() === void 0) return baseFetch;
1392
+ let dispatcherPromise;
1393
+ const getDispatcher = () => {
1394
+ dispatcherPromise ??= import("./undici-HXPKCIY3.js").then(
1395
+ ({ EnvHttpProxyAgent }) => new EnvHttpProxyAgent(),
1396
+ (cause) => {
1397
+ throw new Error(
1398
+ "HTTPS_PROXY / HTTP_PROXY is set but the `undici` package is not available. Install it (it ships with the CLI client) or unset the proxy env var.",
1399
+ { cause }
1400
+ );
1401
+ }
1402
+ );
1403
+ return dispatcherPromise;
1404
+ };
1405
+ const proxiedFetch = async (input, init) => {
1406
+ const dispatcher = await getDispatcher();
1407
+ return baseFetch(input, { ...init, dispatcher });
1408
+ };
1409
+ return proxiedFetch;
1410
+ }
1411
+ function headersFromSettings(settings) {
1412
+ if (!settings || settings.headers.length === 0) return void 0;
1413
+ const out = {};
1414
+ for (const { key, value } of settings.headers) {
1415
+ if (key.trim() === "") continue;
1416
+ out[key] = value;
1417
+ }
1418
+ return Object.keys(out).length > 0 ? out : void 0;
1419
+ }
1420
+ function createTransportNode(config, options = {}) {
1421
+ const serverType = getServerType(config);
1422
+ const {
1423
+ fetchFn: optionsFetchFn,
1424
+ onStderr,
1425
+ pipeStderr = false,
1426
+ onFetchRequest,
1427
+ onFetchResponseBody,
1428
+ authProvider,
1429
+ settings,
1430
+ interceptAuthChallenges = false
1431
+ } = options;
1432
+ const baseFetch = withProxyDispatcher(optionsFetchFn ?? globalThis.fetch);
1433
+ const fetchWithOptionalAuthIntercept = interceptAuthChallenges ? createAuthChallengeInterceptFetch(baseFetch) : baseFetch;
1434
+ if (serverType === "stdio") {
1435
+ const stdioConfig = config;
1436
+ const transport = new StdioClientTransport({
1437
+ command: stdioConfig.command,
1438
+ args: stdioConfig.args || [],
1439
+ env: stdioConfig.env,
1440
+ cwd: stdioConfig.cwd,
1441
+ stderr: pipeStderr ? "pipe" : void 0
1442
+ });
1443
+ if (pipeStderr && transport.stderr && onStderr) {
1444
+ transport.stderr.on("data", (data) => {
1445
+ const logEntry = data.toString().trim();
1446
+ if (logEntry) {
1447
+ onStderr({
1448
+ timestamp: /* @__PURE__ */ new Date(),
1449
+ message: logEntry
1450
+ });
1451
+ }
1452
+ });
1453
+ }
1454
+ return { transport };
1455
+ } else if (serverType === "sse") {
1456
+ const sseConfig = config;
1457
+ const url = new URL(sseConfig.url);
1458
+ const sseFetch = sseConfig.eventSourceInit?.fetch || fetchWithOptionalAuthIntercept;
1459
+ const trackedFetch = onFetchRequest ? createFetchTracker(sseFetch, {
1460
+ trackRequest: onFetchRequest,
1461
+ updateResponseBody: onFetchResponseBody
1462
+ }) : sseFetch;
1463
+ const headers = headersFromSettings(settings);
1464
+ const eventSourceInit = {
1465
+ ...sseConfig.eventSourceInit,
1466
+ ...headers && { headers },
1467
+ fetch: trackedFetch
1468
+ };
1469
+ const requestInit = {
1470
+ ...sseConfig.requestInit,
1471
+ ...headers && { headers }
1472
+ };
1473
+ const postFetch = onFetchRequest ? createFetchTracker(fetchWithOptionalAuthIntercept, {
1474
+ trackRequest: onFetchRequest,
1475
+ updateResponseBody: onFetchResponseBody
1476
+ }) : fetchWithOptionalAuthIntercept;
1477
+ const transport = new SSEClientTransport(url, {
1478
+ authProvider,
1479
+ eventSourceInit,
1480
+ requestInit,
1481
+ fetch: postFetch
1482
+ });
1483
+ return { transport };
1484
+ } else {
1485
+ const httpConfig = config;
1486
+ const url = new URL(httpConfig.url);
1487
+ const headers = headersFromSettings(settings);
1488
+ const requestInit = {
1489
+ ...httpConfig.requestInit,
1490
+ ...headers && { headers }
1491
+ };
1492
+ const transportFetch = onFetchRequest ? createFetchTracker(fetchWithOptionalAuthIntercept, {
1493
+ trackRequest: onFetchRequest,
1494
+ updateResponseBody: onFetchResponseBody
1495
+ }) : fetchWithOptionalAuthIntercept;
1496
+ const transport = new StreamableHTTPClientTransport(url, {
1497
+ authProvider,
1498
+ requestInit,
1499
+ fetch: transportFetch,
1500
+ // SEP-2350: how the transport reacts to a `403 insufficient_scope`
1501
+ // challenge. Defaults to the SDK's `reauthorize` when unset.
1502
+ ...settings?.oauthOnInsufficientScope && {
1503
+ onInsufficientScope: settings.oauthOnInsufficientScope
1504
+ }
1505
+ });
1506
+ return { transport };
1507
+ }
1508
+ }
1509
+
1510
+ // ../../core/mcp/import/clientConfig.ts
1511
+ function parseJsonObject(raw) {
1512
+ let data;
1513
+ try {
1514
+ data = JSON.parse(raw);
1515
+ } catch (err) {
1516
+ const detail = err instanceof Error ? err.message : String(err);
1517
+ throw new Error(`Invalid JSON: ${detail}`);
1518
+ }
1519
+ if (!data || typeof data !== "object" || Array.isArray(data)) {
1520
+ throw new Error("Expected a JSON object at the top level");
1521
+ }
1522
+ return data;
1523
+ }
1524
+ function normalizeServerMap(servers) {
1525
+ const out = {};
1526
+ for (const [id, raw] of Object.entries(servers)) {
1527
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
1528
+ const normalized = normalizeServerType(
1529
+ raw
1530
+ );
1531
+ out[id] = normalized;
1532
+ }
1533
+ return out;
1534
+ }
1535
+ function parseMcpServersConfig(raw) {
1536
+ const data = parseJsonObject(raw);
1537
+ const servers = data.mcpServers;
1538
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
1539
+ throw new Error("No 'mcpServers' object found in config file");
1540
+ }
1541
+ return {
1542
+ mcpServers: normalizeServerMap(servers)
1543
+ };
1544
+ }
1545
+ function parseVsCodeConfig(raw) {
1546
+ const data = parseJsonObject(raw);
1547
+ const servers = data.servers;
1548
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
1549
+ throw new Error("No 'servers' object found in VS Code config file");
1550
+ }
1551
+ return {
1552
+ mcpServers: normalizeServerMap(servers)
1553
+ };
1554
+ }
1555
+
1556
+ // ../../core/mcp/import/strategies.ts
1557
+ function join4(...segments) {
1558
+ return segments.map(
1559
+ (s, i) => i === 0 ? s.replace(/\/+$/, "") : s.replace(/^\/+|\/+$/g, "")
1560
+ ).filter((s) => s.length > 0).join("/");
1561
+ }
1562
+ function appData(homeDir) {
1563
+ return join4(homeDir, "AppData", "Roaming");
1564
+ }
1565
+ function appSupportDir(platform, homeDir, app) {
1566
+ if (platform === "darwin") {
1567
+ return join4(homeDir, "Library", "Application Support", app);
1568
+ }
1569
+ if (platform === "win32") {
1570
+ return join4(appData(homeDir), app);
1571
+ }
1572
+ return join4(homeDir, ".config", app);
1573
+ }
1574
+ var claudeDesktop = {
1575
+ id: "claude-desktop",
1576
+ label: "Claude Desktop",
1577
+ defaultPaths: (platform, homeDir) => [
1578
+ join4(
1579
+ appSupportDir(platform, homeDir, "Claude"),
1580
+ "claude_desktop_config.json"
1581
+ )
1582
+ ],
1583
+ parse: parseMcpServersConfig
1584
+ };
1585
+ var cursor = {
1586
+ id: "cursor",
1587
+ label: "Cursor",
1588
+ defaultPaths: (_platform, homeDir) => [join4(homeDir, ".cursor", "mcp.json")],
1589
+ parse: parseMcpServersConfig
1590
+ };
1591
+ var cline = {
1592
+ id: "cline",
1593
+ label: "Cline",
1594
+ defaultPaths: (platform, homeDir) => [
1595
+ // Cline (VS Code extension) global settings live under the editor's
1596
+ // globalStorage; the documented cross-platform fallback is ~/Documents.
1597
+ join4(
1598
+ appSupportDir(platform, homeDir, "Code"),
1599
+ "User",
1600
+ "globalStorage",
1601
+ "saoudrizwan.claude-dev",
1602
+ "settings",
1603
+ "cline_mcp_settings.json"
1604
+ ),
1605
+ join4(homeDir, "Documents", "Cline", "MCP", "cline_mcp_settings.json")
1606
+ ],
1607
+ parse: parseMcpServersConfig
1608
+ };
1609
+ var vscode = {
1610
+ id: "vscode",
1611
+ label: "VS Code",
1612
+ defaultPaths: (platform, homeDir) => [
1613
+ join4(appSupportDir(platform, homeDir, "Code"), "User", "mcp.json")
1614
+ ],
1615
+ parse: parseVsCodeConfig
1616
+ };
1617
+ var IMPORT_STRATEGY_LIST = [
1618
+ claudeDesktop,
1619
+ cursor,
1620
+ cline,
1621
+ vscode
1622
+ ];
1623
+ var IMPORT_STRATEGIES = Object.fromEntries(IMPORT_STRATEGY_LIST.map((s) => [s.id, s]));
1624
+ function getImportStrategy(id) {
1625
+ return IMPORT_STRATEGIES[id];
1626
+ }
1627
+
1628
+ // ../../core/mcp/import/resolveSource.ts
1629
+ function resolveImportSource(type, platform, homeDir, readFile2) {
1630
+ const strategy = getImportStrategy(type);
1631
+ if (!strategy) return null;
1632
+ const searched = strategy.defaultPaths(platform, homeDir);
1633
+ for (const path3 of searched) {
1634
+ let raw;
1635
+ try {
1636
+ raw = readFile2(path3);
1637
+ } catch (err) {
1638
+ const msg = err instanceof Error ? err.message : String(err);
1639
+ return {
1640
+ type,
1641
+ found: true,
1642
+ path: path3,
1643
+ error: `Failed to read ${path3}: ${msg}`,
1644
+ searched
1645
+ };
1646
+ }
1647
+ if (raw === null) continue;
1648
+ try {
1649
+ const config = strategy.parse(raw);
1650
+ return { type, found: true, path: path3, config, searched };
1651
+ } catch (err) {
1652
+ const msg = err instanceof Error ? err.message : String(err);
1653
+ return { type, found: true, path: path3, error: msg, searched };
1654
+ }
1655
+ }
1656
+ return { type, found: false, searched };
1657
+ }
1658
+
1659
+ // ../../core/mcp/remote/node/remote-session.ts
1660
+ var RemoteSession = class _RemoteSession {
1661
+ sessionId;
1662
+ transport;
1663
+ eventQueue = [];
1664
+ eventConsumer = null;
1665
+ transportDead = false;
1666
+ transportError = null;
1667
+ activeSendCount = 0;
1668
+ /**
1669
+ * Suppress duplicate ambient SSE auth echoes after HTTP command-scoped delivery.
1670
+ * Stale transport `onerror` may arrive after the send completes.
1671
+ */
1672
+ authHttpEchoSuppressUntilMs = 0;
1673
+ static AUTH_HTTP_ECHO_SUPPRESS_MS = 3e4;
1674
+ requestWaits = /* @__PURE__ */ new Map();
1675
+ authProviderHandle = null;
1676
+ constructor(sessionId) {
1677
+ this.sessionId = sessionId;
1678
+ }
1679
+ setAuthProviderHandle(handle) {
1680
+ this.authProviderHandle = handle;
1681
+ }
1682
+ setAuthState(authState) {
1683
+ if (!this.authProviderHandle) {
1684
+ throw new Error("Session has no OAuth auth provider");
1685
+ }
1686
+ this.authProviderHandle.setAuthState(authState);
1687
+ }
1688
+ setTransport(transport) {
1689
+ this.transport = transport;
1690
+ }
1691
+ setEventConsumer(consumer) {
1692
+ this.eventConsumer = consumer;
1693
+ while (this.eventQueue.length > 0) {
1694
+ const ev = this.eventQueue.shift();
1695
+ consumer(ev);
1696
+ }
1697
+ }
1698
+ clearEventConsumer() {
1699
+ this.eventConsumer = null;
1700
+ return this.transportDead;
1701
+ }
1702
+ markTransportDead(error) {
1703
+ this.transportDead = true;
1704
+ this.transportError = error;
1705
+ this.pushEvent({
1706
+ type: "transport_error",
1707
+ data: {
1708
+ error,
1709
+ code: -32e3
1710
+ // MCP error code for connection closed
1711
+ }
1712
+ });
1713
+ }
1714
+ isTransportDead() {
1715
+ return this.transportDead;
1716
+ }
1717
+ getTransportError() {
1718
+ return this.transportError;
1719
+ }
1720
+ hasEventConsumer() {
1721
+ return this.eventConsumer !== null;
1722
+ }
1723
+ beginSend() {
1724
+ this.activeSendCount++;
1725
+ }
1726
+ endSend() {
1727
+ if (this.activeSendCount > 0) {
1728
+ this.activeSendCount--;
1729
+ }
1730
+ }
1731
+ /** Command-scoped auth was returned on HTTP; suppress async onerror echo on SSE. */
1732
+ noteAuthChallengeDeliveredViaHttp() {
1733
+ this.extendAuthHttpEchoSuppression();
1734
+ }
1735
+ extendAuthHttpEchoSuppression() {
1736
+ this.authHttpEchoSuppressUntilMs = Date.now() + _RemoteSession.AUTH_HTTP_ECHO_SUPPRESS_MS;
1737
+ }
1738
+ shouldSuppressAuthEcho() {
1739
+ return Date.now() < this.authHttpEchoSuppressUntilMs;
1740
+ }
1741
+ hasActiveSend() {
1742
+ return this.activeSendCount > 0;
1743
+ }
1744
+ pushAuthChallenge(challenge) {
1745
+ this.pushEvent({ type: "auth_challenge", data: challenge });
1746
+ }
1747
+ /**
1748
+ * Streamable HTTP `transport.send()` can return before the JSON-RPC response
1749
+ * arrives. The remote send handler awaits this so `/api/mcp/send` returns
1750
+ * `auth_challenge` or `ok: true` only after the MCP round-trip (or auth error).
1751
+ */
1752
+ waitForRequestResponse(requestId, timeoutMs = 6e4) {
1753
+ return new Promise((resolve6, reject) => {
1754
+ const timer = timeoutMs > 0 ? setTimeout(() => {
1755
+ this.requestWaits.delete(requestId);
1756
+ reject(
1757
+ new Error(
1758
+ `MCP request ${String(requestId)} timed out after ${timeoutMs}ms`
1759
+ )
1760
+ );
1761
+ }, timeoutMs) : void 0;
1762
+ this.requestWaits.set(requestId, {
1763
+ resolve: () => {
1764
+ if (timer !== void 0) {
1765
+ clearTimeout(timer);
1766
+ }
1767
+ resolve6();
1768
+ },
1769
+ reject: (error) => {
1770
+ if (timer !== void 0) {
1771
+ clearTimeout(timer);
1772
+ }
1773
+ reject(error);
1774
+ }
1775
+ });
1776
+ });
1777
+ }
1778
+ cancelRequestWait(requestId) {
1779
+ const wait = this.requestWaits.get(requestId);
1780
+ if (!wait) {
1781
+ return;
1782
+ }
1783
+ this.requestWaits.delete(requestId);
1784
+ wait.reject(new Error("MCP request wait cancelled"));
1785
+ }
1786
+ rejectActiveRequestWaits(error) {
1787
+ for (const wait of this.requestWaits.values()) {
1788
+ wait.reject(error);
1789
+ }
1790
+ this.requestWaits.clear();
1791
+ }
1792
+ /**
1793
+ * Auth errors from the MCP transport.
1794
+ * - During an active send: command path owns delivery (HTTP); never SSE.
1795
+ * - After HTTP already returned auth_challenge: swallow async onerror echo.
1796
+ * - Otherwise: ambient SSE (idle session).
1797
+ */
1798
+ handleTransportAuthError(error) {
1799
+ if (!(error instanceof AuthChallengeError)) {
1800
+ return false;
1801
+ }
1802
+ this.rejectActiveRequestWaits(error);
1803
+ if (this.hasActiveSend()) {
1804
+ this.extendAuthHttpEchoSuppression();
1805
+ return true;
1806
+ }
1807
+ if (this.shouldSuppressAuthEcho()) {
1808
+ return true;
1809
+ }
1810
+ this.pushAuthChallenge(error.authChallenge);
1811
+ return true;
1812
+ }
1813
+ settleRequestWait(requestId) {
1814
+ const wait = this.requestWaits.get(requestId);
1815
+ if (!wait) {
1816
+ return;
1817
+ }
1818
+ this.requestWaits.delete(requestId);
1819
+ wait.resolve();
1820
+ }
1821
+ pushEvent(event) {
1822
+ if (this.eventConsumer) {
1823
+ this.eventConsumer(event);
1824
+ } else {
1825
+ this.eventQueue.push(event);
1826
+ }
1827
+ }
1828
+ onMessage(message) {
1829
+ if ("id" in message && message.id !== null && message.id !== void 0 && ("result" in message || "error" in message)) {
1830
+ this.settleRequestWait(message.id);
1831
+ }
1832
+ this.pushEvent({ type: "message", data: message });
1833
+ }
1834
+ onFetchRequest(entry) {
1835
+ this.pushEvent({
1836
+ type: "fetch_request",
1837
+ data: {
1838
+ ...entry,
1839
+ timestamp: entry.timestamp instanceof Date ? entry.timestamp.toISOString() : entry.timestamp
1840
+ }
1841
+ });
1842
+ }
1843
+ onFetchResponseBody(id, responseBody) {
1844
+ this.pushEvent({
1845
+ type: "fetch_request_body_update",
1846
+ data: { id, responseBody }
1847
+ });
1848
+ }
1849
+ onStderr(entry) {
1850
+ this.pushEvent({
1851
+ type: "stdio_log",
1852
+ data: {
1853
+ timestamp: entry.timestamp.toISOString(),
1854
+ message: entry.message
1855
+ }
1856
+ });
1857
+ }
1858
+ };
1859
+
1860
+ // ../../core/mcp/remote/node/tokenAuthProvider.ts
1861
+ var REMOTE_OAUTH_STUB_METADATA = {
1862
+ redirect_uris: [],
1863
+ scope: ""
1864
+ };
1865
+ function cloneAuthState(state) {
1866
+ return {
1867
+ ...state.oauthTokens && {
1868
+ oauthTokens: { ...state.oauthTokens }
1869
+ },
1870
+ ...state.oauthClient && {
1871
+ oauthClient: { ...state.oauthClient }
1872
+ }
1873
+ };
1874
+ }
1875
+ function createRemoteAuthProvider(initialAuthState) {
1876
+ if (!initialAuthState?.oauthTokens && !initialAuthState?.oauthClient) {
1877
+ return void 0;
1878
+ }
1879
+ let authState = cloneAuthState(initialAuthState);
1880
+ const provider = {
1881
+ get clientMetadata() {
1882
+ return REMOTE_OAUTH_STUB_METADATA;
1883
+ },
1884
+ get redirectUrl() {
1885
+ return "";
1886
+ },
1887
+ async tokens() {
1888
+ return authState.oauthTokens;
1889
+ },
1890
+ async clientInformation() {
1891
+ if (!authState.oauthClient?.client_id) {
1892
+ return void 0;
1893
+ }
1894
+ return {
1895
+ client_id: authState.oauthClient.client_id,
1896
+ ...authState.oauthClient.client_secret && {
1897
+ client_secret: authState.oauthClient.client_secret
1898
+ }
1899
+ };
1900
+ },
1901
+ async saveTokens(tokens) {
1902
+ authState = {
1903
+ ...authState,
1904
+ oauthTokens: {
1905
+ access_token: tokens.access_token,
1906
+ token_type: tokens.token_type,
1907
+ expires_in: tokens.expires_in,
1908
+ refresh_token: tokens.refresh_token,
1909
+ scope: tokens.scope,
1910
+ id_token: tokens.id_token
1911
+ }
1912
+ };
1913
+ },
1914
+ codeVerifier() {
1915
+ return void 0;
1916
+ },
1917
+ async saveCodeVerifier() {
1918
+ },
1919
+ clear() {
1920
+ authState = {};
1921
+ },
1922
+ async redirectToAuthorization() {
1923
+ throw new Error(
1924
+ "OAuth re-authorization must be performed in the Inspector web client (remote server cannot complete OAuth flows)"
1925
+ );
1926
+ },
1927
+ state() {
1928
+ return "";
1929
+ }
1930
+ // This provider implements only the members the remote backend transport
1931
+ // exercises (token get/save, client info, non-interactive redirect); the
1932
+ // interactive-only members the SDK never calls on the server side are stubbed
1933
+ // with narrower shapes (e.g. `codeVerifier()` returns `undefined`, not the
1934
+ // interface's `string | Promise<string>`), so it can't be assigned to the
1935
+ // full `OAuthClientProvider` without the double cast.
1936
+ };
1937
+ return {
1938
+ provider,
1939
+ setAuthState(next) {
1940
+ authState = cloneAuthState(next);
1941
+ },
1942
+ getAuthState() {
1943
+ return cloneAuthState(authState);
1944
+ }
1945
+ };
1946
+ }
1947
+
1948
+ // ../../core/auth/node/secret-store.ts
1949
+ import { AsyncEntry, findCredentialsAsync } from "@napi-rs/keyring";
1950
+ var SERVICE_NAME = "mcp-inspector";
1951
+ function parseAccount(account) {
1952
+ const idx = account.indexOf(":");
1953
+ if (idx <= 0 || idx === account.length - 1) return null;
1954
+ return {
1955
+ serverId: account.slice(0, idx),
1956
+ field: account.slice(idx + 1)
1957
+ };
1958
+ }
1959
+ var buildAccount = (serverId, field) => `${serverId}:${field}`;
1960
+ var KeychainUnavailableError = class extends Error {
1961
+ constructor(cause) {
1962
+ super(
1963
+ `OS keychain is not available. On Linux, install libsecret / gnome-keyring. Underlying error: ${cause instanceof Error ? cause.message : String(cause)}`
1964
+ );
1965
+ this.name = "KeychainUnavailableError";
1966
+ }
1967
+ };
1968
+ var KeyringSecretStore = class {
1969
+ async get(serverId, field) {
1970
+ const entry = new AsyncEntry(SERVICE_NAME, buildAccount(serverId, field));
1971
+ try {
1972
+ const v = await entry.getPassword();
1973
+ return v ?? null;
1974
+ } catch {
1975
+ return null;
1976
+ }
1977
+ }
1978
+ async set(serverId, field, value) {
1979
+ const entry = new AsyncEntry(SERVICE_NAME, buildAccount(serverId, field));
1980
+ try {
1981
+ await entry.setPassword(value);
1982
+ } catch (err) {
1983
+ throw new KeychainUnavailableError(err);
1984
+ }
1985
+ }
1986
+ async delete(serverId, field) {
1987
+ const entry = new AsyncEntry(SERVICE_NAME, buildAccount(serverId, field));
1988
+ try {
1989
+ await entry.deleteCredential();
1990
+ } catch {
1991
+ }
1992
+ }
1993
+ async deleteAllForServer(serverId) {
1994
+ let creds;
1995
+ try {
1996
+ creds = await findCredentialsAsync(SERVICE_NAME);
1997
+ } catch {
1998
+ return;
1999
+ }
2000
+ const prefix = `${serverId}:`;
2001
+ for (const c of creds) {
2002
+ if (!c.account.startsWith(prefix)) continue;
2003
+ const parsed = parseAccount(c.account);
2004
+ if (!parsed || parsed.serverId !== serverId) continue;
2005
+ await this.delete(serverId, parsed.field);
2006
+ }
2007
+ }
2008
+ };
2009
+
2010
+ // ../../core/client/config-parse.ts
2011
+ import { z } from "zod";
2012
+ function isAbsoluteHttpUrl(value) {
2013
+ let url;
2014
+ try {
2015
+ url = new URL(value.trim());
2016
+ } catch {
2017
+ return false;
2018
+ }
2019
+ if (url.protocol !== "http:" && url.protocol !== "https:") return false;
2020
+ return isRealHost(url.hostname);
2021
+ }
2022
+ function isRealHost(hostname) {
2023
+ if (hostname === "localhost") return true;
2024
+ if (hostname.startsWith("[")) return hostname.endsWith("]");
2025
+ const labels = hostname.split(".");
2026
+ return labels.length >= 2 && labels.every((label) => label !== "");
2027
+ }
2028
+ var HttpUrlStringSchema = z.string().min(1).superRefine((val, ctx) => {
2029
+ const trimmed = val.trim();
2030
+ if (!isAbsoluteHttpUrl(trimmed)) {
2031
+ ctx.addIssue({
2032
+ code: z.ZodIssueCode.custom,
2033
+ message: `Invalid URL: "${trimmed}" \u2014 must be an http(s) URL (e.g. https://idp.example.com)`
2034
+ });
2035
+ }
2036
+ });
2037
+ var CIMD_METADATA_URL_INVALID_ERROR = "Must be a valid URL, like https://example.com/oauth/client.json";
2038
+ var CIMD_METADATA_URL_HTTPS_ERROR = "CIMD client metadata URL must use HTTPS";
2039
+ var CIMD_METADATA_URL_PATH_ERROR = "Must include a path (not the site root), like https://example.com/oauth/client.json";
2040
+ function getCimdClientMetadataUrlError(value) {
2041
+ const trimmed = value.trim();
2042
+ if (trimmed === "") return void 0;
2043
+ if (!isAbsoluteHttpUrl(trimmed)) {
2044
+ return CIMD_METADATA_URL_INVALID_ERROR;
2045
+ }
2046
+ try {
2047
+ const url = new URL(trimmed);
2048
+ if (url.protocol !== "https:") {
2049
+ return CIMD_METADATA_URL_HTTPS_ERROR;
2050
+ }
2051
+ if (url.pathname === "/" || url.pathname === "") {
2052
+ return CIMD_METADATA_URL_PATH_ERROR;
2053
+ }
2054
+ } catch {
2055
+ return CIMD_METADATA_URL_INVALID_ERROR;
2056
+ }
2057
+ return void 0;
2058
+ }
2059
+ function refineCimdMetadataUrl(val, ctx, required) {
2060
+ const trimmed = val.trim();
2061
+ if (trimmed === "") {
2062
+ if (required) {
2063
+ ctx.addIssue({
2064
+ code: z.ZodIssueCode.custom,
2065
+ message: "CIMD client metadata URL is required when CIMD is enabled"
2066
+ });
2067
+ }
2068
+ return;
2069
+ }
2070
+ const error = getCimdClientMetadataUrlError(trimmed);
2071
+ if (error) {
2072
+ ctx.addIssue({
2073
+ code: z.ZodIssueCode.custom,
2074
+ message: error
2075
+ });
2076
+ }
2077
+ }
2078
+ var CimdConfigSchema = z.object({
2079
+ enabled: z.boolean(),
2080
+ clientMetadataUrl: z.string()
2081
+ }).superRefine((data, ctx) => {
2082
+ refineCimdMetadataUrl(data.clientMetadataUrl, ctx, data.enabled === true);
2083
+ });
2084
+ var EnterpriseManagedAuthIdpConfigSchema = z.object({
2085
+ issuer: HttpUrlStringSchema,
2086
+ clientId: z.string().min(1),
2087
+ clientSecret: z.string().optional()
2088
+ });
2089
+ var ClientConfigSchema = z.object({
2090
+ enterpriseManagedAuth: z.object({
2091
+ enabled: z.boolean().optional(),
2092
+ idp: EnterpriseManagedAuthIdpConfigSchema
2093
+ }).optional(),
2094
+ cimd: CimdConfigSchema.optional()
2095
+ });
2096
+ function parseClientConfig(raw) {
2097
+ return ClientConfigSchema.parse(raw);
2098
+ }
2099
+ function formatClientConfigLoadError(error) {
2100
+ if (error instanceof z.ZodError) {
2101
+ const issue = error.issues[0];
2102
+ if (issue) {
2103
+ const field = issue.path.length > 0 ? issue.path.map(String).join(".") : "config";
2104
+ return `${field}: ${issue.message}`;
2105
+ }
2106
+ return "Invalid client.json shape";
2107
+ }
2108
+ if (error instanceof Error) {
2109
+ return error.message;
2110
+ }
2111
+ return String(error);
2112
+ }
2113
+
2114
+ // ../../core/client/secrets.ts
2115
+ var CLIENT_KEYCHAIN_ID = "client";
2116
+ function hasClientPlaintextSecret(config) {
2117
+ const secret = config.enterpriseManagedAuth?.idp?.clientSecret;
2118
+ return typeof secret === "string" && secret.length > 0;
2119
+ }
2120
+ function extractSecretsFromClientConfig(config) {
2121
+ const secrets = {};
2122
+ const ema = config.enterpriseManagedAuth;
2123
+ if (!ema?.idp?.clientSecret) {
2124
+ return { stripped: config, secrets };
2125
+ }
2126
+ const { clientSecret, ...idpRest } = ema.idp;
2127
+ secrets[SECRET_FIELD_IDP_CLIENT_SECRET] = clientSecret;
2128
+ return {
2129
+ stripped: {
2130
+ ...config,
2131
+ enterpriseManagedAuth: {
2132
+ ...ema,
2133
+ idp: idpRest
2134
+ }
2135
+ },
2136
+ secrets
2137
+ };
2138
+ }
2139
+ function mergeSecretsIntoClientConfig(config, secrets) {
2140
+ const idpSecret = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
2141
+ const ema = config.enterpriseManagedAuth;
2142
+ if (!idpSecret || !ema?.idp) {
2143
+ return config;
2144
+ }
2145
+ return {
2146
+ ...config,
2147
+ enterpriseManagedAuth: {
2148
+ ...ema,
2149
+ idp: {
2150
+ ...ema.idp,
2151
+ clientSecret: idpSecret
2152
+ }
2153
+ }
2154
+ };
2155
+ }
2156
+
2157
+ // ../../core/client/node-persistence.ts
2158
+ async function readIdpSecretFromKeychain(secretStore) {
2159
+ const secret = await secretStore.get(
2160
+ CLIENT_KEYCHAIN_ID,
2161
+ SECRET_FIELD_IDP_CLIENT_SECRET
2162
+ );
2163
+ if (!secret) return {};
2164
+ return { [SECRET_FIELD_IDP_CLIENT_SECRET]: secret };
2165
+ }
2166
+ async function migrateClientPlaintextSecret(filePath, config, secretStore) {
2167
+ const { stripped, secrets } = extractSecretsFromClientConfig(config);
2168
+ const value = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
2169
+ if (!value) return config;
2170
+ try {
2171
+ const existing = await secretStore.get(
2172
+ CLIENT_KEYCHAIN_ID,
2173
+ SECRET_FIELD_IDP_CLIENT_SECRET
2174
+ );
2175
+ if (existing === null) {
2176
+ await secretStore.set(
2177
+ CLIENT_KEYCHAIN_ID,
2178
+ SECRET_FIELD_IDP_CLIENT_SECRET,
2179
+ value
2180
+ );
2181
+ }
2182
+ await writeStoreFile(filePath, serializeStore(stripped));
2183
+ return stripped;
2184
+ } catch (err) {
2185
+ if (err instanceof KeychainUnavailableError) {
2186
+ return config;
2187
+ }
2188
+ throw err;
2189
+ }
2190
+ }
2191
+ async function readClientConfigStore(filePath, secretStore) {
2192
+ const raw = await readStoreFile(filePath);
2193
+ if (raw === null) {
2194
+ return {};
2195
+ }
2196
+ let config = parseClientConfig(parseStore(raw));
2197
+ if (hasClientPlaintextSecret(config)) {
2198
+ config = await migrateClientPlaintextSecret(filePath, config, secretStore);
2199
+ }
2200
+ const secrets = await readIdpSecretFromKeychain(secretStore);
2201
+ return mergeSecretsIntoClientConfig(config, secrets);
2202
+ }
2203
+ async function writeClientConfigStore(filePath, body, secretStore) {
2204
+ const validated = parseClientConfig(body);
2205
+ const { stripped, secrets } = extractSecretsFromClientConfig(validated);
2206
+ const idpSecret = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
2207
+ if (idpSecret) {
2208
+ await secretStore.set(
2209
+ CLIENT_KEYCHAIN_ID,
2210
+ SECRET_FIELD_IDP_CLIENT_SECRET,
2211
+ idpSecret
2212
+ );
2213
+ } else {
2214
+ await secretStore.delete(
2215
+ CLIENT_KEYCHAIN_ID,
2216
+ SECRET_FIELD_IDP_CLIENT_SECRET
2217
+ );
2218
+ }
2219
+ await writeStoreFile(filePath, serializeStore(stripped));
2220
+ }
2221
+ async function deleteClientConfigStore(filePath, secretStore) {
2222
+ await deleteStoreFile(filePath);
2223
+ await secretStore.delete(CLIENT_KEYCHAIN_ID, SECRET_FIELD_IDP_CLIENT_SECRET);
2224
+ }
2225
+
2226
+ // ../../core/mcp/remote/node/server.ts
2227
+ import { ZodError as ZodError2 } from "zod";
2228
+ function createOriginMiddleware(allowedOrigins) {
2229
+ return async (c, next) => {
2230
+ if (!allowedOrigins || allowedOrigins.length === 0) {
2231
+ await next();
2232
+ return;
2233
+ }
2234
+ const origin = c.req.header("origin");
2235
+ if (c.req.method === "OPTIONS") {
2236
+ if (origin && allowedOrigins.includes(origin)) {
2237
+ c.header("Access-Control-Allow-Origin", origin);
2238
+ c.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
2239
+ c.header(
2240
+ "Access-Control-Allow-Headers",
2241
+ "Content-Type, x-mcp-remote-auth"
2242
+ );
2243
+ c.header("Access-Control-Max-Age", "86400");
2244
+ return c.body(null, 204);
2245
+ }
2246
+ return c.json(
2247
+ {
2248
+ error: "Forbidden",
2249
+ message: "Invalid origin. Request blocked to prevent DNS rebinding attacks."
2250
+ },
2251
+ 403
2252
+ );
2253
+ }
2254
+ if (origin) {
2255
+ if (!allowedOrigins.includes(origin)) {
2256
+ return c.json(
2257
+ {
2258
+ error: "Forbidden",
2259
+ message: "Invalid origin. Request blocked to prevent DNS rebinding attacks. Configure allowed origins via allowedOrigins option."
2260
+ },
2261
+ 403
2262
+ );
2263
+ }
2264
+ c.header("Access-Control-Allow-Origin", origin);
2265
+ }
2266
+ await next();
2267
+ };
2268
+ }
2269
+ function createAuthMiddleware(authToken) {
2270
+ return async (c, next) => {
2271
+ const authHeader = c.req.header("x-mcp-remote-auth");
2272
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
2273
+ return c.json(
2274
+ {
2275
+ error: "Unauthorized",
2276
+ message: "Authentication required. Use the x-mcp-remote-auth header with Bearer token."
2277
+ },
2278
+ 401
2279
+ );
2280
+ }
2281
+ const providedToken = authHeader.substring(7);
2282
+ const expectedToken = authToken;
2283
+ const providedBuffer = Buffer.from(providedToken);
2284
+ const expectedBuffer = Buffer.from(expectedToken);
2285
+ if (providedBuffer.length !== expectedBuffer.length) {
2286
+ return c.json(
2287
+ {
2288
+ error: "Unauthorized",
2289
+ message: "Authentication required. Use the x-mcp-remote-auth header with Bearer token."
2290
+ },
2291
+ 401
2292
+ );
2293
+ }
2294
+ if (!timingSafeEqual(providedBuffer, expectedBuffer)) {
2295
+ return c.json(
2296
+ {
2297
+ error: "Unauthorized",
2298
+ message: "Authentication required. Use the x-mcp-remote-auth header with Bearer token."
2299
+ },
2300
+ 401
2301
+ );
2302
+ }
2303
+ await next();
2304
+ };
2305
+ }
2306
+ function forwardLogEvent(logger, logEvent) {
2307
+ const levelLabel = (logEvent?.level?.label ?? "info").toLowerCase();
2308
+ const method = toRecord(logger)[levelLabel];
2309
+ if (typeof method !== "function") return;
2310
+ const bindings = Object.assign(
2311
+ {},
2312
+ ...Array.isArray(logEvent.bindings) ? logEvent.bindings : []
2313
+ );
2314
+ const messages = Array.isArray(logEvent.messages) ? logEvent.messages : [];
2315
+ if (messages.length === 0) {
2316
+ method.call(logger, bindings);
2317
+ return;
2318
+ }
2319
+ const first = messages[0];
2320
+ if (typeof first === "object" && first !== null && !Array.isArray(first)) {
2321
+ const obj = { ...bindings, ...first };
2322
+ const msg = messages[1];
2323
+ const args = messages.slice(2);
2324
+ method.call(
2325
+ logger,
2326
+ obj,
2327
+ msg,
2328
+ ...args
2329
+ );
2330
+ } else {
2331
+ const msg = messages[0];
2332
+ const args = messages.slice(1);
2333
+ method.call(
2334
+ logger,
2335
+ bindings,
2336
+ msg,
2337
+ ...args
2338
+ );
2339
+ }
2340
+ }
2341
+ function requestIdForSendWait(message) {
2342
+ if ("method" in message && "id" in message && message.id !== null && message.id !== void 0) {
2343
+ if (message.method === "subscriptions/listen") {
2344
+ return void 0;
2345
+ }
2346
+ return message.id;
2347
+ }
2348
+ return void 0;
2349
+ }
2350
+ function createRemoteApp(options) {
2351
+ const dangerouslyOmitAuth = !!options.dangerouslyOmitAuth;
2352
+ const authToken = dangerouslyOmitAuth ? "" : options.authToken || process.env[API_SERVER_ENV_VARS.AUTH_TOKEN] || randomBytes(32).toString("hex");
2353
+ const app = new Hono();
2354
+ const sessions = /* @__PURE__ */ new Map();
2355
+ const { logger: fileLogger, allowedOrigins } = options;
2356
+ const storageDir = options.storageDir ?? getDefaultStorageDir();
2357
+ const mcpConfigPath = options.mcpConfigPath ?? getDefaultMcpConfigPath();
2358
+ const secretStore = options.secretStore ?? new KeyringSecretStore();
2359
+ const writable = options.writable ?? true;
2360
+ const inMemoryServers = options.initialServers;
2361
+ const watchable = !inMemoryServers;
2362
+ const serverEventSubscribers = /* @__PURE__ */ new Set();
2363
+ let mcpConfigWatcher = null;
2364
+ let lastWrittenMtimeMs = null;
2365
+ const broadcastServerListChange = () => {
2366
+ const payload = JSON.stringify({ type: "change" });
2367
+ for (const send of serverEventSubscribers) {
2368
+ try {
2369
+ send(payload);
2370
+ } catch {
2371
+ }
2372
+ }
2373
+ };
2374
+ const writeMcpAndTrackMtime = async (data) => {
2375
+ let externalEditDetected = false;
2376
+ if (lastWrittenMtimeMs !== null) {
2377
+ try {
2378
+ const s = await fsStat(mcpConfigPath);
2379
+ if (s.mtimeMs !== lastWrittenMtimeMs) {
2380
+ externalEditDetected = true;
2381
+ }
2382
+ } catch {
2383
+ externalEditDetected = true;
2384
+ }
2385
+ }
2386
+ await writeStoreFile(mcpConfigPath, data);
2387
+ try {
2388
+ const s = await fsStat(mcpConfigPath);
2389
+ lastWrittenMtimeMs = s.mtimeMs;
2390
+ } catch {
2391
+ }
2392
+ if (externalEditDetected) {
2393
+ broadcastServerListChange();
2394
+ }
2395
+ };
2396
+ const handleWatcherEvent = async (event) => {
2397
+ if (event !== "add" && event !== "change" && event !== "unlink") return;
2398
+ if (event !== "unlink") {
2399
+ try {
2400
+ const s = await fsStat(mcpConfigPath);
2401
+ if (lastWrittenMtimeMs !== null && s.mtimeMs === lastWrittenMtimeMs) {
2402
+ return;
2403
+ }
2404
+ } catch {
2405
+ }
2406
+ }
2407
+ broadcastServerListChange();
2408
+ };
2409
+ const handleWatcherError = (err) => {
2410
+ const msg = err instanceof Error ? err.message : String(err);
2411
+ if (fileLogger) {
2412
+ fileLogger.warn({ err: msg }, "mcp.json watcher error");
2413
+ } else {
2414
+ console.warn("[mcp.json watcher]", msg);
2415
+ }
2416
+ };
2417
+ const ensureWatcher = () => {
2418
+ if (mcpConfigWatcher) return;
2419
+ mcpConfigWatcher = chokidarWatch(mcpConfigPath, {
2420
+ ignoreInitial: true,
2421
+ awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
2422
+ });
2423
+ mcpConfigWatcher.on("all", (event) => {
2424
+ void handleWatcherEvent(event);
2425
+ });
2426
+ mcpConfigWatcher.on("error", handleWatcherError);
2427
+ };
2428
+ const maybeStopWatcher = async () => {
2429
+ if (serverEventSubscribers.size > 0) return;
2430
+ if (!mcpConfigWatcher) return;
2431
+ const w = mcpConfigWatcher;
2432
+ mcpConfigWatcher = null;
2433
+ try {
2434
+ await w.close();
2435
+ } catch {
2436
+ }
2437
+ };
2438
+ app.use("*", createOriginMiddleware(allowedOrigins));
2439
+ if (!dangerouslyOmitAuth) {
2440
+ app.use("*", createAuthMiddleware(authToken));
2441
+ }
2442
+ app.get("/api/config", (c) => {
2443
+ const payload = {
2444
+ ...options.initialConfig,
2445
+ writable,
2446
+ ...options.sandboxUrl ? { sandboxUrl: options.sandboxUrl } : {}
2447
+ };
2448
+ return c.json(payload);
2449
+ });
2450
+ app.post("/api/mcp/connect", async (c) => {
2451
+ let body;
2452
+ try {
2453
+ body = await c.req.json();
2454
+ } catch {
2455
+ return c.json({ error: "Invalid JSON body" }, 400);
2456
+ }
2457
+ const config = body.config;
2458
+ if (!config) {
2459
+ return c.json({ error: "Missing config" }, 400);
2460
+ }
2461
+ const sessionId = crypto.randomUUID();
2462
+ const session = new RemoteSession(sessionId);
2463
+ let transport;
2464
+ let authHandle;
2465
+ try {
2466
+ const initialAuthState = body.authState ?? (body.oauthTokens ? { oauthTokens: body.oauthTokens } : void 0);
2467
+ authHandle = createRemoteAuthProvider(initialAuthState);
2468
+ const result = createTransportNode(config, {
2469
+ pipeStderr: true,
2470
+ onStderr: (entry) => session.onStderr(entry),
2471
+ onFetchRequest: (entry) => session.onFetchRequest(entry),
2472
+ onFetchResponseBody: (id, body2) => session.onFetchResponseBody(id, body2),
2473
+ authProvider: authHandle?.provider,
2474
+ settings: body.settings,
2475
+ // Always intercept 401/403 on the node MCP transport. Without this, the
2476
+ // SDK's built-in auth() retry on a frozen token snapshot can hang or
2477
+ // succeed locally while /api/mcp/send still awaits a JSON-RPC response.
2478
+ interceptAuthChallenges: true
2479
+ });
2480
+ transport = result.transport;
2481
+ } catch (err) {
2482
+ const msg = err instanceof Error ? err.message : String(err);
2483
+ return c.json({ error: `Failed to create transport: ${msg}` }, 500);
2484
+ }
2485
+ session.setAuthProviderHandle(authHandle ?? null);
2486
+ session.setTransport(transport);
2487
+ transport.onmessage = (msg) => session.onMessage(msg);
2488
+ let transportFailed = false;
2489
+ let transportError = null;
2490
+ const originalOnclose = transport.onclose;
2491
+ const originalOnerror = transport.onerror;
2492
+ transport.onerror = (err) => {
2493
+ if (session.handleTransportAuthError(err)) {
2494
+ originalOnerror?.(err);
2495
+ return;
2496
+ }
2497
+ transportFailed = true;
2498
+ transportError = err instanceof Error ? err.message : String(err);
2499
+ originalOnerror?.(err);
2500
+ };
2501
+ transport.onclose = () => {
2502
+ const session2 = sessions.get(sessionId);
2503
+ if (session2) {
2504
+ const errorMsg = transportError || "Transport closed - process may have exited";
2505
+ session2.markTransportDead(errorMsg);
2506
+ if (!session2.hasEventConsumer()) {
2507
+ setTimeout(() => {
2508
+ const stale = sessions.get(sessionId);
2509
+ if (stale && !stale.hasEventConsumer()) {
2510
+ sessions.delete(sessionId);
2511
+ }
2512
+ }, 3e4).unref();
2513
+ }
2514
+ } else {
2515
+ transportFailed = true;
2516
+ transportError = transportError || "Transport closed during start - process may have failed";
2517
+ }
2518
+ originalOnclose?.();
2519
+ };
2520
+ try {
2521
+ await transport.start();
2522
+ if (transportFailed) {
2523
+ const errorMsg = transportError || "Transport failed during start";
2524
+ return c.json({ error: `Failed to start transport: ${errorMsg}` }, 500);
2525
+ }
2526
+ } catch (err) {
2527
+ if (err instanceof AuthChallengeError) {
2528
+ try {
2529
+ await transport.close();
2530
+ } catch {
2531
+ }
2532
+ session.noteAuthChallengeDeliveredViaHttp();
2533
+ return c.json({
2534
+ ok: false,
2535
+ kind: "auth_challenge",
2536
+ authChallenge: err.authChallenge
2537
+ });
2538
+ }
2539
+ const msg = err instanceof Error ? err.message : String(err);
2540
+ const status = err.code ?? err.status;
2541
+ const is401 = status === 401;
2542
+ return c.json(
2543
+ { error: `Failed to start transport: ${msg}` },
2544
+ is401 ? 401 : 500
2545
+ );
2546
+ }
2547
+ sessions.set(sessionId, session);
2548
+ return c.json({ sessionId });
2549
+ });
2550
+ app.post("/api/mcp/send", async (c) => {
2551
+ let body;
2552
+ try {
2553
+ body = await c.req.json();
2554
+ } catch {
2555
+ return c.json({ error: "Invalid JSON body" }, 400);
2556
+ }
2557
+ const { sessionId, message, relatedRequestId } = body;
2558
+ if (!sessionId || !message) {
2559
+ return c.json({ error: "Missing sessionId or message" }, 400);
2560
+ }
2561
+ const session = sessions.get(sessionId);
2562
+ if (!session) {
2563
+ return c.json({ error: "Session not found" }, 404);
2564
+ }
2565
+ if (session.isTransportDead()) {
2566
+ const errorMsg = session.getTransportError() || "Transport closed";
2567
+ return c.json({ ok: false, kind: "transport_error", error: errorMsg });
2568
+ }
2569
+ session.beginSend();
2570
+ const requestId = requestIdForSendWait(message);
2571
+ let responseWait;
2572
+ if (requestId !== void 0) {
2573
+ responseWait = session.waitForRequestResponse(requestId);
2574
+ void responseWait.catch(() => {
2575
+ });
2576
+ }
2577
+ try {
2578
+ await session.transport.send(message, {
2579
+ relatedRequestId
2580
+ });
2581
+ if (responseWait) {
2582
+ await responseWait;
2583
+ }
2584
+ return c.json({ ok: true });
2585
+ } catch (err) {
2586
+ if (requestId !== void 0) {
2587
+ session.cancelRequestWait(requestId);
2588
+ }
2589
+ if (err instanceof AuthChallengeError) {
2590
+ session.noteAuthChallengeDeliveredViaHttp();
2591
+ return c.json({
2592
+ ok: false,
2593
+ kind: "auth_challenge",
2594
+ authChallenge: err.authChallenge
2595
+ });
2596
+ }
2597
+ const msg = err instanceof Error ? err.message : String(err);
2598
+ return c.json({ ok: false, kind: "transport_error", error: msg });
2599
+ } finally {
2600
+ session.endSend();
2601
+ }
2602
+ });
2603
+ app.get("/api/mcp/events", async (c) => {
2604
+ const sessionId = c.req.query("sessionId");
2605
+ if (!sessionId) {
2606
+ return c.json({ error: "Missing sessionId query" }, 400);
2607
+ }
2608
+ const session = sessions.get(sessionId);
2609
+ if (!session) {
2610
+ return c.json({ error: "Session not found" }, 404);
2611
+ }
2612
+ return streamSSE(c, async (stream) => {
2613
+ session.setEventConsumer((event) => {
2614
+ const data = JSON.stringify(event);
2615
+ void stream.writeSSE({
2616
+ event: event.type,
2617
+ data
2618
+ });
2619
+ });
2620
+ if (session.isTransportDead()) {
2621
+ await new Promise((resolve6) => setTimeout(resolve6, 0));
2622
+ session.clearEventConsumer();
2623
+ sessions.delete(sessionId);
2624
+ return;
2625
+ }
2626
+ stream.onAbort(() => {
2627
+ const shouldCleanup = session.clearEventConsumer();
2628
+ stream.close();
2629
+ if (shouldCleanup || session.isTransportDead()) {
2630
+ sessions.delete(sessionId);
2631
+ }
2632
+ });
2633
+ await new Promise((resolve6) => {
2634
+ stream.onAbort(() => {
2635
+ resolve6();
2636
+ });
2637
+ });
2638
+ });
2639
+ });
2640
+ app.post("/api/mcp/disconnect", async (c) => {
2641
+ let body;
2642
+ try {
2643
+ body = await c.req.json();
2644
+ } catch {
2645
+ return c.json({ error: "Invalid JSON body" }, 400);
2646
+ }
2647
+ const sessionId = body.sessionId;
2648
+ if (!sessionId) {
2649
+ return c.json({ error: "Missing sessionId" }, 400);
2650
+ }
2651
+ const session = sessions.get(sessionId);
2652
+ if (session) {
2653
+ session.clearEventConsumer();
2654
+ await session.transport.close();
2655
+ sessions.delete(sessionId);
2656
+ }
2657
+ return c.json({ ok: true });
2658
+ });
2659
+ app.post("/api/mcp/auth-state", async (c) => {
2660
+ let body;
2661
+ try {
2662
+ body = await c.req.json();
2663
+ } catch {
2664
+ return c.json({ error: "Invalid JSON body" }, 400);
2665
+ }
2666
+ const { sessionId, authState } = body;
2667
+ if (!sessionId || !authState) {
2668
+ return c.json({ error: "Missing sessionId or authState" }, 400);
2669
+ }
2670
+ const session = sessions.get(sessionId);
2671
+ if (!session) {
2672
+ return c.json({ error: "Session not found" }, 404);
2673
+ }
2674
+ if (session.isTransportDead()) {
2675
+ const errorMsg = session.getTransportError() || "Transport closed";
2676
+ return c.json({ ok: false, kind: "transport_error", error: errorMsg });
2677
+ }
2678
+ try {
2679
+ session.setAuthState(authState);
2680
+ } catch (err) {
2681
+ const msg = err instanceof Error ? err.message : String(err);
2682
+ return c.json({ error: msg }, 400);
2683
+ }
2684
+ return c.json({ ok: true });
2685
+ });
2686
+ app.post("/api/fetch", async (c) => {
2687
+ let body;
2688
+ try {
2689
+ body = await c.req.json();
2690
+ } catch {
2691
+ return c.json({ error: "Invalid JSON body" }, 400);
2692
+ }
2693
+ const { url, method = "GET", headers = {}, body: reqBody } = body;
2694
+ if (!url) {
2695
+ return c.json({ error: "Missing url" }, 400);
2696
+ }
2697
+ try {
2698
+ const res = await fetch(url, {
2699
+ method,
2700
+ headers: new Headers(headers),
2701
+ body: reqBody
2702
+ });
2703
+ const resHeaders = {};
2704
+ res.headers.forEach((v, k) => {
2705
+ resHeaders[k] = v;
2706
+ });
2707
+ const contentType = res.headers.get("content-type");
2708
+ const isStream = contentType?.includes("text/event-stream") || contentType?.includes("application/x-ndjson");
2709
+ let resBody;
2710
+ if (!isStream && res.body) {
2711
+ resBody = await res.text();
2712
+ }
2713
+ return c.json({
2714
+ ok: res.ok,
2715
+ status: res.status,
2716
+ statusText: res.statusText,
2717
+ headers: resHeaders,
2718
+ body: resBody
2719
+ });
2720
+ } catch (err) {
2721
+ const msg = err instanceof Error ? err.message : String(err);
2722
+ return c.json({ error: msg }, 500);
2723
+ }
2724
+ });
2725
+ app.post("/api/log", async (c) => {
2726
+ const body = await c.req.json().catch(() => ({}));
2727
+ if (fileLogger) {
2728
+ forwardLogEvent(fileLogger, body);
2729
+ }
2730
+ return c.json({ ok: true });
2731
+ });
2732
+ app.get("/api/storage/:storeId", async (c) => {
2733
+ const storeId = c.req.param("storeId");
2734
+ if (!storeId || !validateStoreId(storeId)) {
2735
+ return c.json({ error: "Invalid storeId" }, 400);
2736
+ }
2737
+ const filePath = getStoreFilePath(storageDir, storeId);
2738
+ try {
2739
+ if (storeId === "client") {
2740
+ const config = await readClientConfigStore(filePath, secretStore);
2741
+ return c.json(config);
2742
+ }
2743
+ const raw = await readStoreFile(filePath);
2744
+ if (raw === null) {
2745
+ return c.json({}, 200);
2746
+ }
2747
+ const store = parseStore(raw);
2748
+ return c.json(store);
2749
+ } catch (error) {
2750
+ const msg = error instanceof Error ? error.message : String(error);
2751
+ return c.json({ error: `Failed to read store: ${msg}` }, 500);
2752
+ }
2753
+ });
2754
+ app.post("/api/storage/:storeId", async (c) => {
2755
+ const storeId = c.req.param("storeId");
2756
+ if (!storeId || !validateStoreId(storeId)) {
2757
+ return c.json({ error: "Invalid storeId" }, 400);
2758
+ }
2759
+ let body;
2760
+ try {
2761
+ body = await c.req.json();
2762
+ } catch {
2763
+ return c.json({ error: "Invalid JSON body" }, 400);
2764
+ }
2765
+ const filePath = getStoreFilePath(storageDir, storeId);
2766
+ try {
2767
+ if (storeId === "client") {
2768
+ await writeClientConfigStore(filePath, body, secretStore);
2769
+ return c.json({ ok: true });
2770
+ }
2771
+ const jsonData = serializeStore(body);
2772
+ await writeStoreFile(filePath, jsonData);
2773
+ return c.json({ ok: true });
2774
+ } catch (error) {
2775
+ if (error instanceof ZodError2) {
2776
+ return c.json({ error: formatClientConfigLoadError(error) }, 400);
2777
+ }
2778
+ const keychainResp = storeId === "client" ? keychainErrorResponse(c, error) : void 0;
2779
+ if (keychainResp) return keychainResp;
2780
+ const msg = error instanceof Error ? error.message : String(error);
2781
+ return c.json({ error: `Failed to write store: ${msg}` }, 500);
2782
+ }
2783
+ });
2784
+ app.delete("/api/storage/:storeId", async (c) => {
2785
+ const storeId = c.req.param("storeId");
2786
+ if (!storeId || !validateStoreId(storeId)) {
2787
+ return c.json({ error: "Invalid storeId" }, 400);
2788
+ }
2789
+ const filePath = getStoreFilePath(storageDir, storeId);
2790
+ try {
2791
+ if (storeId === "client") {
2792
+ await deleteClientConfigStore(filePath, secretStore);
2793
+ return c.json({ ok: true });
2794
+ }
2795
+ await deleteStoreFile(filePath);
2796
+ return c.json({ ok: true });
2797
+ } catch (error) {
2798
+ const msg = error instanceof Error ? error.message : String(error);
2799
+ return c.json({ error: `Failed to delete store: ${msg}` }, 500);
2800
+ }
2801
+ });
2802
+ const logWarn = (bindings, msg) => {
2803
+ if (fileLogger) {
2804
+ fileLogger.warn(bindings, msg);
2805
+ } else {
2806
+ console.warn("[mcp.json]", msg, bindings);
2807
+ }
2808
+ };
2809
+ const isStringRecord = (v) => {
2810
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
2811
+ for (const val of Object.values(v)) {
2812
+ if (typeof val !== "string") return false;
2813
+ }
2814
+ return true;
2815
+ };
2816
+ const isKvArray = (v) => {
2817
+ if (!Array.isArray(v)) return false;
2818
+ return v.every(
2819
+ (e) => e !== null && typeof e === "object" && typeof e.key === "string" && typeof e.value === "string"
2820
+ );
2821
+ };
2822
+ const isOauthObject = (v) => {
2823
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
2824
+ const o = v;
2825
+ for (const k of ["clientId", "clientSecret", "scopes"]) {
2826
+ if (o[k] !== void 0 && typeof o[k] !== "string") return false;
2827
+ }
2828
+ if (o.enterpriseManaged !== void 0 && typeof o.enterpriseManaged !== "boolean") {
2829
+ return false;
2830
+ }
2831
+ return true;
2832
+ };
2833
+ const isRootArray = (v) => {
2834
+ if (!Array.isArray(v)) return false;
2835
+ return v.every((e) => {
2836
+ if (e === null || typeof e !== "object") return false;
2837
+ const o = e;
2838
+ if (typeof o.uri !== "string") return false;
2839
+ if (o.name !== void 0 && typeof o.name !== "string") return false;
2840
+ return true;
2841
+ });
2842
+ };
2843
+ const isNonNegNumber = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
2844
+ const normalizeMcpServers = (raw) => {
2845
+ if (!raw || typeof raw !== "object") return {};
2846
+ const out = {};
2847
+ for (const [id, val] of Object.entries(raw)) {
2848
+ if (!val || typeof val !== "object") continue;
2849
+ const valObj = val;
2850
+ if ("settings" in valObj) {
2851
+ logWarn(
2852
+ { route: "/api/servers", id, droppedKey: "settings" },
2853
+ "Dropping legacy `settings` node from mcp.json entry \u2014 fields now live at the top level. Re-enter via the settings form or hand-edit the file into the flat shape."
2854
+ );
2855
+ delete valObj.settings;
2856
+ }
2857
+ if ("headers" in valObj && !isStringRecord(valObj.headers)) {
2858
+ logWarn(
2859
+ { route: "/api/servers", id, droppedKey: "headers" },
2860
+ "Dropping malformed `headers` field \u2014 expected `Record<string, string>`."
2861
+ );
2862
+ delete valObj.headers;
2863
+ }
2864
+ if ("metadata" in valObj && !isKvArray(valObj.metadata)) {
2865
+ logWarn(
2866
+ { route: "/api/servers", id, droppedKey: "metadata" },
2867
+ "Dropping malformed `metadata` field \u2014 expected `Array<{ key: string, value: string }>`."
2868
+ );
2869
+ delete valObj.metadata;
2870
+ }
2871
+ if ("connectionTimeout" in valObj && !isNonNegNumber(valObj.connectionTimeout)) {
2872
+ logWarn(
2873
+ { route: "/api/servers", id, droppedKey: "connectionTimeout" },
2874
+ "Dropping malformed `connectionTimeout` field \u2014 expected non-negative number."
2875
+ );
2876
+ delete valObj.connectionTimeout;
2877
+ }
2878
+ if ("requestTimeout" in valObj && !isNonNegNumber(valObj.requestTimeout)) {
2879
+ logWarn(
2880
+ { route: "/api/servers", id, droppedKey: "requestTimeout" },
2881
+ "Dropping malformed `requestTimeout` field \u2014 expected non-negative number."
2882
+ );
2883
+ delete valObj.requestTimeout;
2884
+ }
2885
+ if ("taskTtl" in valObj && !isNonNegNumber(valObj.taskTtl)) {
2886
+ logWarn(
2887
+ { route: "/api/servers", id, droppedKey: "taskTtl" },
2888
+ "Dropping malformed `taskTtl` field \u2014 expected non-negative number."
2889
+ );
2890
+ delete valObj.taskTtl;
2891
+ }
2892
+ if ("maxFetchRequests" in valObj && !isNonNegNumber(valObj.maxFetchRequests)) {
2893
+ logWarn(
2894
+ { route: "/api/servers", id, droppedKey: "maxFetchRequests" },
2895
+ "Dropping malformed `maxFetchRequests` field \u2014 expected non-negative number."
2896
+ );
2897
+ delete valObj.maxFetchRequests;
2898
+ }
2899
+ if ("oauth" in valObj && !isOauthObject(valObj.oauth)) {
2900
+ logWarn(
2901
+ { route: "/api/servers", id, droppedKey: "oauth" },
2902
+ "Dropping malformed `oauth` field \u2014 expected `{ clientId?, clientSecret?, scopes?, enterpriseManaged? }`."
2903
+ );
2904
+ delete valObj.oauth;
2905
+ }
2906
+ if ("roots" in valObj && !isRootArray(valObj.roots)) {
2907
+ logWarn(
2908
+ { route: "/api/servers", id, droppedKey: "roots" },
2909
+ "Dropping malformed `roots` field \u2014 expected `Array<{ uri: string, name?: string }>`."
2910
+ );
2911
+ delete valObj.roots;
2912
+ }
2913
+ if ("protocolEra" in valObj && !isProtocolEra(valObj.protocolEra)) {
2914
+ logWarn(
2915
+ { route: "/api/servers", id, droppedKey: "protocolEra" },
2916
+ "Dropping malformed `protocolEra` field \u2014 expected 'legacy', 'auto', or 'modern'."
2917
+ );
2918
+ delete valObj.protocolEra;
2919
+ }
2920
+ if ("modernLogLevel" in valObj && !isModernLogLevel(valObj.modernLogLevel)) {
2921
+ logWarn(
2922
+ { route: "/api/servers", id, droppedKey: "modernLogLevel" },
2923
+ "Dropping malformed `modernLogLevel` field \u2014 expected 'off' or a logging level."
2924
+ );
2925
+ delete valObj.modernLogLevel;
2926
+ }
2927
+ out[id] = normalizeServerType(
2928
+ valObj
2929
+ );
2930
+ }
2931
+ return out;
2932
+ };
2933
+ const SMUGGLE_GUARDED_KEYS = /* @__PURE__ */ new Set([
2934
+ ...INSPECTOR_FIELD_KEYS,
2935
+ "settings"
2936
+ ]);
2937
+ const buildStoredEntry = (id, config, settings) => {
2938
+ const configObj = config !== null && typeof config === "object" ? config : {};
2939
+ const smuggled = [];
2940
+ const configOnly = {};
2941
+ for (const [k, v] of Object.entries(configObj)) {
2942
+ if (SMUGGLE_GUARDED_KEYS.has(k)) {
2943
+ smuggled.push(k);
2944
+ continue;
2945
+ }
2946
+ configOnly[k] = v;
2947
+ }
2948
+ if (smuggled.length > 0) {
2949
+ logWarn(
2950
+ { route: "/api/servers", id, smuggledKeys: smuggled },
2951
+ "Stripping Inspector-extension keys from request body's `config` \u2014 those must travel through the top-level `settings` field, not nested inside `config`."
2952
+ );
2953
+ }
2954
+ const normalized = normalizeServerType(
2955
+ configOnly
2956
+ );
2957
+ if (settings !== void 0) {
2958
+ Object.assign(normalized, inspectorSettingsToStoredFields(settings));
2959
+ }
2960
+ return normalized;
2961
+ };
2962
+ const applyStdioSettingsToConfig = (entry, settings, provided) => {
2963
+ if (!(entry.type === "stdio" || entry.type === void 0)) return;
2964
+ const stdio = entry;
2965
+ if (provided.env) {
2966
+ const env = envPairsToRecord(settings.env);
2967
+ if (Object.keys(env).length > 0) {
2968
+ stdio.env = env;
2969
+ } else {
2970
+ delete stdio.env;
2971
+ }
2972
+ }
2973
+ if (provided.cwd) {
2974
+ const cwd = settings.cwd?.trim();
2975
+ if (cwd) {
2976
+ stdio.cwd = cwd;
2977
+ } else {
2978
+ delete stdio.cwd;
2979
+ }
2980
+ }
2981
+ };
2982
+ const validateSettings = (raw) => {
2983
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
2984
+ return { ok: false, error: "settings must be an object" };
2985
+ }
2986
+ const obj = raw;
2987
+ const isKvArray2 = (v) => {
2988
+ if (!Array.isArray(v)) return false;
2989
+ return v.every(
2990
+ (e) => e !== null && typeof e === "object" && typeof e.key === "string" && typeof e.value === "string"
2991
+ );
2992
+ };
2993
+ const isBooleanRecord = (v) => {
2994
+ if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
2995
+ return Object.values(v).every(
2996
+ (flag) => typeof flag === "boolean"
2997
+ );
2998
+ };
2999
+ if (!isKvArray2(obj.headers)) {
3000
+ return {
3001
+ ok: false,
3002
+ error: "settings.headers must be an array of { key, value }"
3003
+ };
3004
+ }
3005
+ if (!isKvArray2(obj.metadata)) {
3006
+ return {
3007
+ ok: false,
3008
+ error: "settings.metadata must be an array of { key, value }"
3009
+ };
3010
+ }
3011
+ if (obj.env !== void 0 && !isKvArray2(obj.env)) {
3012
+ return {
3013
+ ok: false,
3014
+ error: "settings.env must be an array of { key, value }"
3015
+ };
3016
+ }
3017
+ if (obj.cwd !== void 0 && typeof obj.cwd !== "string") {
3018
+ return { ok: false, error: "settings.cwd must be a string" };
3019
+ }
3020
+ if (typeof obj.connectionTimeout !== "number" || obj.connectionTimeout < 0) {
3021
+ return {
3022
+ ok: false,
3023
+ error: "settings.connectionTimeout must be a non-negative number"
3024
+ };
3025
+ }
3026
+ if (typeof obj.requestTimeout !== "number" || obj.requestTimeout < 0) {
3027
+ return {
3028
+ ok: false,
3029
+ error: "settings.requestTimeout must be a non-negative number"
3030
+ };
3031
+ }
3032
+ if (obj.taskTtl !== void 0 && (typeof obj.taskTtl !== "number" || obj.taskTtl < 0)) {
3033
+ return {
3034
+ ok: false,
3035
+ error: "settings.taskTtl must be a non-negative number"
3036
+ };
3037
+ }
3038
+ if (obj.autoRefreshOnListChanged !== void 0 && typeof obj.autoRefreshOnListChanged !== "boolean") {
3039
+ return {
3040
+ ok: false,
3041
+ error: "settings.autoRefreshOnListChanged must be a boolean"
3042
+ };
3043
+ }
3044
+ if (obj.paginatedLists !== void 0 && typeof obj.paginatedLists !== "boolean") {
3045
+ return {
3046
+ ok: false,
3047
+ error: "settings.paginatedLists must be a boolean"
3048
+ };
3049
+ }
3050
+ if (obj.maxFetchRequests !== void 0 && (typeof obj.maxFetchRequests !== "number" || obj.maxFetchRequests < 0)) {
3051
+ return {
3052
+ ok: false,
3053
+ error: "settings.maxFetchRequests must be a non-negative number"
3054
+ };
3055
+ }
3056
+ for (const optional of [
3057
+ "oauthClientId",
3058
+ "oauthClientSecret",
3059
+ "oauthScopes"
3060
+ ]) {
3061
+ if (obj[optional] !== void 0 && typeof obj[optional] !== "string") {
3062
+ return { ok: false, error: `settings.${optional} must be a string` };
3063
+ }
3064
+ }
3065
+ if (obj.enterpriseManaged !== void 0 && typeof obj.enterpriseManaged !== "boolean") {
3066
+ return {
3067
+ ok: false,
3068
+ error: "settings.enterpriseManaged must be a boolean"
3069
+ };
3070
+ }
3071
+ if (obj.oauthOnInsufficientScope !== void 0 && obj.oauthOnInsufficientScope !== "reauthorize" && obj.oauthOnInsufficientScope !== "throw") {
3072
+ return {
3073
+ ok: false,
3074
+ error: "settings.oauthOnInsufficientScope must be 'reauthorize' or 'throw'"
3075
+ };
3076
+ }
3077
+ if (obj.roots !== void 0 && !isRootArray(obj.roots)) {
3078
+ return {
3079
+ ok: false,
3080
+ error: "settings.roots must be an array of { uri, name? }"
3081
+ };
3082
+ }
3083
+ if (obj.protocolEra !== void 0 && !isProtocolEra(obj.protocolEra)) {
3084
+ return {
3085
+ ok: false,
3086
+ error: "settings.protocolEra must be 'legacy', 'auto', or 'modern'"
3087
+ };
3088
+ }
3089
+ if (obj.modernLogLevel !== void 0 && !isModernLogLevel(obj.modernLogLevel)) {
3090
+ return {
3091
+ ok: false,
3092
+ error: "settings.modernLogLevel must be 'off' or a logging level (debug\u2026emergency)"
3093
+ };
3094
+ }
3095
+ if (obj.advertisedExtensions !== void 0 && !isBooleanRecord(obj.advertisedExtensions)) {
3096
+ return {
3097
+ ok: false,
3098
+ error: "settings.advertisedExtensions must be an object of boolean flags"
3099
+ };
3100
+ }
3101
+ const value = {
3102
+ headers: obj.headers,
3103
+ // Absent → empty list, matching the read side. The write-through drops
3104
+ // empty-key rows and clears `config.env` when the list is empty.
3105
+ env: isKvArray2(obj.env) ? obj.env : [],
3106
+ metadata: obj.metadata,
3107
+ connectionTimeout: obj.connectionTimeout,
3108
+ requestTimeout: obj.requestTimeout,
3109
+ // Absent → product default, matching the read side
3110
+ // (storedFieldsToInspectorSettings). The default is the omit-sentinel in
3111
+ // inspectorSettingsToStoredFields, so this won't write a spurious taskTtl
3112
+ // to disk for a client that didn't send one.
3113
+ taskTtl: typeof obj.taskTtl === "number" ? obj.taskTtl : DEFAULT_TASK_TTL_MS,
3114
+ // Absent → false, matching the read side. The omit-on-false logic lives
3115
+ // in inspectorSettingsToStoredFields, so a false value writes nothing.
3116
+ autoRefreshOnListChanged: obj.autoRefreshOnListChanged === true,
3117
+ // Absent → false, matching the read side (omit-on-false on the write side).
3118
+ paginatedLists: obj.paginatedLists === true,
3119
+ // Absent → product default, matching the read side. The default is the
3120
+ // omit-sentinel in inspectorSettingsToStoredFields, so a client that
3121
+ // didn't send one writes no spurious maxFetchRequests to disk.
3122
+ maxFetchRequests: typeof obj.maxFetchRequests === "number" ? obj.maxFetchRequests : DEFAULT_MAX_FETCH_REQUESTS,
3123
+ // Absent → empty list, matching the read side
3124
+ // (storedFieldsToInspectorSettings). Empty rows are dropped on the way
3125
+ // to disk by inspectorSettingsToStoredFields, so an empty array here
3126
+ // writes no spurious `roots` field.
3127
+ roots: isRootArray(obj.roots) ? obj.roots : []
3128
+ };
3129
+ if (typeof obj.oauthClientId === "string" && obj.oauthClientId !== "") {
3130
+ value.oauthClientId = obj.oauthClientId;
3131
+ }
3132
+ if (typeof obj.oauthClientSecret === "string" && obj.oauthClientSecret !== "") {
3133
+ value.oauthClientSecret = obj.oauthClientSecret;
3134
+ }
3135
+ if (typeof obj.oauthScopes === "string" && obj.oauthScopes !== "") {
3136
+ value.oauthScopes = obj.oauthScopes;
3137
+ }
3138
+ if (obj.enterpriseManaged === true) {
3139
+ value.enterpriseManaged = true;
3140
+ }
3141
+ if (obj.oauthOnInsufficientScope === "reauthorize" || obj.oauthOnInsufficientScope === "throw") {
3142
+ value.oauthOnInsufficientScope = obj.oauthOnInsufficientScope;
3143
+ }
3144
+ if (isProtocolEra(obj.protocolEra)) {
3145
+ value.protocolEra = obj.protocolEra;
3146
+ }
3147
+ if (isModernLogLevel(obj.modernLogLevel)) {
3148
+ value.modernLogLevel = obj.modernLogLevel;
3149
+ }
3150
+ if (isBooleanRecord(obj.advertisedExtensions) && Object.keys(obj.advertisedExtensions).length > 0) {
3151
+ value.advertisedExtensions = { ...obj.advertisedExtensions };
3152
+ }
3153
+ if (typeof obj.cwd === "string" && obj.cwd !== "") {
3154
+ value.cwd = obj.cwd;
3155
+ }
3156
+ return {
3157
+ ok: true,
3158
+ value,
3159
+ envProvided: obj.env !== void 0,
3160
+ cwdProvided: obj.cwd !== void 0
3161
+ };
3162
+ };
3163
+ let writeQueue = Promise.resolve();
3164
+ const withWriteLock = async (fn) => {
3165
+ const prev = writeQueue;
3166
+ let release = () => {
3167
+ };
3168
+ writeQueue = new Promise((r) => {
3169
+ release = r;
3170
+ });
3171
+ try {
3172
+ await prev;
3173
+ return await fn();
3174
+ } finally {
3175
+ release();
3176
+ }
3177
+ };
3178
+ const readMcpConfig = async () => {
3179
+ const raw = await readStoreFile(mcpConfigPath);
3180
+ if (raw === null) return { mcpServers: {} };
3181
+ const parsed = parseStore(raw);
3182
+ return { mcpServers: normalizeMcpServers(parsed?.mcpServers) };
3183
+ };
3184
+ const writeKeychainEntriesFor = async (id, secrets) => {
3185
+ await Promise.all(
3186
+ Object.entries(secrets).map(
3187
+ ([field, value]) => secretStore.set(id, field, value)
3188
+ )
3189
+ );
3190
+ };
3191
+ const readKeychainEntriesFor = async (id, fields) => {
3192
+ const values = await Promise.all(
3193
+ fields.map(
3194
+ async (field) => [field, await secretStore.get(id, field)]
3195
+ )
3196
+ );
3197
+ const out = {};
3198
+ for (const [field, v] of values) {
3199
+ if (v !== null) out[field] = v;
3200
+ }
3201
+ return out;
3202
+ };
3203
+ const hasPlaintextSecrets = (config) => {
3204
+ for (const stored of Object.values(config.mcpServers)) {
3205
+ const { secrets } = extractSecretsFromStored(stored);
3206
+ if (Object.keys(secrets).length > 0) return true;
3207
+ }
3208
+ return false;
3209
+ };
3210
+ const migratePlaintextSecrets = async (config) => {
3211
+ let changed = false;
3212
+ const next = { mcpServers: {} };
3213
+ try {
3214
+ for (const [id, stored] of Object.entries(config.mcpServers)) {
3215
+ const { stripped, secrets } = extractSecretsFromStored(stored);
3216
+ if (Object.keys(secrets).length === 0) {
3217
+ next.mcpServers[id] = stored;
3218
+ continue;
3219
+ }
3220
+ for (const [field, value] of Object.entries(secrets)) {
3221
+ const existing = await secretStore.get(id, field);
3222
+ if (existing === null) {
3223
+ await secretStore.set(id, field, value);
3224
+ }
3225
+ }
3226
+ next.mcpServers[id] = stripped;
3227
+ changed = true;
3228
+ }
3229
+ } catch (err) {
3230
+ if (err instanceof KeychainUnavailableError) {
3231
+ if (fileLogger) {
3232
+ fileLogger.warn(
3233
+ { err: err.message },
3234
+ "Keychain unavailable; skipping plaintext-secret migration on this read. Existing mcp.json plaintext values are preserved."
3235
+ );
3236
+ }
3237
+ return { migrated: config, changed: false };
3238
+ }
3239
+ throw err;
3240
+ }
3241
+ return { migrated: next, changed };
3242
+ };
3243
+ const rehydrateConfig = async (config) => {
3244
+ const out = { mcpServers: {} };
3245
+ for (const [id, stored] of Object.entries(config.mcpServers)) {
3246
+ const fields = expectedSecretFields(stored);
3247
+ const secrets = await readKeychainEntriesFor(id, fields);
3248
+ out.mcpServers[id] = mergeSecretsIntoStored(stored, secrets);
3249
+ }
3250
+ return out;
3251
+ };
3252
+ const computeObsoleteFields = (previousFields, nextSecrets) => {
3253
+ const nextFieldSet = new Set(Object.keys(nextSecrets));
3254
+ const obsolete = [];
3255
+ for (const field of previousFields) {
3256
+ if (!nextFieldSet.has(field)) obsolete.push(field);
3257
+ }
3258
+ return obsolete;
3259
+ };
3260
+ const mergeRenameKeychainSecrets = (stripped, keychainSecrets, nextSecrets) => {
3261
+ const allowedFields = new Set(expectedSecretFields(stripped));
3262
+ const merged = { ...keychainSecrets, ...nextSecrets };
3263
+ const out = {};
3264
+ for (const [field, value] of Object.entries(merged)) {
3265
+ if (allowedFields.has(field)) out[field] = value;
3266
+ }
3267
+ return out;
3268
+ };
3269
+ const deleteKeychainFields = async (id, fields) => {
3270
+ await Promise.all(fields.map((field) => secretStore.delete(id, field)));
3271
+ };
3272
+ const keychainErrorResponse = (c, err) => {
3273
+ if (err instanceof KeychainUnavailableError) {
3274
+ return c.json({ error: err.message }, 503);
3275
+ }
3276
+ return void 0;
3277
+ };
3278
+ const requireWritable = (c) => {
3279
+ if (writable) return void 0;
3280
+ return c.json({ error: "Server list is read-only for this session." }, 403);
3281
+ };
3282
+ app.get("/api/servers", async (c) => {
3283
+ try {
3284
+ if (inMemoryServers) {
3285
+ const normalized = {
3286
+ mcpServers: normalizeMcpServers(inMemoryServers.mcpServers)
3287
+ };
3288
+ return c.json(await rehydrateConfig(normalized));
3289
+ }
3290
+ const raw = await readStoreFile(mcpConfigPath);
3291
+ if (raw !== null) {
3292
+ const parsed = parseStore(raw);
3293
+ const onDisk = {
3294
+ mcpServers: normalizeMcpServers(parsed?.mcpServers)
3295
+ };
3296
+ if (!writable) {
3297
+ return c.json(await rehydrateConfig(onDisk));
3298
+ }
3299
+ if (!hasPlaintextSecrets(onDisk)) {
3300
+ return c.json(await rehydrateConfig(onDisk));
3301
+ }
3302
+ } else if (!writable) {
3303
+ return c.json({ mcpServers: {} });
3304
+ }
3305
+ const settled = await withWriteLock(async () => {
3306
+ const rawInside = await readStoreFile(mcpConfigPath);
3307
+ if (rawInside === null) {
3308
+ await writeMcpAndTrackMtime(serializeStore(DEFAULT_SEED_CONFIG));
3309
+ return DEFAULT_SEED_CONFIG;
3310
+ }
3311
+ const parsedInside = parseStore(rawInside);
3312
+ const inside = {
3313
+ mcpServers: normalizeMcpServers(parsedInside?.mcpServers)
3314
+ };
3315
+ if (!hasPlaintextSecrets(inside)) return inside;
3316
+ const { migrated, changed } = await migratePlaintextSecrets(inside);
3317
+ if (changed) {
3318
+ await writeMcpAndTrackMtime(serializeStore(migrated));
3319
+ }
3320
+ return migrated;
3321
+ });
3322
+ return c.json(await rehydrateConfig(settled));
3323
+ } catch (error) {
3324
+ const keychainResp = keychainErrorResponse(c, error);
3325
+ if (keychainResp) return keychainResp;
3326
+ const msg = error instanceof Error ? error.message : String(error);
3327
+ return c.json({ error: `Failed to read server list: ${msg}` }, 500);
3328
+ }
3329
+ });
3330
+ app.get("/api/import-source", (c) => {
3331
+ const type = c.req.query("type") ?? "";
3332
+ const result = resolveImportSource(
3333
+ type,
3334
+ process.platform,
3335
+ homedir(),
3336
+ (path3) => existsSync2(path3) ? readFileSync4(path3, "utf-8") : null
3337
+ );
3338
+ if (!result) {
3339
+ return c.json(
3340
+ { error: `Unknown import source: ${type || "(none)"}` },
3341
+ 400
3342
+ );
3343
+ }
3344
+ return c.json(result);
3345
+ });
3346
+ app.post("/api/servers", async (c) => {
3347
+ const ro = requireWritable(c);
3348
+ if (ro) return ro;
3349
+ let body;
3350
+ try {
3351
+ body = await c.req.json();
3352
+ } catch {
3353
+ return c.json({ error: "Invalid JSON body" }, 400);
3354
+ }
3355
+ if (typeof body.id !== "string" || !validateStoreId(body.id)) {
3356
+ return c.json(
3357
+ {
3358
+ error: "Invalid id: must be non-empty and contain only alphanumeric, hyphen, or underscore"
3359
+ },
3360
+ 400
3361
+ );
3362
+ }
3363
+ if (!body.config || typeof body.config !== "object") {
3364
+ return c.json({ error: "Missing or invalid config" }, 400);
3365
+ }
3366
+ let postSettings;
3367
+ if (body.settings !== void 0 && body.settings !== null) {
3368
+ const validated = validateSettings(body.settings);
3369
+ if (!validated.ok) return c.json({ error: validated.error }, 400);
3370
+ postSettings = validated.value;
3371
+ }
3372
+ const id = body.id;
3373
+ try {
3374
+ return await withWriteLock(async () => {
3375
+ const current = await readMcpConfig();
3376
+ if (id in current.mcpServers) {
3377
+ return c.json({ error: `Server '${id}' already exists` }, 409);
3378
+ }
3379
+ const built = buildStoredEntry(id, body.config, postSettings);
3380
+ const { stripped, secrets } = extractSecretsFromStored(built);
3381
+ await secretStore.deleteAllForServer(id);
3382
+ await writeKeychainEntriesFor(id, secrets);
3383
+ current.mcpServers[id] = stripped;
3384
+ await writeMcpAndTrackMtime(serializeStore(current));
3385
+ return c.json({ ok: true });
3386
+ });
3387
+ } catch (error) {
3388
+ const keychainResp = keychainErrorResponse(c, error);
3389
+ if (keychainResp) return keychainResp;
3390
+ const msg = error instanceof Error ? error.message : String(error);
3391
+ return c.json({ error: `Failed to add server: ${msg}` }, 500);
3392
+ }
3393
+ });
3394
+ app.put("/api/servers/order", async (c) => {
3395
+ const ro = requireWritable(c);
3396
+ if (ro) return ro;
3397
+ let body;
3398
+ try {
3399
+ body = await c.req.json();
3400
+ } catch {
3401
+ return c.json({ error: "Invalid JSON body" }, 400);
3402
+ }
3403
+ if (!Array.isArray(body.order) || !body.order.every((id) => typeof id === "string")) {
3404
+ return c.json({ error: "order must be an array of strings" }, 400);
3405
+ }
3406
+ const order = body.order;
3407
+ if (new Set(order).size !== order.length) {
3408
+ return c.json({ error: "order contains duplicate ids" }, 400);
3409
+ }
3410
+ try {
3411
+ return await withWriteLock(async () => {
3412
+ const current = await readMcpConfig();
3413
+ const currentIds = Object.keys(current.mcpServers);
3414
+ const currentSet = new Set(currentIds);
3415
+ const sameSet = currentIds.length === order.length && order.every((id) => currentSet.has(id));
3416
+ if (!sameSet) {
3417
+ return c.json(
3418
+ {
3419
+ error: "order does not match the current server set (it may have changed on disk)"
3420
+ },
3421
+ 409
3422
+ );
3423
+ }
3424
+ const next = { mcpServers: {} };
3425
+ for (const id of order) {
3426
+ next.mcpServers[id] = current.mcpServers[id];
3427
+ }
3428
+ await writeMcpAndTrackMtime(serializeStore(next));
3429
+ return c.json({ ok: true });
3430
+ });
3431
+ } catch (error) {
3432
+ const msg = error instanceof Error ? error.message : String(error);
3433
+ return c.json({ error: `Failed to reorder servers: ${msg}` }, 500);
3434
+ }
3435
+ });
3436
+ app.put("/api/servers/:id", async (c) => {
3437
+ const ro = requireWritable(c);
3438
+ if (ro) return ro;
3439
+ const originalId = c.req.param("id");
3440
+ if (!originalId || !validateStoreId(originalId)) {
3441
+ return c.json({ error: "Invalid id" }, 400);
3442
+ }
3443
+ let body;
3444
+ try {
3445
+ body = await c.req.json();
3446
+ } catch {
3447
+ return c.json({ error: "Invalid JSON body" }, 400);
3448
+ }
3449
+ if (body.config !== void 0 && (body.config === null || typeof body.config !== "object")) {
3450
+ return c.json({ error: "Invalid config" }, 400);
3451
+ }
3452
+ let settingsIntent;
3453
+ if (body.settings === void 0) {
3454
+ settingsIntent = { kind: "preserve" };
3455
+ } else if (body.settings === null) {
3456
+ settingsIntent = { kind: "clear" };
3457
+ } else {
3458
+ const validated = validateSettings(body.settings);
3459
+ if (!validated.ok) return c.json({ error: validated.error }, 400);
3460
+ settingsIntent = {
3461
+ kind: "apply",
3462
+ value: validated.value,
3463
+ envProvided: validated.envProvided,
3464
+ cwdProvided: validated.cwdProvided
3465
+ };
3466
+ }
3467
+ const newId = typeof body.id === "string" ? body.id : originalId;
3468
+ if (!validateStoreId(newId)) {
3469
+ return c.json({ error: "Invalid new id" }, 400);
3470
+ }
3471
+ try {
3472
+ return await withWriteLock(async () => {
3473
+ const current = await readMcpConfig();
3474
+ if (!(originalId in current.mcpServers)) {
3475
+ return c.json({ error: `Server '${originalId}' not found` }, 404);
3476
+ }
3477
+ if (newId !== originalId && newId in current.mcpServers) {
3478
+ return c.json({ error: `Server '${newId}' already exists` }, 409);
3479
+ }
3480
+ const existing = current.mcpServers[originalId];
3481
+ if (!existing) {
3482
+ return c.json({ error: `Server '${originalId}' not found` }, 404);
3483
+ }
3484
+ const existingConfig = stripInspectorFields(existing);
3485
+ const existingSettings = storedFieldsToInspectorSettings(existing);
3486
+ const nextConfig = body.config !== void 0 ? body.config : existingConfig;
3487
+ let nextSettings;
3488
+ switch (settingsIntent.kind) {
3489
+ case "preserve":
3490
+ nextSettings = existingSettings;
3491
+ break;
3492
+ case "clear":
3493
+ nextSettings = void 0;
3494
+ break;
3495
+ case "apply":
3496
+ nextSettings = settingsIntent.value;
3497
+ break;
3498
+ }
3499
+ const built = buildStoredEntry(newId, nextConfig, nextSettings);
3500
+ if (body.config === void 0 && settingsIntent.kind === "apply") {
3501
+ applyStdioSettingsToConfig(built, settingsIntent.value, {
3502
+ env: settingsIntent.envProvided,
3503
+ cwd: settingsIntent.cwdProvided
3504
+ });
3505
+ if (!settingsIntent.envProvided && (built.type === "stdio" || built.type === void 0)) {
3506
+ const stdio = built;
3507
+ if (stdio.env && Object.keys(stdio.env).length > 0) {
3508
+ const keys = Object.keys(stdio.env);
3509
+ const existingSecrets = await readKeychainEntriesFor(
3510
+ originalId,
3511
+ keys.map((k) => envSecretField(k))
3512
+ );
3513
+ const rehydrated = { ...stdio.env };
3514
+ for (const k of keys) {
3515
+ const v = existingSecrets[envSecretField(k)];
3516
+ if (v !== void 0) rehydrated[k] = v;
3517
+ }
3518
+ stdio.env = rehydrated;
3519
+ }
3520
+ }
3521
+ }
3522
+ const { stripped, secrets } = extractSecretsFromStored(built);
3523
+ const next = { mcpServers: {} };
3524
+ for (const [key, val] of Object.entries(current.mcpServers)) {
3525
+ if (key === originalId) {
3526
+ next.mcpServers[newId] = stripped;
3527
+ } else {
3528
+ next.mcpServers[key] = val;
3529
+ }
3530
+ }
3531
+ if (newId !== originalId) {
3532
+ const previousFields = expectedSecretFields(existing);
3533
+ const keychainSecrets = await readKeychainEntriesFor(
3534
+ originalId,
3535
+ previousFields
3536
+ );
3537
+ const secretsToWrite = mergeRenameKeychainSecrets(
3538
+ stripped,
3539
+ keychainSecrets,
3540
+ secrets
3541
+ );
3542
+ await writeKeychainEntriesFor(newId, secretsToWrite);
3543
+ await writeMcpAndTrackMtime(serializeStore(next));
3544
+ await secretStore.deleteAllForServer(originalId);
3545
+ } else {
3546
+ const previousFields = new Set(expectedSecretFields(existing));
3547
+ const obsolete = computeObsoleteFields(previousFields, secrets);
3548
+ await writeKeychainEntriesFor(newId, secrets);
3549
+ await writeMcpAndTrackMtime(serializeStore(next));
3550
+ await deleteKeychainFields(newId, obsolete);
3551
+ }
3552
+ return c.json({ ok: true });
3553
+ });
3554
+ } catch (error) {
3555
+ const keychainResp = keychainErrorResponse(c, error);
3556
+ if (keychainResp) return keychainResp;
3557
+ const msg = error instanceof Error ? error.message : String(error);
3558
+ return c.json({ error: `Failed to update server: ${msg}` }, 500);
3559
+ }
3560
+ });
3561
+ app.delete("/api/servers/:id", async (c) => {
3562
+ const ro = requireWritable(c);
3563
+ if (ro) return ro;
3564
+ const id = c.req.param("id");
3565
+ if (!id || !validateStoreId(id)) {
3566
+ return c.json({ error: "Invalid id" }, 400);
3567
+ }
3568
+ try {
3569
+ return await withWriteLock(async () => {
3570
+ const current = await readMcpConfig();
3571
+ if (!(id in current.mcpServers)) {
3572
+ await secretStore.deleteAllForServer(id);
3573
+ return c.json({ ok: true });
3574
+ }
3575
+ delete current.mcpServers[id];
3576
+ await writeMcpAndTrackMtime(serializeStore(current));
3577
+ await secretStore.deleteAllForServer(id);
3578
+ return c.json({ ok: true });
3579
+ });
3580
+ } catch (error) {
3581
+ const keychainResp = keychainErrorResponse(c, error);
3582
+ if (keychainResp) return keychainResp;
3583
+ const msg = error instanceof Error ? error.message : String(error);
3584
+ return c.json({ error: `Failed to delete server: ${msg}` }, 500);
3585
+ }
3586
+ });
3587
+ app.get("/api/servers/events", async (c) => {
3588
+ return streamSSE(c, async (stream) => {
3589
+ const send = (data) => {
3590
+ void stream.writeSSE({ event: "change", data });
3591
+ };
3592
+ if (watchable) {
3593
+ serverEventSubscribers.add(send);
3594
+ ensureWatcher();
3595
+ }
3596
+ stream.onAbort(() => {
3597
+ serverEventSubscribers.delete(send);
3598
+ void maybeStopWatcher();
3599
+ stream.close();
3600
+ });
3601
+ await new Promise((resolve6) => {
3602
+ stream.onAbort(() => resolve6());
3603
+ });
3604
+ });
3605
+ });
3606
+ return {
3607
+ app,
3608
+ authToken,
3609
+ close: async () => {
3610
+ serverEventSubscribers.clear();
3611
+ await maybeStopWatcher();
3612
+ }
3613
+ };
3614
+ }
3615
+
3616
+ // server/inject-auth-token.ts
3617
+ function serializeTokenForScript(token) {
3618
+ return JSON.stringify(token).replace(/</g, "\\u003c");
3619
+ }
3620
+ function injectAuthToken(html, token) {
3621
+ if (!token) return html;
3622
+ const script = `<script>window.${INSPECTOR_API_TOKEN_GLOBAL} = ${serializeTokenForScript(
3623
+ token
3624
+ )};</script>`;
3625
+ const headClose = html.indexOf("</head>");
3626
+ if (headClose !== -1) {
3627
+ return html.slice(0, headClose) + script + html.slice(headClose);
3628
+ }
3629
+ const bodyClose = html.indexOf("</body>");
3630
+ if (bodyClose !== -1) {
3631
+ return html.slice(0, bodyClose) + script + html.slice(bodyClose);
3632
+ }
3633
+ return script + html;
3634
+ }
3635
+
3636
+ // server/vite-hono-plugin.ts
3637
+ function honoMiddlewarePlugin(config) {
3638
+ let resolvedAuthToken = "";
3639
+ return {
3640
+ name: "hono-api-middleware",
3641
+ // Embed the API token into the dev-served index.html so a reload at the
3642
+ // bare URL (no `?MCP_INSPECTOR_API_TOKEN=…`) still authenticates. The
3643
+ // prod server applies the same injection in `server.ts`.
3644
+ transformIndexHtml(html) {
3645
+ return injectAuthToken(html, resolvedAuthToken);
3646
+ },
3647
+ // `apply: 'serve'` keeps the plugin out of `vite build`, but Vitest still
3648
+ // instantiates a Vite server in middleware mode (no HTTP server) for
3649
+ // transforms and invokes `configureServer` regardless. Returning early
3650
+ // when `server.httpServer` is missing keeps the plugin inert in that
3651
+ // context — only an actual `vite dev` (or `vite preview`) instance has
3652
+ // an HTTP server to attach to.
3653
+ apply: "serve",
3654
+ async configureServer(server) {
3655
+ if (process.env.VITEST) {
3656
+ return;
3657
+ }
3658
+ if (!server.httpServer) {
3659
+ return;
3660
+ }
3661
+ const sandboxController = createSandboxController({
3662
+ port: config.sandboxPort,
3663
+ host: config.sandboxHost,
3664
+ allowedOrigins: config.allowedOrigins
3665
+ });
3666
+ await sandboxController.start();
3667
+ const {
3668
+ app: honoApp,
3669
+ authToken: resolvedToken,
3670
+ close: closeApi
3671
+ } = createRemoteApp({
3672
+ authToken: config.dangerouslyOmitAuth ? void 0 : config.authToken,
3673
+ dangerouslyOmitAuth: config.dangerouslyOmitAuth,
3674
+ storageDir: config.storageDir,
3675
+ mcpConfigPath: config.mcpConfigPath,
3676
+ writable: config.writable,
3677
+ initialServers: config.initialServers ?? void 0,
3678
+ allowedOrigins: config.allowedOrigins,
3679
+ sandboxUrl: sandboxController.getUrl() ?? void 0,
3680
+ logger: config.logger,
3681
+ initialConfig: webServerConfigToInitialPayload(config)
3682
+ });
3683
+ resolvedAuthToken = config.dangerouslyOmitAuth ? "" : resolvedToken;
3684
+ const originalClose = server.close.bind(server);
3685
+ server.close = async () => {
3686
+ await closeApi();
3687
+ await sandboxController.close();
3688
+ return originalClose();
3689
+ };
3690
+ const sandboxUrl = sandboxController.getUrl();
3691
+ const logBanner = () => {
3692
+ const address = server.httpServer?.address();
3693
+ const actualPort = typeof address === "object" && address !== null ? address.port : config.port;
3694
+ const url = printServerBanner(
3695
+ config,
3696
+ actualPort,
3697
+ resolvedToken,
3698
+ sandboxUrl ?? void 0
3699
+ );
3700
+ if (config.autoOpen) {
3701
+ open(url);
3702
+ }
3703
+ };
3704
+ server.httpServer.once("listening", () => {
3705
+ setImmediate(async () => {
3706
+ try {
3707
+ await server.warmupRequest("/src/main.tsx");
3708
+ } catch (err) {
3709
+ console.warn("[vite] entry warmup failed:", err);
3710
+ }
3711
+ logBanner();
3712
+ });
3713
+ });
3714
+ const honoMiddleware = async (req, res, next) => {
3715
+ try {
3716
+ const pathname = req.url || "";
3717
+ if (!pathname.startsWith("/api")) {
3718
+ return next();
3719
+ }
3720
+ const url = `http://${req.headers.host}${pathname}`;
3721
+ const headers = new Headers();
3722
+ Object.entries(req.headers).forEach(
3723
+ ([key, value]) => {
3724
+ if (value) {
3725
+ headers.set(
3726
+ key,
3727
+ Array.isArray(value) ? value.join(", ") : value
3728
+ );
3729
+ }
3730
+ }
3731
+ );
3732
+ const init = { method: req.method, headers };
3733
+ if (req.method !== "GET" && req.method !== "HEAD") {
3734
+ const chunks = [];
3735
+ req.on("data", (chunk) => chunks.push(chunk));
3736
+ await new Promise((resolve6) => {
3737
+ req.once("end", () => resolve6());
3738
+ req.once("error", () => resolve6());
3739
+ req.once("close", () => resolve6());
3740
+ });
3741
+ if (chunks.length > 0) {
3742
+ init.body = Buffer.concat(chunks);
3743
+ }
3744
+ }
3745
+ const response = await honoApp.fetch(new Request(url, init));
3746
+ res.statusCode = response.status;
3747
+ response.headers.forEach((value, key) => {
3748
+ res.setHeader(key, value);
3749
+ });
3750
+ const isSSE = response.headers.get("content-type")?.includes("text/event-stream");
3751
+ if (isSSE) {
3752
+ res.setHeader("X-Accel-Buffering", "no");
3753
+ res.setHeader("Cache-Control", "no-cache");
3754
+ res.setHeader("Connection", "keep-alive");
3755
+ }
3756
+ if (response.body) {
3757
+ res.flushHeaders?.();
3758
+ const reader = response.body.getReader();
3759
+ const isDisconnect = (code) => code === "ERR_STREAM_DESTROYED" || code === "EPIPE" || code === "ECONNRESET";
3760
+ let clientGone = false;
3761
+ const onClose = () => {
3762
+ clientGone = true;
3763
+ reader.cancel().catch(() => {
3764
+ });
3765
+ };
3766
+ const onError = (err) => {
3767
+ clientGone = true;
3768
+ reader.cancel().catch(() => {
3769
+ });
3770
+ if (!isDisconnect(err.code)) {
3771
+ console.error("[Hono Middleware] Response error:", err);
3772
+ }
3773
+ };
3774
+ res.on("close", onClose);
3775
+ res.on("error", onError);
3776
+ const pump = async () => {
3777
+ try {
3778
+ while (true) {
3779
+ const { done, value } = await reader.read();
3780
+ if (done || clientGone || res.destroyed) break;
3781
+ await new Promise((resolve6, reject) => {
3782
+ res.write(
3783
+ Buffer.from(value),
3784
+ (err) => err ? reject(err) : resolve6()
3785
+ );
3786
+ });
3787
+ }
3788
+ } catch (err) {
3789
+ const code = err?.code;
3790
+ if (!clientGone && !res.destroyed && !isDisconnect(code)) {
3791
+ console.error("[Hono Middleware] Stream error:", err);
3792
+ }
3793
+ } finally {
3794
+ res.off("close", onClose);
3795
+ res.off("error", onError);
3796
+ await reader.cancel().catch(() => {
3797
+ });
3798
+ if (!res.writableEnded && !res.destroyed) {
3799
+ res.end();
3800
+ }
3801
+ }
3802
+ };
3803
+ void pump();
3804
+ } else {
3805
+ res.end();
3806
+ }
3807
+ } catch (error) {
3808
+ next(error);
3809
+ }
3810
+ };
3811
+ server.middlewares.use(honoMiddleware);
3812
+ }
3813
+ };
3814
+ }
3815
+
3816
+ // server/vite-base-config.ts
3817
+ import { rmSync } from "fs";
3818
+ import { join as join5 } from "path";
3819
+ var NODE_ONLY_OPTIMIZE_DEPS_EXCLUDE = [
3820
+ "@modelcontextprotocol/client/stdio",
3821
+ // `atomically` is reached only through `core/storage/store-io.ts`,
3822
+ // which is imported by `core/mcp/remote/node/server.ts` (the Hono
3823
+ // app). The module never lands in the browser graph; excluding it
3824
+ // keeps Vite's dev-time scanner from chasing it through the plugin's
3825
+ // node-only import chain.
3826
+ "atomically",
3827
+ // `chokidar` is only loaded inside `core/mcp/remote/node/server.ts`
3828
+ // when the lazy mcp.json watcher starts. It transitively imports
3829
+ // `readdirp` and core node fs/os modules; excluding it keeps Vite's
3830
+ // dep scanner from walking into them during dev startup.
3831
+ "chokidar",
3832
+ "cross-spawn",
3833
+ "which",
3834
+ // `@napi-rs/keyring` is loaded only inside
3835
+ // `core/auth/node/secret-store.ts` from the Hono `/api/servers`
3836
+ // handlers. It's a native-binding package (no browser code path) so
3837
+ // excluding it keeps Vite's dep scanner from chasing into the
3838
+ // platform-specific binaries during dev startup.
3839
+ "@napi-rs/keyring"
3840
+ ];
3841
+ function getViteDevOptimizeDeps() {
3842
+ return {
3843
+ exclude: [...NODE_ONLY_OPTIMIZE_DEPS_EXCLUDE],
3844
+ force: true,
3845
+ ignoreOutdatedRequests: true,
3846
+ include: ["ajv", "@modelcontextprotocol/client/validators/ajv"]
3847
+ };
3848
+ }
3849
+ function clearViteDepsCache(clientWebRoot) {
3850
+ rmSync(join5(clientWebRoot, "node_modules", ".vite"), {
3851
+ recursive: true,
3852
+ force: true
3853
+ });
3854
+ }
3855
+
3856
+ // ../../vitest.shared.mts
3857
+ import path2 from "path";
3858
+ import { fileURLToPath as fileURLToPath3 } from "url";
3859
+ function vitestSharedPaths(clientDir) {
3860
+ const dirname8 = path2.resolve(clientDir);
3861
+ const repoRoot = path2.resolve(dirname8, "../..");
3862
+ const sharedAliases = {
3863
+ "@inspector/core": path2.resolve(repoRoot, "core"),
3864
+ "@modelcontextprotocol/inspector-test-server": path2.resolve(
3865
+ repoRoot,
3866
+ "test-servers/build/index.js"
3867
+ )
3868
+ };
3869
+ const sharedDedupe = [
3870
+ "react",
3871
+ "react-dom",
3872
+ "@modelcontextprotocol/client",
3873
+ "@modelcontextprotocol/core"
3874
+ ];
3875
+ const nodeModulesAliases = [
3876
+ {
3877
+ find: /^react$/,
3878
+ replacement: path2.resolve(dirname8, "node_modules/react")
3879
+ },
3880
+ {
3881
+ find: /^react\/jsx-runtime$/,
3882
+ replacement: path2.resolve(dirname8, "node_modules/react/jsx-runtime.js")
3883
+ },
3884
+ {
3885
+ find: /^react\/jsx-dev-runtime$/,
3886
+ replacement: path2.resolve(
3887
+ dirname8,
3888
+ "node_modules/react/jsx-dev-runtime.js"
3889
+ )
3890
+ },
3891
+ {
3892
+ find: /^react-dom$/,
3893
+ replacement: path2.resolve(dirname8, "node_modules/react-dom")
3894
+ },
3895
+ {
3896
+ find: /^react-dom\/client$/,
3897
+ replacement: path2.resolve(dirname8, "node_modules/react-dom/client.js")
3898
+ },
3899
+ { find: /^pino$/, replacement: path2.resolve(dirname8, "node_modules/pino") },
3900
+ {
3901
+ find: /^pino\/browser\.js$/,
3902
+ replacement: path2.resolve(dirname8, "node_modules/pino/browser.js")
3903
+ },
3904
+ {
3905
+ find: /^hono$/,
3906
+ replacement: path2.resolve(dirname8, "node_modules/hono/dist/index.js")
3907
+ },
3908
+ {
3909
+ find: /^hono\/streaming$/,
3910
+ replacement: path2.resolve(
3911
+ dirname8,
3912
+ "node_modules/hono/dist/helper/streaming/index.js"
3913
+ )
3914
+ },
3915
+ {
3916
+ find: /^@hono\/node-server$/,
3917
+ replacement: path2.resolve(dirname8, "node_modules/@hono/node-server")
3918
+ },
3919
+ {
3920
+ find: /^atomically$/,
3921
+ replacement: path2.resolve(dirname8, "node_modules/atomically")
3922
+ },
3923
+ {
3924
+ find: /^chokidar$/,
3925
+ replacement: path2.resolve(dirname8, "node_modules/chokidar")
3926
+ },
3927
+ {
3928
+ find: /^@napi-rs\/keyring$/,
3929
+ replacement: path2.resolve(dirname8, "node_modules/@napi-rs/keyring")
3930
+ },
3931
+ {
3932
+ find: /^express$/,
3933
+ replacement: path2.resolve(dirname8, "node_modules/express")
3934
+ },
3935
+ {
3936
+ find: /^yaml$/,
3937
+ replacement: path2.resolve(repoRoot, "node_modules/yaml")
3938
+ }
3939
+ ];
3940
+ const projectResolve = {
3941
+ alias: [
3942
+ ...Object.entries(sharedAliases).map(([find, replacement]) => ({
3943
+ find,
3944
+ replacement
3945
+ })),
3946
+ ...nodeModulesAliases
3947
+ ],
3948
+ dedupe: sharedDedupe
3949
+ };
3950
+ return {
3951
+ repoRoot,
3952
+ sharedAliases,
3953
+ sharedDedupe,
3954
+ nodeModulesAliases,
3955
+ projectResolve
3956
+ };
3957
+ }
3958
+
3959
+ // server/start-vite-dev-server.ts
3960
+ var __dirname2 = dirname5(fileURLToPath4(import.meta.url));
3961
+ async function startViteDevServer(config) {
3962
+ const root = resolve3(join6(__dirname2, ".."));
3963
+ clearViteDepsCache(root);
3964
+ const { repoRoot, sharedAliases, sharedDedupe, nodeModulesAliases } = vitestSharedPaths(root);
3965
+ const inlineConfig = {
3966
+ optimizeDeps: getViteDevOptimizeDeps(),
3967
+ configFile: false,
3968
+ root,
3969
+ resolve: {
3970
+ alias: [
3971
+ ...Object.entries(sharedAliases).map(([find, replacement]) => ({
3972
+ find,
3973
+ replacement
3974
+ })),
3975
+ ...nodeModulesAliases
3976
+ ],
3977
+ dedupe: sharedDedupe
3978
+ },
3979
+ server: {
3980
+ port: config.port,
3981
+ host: config.hostname,
3982
+ // `strictPort: true` (matching `vite.config.ts`) so a busy port fails
3983
+ // loudly instead of silently binding a different one — the origin
3984
+ // allow-list and the sandbox `frame-ancestors` are derived from
3985
+ // `config.port`, so a drifted bind would 403 every connect and CSP-block
3986
+ // the MCP Apps iframe while the banner advertises the unusable port.
3987
+ strictPort: true,
3988
+ // Allow Vite to serve source files from the repo root (core/ lives
3989
+ // outside clients/web), matching `vite.config.ts`'s `server.fs.allow`.
3990
+ fs: {
3991
+ allow: [repoRoot]
3992
+ }
3993
+ },
3994
+ plugins: [react(), honoMiddlewarePlugin(config)]
3995
+ };
3996
+ const server = await createServer2(inlineConfig);
3997
+ await server.listen();
3998
+ return {
3999
+ async close() {
4000
+ await server.close();
4001
+ }
4002
+ };
4003
+ }
4004
+
4005
+ // server/server.ts
4006
+ import { readFileSync as readFileSync5 } from "fs";
4007
+ import { join as join7, dirname as dirname6 } from "path";
4008
+ import { fileURLToPath as fileURLToPath5 } from "url";
4009
+ import { randomBytes as randomBytes2 } from "crypto";
4010
+ import open2 from "open";
4011
+ import { serve } from "@hono/node-server";
4012
+ import { serveStatic } from "@hono/node-server/serve-static";
4013
+ import { Hono as Hono2 } from "hono";
4014
+ var __filename = fileURLToPath5(import.meta.url);
4015
+ var __dirname3 = dirname6(__filename);
4016
+ async function startHonoServer(config) {
4017
+ const sandboxController = createSandboxController({
4018
+ port: config.sandboxPort,
4019
+ host: config.sandboxHost,
4020
+ allowedOrigins: config.allowedOrigins
4021
+ });
4022
+ await sandboxController.start();
4023
+ const resolvedAuthToken = config.authToken || (config.dangerouslyOmitAuth ? "" : randomBytes2(32).toString("hex"));
4024
+ const rootPath = config.staticRoot ?? __dirname3;
4025
+ const { app: apiApp, close: closeApi } = createRemoteApp({
4026
+ authToken: config.dangerouslyOmitAuth ? void 0 : resolvedAuthToken,
4027
+ dangerouslyOmitAuth: config.dangerouslyOmitAuth,
4028
+ storageDir: config.storageDir,
4029
+ mcpConfigPath: config.mcpConfigPath,
4030
+ writable: config.writable,
4031
+ initialServers: config.initialServers ?? void 0,
4032
+ allowedOrigins: config.allowedOrigins,
4033
+ sandboxUrl: sandboxController.getUrl() ?? void 0,
4034
+ logger: config.logger,
4035
+ initialConfig: webServerConfigToInitialPayload(config)
4036
+ });
4037
+ const app = new Hono2();
4038
+ app.use("/api/*", async (c) => {
4039
+ return apiApp.fetch(c.req.raw);
4040
+ });
4041
+ const serveIndexHtml = (c) => {
4042
+ const indexPath = join7(rootPath, "index.html");
4043
+ const html = readFileSync5(indexPath, "utf-8");
4044
+ c.header("Cache-Control", "no-store");
4045
+ return c.html(injectAuthToken(html, resolvedAuthToken));
4046
+ };
4047
+ app.get("/", async (c) => {
4048
+ try {
4049
+ return serveIndexHtml(c);
4050
+ } catch (error) {
4051
+ console.error("Error serving index.html:", error);
4052
+ return c.notFound();
4053
+ }
4054
+ });
4055
+ app.use("/*", serveStatic({ root: rootPath }));
4056
+ app.get("*", async (c) => {
4057
+ if (c.req.path.startsWith("/api")) {
4058
+ return c.notFound();
4059
+ }
4060
+ try {
4061
+ return serveIndexHtml(c);
4062
+ } catch (error) {
4063
+ console.error("Error serving index.html:", error);
4064
+ return c.notFound();
4065
+ }
4066
+ });
4067
+ const httpServer = serve(
4068
+ {
4069
+ fetch: app.fetch,
4070
+ port: config.port,
4071
+ hostname: config.hostname
4072
+ },
4073
+ (info) => {
4074
+ const sandboxUrl = sandboxController.getUrl();
4075
+ const url = printServerBanner(
4076
+ config,
4077
+ info.port,
4078
+ resolvedAuthToken,
4079
+ sandboxUrl ?? void 0
4080
+ );
4081
+ if (config.autoOpen) {
4082
+ open2(url);
4083
+ }
4084
+ }
4085
+ );
4086
+ httpServer.on("error", (err) => {
4087
+ if (err.message.includes("EADDRINUSE")) {
4088
+ console.error(
4089
+ `MCP Inspector PORT IS IN USE at http://${formatHostForUrl(config.hostname)}:${config.port}`
4090
+ );
4091
+ process.exit(1);
4092
+ } else {
4093
+ throw err;
4094
+ }
4095
+ });
4096
+ return {
4097
+ async close() {
4098
+ await closeApi();
4099
+ await sandboxController.close();
4100
+ if ("closeAllConnections" in httpServer) {
4101
+ httpServer.closeAllConnections();
4102
+ }
4103
+ await new Promise((resolve6, reject) => {
4104
+ httpServer.close((err) => err ? reject(err) : resolve6());
4105
+ });
4106
+ }
4107
+ };
4108
+ }
4109
+
4110
+ // server/ensure-web-build.ts
4111
+ import { existsSync as existsSync3 } from "fs";
4112
+ import { join as join8 } from "path";
4113
+ import { spawnSync } from "child_process";
4114
+ function runViteBuild(webRoot, log) {
4115
+ const result = spawnSync("npm", ["run", "build:client"], {
4116
+ cwd: webRoot,
4117
+ stdio: "inherit",
4118
+ // npm resolves to npm.cmd on Windows, which needs a shell to be spawnable.
4119
+ shell: process.platform === "win32"
4120
+ });
4121
+ if (result.error) {
4122
+ log(`Web build failed to start: ${result.error.message}`);
4123
+ }
4124
+ return result.status;
4125
+ }
4126
+ function ensureWebBuild(webRoot, distRoot, deps = {}) {
4127
+ const exists = deps.exists ?? existsSync3;
4128
+ const build = deps.build ?? runViteBuild;
4129
+ const log = deps.log ?? ((message) => console.log(message));
4130
+ const indexHtml = join8(distRoot, "index.html");
4131
+ if (exists(indexHtml)) return;
4132
+ log(
4133
+ "No production web build found at clients/web/dist. Building the web UI now (first run only; this can take a minute)..."
4134
+ );
4135
+ const status = build(webRoot, log);
4136
+ if (status !== 0 || !exists(indexHtml)) {
4137
+ throw new Error(
4138
+ "Could not build the web UI automatically. Build it manually with `npm run build` (run from clients/web), or launch with `--dev` to use the Vite dev server."
4139
+ );
4140
+ }
4141
+ }
4142
+
4143
+ // server/run-web.ts
4144
+ var __dirname4 = dirname7(fileURLToPath6(import.meta.url));
4145
+ function ensureStdioCwd(config) {
4146
+ if ((config.type === "stdio" || config.type === void 0) && config.command && !config.cwd) {
4147
+ return { ...config, cwd: resolve4(process.cwd()) };
4148
+ }
4149
+ return config;
4150
+ }
4151
+ function deriveSeedServerId(config) {
4152
+ if (config.type === "sse" || config.type === "streamable-http") {
4153
+ try {
4154
+ return new URL(config.url).host || "server";
4155
+ } catch {
4156
+ return "server";
4157
+ }
4158
+ }
4159
+ return config.command?.split(/[\\/]/).pop() || "server";
4160
+ }
4161
+ async function runWeb(argv) {
4162
+ const program = new Command();
4163
+ const argSeparatorIndex = argv.indexOf("--");
4164
+ let preArgs = argv;
4165
+ let postArgs = [];
4166
+ if (argSeparatorIndex !== -1) {
4167
+ preArgs = argv.slice(0, argSeparatorIndex);
4168
+ postArgs = argv.slice(argSeparatorIndex + 1);
4169
+ }
4170
+ program.name("mcp-inspector-web").description("Web UI for MCP Inspector").allowExcessArguments().allowUnknownOption().option(
4171
+ "-e <env>",
4172
+ "environment variables in KEY=VALUE format",
4173
+ parseKeyValuePair,
4174
+ {}
4175
+ ).option("--catalog <path>", "writable catalog file path").option("--config <path>", "read-only session config file path").option("--server <name>", "server name from config file").option("--transport <type>", "transport type (stdio, sse, http)").option("--server-url <url>", "server URL for SSE/HTTP transport").option("--cwd <path>", "working directory for stdio server process").option(
4176
+ "--header <headers...>",
4177
+ 'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)',
4178
+ parseHeaderPair,
4179
+ {}
4180
+ ).option("--dev", "run in development mode (Vite)").parse(preArgs);
4181
+ const opts = program.opts();
4182
+ const args = program.args;
4183
+ const target = [...args, ...postArgs];
4184
+ const isDev = !!opts.dev;
4185
+ const catalogPath = opts.catalog ?? process.env.MCP_CATALOG_PATH;
4186
+ const hasCatalog = !!catalogPath;
4187
+ const hasConfig = !!opts.config;
4188
+ const hasHeaders = !!opts.header && Object.keys(opts.header).length > 0;
4189
+ const hasAdHocServer = target.length > 0 || !!opts.serverUrl || !!opts.transport && opts.transport !== "stdio";
4190
+ const conflict = hasCatalog && hasConfig ? "--catalog and --config are mutually exclusive. --catalog is the writable catalog; --config is a read-only session file." : hasCatalog && hasAdHocServer ? "--catalog cannot be combined with an ad-hoc server URL/command." : hasCatalog && hasHeaders ? "--header cannot be combined with --catalog. Set per-server headers in the catalog file." : hasConfig && hasAdHocServer ? "--config cannot be combined with an ad-hoc server URL/command. --config selects a read-only session file." : hasConfig && hasHeaders ? "--header cannot be combined with --config. Set per-server headers inside the config file instead." : null;
4191
+ if (conflict) {
4192
+ console.error(`Error: ${conflict}`);
4193
+ process.exit(1);
4194
+ }
4195
+ let initialMcpConfig = null;
4196
+ let mcpConfigPath;
4197
+ let writable = true;
4198
+ let initialServers = null;
4199
+ if (hasCatalog) {
4200
+ if (opts.server) {
4201
+ console.warn(
4202
+ "Note: --server has no effect on the web UI yet; it lists every server in the catalog."
4203
+ );
4204
+ }
4205
+ mcpConfigPath = resolve4(process.cwd(), catalogPath);
4206
+ writable = true;
4207
+ } else if (hasConfig) {
4208
+ if (opts.server) {
4209
+ console.warn(
4210
+ "Note: --server has no effect on the web UI yet; it lists every server in the file."
4211
+ );
4212
+ }
4213
+ const resolvedConfigPath = resolve4(process.cwd(), opts.config);
4214
+ try {
4215
+ resolveServerConfigs({ configPath: resolvedConfigPath }, "multi");
4216
+ } catch (err) {
4217
+ const message = err instanceof Error ? err.message : "Could not load config file.";
4218
+ console.error("Error:", message);
4219
+ process.exit(1);
4220
+ }
4221
+ mcpConfigPath = resolvedConfigPath;
4222
+ writable = false;
4223
+ } else if (hasAdHocServer) {
4224
+ let config;
4225
+ try {
4226
+ const serverOptions = {
4227
+ serverName: opts.server,
4228
+ target: target.length > 0 ? target : void 0,
4229
+ transport: opts.transport,
4230
+ serverUrl: opts.serverUrl,
4231
+ cwd: opts.cwd,
4232
+ env: opts.e
4233
+ };
4234
+ const resolved = resolveServerConfigs(serverOptions, "single")[0];
4235
+ if (!resolved) {
4236
+ console.error(
4237
+ "Error: Could not resolve server config. Pass a command/URL, or use --catalog / --config to serve a file."
4238
+ );
4239
+ process.exit(1);
4240
+ }
4241
+ config = ensureStdioCwd(resolved);
4242
+ } catch (err) {
4243
+ const message = err instanceof Error ? err.message : "Could not resolve server config.";
4244
+ console.error("Error:", message);
4245
+ process.exit(1);
4246
+ }
4247
+ if (hasHeaders && (config.type === "stdio" || config.type === void 0)) {
4248
+ console.error(
4249
+ "Error: --header only applies to HTTP/SSE servers; stdio servers take no HTTP headers."
4250
+ );
4251
+ process.exit(1);
4252
+ }
4253
+ initialMcpConfig = config;
4254
+ const entry = hasHeaders && config.type !== "stdio" && config.type !== void 0 ? { ...config, headers: opts.header } : { ...config };
4255
+ initialServers = {
4256
+ mcpServers: { [deriveSeedServerId(config)]: entry }
4257
+ };
4258
+ writable = false;
4259
+ } else if (hasHeaders) {
4260
+ console.error(
4261
+ "Error: --header requires an ad-hoc --server-url or command target."
4262
+ );
4263
+ process.exit(1);
4264
+ }
4265
+ let webConfig;
4266
+ try {
4267
+ webConfig = buildWebServerConfig({
4268
+ initialMcpConfig,
4269
+ mcpConfigPath,
4270
+ writable,
4271
+ initialServers
4272
+ });
4273
+ } catch (err) {
4274
+ const message = err instanceof Error ? err.message : (
4275
+ /* v8 ignore next -- buildWebServerConfig only ever throws Error instances; this fallback is a defensive guard for the impossible non-Error throw. */
4276
+ "Invalid web server configuration."
4277
+ );
4278
+ console.error("Error:", message);
4279
+ process.exit(1);
4280
+ }
4281
+ const webRoot = join9(__dirname4, "..");
4282
+ const distRoot = join9(webRoot, "dist");
4283
+ if (!isDev) {
4284
+ webConfig.staticRoot = distRoot;
4285
+ }
4286
+ console.log(
4287
+ isDev ? "Starting MCP inspector in development mode..." : "Starting MCP inspector..."
4288
+ );
4289
+ let handle;
4290
+ try {
4291
+ if (isDev) {
4292
+ handle = await startViteDevServer(webConfig);
4293
+ } else {
4294
+ ensureWebBuild(webRoot, distRoot);
4295
+ handle = await startHonoServer(webConfig);
4296
+ }
4297
+ } catch (err) {
4298
+ const message = err instanceof Error ? err.message : "Web client failed to start.";
4299
+ console.error("Error:", message);
4300
+ process.exit(1);
4301
+ }
4302
+ const shutdown = () => {
4303
+ void handle.close().then(() => process.exit(0));
4304
+ };
4305
+ process.on("SIGINT", shutdown);
4306
+ process.on("SIGTERM", shutdown);
4307
+ return new Promise(() => {
4308
+ });
4309
+ }
4310
+
4311
+ // src/index.ts
4312
+ var __filename2 = fileURLToPath7(import.meta.url);
4313
+ var isMain = process.argv[1] !== void 0 && resolve5(process.argv[1]) === resolve5(__filename2);
4314
+ if (isMain) {
4315
+ runWeb(process.argv).then((code) => process.exit(code ?? 0)).catch((e) => {
4316
+ console.error(e);
4317
+ process.exit(1);
4318
+ });
4319
+ }
4320
+ export {
4321
+ runWeb
4322
+ };