@modelcontextprotocol/inspector 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -109,6 +109,7 @@ var MODERN_LOG_LEVELS = [
109
109
  function isModernLogLevel(value) {
110
110
  return typeof value === "string" && MODERN_LOG_LEVELS.includes(value);
111
111
  }
112
+ var MODERN_PROTOCOL_VERSION = "2026-07-28";
112
113
 
113
114
  // ../../core/mcp/skillsSchemas.ts
114
115
  import { z } from "zod/v4";
@@ -424,7 +425,7 @@ function cleanEndpointOverride(value, field) {
424
425
  return value.trim() || void 0;
425
426
  }
426
427
  function storedFieldsToInspectorSettings(stored) {
427
- 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.skillCatalogMaxSkills !== void 0 || stored.skillCatalogMaxBytes !== 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;
428
+ 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.suppressNotificationStream !== void 0 || stored.advertisedExtensions !== void 0 || stored.maxFetchRequests !== void 0 || stored.skillCatalogMaxSkills !== void 0 || stored.skillCatalogMaxBytes !== 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;
428
429
  if (!hasAny) return void 0;
429
430
  const headersPairs = stored.headers ? Object.entries(stored.headers).map(([key, value]) => ({ key, value })) : [];
430
431
  const settings = {
@@ -456,6 +457,9 @@ function storedFieldsToInspectorSettings(stored) {
456
457
  if (isSkillCatalogLimit(stored.skillCatalogMaxBytes)) {
457
458
  settings.skillCatalogMaxBytes = stored.skillCatalogMaxBytes;
458
459
  }
460
+ if (stored.suppressNotificationStream === true) {
461
+ settings.suppressNotificationStream = true;
462
+ }
459
463
  if (isProtocolEra(stored.protocolEra)) {
460
464
  settings.protocolEra = stored.protocolEra;
461
465
  }
@@ -525,6 +529,9 @@ function inspectorSettingsToStoredFields(settings) {
525
529
  if (settings.paginatedLists) {
526
530
  out.paginatedLists = true;
527
531
  }
532
+ if (settings.suppressNotificationStream) {
533
+ out.suppressNotificationStream = true;
534
+ }
528
535
  if (settings.advertisedExtensions && Object.keys(settings.advertisedExtensions).length > 0) {
529
536
  out.advertisedExtensions = { ...settings.advertisedExtensions };
530
537
  }
@@ -591,6 +598,7 @@ var INSPECTOR_FIELD_KEY_MAP = {
591
598
  taskTtl: true,
592
599
  autoRefreshOnListChanged: true,
593
600
  paginatedLists: true,
601
+ suppressNotificationStream: true,
594
602
  advertisedExtensions: true,
595
603
  maxFetchRequests: true,
596
604
  skillCatalogMaxSkills: true,
@@ -954,7 +962,7 @@ function secretStorageCaveat(info) {
954
962
  return `The secrets file could not be read (${info.encryptionUnknown}). Saving a secret will fail rather than overwrite it.`;
955
963
  }
956
964
  if (info.plaintext) {
957
- 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.";
965
+ 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 or MCP_INSPECTOR_SECRET_KEY_FILE to encrypt them.";
958
966
  }
959
967
  return void 0;
960
968
  }
@@ -1058,14 +1066,14 @@ function isAllInterfacesHost(host) {
1058
1066
 
1059
1067
  // server/resolve-bind-host.ts
1060
1068
  var BIND_ALL_INTERFACES_ENV = "DANGEROUSLY_BIND_ALL_INTERFACES";
1061
- function isEnabled(value) {
1069
+ function isEnvFlagEnabled(value) {
1062
1070
  const v = value?.trim().toLowerCase();
1063
1071
  return v === "true" || v === "1";
1064
1072
  }
1065
1073
  var DEFAULT_BIND_HOST = "127.0.0.1";
1066
1074
  function resolveBindHostname(env = process.env) {
1067
1075
  const host = (env.HOST ?? DEFAULT_BIND_HOST).trim();
1068
- if (isAllInterfacesHost(host) && !isEnabled(env[BIND_ALL_INTERFACES_ENV])) {
1076
+ if (isAllInterfacesHost(host) && !isEnvFlagEnabled(env[BIND_ALL_INTERFACES_ENV])) {
1069
1077
  const resolved2 = canonicalUrlHost(host);
1070
1078
  const shown = stripBrackets(resolved2) === stripBrackets(host.toLowerCase()) ? `HOST="${host}"` : `HOST="${host}" (resolves to ${resolved2})`;
1071
1079
  throw new Error(
@@ -1658,7 +1666,9 @@ function buildWebServerConfig(options = {}) {
1658
1666
  );
1659
1667
  }
1660
1668
  const hostname = resolveBindHostname();
1661
- const dangerouslyOmitAuth = !!process.env.DANGEROUSLY_OMIT_AUTH;
1669
+ const dangerouslyOmitAuth = isEnvFlagEnabled(
1670
+ process.env.DANGEROUSLY_OMIT_AUTH
1671
+ );
1662
1672
  const authToken = dangerouslyOmitAuth ? "" : process.env[API_SERVER_ENV_VARS.AUTH_TOKEN] ?? process.env[LEGACY_AUTH_TOKEN_ENV] ?? "";
1663
1673
  const sandboxPort = resolveSandboxPort();
1664
1674
  const requestedAppOriginPort = resolveAppOriginPort();
@@ -1736,7 +1746,7 @@ import react from "@vitejs/plugin-react";
1736
1746
 
1737
1747
  // ../../core/mcp/remote/node/server.ts
1738
1748
  import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
1739
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "fs";
1749
+ import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
1740
1750
  import { stat as fsStat } from "fs/promises";
1741
1751
  import { homedir } from "os";
1742
1752
  import { Hono } from "hono";
@@ -2359,6 +2369,73 @@ function createProxyFetch() {
2359
2369
  };
2360
2370
  }
2361
2371
 
2372
+ // ../../core/mcp/node/notificationHeadersFetch.ts
2373
+ import { PROTOCOL_VERSION_META_KEY } from "@modelcontextprotocol/client";
2374
+ function createNotificationHeadersFetch(baseFetch) {
2375
+ return (input, init) => {
2376
+ const method = modernNotificationMethod(init);
2377
+ if (method === void 0) return baseFetch(input, init);
2378
+ const headers = new Headers(init?.headers);
2379
+ headers.set("mcp-protocol-version", method.version);
2380
+ headers.set("mcp-method", method.method);
2381
+ return baseFetch(input, { ...init, headers });
2382
+ };
2383
+ }
2384
+ function modernNotificationMethod(init) {
2385
+ if (init?.method?.toUpperCase() !== "POST") return void 0;
2386
+ if (typeof init.body !== "string") return void 0;
2387
+ let message;
2388
+ try {
2389
+ message = JSON.parse(init.body);
2390
+ } catch {
2391
+ return void 0;
2392
+ }
2393
+ if (!isRecord(message) || "id" in message) return void 0;
2394
+ if (typeof message.method !== "string") return void 0;
2395
+ const meta = isRecord(message.params) ? message.params._meta : void 0;
2396
+ const version = isRecord(meta) ? meta[PROTOCOL_VERSION_META_KEY] : void 0;
2397
+ if (typeof version !== "string" || version < MODERN_PROTOCOL_VERSION) {
2398
+ return void 0;
2399
+ }
2400
+ return { method: message.method, version };
2401
+ }
2402
+ function isRecord(value) {
2403
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2404
+ }
2405
+
2406
+ // ../../core/mcp/node/suppressNotificationStreamFetch.ts
2407
+ function createSuppressNotificationStreamFetch(baseFetch, endpoint) {
2408
+ return async (input, init) => {
2409
+ if (isStandaloneStreamRequest(input, init, endpoint)) {
2410
+ return new Response(null, {
2411
+ status: 405,
2412
+ statusText: "Method Not Allowed"
2413
+ });
2414
+ }
2415
+ return baseFetch(input, init);
2416
+ };
2417
+ }
2418
+ function requestUrl(input) {
2419
+ const raw = input instanceof Request ? input.url : input instanceof URL ? input.href : input;
2420
+ try {
2421
+ return new URL(raw);
2422
+ } catch {
2423
+ return void 0;
2424
+ }
2425
+ }
2426
+ function isStandaloneStreamRequest(input, init, endpoint) {
2427
+ const request = input instanceof Request ? input : void 0;
2428
+ const method = (init?.method ?? request?.method ?? "GET").toUpperCase();
2429
+ if (method !== "GET") return false;
2430
+ const url = requestUrl(input);
2431
+ if (!url || url.origin !== endpoint.origin || url.pathname !== endpoint.pathname || url.search !== endpoint.search) {
2432
+ return false;
2433
+ }
2434
+ const headers = new Headers(init?.headers ?? request?.headers);
2435
+ const accept = headers.get("accept")?.toLowerCase() ?? "";
2436
+ return accept.includes("text/event-stream") && !headers.has("last-event-id");
2437
+ }
2438
+
2362
2439
  // ../../core/mcp/node/transport.ts
2363
2440
  function headersFromSettings(settings) {
2364
2441
  if (!settings || settings.headers.length === 0) return void 0;
@@ -2446,10 +2523,14 @@ function createTransportNode(config, options = {}) {
2446
2523
  ...httpConfig.requestInit,
2447
2524
  ...headers && { headers }
2448
2525
  };
2526
+ const stampedFetch = createNotificationHeadersFetch(
2527
+ fetchWithOptionalAuthIntercept
2528
+ );
2529
+ const httpFetch = settings?.suppressNotificationStream === true ? createSuppressNotificationStreamFetch(stampedFetch, url) : stampedFetch;
2449
2530
  const transport = new StreamableHTTPClientTransport(url, {
2450
2531
  authProvider,
2451
2532
  requestInit,
2452
- fetch: fetchWithOptionalAuthIntercept,
2533
+ fetch: httpFetch,
2453
2534
  // SEP-2350: how the transport reacts to a `403 insufficient_scope`
2454
2535
  // challenge. Defaults to the SDK's `reauthorize` when unset.
2455
2536
  ...settings?.oauthOnInsufficientScope && {
@@ -3214,6 +3295,7 @@ import * as path4 from "path";
3214
3295
 
3215
3296
  // ../../core/auth/node/file-secret-store.ts
3216
3297
  import * as crypto2 from "crypto";
3298
+ import { readFileSync as readFileSync4, statSync as statSync2 } from "fs";
3217
3299
  import * as fs3 from "fs/promises";
3218
3300
  import * as path3 from "path";
3219
3301
 
@@ -3416,6 +3498,55 @@ var SCRYPT_N = 16384;
3416
3498
  var SCRYPT_R = 8;
3417
3499
  var SCRYPT_P = 1;
3418
3500
  var SECRET_KEY_ENV = "MCP_INSPECTOR_SECRET_KEY";
3501
+ var SECRET_KEY_FILE_ENV = "MCP_INSPECTOR_SECRET_KEY_FILE";
3502
+ function isSameFile(a, b) {
3503
+ if (path3.resolve(a) === path3.resolve(b)) return true;
3504
+ try {
3505
+ const sa = statSync2(a);
3506
+ const sb = statSync2(b);
3507
+ return sa.dev === sb.dev && sa.ino === sb.ino;
3508
+ } catch {
3509
+ return false;
3510
+ }
3511
+ }
3512
+ function resolveSecretPassphrase(env = process.env, secretFilePath) {
3513
+ const direct = env[SECRET_KEY_ENV];
3514
+ const hasDirect = direct !== void 0 && direct.trim() !== "";
3515
+ const rawKeyFile = env[SECRET_KEY_FILE_ENV];
3516
+ const wantsKeyFile = rawKeyFile !== void 0;
3517
+ const keyFile = rawKeyFile?.trim();
3518
+ if (hasDirect && wantsKeyFile) {
3519
+ return {
3520
+ problem: `both ${SECRET_KEY_ENV} and ${SECRET_KEY_FILE_ENV} are set; set only one`
3521
+ };
3522
+ }
3523
+ if (hasDirect) return { passphrase: direct };
3524
+ if (!wantsKeyFile) return {};
3525
+ if (!keyFile) {
3526
+ return { problem: `${SECRET_KEY_FILE_ENV} is set but empty` };
3527
+ }
3528
+ const resolved2 = path3.resolve(keyFile);
3529
+ if (secretFilePath !== void 0 && isSameFile(resolved2, secretFilePath)) {
3530
+ return {
3531
+ problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) is the secrets file itself; point it at a separate key file`
3532
+ };
3533
+ }
3534
+ let contents;
3535
+ try {
3536
+ contents = readFileSync4(resolved2, "utf-8");
3537
+ } catch (err) {
3538
+ return {
3539
+ // `String(err)` rather than `.message`: `readFileSync` only ever throws
3540
+ // an `Error`, so an `instanceof` guard would be a branch no test can take.
3541
+ problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) could not be read: ${String(err)}`
3542
+ };
3543
+ }
3544
+ const passphrase = contents.replace(/[\r\n]+$/, "");
3545
+ if (passphrase.trim() === "") {
3546
+ return { problem: `${SECRET_KEY_FILE_ENV} (${resolved2}) is empty` };
3547
+ }
3548
+ return { passphrase };
3549
+ }
3419
3550
  var buildAccount2 = (serverId, field) => `${serverId}:${field}`;
3420
3551
  function asSecretMap(value, filePath) {
3421
3552
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
@@ -3435,7 +3566,7 @@ function asSecretMap(value, filePath) {
3435
3566
  var SecretFileKeyMismatchError = class extends SecretStoreUnavailableError {
3436
3567
  constructor(filePath, hasKey) {
3437
3568
  super(
3438
- 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.`
3569
+ hasKey ? `The secrets file at ${filePath} could not be decrypted with the current ${SECRET_KEY_ENV} (or ${SECRET_KEY_FILE_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 (nor ${SECRET_KEY_FILE_ENV}). Refusing to write, which would overwrite the existing secrets. Set the passphrase this file was written with, or delete the file to start over.`
3439
3570
  );
3440
3571
  this.name = "SecretFileKeyMismatchError";
3441
3572
  }
@@ -3493,10 +3624,17 @@ function encryptedEnvelopeProblem(parsed) {
3493
3624
  var FileSecretStore = class {
3494
3625
  filePath;
3495
3626
  passphrase;
3627
+ /**
3628
+ * Why a configured key could not be obtained. While set, every read throws
3629
+ * and every write refuses — see {@link SecretPassphrase}.
3630
+ */
3631
+ keyProblem;
3496
3632
  constructor(options) {
3497
3633
  this.filePath = options.filePath;
3498
- const raw = options.passphrase ?? process.env[SECRET_KEY_ENV];
3634
+ const resolved2 = options.passphrase !== void 0 ? { passphrase: options.passphrase } : resolveSecretPassphrase(process.env, this.filePath);
3635
+ const raw = resolved2.passphrase;
3499
3636
  this.passphrase = raw && raw.trim() ? raw : void 0;
3637
+ this.keyProblem = resolved2.problem;
3500
3638
  }
3501
3639
  /**
3502
3640
  * True when the *next write* will encrypt. This is the configured policy,
@@ -3511,7 +3649,8 @@ var FileSecretStore = class {
3511
3649
  * no file yet, or when it cannot be read or parsed.
3512
3650
  *
3513
3651
  * Separate from {@link encrypted} because the two genuinely disagree for a
3514
- * whole session: adding `MCP_INSPECTOR_SECRET_KEY` to an install that
3652
+ * whole session: adding a passphrase (`MCP_INSPECTOR_SECRET_KEY` or
3653
+ * `MCP_INSPECTOR_SECRET_KEY_FILE`) to an install that
3515
3654
  * already has a plaintext file flips `encrypted` to true immediately,
3516
3655
  * while the bytes stay readable until the next `set`. A descriptor built
3517
3656
  * from the policy would tell that user "File (encrypted)" while their
@@ -3535,6 +3674,9 @@ var FileSecretStore = class {
3535
3674
  * `set` is in fact about to refuse.
3536
3675
  */
3537
3676
  async readOnDiskEncryption() {
3677
+ if (this.keyProblem !== void 0) {
3678
+ return { state: "unreadable", detail: this.keyProblem };
3679
+ }
3538
3680
  let raw;
3539
3681
  try {
3540
3682
  raw = await readStoreFile(this.filePath);
@@ -3572,7 +3714,7 @@ var FileSecretStore = class {
3572
3714
  } catch (err) {
3573
3715
  return {
3574
3716
  state: "unreadable",
3575
- detail: err instanceof SecretFileKeyMismatchError ? `it cannot be decrypted with the current ${SECRET_KEY_ENV}` : err instanceof Error ? err.message : String(err)
3717
+ detail: err instanceof SecretFileKeyMismatchError ? `it cannot be decrypted with the current ${SECRET_KEY_ENV} (or ${SECRET_KEY_FILE_ENV})` : err instanceof Error ? err.message : String(err)
3576
3718
  };
3577
3719
  }
3578
3720
  return { state: "encrypted" };
@@ -3623,6 +3765,11 @@ var FileSecretStore = class {
3623
3765
  return this.serialize(() => this.readMap());
3624
3766
  }
3625
3767
  async readMap() {
3768
+ if (this.keyProblem !== void 0) {
3769
+ throw new SecretStoreUnavailableError(
3770
+ `The secrets file at ${this.filePath} cannot be used: ${this.keyProblem}. Refusing to read or write it rather than store secrets unencrypted.`
3771
+ );
3772
+ }
3626
3773
  const raw = await readStoreFile(this.filePath);
3627
3774
  if (raw === null) return null;
3628
3775
  let parsed;
@@ -3978,6 +4125,7 @@ async function readSecretFilePermissions(filePath) {
3978
4125
  var SECRET_STORE_ENV = "MCP_INSPECTOR_SECRET_STORE";
3979
4126
  var SECRET_FILE_ENV = "MCP_INSPECTOR_SECRET_FILE";
3980
4127
  var STORAGE_DIR_ENV = "MCP_STORAGE_DIR";
4128
+ var SECRET_STORAGE_DOCS_URL = "https://github.com/modelcontextprotocol/inspector/blob/main/docs/secret-storage.md";
3981
4129
  var KINDS = ["keyring", "file", "memory"];
3982
4130
  function parseSecretStoreEnv(raw) {
3983
4131
  const value = raw?.trim().toLowerCase();
@@ -4120,6 +4268,11 @@ function warnAboutSecretStorage(info) {
4120
4268
  }
4121
4269
  const caveat = secretStorageCaveat(info);
4122
4270
  if (caveat) console.warn(`[mcp-inspector] ${caveat}`);
4271
+ if (info.reason === "fallback" || caveat) {
4272
+ console.warn(
4273
+ `[mcp-inspector] How the secret store is chosen, and how to secure it: ${SECRET_STORAGE_DOCS_URL}`
4274
+ );
4275
+ }
4123
4276
  }
4124
4277
  var resolved;
4125
4278
  function resolveSecretStore() {
@@ -5584,6 +5737,12 @@ function createRemoteApp(options) {
5584
5737
  error: "settings.paginatedLists must be a boolean"
5585
5738
  };
5586
5739
  }
5740
+ if (obj.suppressNotificationStream !== void 0 && typeof obj.suppressNotificationStream !== "boolean") {
5741
+ return {
5742
+ ok: false,
5743
+ error: "settings.suppressNotificationStream must be a boolean"
5744
+ };
5745
+ }
5587
5746
  if (obj.maxFetchRequests !== void 0 && (typeof obj.maxFetchRequests !== "number" || obj.maxFetchRequests < 0)) {
5588
5747
  return {
5589
5748
  ok: false,
@@ -5686,6 +5845,10 @@ function createRemoteApp(options) {
5686
5845
  autoRefreshOnListChanged: obj.autoRefreshOnListChanged === true,
5687
5846
  // Absent → false, matching the read side (omit-on-false on the write side).
5688
5847
  paginatedLists: obj.paginatedLists === true,
5848
+ // Absent → unset (off); only an explicit true is carried (#2317).
5849
+ ...obj.suppressNotificationStream === true && {
5850
+ suppressNotificationStream: true
5851
+ },
5689
5852
  // Absent → product default, matching the read side. The default is the
5690
5853
  // omit-sentinel in inspectorSettingsToStoredFields, so a client that
5691
5854
  // didn't send one writes no spurious maxFetchRequests to disk.
@@ -5936,7 +6099,7 @@ function createRemoteApp(options) {
5936
6099
  type,
5937
6100
  process.platform,
5938
6101
  homedir(),
5939
- (path6) => existsSync3(path6) ? readFileSync5(path6, "utf-8") : null
6102
+ (path6) => existsSync3(path6) ? readFileSync6(path6, "utf-8") : null
5940
6103
  );
5941
6104
  if (!result) {
5942
6105
  return c.json(
@@ -6745,7 +6908,7 @@ async function startViteDevServer(config) {
6745
6908
  }
6746
6909
 
6747
6910
  // server/server.ts
6748
- import { readFileSync as readFileSync6 } from "fs";
6911
+ import { readFileSync as readFileSync7 } from "fs";
6749
6912
  import { join as join8, dirname as dirname8 } from "path";
6750
6913
  import { fileURLToPath as fileURLToPath5 } from "url";
6751
6914
  import { randomBytes as randomBytes4 } from "crypto";
@@ -6799,7 +6962,7 @@ async function startHonoServer(config) {
6799
6962
  });
6800
6963
  const serveIndexHtml = (c) => {
6801
6964
  const indexPath = join8(rootPath, "index.html");
6802
- const html = readFileSync6(indexPath, "utf-8");
6965
+ const html = readFileSync7(indexPath, "utf-8");
6803
6966
  c.header("Cache-Control", "no-store");
6804
6967
  return c.html(injectAuthToken(html, resolvedAuthToken));
6805
6968
  };