add-mcp 1.8.0 → 1.9.1

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 +1 -1
  2. package/dist/index.js +412 -25
  3. package/package.json +8 -6
package/README.md CHANGED
@@ -18,7 +18,7 @@ Example installing the Context7 remote MCP server:
18
18
  npx add-mcp https://mcp.context7.com/mcp
19
19
  ```
20
20
 
21
- You can add env variables and arguments (stdio) and headers (remote) to the server config using the `--env`, `--args` and `--header` options.
21
+ You can add env variables and arguments (stdio) and headers (remote) to the server config using the `--env`, `--args` and `--header` options. With `${VAR}` placeholders, interactive installs prompt for each variable (omit skipped optional keys so empty strings are not written to config).
22
22
 
23
23
  ## Find an MCP Servers
24
24
 
package/dist/index.js CHANGED
@@ -816,6 +816,234 @@ function isRemoteSource(parsed) {
816
816
 
817
817
  // src/find.ts
818
818
  import * as p2 from "@clack/prompts";
819
+
820
+ // src/template.ts
821
+ var TEMPLATE_PATTERN = /\$\{([^}]+)\}/g;
822
+ function findTemplateVars(value) {
823
+ const vars = [];
824
+ let match;
825
+ while ((match = TEMPLATE_PATTERN.exec(value)) !== null) {
826
+ vars.push(match[1]);
827
+ }
828
+ TEMPLATE_PATTERN.lastIndex = 0;
829
+ return vars;
830
+ }
831
+ function substituteTemplatePlaceholders(value, replacement) {
832
+ return value.replace(/\$\{([^}]+)\}/g, () => replacement);
833
+ }
834
+ async function resolveTemplates(value, ask) {
835
+ const vars = findTemplateVars(value);
836
+ if (vars.length === 0) return { resolved: value, cancelled: false };
837
+ let result = value;
838
+ for (const varName of vars) {
839
+ const answer = await ask(varName);
840
+ if (typeof answer === "symbol") return { resolved: value, cancelled: true };
841
+ const entered = typeof answer === "string" ? answer : "";
842
+ result = result.replace(`\${${varName}}`, entered);
843
+ }
844
+ return { resolved: result, cancelled: false };
845
+ }
846
+ async function resolveRecordTemplates(record, ask) {
847
+ const resolved = {};
848
+ for (const [key, value] of Object.entries(record)) {
849
+ const result = await resolveTemplates(value, ask);
850
+ if (result.cancelled) return { resolved: record, cancelled: true };
851
+ resolved[key] = result.resolved;
852
+ }
853
+ return { resolved, cancelled: false };
854
+ }
855
+ async function resolveArrayTemplates(values, ask) {
856
+ const resolved = [];
857
+ for (const value of values) {
858
+ const result = await resolveTemplates(value, ask);
859
+ if (result.cancelled) return { resolved: values, cancelled: true };
860
+ resolved.push(result.resolved);
861
+ }
862
+ return { resolved, cancelled: false };
863
+ }
864
+ function hasTemplateVars(values) {
865
+ const strings = Array.isArray(values) ? values : Object.values(values);
866
+ return strings.some((v) => findTemplateVars(v).length > 0);
867
+ }
868
+
869
+ // src/package-arguments.ts
870
+ var ARG_PLACEHOLDER = "<your-variable-value-here>";
871
+ function normalizeNamedFlag(name) {
872
+ const t = name.trim();
873
+ if (t.startsWith("-")) return t;
874
+ return `--${t}`;
875
+ }
876
+ function definitionsFromPackage(pkg) {
877
+ return pkg.packageArguments ?? [];
878
+ }
879
+ function isNamedArg(d) {
880
+ if (d.type === "positional") return false;
881
+ if (d.type === "named") return true;
882
+ return Boolean(d.name?.trim());
883
+ }
884
+ function labelForArg(d, index) {
885
+ if (isNamedArg(d)) {
886
+ return `Flag ${normalizeNamedFlag(d.name)}`;
887
+ }
888
+ return d.valueHint?.trim() || d.description?.trim() || `Positional argument #${index + 1}`;
889
+ }
890
+ function hasTemplates(s) {
891
+ return Boolean(s && findTemplateVars(s).length > 0);
892
+ }
893
+ function buildPackageArgumentsArgvNonInteractive(definitions, placeholder = ARG_PLACEHOLDER) {
894
+ const out = [];
895
+ for (let i = 0; i < definitions.length; i++) {
896
+ const d = definitions[i];
897
+ if (isNamedArg(d)) {
898
+ const flag = normalizeNamedFlag(d.name);
899
+ let val = d.value?.trim() ?? "";
900
+ if (hasTemplates(d.value)) {
901
+ val = substituteTemplatePlaceholders(d.value, placeholder);
902
+ } else if (!val && d.default?.trim()) {
903
+ val = d.default.trim();
904
+ }
905
+ if (!val) {
906
+ if (d.isRequired === true) {
907
+ out.push(flag, placeholder);
908
+ }
909
+ continue;
910
+ }
911
+ out.push(flag, val);
912
+ } else {
913
+ let val = d.value?.trim() ?? "";
914
+ if (hasTemplates(d.value)) {
915
+ val = substituteTemplatePlaceholders(d.value, placeholder);
916
+ pushPositionalSegments(out, d, val);
917
+ continue;
918
+ }
919
+ if (val) {
920
+ pushPositionalSegments(out, d, val);
921
+ continue;
922
+ }
923
+ if (d.default?.trim()) {
924
+ pushPositionalSegments(out, d, d.default.trim());
925
+ continue;
926
+ }
927
+ pushPositionalSegments(out, d, placeholder);
928
+ }
929
+ }
930
+ return out;
931
+ }
932
+ function pushPositionalSegments(out, d, raw) {
933
+ if (d.isRepeated === true) {
934
+ const parts = raw.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
935
+ if (parts.length === 0) {
936
+ out.push(raw);
937
+ return;
938
+ }
939
+ for (const p4 of parts) {
940
+ out.push(p4);
941
+ }
942
+ return;
943
+ }
944
+ out.push(raw);
945
+ }
946
+ async function buildPackageArgumentsArgvInteractive(definitions, prompt) {
947
+ const out = [];
948
+ for (let i = 0; i < definitions.length; i++) {
949
+ const d = definitions[i];
950
+ const label = labelForArg(d, i);
951
+ if (isNamedArg(d)) {
952
+ const flag = normalizeNamedFlag(d.name);
953
+ let val2 = d.value ?? "";
954
+ if (hasTemplates(d.value)) {
955
+ const res = await resolveTemplates(d.value, async (varName) => {
956
+ const answer = await prompt({
957
+ label: `${label} (${varName})`,
958
+ isRequired: true,
959
+ placeholder: `<${varName}>`
960
+ });
961
+ return answer;
962
+ });
963
+ if (res.cancelled) return { cancelled: true };
964
+ val2 = res.resolved;
965
+ out.push(flag, val2);
966
+ continue;
967
+ }
968
+ if (val2.trim()) {
969
+ out.push(flag, val2.trim());
970
+ continue;
971
+ }
972
+ const defVal2 = d.default?.trim() ?? "";
973
+ const raw2 = await prompt({
974
+ label,
975
+ isRequired: d.isRequired === true,
976
+ placeholder: defVal2 || ARG_PLACEHOLDER
977
+ });
978
+ if (typeof raw2 === "symbol") return { cancelled: true };
979
+ const trimmed2 = typeof raw2 === "string" ? raw2.trim() : "";
980
+ if (!trimmed2) {
981
+ if (d.isRequired === true) {
982
+ out.push(flag, defVal2 || ARG_PLACEHOLDER);
983
+ } else if (defVal2) {
984
+ out.push(flag, defVal2);
985
+ }
986
+ continue;
987
+ }
988
+ out.push(flag, trimmed2);
989
+ continue;
990
+ }
991
+ let val = d.value ?? "";
992
+ if (hasTemplates(d.value)) {
993
+ const res = await resolveTemplates(d.value, async (varName) => {
994
+ const answer = await prompt({
995
+ label: `${label} (${varName})`,
996
+ isRequired: true,
997
+ placeholder: `<${varName}>`
998
+ });
999
+ return answer;
1000
+ });
1001
+ if (res.cancelled) return { cancelled: true };
1002
+ val = res.resolved;
1003
+ pushPositionalSegments(out, d, val);
1004
+ continue;
1005
+ }
1006
+ if (val.trim()) {
1007
+ pushPositionalSegments(out, d, val.trim());
1008
+ continue;
1009
+ }
1010
+ const defVal = d.default?.trim() ?? "";
1011
+ if (defVal) {
1012
+ pushPositionalSegments(out, d, defVal);
1013
+ continue;
1014
+ }
1015
+ if (d.isRequired !== true) {
1016
+ const raw2 = await prompt({
1017
+ label,
1018
+ isRequired: false,
1019
+ placeholder: ARG_PLACEHOLDER
1020
+ });
1021
+ if (typeof raw2 === "symbol") return { cancelled: true };
1022
+ const trimmed2 = typeof raw2 === "string" ? raw2.trim() : "";
1023
+ if (!trimmed2) {
1024
+ pushPositionalSegments(out, d, ARG_PLACEHOLDER);
1025
+ } else {
1026
+ pushPositionalSegments(out, d, trimmed2);
1027
+ }
1028
+ continue;
1029
+ }
1030
+ const raw = await prompt({
1031
+ label,
1032
+ isRequired: true,
1033
+ placeholder: ARG_PLACEHOLDER
1034
+ });
1035
+ if (typeof raw === "symbol") return { cancelled: true };
1036
+ const trimmed = typeof raw === "string" ? raw.trim() : "";
1037
+ pushPositionalSegments(
1038
+ out,
1039
+ d,
1040
+ trimmed.length > 0 ? trimmed : ARG_PLACEHOLDER
1041
+ );
1042
+ }
1043
+ return { argv: out, cancelled: false };
1044
+ }
1045
+
1046
+ // src/find.ts
819
1047
  function buildPlaceholderValue(kind) {
820
1048
  return kind === "header" ? "<your-header-value-here>" : "<your-variable-value-here>";
821
1049
  }
@@ -1009,9 +1237,6 @@ function pickRemote(entry, preferredTransport) {
1009
1237
  return preferred ?? remotes[0] ?? null;
1010
1238
  }
1011
1239
  function formatPackageTarget(pkg) {
1012
- if (pkg.version) {
1013
- return `${pkg.identifier}@${pkg.version}`;
1014
- }
1015
1240
  return pkg.identifier;
1016
1241
  }
1017
1242
  function transportLabel(entry) {
@@ -1064,6 +1289,15 @@ function headerFields(headers) {
1064
1289
  placeholder: buildPlaceholderValue("header")
1065
1290
  }));
1066
1291
  }
1292
+ function packageVariableFields(variables) {
1293
+ if (!variables) return [];
1294
+ return variables.filter((variable) => variable.name && variable.name.trim().length > 0).map((variable) => ({
1295
+ key: variable.name,
1296
+ label: `Environment variable ${variable.name}`,
1297
+ isRequired: variable.isRequired === true,
1298
+ placeholder: buildPlaceholderValue("variable")
1299
+ }));
1300
+ }
1067
1301
  function resolveNonInteractiveRemote(remote) {
1068
1302
  const variableValues = {};
1069
1303
  for (const [key, def] of Object.entries(remote.variables ?? {})) {
@@ -1098,6 +1332,55 @@ async function resolveInteractiveRemote(remote) {
1098
1332
  headers: Object.keys(headerResult.values).length > 0 ? headerResult.values : void 0
1099
1333
  };
1100
1334
  }
1335
+ function resolveNonInteractivePackage(pkg) {
1336
+ const env = {};
1337
+ for (const field of packageVariableFields(pkg.environmentVariables)) {
1338
+ if (field.isRequired) {
1339
+ env[field.key] = field.placeholder;
1340
+ }
1341
+ }
1342
+ const headers = {};
1343
+ for (const field of headerFields(pkg.headers)) {
1344
+ if (field.isRequired) {
1345
+ headers[field.key] = field.placeholder;
1346
+ }
1347
+ }
1348
+ const argv = buildPackageArgumentsArgvNonInteractive(
1349
+ definitionsFromPackage(pkg),
1350
+ buildPlaceholderValue("variable")
1351
+ );
1352
+ return {
1353
+ env: Object.keys(env).length > 0 ? env : void 0,
1354
+ headers: Object.keys(headers).length > 0 ? headers : void 0,
1355
+ args: argv.length > 0 ? argv : void 0
1356
+ };
1357
+ }
1358
+ async function resolveInteractivePackage(pkg) {
1359
+ const promptPackageArg = async (info) => p2.text({
1360
+ message: `${info.label} ${info.isRequired ? "(required)" : "(optional)"}`,
1361
+ placeholder: info.placeholder
1362
+ });
1363
+ const envResult = await collectPromptValues(
1364
+ packageVariableFields(pkg.environmentVariables),
1365
+ promptValue
1366
+ );
1367
+ if (envResult.cancelled) return null;
1368
+ const headerResult = await collectPromptValues(
1369
+ headerFields(pkg.headers),
1370
+ promptValue
1371
+ );
1372
+ if (headerResult.cancelled) return null;
1373
+ const argvResult = await buildPackageArgumentsArgvInteractive(
1374
+ definitionsFromPackage(pkg),
1375
+ promptPackageArg
1376
+ );
1377
+ if (argvResult.cancelled) return null;
1378
+ return {
1379
+ env: Object.keys(envResult.values).length > 0 ? envResult.values : void 0,
1380
+ headers: Object.keys(headerResult.values).length > 0 ? headerResult.values : void 0,
1381
+ args: argvResult.argv.length > 0 ? argvResult.argv : void 0
1382
+ };
1383
+ }
1101
1384
  async function buildInstallPlanForEntry(entry, options) {
1102
1385
  const remote = pickRemote(entry, options.preferredTransport);
1103
1386
  const pkg = entry.package ?? null;
@@ -1130,9 +1413,14 @@ async function buildInstallPlanForEntry(entry, options) {
1130
1413
  mode = hasRemote ? "remote" : "package";
1131
1414
  }
1132
1415
  if (mode === "package" && pkg) {
1416
+ const resolved2 = options.yes ? resolveNonInteractivePackage(pkg) : await resolveInteractivePackage(pkg);
1417
+ if (!resolved2) return null;
1133
1418
  return {
1134
1419
  target: formatPackageTarget(pkg),
1135
- serverName: resolveServerName(entry)
1420
+ serverName: resolveServerName(entry),
1421
+ env: resolved2.env,
1422
+ headers: resolved2.headers,
1423
+ args: resolved2.args
1136
1424
  };
1137
1425
  }
1138
1426
  if (!remote) return null;
@@ -1306,13 +1594,13 @@ function getNestedValue(obj, path) {
1306
1594
  }
1307
1595
 
1308
1596
  // src/formats/json.ts
1309
- function detectIndent(text2) {
1597
+ function detectIndent(text3) {
1310
1598
  let result = null;
1311
- jsonc.visit(text2, {
1599
+ jsonc.visit(text3, {
1312
1600
  onObjectProperty: (_property, offset, _length, startLine, startCharacter) => {
1313
1601
  if (result === null && startLine > 0 && startCharacter > 0) {
1314
- const lineStart = text2.lastIndexOf("\n", offset - 1) + 1;
1315
- const whitespace = text2.slice(lineStart, offset);
1602
+ const lineStart = text3.lastIndexOf("\n", offset - 1) + 1;
1603
+ const whitespace = text3.slice(lineStart, offset);
1316
1604
  result = {
1317
1605
  tabSize: startCharacter,
1318
1606
  insertSpaces: !whitespace.includes(" ")
@@ -1562,7 +1850,7 @@ function buildServerConfig(parsed, options = {}) {
1562
1850
  if (parsed.type === "command") {
1563
1851
  const parts = parsed.value.split(" ");
1564
1852
  const command = parts[0];
1565
- const args = parts.slice(1);
1853
+ const args = [...parts.slice(1), ...options.args ?? []];
1566
1854
  const config2 = {
1567
1855
  command,
1568
1856
  args
@@ -1574,7 +1862,7 @@ function buildServerConfig(parsed, options = {}) {
1574
1862
  }
1575
1863
  const config = {
1576
1864
  command: "npx",
1577
- args: ["-y", parsed.value]
1865
+ args: ["-y", parsed.value, ...options.args ?? []]
1578
1866
  };
1579
1867
  if (options.env && Object.keys(options.env).length > 0) {
1580
1868
  config.env = options.env;
@@ -1797,7 +2085,7 @@ function findMatchingServers(agentServersList, query) {
1797
2085
  // package.json
1798
2086
  var package_default = {
1799
2087
  name: "add-mcp",
1800
- version: "1.8.0",
2088
+ version: "1.9.1",
1801
2089
  description: "Add MCP servers to your favorite coding agents with a single command.",
1802
2090
  author: "Andre Landgraf <andre@neon.tech>",
1803
2091
  license: "Apache-2.0",
@@ -1813,12 +2101,13 @@ var package_default = {
1813
2101
  fmt: "prettier --write .",
1814
2102
  build: "tsup src/index.ts --format esm --dts --clean",
1815
2103
  dev: "tsx src/index.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",
2104
+ test: "tsx tests/source-parser.test.ts && tsx tests/agents.test.ts && tsx tests/config.test.ts && tsx tests/installer.test.ts && tsx tests/find.test.ts && tsx tests/package-arguments.test.ts && tsx tests/template.test.ts && tsx tests/reader.test.ts && tsx tests/formats-remove.test.ts && tsx tests/e2e/install.test.ts && tsx tests/e2e/cli.test.ts",
2105
+ "test:unit": "tsx tests/source-parser.test.ts && tsx tests/agents.test.ts && tsx tests/config.test.ts && tsx tests/installer.test.ts && tsx tests/find.test.ts && tsx tests/package-arguments.test.ts && tsx tests/template.test.ts && tsx tests/reader.test.ts && tsx tests/formats-remove.test.ts",
1818
2106
  "registry:sort": "tsx scripts/sort-registry.ts",
1819
2107
  "registry:verify": "tsx scripts/verify-registry.ts",
1820
2108
  "test:e2e": "tsx tests/e2e/install.test.ts && tsx tests/e2e/cli.test.ts",
1821
2109
  typecheck: "tsc --noEmit",
2110
+ fallow: "fallow",
1822
2111
  prepublishOnly: "npm run build"
1823
2112
  },
1824
2113
  keywords: [
@@ -1842,11 +2131,11 @@ var package_default = {
1842
2131
  ],
1843
2132
  repository: {
1844
2133
  type: "git",
1845
- url: "git+https://github.com/neondatabase/add-mcp.git"
2134
+ url: "git+https://github.com/neon-solutions/add-mcp.git"
1846
2135
  },
1847
- homepage: "https://github.com/neondatabase/add-mcp#readme",
2136
+ homepage: "https://github.com/neon-solutions/add-mcp#readme",
1848
2137
  bugs: {
1849
- url: "https://github.com/neondatabase/add-mcp/issues"
2138
+ url: "https://github.com/neon-solutions/add-mcp/issues"
1850
2139
  },
1851
2140
  dependencies: {
1852
2141
  "@clack/prompts": "^0.9.1",
@@ -1859,6 +2148,7 @@ var package_default = {
1859
2148
  devDependencies: {
1860
2149
  "@types/js-yaml": "^4.0.9",
1861
2150
  "@types/node": "^22.10.0",
2151
+ fallow: "^2.61.0",
1862
2152
  prettier: "^3.8.1",
1863
2153
  tsup: "^8.3.5",
1864
2154
  tsx: "^4.19.2",
@@ -1976,6 +2266,27 @@ function extractSubcommandOptionsFromArgv() {
1976
2266
  result.gitignore = true;
1977
2267
  continue;
1978
2268
  }
2269
+ if (arg === "--header" && argv[i + 1]) {
2270
+ const headers = result.header ? [...result.header] : [];
2271
+ headers.push(argv[i + 1]);
2272
+ result.header = headers;
2273
+ i += 1;
2274
+ continue;
2275
+ }
2276
+ if (arg === "--env" && argv[i + 1]) {
2277
+ const env = result.env ? [...result.env] : [];
2278
+ env.push(argv[i + 1]);
2279
+ result.env = env;
2280
+ i += 1;
2281
+ continue;
2282
+ }
2283
+ if (arg === "--args" && argv[i + 1]) {
2284
+ const args = result.args ? [...result.args] : [];
2285
+ args.push(argv[i + 1]);
2286
+ result.args = args;
2287
+ i += 1;
2288
+ continue;
2289
+ }
1979
2290
  if ((arg === "-n" || arg === "--name") && argv[i + 1]) {
1980
2291
  result.name = argv[i + 1];
1981
2292
  i += 1;
@@ -2020,6 +2331,16 @@ function inferFindPreferredTransport(options) {
2020
2331
  function collect(value, previous) {
2021
2332
  return previous.concat([value]);
2022
2333
  }
2334
+ function looksLikeEatenShellVar(invalidEntries, separator) {
2335
+ for (const entry of invalidEntries) {
2336
+ const separatorIndex = entry.indexOf(separator);
2337
+ if (separatorIndex <= 0) continue;
2338
+ const key = entry.slice(0, separatorIndex).trim();
2339
+ const value = entry.slice(separatorIndex + 1).trim();
2340
+ if (key && !value) return true;
2341
+ }
2342
+ return false;
2343
+ }
2023
2344
  function parseHeaders(values) {
2024
2345
  const headers = {};
2025
2346
  const invalid = [];
@@ -2049,8 +2370,8 @@ function parseEnv(values) {
2049
2370
  continue;
2050
2371
  }
2051
2372
  const key = entry.slice(0, separatorIndex).trim();
2052
- const value = entry.slice(separatorIndex + 1);
2053
- if (!key) {
2373
+ const value = entry.slice(separatorIndex + 1).trim();
2374
+ if (!key || !value) {
2054
2375
  invalid.push(entry);
2055
2376
  continue;
2056
2377
  }
@@ -2058,6 +2379,13 @@ function parseEnv(values) {
2058
2379
  }
2059
2380
  return { env, invalid };
2060
2381
  }
2382
+ function omitEmptyStringValues(record) {
2383
+ return Object.fromEntries(
2384
+ Object.entries(record).filter(
2385
+ ([, v]) => typeof v === "string" && v.trim().length > 0
2386
+ )
2387
+ );
2388
+ }
2061
2389
  program.name("add-mcp").description(
2062
2390
  "Install MCP servers for coding agents (Claude Code, Cursor, VS Code, OpenCode, Codex, and more \u2014 run list-agents for the full list)"
2063
2391
  ).version(version).argument("[target]", "MCP server URL (remote) or package name (local stdio)").option(
@@ -2071,12 +2399,17 @@ program.name("add-mcp").description(
2071
2399
  "Transport type for remote servers (http, sse)"
2072
2400
  ).option("--type <type>", "Alias for --transport").option(
2073
2401
  "--header <header>",
2074
- "HTTP header for remote servers (repeatable, 'Key: Value')",
2402
+ "HTTP header for remote servers (repeatable, 'Key: Value'). Placeholders ${VAR} prompt interactively when not using --yes. Use single quotes so your shell does not expand the ${VAR}.",
2075
2403
  collect,
2076
2404
  []
2077
2405
  ).option(
2078
2406
  "--env <env>",
2079
- "Environment variable for local stdio servers (repeatable, 'KEY=VALUE')",
2407
+ "Environment variable for local stdio servers (repeatable, 'KEY=VALUE'). Placeholders ${VAR} prompt interactively when not using --yes. Use single quotes so your shell does not expand the ${VAR}.",
2408
+ collect,
2409
+ []
2410
+ ).option(
2411
+ "--args <arg>",
2412
+ "Argument for local stdio servers (repeatable). Placeholders ${VAR} prompt interactively when not using --yes. Use single quotes so your shell does not expand the ${VAR}.",
2080
2413
  collect,
2081
2414
  []
2082
2415
  ).option("-y, --yes", "Skip confirmation prompts").option("--all", "Install to all agents").option("--gitignore", "Add generated project config files to .gitignore").action(async (target, options) => {
@@ -2111,7 +2444,9 @@ async function runFindCommand(keyword, rawOptions) {
2111
2444
  transport: installPlan.transport,
2112
2445
  header: installPlan.headers ? Object.entries(installPlan.headers).map(
2113
2446
  ([key, value]) => `${key}: ${value}`
2114
- ) : options.header
2447
+ ) : options.header,
2448
+ env: installPlan.env ? Object.entries(installPlan.env).map(([key, value]) => `${key}=${value}`) : options.env,
2449
+ args: installPlan.args ?? options.args
2115
2450
  };
2116
2451
  await main(installPlan.target, mergedOptions);
2117
2452
  }
@@ -2656,8 +2991,9 @@ async function main(target, options) {
2656
2991
  const headerValues = options.header ?? [];
2657
2992
  const headerResult = parseHeaders(headerValues);
2658
2993
  if (headerResult.invalid.length > 0) {
2994
+ const hint = looksLikeEatenShellVar(headerResult.invalid, ":") ? " (looks like your shell expanded a ${VAR} to an empty string; use single quotes: --header 'Key: ${VAR}' to pass the template literally)" : "";
2659
2995
  p3.log.error(
2660
- `Invalid --header value(s): ${headerResult.invalid.join(", ")}. Use "Key: Value" format.`
2996
+ `Invalid --header value(s): ${headerResult.invalid.join(", ")}. Use "Key: Value" format.${hint}`
2661
2997
  );
2662
2998
  process.exit(1);
2663
2999
  }
@@ -2669,8 +3005,9 @@ async function main(target, options) {
2669
3005
  const envValues = options.env ?? [];
2670
3006
  const envResult = parseEnv(envValues);
2671
3007
  if (envResult.invalid.length > 0) {
3008
+ const hint = looksLikeEatenShellVar(envResult.invalid, "=") ? " (looks like your shell expanded a ${VAR} to an empty string; use single quotes: --env 'KEY=${VAR}' to pass the template literally)" : "";
2672
3009
  p3.log.error(
2673
- `Invalid --env value(s): ${envResult.invalid.join(", ")}. Use "KEY=VALUE" format.`
3010
+ `Invalid --env value(s): ${envResult.invalid.join(", ")}. Use "KEY=VALUE" format.${hint}`
2674
3011
  );
2675
3012
  process.exit(1);
2676
3013
  }
@@ -2681,6 +3018,55 @@ async function main(target, options) {
2681
3018
  "--env is only used for local/package/command installs, ignoring"
2682
3019
  );
2683
3020
  }
3021
+ const argsValues = options.args ?? [];
3022
+ const hasArgsValues = argsValues.length > 0;
3023
+ if (hasArgsValues && isRemote) {
3024
+ p3.log.warn(
3025
+ "--args is only used for local/package/command installs, ignoring"
3026
+ );
3027
+ }
3028
+ const promptTemplateVar = (varName) => p3.text({
3029
+ message: `Enter value for ${varName}`,
3030
+ placeholder: `<${varName}>`
3031
+ });
3032
+ let resolvedArgs = [...argsValues];
3033
+ if (!options.yes && hasHeaderValues && hasTemplateVars(headerResult.headers)) {
3034
+ const result = await resolveRecordTemplates(
3035
+ headerResult.headers,
3036
+ promptTemplateVar
3037
+ );
3038
+ if (result.cancelled) {
3039
+ p3.cancel("Cancelled");
3040
+ process.exit(0);
3041
+ }
3042
+ for (const [key, value] of Object.entries(result.resolved)) {
3043
+ headerResult.headers[key] = value;
3044
+ }
3045
+ }
3046
+ if (!options.yes && hasEnvValues && hasTemplateVars(envResult.env)) {
3047
+ const result = await resolveRecordTemplates(
3048
+ envResult.env,
3049
+ promptTemplateVar
3050
+ );
3051
+ if (result.cancelled) {
3052
+ p3.cancel("Cancelled");
3053
+ process.exit(0);
3054
+ }
3055
+ for (const [key, value] of Object.entries(result.resolved)) {
3056
+ envResult.env[key] = value;
3057
+ }
3058
+ }
3059
+ if (!options.yes && hasArgsValues && hasTemplateVars(resolvedArgs)) {
3060
+ const result = await resolveArrayTemplates(resolvedArgs, promptTemplateVar);
3061
+ if (result.cancelled) {
3062
+ p3.cancel("Cancelled");
3063
+ process.exit(0);
3064
+ }
3065
+ resolvedArgs = result.resolved;
3066
+ }
3067
+ const headersForConfig = isRemote && hasHeaderValues ? omitEmptyStringValues(headerResult.headers) : void 0;
3068
+ const envForConfig = !isRemote && hasEnvValues ? omitEmptyStringValues(envResult.env) : void 0;
3069
+ const argsForConfig = !isRemote && hasArgsValues ? resolvedArgs.filter((a) => a.trim().length > 0) : void 0;
2684
3070
  const serverName = options.name || parsed.inferredName;
2685
3071
  p3.log.info(`Server name: ${chalk.cyan(serverName)}`);
2686
3072
  const transportValue = options.transport || options.type;
@@ -2700,8 +3086,9 @@ async function main(target, options) {
2700
3086
  }
2701
3087
  const serverConfig = buildServerConfig(parsed, {
2702
3088
  transport: resolvedTransport,
2703
- headers: isRemote && hasHeaderValues ? headerResult.headers : void 0,
2704
- env: !isRemote && hasEnvValues ? envResult.env : void 0
3089
+ headers: headersForConfig && Object.keys(headersForConfig).length > 0 ? headersForConfig : void 0,
3090
+ env: envForConfig && Object.keys(envForConfig).length > 0 ? envForConfig : void 0,
3091
+ args: argsForConfig && argsForConfig.length > 0 ? argsForConfig : void 0
2705
3092
  });
2706
3093
  let targetAgents;
2707
3094
  const allAgentTypes = getAgentTypes();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "add-mcp",
3
- "version": "1.8.0",
3
+ "version": "1.9.1",
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,12 +16,13 @@
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/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",
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/package-arguments.test.ts && tsx tests/template.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/package-arguments.test.ts && tsx tests/template.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",
24
24
  "typecheck": "tsc --noEmit",
25
+ "fallow": "fallow",
25
26
  "prepublishOnly": "npm run build"
26
27
  },
27
28
  "keywords": [
@@ -45,11 +46,11 @@
45
46
  ],
46
47
  "repository": {
47
48
  "type": "git",
48
- "url": "git+https://github.com/neondatabase/add-mcp.git"
49
+ "url": "git+https://github.com/neon-solutions/add-mcp.git"
49
50
  },
50
- "homepage": "https://github.com/neondatabase/add-mcp#readme",
51
+ "homepage": "https://github.com/neon-solutions/add-mcp#readme",
51
52
  "bugs": {
52
- "url": "https://github.com/neondatabase/add-mcp/issues"
53
+ "url": "https://github.com/neon-solutions/add-mcp/issues"
53
54
  },
54
55
  "dependencies": {
55
56
  "@clack/prompts": "^0.9.1",
@@ -62,6 +63,7 @@
62
63
  "devDependencies": {
63
64
  "@types/js-yaml": "^4.0.9",
64
65
  "@types/node": "^22.10.0",
66
+ "fallow": "^2.61.0",
65
67
  "prettier": "^3.8.1",
66
68
  "tsup": "^8.3.5",
67
69
  "tsx": "^4.19.2",