@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.
- package/README.md +170 -23
- package/clients/cli/build/index.js +1696 -475
- package/clients/tui/build/index.js +4002 -715
- package/clients/tui/build/{undici-HXPKCIY3.js → undici-KNXAVD2F.js} +1225 -370
- package/clients/web/build/index.js +262 -59
- package/clients/web/build/{undici-HXPKCIY3.js → undici-KNXAVD2F.js} +1225 -370
- package/clients/web/dist/assets/index-CwhDusR3.js +237 -0
- package/clients/web/dist/assets/{prism-light-CCFFzO_n.js → prism-light-DBxbqjnM.js} +1 -1
- package/clients/web/dist/index.html +1 -1
- package/clients/web/static/sandbox_proxy.html +237 -0
- package/package.json +13 -14
- package/clients/web/dist/assets/index-CyPh2pEE.js +0 -300
|
@@ -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) ??
|
|
806
|
+
return fromSandbox ?? parseListenPort(process.env.SERVER_PORT) ?? DEFAULT_SANDBOX_PORT;
|
|
736
807
|
}
|
|
737
808
|
function createSandboxController(options) {
|
|
738
|
-
const { port, host =
|
|
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-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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 [
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
4304
|
+
openBrowser(url);
|
|
4102
4305
|
}
|
|
4103
4306
|
}
|
|
4104
4307
|
);
|