add-mcp
Version:
Add MCP servers to your favorite coding agents with a single command.
2,973 lines • 92.1 kB
JavaScript
// src/config.ts
import { readFile, writeFile, mkdir, rm } from "fs/promises";
import { dirname, join } from "path";
import { homedir } from "os";
var CONFIG_DIR = "add-mcp";
var CONFIG_FILE = "config.json";
var CURRENT_VERSION = 1;
var LEGACY_AGENTS_DIR = ".agents";
var LEGACY_LOCK_FILE = ".mcp-lock.json";
var DEFAULT_FIND_REGISTRY_URL = "https://add-mcp.com/registry/api/v1/servers";
var DEFAULT_FIND_REGISTRY_LABEL = "add-mcp registry";
var LEGACY_FIND_REGISTRY_URL = "https://mcp.agent-tooling.dev/api/v1/servers";
var LEGACY_FIND_REGISTRY_LABEL = "integrations.sh MCP registry";
function migrateFindRegistryEntry(entry) {
if (entry.url !== LEGACY_FIND_REGISTRY_URL) {
return entry;
}
const label = !entry.label || entry.label === LEGACY_FIND_REGISTRY_LABEL ? DEFAULT_FIND_REGISTRY_LABEL : entry.label;
return { url: DEFAULT_FIND_REGISTRY_URL, label };
}
function getXdgConfigHome() {
return process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
}
function getConfigPath() {
return join(getXdgConfigHome(), CONFIG_DIR, CONFIG_FILE);
}
function getLegacyConfigPath() {
return join(homedir(), LEGACY_AGENTS_DIR, LEGACY_LOCK_FILE);
}
async function readConfig() {
const configPath = getConfigPath();
try {
const content = await readFile(configPath, "utf-8");
const parsed = JSON.parse(content);
if (typeof parsed.version !== "number") {
return createEmptyConfig();
}
if (parsed.version < CURRENT_VERSION) {
return createEmptyConfig();
}
return parsed;
} catch {
}
const legacyPath = getLegacyConfigPath();
try {
const content = await readFile(legacyPath, "utf-8");
const parsed = JSON.parse(content);
if (typeof parsed.version !== "number" || parsed.version < CURRENT_VERSION) {
return createEmptyConfig();
}
await writeConfig(parsed);
await cleanupLegacyConfig();
return parsed;
} catch {
return createEmptyConfig();
}
}
async function writeConfig(config) {
const configPath = getConfigPath();
await mkdir(dirname(configPath), { recursive: true });
const content = JSON.stringify(config, null, 2);
await writeFile(configPath, content, "utf-8");
}
async function cleanupLegacyConfig() {
const legacyPath = getLegacyConfigPath();
try {
await rm(legacyPath, { force: true });
} catch {
}
}
async function getLastSelectedAgents() {
const config = await readConfig();
return config.lastSelectedAgents;
}
async function saveSelectedAgents(agents2) {
const config = await readConfig();
config.lastSelectedAgents = agents2;
await writeConfig(config);
}
async function getFindRegistries() {
const config = await readConfig();
if (!config.findRegistries) return [];
const normalized = config.findRegistries.map(normalizeFindRegistryEntry);
const migrated = [];
const seen = /* @__PURE__ */ new Set();
for (const entry of normalized.map(migrateFindRegistryEntry)) {
if (seen.has(entry.url)) continue;
seen.add(entry.url);
migrated.push(entry);
}
const changed = migrated.length !== normalized.length || migrated.some(
(entry, i) => entry.url !== normalized[i]?.url || entry.label !== normalized[i]?.label
);
if (changed) {
try {
config.findRegistries = migrated;
await writeConfig(config);
} catch {
}
}
return migrated;
}
function normalizeFindRegistryEntry(raw) {
const legacy = raw;
const url = legacy.url ?? legacy.serversUrl;
if (!url || typeof url !== "string") {
throw new Error("Registry entry missing url");
}
return {
url,
...legacy.label ? { label: legacy.label } : {}
};
}
async function saveFindRegistries(registries) {
const config = await readConfig();
config.findRegistries = registries;
await writeConfig(config);
}
function createEmptyConfig() {
return {
version: CURRENT_VERSION
};
}
// src/schema.ts
var OPTIONAL_FIELD_SPECS = {
timeout: {
label: "request timeout",
isSet: (config) => typeof config.timeout === "number",
clear: (config) => {
delete config.timeout;
}
},
scopes: {
label: "OAuth scopes",
isSet: (config) => Array.isArray(config.oauthScopes) && config.oauthScopes.length > 0,
clear: (config) => {
delete config.oauthScopes;
}
},
autoApprove: {
label: "tool auto-approval",
// An empty array is meaningful ("approve all tools"), so presence alone
// counts as set — unlike scopes where an empty list means "nothing".
isSet: (config) => Array.isArray(config.autoApproveTools),
clear: (config) => {
delete config.autoApproveTools;
}
},
bearerTokenEnv: {
label: "bearer token env var",
isSet: (config) => resolvedBearerTokenEnv(config) !== void 0,
clear: (config) => {
delete config.bearerTokenEnv;
}
}
};
var BEARER_TOKEN_ENV_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
function isBearerTokenEnvName(name) {
return BEARER_TOKEN_ENV_NAME.test(name);
}
function resolvedBearerTokenEnv(config) {
if (typeof config.bearerTokenEnv !== "string") {
return void 0;
}
const name = config.bearerTokenEnv.trim();
return isBearerTokenEnvName(name) ? name : void 0;
}
function invalidBearerTokenEnvMessage(value) {
const name = value.trim();
if (name.length === 0) {
return "Invalid bearerTokenEnv. The name cannot be empty.";
}
return `Invalid bearerTokenEnv. "${name}" is not an environment variable name ([A-Za-z_][A-Za-z0-9_]*).`;
}
var ALL_OPTIONAL_FIELDS = Object.keys(
OPTIONAL_FIELD_SPECS
);
function describeOptionalField(field) {
return OPTIONAL_FIELD_SPECS[field].label;
}
function applyFieldSupport(config, supportedFields) {
const supported = new Set(supportedFields);
const copy = { ...config };
if (copy.oauthScopes) copy.oauthScopes = [...copy.oauthScopes];
if (copy.autoApproveTools) copy.autoApproveTools = [...copy.autoApproveTools];
if (typeof copy.bearerTokenEnv === "string") {
const name = resolvedBearerTokenEnv(copy);
if (name) {
copy.bearerTokenEnv = name;
} else {
delete copy.bearerTokenEnv;
}
}
const dropped = [];
for (const field of ALL_OPTIONAL_FIELDS) {
const spec = OPTIONAL_FIELD_SPECS[field];
if (spec.isSet(copy) && !supported.has(field)) {
spec.clear(copy);
dropped.push(field);
}
}
return { config: copy, dropped };
}
// src/formats/utils.ts
function deepMerge(target, source) {
const result = { ...target };
for (const key in source) {
const sourceValue = source[key];
const targetValue = result[key];
if (sourceValue && typeof sourceValue === "object" && !Array.isArray(sourceValue)) {
result[key] = deepMerge(
targetValue && typeof targetValue === "object" ? targetValue : {},
sourceValue
);
} else {
result[key] = sourceValue;
}
}
return result;
}
function dropReplacedServers(existing, incoming, configKey) {
const incomingServers = getNestedValue(incoming, configKey);
const existingServers = getNestedValue(existing, configKey);
if (!incomingServers || typeof incomingServers !== "object" || Array.isArray(incomingServers) || !existingServers || typeof existingServers !== "object" || Array.isArray(existingServers)) {
return;
}
for (const name of Object.keys(incomingServers)) {
delete existingServers[name];
}
}
var ROOT_CONFIG_KEY = "";
function looksLikeServerEntry(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false;
}
const entry = value;
return typeof entry.command === "string" || typeof entry.url === "string" || typeof entry.type === "string";
}
function isConfigObject(value) {
return !!value && typeof value === "object" && !Array.isArray(value);
}
function isBareServerMap(obj) {
if (!isConfigObject(obj)) return false;
if (obj.mcpServers !== void 0) return false;
const values = Object.values(obj);
return values.length > 0 && values.every(looksLikeServerEntry);
}
function jsoncPath(configKey) {
return configKey === ROOT_CONFIG_KEY ? [] : configKey.split(".");
}
function getNestedValue(obj, path) {
if (path === ROOT_CONFIG_KEY) {
return obj;
}
const keys = path.split(".");
let current = obj;
for (const key of keys) {
if (current && typeof current === "object" && key in current) {
current = current[key];
} else {
return void 0;
}
}
return current;
}
// src/opencode-config.ts
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
import * as jsonc2 from "jsonc-parser";
// src/formats/json.ts
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
import { dirname as dirname2 } from "path";
import * as jsonc from "jsonc-parser";
function detectIndent(text) {
let result = null;
jsonc.visit(text, {
onObjectProperty: (_property, offset, _length, startLine, startCharacter) => {
if (result === null && startLine > 0 && startCharacter > 0) {
const lineStart2 = text.lastIndexOf("\n", offset - 1) + 1;
const whitespace = text.slice(lineStart2, offset);
result = {
tabSize: startCharacter,
insertSpaces: !whitespace.includes(" ")
};
}
}
});
return result || { tabSize: 2, insertSpaces: true };
}
function readJsonConfig(filePath) {
if (!existsSync(filePath)) {
return {};
}
const content = readFileSync(filePath, "utf-8");
const parsed = jsonc.parse(content);
return parsed;
}
function writeJsonConfig(filePath, config, configKey) {
const dir = dirname2(filePath);
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true });
}
let originalContent = "";
let existingConfig = {};
if (existsSync(filePath)) {
originalContent = readFileSync(filePath, "utf-8");
const parsed = jsonc.parse(originalContent);
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
existingConfig = parsed;
}
}
const replaceDocument = configKey !== ROOT_CONFIG_KEY && isBareServerMap(existingConfig);
if (replaceDocument) {
existingConfig = { mcpServers: { ...existingConfig } };
}
dropReplacedServers(existingConfig, config, configKey);
const mergedConfig = deepMerge(existingConfig, config);
if (originalContent) {
try {
const configKeyPath = replaceDocument ? [] : jsoncPath(configKey);
const newValue = replaceDocument ? mergedConfig : getNestedValue(mergedConfig, configKey);
const edits = jsonc.modify(originalContent, configKeyPath, newValue, {
formattingOptions: detectIndent(originalContent)
});
const updatedContent = jsonc.applyEdits(originalContent, edits);
writeFileSync(filePath, updatedContent);
return;
} catch {
}
}
writeFileSync(filePath, JSON.stringify(mergedConfig, null, 2));
}
function rewriteJsoncAsJson(filePath) {
if (!existsSync(filePath)) {
return;
}
const parsed = jsonc.parse(readFileSync(filePath, "utf-8"));
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return;
}
writeFileSync(filePath, `${JSON.stringify(parsed, null, 2)}
`);
}
function removeJsonConfigKey(filePath, configKey, serverName) {
if (!existsSync(filePath)) {
return;
}
const originalContent = readFileSync(filePath, "utf-8");
const configKeyPath = jsoncPath(configKey);
try {
const edits = jsonc.modify(
originalContent,
[...configKeyPath, serverName],
void 0,
{ formattingOptions: detectIndent(originalContent) }
);
const updatedContent = jsonc.applyEdits(originalContent, edits);
writeFileSync(filePath, updatedContent);
return;
} catch {
}
const parsed = jsonc.parse(originalContent);
const servers = getNestedValue(parsed, configKey);
if (servers && typeof servers === "object" && serverName in servers) {
delete servers[serverName];
}
writeFileSync(filePath, JSON.stringify(parsed, null, 2));
}
function setNestedValue(obj, path, value) {
if (path === ROOT_CONFIG_KEY) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return;
}
for (const key of Object.keys(obj)) {
delete obj[key];
}
Object.assign(obj, value);
return;
}
const keys = path.split(".");
const lastKey = keys.pop();
if (!lastKey) return;
let current = obj;
for (const key of keys) {
if (!current[key] || typeof current[key] !== "object") {
current[key] = {};
}
current = current[key];
}
current[lastKey] = value;
}
// src/opencode-config.ts
var OPENCODE_LEGACY_CONFIG_KEY = "mcp";
var OPENCODE_NATIVE_CONFIG_KEY = "mcp.servers";
function isJsonObject(value) {
return !!value && typeof value === "object" && !Array.isArray(value);
}
function isEnablementOnly(value) {
return (typeof value.enabled === "boolean" || typeof value.disabled === "boolean") && value.type === void 0 && typeof value.url !== "string" && typeof value.command !== "string" && !Array.isArray(value.command);
}
function isOpenCodeServerEntry(value) {
if (!isJsonObject(value)) {
return false;
}
if (value.type === "local" || value.type === "remote") {
return true;
}
if (typeof value.url === "string" && value.url.length > 0) {
return true;
}
if (typeof value.command === "string" && value.command.length > 0) {
return true;
}
if (Array.isArray(value.command) && value.command.some((part) => typeof part === "string")) {
return true;
}
return isEnablementOnly(value);
}
function isTimeoutMetadata(value) {
if (typeof value === "number") {
return true;
}
if (!isJsonObject(value) || isOpenCodeServerEntry(value)) {
return false;
}
return "startup" in value || "catalog" in value || "execution" in value || "request" in value;
}
function parseOpenCodeJsonc(configPath) {
if (!existsSync2(configPath)) {
return {};
}
const text = readFileSync2(configPath, "utf-8");
const errors = [];
const parsed = jsonc2.parse(text, errors, {
allowTrailingComma: true
});
if (parsed === void 0 && jsonc2.stripComments(text).trim() === "" && errors.every((error) => error.error === jsonc2.ParseErrorCode.ValueExpected)) {
return {};
}
if (errors.length > 0) {
throw new Error(`Invalid JSON in ${configPath}`);
}
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error(`${configPath} must be a JSON object`);
}
return parsed;
}
function classifyOpenCodeDocument(document, configPath) {
if (!("mcp" in document)) {
return { nativeMap: null, legacy: {}, timeoutIsMetadata: false };
}
const mcp = document.mcp;
if (!isJsonObject(mcp)) {
throw new Error(`${configPath} mcp must be a JSON object`);
}
let nativeMap = null;
if ("servers" in mcp) {
const servers = mcp.servers;
if (!isJsonObject(servers)) {
throw new Error(`${configPath} mcp.servers must be a JSON object`);
}
if (!isOpenCodeServerEntry(servers)) {
nativeMap = servers;
}
}
const legacy = {};
for (const [name, value] of Object.entries(mcp)) {
if (name === "servers" && nativeMap) {
continue;
}
if (name === "timeout" && isTimeoutMetadata(value)) {
continue;
}
if (isOpenCodeServerEntry(value)) {
legacy[name] = value;
}
}
return {
nativeMap,
legacy,
timeoutIsMetadata: isTimeoutMetadata(mcp.timeout)
};
}
function readOpenCodeLayout(configPath) {
return classifyOpenCodeDocument(parseOpenCodeJsonc(configPath), configPath);
}
function listedNativeServers(nativeMap) {
if (!nativeMap) {
return {};
}
const listed = {};
for (const [name, value] of Object.entries(nativeMap)) {
if (isOpenCodeServerEntry(value)) {
listed[name] = value;
}
}
return listed;
}
function listOpenCodeServers(configPath) {
const layout = readOpenCodeLayout(configPath);
const nativeListed = listedNativeServers(layout.nativeMap);
const names = /* @__PURE__ */ new Set([
...Object.keys(layout.legacy),
...Object.keys(layout.nativeMap ?? {})
]);
const servers = [];
for (const serverName of names) {
if (Object.hasOwn(nativeListed, serverName)) {
const nativeEntry = nativeListed[serverName];
if (nativeEntry) {
servers.push({
serverName,
config: nativeEntry,
configKey: OPENCODE_NATIVE_CONFIG_KEY
});
}
continue;
}
if (layout.nativeMap && Object.hasOwn(layout.nativeMap, serverName)) {
continue;
}
const legacyEntry = layout.legacy[serverName];
if (legacyEntry) {
servers.push({
serverName,
config: legacyEntry,
configKey: OPENCODE_LEGACY_CONFIG_KEY
});
}
}
return servers;
}
function slotHoldsNonServer(mcp, name) {
if (!mcp || !(name in mcp)) {
return false;
}
return !isOpenCodeServerEntry(mcp[name]);
}
function resolveOpenCodeUpsertKey(configPath, serverName) {
const document = parseOpenCodeJsonc(configPath);
const layout = classifyOpenCodeDocument(document, configPath);
if (layout.nativeMap && Object.hasOwn(layout.nativeMap, serverName)) {
return OPENCODE_NATIVE_CONFIG_KEY;
}
if (Object.hasOwn(layout.legacy, serverName)) {
return OPENCODE_LEGACY_CONFIG_KEY;
}
if (layout.nativeMap && Object.keys(layout.legacy).length === 0) {
return OPENCODE_NATIVE_CONFIG_KEY;
}
const mcp = isJsonObject(document.mcp) ? document.mcp : void 0;
const collidingMetadata = (serverName === "servers" || serverName === "timeout") && slotHoldsNonServer(mcp, serverName);
if (collidingMetadata) {
if (layout.nativeMap) {
return OPENCODE_NATIVE_CONFIG_KEY;
}
throw new Error(
`${configPath} already uses mcp.${serverName} for OpenCode settings. Rename the server or move that setting before installing "${serverName}".`
);
}
return OPENCODE_LEGACY_CONFIG_KEY;
}
var OAUTH_LEGACY_TO_NATIVE = {
clientId: "client_id",
clientSecret: "client_secret",
callbackPort: "callback_port",
redirectUri: "redirect_uri"
};
var OAUTH_NATIVE_TO_LEGACY = {
client_id: "clientId",
client_secret: "clientSecret",
callback_port: "callbackPort",
redirect_uri: "redirectUri"
};
function renameOAuthFields(oauth, names) {
const next = {};
const targetNames = new Set(Object.values(names));
for (const [key, value] of Object.entries(oauth)) {
const mapped = names[key];
if (mapped) {
if (!Object.hasOwn(next, mapped)) {
next[mapped] = value;
}
continue;
}
if (targetNames.has(key) && Object.hasOwn(next, key)) {
continue;
}
next[key] = value;
}
return next;
}
function translateTimeout(timeout, toNative) {
if (toNative) {
if (typeof timeout === "number") {
return { request: timeout };
}
if (isJsonObject(timeout)) {
return timeout;
}
return void 0;
}
if (typeof timeout === "number") {
return timeout;
}
if (isJsonObject(timeout) && typeof timeout.request === "number") {
return timeout.request;
}
return void 0;
}
function translateOpenCodeEntry(config, fromKey, toKey) {
const next = { ...config };
if (fromKey === toKey) {
return next;
}
const toNative = toKey === OPENCODE_NATIVE_CONFIG_KEY;
if (toNative) {
const disabled = next.disabled === true || next.enabled === false;
delete next.enabled;
delete next.disabled;
if (disabled) {
next.disabled = true;
}
} else {
if (next.disabled === true) {
next.enabled = false;
} else if (typeof next.enabled !== "boolean") {
next.enabled = true;
}
delete next.disabled;
}
if ("timeout" in next) {
const timeout = translateTimeout(next.timeout, toNative);
if (timeout === void 0) {
delete next.timeout;
} else {
next.timeout = timeout;
}
}
if (isJsonObject(next.oauth)) {
next.oauth = renameOAuthFields(
next.oauth,
toNative ? OAUTH_LEGACY_TO_NATIVE : OAUTH_NATIVE_TO_LEGACY
);
}
return next;
}
function resolveOpenCodeRelocateKey(configPath, newName, sourceKey) {
const document = parseOpenCodeJsonc(configPath);
const layout = classifyOpenCodeDocument(document, configPath);
const mcp = isJsonObject(document.mcp) ? document.mcp : void 0;
const collidingMetadata = (newName === "servers" || newName === "timeout") && slotHoldsNonServer(mcp, newName);
if (!collidingMetadata) {
return sourceKey;
}
if (layout.nativeMap) {
return OPENCODE_NATIVE_CONFIG_KEY;
}
throw new Error(
`${configPath} already uses mcp.${newName} for OpenCode settings. Rename the server or move that setting before using "${newName}".`
);
}
function openCodeServerIdentity(config) {
if (typeof config.url === "string" && config.url.length > 0) {
return config.url;
}
if (typeof config.command === "string" && config.command.length > 0) {
return config.command;
}
if (Array.isArray(config.command)) {
const parts = config.command.filter(
(part) => typeof part === "string"
);
if (parts.length > 0) {
return parts.join(" ");
}
}
return "";
}
function relocateOpenCodeServer(configPath, oldName, newName) {
if (oldName === newName) {
return;
}
const listed = listOpenCodeServers(configPath);
const existing = listed.find((entry) => entry.serverName === oldName);
if (!existing) {
throw new Error(`${configPath} has no OpenCode server named "${oldName}"`);
}
const dest = listed.find((entry) => entry.serverName === newName);
if (dest) {
const sourceId = openCodeServerIdentity(existing.config);
const destId = openCodeServerIdentity(dest.config);
if (sourceId && destId && sourceId === destId) {
removeOpenCodeServer(configPath, oldName);
return;
}
throw new Error(
`${configPath} already has an OpenCode server named "${newName}". Rename that server first.`
);
}
const destKey = resolveOpenCodeRelocateKey(
configPath,
newName,
existing.configKey
);
const written = translateOpenCodeEntry(
existing.config,
existing.configKey,
destKey
);
const document = {};
setNestedValue(document, destKey, { [newName]: written });
writeJsonConfig(configPath, document, destKey);
removeOpenCodeServer(configPath, oldName);
}
function removeOpenCodeServer(configPath, serverName) {
if (!existsSync2(configPath)) {
return false;
}
const layout = readOpenCodeLayout(configPath);
const inNative = !!(layout.nativeMap && Object.hasOwn(layout.nativeMap, serverName));
const inLegacy = Object.hasOwn(layout.legacy, serverName);
if (!inNative && !inLegacy) {
return false;
}
if (inNative) {
removeJsonConfigKey(configPath, OPENCODE_NATIVE_CONFIG_KEY, serverName);
}
if (inLegacy) {
removeJsonConfigKey(configPath, OPENCODE_LEGACY_CONFIG_KEY, serverName);
}
return true;
}
// src/agents.ts
import * as p from "@clack/prompts";
import { homedir as homedir2 } from "os";
import { basename, dirname as dirname3, join as join2 } from "path";
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
import * as jsonc3 from "jsonc-parser";
var home = homedir2();
var defaultGrokHome = join2(home, ".grok");
function getGrokHome() {
return process.env.GROK_HOME || defaultGrokHome;
}
var copilotHome = process.env.COPILOT_HOME || join2(home, ".copilot");
function getKimiCodeHome() {
return process.env.KIMI_CODE_HOME || join2(home, ".kimi-code");
}
function getJunieHome() {
return process.env.JUNIE_HOME || join2(home, ".junie");
}
function expandHermesHome(raw) {
const expanded = raw.replace(
/\$(\w+)|\$\{(\w+)\}|%(\w+)%/g,
(match, bare, braced, percent) => {
if (percent && process.platform !== "win32") {
return match;
}
return process.env[bare ?? braced ?? percent ?? ""] ?? match;
}
);
return expanded === "~" || /^~[\\/]/.test(expanded) ? join2(home, expanded.slice(1)) : expanded;
}
function getHermesRoot() {
if (process.env.HERMES_HOME?.trim()) {
return expandHermesHome(process.env.HERMES_HOME.trim());
}
if (process.platform === "win32") {
return join2(
process.env.LOCALAPPDATA || join2(home, "AppData", "Local"),
"hermes"
);
}
return join2(home, ".hermes");
}
var HERMES_PROFILE_NAME = /^[a-z0-9][a-z0-9_-]{0,63}$/;
function getHermesHome() {
const root = getHermesRoot();
const activeProfilePath = join2(root, "active_profile");
if (basename(dirname3(root)) === "profiles" || !existsSync3(activeProfilePath)) {
return root;
}
let activeProfile;
try {
activeProfile = readFileSync3(activeProfilePath, "utf-8").trim();
} catch (error) {
throw new Error(
`Could not read the Hermes profile selection at ${activeProfilePath}: ${error instanceof Error ? error.message : String(error)}`
);
}
const profile = activeProfile.toLowerCase();
if (!profile || profile === "default") {
return root;
}
if (!HERMES_PROFILE_NAME.test(profile)) {
throw new Error(
`The Hermes profile selection at ${activeProfilePath} names "${activeProfile}", which is not a valid profile name.`
);
}
return join2(root, "profiles", profile);
}
function getMastraCodeDir() {
return join2(home, ".mastracode");
}
function getPiAgentDir() {
return process.env.PI_CODING_AGENT_DIR || join2(home, ".pi", "agent");
}
function getKiloConfigDir() {
return join2(process.env.XDG_CONFIG_HOME || join2(home, ".config"), "kilo");
}
function shortenPath(fullPath) {
if (fullPath.startsWith(home)) {
return fullPath.replace(home, "~");
}
return fullPath;
}
function getPlatformPaths() {
const platform = process.platform;
if (platform === "win32") {
const appData = process.env.APPDATA || join2(home, "AppData", "Roaming");
return {
appSupport: appData,
vscodePath: join2(appData, "Code", "User"),
gooseConfigPath: join2(appData, "Block", "goose", "config", "config.yaml")
};
} else if (platform === "darwin") {
return {
appSupport: join2(home, "Library", "Application Support"),
vscodePath: join2(home, "Library", "Application Support", "Code", "User"),
gooseConfigPath: join2(home, ".config", "goose", "config.yaml")
};
} else {
const configDir = process.env.XDG_CONFIG_HOME || join2(home, ".config");
return {
appSupport: configDir,
vscodePath: join2(configDir, "Code", "User"),
gooseConfigPath: join2(configDir, "goose", "config.yaml")
};
}
}
var { appSupport, vscodePath, gooseConfigPath } = getPlatformPaths();
var antigravityConfigPath = join2(
home,
".gemini",
"config",
"mcp_config.json"
);
var clineCliConfigPath = join2(
process.env.CLINE_DIR || join2(home, ".cline"),
"data",
"settings",
"cline_mcp_settings.json"
);
var clineExtensionConfigPath = join2(
vscodePath,
"globalStorage",
"saoudrizwan.claude-dev",
"settings",
"cline_mcp_settings.json"
);
var kiloVscodeStoragePath = join2(
vscodePath,
"globalStorage",
"kilocode.kilo-code"
);
var windsurfConfigPath = join2(
home,
".codeium",
"windsurf",
"mcp_config.json"
);
function buildStandardRemote(config) {
const remote = {};
if (config.type) remote.type = config.type;
if (config.url) remote.url = config.url;
if (config.headers && Object.keys(config.headers).length > 0) {
remote.headers = config.headers;
}
if (typeof config.timeout === "number") {
remote.timeout = config.timeout;
}
return remote;
}
function buildStandardLocal(config) {
const local = {
command: config.command,
args: config.args || []
};
if (config.env && Object.keys(config.env).length > 0) {
local.env = config.env;
}
return local;
}
function transformStandardConfig(_serverName, config) {
return config.url ? buildStandardRemote(config) : buildStandardLocal(config);
}
function transformGeminiConfig(_serverName, config) {
if (!config.url) {
return buildStandardLocal(config);
}
const remote = buildStandardRemote(config);
if (config.oauthScopes && config.oauthScopes.length > 0) {
remote.oauth = { scopes: config.oauthScopes };
}
return remote;
}
function transformGooseConfig(serverName, config) {
if (config.url) {
const gooseType = config.type === "sse" ? "sse" : "streamable_http";
return {
name: serverName,
description: "",
type: gooseType,
uri: config.url,
headers: config.headers || {},
enabled: true,
timeout: 300
};
}
return {
name: serverName,
description: "",
cmd: config.command,
args: config.args || [],
enabled: true,
envs: config.env || {},
type: "stdio",
timeout: 300
};
}
function transformZedConfig(_serverName, config) {
if (config.url) {
return {
source: "custom",
type: config.type || "http",
url: config.url,
headers: config.headers || {}
};
}
return {
source: "custom",
command: config.command,
args: config.args || [],
env: config.env || {}
};
}
function transformOpenCodeConfig(_serverName, config, context) {
const native = context?.configKey === OPENCODE_NATIVE_CONFIG_KEY;
if (config.url) {
const remote = {
type: "remote",
url: config.url,
headers: config.headers
};
if (!native) {
remote.enabled = true;
}
return remote;
}
const local = {
type: "local",
command: [config.command, ...config.args || []],
environment: config.env || {}
};
if (!native) {
local.enabled = true;
}
return local;
}
function transformKiloCodeConfig(serverName, config) {
const entry = transformOpenCodeConfig(serverName, config);
if (typeof config.timeout === "number") {
entry.timeout = config.timeout;
}
return entry;
}
var KIMI_MAX_TIMEOUT_MS = 2147483647;
function kimiToolTimeoutMs(timeout) {
if (typeof timeout !== "number" || !Number.isInteger(timeout)) {
return void 0;
}
if (timeout < 1 || timeout > KIMI_MAX_TIMEOUT_MS) {
return void 0;
}
return timeout;
}
function transformKimiCodeConfig(_serverName, config) {
let entry;
if (config.url) {
entry = {
transport: config.type === "sse" ? "sse" : "http",
url: config.url
};
if (config.headers && Object.keys(config.headers).length > 0) {
entry.headers = config.headers;
}
} else {
entry = {
transport: "stdio",
command: config.command,
args: config.args || []
};
if (config.env && Object.keys(config.env).length > 0) {
entry.env = config.env;
}
}
const toolTimeoutMs = kimiToolTimeoutMs(config.timeout);
if (toolTimeoutMs !== void 0) {
entry.toolTimeoutMs = toolTimeoutMs;
}
return entry;
}
function applyCodexApproval(target, autoApproveTools) {
if (!autoApproveTools) return target;
if (autoApproveTools.length === 0) {
target.default_tools_approval_mode = "approve";
return target;
}
target.tools = Object.fromEntries(
autoApproveTools.map((tool) => [tool, { approval_mode: "approve" }])
);
return target;
}
function transformCodexConfig(_serverName, config) {
if (config.url) {
const remoteConfig = {
type: config.type || "http",
url: config.url
};
if (config.headers && Object.keys(config.headers).length > 0) {
remoteConfig.http_headers = config.headers;
}
return applyCodexApproval(remoteConfig, config.autoApproveTools);
}
return applyCodexApproval(
{
command: config.command,
args: config.args || [],
env: config.env
},
config.autoApproveTools
);
}
function transformGrokBuildConfig(_serverName, config) {
if (config.url) {
const remoteConfig = {
url: config.url
};
if (config.headers && Object.keys(config.headers).length > 0) {
remoteConfig.headers = config.headers;
}
if (typeof config.timeout === "number") {
remoteConfig.tool_timeout_sec = config.timeout / 1e3;
}
return remoteConfig;
}
return buildStandardLocal(config);
}
function transformKiroCliConfig(_serverName, config) {
if (!config.url) {
return buildStandardLocal(config);
}
const remoteConfig = {
url: config.url
};
if (config.headers && Object.keys(config.headers).length > 0) {
remoteConfig.headers = config.headers;
}
if (typeof config.timeout === "number") {
remoteConfig.timeout = config.timeout;
}
return remoteConfig;
}
function transformJunieConfig(_serverName, config) {
if (!config.url) {
return buildStandardLocal(config);
}
const remote = { url: config.url };
if (config.headers && Object.keys(config.headers).length > 0) {
remote.headers = config.headers;
}
return remote;
}
function transformFactoryDroidConfig(_serverName, config) {
if (config.url) {
return {
...buildStandardRemote(config),
type: config.type === "sse" ? "sse" : "http"
};
}
const local = {
type: "stdio",
...buildStandardLocal(config)
};
if (typeof config.timeout === "number") {
local.timeout = config.timeout;
}
return local;
}
function transformHermesConfig(_serverName, config) {
let entry;
if (config.url) {
entry = { url: config.url };
if (config.headers && Object.keys(config.headers).length > 0) {
entry.headers = config.headers;
}
if (config.type === "sse") {
entry.transport = "sse";
}
} else {
entry = buildStandardLocal(config);
}
if (typeof config.timeout === "number") {
entry.timeout = config.timeout / 1e3;
}
return entry;
}
function transformPiConfig(_serverName, config) {
if (config.url) {
const remote = { url: config.url };
if (config.headers && Object.keys(config.headers).length > 0) {
remote.headers = config.headers;
}
if (typeof config.timeout === "number") {
remote.requestTimeoutMs = config.timeout;
}
return remote;
}
const local = buildStandardLocal(config);
if (typeof config.timeout === "number") {
local.requestTimeoutMs = config.timeout;
}
return local;
}
function transformMastraCodeConfig(_serverName, config) {
if (!config.url) {
return buildStandardLocal(config);
}
const remote = { url: config.url };
if (config.headers && Object.keys(config.headers).length > 0) {
remote.headers = config.headers;
}
if (config.oauthScopes && config.oauthScopes.length > 0) {
remote.oauth = { scopes: config.oauthScopes };
}
return remote;
}
function headersWithoutAuthorization(headers) {
const next = {};
for (const [name, value] of Object.entries(headers)) {
if (name.toLowerCase() !== "authorization") {
next[name] = value;
}
}
return next;
}
function hasAuthorizationHeader(headers) {
return Object.keys(headers).some(
(name) => name.toLowerCase() === "authorization"
);
}
function transformFxConfig(_serverName, config) {
if (config.url) {
const bearerTokenEnv = resolvedBearerTokenEnv(config);
const headers = config.headers ? bearerTokenEnv ? headersWithoutAuthorization(config.headers) : config.headers : void 0;
if (headers && hasAuthorizationHeader(headers)) {
throw new Error(
"fx rejects a literal Authorization header. Pass --bearer-token-env <name>, use a non-Authorization header, or set header_env or oauth in ~/.fx/mcp.json."
);
}
const entry2 = {
type: config.type === "sse" ? "sse" : "http",
url: config.url,
enabled: true
};
if (headers && Object.keys(headers).length > 0) {
entry2.headers = headers;
}
if (bearerTokenEnv) {
entry2.bearer_token_env = bearerTokenEnv;
}
return entry2;
}
const entry = {
type: "local",
command: [config.command, ...config.args || []],
enabled: true
};
if (config.env && Object.keys(config.env).length > 0) {
entry.environment = config.env;
}
return entry;
}
function transformCursorConfig(_serverName, config) {
if (config.url) {
const remoteConfig = {
url: config.url
};
if (config.headers && Object.keys(config.headers).length > 0) {
remoteConfig.headers = config.headers;
}
if (config.oauthScopes && config.oauthScopes.length > 0) {
remoteConfig.auth = { scopes: config.oauthScopes };
}
return remoteConfig;
}
return buildStandardLocal(config);
}
function transformClineConfig(_serverName, config) {
if (config.url) {
const remoteConfig = {
url: config.url,
type: config.type === "sse" ? "sse" : "streamableHttp",
disabled: false
};
if (config.headers && Object.keys(config.headers).length > 0) {
remoteConfig.headers = config.headers;
}
return remoteConfig;
}
const localConfig = {
command: config.command,
args: config.args || [],
disabled: false
};
if (config.env && Object.keys(config.env).length > 0) {
localConfig.env = config.env;
}
return localConfig;
}
function transformAntigravityConfig(_serverName, config) {
if (config.url) {
const remoteConfig = {
serverUrl: config.url
};
if (config.headers && Object.keys(config.headers).length > 0) {
remoteConfig.headers = config.headers;
}
return remoteConfig;
}
const localConfig = {
command: config.command,
args: config.args || []
};
if (config.env && Object.keys(config.env).length > 0) {
localConfig.env = config.env;
}
return localConfig;
}
function looksLikeServerEntry2(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return false;
}
const entry = value;
return typeof entry.command === "string" || typeof entry.url === "string" || typeof entry.type === "string";
}
function isVscodeServersWrapper(obj) {
if (obj.mcpServers !== void 0) return false;
if (!obj.servers || typeof obj.servers !== "object" || Array.isArray(obj.servers)) {
return false;
}
if (looksLikeServerEntry2(obj.servers)) return false;
return Object.values(obj.servers).some(
looksLikeServerEntry2
);
}
function parseCopilotProjectFile(configPath) {
const errors = [];
const parsed = jsonc3.parse(
readFileSync3(configPath, "utf-8"),
errors
);
if (parsed === void 0 && errors.every((error) => error.error === jsonc3.ParseErrorCode.ValueExpected)) {
return {};
}
if (errors.length > 0) {
throw new Error(`Invalid JSON in ${configPath}`);
}
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
throw new Error(`${configPath} must be a JSON object`);
}
return parsed;
}
function githubCopilotCliProjectConfigKey(configPath) {
if (!existsSync3(configPath)) {
return "mcpServers";
}
const obj = parseCopilotProjectFile(configPath);
if (isVscodeServersWrapper(obj)) {
throw new Error(
`${configPath} uses VS Code's "servers" key. Copilot CLI reads mcpServers in .mcp.json or .github/mcp.json; use --agent vscode for .vscode/mcp.json.`
);
}
if (isBareServerMap(obj)) {
return "";
}
return "mcpServers";
}
function transformGitHubCopilotCliConfig(_serverName, config, context) {
const includeTools = !context?.local;
if (config.url) {
const remoteConfig = {
type: config.type || "http",
url: config.url
};
if (includeTools) {
remoteConfig.tools = ["*"];
}
if (config.headers && Object.keys(config.headers).length > 0) {
remoteConfig.headers = config.headers;
}
return remoteConfig;
}
const stdioConfig = {
type: "stdio",
command: config.command,
args: config.args || []
};
if (includeTools) {
stdioConfig.tools = ["*"];
}
if (config.env && Object.keys(config.env).length > 0) {
stdioConfig.env = config.env;
}
return stdioConfig;
}
function resolveGitHubCopilotCliConfigPath(agent, options) {
if (!options.local) {
return agent.configPath;
}
const mcpJson = join2(options.cwd, ".mcp.json");
const githubMcpJson = join2(options.cwd, ".github", "mcp.json");
if (existsSync3(mcpJson)) return mcpJson;
if (existsSync3(githubMcpJson)) return githubMcpJson;
return mcpJson;
}
function resolveMcporterConfigPath(agent, options) {
if (options.local && agent.localConfigPath) {
return join2(options.cwd, agent.localConfigPath);
}
const globalJsonPath = agent.configPath;
const globalJsoncPath = join2(dirname3(globalJsonPath), "mcporter.jsonc");
if (existsSync3(globalJsonPath)) {
return globalJsonPath;
}
if (existsSync3(globalJsoncPath)) {
return globalJsoncPath;
}
return globalJsonPath;
}
function resolveOpenCodeConfigPath(agent, options) {
if (options.local) {
const candidates2 = [
join2(options.cwd, "opencode.jsonc"),
join2(options.cwd, "opencode.json"),
join2(options.cwd, ".opencode", "opencode.jsonc"),
join2(options.cwd, ".opencode", "opencode.json")
];
for (const path of candidates2) {
if (existsSync3(path)) return path;
}
return join2(options.cwd, "opencode.jsonc");
}
const candidates = [
join2(home, ".config", "opencode", "opencode.jsonc"),
join2(home, ".config", "opencode", "opencode.json")
];
for (const path of candidates) {
if (existsSync3(path)) return path;
}
return join2(home, ".config", "opencode", "opencode.jsonc");
}
function resolveGrokBuildConfigPath(agent, options) {
if (options.local && agent.localConfigPath) {
return join2(options.cwd, agent.localConfigPath);
}
return join2(getGrokHome(), "config.toml");
}
function resolveKimiCodeConfigPath(agent, options) {
if (options.local && agent.localConfigPath) {
return join2(options.cwd, agent.localConfigPath);
}
return join2(getKimiCodeHome(), "mcp.json");
}
function resolveJunieConfigPath(agent, options) {
if (options.local && agent.localConfigPath) {
return join2(options.cwd, agent.localConfigPath);
}
return join2(getJunieHome(), "mcp", "mcp.json");
}
function resolvePiConfigPath(agent, options) {
if (options.local && agent.localConfigPath) {
return join2(options.cwd, agent.localConfigPath);
}
return join2(getPiAgentDir(), "mcp.json");
}
function resolveKiloCodeConfigPath(_agent, options) {
const baseDir = options.local ? options.cwd : getKiloConfigDir();
const candidates = options.local ? [
join2(".kilo", "kilo.jsonc"),
join2(".kilo", "kilo.json"),
join2(".kilocode", "kilo.jsonc"),
join2(".kilocode", "kilo.json"),
"kilo.jsonc"
] : ["kilo.jsonc"];
for (const candidate of candidates) {
const candidatePath = join2(baseDir, candidate);
if (existsSync3(candidatePath)) {
return candidatePath;
}
}
return join2(baseDir, "kilo.json");
}
var agents = {
antigravity: {
name: "antigravity",
displayName: "Antigravity",
configPath: antigravityConfigPath,
projectDetectPaths: [],
// Global only - no project support
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: [],
detectGlobalInstall: async () => {
return existsSync3(join2(home, ".gemini", "config"));
},
transformConfig: transformAntigravityConfig
},
cline: {
name: "cline",
displayName: "Cline VSCode Extension",
configPath: clineExtensionConfigPath,
projectDetectPaths: [],
// Global only - no project support
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: [],
detectGlobalInstall: async () => {
return existsSync3(dirname3(clineExtensionConfigPath));
},
transformConfig: transformClineConfig
},
"cline-cli": {
name: "cline-cli",
displayName: "Cline CLI",
configPath: clineCliConfigPath,
projectDetectPaths: [],
// Global only - no project support
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: [],
detectGlobalInstall: async () => {
return existsSync3(dirname3(clineCliConfigPath));
},
transformConfig: transformClineConfig
},
"claude-code": {
name: "claude-code",
displayName: "Claude Code",
configPath: join2(home, ".claude.json"),
localConfigPath: ".mcp.json",
projectDetectPaths: [".mcp.json", ".claude"],
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: ["timeout", "autoApprove"],
detectGlobalInstall: async () => {
return existsSync3(join2(home, ".claude"));
},
transformConfig: transformStandardConfig
},
"claude-desktop": {
name: "claude-desktop",
displayName: "Claude Desktop",
configPath: join2(appSupport, "Claude", "claude_desktop_config.json"),
projectDetectPaths: [],
// Global only - no project support
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio"],
supportedFields: [],
unsupportedTransportMessage: "Claude Desktop only supports local (stdio) servers via its config file. Add remote servers through Settings \u2192 Connectors in the app instead.",
detectGlobalInstall: async () => {
return existsSync3(join2(appSupport, "Claude"));
},
transformConfig: transformStandardConfig
},
codex: {
name: "codex",
displayName: "Codex",
configPath: join2(
process.env.CODEX_HOME || join2(home, ".codex"),
"config.toml"
),
localConfigPath: ".codex/config.toml",
projectDetectPaths: [".codex"],
configKey: "mcp_servers",
format: "toml",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: ["autoApprove"],
detectGlobalInstall: async () => {
return existsSync3(join2(home, ".codex"));
},
transformConfig: transformCodexConfig
},
cursor: {
name: "cursor",
displayName: "Cursor",
configPath: join2(home, ".cursor", "mcp.json"),
localConfigPath: ".cursor/mcp.json",
projectDetectPaths: [".cursor"],
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: ["scopes"],
detectGlobalInstall: async () => {
return existsSync3(join2(home, ".cursor"));
},
transformConfig: transformCursorConfig
},
"factory-droid": {
name: "factory-droid",
displayName: "Factory Droid",
configPath: join2(home, ".factory", "mcp.json"),
localConfigPath: ".factory/mcp.json",
projectDetectPaths: [".factory"],
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: ["timeout"],
detectGlobalInstall: async () => {
return existsSync3(join2(home, ".factory"));
},
transformConfig: transformFactoryDroidConfig
},
fx: {
name: "fx",
displayName: "fx",
// fx reads MCP config only from its trusted global profile:
// https://fx.sh/docs/capabilities/mcp
configPath: join2(home, ".fx", "mcp.json"),
projectDetectPaths: [],
configKey: "mcp",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: ["bearerTokenEnv"],
detectGlobalInstall: async () => {
return existsSync3(join2(home, ".fx"));
},
transformConfig: transformFxConfig
},
"gemini-cli": {
name: "gemini-cli",
displayName: "Gemini CLI",
configPath: join2(home, ".gemini", "settings.json"),
localConfigPath: ".gemini/settings.json",
projectDetectPaths: [".gemini"],
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: ["timeout", "scopes"],
detectGlobalInstall: async () => {
return existsSync3(join2(home, ".gemini"));
},
transformConfig: transformGeminiConfig
},
goose: {
name: "goose",
displayName: "Goose",
configPath: gooseConfigPath,
projectDetectPaths: [],
// Global only - no project support
configKey: "extensions",
format: "yaml",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: [],
detectGlobalInstall: async () => {
return existsSync3(gooseConfigPath);
},
transformConfig: transformGooseConfig
},
"github-copilot-cli": {
name: "github-copilot-cli",
displayName: "GitHub Copilot CLI",
configPath: join2(copilotHome, "mcp-config.json"),
localConfigPath: ".mcp.json",
projectDetectPaths: [".mcp.json", ".github/mcp.json"],
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: [],
detectGlobalInstall: async () => {
return existsSync3(copilotHome);
},
resolveConfigPath: resolveGitHubCopilotCliConfigPath,
transformConfig: transformGitHubCopilotCliConfig
},
"grok-build": {
name: "grok-build",
displayName: "Grok Build",
configPath: join2(getGrokHome(), "config.toml"),
localConfigPath: ".grok/config.toml",
projectDetectPaths: [".grok"],
configKey: "mcp_servers",
format: "toml",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: ["timeout"],
detectGlobalInstall: async () => {
return existsSync3(getGrokHome());
},
resolveConfigPath: resolveGrokBuildConfigPath,
transformConfig: transformGrokBuildConfig
},
hermes: {
name: "hermes",
displayName: "Hermes Agent",
configPath: join2(getHermesRoot(), "config.yaml"),
projectDetectPaths: [],
configKey: "mcp_servers",
format: "yaml-1.1",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: ["timeout"],
detectGlobalInstall: async () => {
return existsSync3(getHermesRoot());
},
resolveConfigPath: () => join2(getHermesHome(), "config.yaml"),
transformConfig: transformHermesConfig
},
junie: {
name: "junie",
displayName: "Junie",
configPath: join2(getJunieHome(), "mcp", "mcp.json"),
localConfigPath: ".junie/mcp/mcp.json",
projectDetectPaths: [".junie"],
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: [],
detectGlobalInstall: async () => {
return existsSync3(getJunieHome());
},
resolveConfigPath: resolveJunieConfigPath,
transformConfig: transformJunieConfig
},
"kilo-code": {
name: "kilo-code",
displayName: "Kilo Code",
configPath: join2(getKiloConfigDir(), "kilo.json"),
localConfigPath: "kilo.json",
projectDetectPaths: [".kilo", ".kilocode", "kilo.json", "kilo.jsonc"],
configKey: "mcp",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: ["timeout"],
detectGlobalInstall: async () => {
return existsSync3(getKiloConfigDir()) || existsSync3(kiloVscodeStoragePath);
},
resolveConfigPath: resolveKiloCodeConfigPath,
transformConfig: transformKiloCodeConfig
},
"kimi-code": {
name: "kimi-code",
displayName: "Kimi Code",
configPath: join2(getKimiCodeHome(), "mcp.json"),
localConfigPath: ".kimi-code/mcp.json",
projectDetectPaths: [".kimi-code"],
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: ["timeout"],
detectGlobalInstall: async () => {
return existsSync3(getKimiCodeHome());
},
resolveConfigPath: resolveKimiCodeConfigPath,
transformConfig: transformKimiCodeConfig
},
"kiro-cli": {
name: "kiro-cli",
displayName: "Kiro CLI",
// Shared with the Kiro IDE, which reads the same two files.
configPath: join2(home, ".kiro", "settings", "mcp.json"),
localConfigPath: ".kiro/settings/mcp.json",
projectDetectPaths: [".kiro"],
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: ["timeout"],
detectGlobalInstall: async () => {
return existsSync3(join2(home, ".kiro"));
},
transformConfig: transformKiroCliConfig
},
mastracode: {
name: "mastracode",
displayName: "Mastra Code",
configPath: join2(getMastraCodeDir(), "mcp.json"),
localConfigPath: ".mastracode/mcp.json",
projectDetectPaths: [".mastracode"],
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: ["scopes"],
detectGlobalInstall: async () => {
return existsSync3(getMastraCodeDir());
},
transformConfig: transformMastraCodeConfig
},
mcporter: {
name: "mcporter",
displayName: "MCPorter",
configPath: join2(home, ".mcporter", "mcporter.json"),
localConfigPath: "config/mcporter.json",
projectDetectPaths: ["config/mcporter.json"],
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: [],
detectGlobalInstall: async () => {
return existsSync3(join2(home, ".mcporter"));
},
resolveConfigPath: resolveMcporterConfigPath,
transformConfig: transformStandardConfig
},
opencode: {
name: "opencode",
displayName: "OpenCode",
configPath: join2(home, ".config", "opencode", "opencode.jsonc"),
localConfigPath: "opencode.jsonc",
projectDetectPaths: ["opencode.jsonc", "opencode.json", ".opencode"],
configKey: "mcp",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: [],
detectGlobalInstall: async () => {
return existsSync3(join2(home, ".config", "opencode"));
},
resolveConfigPath: resolveOpenCodeConfigPath,
transformConfig: transformOpenCodeConfig
},
pi: {
name: "pi",
displayName: "Pi",
configPath: join2(getPiAgentDir(), "mcp.json"),
localConfigPath: ".pi/mcp.json",
projectDetectPaths: [".pi"],
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: ["timeout"],
detectGlobalInstall: async () => {
return existsSync3(getPiAgentDir());
},
resolveConfigPath: resolvePiConfigPath,
transformConfig: transformPiConfig
},
vscode: {
name: "vscode",
displayName: "VS Code",
configPath: join2(vscodePath, "mcp.json"),
localConfigPath: ".vscode/mcp.json",
projectDetectPaths: [".vscode"],
configKey: "servers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: [],
detectGlobalInstall: async () => {
return existsSync3(vscodePath);
},
transformConfig: transformStandardConfig
},
windsurf: {
name: "windsurf",
displayName: "Windsurf",
configPath: windsurfConfigPath,
projectDetectPaths: [],
// Global only - no project support
configKey: "mcpServers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: [],
detectGlobalInstall: async () => {
return existsSync3(join2(home, ".codeium", "windsurf"));
},
transformConfig: transformAntigravityConfig
},
zed: {
name: "zed",
displayName: "Zed",
configPath: process.platform === "darwin" || process.platform === "win32" ? join2(appSupport, "Zed", "settings.json") : join2(appSupport, "zed", "settings.json"),
localConfigPath: ".zed/settings.json",
projectDetectPaths: [".zed"],
configKey: "context_servers",
format: "json",
supportedTransports: ["stdio", "http", "sse"],
supportedFields: [],
detectGlobalInstall: async () => {
const configDir = process.platform === "darwin" || process.platform === "win32" ? join2(appSupport, "Zed") : join2(appSupport, "zed");
return existsSync3(configDir);
},
transformConfig: transformZedConfig
}
};
function getAgentTypes() {
return Object.keys(agents);
}
function supportsProjectConfig(agentType) {
return agents[agentType].localConfigPath !== void 0;
}
function getCommonInstallScopes(agentTypes) {
if (agentTypes.length === 0) return [];
if (agentTypes.some((agentType) => !supportsProjectConfig(agentType))) {
return ["global"];
}
return ["local", "global"];
}
function getProjectCapableAgents() {
return Object.keys(agents).filter(
(type) => supportsProjectConfig(type)
);
}
function detectProjectAgents(cwd) {
const dir = cwd || process.cwd();
const detected = [];
for (const [type, config] of Object.entries(agents)) {
if (!config.localConfigPath) continue;
for (const detectPath of config.projectDetectPaths) {
if (existsSync3(join2(dir, detectPath))) {
detected.push(type);
break;
}
}
}
return detected;
}
async function detectGlobalAgents() {
const detected = [];
for (const [type, config] of Object.entries(agents)) {
if (await config.detectGlobalInstall()) {
detected.push(type);
}
}
return detected;
}
function isTransportSupported(agentType, transport) {
return agents[agentType].supportedTransports.includes(transport);
}
function buildAgentSelectionChoices(options) {
const { availableAgents, detectedAgents, agentRouting, lastSelected } = options;
const detectedSet = new Set(detectedAgents);
const validLastSelected = lastSelected?.filter(
(agent) => availableAgents.includes(agent) && !detectedSet.has(agent)
) ?? [];
const remainingAgents = availableAgents.filter(
(agent) => !detectedSet.has(agent) && !validLastSelected.includes(agent)
);
const orderedAgents = [
...detectedAgents,
...validLastSelected,
...remainingAgents
];
const choices = orderedAgents.map((agentType) => {
const routing = agentRouting.get(agentType);
const baseHint = routing === "local" ? "project" : routing === "global" ? "global" : shortenPath(agents[agentType].configPath);
const lastSelectedHint = validLastSelected.includes(agentType) ? "selected last time" : "";
const hint = lastSelectedHint ? `${baseHint} \xB7 ${lastSelectedHint}` : baseHint;
return {
value: agentType,
label: agents[agentType].displayName,
hint
};
});
return { choices, initialValues: detectedAgents };
}
async function promptForAgents(message, choices, defaultToAll = false) {
let lastSelected;
try {
lastSelected = await getLastSelectedAgents();
} catch {
}
const validAgents = choices.map((c) => c.value);
let initialValues;
if (lastSelected && lastSelected.length > 0) {
initialValues = lastSelected.filter(
(a) => validAgents.includes(a)
);
if (initialValues.length === 0 && defaultToAll) {
initialValues = validAgents;
}
} else {
initialValues = defaultToAll ? validAgents : [];
}
const selected = await p.multiselect({
message,
options: choices,
required: true,
initialValues
});
if (!p.isCancel(selected)) {
try {
await saveSelectedAgents(selected);
} catch {
}
}
return selected;
}
async function selectAgentsInteractive(availableAgents, options) {
let lastSelected;
try {
lastSelected = await getLastSelectedAgents();
} catch {
}
const validLastSelected = lastSelected?.filter(
(a) => availableAgents.includes(a)
);
const selectOptions = [];
const hasPrevious = validLastSelected && validLastSelected.length > 0;
if (hasPrevious) {
const agentNames = validLastSelected.map((a) => agents[a].displayName).join(", ");
selectOptions.push({
value: "previous",
label: "Same as last time",
hint: agentNames
});
}
selectOptions.push({
value: "all",
label: hasPrevious ? "All available agents" : "All available agents",
hint: `Install to all ${availableAgents.length} available agents`
});
selectOptions.push({
value: "select",
label: "Select specific agents",
hint: "Choose which agents to install to"
});
const installChoice = await p.select({
message: "Install to",
options: selectOptions
});
if (p.isCancel(installChoice)) {
return installChoice;
}
if (installChoice === "all") {
return availableAgents;
}
if (installChoice === "previous" && validLastSelected) {
return validLastSelected;
}
const agentChoices = availableAgents.map((agentType) => {
const localPath = agents[agentType].localConfigPath;
const hint = options.global ? shortenPath(agents[agentType].configPath) : localPath ?? shortenPath(agents[agentType].configPath);
return {
value: agentType,
label: agents[agentType].displayName,
hint
};
});
return promptForAgents("Select agents to install to", agentChoices, false);
}
// src/source-parser.ts
function isUrl(input) {
return input.startsWith("http://") || input.startsWith("https://");
}
function looksLikePath(input) {
if (input.startsWith("/")) return true;
if (input.startsWith("~/") || input === "~") return true;
if (input.startsWith("./") || input.startsWith("../")) return true;
if (/^[A-Za-z]:[\\/]/.test(input)) return true;
return false;
}
function isCommand(input) {
if (looksLikePath(input)) {
return true;
}
if (input.includes(" ")) {
return true;
}
if (input.startsWith("npx ") || input.startsWith("node ") || input.startsWith("python ")) {
return true;
}
return false;
}
function isPackageName(input) {
if (input.startsWith("@") && input.includes("/")) {
return true;
}
if (/^[a-z0-9][\w.-]*(@[\w.-]+)?$/i.test(input)) {
return true;
}
return false;
}
var commonTlds = /* @__PURE__ */ new Set([
"com",
"org",
"net",
"io",
"dev",
"ai",
"tech",
"co",
"app",
"cloud",
"sh",
"run"
]);
function extractBrandFromHostname(hostname) {
const parts = hostname.split(".");
const meaningfulParts = parts.filter((part) => {
const lower = part.toLowerCase();
if (commonTlds.has(lower)) return false;
if (lower === "mcp" || lower === "api" || lower === "www") return false;
return true;
});
if (meaningfulParts.length > 0) {
return meaningfulParts[0];
}
if (parts.length >= 2) {
return parts[parts.length - 2];
}
return "mcp-server";
}
function inferName(input, type) {
if (type === "remote") {
try {
const url = new URL(input);
return extractBrandFromHostname(url.hostname);
} catch {
return "mcp-server";
}
}
if (type === "command") {
if (looksLikePath(input)) {
const segments = input.split(/[\\/]/);
const base = segments[segments.length - 1] || "mcp-server";
const withoutExt = base.replace(/\.[^.]+$/, "");
return extractPackageName(withoutExt || base);
}
const parts = input.split(" ");
let startIndex = 0;
if (parts[0] === "npx" || parts[0] === "node" || parts[0] === "python") {
startIndex = 1;
}
for (let i = startIndex; i < parts.length; i++) {
const part = parts[i];
if (part && !part.startsWith("-")) {
return extractPackageName(part);
}
}
return "mcp-server";
}
return extractPackageName(input);
}
function extractPackageName(input) {
let name = input;
const atIndex = name.lastIndexOf("@");
if (atIndex > 0 && !name.startsWith("@")) {
name = name.slice(0, atIndex);
} else if (name.startsWith("@") && name.indexOf("@", 1) > 0) {
const secondAt = name.indexOf("@", 1);
name = name.slice(0, secondAt);
}
if (name.startsWith("@") && name.includes("/")) {
const parts = name.split("/");
name = parts[1] || name;
}
name = name.replace(/^mcp-server-/, "");
name = name.replace(/^server-/, "");
name = name.replace(/-mcp$/, "");
return name || "mcp-server";
}
function parseSource(input) {
const trimmed = input.trim();
if (isUrl(trimmed)) {
return {
type: "remote",
value: trimmed,
inferredName: inferName(trimmed, "remote")
};
}
if (isCommand(trimmed)) {
return {
type: "command",
value: trimmed,
inferredName: inferName(trimmed, "command")
};
}
if (isPackageName(trimmed)) {
return {
type: "package",
value: trimmed,
inferredName: inferName(trimmed, "package")
};
}
return {
type: "package",
value: trimmed,
inferredName: inferName(trimmed, "package")
};
}
function isRemoteSource(parsed) {
return parsed.type === "remote";
}
// src/formats/yaml.ts
import { isDeepStrictEqual } from "util";
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
import { dirname as dirname4 } from "path";
import {
Document,
isAlias,
isMap,
isScalar,
LineCounter,
parseDocument
} from "yaml";
var PYYAML_SEXAGESIMAL_INT = /^[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+$/;
function isSexagesimalIntTag(tag) {
return typeof tag === "object" && !tag.collection && tag.tag === "tag:yaml.org,2002:int" && tag.format === "TIME";
}
function matchPyYamlTags(tags) {
return tags.map(
(tag) => isSexagesimalIntTag(tag) ? { ...tag, test: PYYAML_SEXAGESIMAL_INT } : tag
);
}
function yamlOptions(version) {
return version === "1.1" ? { version, merge: true, customTags: matchPyYamlTags } : { version, merge: true };
}
function parseYaml(source, filePath, version) {
const lineCounter = new LineCounter();
const doc = parseDocument(source, {
...yamlOptions(version),
prettyErrors: false,
logLevel: "error",
lineCounter
});
const [error] = doc.errors;
if (error) {
const { line, col } = lineCounter.linePos(error.pos[0]);
throw new Error(
`Invalid YAML in ${filePath} at line ${line}, column ${col} (${error.code}).`
);
}
return doc;
}
function toJSValue(filePath, convert) {
try {
return convert();
} catch (error) {
const reason = error instanceof ReferenceError ? "an alias could not be resolved" : "it could not be converted to a value";
throw new Error(`Invalid YAML in ${filePath}: ${reason}.`);
}
}
function parseYamlValue(source, filePath, version) {
const doc = parseYaml(source, filePath, version);
return toJSValue(filePath, () => doc.toJS());
}
function readYamlConfig(filePath, version) {
if (!existsSync4(filePath)) {
return {};
}
return parseYamlValue(readFileSync4(filePath, "utf-8"), filePath, version) || {};
}
function parseSource2(source, filePath, version) {
const doc = parseYaml(source, filePath, version);
if (doc.contents !== null && !isMap(doc.contents)) {
throw new Error(`${filePath} is not a YAML map.`);
}
return doc;
}
function isAliasOrMerge(node) {
return isAlias(node) || isMap(node) && node.items.some(
(pair) => isScalar(pair.key) && typeof pair.key.value === "symbol" && pair.key.value.description === "<<"
);
}
function getServersNode(doc, filePath, configKey) {
const keyPath = configKey.split(".");
const path = [
doc.contents,
...keyPath.map((_, i) => doc.getIn(keyPath.slice(0, i + 1), true))
];
const servers = path[path.length - 1];
if (path.some(isAliasOrMerge)) {
throw new Error(
`${configKey} in ${filePath} uses a YAML alias or merge key, which add-mcp does not edit. Change it by hand.`
);
}
if (servers === void 0 || isScalar(servers) && servers.value === null) {
return doc.hasIn(keyPath) ? null : void 0;
}
if (!isMap(servers)) {
throw new Error(`${configKey} in ${filePath} is not a YAML map.`);
}
if (servers.flow && servers.items.length > 0) {
throw new Error(
`${configKey} in ${filePath} is a flow-style map, which add-mcp does not edit. Change it by hand.`
);
}
return servers;
}
function lineStart(source, offset) {
return source.lastIndexOf("\n", offset - 1) + 1;
}
function lineEnd(source, offset) {
if (offset > 0 && source[offset - 1] === "\n") {
return offset;
}
const newline = source.indexOf("\n", offset);
return newline === -1 ? source.length : newline + 1;
}
function pairSpan(source, pair) {
const key = pair.key;
const value = pair.value;
const end = value?.range ? value.range[2] : key.range[2];
return [lineStart(source, key.range[0]), lineEnd(source, end)];
}
function lineEnding(source) {
return source.includes("\r\n") ? "\r\n" : "\n";
}
function render(value, version, eol, indent, unit = 2) {
const text = new Document(value, yamlOptions(version)).toString({
lineWidth: 0,
indent: unit
});
const pad = " ".repeat(indent);
return text.replace(/^(?=.)/gm, pad).replace(/\n/g, eol);
}
function withTrailingNewline(source, eol) {
return source === "" || source.endsWith("\n") ? source : source + eol;
}
function serversKeyLine(doc, source, configKey) {
const keyPath = configKey.split(".");
const parent = keyPath.length > 1 ? doc.getIn(keyPath.slice(0, -1), true) : doc.contents;
const pair = parent.items.find(
(item) => isScalar(item.key) && item.key.value === keyPath[keyPath.length - 1]
);
const keyRange = pair.key.range;
const start = lineStart(source, keyRange[0]);
const end = lineEnd(source, keyRange[2]);
const line = source.slice(start, end).replace(
/:[ \t]*(?:\{[ \t]*\}|~|null|Null|NULL)?[ \t]*(#[^\r\n]*)?(?:\r?\n)?$/,
(_match, comment) => comment ? `: ${comment}${lineEnding(source)}` : `:${lineEnding(source)}`
);
return { start, end, indent: keyRange[0] - start, line };
}
function upsertServer(source, filePath, configKey, name, entry, version) {
const doc = parseSource2(source, filePath, version);
const servers = getServersNode(doc, filePath, configKey);
const eol = lineEnding(source);
if (servers === void 0) {
const nested = configKey.split(".").reduceRight((inner, key) => ({ [key]: inner }), {
[name]: entry
});
return withTrailingNewline(source, eol) + render(nested, version, eol, 0);
}
if (servers === null || servers.items.length === 0) {
const key = serversKeyLine(doc, source, configKey);
return source.slice(0, key.start) + key.line + render({ [name]: entry }, version, eol, key.indent + 2) + source.slice(key.end);
}
const first = servers.items[0].key;
const indent = first.range[0] - lineStart(source, first.range[0]);
const unit = indent - serversKeyLine(doc, source, configKey).indent;
const snippet = render({ [name]: entry }, version, eol, indent, unit);
const existing = servers.items.find(
(pair) => isScalar(pair.key) && pair.key.value === name
);
if (existing) {
const [start, end2] = pairSpan(source, existing);
return source.slice(0, start) + snippet + source.slice(end2);
}
const [, end] = pairSpan(source, servers.items[servers.items.length - 1]);
return withTrailingNewline(source.slice(0, end), eol) + snippet + source.slice(end);
}
function verifyEdit(updated, filePath, keyPath, expected, version) {
const after = parseYamlValue(updated, filePath, version);
const servers = getNestedValue(after ?? {}, keyPath.join("."));
if (!isDeepStrictEqual(servers, expected)) {
throw new Error(
`add-mcp could not edit ${filePath} safely. The file was not changed.`
);
}
}
function removeYamlConfigKey(filePath, configKey, serverName, version) {
if (!existsSync4(filePath)) {
return;
}
const source = readFileSync4(filePath, "utf-8");
const doc = parseSource2(source, filePath, version);
const servers = getServersNode(doc, filePath, configKey);
const pair = servers?.items.find(
(item) => isScalar(item.key) && item.key.value === serverName
);
if (!servers || !pair) {
return;
}
const [start, end] = pairSpan(source, pair);
let updated = source.slice(0, start) + source.slice(end);
if (servers.items.length === 1) {
const key = serversKeyLine(doc, source, configKey);
updated = source.slice(0, key.start) + key.line.replace(
/:( #[^\r\n]*)?\r?\n$/,
(_match, comment = "") => `: {}${comment}${lineEnding(source)}`
) + source.slice(key.end, start) + source.slice(end);
}
const expected = {
...toJSValue(filePath, () => servers.toJS(doc))
};
delete expected[serverName];
verifyEdit(updated, filePath, configKey.split("."), expected, version);
writeFileSync2(filePath, updated);
}
function writeYamlConfig(filePath, config, configKey, version) {
const dir = dirname4(filePath);
if (!existsSync4(dir)) {
mkdirSync2(dir, { recursive: true });
}
const original = existsSync4(filePath) ? readFileSync4(filePath, "utf-8") : "";
const keyPath = configKey.split(".");
const before = getNestedValue(
parseYamlValue(original, filePath, version) ?? {},
configKey
);
const expected = {
...before && typeof before === "object" ? before : {}
};
let updated = original;
const incoming = getNestedValue(config, configKey);
for (const [name, entry] of Object.entries(incoming)) {
updated = upsertServer(updated, filePath, configKey, name, entry, version);
expected[name] = entry;
}
verifyEdit(updated, filePath, keyPath, expected, version);
writeFileSync2(filePath, updated);
}
// src/formats/toml.ts
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
import { dirname as dirname5 } from "path";
import * as TOML from "@iarna/toml";
function readTomlConfig(filePath) {
if (!existsSync5(filePath)) {
return {};
}
const content = readFileSync5(filePath, "utf-8");
const parsed = TOML.parse(content);
return parsed;
}
function removeTomlConfigKey(filePath, configKey, serverName) {
if (!existsSync5(filePath)) {
return;
}
const existing = readTomlConfig(filePath);
const keys = configKey.split(".");
let current = existing;
for (const key of keys) {
if (current && typeof current === "object" && key in current) {
current = current[key];
} else {
return;
}
}
if (current && typeof current === "object" && serverName in current) {
delete current[serverName];
}
const content = TOML.stringify(existing);
writeFileSync3(filePath, content);
}
function writeTomlConfig(filePath, config, configKey) {
const dir = dirname5(filePath);
if (!existsSync5(dir)) {
mkdirSync3(dir, { recursive: true });
}
let existingConfig = {};
if (existsSync5(filePath)) {
existingConfig = readTomlConfig(filePath);
}
dropReplacedServers(existingConfig, config, configKey);
const mergedConfig = deepMerge(existingConfig, config);
const content = TOML.stringify(mergedConfig);
writeFileSync3(filePath, content);
}
// src/formats/index.ts
function readConfig2(filePath, format) {
switch (format) {
case "json":
return readJsonConfig(filePath);
case "yaml":
return readYamlConfig(filePath, "1.2");
case "yaml-1.1":
return readYamlConfig(filePath, "1.1");
case "toml":
return readTomlConfig(filePath);
default:
throw new Error(`Unsupported config format: ${format}`);
}
}
function removeServerFromConfig(filePath, format, configKey, serverName) {
switch (format) {
case "json":
removeJsonConfigKey(filePath, configKey, serverName);
break;
case "yaml":
removeYamlConfigKey(filePath, configKey, serverName, "1.2");
break;
case "yaml-1.1":
removeYamlConfigKey(filePath, configKey, serverName, "1.1");
break;
case "toml":
removeTomlConfigKey(filePath, configKey, serverName);
break;
default:
throw new Error(`Unsupported config format: ${format}`);
}
}
function writeConfig2(filePath, config, format, configKey) {
switch (format) {
case "json":
writeJsonConfig(filePath, config, configKey);
break;
case "yaml":
writeYamlConfig(filePath, config, configKey, "1.2");
break;
case "yaml-1.1":
writeYamlConfig(filePath, config, configKey, "1.1");
break;
case "toml":
writeTomlConfig(filePath, config, configKey);
break;
default:
throw new Error(`Unsupported config format: ${format}`);
}
}
function buildConfigWithKey(configKey, serverName, serverConfig) {
if (configKey === ROOT_CONFIG_KEY) {
return { [serverName]: serverConfig };
}
const config = {};
const servers = {};
servers[serverName] = serverConfig;
setNestedValue(config, configKey, servers);
return config;
}
// src/installer.ts
import {
chmodSync,
existsSync as existsSync6,
mkdirSync as mkdirSync4,
readFileSync as readFileSync6,
writeFileSync as writeFileSync4
} from "fs";
import { homedir as homedir3 } from "os";
import { join as join3, dirname as dirname6, isAbsolute, relative, sep } from "path";
var home2 = homedir3();
function buildServerConfig(parsed, options = {}) {
if (parsed.type === "remote") {
const config2 = {
type: options.transport ?? "http",
url: parsed.value
};
if (options.headers && Object.keys(options.headers).length > 0) {
config2.headers = options.headers;
}
if (typeof options.timeout === "number") {
config2.timeout = options.timeout;
}
if (options.oauthScopes && options.oauthScopes.length > 0) {
config2.oauthScopes = options.oauthScopes;
}
if (options.autoApproveTools) {
config2.autoApproveTools = options.autoApproveTools;
}
if (typeof options.bearerTokenEnv === "string") {
const name = options.bearerTokenEnv.trim();
if (name.length > 0) {
config2.bearerTokenEnv = name;
}
}
return config2;
}
if (parsed.type === "command") {
let command;
let parsedArgs;
if (looksLikePath(parsed.value)) {
command = parsed.value;
parsedArgs = [];
} else {
const parts = parsed.value.split(" ");
command = parts[0];
parsedArgs = parts.slice(1);
}
const args = [...parsedArgs, ...options.args ?? []];
const config2 = {
command,
args
};
if (options.env && Object.keys(options.env).length > 0) {
config2.env = options.env;
}
if (options.autoApproveTools) {
config2.autoApproveTools = options.autoApproveTools;
}
return config2;
}
const config = {
command: "npx",
args: ["-y", parsed.value, ...options.args ?? []]
};
if (options.env && Object.keys(options.env).length > 0) {
config.env = options.env;
}
if (options.autoApproveTools) {
config.autoApproveTools = options.autoApproveTools;
}
return config;
}
function readJsonObject(filePath) {
if (!existsSync6(filePath)) {
return {};
}
const parsed = JSON.parse(readFileSync6(filePath, "utf-8"));
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
}
function writeJsonObject(filePath, value) {
const dir = dirname6(filePath);
if (!existsSync6(dir)) {
mkdirSync4(dir, { recursive: true });
}
writeFileSync4(filePath, `${JSON.stringify(value, null, 2)}
`, "utf-8");
}
function addUniqueStrings(existing, additions) {
const values = Array.isArray(existing) ? existing.filter((value) => typeof value === "string") : [];
for (const addition of additions) {
if (!values.includes(addition)) {
values.push(addition);
}
}
return values;
}
function claudeAutoApproveRules(serverName, tools) {
if (tools.length === 0) {
return [`mcp__${serverName}`];
}
return tools.map((tool) => `mcp__${serverName}__${tool}`);
}
function getClaudeCodeSettingsPath(options = {}) {
const local = Boolean(options.local);
const cwd = options.cwd || process.cwd();
return local ? join3(cwd, ".claude", "settings.local.json") : join3(home2, ".claude", "settings.json");
}
function installClaudeCodeAutoApproval(serverName, tools, options = {}) {
const settingsPath = getClaudeCodeSettingsPath(options);
try {
const settings = readJsonObject(settingsPath);
const permissions = settings.permissions && typeof settings.permissions === "object" && !Array.isArray(settings.permissions) ? settings.permissions : {};
permissions.allow = addUniqueStrings(
permissions.allow,
claudeAutoApproveRules(serverName, tools)
);
settings.permissions = permissions;
writeJsonObject(settingsPath, settings);
return { success: true, path: settingsPath };
} catch (error) {
return {
success: false,
path: settingsPath,
error: error instanceof Error ? error.message : "Unknown error"
};
}
}
function updateGitignoreWithPaths(paths, options = {}) {
const cwd = options.cwd || process.cwd();
const gitignorePath = join3(cwd, ".gitignore");
const existingContent = existsSync6(gitignorePath) ? readFileSync6(gitignorePath, "utf-8") : "";
const existingEntries = new Set(
existingContent.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0)
);
const entriesToAdd = [];
for (const filePath of paths) {
const relativePath = isAbsolute(filePath) ? relative(cwd, filePath) : filePath;
if (!relativePath || relativePath.startsWith("..") || isAbsolute(relativePath)) {
continue;
}
const normalizedPath = relativePath.split(sep).join("/");
const cleanPath = normalizedPath.startsWith("./") ? normalizedPath.slice(2) : normalizedPath;
if (!cleanPath || cleanPath === ".gitignore" || existingEntries.has(cleanPath)) {
continue;
}
existingEntries.add(cleanPath);
entriesToAdd.push(cleanPath);
}
if (entriesToAdd.length > 0) {
let nextContent = existingContent;
if (nextContent.length > 0 && !nextContent.endsWith("\n")) {
nextContent += "\n";
}
nextContent += `${entriesToAdd.join("\n")}
`;
writeFileSync4(gitignorePath, nextContent, "utf-8");
}
return {
path: gitignorePath,
added: entriesToAdd
};
}
function getConfigPath2(agent, options = {}) {
const local = Boolean(options.local);
const cwd = options.cwd || process.cwd();
if (agent.resolveConfigPath) {
return agent.resolveConfigPath(agent, { local, cwd });
}
if (local && agent.localConfigPath) {
return join3(cwd, agent.localConfigPath);
}
return agent.configPath;
}
function getConfigKey(agent, options = {}) {
if (options.local && agent.name === "github-copilot-cli") {
return githubCopilotCliProjectConfigKey(getConfigPath2(agent, options));
}
if (options.local && agent.localConfigKey) {
return agent.localConfigKey;
}
return agent.configKey;
}
function rewriteCopilotCliConfig(agentType, configPath) {
if (agentType === "github-copilot-cli") {
rewriteJsoncAsJson(configPath);
}
}
function claudeCopilotGithubShadowError(cwd) {
if (existsSync6(join3(cwd, ".mcp.json"))) {
return null;
}
if (!existsSync6(join3(cwd, ".github", "mcp.json"))) {
return null;
}
return "No changes made for Claude Code or GitHub Copilot CLI. Merge the servers from `.github/mcp.json` into `.mcp.json` under `mcpServers`, resolve duplicate names, then rerun this command.";
}
function copilotClaudeGithubLayoutError(agentTypes, options = {}) {
const cwd = options.cwd || process.cwd();
const copilotLocal = options.routing?.get("github-copilot-cli") === "local";
const claudeLocal = options.routing?.get("claude-code") === "local";
if (!agentTypes.includes("github-copilot-cli") || !agentTypes.includes("claude-code") || !copilotLocal || !claudeLocal) {
return null;
}
return claudeCopilotGithubShadowError(cwd);
}
function installServerForAgent(serverName, serverConfig, agentType, options = {}) {
const agent = agents[agentType];
let configPath = "";
try {
configPath = getConfigPath2(agent, options);
if (typeof serverConfig.bearerTokenEnv === "string") {
const name = serverConfig.bearerTokenEnv.trim();
if (!isBearerTokenEnvName(name)) {
return {
success: false,
path: configPath,
error: invalidBearerTokenEnvMessage(serverConfig.bearerTokenEnv)
};
}
}
const { config: gatedConfig, dropped } = applyFieldSupport(
serverConfig,
agent.supportedFields
);
const configKey = agentType === "opencode" ? resolveOpenCodeUpsertKey(configPath, serverName) : getConfigKey(agent, options);
const transformedConfig = agent.transformConfig(serverName, gatedConfig, {
local: Boolean(options.local),
configKey
});
if (options.local && agentType === "claude-code") {
const layoutError = claudeCopilotGithubShadowError(
options.cwd || process.cwd()
);
if (layoutError) {
return {
success: false,
path: configPath,
error: layoutError
};
}
}
const dir = dirname6(configPath);
if (!existsSync6(dir)) {
mkdirSync4(dir, { recursive: true });
}
const config = buildConfigWithKey(configKey, serverName, transformedConfig);
writeConfig2(configPath, config, agent.format, configKey);
rewriteCopilotCliConfig(agentType, configPath);
if (agentType === "fx") {
chmodSync(dirname6(configPath), 448);
chmodSync(configPath, 384);
}
const extraPaths = [];
if (agentType === "claude-code" && gatedConfig.autoApproveTools) {
const approval = installClaudeCodeAutoApproval(
serverName,
gatedConfig.autoApproveTools,
options
);
if (!approval.success) {
return {
success: false,
path: approval.path,
error: approval.error
};
}
extraPaths.push(approval.path);
}
return {
success: true,
path: configPath,
...dropped.length > 0 ? { droppedFields: dropped } : {},
...extraPaths.length > 0 ? { extraPaths } : {}
};
} catch (error) {
return {
success: false,
path: configPath,
error: error instanceof Error ? error.message : "Unknown error"
};
}
}
function installServer(serverName, serverConfig, agentTypes, options = {}) {
const results = /* @__PURE__ */ new Map();
const layoutError = copilotClaudeGithubLayoutError(agentTypes, options);
for (const agentType of agentTypes) {
if (layoutError && (agentType === "github-copilot-cli" || agentType === "claude-code")) {
results.set(agentType, {
success: false,
path: "",
error: layoutError
});
continue;
}
const routing = options.routing?.get(agentType);
const installOptions = {
local: routing === "local",
cwd: options.cwd
};
const result = installServerForAgent(
serverName,
serverConfig,
agentType,
installOptions
);
results.set(agentType, result);
}
return results;
}
// src/reader.ts
function normalizeStoredCommand(serverConfig) {
const commandParts = Array.isArray(serverConfig.command) ? serverConfig.command.filter((a) => typeof a === "string") : void 0;
const command = typeof serverConfig.command === "string" ? serverConfig.command : typeof serverConfig.cmd === "string" ? serverConfig.cmd : commandParts?.[0];
const args = Array.isArray(serverConfig.args) ? serverConfig.args.filter((a) => typeof a === "string") : commandParts?.slice(1) ?? [];
return { command, args };
}
function extractServerIdentity(serverConfig) {
for (const key of ["httpUrl", "url", "uri", "serverUrl"]) {
const value = serverConfig[key];
if (typeof value === "string" && value.length > 0) {
return value;
}
}
const { command, args: rawArgs } = normalizeStoredCommand(serverConfig);
if (!command) {
return "";
}
if (command === "npx" || command === "bunx") {
const yIndex = rawArgs.indexOf("-y");
const pkgIndex = yIndex >= 0 ? yIndex + 1 : 0;
const pkg = rawArgs[pkgIndex];
if (pkg && !pkg.startsWith("-")) {
return pkg;
}
}
if (rawArgs.length > 0) {
return `${command} ${rawArgs.join(" ")}`;
}
return command;
}
function readServersForAgent(agentType, options) {
const agent = agents[agentType];
const installOptions = {
local: options.scope === "local",
cwd: options.cwd
};
const configPath = getConfigPath2(agent, installOptions);
if (agentType === "opencode") {
const servers2 = listOpenCodeServers(configPath).map(
(entry) => ({
serverName: entry.serverName,
config: entry.config,
identity: extractServerIdentity(entry.config),
agentType,
scope: options.scope,
configPath,
configKey: entry.configKey
})
);
return {
agentType,
displayName: agent.displayName,
detected: true,
scope: options.scope,
configPath,
servers: servers2
};
}
const configKey = getConfigKey(agent, installOptions);
const fullConfig = readConfig2(configPath, agent.format);
const serversObj = getNestedValue(fullConfig, configKey);
const servers = [];
if (serversObj && typeof serversObj === "object" && !Array.isArray(serversObj)) {
for (const [serverName, serverConfig] of Object.entries(serversObj)) {
if (serverConfig && typeof serverConfig === "object") {
const config = serverConfig;
servers.push({
serverName,
config,
identity: extractServerIdentity(config),
agentType,
scope: options.scope,
configPath,
configKey
});
}
}
}
return {
agentType,
displayName: agent.displayName,
detected: true,
scope: options.scope,
configPath,
servers
};
}
function readServersForAgentSafe(agentType, options) {
const agent = agents[agentType];
let configPath = "";
try {
configPath = getConfigPath2(agent, {
local: options.scope === "local",
cwd: options.cwd
});
return readServersForAgent(agentType, options);
} catch (error) {
return {
agentType,
displayName: agent.displayName,
detected: true,
scope: options.scope,
configPath,
servers: [],
error: error instanceof Error ? error.message : String(error)
};
}
}
async function listInstalledServers(options) {
const scope = options.global ? "global" : "local";
const results = [];
if (options.agents && options.agents.length > 0) {
const detectedSet = new Set(
options.global ? await detectGlobalAgents() : detectProjectAgents(options.cwd)
);
for (const agentType of options.agents) {
const detected = detectedSet.has(agentType);
if (detected) {
results.push(
readServersForAgentSafe(agentType, { scope, cwd: options.cwd })
);
} else {
const agent = agents[agentType];
let configPath = "";
let error;
try {
configPath = getConfigPath2(agent, {
local: scope === "local",
cwd: options.cwd
});
} catch (resolveError) {
error = resolveError instanceof Error ? resolveError.message : String(resolveError);
}
results.push({
agentType,
displayName: agent.displayName,
detected: false,
scope,
configPath,
servers: [],
...error ? { error } : {}
});
}
}
} else {
const detected = options.global ? await detectGlobalAgents() : detectProjectAgents(options.cwd);
for (const agentType of detected) {
results.push(
readServersForAgentSafe(agentType, { scope, cwd: options.cwd })
);
}
}
return results;
}
function findMatchingServers(agentServersList, query) {
const lowerQuery = query.toLowerCase();
const matches = [];
for (const agentServers of agentServersList) {
for (const server of agentServers.servers) {
const nameMatch = server.serverName.toLowerCase().includes(lowerQuery);
const identityMatch = server.identity === query;
if (nameMatch || identityMatch) {
matches.push(server);
}
}
}
return matches;
}
export {
DEFAULT_FIND_REGISTRY_URL,
DEFAULT_FIND_REGISTRY_LABEL,
getConfigPath,
getLastSelectedAgents,
getFindRegistries,
saveFindRegistries,
isBearerTokenEnvName,
describeOptionalField,
getNestedValue,
listOpenCodeServers,
relocateOpenCodeServer,
removeOpenCodeServer,
agents,
getAgentTypes,
supportsProjectConfig,
getCommonInstallScopes,
getProjectCapableAgents,
detectProjectAgents,
detectGlobalAgents,
isTransportSupported,
buildAgentSelectionChoices,
selectAgentsInteractive,
parseSource,
isRemoteSource,
readConfig2 as readConfig,
removeServerFromConfig,
buildServerConfig,
updateGitignoreWithPaths,
getConfigPath2,
getConfigKey,
rewriteCopilotCliConfig,
claudeCopilotGithubShadowError,
installServerForAgent,
installServer,
normalizeStoredCommand,
extractServerIdentity,
listInstalledServers,
findMatchingServers
};