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.
- package/README.md +1 -1
- package/dist/index.js +412 -25
- 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(
|
|
1597
|
+
function detectIndent(text3) {
|
|
1310
1598
|
let result = null;
|
|
1311
|
-
jsonc.visit(
|
|
1599
|
+
jsonc.visit(text3, {
|
|
1312
1600
|
onObjectProperty: (_property, offset, _length, startLine, startCharacter) => {
|
|
1313
1601
|
if (result === null && startLine > 0 && startCharacter > 0) {
|
|
1314
|
-
const lineStart =
|
|
1315
|
-
const whitespace =
|
|
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.
|
|
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/
|
|
2134
|
+
url: "git+https://github.com/neon-solutions/add-mcp.git"
|
|
1846
2135
|
},
|
|
1847
|
-
homepage: "https://github.com/
|
|
2136
|
+
homepage: "https://github.com/neon-solutions/add-mcp#readme",
|
|
1848
2137
|
bugs: {
|
|
1849
|
-
url: "https://github.com/
|
|
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:
|
|
2704
|
-
env:
|
|
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.
|
|
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/
|
|
49
|
+
"url": "git+https://github.com/neon-solutions/add-mcp.git"
|
|
49
50
|
},
|
|
50
|
-
"homepage": "https://github.com/
|
|
51
|
+
"homepage": "https://github.com/neon-solutions/add-mcp#readme",
|
|
51
52
|
"bugs": {
|
|
52
|
-
"url": "https://github.com/
|
|
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",
|