feat: add --protocol-era launch flag for ad-hoc servers (#2208)
An ad-hoc --server-url / command launch always negotiated the legacy era, because protocolEra was only settable per server in mcp.json. A wrapper that mints a short-lived bearer token therefore had to write that token to disk to exercise a server under auto or modern. --protocol-era <legacy|auto|modern> now feeds settings.protocolEra in all three clients through a shared parseProtocolEra coercer: - CLI/TUI overlay it in loadServerEntries alongside --header, onto an ad-hoc target and onto a --catalog/--config entry for that run. - Web stamps it on the in-memory ad-hoc entry, which the backend lifts into settings like headers; as with --header it is rejected alongside --catalog/--config, but unlike --header it applies to stdio too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SZ4rrvH6xaCfrst75gJh7x Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
parent
3e728a3b8c
commit
55db703196
3 changed files with 324 additions and 30 deletions
|
|
@ -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`;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export {
|
||||
parseKeyValuePair,
|
||||
parseHeaderPair,
|
||||
parseProtocolEra,
|
||||
withDefaultCatalogPath,
|
||||
resolveServerConfigs,
|
||||
getNamedServerConfigs,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue