add-mcp
Version:
Add MCP servers to your favorite coding agents with a single command.
2,395 lines • 81.7 kB
JavaScript
#!/usr/bin/env node
import {
DEFAULT_FIND_REGISTRY_LABEL,
DEFAULT_FIND_REGISTRY_URL,
agents,
buildAgentSelectionChoices,
buildServerConfig,
claudeCopilotGithubShadowError,
describeOptionalField,
detectGlobalAgents,
detectProjectAgents,
extractServerIdentity,
findMatchingServers,
getAgentTypes,
getCommonInstallScopes,
getConfigKey,
getConfigPath,
getFindRegistries,
getLastSelectedAgents,
getProjectCapableAgents,
installServer,
installServerForAgent,
isBearerTokenEnvName,
isRemoteSource,
isTransportSupported,
listInstalledServers,
listOpenCodeServers,
normalizeStoredCommand,
parseSource,
relocateOpenCodeServer,
removeOpenCodeServer,
removeServerFromConfig,
rewriteCopilotCliConfig,
saveFindRegistries,
selectAgentsInteractive,
supportsProjectConfig,
updateGitignoreWithPaths
} from "./chunk-JZK2SME6.js";
// src/index.ts
import { program } from "commander";
import * as p2 from "@clack/prompts";
import chalk from "chalk";
import { homedir } from "os";
// src/types.ts
var agentAliases = {
"cline-vscode": "cline",
codeium: "windsurf",
cascade: "windsurf",
droid: "factory-droid",
factory: "factory-droid",
gemini: "gemini-cli",
"github-copilot": "vscode",
grok: "grok-build",
"hermes-agent": "hermes",
kilo: "kilo-code",
kilocode: "kilo-code",
kimi: "kimi-code",
kiro: "kiro-cli",
mastra: "mastracode",
"pi-agent": "pi"
};
// src/find.ts
import * as p from "@clack/prompts";
// src/template.ts
var TEMPLATE_PATTERN = /\$\{([^}]+)\}/g;
function findTemplateVars(value) {
const vars = [];
let match;
while ((match = TEMPLATE_PATTERN.exec(value)) !== null) {
vars.push(match[1]);
}
TEMPLATE_PATTERN.lastIndex = 0;
return vars;
}
function substituteTemplatePlaceholders(value, replacement) {
return value.replace(/\$\{([^}]+)\}/g, () => replacement);
}
async function resolveTemplates(value, ask) {
const vars = findTemplateVars(value);
if (vars.length === 0) return { resolved: value, cancelled: false };
let result = value;
for (const varName of vars) {
const answer = await ask(varName);
if (typeof answer === "symbol") return { resolved: value, cancelled: true };
const entered = typeof answer === "string" ? answer : "";
result = result.replace(`\${${varName}}`, entered);
}
return { resolved: result, cancelled: false };
}
async function resolveRecordTemplates(record, ask) {
const resolved = {};
for (const [key, value] of Object.entries(record)) {
const result = await resolveTemplates(value, ask);
if (result.cancelled) return { resolved: record, cancelled: true };
resolved[key] = result.resolved;
}
return { resolved, cancelled: false };
}
async function resolveArrayTemplates(values, ask) {
const resolved = [];
for (const value of values) {
const result = await resolveTemplates(value, ask);
if (result.cancelled) return { resolved: values, cancelled: true };
resolved.push(result.resolved);
}
return { resolved, cancelled: false };
}
function hasTemplateVars(values) {
const strings = Array.isArray(values) ? values : Object.values(values);
return strings.some((v) => findTemplateVars(v).length > 0);
}
// src/package-arguments.ts
var ARG_PLACEHOLDER = "<your-variable-value-here>";
function normalizeNamedFlag(name) {
const t = name.trim();
if (t.startsWith("-")) return t;
return `--${t}`;
}
function definitionsFromPackage(pkg) {
return pkg.packageArguments ?? [];
}
function isNamedArg(d) {
if (d.type === "positional") return false;
if (d.type === "named") return true;
return Boolean(d.name?.trim());
}
function labelForArg(d, index) {
if (isNamedArg(d)) {
return `Flag ${normalizeNamedFlag(d.name)}`;
}
return d.valueHint?.trim() || d.description?.trim() || `Positional argument #${index + 1}`;
}
function hasTemplates(s) {
return Boolean(s && findTemplateVars(s).length > 0);
}
function buildPackageArgumentsArgvNonInteractive(definitions, placeholder = ARG_PLACEHOLDER) {
const out = [];
for (let i = 0; i < definitions.length; i++) {
const d = definitions[i];
if (isNamedArg(d)) {
const flag = normalizeNamedFlag(d.name);
let val = d.value?.trim() ?? "";
if (hasTemplates(d.value)) {
val = substituteTemplatePlaceholders(d.value, placeholder);
} else if (!val && d.default?.trim()) {
val = d.default.trim();
}
if (!val) {
if (d.isRequired === true) {
out.push(flag, placeholder);
}
continue;
}
out.push(flag, val);
} else {
let val = d.value?.trim() ?? "";
if (hasTemplates(d.value)) {
val = substituteTemplatePlaceholders(d.value, placeholder);
pushPositionalSegments(out, d, val);
continue;
}
if (val) {
pushPositionalSegments(out, d, val);
continue;
}
if (d.default?.trim()) {
pushPositionalSegments(out, d, d.default.trim());
continue;
}
pushPositionalSegments(out, d, placeholder);
}
}
return out;
}
function pushPositionalSegments(out, d, raw) {
if (d.isRepeated === true) {
const parts = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
if (parts.length === 0) {
out.push(raw);
return;
}
for (const p3 of parts) {
out.push(p3);
}
return;
}
out.push(raw);
}
async function buildPackageArgumentsArgvInteractive(definitions, prompt) {
const out = [];
for (let i = 0; i < definitions.length; i++) {
const d = definitions[i];
const label = labelForArg(d, i);
if (isNamedArg(d)) {
const flag = normalizeNamedFlag(d.name);
let val2 = d.value ?? "";
if (hasTemplates(d.value)) {
const res = await resolveTemplates(d.value, async (varName) => {
const answer = await prompt({
label: `${label} (${varName})`,
isRequired: true,
placeholder: `<${varName}>`
});
return answer;
});
if (res.cancelled) return { cancelled: true };
val2 = res.resolved;
out.push(flag, val2);
continue;
}
if (val2.trim()) {
out.push(flag, val2.trim());
continue;
}
const defVal2 = d.default?.trim() ?? "";
const raw2 = await prompt({
label,
isRequired: d.isRequired === true,
placeholder: defVal2 || ARG_PLACEHOLDER
});
if (typeof raw2 === "symbol") return { cancelled: true };
const trimmed2 = typeof raw2 === "string" ? raw2.trim() : "";
if (!trimmed2) {
if (d.isRequired === true) {
out.push(flag, defVal2 || ARG_PLACEHOLDER);
} else if (defVal2) {
out.push(flag, defVal2);
}
continue;
}
out.push(flag, trimmed2);
continue;
}
let val = d.value ?? "";
if (hasTemplates(d.value)) {
const res = await resolveTemplates(d.value, async (varName) => {
const answer = await prompt({
label: `${label} (${varName})`,
isRequired: true,
placeholder: `<${varName}>`
});
return answer;
});
if (res.cancelled) return { cancelled: true };
val = res.resolved;
pushPositionalSegments(out, d, val);
continue;
}
if (val.trim()) {
pushPositionalSegments(out, d, val.trim());
continue;
}
const defVal = d.default?.trim() ?? "";
if (defVal) {
pushPositionalSegments(out, d, defVal);
continue;
}
if (d.isRequired !== true) {
const raw2 = await prompt({
label,
isRequired: false,
placeholder: ARG_PLACEHOLDER
});
if (typeof raw2 === "symbol") return { cancelled: true };
const trimmed2 = typeof raw2 === "string" ? raw2.trim() : "";
if (!trimmed2) {
pushPositionalSegments(out, d, ARG_PLACEHOLDER);
} else {
pushPositionalSegments(out, d, trimmed2);
}
continue;
}
const raw = await prompt({
label,
isRequired: true,
placeholder: ARG_PLACEHOLDER
});
if (typeof raw === "symbol") return { cancelled: true };
const trimmed = typeof raw === "string" ? raw.trim() : "";
pushPositionalSegments(
out,
d,
trimmed.length > 0 ? trimmed : ARG_PLACEHOLDER
);
}
return { argv: out, cancelled: false };
}
// src/find.ts
function buildPlaceholderValue(kind) {
return kind === "header" ? "<your-header-value-here>" : "<your-variable-value-here>";
}
function normalize(value) {
return value.trim().toLowerCase();
}
function tokenize(value) {
return normalize(value).split(/[^a-z0-9]+/i).filter((token) => token.length > 0);
}
var TRUSTED_NAMESPACE_PREFIXES = [
"com.supabase/",
"io.github.github/",
"com.postman/",
"com.stripe/",
"com.vercel/",
"io.github.vercel/",
"com.notion/",
"app.linear/",
"com.atlassian/",
"com.cloudflare.",
"io.github.getsentry/",
"io.github.mongodb-js/",
"io.github.railwayapp/"
];
var NOISY_NAMESPACE_PREFIXES = ["ai.smithery/"];
var SMITHERY_PREFIX = "ai.smithery/";
function resolveTemplateUrl(templateUrl, values) {
return templateUrl.replace(/\{([^}]+)\}/g, (fullMatch, rawName) => {
const key = String(rawName);
const replacement = values[key];
return replacement && replacement.length > 0 ? replacement : fullMatch;
});
}
function rankRegistryEntry(query, entry) {
const normalizedQuery = normalize(query);
const queryTokens = tokenize(normalizedQuery);
const name = normalize(entry.name);
const title = normalize(entry.title ?? "");
const description = normalize(entry.description);
const haystack = `${name} ${title} ${description}`;
const haystackTokens = new Set(tokenize(haystack));
let score = 0;
if (name === normalizedQuery) score += 800;
if (name.includes(normalizedQuery)) score += 350;
if (title.includes(normalizedQuery)) score += 250;
if (description.includes(normalizedQuery)) score += 120;
for (const token of queryTokens) {
if (token.length === 0) continue;
if (haystackTokens.has(token)) {
score += 60;
} else if (token.length >= 3 && haystack.includes(token)) {
score += 20;
}
}
if (TRUSTED_NAMESPACE_PREFIXES.some((prefix) => name.startsWith(prefix))) {
score += 500;
}
if (NOISY_NAMESPACE_PREFIXES.some((prefix) => name.startsWith(prefix))) {
score -= 500;
}
return score;
}
function rankRegistryEntries(query, entries) {
return [...entries].sort((a, b) => {
const scoreDiff = rankRegistryEntry(query, b) - rankRegistryEntry(query, a);
if (scoreDiff !== 0) return scoreDiff;
return a.name.localeCompare(b.name);
});
}
function isSmitheryEntry(entry) {
return normalize(entry.name).startsWith(SMITHERY_PREFIX);
}
function filterSmitheryWhenAlternativesExist(entries) {
if (entries.length === 0) return entries;
const hasNonSmithery = entries.some((entry) => !isSmitheryEntry(entry));
if (!hasNonSmithery) return entries;
return entries.filter((entry) => !isSmitheryEntry(entry));
}
var OFFICIAL_REGISTRY_BASE_URL = "https://registry.modelcontextprotocol.io";
function resolveOfficialRegistryServersUrl() {
return `${OFFICIAL_REGISTRY_BASE_URL}/v0.1/servers`;
}
function getDefaultFindRegistries() {
return [
{
url: DEFAULT_FIND_REGISTRY_URL,
label: DEFAULT_FIND_REGISTRY_LABEL
},
{
url: resolveOfficialRegistryServersUrl(),
label: "Official Anthropic registry"
}
];
}
function formatRegistryFailure(failure) {
const { registry, detail } = failure;
const defaults = getDefaultFindRegistries();
const isKnown = defaults.some((d) => d.url === registry.url);
if (isKnown && registry.label) {
return `"${registry.label}" (${registry.url}) is unavailable \u2014 ${detail}`;
}
return `Registry ${registry.url} is unavailable \u2014 ${detail}`;
}
function toEntry(item) {
const server = item?.server;
if (!server?.name || !server?.description || !server?.version) {
return null;
}
const npmPackage = (server.packages ?? []).find(
(pkg) => pkg.registryType === "npm"
);
const hasRemotes = Array.isArray(server.remotes) && server.remotes.length > 0;
if (!npmPackage && !hasRemotes) {
return null;
}
return {
name: server.name,
title: server.title,
description: server.description,
version: server.version,
repositoryUrl: server.repository?.url,
remotes: server.remotes,
package: npmPackage
};
}
function buildRegistryRequestUrl(registryUrl, query) {
const params = new URLSearchParams({
version: "latest",
limit: "100"
});
if (query.length > 0) {
params.set("search", query);
params.set("source", "cli");
}
const url = new URL(registryUrl);
for (const [key, value] of params.entries()) {
url.searchParams.set(key, value);
}
return url.toString();
}
async function searchRegistry(query, registries) {
const trimmedQuery = normalize(query);
const deduped = /* @__PURE__ */ new Map();
const failedRegistries = [];
for (const registry of registries) {
try {
const requestUrl = buildRegistryRequestUrl(registry.url, trimmedQuery);
const response = await fetch(requestUrl);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const payload = await response.json();
for (const item of payload.servers ?? []) {
const entry = toEntry(item);
if (!entry) continue;
const key = `${entry.name}@${entry.version}`;
if (!deduped.has(key)) {
deduped.set(key, entry);
}
}
} catch (error) {
const detail = error instanceof Error ? error.message : "unknown error";
failedRegistries.push({ registry, detail });
}
}
return {
entries: [...deduped.values()],
failedRegistries
};
}
function resolveServerName(entry) {
const title = entry.title?.trim();
if (title && title.length > 0) {
return title.toLowerCase();
}
const cleaned = entry.name.toLowerCase().replace(/mcp/g, "").replace(/com/g, "");
const tokens = cleaned.split(/[^a-z0-9]+/i).filter((token) => token.length > 0);
if (tokens.length > 0) {
return tokens.join("-");
}
const fallback = entry.name.toLowerCase().split(/[^a-z0-9]+/i).filter((token) => token.length > 0).join("-");
return fallback || "server";
}
function remoteToTransport(type) {
return type === "sse" ? "sse" : "http";
}
function pickRemote(entry, preferredTransport) {
const remotes = entry.remotes ?? [];
if (remotes.length === 0) return null;
const preferred = preferredTransport === "sse" ? remotes.find((remote) => remote.type === "sse") : remotes.find((remote) => remote.type === "streamable-http");
return preferred ?? remotes[0] ?? null;
}
function formatPackageTarget(pkg) {
return pkg.identifier;
}
function transportLabel(entry) {
const parts = [];
if (entry.package) parts.push("stdio");
if (entry.remotes && entry.remotes.length > 0) parts.push("remote");
return parts.length > 0 ? parts.join(", ") : "unknown";
}
function formatRemoteTarget(url) {
return url.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "").replace(/\/$/, "");
}
function formatFindResultTarget(entry) {
const remote = pickRemote(entry);
if (remote) {
return formatRemoteTarget(remote.url);
}
if (entry.package) {
return formatPackageTarget(entry.package);
}
return entry.name;
}
function formatFindResultRow(entry) {
const display = entry.title ?? entry.name;
return `${display} (${formatFindResultTarget(entry)}) [${transportLabel(entry)}]`;
}
async function promptValue(field) {
return p.text({
message: `${field.label} ${field.isRequired ? "(required)" : "(optional)"}`,
placeholder: field.placeholder
});
}
async function collectPromptValues(fields, ask) {
const values = {};
for (const field of fields) {
const raw = await ask(field);
if (p.isCancel(raw)) {
return { values, cancelled: true };
}
const value = raw != null && typeof raw === "string" ? raw.trim() : "";
if (value.length > 0) {
values[field.key] = value;
} else if (field.isRequired) {
values[field.key] = field.placeholder;
}
}
return { values, cancelled: false };
}
function variableFields(variables) {
if (!variables) return [];
return Object.entries(variables).map(([key, definition]) => ({
key,
label: `Variable ${key}`,
isRequired: definition.isRequired === true,
placeholder: buildPlaceholderValue("variable")
}));
}
function headerFields(headers) {
if (!headers) return [];
return headers.map((header) => ({
key: header.name,
label: `Header ${header.name}`,
isRequired: header.isRequired === true,
placeholder: buildPlaceholderValue("header")
}));
}
function packageVariableFields(variables) {
if (!variables) return [];
return variables.filter((variable) => variable.name && variable.name.trim().length > 0).map((variable) => ({
key: variable.name,
label: `Environment variable ${variable.name}`,
isRequired: variable.isRequired === true,
placeholder: buildPlaceholderValue("variable")
}));
}
function resolveNonInteractiveRemote(remote) {
const variableValues = {};
for (const [key, def] of Object.entries(remote.variables ?? {})) {
if (def.isRequired) {
variableValues[key] = buildPlaceholderValue("variable");
}
}
const headerValues = {};
for (const header of remote.headers ?? []) {
if (header.isRequired) {
headerValues[header.name] = buildPlaceholderValue("header");
}
}
return {
url: resolveTemplateUrl(remote.url, variableValues),
headers: Object.keys(headerValues).length > 0 ? headerValues : void 0
};
}
async function resolveInteractiveRemote(remote) {
const variableResult = await collectPromptValues(
variableFields(remote.variables),
promptValue
);
if (variableResult.cancelled) return null;
const headerResult = await collectPromptValues(
headerFields(remote.headers),
promptValue
);
if (headerResult.cancelled) return null;
return {
url: resolveTemplateUrl(remote.url, variableResult.values),
headers: Object.keys(headerResult.values).length > 0 ? headerResult.values : void 0
};
}
function resolveNonInteractivePackage(pkg) {
const env = {};
for (const field of packageVariableFields(pkg.environmentVariables)) {
if (field.isRequired) {
env[field.key] = field.placeholder;
}
}
const headers = {};
for (const field of headerFields(pkg.headers)) {
if (field.isRequired) {
headers[field.key] = field.placeholder;
}
}
const argv = buildPackageArgumentsArgvNonInteractive(
definitionsFromPackage(pkg),
buildPlaceholderValue("variable")
);
return {
env: Object.keys(env).length > 0 ? env : void 0,
headers: Object.keys(headers).length > 0 ? headers : void 0,
args: argv.length > 0 ? argv : void 0
};
}
async function resolveInteractivePackage(pkg) {
const promptPackageArg = async (info) => p.text({
message: `${info.label} ${info.isRequired ? "(required)" : "(optional)"}`,
placeholder: info.placeholder
});
const envResult = await collectPromptValues(
packageVariableFields(pkg.environmentVariables),
promptValue
);
if (envResult.cancelled) return null;
const headerResult = await collectPromptValues(
headerFields(pkg.headers),
promptValue
);
if (headerResult.cancelled) return null;
const argvResult = await buildPackageArgumentsArgvInteractive(
definitionsFromPackage(pkg),
promptPackageArg
);
if (argvResult.cancelled) return null;
return {
env: Object.keys(envResult.values).length > 0 ? envResult.values : void 0,
headers: Object.keys(headerResult.values).length > 0 ? headerResult.values : void 0,
args: argvResult.argv.length > 0 ? argvResult.argv : void 0
};
}
async function buildInstallPlanForEntry(entry, options) {
const remote = pickRemote(entry, options.preferredTransport);
const pkg = entry.package ?? null;
const hasRemote = remote !== null;
const hasPackage = pkg !== null;
if (!hasRemote && !hasPackage) {
return null;
}
let mode;
if (hasRemote && hasPackage) {
if (options.yes) {
mode = "remote";
} else {
const selected = await p.select({
message: `Install mode for ${entry.name}`,
initialValue: "remote",
options: [
{ value: "remote", label: "Remote", hint: "Recommended default" },
{
value: "package",
label: "Stdio package",
hint: "Local stdio package"
}
]
});
if (p.isCancel(selected)) return null;
mode = selected;
}
} else {
mode = hasRemote ? "remote" : "package";
}
if (mode === "package" && pkg) {
const resolved2 = options.yes ? resolveNonInteractivePackage(pkg) : await resolveInteractivePackage(pkg);
if (!resolved2) return null;
return {
target: formatPackageTarget(pkg),
serverName: resolveServerName(entry),
env: resolved2.env,
headers: resolved2.headers,
args: resolved2.args
};
}
if (!remote) return null;
const resolved = options.yes ? resolveNonInteractiveRemote(remote) : await resolveInteractiveRemote(remote);
if (!resolved) return null;
return {
target: resolved.url,
serverName: resolveServerName(entry),
transport: remoteToTransport(remote.type),
headers: resolved.headers
};
}
async function offerFallbackSearch(query, alreadyQueried) {
const defaults = getDefaultFindRegistries();
const queriedUrls = new Set(alreadyQueried.map((r) => r.url));
const candidates = defaults.filter((r) => !queriedUrls.has(r.url));
if (candidates.length === 0) {
return null;
}
let selectedRegistries;
if (candidates.length === 1) {
const candidate = candidates[0];
const confirmed = await p.confirm({
message: `Search "${candidate.label ?? candidate.url}" (${candidate.url}) instead?`
});
if (p.isCancel(confirmed) || !confirmed) {
return null;
}
selectedRegistries = [candidate];
} else {
const selected = await p.multiselect({
message: "Search other registries instead?",
options: candidates.map((r) => ({
value: r.url,
label: r.label ?? r.url
})),
required: true
});
if (p.isCancel(selected)) {
return null;
}
selectedRegistries = candidates.filter(
(r) => selected.includes(r.url)
);
}
const fallbackResult = await searchRegistry(query, selectedRegistries);
if (fallbackResult.failedRegistries.length > 0) {
for (const failure of fallbackResult.failedRegistries) {
p.log.error(formatRegistryFailure(failure));
}
}
if (fallbackResult.entries.length === 0) {
if (fallbackResult.failedRegistries.length === 0) {
p.log.warn(`No MCP servers found for "${query}"`);
}
return null;
}
return fallbackResult.entries;
}
var FIND_PAGE_SIZE = 15;
var LOAD_MORE_SENTINEL = "__load_more__";
async function selectEntryWithPagination(visibleEntries, message) {
let page = 0;
while (true) {
const start = page * FIND_PAGE_SIZE;
const slice = visibleEntries.slice(start, start + FIND_PAGE_SIZE);
const hasMore = visibleEntries.length > start + FIND_PAGE_SIZE;
const remaining = visibleEntries.length - (start + FIND_PAGE_SIZE);
const options = slice.map((entryOption) => ({
value: entryOption.name,
label: formatFindResultRow(entryOption),
hint: entryOption.description
}));
if (hasMore) {
options.push({
value: LOAD_MORE_SENTINEL,
label: `\u25BC Show more results (${remaining} remaining)`
});
}
const selected = await p.select({ message, options });
if (p.isCancel(selected)) return null;
if (selected === LOAD_MORE_SENTINEL) {
page++;
continue;
}
return visibleEntries.find((e) => e.name === selected) ?? null;
}
}
async function runFind(query, options) {
const registries = options.registries && options.registries.length > 0 ? options.registries : [
{
url: resolveOfficialRegistryServersUrl(),
label: "Official Anthropic registry"
}
];
const isBrowseMode = query.trim().length === 0;
const result = await searchRegistry(query, registries);
let entries = result.entries;
const { failedRegistries } = result;
if (failedRegistries.length > 0 && entries.length > 0) {
for (const failure of failedRegistries) {
p.log.warn(formatRegistryFailure(failure));
}
}
if (entries.length === 0 && failedRegistries.length > 0) {
for (const failure of failedRegistries) {
p.log.error(formatRegistryFailure(failure));
}
if (!options.yes && !isBrowseMode) {
const fallbackEntries = await offerFallbackSearch(query, registries);
if (fallbackEntries) {
entries = fallbackEntries;
}
}
}
if (entries.length === 0) {
if (failedRegistries.length === 0) {
p.log.warn(
isBrowseMode ? "No MCP servers found in the configured registries" : `No MCP servers found for "${query}"`
);
}
return null;
}
const visibleEntries = isBrowseMode ? filterSmitheryWhenAlternativesExist(entries) : filterSmitheryWhenAlternativesExist(rankRegistryEntries(query, entries));
const count = visibleEntries.length;
const message = isBrowseMode ? "Browse MCP servers" : `Found ${count} server${count === 1 ? "" : "s"} for "${query}" \u2014 pick one`;
const entry = options.yes ? visibleEntries[0] ?? null : await selectEntryWithPagination(visibleEntries, message);
if (!entry) {
return null;
}
return buildInstallPlanForEntry(entry, options);
}
// package.json
var package_default = {
name: "add-mcp",
version: "2.6.1",
description: "Add MCP servers to your favorite coding agents with a single command.",
author: "Andre Landgraf <andre@neon.tech>",
license: "Apache-2.0",
type: "module",
main: "./dist/lib.js",
types: "./dist/lib.d.ts",
exports: {
".": {
types: "./dist/lib.d.ts",
import: "./dist/lib.js"
},
"./package.json": "./package.json"
},
bin: {
"add-mcp": "dist/index.js"
},
files: [
"dist",
"README.md"
],
scripts: {
fmt: "prettier --write .",
build: "tsup src/index.ts src/lib.ts --format esm --dts --clean",
dev: "tsx src/index.ts",
test: "tsx tests/source-parser.test.ts && tsx tests/agents.test.ts && tsx tests/config.test.ts && tsx tests/installer.test.ts && tsx tests/find.test.ts && tsx tests/package-arguments.test.ts && tsx tests/template.test.ts && tsx tests/reader.test.ts && tsx tests/formats-remove.test.ts && tsx tests/formats-utils.test.ts && tsx tests/opencode-config.test.ts && tsx tests/lib.test.ts && tsx tests/e2e/install.test.ts && tsx tests/e2e/cli.test.ts",
"test:unit": "tsx tests/source-parser.test.ts && tsx tests/agents.test.ts && tsx tests/config.test.ts && tsx tests/installer.test.ts && tsx tests/find.test.ts && tsx tests/package-arguments.test.ts && tsx tests/template.test.ts && tsx tests/reader.test.ts && tsx tests/formats-remove.test.ts && tsx tests/formats-utils.test.ts && tsx tests/opencode-config.test.ts && tsx tests/lib.test.ts",
"registry:sort": "tsx scripts/sort-registry.ts",
"registry:sync": "node scripts/sync-integrations-sh.mjs",
"registry:verify": "tsx scripts/verify-registry.ts",
"test:e2e": "tsx tests/e2e/install.test.ts && tsx tests/e2e/cli.test.ts",
typecheck: "tsc --noEmit",
fallow: "fallow",
prepublishOnly: "npm run build"
},
keywords: [
"cli",
"mcp",
"model-context-protocol",
"ai-agents",
"antigravity",
"cline",
"claude-code",
"claude-desktop",
"codex",
"cursor",
"fx",
"github-copilot-cli",
"gemini-cli",
"factory-droid",
"goose",
"grok",
"grok-build",
"hermes",
"junie",
"kilo-code",
"kimi-code",
"kiro",
"kiro-cli",
"mastracode",
"mcporter",
"opencode",
"pi",
"vscode",
"windsurf",
"zed"
],
repository: {
type: "git",
url: "git+https://github.com/neon-solutions/add-mcp.git"
},
homepage: "https://github.com/neon-solutions/add-mcp#readme",
bugs: {
url: "https://github.com/neon-solutions/add-mcp/issues"
},
dependencies: {
"@clack/prompts": "^0.9.1",
"@iarna/toml": "^2.2.5",
chalk: "^5.4.1",
commander: "^13.1.0",
"jsonc-parser": "^3.3.1",
yaml: "^2.9.1"
},
devDependencies: {
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.10.0",
fallow: "^2.61.0",
"js-yaml": "^4.1.0",
prettier: "^3.8.1",
tsup: "^8.3.5",
tsx: "^4.19.2",
typescript: "^5.7.2",
zod: "^4.3.6"
},
engines: {
node: ">=18"
}
};
// src/index.ts
var version = package_default.version;
var RESET = "\x1B[0m";
var DIM = "\x1B[38;5;102m";
var TEXT = "\x1B[38;5;145m";
var LOGO_LINES = [
" \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557 ",
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557",
"\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2554\u2588\u2588\u2588\u2588\u2554\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D",
"\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u255A\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551\u255A\u2588\u2588\u2554\u255D\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u255D ",
"\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D \u2588\u2588\u2551 \u255A\u2550\u255D \u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551 ",
"\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D "
];
var GRAYS = [
"\x1B[38;5;250m",
"\x1B[38;5;248m",
"\x1B[38;5;245m",
"\x1B[38;5;243m",
"\x1B[38;5;240m",
"\x1B[38;5;238m"
];
function showLogo() {
console.log();
LOGO_LINES.forEach((line, i) => {
console.log(`${GRAYS[i]}${line}${RESET}`);
});
}
function shortenPath(fullPath) {
const home = homedir();
if (fullPath.startsWith(home)) {
return fullPath.replace(home, "~");
}
return fullPath;
}
function resolveAgentType(input) {
const lower = input.toLowerCase();
if (lower in agents) {
return lower;
}
if (lower in agentAliases) {
return agentAliases[lower];
}
return null;
}
async function ensureFindRegistriesConfigured() {
const configured = await getFindRegistries();
if (configured.length > 0) {
return configured;
}
const defaultRegistry = getDefaultFindRegistries()[0];
if (!defaultRegistry) {
throw new Error("No default find registry is configured");
}
const selectedRegistries = [defaultRegistry];
await saveFindRegistries(selectedRegistries);
p2.log.info(
`Using ${defaultRegistry.label ?? defaultRegistry.url}. Saved to ${shortenPath(getConfigPath())} - you can remove or update it any time.`
);
return selectedRegistries;
}
function extractOptions(raw) {
if (typeof raw.optsWithGlobals === "function") {
return raw.optsWithGlobals();
}
if (typeof raw.opts === "function") {
return raw.opts();
}
return raw;
}
function extractSubcommandOptionsFromArgv() {
const argv = process.argv.slice(2);
const result = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (!arg) continue;
if (arg === "-y" || arg === "--yes") {
result.yes = true;
continue;
}
if (arg === "-g" || arg === "--global") {
result.global = true;
continue;
}
if (arg === "--all") {
result.all = true;
continue;
}
if (arg === "--gitignore") {
result.gitignore = true;
continue;
}
if (arg === "--auto-approve") {
result.autoApprove = true;
continue;
}
if (arg === "--approve-tool") {
const tools = result.approveTool ? [...result.approveTool] : [];
let j = i + 1;
while (j < argv.length) {
const value = argv[j];
if (!value || value.startsWith("-")) break;
tools.push(value);
j += 1;
}
if (tools.length > 0) {
result.approveTool = tools;
}
i = j - 1;
continue;
}
if ((arg === "-h" || arg === "--header") && argv[i + 1]) {
const headers = result.header ? [...result.header] : [];
headers.push(argv[i + 1]);
result.header = headers;
i += 1;
continue;
}
if (arg === "--env" && argv[i + 1]) {
const env = result.env ? [...result.env] : [];
env.push(argv[i + 1]);
result.env = env;
i += 1;
continue;
}
if (arg === "--args" && argv[i + 1]) {
const args = result.args ? [...result.args] : [];
args.push(argv[i + 1]);
result.args = args;
i += 1;
continue;
}
if ((arg === "-n" || arg === "--name") && argv[i + 1]) {
result.name = argv[i + 1];
i += 1;
continue;
}
if (arg === "-a" || arg === "--agent") {
const agents2 = result.agent ? [...result.agent] : [];
let j = i + 1;
while (j < argv.length) {
const value = argv[j];
if (!value || value.startsWith("-")) break;
agents2.push(value);
j += 1;
}
if (agents2.length > 0) {
result.agent = agents2;
}
i = j - 1;
}
}
return result;
}
function inferFindPreferredTransport(options) {
if (!options.agent || options.agent.length === 0) {
return void 0;
}
const resolvedAgents = options.agent.map((value) => resolveAgentType(value)).filter((value) => value !== null);
if (resolvedAgents.length === 0) {
return void 0;
}
const supportsHttp = resolvedAgents.some(
(agent) => isTransportSupported(agent, "http")
);
if (supportsHttp) {
return void 0;
}
const supportsSse = resolvedAgents.every(
(agent) => isTransportSupported(agent, "sse")
);
return supportsSse ? "sse" : void 0;
}
function collect(value, previous) {
return previous.concat([value]);
}
function looksLikeEatenShellVar(invalidEntries, separator) {
for (const entry of invalidEntries) {
const separatorIndex = entry.indexOf(separator);
if (separatorIndex <= 0) continue;
const key = entry.slice(0, separatorIndex).trim();
const value = entry.slice(separatorIndex + 1).trim();
if (key && !value) return true;
}
return false;
}
function parseHeaders(values) {
const headers = {};
const invalid = [];
for (const entry of values) {
const separatorIndex = entry.indexOf(":");
if (separatorIndex <= 0) {
invalid.push(entry);
continue;
}
const key = entry.slice(0, separatorIndex).trim();
const value = entry.slice(separatorIndex + 1).trim();
if (!key || !value) {
invalid.push(entry);
continue;
}
headers[key] = value;
}
return { headers, invalid };
}
function parseEnv(values) {
const env = {};
const invalid = [];
for (const entry of values) {
const separatorIndex = entry.indexOf("=");
if (separatorIndex <= 0) {
invalid.push(entry);
continue;
}
const key = entry.slice(0, separatorIndex).trim();
const value = entry.slice(separatorIndex + 1).trim();
if (!key || !value) {
invalid.push(entry);
continue;
}
env[key] = value;
}
return { env, invalid };
}
function omitEmptyStringValues(record) {
return Object.fromEntries(
Object.entries(record).filter(
([, v]) => typeof v === "string" && v.trim().length > 0
)
);
}
program.name("add-mcp").description(
"Install MCP servers for coding agents (Claude Code, Cursor, VS Code, OpenCode, Codex, and more \u2014 run list-agents for the full list)"
).version(version).helpOption("--help", "display help for command").argument("[target]", "MCP server URL (remote) or package name (local stdio)").option(
"-g, --global",
"Install globally (user-level) instead of project-level"
).option("-a, --agent <agent>", "Specify agents to install to", collect, []).option(
"-n, --name <name>",
"Server name (auto-inferred from target if not provided)"
).option(
"-t, --transport <type>",
"Transport type for remote servers (http, sse)"
).option("--type <type>", "Alias for --transport").option(
"-h, --header <header>",
"HTTP header for remote servers (repeatable, 'Key: Value'). Placeholders ${VAR} prompt interactively when not using --yes. Use single quotes so your shell does not expand the ${VAR}.",
collect,
[]
).option(
"--env <env>",
"Environment variable for local stdio servers (repeatable, 'KEY=VALUE'). Placeholders ${VAR} prompt interactively when not using --yes. Use single quotes so your shell does not expand the ${VAR}.",
collect,
[]
).option(
"--args <arg>",
"Argument for local stdio servers (repeatable). Placeholders ${VAR} prompt interactively when not using --yes. Use single quotes so your shell does not expand the ${VAR}.",
collect,
[]
).option(
"--timeout <ms>",
"Request timeout in milliseconds for remote servers. Only applied to agents that support it (e.g. Claude Code, Gemini CLI, Grok Build); dropped with a warning elsewhere."
).option(
"--scopes <scopes>",
"OAuth scopes to request for remote servers (comma-separated). Only applied to agents that support it (e.g. Cursor, Gemini CLI); dropped with a warning elsewhere."
).option("--oauth-scopes <scopes>", "Alias for --scopes").option(
"--bearer-token-env <name>",
"Environment variable name whose value fx sends as a bearer token for remote servers. Written as bearer_token_env for fx; dropped with a warning elsewhere."
).option(
"--auto-approve",
"Auto-approve MCP tool calls for agents that support it (Codex, Claude Code). Dropped with a warning for other agents."
).option(
"--approve-tool <tool>",
"Tool name to auto-approve when --auto-approve is set (repeatable; defaults to all tools)",
collect,
[]
).option("-y, --yes", "Skip confirmation prompts").option("--all", "Install to all agents").option("--gitignore", "Add generated project config files to .gitignore").action(async (target, options) => {
await main(target, options);
});
program.command("list-agents").description("List all supported coding agents").action(() => {
listAgents();
});
async function runFindCommand(keyword, rawOptions) {
const options = {
...extractOptions(rawOptions),
...extractSubcommandOptionsFromArgv()
};
const query = (keyword ?? "").trim();
const registries = await ensureFindRegistriesConfigured();
if (!registries) {
p2.cancel("Find cancelled");
process.exit(0);
}
const installPlan = await runFind(query, {
yes: options.yes,
registries,
preferredTransport: inferFindPreferredTransport(options)
});
if (!installPlan) {
p2.cancel("Find cancelled");
process.exit(0);
}
const mergedOptions = {
...options,
name: options.name || installPlan.serverName,
transport: installPlan.transport,
header: installPlan.headers ? Object.entries(installPlan.headers).map(
([key, value]) => `${key}: ${value}`
) : options.header,
env: installPlan.env ? Object.entries(installPlan.env).map(([key, value]) => `${key}=${value}`) : options.env,
args: installPlan.args ?? options.args
};
await main(installPlan.target, mergedOptions);
}
program.command("find [keyword]").description(
"Find MCP servers from integrations.sh registry data (omit keyword to browse)"
).option(
"-g, --global",
"Install globally (user-level) instead of project-level"
).option("-a, --agent <agent>", "Specify agents to install to", collect, []).option(
"-n, --name <name>",
"Server name override (defaults to catalog entry name)"
).option("-y, --yes", "Skip confirmation prompts").option(
"--auto-approve",
"Auto-approve MCP tool calls for agents that support it (Codex, Claude Code)"
).option(
"--approve-tool <tool>",
"Tool name to auto-approve when --auto-approve is set (repeatable; defaults to all tools)",
collect,
[]
).option("--all", "Install to all agents").option("--gitignore", "Add generated project config files to .gitignore").action(
async (keyword, options) => {
await runFindCommand(keyword, options);
}
);
program.command("search [keyword]").description("Alias for find").option(
"-g, --global",
"Install globally (user-level) instead of project-level"
).option("-a, --agent <agent>", "Specify agents to install to", collect, []).option(
"-n, --name <name>",
"Server name override (defaults to catalog entry name)"
).option("-y, --yes", "Skip confirmation prompts").option(
"--auto-approve",
"Auto-approve MCP tool calls for agents that support it (Codex, Claude Code)"
).option(
"--approve-tool <tool>",
"Tool name to auto-approve when --auto-approve is set (repeatable; defaults to all tools)",
collect,
[]
).option("--all", "Install to all agents").option("--gitignore", "Add generated project config files to .gitignore").action(
async (keyword, options) => {
await runFindCommand(keyword, options);
}
);
program.command("list").description("List installed MCP servers across detected agents").option("-g, --global", "List global configs instead of project-level").option("-a, --agent <agent>", "Filter to specific agent(s)", collect, []).action(async (rawOptions) => {
const options = {
...extractOptions(rawOptions),
...extractSubcommandOptionsFromArgv()
};
await runListCommand(options);
});
program.command("remove <query>").description("Remove an MCP server from agent configurations").option("-g, --global", "Remove from global configs instead of project-level").option("-a, --agent <agent>", "Filter to specific agent(s)", collect, []).option("-y, --yes", "Remove all matches without prompting").action(
async (query, rawOptions) => {
const options = {
...extractOptions(rawOptions),
...extractSubcommandOptionsFromArgv()
};
await runRemoveCommand(query, options);
}
);
program.command("sync").description(
"Synchronize server names and installations across all detected agents"
).option("-g, --global", "Sync global configs instead of project-level").option("-y, --yes", "Skip confirmation prompts").action(async (rawOptions) => {
const options = {
...extractOptions(rawOptions),
...extractSubcommandOptionsFromArgv()
};
await runSyncCommand(options);
});
program.command("unify").description("Alias for sync").option("-g, --global", "Sync global configs instead of project-level").option("-y, --yes", "Skip confirmation prompts").action(async (rawOptions) => {
const options = {
...extractOptions(rawOptions),
...extractSubcommandOptionsFromArgv()
};
await runSyncCommand(options);
});
program.parse();
async function runListCommand(options) {
showLogo();
console.log();
const explicitAgents = resolveAgentFlags(options.agent);
const agentServersList = await listInstalledServers({
global: options.global,
agents: explicitAgents.length > 0 ? explicitAgents : void 0
});
if (agentServersList.length === 0) {
const hint = options.global ? "No agents detected globally. Use -a to target a specific agent." : "No agents detected in this project. Use -g for global or -a to target a specific agent.";
p2.log.info(hint);
console.log();
return;
}
let hadError = false;
for (const agentServers of agentServersList) {
if (agentServers.error) {
p2.log.error(`${agentServers.displayName}: ${agentServers.error}`);
hadError = true;
continue;
}
if (!agentServers.detected) {
console.log(
`${TEXT}${agentServers.displayName}:${RESET} ${DIM}not detected${RESET}`
);
continue;
}
if (agentServers.servers.length === 0) {
console.log(
`${TEXT}${agentServers.displayName}:${RESET} ${DIM}no servers configured${RESET}`
);
continue;
}
console.log(`${TEXT}${agentServers.displayName}:${RESET}`);
for (const server of agentServers.servers) {
const identityHint = server.identity ? ` ${DIM}(${server.identity})${RESET}` : "";
console.log(
` ${DIM}-${RESET} ${TEXT}${server.serverName}${RESET}${identityHint}`
);
}
}
console.log();
if (hadError) {
process.exitCode = 1;
}
}
async function runRemoveCommand(query, options) {
showLogo();
console.log();
const explicitAgents = resolveAgentFlags(options.agent);
const agentServersList = await listInstalledServers({
global: options.global,
agents: explicitAgents.length > 0 ? explicitAgents : void 0
});
const hadReadError = reportAgentReadErrors(agentServersList);
const matches = findMatchingServers(
agentsWithReadableConfigs(agentServersList),
query
);
if (matches.length === 0) {
if (!hadReadError) {
p2.log.info(`No matching servers found for '${query}'`);
}
if (hadReadError) {
process.exitCode = 1;
}
console.log();
return;
}
if (matches.some(
(server) => (server.agentType === "claude-code" || server.agentType === "github-copilot-cli") && server.configPath.endsWith(".mcp.json")
)) {
p2.log.warn(
"`.mcp.json` is shared by Claude Code and GitHub Copilot CLI. Removing a server from either removes it for both."
);
}
const matchOptions = matches.map((m, i) => ({
value: i,
label: `${m.serverName} (${agents[m.agentType].displayName})`,
hint: m.identity || m.configPath
}));
let selectedIndices;
if (options.yes) {
selectedIndices = matches.map((_, i) => i);
p2.log.info(
`Removing ${matches.length} server${matches.length !== 1 ? "s" : ""} matching '${query}'`
);
} else {
const selected = await p2.multiselect({
message: `Select servers to remove (${matches.length} match${matches.length !== 1 ? "es" : ""} found)`,
options: matchOptions,
required: false,
initialValues: matches.map((_, i) => i)
});
if (p2.isCancel(selected)) {
p2.log.info("No changes made");
console.log();
return;
}
selectedIndices = selected;
if (selectedIndices.length === 0) {
p2.log.info("No changes made");
console.log();
return;
}
}
let removedCount = 0;
const affectedAgents = /* @__PURE__ */ new Set();
let mutationFailed = false;
for (const idx of selectedIndices) {
const server = matches[idx];
const agent = agents[server.agentType];
try {
if (server.agentType === "opencode") {
removeOpenCodeServer(server.configPath, server.serverName);
} else {
removeServerFromConfig(
server.configPath,
agent.format,
getConfigKeyForServer(server),
server.serverName
);
rewriteCopilotCliConfig(server.agentType, server.configPath);
}
removedCount++;
affectedAgents.add(agent.displayName);
} catch (error) {
mutationFailed = true;
p2.log.error(
`Failed to remove ${server.serverName} from ${agent.displayName}: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
if (removedCount > 0) {
p2.log.success(
`Removed ${removedCount} server${removedCount !== 1 ? "s" : ""} from ${affectedAgents.size} agent${affectedAgents.size !== 1 ? "s" : ""}`
);
}
if (hadReadError || mutationFailed) {
process.exitCode = 1;
}
console.log();
}
function failedConfigPaths(agentServersList) {
return new Set(
agentServersList.filter((agentServers) => agentServers.error).map((agentServers) => agentServers.configPath)
);
}
function agentsWithReadableConfigs(agentServersList) {
const failedPaths = failedConfigPaths(agentServersList);
return agentServersList.filter(
(agentServers) => !agentServers.error && !failedPaths.has(agentServers.configPath)
);
}
function reportAgentReadErrors(agentServersList) {
let hadError = false;
for (const agentServers of agentServersList) {
if (agentServers.error) {
p2.log.error(`${agentServers.displayName}: ${agentServers.error}`);
hadError = true;
}
}
return hadError;
}
function getConfigKeyForServer(server) {
return server.configKey;
}
function deepEqual(a, b) {
if (a === b) return true;
if (a === null || b === null) return false;
if (a === void 0 || b === void 0) return a === b;
if (typeof a !== typeof b) return false;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
return a.every((val, i) => deepEqual(val, b[i]));
}
if (typeof a === "object" && typeof b === "object") {
const aObj = a;
const bObj = b;
const aKeys = Object.keys(aObj).sort();
const bKeys = Object.keys(bObj).sort();
if (!deepEqual(aKeys, bKeys)) return false;
return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
}
return false;
}
function pickCanonicalName(entries) {
const nameFreq = /* @__PURE__ */ new Map();
for (const entry of entries) {
nameFreq.set(entry.serverName, (nameFreq.get(entry.serverName) ?? 0) + 1);
}
const names = [...nameFreq.entries()];
names.sort(([nameA, freqA], [nameB, freqB]) => {
if (nameA.length !== nameB.length) return nameA.length - nameB.length;
if (freqA !== freqB) return freqB - freqA;
return nameA.localeCompare(nameB);
});
return names[0][0];
}
function extractConflictFields(config) {
return {
headers: config.headers ?? config.http_headers ?? null,
env: config.env ?? config.envs ?? config.environment ?? null,
args: normalizeStoredCommand(config).args
};
}
function buildSyncGroups(agentServersList) {
const byIdentity = /* @__PURE__ */ new Map();
for (const agentServers of agentServersList) {
for (const server of agentServers.servers) {
if (!server.identity) continue;
const existing = byIdentity.get(server.identity) ?? [];
existing.push(server);
byIdentity.set(server.identity, existing);
}
}
const groups = [];
for (const [identity, entries] of byIdentity) {
const fieldSets = entries.map((e) => extractConflictFields(e.config));
const reference = fieldSets[0];
let hasConflict = false;
let conflictReason;
for (let i = 1; i < fieldSets.length; i++) {
const other = fieldSets[i];
if (!deepEqual(reference.headers, other.headers)) {
hasConflict = true;
conflictReason = `headers differ between ${agents[entries[0].agentType].displayName} and ${agents[entries[i].agentType].displayName}`;
break;
}
if (!deepEqual(reference.env, other.env)) {
hasConflict = true;
conflictReason = `env differs between ${agents[entries[0].agentType].displayName} and ${agents[entries[i].agentType].displayName}`;
break;
}
if (!deepEqual(reference.args, other.args)) {
hasConflict = true;
conflictReason = `args differ between ${agents[entries[0].agentType].displayName} and ${agents[entries[i].agentType].displayName}`;
break;
}
}
groups.push({
identity,
entries,
canonicalName: pickCanonicalName(entries),
canonicalConfig: entries[0].config,
hasConflict,
conflictReason
});
}
return groups;
}
async function runSyncCommand(options) {
showLogo();
console.log();
const agentServersList = await listInstalledServers({
global: options.global
});
const hadReadError = reportAgentReadErrors(agentServersList);
if (hadReadError) {
process.exitCode = 1;
}
const readable = agentsWithReadableConfigs(agentServersList);
if (hadReadError && readable.length < 2) {
console.log();
return;
}
if (readable.length < 2) {
p2.log.info("Need at least 2 detected agents to sync");
console.log();
return;
}
const groups = buildSyncGroups(readable);
const detectedAgentTypes = new Set(readable.map((a) => a.agentType));
const renames = [];
const additions = [];
const skipped = [];
for (const group of groups) {
if (group.hasConflict) {
skipped.push(group);
continue;
}
const presentAgents = new Set(group.entries.map((e) => e.agentType));
for (const entry of group.entries) {
if (entry.serverName !== group.canonicalName) {
renames.push({
group,
agentType: entry.agentType,
oldName: entry.serverName
});
}
}
for (const agentType of detectedAgentTypes) {
if (!presentAgents.has(agentType)) {
additions.push({ group, agentType });
}
}
}
const scope = options.global ? "global" : "local";
const layoutError = scope === "local" && detectedAgentTypes.has("github-copilot-cli") && detectedAgentTypes.has("claude-code") ? claudeCopilotGithubShadowError(process.cwd()) : null;
const isBlocked = (agentType) => Boolean(layoutError) && (agentType === "github-copilot-cli" || agentType === "claude-code");
const actionRenames = renames.filter((r) => !isBlocked(r.agentType));
const actionAdditions = additions.filter((a) => !isBlocked(a.agentType));
const blockedRenames = renames.filter((r) => isBlocked(r.agentType));
const blockedAdditions = additions.filter((a) => isBlocked(a.agentType));
if (actionRenames.length === 0 && actionAdditions.length === 0 && blockedRenames.length === 0 && blockedAdditions.length === 0 && skipped.length === 0) {
if (!hadReadError) {
p2.log.info("All servers are already in sync");
}
console.log();
return;
}
const planLines = [];
if (actionRenames.length > 0) {
planLines.push(chalk.cyan("Renames:"));
for (const r of actionRenames) {
planLines.push(
` ${agents[r.agentType].displayName}: ${r.oldName} \u2192 ${r.group.canonicalName}`
);
}
}
if (actionAdditions.length > 0) {
planLines.push(chalk.cyan("Additions:"));
for (const a of actionAdditions) {
planLines.push(
` ${agents[a.agentType].displayName}: + ${a.group.canonicalName} (${a.group.identity})`
);
}
}
if (blockedRenames.length > 0 || blockedAdditions.length > 0) {
planLines.push(chalk.yellow("Blocked:"));
for (const r of blockedRenames) {
planLines.push(
` ${agents[r.agentType].displayName}: ${r.oldName} \u2192 ${r.group.canonicalName}`
);
}
for (const a of blockedAdditions) {
planLines.push(
` ${agents[a.agentType].displayName}: + ${a.group.canonicalName} (${a.group.identity})`
);
}
if (layoutError) {
planLines.push(` ${layoutError}`);
}
}
if (skipped.length > 0) {
planLines.push(chalk.yellow("Skipped (conflicts):"));
for (const s of skipped) {
planLines.push(` ${s.identity}: ${s.conflictReason}`);
}
}
if (actionRenames.length === 0 && actionAdditions.length === 0) {
p2.note(planLines.join("\n"), "Sync Plan");
if (layoutError) {
p2.log.error(layoutError);
process.exitCode = 1;
} else if (!hadReadError) {
p2.log.info(
"All servers are already in sync (some skipped due to conflicts)"
);
}
console.log();
return;
}
p2.note(planLines.join("\n"), "Sync Plan");
if (!options.yes) {
const confirmed = await p2.confirm({
message: "Proceed with sync?"
});
if (p2.isCancel(confirmed) || !confirmed) {
p2.log.info("No changes made");
console.log();
return;
}
}
let changeCount = 0;
let mutationFailed = false;
const renamed = /* @__PURE__ */ new Set();
for (const rename of actionRenames) {
const { group, agentType, oldName } = rename;
if (agentType === "opencode") {
const source = group.entries.find(
(item) => item.agentType === "opencode" && item.serverName === oldName
);
if (!source) {
mutationFailed = true;
p2.log.error(
`Failed to write ${group.canonicalName} to ${agents.opencode.displayName}: missing source entry ${oldName}`
);
continue;
}
try {
relocateOpenCodeServer(source.configPath, oldName, group.canonicalName);
changeCount++;
renamed.add(rename);
} catch (error) {
mutationFailed = true;
p2.log.error(
`Failed to write ${group.canonicalName} to ${agents.opencode.displayName}: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
continue;
}
const result = installServerForAgent(
group.canonicalName,
buildServerConfigFromStored(group.canonicalConfig),
agentType,
{ local: scope === "local" }
);
if (result.success) {
changeCount++;
renamed.add(rename);
} else {
mutationFailed = true;
p2.log.error(
`Failed to write ${group.canonicalName} to ${agents[agentType].displayName}: ${result.error}`
);
}
}
for (const addition of actionAdditions) {
const { group, agentType } = addition;
if (agentType === "opencode") {
const opencode = readable.find((item) => item.agentType === "opencode");
if (opencode) {
const dest = listOpenCodeServers(opencode.configPath).find(
(entry) => entry.serverName === group.canonicalName
);
if (dest) {
if (extractServerIdentity(dest.config) === group.identity) {
continue;
}
mutationFailed = true;
p2.log.error(
`Failed to add ${group.canonicalName} to ${agents.opencode.displayName}: already has a different server named "${group.canonicalName}"`
);
continue;
}
}
}
const result = installServerForAgent(
group.canonicalName,
buildServerConfigFromStored(group.canonicalConfig),
agentType,
{ local: scope === "local" }
);
if (result.success) {
changeCount++;
} else {
mutationFailed = true;
p2.log.error(
`Failed to add ${group.canonicalName} to ${agents[agentType].displayName}: ${result.error}`
);
}
}
for (const rename of actionRenames) {
if (!renamed.has(rename)) {
continue;
}
const { group, agentType, oldName } = rename;
const agentConfig = agents[agentType];
const entry = group.entries.find((e) => e.agentType === agentType);
if (!entry) continue;
try {
if (agentType === "opencode") {
continue;
}
removeServerFromConfig(
entry.configPath,
agentConfig.format,
getConfigKey(agentConfig, { local: scope === "local" }),
oldName
);
rewriteCopilotCliConfig(agentType, entry.configPath);
} catch (error) {
mutationFailed = true;
p2.log.error(
`Failed to remove old alias ${oldName} from ${agentConfig.displayName}: ${error instanceof Error ? error.message : "Unknown error"}`
);
}
}
if (layoutError || mutationFailed) {
if (layoutError) {
p2.log.error(layoutError);
}
process.exitCode = 1;
}
p2.log.success(
`Synced ${changeCount} server${changeCount !== 1 ? "s" : ""} across ${detectedAgentTypes.size} agent${detectedAgentTypes.size !== 1 ? "s" : ""}`
);
console.log();
}
var TRANSPORT_ALIASES = {
http: "http",
sse: "sse",
streamable_http: "http",
streamableHttp: "http",
"streamable-http": "http",
remote: "http"
};
function normalizeTransportType(raw) {
if (typeof raw === "string" && raw in TRANSPORT_ALIASES) {
return TRANSPORT_ALIASES[raw];
}
return "http";
}
function buildServerConfigFromStored(config) {
const httpUrl = typeof config.httpUrl === "string" && config.httpUrl.length > 0 ? config.httpUrl : void 0;
const url = httpUrl ?? (typeof config.url === "string" ? config.url : typeof config.uri === "string" ? config.uri : typeof config.serverUrl === "string" ? config.serverUrl : void 0);
if (url) {
const result2 = {
type: httpUrl ? "http" : normalizeTransportType(config.type ?? config.transport),
url
};
const headers = config.headers && typeof config.headers === "object" ? config.headers : config.http_headers && typeof config.http_headers === "object" ? config.http_headers : void 0;
if (headers && Object.keys(headers).length > 0) {
result2.headers = headers;
}
return result2;
}
const { command, args } = normalizeStoredCommand(config);
const env = config.env && typeof config.env === "object" ? config.env : config.envs && typeof config.envs === "object" ? config.envs : config.environment && typeof config.environment === "object" ? config.environment : void 0;
const result = {};
if (command) result.command = command;
if (args.length > 0) result.args = args;
if (env && Object.keys(env).length > 0) result.env = env;
return result;
}
function resolveAgentFlags(agentFlags) {
if (!agentFlags || agentFlags.length === 0) return [];
const resolved = [];
const invalid = [];
for (const input of agentFlags) {
const agentType = resolveAgentType(input);
if (agentType) {
resolved.push(agentType);
} else {
invalid.push(input);
}
}
if (invalid.length > 0) {
p2.log.error(`Invalid agents: ${invalid.join(", ")}`);
p2.log.info(`Valid agents: ${getAgentTypes().join(", ")}`);
process.exit(1);
}
return resolved;
}
function listAgents() {
showLogo();
console.log();
console.log(`${DIM}Supported agents:${RESET}`);
console.log();
const allAgentTypes = getAgentTypes();
const aliasesByAgent = {};
for (const [alias, target] of Object.entries(agentAliases)) {
if (!aliasesByAgent[target]) {
aliasesByAgent[target] = [];
}
aliasesByAgent[target].push(alias);
}
const nameColWidth = Math.max(
"Argument".length,
...allAgentTypes.map((t) => t.length)
);
const clientColWidth = Math.max(
"MCP Client".length,
...allAgentTypes.map((t) => agents[t].displayName.length)
);
const aliasColWidth = Math.max(
"Aliases".length,
...allAgentTypes.map(
(t) => (aliasesByAgent[t] ? aliasesByAgent[t].join(", ") : "").length
)
);
const pad = (str, width) => str + " ".repeat(Math.max(0, width - str.length));
const header = ` ${pad("Argument", nameColWidth)} ${pad("MCP Client", clientColWidth)} ${pad("Aliases", aliasColWidth)} Local Global`;
const separator = ` ${"-".repeat(nameColWidth)} ${"-".repeat(clientColWidth)} ${"-".repeat(aliasColWidth)} ----- ------`;
console.log(`${DIM}${header}${RESET}`);
console.log(`${DIM}${separator}${RESET}`);
for (const agentType of allAgentTypes) {
const agent = agents[agentType];
const hasLocal = supportsProjectConfig(agentType);
const localMark = hasLocal ? " \u2713 " : " - ";
const globalMark = " \u2713 ";
const aliasStr = aliasesByAgent[agentType] ? aliasesByAgent[agentType].join(", ") : "";
console.log(
` ${TEXT}${pad(agentType, nameColWidth)}${RESET} ${DIM}${pad(agent.displayName, clientColWidth)}${RESET} ${DIM}${pad(aliasStr, aliasColWidth)}${RESET} ${TEXT}${localMark}${RESET} ${TEXT}${globalMark}${RESET}`
);
}
console.log();
}
async function main(target, options) {
showLogo();
if (!target) {
console.log();
console.log(`${DIM}Add MCP servers to your favorite coding agents${RESET}`);
console.log();
console.log(
` ${DIM}$${RESET} ${TEXT}npx add-mcp ${DIM}<url>${RESET} ${DIM}Install remote MCP server${RESET}`
);
console.log(
` ${DIM}$${RESET} ${TEXT}npx add-mcp ${DIM}<package>${RESET} ${DIM}Install npm package${RESET}`
);
console.log(
` ${DIM}$${RESET} ${TEXT}npx add-mcp ${DIM}<url> ${TEXT}-g${RESET} ${DIM}Install globally${RESET}`
);
console.log(
` ${DIM}$${RESET} ${TEXT}npx add-mcp ${DIM}<url> ${TEXT}-a cursor${RESET} ${DIM}Install to specific agent${RESET}`
);
console.log(
` ${DIM}$${RESET} ${TEXT}npx add-mcp find ${DIM}<keyword>${RESET} ${DIM}Search integrations.sh MCP servers${RESET}`
);
console.log();
console.log(
`${DIM}Supports:${RESET} Claude Code, Cursor, VS Code, OpenCode, and more`
);
console.log();
console.log(
`${DIM}Learn more at${RESET} ${TEXT}https://github.com/neondatabase/add-mcp${RESET}`
);
console.log();
process.exit(0);
}
console.log();
const spinner2 = p2.spinner();
spinner2.start("Parsing source...");
const parsed = parseSource(target);
const isRemote = isRemoteSource(parsed);
const sourceType = isRemote ? "remote" : "local";
spinner2.stop(`Source: ${chalk.cyan(parsed.value)} (${sourceType})`);
const headerValues = options.header ?? [];
const headerResult = parseHeaders(headerValues);
if (headerResult.invalid.length > 0) {
const hint = looksLikeEatenShellVar(headerResult.invalid, ":") ? " (looks like your shell expanded a ${VAR} to an empty string; use single quotes: --header 'Key: ${VAR}' to pass the template literally)" : "";
p2.log.error(
`Invalid --header value(s): ${headerResult.invalid.join(", ")}. Use 'Key: Value' format.${hint}`
);
process.exit(1);
}
const headerKeys = Object.keys(headerResult.headers);
const hasHeaderValues = headerKeys.length > 0;
if (hasHeaderValues && !isRemote) {
p2.log.warn("--header is only used for remote URLs, ignoring");
}
const envValues = options.env ?? [];
const envResult = parseEnv(envValues);
if (envResult.invalid.length > 0) {
const hint = looksLikeEatenShellVar(envResult.invalid, "=") ? " (looks like your shell expanded a ${VAR} to an empty string; use single quotes: --env 'KEY=${VAR}' to pass the template literally)" : "";
p2.log.error(
`Invalid --env value(s): ${envResult.invalid.join(", ")}. Use 'KEY=VALUE' format.${hint}`
);
process.exit(1);
}
const envKeys = Object.keys(envResult.env);
const hasEnvValues = envKeys.length > 0;
if (hasEnvValues && isRemote) {
p2.log.warn(
"--env is only used for local/package/command installs, ignoring"
);
}
const argsValues = options.args ?? [];
const hasArgsValues = argsValues.length > 0;
if (hasArgsValues && isRemote) {
p2.log.warn(
"--args is only used for local/package/command installs, ignoring"
);
}
const promptTemplateVar = (varName) => p2.text({
message: `Enter value for ${varName}`,
placeholder: `<${varName}>`
});
let resolvedArgs = [...argsValues];
if (!options.yes && hasHeaderValues && hasTemplateVars(headerResult.headers)) {
const result = await resolveRecordTemplates(
headerResult.headers,
promptTemplateVar
);
if (result.cancelled) {
p2.cancel("Cancelled");
process.exit(0);
}
for (const [key, value] of Object.entries(result.resolved)) {
headerResult.headers[key] = value;
}
}
if (!options.yes && hasEnvValues && hasTemplateVars(envResult.env)) {
const result = await resolveRecordTemplates(
envResult.env,
promptTemplateVar
);
if (result.cancelled) {
p2.cancel("Cancelled");
process.exit(0);
}
for (const [key, value] of Object.entries(result.resolved)) {
envResult.env[key] = value;
}
}
if (!options.yes && hasArgsValues && hasTemplateVars(resolvedArgs)) {
const result = await resolveArrayTemplates(resolvedArgs, promptTemplateVar);
if (result.cancelled) {
p2.cancel("Cancelled");
process.exit(0);
}
resolvedArgs = result.resolved;
}
const headersForConfig = isRemote && hasHeaderValues ? omitEmptyStringValues(headerResult.headers) : void 0;
const envForConfig = !isRemote && hasEnvValues ? omitEmptyStringValues(envResult.env) : void 0;
const argsForConfig = !isRemote && hasArgsValues ? resolvedArgs.filter((a) => a.trim().length > 0) : void 0;
const serverName = options.name || parsed.inferredName;
p2.log.info(`Server name: ${chalk.cyan(serverName)}`);
const transportValue = options.transport || options.type;
let resolvedTransport;
if (transportValue) {
const validTransports = ["http", "sse"];
if (!validTransports.includes(transportValue)) {
p2.log.error(
`Invalid transport: ${transportValue}. Valid options: ${validTransports.join(", ")}`
);
process.exit(1);
}
resolvedTransport = transportValue;
if (!isRemoteSource(parsed)) {
p2.log.warn("--transport is only used for remote URLs, ignoring");
}
}
let resolvedTimeout;
if (options.timeout !== void 0) {
const parsedTimeout = Number(options.timeout);
if (!Number.isInteger(parsedTimeout) || parsedTimeout <= 0) {
p2.log.error(
`Invalid --timeout value: ${options.timeout}. Provide a positive integer (milliseconds).`
);
process.exit(1);
}
if (isRemote) {
resolvedTimeout = parsedTimeout;
} else {
p2.log.warn("--timeout is only used for remote URLs, ignoring");
}
}
const scopesValue = options.scopes ?? options.oauthScopes;
let resolvedScopes;
if (scopesValue !== void 0) {
const parsedScopes = scopesValue.split(",").map((scope) => scope.trim()).filter((scope) => scope.length > 0);
if (parsedScopes.length === 0) {
p2.log.error(
`Invalid --scopes value: ${scopesValue}. Provide one or more comma-separated scopes.`
);
process.exit(1);
}
if (isRemote) {
resolvedScopes = parsedScopes;
} else {
p2.log.warn("--scopes is only used for remote URLs, ignoring");
}
}
const approveTools = [...new Set(options.approveTool ?? [])];
const autoApproveTools = options.autoApprove || approveTools.length > 0 ? approveTools : void 0;
let resolvedBearerTokenEnv;
if (options.bearerTokenEnv !== void 0) {
const name = options.bearerTokenEnv.trim();
if (name.length === 0) {
p2.log.error("Invalid --bearer-token-env. The name cannot be empty.");
process.exit(1);
}
if (!isBearerTokenEnvName(name)) {
p2.log.error(
`--bearer-token-env takes an environment variable name ([A-Za-z_][A-Za-z0-9_]*). "${name}" is not a valid name.`
);
process.exit(1);
}
if (isRemote) {
resolvedBearerTokenEnv = name;
} else {
p2.log.warn("--bearer-token-env is only used for remote URLs, ignoring");
}
}
const serverConfig = buildServerConfig(parsed, {
transport: resolvedTransport,
headers: headersForConfig && Object.keys(headersForConfig).length > 0 ? headersForConfig : void 0,
env: envForConfig && Object.keys(envForConfig).length > 0 ? envForConfig : void 0,
args: argsForConfig && argsForConfig.length > 0 ? argsForConfig : void 0,
timeout: resolvedTimeout,
oauthScopes: resolvedScopes,
autoApproveTools,
bearerTokenEnv: resolvedBearerTokenEnv
});
let targetAgents;
const allAgentTypes = getAgentTypes();
let agentRouting = /* @__PURE__ */ new Map();
if (options.agent && options.agent.length > 0) {
const resolved = [];
const invalid = [];
for (const input of options.agent) {
const agentType = resolveAgentType(input);
if (agentType) {
resolved.push(agentType);
} else {
invalid.push(input);
}
}
if (invalid.length > 0) {
p2.log.error(`Invalid agents: ${invalid.join(", ")}`);
p2.log.info(`Valid agents: ${allAgentTypes.join(", ")}`);
process.exit(1);
}
targetAgents = resolved;
} else if (options.all) {
targetAgents = allAgentTypes;
p2.log.info(`Installing to all ${targetAgents.length} agents`);
} else {
spinner2.start("Detecting agents...");
let detectedAgents;
if (options.global) {
detectedAgents = await detectGlobalAgents();
for (const agent of detectedAgents) {
agentRouting.set(agent, "global");
}
} else {
const projectAgents = detectProjectAgents();
detectedAgents = projectAgents;
for (const agent of projectAgents) {
agentRouting.set(agent, "local");
}
}
spinner2.stop(
`Detected ${detectedAgents.length} agent${detectedAgents.length !== 1 ? "s" : ""}`
);
if (detectedAgents.length === 0) {
if (options.yes) {
if (options.global) {
targetAgents = allAgentTypes;
for (const agent of targetAgents) {
agentRouting.set(agent, "global");
}
p2.log.info(
`Installing to ${targetAgents.length} agents globally (none detected)`
);
} else {
targetAgents = getProjectCapableAgents();
for (const agent of targetAgents) {
agentRouting.set(agent, "local");
}
p2.log.info(
`Installing to ${targetAgents.length} project-capable agents (none detected)`
);
}
} else {
const availableAgents = allAgentTypes;
p2.log.warn(
options.global ? "No coding agents detected." : "No agents detected in this project."
);
const selected = await selectAgentsInteractive(availableAgents, {
global: options.global
});
if (p2.isCancel(selected)) {
p2.cancel("Installation cancelled");
process.exit(0);
}
targetAgents = selected;
}
} else if (options.yes) {
targetAgents = detectedAgents;
const agentNames = detectedAgents.map((a) => chalk.cyan(agents[a].displayName)).join(", ");
p2.log.info(`Installing to: ${agentNames}`);
} else {
const availableAgents = allAgentTypes;
let lastSelected;
try {
lastSelected = await getLastSelectedAgents();
} catch {
}
const { choices: agentChoices, initialValues } = buildAgentSelectionChoices({
availableAgents,
detectedAgents,
agentRouting,
lastSelected
});
const selected = await p2.multiselect({
message: "Select agents to install to",
options: agentChoices,
required: true,
initialValues
});
if (p2.isCancel(selected)) {
p2.cancel("Installation cancelled");
process.exit(0);
}
targetAgents = selected;
}
}
const requiredTransport = isRemoteSource(parsed) ? resolvedTransport ?? "http" : "stdio";
const unsupportedAgents = targetAgents.filter(
(a) => !isTransportSupported(a, requiredTransport)
);
if (unsupportedAgents.length > 0) {
const unsupportedNames = unsupportedAgents.map((a) => agents[a].displayName).join(", ");
const hints = unsupportedAgents.map((a) => agents[a].unsupportedTransportMessage).filter(Boolean);
if (options.all) {
p2.log.warn(
`Skipping agents that don't support ${requiredTransport} transport: ${unsupportedNames}`
);
for (const hint of hints) {
p2.log.info(hint);
}
targetAgents = targetAgents.filter(
(a) => isTransportSupported(a, requiredTransport)
);
if (targetAgents.length === 0) {
p2.log.error("No agents support this transport type");
process.exit(1);
}
} else {
p2.log.error(
`The following agents don't support ${requiredTransport} transport: ${unsupportedNames}`
);
for (const hint of hints) {
p2.log.info(hint);
}
process.exit(1);
}
}
if (options.global) {
agentRouting = /* @__PURE__ */ new Map();
for (const agent of targetAgents) {
agentRouting.set(agent, "global");
}
} else {
const commonScopes = getCommonInstallScopes(targetAgents);
if (commonScopes.length === 0) {
p2.log.error("No agents selected");
process.exit(1);
}
let installScope = commonScopes[0];
if (commonScopes.length > 1 && !options.yes) {
const scope = await p2.select({
message: "Installation scope",
options: [
{
value: "local",
label: "Project",
hint: "Install in current directory (committed with your project)"
},
{
value: "global",
label: "Global",
hint: "Install in home directory (available across all projects)"
}
]
});
if (p2.isCancel(scope)) {
p2.cancel("Installation cancelled");
process.exit(0);
}
installScope = scope;
} else if (installScope === "global") {
p2.log.info("Selected agents require global installation");
}
agentRouting = /* @__PURE__ */ new Map();
for (const agent of targetAgents) {
agentRouting.set(agent, installScope);
}
}
const summaryLines = [];
summaryLines.push(`${chalk.cyan("Server:")} ${serverName}`);
summaryLines.push(`${chalk.cyan("Type:")} ${sourceType}`);
if (autoApproveTools) {
summaryLines.push(
`${chalk.cyan("Auto-approve:")} ${autoApproveTools.length === 0 ? "All tools" : autoApproveTools.join(", ")}`
);
}
const localAgents = targetAgents.filter(
(a) => agentRouting.get(a) === "local"
);
const globalAgents = targetAgents.filter(
(a) => agentRouting.get(a) === "global"
);
if (localAgents.length > 0) {
summaryLines.push(`${chalk.cyan("Scope:")} Project`);
summaryLines.push(
`${chalk.cyan("Agents:")} ${localAgents.map((a) => agents[a].displayName).join(", ")}`
);
} else {
summaryLines.push(`${chalk.cyan("Scope:")} Global`);
summaryLines.push(
`${chalk.cyan("Agents:")} ${globalAgents.map((a) => agents[a].displayName).join(", ")}`
);
}
console.log();
p2.note(summaryLines.join("\n"), "Installation Summary");
if (!options.yes) {
const confirmed = await p2.confirm({
message: "Proceed with installation?"
});
if (p2.isCancel(confirmed) || !confirmed) {
p2.cancel("Installation cancelled");
process.exit(0);
}
}
spinner2.start("Installing MCP server...");
const results = installServer(serverName, serverConfig, targetAgents, {
routing: agentRouting
});
spinner2.stop("Installation complete");
console.log();
const successful = [...results.entries()].filter(([_, r]) => r.success);
const failed = [...results.entries()].filter(([_, r]) => !r.success);
if (successful.length > 0) {
const resultLines = [];
for (const [agentType, result] of successful) {
const agent = agents[agentType];
const shortPath = shortenPath(result.path);
resultLines.push(
`${chalk.green("\u2713")} ${agent.displayName}: ${chalk.dim(shortPath)}`
);
for (const extraPath of result.extraPaths ?? []) {
resultLines.push(
` ${chalk.dim("\u21B3 permissions:")} ${chalk.dim(shortenPath(extraPath))}`
);
}
}
p2.note(
resultLines.join("\n"),
chalk.green(
`Installed to ${successful.length} agent${successful.length !== 1 ? "s" : ""}`
)
);
}
if (failed.length > 0) {
console.log();
p2.log.error(
chalk.red(
`Failed to install to ${failed.length} agent${failed.length !== 1 ? "s" : ""}`
)
);
for (const [agentType, result] of failed) {
const agent = agents[agentType];
p2.log.message(
` ${chalk.red("\u2717")} ${agent.displayName}: ${chalk.dim(result.error)}`
);
}
}
const droppedByField = /* @__PURE__ */ new Map();
for (const [agentType, result] of results) {
for (const field of result.droppedFields ?? []) {
const agentNames = droppedByField.get(field) ?? [];
agentNames.push(agents[agentType].displayName);
droppedByField.set(field, agentNames);
}
}
for (const [field, agentNames] of droppedByField) {
p2.log.warn(
`${describeOptionalField(field)} is not supported by ${agentNames.join(", ")}; dropped from ${agentNames.length === 1 ? "that config" : "those configs"}.`
);
}
if (results.get("fx")?.success && resolvedBearerTokenEnv && serverConfig.headers && Object.keys(serverConfig.headers).some(
(name) => name.toLowerCase() === "authorization"
)) {
p2.log.warn(
`Authorization header dropped from the fx config; fx reads the token from ${resolvedBearerTokenEnv}.`
);
}
if (options.gitignore && localAgents.length === 0) {
p2.log.warn(
"--gitignore is only supported for project-scoped installations; ignoring."
);
} else if (options.gitignore) {
const successfulPaths = successful.flatMap(([_, result]) => [
result.path,
...result.extraPaths ?? []
]);
const gitignoreUpdate = updateGitignoreWithPaths(successfulPaths);
if (gitignoreUpdate.added.length > 0) {
p2.log.info(
`Added ${gitignoreUpdate.added.length} entr${gitignoreUpdate.added.length === 1 ? "y" : "ies"} to .gitignore`
);
} else {
p2.log.info("No new local config paths to add to .gitignore");
}
}
console.log();
if (failed.length === 0) {
p2.outro(chalk.green("Done!"));
return;
}
process.exitCode = 1;
if (successful.length === 0) {
p2.outro(chalk.red("Failed"));
return;
}
p2.outro(chalk.yellow("Installed with errors"));
}