@modelcontextprotocol/inspector 2.10.0 → 2.11.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.
@@ -821,16 +821,30 @@ function buildConfigFromOptions(options) {
821
821
  const target = options.target ?? [];
822
822
  const first = target[0];
823
823
  const rest = target.slice(1);
824
+ if (first && options.serverUrl) {
825
+ throw new Error(
826
+ `Cannot combine a positional target (${first}) with --server-url (${options.serverUrl}). Pass one or the other.`
827
+ );
828
+ }
824
829
  const urlFromTarget = first && (first.startsWith("http://") || first.startsWith("https://")) ? first : null;
825
830
  const url = urlFromTarget ?? options.serverUrl ?? null;
826
831
  if (url) {
827
- if (rest.length > 0 && urlFromTarget) {
832
+ if (rest.length > 0) {
828
833
  throw new Error("Arguments cannot be passed to a URL-based MCP server.");
829
834
  }
835
+ if (options.transport === "stdio") {
836
+ throw new Error(
837
+ `stdio transport cannot be used with a URL (${url}). Use --transport http or --transport sse, or pass a local command instead.`
838
+ );
839
+ }
830
840
  let transportType;
831
841
  const t = options.transport === "http" ? "streamable-http" : options.transport;
832
842
  if (t === "sse" || t === "streamable-http") {
833
843
  transportType = t;
844
+ } else if (t) {
845
+ throw new Error(
846
+ `Invalid transport type: ${t}. Valid types are: sse, http, stdio`
847
+ );
834
848
  } else {
835
849
  const u = new URL(url);
836
850
  if (u.pathname.endsWith("/mcp")) {
@@ -1683,7 +1697,10 @@ MCP Inspector Web is up and running at:
1683
1697
  }
1684
1698
  function openBrowser(url) {
1685
1699
  openUrl(url).catch((err) => {
1686
- console.warn("Could not open a browser automatically:", err);
1700
+ const reason = err instanceof Error ? err.message : String(err);
1701
+ console.warn(
1702
+ `Could not open a browser automatically (${reason}). Open the URL above manually.`
1703
+ );
1687
1704
  });
1688
1705
  }
1689
1706
  var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
@@ -2291,7 +2308,10 @@ function parseAuthChallengeFromResponse(response, context) {
2291
2308
  if (status !== 401 && status !== 403) {
2292
2309
  return void 0;
2293
2310
  }
2294
- const wwwAuthenticate = response.headers.get("WWW-Authenticate") ?? void 0;
2311
+ const wwwAuthenticate = response.headers.get("WWW-Authenticate") || void 0;
2312
+ if (status === 403 && !wwwAuthenticate) {
2313
+ return void 0;
2314
+ }
2295
2315
  const bearer = wwwAuthenticate ? parseWwwAuthenticateBearer(wwwAuthenticate) : {};
2296
2316
  const requiredScopes = parseScopeString(bearer.scope);
2297
2317
  return {
@@ -2623,6 +2643,9 @@ function createTransportNode(config, options = {}) {
2623
2643
  // ../../core/auth/requestTimeout.ts
2624
2644
  var OAUTH_TIMEOUT_WIRE_CODE = "oauth_request_timeout";
2625
2645
 
2646
+ // ../../core/mcp/remote/node/server.ts
2647
+ import { SdkHttpError } from "@modelcontextprotocol/client";
2648
+
2626
2649
  // ../../core/auth/oauth-persist.ts
2627
2650
  import { OAuthTokensSchema } from "@modelcontextprotocol/core";
2628
2651
  var OAUTH_PERSIST_STORE_ID = "oauth";
@@ -4723,6 +4746,17 @@ async function recordNamespaceKeys(stateFilePath, secretStore, namespace, server
4723
4746
  warnLedgerFailure("update", error);
4724
4747
  }
4725
4748
  }
4749
+ async function recordedNamespaces(stateFilePath) {
4750
+ try {
4751
+ const ledger = parseLedger(
4752
+ await readStoreFile(namespaceLedgerPath(stateFilePath))
4753
+ );
4754
+ return [...ledger.keys()].reverse();
4755
+ } catch (error) {
4756
+ warnLedgerFailure("read", error);
4757
+ return [];
4758
+ }
4759
+ }
4726
4760
  async function purgeSupersededNamespaces(stateFilePath, secretStore, current) {
4727
4761
  let ledger;
4728
4762
  let location;
@@ -4886,24 +4920,13 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
4886
4920
  if (raw !== null && snapshot === null) {
4887
4921
  throw new OAuthStateFileUnrecognizedError(filePath, "save");
4888
4922
  }
4889
- if (locked) {
4890
- await purgeSupersededNamespaces(filePath, secretStore, void 0);
4891
- }
4892
4923
  const namespace = newSecretsNamespace();
4893
- if (snapshot === null) return namespace;
4894
- const moves = [
4895
- ...Object.entries(snapshot.servers).map(([url, state]) => ({
4896
- legacyId: oauthSecretServerId(url),
4897
- scopedId: oauthSecretServerId(url, namespace),
4898
- fields: serverSecretFields(state)
4899
- })),
4900
- ...Object.keys(snapshot.idpSessions).map((issuer) => ({
4901
- legacyId: oauthIdpSecretServerId(issuer),
4902
- scopedId: oauthIdpSecretServerId(issuer, namespace),
4903
- fields: [IDP_SESSION_FIELD]
4904
- }))
4905
- ];
4906
- if (moves.length === 0) return namespace;
4924
+ if (snapshot === null || Object.keys(snapshot.servers).length === 0 && Object.keys(snapshot.idpSessions).length === 0) {
4925
+ if (locked) {
4926
+ await purgeSupersededNamespaces(filePath, secretStore, void 0);
4927
+ }
4928
+ return namespace;
4929
+ }
4907
4930
  if (!locked) {
4908
4931
  throw new SecretStoreUnavailableError(
4909
4932
  `Could not save OAuth state: ${filePath} predates per-state-file secret namespaces, and migrating its secret-store entries needs the file lock, which is unavailable here (see the lock warning above). Migrating without it could lose credentials if two processes migrate at once. Nothing was changed; make the lock directory writable and retry \u2014 or, if you cannot, clear this file's stored OAuth state and re-authorize (clearing does not migrate, and the fresh state file mints its namespace without the lock).`
@@ -4918,10 +4941,24 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
4918
4941
  );
4919
4942
  const touched = [];
4920
4943
  try {
4921
- for (const { legacyId, scopedId, fields } of moves) {
4922
- for (const field of fields) {
4923
- const value = await secretStoreGetStrict(secretStore, legacyId, field);
4924
- if (value === null) continue;
4944
+ const values = await readEntrySecrets(
4945
+ snapshot,
4946
+ secretStore,
4947
+ void 0,
4948
+ await recordedNamespaces(filePath)
4949
+ );
4950
+ const copies = [
4951
+ ...[...values.servers].map(([url, secrets]) => ({
4952
+ scopedId: oauthSecretServerId(url, namespace),
4953
+ secrets
4954
+ })),
4955
+ ...[...values.idpSessions].map(([issuer, secrets]) => ({
4956
+ scopedId: oauthIdpSecretServerId(issuer, namespace),
4957
+ secrets
4958
+ }))
4959
+ ];
4960
+ for (const { scopedId, secrets } of copies) {
4961
+ for (const [field, value] of Object.entries(secrets)) {
4925
4962
  touched.push({ serverId: scopedId, field, value: null });
4926
4963
  await secretStore.set(scopedId, field, value);
4927
4964
  }
@@ -4931,7 +4968,14 @@ async function adoptSecretsNamespace(filePath, secretStore, locked) {
4931
4968
  await restoreSecretFields(secretStore, touched, warnRestoreFailure);
4932
4969
  throw error;
4933
4970
  }
4934
- for (const { legacyId } of moves) {
4971
+ await purgeSupersededNamespaces(filePath, secretStore, namespace);
4972
+ const legacyIds = [
4973
+ ...Object.keys(snapshot.servers).map((url) => oauthSecretServerId(url)),
4974
+ ...Object.keys(snapshot.idpSessions).map(
4975
+ (issuer) => oauthIdpSecretServerId(issuer)
4976
+ )
4977
+ ];
4978
+ for (const legacyId of legacyIds) {
4935
4979
  try {
4936
4980
  await secretStore.deleteAllForServer(legacyId);
4937
4981
  } catch (error) {
@@ -4984,6 +5028,14 @@ function preserveNonDurableSecrets(diskSecrets, secrets) {
4984
5028
  return keep;
4985
5029
  }
4986
5030
  async function writeOAuthSections(filePath, snapshot, sections, secretStore = defaultSecretStore()) {
5031
+ await writeOAuthSectionsFrom(
5032
+ filePath,
5033
+ async () => snapshot,
5034
+ sections,
5035
+ secretStore
5036
+ );
5037
+ }
5038
+ async function writeOAuthSectionsFrom(filePath, resolveSnapshot, sections, secretStore) {
4987
5039
  const policy = getPersistTokensPolicy();
4988
5040
  const durable = await secretStoreIsDurable(secretStore);
4989
5041
  await withOAuthStateLock(filePath, "save", async (locked) => {
@@ -5067,6 +5119,7 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
5067
5119
  unconfirmed = null;
5068
5120
  namespace = diskNamespace;
5069
5121
  }
5122
+ const snapshot = await resolveSnapshot(disk, namespace);
5070
5123
  const effective = sections ? {
5071
5124
  servers: sections.servers && [...new Set(sections.servers)],
5072
5125
  idpSessions: sections.idpSessions && [
@@ -5100,7 +5153,8 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
5100
5153
  const candidates = [
5101
5154
  .../* @__PURE__ */ new Set([
5102
5155
  ...serverSecretFields(getOwnEntry(disk?.servers, url)),
5103
- ...serverSecretFields(next)
5156
+ ...serverSecretFields(next),
5157
+ ...[...restoreBaseline.values()].filter((prior) => prior.serverId === serverId).map(({ field }) => field)
5104
5158
  ])
5105
5159
  ];
5106
5160
  const entryPrior = await snapshotSecretFields(
@@ -5109,8 +5163,12 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
5109
5163
  candidates
5110
5164
  );
5111
5165
  priorSecrets.push(...entryPrior);
5166
+ const entryPriorValue = new Map(
5167
+ entryPrior.map(({ field, value }) => [field, value])
5168
+ );
5112
5169
  if (next === void 0) {
5113
5170
  for (const field of candidates) {
5171
+ if (entryPriorValue.get(field) === null) continue;
5114
5172
  ourWrites.set(priorKey({ serverId, field }), null);
5115
5173
  }
5116
5174
  await secretStore.deleteAllForServer(serverId);
@@ -5125,10 +5183,9 @@ async function writeOAuthSections(filePath, snapshot, sections, secretStore = de
5125
5183
  }
5126
5184
  const { residue, secrets } = splitServerOAuthState(next, policy);
5127
5185
  for (const field of candidates) {
5128
- ourWrites.set(
5129
- priorKey({ serverId, field }),
5130
- secrets[field] ?? null
5131
- );
5186
+ const value = secrets[field] ?? null;
5187
+ if (value === entryPriorValue.get(field)) continue;
5188
+ ourWrites.set(priorKey({ serverId, field }), value);
5132
5189
  }
5133
5190
  setOwnEntry(merged.servers, url, residue);
5134
5191
  if (!durable) {
@@ -5271,26 +5328,52 @@ function secretRequestsFor(snapshot, namespace) {
5271
5328
  }
5272
5329
  return requests;
5273
5330
  }
5274
- async function joinSnapshot(snapshot, secretStore, namespace) {
5275
- const requests = secretRequestsFor(snapshot, namespace);
5276
- const values = requests.length > 0 ? await secretStoreGetManyStrict(secretStore, requests) : {};
5331
+ function fillMissingSecrets(target, source) {
5332
+ for (const [field, value] of Object.entries(source ?? {})) {
5333
+ if (!Object.hasOwn(target, field)) setOwnEntry(target, field, value);
5334
+ }
5335
+ }
5336
+ async function readEntrySecrets(snapshot, secretStore, namespace, fallbacks) {
5337
+ const servers = /* @__PURE__ */ new Map();
5338
+ const idpSessions = /* @__PURE__ */ new Map();
5339
+ for (const url of Object.keys(snapshot.servers)) servers.set(url, {});
5340
+ for (const issuer of Object.keys(snapshot.idpSessions)) {
5341
+ idpSessions.set(issuer, {});
5342
+ }
5343
+ for (const source of [namespace, ...fallbacks]) {
5344
+ const requests = secretRequestsFor(snapshot, source);
5345
+ if (requests.length === 0) break;
5346
+ const values = await secretStoreGetManyStrict(secretStore, requests);
5347
+ for (const [url, target] of servers) {
5348
+ fillMissingSecrets(target, values[oauthSecretServerId(url, source)]);
5349
+ }
5350
+ for (const [issuer, target] of idpSessions) {
5351
+ fillMissingSecrets(
5352
+ target,
5353
+ values[oauthIdpSecretServerId(issuer, source)]
5354
+ );
5355
+ }
5356
+ }
5357
+ return { servers, idpSessions };
5358
+ }
5359
+ async function joinSnapshot(snapshot, secretStore, namespace, fallbacks) {
5360
+ const values = await readEntrySecrets(
5361
+ snapshot,
5362
+ secretStore,
5363
+ namespace,
5364
+ fallbacks
5365
+ );
5277
5366
  return {
5278
5367
  servers: Object.fromEntries(
5279
5368
  Object.entries(snapshot.servers).map(([url, state]) => [
5280
5369
  url,
5281
- joinServerOAuthState(
5282
- state,
5283
- values[oauthSecretServerId(url, namespace)] ?? {}
5284
- )
5370
+ joinServerOAuthState(state, values.servers.get(url) ?? {})
5285
5371
  ])
5286
5372
  ),
5287
5373
  idpSessions: Object.fromEntries(
5288
5374
  Object.entries(snapshot.idpSessions).map(([issuer, session]) => [
5289
5375
  issuer,
5290
- joinIdpSession(
5291
- session,
5292
- values[oauthIdpSecretServerId(issuer, namespace)] ?? {}
5293
- )
5376
+ joinIdpSession(session, values.idpSessions.get(issuer) ?? {})
5294
5377
  ])
5295
5378
  )
5296
5379
  };
@@ -5357,7 +5440,9 @@ async function readOAuthStore(filePath, secretStore = defaultSecretStore()) {
5357
5440
  warnMigrationFailure(error);
5358
5441
  }
5359
5442
  }
5360
- return joinSnapshot(snapshot, secretStore, parseSecretsNamespace(raw));
5443
+ const namespace = parseSecretsNamespace(raw);
5444
+ const fallbacks = namespace === void 0 ? await recordedNamespaces(filePath) : [];
5445
+ return joinSnapshot(snapshot, secretStore, namespace, fallbacks);
5361
5446
  });
5362
5447
  }
5363
5448
  async function removeOAuthStore(filePath, secretStore = defaultSecretStore()) {
@@ -6714,6 +6799,22 @@ function createRemoteApp(options) {
6714
6799
  });
6715
6800
  }
6716
6801
  const msg = err instanceof Error ? err.message : String(err);
6802
+ if (err instanceof SdkHttpError) {
6803
+ const text = err.data.text;
6804
+ return c.json({
6805
+ ok: false,
6806
+ kind: "transport_error",
6807
+ error: msg,
6808
+ http: {
6809
+ code: err.code,
6810
+ status: err.status,
6811
+ ...err.statusText !== void 0 && {
6812
+ statusText: err.statusText
6813
+ },
6814
+ ...typeof text === "string" && { text }
6815
+ }
6816
+ });
6817
+ }
6717
6818
  return c.json({ ok: false, kind: "transport_error", error: msg });
6718
6819
  } finally {
6719
6820
  clientSignal.removeEventListener("abort", onClientDisconnect);
@@ -8768,7 +8869,11 @@ async function runWeb(argv) {
8768
8869
  preArgs = argv.slice(0, argSeparatorIndex);
8769
8870
  postArgs = argv.slice(argSeparatorIndex + 1);
8770
8871
  }
8771
- program.name("mcp-inspector-web").description("Web UI for MCP Inspector").allowExcessArguments().allowUnknownOption().option(
8872
+ program.name("mcp-inspector-web").description("Web UI for MCP Inspector").version(
8873
+ readInspectorVersionSafe(import.meta.url) ?? "unknown",
8874
+ "-V, --version",
8875
+ "Print the Inspector version"
8876
+ ).allowExcessArguments().allowUnknownOption().option(
8772
8877
  "-e <env>",
8773
8878
  "environment variables in KEY=VALUE format",
8774
8879
  parseKeyValuePair,