@modelcontextprotocol/inspector 2.2.0 → 2.3.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.
@@ -180,6 +180,38 @@ function envPairsToRecord(pairs) {
180
180
  }
181
181
  return out;
182
182
  }
183
+ function cleanAuthorizationParams(params) {
184
+ if (params === void 0) return void 0;
185
+ if (typeof params !== "object" || params === null || Array.isArray(params)) {
186
+ console.warn(
187
+ "Ignoring `oauth.authorizationParams`: expected an object of string values, got",
188
+ Array.isArray(params) ? "array" : typeof params
189
+ );
190
+ return void 0;
191
+ }
192
+ const out = {};
193
+ for (const [key, value] of Object.entries(params)) {
194
+ if (typeof value !== "string") {
195
+ console.warn(
196
+ `Dropping \`oauth.authorizationParams.${key}\`: expected a string value, got ${typeof value}.`
197
+ );
198
+ continue;
199
+ }
200
+ if (key.trim() === "") continue;
201
+ out[key] = value;
202
+ }
203
+ return Object.keys(out).length > 0 ? out : void 0;
204
+ }
205
+ function cleanEndpointOverride(value, field) {
206
+ if (value === void 0) return void 0;
207
+ if (typeof value !== "string") {
208
+ console.warn(
209
+ `Ignoring \`oauth.${field}\`: expected a string, got ${typeof value}.`
210
+ );
211
+ return void 0;
212
+ }
213
+ return value.trim() || void 0;
214
+ }
183
215
  function storedFieldsToInspectorSettings(stored) {
184
216
  const hasAny = stored.headers !== void 0 || stored.metadata !== void 0 || stored.connectionTimeout !== void 0 || stored.requestTimeout !== void 0 || stored.taskTtl !== void 0 || stored.autoRefreshOnListChanged !== void 0 || stored.paginatedLists !== void 0 || stored.advertisedExtensions !== void 0 || stored.maxFetchRequests !== void 0 || stored.oauth !== void 0 || stored.roots !== void 0 || stored.protocolEra !== void 0 || stored.modernLogLevel !== void 0 || stored.env !== void 0 || stored.cwd !== void 0;
185
217
  if (!hasAny) return void 0;
@@ -217,6 +249,19 @@ function storedFieldsToInspectorSettings(stored) {
217
249
  if (stored.oauth?.clientSecret)
218
250
  settings.oauthClientSecret = stored.oauth.clientSecret;
219
251
  if (stored.oauth?.scopes) settings.oauthScopes = stored.oauth.scopes;
252
+ const authorizationParams = cleanAuthorizationParams(
253
+ stored.oauth?.authorizationParams
254
+ );
255
+ if (authorizationParams) {
256
+ settings.oauthAuthorizationParams = envRecordToPairs(authorizationParams);
257
+ }
258
+ const authorizationUrl = cleanEndpointOverride(
259
+ stored.oauth?.authorizationUrl,
260
+ "authorizationUrl"
261
+ );
262
+ if (authorizationUrl) settings.oauthAuthorizationUrl = authorizationUrl;
263
+ const tokenUrl = cleanEndpointOverride(stored.oauth?.tokenUrl, "tokenUrl");
264
+ if (tokenUrl) settings.oauthTokenUrl = tokenUrl;
220
265
  if (stored.oauth?.onInsufficientScope) {
221
266
  settings.oauthOnInsufficientScope = stored.oauth.onInsufficientScope;
222
267
  }
@@ -272,6 +317,18 @@ function inspectorSettingsToStoredFields(settings) {
272
317
  if (settings.oauthClientSecret)
273
318
  oauthFields.clientSecret = settings.oauthClientSecret;
274
319
  if (settings.oauthScopes) oauthFields.scopes = settings.oauthScopes;
320
+ if (settings.oauthAuthorizationParams) {
321
+ const authParams = envPairsToRecord(settings.oauthAuthorizationParams);
322
+ if (Object.keys(authParams).length > 0) {
323
+ oauthFields.authorizationParams = authParams;
324
+ }
325
+ }
326
+ if (settings.oauthAuthorizationUrl?.trim()) {
327
+ oauthFields.authorizationUrl = settings.oauthAuthorizationUrl.trim();
328
+ }
329
+ if (settings.oauthTokenUrl?.trim()) {
330
+ oauthFields.tokenUrl = settings.oauthTokenUrl.trim();
331
+ }
275
332
  if (settings.oauthOnInsufficientScope) {
276
333
  oauthFields.onInsufficientScope = settings.oauthOnInsufficientScope;
277
334
  }
@@ -318,14 +375,7 @@ function extractSecretsFromStored(stored) {
318
375
  const stripped = { ...stored };
319
376
  if (stored.oauth?.clientSecret) {
320
377
  secrets[SECRET_FIELD_OAUTH_CLIENT_SECRET] = stored.oauth.clientSecret;
321
- const restOauth = {};
322
- if (stored.oauth.clientId !== void 0)
323
- restOauth.clientId = stored.oauth.clientId;
324
- if (stored.oauth.scopes !== void 0)
325
- restOauth.scopes = stored.oauth.scopes;
326
- if (stored.oauth.enterpriseManaged === true) {
327
- restOauth.enterpriseManaged = true;
328
- }
378
+ const { clientSecret: _clientSecret, ...restOauth } = stored.oauth;
329
379
  if (Object.keys(restOauth).length > 0) {
330
380
  stripped.oauth = restOauth;
331
381
  } else {
@@ -608,6 +658,7 @@ function resolveServerConfigs(options, mode) {
608
658
  }
609
659
 
610
660
  // server/web-server-config.ts
661
+ import open from "open";
611
662
  import pino from "pino";
612
663
 
613
664
  // ../../core/mcp/remote/constants.ts
@@ -710,6 +761,25 @@ function isAllInterfacesHost(host) {
710
761
  return ALL_INTERFACES_LITERALS.has(normalized) || isAllZeroIpv4(normalized);
711
762
  }
712
763
 
764
+ // server/resolve-bind-host.ts
765
+ var BIND_ALL_INTERFACES_ENV = "DANGEROUSLY_BIND_ALL_INTERFACES";
766
+ function isEnabled(value) {
767
+ const v = value?.trim().toLowerCase();
768
+ return v === "true" || v === "1";
769
+ }
770
+ var DEFAULT_BIND_HOST = "127.0.0.1";
771
+ function resolveBindHostname(env = process.env) {
772
+ const host = (env.HOST ?? DEFAULT_BIND_HOST).trim();
773
+ if (isAllInterfacesHost(host) && !isEnabled(env[BIND_ALL_INTERFACES_ENV])) {
774
+ const resolved = canonicalUrlHost(host);
775
+ const shown = stripBrackets(resolved) === stripBrackets(host.toLowerCase()) ? `HOST="${host}"` : `HOST="${host}" (resolves to ${resolved})`;
776
+ throw new Error(
777
+ `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.`
778
+ );
779
+ }
780
+ return stripBrackets(host);
781
+ }
782
+
713
783
  // server/sandbox-controller.ts
714
784
  var __dirname = dirname4(fileURLToPath2(import.meta.url));
715
785
  var LOOPBACK_FRAME_ANCESTORS = ["http://127.0.0.1:*", "http://localhost:*"];
@@ -725,6 +795,7 @@ function parseListenPort(raw) {
725
795
  const n = parseInt(v, 10);
726
796
  return n <= 65535 ? n : void 0;
727
797
  }
798
+ var DEFAULT_SANDBOX_PORT = 6275;
728
799
  function resolveSandboxPort() {
729
800
  const fromSandbox = parseListenPort(process.env.MCP_SANDBOX_PORT);
730
801
  if (fromSandbox === void 0 && process.env.MCP_SANDBOX_PORT?.trim()) {
@@ -732,10 +803,10 @@ function resolveSandboxPort() {
732
803
  `Ignoring invalid MCP_SANDBOX_PORT="${process.env.MCP_SANDBOX_PORT}" (need an integer 0\u201365535); falling back.`
733
804
  );
734
805
  }
735
- return fromSandbox ?? parseListenPort(process.env.SERVER_PORT) ?? 0;
806
+ return fromSandbox ?? parseListenPort(process.env.SERVER_PORT) ?? DEFAULT_SANDBOX_PORT;
736
807
  }
737
808
  function createSandboxController(options) {
738
- const { port, host = "localhost", allowedOrigins } = options;
809
+ const { port, host = DEFAULT_BIND_HOST, allowedOrigins } = options;
739
810
  let server = null;
740
811
  let sandboxUrl = null;
741
812
  const SANDBOX_PROXY_CSP = sandboxFrameAncestors(allowedOrigins);
@@ -773,8 +844,17 @@ function createSandboxController(options) {
773
844
  });
774
845
  res.end(sandboxHtml);
775
846
  });
847
+ let retriedDynamic = false;
776
848
  server.on("error", (err) => {
777
849
  if (err.code === "EADDRINUSE") {
850
+ if (!retriedDynamic && port !== 0) {
851
+ retriedDynamic = true;
852
+ console.warn(
853
+ `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).`
854
+ );
855
+ server.listen(0, host);
856
+ return;
857
+ }
778
858
  console.error(
779
859
  `Sandbox: port ${port || "dynamic"} in use. MCP Apps tab may not work.`
780
860
  );
@@ -816,24 +896,6 @@ function createSandboxController(options) {
816
896
  };
817
897
  }
818
898
 
819
- // server/resolve-bind-host.ts
820
- var BIND_ALL_INTERFACES_ENV = "DANGEROUSLY_BIND_ALL_INTERFACES";
821
- function isEnabled(value) {
822
- const v = value?.trim().toLowerCase();
823
- return v === "true" || v === "1";
824
- }
825
- function resolveBindHostname(env = process.env) {
826
- const host = (env.HOST ?? "localhost").trim();
827
- if (isAllInterfacesHost(host) && !isEnabled(env[BIND_ALL_INTERFACES_ENV])) {
828
- const resolved = canonicalUrlHost(host);
829
- const shown = stripBrackets(resolved) === stripBrackets(host.toLowerCase()) ? `HOST="${host}"` : `HOST="${host}" (resolves to ${resolved})`;
830
- throw new Error(
831
- `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.`
832
- );
833
- }
834
- return stripBrackets(host);
835
- }
836
-
837
899
  // server/web-server-config.ts
838
900
  var inspectorVersion = readInspectorVersionSafe(import.meta.url);
839
901
  function defaultEnvironmentFromProcess(extra) {
@@ -928,6 +990,11 @@ MCP Inspector Web is up and running at:
928
990
  }
929
991
  return url;
930
992
  }
993
+ function openBrowser(url) {
994
+ open(url).catch((err) => {
995
+ console.warn("Could not open a browser automatically:", err);
996
+ });
997
+ }
931
998
  var LOOPBACK_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "127.0.0.1", "[::1]"]);
932
999
  function httpOrigin(host, port) {
933
1000
  return port === 80 ? `http://${host}` : `http://${host}:${port}`;
@@ -1023,9 +1090,6 @@ import { fileURLToPath as fileURLToPath4 } from "url";
1023
1090
  import { createServer as createServer2 } from "vite";
1024
1091
  import react from "@vitejs/plugin-react";
1025
1092
 
1026
- // server/vite-hono-plugin.ts
1027
- import open from "open";
1028
-
1029
1093
  // ../../core/mcp/remote/node/server.ts
1030
1094
  import { randomBytes, timingSafeEqual } from "crypto";
1031
1095
  import { existsSync as existsSync2, readFileSync as readFileSync4 } from "fs";
@@ -1395,7 +1459,7 @@ function withProxyDispatcher(baseFetch) {
1395
1459
  if (readProxyEnv() === void 0) return baseFetch;
1396
1460
  let dispatcherPromise;
1397
1461
  const getDispatcher = () => {
1398
- dispatcherPromise ??= import("./undici-HXPKCIY3.js").then(
1462
+ dispatcherPromise ??= import("./undici-KNXAVD2F.js").then(
1399
1463
  ({ EnvHttpProxyAgent }) => new EnvHttpProxyAgent(),
1400
1464
  (cause) => {
1401
1465
  throw new Error(
@@ -2319,6 +2383,24 @@ async function deleteClientConfigStore(filePath, secretStore) {
2319
2383
  // ../../core/mcp/remote/node/server.ts
2320
2384
  import { ZodError as ZodError2 } from "zod";
2321
2385
  var SSE_PRIMING_COMMENT = ":\n\n";
2386
+ function closeAbortedStream(stream) {
2387
+ stream.close().catch(() => {
2388
+ });
2389
+ }
2390
+ async function primeAndHoldSseStream(stream, cleanup) {
2391
+ let done = false;
2392
+ const aborted = new Promise((resolve6) => {
2393
+ stream.onAbort(() => {
2394
+ if (done) return;
2395
+ done = true;
2396
+ cleanup();
2397
+ closeAbortedStream(stream);
2398
+ resolve6();
2399
+ });
2400
+ });
2401
+ await stream.write(SSE_PRIMING_COMMENT);
2402
+ await aborted;
2403
+ }
2322
2404
  function createOriginMiddleware(allowedOrigins) {
2323
2405
  return async (c, next) => {
2324
2406
  if (!allowedOrigins || allowedOrigins.length === 0) {
@@ -2730,19 +2812,12 @@ function createRemoteApp(options) {
2730
2812
  sessions.delete(sessionId);
2731
2813
  return;
2732
2814
  }
2733
- await stream.write(SSE_PRIMING_COMMENT);
2734
- stream.onAbort(() => {
2815
+ await primeAndHoldSseStream(stream, () => {
2735
2816
  const shouldCleanup = session.clearEventConsumer();
2736
- stream.close();
2737
2817
  if (shouldCleanup || session.isTransportDead()) {
2738
2818
  sessions.delete(sessionId);
2739
2819
  }
2740
2820
  });
2741
- await new Promise((resolve6) => {
2742
- stream.onAbort(() => {
2743
- resolve6();
2744
- });
2745
- });
2746
2821
  });
2747
2822
  });
2748
2823
  app.post("/api/mcp/disconnect", async (c) => {
@@ -2930,9 +3005,21 @@ function createRemoteApp(options) {
2930
3005
  const isOauthObject = (v) => {
2931
3006
  if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
2932
3007
  const o = v;
2933
- for (const k of ["clientId", "clientSecret", "scopes"]) {
3008
+ for (const k of [
3009
+ "clientId",
3010
+ "clientSecret",
3011
+ "scopes",
3012
+ // #1906 — shape only; the values are validated as absolute http(s) URLs
3013
+ // where they're applied (core/auth/endpointOverrides.ts), so a typo drops
3014
+ // one field with a warning instead of dropping the whole `oauth` block.
3015
+ "authorizationUrl",
3016
+ "tokenUrl"
3017
+ ]) {
2934
3018
  if (o[k] !== void 0 && typeof o[k] !== "string") return false;
2935
3019
  }
3020
+ if (o.authorizationParams !== void 0 && !isStringRecord(o.authorizationParams)) {
3021
+ return false;
3022
+ }
2936
3023
  if (o.enterpriseManaged !== void 0 && typeof o.enterpriseManaged !== "boolean") {
2937
3024
  return false;
2938
3025
  }
@@ -3007,7 +3094,7 @@ function createRemoteApp(options) {
3007
3094
  if ("oauth" in valObj && !isOauthObject(valObj.oauth)) {
3008
3095
  logWarn(
3009
3096
  { route: "/api/servers", id, droppedKey: "oauth" },
3010
- "Dropping malformed `oauth` field \u2014 expected `{ clientId?, clientSecret?, scopes?, enterpriseManaged? }`."
3097
+ "Dropping malformed `oauth` field \u2014 expected `{ clientId?, clientSecret?, scopes?, authorizationParams?, authorizationUrl?, tokenUrl?, enterpriseManaged?, onInsufficientScope? }`."
3011
3098
  );
3012
3099
  delete valObj.oauth;
3013
3100
  }
@@ -3164,12 +3251,22 @@ function createRemoteApp(options) {
3164
3251
  for (const optional of [
3165
3252
  "oauthClientId",
3166
3253
  "oauthClientSecret",
3167
- "oauthScopes"
3254
+ "oauthScopes",
3255
+ // #1906 — string-shaped on the wire; URL validity is checked where the
3256
+ // override is applied, not here.
3257
+ "oauthAuthorizationUrl",
3258
+ "oauthTokenUrl"
3168
3259
  ]) {
3169
3260
  if (obj[optional] !== void 0 && typeof obj[optional] !== "string") {
3170
3261
  return { ok: false, error: `settings.${optional} must be a string` };
3171
3262
  }
3172
3263
  }
3264
+ if (obj.oauthAuthorizationParams !== void 0 && !isKvArray2(obj.oauthAuthorizationParams)) {
3265
+ return {
3266
+ ok: false,
3267
+ error: "settings.oauthAuthorizationParams must be an array of { key, value }"
3268
+ };
3269
+ }
3173
3270
  if (obj.enterpriseManaged !== void 0 && typeof obj.enterpriseManaged !== "boolean") {
3174
3271
  return {
3175
3272
  ok: false,
@@ -3243,6 +3340,15 @@ function createRemoteApp(options) {
3243
3340
  if (typeof obj.oauthScopes === "string" && obj.oauthScopes !== "") {
3244
3341
  value.oauthScopes = obj.oauthScopes;
3245
3342
  }
3343
+ if (isKvArray2(obj.oauthAuthorizationParams)) {
3344
+ value.oauthAuthorizationParams = obj.oauthAuthorizationParams;
3345
+ }
3346
+ if (typeof obj.oauthAuthorizationUrl === "string" && obj.oauthAuthorizationUrl !== "") {
3347
+ value.oauthAuthorizationUrl = obj.oauthAuthorizationUrl;
3348
+ }
3349
+ if (typeof obj.oauthTokenUrl === "string" && obj.oauthTokenUrl !== "") {
3350
+ value.oauthTokenUrl = obj.oauthTokenUrl;
3351
+ }
3246
3352
  if (obj.enterpriseManaged === true) {
3247
3353
  value.enterpriseManaged = true;
3248
3354
  }
@@ -3701,14 +3807,9 @@ function createRemoteApp(options) {
3701
3807
  serverEventSubscribers.add(send);
3702
3808
  ensureWatcher();
3703
3809
  }
3704
- await stream.write(SSE_PRIMING_COMMENT);
3705
- stream.onAbort(() => {
3810
+ await primeAndHoldSseStream(stream, () => {
3706
3811
  serverEventSubscribers.delete(send);
3707
3812
  void maybeStopWatcher();
3708
- stream.close();
3709
- });
3710
- await new Promise((resolve6) => {
3711
- stream.onAbort(() => resolve6());
3712
3813
  });
3713
3814
  });
3714
3815
  });
@@ -3807,7 +3908,7 @@ function honoMiddlewarePlugin(config) {
3807
3908
  sandboxUrl ?? void 0
3808
3909
  );
3809
3910
  if (config.autoOpen) {
3810
- open(url);
3911
+ openBrowser(url);
3811
3912
  }
3812
3913
  };
3813
3914
  server.httpServer.once("listening", () => {
@@ -4129,7 +4230,6 @@ import { readFileSync as readFileSync5 } from "fs";
4129
4230
  import { join as join7, dirname as dirname6 } from "path";
4130
4231
  import { fileURLToPath as fileURLToPath5 } from "url";
4131
4232
  import { randomBytes as randomBytes2 } from "crypto";
4132
- import open2 from "open";
4133
4233
  import { serve } from "@hono/node-server";
4134
4234
  import { serveStatic } from "@hono/node-server/serve-static";
4135
4235
  import { Hono as Hono2 } from "hono";
@@ -4201,7 +4301,7 @@ async function startHonoServer(config) {
4201
4301
  sandboxUrl ?? void 0
4202
4302
  );
4203
4303
  if (config.autoOpen) {
4204
- open2(url);
4304
+ openBrowser(url);
4205
4305
  }
4206
4306
  }
4207
4307
  );