Merge remote-tracking branch 'origin/v2/main' into v2/fix/2297-401-response-body

Signed-off-by: cliffhall <cliff@futurescale.com>

# Conflicts:
#	core/mcp/node/transport.ts
This commit is contained in:
cliffhall 2026-09-14 01:47:25 -04:00
commit 860b8d27ad
4 changed files with 4402 additions and 100 deletions

View file

@ -8,7 +8,8 @@ import type {
SseServerConfig,
StreamableHttpServerConfig,
} from "../types.js";
import { normalizeServerType } from "../serverList.js";
import { isProtocolEra, normalizeServerType } from "../serverList.js";
import type { ServerProtocolEra } from "../types.js";
import { toRecord } from "../../json/jsonUtils.js";
/**
@ -86,6 +87,20 @@ export function parseHeaderPair(
return { ...previous, [key]: val };
}
/**
* Validate a `--protocol-era` value. Used as the Commander option coerce for
* all three clients, so an ad-hoc launch can pick the era without writing an
* `mcp.json` entry for it (#2208). Pure function; no Commander dependency.
*/
export function parseProtocolEra(value: string): ServerProtocolEra {
if (!isProtocolEra(value)) {
throw new Error(
`Invalid protocol era: ${value}. Valid eras are: legacy, auto, modern`,
);
}
return value;
}
/** On-disk contents of a freshly seeded empty catalog (pretty-printed). */
const EMPTY_CATALOG_CONTENT = `${JSON.stringify({ mcpServers: {} }, null, 2)}\n`;

View file

@ -1,6 +1,7 @@
export {
parseKeyValuePair,
parseHeaderPair,
parseProtocolEra,
withDefaultCatalogPath,
resolveServerConfigs,
getNamedServerConfigs,

View file

@ -1,6 +1,10 @@
import type { SecretStore } from "../../auth/node/secret-store.js";
import { defaultSecretStore } from "../../auth/node/secret-store-selection.js";
import type { InspectorServerSettings, MCPServerConfig } from "../types.js";
import type {
InspectorServerSettings,
MCPServerConfig,
ServerProtocolEra,
} from "../types.js";
import {
DEFAULT_CONNECTION_TIMEOUT_MS,
DEFAULT_MAX_FETCH_REQUESTS,
@ -30,10 +34,14 @@ export type ResolvedServer = {
settings?: InspectorServerSettings;
};
/** Loader options: the shared source flags plus a single `--header` set that is
* broadcast into every resolved server's settings. */
/** Loader options: the shared source flags plus a single `--header` set and an
* optional `--protocol-era`, both broadcast into every resolved server's
* settings. */
export type ServerLoadOptions = ServerConfigOptions & {
headers?: Record<string, string>;
/** `--protocol-era`: overrides the file's `protocolEra` (or the legacy
* default) the way `--header` overrides its headers (#2208). */
protocolEra?: ServerProtocolEra;
/** Test injection; defaults to the selected store (see
* `secret-store-selection.ts`) for catalog/config loads. */
secretStore?: SecretStore;
@ -51,7 +59,15 @@ export function headersToServerSettings(
return undefined;
}
return {
...defaultServerSettings(),
headers: Object.entries(headers).map(([key, value]) => ({ key, value })),
};
}
/** A settings node with every field at its product default. */
function defaultServerSettings(): InspectorServerSettings {
return {
headers: [],
env: [],
metadata: {},
connectionTimeout: DEFAULT_CONNECTION_TIMEOUT_MS,
@ -65,18 +81,29 @@ export function headersToServerSettings(
}
/**
* Overlay CLI `--header` values onto the settings lifted from the file. Only the
* `headers` field is overridden — timeouts, OAuth, and the rest of the file's
* settings are preserved.
* Overlay `--header` / `--protocol-era` onto the settings lifted from the file
* (or onto nothing, for an ad-hoc target). Only those two fields are overridden
* — timeouts, OAuth, and the rest of the file's settings are preserved. Returns
* `base` untouched when neither flag was given.
*/
function mergeSettings(
base: InspectorServerSettings | undefined,
headers?: Record<string, string>,
overrides: Pick<ServerLoadOptions, "headers" | "protocolEra">,
): InspectorServerSettings | undefined {
const fromHeaders = headersToServerSettings(headers);
if (!fromHeaders) return base;
if (!base) return fromHeaders;
return { ...base, headers: fromHeaders.headers };
let settings = base;
const fromHeaders = headersToServerSettings(overrides.headers);
if (fromHeaders) {
settings = settings
? { ...settings, headers: fromHeaders.headers }
: fromHeaders;
}
if (overrides.protocolEra) {
settings = {
...(settings ?? defaultServerSettings()),
protocolEra: overrides.protocolEra,
};
}
return settings;
}
/**
@ -117,10 +144,11 @@ export async function loadServerEntries(
env: serverOptions.env,
cwd: serverOptions.cwd,
}),
// Deliberate broadcast: a single `--header` set is merged into EVERY
// server in the catalog/config (fine for the common single-server case;
// for multi-server files, prefer per-server headers in the file itself).
settings: mergeSettings(entry.settings, serverOptions.headers),
// Deliberate broadcast: a single `--header` set (and `--protocol-era`)
// is merged into EVERY server in the catalog/config (fine for the
// common single-server case; for multi-server files, prefer per-server
// settings in the file itself).
settings: mergeSettings(entry.settings, serverOptions),
};
}
return result;
@ -135,7 +163,7 @@ export async function loadServerEntries(
return {
default: {
config: configs[0]!,
settings: headersToServerSettings(serverOptions.headers),
settings: mergeSettings(undefined, serverOptions),
},
};
}

View file

@ -49,11 +49,18 @@ export function createTransportNode(
pipeStderr = false,
onFetchRequest,
onFetchResponseBody,
onFetchStreamUpdate,
authProvider,
settings,
interceptAuthChallenges = false,
onAuthChallengeObserved,
} = options;
// Any of the three tracking callbacks earns a tracker: each is optional on
// the tracker itself, and a caller that wants only stream updates (or only
// bodies) must not be silently handed the bare fetch (#2318).
const wantsFetchTracking = Boolean(
onFetchRequest || onFetchResponseBody || onFetchStreamUpdate,
);
// `optionsFetchFn` is the caller's whole fetch stack and already sits on top of
// a proxy-aware base when one is needed — the Node clients install
@ -68,10 +75,11 @@ export function createTransportNode(
? createAuthChallengeObserverFetch(inner, onAuthChallengeObserved)
: inner;
const withTracking = (inner: typeof fetch): typeof fetch =>
onFetchRequest
wantsFetchTracking
? createFetchTracker(inner, {
trackRequest: onFetchRequest,
updateResponseBody: onFetchResponseBody,
updateStream: onFetchStreamUpdate,
})
: inner;
// The observer sits *under* the interceptor so it still reports the