add-mcp 1.7.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +71 -1
  2. package/dist/index.js +655 -5
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -89,6 +89,10 @@ Besides the implicit add command, `add-mcp` also supports the following commands
89
89
  | ------------- | ------------------------------------------------------------ |
90
90
  | `find` | Search MCP registry servers and install a selected match |
91
91
  | `search` | Alias for `find` |
92
+ | `list` | List installed MCP servers across detected agents |
93
+ | `remove` | Remove an MCP server from agent configurations |
94
+ | `sync` | Synchronize server names and installations across agents |
95
+ | `unify` | Alias for `sync` |
92
96
  | `list-agents` | List all supported coding agents with scope (project/global) |
93
97
 
94
98
  ## Add Command
@@ -219,7 +223,7 @@ If you run with `-y` before this one-time registry setup is completed, the CLI e
219
223
 
220
224
  | Registry | Base URL | Description |
221
225
  | ------------------------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
222
- | **add-mcp curated registry** | `https://mcp.agent-tooling.dev/api/v1/servers` | A curated list of first-party, verified MCP servers from popular developer tools and SaaS services. Designed to surface high-quality, officially maintained servers instead of a long tail of unmaintained or third-party entries. |
226
+ | **add-mcp curated registry** | `https://mcp.agent-tooling.dev/api/v1/servers` | A curated list of first-party, verified MCP servers from popular developer tools and SaaS services. Designed to surface high-quality, officially maintained servers instead of a long tail of unmaintained or third-party entries. |
223
227
  | **Official Anthropic registry** | `https://registry.modelcontextprotocol.io/v0.1/servers` | The community-driven MCP server registry maintained by Anthropic. Contains the broadest catalog of MCP servers. |
224
228
 
225
229
  ### Missing A Server in add-mcp Curated Registry?
@@ -289,6 +293,72 @@ To add your own registry, append an entry to `findRegistries` in `~/.config/add-
289
293
  }
290
294
  ```
291
295
 
296
+ ## List Command
297
+
298
+ List installed MCP servers across detected agents:
299
+
300
+ ```bash
301
+ # List servers for all detected agents in the project
302
+ npx add-mcp list
303
+
304
+ # List global server configs
305
+ npx add-mcp list -g
306
+
307
+ # List servers for a specific agent (shown even if not detected)
308
+ npx add-mcp list -a cursor
309
+ ```
310
+
311
+ | Option | Description |
312
+ | --------------------- | -------------------------------------- |
313
+ | `-g, --global` | List global configs instead of project |
314
+ | `-a, --agent <agent>` | Filter to specific agent(s) |
315
+
316
+ ## Remove Command
317
+
318
+ Remove an MCP server from agent configurations by server name, URL, or package name:
319
+
320
+ ```bash
321
+ # Remove by server name (interactive selection by default)
322
+ npx add-mcp remove neon
323
+
324
+ # Remove all matches without prompting
325
+ npx add-mcp remove neon -y
326
+
327
+ # Remove by URL
328
+ npx add-mcp remove https://mcp.neon.tech/mcp -y
329
+
330
+ # Remove from global configs for a specific agent
331
+ npx add-mcp remove neon -g -a cursor -y
332
+ ```
333
+
334
+ | Option | Description |
335
+ | --------------------- | ------------------------------------ |
336
+ | `-g, --global` | Remove from global configs |
337
+ | `-a, --agent <agent>` | Filter to specific agent(s) |
338
+ | `-y, --yes` | Remove all matches without prompting |
339
+
340
+ ## Sync Command
341
+
342
+ Synchronize server names and installations across all detected agents. Servers are grouped by URL or package name, and each group is unified to the shortest server name. Servers with conflicting headers, env, or args across agents are skipped with a warning.
343
+
344
+ ```bash
345
+ # Sync project-level configs (interactive confirmation)
346
+ npx add-mcp sync
347
+
348
+ # Sync without prompting
349
+ npx add-mcp sync -y
350
+
351
+ # Sync global configs
352
+ npx add-mcp sync -g -y
353
+ ```
354
+
355
+ | Option | Description |
356
+ | -------------- | -------------------------------------- |
357
+ | `-g, --global` | Sync global configs instead of project |
358
+ | `-y, --yes` | Skip confirmation prompts |
359
+
360
+ `unify` is an alias for `sync`.
361
+
292
362
  ## Troubleshooting
293
363
 
294
364
  ### Server not loading
package/dist/index.js CHANGED
@@ -1322,6 +1322,14 @@ function detectIndent(text2) {
1322
1322
  });
1323
1323
  return result || { tabSize: 2, insertSpaces: true };
1324
1324
  }
1325
+ function readJsonConfig(filePath) {
1326
+ if (!existsSync2(filePath)) {
1327
+ return {};
1328
+ }
1329
+ const content = readFileSync(filePath, "utf-8");
1330
+ const parsed = jsonc.parse(content);
1331
+ return parsed;
1332
+ }
1325
1333
  function writeJsonConfig(filePath, config, configKey) {
1326
1334
  const dir = dirname3(filePath);
1327
1335
  if (!existsSync2(dir)) {
@@ -1349,6 +1357,31 @@ function writeJsonConfig(filePath, config, configKey) {
1349
1357
  }
1350
1358
  writeFileSync(filePath, JSON.stringify(mergedConfig, null, 2));
1351
1359
  }
1360
+ function removeJsonConfigKey(filePath, configKey, serverName) {
1361
+ if (!existsSync2(filePath)) {
1362
+ return;
1363
+ }
1364
+ const originalContent = readFileSync(filePath, "utf-8");
1365
+ const configKeyPath = configKey.split(".");
1366
+ try {
1367
+ const edits = jsonc.modify(
1368
+ originalContent,
1369
+ [...configKeyPath, serverName],
1370
+ void 0,
1371
+ { formattingOptions: detectIndent(originalContent) }
1372
+ );
1373
+ const updatedContent = jsonc.applyEdits(originalContent, edits);
1374
+ writeFileSync(filePath, updatedContent);
1375
+ return;
1376
+ } catch {
1377
+ }
1378
+ const parsed = jsonc.parse(originalContent);
1379
+ const servers = getNestedValue(parsed, configKey);
1380
+ if (servers && typeof servers === "object" && serverName in servers) {
1381
+ delete servers[serverName];
1382
+ }
1383
+ writeFileSync(filePath, JSON.stringify(parsed, null, 2));
1384
+ }
1352
1385
  function setNestedValue(obj, path, value) {
1353
1386
  const keys = path.split(".");
1354
1387
  const lastKey = keys.pop();
@@ -1375,6 +1408,30 @@ function readYamlConfig(filePath) {
1375
1408
  const parsed = yaml.load(content);
1376
1409
  return parsed || {};
1377
1410
  }
1411
+ function removeYamlConfigKey(filePath, configKey, serverName) {
1412
+ if (!existsSync3(filePath)) {
1413
+ return;
1414
+ }
1415
+ const existing = readYamlConfig(filePath);
1416
+ const keys = configKey.split(".");
1417
+ let current = existing;
1418
+ for (const key of keys) {
1419
+ if (current && typeof current === "object" && key in current) {
1420
+ current = current[key];
1421
+ } else {
1422
+ return;
1423
+ }
1424
+ }
1425
+ if (current && typeof current === "object" && serverName in current) {
1426
+ delete current[serverName];
1427
+ }
1428
+ const content = yaml.dump(existing, {
1429
+ indent: 2,
1430
+ lineWidth: -1,
1431
+ noRefs: true
1432
+ });
1433
+ writeFileSync2(filePath, content);
1434
+ }
1378
1435
  function writeYamlConfig(filePath, config) {
1379
1436
  const dir = dirname4(filePath);
1380
1437
  if (!existsSync3(dir)) {
@@ -1405,6 +1462,26 @@ function readTomlConfig(filePath) {
1405
1462
  const parsed = TOML.parse(content);
1406
1463
  return parsed;
1407
1464
  }
1465
+ function removeTomlConfigKey(filePath, configKey, serverName) {
1466
+ if (!existsSync4(filePath)) {
1467
+ return;
1468
+ }
1469
+ const existing = readTomlConfig(filePath);
1470
+ const keys = configKey.split(".");
1471
+ let current = existing;
1472
+ for (const key of keys) {
1473
+ if (current && typeof current === "object" && key in current) {
1474
+ current = current[key];
1475
+ } else {
1476
+ return;
1477
+ }
1478
+ }
1479
+ if (current && typeof current === "object" && serverName in current) {
1480
+ delete current[serverName];
1481
+ }
1482
+ const content = TOML.stringify(existing);
1483
+ writeFileSync3(filePath, content);
1484
+ }
1408
1485
  function writeTomlConfig(filePath, config) {
1409
1486
  const dir = dirname5(filePath);
1410
1487
  if (!existsSync4(dir)) {
@@ -1420,6 +1497,33 @@ function writeTomlConfig(filePath, config) {
1420
1497
  }
1421
1498
 
1422
1499
  // src/formats/index.ts
1500
+ function readConfig2(filePath, format) {
1501
+ switch (format) {
1502
+ case "json":
1503
+ return readJsonConfig(filePath);
1504
+ case "yaml":
1505
+ return readYamlConfig(filePath);
1506
+ case "toml":
1507
+ return readTomlConfig(filePath);
1508
+ default:
1509
+ throw new Error(`Unsupported config format: ${format}`);
1510
+ }
1511
+ }
1512
+ function removeServerFromConfig(filePath, format, configKey, serverName) {
1513
+ switch (format) {
1514
+ case "json":
1515
+ removeJsonConfigKey(filePath, configKey, serverName);
1516
+ break;
1517
+ case "yaml":
1518
+ removeYamlConfigKey(filePath, configKey, serverName);
1519
+ break;
1520
+ case "toml":
1521
+ removeTomlConfigKey(filePath, configKey, serverName);
1522
+ break;
1523
+ default:
1524
+ throw new Error(`Unsupported config format: ${format}`);
1525
+ }
1526
+ }
1423
1527
  function writeConfig2(filePath, config, format, configKey) {
1424
1528
  switch (format) {
1425
1529
  case "json":
@@ -1574,10 +1678,126 @@ function installServer(serverName, serverConfig, agentTypes, options = {}) {
1574
1678
  return results;
1575
1679
  }
1576
1680
 
1681
+ // src/reader.ts
1682
+ function extractServerIdentity(serverConfig) {
1683
+ for (const key of ["url", "uri", "serverUrl"]) {
1684
+ const value = serverConfig[key];
1685
+ if (typeof value === "string" && value.length > 0) {
1686
+ return value;
1687
+ }
1688
+ }
1689
+ const command = typeof serverConfig.command === "string" ? serverConfig.command : typeof serverConfig.cmd === "string" ? serverConfig.cmd : void 0;
1690
+ if (!command) {
1691
+ return "";
1692
+ }
1693
+ const rawArgs = Array.isArray(serverConfig.args) ? serverConfig.args.filter((a) => typeof a === "string") : Array.isArray(serverConfig.command) ? serverConfig.command.slice(1).filter((a) => typeof a === "string") : [];
1694
+ if (command === "npx" || command === "bunx") {
1695
+ const yIndex = rawArgs.indexOf("-y");
1696
+ const pkgIndex = yIndex >= 0 ? yIndex + 1 : 0;
1697
+ const pkg = rawArgs[pkgIndex];
1698
+ if (pkg && !pkg.startsWith("-")) {
1699
+ return pkg;
1700
+ }
1701
+ }
1702
+ if (Array.isArray(serverConfig.command)) {
1703
+ return serverConfig.command.join(" ");
1704
+ }
1705
+ if (rawArgs.length > 0) {
1706
+ return `${command} ${rawArgs.join(" ")}`;
1707
+ }
1708
+ return command;
1709
+ }
1710
+ function readServersForAgent(agentType, options) {
1711
+ const agent = agents[agentType];
1712
+ const installOptions = {
1713
+ local: options.scope === "local",
1714
+ cwd: options.cwd
1715
+ };
1716
+ const configPath = getConfigPath2(agent, installOptions);
1717
+ const configKey = getConfigKey(agent, installOptions);
1718
+ const fullConfig = readConfig2(configPath, agent.format);
1719
+ const serversObj = getNestedValue(fullConfig, configKey);
1720
+ const servers = [];
1721
+ if (serversObj && typeof serversObj === "object" && !Array.isArray(serversObj)) {
1722
+ for (const [serverName, serverConfig] of Object.entries(serversObj)) {
1723
+ if (serverConfig && typeof serverConfig === "object") {
1724
+ const config = serverConfig;
1725
+ servers.push({
1726
+ serverName,
1727
+ config,
1728
+ identity: extractServerIdentity(config),
1729
+ agentType,
1730
+ scope: options.scope,
1731
+ configPath
1732
+ });
1733
+ }
1734
+ }
1735
+ }
1736
+ return {
1737
+ agentType,
1738
+ displayName: agent.displayName,
1739
+ detected: true,
1740
+ scope: options.scope,
1741
+ configPath,
1742
+ servers
1743
+ };
1744
+ }
1745
+ async function gatherInstalledServers(options) {
1746
+ const scope = options.global ? "global" : "local";
1747
+ const results = [];
1748
+ if (options.agents && options.agents.length > 0) {
1749
+ const detectedSet = new Set(
1750
+ options.global ? await detectAllGlobalAgents() : detectProjectAgents(options.cwd)
1751
+ );
1752
+ for (const agentType of options.agents) {
1753
+ const detected = detectedSet.has(agentType);
1754
+ if (detected) {
1755
+ results.push(
1756
+ readServersForAgent(agentType, { scope, cwd: options.cwd })
1757
+ );
1758
+ } else {
1759
+ const agent = agents[agentType];
1760
+ const installOptions = {
1761
+ local: scope === "local",
1762
+ cwd: options.cwd
1763
+ };
1764
+ results.push({
1765
+ agentType,
1766
+ displayName: agent.displayName,
1767
+ detected: false,
1768
+ scope,
1769
+ configPath: getConfigPath2(agent, installOptions),
1770
+ servers: []
1771
+ });
1772
+ }
1773
+ }
1774
+ } else {
1775
+ const detected = options.global ? await detectAllGlobalAgents() : detectProjectAgents(options.cwd);
1776
+ for (const agentType of detected) {
1777
+ results.push(readServersForAgent(agentType, { scope, cwd: options.cwd }));
1778
+ }
1779
+ }
1780
+ return results;
1781
+ }
1782
+ function findMatchingServers(agentServersList, query) {
1783
+ const lowerQuery = query.toLowerCase();
1784
+ const matches = [];
1785
+ for (const agentServers of agentServersList) {
1786
+ for (const server of agentServers.servers) {
1787
+ const nameMatch = server.serverName.toLowerCase().includes(lowerQuery);
1788
+ const identityMatch = server.identity === query;
1789
+ if (nameMatch || identityMatch) {
1790
+ matches.push(server);
1791
+ }
1792
+ }
1793
+ }
1794
+ return matches;
1795
+ }
1796
+
1577
1797
  // package.json
1578
1798
  var package_default = {
1579
1799
  name: "add-mcp",
1580
- version: "1.7.0",
1800
+ version: "1.8.0",
1581
1801
  description: "Add MCP servers to your favorite coding agents with a single command.",
1582
1802
  author: "Andre Landgraf <andre@neon.tech>",
1583
1803
  license: "Apache-2.0",
@@ -1593,8 +1813,8 @@ var package_default = {
1593
1813
  fmt: "prettier --write .",
1594
1814
  build: "tsup src/index.ts --format esm --dts --clean",
1595
1815
  dev: "tsx src/index.ts",
1596
- 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/e2e/install.test.ts && tsx tests/e2e/cli.test.ts",
1597
- "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",
1816
+ 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/reader.test.ts && tsx tests/formats-remove.test.ts && tsx tests/e2e/install.test.ts && tsx tests/e2e/cli.test.ts",
1817
+ "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/reader.test.ts && tsx tests/formats-remove.test.ts",
1598
1818
  "registry:sort": "tsx scripts/sort-registry.ts",
1599
1819
  "registry:verify": "tsx scripts/verify-registry.ts",
1600
1820
  "test:e2e": "tsx tests/e2e/install.test.ts && tsx tests/e2e/cli.test.ts",
@@ -1734,7 +1954,7 @@ function extractOptions(raw) {
1734
1954
  }
1735
1955
  return raw;
1736
1956
  }
1737
- function extractFindOptionsFromArgv() {
1957
+ function extractSubcommandOptionsFromArgv() {
1738
1958
  const argv = process.argv.slice(2);
1739
1959
  const result = {};
1740
1960
  for (let i = 0; i < argv.length; i++) {
@@ -1868,7 +2088,7 @@ program.command("list-agents").description("List all supported coding agents").a
1868
2088
  async function runFindCommand(keyword, rawOptions) {
1869
2089
  const options = {
1870
2090
  ...extractOptions(rawOptions),
1871
- ...extractFindOptionsFromArgv()
2091
+ ...extractSubcommandOptionsFromArgv()
1872
2092
  };
1873
2093
  const query = (keyword ?? "").trim();
1874
2094
  const registries = await ensureFindRegistriesConfigured(options.yes);
@@ -1919,7 +2139,437 @@ program.command("search [keyword]").description("Alias for find").option(
1919
2139
  await runFindCommand(keyword, options);
1920
2140
  }
1921
2141
  );
2142
+ 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) => {
2143
+ const options = {
2144
+ ...extractOptions(rawOptions),
2145
+ ...extractSubcommandOptionsFromArgv()
2146
+ };
2147
+ await runListCommand(options);
2148
+ });
2149
+ 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(
2150
+ async (query, rawOptions) => {
2151
+ const options = {
2152
+ ...extractOptions(rawOptions),
2153
+ ...extractSubcommandOptionsFromArgv()
2154
+ };
2155
+ await runRemoveCommand(query, options);
2156
+ }
2157
+ );
2158
+ program.command("sync").description(
2159
+ "Synchronize server names and installations across all detected agents"
2160
+ ).option("-g, --global", "Sync global configs instead of project-level").option("-y, --yes", "Skip confirmation prompts").action(async (rawOptions) => {
2161
+ const options = {
2162
+ ...extractOptions(rawOptions),
2163
+ ...extractSubcommandOptionsFromArgv()
2164
+ };
2165
+ await runSyncCommand(options);
2166
+ });
2167
+ 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) => {
2168
+ const options = {
2169
+ ...extractOptions(rawOptions),
2170
+ ...extractSubcommandOptionsFromArgv()
2171
+ };
2172
+ await runSyncCommand(options);
2173
+ });
1922
2174
  program.parse();
2175
+ async function runListCommand(options) {
2176
+ showLogo();
2177
+ console.log();
2178
+ const explicitAgents = resolveAgentFlags(options.agent);
2179
+ const agentServersList = await gatherInstalledServers({
2180
+ global: options.global,
2181
+ agents: explicitAgents.length > 0 ? explicitAgents : void 0
2182
+ });
2183
+ if (agentServersList.length === 0) {
2184
+ 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.";
2185
+ p3.log.info(hint);
2186
+ console.log();
2187
+ return;
2188
+ }
2189
+ for (const agentServers of agentServersList) {
2190
+ if (!agentServers.detected) {
2191
+ console.log(
2192
+ `${TEXT}${agentServers.displayName}:${RESET} ${DIM}not detected${RESET}`
2193
+ );
2194
+ continue;
2195
+ }
2196
+ if (agentServers.servers.length === 0) {
2197
+ console.log(
2198
+ `${TEXT}${agentServers.displayName}:${RESET} ${DIM}no servers configured${RESET}`
2199
+ );
2200
+ continue;
2201
+ }
2202
+ console.log(`${TEXT}${agentServers.displayName}:${RESET}`);
2203
+ for (const server of agentServers.servers) {
2204
+ const identityHint = server.identity ? ` ${DIM}(${server.identity})${RESET}` : "";
2205
+ console.log(
2206
+ ` ${DIM}-${RESET} ${TEXT}${server.serverName}${RESET}${identityHint}`
2207
+ );
2208
+ }
2209
+ }
2210
+ console.log();
2211
+ }
2212
+ async function runRemoveCommand(query, options) {
2213
+ showLogo();
2214
+ console.log();
2215
+ const explicitAgents = resolveAgentFlags(options.agent);
2216
+ const agentServersList = await gatherInstalledServers({
2217
+ global: options.global,
2218
+ agents: explicitAgents.length > 0 ? explicitAgents : void 0
2219
+ });
2220
+ const matches = findMatchingServers(agentServersList, query);
2221
+ if (matches.length === 0) {
2222
+ p3.log.info(`No matching servers found for '${query}'`);
2223
+ console.log();
2224
+ return;
2225
+ }
2226
+ const matchOptions = matches.map((m, i) => ({
2227
+ value: i,
2228
+ label: `${m.serverName} (${agents[m.agentType].displayName})`,
2229
+ hint: m.identity || m.configPath
2230
+ }));
2231
+ let selectedIndices;
2232
+ if (options.yes) {
2233
+ selectedIndices = matches.map((_, i) => i);
2234
+ p3.log.info(
2235
+ `Removing ${matches.length} server${matches.length !== 1 ? "s" : ""} matching '${query}'`
2236
+ );
2237
+ } else {
2238
+ const selected = await p3.multiselect({
2239
+ message: `Select servers to remove (${matches.length} match${matches.length !== 1 ? "es" : ""} found)`,
2240
+ options: matchOptions,
2241
+ required: false,
2242
+ initialValues: matches.map((_, i) => i)
2243
+ });
2244
+ if (p3.isCancel(selected)) {
2245
+ p3.log.info("No changes made");
2246
+ console.log();
2247
+ return;
2248
+ }
2249
+ selectedIndices = selected;
2250
+ if (selectedIndices.length === 0) {
2251
+ p3.log.info("No changes made");
2252
+ console.log();
2253
+ return;
2254
+ }
2255
+ }
2256
+ let removedCount = 0;
2257
+ const affectedAgents = /* @__PURE__ */ new Set();
2258
+ for (const idx of selectedIndices) {
2259
+ const server = matches[idx];
2260
+ const agent = agents[server.agentType];
2261
+ try {
2262
+ removeServerFromConfig(
2263
+ server.configPath,
2264
+ agent.format,
2265
+ getConfigKeyForServer(server),
2266
+ server.serverName
2267
+ );
2268
+ removedCount++;
2269
+ affectedAgents.add(agent.displayName);
2270
+ } catch (error) {
2271
+ p3.log.error(
2272
+ `Failed to remove ${server.serverName} from ${agent.displayName}: ${error instanceof Error ? error.message : "Unknown error"}`
2273
+ );
2274
+ }
2275
+ }
2276
+ if (removedCount > 0) {
2277
+ p3.log.success(
2278
+ `Removed ${removedCount} server${removedCount !== 1 ? "s" : ""} from ${affectedAgents.size} agent${affectedAgents.size !== 1 ? "s" : ""}`
2279
+ );
2280
+ }
2281
+ console.log();
2282
+ }
2283
+ function getConfigKeyForServer(server) {
2284
+ const agent = agents[server.agentType];
2285
+ if (server.scope === "local" && agent.localConfigKey) {
2286
+ return agent.localConfigKey;
2287
+ }
2288
+ return agent.configKey;
2289
+ }
2290
+ function deepEqual(a, b) {
2291
+ if (a === b) return true;
2292
+ if (a === null || b === null) return false;
2293
+ if (a === void 0 || b === void 0) return a === b;
2294
+ if (typeof a !== typeof b) return false;
2295
+ if (Array.isArray(a) && Array.isArray(b)) {
2296
+ if (a.length !== b.length) return false;
2297
+ return a.every((val, i) => deepEqual(val, b[i]));
2298
+ }
2299
+ if (typeof a === "object" && typeof b === "object") {
2300
+ const aObj = a;
2301
+ const bObj = b;
2302
+ const aKeys = Object.keys(aObj).sort();
2303
+ const bKeys = Object.keys(bObj).sort();
2304
+ if (!deepEqual(aKeys, bKeys)) return false;
2305
+ return aKeys.every((key) => deepEqual(aObj[key], bObj[key]));
2306
+ }
2307
+ return false;
2308
+ }
2309
+ function pickCanonicalName(entries) {
2310
+ const nameFreq = /* @__PURE__ */ new Map();
2311
+ for (const entry of entries) {
2312
+ nameFreq.set(entry.serverName, (nameFreq.get(entry.serverName) ?? 0) + 1);
2313
+ }
2314
+ const names = [...nameFreq.entries()];
2315
+ names.sort(([nameA, freqA], [nameB, freqB]) => {
2316
+ if (nameA.length !== nameB.length) return nameA.length - nameB.length;
2317
+ if (freqA !== freqB) return freqB - freqA;
2318
+ return nameA.localeCompare(nameB);
2319
+ });
2320
+ return names[0][0];
2321
+ }
2322
+ function extractConflictFields(config) {
2323
+ return {
2324
+ headers: config.headers ?? config.http_headers ?? null,
2325
+ env: config.env ?? config.envs ?? config.environment ?? null,
2326
+ args: config.args ?? null
2327
+ };
2328
+ }
2329
+ function buildSyncGroups(agentServersList) {
2330
+ const byIdentity = /* @__PURE__ */ new Map();
2331
+ for (const agentServers of agentServersList) {
2332
+ for (const server of agentServers.servers) {
2333
+ if (!server.identity) continue;
2334
+ const existing = byIdentity.get(server.identity) ?? [];
2335
+ existing.push(server);
2336
+ byIdentity.set(server.identity, existing);
2337
+ }
2338
+ }
2339
+ const groups = [];
2340
+ for (const [identity, entries] of byIdentity) {
2341
+ const fieldSets = entries.map((e) => extractConflictFields(e.config));
2342
+ const reference = fieldSets[0];
2343
+ let hasConflict = false;
2344
+ let conflictReason;
2345
+ for (let i = 1; i < fieldSets.length; i++) {
2346
+ const other = fieldSets[i];
2347
+ if (!deepEqual(reference.headers, other.headers)) {
2348
+ hasConflict = true;
2349
+ conflictReason = `headers differ between ${agents[entries[0].agentType].displayName} and ${agents[entries[i].agentType].displayName}`;
2350
+ break;
2351
+ }
2352
+ if (!deepEqual(reference.env, other.env)) {
2353
+ hasConflict = true;
2354
+ conflictReason = `env differs between ${agents[entries[0].agentType].displayName} and ${agents[entries[i].agentType].displayName}`;
2355
+ break;
2356
+ }
2357
+ if (!deepEqual(reference.args, other.args)) {
2358
+ hasConflict = true;
2359
+ conflictReason = `args differ between ${agents[entries[0].agentType].displayName} and ${agents[entries[i].agentType].displayName}`;
2360
+ break;
2361
+ }
2362
+ }
2363
+ groups.push({
2364
+ identity,
2365
+ entries,
2366
+ canonicalName: pickCanonicalName(entries),
2367
+ canonicalConfig: entries[0].config,
2368
+ hasConflict,
2369
+ conflictReason
2370
+ });
2371
+ }
2372
+ return groups;
2373
+ }
2374
+ async function runSyncCommand(options) {
2375
+ showLogo();
2376
+ console.log();
2377
+ const agentServersList = await gatherInstalledServers({
2378
+ global: options.global
2379
+ });
2380
+ const agentsWithServers = agentServersList.filter(
2381
+ (a) => a.servers.length > 0
2382
+ );
2383
+ if (agentServersList.length < 2) {
2384
+ p3.log.info("Need at least 2 detected agents to sync");
2385
+ console.log();
2386
+ return;
2387
+ }
2388
+ const groups = buildSyncGroups(agentServersList);
2389
+ const detectedAgentTypes = new Set(agentServersList.map((a) => a.agentType));
2390
+ const renames = [];
2391
+ const additions = [];
2392
+ const skipped = [];
2393
+ for (const group of groups) {
2394
+ if (group.hasConflict) {
2395
+ skipped.push(group);
2396
+ continue;
2397
+ }
2398
+ const presentAgents = new Set(group.entries.map((e) => e.agentType));
2399
+ for (const entry of group.entries) {
2400
+ if (entry.serverName !== group.canonicalName) {
2401
+ renames.push({
2402
+ group,
2403
+ agentType: entry.agentType,
2404
+ oldName: entry.serverName
2405
+ });
2406
+ }
2407
+ }
2408
+ for (const agentType of detectedAgentTypes) {
2409
+ if (!presentAgents.has(agentType)) {
2410
+ additions.push({ group, agentType });
2411
+ }
2412
+ }
2413
+ }
2414
+ if (renames.length === 0 && additions.length === 0 && skipped.length === 0) {
2415
+ p3.log.info("All servers are already in sync");
2416
+ console.log();
2417
+ return;
2418
+ }
2419
+ const planLines = [];
2420
+ if (renames.length > 0) {
2421
+ planLines.push(chalk.cyan("Renames:"));
2422
+ for (const r of renames) {
2423
+ planLines.push(
2424
+ ` ${agents[r.agentType].displayName}: ${r.oldName} \u2192 ${r.group.canonicalName}`
2425
+ );
2426
+ }
2427
+ }
2428
+ if (additions.length > 0) {
2429
+ planLines.push(chalk.cyan("Additions:"));
2430
+ for (const a of additions) {
2431
+ planLines.push(
2432
+ ` ${agents[a.agentType].displayName}: + ${a.group.canonicalName} (${a.group.identity})`
2433
+ );
2434
+ }
2435
+ }
2436
+ if (skipped.length > 0) {
2437
+ planLines.push(chalk.yellow("Skipped (conflicts):"));
2438
+ for (const s of skipped) {
2439
+ planLines.push(` ${s.identity}: ${s.conflictReason}`);
2440
+ }
2441
+ }
2442
+ if (renames.length === 0 && additions.length === 0) {
2443
+ p3.note(planLines.join("\n"), "Sync Plan");
2444
+ p3.log.info(
2445
+ "All servers are already in sync (some skipped due to conflicts)"
2446
+ );
2447
+ console.log();
2448
+ return;
2449
+ }
2450
+ p3.note(planLines.join("\n"), "Sync Plan");
2451
+ if (!options.yes) {
2452
+ const confirmed = await p3.confirm({
2453
+ message: "Proceed with sync?"
2454
+ });
2455
+ if (p3.isCancel(confirmed) || !confirmed) {
2456
+ p3.log.info("No changes made");
2457
+ console.log();
2458
+ return;
2459
+ }
2460
+ }
2461
+ const scope = options.global ? "global" : "local";
2462
+ let changeCount = 0;
2463
+ for (const rename of renames) {
2464
+ const { group, agentType } = rename;
2465
+ const result = installServerForAgent(
2466
+ group.canonicalName,
2467
+ buildServerConfigFromStored(group.canonicalConfig),
2468
+ agentType,
2469
+ { local: scope === "local" }
2470
+ );
2471
+ if (result.success) {
2472
+ changeCount++;
2473
+ } else {
2474
+ p3.log.error(
2475
+ `Failed to write ${group.canonicalName} to ${agents[agentType].displayName}: ${result.error}`
2476
+ );
2477
+ }
2478
+ }
2479
+ for (const addition of additions) {
2480
+ const { group, agentType } = addition;
2481
+ const result = installServerForAgent(
2482
+ group.canonicalName,
2483
+ buildServerConfigFromStored(group.canonicalConfig),
2484
+ agentType,
2485
+ { local: scope === "local" }
2486
+ );
2487
+ if (result.success) {
2488
+ changeCount++;
2489
+ } else {
2490
+ p3.log.error(
2491
+ `Failed to add ${group.canonicalName} to ${agents[agentType].displayName}: ${result.error}`
2492
+ );
2493
+ }
2494
+ }
2495
+ for (const rename of renames) {
2496
+ const { group, agentType, oldName } = rename;
2497
+ const agentConfig = agents[agentType];
2498
+ const entry = group.entries.find((e) => e.agentType === agentType);
2499
+ if (!entry) continue;
2500
+ try {
2501
+ removeServerFromConfig(
2502
+ entry.configPath,
2503
+ agentConfig.format,
2504
+ getConfigKeyForServer(entry),
2505
+ oldName
2506
+ );
2507
+ } catch (error) {
2508
+ p3.log.error(
2509
+ `Failed to remove old alias ${oldName} from ${agentConfig.displayName}: ${error instanceof Error ? error.message : "Unknown error"}`
2510
+ );
2511
+ }
2512
+ }
2513
+ p3.log.success(
2514
+ `Synced ${changeCount} server${changeCount !== 1 ? "s" : ""} across ${detectedAgentTypes.size} agent${detectedAgentTypes.size !== 1 ? "s" : ""}`
2515
+ );
2516
+ console.log();
2517
+ }
2518
+ var TRANSPORT_ALIASES = {
2519
+ http: "http",
2520
+ sse: "sse",
2521
+ streamable_http: "http",
2522
+ streamableHttp: "http",
2523
+ "streamable-http": "http",
2524
+ remote: "http"
2525
+ };
2526
+ function normalizeTransportType(raw) {
2527
+ if (typeof raw === "string" && raw in TRANSPORT_ALIASES) {
2528
+ return TRANSPORT_ALIASES[raw];
2529
+ }
2530
+ return "http";
2531
+ }
2532
+ function buildServerConfigFromStored(config) {
2533
+ const url = typeof config.url === "string" ? config.url : typeof config.uri === "string" ? config.uri : typeof config.serverUrl === "string" ? config.serverUrl : void 0;
2534
+ if (url) {
2535
+ const result2 = {
2536
+ type: normalizeTransportType(config.type),
2537
+ url
2538
+ };
2539
+ const headers = config.headers && typeof config.headers === "object" ? config.headers : config.http_headers && typeof config.http_headers === "object" ? config.http_headers : void 0;
2540
+ if (headers && Object.keys(headers).length > 0) {
2541
+ result2.headers = headers;
2542
+ }
2543
+ return result2;
2544
+ }
2545
+ const command = typeof config.command === "string" ? config.command : typeof config.cmd === "string" ? config.cmd : void 0;
2546
+ const args = Array.isArray(config.args) ? config.args.filter((a) => typeof a === "string") : [];
2547
+ 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;
2548
+ const result = {};
2549
+ if (command) result.command = command;
2550
+ if (args.length > 0) result.args = args;
2551
+ if (env && Object.keys(env).length > 0) result.env = env;
2552
+ return result;
2553
+ }
2554
+ function resolveAgentFlags(agentFlags) {
2555
+ if (!agentFlags || agentFlags.length === 0) return [];
2556
+ const resolved = [];
2557
+ const invalid = [];
2558
+ for (const input of agentFlags) {
2559
+ const agentType = resolveAgentType(input);
2560
+ if (agentType) {
2561
+ resolved.push(agentType);
2562
+ } else {
2563
+ invalid.push(input);
2564
+ }
2565
+ }
2566
+ if (invalid.length > 0) {
2567
+ p3.log.error(`Invalid agents: ${invalid.join(", ")}`);
2568
+ p3.log.info(`Valid agents: ${getAgentTypes().join(", ")}`);
2569
+ process.exit(1);
2570
+ }
2571
+ return resolved;
2572
+ }
1923
2573
  function listAgents() {
1924
2574
  showLogo();
1925
2575
  console.log();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "add-mcp",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "Add MCP servers to your favorite coding agents with a single command.",
5
5
  "author": "Andre Landgraf <andre@neon.tech>",
6
6
  "license": "Apache-2.0",
@@ -16,8 +16,8 @@
16
16
  "fmt": "prettier --write .",
17
17
  "build": "tsup src/index.ts --format esm --dts --clean",
18
18
  "dev": "tsx src/index.ts",
19
- "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/e2e/install.test.ts && tsx tests/e2e/cli.test.ts",
20
- "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",
19
+ "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/reader.test.ts && tsx tests/formats-remove.test.ts && tsx tests/e2e/install.test.ts && tsx tests/e2e/cli.test.ts",
20
+ "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/reader.test.ts && tsx tests/formats-remove.test.ts",
21
21
  "registry:sort": "tsx scripts/sort-registry.ts",
22
22
  "registry:verify": "tsx scripts/verify-registry.ts",
23
23
  "test:e2e": "tsx tests/e2e/install.test.ts && tsx tests/e2e/cli.test.ts",