#!/usr/bin/env node

// src/index.ts
import { resolve as resolve8 } from "path";
import { fileURLToPath as fileURLToPath7 } from "url";

// server/run-web.ts
import { resolve as resolve7, join as join10, dirname as dirname9 } from "path";
import { fileURLToPath as fileURLToPath6 } from "url";
import { Command } from "commander";

// ../../core/mcp/node/config.ts
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { dirname as dirname2, resolve as resolve2 } from "path";

// ../../core/storage/store-io.ts
import * as path from "path";
import * as fs from "fs/promises";
import { readFile, writeFile } from "atomically";

// ../../core/storage/store-id.ts
function validateStoreId(storeId) {
  return /^[a-zA-Z0-9_-]+$/.test(storeId) && storeId.length > 0;
}

// ../../core/storage/store-serialize.ts
function serializeStore(data) {
  return JSON.stringify(data, null, 2);
}
function parseStore(raw) {
  return JSON.parse(raw);
}

// ../../core/storage/store-io.ts
function getDefaultStorageDir() {
  const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
  return path.join(homeDir, ".mcp-inspector", "storage");
}
function getDefaultMcpConfigPath() {
  const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
  return path.join(homeDir, ".mcp-inspector", "mcp.json");
}
function getStoreFilePath(storageDir, storeId) {
  return path.join(storageDir, `${storeId}.json`);
}
async function readStoreFile(filePath) {
  try {
    const data = await readFile(filePath, { encoding: "utf-8" });
    return data;
  } catch (error) {
    const err = error;
    if (err.code === "ENOENT") {
      return null;
    }
    throw error;
  }
}
var pendingWrites = /* @__PURE__ */ new Map();
async function writeStoreFile(filePath, data) {
  const key = path.resolve(filePath);
  const run = async () => {
    const dir = path.dirname(filePath);
    await fs.mkdir(dir, { recursive: true });
    await writeFile(filePath, data, {
      encoding: "utf-8",
      mode: 384
    });
  };
  const prior = pendingWrites.get(key);
  const tracked = (prior ?? Promise.resolve()).catch(() => {
  }).then(run);
  pendingWrites.set(key, tracked);
  try {
    await tracked;
  } finally {
    if (pendingWrites.get(key) === tracked) {
      pendingWrites.delete(key);
    }
  }
}
async function deleteStoreFile(filePath) {
  try {
    await fs.unlink(filePath);
  } catch (error) {
    const err = error;
    if (err.code !== "ENOENT") {
      throw error;
    }
  }
}

// ../../core/mcp/types.ts
var DEFAULT_TASK_TTL_MS = 6e4;
var DEFAULT_MAX_FETCH_REQUESTS = 1e3;
var DEFAULT_PROTOCOL_ERA = "legacy";
var DEFAULT_MODERN_LOG_LEVEL = "debug";
var MODERN_LOG_LEVELS = [
  "off",
  "debug",
  "info",
  "notice",
  "warning",
  "error",
  "critical",
  "alert",
  "emergency"
];
function isModernLogLevel(value) {
  return typeof value === "string" && MODERN_LOG_LEVELS.includes(value);
}

// ../../core/auth/secret-fields.ts
var SECRET_FIELD_OAUTH_CLIENT_SECRET = "oauth-client-secret";
var SECRET_FIELD_IDP_CLIENT_SECRET = "idp-client-secret";
var envSecretField = (envKey) => `env:${envKey}`;

// ../../core/json/jsonUtils.ts
function isSerializableJson(value) {
  if (value === null) return true;
  switch (typeof value) {
    case "string":
    case "boolean":
      return true;
    case "number":
      return Number.isFinite(value);
    case "object":
      return Array.isArray(value) ? value.every(isSerializableJson) : Object.values(value).every(
        isSerializableJson
      );
    default:
      return false;
  }
}
function toRecord(value) {
  return value;
}

// ../../core/mcp/serverList.ts
var VALID_SERVER_TYPES = /* @__PURE__ */ new Set([
  "stdio",
  "sse",
  "streamable-http"
]);
var VALID_PROTOCOL_ERAS = /* @__PURE__ */ new Set([
  "legacy",
  "auto",
  "modern"
]);
function isProtocolEra(value) {
  return typeof value === "string" && VALID_PROTOCOL_ERAS.has(value);
}
function normalizeServerType(config) {
  const type = config.type;
  let normalizedType;
  if (typeof type !== "string") {
    normalizedType = "stdio";
  } else if (type === "http") {
    normalizedType = "streamable-http";
  } else if (VALID_SERVER_TYPES.has(type)) {
    normalizedType = type;
  } else {
    normalizedType = "stdio";
  }
  return { ...config, type: normalizedType };
}
function cleanRoots(roots) {
  if (!Array.isArray(roots)) {
    console.warn("Ignoring `roots`: expected an array, got", typeof roots);
    return [];
  }
  return roots.filter((r) => {
    if (typeof r?.uri !== "string") {
      console.warn("Dropping root without a string `uri`:", r);
      return false;
    }
    return r.uri.trim() !== "";
  }).map((r) => {
    const rawName = r.name;
    if (rawName !== void 0 && typeof rawName !== "string") {
      console.warn("Dropping non-string `name` on root:", r);
    }
    const trimmedName = typeof rawName === "string" ? rawName.trim() : void 0;
    const { name: _name, ...rest } = r;
    return trimmedName ? { ...rest, name: trimmedName } : rest;
  });
}
function envRecordToPairs(env) {
  return env ? Object.entries(env).map(([key, value]) => ({ key, value })) : [];
}
function envPairsToRecord(pairs) {
  const out = {};
  for (const { key, value } of pairs) {
    if (key.trim() === "") continue;
    out[key] = value;
  }
  return out;
}
function stdioConfigFieldsFromSettings(settings) {
  const env = envPairsToRecord(settings.env);
  const cwd = settings.cwd?.trim();
  return {
    env: Object.keys(env).length > 0 ? env : void 0,
    cwd: cwd ? cwd : void 0
  };
}
function normalizeStoredMetadata(metadata) {
  if (metadata === void 0 || metadata === null) return {};
  if (Array.isArray(metadata)) {
    const out = {};
    for (const entry of metadata) {
      if (typeof entry !== "object" || entry === null || typeof entry.key !== "string") {
        console.warn(
          "Ignoring malformed legacy metadata entry (expected `{ key, value }`)."
        );
        continue;
      }
      const { key, value } = entry;
      if (key.trim() === "") continue;
      const resolved2 = value === void 0 ? "" : value;
      if (!keepSerializable(key, resolved2)) continue;
      defineMetadataKey(out, key, resolved2);
    }
    return out;
  }
  if (typeof metadata === "object") {
    const out = {};
    for (const [key, value] of Object.entries(metadata)) {
      if (keepSerializable(key, value)) defineMetadataKey(out, key, value);
    }
    return out;
  }
  console.warn(
    `Ignoring malformed \`metadata\` (expected a JSON object, got ${describeJsonType(metadata)}).`
  );
  return {};
}
function defineMetadataKey(out, key, value) {
  Object.defineProperty(out, key, {
    value,
    writable: true,
    enumerable: true,
    configurable: true
  });
}
function keepSerializable(key, value) {
  if (isSerializableJson(value)) return true;
  console.warn(
    `Ignoring metadata key "${key}" \u2014 its value is not JSON that can be sent (got ${describeJsonType(value)}; note \`1e400\` parses to Infinity).`
  );
  return false;
}
function describeJsonType(value) {
  if (value === null) return "null";
  if (Array.isArray(value)) return "array";
  if (typeof value === "number" && !Number.isFinite(value)) {
    return Number.isNaN(value) ? "NaN" : "a non-finite number";
  }
  return typeof value;
}
function cleanAuthorizationParams(params) {
  if (params === void 0) return void 0;
  if (typeof params !== "object" || params === null || Array.isArray(params)) {
    console.warn(
      "Ignoring `oauth.authorizationParams`: expected an object of string values, got",
      Array.isArray(params) ? "array" : typeof params
    );
    return void 0;
  }
  const out = {};
  for (const [key, value] of Object.entries(params)) {
    if (typeof value !== "string") {
      console.warn(
        `Dropping \`oauth.authorizationParams.${key}\`: expected a string value, got ${typeof value}.`
      );
      continue;
    }
    if (key.trim() === "") continue;
    out[key] = value;
  }
  return Object.keys(out).length > 0 ? out : void 0;
}
function cleanEndpointOverride(value, field) {
  if (value === void 0) return void 0;
  if (typeof value !== "string") {
    console.warn(
      `Ignoring \`oauth.${field}\`: expected a string, got ${typeof value}.`
    );
    return void 0;
  }
  return value.trim() || void 0;
}
function storedFieldsToInspectorSettings(stored) {
  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;
  if (!hasAny) return void 0;
  const headersPairs = stored.headers ? Object.entries(stored.headers).map(([key, value]) => ({ key, value })) : [];
  const settings = {
    headers: headersPairs,
    env: envRecordToPairs(stored.env),
    metadata: normalizeStoredMetadata(stored.metadata),
    connectionTimeout: stored.connectionTimeout ?? 0,
    requestTimeout: stored.requestTimeout ?? 0,
    // Unlike the timeouts (0 = "SDK default"), task TTL has a concrete product
    // default so the form shows it and "Run as task" has a value to send.
    taskTtl: stored.taskTtl ?? DEFAULT_TASK_TTL_MS,
    autoRefreshOnListChanged: stored.autoRefreshOnListChanged ?? false,
    paginatedLists: stored.paginatedLists ?? false,
    // Concrete default like taskTtl (not a 0-sentinel): the form needs a value
    // to render and the log state needs one to size its buffer. An absent
    // on-disk field reads back as the default, which the write side then omits.
    maxFetchRequests: stored.maxFetchRequests ?? DEFAULT_MAX_FETCH_REQUESTS,
    // Defaults to an empty list so the form always has a concrete array to
    // render controlled rows from. An absent on-disk `roots` reads back as
    // `[]`, which `inspectorSettingsToStoredFields` then omits on write.
    roots: stored.roots ?? []
  };
  if (isProtocolEra(stored.protocolEra)) {
    settings.protocolEra = stored.protocolEra;
  }
  if (isModernLogLevel(stored.modernLogLevel)) {
    settings.modernLogLevel = stored.modernLogLevel;
  }
  if (stored.advertisedExtensions && Object.keys(stored.advertisedExtensions).length > 0) {
    settings.advertisedExtensions = { ...stored.advertisedExtensions };
  }
  if (stored.oauth?.clientId) settings.oauthClientId = stored.oauth.clientId;
  if (stored.oauth?.clientSecret)
    settings.oauthClientSecret = stored.oauth.clientSecret;
  if (stored.oauth?.scopes) settings.oauthScopes = stored.oauth.scopes;
  const authorizationParams = cleanAuthorizationParams(
    stored.oauth?.authorizationParams
  );
  if (authorizationParams) {
    settings.oauthAuthorizationParams = envRecordToPairs(authorizationParams);
  }
  const authorizationUrl = cleanEndpointOverride(
    stored.oauth?.authorizationUrl,
    "authorizationUrl"
  );
  if (authorizationUrl) settings.oauthAuthorizationUrl = authorizationUrl;
  const tokenUrl = cleanEndpointOverride(stored.oauth?.tokenUrl, "tokenUrl");
  if (tokenUrl) settings.oauthTokenUrl = tokenUrl;
  if (stored.oauth?.onInsufficientScope) {
    settings.oauthOnInsufficientScope = stored.oauth.onInsufficientScope;
  }
  if (stored.oauth?.enterpriseManaged === true) {
    settings.enterpriseManaged = true;
  }
  if (stored.oauth?.requestRefreshToken === false) {
    settings.oauthRequestRefreshToken = false;
  }
  if (stored.cwd) settings.cwd = stored.cwd;
  return settings;
}
function inspectorSettingsToStoredFields(settings) {
  const out = {};
  const headersRecord = {};
  for (const { key, value } of settings.headers) {
    if (key.trim() === "") continue;
    headersRecord[key] = value;
  }
  if (Object.keys(headersRecord).length > 0) {
    out.headers = headersRecord;
  }
  if (Object.keys(settings.metadata).length > 0) {
    out.metadata = settings.metadata;
  }
  if (settings.connectionTimeout > 0) {
    out.connectionTimeout = settings.connectionTimeout;
  }
  if (settings.requestTimeout > 0) {
    out.requestTimeout = settings.requestTimeout;
  }
  if (settings.taskTtl > 0 && settings.taskTtl !== DEFAULT_TASK_TTL_MS) {
    out.taskTtl = settings.taskTtl;
  }
  if (settings.autoRefreshOnListChanged) {
    out.autoRefreshOnListChanged = true;
  }
  if (settings.paginatedLists) {
    out.paginatedLists = true;
  }
  if (settings.advertisedExtensions && Object.keys(settings.advertisedExtensions).length > 0) {
    out.advertisedExtensions = { ...settings.advertisedExtensions };
  }
  if (settings.protocolEra !== void 0 && settings.protocolEra !== DEFAULT_PROTOCOL_ERA) {
    out.protocolEra = settings.protocolEra;
  }
  if (settings.modernLogLevel !== void 0 && settings.modernLogLevel !== DEFAULT_MODERN_LOG_LEVEL) {
    out.modernLogLevel = settings.modernLogLevel;
  }
  if (settings.maxFetchRequests !== DEFAULT_MAX_FETCH_REQUESTS) {
    out.maxFetchRequests = settings.maxFetchRequests;
  }
  const oauthFields = {};
  if (settings.oauthClientId) oauthFields.clientId = settings.oauthClientId;
  if (settings.oauthClientSecret)
    oauthFields.clientSecret = settings.oauthClientSecret;
  if (settings.oauthScopes) oauthFields.scopes = settings.oauthScopes;
  if (settings.oauthAuthorizationParams) {
    const authParams = envPairsToRecord(settings.oauthAuthorizationParams);
    if (Object.keys(authParams).length > 0) {
      oauthFields.authorizationParams = authParams;
    }
  }
  if (settings.oauthAuthorizationUrl?.trim()) {
    oauthFields.authorizationUrl = settings.oauthAuthorizationUrl.trim();
  }
  if (settings.oauthTokenUrl?.trim()) {
    oauthFields.tokenUrl = settings.oauthTokenUrl.trim();
  }
  if (settings.oauthOnInsufficientScope) {
    oauthFields.onInsufficientScope = settings.oauthOnInsufficientScope;
  }
  if (settings.enterpriseManaged === true) {
    oauthFields.enterpriseManaged = true;
  }
  if (settings.oauthRequestRefreshToken === false) {
    oauthFields.requestRefreshToken = false;
  }
  if (Object.keys(oauthFields).length > 0) {
    out.oauth = oauthFields;
  }
  const rootsFiltered = cleanRoots(settings.roots);
  if (rootsFiltered.length > 0) {
    out.roots = rootsFiltered;
  }
  return out;
}
var INSPECTOR_FIELD_KEY_MAP = {
  headers: true,
  metadata: true,
  protocolEra: true,
  modernLogLevel: true,
  connectionTimeout: true,
  requestTimeout: true,
  taskTtl: true,
  autoRefreshOnListChanged: true,
  paginatedLists: true,
  advertisedExtensions: true,
  maxFetchRequests: true,
  oauth: true,
  roots: true
};
var INSPECTOR_FIELD_KEYS = new Set(
  Object.keys(INSPECTOR_FIELD_KEY_MAP)
);
function stripInspectorFields(stored) {
  const out = { ...stored };
  for (const key of INSPECTOR_FIELD_KEYS) {
    delete out[key];
  }
  return out;
}
var isStdioStored = (stored) => stored.type === "stdio" || stored.type === void 0;
function extractSecretsFromStored(stored) {
  const secrets = {};
  const stripped = { ...stored };
  if (stored.oauth?.clientSecret) {
    secrets[SECRET_FIELD_OAUTH_CLIENT_SECRET] = stored.oauth.clientSecret;
    const { clientSecret: _clientSecret, ...restOauth } = stored.oauth;
    if (Object.keys(restOauth).length > 0) {
      stripped.oauth = restOauth;
    } else {
      delete stripped.oauth;
    }
  }
  if (isStdioStored(stripped)) {
    const env = stripped.env;
    if (env) {
      const newEnv = {};
      for (const [k, v] of Object.entries(env)) {
        if (typeof v === "string" && v.length > 0) {
          secrets[envSecretField(k)] = v;
        }
        newEnv[k] = "";
      }
      stripped.env = newEnv;
    }
  }
  return { stripped, secrets };
}
function mergeSecretsIntoStored(stored, secrets) {
  const out = { ...stored };
  const oauthSecret = secrets[SECRET_FIELD_OAUTH_CLIENT_SECRET];
  if (oauthSecret) {
    out.oauth = { ...out.oauth ?? {}, clientSecret: oauthSecret };
  }
  if (isStdioStored(out) && out.env) {
    const newEnv = { ...out.env };
    let mutated = false;
    for (const k of Object.keys(out.env)) {
      const val = secrets[envSecretField(k)];
      if (val !== void 0) {
        newEnv[k] = val;
        mutated = true;
      }
    }
    if (mutated) {
      out.env = newEnv;
    }
  }
  return out;
}
function expectedSecretFields(stored) {
  const fields = [];
  fields.push(SECRET_FIELD_OAUTH_CLIENT_SECRET);
  if (isStdioStored(stored) && stored.env) {
    for (const k of Object.keys(stored.env)) {
      fields.push(envSecretField(k));
    }
  }
  return fields;
}
var DEFAULT_SEED_CONFIG = {
  mcpServers: {
    "filesystem-server-default": {
      type: "stdio",
      command: "npx",
      args: ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
    },
    "everything-server-default": {
      type: "stdio",
      command: "npx",
      args: ["-y", "@modelcontextprotocol/server-everything"]
    }
  }
};

// ../../core/mcp/node/config.ts
function parseKeyValuePair(value, previous = {}) {
  const parts = value.split("=");
  const key = parts[0] ?? "";
  const val = parts.slice(1).join("=");
  if (!key || val === void 0 || val === "") {
    throw new Error(
      `Invalid parameter format: ${value}. Use key=value format.`
    );
  }
  return { ...previous, [key]: val };
}
function parseHeaderPair(value, previous = {}) {
  const colonIndex = value.indexOf(":");
  if (colonIndex === -1) {
    throw new Error(
      `Invalid header format: ${value}. Use "HeaderName: Value" format.`
    );
  }
  const key = value.slice(0, colonIndex).trim();
  const val = value.slice(colonIndex + 1).trim();
  if (key === "" || val === "") {
    throw new Error(
      `Invalid header format: ${value}. Use "HeaderName: Value" format.`
    );
  }
  return { ...previous, [key]: val };
}
var EMPTY_CATALOG_CONTENT = `${JSON.stringify({ mcpServers: {} }, null, 2)}
`;
function seedEmptyCatalog(resolvedPath) {
  mkdirSync(dirname2(resolvedPath), { recursive: true });
  writeFileSync(resolvedPath, EMPTY_CATALOG_CONTENT, { mode: 384 });
}
function readServerListContent(configPath, writable) {
  const resolvedPath = resolve2(process.cwd(), configPath);
  if (!existsSync(resolvedPath)) {
    if (writable) {
      seedEmptyCatalog(resolvedPath);
      return EMPTY_CATALOG_CONTENT;
    }
    throw new Error(`Config file not found: ${resolvedPath}`);
  }
  return readFileSync(resolvedPath, "utf-8");
}
function loadMcpServersConfig(configPath, writable) {
  try {
    const configContent = readServerListContent(configPath, writable);
    const config = JSON.parse(configContent);
    if (!config.mcpServers) {
      throw new Error("Configuration file must contain an mcpServers element");
    }
    const normalizedServers = {};
    for (const [name, raw] of Object.entries(config.mcpServers)) {
      normalizedServers[name] = normalizeServerType(toRecord(raw));
    }
    return { ...config, mcpServers: normalizedServers };
  } catch (error) {
    if (error instanceof Error) {
      throw new Error(`Error loading configuration: ${error.message}`, {
        cause: error
      });
    }
    throw new Error("Error loading configuration: Unknown error", {
      cause: error
    });
  }
}
function loadServerFromConfig(configPath, serverName, writable) {
  const config = loadMcpServersConfig(configPath, writable);
  if (!config.mcpServers[serverName]) {
    const available = Object.keys(config.mcpServers).join(", ");
    throw new Error(
      `Server '${serverName}' not found in config file. Available servers: ${available}`
    );
  }
  return config.mcpServers[serverName];
}
function buildConfigFromOptions(options) {
  const target = options.target ?? [];
  const first = target[0];
  const rest = target.slice(1);
  const urlFromTarget = first && (first.startsWith("http://") || first.startsWith("https://")) ? first : null;
  const url = urlFromTarget ?? options.serverUrl ?? null;
  if (url) {
    if (rest.length > 0 && urlFromTarget) {
      throw new Error("Arguments cannot be passed to a URL-based MCP server.");
    }
    let transportType;
    const t = options.transport === "http" ? "streamable-http" : options.transport;
    if (t === "sse" || t === "streamable-http") {
      transportType = t;
    } else {
      const u = new URL(url);
      if (u.pathname.endsWith("/mcp")) {
        transportType = "streamable-http";
      } else if (u.pathname.endsWith("/sse")) {
        transportType = "sse";
      } else {
        throw new Error(
          `Transport type not specified and could not be determined from URL: ${url}.`
        );
      }
    }
    if (transportType === "sse") {
      const config3 = { type: "sse", url };
      return config3;
    }
    const config2 = { type: "streamable-http", url };
    return config2;
  }
  if (target.length === 0 || !first) {
    throw new Error(
      "Target is required. Specify a URL or a command to execute."
    );
  }
  if (options.transport && options.transport !== "stdio") {
    throw new Error("Only stdio transport can be used with local commands.");
  }
  const config = { type: "stdio", command: first };
  if (rest.length > 0) config.args = rest;
  if (options.env && Object.keys(options.env).length > 0)
    config.env = options.env;
  if (options.cwd?.trim()) config.cwd = options.cwd.trim();
  return config;
}
function applyOverrides(config, overrides) {
  if (config.type === "stdio") {
    const c = { ...config };
    if (overrides.env && Object.keys(overrides.env).length > 0) {
      c.env = { ...c.env ?? {}, ...overrides.env };
    }
    if (overrides.cwd?.trim()) c.cwd = overrides.cwd.trim();
    return c;
  }
  return config;
}
function hasAdHocServerOptions(options) {
  return options.target != null && options.target.length > 0 || Boolean(options.transport) || Boolean(options.serverUrl?.trim());
}
function resolveServerSource(options) {
  if (options.catalogPath?.trim()) {
    return { path: options.catalogPath, writable: true };
  }
  if (options.configPath?.trim()) {
    return { path: options.configPath, writable: false };
  }
  return null;
}
function resolveServerConfigs(options, mode) {
  const source = resolveServerSource(options);
  const hasAdHoc = hasAdHocServerOptions(options);
  if (mode === "single") {
    if (source && options.serverName) {
      const config = loadServerFromConfig(
        source.path,
        options.serverName,
        source.writable
      );
      return [
        applyOverrides(config, {
          env: options.env,
          cwd: options.cwd
        })
      ];
    }
    if (source && !options.serverName) {
      const mcpConfig = loadMcpServersConfig(source.path, source.writable);
      const servers = Object.keys(mcpConfig.mcpServers);
      if (servers.length === 0)
        throw new Error("No servers found in config file");
      if (servers.length > 1) {
        throw new Error(
          `Multiple servers found in config file. Please specify one with --server. Available servers: ${servers.join(", ")}`
        );
      }
      const serverName = servers[0];
      if (!serverName) throw new Error("No servers found in config file");
      const config = loadServerFromConfig(
        source.path,
        serverName,
        source.writable
      );
      return [
        applyOverrides(config, {
          env: options.env,
          cwd: options.cwd
        })
      ];
    }
    return [buildConfigFromOptions(options)];
  }
  if (mode === "multi") {
    if (source && hasAdHoc) {
      throw new Error(
        "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."
      );
    }
    if (source) {
      const mcpConfig = loadMcpServersConfig(source.path, source.writable);
      const configs = Object.values(mcpConfig.mcpServers).map(
        (c) => applyOverrides({ ...c }, {
          env: options.env,
          cwd: options.cwd
        })
      );
      return configs;
    }
    return [buildConfigFromOptions(options)];
  }
  return [];
}

// server/web-server-config.ts
import open from "open";
import pino from "pino";

// ../../core/mcp/remote/constants.ts
var LEGACY_AUTH_TOKEN_ENV = "MCP_PROXY_AUTH_TOKEN";
var API_SERVER_ENV_VARS = {
  /**
   * Auth token for authenticating requests to the remote API server.
   * Used by the x-mcp-remote-auth header (or Authorization header if changed).
   */
  AUTH_TOKEN: "MCP_INSPECTOR_API_TOKEN"
};
var INSPECTOR_API_TOKEN_GLOBAL = "__INSPECTOR_API_TOKEN__";

// ../../core/auth/secret-storage-info.ts
function secretStorageLabel(info) {
  switch (info.kind) {
    case "keyring":
      return "OS keychain";
    case "file":
      if (info.encryptionUnknown !== void 0) return "File (unreadable)";
      return info.plaintext ? "File (unencrypted)" : "File (encrypted)";
    case "memory":
      return "Memory (this session only)";
  }
}
function secretStorageCaveat(info) {
  if (info.kind === "memory") {
    return "Secrets are not written anywhere and are lost on exit.";
  }
  if (info.looseMode !== void 0) {
    const mode = info.looseMode.toString(8).padStart(4, "0");
    if (info.encryptionUnknown !== void 0) {
      return `The secrets file is mode ${mode}, not 0600, and could not be tightened \u2014 others can copy it, and whether that exposes the secrets depends on encryption this build could not determine (${info.encryptionUnknown}).`;
    }
    return info.plaintext === false ? `The secrets file is mode ${mode}, not 0600, and could not be tightened \u2014 others can copy it, and the passphrase is then the only thing protecting its contents.` : `The secrets file is mode ${mode}, not 0600, and could not be tightened \u2014 anyone who can read it can read the secrets in it.`;
  }
  if (info.permissionsUnknown !== void 0) {
    return `The secrets file's permissions could not be checked (${info.permissionsUnknown}), so it is not known whether others can read it.`;
  }
  if (info.encryptionUnknown !== void 0) {
    return `The secrets file could not be read (${info.encryptionUnknown}). Saving a secret will fail rather than overwrite it.`;
  }
  if (info.plaintext) {
    return info.pendingEncryption ? "Existing secrets in this file are still unencrypted (file mode 0600). They are re-encrypted the next time a secret is saved." : "Secrets are stored unencrypted (file mode 0600). Set MCP_INSPECTOR_SECRET_KEY to encrypt them.";
  }
  return void 0;
}
function secretStorageSummary(info) {
  const parts = [secretStorageLabel(info)];
  if (info.path) parts.push(`at ${info.path}`);
  if (info.reason === "fallback") {
    parts.push("\u2014 fell back from the OS keychain");
  }
  return parts.join(" ");
}

// ../../core/node/version.ts
import { readFileSync as readFileSync2 } from "fs";
import { dirname as dirname3, join as join2, parse } from "path";
import { fileURLToPath } from "url";
var ROOT_PACKAGE_NAME = "@modelcontextprotocol/inspector";
function readInspectorVersion(callerUrl) {
  const startDir = dirname3(fileURLToPath(callerUrl));
  const { root } = parse(startDir);
  let dir = startDir;
  for (; ; ) {
    let manifest;
    try {
      manifest = JSON.parse(
        readFileSync2(join2(dir, "package.json"), "utf-8")
      );
    } catch {
    }
    if (manifest?.name === ROOT_PACKAGE_NAME && manifest.version) {
      return manifest.version;
    }
    if (dir === root) break;
    dir = dirname3(dir);
  }
  throw new Error(
    `Could not locate the ${ROOT_PACKAGE_NAME} root package.json above ${startDir}`
  );
}
function readInspectorVersionSafe(callerUrl) {
  try {
    return readInspectorVersion(callerUrl);
  } catch {
    return void 0;
  }
}

// server/sandbox-controller.ts
import { createServer } from "http";
import { readFileSync as readFileSync3 } from "fs";
import { dirname as dirname4, join as join3 } from "path";
import { fileURLToPath as fileURLToPath2 } from "url";

// ../../core/node/hostUrl.ts
import { isIPv6 } from "net";
function stripBrackets(host) {
  return host.replace(/^\[(.*)\]$/, "$1");
}
function formatHostForUrl(host) {
  const h = host.trim();
  const bare = stripBrackets(h).split("%")[0];
  return isIPv6(bare) ? `[${bare}]` : h;
}
function unmapIpv4MappedHost(host) {
  const m = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/i.exec(
    stripBrackets(host)
  );
  if (!m) return host;
  const hi = parseInt(m[1], 16);
  const lo = parseInt(m[2], 16);
  return `${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`;
}
function canonicalUrlHost(host) {
  const formatted = formatHostForUrl(host.trim().toLowerCase());
  try {
    return unmapIpv4MappedHost(new URL(`http://${formatted}`).hostname);
  } catch {
    return formatted;
  }
}
var ALL_INTERFACES_LITERALS = /* @__PURE__ */ new Set(["", "0.0.0.0", "::", "::ffff:0:0"]);
function canonicalizeIpv6(value) {
  if (!isIPv6(value)) return value;
  const [address] = value.split("%");
  return new URL(`http://[${address}]`).hostname.slice(1, -1);
}
function parseAddressPart(part) {
  if (/^0x[0-9a-f]+$/.test(part)) return parseInt(part, 16);
  if (/^[0-9]+$/.test(part)) return parseInt(part, 10);
  return NaN;
}
function isAllZeroIpv4(value) {
  const parts = value.split(".");
  if (parts.length > 4) return false;
  return parts.every((part) => parseAddressPart(part) === 0);
}
function isAllInterfacesHost(host) {
  const normalized = canonicalizeIpv6(stripBrackets(canonicalUrlHost(host)));
  return ALL_INTERFACES_LITERALS.has(normalized) || isAllZeroIpv4(normalized);
}

// server/resolve-bind-host.ts
var BIND_ALL_INTERFACES_ENV = "DANGEROUSLY_BIND_ALL_INTERFACES";
function isEnabled(value) {
  const v = value?.trim().toLowerCase();
  return v === "true" || v === "1";
}
var DEFAULT_BIND_HOST = "127.0.0.1";
function resolveBindHostname(env = process.env) {
  const host = (env.HOST ?? DEFAULT_BIND_HOST).trim();
  if (isAllInterfacesHost(host) && !isEnabled(env[BIND_ALL_INTERFACES_ENV])) {
    const resolved2 = canonicalUrlHost(host);
    const shown = stripBrackets(resolved2) === stripBrackets(host.toLowerCase()) ? `HOST="${host}"` : `HOST="${host}" (resolves to ${resolved2})`;
    throw new Error(
      `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.`
    );
  }
  return stripBrackets(host);
}

// server/sandbox-controller.ts
var __dirname = dirname4(fileURLToPath2(import.meta.url));
var LOOPBACK_FRAME_ANCESTORS = ["http://127.0.0.1:*", "http://localhost:*"];
var CSP_HOST_SOURCE = /^[a-z][a-z0-9+.-]*:\/\/[^\s;,'"[\]*]+$/i;
function sandboxFrameAncestors(allowedOrigins) {
  return frameAncestorsDirective(allowedOrigins);
}
function frameAncestorsDirective(origins) {
  const valid = (origins ?? []).filter((o) => CSP_HOST_SOURCE.test(o));
  const sources = valid.length > 0 ? valid : LOOPBACK_FRAME_ANCESTORS;
  return `frame-ancestors ${sources.join(" ")}`;
}
function parseListenPort(raw) {
  const v = raw?.trim();
  if (!v || !/^\d+$/.test(v)) return void 0;
  const n = parseInt(v, 10);
  return n <= 65535 ? n : void 0;
}
var DEFAULT_SANDBOX_PORT = 6275;
function resolveSandboxPort() {
  const fromSandbox = parseListenPort(process.env.MCP_SANDBOX_PORT);
  if (fromSandbox === void 0 && process.env.MCP_SANDBOX_PORT?.trim()) {
    console.warn(
      `Ignoring invalid MCP_SANDBOX_PORT="${process.env.MCP_SANDBOX_PORT}" (need an integer 0\u201365535); falling back.`
    );
  }
  return fromSandbox ?? parseListenPort(process.env.SERVER_PORT) ?? DEFAULT_SANDBOX_PORT;
}
function createSandboxController(options) {
  const { port, host = DEFAULT_BIND_HOST, allowedOrigins } = options;
  let server = null;
  let sandboxUrl = null;
  const SANDBOX_PROXY_CSP = sandboxFrameAncestors(allowedOrigins);
  let sandboxHtml;
  try {
    const sandboxHtmlPath = join3(__dirname, "../static/sandbox_proxy.html");
    sandboxHtml = readFileSync3(sandboxHtmlPath, "utf-8");
  } catch (e) {
    sandboxHtml = "<!DOCTYPE html><html><body>Sandbox not loaded: " + String(e.message) + "</body></html>";
  }
  return {
    async start() {
      if (server && sandboxUrl) {
        const p = parseInt(new URL(sandboxUrl).port, 10);
        return { port: p, url: sandboxUrl };
      }
      return new Promise((resolve9) => {
        let settled = false;
        const settle = (value) => {
          if (settled) return;
          settled = true;
          resolve9(value);
        };
        server = createServer((req, res) => {
          if (req.method !== "GET" || req.url !== "/sandbox" && req.url !== "/sandbox/") {
            res.writeHead(404, { "Content-Type": "text/plain" });
            res.end("Not Found");
            return;
          }
          res.writeHead(200, {
            "Content-Type": "text/html; charset=utf-8",
            "Cache-Control": "no-store, no-cache, must-revalidate",
            Pragma: "no-cache",
            "Content-Security-Policy": SANDBOX_PROXY_CSP
          });
          res.end(sandboxHtml);
        });
        let retriedDynamic = false;
        server.on("error", (err) => {
          if (err.code === "EADDRINUSE") {
            if (!retriedDynamic && port !== 0) {
              retriedDynamic = true;
              console.warn(
                `Sandbox: port ${port} in use; falling back to an OS-assigned port. The MCP Apps tab will work locally, but the sandbox is no longer on a predictable port \u2014 set MCP_SANDBOX_PORT to a free one if you need to forward it (container / SSH tunnel / reverse proxy).`
              );
              server.listen(0, host);
              return;
            }
            console.error(
              `Sandbox: port ${port || "dynamic"} in use. MCP Apps tab may not work.`
            );
          } else {
            console.error("Sandbox server error:", err);
          }
          server = null;
          settle({ port: 0, url: "" });
        });
        server.listen(port, host, () => {
          const addr = server.address();
          const actualPort = typeof addr === "object" && addr !== null && "port" in addr ? addr.port : (
            /* v8 ignore next -- unreachable for a TCP listen: `address()`
               always returns an `AddressInfo` object with `port`. The
               string form only occurs for unix-socket/pipe listens, which
               this controller never performs. */
            addr
          );
          const canonicalHost = canonicalUrlHost(host);
          const urlHost = isAllInterfacesHost(canonicalHost) ? "localhost" : canonicalHost;
          sandboxUrl = `http://${urlHost}:${actualPort}/sandbox`;
          settle({ port: actualPort, url: sandboxUrl });
        });
      });
    },
    async close() {
      if (!server) return;
      return new Promise((resolve9) => {
        server.close(() => {
          server = null;
          sandboxUrl = null;
          resolve9();
        });
      });
    },
    getUrl() {
      return sandboxUrl;
    }
  };
}

// server/app-origin-controller.ts
import { createServer as createServer2 } from "http";
import { randomBytes } from "crypto";
var DEFAULT_APP_ORIGIN_PORT = 6278;
var MAX_DOCUMENTS = 32;
var MAX_RETAINED_CHARS = 64 * 1024 * 1024;
var DOCUMENT_TTL_MS = 60 * 60 * 1e3;
function appDocumentEmbedders(sandboxUrl, allowedOrigins) {
  const origins = [];
  if (sandboxUrl) {
    try {
      origins.push(new URL(sandboxUrl).origin);
    } catch {
    }
  }
  origins.push(...allowedOrigins ?? []);
  return origins.length > 0 ? origins : void 0;
}
function parseListenPort2(raw) {
  const v = raw?.trim();
  if (!v || !/^\d+$/.test(v)) return void 0;
  const n = parseInt(v, 10);
  return n <= 65535 ? n : void 0;
}
function resolveAppOriginPort() {
  const parsed = parseListenPort2(process.env.MCP_APP_ORIGIN_PORT);
  if (parsed === void 0 && process.env.MCP_APP_ORIGIN_PORT?.trim()) {
    console.warn(
      `Ignoring invalid MCP_APP_ORIGIN_PORT="${process.env.MCP_APP_ORIGIN_PORT}" (need an integer 0\u201365535); falling back.`
    );
  }
  return parsed ?? DEFAULT_APP_ORIGIN_PORT;
}
var DOC_PATH_PREFIX = "/app-document/";
function parseDocumentId(url) {
  if (!url || !url.startsWith(DOC_PATH_PREFIX)) return null;
  const id = url.slice(DOC_PATH_PREFIX.length).split(/[?#]/)[0];
  return /^[0-9a-f]{32}$/.test(id ?? "") ? id ?? null : null;
}
function createAppOriginController(options) {
  const { port, host = DEFAULT_BIND_HOST, embedderOrigins } = options;
  let server = null;
  let origin = null;
  const documents = /* @__PURE__ */ new Map();
  let retainedChars = 0;
  function drop(id) {
    const doc = documents.get(id);
    if (!doc) return;
    retainedChars -= doc.size;
    documents.delete(id);
  }
  const FRAME_ANCESTORS = frameAncestorsDirective(embedderOrigins);
  function evictExpired(now) {
    for (const [id, doc] of documents) {
      if (doc.expiresAt <= now) drop(id);
    }
  }
  return {
    async start() {
      if (server && origin) {
        return { port: parseInt(new URL(origin).port, 10), url: origin };
      }
      return new Promise((resolve9) => {
        let settled = false;
        const settle = (value) => {
          if (settled) return;
          settled = true;
          resolve9(value);
        };
        server = createServer2((req, res) => {
          const id = req.method === "GET" ? parseDocumentId(req.url) : null;
          const doc = id ? documents.get(id) : void 0;
          if (!doc || doc.expiresAt <= Date.now()) {
            if (id) drop(id);
            res.writeHead(404, { "Content-Type": "text/plain" });
            res.end("Not Found");
            return;
          }
          res.writeHead(200, {
            "Content-Type": "text/html; charset=utf-8",
            "Cache-Control": "no-store, no-cache, must-revalidate",
            Pragma: "no-cache",
            // `nosniff` matters more here than on a page we authored: these
            // bytes are server-supplied and untrusted.
            "X-Content-Type-Options": "nosniff",
            // The per-app policy as a real header. It is the same policy baked
            // into the document's <meta>; delivering both is deliberate —
            // multiple policies intersect, and two copies of one policy
            // intersect to itself, so this can only ever be at least as strict.
            "Content-Security-Policy": doc.csp ? `${doc.csp}; ${FRAME_ANCESTORS}` : FRAME_ANCESTORS
          });
          res.end(doc.html);
        });
        let retriedDynamic = false;
        server.on("error", (err) => {
          if (err.code === "EADDRINUSE") {
            if (!retriedDynamic && port !== 0) {
              retriedDynamic = true;
              console.warn(
                `App origin: port ${port} in use; falling back to an OS-assigned port. Apps declaring _meta.ui.domain will still render, but the origin they are served from is no longer predictable \u2014 set MCP_APP_ORIGIN_PORT to a free one if your app's backend allowlists it.`
              );
              server.listen(0, host);
              return;
            }
            console.error(
              `App origin: port ${port || "dynamic"} in use. Apps declaring _meta.ui.domain will fall back to an opaque origin.`
            );
          } else {
            console.error("App origin server error:", err);
          }
          server = null;
          origin = null;
          documents.clear();
          retainedChars = 0;
          settle({ port: 0, url: "" });
        });
        server.listen(port, host, () => {
          const addr = server.address();
          if (typeof addr !== "object" || addr === null) {
            settle({ port: 0, url: "" });
            return;
          }
          const actualPort = addr.port;
          const canonicalHost = canonicalUrlHost(host);
          const urlHost = isAllInterfacesHost(canonicalHost) ? "localhost" : canonicalHost;
          origin = `http://${urlHost}:${actualPort}`;
          settle({ port: actualPort, url: origin });
        });
      });
    },
    async close() {
      documents.clear();
      retainedChars = 0;
      if (!server) return;
      return new Promise((resolve9) => {
        server.close(() => {
          server = null;
          origin = null;
          resolve9();
        });
      });
    },
    getOrigin() {
      return origin;
    },
    publish(doc) {
      if (!origin) return null;
      const now = Date.now();
      evictExpired(now);
      const size = doc.html.length + (doc.csp?.length ?? 0);
      while (documents.size >= MAX_DOCUMENTS || documents.size > 0 && retainedChars + size > MAX_RETAINED_CHARS) {
        const oldest = documents.keys().next();
        if (oldest.done) break;
        drop(oldest.value);
      }
      const id = randomBytes(16).toString("hex");
      documents.set(id, { ...doc, expiresAt: now + DOCUMENT_TTL_MS, size });
      retainedChars += size;
      return { url: `${origin}${DOC_PATH_PREFIX}${id}` };
    }
  };
}

// server/web-server-config.ts
var inspectorVersion = readInspectorVersionSafe(import.meta.url);
function defaultEnvironmentFromProcess(extra) {
  const keys = process.platform === "win32" ? [
    "APPDATA",
    "HOMEDRIVE",
    "HOMEPATH",
    "LOCALAPPDATA",
    "PATH",
    "PROCESSOR_ARCHITECTURE",
    "SYSTEMDRIVE",
    "SYSTEMROOT",
    "TEMP",
    "USERNAME",
    "USERPROFILE",
    "PROGRAMFILES"
  ] : ["HOME", "LOGNAME", "PATH", "SHELL", "TERM", "USER"];
  const out = {};
  for (const key of keys) {
    const value = process.env[key];
    if (value && !value.startsWith("()")) {
      out[key] = value;
    }
  }
  if (extra) {
    Object.assign(out, extra);
  }
  return out;
}
function webServerConfigToInitialPayload(config, secretStorage) {
  return {
    ...transportDefaults(config),
    version: inspectorVersion,
    // Omitted rather than defaulted when the caller didn't resolve a store:
    // the UI treats an absent descriptor as "unknown" and shows nothing,
    // which is the only honest rendering. A default of "keychain" would put a
    // confident wrong answer under the field where a secret is typed.
    ...secretStorage ? { secretStorage } : {}
  };
}
function transportDefaults(config) {
  const mc = config.initialMcpConfig;
  const defaultEnvironment = defaultEnvironmentFromProcess(
    mc && "env" in mc && mc.env ? mc.env : void 0
  );
  if (!mc) {
    return { defaultEnvironment };
  }
  if (mc.type === "stdio" || mc.type === void 0) {
    return {
      defaultCommand: mc.command,
      defaultArgs: mc.args ?? [],
      defaultTransport: "stdio",
      defaultCwd: mc.cwd,
      defaultEnvironment
    };
  }
  if (mc.type === "sse") {
    return {
      defaultTransport: "sse",
      defaultServerUrl: mc.url,
      defaultEnvironment
    };
  }
  if (mc.type === "streamable-http") {
    return {
      defaultTransport: "streamable-http",
      defaultServerUrl: mc.url,
      defaultEnvironment
    };
  }
  const c = mc;
  return {
    defaultTransport: "streamable-http",
    defaultServerUrl: c.url,
    defaultEnvironment
  };
}
function printServerBanner(config, actualPort, resolvedToken, sandboxUrl, secretStorage) {
  const h = canonicalUrlHost(config.hostname);
  const bannerHost = isAllInterfacesHost(h) ? "localhost" : h;
  const baseUrl = httpOrigin(bannerHost, actualPort);
  const url = config.dangerouslyOmitAuth || !resolvedToken ? baseUrl : `${baseUrl}?${API_SERVER_ENV_VARS.AUTH_TOKEN}=${resolvedToken}`;
  console.log(`
MCP Inspector Web is up and running at:
   ${url}
`);
  if (sandboxUrl) {
    console.log(`   Sandbox (MCP Apps): ${sandboxUrl}
`);
  }
  if (config.dangerouslyOmitAuth) {
    console.log("   Auth: disabled (DANGEROUSLY_OMIT_AUTH)\n");
  } else {
    console.log(`   Auth token: ${resolvedToken}
`);
  }
  if (secretStorage) {
    console.log(`   Secrets: ${secretStorageSummary(secretStorage)}
`);
  }
  if (config.autoOpen) {
    console.log("Opening browser...");
  }
  return url;
}
function openBrowser(url) {
  open(url).catch((err) => {
    console.warn("Could not open a browser automatically:", err);
  });
}
var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
function httpOrigin(host, port) {
  return port === 80 ? `http://${host}` : `http://${host}:${port}`;
}
function loopbackOrigins(port) {
  return [
    httpOrigin("localhost", port),
    httpOrigin("127.0.0.1", port),
    httpOrigin("[::1]", port)
  ];
}
function defaultAllowedOrigins(hostname, port) {
  const h = canonicalUrlHost(hostname);
  if (LOOPBACK_HOSTNAMES.has(h)) {
    return loopbackOrigins(port);
  }
  if (isAllInterfacesHost(h)) {
    return [
      ...loopbackOrigins(port),
      httpOrigin("0.0.0.0", port),
      httpOrigin("[::]", port)
    ];
  }
  return [httpOrigin(h, port)];
}
function buildWebServerConfig(options = {}) {
  const {
    initialMcpConfig = null,
    mcpConfigPath,
    writable = true,
    initialServers = null
  } = options;
  const rawPort = process.env.CLIENT_PORT?.trim() || "6274";
  const port = parseInt(rawPort, 10);
  if (!/^\d+$/.test(rawPort) || port < 1 || port > 65535) {
    throw new Error(
      `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).`
    );
  }
  const hostname = resolveBindHostname();
  const dangerouslyOmitAuth = !!process.env.DANGEROUSLY_OMIT_AUTH;
  const authToken = dangerouslyOmitAuth ? "" : process.env[API_SERVER_ENV_VARS.AUTH_TOKEN] ?? process.env[LEGACY_AUTH_TOKEN_ENV] ?? "";
  const sandboxPort = resolveSandboxPort();
  const requestedAppOriginPort = resolveAppOriginPort();
  const appOriginCollidesWith = requestedAppOriginPort !== 0 && (requestedAppOriginPort === port ? "the web server (CLIENT_PORT)" : requestedAppOriginPort === sandboxPort ? "the MCP Apps sandbox (MCP_SANDBOX_PORT)" : null);
  if (appOriginCollidesWith) {
    console.warn(
      `App origin: port ${requestedAppOriginPort} is already ${appOriginCollidesWith}; using an OS-assigned port instead. Apps declaring _meta.ui.domain will still render, but the origin they are served from is no longer predictable \u2014 set MCP_APP_ORIGIN_PORT to a free one if your app's backend allowlists it.`
    );
  }
  const appOriginPort = appOriginCollidesWith ? 0 : requestedAppOriginPort;
  let logger;
  if (process.env.MCP_LOG_FILE) {
    logger = pino(
      { level: "info" },
      pino.destination({
        dest: process.env.MCP_LOG_FILE,
        append: true,
        mkdir: true
      })
    );
  }
  const configuredOrigins = process.env.ALLOWED_ORIGINS?.split(",").map((o) => o.trim()).filter(Boolean).map((o) => {
    try {
      const { origin } = new URL(o);
      if (origin === "null") throw new Error("opaque origin");
      if (origin.includes("*")) throw new Error("wildcard origin");
      return origin;
    } catch {
      console.warn(`Ignoring invalid ALLOWED_ORIGINS entry: ${o}`);
      return null;
    }
  }).filter((o) => o !== null);
  return {
    port,
    hostname,
    authToken,
    dangerouslyOmitAuth,
    initialMcpConfig,
    mcpConfigPath,
    writable,
    initialServers,
    storageDir: process.env.MCP_STORAGE_DIR,
    allowedOrigins: configuredOrigins?.length ? configuredOrigins : defaultAllowedOrigins(hostname, port),
    sandboxPort,
    sandboxHost: hostname,
    appOriginPort,
    logger,
    autoOpen: resolveAutoOpen()
  };
}
function resolveAutoOpen() {
  const flag = process.env.MCP_AUTO_OPEN_ENABLED;
  if (flag === "true") return true;
  if (flag === "false") return false;
  return !process.env.VITEST;
}

// server/start-vite-dev-server.ts
import { join as join7, dirname as dirname7, resolve as resolve6 } from "path";
import { fileURLToPath as fileURLToPath4 } from "url";
import { createServer as createServer3 } from "vite";
import react from "@vitejs/plugin-react";

// ../../core/mcp/remote/node/server.ts
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
import { stat as fsStat } from "fs/promises";
import { homedir } from "os";
import { Hono } from "hono";
import { streamSSE } from "hono/streaming";
import { bodyLimit } from "hono/body-limit";
import { watch as chokidarWatch } from "chokidar";

// ../../core/mcp/config.ts
function getServerType(config) {
  if (!("type" in config) || config.type === void 0) {
    return "stdio";
  }
  const type = config.type;
  if (type === "stdio") {
    return "stdio";
  }
  if (type === "sse") {
    return "sse";
  }
  if (type === "streamable-http") {
    return "streamable-http";
  }
  throw new Error(
    `Invalid server type: ${type}. Valid types are: stdio, sse, streamable-http`
  );
}

// ../../core/mcp/node/transport.ts
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { SSEClientTransport } from "@modelcontextprotocol/client";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/client";

// ../../core/mcp/fetchTracking.ts
var SENSITIVE_HEADERS = /* @__PURE__ */ new Set([
  "authorization",
  "cookie",
  "set-cookie",
  "proxy-authorization",
  "x-api-key",
  // The inspector backend's own bearer (createRemoteFetch stamps this on every
  // proxied request); same exposure as Authorization.
  "x-mcp-remote-auth"
]);
var REDACTED_HEADER_VALUE = "[REDACTED]";
var SENSITIVE_BODY_FIELDS = /* @__PURE__ */ new Set([
  "client_secret",
  "code",
  "refresh_token",
  "access_token",
  "id_token",
  "code_verifier",
  "client_assertion",
  "assertion",
  "password",
  "token"
]);
var REDACTED_VALUE = "[REDACTED]";
function isSensitiveField(name) {
  return SENSITIVE_BODY_FIELDS.has(name.toLowerCase());
}
function redactSensitiveHeaders(headers) {
  const out = {};
  for (const [key, value] of Object.entries(headers)) {
    out[key] = SENSITIVE_HEADERS.has(key.toLowerCase()) ? REDACTED_HEADER_VALUE : value;
  }
  return out;
}
function redactUrlQuery(url) {
  const queryStart = url.indexOf("?");
  if (queryStart === -1) return url;
  const base = url.slice(0, queryStart);
  const afterQuery = url.slice(queryStart + 1);
  const hashStart = afterQuery.indexOf("#");
  const query = hashStart === -1 ? afterQuery : afterQuery.slice(0, hashStart);
  const fragment = hashStart === -1 ? "" : afterQuery.slice(hashStart);
  try {
    const params = new URLSearchParams(query);
    let changed = false;
    for (const key of new Set(params.keys())) {
      if (isSensitiveField(key)) {
        changed = true;
        params.set(key, REDACTED_VALUE);
      }
    }
    if (!changed) return url;
    return `${base}?${params.toString()}${fragment}`;
  } catch {
    return url;
  }
}
function redactJsonValue(value) {
  if (Array.isArray(value)) {
    return value.map(redactJsonValue);
  }
  if (value !== null && typeof value === "object") {
    const out = {};
    for (const [key, val] of Object.entries(value)) {
      out[key] = isSensitiveField(key) && typeof val === "string" ? REDACTED_VALUE : redactJsonValue(val);
    }
    return out;
  }
  return value;
}
function redactBody(body, contentType) {
  if (!body) return body;
  const type = (contentType ?? "").toLowerCase();
  if (type.includes("application/x-www-form-urlencoded")) {
    try {
      const params = new URLSearchParams(body);
      let changed = false;
      for (const key of new Set(params.keys())) {
        if (isSensitiveField(key)) {
          changed = true;
          params.set(key, REDACTED_VALUE);
        }
      }
      return changed ? params.toString() : body;
    } catch {
      return body;
    }
  }
  try {
    const parsed = JSON.parse(body);
    if (parsed !== null && typeof parsed === "object") {
      return JSON.stringify(redactJsonValue(parsed));
    }
  } catch {
  }
  return body;
}
function findHeader(headers, name) {
  const target = name.toLowerCase();
  for (const [key, value] of Object.entries(headers)) {
    if (key.toLowerCase() === target) return value;
  }
  return void 0;
}
function isLongLivedStreamResponse(method, contentType) {
  if (method !== "GET") return false;
  if (!contentType) return false;
  return contentType.includes("text/event-stream") || contentType.includes("application/x-ndjson");
}
function createFetchTracker(baseFetch, callbacks) {
  return async (input, init) => {
    const startTime = Date.now();
    const timestamp = /* @__PURE__ */ new Date();
    const id = `${timestamp.getTime()}-${Math.random().toString(36).slice(2, 11)}`;
    const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
    const method = init?.method || "GET";
    const rawRequestHeaders = {};
    if (input instanceof Request) {
      input.headers.forEach((value, key) => {
        rawRequestHeaders[key] = value;
      });
    }
    if (init?.headers) {
      const headers = new Headers(init.headers);
      headers.forEach((value, key) => {
        rawRequestHeaders[key] = value;
      });
    }
    const requestHeaders = redactSensitiveHeaders(rawRequestHeaders);
    const requestContentType = findHeader(rawRequestHeaders, "content-type");
    const redactedUrl = redactUrlQuery(url);
    let requestBody;
    if (init?.body) {
      if (typeof init.body === "string") {
        requestBody = init.body;
      } else {
        try {
          requestBody = String(init.body);
        } catch {
          requestBody = void 0;
        }
      }
    } else if (input instanceof Request && input.body) {
      try {
        const cloned = input.clone();
        requestBody = await cloned.text();
      } catch {
        requestBody = void 0;
      }
    }
    const redactedRequestBody = redactBody(requestBody, requestContentType);
    let response;
    let error;
    try {
      response = await baseFetch(input, init);
    } catch (err) {
      error = err instanceof Error ? err.message : String(err);
      const entry2 = {
        id,
        timestamp,
        method,
        url: redactedUrl,
        requestHeaders,
        requestBody: redactedRequestBody,
        error,
        duration: Date.now() - startTime
      };
      callbacks.trackRequest?.(entry2);
      throw err;
    }
    const responseStatus = response.status;
    const responseStatusText = response.statusText;
    const rawResponseHeaders = {};
    response.headers.forEach((value, key) => {
      rawResponseHeaders[key] = value;
    });
    const responseHeaders = redactSensitiveHeaders(rawResponseHeaders);
    const isLongLivedStream = isLongLivedStreamResponse(
      method,
      response.headers.get("content-type")
    );
    const duration = Date.now() - startTime;
    const entry = {
      id,
      timestamp,
      method,
      url: redactedUrl,
      requestHeaders,
      requestBody: redactedRequestBody,
      responseStatus,
      responseStatusText,
      responseHeaders,
      responseBody: void 0,
      duration
    };
    callbacks.trackRequest?.(entry);
    if (!isLongLivedStream && response.body && !response.bodyUsed) {
      const responseContentType = response.headers.get("content-type");
      try {
        const cloned = response.clone();
        cloned.text().then((body) => {
          callbacks.updateResponseBody?.(
            id,
            redactBody(body, responseContentType) ?? body
          );
        }).catch(() => {
        });
      } catch {
      }
    }
    return response;
  };
}

// ../../core/auth/utils.ts
import { UnauthorizedError } from "@modelcontextprotocol/client";
import { ZodError } from "zod";

// ../../core/auth/challenge.ts
var EMA_STEP_UP_PENDING_URL = new URL("mcp-inspector:ema-step-up");
var AuthChallengeError = class extends Error {
  authChallenge;
  status;
  constructor(authChallenge, status, message) {
    super(message ?? `Auth challenge: ${authChallenge.reason}`);
    this.name = "AuthChallengeError";
    this.authChallenge = authChallenge;
    this.status = status;
  }
};
function parseWwwAuthenticateBearer(header) {
  const match = header.match(/Bearer\s+(.+)/i);
  if (!match) {
    return {};
  }
  const params = {};
  const paramRegex = /([\w-]+)(?:="([^"]*)"|=([^\s,]+))?/g;
  let m;
  while ((m = paramRegex.exec(match[1])) !== null) {
    const value = m[2] ?? m[3];
    if (value !== void 0) {
      params[m[1].toLowerCase()] = value;
    }
  }
  return {
    error: params.error,
    scope: params.scope,
    resourceMetadata: params.resource_metadata,
    errorDescription: params.error_description
  };
}
function parseScopeString(scope) {
  if (!scope?.trim()) {
    return [];
  }
  return scope.trim().split(/\s+/).filter(Boolean);
}
function reasonFromHttpResponse(status, bearer) {
  if (status === 403) {
    if (bearer.error === "insufficient_scope") {
      return "insufficient_scope";
    }
    return "unauthorized";
  }
  if (bearer.error === "insufficient_scope") {
    return "insufficient_scope";
  }
  if (bearer.error === "invalid_token") {
    return "invalid_token";
  }
  if (status === 401) {
    return "token_expired";
  }
  return "unauthorized";
}
function parseAuthChallengeFromResponse(response, context) {
  const status = response.status;
  if (status !== 401 && status !== 403) {
    return void 0;
  }
  const wwwAuthenticate = response.headers.get("WWW-Authenticate") ?? void 0;
  const bearer = wwwAuthenticate ? parseWwwAuthenticateBearer(wwwAuthenticate) : {};
  const requiredScopes = parseScopeString(bearer.scope);
  return {
    reason: reasonFromHttpResponse(status, bearer),
    ...requiredScopes.length > 0 ? { requiredScopes } : {},
    ...bearer.resourceMetadata ? { resourceMetadataUrl: bearer.resourceMetadata } : {},
    ...bearer.errorDescription ? { message: bearer.errorDescription } : {},
    ...context ? { context } : {},
    raw: {
      httpStatus: status,
      ...wwwAuthenticate ? { wwwAuthenticate } : {}
    }
  };
}

// ../../core/mcp/node/authChallengeFetch.ts
function createAuthChallengeInterceptFetch(baseFetch) {
  return async (input, init) => {
    const response = await baseFetch(input, init);
    if (response.status !== 401 && response.status !== 403) {
      return response;
    }
    const challenge = parseAuthChallengeFromResponse(response);
    if (!challenge) {
      return response;
    }
    await response.body?.cancel().catch(() => {
    });
    throw new AuthChallengeError(
      challenge,
      response.status,
      `MCP auth challenge (${response.status})`
    );
  };
}
function createAuthChallengeObserverFetch(baseFetch, onChallenge) {
  return async (input, init) => {
    const response = await baseFetch(input, init);
    if (response.status === 401 || response.status === 403) {
      const challenge = parseAuthChallengeFromResponse(response);
      if (challenge) {
        onChallenge(challenge);
      }
    }
    return response;
  };
}

// ../../core/mcp/node/proxyFetch.ts
function asUndiciInit(init) {
  return init;
}
function asBodyInit(body) {
  return body;
}
var PROXY_ENV_VARS = [
  "HTTPS_PROXY",
  "https_proxy",
  "HTTP_PROXY",
  "http_proxy"
];
function readProxyEnv() {
  for (const name of PROXY_ENV_VARS) {
    const value = process.env[name];
    if (value && value.trim() !== "") return value;
  }
  return void 0;
}
var NULL_BODY_STATUSES = /* @__PURE__ */ new Set([
  101,
  103,
  204,
  205,
  304
]);
function toGlobalResponse(res) {
  const headers = new Headers();
  for (const [key, value] of res.headers) headers.append(key, value);
  const out = new Response(
    NULL_BODY_STATUSES.has(res.status) ? null : asBodyInit(res.body),
    {
      status: res.status,
      statusText: res.statusText,
      headers
    }
  );
  for (const [key, value] of [
    ["url", res.url],
    ["redirected", res.redirected],
    ["type", res.type]
  ]) {
    Object.defineProperty(out, key, {
      value,
      configurable: true,
      enumerable: false
    });
  }
  return out;
}
var loadedProxiedFetch;
async function loadProxiedFetch() {
  let undici;
  try {
    undici = await import("undici");
  } catch (cause) {
    throw new Error(
      "HTTPS_PROXY / HTTP_PROXY is set but the `undici` package could not be loaded, so requests cannot be routed through the proxy. This is a packaging fault rather than something to fix by installing undici; please report it, or unset the proxy env var to fall back to a direct connection.",
      { cause }
    );
  }
  const dispatcher = new undici.EnvHttpProxyAgent();
  return async (input, init) => {
    const [target, requestInit] = toUndiciRequest(input, init);
    return toGlobalResponse(
      await undici.fetch(target, { ...asUndiciInit(requestInit), dispatcher })
    );
  };
}
function toUndiciRequest(input, init) {
  if (input instanceof Request) {
    return [
      input.url,
      {
        method: input.method,
        headers: input.headers,
        // A stream body needs `duplex: "half"`; the field is undici's, not in
        // the DOM `RequestInit`, so it rides along as an extra property.
        ...input.body ? { body: input.body, duplex: "half" } : {},
        signal: input.signal,
        // Everything else the Request carries. Dropping these would silently
        // change fetch semantics under a proxy — most visibly `redirect`, where
        // a `new Request(url, { redirect: "manual" })` would start following
        // redirects instead of surfacing them.
        redirect: input.redirect,
        cache: input.cache,
        credentials: input.credentials,
        integrity: input.integrity,
        keepalive: input.keepalive,
        mode: input.mode,
        referrer: input.referrer,
        referrerPolicy: input.referrerPolicy,
        // An explicit `init` still wins, per the fetch contract.
        ...init
      }
    ];
  }
  return [input instanceof URL ? input.href : input, init ?? {}];
}
function createProxyFetch() {
  if (readProxyEnv() === void 0) return void 0;
  return async (input, init) => {
    loadedProxiedFetch ??= loadProxiedFetch();
    return (await loadedProxiedFetch)(input, init);
  };
}

// ../../core/mcp/node/transport.ts
function headersFromSettings(settings) {
  if (!settings || settings.headers.length === 0) return void 0;
  const out = {};
  for (const { key, value } of settings.headers) {
    if (key.trim() === "") continue;
    out[key] = value;
  }
  return Object.keys(out).length > 0 ? out : void 0;
}
function createTransportNode(config, options = {}) {
  const serverType = getServerType(config);
  const {
    fetchFn: optionsFetchFn,
    onStderr,
    pipeStderr = false,
    onFetchRequest,
    onFetchResponseBody,
    authProvider,
    settings,
    interceptAuthChallenges = false,
    onAuthChallengeObserved
  } = options;
  const baseFetch = optionsFetchFn ?? createProxyFetch() ?? globalThis.fetch;
  const withChallengeObserver = (inner) => onAuthChallengeObserved ? createAuthChallengeObserverFetch(inner, onAuthChallengeObserved) : inner;
  const fetchWithOptionalAuthIntercept = interceptAuthChallenges ? createAuthChallengeInterceptFetch(withChallengeObserver(baseFetch)) : withChallengeObserver(baseFetch);
  if (serverType === "stdio") {
    const stdioConfig = config;
    const transport = new StdioClientTransport({
      command: stdioConfig.command,
      args: stdioConfig.args || [],
      env: stdioConfig.env,
      cwd: stdioConfig.cwd,
      stderr: pipeStderr ? "pipe" : void 0
    });
    if (pipeStderr && transport.stderr && onStderr) {
      transport.stderr.on("data", (data) => {
        const logEntry = data.toString().trim();
        if (logEntry) {
          onStderr({
            timestamp: /* @__PURE__ */ new Date(),
            message: logEntry
          });
        }
      });
    }
    return { transport };
  } else if (serverType === "sse") {
    const sseConfig = config;
    const url = new URL(sseConfig.url);
    const configuredSseFetch = sseConfig.eventSourceInit?.fetch;
    const sseFetch = configuredSseFetch ? withChallengeObserver(configuredSseFetch) : fetchWithOptionalAuthIntercept;
    const trackedFetch = onFetchRequest ? createFetchTracker(sseFetch, {
      trackRequest: onFetchRequest,
      updateResponseBody: onFetchResponseBody
    }) : sseFetch;
    const headers = headersFromSettings(settings);
    const eventSourceInit = {
      ...sseConfig.eventSourceInit,
      ...headers && { headers },
      fetch: trackedFetch
    };
    const requestInit = {
      ...sseConfig.requestInit,
      ...headers && { headers }
    };
    const postFetch = onFetchRequest ? createFetchTracker(fetchWithOptionalAuthIntercept, {
      trackRequest: onFetchRequest,
      updateResponseBody: onFetchResponseBody
    }) : fetchWithOptionalAuthIntercept;
    const transport = new SSEClientTransport(url, {
      authProvider,
      eventSourceInit,
      requestInit,
      fetch: postFetch
    });
    return { transport };
  } else {
    const httpConfig = config;
    const url = new URL(httpConfig.url);
    const headers = headersFromSettings(settings);
    const requestInit = {
      ...httpConfig.requestInit,
      ...headers && { headers }
    };
    const transportFetch = onFetchRequest ? createFetchTracker(fetchWithOptionalAuthIntercept, {
      trackRequest: onFetchRequest,
      updateResponseBody: onFetchResponseBody
    }) : fetchWithOptionalAuthIntercept;
    const transport = new StreamableHTTPClientTransport(url, {
      authProvider,
      requestInit,
      fetch: transportFetch,
      // SEP-2350: how the transport reacts to a `403 insufficient_scope`
      // challenge. Defaults to the SDK's `reauthorize` when unset.
      ...settings?.oauthOnInsufficientScope && {
        onInsufficientScope: settings.oauthOnInsufficientScope
      }
    });
    return { transport };
  }
}

// ../../core/json/xMcpHeader.ts
var MCP_PARAM_HEADER_PREFIX = "Mcp-Param-";

// ../../core/mcp/import/clientConfig.ts
function parseJsonObject(raw) {
  let data;
  try {
    data = JSON.parse(raw);
  } catch (err) {
    const detail = err instanceof Error ? err.message : String(err);
    throw new Error(`Invalid JSON: ${detail}`, { cause: err });
  }
  if (!data || typeof data !== "object" || Array.isArray(data)) {
    throw new Error("Expected a JSON object at the top level");
  }
  return data;
}
function normalizeServerMap(servers) {
  const out = {};
  for (const [id, raw] of Object.entries(servers)) {
    if (!raw || typeof raw !== "object" || Array.isArray(raw)) continue;
    const normalized = normalizeServerType(
      raw
    );
    out[id] = normalized;
  }
  return out;
}
function parseMcpServersConfig(raw) {
  const data = parseJsonObject(raw);
  const servers = data.mcpServers;
  if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
    throw new Error("No 'mcpServers' object found in config file");
  }
  return {
    mcpServers: normalizeServerMap(servers)
  };
}
function parseVsCodeConfig(raw) {
  const data = parseJsonObject(raw);
  const servers = data.servers;
  if (!servers || typeof servers !== "object" || Array.isArray(servers)) {
    throw new Error("No 'servers' object found in VS Code config file");
  }
  return {
    mcpServers: normalizeServerMap(servers)
  };
}

// ../../core/mcp/import/strategies.ts
function join4(...segments) {
  return segments.map(
    (s, i) => i === 0 ? s.replace(/\/+$/, "") : s.replace(/^\/+|\/+$/g, "")
  ).filter((s) => s.length > 0).join("/");
}
function appData(homeDir) {
  return join4(homeDir, "AppData", "Roaming");
}
function appSupportDir(platform, homeDir, app) {
  if (platform === "darwin") {
    return join4(homeDir, "Library", "Application Support", app);
  }
  if (platform === "win32") {
    return join4(appData(homeDir), app);
  }
  return join4(homeDir, ".config", app);
}
var claudeDesktop = {
  id: "claude-desktop",
  label: "Claude Desktop",
  defaultPaths: (platform, homeDir) => [
    join4(
      appSupportDir(platform, homeDir, "Claude"),
      "claude_desktop_config.json"
    )
  ],
  parse: parseMcpServersConfig
};
var cursor = {
  id: "cursor",
  label: "Cursor",
  defaultPaths: (_platform, homeDir) => [join4(homeDir, ".cursor", "mcp.json")],
  parse: parseMcpServersConfig
};
var cline = {
  id: "cline",
  label: "Cline",
  defaultPaths: (platform, homeDir) => [
    // Cline (VS Code extension) global settings live under the editor's
    // globalStorage; the documented cross-platform fallback is ~/Documents.
    join4(
      appSupportDir(platform, homeDir, "Code"),
      "User",
      "globalStorage",
      "saoudrizwan.claude-dev",
      "settings",
      "cline_mcp_settings.json"
    ),
    join4(homeDir, "Documents", "Cline", "MCP", "cline_mcp_settings.json")
  ],
  parse: parseMcpServersConfig
};
var vscode = {
  id: "vscode",
  label: "VS Code",
  defaultPaths: (platform, homeDir) => [
    join4(appSupportDir(platform, homeDir, "Code"), "User", "mcp.json")
  ],
  parse: parseVsCodeConfig
};
var IMPORT_STRATEGY_LIST = [
  claudeDesktop,
  cursor,
  cline,
  vscode
];
var IMPORT_STRATEGIES = Object.fromEntries(IMPORT_STRATEGY_LIST.map((s) => [s.id, s]));
function getImportStrategy(id) {
  return IMPORT_STRATEGIES[id];
}

// ../../core/mcp/import/resolveSource.ts
function resolveImportSource(type, platform, homeDir, readFile2) {
  const strategy = getImportStrategy(type);
  if (!strategy) return null;
  const searched = strategy.defaultPaths(platform, homeDir);
  for (const path6 of searched) {
    let raw;
    try {
      raw = readFile2(path6);
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      return {
        type,
        found: true,
        path: path6,
        error: `Failed to read ${path6}: ${msg}`,
        searched
      };
    }
    if (raw === null) continue;
    try {
      const config = strategy.parse(raw);
      return { type, found: true, path: path6, config, searched };
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      return { type, found: true, path: path6, error: msg, searched };
    }
  }
  return { type, found: false, searched };
}

// ../../core/mcp/remote/node/remote-session.ts
var RemoteSession = class _RemoteSession {
  sessionId;
  transport;
  eventQueue = [];
  eventConsumer = null;
  transportDead = false;
  transportError = null;
  activeSendCount = 0;
  /**
   * Suppress duplicate ambient SSE auth echoes after HTTP command-scoped delivery.
   * Stale transport `onerror` may arrive after the send completes.
   */
  authHttpEchoSuppressUntilMs = 0;
  static AUTH_HTTP_ECHO_SUPPRESS_MS = 3e4;
  requestWaits = /* @__PURE__ */ new Map();
  authProviderHandle = null;
  appliedProtocolVersion;
  /** A protocol version is a dated token (`2025-11-25`, `2026-07-28`, …). */
  static PROTOCOL_VERSION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
  constructor(sessionId) {
    this.sessionId = sessionId;
  }
  setAuthProviderHandle(handle) {
    this.authProviderHandle = handle;
  }
  setAuthState(authState) {
    if (!this.authProviderHandle) {
      throw new Error("Session has no OAuth auth provider");
    }
    this.authProviderHandle.setAuthState(authState);
  }
  setTransport(transport) {
    this.transport = transport;
  }
  /**
   * Apply the client's negotiated protocol version to the upstream transport
   * (#1935). The browser's SDK Client owns the `initialize` handshake, so the
   * `setProtocolVersion` call it makes lands on the *remote* transport; without
   * this the backend's real HTTP transport never learns the version and drops
   * `Mcp-Protocol-Version` from every subsequent request.
   *
   * Ignores anything that isn't a plausible version token so a client can't
   * push arbitrary bytes into an upstream header, and re-applies only on
   * change (the value rides every `/api/mcp/send`). The value arrives from an
   * unvalidated JSON body, so the type is checked rather than assumed —
   * `RegExp.test` would otherwise coerce a `123` or `true` into a "matching"
   * string and hand the wrong type to the transport.
   */
  applyProtocolVersion(version) {
    if (typeof version !== "string" || version === this.appliedProtocolVersion || !_RemoteSession.PROTOCOL_VERSION_PATTERN.test(version)) {
      return;
    }
    this.appliedProtocolVersion = version;
    this.transport.setProtocolVersion?.(version);
  }
  setEventConsumer(consumer) {
    this.eventConsumer = consumer;
    while (this.eventQueue.length > 0) {
      const ev = this.eventQueue.shift();
      consumer(ev);
    }
  }
  clearEventConsumer() {
    this.eventConsumer = null;
    return this.transportDead;
  }
  markTransportDead(error) {
    this.transportDead = true;
    this.transportError = error;
    this.pushEvent({
      type: "transport_error",
      data: {
        error,
        code: -32e3
        // MCP error code for connection closed
      }
    });
  }
  isTransportDead() {
    return this.transportDead;
  }
  getTransportError() {
    return this.transportError;
  }
  hasEventConsumer() {
    return this.eventConsumer !== null;
  }
  beginSend() {
    this.activeSendCount++;
  }
  endSend() {
    if (this.activeSendCount > 0) {
      this.activeSendCount--;
    }
  }
  /** Command-scoped auth was returned on HTTP; suppress async onerror echo on SSE. */
  noteAuthChallengeDeliveredViaHttp() {
    this.extendAuthHttpEchoSuppression();
  }
  extendAuthHttpEchoSuppression() {
    this.authHttpEchoSuppressUntilMs = Date.now() + _RemoteSession.AUTH_HTTP_ECHO_SUPPRESS_MS;
  }
  shouldSuppressAuthEcho() {
    return Date.now() < this.authHttpEchoSuppressUntilMs;
  }
  hasActiveSend() {
    return this.activeSendCount > 0;
  }
  pushAuthChallenge(challenge) {
    this.pushEvent({ type: "auth_challenge", data: challenge });
  }
  /**
   * Streamable HTTP `transport.send()` can return before the JSON-RPC response
   * arrives. The remote send handler awaits this so `/api/mcp/send` returns
   * `auth_challenge` or `ok: true` only after the MCP round-trip (or auth error).
   */
  waitForRequestResponse(requestId, timeoutMs = 6e4) {
    return new Promise((resolve9, reject) => {
      const timer = timeoutMs > 0 ? setTimeout(() => {
        this.requestWaits.delete(requestId);
        reject(
          new Error(
            `MCP request ${String(requestId)} timed out after ${timeoutMs}ms`
          )
        );
      }, timeoutMs) : void 0;
      this.requestWaits.set(requestId, {
        resolve: () => {
          if (timer !== void 0) {
            clearTimeout(timer);
          }
          resolve9();
        },
        reject: (error) => {
          if (timer !== void 0) {
            clearTimeout(timer);
          }
          reject(error);
        }
      });
    });
  }
  cancelRequestWait(requestId) {
    const wait = this.requestWaits.get(requestId);
    if (!wait) {
      return;
    }
    this.requestWaits.delete(requestId);
    wait.reject(new Error("MCP request wait cancelled"));
  }
  rejectActiveRequestWaits(error) {
    for (const wait of this.requestWaits.values()) {
      wait.reject(error);
    }
    this.requestWaits.clear();
  }
  /**
   * Auth errors from the MCP transport.
   * - During an active send: command path owns delivery (HTTP); never SSE.
   * - After HTTP already returned auth_challenge: swallow async onerror echo.
   * - Otherwise: ambient SSE (idle session).
   */
  handleTransportAuthError(error) {
    if (!(error instanceof AuthChallengeError)) {
      return false;
    }
    this.rejectActiveRequestWaits(error);
    if (this.hasActiveSend()) {
      this.extendAuthHttpEchoSuppression();
      return true;
    }
    if (this.shouldSuppressAuthEcho()) {
      return true;
    }
    this.pushAuthChallenge(error.authChallenge);
    return true;
  }
  settleRequestWait(requestId) {
    const wait = this.requestWaits.get(requestId);
    if (!wait) {
      return;
    }
    this.requestWaits.delete(requestId);
    wait.resolve();
  }
  pushEvent(event) {
    if (this.eventConsumer) {
      this.eventConsumer(event);
    } else {
      this.eventQueue.push(event);
    }
  }
  onMessage(message) {
    if ("id" in message && message.id !== null && message.id !== void 0 && ("result" in message || "error" in message)) {
      this.settleRequestWait(message.id);
    }
    this.pushEvent({ type: "message", data: message });
  }
  onFetchRequest(entry) {
    this.pushEvent({
      type: "fetch_request",
      data: {
        ...entry,
        timestamp: entry.timestamp instanceof Date ? entry.timestamp.toISOString() : entry.timestamp
      }
    });
  }
  onFetchResponseBody(id, responseBody) {
    this.pushEvent({
      type: "fetch_request_body_update",
      data: { id, responseBody }
    });
  }
  onStderr(entry) {
    this.pushEvent({
      type: "stdio_log",
      data: {
        timestamp: entry.timestamp.toISOString(),
        message: entry.message
      }
    });
  }
};

// ../../core/mcp/remote/node/tokenAuthProvider.ts
var REMOTE_OAUTH_STUB_METADATA = {
  redirect_uris: [],
  scope: ""
};
function cloneAuthState(state) {
  return {
    ...state.oauthTokens && {
      oauthTokens: { ...state.oauthTokens }
    },
    ...state.oauthClient && {
      oauthClient: { ...state.oauthClient }
    }
  };
}
function createRemoteAuthProvider(initialAuthState) {
  if (!initialAuthState?.oauthTokens && !initialAuthState?.oauthClient) {
    return void 0;
  }
  let authState = cloneAuthState(initialAuthState);
  const provider = {
    get clientMetadata() {
      return REMOTE_OAUTH_STUB_METADATA;
    },
    get redirectUrl() {
      return "";
    },
    async tokens() {
      return authState.oauthTokens;
    },
    async clientInformation() {
      if (!authState.oauthClient?.client_id) {
        return void 0;
      }
      return {
        client_id: authState.oauthClient.client_id,
        ...authState.oauthClient.client_secret && {
          client_secret: authState.oauthClient.client_secret
        }
      };
    },
    async saveTokens(tokens) {
      authState = {
        ...authState,
        oauthTokens: {
          access_token: tokens.access_token,
          token_type: tokens.token_type,
          expires_in: tokens.expires_in,
          refresh_token: tokens.refresh_token,
          scope: tokens.scope,
          id_token: tokens.id_token
        }
      };
    },
    codeVerifier() {
      return void 0;
    },
    async saveCodeVerifier() {
    },
    clear() {
      authState = {};
    },
    async redirectToAuthorization() {
      throw new Error(
        "OAuth re-authorization must be performed in the Inspector web client (remote server cannot complete OAuth flows)"
      );
    },
    state() {
      return "";
    }
    // This provider implements only the members the remote backend transport
    // exercises (token get/save, client info, non-interactive redirect); the
    // interactive-only members the SDK never calls on the server side are stubbed
    // with narrower shapes (e.g. `codeVerifier()` returns `undefined`, not the
    // interface's `string | Promise<string>`), so it can't be assigned to the
    // full `OAuthClientProvider` without the double cast.
  };
  return {
    provider,
    setAuthState(next) {
      authState = cloneAuthState(next);
    },
    getAuthState() {
      return cloneAuthState(authState);
    }
  };
}

// ../../core/auth/node/secret-store.ts
var SERVICE_NAME = "mcp-inspector";
var keyringLoad;
var KeyringModuleShapeError = class extends Error {
  constructor(detail, options) {
    super(`@napi-rs/keyring loaded but ${detail}`, options);
    this.name = "KeyringModuleShapeError";
  }
};
var checkKeyringShape = (mod) => {
  try {
    if (typeof mod.AsyncEntry === "function" && typeof mod.findCredentialsAsync === "function") {
      return { ok: true, mod };
    }
    return {
      ok: false,
      err: new KeyringModuleShapeError(
        "did not expose AsyncEntry / findCredentialsAsync"
      )
    };
  } catch (err) {
    return {
      ok: false,
      err: new KeyringModuleShapeError(
        `its exports could not be read: ${err instanceof Error ? err.message : String(err)}`,
        { cause: err }
      )
    };
  }
};
var loadKeyring = () => {
  keyringLoad ??= import("@napi-rs/keyring").then(
    checkKeyringShape,
    (err) => ({ ok: false, err })
  );
  return keyringLoad;
};
function parseAccount(account) {
  const idx = account.indexOf(":");
  if (idx <= 0 || idx === account.length - 1) return null;
  return {
    serverId: account.slice(0, idx),
    field: account.slice(idx + 1)
  };
}
var buildAccount = (serverId, field) => `${serverId}:${field}`;
var SecretStoreUnavailableError = class extends Error {
  constructor(message, options) {
    super(message, options);
    this.name = "SecretStoreUnavailableError";
  }
};
var KeychainUnavailableError = class extends SecretStoreUnavailableError {
  constructor(cause) {
    const message = cause instanceof Error ? cause.message : String(cause);
    super(
      `OS keychain is not available. ${hintFor(cause, message)} Underlying error: ${message}`
    );
    this.name = "KeychainUnavailableError";
  }
};
var hintFor = (cause, message) => {
  if (cause instanceof KeyringModuleShapeError) {
    return `The @napi-rs/keyring package loaded but does not expose the API this build expects \u2014 most likely a version or packaging mismatch; reinstall the Inspector, and report this if it persists.`;
  }
  if (message.includes("Cannot find native binding")) {
    return `The @napi-rs/keyring platform package for this OS is missing or unavailable \u2014 reinstall the Inspector (for npx, clear the npx cache under your npm cache directory first).`;
  }
  return `On Linux, install libsecret / gnome-keyring.`;
};
async function probeKeyringAvailable() {
  const keyring = await loadKeyring();
  if (!keyring.ok) {
    const err = keyring.err;
    return {
      available: false,
      detail: err instanceof Error ? err.message : String(err)
    };
  }
  try {
    const entry = new keyring.mod.AsyncEntry(SERVICE_NAME, PROBE_ACCOUNT);
    await entry.getPassword();
    return { available: true };
  } catch (err) {
    return {
      available: false,
      detail: err instanceof Error ? err.message : String(err)
    };
  }
}
var PROBE_ACCOUNT = "__inspector:probe";
var KeyringSecretStore = class {
  /**
   * The intolerant read. Same lookup as {@link get}, minus the catch — a
   * keychain that cannot answer must not be reported as an empty one to a
   * caller that is about to write based on the answer.
   */
  async getStrict(serverId, field) {
    try {
      const keyring = await loadKeyring();
      if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
      const entry = new keyring.mod.AsyncEntry(
        SERVICE_NAME,
        buildAccount(serverId, field)
      );
      return await entry.getPassword() ?? null;
    } catch (err) {
      if (err instanceof KeychainUnavailableError) throw err;
      throw new KeychainUnavailableError(err);
    }
  }
  async get(serverId, field) {
    try {
      const keyring = await loadKeyring();
      if (!keyring.ok) return null;
      const entry = new keyring.mod.AsyncEntry(
        SERVICE_NAME,
        buildAccount(serverId, field)
      );
      const v = await entry.getPassword();
      return v ?? null;
    } catch {
      return null;
    }
  }
  async set(serverId, field, value) {
    try {
      const keyring = await loadKeyring();
      if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
      const entry = new keyring.mod.AsyncEntry(
        SERVICE_NAME,
        buildAccount(serverId, field)
      );
      await entry.setPassword(value);
    } catch (err) {
      if (err instanceof KeychainUnavailableError) throw err;
      throw new KeychainUnavailableError(err);
    }
  }
  async delete(serverId, field) {
    try {
      const keyring = await loadKeyring();
      if (!keyring.ok) return;
      const entry = new keyring.mod.AsyncEntry(
        SERVICE_NAME,
        buildAccount(serverId, field)
      );
      await entry.deleteCredential();
    } catch {
    }
  }
  async deleteAllForServer(serverId) {
    let creds;
    try {
      const keyring = await loadKeyring();
      if (!keyring.ok) return;
      creds = await keyring.mod.findCredentialsAsync(SERVICE_NAME);
    } catch {
      return;
    }
    const prefix = `${serverId}:`;
    for (const c of creds) {
      if (!c.account.startsWith(prefix)) continue;
      const parsed = parseAccount(c.account);
      if (!parsed || parsed.serverId !== serverId) continue;
      await this.delete(serverId, parsed.field);
    }
  }
};
async function secretStoreGetStrict(store, serverId, field) {
  return store.getStrict ? store.getStrict(serverId, field) : store.get(serverId, field);
}
async function secretStoreGetMany(store, requests) {
  if (store.getMany) return store.getMany(requests);
  const out = {};
  await Promise.all(
    requests.map(async ({ serverId, fields }) => {
      const entries = await Promise.all(
        fields.map(
          async (field) => [field, await store.get(serverId, field)]
        )
      );
      const found = {};
      for (const [field, value] of entries) {
        if (value !== null) found[field] = value;
      }
      out[serverId] = found;
    })
  );
  return out;
}
async function secretStoreSetMany(store, serverId, values) {
  if (store.setMany) return store.setMany(serverId, values);
  await Promise.all(
    Object.entries(values).map(
      ([field, value]) => store.set(serverId, field, value)
    )
  );
}
async function secretStoreIsDurable(store) {
  return store.isDurable ? store.isDurable() : true;
}
var InMemorySecretStore = class {
  map = /* @__PURE__ */ new Map();
  async get(serverId, field) {
    return this.map.get(buildAccount(serverId, field)) ?? null;
  }
  async set(serverId, field, value) {
    this.map.set(buildAccount(serverId, field), value);
  }
  async delete(serverId, field) {
    this.map.delete(buildAccount(serverId, field));
  }
  async deleteAllForServer(serverId) {
    const prefix = `${serverId}:`;
    for (const key of [...this.map.keys()]) {
      if (key.startsWith(prefix)) this.map.delete(key);
    }
  }
};
var SessionSecretStore = class extends InMemorySecretStore {
  async isDurable() {
    return false;
  }
};

// ../../core/auth/node/secret-store-selection.ts
import { randomUUID } from "crypto";
import * as fsSync from "fs";
import * as fs4 from "fs/promises";
import * as path4 from "path";

// ../../core/auth/node/file-secret-store.ts
import * as crypto2 from "crypto";
import * as fs3 from "fs/promises";
import * as path3 from "path";

// ../../core/auth/node/file-lock.ts
import nodeFs, { statSync, rmdirSync } from "fs";
import * as fs2 from "fs/promises";
import * as path2 from "path";
import properLockfile from "proper-lockfile";
var STALE_MS = 1e4;
var RETRY = {
  retries: 20,
  factor: 2,
  minTimeout: 20,
  maxTimeout: 1e3
};
var RETRY_BUDGET_MS = (() => {
  let total = 0;
  let delay = RETRY.minTimeout;
  for (let i = 0; i < RETRY.retries; i++) {
    total += Math.min(delay, RETRY.maxTimeout);
    delay *= RETRY.factor;
  }
  return total;
})();
var warned = /* @__PURE__ */ new Set();
function warnOnce(message) {
  if (warned.has(message)) return;
  warned.add(message);
  console.warn(`[mcp-inspector] ${message}`);
}
function isStuckOrHeld(err, lockPath) {
  return isHeldElsewhere(err) || identifySync(lockPath) !== null;
}
var isHeldElsewhere = (err) => err.code === "ELOCKED";
var describeError = (err) => err instanceof Error ? err.message : String(err);
var lockPathOf = (target) => `${target}.lock`;
function identifySync(lockPath) {
  try {
    const stat2 = statSync(lockPath);
    return { ino: stat2.ino, birthtimeMs: stat2.birthtimeMs };
  } catch {
    return null;
  }
}
function guardedFs(lockPath, owned, onRefused) {
  const mine = () => {
    if (owned.id === null) return true;
    const now = identifySync(lockPath);
    if (now === null) return false;
    return now.ino === owned.id.ino && now.birthtimeMs === owned.id.birthtimeMs;
  };
  const removeIfMine = () => {
    if (!mine()) {
      onRefused();
      return;
    }
    rmdirSync(lockPath);
  };
  return {
    ...nodeFs,
    // **Identity is captured here, not after `lock()` resolves.** `mkdir` is
    // the moment the directory becomes ours, and it is synchronous with
    // respect to this process: recording it in the callback, before yielding,
    // leaves no window. Capturing after the acquire promise settled — as this
    // did originally — spans the library's own `utimes`/`stat` probe, and a
    // waiter replacing our directory inside that span would have us record
    // *the winner's* identity as our own, after which the guard below would
    // cheerfully delete their lock.
    mkdir: (p, cb) => {
      nodeFs.mkdir(p, (err) => {
        if (!err) owned.id = identifySync(lockPath);
        cb(err);
      });
    },
    // Reported as success when refused: the library's bookkeeping should
    // forget this lock either way. Leaving it registered would hand the
    // `signal-exit` handler a record pointing at the winner's directory.
    rmdir: (_p, cb) => {
      try {
        removeIfMine();
        cb(null);
      } catch (err) {
        cb(err);
      }
    },
    /* v8 ignore next 3 -- @preserve: only reachable from proper-lockfile's
       signal-exit handler, i.e. at real process exit, which no in-process
       test can drive. Its logic is `removeIfMine`, covered via `rmdir`. */
    rmdirSync: () => {
      removeIfMine();
    }
  };
}
function acquire(target, retries, fsShim) {
  return properLockfile.lock(target, {
    realpath: false,
    stale: STALE_MS,
    retries,
    // Every directory removal the library performs — on release and from its
    // exit handler — routes through here. See `guardedFs`.
    fs: fsShim,
    // The library's default `onCompromised` *throws* — from a timer, with no
    // caller on the stack, so it lands as an uncaught exception and takes the
    // process down. This is also the library's *only* signal for the
    // stale-takeover race described at the top of this file — someone
    // declared our lock stale and replaced it — so it is the one place a user
    // learns the guarantee was lost. Worth saying; not worth killing an
    // Inspector session over.
    onCompromised: (err) => warnOnce(
      `The lock on the secrets file at ${target} was taken over by another process while a write was in progress (${err.message}). If a secret you just saved is missing, save it again.`
    )
  });
}
async function isFileLockHeld(filePath) {
  try {
    return await properLockfile.check(path2.resolve(filePath), {
      realpath: false,
      stale: STALE_MS
    });
  } catch {
    return false;
  }
}
async function openSecretFileLock(filePath) {
  const target = path2.resolve(filePath);
  await fs2.mkdir(path2.dirname(target), { recursive: true }).catch(() => {
  });
  const owned = { id: null };
  const fsShim = guardedFs(
    lockPathOf(target),
    owned,
    () => warnOnce(
      `The lock on the secrets file at ${target} was taken over by another process while a write was in progress, so it was left alone rather than removed. If a secret you just saved is missing, save it again.`
    )
  );
  let release;
  try {
    release = await acquire(target, 0, fsShim).catch((err) => {
      if (!isStuckOrHeld(err, lockPathOf(target))) throw err;
      return acquire(target, RETRY, fsShim);
    });
  } catch (err) {
    if (isStuckOrHeld(err, lockPathOf(target))) {
      throw new SecretStoreUnavailableError(
        `Could not save to the secrets file at ${target}: its lock (${lockPathOf(target)}) was still held after the ${Math.round(RETRY_BUDGET_MS / 1e3)} seconds this save waited. Its secrets are intact; the value you just entered was not saved. If no other Inspector is running, remove that lock and try again.`
      );
    }
    warnOnce(
      `Could not take a lock on the secrets file at ${target} (${describeError(err)}), so writes to it are not protected against another process writing at the same moment.`
    );
    return null;
  }
  return async () => {
    try {
      await release();
    } catch (err) {
      if (err.code === "ERELEASED") return;
      warnOnce(
        // **Conditional, not an instruction.** Two blanket versions of this
        // have now been wrong in opposite directions: "it expires on its own"
        // (false whenever the same `rmdir` that blocked us also blocks stale
        // takeover — `ENOTEMPTY`, `EACCES`, `EPERM`, `EROFS`), and "remove it
        // by hand" (false whenever the failure was transient, since the path
        // may by then hold a *different, live* Inspector's lock, and deleting
        // that destroys the exclusion of a process that did nothing wrong).
        //
        // `proper-lockfile` forwards whatever the filesystem returned, and
        // nothing here can tell a permanent refusal from a passing one. So
        // this reports what happened and states the two conditions the
        // operator can check for themselves, rather than asserting an outcome
        // this code does not know.
        `Could not release the lock on the secrets file at ${target} (${describeError(err)}). It may clear on its own \u2014 stale takeover reclaims a lock through the same directory removal, so it will not if whatever blocked this persists. If saves keep failing against this file and no other Inspector is running, remove ${lockPathOf(target)} by hand.`
      );
    }
  };
}
async function withSecretFileLock(filePath, fn) {
  const release = await openSecretFileLock(filePath);
  if (release === null) return fn();
  try {
    return await fn();
  } finally {
    await release();
  }
}

// ../../core/auth/node/file-secret-store.ts
var scrypt2 = (passphrase, salt, keylen, options) => new Promise((resolve9, reject) => {
  crypto2.scrypt(passphrase, salt, keylen, options, (err, key) => {
    if (err) reject(err);
    else resolve9(key);
  });
});
var FORMAT_VERSION = 1;
var CIPHER = "aes-256-gcm";
var KEY_BYTES = 32;
var IV_BYTES = 12;
var GCM_TAG_BYTES = 16;
var SCRYPT_N = 16384;
var SCRYPT_R = 8;
var SCRYPT_P = 1;
var SECRET_KEY_ENV = "MCP_INSPECTOR_SECRET_KEY";
var buildAccount2 = (serverId, field) => `${serverId}:${field}`;
function asSecretMap(value, filePath) {
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
    throw new SecretStoreUnavailableError(
      `The secrets file at ${filePath} does not hold a secret map (found ${Array.isArray(value) ? "an array" : typeof value}). Refusing to write, which would replace it. Move the file aside to start over.`
    );
  }
  for (const [key, entry] of Object.entries(value)) {
    if (typeof entry !== "string") {
      throw new SecretStoreUnavailableError(
        `The secrets file at ${filePath} holds a non-string value for "${key}". Refusing to write, which would replace it. Move the file aside to start over.`
      );
    }
  }
  return { ...value };
}
var SecretFileKeyMismatchError = class extends SecretStoreUnavailableError {
  constructor(filePath, hasKey) {
    super(
      hasKey ? `The secrets file at ${filePath} could not be decrypted with the current ${SECRET_KEY_ENV}. Refusing to write, which would overwrite the existing secrets. Restore the original passphrase, or delete the file to start over.` : `The secrets file at ${filePath} is encrypted but ${SECRET_KEY_ENV} is not set. Refusing to write, which would overwrite the existing secrets. Set the passphrase this file was written with, or delete the file to start over.`
    );
    this.name = "SecretFileKeyMismatchError";
  }
};
var encodeParts = (iv, tag, body) => `${iv.toString("base64")}.${tag.toString("base64")}.${body.toString("base64")}`;
function decodeParts(payload) {
  const parts = payload.split(".");
  if (parts.length !== 3) return null;
  const [iv, tag, body] = parts;
  return {
    iv: Buffer.from(iv, "base64"),
    tag: Buffer.from(tag, "base64"),
    body: Buffer.from(body, "base64")
  };
}
var MAX_WRITE_ATTEMPTS = 5;
var fileQueues = /* @__PURE__ */ new Map();
function sameMap(a, b) {
  const ak = Object.keys(a);
  if (ak.length !== Object.keys(b).length) return false;
  return ak.every(
    (k) => Object.prototype.hasOwnProperty.call(b, k) && a[k] === b[k]
  );
}
var isPositiveInt = (v) => typeof v === "number" && Number.isInteger(v) && v > 0;
function encryptedEnvelopeProblem(parsed) {
  if (!parsed.kdf || typeof parsed.data !== "string") {
    return "encrypted envelope is missing its kdf or data";
  }
  const parts = decodeParts(parsed.data);
  if (!parts) return "encrypted payload is not iv.tag.ciphertext";
  if (parts.iv.length !== IV_BYTES) {
    return `initialization vector is ${parts.iv.length} bytes, expected ${IV_BYTES}`;
  }
  if (parts.tag.length !== GCM_TAG_BYTES) {
    return `authentication tag is ${parts.tag.length} bytes, expected ${GCM_TAG_BYTES}`;
  }
  if (parts.body.length === 0) return "ciphertext is empty";
  const { algorithm, salt, N, r, p } = parsed.kdf;
  if (algorithm !== "scrypt") return `unsupported kdf "${String(algorithm)}"`;
  if (typeof salt !== "string" || Buffer.from(salt, "base64").length === 0) {
    return "kdf salt is missing or not base64";
  }
  if (!isPositiveInt(N) || N < 2 || (N & N - 1) !== 0) {
    return `kdf N is ${String(N)}, which is not a power of two above 1`;
  }
  if (!isPositiveInt(r) || !isPositiveInt(p)) {
    return `kdf r/p are ${String(r)}/${String(p)}, which are not positive integers`;
  }
  if (N > SCRYPT_N || r > SCRYPT_R || p > SCRYPT_P) {
    return `kdf cost parameters (N=${N}, r=${r}, p=${p}) exceed the supported maximum (N=${SCRYPT_N}, r=${SCRYPT_R}, p=${SCRYPT_P})`;
  }
  return null;
}
var FileSecretStore = class {
  filePath;
  passphrase;
  constructor(options) {
    this.filePath = options.filePath;
    const raw = options.passphrase ?? process.env[SECRET_KEY_ENV];
    this.passphrase = raw && raw.trim() ? raw : void 0;
  }
  /**
   * True when the *next write* will encrypt. This is the configured policy,
   * not the state of the file — see {@link readOnDiskEncryption}, which is
   * what the user-facing descriptor is built from.
   */
  get encrypted() {
    return this.passphrase !== void 0;
  }
  /**
   * What the file on disk is *currently* written as — `null` when there is
   * no file yet, or when it cannot be read or parsed.
   *
   * Separate from {@link encrypted} because the two genuinely disagree for a
   * whole session: adding `MCP_INSPECTOR_SECRET_KEY` to an install that
   * already has a plaintext file flips `encrypted` to true immediately,
   * while the bytes stay readable until the next `set`. A descriptor built
   * from the policy would tell that user "File (encrypted)" while their
   * existing secrets sat in the clear — the precise reassurance this whole
   * subsystem exists not to give.
   *
   * Reads the envelope, and for an encrypted one **attempts to open it**.
   * Structure alone is not access: a well-formed file this passphrase cannot
   * decrypt answers `null` from every `get` and refuses every `set`, so
   * reporting it as a healthy "encrypted" store hands the quiet footer to
   * exactly the state the unreadable one exists to warn about. The
   * authentication costs one scrypt derivation per call, which is once per
   * `/api/config`, and the decrypted bytes are discarded.
   *
   * `absent` and `unreadable` are kept apart deliberately. Collapsing them
   * (as a bare `null` did) means an existing file that is corrupt, or
   * written in an envelope this build does not know, is reported as "no
   * file yet" — and the descriptor then falls back to the configured write
   * policy, so a passphrase-configured install claims "Encrypted file"
   * about a file whose encryption state was never established and which
   * `set` is in fact about to refuse.
   */
  async readOnDiskEncryption() {
    let raw;
    try {
      raw = await readStoreFile(this.filePath);
    } catch (err) {
      return {
        state: "unreadable",
        detail: err instanceof Error ? err.message : String(err)
      };
    }
    if (raw === null) return { state: "absent" };
    try {
      const parsed = JSON.parse(raw);
      if (parsed.version !== FORMAT_VERSION) {
        return {
          state: "unreadable",
          detail: `format version ${String(parsed.version)}, expected ${FORMAT_VERSION}`
        };
      }
      if (parsed.encryption === "none") {
        try {
          asSecretMap(parsed.secrets ?? {}, this.filePath);
        } catch (err) {
          return {
            state: "unreadable",
            detail: err instanceof Error ? err.message : String(err)
          };
        }
        return { state: "plaintext" };
      }
      if (parsed.encryption === CIPHER) {
        const problem = encryptedEnvelopeProblem(parsed);
        if (problem) return { state: "unreadable", detail: problem };
        try {
          await this.readMap();
        } catch (err) {
          return {
            state: "unreadable",
            detail: err instanceof SecretFileKeyMismatchError ? `it cannot be decrypted with the current ${SECRET_KEY_ENV}` : err instanceof Error ? err.message : String(err)
          };
        }
        return { state: "encrypted" };
      }
      return {
        state: "unreadable",
        detail: `unsupported envelope (encryption="${String(parsed.encryption)}", version=${String(parsed.version)})`
      };
    } catch {
      return { state: "unreadable", detail: "not valid JSON" };
    }
  }
  async deriveKey(kdf) {
    return scrypt2(
      // Only reached when the passphrase is set — every caller checks first,
      // which is what makes the non-null assertion safe here rather than a
      // widened `string | undefined` threaded through the crypto helpers.
      this.passphrase,
      Buffer.from(kdf.salt, "base64"),
      KEY_BYTES,
      { N: kdf.N, r: kdf.r, p: kdf.p }
    );
  }
  /**
   * Read and decode the file into a plain map.
   *
   * Distinguishes three outcomes that callers treat differently: `null` for
   * "no file yet" (a first write should create one), a map for success, and a
   * throw for "there is a file and we cannot read it" — which must never be
   * flattened into an empty map, or `set` would cheerfully overwrite it.
   */
  /**
   * Every secret in the file, keyed by `${serverId}:${field}` — `null` when
   * there is no file.
   *
   * Exists for one caller: the hand-off performed when a keychain becomes
   * available on a box that had been falling back to a file (see
   * `absorbFileSecretsIntoKeyring`). Nothing in the normal request path
   * enumerates secrets, and deliberately so — this is the widest possible
   * read of the store and it should stay a one-purpose seam rather than a
   * general convenience.
   *
   * Throws for the same reasons `get` swallows: an unreadable or
   * undecryptable file must not present as "no secrets", because the caller
   * would conclude there was nothing to migrate and move on.
   */
  async readAll() {
    return this.serialize(() => this.readMap());
  }
  async readMap() {
    const raw = await readStoreFile(this.filePath);
    if (raw === null) return null;
    let parsed;
    try {
      parsed = JSON.parse(raw);
    } catch (err) {
      throw new SecretStoreUnavailableError(
        `The secrets file at ${this.filePath} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
      );
    }
    if (parsed.version !== FORMAT_VERSION) {
      throw new SecretStoreUnavailableError(
        `The secrets file at ${this.filePath} declares format version ${String(parsed.version)}, but this Inspector only understands version ${FORMAT_VERSION}. It was probably written by a newer Inspector; upgrade, or move the file aside to start over.`
      );
    }
    if (parsed.encryption === "none") {
      return asSecretMap(parsed.secrets ?? {}, this.filePath);
    }
    if (parsed.encryption !== CIPHER || !parsed.kdf || !parsed.data) {
      throw new SecretStoreUnavailableError(
        `The secrets file at ${this.filePath} declares an unsupported format (encryption="${String(parsed.encryption)}", version=${String(parsed.version)}). It may have been written by a newer Inspector.`
      );
    }
    if (!this.passphrase)
      throw new SecretFileKeyMismatchError(this.filePath, false);
    const structural = encryptedEnvelopeProblem(parsed);
    if (structural) {
      throw new SecretStoreUnavailableError(
        `The secrets file at ${this.filePath} is not readable: ${structural}. Restoring a passphrase will not repair it; the file itself is malformed.`
      );
    }
    const parts = decodeParts(parsed.data);
    if (!parts) throw new SecretFileKeyMismatchError(this.filePath, true);
    let plain;
    try {
      const key = await this.deriveKey(parsed.kdf);
      const decipher = crypto2.createDecipheriv(CIPHER, key, parts.iv);
      decipher.setAuthTag(parts.tag);
      plain = Buffer.concat([
        decipher.update(parts.body),
        decipher.final()
      ]).toString("utf-8");
    } catch {
      throw new SecretFileKeyMismatchError(this.filePath, true);
    }
    let payload;
    try {
      payload = JSON.parse(plain);
    } catch (err) {
      throw new SecretStoreUnavailableError(
        `The secrets file at ${this.filePath} decrypted successfully but its contents are not valid JSON: ${err instanceof Error ? err.message : String(err)}. The passphrase is correct; the file itself is corrupt.`
      );
    }
    return asSecretMap(payload, this.filePath);
  }
  async writeMap(map) {
    let file;
    if (this.passphrase) {
      const kdf = {
        algorithm: "scrypt",
        // A fresh salt per write, not per file: rewriting with the same salt
        // and a fresh IV would still be safe for GCM, but re-salting costs one
        // scrypt derivation we were doing anyway and removes the question.
        salt: crypto2.randomBytes(16).toString("base64"),
        N: SCRYPT_N,
        r: SCRYPT_R,
        p: SCRYPT_P
      };
      const key = await this.deriveKey(kdf);
      const iv = crypto2.randomBytes(IV_BYTES);
      const cipher = crypto2.createCipheriv(CIPHER, key, iv);
      const body = Buffer.concat([
        cipher.update(JSON.stringify(map), "utf-8"),
        cipher.final()
      ]);
      file = {
        version: FORMAT_VERSION,
        encryption: CIPHER,
        kdf,
        data: encodeParts(iv, cipher.getAuthTag(), body)
      };
    } else {
      file = { version: FORMAT_VERSION, encryption: "none", secrets: map };
    }
    await writeStoreFile(this.filePath, `${JSON.stringify(file, null, 2)}
`);
  }
  /**
   * Apply a mutation to the file, under a cross-process lock, and confirm it
   * survived.
   *
   * **Two mechanisms, and they answer different questions** — this is not
   * belt-and-braces.
   *
   * The lock ({@link withSecretFileLock}, #2082) is the one that provides
   * mutual exclusion: a durable secrets file is shared state, and a second
   * Inspector on the same box — a CLI run beside a web session is the
   * ordinary case, not a contrived one — otherwise reads the same "before"
   * map and then atomically replaces the file, dropping whatever the first
   * process had just added. Both writes report success; one secret is simply
   * gone. Held across the read *and* the write, so no other holder can
   * observe or replace the map in between.
   *
   * The verify covers what a lock structurally cannot. A lock is an advisory
   * convention between the processes that take it, so it says nothing about
   * a writer that does not: an editor, a restored backup, a `jq` one-liner,
   * or an Inspector predating #2082. It is also the fallback when the lock
   * is unavailable at all — `withSecretFileLock` runs the body anyway on a
   * read-only or lock-hostile directory rather than failing a `set` on
   * exactly the deployments this store exists for (#1848, #1905).
   *
   * So: read `M0`, apply the mutation to get `M1`, write it, then read back
   * `M2`. If `M2` equals `M1` nothing interleaved. If it does not, someone
   * wrote between our write and our read — so re-apply onto what they left
   * and try again.
   *
   * The comparison is over the **whole map**, not just the entry we touched.
   * Checking only our own key would pass in precisely the case that loses
   * data: our entry is present, and it is the *other* writer's that is gone.
   * When the verify does fire, the writer that was clobbered is the one that
   * repairs it — A writes `MA`, B writes `MB` over it, B verifies `MB`
   * correctly, A verifies, sees `M2 !== MA`, re-applies onto `MB`.
   *
   * **What is left, stated without overselling.** Against an unlocked writer
   * the verify is not mutual exclusion: it only catches a clobber that has
   * already landed, so ordering the two as write-A, verify-A, write-B,
   * verify-B loses A's entry with both reporting success. That residual is
   * now confined to a writer outside this codebase, or to a directory where
   * no lock could be taken — and the latter announces itself, once, on the
   * console.
   *
   * Reads deliberately do not participate: `writeStoreFile` is atomic
   * (write-temp-then-rename), so a reader sees either the old file or the
   * new one, never a torn one. Taking the lock for them would serialize
   * every `GET /api/servers` behind whatever else holds it, to prevent
   * nothing.
   */
  async mutate(apply) {
    await withSecretFileLock(this.filePath, () => this.mutateLocked(apply));
  }
  /** {@link mutate}'s body, with the lock already held. */
  async mutateLocked(apply) {
    for (let attempt = 0; attempt < MAX_WRITE_ATTEMPTS; attempt++) {
      let current;
      try {
        current = await this.readMap() ?? {};
      } catch (err) {
        if (err instanceof SecretStoreUnavailableError) throw err;
        throw new SecretStoreUnavailableError(
          `Could not read the secrets file at ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
        );
      }
      const next = apply(current);
      if (next === null) return;
      try {
        await this.writeMap(next);
      } catch (err) {
        throw new SecretStoreUnavailableError(
          `Could not write the secrets file at ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
        );
      }
      let observed;
      try {
        observed = await this.readMap() ?? {};
      } catch {
        continue;
      }
      if (sameMap(next, observed)) return;
    }
    throw new SecretStoreUnavailableError(
      `Could not write the secrets file at ${this.filePath}: another process kept overwriting it (gave up after ${MAX_WRITE_ATTEMPTS} attempts). Its secrets are intact; the value you just entered was not saved.`
    );
  }
  /**
   * Run `fn` with the file to itself, so concurrent callers can't lose
   * writes.
   *
   * Keyed on the **resolved path, process-wide** rather than on this
   * instance. Two `FileSecretStore`s pointing at one file are ordinary —
   * the resolved store holds one and `absorbFileSecretsIntoKeyring`
   * constructs another — and a per-instance queue does not order them
   * against each other at all. They then race in-process, which the
   * optimistic verify is only *supposed* to catch for writers in different
   * processes: both read the same map, both write, and whichever verifies
   * before the other's write lands sees its own value and returns happy.
   * Measured, not theorised — two instances racing `set` lost an entry on
   * the first run of the convergence test below.
   *
   * So the in-process case is made correct by construction here, and
   * {@link mutate}'s cross-process lock covers what this cannot see: a
   * second Inspector process.
   *
   * The queue is *also* what keeps that lock usable. `proper-lockfile` is
   * not reentrant — a second `lock()` on a path this process already holds
   * fails `ELOCKED`, which is indistinguishable from a genuine remote
   * holder. Serializing here means the lock is only ever contended between
   * processes, which is the only contention it is asked to arbitrate.
   */
  serialize(fn) {
    const key = path3.resolve(this.filePath);
    const run = (fileQueues.get(key) ?? Promise.resolve()).then(fn);
    const settled = run.then(
      () => {
      },
      () => {
      }
    );
    fileQueues.set(key, settled);
    void settled.then(() => {
      if (fileQueues.get(key) === settled) fileQueues.delete(key);
    });
    return run;
  }
  /**
   * One read and one derivation for an entire rehydration.
   *
   * `get` reads and decrypts the whole file, so the cost is per *call*, not
   * per field — which is why the batch has to span servers. A per-server
   * version of this shipped first and left a 20-server catalog paying 20
   * serialized derivations, the same stall it was written to remove.
   * Tolerant like `get`: an unreadable store yields no fields for anyone
   * rather than throwing.
   */
  async getMany(requests) {
    let map;
    try {
      map = await this.serialize(() => this.readMap());
    } catch {
      map = null;
    }
    const out = {};
    for (const { serverId, fields } of requests) {
      const found = {};
      for (const field of fields) {
        const value = map?.[buildAccount2(serverId, field)];
        if (value !== void 0) found[field] = value;
      }
      out[serverId] = found;
    }
    return out;
  }
  /**
   * The intolerant read — same lookup as {@link get}, minus the catch.
   *
   * Without this, `secretStoreGetStrict` fell back to `get` for the file
   * store and turned every read failure into `null`. Both plaintext
   * migrations *write* on `null` and then strip the disk copy, so a
   * transient read error that cleared before the write let an older
   * on-disk value replace a newer stored one — the same inversion the
   * strict seam was introduced to prevent, still open on the store the
   * seam was introduced for.
   */
  async getStrict(serverId, field) {
    try {
      const map = await this.serialize(() => this.readMap());
      return map?.[buildAccount2(serverId, field)] ?? null;
    } catch (err) {
      if (err instanceof SecretStoreUnavailableError) throw err;
      throw new SecretStoreUnavailableError(
        `Could not read the secrets file at ${this.filePath}: ${err instanceof Error ? err.message : String(err)}`
      );
    }
  }
  async get(serverId, field) {
    try {
      const map = await this.serialize(() => this.readMap());
      return map?.[buildAccount2(serverId, field)] ?? null;
    } catch {
      return null;
    }
  }
  /**
   * One read-modify-write-verify cycle for a whole server's fields.
   *
   * `set` pays that cycle — and, encrypted, three scrypt derivations — per
   * call, serialized behind this store's queue. The settings form resends a
   * server's full `env` map on any edit, so a 10-variable stdio server spent
   * ~30 derivations saving an unrelated timeout change.
   */
  async setMany(serverId, values) {
    if (Object.keys(values).length === 0) return;
    await this.serialize(
      () => this.mutate((map) => {
        const next = { ...map };
        for (const [field, value] of Object.entries(values)) {
          next[buildAccount2(serverId, field)] = value;
        }
        return next;
      })
    );
  }
  async set(serverId, field, value) {
    await this.serialize(
      () => this.mutate((map) => ({
        ...map,
        [buildAccount2(serverId, field)]: value
      }))
    );
  }
  async delete(serverId, field) {
    await this.deleteWhere((key) => key === buildAccount2(serverId, field));
  }
  async deleteAllForServer(serverId) {
    const prefix = `${serverId}:`;
    await this.deleteWhere((key) => key.startsWith(prefix));
  }
  /**
   * Shared delete body. Silent on every failure, matching the keyring store:
   * every reason a delete can fail collapses to "the entry isn't there
   * anymore", and `set` is the operation that reports a broken store.
   */
  async deleteWhere(match) {
    try {
      await this.serialize(
        () => this.mutate((map) => {
          const keys = Object.keys(map).filter(match);
          if (keys.length === 0) return null;
          const next = { ...map };
          for (const key of keys) delete next[key];
          return next;
        })
      );
    } catch {
    }
  }
};
async function tightenSecretFilePermissions(filePath) {
  try {
    await fs3.chmod(filePath, 384);
  } catch {
  }
  return readSecretFilePermissions(filePath);
}
async function readSecretFilePermissions(filePath) {
  try {
    if (process.platform === "win32") {
      await fs3.stat(filePath);
      return { state: "unknown", detail: "not checked on Windows" };
    }
    const mode = (await fs3.stat(filePath)).mode & 511;
    if (mode === 384) return { state: "ok" };
    return { state: "loose", mode };
  } catch (err) {
    const code = err.code;
    if (code === "ENOENT") {
      return { state: "absent" };
    }
    return { state: "unknown", detail: code ?? "unknown error" };
  }
}

// ../../core/auth/node/secret-store-selection.ts
var SECRET_STORE_ENV = "MCP_INSPECTOR_SECRET_STORE";
var SECRET_FILE_ENV = "MCP_INSPECTOR_SECRET_FILE";
var STORAGE_DIR_ENV = "MCP_STORAGE_DIR";
var KINDS = ["keyring", "file", "memory"];
function parseSecretStoreEnv(raw) {
  const value = raw?.trim().toLowerCase();
  if (!value) return void 0;
  if (KINDS.includes(value)) return value;
  console.warn(
    `[mcp-inspector] Ignoring ${SECRET_STORE_ENV}="${raw}": expected one of ${KINDS.join(", ")}.`
  );
  return void 0;
}
function defaultSecretFilePath() {
  const override = process.env[SECRET_FILE_ENV]?.trim();
  if (override) return path4.resolve(override);
  const storageDir = process.env[STORAGE_DIR_ENV]?.trim();
  if (storageDir) return path4.resolve(storageDir, "secrets.json");
  const homeDir = process.env.HOME || process.env.USERPROFILE || ".";
  return path4.resolve(homeDir, ".mcp-inspector", "secrets.json");
}
function isContainer() {
  if (process.env.KUBERNETES_SERVICE_HOST) return true;
  try {
    if (fsSync.existsSync("/.dockerenv") || fsSync.existsSync("/run/.containerenv")) {
      return true;
    }
  } catch {
  }
  try {
    const cgroup = fsSync.readFileSync("/proc/1/cgroup", "utf-8");
    return /\b(docker|containerd|kubepods|podman|libpod|lxc)\b/.test(cgroup);
  } catch {
    return false;
  }
}
function nearestExisting(dir) {
  let current = path4.resolve(dir);
  while (!fsSync.existsSync(current)) {
    const parent = path4.dirname(current);
    if (parent === current) return null;
    current = parent;
  }
  return current;
}
function unescapeMountPoint(point) {
  return point.replace(
    /\\(\d{3})/g,
    (_match, octal) => String.fromCharCode(parseInt(octal, 8))
  );
}
var EPHEMERAL_FSTYPES = /* @__PURE__ */ new Set(["tmpfs", "ramfs"]);
function readMountPoints() {
  try {
    return fsSync.readFileSync("/proc/self/mountinfo", "utf-8").split("\n").flatMap((line) => {
      const point = line.split(" ")[4];
      if (!point) return [];
      const after = line.split(" - ")[1];
      const fstype = after?.trim().split(/\s+/)[0];
      if (fstype && EPHEMERAL_FSTYPES.has(fstype)) return [];
      return [unescapeMountPoint(point)];
    }).filter((point) => point !== "/");
  } catch {
    return null;
  }
}
function isOnMountPoint(dir) {
  try {
    const nearest = nearestExisting(dir);
    if (nearest === null) return false;
    let current = nearest;
    try {
      current = fsSync.realpathSync(nearest);
    } catch {
    }
    const mounts = readMountPoints();
    if (mounts !== null) {
      return mounts.some(
        (point) => current === point || current.startsWith(`${point}/`)
      );
    }
    const parent = path4.dirname(current);
    if (parent === current) return false;
    return fsSync.statSync(current).dev !== fsSync.statSync("/").dev;
  } catch {
    return false;
  }
}
async function buildStore(kind, reason, detail) {
  if (kind === "keyring") {
    return {
      store: new KeyringSecretStore(),
      info: { kind, reason, durable: true }
    };
  }
  if (kind === "memory") {
    return {
      store: new SessionSecretStore(),
      info: { kind, reason, durable: false, detail }
    };
  }
  const filePath = defaultSecretFilePath();
  const store = new FileSecretStore({ filePath });
  await tightenSecretFilePermissions(filePath);
  return {
    store,
    info: await describeFileStore(store, filePath, kind, reason, detail)
  };
}
async function describeFileStore(store, filePath, kind, reason, detail) {
  const onDisk = await store.readOnDiskEncryption();
  const unreadable = onDisk.state === "unreadable" ? onDisk.detail : void 0;
  const plaintext = onDisk.state === "absent" ? !store.encrypted : onDisk.state === "plaintext";
  const perms = await readSecretFilePermissions(filePath);
  return {
    kind,
    reason,
    path: filePath,
    // Omitted entirely when the envelope could not be read, so no consumer
    // can read a default out of it.
    ...unreadable === void 0 ? { plaintext } : {},
    ...unreadable !== void 0 ? { encryptionUnknown: unreadable } : {},
    ...perms.state === "loose" ? { looseMode: perms.mode } : {},
    ...perms.state === "unknown" ? { permissionsUnknown: perms.detail } : {},
    // Only meaningful while the two disagree; omitted otherwise so the
    // payload does not carry a flag that says nothing.
    ...unreadable === void 0 && plaintext && store.encrypted ? { pendingEncryption: true } : {},
    durable: true,
    detail
  };
}
function chooseFallbackKind(opts) {
  return opts.container && !opts.mounted ? "memory" : "file";
}
function warnAboutSecretStorage(info) {
  if (info.reason === "fallback") {
    console.warn(
      `
[mcp-inspector] The OS keychain is not available, so secrets will be kept in: ${secretStorageSummary(info)}.` + (info.detail ? `
[mcp-inspector] Keychain error: ${info.detail}` : "") + `
[mcp-inspector] Set ${SECRET_STORE_ENV}=keyring|file|memory to choose explicitly.`
    );
  }
  const caveat = secretStorageCaveat(info);
  if (caveat) console.warn(`[mcp-inspector] ${caveat}`);
}
var resolved;
function resolveSecretStore() {
  resolved ??= (async () => {
    const configured = parseSecretStoreEnv(process.env[SECRET_STORE_ENV]);
    if (configured) {
      const result2 = await buildStore(configured, "configured");
      if (configured === "keyring") {
        await absorbFileSecretsIntoKeyring(result2.store);
      }
      warnAboutSecretStorage(result2.info);
      return result2;
    }
    const probe = await probeKeyringAvailable();
    if (probe.available) {
      const result2 = await buildStore("keyring", "default");
      await absorbFileSecretsIntoKeyring(result2.store);
      return result2;
    }
    const kind = chooseFallbackKind({
      container: isContainer(),
      mounted: isOnMountPoint(path4.dirname(defaultSecretFilePath()))
    });
    const result = await buildStore(kind, "fallback", probe.detail);
    warnAboutSecretStorage(result.info);
    return result;
  })();
  return resolved;
}
async function absorbFileSecretsIntoKeyring(keyring) {
  const filePath = defaultSecretFilePath();
  if (!await anythingToMigrate(filePath)) return;
  const claimed = await withSecretFileLock(filePath, async () => {
    await recoverOrphanedSnapshots(filePath);
    if (!fsSync.existsSync(filePath)) return null;
    const staged = `${filePath}.migrating-${process.pid}-${randomUUID()}`;
    try {
      await fs4.rename(filePath, staged);
    } catch (err) {
      const code = err.code;
      if (code !== "ENOENT") {
        console.warn(
          `
[mcp-inspector] Could not claim the secrets file at ${filePath} for migration into the OS keychain (${code ?? "unknown error"}), so it has been left in place. Its secrets are not visible to this session.`
        );
      }
      return null;
    }
    const releaseSnapshot = await openSecretFileLock(staged).catch(() => null);
    return { staged, releaseSnapshot };
  }).catch((err) => {
    console.warn(
      `
[mcp-inspector] Could not lock the secrets file at ${filePath} to migrate it into the OS keychain (${err.message}), so it has been left in place. Its secrets are not visible to this session; the next run will try again.`
    );
    return null;
  });
  if (claimed === null) return;
  try {
    await handOffStagedSecrets(claimed.staged, filePath, keyring);
  } finally {
    await claimed.releaseSnapshot?.();
  }
}
async function handOffStagedSecrets(staged, filePath, keyring) {
  const file = new FileSecretStore({ filePath: staged });
  const absorbed = await handOff(file, staged, keyring);
  if (absorbed) {
    try {
      await fs4.rm(staged, { force: true });
      console.warn(
        `
[mcp-inspector] The OS keychain is available again. Secrets kept in ${filePath} while it was not have been moved into the keychain, and the file has been removed.`
      );
    } catch (err) {
      console.warn(
        `
[mcp-inspector] Secrets were moved into the OS keychain, but the temporary copy at ${staged} could not be removed (${err instanceof Error ? err.message : String(err)}). Delete it once you have confirmed the keychain has your secrets.`
      );
    }
    return;
  }
  try {
    await fs4.link(staged, filePath);
    await fs4.rm(staged, { force: true });
  } catch {
    console.warn(
      `
[mcp-inspector] Could not restore ${staged} to ${filePath}; both files exist and the snapshot holds the secrets that were not migrated. Merge it by hand, or delete it once you have confirmed nothing is missing.`
    );
  }
}
var isSnapshotName = (name, base) => name.startsWith(`${base}.migrating-`) && !name.endsWith(".lock");
async function anythingToMigrate(filePath) {
  const base = path4.basename(filePath);
  try {
    return (await fs4.readdir(path4.dirname(filePath))).some(
      (name) => name === base || isSnapshotName(name, base)
    );
  } catch (err) {
    const code = err.code;
    return code !== "ENOENT" && code !== "ENOTDIR";
  }
}
async function recoverOrphanedSnapshots(filePath) {
  const dir = path4.dirname(filePath);
  const base = path4.basename(filePath);
  let names;
  try {
    names = (await fs4.readdir(dir)).filter((n) => isSnapshotName(n, base));
  } catch {
    return;
  }
  for (const name of names) {
    const orphan = path4.join(dir, name);
    if (await isFileLockHeld(orphan)) continue;
    try {
      await fs4.link(orphan, filePath);
      await fs4.rm(orphan, { force: true });
      console.warn(
        `
[mcp-inspector] Recovered secrets left behind by an interrupted migration (${orphan} \u2192 ${filePath}).`
      );
    } catch {
      console.warn(
        `
[mcp-inspector] Found secrets from an interrupted migration at ${orphan}, and a current ${filePath} already exists, so it was left alone. It may hold values the current file does not.`
      );
    }
  }
}
async function handOff(file, filePath, keyring) {
  let entries;
  try {
    entries = await file.readAll();
  } catch (err) {
    console.warn(
      `
[mcp-inspector] The OS keychain is available again, but the secrets file at ${filePath} could not be read, so its contents were left there: ${err instanceof Error ? err.message : String(err)}`
    );
    return false;
  }
  if (!entries || Object.keys(entries).length === 0) return false;
  let complete = true;
  try {
    for (const [account, value] of Object.entries(entries)) {
      const parsed = parseAccount(account);
      if (!parsed) {
        complete = false;
        continue;
      }
      const existing = await secretStoreGetStrict(
        keyring,
        parsed.serverId,
        parsed.field
      );
      if (existing === null) {
        await keyring.set(parsed.serverId, parsed.field, value);
      }
    }
  } catch (err) {
    console.warn(
      `
[mcp-inspector] Could not move the secrets file at ${filePath} into the OS keychain, so it has been left in place: ${err instanceof Error ? err.message : String(err)}`
    );
    return false;
  }
  if (!complete) {
    console.warn(
      `
[mcp-inspector] Secrets from ${filePath} have been copied into the OS keychain, but at least one entry could not be read (its key is not in \`serverId:field\` form). The file has been left in place so nothing is lost.`
    );
    return false;
  }
  return true;
}
async function getSecretStorageInfo() {
  const { store, info } = await resolveSecretStore();
  if (info.kind !== "file" || !(store instanceof FileSecretStore)) return info;
  return describeFileStore(
    store,
    store.filePath,
    "file",
    info.reason,
    info.detail
  );
}
var DeferredSecretStore = class {
  async target() {
    return (await resolveSecretStore()).store;
  }
  async isDurable() {
    return secretStoreIsDurable(await this.target());
  }
  /**
   * Forwarded, and its absence was a real defect rather than an omission:
   * this is the store production actually uses, so a `secretStoreGetStrict`
   * call against it fell through to the tolerant `get` and the strictness
   * existed only in tests that injected a concrete store.
   */
  async getStrict(serverId, field) {
    return secretStoreGetStrict(await this.target(), serverId, field);
  }
  async getMany(requests) {
    return secretStoreGetMany(await this.target(), requests);
  }
  async get(serverId, field) {
    return (await this.target()).get(serverId, field);
  }
  async set(serverId, field, value) {
    await (await this.target()).set(serverId, field, value);
  }
  async setMany(serverId, values) {
    await secretStoreSetMany(await this.target(), serverId, values);
  }
  async delete(serverId, field) {
    await (await this.target()).delete(serverId, field);
  }
  async deleteAllForServer(serverId) {
    await (await this.target()).deleteAllForServer(serverId);
  }
};
function defaultSecretStore() {
  return new DeferredSecretStore();
}

// ../../core/client/config-parse.ts
import { z } from "zod";
function isAbsoluteHttpUrl(value) {
  let url;
  try {
    url = new URL(value.trim());
  } catch {
    return false;
  }
  if (url.protocol !== "http:" && url.protocol !== "https:") return false;
  return isRealHost(url.hostname);
}
function isRealHost(hostname) {
  if (hostname === "localhost") return true;
  if (hostname.startsWith("[")) return hostname.endsWith("]");
  const labels = hostname.split(".");
  return labels.length >= 2 && labels.every((label) => label !== "");
}
var HttpUrlStringSchema = z.string().min(1).superRefine((val, ctx) => {
  const trimmed = val.trim();
  if (!isAbsoluteHttpUrl(trimmed)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: `Invalid URL: "${trimmed}" \u2014 must be an http(s) URL (e.g. https://idp.example.com)`
    });
  }
});
var CIMD_METADATA_URL_INVALID_ERROR = "Must be a valid URL, like https://example.com/oauth/client.json";
var CIMD_METADATA_URL_HTTPS_ERROR = "CIMD client metadata URL must use HTTPS";
var CIMD_METADATA_URL_PATH_ERROR = "Must include a path (not the site root), like https://example.com/oauth/client.json";
function getCimdClientMetadataUrlError(value) {
  const trimmed = value.trim();
  if (trimmed === "") return void 0;
  if (!isAbsoluteHttpUrl(trimmed)) {
    return CIMD_METADATA_URL_INVALID_ERROR;
  }
  try {
    const url = new URL(trimmed);
    if (url.protocol !== "https:") {
      return CIMD_METADATA_URL_HTTPS_ERROR;
    }
    if (url.pathname === "/" || url.pathname === "") {
      return CIMD_METADATA_URL_PATH_ERROR;
    }
  } catch {
    return CIMD_METADATA_URL_INVALID_ERROR;
  }
  return void 0;
}
function refineCimdMetadataUrl(val, ctx, required) {
  const trimmed = val.trim();
  if (trimmed === "") {
    if (required) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "CIMD client metadata URL is required when CIMD is enabled"
      });
    }
    return;
  }
  const error = getCimdClientMetadataUrlError(trimmed);
  if (error) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: error
    });
  }
}
var CimdConfigSchema = z.object({
  enabled: z.boolean(),
  clientMetadataUrl: z.string()
}).superRefine((data, ctx) => {
  refineCimdMetadataUrl(data.clientMetadataUrl, ctx, data.enabled === true);
});
var EnterpriseManagedAuthIdpConfigSchema = z.object({
  issuer: HttpUrlStringSchema,
  clientId: z.string().min(1),
  clientSecret: z.string().optional()
});
var ClientConfigSchema = z.object({
  enterpriseManagedAuth: z.object({
    enabled: z.boolean().optional(),
    idp: EnterpriseManagedAuthIdpConfigSchema
  }).optional(),
  cimd: CimdConfigSchema.optional()
});
function parseClientConfig(raw) {
  return ClientConfigSchema.parse(raw);
}
function formatClientConfigLoadError(error) {
  if (error instanceof z.ZodError) {
    const issue = error.issues[0];
    if (issue) {
      const field = issue.path.length > 0 ? issue.path.map(String).join(".") : "config";
      return `${field}: ${issue.message}`;
    }
    return "Invalid client.json shape";
  }
  if (error instanceof Error) {
    return error.message;
  }
  return String(error);
}

// ../../core/client/secrets.ts
var CLIENT_KEYCHAIN_ID = "client";
function hasClientPlaintextSecret(config) {
  const secret = config.enterpriseManagedAuth?.idp?.clientSecret;
  return typeof secret === "string" && secret.length > 0;
}
function extractSecretsFromClientConfig(config) {
  const secrets = {};
  const ema = config.enterpriseManagedAuth;
  if (!ema?.idp?.clientSecret) {
    return { stripped: config, secrets };
  }
  const { clientSecret, ...idpRest } = ema.idp;
  secrets[SECRET_FIELD_IDP_CLIENT_SECRET] = clientSecret;
  return {
    stripped: {
      ...config,
      enterpriseManagedAuth: {
        ...ema,
        idp: idpRest
      }
    },
    secrets
  };
}
function mergeSecretsIntoClientConfig(config, secrets) {
  const idpSecret = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
  const ema = config.enterpriseManagedAuth;
  if (!idpSecret || !ema?.idp) {
    return config;
  }
  return {
    ...config,
    enterpriseManagedAuth: {
      ...ema,
      idp: {
        ...ema.idp,
        clientSecret: idpSecret
      }
    }
  };
}

// ../../core/client/node-persistence.ts
async function readIdpSecretFromKeychain(secretStore) {
  const secret = await secretStore.get(
    CLIENT_KEYCHAIN_ID,
    SECRET_FIELD_IDP_CLIENT_SECRET
  );
  if (!secret) return {};
  return { [SECRET_FIELD_IDP_CLIENT_SECRET]: secret };
}
async function migrateClientPlaintextSecret(filePath, config, secretStore) {
  const { stripped, secrets } = extractSecretsFromClientConfig(config);
  const value = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
  if (!value) return config;
  try {
    const existing = await secretStoreGetStrict(
      secretStore,
      CLIENT_KEYCHAIN_ID,
      SECRET_FIELD_IDP_CLIENT_SECRET
    );
    if (existing === null) {
      await secretStore.set(
        CLIENT_KEYCHAIN_ID,
        SECRET_FIELD_IDP_CLIENT_SECRET,
        value
      );
    }
    if (!await secretStoreIsDurable(secretStore)) return config;
    await writeStoreFile(filePath, serializeStore(stripped));
    return stripped;
  } catch (err) {
    if (err instanceof SecretStoreUnavailableError) {
      return config;
    }
    throw err;
  }
}
async function readClientConfigStore(filePath, secretStore) {
  const raw = await readStoreFile(filePath);
  if (raw === null) {
    return {};
  }
  let config = parseClientConfig(parseStore(raw));
  if (hasClientPlaintextSecret(config)) {
    config = await migrateClientPlaintextSecret(filePath, config, secretStore);
  }
  const secrets = await readIdpSecretFromKeychain(secretStore);
  return mergeSecretsIntoClientConfig(config, secrets);
}
async function writeClientConfigStore(filePath, body, secretStore) {
  const validated = parseClientConfig(body);
  const { stripped, secrets } = extractSecretsFromClientConfig(validated);
  const idpSecret = secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
  if (idpSecret) {
    await secretStore.set(
      CLIENT_KEYCHAIN_ID,
      SECRET_FIELD_IDP_CLIENT_SECRET,
      idpSecret
    );
  } else {
    await secretStore.delete(
      CLIENT_KEYCHAIN_ID,
      SECRET_FIELD_IDP_CLIENT_SECRET
    );
  }
  const durable = await secretStoreIsDurable(secretStore);
  await writeStoreFile(
    filePath,
    serializeStore(
      durable ? stripped : await preserveLegacyPlaintext(
        filePath,
        validated,
        stripped,
        idpSecret
      )
    )
  );
}
async function preserveLegacyPlaintext(filePath, validated, stripped, idpSecret) {
  if (!idpSecret) return stripped;
  try {
    const raw = await readStoreFile(filePath);
    if (raw === null) return stripped;
    const prior = parseClientConfig(parseStore(raw));
    const priorSecret = extractSecretsFromClientConfig(prior).secrets[SECRET_FIELD_IDP_CLIENT_SECRET];
    return priorSecret === idpSecret ? validated : stripped;
  } catch {
    return stripped;
  }
}
async function deleteClientConfigStore(filePath, secretStore) {
  await deleteStoreFile(filePath);
  await secretStore.delete(CLIENT_KEYCHAIN_ID, SECRET_FIELD_IDP_CLIENT_SECRET);
}

// ../../core/mcp/remote/node/server.ts
import { ZodError as ZodError2 } from "zod";
var SSE_PRIMING_COMMENT = ":\n\n";
function closeAbortedStream(stream) {
  stream.close().catch(() => {
  });
}
async function primeAndHoldSseStream(stream, cleanup) {
  let done = false;
  const aborted = new Promise((resolve9) => {
    stream.onAbort(() => {
      if (done) return;
      done = true;
      cleanup();
      closeAbortedStream(stream);
      resolve9();
    });
  });
  await stream.write(SSE_PRIMING_COMMENT);
  await aborted;
}
var MAX_APP_DOCUMENT_CHARS = 8 * 1024 * 1024;
var MAX_APP_DOCUMENT_BODY_BYTES = 24 * 1024 * 1024;
var MAX_APP_CSP_CHARS = 8 * 1024;
function isHeaderSafeValue(value) {
  return !/[^\t\x20-\x7e]/.test(value);
}
function createOriginMiddleware(allowedOrigins) {
  return async (c, next) => {
    if (!allowedOrigins || allowedOrigins.length === 0) {
      await next();
      return;
    }
    const origin = c.req.header("origin");
    if (c.req.method === "OPTIONS") {
      if (origin && allowedOrigins.includes(origin)) {
        c.header("Access-Control-Allow-Origin", origin);
        c.header("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
        c.header(
          "Access-Control-Allow-Headers",
          "Content-Type, x-mcp-remote-auth"
        );
        c.header("Access-Control-Max-Age", "86400");
        return c.body(null, 204);
      }
      return c.json(
        {
          error: "Forbidden",
          message: "Invalid origin. Request blocked to prevent DNS rebinding attacks."
        },
        403
      );
    }
    if (origin) {
      if (!allowedOrigins.includes(origin)) {
        return c.json(
          {
            error: "Forbidden",
            message: "Invalid origin. Request blocked to prevent DNS rebinding attacks. Configure allowed origins via allowedOrigins option."
          },
          403
        );
      }
      c.header("Access-Control-Allow-Origin", origin);
    }
    await next();
  };
}
function createAuthMiddleware(authToken) {
  return async (c, next) => {
    const authHeader = c.req.header("x-mcp-remote-auth");
    if (!authHeader || !authHeader.startsWith("Bearer ")) {
      return c.json(
        {
          error: "Unauthorized",
          message: "Authentication required. Use the x-mcp-remote-auth header with Bearer token."
        },
        401
      );
    }
    const providedToken = authHeader.substring(7);
    const expectedToken = authToken;
    const providedBuffer = Buffer.from(providedToken);
    const expectedBuffer = Buffer.from(expectedToken);
    if (providedBuffer.length !== expectedBuffer.length) {
      return c.json(
        {
          error: "Unauthorized",
          message: "Authentication required. Use the x-mcp-remote-auth header with Bearer token."
        },
        401
      );
    }
    if (!timingSafeEqual(providedBuffer, expectedBuffer)) {
      return c.json(
        {
          error: "Unauthorized",
          message: "Authentication required. Use the x-mcp-remote-auth header with Bearer token."
        },
        401
      );
    }
    await next();
  };
}
function forwardLogEvent(logger, logEvent) {
  const levelLabel = (logEvent?.level?.label ?? "info").toLowerCase();
  const method = toRecord(logger)[levelLabel];
  if (typeof method !== "function") return;
  const bindings = Object.assign(
    {},
    ...Array.isArray(logEvent.bindings) ? logEvent.bindings : []
  );
  const messages = Array.isArray(logEvent.messages) ? logEvent.messages : [];
  if (messages.length === 0) {
    method.call(logger, bindings);
    return;
  }
  const first = messages[0];
  if (typeof first === "object" && first !== null && !Array.isArray(first)) {
    const obj = { ...bindings, ...first };
    const msg = messages[1];
    const args = messages.slice(2);
    method.call(
      logger,
      obj,
      msg,
      ...args
    );
  } else {
    const msg = messages[0];
    const args = messages.slice(1);
    method.call(
      logger,
      bindings,
      msg,
      ...args
    );
  }
}
function requestIdForSendWait(message) {
  if ("method" in message && "id" in message && message.id !== null && message.id !== void 0) {
    if (message.method === "subscriptions/listen") {
      return void 0;
    }
    return message.id;
  }
  return void 0;
}
function mcpParamHeadersOnly(headers) {
  if (headers === null || typeof headers !== "object") return void 0;
  const out = {};
  for (const [key, value] of Object.entries(headers)) {
    if (typeof value === "string" && key.toLowerCase().startsWith(MCP_PARAM_HEADER_PREFIX.toLowerCase())) {
      out[key] = value;
    }
  }
  return Object.keys(out).length > 0 ? out : void 0;
}
function createRemoteApp(options) {
  const dangerouslyOmitAuth = !!options.dangerouslyOmitAuth;
  const authToken = dangerouslyOmitAuth ? "" : options.authToken || process.env[API_SERVER_ENV_VARS.AUTH_TOKEN] || randomBytes3(32).toString("hex");
  const app = new Hono();
  const sessions = /* @__PURE__ */ new Map();
  const { logger: fileLogger, allowedOrigins } = options;
  const storageDir = options.storageDir ?? getDefaultStorageDir();
  const mcpConfigPath = options.mcpConfigPath ?? getDefaultMcpConfigPath();
  const secretStore = options.secretStore ?? defaultSecretStore();
  const writable = options.writable ?? true;
  const inMemoryServers = options.initialServers;
  const watchable = !inMemoryServers;
  const serverEventSubscribers = /* @__PURE__ */ new Set();
  let mcpConfigWatcher = null;
  let lastWrittenMtimeMs = null;
  const broadcastServerListChange = () => {
    const payload = JSON.stringify({ type: "change" });
    for (const send of serverEventSubscribers) {
      try {
        send(payload);
      } catch {
      }
    }
  };
  const writeMcpAndTrackMtime = async (data) => {
    let externalEditDetected = false;
    if (lastWrittenMtimeMs !== null) {
      try {
        const s = await fsStat(mcpConfigPath);
        if (s.mtimeMs !== lastWrittenMtimeMs) {
          externalEditDetected = true;
        }
      } catch {
        externalEditDetected = true;
      }
    }
    await writeStoreFile(mcpConfigPath, data);
    try {
      const s = await fsStat(mcpConfigPath);
      lastWrittenMtimeMs = s.mtimeMs;
    } catch {
    }
    if (externalEditDetected) {
      broadcastServerListChange();
    }
  };
  const handleWatcherEvent = async (event) => {
    if (event !== "add" && event !== "change" && event !== "unlink") return;
    if (event !== "unlink") {
      try {
        const s = await fsStat(mcpConfigPath);
        if (lastWrittenMtimeMs !== null && s.mtimeMs === lastWrittenMtimeMs) {
          return;
        }
      } catch {
      }
    }
    broadcastServerListChange();
  };
  const handleWatcherError = (err) => {
    const msg = err instanceof Error ? err.message : String(err);
    if (fileLogger) {
      fileLogger.warn({ err: msg }, "mcp.json watcher error");
    } else {
      console.warn("[mcp.json watcher]", msg);
    }
  };
  const ensureWatcher = () => {
    if (mcpConfigWatcher) return;
    mcpConfigWatcher = chokidarWatch(mcpConfigPath, {
      ignoreInitial: true,
      awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 50 }
    });
    mcpConfigWatcher.on("all", (event) => {
      void handleWatcherEvent(event);
    });
    mcpConfigWatcher.on("error", handleWatcherError);
  };
  const maybeStopWatcher = async () => {
    if (serverEventSubscribers.size > 0) return;
    if (!mcpConfigWatcher) return;
    const w = mcpConfigWatcher;
    mcpConfigWatcher = null;
    try {
      await w.close();
    } catch {
    }
  };
  app.use("*", createOriginMiddleware(allowedOrigins));
  if (!dangerouslyOmitAuth) {
    app.use("*", createAuthMiddleware(authToken));
  }
  app.get("/api/config", async (c) => {
    const secretStorage = options.secretStorageResolver ? await options.secretStorageResolver() : options.initialConfig?.secretStorage;
    const payload = {
      ...options.initialConfig,
      writable,
      // Assigned unconditionally, not spread-when-truthy. The resolver's
      // contract is that `undefined` means "not known right now", and a
      // conditional spread leaves the *startup* descriptor from
      // `initialConfig` standing in its place — so a store that became
      // undescribable would keep serving a stale, confident answer. Setting
      // it to `undefined` is what clears it; `c.json` omits the key.
      secretStorage,
      ...options.sandboxUrl ? { sandboxUrl: options.sandboxUrl } : {}
    };
    return c.json(payload);
  });
  app.post(
    "/api/app-document",
    // Before the parse, not after: see MAX_APP_DOCUMENT_BODY_BYTES.
    bodyLimit({
      maxSize: MAX_APP_DOCUMENT_BODY_BYTES,
      onError: (c) => c.json({ error: "Document too large" }, 413)
    }),
    async (c) => {
      const publish = options.publishAppDocument;
      if (!publish) {
        return c.json({ error: "App-origin hosting is not available" }, 503);
      }
      let parsed;
      try {
        parsed = await c.req.json();
      } catch {
        return c.json({ error: "Invalid JSON body" }, 400);
      }
      if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
        return c.json({ error: "Invalid JSON body" }, 400);
      }
      const { html, csp } = parsed;
      if (typeof html !== "string" || html === "") {
        return c.json({ error: "Missing html" }, 400);
      }
      if (csp !== void 0 && typeof csp !== "string") {
        return c.json({ error: "csp must be a string" }, 400);
      }
      if (csp !== void 0 && !isHeaderSafeValue(csp)) {
        return c.json({ error: "csp is not a valid header value" }, 400);
      }
      if (html.length > MAX_APP_DOCUMENT_CHARS) {
        return c.json({ error: "Document too large" }, 413);
      }
      if (csp !== void 0 && csp.length > MAX_APP_CSP_CHARS) {
        return c.json({ error: "csp too large" }, 413);
      }
      const published = publish({ html, csp });
      if (!published) {
        return c.json({ error: "App-origin hosting is not available" }, 503);
      }
      return c.json(published);
    }
  );
  app.post("/api/mcp/connect", async (c) => {
    let body;
    try {
      body = await c.req.json();
    } catch {
      return c.json({ error: "Invalid JSON body" }, 400);
    }
    const config = body.config;
    if (!config) {
      return c.json({ error: "Missing config" }, 400);
    }
    const sessionId = crypto.randomUUID();
    const session = new RemoteSession(sessionId);
    let transport;
    let authHandle;
    try {
      const initialAuthState = body.authState ?? (body.oauthTokens ? { oauthTokens: body.oauthTokens } : void 0);
      authHandle = createRemoteAuthProvider(initialAuthState);
      const result = createTransportNode(config, {
        pipeStderr: true,
        onStderr: (entry) => session.onStderr(entry),
        onFetchRequest: (entry) => session.onFetchRequest(entry),
        onFetchResponseBody: (id, body2) => session.onFetchResponseBody(id, body2),
        authProvider: authHandle?.provider,
        settings: body.settings,
        // Always intercept 401/403 on the node MCP transport. Without this, the
        // SDK's built-in auth() retry on a frozen token snapshot can hang or
        // succeed locally while /api/mcp/send still awaits a JSON-RPC response.
        interceptAuthChallenges: true
      });
      transport = result.transport;
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      return c.json({ error: `Failed to create transport: ${msg}` }, 500);
    }
    session.setAuthProviderHandle(authHandle ?? null);
    session.setTransport(transport);
    transport.onmessage = (msg) => session.onMessage(msg);
    let transportFailed = false;
    let transportError = null;
    const originalOnclose = transport.onclose;
    const originalOnerror = transport.onerror;
    transport.onerror = (err) => {
      if (session.handleTransportAuthError(err)) {
        originalOnerror?.(err);
        return;
      }
      transportFailed = true;
      transportError = err instanceof Error ? err.message : String(err);
      originalOnerror?.(err);
    };
    transport.onclose = () => {
      const session2 = sessions.get(sessionId);
      if (session2) {
        const errorMsg = transportError || "Transport closed - process may have exited";
        session2.markTransportDead(errorMsg);
        if (!session2.hasEventConsumer()) {
          setTimeout(() => {
            const stale = sessions.get(sessionId);
            if (stale && !stale.hasEventConsumer()) {
              sessions.delete(sessionId);
            }
          }, 3e4).unref();
        }
      } else {
        transportFailed = true;
        transportError = transportError || "Transport closed during start - process may have failed";
      }
      originalOnclose?.();
    };
    try {
      await transport.start();
      if (transportFailed) {
        const errorMsg = transportError || "Transport failed during start";
        return c.json({ error: `Failed to start transport: ${errorMsg}` }, 500);
      }
    } catch (err) {
      if (err instanceof AuthChallengeError) {
        try {
          await transport.close();
        } catch {
        }
        session.noteAuthChallengeDeliveredViaHttp();
        return c.json({
          ok: false,
          kind: "auth_challenge",
          authChallenge: err.authChallenge
        });
      }
      const msg = err instanceof Error ? err.message : String(err);
      const status = err.code ?? err.status;
      const is401 = status === 401;
      return c.json(
        { error: `Failed to start transport: ${msg}` },
        is401 ? 401 : 500
      );
    }
    sessions.set(sessionId, session);
    return c.json({ sessionId });
  });
  app.post("/api/mcp/send", async (c) => {
    let body;
    try {
      body = await c.req.json();
    } catch {
      return c.json({ error: "Invalid JSON body" }, 400);
    }
    const { sessionId, message, relatedRequestId, headers, protocolVersion } = body;
    if (!sessionId || !message) {
      return c.json({ error: "Missing sessionId or message" }, 400);
    }
    const session = sessions.get(sessionId);
    if (!session) {
      return c.json({ error: "Session not found" }, 404);
    }
    if (session.isTransportDead()) {
      const errorMsg = session.getTransportError() || "Transport closed";
      return c.json({ ok: false, kind: "transport_error", error: errorMsg });
    }
    session.applyProtocolVersion(protocolVersion);
    session.beginSend();
    const requestId = requestIdForSendWait(message);
    let responseWait;
    if (requestId !== void 0) {
      responseWait = session.waitForRequestResponse(requestId);
      void responseWait.catch(() => {
      });
    }
    try {
      const paramHeaders = mcpParamHeadersOnly(headers);
      await session.transport.send(message, {
        relatedRequestId,
        ...paramHeaders && { headers: paramHeaders }
      });
      if (responseWait) {
        await responseWait;
      }
      return c.json({ ok: true });
    } catch (err) {
      if (requestId !== void 0) {
        session.cancelRequestWait(requestId);
      }
      if (err instanceof AuthChallengeError) {
        session.noteAuthChallengeDeliveredViaHttp();
        return c.json({
          ok: false,
          kind: "auth_challenge",
          authChallenge: err.authChallenge
        });
      }
      const msg = err instanceof Error ? err.message : String(err);
      return c.json({ ok: false, kind: "transport_error", error: msg });
    } finally {
      session.endSend();
    }
  });
  app.get("/api/mcp/events", async (c) => {
    const sessionId = c.req.query("sessionId");
    if (!sessionId) {
      return c.json({ error: "Missing sessionId query" }, 400);
    }
    const session = sessions.get(sessionId);
    if (!session) {
      return c.json({ error: "Session not found" }, 404);
    }
    return streamSSE(c, async (stream) => {
      session.setEventConsumer((event) => {
        const data = JSON.stringify(event);
        void stream.writeSSE({
          event: event.type,
          data
        });
      });
      if (session.isTransportDead()) {
        await new Promise((resolve9) => setTimeout(resolve9, 0));
        session.clearEventConsumer();
        sessions.delete(sessionId);
        return;
      }
      await primeAndHoldSseStream(stream, () => {
        const shouldCleanup = session.clearEventConsumer();
        if (shouldCleanup || session.isTransportDead()) {
          sessions.delete(sessionId);
        }
      });
    });
  });
  app.post("/api/mcp/disconnect", async (c) => {
    let body;
    try {
      body = await c.req.json();
    } catch {
      return c.json({ error: "Invalid JSON body" }, 400);
    }
    const sessionId = body.sessionId;
    if (!sessionId) {
      return c.json({ error: "Missing sessionId" }, 400);
    }
    const session = sessions.get(sessionId);
    if (session) {
      session.clearEventConsumer();
      await session.transport.close();
      sessions.delete(sessionId);
    }
    return c.json({ ok: true });
  });
  app.post("/api/mcp/auth-state", async (c) => {
    let body;
    try {
      body = await c.req.json();
    } catch {
      return c.json({ error: "Invalid JSON body" }, 400);
    }
    const { sessionId, authState } = body;
    if (!sessionId || !authState) {
      return c.json({ error: "Missing sessionId or authState" }, 400);
    }
    const session = sessions.get(sessionId);
    if (!session) {
      return c.json({ error: "Session not found" }, 404);
    }
    if (session.isTransportDead()) {
      const errorMsg = session.getTransportError() || "Transport closed";
      return c.json({ ok: false, kind: "transport_error", error: errorMsg });
    }
    try {
      session.setAuthState(authState);
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      return c.json({ error: msg }, 400);
    }
    return c.json({ ok: true });
  });
  app.post("/api/fetch", async (c) => {
    let body;
    try {
      body = await c.req.json();
    } catch {
      return c.json({ error: "Invalid JSON body" }, 400);
    }
    const { url, method = "GET", headers = {}, body: reqBody } = body;
    if (!url) {
      return c.json({ error: "Missing url" }, 400);
    }
    try {
      const res = await (createProxyFetch() ?? fetch)(url, {
        method,
        headers: new Headers(headers),
        body: reqBody
      });
      const resHeaders = {};
      res.headers.forEach((v, k) => {
        resHeaders[k] = v;
      });
      const contentType = res.headers.get("content-type");
      const isStream = contentType?.includes("text/event-stream") || contentType?.includes("application/x-ndjson");
      let resBody;
      if (!isStream && res.body) {
        resBody = await res.text();
      }
      return c.json({
        ok: res.ok,
        status: res.status,
        statusText: res.statusText,
        headers: resHeaders,
        body: resBody
      });
    } catch (err) {
      const msg = err instanceof Error ? err.message : String(err);
      return c.json({ error: msg }, 500);
    }
  });
  app.post("/api/log", async (c) => {
    const body = await c.req.json().catch(() => ({}));
    if (fileLogger) {
      forwardLogEvent(fileLogger, body);
    }
    return c.json({ ok: true });
  });
  app.get("/api/storage/:storeId", async (c) => {
    const storeId = c.req.param("storeId");
    if (!storeId || !validateStoreId(storeId)) {
      return c.json({ error: "Invalid storeId" }, 400);
    }
    const filePath = getStoreFilePath(storageDir, storeId);
    try {
      if (storeId === "client") {
        const config = await readClientConfigStore(filePath, secretStore);
        return c.json(config);
      }
      const raw = await readStoreFile(filePath);
      if (raw === null) {
        return c.json({}, 200);
      }
      const store = parseStore(raw);
      return c.json(store);
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error);
      return c.json({ error: `Failed to read store: ${msg}` }, 500);
    }
  });
  app.post("/api/storage/:storeId", async (c) => {
    const storeId = c.req.param("storeId");
    if (!storeId || !validateStoreId(storeId)) {
      return c.json({ error: "Invalid storeId" }, 400);
    }
    let body;
    try {
      body = await c.req.json();
    } catch {
      return c.json({ error: "Invalid JSON body" }, 400);
    }
    const filePath = getStoreFilePath(storageDir, storeId);
    try {
      if (storeId === "client") {
        await writeClientConfigStore(filePath, body, secretStore);
        return c.json({ ok: true });
      }
      const jsonData = serializeStore(body);
      await writeStoreFile(filePath, jsonData);
      return c.json({ ok: true });
    } catch (error) {
      if (error instanceof ZodError2) {
        return c.json({ error: formatClientConfigLoadError(error) }, 400);
      }
      const keychainResp = storeId === "client" ? keychainErrorResponse(c, error) : void 0;
      if (keychainResp) return keychainResp;
      const msg = error instanceof Error ? error.message : String(error);
      return c.json({ error: `Failed to write store: ${msg}` }, 500);
    }
  });
  app.delete("/api/storage/:storeId", async (c) => {
    const storeId = c.req.param("storeId");
    if (!storeId || !validateStoreId(storeId)) {
      return c.json({ error: "Invalid storeId" }, 400);
    }
    const filePath = getStoreFilePath(storageDir, storeId);
    try {
      if (storeId === "client") {
        await deleteClientConfigStore(filePath, secretStore);
        return c.json({ ok: true });
      }
      await deleteStoreFile(filePath);
      return c.json({ ok: true });
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error);
      return c.json({ error: `Failed to delete store: ${msg}` }, 500);
    }
  });
  const logWarn = (bindings, msg) => {
    if (fileLogger) {
      fileLogger.warn(bindings, msg);
    } else {
      console.warn("[mcp.json]", msg, bindings);
    }
  };
  const isJsonObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
  const isStringRecord = (v) => {
    if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
    for (const val of Object.values(v)) {
      if (typeof val !== "string") return false;
    }
    return true;
  };
  const isKvArray = (v) => {
    if (!Array.isArray(v)) return false;
    return v.every(
      (e) => e !== null && typeof e === "object" && typeof e.key === "string" && typeof e.value === "string"
    );
  };
  const isOauthObject = (v) => {
    if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
    const o = v;
    for (const k of [
      "clientId",
      "clientSecret",
      "scopes",
      // #1906 — shape only; the values are validated as absolute http(s) URLs
      // where they're applied (core/auth/endpointOverrides.ts), so a typo drops
      // one field with a warning instead of dropping the whole `oauth` block.
      "authorizationUrl",
      "tokenUrl"
    ]) {
      if (o[k] !== void 0 && typeof o[k] !== "string") return false;
    }
    if (o.authorizationParams !== void 0 && !isStringRecord(o.authorizationParams)) {
      return false;
    }
    if (o.enterpriseManaged !== void 0 && typeof o.enterpriseManaged !== "boolean") {
      return false;
    }
    if (o.requestRefreshToken !== void 0 && typeof o.requestRefreshToken !== "boolean") {
      return false;
    }
    return true;
  };
  const isRootArray = (v) => {
    if (!Array.isArray(v)) return false;
    return v.every((e) => {
      if (e === null || typeof e !== "object") return false;
      const o = e;
      if (typeof o.uri !== "string") return false;
      if (o.name !== void 0 && typeof o.name !== "string") return false;
      return true;
    });
  };
  const isNonNegNumber = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
  const normalizeMcpServers = (raw) => {
    if (!raw || typeof raw !== "object") return {};
    const out = {};
    for (const [id, val] of Object.entries(raw)) {
      if (!val || typeof val !== "object") continue;
      const valObj = val;
      if ("settings" in valObj) {
        logWarn(
          { route: "/api/servers", id, droppedKey: "settings" },
          "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."
        );
        delete valObj.settings;
      }
      if ("headers" in valObj && !isStringRecord(valObj.headers)) {
        logWarn(
          { route: "/api/servers", id, droppedKey: "headers" },
          "Dropping malformed `headers` field \u2014 expected `Record<string, string>`."
        );
        delete valObj.headers;
      }
      if ("metadata" in valObj && !isJsonObject(valObj.metadata) && !isKvArray(valObj.metadata)) {
        logWarn(
          { route: "/api/servers", id, droppedKey: "metadata" },
          "Dropping malformed `metadata` field \u2014 expected a JSON object."
        );
        delete valObj.metadata;
      }
      if ("connectionTimeout" in valObj && !isNonNegNumber(valObj.connectionTimeout)) {
        logWarn(
          { route: "/api/servers", id, droppedKey: "connectionTimeout" },
          "Dropping malformed `connectionTimeout` field \u2014 expected non-negative number."
        );
        delete valObj.connectionTimeout;
      }
      if ("requestTimeout" in valObj && !isNonNegNumber(valObj.requestTimeout)) {
        logWarn(
          { route: "/api/servers", id, droppedKey: "requestTimeout" },
          "Dropping malformed `requestTimeout` field \u2014 expected non-negative number."
        );
        delete valObj.requestTimeout;
      }
      if ("taskTtl" in valObj && !isNonNegNumber(valObj.taskTtl)) {
        logWarn(
          { route: "/api/servers", id, droppedKey: "taskTtl" },
          "Dropping malformed `taskTtl` field \u2014 expected non-negative number."
        );
        delete valObj.taskTtl;
      }
      if ("maxFetchRequests" in valObj && !isNonNegNumber(valObj.maxFetchRequests)) {
        logWarn(
          { route: "/api/servers", id, droppedKey: "maxFetchRequests" },
          "Dropping malformed `maxFetchRequests` field \u2014 expected non-negative number."
        );
        delete valObj.maxFetchRequests;
      }
      if ("oauth" in valObj && !isOauthObject(valObj.oauth)) {
        logWarn(
          { route: "/api/servers", id, droppedKey: "oauth" },
          "Dropping malformed `oauth` field \u2014 expected `{ clientId?, clientSecret?, scopes?, authorizationParams?, authorizationUrl?, tokenUrl?, enterpriseManaged?, onInsufficientScope?, requestRefreshToken? }`."
        );
        delete valObj.oauth;
      }
      if ("roots" in valObj && !isRootArray(valObj.roots)) {
        logWarn(
          { route: "/api/servers", id, droppedKey: "roots" },
          "Dropping malformed `roots` field \u2014 expected `Array<{ uri: string, name?: string }>`."
        );
        delete valObj.roots;
      }
      if ("protocolEra" in valObj && !isProtocolEra(valObj.protocolEra)) {
        logWarn(
          { route: "/api/servers", id, droppedKey: "protocolEra" },
          "Dropping malformed `protocolEra` field \u2014 expected 'legacy', 'auto', or 'modern'."
        );
        delete valObj.protocolEra;
      }
      if ("modernLogLevel" in valObj && !isModernLogLevel(valObj.modernLogLevel)) {
        logWarn(
          { route: "/api/servers", id, droppedKey: "modernLogLevel" },
          "Dropping malformed `modernLogLevel` field \u2014 expected 'off' or a logging level."
        );
        delete valObj.modernLogLevel;
      }
      out[id] = normalizeServerType(
        valObj
      );
    }
    return out;
  };
  const SMUGGLE_GUARDED_KEYS = /* @__PURE__ */ new Set([
    ...INSPECTOR_FIELD_KEYS,
    "settings"
  ]);
  const buildStoredEntry = (id, config, settings) => {
    const configObj = config !== null && typeof config === "object" ? config : {};
    const smuggled = [];
    const configOnly = {};
    for (const [k, v] of Object.entries(configObj)) {
      if (SMUGGLE_GUARDED_KEYS.has(k)) {
        smuggled.push(k);
        continue;
      }
      configOnly[k] = v;
    }
    if (smuggled.length > 0) {
      logWarn(
        { route: "/api/servers", id, smuggledKeys: smuggled },
        "Stripping Inspector-extension keys from request body's `config` \u2014 those must travel through the top-level `settings` field, not nested inside `config`."
      );
    }
    const normalized = normalizeServerType(
      configOnly
    );
    if (settings !== void 0) {
      Object.assign(normalized, inspectorSettingsToStoredFields(settings));
    }
    return normalized;
  };
  const applyStdioSettingsToConfig = (entry, settings, provided) => {
    if (!(entry.type === "stdio" || entry.type === void 0)) return;
    const stdio = entry;
    const { env, cwd } = stdioConfigFieldsFromSettings(settings);
    if (provided.env) {
      if (env) {
        stdio.env = env;
      } else {
        delete stdio.env;
      }
    }
    if (provided.cwd) {
      if (cwd) {
        stdio.cwd = cwd;
      } else {
        delete stdio.cwd;
      }
    }
  };
  const validateSettings = (raw) => {
    if (raw === null || typeof raw !== "object" || Array.isArray(raw)) {
      return { ok: false, error: "settings must be an object" };
    }
    const obj = raw;
    const isKvArray2 = (v) => {
      if (!Array.isArray(v)) return false;
      return v.every(
        (e) => e !== null && typeof e === "object" && typeof e.key === "string" && typeof e.value === "string"
      );
    };
    const isBooleanRecord = (v) => {
      if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
      return Object.values(v).every(
        (flag) => typeof flag === "boolean"
      );
    };
    if (!isKvArray2(obj.headers)) {
      return {
        ok: false,
        error: "settings.headers must be an array of { key, value }"
      };
    }
    if (!isJsonObject(obj.metadata)) {
      return {
        ok: false,
        error: "settings.metadata must be a JSON object"
      };
    }
    if (obj.env !== void 0 && !isKvArray2(obj.env)) {
      return {
        ok: false,
        error: "settings.env must be an array of { key, value }"
      };
    }
    if (obj.cwd !== void 0 && typeof obj.cwd !== "string") {
      return { ok: false, error: "settings.cwd must be a string" };
    }
    if (typeof obj.connectionTimeout !== "number" || obj.connectionTimeout < 0) {
      return {
        ok: false,
        error: "settings.connectionTimeout must be a non-negative number"
      };
    }
    if (typeof obj.requestTimeout !== "number" || obj.requestTimeout < 0) {
      return {
        ok: false,
        error: "settings.requestTimeout must be a non-negative number"
      };
    }
    if (obj.taskTtl !== void 0 && (typeof obj.taskTtl !== "number" || obj.taskTtl < 0)) {
      return {
        ok: false,
        error: "settings.taskTtl must be a non-negative number"
      };
    }
    if (obj.autoRefreshOnListChanged !== void 0 && typeof obj.autoRefreshOnListChanged !== "boolean") {
      return {
        ok: false,
        error: "settings.autoRefreshOnListChanged must be a boolean"
      };
    }
    if (obj.paginatedLists !== void 0 && typeof obj.paginatedLists !== "boolean") {
      return {
        ok: false,
        error: "settings.paginatedLists must be a boolean"
      };
    }
    if (obj.maxFetchRequests !== void 0 && (typeof obj.maxFetchRequests !== "number" || obj.maxFetchRequests < 0)) {
      return {
        ok: false,
        error: "settings.maxFetchRequests must be a non-negative number"
      };
    }
    for (const optional of [
      "oauthClientId",
      "oauthClientSecret",
      "oauthScopes",
      // #1906 — string-shaped on the wire; URL validity is checked where the
      // override is applied, not here.
      "oauthAuthorizationUrl",
      "oauthTokenUrl"
    ]) {
      if (obj[optional] !== void 0 && typeof obj[optional] !== "string") {
        return { ok: false, error: `settings.${optional} must be a string` };
      }
    }
    if (obj.oauthAuthorizationParams !== void 0 && !isKvArray2(obj.oauthAuthorizationParams)) {
      return {
        ok: false,
        error: "settings.oauthAuthorizationParams must be an array of { key, value }"
      };
    }
    if (obj.enterpriseManaged !== void 0 && typeof obj.enterpriseManaged !== "boolean") {
      return {
        ok: false,
        error: "settings.enterpriseManaged must be a boolean"
      };
    }
    if (obj.oauthRequestRefreshToken !== void 0 && typeof obj.oauthRequestRefreshToken !== "boolean") {
      return {
        ok: false,
        error: "settings.oauthRequestRefreshToken must be a boolean"
      };
    }
    if (obj.oauthOnInsufficientScope !== void 0 && obj.oauthOnInsufficientScope !== "reauthorize" && obj.oauthOnInsufficientScope !== "throw") {
      return {
        ok: false,
        error: "settings.oauthOnInsufficientScope must be 'reauthorize' or 'throw'"
      };
    }
    if (obj.roots !== void 0 && !isRootArray(obj.roots)) {
      return {
        ok: false,
        error: "settings.roots must be an array of { uri, name? }"
      };
    }
    if (obj.protocolEra !== void 0 && !isProtocolEra(obj.protocolEra)) {
      return {
        ok: false,
        error: "settings.protocolEra must be 'legacy', 'auto', or 'modern'"
      };
    }
    if (obj.modernLogLevel !== void 0 && !isModernLogLevel(obj.modernLogLevel)) {
      return {
        ok: false,
        error: "settings.modernLogLevel must be 'off' or a logging level (debug\u2026emergency)"
      };
    }
    if (obj.advertisedExtensions !== void 0 && !isBooleanRecord(obj.advertisedExtensions)) {
      return {
        ok: false,
        error: "settings.advertisedExtensions must be an object of boolean flags"
      };
    }
    const value = {
      headers: obj.headers,
      // Absent → empty list, matching the read side. The write-through drops
      // empty-key rows and clears `config.env` when the list is empty.
      env: isKvArray2(obj.env) ? obj.env : [],
      metadata: obj.metadata,
      connectionTimeout: obj.connectionTimeout,
      requestTimeout: obj.requestTimeout,
      // Absent → product default, matching the read side
      // (storedFieldsToInspectorSettings). The default is the omit-sentinel in
      // inspectorSettingsToStoredFields, so this won't write a spurious taskTtl
      // to disk for a client that didn't send one.
      taskTtl: typeof obj.taskTtl === "number" ? obj.taskTtl : DEFAULT_TASK_TTL_MS,
      // Absent → false, matching the read side. The omit-on-false logic lives
      // in inspectorSettingsToStoredFields, so a false value writes nothing.
      autoRefreshOnListChanged: obj.autoRefreshOnListChanged === true,
      // Absent → false, matching the read side (omit-on-false on the write side).
      paginatedLists: obj.paginatedLists === true,
      // Absent → product default, matching the read side. The default is the
      // omit-sentinel in inspectorSettingsToStoredFields, so a client that
      // didn't send one writes no spurious maxFetchRequests to disk.
      maxFetchRequests: typeof obj.maxFetchRequests === "number" ? obj.maxFetchRequests : DEFAULT_MAX_FETCH_REQUESTS,
      // Absent → empty list, matching the read side
      // (storedFieldsToInspectorSettings). Empty rows are dropped on the way
      // to disk by inspectorSettingsToStoredFields, so an empty array here
      // writes no spurious `roots` field.
      roots: isRootArray(obj.roots) ? obj.roots : []
    };
    if (typeof obj.oauthClientId === "string" && obj.oauthClientId !== "") {
      value.oauthClientId = obj.oauthClientId;
    }
    if (typeof obj.oauthClientSecret === "string" && obj.oauthClientSecret !== "") {
      value.oauthClientSecret = obj.oauthClientSecret;
    }
    if (typeof obj.oauthScopes === "string" && obj.oauthScopes !== "") {
      value.oauthScopes = obj.oauthScopes;
    }
    if (isKvArray2(obj.oauthAuthorizationParams)) {
      value.oauthAuthorizationParams = obj.oauthAuthorizationParams;
    }
    if (typeof obj.oauthAuthorizationUrl === "string" && obj.oauthAuthorizationUrl !== "") {
      value.oauthAuthorizationUrl = obj.oauthAuthorizationUrl;
    }
    if (typeof obj.oauthTokenUrl === "string" && obj.oauthTokenUrl !== "") {
      value.oauthTokenUrl = obj.oauthTokenUrl;
    }
    if (obj.enterpriseManaged === true) {
      value.enterpriseManaged = true;
    }
    if (obj.oauthRequestRefreshToken === false) {
      value.oauthRequestRefreshToken = false;
    }
    if (obj.oauthOnInsufficientScope === "reauthorize" || obj.oauthOnInsufficientScope === "throw") {
      value.oauthOnInsufficientScope = obj.oauthOnInsufficientScope;
    }
    if (isProtocolEra(obj.protocolEra)) {
      value.protocolEra = obj.protocolEra;
    }
    if (isModernLogLevel(obj.modernLogLevel)) {
      value.modernLogLevel = obj.modernLogLevel;
    }
    if (isBooleanRecord(obj.advertisedExtensions) && Object.keys(obj.advertisedExtensions).length > 0) {
      value.advertisedExtensions = { ...obj.advertisedExtensions };
    }
    if (typeof obj.cwd === "string" && obj.cwd !== "") {
      value.cwd = obj.cwd;
    }
    return {
      ok: true,
      value,
      envProvided: obj.env !== void 0,
      cwdProvided: obj.cwd !== void 0
    };
  };
  let writeQueue = Promise.resolve();
  const withWriteLock = async (fn) => {
    const prev = writeQueue;
    let release = () => {
    };
    writeQueue = new Promise((r) => {
      release = r;
    });
    try {
      await prev;
      return await fn();
    } finally {
      release();
    }
  };
  const readMcpConfig = async () => {
    const raw = await readStoreFile(mcpConfigPath);
    if (raw === null) return { mcpServers: {} };
    const parsed = parseStore(raw);
    return { mcpServers: normalizeMcpServers(parsed?.mcpServers) };
  };
  const writeKeychainEntriesFor = async (id, secrets) => {
    await secretStoreSetMany(secretStore, id, secrets);
  };
  const readKeychainEntriesFor = async (id, fields) => {
    const many = await secretStoreGetMany(secretStore, [
      { serverId: id, fields }
    ]);
    return many[id] ?? {};
  };
  const hasPlaintextSecrets = (config) => {
    for (const stored of Object.values(config.mcpServers)) {
      const { secrets } = extractSecretsFromStored(stored);
      if (Object.keys(secrets).length > 0) return true;
    }
    return false;
  };
  let warnedSessionPreserved = false;
  const migratePlaintextSecrets = async (config) => {
    let changed = false;
    const next = { mcpServers: {} };
    const durable = await secretStoreIsDurable(secretStore);
    const warnSessionPreserved = () => {
      if (warnedSessionPreserved || durable || !fileLogger) return;
      warnedSessionPreserved = true;
      fileLogger.warn(
        "Secrets are kept in memory for this session, so plaintext values in mcp.json are left on disk rather than migrated away. They would otherwise be lost when the Inspector exits."
      );
    };
    try {
      for (const [id, stored] of Object.entries(config.mcpServers)) {
        const { stripped, secrets } = extractSecretsFromStored(stored);
        if (Object.keys(secrets).length === 0) {
          next.mcpServers[id] = stored;
          continue;
        }
        for (const [field, value] of Object.entries(secrets)) {
          const existing = await secretStoreGetStrict(secretStore, id, field);
          if (existing === null) {
            await secretStore.set(id, field, value);
          }
        }
        if (!durable) {
          warnSessionPreserved();
          next.mcpServers[id] = stored;
          continue;
        }
        next.mcpServers[id] = stripped;
        changed = true;
      }
    } catch (err) {
      if (err instanceof SecretStoreUnavailableError) {
        if (fileLogger) {
          fileLogger.warn(
            { err: err.message },
            "Secret store unavailable; skipping plaintext-secret migration on this read. Existing mcp.json plaintext values are preserved."
          );
        }
        return { migrated: config, changed: false };
      }
      throw err;
    }
    return { migrated: next, changed };
  };
  const rehydrateConfig = async (config) => {
    const entries = Object.entries(config.mcpServers);
    const secrets = await secretStoreGetMany(
      secretStore,
      entries.map(([serverId, stored]) => ({
        serverId,
        fields: expectedSecretFields(stored)
      }))
    );
    const out = { mcpServers: {} };
    for (const [id, stored] of entries) {
      out.mcpServers[id] = mergeSecretsIntoStored(stored, secrets[id] ?? {});
    }
    return out;
  };
  const mergeRenameKeychainSecrets = (stripped, keychainSecrets, nextSecrets) => {
    const allowedFields = new Set(expectedSecretFields(stripped));
    const merged = { ...keychainSecrets, ...nextSecrets };
    const out = {};
    for (const [field, value] of Object.entries(merged)) {
      if (allowedFields.has(field)) out[field] = value;
    }
    return out;
  };
  const deleteKeychainFields = async (id, fields) => {
    await Promise.all(fields.map((field) => secretStore.delete(id, field)));
  };
  const entryForDisk = async (built, stripped, previous) => {
    if (await secretStoreIsDurable(secretStore)) return stripped;
    const prior = previous ? extractSecretsFromStored(previous).secrets : {};
    const submitted = extractSecretsFromStored(built).secrets;
    const carry = {};
    for (const [field, value] of Object.entries(submitted)) {
      if (prior[field] === value) carry[field] = value;
    }
    return Object.keys(carry).length > 0 ? mergeSecretsIntoStored(stripped, carry) : stripped;
  };
  const keychainErrorResponse = (c, err) => {
    if (err instanceof SecretStoreUnavailableError) {
      return c.json({ error: err.message }, 503);
    }
    return void 0;
  };
  const requireWritable = (c) => {
    if (writable) return void 0;
    return c.json({ error: "Server list is read-only for this session." }, 403);
  };
  app.get("/api/servers", async (c) => {
    try {
      if (inMemoryServers) {
        const normalized = {
          mcpServers: normalizeMcpServers(inMemoryServers.mcpServers)
        };
        return c.json(await rehydrateConfig(normalized));
      }
      const raw = await readStoreFile(mcpConfigPath);
      if (raw !== null) {
        const parsed = parseStore(raw);
        const onDisk = {
          mcpServers: normalizeMcpServers(parsed?.mcpServers)
        };
        if (!writable) {
          return c.json(await rehydrateConfig(onDisk));
        }
        if (!hasPlaintextSecrets(onDisk)) {
          return c.json(await rehydrateConfig(onDisk));
        }
      } else if (!writable) {
        return c.json({ mcpServers: {} });
      }
      const settled = await withWriteLock(async () => {
        const rawInside = await readStoreFile(mcpConfigPath);
        if (rawInside === null) {
          await writeMcpAndTrackMtime(serializeStore(DEFAULT_SEED_CONFIG));
          return DEFAULT_SEED_CONFIG;
        }
        const parsedInside = parseStore(rawInside);
        const inside = {
          mcpServers: normalizeMcpServers(parsedInside?.mcpServers)
        };
        if (!hasPlaintextSecrets(inside)) return inside;
        const { migrated, changed } = await migratePlaintextSecrets(inside);
        if (changed) {
          await writeMcpAndTrackMtime(serializeStore(migrated));
        }
        return migrated;
      });
      return c.json(await rehydrateConfig(settled));
    } catch (error) {
      const keychainResp = keychainErrorResponse(c, error);
      if (keychainResp) return keychainResp;
      const msg = error instanceof Error ? error.message : String(error);
      return c.json({ error: `Failed to read server list: ${msg}` }, 500);
    }
  });
  app.get("/api/import-source", (c) => {
    const type = c.req.query("type") ?? "";
    const result = resolveImportSource(
      type,
      process.platform,
      homedir(),
      (path6) => existsSync3(path6) ? readFileSync5(path6, "utf-8") : null
    );
    if (!result) {
      return c.json(
        { error: `Unknown import source: ${type || "(none)"}` },
        400
      );
    }
    return c.json(result);
  });
  app.post("/api/servers", async (c) => {
    const ro = requireWritable(c);
    if (ro) return ro;
    let body;
    try {
      body = await c.req.json();
    } catch {
      return c.json({ error: "Invalid JSON body" }, 400);
    }
    if (typeof body.id !== "string" || !validateStoreId(body.id)) {
      return c.json(
        {
          error: "Invalid id: must be non-empty and contain only alphanumeric, hyphen, or underscore"
        },
        400
      );
    }
    if (!body.config || typeof body.config !== "object") {
      return c.json({ error: "Missing or invalid config" }, 400);
    }
    let postSettings;
    if (body.settings !== void 0 && body.settings !== null) {
      const validated = validateSettings(body.settings);
      if (!validated.ok) return c.json({ error: validated.error }, 400);
      postSettings = validated.value;
    }
    const id = body.id;
    try {
      return await withWriteLock(async () => {
        const current = await readMcpConfig();
        if (id in current.mcpServers) {
          return c.json({ error: `Server '${id}' already exists` }, 409);
        }
        const built = buildStoredEntry(id, body.config, postSettings);
        const { stripped, secrets } = extractSecretsFromStored(built);
        await secretStore.deleteAllForServer(id);
        await writeKeychainEntriesFor(id, secrets);
        current.mcpServers[id] = await entryForDisk(built, stripped);
        await writeMcpAndTrackMtime(serializeStore(current));
        return c.json({ ok: true });
      });
    } catch (error) {
      const keychainResp = keychainErrorResponse(c, error);
      if (keychainResp) return keychainResp;
      const msg = error instanceof Error ? error.message : String(error);
      return c.json({ error: `Failed to add server: ${msg}` }, 500);
    }
  });
  app.put("/api/servers/order", async (c) => {
    const ro = requireWritable(c);
    if (ro) return ro;
    let body;
    try {
      body = await c.req.json();
    } catch {
      return c.json({ error: "Invalid JSON body" }, 400);
    }
    if (!Array.isArray(body.order) || !body.order.every((id) => typeof id === "string")) {
      return c.json({ error: "order must be an array of strings" }, 400);
    }
    const order = body.order;
    if (new Set(order).size !== order.length) {
      return c.json({ error: "order contains duplicate ids" }, 400);
    }
    try {
      return await withWriteLock(async () => {
        const current = await readMcpConfig();
        const currentIds = Object.keys(current.mcpServers);
        const currentSet = new Set(currentIds);
        const sameSet = currentIds.length === order.length && order.every((id) => currentSet.has(id));
        if (!sameSet) {
          return c.json(
            {
              error: "order does not match the current server set (it may have changed on disk)"
            },
            409
          );
        }
        const next = { mcpServers: {} };
        for (const id of order) {
          next.mcpServers[id] = current.mcpServers[id];
        }
        await writeMcpAndTrackMtime(serializeStore(next));
        return c.json({ ok: true });
      });
    } catch (error) {
      const msg = error instanceof Error ? error.message : String(error);
      return c.json({ error: `Failed to reorder servers: ${msg}` }, 500);
    }
  });
  app.put("/api/servers/:id", async (c) => {
    const ro = requireWritable(c);
    if (ro) return ro;
    const originalId = c.req.param("id");
    if (!originalId || !validateStoreId(originalId)) {
      return c.json({ error: "Invalid id" }, 400);
    }
    let body;
    try {
      body = await c.req.json();
    } catch {
      return c.json({ error: "Invalid JSON body" }, 400);
    }
    if (body.config !== void 0 && (body.config === null || typeof body.config !== "object")) {
      return c.json({ error: "Invalid config" }, 400);
    }
    let settingsIntent;
    if (body.settings === void 0) {
      settingsIntent = { kind: "preserve" };
    } else if (body.settings === null) {
      settingsIntent = { kind: "clear" };
    } else {
      const validated = validateSettings(body.settings);
      if (!validated.ok) return c.json({ error: validated.error }, 400);
      settingsIntent = {
        kind: "apply",
        value: validated.value,
        envProvided: validated.envProvided,
        cwdProvided: validated.cwdProvided
      };
    }
    const newId = typeof body.id === "string" ? body.id : originalId;
    if (!validateStoreId(newId)) {
      return c.json({ error: "Invalid new id" }, 400);
    }
    try {
      return await withWriteLock(async () => {
        const current = await readMcpConfig();
        if (!(originalId in current.mcpServers)) {
          return c.json({ error: `Server '${originalId}' not found` }, 404);
        }
        if (newId !== originalId && newId in current.mcpServers) {
          return c.json({ error: `Server '${newId}' already exists` }, 409);
        }
        const existing = current.mcpServers[originalId];
        if (!existing) {
          return c.json({ error: `Server '${originalId}' not found` }, 404);
        }
        const existingConfig = stripInspectorFields(existing);
        const existingSettings = storedFieldsToInspectorSettings(existing);
        const nextConfig = body.config !== void 0 ? body.config : existingConfig;
        let nextSettings;
        switch (settingsIntent.kind) {
          case "preserve":
            nextSettings = existingSettings;
            break;
          case "clear":
            nextSettings = void 0;
            break;
          case "apply":
            nextSettings = settingsIntent.value;
            break;
        }
        const built = buildStoredEntry(newId, nextConfig, nextSettings);
        if (body.config === void 0 && settingsIntent.kind === "apply") {
          applyStdioSettingsToConfig(built, settingsIntent.value, {
            env: settingsIntent.envProvided,
            cwd: settingsIntent.cwdProvided
          });
          if (!settingsIntent.envProvided && (built.type === "stdio" || built.type === void 0)) {
            const stdio = built;
            if (stdio.env && Object.keys(stdio.env).length > 0) {
              const keys = Object.keys(stdio.env);
              const existingSecrets = await readKeychainEntriesFor(
                originalId,
                keys.map((k) => envSecretField(k))
              );
              const rehydrated = { ...stdio.env };
              for (const k of keys) {
                const v = existingSecrets[envSecretField(k)];
                if (v !== void 0) rehydrated[k] = v;
              }
              stdio.env = rehydrated;
            }
          }
        }
        const { stripped, secrets } = extractSecretsFromStored(built);
        const onDisk = await entryForDisk(built, stripped, existing);
        const next = { mcpServers: {} };
        for (const [key, val] of Object.entries(current.mcpServers)) {
          if (key === originalId) {
            next.mcpServers[newId] = onDisk;
          } else {
            next.mcpServers[key] = val;
          }
        }
        if (newId !== originalId) {
          const previousFields = expectedSecretFields(existing);
          const keychainSecrets = await readKeychainEntriesFor(
            originalId,
            previousFields
          );
          const secretsToWrite = mergeRenameKeychainSecrets(
            stripped,
            keychainSecrets,
            secrets
          );
          await writeKeychainEntriesFor(newId, secretsToWrite);
          await writeMcpAndTrackMtime(serializeStore(next));
          await secretStore.deleteAllForServer(originalId);
        } else {
          const previousFields = new Set(expectedSecretFields(existing));
          const stillExpected = new Set(expectedSecretFields(built));
          const obsolete = [...previousFields].filter(
            (field) => field === SECRET_FIELD_OAUTH_CLIENT_SECRET ? settingsIntent.kind !== "preserve" && !(field in secrets) : !stillExpected.has(field)
          );
          await writeKeychainEntriesFor(newId, secrets);
          await writeMcpAndTrackMtime(serializeStore(next));
          await deleteKeychainFields(newId, obsolete);
        }
        return c.json({ ok: true });
      });
    } catch (error) {
      const keychainResp = keychainErrorResponse(c, error);
      if (keychainResp) return keychainResp;
      const msg = error instanceof Error ? error.message : String(error);
      return c.json({ error: `Failed to update server: ${msg}` }, 500);
    }
  });
  app.delete("/api/servers/:id", async (c) => {
    const ro = requireWritable(c);
    if (ro) return ro;
    const id = c.req.param("id");
    if (!id || !validateStoreId(id)) {
      return c.json({ error: "Invalid id" }, 400);
    }
    try {
      return await withWriteLock(async () => {
        const current = await readMcpConfig();
        if (!(id in current.mcpServers)) {
          await secretStore.deleteAllForServer(id);
          return c.json({ ok: true });
        }
        delete current.mcpServers[id];
        await writeMcpAndTrackMtime(serializeStore(current));
        await secretStore.deleteAllForServer(id);
        return c.json({ ok: true });
      });
    } catch (error) {
      const keychainResp = keychainErrorResponse(c, error);
      if (keychainResp) return keychainResp;
      const msg = error instanceof Error ? error.message : String(error);
      return c.json({ error: `Failed to delete server: ${msg}` }, 500);
    }
  });
  app.get("/api/servers/events", async (c) => {
    return streamSSE(c, async (stream) => {
      const send = (data) => {
        void stream.writeSSE({ event: "change", data });
      };
      if (watchable) {
        serverEventSubscribers.add(send);
        ensureWatcher();
      }
      await primeAndHoldSseStream(stream, () => {
        serverEventSubscribers.delete(send);
        void maybeStopWatcher();
      });
    });
  });
  return {
    app,
    authToken,
    close: async () => {
      serverEventSubscribers.clear();
      await maybeStopWatcher();
    }
  };
}

// server/inject-auth-token.ts
function serializeTokenForScript(token) {
  return JSON.stringify(token).replace(/</g, "\\u003c");
}
function injectAuthToken(html, token) {
  if (!token) return html;
  const script = `<script>window.${INSPECTOR_API_TOKEN_GLOBAL} = ${serializeTokenForScript(
    token
  )};</script>`;
  const headClose = html.indexOf("</head>");
  if (headClose !== -1) {
    return html.slice(0, headClose) + script + html.slice(headClose);
  }
  const bodyClose = html.indexOf("</body>");
  if (bodyClose !== -1) {
    return html.slice(0, bodyClose) + script + html.slice(bodyClose);
  }
  return script + html;
}

// server/vite-hono-plugin.ts
function honoMiddlewarePlugin(config) {
  let resolvedAuthToken = "";
  return {
    name: "hono-api-middleware",
    // Embed the API token into the dev-served index.html so a reload at the
    // bare URL (no `?MCP_INSPECTOR_API_TOKEN=…`) still authenticates. The
    // prod server applies the same injection in `server.ts`.
    transformIndexHtml(html) {
      return injectAuthToken(html, resolvedAuthToken);
    },
    // `apply: 'serve'` keeps the plugin out of `vite build`, but Vitest still
    // instantiates a Vite server in middleware mode (no HTTP server) for
    // transforms and invokes `configureServer` regardless. Returning early
    // when `server.httpServer` is missing keeps the plugin inert in that
    // context — only an actual `vite dev` (or `vite preview`) instance has
    // an HTTP server to attach to.
    apply: "serve",
    async configureServer(server) {
      if (process.env.VITEST) {
        return;
      }
      if (!server.httpServer) {
        return;
      }
      const sandboxController = createSandboxController({
        port: config.sandboxPort,
        host: config.sandboxHost,
        allowedOrigins: config.allowedOrigins
      });
      await sandboxController.start();
      const appOriginController = createAppOriginController({
        port: config.appOriginPort,
        host: config.sandboxHost,
        embedderOrigins: appDocumentEmbedders(
          sandboxController.getUrl(),
          config.allowedOrigins
        )
      });
      await appOriginController.start();
      const secretStorage = await getSecretStorageInfo();
      const {
        app: honoApp,
        authToken: resolvedToken,
        close: closeApi
      } = createRemoteApp({
        authToken: config.dangerouslyOmitAuth ? void 0 : config.authToken,
        dangerouslyOmitAuth: config.dangerouslyOmitAuth,
        storageDir: config.storageDir,
        mcpConfigPath: config.mcpConfigPath,
        writable: config.writable,
        initialServers: config.initialServers ?? void 0,
        allowedOrigins: config.allowedOrigins,
        sandboxUrl: sandboxController.getUrl() ?? void 0,
        publishAppDocument: (doc) => appOriginController.publish(doc),
        logger: config.logger,
        initialConfig: webServerConfigToInitialPayload(config, secretStorage),
        // The startup value above is what the banner printed; the route
        // re-resolves per request, because the first write under a newly-set
        // passphrase encrypts a pre-existing plaintext file and a captured
        // descriptor would keep reporting the old state until a restart.
        secretStorageResolver: getSecretStorageInfo
      });
      resolvedAuthToken = config.dangerouslyOmitAuth ? "" : resolvedToken;
      const originalClose = server.close.bind(server);
      server.close = async () => {
        await closeApi();
        await sandboxController.close();
        await appOriginController.close();
        return originalClose();
      };
      const sandboxUrl = sandboxController.getUrl();
      const logBanner = () => {
        const address = server.httpServer?.address();
        const actualPort = typeof address === "object" && address !== null ? address.port : config.port;
        const url = printServerBanner(
          config,
          actualPort,
          resolvedToken,
          sandboxUrl ?? void 0,
          secretStorage
        );
        if (config.autoOpen) {
          openBrowser(url);
        }
      };
      server.httpServer.once("listening", () => {
        setImmediate(async () => {
          try {
            await server.warmupRequest("/src/main.tsx");
          } catch (err) {
            console.warn("[vite] entry warmup failed:", err);
          }
          logBanner();
        });
      });
      const honoMiddleware = async (req, res, next) => {
        try {
          const pathname = req.url || "";
          if (!pathname.startsWith("/api")) {
            return next();
          }
          const url = `http://${req.headers.host}${pathname}`;
          const headers = new Headers();
          Object.entries(req.headers).forEach(
            ([key, value]) => {
              if (value) {
                headers.set(
                  key,
                  Array.isArray(value) ? value.join(", ") : value
                );
              }
            }
          );
          const init = { method: req.method, headers };
          if (req.method !== "GET" && req.method !== "HEAD") {
            const chunks = [];
            req.on("data", (chunk) => chunks.push(chunk));
            await new Promise((resolve9) => {
              req.once("end", () => resolve9());
              req.once("error", () => resolve9());
              req.once("close", () => resolve9());
            });
            if (chunks.length > 0) {
              init.body = Buffer.concat(chunks);
            }
          }
          const response = await honoApp.fetch(new Request(url, init));
          res.statusCode = response.status;
          response.headers.forEach((value, key) => {
            res.setHeader(key, value);
          });
          const isSSE = response.headers.get("content-type")?.includes("text/event-stream");
          if (isSSE) {
            res.setHeader("X-Accel-Buffering", "no");
            res.setHeader("Cache-Control", "no-cache");
            res.setHeader("Connection", "keep-alive");
          }
          if (response.body) {
            res.flushHeaders?.();
            const reader = response.body.getReader();
            const isDisconnect = (code) => code === "ERR_STREAM_DESTROYED" || code === "EPIPE" || code === "ECONNRESET";
            let clientGone = false;
            const onClose = () => {
              clientGone = true;
              reader.cancel().catch(() => {
              });
            };
            const onError = (err) => {
              clientGone = true;
              reader.cancel().catch(() => {
              });
              if (!isDisconnect(err.code)) {
                console.error("[Hono Middleware] Response error:", err);
              }
            };
            res.on("close", onClose);
            res.on("error", onError);
            const pump = async () => {
              try {
                while (true) {
                  const { done, value } = await reader.read();
                  if (done || clientGone || res.destroyed) break;
                  await new Promise((resolve9, reject) => {
                    res.write(
                      Buffer.from(value),
                      (err) => err ? reject(err) : resolve9()
                    );
                  });
                }
              } catch (err) {
                const code = err?.code;
                if (!clientGone && !res.destroyed && !isDisconnect(code)) {
                  console.error("[Hono Middleware] Stream error:", err);
                }
              } finally {
                res.off("close", onClose);
                res.off("error", onError);
                await reader.cancel().catch(() => {
                });
                if (!res.writableEnded && !res.destroyed) {
                  res.end();
                }
              }
            };
            void pump();
          } else {
            res.end();
          }
        } catch (error) {
          next(error);
        }
      };
      server.middlewares.use(honoMiddleware);
    }
  };
}

// server/vite-base-config.ts
import { rmSync } from "fs";
import { join as join6 } from "path";
var NODE_ONLY_OPTIMIZE_DEPS_EXCLUDE = [
  "@modelcontextprotocol/client/stdio",
  // `atomically` is reached only through `core/storage/store-io.ts`,
  // which is imported by `core/mcp/remote/node/server.ts` (the Hono
  // app). The module never lands in the browser graph; excluding it
  // keeps Vite's dev-time scanner from chasing it through the plugin's
  // node-only import chain.
  "atomically",
  // `chokidar` is only loaded inside `core/mcp/remote/node/server.ts`
  // when the lazy mcp.json watcher starts. It transitively imports
  // `readdirp` and core node fs/os modules; excluding it keeps Vite's
  // dep scanner from walking into them during dev startup.
  "chokidar",
  "cross-spawn",
  "which",
  // `@napi-rs/keyring` is loaded only inside
  // `core/auth/node/secret-store.ts` from the Hono `/api/servers`
  // handlers — and lazily even there, via a cached dynamic import, so
  // an unsupported platform degrades instead of crashing at startup
  // (#1905). It's a native-binding package (no browser code path) so
  // excluding it keeps Vite's dep scanner from chasing into the
  // platform-specific binaries during dev startup.
  "@napi-rs/keyring",
  // `proper-lockfile` is reached only through `core/auth/node/file-lock.ts`
  // — the secrets file's cross-process lock (#2082) — which the Hono
  // `/api/servers` handlers pull in via `core/auth/node/file-secret-store.ts`.
  // Same node-only import chain as `atomically` above, and the same reason:
  // it is CJS with a `graceful-fs`/`signal-exit`/`retry` graph that Vite's
  // dev scanner has no business walking. Note the tsup `external` lists do
  // **not** cover this — they configure the production bundles, not `vite dev`.
  "proper-lockfile"
];
function getViteDevOptimizeDeps() {
  return {
    exclude: [...NODE_ONLY_OPTIMIZE_DEPS_EXCLUDE],
    force: true,
    ignoreOutdatedRequests: true,
    include: ["ajv", "@modelcontextprotocol/client/validators/ajv"]
  };
}
function clearViteDepsCache(clientWebRoot) {
  rmSync(join6(clientWebRoot, "node_modules", ".vite"), {
    recursive: true,
    force: true
  });
}

// ../../vitest.shared.mts
import path5 from "path";
import { fileURLToPath as fileURLToPath3 } from "url";
function vitestSharedPaths(clientDir) {
  const dirname10 = path5.resolve(clientDir);
  const repoRoot = path5.resolve(dirname10, "../..");
  const sharedAliases = {
    "@inspector/core": path5.resolve(repoRoot, "core"),
    "@modelcontextprotocol/inspector-test-server": path5.resolve(
      repoRoot,
      "test-servers/build/index.js"
    )
  };
  const sharedDedupe = [
    "react",
    "react-dom",
    "@modelcontextprotocol/client",
    "@modelcontextprotocol/core"
  ];
  const nodeModulesAliases = [
    {
      find: /^react$/,
      replacement: path5.resolve(dirname10, "node_modules/react")
    },
    {
      find: /^react\/jsx-runtime$/,
      replacement: path5.resolve(dirname10, "node_modules/react/jsx-runtime.js")
    },
    {
      find: /^react\/jsx-dev-runtime$/,
      replacement: path5.resolve(
        dirname10,
        "node_modules/react/jsx-dev-runtime.js"
      )
    },
    {
      find: /^react-dom$/,
      replacement: path5.resolve(dirname10, "node_modules/react-dom")
    },
    {
      find: /^react-dom\/client$/,
      replacement: path5.resolve(dirname10, "node_modules/react-dom/client.js")
    },
    { find: /^pino$/, replacement: path5.resolve(dirname10, "node_modules/pino") },
    {
      find: /^pino\/browser\.js$/,
      replacement: path5.resolve(dirname10, "node_modules/pino/browser.js")
    },
    {
      find: /^hono$/,
      replacement: path5.resolve(dirname10, "node_modules/hono/dist/index.js")
    },
    {
      find: /^hono\/streaming$/,
      replacement: path5.resolve(
        dirname10,
        "node_modules/hono/dist/helper/streaming/index.js"
      )
    },
    {
      find: /^@hono\/node-server$/,
      replacement: path5.resolve(dirname10, "node_modules/@hono/node-server")
    },
    {
      find: /^atomically$/,
      replacement: path5.resolve(dirname10, "node_modules/atomically")
    },
    {
      find: /^chokidar$/,
      replacement: path5.resolve(dirname10, "node_modules/chokidar")
    },
    {
      find: /^@napi-rs\/keyring$/,
      replacement: path5.resolve(dirname10, "node_modules/@napi-rs/keyring")
    },
    // `express` and `yaml` resolve from the **repo root**, unlike every pin
    // above. Both are reached only through `test-servers/src` — express by the
    // http/oauth servers, yaml by `load-config.ts` — which is root-owned code
    // with no manifest of its own, so the root is where they are declared and
    // the only place a client's resolution chain is guaranteed to find them.
    // Pointing these at `<client>/node_modules` is what broke when the MCP
    // packages moved to the root (#1970): express was never declared by a client
    // at all, it arrived in `clients/cli` as a peer of `express-rate-limit`
    // under `@modelcontextprotocol/server-legacy`, so removing that manifest
    // entry took express with it and every cli test that spawns a test server
    // failed to resolve it.
    {
      find: /^express$/,
      replacement: path5.resolve(repoRoot, "node_modules/express")
    },
    {
      find: /^yaml$/,
      replacement: path5.resolve(repoRoot, "node_modules/yaml")
    },
    // Same reasoning, one layer in: `proper-lockfile` is reached only through
    // `core/` (the secrets file's cross-process lock, #2082), which is the
    // other root-owned tree with no manifest of its own. Resolution finds the
    // root copy on its own today — nothing declares it in a client — and this
    // pin is what keeps that from depending on nothing ever arriving as some
    // client's transitive dependency, which would otherwise give a test two
    // copies of a module whose whole job is a single registry of held locks.
    {
      find: /^proper-lockfile$/,
      replacement: path5.resolve(repoRoot, "node_modules/proper-lockfile")
    }
  ];
  const projectResolve = {
    alias: [
      ...Object.entries(sharedAliases).map(([find, replacement]) => ({
        find,
        replacement
      })),
      ...nodeModulesAliases
    ],
    dedupe: sharedDedupe
  };
  return {
    repoRoot,
    sharedAliases,
    sharedDedupe,
    nodeModulesAliases,
    projectResolve
  };
}

// server/start-vite-dev-server.ts
var __dirname2 = dirname7(fileURLToPath4(import.meta.url));
async function startViteDevServer(config) {
  const root = resolve6(join7(__dirname2, ".."));
  clearViteDepsCache(root);
  const { repoRoot, sharedAliases, sharedDedupe, nodeModulesAliases } = vitestSharedPaths(root);
  const inlineConfig = {
    optimizeDeps: getViteDevOptimizeDeps(),
    configFile: false,
    root,
    resolve: {
      alias: [
        ...Object.entries(sharedAliases).map(([find, replacement]) => ({
          find,
          replacement
        })),
        ...nodeModulesAliases
      ],
      dedupe: sharedDedupe
    },
    server: {
      port: config.port,
      host: config.hostname,
      // `strictPort: true` (matching `vite.config.ts`) so a busy port fails
      // loudly instead of silently binding a different one — the origin
      // allow-list and the sandbox `frame-ancestors` are derived from
      // `config.port`, so a drifted bind would 403 every connect and CSP-block
      // the MCP Apps iframe while the banner advertises the unusable port.
      strictPort: true,
      // Allow Vite to serve source files from the repo root (core/ lives
      // outside clients/web), matching `vite.config.ts`'s `server.fs.allow`.
      fs: {
        allow: [repoRoot]
      }
    },
    plugins: [react(), honoMiddlewarePlugin(config)]
  };
  const server = await createServer3(inlineConfig);
  await server.listen();
  return {
    async close() {
      await server.close();
    }
  };
}

// server/server.ts
import { readFileSync as readFileSync6 } from "fs";
import { join as join8, dirname as dirname8 } from "path";
import { fileURLToPath as fileURLToPath5 } from "url";
import { randomBytes as randomBytes4 } from "crypto";
import { serve } from "@hono/node-server";
import { serveStatic } from "@hono/node-server/serve-static";
import { Hono as Hono2 } from "hono";
var __filename = fileURLToPath5(import.meta.url);
var __dirname3 = dirname8(__filename);
async function startHonoServer(config) {
  const sandboxController = createSandboxController({
    port: config.sandboxPort,
    host: config.sandboxHost,
    allowedOrigins: config.allowedOrigins
  });
  await sandboxController.start();
  const appOriginController = createAppOriginController({
    port: config.appOriginPort,
    host: config.sandboxHost,
    embedderOrigins: appDocumentEmbedders(
      sandboxController.getUrl(),
      config.allowedOrigins
    )
  });
  await appOriginController.start();
  const resolvedAuthToken = config.authToken || (config.dangerouslyOmitAuth ? "" : randomBytes4(32).toString("hex"));
  const rootPath = config.staticRoot ?? __dirname3;
  const secretStorage = await getSecretStorageInfo();
  const { app: apiApp, close: closeApi } = createRemoteApp({
    authToken: config.dangerouslyOmitAuth ? void 0 : resolvedAuthToken,
    dangerouslyOmitAuth: config.dangerouslyOmitAuth,
    storageDir: config.storageDir,
    mcpConfigPath: config.mcpConfigPath,
    writable: config.writable,
    initialServers: config.initialServers ?? void 0,
    allowedOrigins: config.allowedOrigins,
    sandboxUrl: sandboxController.getUrl() ?? void 0,
    publishAppDocument: (doc) => appOriginController.publish(doc),
    logger: config.logger,
    initialConfig: webServerConfigToInitialPayload(config, secretStorage),
    // The startup value above is what the banner printed; the route
    // re-resolves per request, because the first write under a newly-set
    // passphrase encrypts a pre-existing plaintext file and a captured
    // descriptor would keep reporting the old state until a restart.
    secretStorageResolver: getSecretStorageInfo
  });
  const app = new Hono2();
  app.use("/api/*", async (c) => {
    return apiApp.fetch(c.req.raw);
  });
  const serveIndexHtml = (c) => {
    const indexPath = join8(rootPath, "index.html");
    const html = readFileSync6(indexPath, "utf-8");
    c.header("Cache-Control", "no-store");
    return c.html(injectAuthToken(html, resolvedAuthToken));
  };
  app.get("/", async (c) => {
    try {
      return serveIndexHtml(c);
    } catch (error) {
      console.error("Error serving index.html:", error);
      return c.notFound();
    }
  });
  app.use("/*", serveStatic({ root: rootPath }));
  app.get("*", async (c) => {
    if (c.req.path.startsWith("/api")) {
      return c.notFound();
    }
    try {
      return serveIndexHtml(c);
    } catch (error) {
      console.error("Error serving index.html:", error);
      return c.notFound();
    }
  });
  const httpServer = serve(
    {
      fetch: app.fetch,
      port: config.port,
      hostname: config.hostname
    },
    (info) => {
      const sandboxUrl = sandboxController.getUrl();
      const url = printServerBanner(
        config,
        info.port,
        resolvedAuthToken,
        sandboxUrl ?? void 0,
        secretStorage
      );
      if (config.autoOpen) {
        openBrowser(url);
      }
    }
  );
  httpServer.on("error", (err) => {
    if (err.message.includes("EADDRINUSE")) {
      console.error(
        `MCP Inspector PORT IS IN USE at http://${formatHostForUrl(config.hostname)}:${config.port}`
      );
      process.exit(1);
    } else {
      throw err;
    }
  });
  return {
    async close() {
      await closeApi();
      await sandboxController.close();
      await appOriginController.close();
      if ("closeAllConnections" in httpServer) {
        httpServer.closeAllConnections();
      }
      await new Promise((resolve9, reject) => {
        httpServer.close((err) => err ? reject(err) : resolve9());
      });
    }
  };
}

// server/ensure-web-build.ts
import { existsSync as existsSync4 } from "fs";
import { join as join9 } from "path";
import { spawnSync } from "child_process";
function runViteBuild(webRoot, log) {
  const result = spawnSync("npm", ["run", "build:client"], {
    cwd: webRoot,
    stdio: "inherit",
    // npm resolves to npm.cmd on Windows, which needs a shell to be spawnable.
    shell: process.platform === "win32"
  });
  if (result.error) {
    log(`Web build failed to start: ${result.error.message}`);
  }
  return result.status;
}
function ensureWebBuild(webRoot, distRoot, deps = {}) {
  const exists = deps.exists ?? existsSync4;
  const build = deps.build ?? runViteBuild;
  const log = deps.log ?? ((message) => console.log(message));
  const indexHtml = join9(distRoot, "index.html");
  if (exists(indexHtml)) return;
  log(
    "No production web build found at clients/web/dist. Building the web UI now (first run only; this can take a minute)..."
  );
  const status = build(webRoot, log);
  if (status !== 0 || !exists(indexHtml)) {
    throw new Error(
      "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."
    );
  }
}

// server/run-web.ts
var __dirname4 = dirname9(fileURLToPath6(import.meta.url));
function ensureStdioCwd(config) {
  if ((config.type === "stdio" || config.type === void 0) && config.command && !config.cwd) {
    return { ...config, cwd: resolve7(process.cwd()) };
  }
  return config;
}
function deriveSeedServerId(config) {
  if (config.type === "sse" || config.type === "streamable-http") {
    try {
      return new URL(config.url).host || "server";
    } catch {
      return "server";
    }
  }
  return config.command?.split(/[\\/]/).pop() || "server";
}
async function runWeb(argv) {
  const program = new Command();
  const argSeparatorIndex = argv.indexOf("--");
  let preArgs = argv;
  let postArgs = [];
  if (argSeparatorIndex !== -1) {
    preArgs = argv.slice(0, argSeparatorIndex);
    postArgs = argv.slice(argSeparatorIndex + 1);
  }
  program.name("mcp-inspector-web").description("Web UI for MCP Inspector").allowExcessArguments().allowUnknownOption().option(
    "-e <env>",
    "environment variables in KEY=VALUE format",
    parseKeyValuePair,
    {}
  ).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(
    "--header <headers...>",
    'HTTP headers as "HeaderName: Value" pairs (for HTTP/SSE transports)',
    parseHeaderPair,
    {}
  ).option("--dev", "run in development mode (Vite)").parse(preArgs);
  const opts = program.opts();
  const args = program.args;
  const target = [...args, ...postArgs];
  const isDev = !!opts.dev;
  const catalogPath = opts.catalog ?? process.env.MCP_CATALOG_PATH;
  const hasCatalog = !!catalogPath;
  const hasConfig = !!opts.config;
  const hasHeaders = !!opts.header && Object.keys(opts.header).length > 0;
  const hasAdHocServer = target.length > 0 || !!opts.serverUrl || !!opts.transport && opts.transport !== "stdio";
  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;
  if (conflict) {
    console.error(`Error: ${conflict}`);
    process.exit(1);
  }
  let initialMcpConfig = null;
  let mcpConfigPath;
  let writable = true;
  let initialServers = null;
  if (hasCatalog) {
    if (opts.server) {
      console.warn(
        "Note: --server has no effect on the web UI yet; it lists every server in the catalog."
      );
    }
    mcpConfigPath = resolve7(process.cwd(), catalogPath);
    writable = true;
  } else if (hasConfig) {
    if (opts.server) {
      console.warn(
        "Note: --server has no effect on the web UI yet; it lists every server in the file."
      );
    }
    const resolvedConfigPath = resolve7(process.cwd(), opts.config);
    try {
      resolveServerConfigs({ configPath: resolvedConfigPath }, "multi");
    } catch (err) {
      const message = err instanceof Error ? err.message : "Could not load config file.";
      console.error("Error:", message);
      process.exit(1);
    }
    mcpConfigPath = resolvedConfigPath;
    writable = false;
  } else if (hasAdHocServer) {
    let config;
    try {
      const serverOptions = {
        serverName: opts.server,
        target: target.length > 0 ? target : void 0,
        transport: opts.transport,
        serverUrl: opts.serverUrl,
        cwd: opts.cwd,
        env: opts.e
      };
      const resolved2 = resolveServerConfigs(serverOptions, "single")[0];
      if (!resolved2) {
        console.error(
          "Error: Could not resolve server config. Pass a command/URL, or use --catalog / --config to serve a file."
        );
        process.exit(1);
      }
      config = ensureStdioCwd(resolved2);
    } catch (err) {
      const message = err instanceof Error ? err.message : "Could not resolve server config.";
      console.error("Error:", message);
      process.exit(1);
    }
    if (hasHeaders && (config.type === "stdio" || config.type === void 0)) {
      console.error(
        "Error: --header only applies to HTTP/SSE servers; stdio servers take no HTTP headers."
      );
      process.exit(1);
    }
    initialMcpConfig = config;
    const entry = hasHeaders && config.type !== "stdio" && config.type !== void 0 ? { ...config, headers: opts.header } : { ...config };
    initialServers = {
      mcpServers: { [deriveSeedServerId(config)]: entry }
    };
    writable = false;
  } else if (hasHeaders) {
    console.error(
      "Error: --header requires an ad-hoc --server-url or command target."
    );
    process.exit(1);
  }
  let webConfig;
  try {
    webConfig = buildWebServerConfig({
      initialMcpConfig,
      mcpConfigPath,
      writable,
      initialServers
    });
  } catch (err) {
    const message = err instanceof Error ? err.message : (
      /* v8 ignore next -- buildWebServerConfig only ever throws Error instances; this fallback is a defensive guard for the impossible non-Error throw. */
      "Invalid web server configuration."
    );
    console.error("Error:", message);
    process.exit(1);
  }
  const webRoot = join10(__dirname4, "..");
  const distRoot = join10(webRoot, "dist");
  if (!isDev) {
    webConfig.staticRoot = distRoot;
  }
  console.log(
    isDev ? "Starting MCP inspector in development mode..." : "Starting MCP inspector..."
  );
  let handle;
  try {
    if (isDev) {
      handle = await startViteDevServer(webConfig);
    } else {
      ensureWebBuild(webRoot, distRoot);
      handle = await startHonoServer(webConfig);
    }
  } catch (err) {
    const message = err instanceof Error ? err.message : "Web client failed to start.";
    console.error("Error:", message);
    process.exit(1);
  }
  const shutdown = () => {
    void handle.close().then(() => process.exit(0));
  };
  process.on("SIGINT", shutdown);
  process.on("SIGTERM", shutdown);
  return new Promise(() => {
  });
}

// src/index.ts
var __filename2 = fileURLToPath7(import.meta.url);
var isMain = process.argv[1] !== void 0 && resolve8(process.argv[1]) === resolve8(__filename2);
if (isMain) {
  runWeb(process.argv).then((code) => process.exit(code ?? 0)).catch((e) => {
    console.error(e);
    process.exit(1);
  });
}
export {
  runWeb
};
/* v8 ignore next 2 -- @preserve: the non-Error arm is unreachable via
   proper-lockfile, which rejects only with Errors. */
/* v8 ignore next -- @preserve: node validates the cost parameters
   synchronously (an out-of-grammar `N` throws from the call itself,
   which the corrupt-KDF test exercises), so this asynchronous channel
   is left for allocation failure — not provokable deterministically. */
/* v8 ignore next -- @preserve: unreachable, `encryptedEnvelopeProblem`
   above returns a reason for anything `decodeParts` would reject. Kept as
   the type narrowing `parts` needs. */
/* v8 ignore next -- @preserve: the walk can only run out of ancestors if
   the filesystem root itself does not exist, which would mean the process
   has no cwd to have resolved against. Kept as the loop's termination
   guard rather than trusting that invariant. */
