* feat(web,core): import client configs and registry server.json (#1348) Wire up the two server-import affordances that were previously todoNoop: - Import config: source picker (Claude Desktop / Cursor / Cline / VS Code) read on the backend via a new GET /api/import-source, plus a file-upload fallback. Per-server review with import/skip on new servers and overwrite/skip/rename on id conflicts, then a per-server outcome summary. - Import server.json: parse the MCP registry single-server format (npm/pypi/oci packages + remotes, env vars, package/runtime args, URL template vars) into a runnable config, with a file picker and debounced live validation; Add Server is disabled until validation passes. Core gains an isomorphic import strategy layer (core/mcp/import) with a strategy registry, client-config + server.json parsers, a strategy-agnostic merge, and a dependency-injected source resolver. useServers exposes importSource(). Modal headers are bold and the redundant server.json section header is folded into the modal title. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): collapse the server.json textarea into a File Contents disclosure Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): auto-collapse File Contents disclosure after content loads Once content is pasted or a file is loaded, collapse the File Contents disclosure after ~1s with Mantine's built-in accordion animation, signaling the input was accepted and surfacing the validation/env-var sections. The disclosure becomes controlled by the modal; clearing the textarea re-opens it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): flash File Contents hover highlight before auto-collapse Before the File Contents disclosure auto-collapses, paint it with its hover background for 250ms, then run the collapse over a 325ms animation, so the collapse reads as intentional. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): rename "Import server.json" to "Import from registry config" Rename the Add Servers menu entry and the modal header. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): rename "Import config" to "Import client config" Rename the Add Servers menu entry and the modal header. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(web,core): move import orchestration into core helpers + web hooks Make the import logic reusable by CLI/TUI and keep the modals display-only: - core/mcp/import: add parseClientConfig (mcpServers→servers fallback), resolveServerId, selectServerJsonOption, and buildServerConfigForSelection, so the full import (parse → select → build → merge) is callable from any environment. - core/storage: extract the pure validateStoreId into a Node-free store-id module (store-io re-exports it) so the isomorphic import layer can validate ids without pulling Node deps into the browser bundle. - clients/web/hooks: useServerJsonImport and useImportClientConfig own all the wizard/parse/debounce/apply state; ServerImportConfigModal and ServerImportJsonModal are now thin display shells over the dumb panel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): rename "Import client config" to "Import from client config" Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): remove the Cancel button from the registry import modal The modal header X (and Escape) still dismiss it; drop the redundant Cancel button (and the panel onCancel prop). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): hide validation results + name override until content is present In the registry import modal, the Validation Results section and the Server Name override field stay hidden until something has been pasted/loaded into the textarea. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): make Validation Results a collapsible disclosure Wrap the registry import modal's validation results in a Mantine accordion disclosure (default open), matching the File Contents section. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): Server Name disclosure with read-only derived name + override Make Server Name a collapsible disclosure containing a read-only "From configuration" field (the id derived from the server.json name) and the editable Override field. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): scroll to and highlight a freshly-added server After a manual add/clone or an import (client config / registry), the new server's card scrolls into view and draws an animated border (Mantine Border Animate); clicking anywhere on the card dismisses it. - App tracks highlightedServerId; addServerHighlighted sets it (edits and conflict-overwrites don't). Threaded through InspectorView -> ServerListScreen -> ServerCard. - ServerCard scrolls into view on highlight and wraps in <BorderAnimate>; onClick clears the highlight. - Pin @gfazioli/mantine-border-animate@2.0.3 (Mantine 8 compatible; 2.0.4+ requires Mantine 9). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web): highlighted card fills its cell and its controls work on first click - The BorderAnimate wrapper is display:flex, so the card was shrinking to content width while the border spanned the cell. Stretch the card with w=100% (and the wrapper). - Always render BorderAnimate and toggle show/animate instead of conditionally wrapping: swapping the element type on clear remounted the card, swallowing the click that triggered it (e.g. the connect toggle fired clear but not connect, needing a second click). Verified: one click now connects + clears. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(web,core): address review — OCI env, untrack fixture, rename validation - Untrack the stray inspector-v1-multi.server.json (accidentally committed) and gitignore root *.server.json fixtures. - core/mcp/import/serverJson: forward declared env vars into OCI containers via '-e KEY' docker args (they previously only set the docker CLI process env); add a test. Clarify buildServerConfig (destructure env out instead of spread-then-delete) and document value-vs-default env handling. - useImportClientConfig: validate 'rename' targets inline (invalid charset / collision with existing, additions, or other renames) and gate the Import button on canImport; surface the error on the field. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(web): highlight all servers from a batch import, not just the last Track highlighted servers as a set of ids instead of one. addServerHighlighted accumulates each id (a multi-server client-config import highlights them all); opening an add/import modal starts a fresh batch. Clicking a card clears only that card's highlight; only the first highlighted card scrolls into view. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
5efe9ddc7b
commit
97b1a7fbff
1 changed files with 4238 additions and 176 deletions
420
core/mcp/import/serverJson.ts
Normal file
420
core/mcp/import/serverJson.ts
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
/**
|
||||
* Parser for the official MCP Registry `server.json` format (the file
|
||||
* `mcp-publisher init` generates and publishes). Distinct from the client-config
|
||||
* parsers: a `server.json` describes a *single* server via one or more runnable
|
||||
* `packages` and/or `remotes`, and carries publish-time metadata
|
||||
* (`environmentVariables`, `packageArguments`, URL `variables`) that has to be
|
||||
* mapped onto a runnable Inspector `MCPServerConfig`.
|
||||
*
|
||||
* Pure + isomorphic (no Node deps). The web wiring layer (`useServerJsonImport`)
|
||||
* turns the parsed options into the props the dumb `ImportServerJsonPanel`
|
||||
* renders, then calls `buildServerConfig` once the user has filled in env vars.
|
||||
*
|
||||
* Schema reference: https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json
|
||||
* The published `server.json` file uses camelCase keys (`registryType`,
|
||||
* `runtimeHint`, `environmentVariables`, …); the registry REST API has
|
||||
* historically returned the same fields in snake_case. We read both so a blob
|
||||
* copied from either source imports cleanly.
|
||||
*/
|
||||
import type { MCPServerConfig, StdioServerConfig } from "../types.js";
|
||||
import { validateStoreId } from "../../storage/store-id.js";
|
||||
|
||||
/** A registry package's environment-variable declaration. */
|
||||
export interface ServerJsonEnvVar {
|
||||
name: string;
|
||||
description?: string;
|
||||
required: boolean;
|
||||
/** Declared default/value, pre-filled into the form when present. */
|
||||
default?: string;
|
||||
isSecret?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* One runnable way to launch the server — either a package (npm/pypi/oci/nuget)
|
||||
* resolved to a stdio command, or a remote (streamable-http/sse) URL. The dumb
|
||||
* panel renders these as the package-selection radios; `buildServerConfig`
|
||||
* turns the chosen one + env overrides into the final config.
|
||||
*/
|
||||
export interface ServerJsonOption {
|
||||
/** Registry/transport label source: "npm" | "pypi" | "oci" | "nuget" | "streamable-http" | "sse". */
|
||||
registryType: string;
|
||||
/** Package identifier or remote URL — shown in the radio label. */
|
||||
identifier: string;
|
||||
/** The runtime/transport used to launch it (e.g. "npx", "uvx", "docker", "streamable-http"). */
|
||||
runtimeHint: string;
|
||||
/**
|
||||
* The runnable config with all statically-known values resolved (runtime
|
||||
* command, args, declared env defaults, URL variable defaults). Env vars the
|
||||
* user must supply are surfaced via `envVars` and merged in by
|
||||
* `buildServerConfig`.
|
||||
*/
|
||||
baseConfig: MCPServerConfig;
|
||||
/** Env vars declared for this option (empty for remotes). */
|
||||
envVars: ServerJsonEnvVar[];
|
||||
}
|
||||
|
||||
export interface ParsedServerJson {
|
||||
/** Sanitized default server id, derived from the last segment of `name`. */
|
||||
serverName: string;
|
||||
/** The full registry name (e.g. "io.github.foo/weather"). */
|
||||
fullName: string;
|
||||
/** Runnable launch options, packages first then remotes. */
|
||||
options: ServerJsonOption[];
|
||||
}
|
||||
|
||||
/** Read a field by its camelCase or snake_case key. */
|
||||
function pick(
|
||||
obj: Record<string, unknown>,
|
||||
camel: string,
|
||||
snake: string,
|
||||
): unknown {
|
||||
return obj[camel] !== undefined ? obj[camel] : obj[snake];
|
||||
}
|
||||
|
||||
function asString(value: unknown): string | undefined {
|
||||
return typeof value === "string" ? value : undefined;
|
||||
}
|
||||
|
||||
function asObject(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? (value as Record<string, unknown>)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function asArray(value: unknown): unknown[] {
|
||||
return Array.isArray(value) ? value : [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Derive a safe Inspector server id from a registry name. The registry name is
|
||||
* a reverse-DNS-ish string ("io.github.user/weather"); we take the segment
|
||||
* after the last "/" and strip anything outside `[A-Za-z0-9_-]`. Falls back to
|
||||
* "server" when nothing usable remains.
|
||||
*/
|
||||
export function deriveServerId(fullName: string): string {
|
||||
const lastSegment = fullName.split("/").pop() ?? fullName;
|
||||
const cleaned = lastSegment
|
||||
.replace(/[^A-Za-z0-9_-]/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
return cleaned || "server";
|
||||
}
|
||||
|
||||
/** Parse an `environmentVariables` array into our env-var descriptors. */
|
||||
function parseEnvVars(rawList: unknown[]): ServerJsonEnvVar[] {
|
||||
const out: ServerJsonEnvVar[] = [];
|
||||
for (const entry of rawList) {
|
||||
const obj = asObject(entry);
|
||||
if (!obj) continue;
|
||||
const name = asString(obj.name);
|
||||
if (!name) continue;
|
||||
const required = Boolean(pick(obj, "isRequired", "is_required"));
|
||||
const isSecret = Boolean(pick(obj, "isSecret", "is_secret"));
|
||||
// The schema's `value` is a *fixed* value and `default` a suggestion; we
|
||||
// prefill both as the (editable) input value. We don't currently lock a
|
||||
// fixed `value` against editing — acceptable for an import preview the user
|
||||
// reviews before saving.
|
||||
const def = asString(obj.default) ?? asString(obj.value);
|
||||
out.push({
|
||||
name,
|
||||
description: asString(obj.description),
|
||||
required,
|
||||
...(def !== undefined ? { default: def } : {}),
|
||||
...(isSecret ? { isSecret: true } : {}),
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a registry `Argument` list (positional/named) into argv strings.
|
||||
* Best-effort: positional args contribute their `value`/`default`; named args
|
||||
* contribute their flag `name` plus any `value`/`default`. Arguments with
|
||||
* neither a value nor a default are skipped (the user can add them via the edit
|
||||
* form afterward); advanced features (`isRepeated`, `variables`) are ignored.
|
||||
*/
|
||||
function resolveArguments(rawList: unknown[]): string[] {
|
||||
const out: string[] = [];
|
||||
for (const entry of rawList) {
|
||||
const obj = asObject(entry);
|
||||
if (!obj) continue;
|
||||
const type = asString(obj.type) ?? "positional";
|
||||
const value = asString(obj.value) ?? asString(obj.default);
|
||||
if (type === "named") {
|
||||
const name = asString(obj.name);
|
||||
if (!name) continue;
|
||||
out.push(name);
|
||||
if (value !== undefined) out.push(value);
|
||||
} else {
|
||||
if (value !== undefined) out.push(value);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Build the env map for a package: declared defaults only (overrides applied later). */
|
||||
function defaultEnv(envVars: ServerJsonEnvVar[]): Record<string, string> {
|
||||
const env: Record<string, string> = {};
|
||||
for (const v of envVars) {
|
||||
if (v.default !== undefined) env[v.name] = v.default;
|
||||
}
|
||||
return env;
|
||||
}
|
||||
|
||||
/** runtime command + base args for a given registry package type, or null if unsupported. */
|
||||
function runtimeForPackage(
|
||||
registryType: string,
|
||||
identifier: string,
|
||||
version: string | undefined,
|
||||
): { command: string; runtimeHint: string; ref: string } | null {
|
||||
const versioned = (sep: string) =>
|
||||
version ? `${identifier}${sep}${version}` : identifier;
|
||||
switch (registryType) {
|
||||
case "npm":
|
||||
return { command: "npx", runtimeHint: "npx", ref: versioned("@") };
|
||||
case "pypi":
|
||||
return { command: "uvx", runtimeHint: "uvx", ref: identifier };
|
||||
case "oci":
|
||||
return { command: "docker", runtimeHint: "docker", ref: identifier };
|
||||
case "nuget":
|
||||
return { command: "dnx", runtimeHint: "dnx", ref: versioned("@") };
|
||||
// mcpb artifacts are downloadable bundles, not a runnable command — skip.
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Map a single `packages[]` entry to a launch option, or null if unsupported. */
|
||||
function parsePackage(raw: Record<string, unknown>): ServerJsonOption | null {
|
||||
const registryType =
|
||||
asString(pick(raw, "registryType", "registry_type")) ?? "";
|
||||
const identifier = asString(raw.identifier) ?? "";
|
||||
if (!registryType || !identifier) return null;
|
||||
const version = asString(raw.version);
|
||||
const runtime = runtimeForPackage(registryType, identifier, version);
|
||||
if (!runtime) return null;
|
||||
|
||||
const envVars = parseEnvVars(
|
||||
asArray(pick(raw, "environmentVariables", "environment_variables")),
|
||||
);
|
||||
const runtimeArgs = resolveArguments(
|
||||
asArray(pick(raw, "runtimeArguments", "runtime_arguments")),
|
||||
);
|
||||
const packageArgs = resolveArguments(
|
||||
asArray(pick(raw, "packageArguments", "package_arguments")),
|
||||
);
|
||||
|
||||
const env = defaultEnv(envVars);
|
||||
|
||||
// docker needs `run -i --rm` before the image; package args go to the
|
||||
// container after the image. Declared env vars must be forwarded into the
|
||||
// container with `-e KEY` flags — otherwise they'd only set the `docker` CLI
|
||||
// process's environment (via config.env) and never reach the server. `-e KEY`
|
||||
// (no value) tells docker to pass KEY through from that process env, which is
|
||||
// where buildServerConfig merges the user-supplied values. Other runtimes
|
||||
// inherit config.env directly, so they need no extra flags.
|
||||
const dockerEnvArgs = envVars.flatMap((v) => ["-e", v.name]);
|
||||
const args =
|
||||
runtime.command === "docker"
|
||||
? [
|
||||
"run",
|
||||
"-i",
|
||||
"--rm",
|
||||
...runtimeArgs,
|
||||
...dockerEnvArgs,
|
||||
runtime.ref,
|
||||
...packageArgs,
|
||||
]
|
||||
: registryType === "npm"
|
||||
? [...runtimeArgs, "-y", runtime.ref, ...packageArgs]
|
||||
: [...runtimeArgs, runtime.ref, ...packageArgs];
|
||||
|
||||
const baseConfig: StdioServerConfig = {
|
||||
type: "stdio",
|
||||
command: runtime.command,
|
||||
...(args.length > 0 ? { args } : {}),
|
||||
...(Object.keys(env).length > 0 ? { env } : {}),
|
||||
};
|
||||
return {
|
||||
registryType,
|
||||
identifier,
|
||||
runtimeHint: runtime.runtimeHint,
|
||||
baseConfig,
|
||||
envVars,
|
||||
};
|
||||
}
|
||||
|
||||
/** Substitute `{var}` template tokens in a remote URL with their declared defaults. */
|
||||
function resolveRemoteUrl(
|
||||
url: string,
|
||||
variables: Record<string, unknown> | undefined,
|
||||
): string {
|
||||
if (!variables) return url;
|
||||
return url.replace(/\{([^}]+)\}/g, (match, name: string) => {
|
||||
const v = asObject(variables[name]);
|
||||
const def = v ? asString(v.default) : undefined;
|
||||
return def ?? match;
|
||||
});
|
||||
}
|
||||
|
||||
/** Map a single `remotes[]` entry to a launch option, or null if unsupported. */
|
||||
function parseRemote(raw: Record<string, unknown>): ServerJsonOption | null {
|
||||
const type = asString(raw.type);
|
||||
const url = asString(raw.url);
|
||||
if (!url) return null;
|
||||
const transport = type === "sse" ? "sse" : "streamable-http";
|
||||
const resolvedUrl = resolveRemoteUrl(url, asObject(raw.variables));
|
||||
const baseConfig: MCPServerConfig =
|
||||
transport === "sse"
|
||||
? { type: "sse", url: resolvedUrl }
|
||||
: { type: "streamable-http", url: resolvedUrl };
|
||||
// Label remotes as "remote" (the install method); the transport variant
|
||||
// (streamable-http / sse) is carried in runtimeHint so the picker shows
|
||||
// e.g. "remote: https://… (streamable-http)".
|
||||
return {
|
||||
registryType: "remote",
|
||||
identifier: resolvedUrl,
|
||||
runtimeHint: transport,
|
||||
baseConfig,
|
||||
envVars: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a registry `server.json` blob into its runnable options. Accepts either
|
||||
* a bare server object or a `{ server: {...} }` wrapper (some registry API
|
||||
* responses nest it). Throws when the blob is not a server object or exposes no
|
||||
* runnable package/remote.
|
||||
*/
|
||||
export function parseServerJson(raw: string): ParsedServerJson {
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch (err) {
|
||||
const detail = err instanceof Error ? err.message : String(err);
|
||||
throw new Error(`Invalid JSON: ${detail}`);
|
||||
}
|
||||
const top = asObject(data);
|
||||
if (!top) throw new Error("Expected a JSON object at the top level");
|
||||
const server = asObject(top.server) ?? top;
|
||||
|
||||
const fullName = asString(server.name);
|
||||
if (!fullName) {
|
||||
throw new Error("server.json is missing a 'name' field");
|
||||
}
|
||||
|
||||
const options: ServerJsonOption[] = [];
|
||||
for (const pkg of asArray(server.packages)) {
|
||||
const obj = asObject(pkg);
|
||||
if (!obj) continue;
|
||||
const option = parsePackage(obj);
|
||||
if (option) options.push(option);
|
||||
}
|
||||
for (const remote of asArray(server.remotes)) {
|
||||
const obj = asObject(remote);
|
||||
if (!obj) continue;
|
||||
const option = parseRemote(obj);
|
||||
if (option) options.push(option);
|
||||
}
|
||||
|
||||
if (options.length === 0) {
|
||||
throw new Error(
|
||||
"No runnable package or remote found in server.json (mcpb-only or unsupported registry types are not importable)",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
serverName: deriveServerId(fullName),
|
||||
fullName,
|
||||
options,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the final runnable config for a chosen option, merging user-supplied
|
||||
* env overrides over the declared defaults. Empty-string values are dropped so
|
||||
* an untouched required var doesn't persist as `KEY=""`. Remotes ignore env
|
||||
* overrides (their config carries no env).
|
||||
*/
|
||||
export function buildServerConfig(
|
||||
option: ServerJsonOption,
|
||||
envOverrides: Record<string, string>,
|
||||
): MCPServerConfig {
|
||||
if (option.baseConfig.type !== "stdio") {
|
||||
return { ...option.baseConfig };
|
||||
}
|
||||
const merged: Record<string, string> = { ...(option.baseConfig.env ?? {}) };
|
||||
for (const [key, value] of Object.entries(envOverrides)) {
|
||||
if (value === "") delete merged[key];
|
||||
else merged[key] = value;
|
||||
}
|
||||
// Drop the original `env` from the base (via destructure) so it isn't spread
|
||||
// back in; re-add it only when the merged map is non-empty. This keeps an
|
||||
// entry whose overrides emptied everything out from persisting a stale env.
|
||||
const { env: _baseEnv, ...base } = option.baseConfig;
|
||||
return {
|
||||
...base,
|
||||
...(Object.keys(merged).length > 0 ? { env: merged } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/** The id a parsed server.json will be saved under: the override (if any) wins. */
|
||||
export function resolveServerId(
|
||||
parsed: ParsedServerJson,
|
||||
idOverride?: string,
|
||||
): string {
|
||||
const trimmed = (idOverride ?? "").trim();
|
||||
return trimmed || parsed.serverName;
|
||||
}
|
||||
|
||||
/** The chosen launch option plus the resulting id and its validity. */
|
||||
export interface ServerJsonSelection {
|
||||
selectedIndex: number;
|
||||
selectedOption: ServerJsonOption;
|
||||
serverId: string;
|
||||
idIsValid: boolean;
|
||||
idIsDuplicate: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve which option a parsed server.json will import as, the id it will use,
|
||||
* and whether that id is valid + free. Pure, so both the web wiring and a future
|
||||
* CLI/TUI import can share the selection logic. `selectedIndex` is clamped to the
|
||||
* available options.
|
||||
*/
|
||||
export function selectServerJsonOption(
|
||||
parsed: ParsedServerJson,
|
||||
opts: {
|
||||
selectedIndex?: number;
|
||||
idOverride?: string;
|
||||
existingIds?: readonly string[];
|
||||
} = {},
|
||||
): ServerJsonSelection {
|
||||
const selectedIndex = Math.min(
|
||||
opts.selectedIndex ?? 0,
|
||||
Math.max(parsed.options.length - 1, 0),
|
||||
);
|
||||
const serverId = resolveServerId(parsed, opts.idOverride);
|
||||
return {
|
||||
selectedIndex,
|
||||
selectedOption: parsed.options[selectedIndex],
|
||||
serverId,
|
||||
idIsValid: validateStoreId(serverId),
|
||||
idIsDuplicate: (opts.existingIds ?? []).includes(serverId),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the user's env overrides for the *selected* option only (env vars the
|
||||
* option doesn't declare are ignored) and build the runnable config.
|
||||
*/
|
||||
export function buildServerConfigForSelection(
|
||||
option: ServerJsonOption,
|
||||
envOverrides: Record<string, string>,
|
||||
): MCPServerConfig {
|
||||
const overrides: Record<string, string> = {};
|
||||
for (const v of option.envVars) {
|
||||
const value = envOverrides[v.name];
|
||||
if (value !== undefined) overrides[v.name] = value;
|
||||
}
|
||||
return buildServerConfig(option, overrides);
|
||||
}
|
||||
Loading…
Reference in a new issue