* 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>
116 lines
3.6 KiB
TypeScript
116 lines
3.6 KiB
TypeScript
/**
|
|
* Registry of import strategies (#1348). Each entry knows its well-known
|
|
* config locations per platform and how to parse that client's file into the
|
|
* canonical `MCPConfig`. The source picker enumerates `IMPORT_STRATEGY_LIST`;
|
|
* the backend `GET /api/import-source` route and the browser file-upload path
|
|
* both resolve a strategy via `getImportStrategy(id)`.
|
|
*
|
|
* Adding a new client = add one entry here (+ a fixture test). Clients that use
|
|
* the standard `{ mcpServers: {...} }` shape reuse `parseMcpServersConfig` and
|
|
* only differ by `defaultPaths`; VS Code's `servers`/`inputs` variant gets its
|
|
* own `parseVsCodeConfig`.
|
|
*
|
|
* Pure + isomorphic: `defaultPaths` receives `platform` + `homeDir` so this file
|
|
* never imports Node `os`/`path`. Paths are joined with `/`, which Node `fs`
|
|
* accepts on every platform.
|
|
*/
|
|
import type { ImportStrategy } from "./types.js";
|
|
import { parseMcpServersConfig, parseVsCodeConfig } from "./clientConfig.js";
|
|
|
|
/** Join path segments with `/`, trimming a trailing slash on the base. */
|
|
function join(...segments: string[]): string {
|
|
return segments
|
|
.map((s, i) =>
|
|
i === 0 ? s.replace(/\/+$/, "") : s.replace(/^\/+|\/+$/g, ""),
|
|
)
|
|
.filter((s) => s.length > 0)
|
|
.join("/");
|
|
}
|
|
|
|
/** Windows `%APPDATA%` (Roaming) under the user's home directory. */
|
|
function appData(homeDir: string): string {
|
|
return join(homeDir, "AppData", "Roaming");
|
|
}
|
|
|
|
/**
|
|
* Per-platform application-support directory for a given app folder name.
|
|
* macOS: `~/Library/Application Support/<app>`; Windows: `%APPDATA%\<app>`;
|
|
* Linux/other: `~/.config/<app>`.
|
|
*/
|
|
function appSupportDir(
|
|
platform: NodeJS.Platform,
|
|
homeDir: string,
|
|
app: string,
|
|
): string {
|
|
if (platform === "darwin") {
|
|
return join(homeDir, "Library", "Application Support", app);
|
|
}
|
|
if (platform === "win32") {
|
|
return join(appData(homeDir), app);
|
|
}
|
|
return join(homeDir, ".config", app);
|
|
}
|
|
|
|
const claudeDesktop: ImportStrategy = {
|
|
id: "claude-desktop",
|
|
label: "Claude Desktop",
|
|
defaultPaths: (platform, homeDir) => [
|
|
join(
|
|
appSupportDir(platform, homeDir, "Claude"),
|
|
"claude_desktop_config.json",
|
|
),
|
|
],
|
|
parse: parseMcpServersConfig,
|
|
};
|
|
|
|
const cursor: ImportStrategy = {
|
|
id: "cursor",
|
|
label: "Cursor",
|
|
defaultPaths: (_platform, homeDir) => [join(homeDir, ".cursor", "mcp.json")],
|
|
parse: parseMcpServersConfig,
|
|
};
|
|
|
|
const cline: ImportStrategy = {
|
|
id: "cline",
|
|
label: "Cline",
|
|
defaultPaths: (platform, homeDir) => [
|
|
// Cline (VS Code extension) global settings live under the editor's
|
|
// globalStorage; the documented cross-platform fallback is ~/Documents.
|
|
join(
|
|
appSupportDir(platform, homeDir, "Code"),
|
|
"User",
|
|
"globalStorage",
|
|
"saoudrizwan.claude-dev",
|
|
"settings",
|
|
"cline_mcp_settings.json",
|
|
),
|
|
join(homeDir, "Documents", "Cline", "MCP", "cline_mcp_settings.json"),
|
|
],
|
|
parse: parseMcpServersConfig,
|
|
};
|
|
|
|
const vscode: ImportStrategy = {
|
|
id: "vscode",
|
|
label: "VS Code",
|
|
defaultPaths: (platform, homeDir) => [
|
|
join(appSupportDir(platform, homeDir, "Code"), "User", "mcp.json"),
|
|
],
|
|
parse: parseVsCodeConfig,
|
|
};
|
|
|
|
/** All strategies in source-picker display order. */
|
|
export const IMPORT_STRATEGY_LIST: readonly ImportStrategy[] = [
|
|
claudeDesktop,
|
|
cursor,
|
|
cline,
|
|
vscode,
|
|
];
|
|
|
|
/** Map of `id → ImportStrategy` for resolution. */
|
|
export const IMPORT_STRATEGIES: Readonly<Record<string, ImportStrategy>> =
|
|
Object.fromEntries(IMPORT_STRATEGY_LIST.map((s) => [s.id, s]));
|
|
|
|
/** Resolve a strategy by id, or undefined for an unknown id. */
|
|
export function getImportStrategy(id: string): ImportStrategy | undefined {
|
|
return IMPORT_STRATEGIES[id];
|
|
}
|