add-mcp 0.5.0 → 0.6.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 +3 -3
  2. package/dist/index.js +255 -106
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -7,7 +7,7 @@ Supports **OpenCode**, **Claude Code**, **Codex**, **Cursor**, and [5 more](#sup
7
7
  ## Install an MCP Server
8
8
 
9
9
  ```bash
10
- npx add-mcp https://mcp.example.com/sse
10
+ npx add-mcp https://mcp.example.com/mcp
11
11
  ```
12
12
 
13
13
  ### Usage Examples
@@ -100,8 +100,8 @@ The CLI automatically detects agents based on your environment:
100
100
 
101
101
  **No agents detected:**
102
102
 
103
- - Interactive mode: Shows error with guidance to use `--global` or run in a project
104
- - With `--yes`: Installs to all project-capable agents
103
+ - Interactive mode: Shows an agent selector (all available, last selection, or pick specific)
104
+ - With `--yes`: Installs to all project-capable agents (project mode) or all global-capable agents (global mode)
105
105
 
106
106
  ## Transport Types
107
107
 
package/dist/index.js CHANGED
@@ -2,9 +2,9 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { program } from "commander";
5
- import * as p from "@clack/prompts";
5
+ import * as p2 from "@clack/prompts";
6
6
  import chalk from "chalk";
7
- import { homedir as homedir2 } from "os";
7
+ import { homedir as homedir3 } from "os";
8
8
 
9
9
  // src/types.ts
10
10
  var agentAliases = {
@@ -12,45 +12,107 @@ var agentAliases = {
12
12
  };
13
13
 
14
14
  // src/agents.ts
15
- import { homedir } from "os";
16
- import { join } from "path";
15
+ import * as p from "@clack/prompts";
16
+ import { homedir as homedir2 } from "os";
17
+ import { join as join2 } from "path";
17
18
  import { existsSync } from "fs";
18
- var home = homedir();
19
+
20
+ // src/mcp-lock.ts
21
+ import { readFile, writeFile, mkdir } from "fs/promises";
22
+ import { dirname, join } from "path";
23
+ import { homedir } from "os";
24
+ var AGENTS_DIR = ".agents";
25
+ var LOCK_FILE = ".mcp-lock.json";
26
+ var CURRENT_VERSION = 1;
27
+ function getMcpLockPath() {
28
+ return join(homedir(), AGENTS_DIR, LOCK_FILE);
29
+ }
30
+ async function readMcpLock() {
31
+ const lockPath = getMcpLockPath();
32
+ try {
33
+ const content = await readFile(lockPath, "utf-8");
34
+ const parsed = JSON.parse(content);
35
+ if (typeof parsed.version !== "number") {
36
+ return createEmptyLockFile();
37
+ }
38
+ if (parsed.version < CURRENT_VERSION) {
39
+ return createEmptyLockFile();
40
+ }
41
+ return parsed;
42
+ } catch {
43
+ return createEmptyLockFile();
44
+ }
45
+ }
46
+ async function writeMcpLock(lock) {
47
+ const lockPath = getMcpLockPath();
48
+ await mkdir(dirname(lockPath), { recursive: true });
49
+ const content = JSON.stringify(lock, null, 2);
50
+ await writeFile(lockPath, content, "utf-8");
51
+ }
52
+ async function getLastSelectedAgents() {
53
+ const lock = await readMcpLock();
54
+ return lock.lastSelectedAgents;
55
+ }
56
+ async function saveSelectedAgents(agents2) {
57
+ const lock = await readMcpLock();
58
+ lock.lastSelectedAgents = agents2;
59
+ await writeMcpLock(lock);
60
+ }
61
+ function createEmptyLockFile() {
62
+ return {
63
+ version: CURRENT_VERSION
64
+ };
65
+ }
66
+
67
+ // src/agents.ts
68
+ var home = homedir2();
69
+ function shortenPath(fullPath) {
70
+ if (fullPath.startsWith(home)) {
71
+ return fullPath.replace(home, "~");
72
+ }
73
+ return fullPath;
74
+ }
19
75
  function getPlatformPaths() {
20
76
  const platform = process.platform;
21
77
  if (platform === "win32") {
22
- const appData = process.env.APPDATA || join(home, "AppData", "Roaming");
78
+ const appData = process.env.APPDATA || join2(home, "AppData", "Roaming");
23
79
  return {
24
80
  appSupport: appData,
25
- vscodePath: join(appData, "Code", "User")
81
+ vscodePath: join2(appData, "Code", "User"),
82
+ gooseConfigPath: join2(appData, "Block", "goose", "config", "config.yaml")
26
83
  };
27
84
  } else if (platform === "darwin") {
28
85
  return {
29
- appSupport: join(home, "Library", "Application Support"),
30
- vscodePath: join(home, "Library", "Application Support", "Code", "User")
86
+ appSupport: join2(home, "Library", "Application Support"),
87
+ vscodePath: join2(home, "Library", "Application Support", "Code", "User"),
88
+ gooseConfigPath: join2(home, ".config", "goose", "config.yaml")
31
89
  };
32
90
  } else {
33
- const configDir = process.env.XDG_CONFIG_HOME || join(home, ".config");
91
+ const configDir = process.env.XDG_CONFIG_HOME || join2(home, ".config");
34
92
  return {
35
93
  appSupport: configDir,
36
- vscodePath: join(configDir, "Code", "User")
94
+ vscodePath: join2(configDir, "Code", "User"),
95
+ gooseConfigPath: join2(configDir, "goose", "config.yaml")
37
96
  };
38
97
  }
39
98
  }
40
- var { appSupport, vscodePath } = getPlatformPaths();
99
+ var { appSupport, vscodePath, gooseConfigPath } = getPlatformPaths();
41
100
  function transformGooseConfig(serverName, config) {
42
101
  if (config.url) {
43
102
  const gooseType = config.type === "sse" ? "sse" : "streamable_http";
44
103
  return {
45
104
  name: serverName,
105
+ description: "",
46
106
  type: gooseType,
47
- url: config.url,
107
+ uri: config.url,
108
+ headers: config.headers || {},
48
109
  enabled: true,
49
110
  timeout: 300
50
111
  };
51
112
  }
52
113
  return {
53
114
  name: serverName,
115
+ description: "",
54
116
  cmd: config.command,
55
117
  args: config.args || [],
56
118
  enabled: true,
@@ -110,7 +172,7 @@ var agents = {
110
172
  "claude-code": {
111
173
  name: "claude-code",
112
174
  displayName: "Claude Code",
113
- configPath: join(home, ".claude.json"),
175
+ configPath: join2(home, ".claude.json"),
114
176
  localConfigPath: ".mcp.json",
115
177
  projectDetectPaths: [".mcp.json", ".claude"],
116
178
  configKey: "mcpServers",
@@ -118,13 +180,13 @@ var agents = {
118
180
  supportedTransports: ["stdio", "http", "sse"],
119
181
  supportsHeaders: true,
120
182
  detectGlobalInstall: async () => {
121
- return existsSync(join(home, ".claude"));
183
+ return existsSync(join2(home, ".claude"));
122
184
  }
123
185
  },
124
186
  "claude-desktop": {
125
187
  name: "claude-desktop",
126
188
  displayName: "Claude Desktop",
127
- configPath: join(appSupport, "Claude", "claude_desktop_config.json"),
189
+ configPath: join2(appSupport, "Claude", "claude_desktop_config.json"),
128
190
  projectDetectPaths: [],
129
191
  // Global only - no project support
130
192
  configKey: "mcpServers",
@@ -132,14 +194,14 @@ var agents = {
132
194
  supportedTransports: ["stdio", "http", "sse"],
133
195
  supportsHeaders: true,
134
196
  detectGlobalInstall: async () => {
135
- return existsSync(join(appSupport, "Claude"));
197
+ return existsSync(join2(appSupport, "Claude"));
136
198
  }
137
199
  },
138
200
  codex: {
139
201
  name: "codex",
140
202
  displayName: "Codex",
141
- configPath: join(
142
- process.env.CODEX_HOME || join(home, ".codex"),
203
+ configPath: join2(
204
+ process.env.CODEX_HOME || join2(home, ".codex"),
143
205
  "config.toml"
144
206
  ),
145
207
  localConfigPath: ".codex/config.toml",
@@ -149,14 +211,14 @@ var agents = {
149
211
  supportedTransports: ["stdio", "http", "sse"],
150
212
  supportsHeaders: true,
151
213
  detectGlobalInstall: async () => {
152
- return existsSync(join(home, ".codex"));
214
+ return existsSync(join2(home, ".codex"));
153
215
  },
154
216
  transformConfig: transformCodexConfig
155
217
  },
156
218
  cursor: {
157
219
  name: "cursor",
158
220
  displayName: "Cursor",
159
- configPath: join(home, ".cursor", "mcp.json"),
221
+ configPath: join2(home, ".cursor", "mcp.json"),
160
222
  localConfigPath: ".cursor/mcp.json",
161
223
  projectDetectPaths: [".cursor"],
162
224
  configKey: "mcpServers",
@@ -164,13 +226,13 @@ var agents = {
164
226
  supportedTransports: ["stdio", "http", "sse"],
165
227
  supportsHeaders: true,
166
228
  detectGlobalInstall: async () => {
167
- return existsSync(join(home, ".cursor"));
229
+ return existsSync(join2(home, ".cursor"));
168
230
  }
169
231
  },
170
232
  "gemini-cli": {
171
233
  name: "gemini-cli",
172
234
  displayName: "Gemini CLI",
173
- configPath: join(home, ".gemini", "settings.json"),
235
+ configPath: join2(home, ".gemini", "settings.json"),
174
236
  localConfigPath: ".gemini/settings.json",
175
237
  projectDetectPaths: [".gemini"],
176
238
  configKey: "mcpServers",
@@ -178,28 +240,28 @@ var agents = {
178
240
  supportedTransports: ["stdio", "http", "sse"],
179
241
  supportsHeaders: true,
180
242
  detectGlobalInstall: async () => {
181
- return existsSync(join(home, ".gemini"));
243
+ return existsSync(join2(home, ".gemini"));
182
244
  }
183
245
  },
184
246
  goose: {
185
247
  name: "goose",
186
248
  displayName: "Goose",
187
- configPath: join(home, ".config", "goose", "config.yaml"),
188
- localConfigPath: ".goose/config.yaml",
189
- projectDetectPaths: [".goose"],
249
+ configPath: gooseConfigPath,
250
+ projectDetectPaths: [],
251
+ // Global only - no project support
190
252
  configKey: "extensions",
191
253
  format: "yaml",
192
254
  supportedTransports: ["stdio", "http", "sse"],
193
- supportsHeaders: false,
255
+ supportsHeaders: true,
194
256
  detectGlobalInstall: async () => {
195
- return existsSync(join(home, ".config", "goose"));
257
+ return existsSync(gooseConfigPath);
196
258
  },
197
259
  transformConfig: transformGooseConfig
198
260
  },
199
261
  opencode: {
200
262
  name: "opencode",
201
263
  displayName: "OpenCode",
202
- configPath: join(home, ".config", "opencode", "opencode.json"),
264
+ configPath: join2(home, ".config", "opencode", "opencode.json"),
203
265
  localConfigPath: ".opencode.json",
204
266
  projectDetectPaths: [".opencode.json", ".opencode"],
205
267
  configKey: "mcp",
@@ -207,14 +269,14 @@ var agents = {
207
269
  supportedTransports: ["stdio", "http", "sse"],
208
270
  supportsHeaders: true,
209
271
  detectGlobalInstall: async () => {
210
- return existsSync(join(home, ".config", "opencode"));
272
+ return existsSync(join2(home, ".config", "opencode"));
211
273
  },
212
274
  transformConfig: transformOpenCodeConfig
213
275
  },
214
276
  vscode: {
215
277
  name: "vscode",
216
278
  displayName: "VS Code",
217
- configPath: join(vscodePath, "mcp.json"),
279
+ configPath: join2(vscodePath, "mcp.json"),
218
280
  localConfigPath: ".vscode/mcp.json",
219
281
  projectDetectPaths: [".vscode"],
220
282
  configKey: "servers",
@@ -228,7 +290,7 @@ var agents = {
228
290
  zed: {
229
291
  name: "zed",
230
292
  displayName: "Zed",
231
- configPath: process.platform === "darwin" || process.platform === "win32" ? join(appSupport, "Zed", "settings.json") : join(appSupport, "zed", "settings.json"),
293
+ configPath: process.platform === "darwin" || process.platform === "win32" ? join2(appSupport, "Zed", "settings.json") : join2(appSupport, "zed", "settings.json"),
232
294
  localConfigPath: ".zed/settings.json",
233
295
  projectDetectPaths: [".zed"],
234
296
  configKey: "context_servers",
@@ -236,7 +298,7 @@ var agents = {
236
298
  supportedTransports: ["stdio", "http", "sse"],
237
299
  supportsHeaders: true,
238
300
  detectGlobalInstall: async () => {
239
- const configDir = process.platform === "darwin" || process.platform === "win32" ? join(appSupport, "Zed") : join(appSupport, "zed");
301
+ const configDir = process.platform === "darwin" || process.platform === "win32" ? join2(appSupport, "Zed") : join2(appSupport, "zed");
240
302
  return existsSync(configDir);
241
303
  },
242
304
  transformConfig: transformZedConfig
@@ -259,7 +321,7 @@ function detectProjectAgents(cwd) {
259
321
  for (const [type, config] of Object.entries(agents)) {
260
322
  if (!config.localConfigPath) continue;
261
323
  for (const detectPath of config.projectDetectPaths) {
262
- if (existsSync(join(dir, detectPath))) {
324
+ if (existsSync(join2(dir, detectPath))) {
263
325
  detected.push(type);
264
326
  break;
265
327
  }
@@ -279,6 +341,91 @@ async function detectAllGlobalAgents() {
279
341
  function isTransportSupported(agentType, transport) {
280
342
  return agents[agentType].supportedTransports.includes(transport);
281
343
  }
344
+ async function promptForAgents(message, choices, defaultToAll = false) {
345
+ let lastSelected;
346
+ try {
347
+ lastSelected = await getLastSelectedAgents();
348
+ } catch {
349
+ }
350
+ const validAgents = choices.map((c) => c.value);
351
+ let initialValues;
352
+ if (lastSelected && lastSelected.length > 0) {
353
+ initialValues = lastSelected.filter(
354
+ (a) => validAgents.includes(a)
355
+ );
356
+ if (initialValues.length === 0 && defaultToAll) {
357
+ initialValues = validAgents;
358
+ }
359
+ } else {
360
+ initialValues = defaultToAll ? validAgents : [];
361
+ }
362
+ const selected = await p.multiselect({
363
+ message,
364
+ options: choices,
365
+ required: true,
366
+ initialValues
367
+ });
368
+ if (!p.isCancel(selected)) {
369
+ try {
370
+ await saveSelectedAgents(selected);
371
+ } catch {
372
+ }
373
+ }
374
+ return selected;
375
+ }
376
+ async function selectAgentsInteractive(availableAgents, options) {
377
+ let lastSelected;
378
+ try {
379
+ lastSelected = await getLastSelectedAgents();
380
+ } catch {
381
+ }
382
+ const validLastSelected = lastSelected?.filter(
383
+ (a) => availableAgents.includes(a)
384
+ );
385
+ const selectOptions = [];
386
+ const hasPrevious = validLastSelected && validLastSelected.length > 0;
387
+ if (hasPrevious) {
388
+ const agentNames = validLastSelected.map((a) => agents[a].displayName).join(", ");
389
+ selectOptions.push({
390
+ value: "previous",
391
+ label: "Same as last time (Recommended)",
392
+ hint: agentNames
393
+ });
394
+ }
395
+ selectOptions.push({
396
+ value: "all",
397
+ label: hasPrevious ? "All available agents" : "All available agents (Recommended)",
398
+ hint: `Install to all ${availableAgents.length} available agents`
399
+ });
400
+ selectOptions.push({
401
+ value: "select",
402
+ label: "Select specific agents",
403
+ hint: "Choose which agents to install to"
404
+ });
405
+ const installChoice = await p.select({
406
+ message: "Install to",
407
+ options: selectOptions
408
+ });
409
+ if (p.isCancel(installChoice)) {
410
+ return installChoice;
411
+ }
412
+ if (installChoice === "all") {
413
+ return availableAgents;
414
+ }
415
+ if (installChoice === "previous" && validLastSelected) {
416
+ return validLastSelected;
417
+ }
418
+ const agentChoices = availableAgents.map((agentType) => {
419
+ const localPath = agents[agentType].localConfigPath;
420
+ const hint = options.global ? shortenPath(agents[agentType].configPath) : localPath ?? shortenPath(agents[agentType].configPath);
421
+ return {
422
+ value: agentType,
423
+ label: agents[agentType].displayName,
424
+ hint
425
+ };
426
+ });
427
+ return promptForAgents("Select agents to install to", agentChoices, false);
428
+ }
282
429
 
283
430
  // src/source-parser.ts
284
431
  function isUrl(input) {
@@ -410,11 +557,11 @@ function isRemoteSource(parsed) {
410
557
 
411
558
  // src/installer.ts
412
559
  import { existsSync as existsSync5, mkdirSync as mkdirSync4 } from "fs";
413
- import { join as join2, dirname as dirname4 } from "path";
560
+ import { join as join3, dirname as dirname5 } from "path";
414
561
 
415
562
  // src/formats/json.ts
416
563
  import { readFileSync, writeFileSync, existsSync as existsSync2, mkdirSync } from "fs";
417
- import { dirname } from "path";
564
+ import { dirname as dirname2 } from "path";
418
565
  import * as jsonc from "jsonc-parser";
419
566
 
420
567
  // src/formats/utils.ts
@@ -465,7 +612,7 @@ function detectIndent(text) {
465
612
  return result || { tabSize: 2, insertSpaces: true };
466
613
  }
467
614
  function writeJsonConfig(filePath, config, configKey) {
468
- const dir = dirname(filePath);
615
+ const dir = dirname2(filePath);
469
616
  if (!existsSync2(dir)) {
470
617
  mkdirSync(dir, { recursive: true });
471
618
  }
@@ -507,7 +654,7 @@ function setNestedValue(obj, path, value) {
507
654
 
508
655
  // src/formats/yaml.ts
509
656
  import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
510
- import { dirname as dirname2 } from "path";
657
+ import { dirname as dirname3 } from "path";
511
658
  import yaml from "js-yaml";
512
659
  function readYamlConfig(filePath) {
513
660
  if (!existsSync3(filePath)) {
@@ -518,7 +665,7 @@ function readYamlConfig(filePath) {
518
665
  return parsed || {};
519
666
  }
520
667
  function writeYamlConfig(filePath, config) {
521
- const dir = dirname2(filePath);
668
+ const dir = dirname3(filePath);
522
669
  if (!existsSync3(dir)) {
523
670
  mkdirSync2(dir, { recursive: true });
524
671
  }
@@ -537,7 +684,7 @@ function writeYamlConfig(filePath, config) {
537
684
 
538
685
  // src/formats/toml.ts
539
686
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync4, mkdirSync as mkdirSync3 } from "fs";
540
- import { dirname as dirname3 } from "path";
687
+ import { dirname as dirname4 } from "path";
541
688
  import * as TOML from "@iarna/toml";
542
689
  function readTomlConfig(filePath) {
543
690
  if (!existsSync4(filePath)) {
@@ -548,7 +695,7 @@ function readTomlConfig(filePath) {
548
695
  return parsed;
549
696
  }
550
697
  function writeTomlConfig(filePath, config) {
551
- const dir = dirname3(filePath);
698
+ const dir = dirname4(filePath);
552
699
  if (!existsSync4(dir)) {
553
700
  mkdirSync3(dir, { recursive: true });
554
701
  }
@@ -614,7 +761,7 @@ function buildServerConfig(parsed, options = {}) {
614
761
  function getConfigPath(agent, options = {}) {
615
762
  if (options.local && agent.localConfigPath) {
616
763
  const cwd = options.cwd || process.cwd();
617
- return join2(cwd, agent.localConfigPath);
764
+ return join3(cwd, agent.localConfigPath);
618
765
  }
619
766
  return agent.configPath;
620
767
  }
@@ -622,7 +769,7 @@ function installServerForAgent(serverName, serverConfig, agentType, options = {}
622
769
  const agent = agents[agentType];
623
770
  const configPath = getConfigPath(agent, options);
624
771
  try {
625
- const dir = dirname4(configPath);
772
+ const dir = dirname5(configPath);
626
773
  if (!existsSync5(dir)) {
627
774
  mkdirSync4(dir, { recursive: true });
628
775
  }
@@ -667,7 +814,7 @@ function installServer(serverName, serverConfig, agentTypes, options = {}) {
667
814
  // package.json
668
815
  var package_default = {
669
816
  name: "add-mcp",
670
- version: "0.5.0",
817
+ version: "0.6.0",
671
818
  description: "Add MCP servers to your favorite coding agents with a single command.",
672
819
  author: "Andre Landgraf <andre@neon.tech>",
673
820
  license: "Apache-2.0",
@@ -683,8 +830,8 @@ var package_default = {
683
830
  fmt: "prettier --write .",
684
831
  build: "tsup src/index.ts --format esm --dts --clean",
685
832
  dev: "tsx src/index.ts",
686
- test: "tsx tests/source-parser.test.ts && tsx tests/agents.test.ts && tsx tests/installer.test.ts && tsx tests/e2e/install.test.ts",
687
- "test:unit": "tsx tests/source-parser.test.ts && tsx tests/agents.test.ts && tsx tests/installer.test.ts",
833
+ test: "tsx tests/source-parser.test.ts && tsx tests/agents.test.ts && tsx tests/mcp-lock.test.ts && tsx tests/installer.test.ts && tsx tests/e2e/install.test.ts",
834
+ "test:unit": "tsx tests/source-parser.test.ts && tsx tests/agents.test.ts && tsx tests/mcp-lock.test.ts && tsx tests/installer.test.ts",
688
835
  "test:e2e": "tsx tests/e2e/install.test.ts",
689
836
  typecheck: "tsc --noEmit",
690
837
  prepublishOnly: "npm run build"
@@ -760,8 +907,8 @@ function showLogo() {
760
907
  console.log(`${GRAYS[i]}${line}${RESET}`);
761
908
  });
762
909
  }
763
- function shortenPath(fullPath) {
764
- const home2 = homedir2();
910
+ function shortenPath2(fullPath) {
911
+ const home2 = homedir3();
765
912
  if (fullPath.startsWith(home2)) {
766
913
  return fullPath.replace(home2, "~");
767
914
  }
@@ -868,7 +1015,7 @@ async function main(target, options) {
868
1015
  process.exit(0);
869
1016
  }
870
1017
  console.log();
871
- const spinner2 = p.spinner();
1018
+ const spinner2 = p2.spinner();
872
1019
  spinner2.start("Parsing source...");
873
1020
  const parsed = parseSource(target);
874
1021
  const isRemote = isRemoteSource(parsed);
@@ -877,7 +1024,7 @@ async function main(target, options) {
877
1024
  const headerValues = options.header ?? [];
878
1025
  const headerResult = parseHeaders(headerValues);
879
1026
  if (headerResult.invalid.length > 0) {
880
- p.log.error(
1027
+ p2.log.error(
881
1028
  `Invalid --header value(s): ${headerResult.invalid.join(", ")}. Use "Key: Value" format.`
882
1029
  );
883
1030
  process.exit(1);
@@ -885,23 +1032,23 @@ async function main(target, options) {
885
1032
  const headerKeys = Object.keys(headerResult.headers);
886
1033
  const hasHeaderValues = headerKeys.length > 0;
887
1034
  if (hasHeaderValues && !isRemote) {
888
- p.log.warn("--header is only used for remote URLs, ignoring");
1035
+ p2.log.warn("--header is only used for remote URLs, ignoring");
889
1036
  }
890
1037
  const serverName = options.name || parsed.inferredName;
891
- p.log.info(`Server name: ${chalk.cyan(serverName)}`);
1038
+ p2.log.info(`Server name: ${chalk.cyan(serverName)}`);
892
1039
  const transportValue = options.transport || options.type;
893
1040
  let resolvedTransport;
894
1041
  if (transportValue) {
895
1042
  const validTransports = ["http", "sse"];
896
1043
  if (!validTransports.includes(transportValue)) {
897
- p.log.error(
1044
+ p2.log.error(
898
1045
  `Invalid transport: ${transportValue}. Valid options: ${validTransports.join(", ")}`
899
1046
  );
900
1047
  process.exit(1);
901
1048
  }
902
1049
  resolvedTransport = transportValue;
903
1050
  if (!isRemoteSource(parsed)) {
904
- p.log.warn("--transport is only used for remote URLs, ignoring");
1051
+ p2.log.warn("--transport is only used for remote URLs, ignoring");
905
1052
  }
906
1053
  }
907
1054
  const serverConfig = buildServerConfig(parsed, {
@@ -925,14 +1072,14 @@ async function main(target, options) {
925
1072
  }
926
1073
  }
927
1074
  if (invalid.length > 0) {
928
- p.log.error(`Invalid agents: ${invalid.join(", ")}`);
929
- p.log.info(`Valid agents: ${allAgentTypes.join(", ")}`);
1075
+ p2.log.error(`Invalid agents: ${invalid.join(", ")}`);
1076
+ p2.log.info(`Valid agents: ${allAgentTypes.join(", ")}`);
930
1077
  process.exit(1);
931
1078
  }
932
1079
  targetAgents = resolved;
933
1080
  } else if (options.all) {
934
1081
  targetAgents = allAgentTypes;
935
- p.log.info(`Installing to all ${targetAgents.length} agents`);
1082
+ p2.log.info(`Installing to all ${targetAgents.length} agents`);
936
1083
  } else {
937
1084
  spinner2.start("Detecting agents...");
938
1085
  let detectedAgents;
@@ -953,61 +1100,63 @@ async function main(target, options) {
953
1100
  );
954
1101
  if (detectedAgents.length === 0) {
955
1102
  if (options.yes) {
956
- targetAgents = getProjectCapableAgents();
957
- for (const agent of targetAgents) {
958
- agentRouting.set(agent, "local");
959
- }
960
- p.log.info(
961
- `Installing to ${targetAgents.length} project-capable agents (none detected)`
962
- );
963
- } else {
964
- if (!options.global) {
965
- p.log.error(
966
- "No agents detected in this project. Use --global to install globally, or run in a project with agent config files."
1103
+ if (options.global) {
1104
+ targetAgents = allAgentTypes;
1105
+ for (const agent of targetAgents) {
1106
+ agentRouting.set(agent, "global");
1107
+ }
1108
+ p2.log.info(
1109
+ `Installing to ${targetAgents.length} agents globally (none detected)`
1110
+ );
1111
+ } else {
1112
+ targetAgents = getProjectCapableAgents();
1113
+ for (const agent of targetAgents) {
1114
+ agentRouting.set(agent, "local");
1115
+ }
1116
+ p2.log.info(
1117
+ `Installing to ${targetAgents.length} project-capable agents (none detected)`
967
1118
  );
968
- process.exit(1);
969
1119
  }
970
- p.log.warn(
971
- "No coding agents detected. You can still install MCP servers."
1120
+ } else {
1121
+ const availableAgents = options.global ? allAgentTypes : getProjectCapableAgents();
1122
+ p2.log.warn(
1123
+ options.global ? "No coding agents detected. You can still install MCP servers." : "No agents detected in this project. You can still install MCP servers."
972
1124
  );
973
- const allAgentChoices = allAgentTypes.map((type) => ({
974
- value: type,
975
- label: agents[type].displayName
976
- }));
977
- const selected = await p.multiselect({
978
- message: "Select agents to install to",
979
- options: allAgentChoices,
980
- required: true
1125
+ const selected = await selectAgentsInteractive(availableAgents, {
1126
+ global: options.global
981
1127
  });
982
- if (p.isCancel(selected)) {
983
- p.cancel("Installation cancelled");
1128
+ if (p2.isCancel(selected)) {
1129
+ p2.cancel("Installation cancelled");
984
1130
  process.exit(0);
985
1131
  }
986
1132
  selectedViaPrompt = true;
987
1133
  targetAgents = selected;
1134
+ for (const agent of targetAgents) {
1135
+ agentRouting.set(agent, options.global ? "global" : "local");
1136
+ }
988
1137
  }
989
1138
  } else if (detectedAgents.length === 1 || options.yes) {
990
1139
  targetAgents = detectedAgents;
991
1140
  const agentNames = detectedAgents.map((a) => chalk.cyan(agents[a].displayName)).join(", ");
992
- p.log.info(`Installing to: ${agentNames}`);
1141
+ p2.log.info(`Installing to: ${agentNames}`);
993
1142
  } else {
994
1143
  const agentChoices = detectedAgents.map((a) => {
995
1144
  const routing = agentRouting.get(a);
996
- const hint = routing === "local" ? "project" : routing === "global" ? "global" : shortenPath(agents[a].configPath);
1145
+ const hint = routing === "local" ? "project" : routing === "global" ? "global" : shortenPath2(agents[a].configPath);
997
1146
  return {
998
1147
  value: a,
999
1148
  label: agents[a].displayName,
1000
1149
  hint
1001
1150
  };
1002
1151
  });
1003
- const selected = await p.multiselect({
1152
+ const selected = await p2.multiselect({
1004
1153
  message: "Select agents to install to",
1005
1154
  options: agentChoices,
1006
1155
  required: true,
1007
1156
  initialValues: detectedAgents
1008
1157
  });
1009
- if (p.isCancel(selected)) {
1010
- p.cancel("Installation cancelled");
1158
+ if (p2.isCancel(selected)) {
1159
+ p2.cancel("Installation cancelled");
1011
1160
  process.exit(0);
1012
1161
  }
1013
1162
  selectedViaPrompt = true;
@@ -1021,18 +1170,18 @@ async function main(target, options) {
1021
1170
  if (unsupportedAgents.length > 0) {
1022
1171
  const unsupportedNames = unsupportedAgents.map((a) => agents[a].displayName).join(", ");
1023
1172
  if (options.all) {
1024
- p.log.warn(
1173
+ p2.log.warn(
1025
1174
  `Skipping agents that don't support ${requiredTransport} transport: ${unsupportedNames}`
1026
1175
  );
1027
1176
  targetAgents = targetAgents.filter(
1028
1177
  (a) => isTransportSupported(a, requiredTransport)
1029
1178
  );
1030
1179
  if (targetAgents.length === 0) {
1031
- p.log.error("No agents support this transport type");
1180
+ p2.log.error("No agents support this transport type");
1032
1181
  process.exit(1);
1033
1182
  }
1034
1183
  } else {
1035
- p.log.error(
1184
+ p2.log.error(
1036
1185
  `The following agents don't support ${requiredTransport} transport: ${unsupportedNames}`
1037
1186
  );
1038
1187
  process.exit(1);
@@ -1047,7 +1196,7 @@ async function main(target, options) {
1047
1196
  const unsupportedNames = unsupportedHeaderAgents.map((a) => agents[a].displayName).join(", ");
1048
1197
  const hasExplicitAgentSelection = hasExplicitAgentFlags || selectedViaPrompt;
1049
1198
  if (hasExplicitAgentSelection) {
1050
- p.log.error(
1199
+ p2.log.error(
1051
1200
  `The following agents don't support HTTP headers: ${unsupportedNames}`
1052
1201
  );
1053
1202
  process.exit(1);
@@ -1056,10 +1205,10 @@ async function main(target, options) {
1056
1205
  (a) => agents[a].supportsHeaders
1057
1206
  );
1058
1207
  if (supportedAgents.length === 0) {
1059
- p.log.error("No selected agents support HTTP headers");
1208
+ p2.log.error("No selected agents support HTTP headers");
1060
1209
  process.exit(1);
1061
1210
  }
1062
- p.log.warn(
1211
+ p2.log.warn(
1063
1212
  `Skipping agents that don't support HTTP headers: ${unsupportedNames}`
1064
1213
  );
1065
1214
  targetAgents = supportedAgents;
@@ -1083,7 +1232,7 @@ async function main(target, options) {
1083
1232
  if (selectedWithLocal.length > 0) {
1084
1233
  let installLocally = true;
1085
1234
  if (!options.yes) {
1086
- const scope = await p.select({
1235
+ const scope = await p2.select({
1087
1236
  message: "Installation scope",
1088
1237
  options: [
1089
1238
  {
@@ -1098,8 +1247,8 @@ async function main(target, options) {
1098
1247
  }
1099
1248
  ]
1100
1249
  });
1101
- if (p.isCancel(scope)) {
1102
- p.cancel("Installation cancelled");
1250
+ if (p2.isCancel(scope)) {
1251
+ p2.cancel("Installation cancelled");
1103
1252
  process.exit(0);
1104
1253
  }
1105
1254
  installLocally = scope;
@@ -1108,7 +1257,7 @@ async function main(target, options) {
1108
1257
  agentRouting.set(agent, installLocally ? "local" : "global");
1109
1258
  }
1110
1259
  } else {
1111
- p.log.info("Selected agents only support global installation");
1260
+ p2.log.info("Selected agents only support global installation");
1112
1261
  }
1113
1262
  }
1114
1263
  const summaryLines = [];
@@ -1140,13 +1289,13 @@ async function main(target, options) {
1140
1289
  );
1141
1290
  }
1142
1291
  console.log();
1143
- p.note(summaryLines.join("\n"), "Installation Summary");
1292
+ p2.note(summaryLines.join("\n"), "Installation Summary");
1144
1293
  if (!options.yes) {
1145
- const confirmed = await p.confirm({
1294
+ const confirmed = await p2.confirm({
1146
1295
  message: "Proceed with installation?"
1147
1296
  });
1148
- if (p.isCancel(confirmed) || !confirmed) {
1149
- p.cancel("Installation cancelled");
1297
+ if (p2.isCancel(confirmed) || !confirmed) {
1298
+ p2.cancel("Installation cancelled");
1150
1299
  process.exit(0);
1151
1300
  }
1152
1301
  }
@@ -1162,12 +1311,12 @@ async function main(target, options) {
1162
1311
  const resultLines = [];
1163
1312
  for (const [agentType, result] of successful) {
1164
1313
  const agent = agents[agentType];
1165
- const shortPath = shortenPath(result.path);
1314
+ const shortPath = shortenPath2(result.path);
1166
1315
  resultLines.push(
1167
1316
  `${chalk.green("\u2713")} ${agent.displayName}: ${chalk.dim(shortPath)}`
1168
1317
  );
1169
1318
  }
1170
- p.note(
1319
+ p2.note(
1171
1320
  resultLines.join("\n"),
1172
1321
  chalk.green(
1173
1322
  `Installed to ${successful.length} agent${successful.length !== 1 ? "s" : ""}`
@@ -1176,18 +1325,18 @@ async function main(target, options) {
1176
1325
  }
1177
1326
  if (failed.length > 0) {
1178
1327
  console.log();
1179
- p.log.error(
1328
+ p2.log.error(
1180
1329
  chalk.red(
1181
1330
  `Failed to install to ${failed.length} agent${failed.length !== 1 ? "s" : ""}`
1182
1331
  )
1183
1332
  );
1184
1333
  for (const [agentType, result] of failed) {
1185
1334
  const agent = agents[agentType];
1186
- p.log.message(
1335
+ p2.log.message(
1187
1336
  ` ${chalk.red("\u2717")} ${agent.displayName}: ${chalk.dim(result.error)}`
1188
1337
  );
1189
1338
  }
1190
1339
  }
1191
1340
  console.log();
1192
- p.outro(chalk.green("Done!"));
1341
+ p2.outro(chalk.green("Done!"));
1193
1342
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "add-mcp",
3
- "version": "0.5.0",
3
+ "version": "0.6.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/installer.test.ts && tsx tests/e2e/install.test.ts",
20
- "test:unit": "tsx tests/source-parser.test.ts && tsx tests/agents.test.ts && tsx tests/installer.test.ts",
19
+ "test": "tsx tests/source-parser.test.ts && tsx tests/agents.test.ts && tsx tests/mcp-lock.test.ts && tsx tests/installer.test.ts && tsx tests/e2e/install.test.ts",
20
+ "test:unit": "tsx tests/source-parser.test.ts && tsx tests/agents.test.ts && tsx tests/mcp-lock.test.ts && tsx tests/installer.test.ts",
21
21
  "test:e2e": "tsx tests/e2e/install.test.ts",
22
22
  "typecheck": "tsc --noEmit",
23
23
  "prepublishOnly": "npm run build"