@modelcontextprotocol/inspector 2.1.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(
@@ -1680,6 +1744,9 @@ var RemoteSession = class _RemoteSession {
1680
1744
  static AUTH_HTTP_ECHO_SUPPRESS_MS = 3e4;
1681
1745
  requestWaits = /* @__PURE__ */ new Map();
1682
1746
  authProviderHandle = null;
1747
+ appliedProtocolVersion;
1748
+ /** A protocol version is a dated token (`2025-11-25`, `2026-07-28`, …). */
1749
+ static PROTOCOL_VERSION_PATTERN = /^[A-Za-z0-9._-]{1,64}$/;
1683
1750
  constructor(sessionId) {
1684
1751
  this.sessionId = sessionId;
1685
1752
  }
@@ -1695,6 +1762,27 @@ var RemoteSession = class _RemoteSession {
1695
1762
  setTransport(transport) {
1696
1763
  this.transport = transport;
1697
1764
  }
1765
+ /**
1766
+ * Apply the client's negotiated protocol version to the upstream transport
1767
+ * (#1935). The browser's SDK Client owns the `initialize` handshake, so the
1768
+ * `setProtocolVersion` call it makes lands on the *remote* transport; without
1769
+ * this the backend's real HTTP transport never learns the version and drops
1770
+ * `Mcp-Protocol-Version` from every subsequent request.
1771
+ *
1772
+ * Ignores anything that isn't a plausible version token so a client can't
1773
+ * push arbitrary bytes into an upstream header, and re-applies only on
1774
+ * change (the value rides every `/api/mcp/send`). The value arrives from an
1775
+ * unvalidated JSON body, so the type is checked rather than assumed —
1776
+ * `RegExp.test` would otherwise coerce a `123` or `true` into a "matching"
1777
+ * string and hand the wrong type to the transport.
1778
+ */
1779
+ applyProtocolVersion(version) {
1780
+ if (typeof version !== "string" || version === this.appliedProtocolVersion || !_RemoteSession.PROTOCOL_VERSION_PATTERN.test(version)) {
1781
+ return;
1782
+ }
1783
+ this.appliedProtocolVersion = version;
1784
+ this.transport.setProtocolVersion?.(version);
1785
+ }
1698
1786
  setEventConsumer(consumer) {
1699
1787
  this.eventConsumer = consumer;
1700
1788
  while (this.eventQueue.length > 0) {
@@ -1953,8 +2041,42 @@ function createRemoteAuthProvider(initialAuthState) {
1953
2041
  }
1954
2042
 
1955
2043
  // ../../core/auth/node/secret-store.ts
1956
- import { AsyncEntry, findCredentialsAsync } from "@napi-rs/keyring";
1957
2044
  var SERVICE_NAME = "mcp-inspector";
2045
+ var keyringLoad;
2046
+ var KeyringModuleShapeError = class extends Error {
2047
+ constructor(detail, options) {
2048
+ super(`@napi-rs/keyring loaded but ${detail}`, options);
2049
+ this.name = "KeyringModuleShapeError";
2050
+ }
2051
+ };
2052
+ var checkKeyringShape = (mod) => {
2053
+ try {
2054
+ if (typeof mod.AsyncEntry === "function" && typeof mod.findCredentialsAsync === "function") {
2055
+ return { ok: true, mod };
2056
+ }
2057
+ return {
2058
+ ok: false,
2059
+ err: new KeyringModuleShapeError(
2060
+ "did not expose AsyncEntry / findCredentialsAsync"
2061
+ )
2062
+ };
2063
+ } catch (err) {
2064
+ return {
2065
+ ok: false,
2066
+ err: new KeyringModuleShapeError(
2067
+ `its exports could not be read: ${err instanceof Error ? err.message : String(err)}`,
2068
+ { cause: err }
2069
+ )
2070
+ };
2071
+ }
2072
+ };
2073
+ var loadKeyring = () => {
2074
+ keyringLoad ??= import("@napi-rs/keyring").then(
2075
+ checkKeyringShape,
2076
+ (err) => ({ ok: false, err })
2077
+ );
2078
+ return keyringLoad;
2079
+ };
1958
2080
  function parseAccount(account) {
1959
2081
  const idx = account.indexOf(":");
1960
2082
  if (idx <= 0 || idx === account.length - 1) return null;
@@ -1966,16 +2088,31 @@ function parseAccount(account) {
1966
2088
  var buildAccount = (serverId, field) => `${serverId}:${field}`;
1967
2089
  var KeychainUnavailableError = class extends Error {
1968
2090
  constructor(cause) {
2091
+ const message = cause instanceof Error ? cause.message : String(cause);
1969
2092
  super(
1970
- `OS keychain is not available. On Linux, install libsecret / gnome-keyring. Underlying error: ${cause instanceof Error ? cause.message : String(cause)}`
2093
+ `OS keychain is not available. ${hintFor(cause, message)} Underlying error: ${message}`
1971
2094
  );
1972
2095
  this.name = "KeychainUnavailableError";
1973
2096
  }
1974
2097
  };
2098
+ var hintFor = (cause, message) => {
2099
+ if (cause instanceof KeyringModuleShapeError) {
2100
+ 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.`;
2101
+ }
2102
+ if (message.includes("Cannot find native binding")) {
2103
+ 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).`;
2104
+ }
2105
+ return `On Linux, install libsecret / gnome-keyring.`;
2106
+ };
1975
2107
  var KeyringSecretStore = class {
1976
2108
  async get(serverId, field) {
1977
- const entry = new AsyncEntry(SERVICE_NAME, buildAccount(serverId, field));
1978
2109
  try {
2110
+ const keyring = await loadKeyring();
2111
+ if (!keyring.ok) return null;
2112
+ const entry = new keyring.mod.AsyncEntry(
2113
+ SERVICE_NAME,
2114
+ buildAccount(serverId, field)
2115
+ );
1979
2116
  const v = await entry.getPassword();
1980
2117
  return v ?? null;
1981
2118
  } catch {
@@ -1983,16 +2120,27 @@ var KeyringSecretStore = class {
1983
2120
  }
1984
2121
  }
1985
2122
  async set(serverId, field, value) {
1986
- const entry = new AsyncEntry(SERVICE_NAME, buildAccount(serverId, field));
1987
2123
  try {
2124
+ const keyring = await loadKeyring();
2125
+ if (!keyring.ok) throw new KeychainUnavailableError(keyring.err);
2126
+ const entry = new keyring.mod.AsyncEntry(
2127
+ SERVICE_NAME,
2128
+ buildAccount(serverId, field)
2129
+ );
1988
2130
  await entry.setPassword(value);
1989
2131
  } catch (err) {
2132
+ if (err instanceof KeychainUnavailableError) throw err;
1990
2133
  throw new KeychainUnavailableError(err);
1991
2134
  }
1992
2135
  }
1993
2136
  async delete(serverId, field) {
1994
- const entry = new AsyncEntry(SERVICE_NAME, buildAccount(serverId, field));
1995
2137
  try {
2138
+ const keyring = await loadKeyring();
2139
+ if (!keyring.ok) return;
2140
+ const entry = new keyring.mod.AsyncEntry(
2141
+ SERVICE_NAME,
2142
+ buildAccount(serverId, field)
2143
+ );
1996
2144
  await entry.deleteCredential();
1997
2145
  } catch {
1998
2146
  }
@@ -2000,7 +2148,9 @@ var KeyringSecretStore = class {
2000
2148
  async deleteAllForServer(serverId) {
2001
2149
  let creds;
2002
2150
  try {
2003
- creds = await findCredentialsAsync(SERVICE_NAME);
2151
+ const keyring = await loadKeyring();
2152
+ if (!keyring.ok) return;
2153
+ creds = await keyring.mod.findCredentialsAsync(SERVICE_NAME);
2004
2154
  } catch {
2005
2155
  return;
2006
2156
  }
@@ -2232,6 +2382,25 @@ async function deleteClientConfigStore(filePath, secretStore) {
2232
2382
 
2233
2383
  // ../../core/mcp/remote/node/server.ts
2234
2384
  import { ZodError as ZodError2 } from "zod";
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
+ }
2235
2404
  function createOriginMiddleware(allowedOrigins) {
2236
2405
  return async (c, next) => {
2237
2406
  if (!allowedOrigins || allowedOrigins.length === 0) {
@@ -2571,7 +2740,7 @@ function createRemoteApp(options) {
2571
2740
  } catch {
2572
2741
  return c.json({ error: "Invalid JSON body" }, 400);
2573
2742
  }
2574
- const { sessionId, message, relatedRequestId, headers } = body;
2743
+ const { sessionId, message, relatedRequestId, headers, protocolVersion } = body;
2575
2744
  if (!sessionId || !message) {
2576
2745
  return c.json({ error: "Missing sessionId or message" }, 400);
2577
2746
  }
@@ -2583,6 +2752,7 @@ function createRemoteApp(options) {
2583
2752
  const errorMsg = session.getTransportError() || "Transport closed";
2584
2753
  return c.json({ ok: false, kind: "transport_error", error: errorMsg });
2585
2754
  }
2755
+ session.applyProtocolVersion(protocolVersion);
2586
2756
  session.beginSend();
2587
2757
  const requestId = requestIdForSendWait(message);
2588
2758
  let responseWait;
@@ -2642,18 +2812,12 @@ function createRemoteApp(options) {
2642
2812
  sessions.delete(sessionId);
2643
2813
  return;
2644
2814
  }
2645
- stream.onAbort(() => {
2815
+ await primeAndHoldSseStream(stream, () => {
2646
2816
  const shouldCleanup = session.clearEventConsumer();
2647
- stream.close();
2648
2817
  if (shouldCleanup || session.isTransportDead()) {
2649
2818
  sessions.delete(sessionId);
2650
2819
  }
2651
2820
  });
2652
- await new Promise((resolve6) => {
2653
- stream.onAbort(() => {
2654
- resolve6();
2655
- });
2656
- });
2657
2821
  });
2658
2822
  });
2659
2823
  app.post("/api/mcp/disconnect", async (c) => {
@@ -2841,9 +3005,21 @@ function createRemoteApp(options) {
2841
3005
  const isOauthObject = (v) => {
2842
3006
  if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
2843
3007
  const o = v;
2844
- 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
+ ]) {
2845
3018
  if (o[k] !== void 0 && typeof o[k] !== "string") return false;
2846
3019
  }
3020
+ if (o.authorizationParams !== void 0 && !isStringRecord(o.authorizationParams)) {
3021
+ return false;
3022
+ }
2847
3023
  if (o.enterpriseManaged !== void 0 && typeof o.enterpriseManaged !== "boolean") {
2848
3024
  return false;
2849
3025
  }
@@ -2918,7 +3094,7 @@ function createRemoteApp(options) {
2918
3094
  if ("oauth" in valObj && !isOauthObject(valObj.oauth)) {
2919
3095
  logWarn(
2920
3096
  { route: "/api/servers", id, droppedKey: "oauth" },
2921
- "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? }`."
2922
3098
  );
2923
3099
  delete valObj.oauth;
2924
3100
  }
@@ -3075,12 +3251,22 @@ function createRemoteApp(options) {
3075
3251
  for (const optional of [
3076
3252
  "oauthClientId",
3077
3253
  "oauthClientSecret",
3078
- "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"
3079
3259
  ]) {
3080
3260
  if (obj[optional] !== void 0 && typeof obj[optional] !== "string") {
3081
3261
  return { ok: false, error: `settings.${optional} must be a string` };
3082
3262
  }
3083
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
+ }
3084
3270
  if (obj.enterpriseManaged !== void 0 && typeof obj.enterpriseManaged !== "boolean") {
3085
3271
  return {
3086
3272
  ok: false,
@@ -3154,6 +3340,15 @@ function createRemoteApp(options) {
3154
3340
  if (typeof obj.oauthScopes === "string" && obj.oauthScopes !== "") {
3155
3341
  value.oauthScopes = obj.oauthScopes;
3156
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
+ }
3157
3352
  if (obj.enterpriseManaged === true) {
3158
3353
  value.enterpriseManaged = true;
3159
3354
  }
@@ -3612,13 +3807,9 @@ function createRemoteApp(options) {
3612
3807
  serverEventSubscribers.add(send);
3613
3808
  ensureWatcher();
3614
3809
  }
3615
- stream.onAbort(() => {
3810
+ await primeAndHoldSseStream(stream, () => {
3616
3811
  serverEventSubscribers.delete(send);
3617
3812
  void maybeStopWatcher();
3618
- stream.close();
3619
- });
3620
- await new Promise((resolve6) => {
3621
- stream.onAbort(() => resolve6());
3622
3813
  });
3623
3814
  });
3624
3815
  });
@@ -3717,7 +3908,7 @@ function honoMiddlewarePlugin(config) {
3717
3908
  sandboxUrl ?? void 0
3718
3909
  );
3719
3910
  if (config.autoOpen) {
3720
- open(url);
3911
+ openBrowser(url);
3721
3912
  }
3722
3913
  };
3723
3914
  server.httpServer.once("listening", () => {
@@ -3852,7 +4043,9 @@ var NODE_ONLY_OPTIMIZE_DEPS_EXCLUDE = [
3852
4043
  "which",
3853
4044
  // `@napi-rs/keyring` is loaded only inside
3854
4045
  // `core/auth/node/secret-store.ts` from the Hono `/api/servers`
3855
- // handlers. It's a native-binding package (no browser code path) so
4046
+ // handlers — and lazily even there, via a cached dynamic import, so
4047
+ // an unsupported platform degrades instead of crashing at startup
4048
+ // (#1905). It's a native-binding package (no browser code path) so
3856
4049
  // excluding it keeps Vite's dep scanner from chasing into the
3857
4050
  // platform-specific binaries during dev startup.
3858
4051
  "@napi-rs/keyring"
@@ -3947,9 +4140,20 @@ function vitestSharedPaths(clientDir) {
3947
4140
  find: /^@napi-rs\/keyring$/,
3948
4141
  replacement: path2.resolve(dirname8, "node_modules/@napi-rs/keyring")
3949
4142
  },
4143
+ // `express` and `yaml` resolve from the **repo root**, unlike every pin
4144
+ // above. Both are reached only through `test-servers/src` — express by the
4145
+ // http/oauth servers, yaml by `load-config.ts` — which is root-owned code
4146
+ // with no manifest of its own, so the root is where they are declared and
4147
+ // the only place a client's resolution chain is guaranteed to find them.
4148
+ // Pointing these at `<client>/node_modules` is what broke when the MCP
4149
+ // packages moved to the root (#1970): express was never declared by a client
4150
+ // at all, it arrived in `clients/cli` as a peer of `express-rate-limit`
4151
+ // under `@modelcontextprotocol/server-legacy`, so removing that manifest
4152
+ // entry took express with it and every cli test that spawns a test server
4153
+ // failed to resolve it.
3950
4154
  {
3951
4155
  find: /^express$/,
3952
- replacement: path2.resolve(dirname8, "node_modules/express")
4156
+ replacement: path2.resolve(repoRoot, "node_modules/express")
3953
4157
  },
3954
4158
  {
3955
4159
  find: /^yaml$/,
@@ -4026,7 +4230,6 @@ import { readFileSync as readFileSync5 } from "fs";
4026
4230
  import { join as join7, dirname as dirname6 } from "path";
4027
4231
  import { fileURLToPath as fileURLToPath5 } from "url";
4028
4232
  import { randomBytes as randomBytes2 } from "crypto";
4029
- import open2 from "open";
4030
4233
  import { serve } from "@hono/node-server";
4031
4234
  import { serveStatic } from "@hono/node-server/serve-static";
4032
4235
  import { Hono as Hono2 } from "hono";
@@ -4098,7 +4301,7 @@ async function startHonoServer(config) {
4098
4301
  sandboxUrl ?? void 0
4099
4302
  );
4100
4303
  if (config.autoOpen) {
4101
- open2(url);
4304
+ openBrowser(url);
4102
4305
  }
4103
4306
  }
4104
4307
  );