Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# Changelog

## [1.9.0] - 2026-04-08

- prompt for `${VAR}` template values in `--env` and `--header` flags during interactive mode
- prompt for package environment variables, headers, and arguments during `find` search installs

## [1.8.1] - 2026-04-07

- fix `find` / `search` package installs to stop pinning npm versions and resolve latest implicitly
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "add-mcp",
"version": "1.8.1",
"version": "1.9.0",
"description": "Add MCP servers to your favorite coding agents with a single command.",
"author": "Andre Landgraf <andre@neon.tech>",
"license": "Apache-2.0",
Expand All @@ -16,8 +16,8 @@
"fmt": "prettier --write .",
"build": "tsup src/index.ts --format esm --dts --clean",
"dev": "tsx src/index.ts",
"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",
"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",
"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/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",
"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/template.test.ts && tsx tests/reader.test.ts && tsx tests/formats-remove.test.ts",
"registry:sort": "tsx scripts/sort-registry.ts",
"registry:verify": "tsx scripts/verify-registry.ts",
"test:e2e": "tsx tests/e2e/install.test.ts && tsx tests/e2e/cli.test.ts",
Expand Down
132 changes: 132 additions & 0 deletions src/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,30 @@ export interface RegistryPackageDefinition {
registryType: "npm" | "oci" | "nuget" | "mcpb";
identifier: string;
version?: string;
environmentVariables?: RegistryNamedVariableDefinition[];
headers?: RegistryHeaderDefinition[];
args?: RegistryPackageArgumentDefinition[];
arguments?: RegistryPackageArgumentDefinition[];
commandArguments?: RegistryPackageArgumentDefinition[];
transport: {
type: "stdio";
};
}

export interface RegistryNamedVariableDefinition extends RegistryVariableDefinition {
name: string;
}

export interface RegistryPackageArgumentDefinition {
name?: string;
value?: string;
description?: string;
isRequired?: boolean;
isSecret?: boolean;
default?: string;
choices?: string[];
}

export interface RegistryServerEntry {
name: string;
title?: string;
Expand All @@ -55,6 +74,8 @@ export interface FindInstallPlan {
serverName: string;
transport?: TransportType;
headers?: Record<string, string>;
env?: Record<string, string>;
args?: string[];
}

export interface PromptField {
Expand Down Expand Up @@ -433,6 +454,41 @@ function headerFields(
}));
}

function packageVariableFields(
variables: RegistryNamedVariableDefinition[] | undefined,
): PromptField[] {
if (!variables) return [];
return variables
.filter((variable) => variable.name && variable.name.trim().length > 0)
.map((variable) => ({
key: variable.name,
label: `Environment variable ${variable.name}`,
isRequired: variable.isRequired === true,
placeholder: buildPlaceholderValue("variable"),
}));
}

function packageArgumentFields(pkg: RegistryPackageDefinition): PromptField[] {
const definitions = [
...(pkg.args ?? []),
...(pkg.arguments ?? []),
...(pkg.commandArguments ?? []),
];
return definitions.map((arg, index) => {
const descriptor =
arg.name?.trim() ||
arg.value?.trim() ||
arg.description?.trim() ||
`#${index + 1}`;
return {
key: String(index),
label: `Argument ${descriptor}`,
isRequired: arg.isRequired === true,
placeholder: buildPlaceholderValue("variable"),
};
});
}

function resolveNonInteractiveRemote(remote: RegistryRemoteDefinition): {
url: string;
headers?: Record<string, string>;
Expand Down Expand Up @@ -481,6 +537,74 @@ async function resolveInteractiveRemote(
};
}

function resolveNonInteractivePackage(pkg: RegistryPackageDefinition): {
env?: Record<string, string>;
headers?: Record<string, string>;
args?: string[];
} {
const env: Record<string, string> = {};
for (const field of packageVariableFields(pkg.environmentVariables)) {
if (field.isRequired) {
env[field.key] = field.placeholder;
}
}

const headers: Record<string, string> = {};
for (const field of headerFields(pkg.headers)) {
if (field.isRequired) {
headers[field.key] = field.placeholder;
}
}

const args = packageArgumentFields(pkg)
.filter((field) => field.isRequired)
.map((field) => field.placeholder);

return {
env: Object.keys(env).length > 0 ? env : undefined,
headers: Object.keys(headers).length > 0 ? headers : undefined,
args: args.length > 0 ? args : undefined,
};
}

async function resolveInteractivePackage(
pkg: RegistryPackageDefinition,
): Promise<{
env?: Record<string, string>;
headers?: Record<string, string>;
args?: string[];
} | null> {
const envResult = await collectPromptValues(
packageVariableFields(pkg.environmentVariables),
promptValue,
);
if (envResult.cancelled) return null;

const headerResult = await collectPromptValues(
headerFields(pkg.headers),
promptValue,
);
if (headerResult.cancelled) return null;

const argumentFields = packageArgumentFields(pkg);
const argsResult = await collectPromptValues(argumentFields, promptValue);
if (argsResult.cancelled) return null;

const args = argumentFields
.map((field) => argsResult.values[field.key])
.filter((value): value is string => typeof value === "string");

return {
env:
Object.keys(envResult.values).length > 0 ? envResult.values : undefined,
headers:
Object.keys(headerResult.values).length > 0
? headerResult.values
: undefined,
args: args.length > 0 ? args : undefined,
};
}

export async function buildInstallPlanForEntry(
entry: RegistryServerEntry,
options: FindCommandOptions,
Expand Down Expand Up @@ -519,9 +643,17 @@ export async function buildInstallPlanForEntry(
}

if (mode === "package" && pkg) {
const resolved = options.yes
? resolveNonInteractivePackage(pkg)
: await resolveInteractivePackage(pkg);
if (!resolved) return null;

return {
target: formatPackageTarget(pkg),
serverName: resolveServerName(entry),
env: resolved.env,
headers: resolved.headers,
args: resolved.args,
};
}

Expand Down
81 changes: 81 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ interface Options {
type?: string;
header?: string[];
env?: string[];
args?: string[];
yes?: boolean;
all?: boolean;
gitignore?: boolean;
Expand Down Expand Up @@ -236,6 +237,27 @@ function extractSubcommandOptionsFromArgv(): Partial<Options> {
result.gitignore = true;
continue;
}
if (arg === "--header" && argv[i + 1]) {
const headers: string[] = result.header ? [...result.header] : [];
headers.push(argv[i + 1]!);
result.header = headers;
i += 1;
continue;
}
if (arg === "--env" && argv[i + 1]) {
const env: string[] = result.env ? [...result.env] : [];
env.push(argv[i + 1]!);
result.env = env;
i += 1;
continue;
}
if (arg === "--args" && argv[i + 1]) {
const args: string[] = result.args ? [...result.args] : [];
args.push(argv[i + 1]!);
result.args = args;
i += 1;
continue;
}
if ((arg === "-n" || arg === "--name") && argv[i + 1]) {
result.name = argv[i + 1];
i += 1;
Expand Down Expand Up @@ -357,6 +379,8 @@ function parseEnv(values: string[]): ParsedEnvResult {
return { env, invalid };
}

import { hasTemplateVars, resolveRecordTemplates } from "./template.js";

program
.name("add-mcp")
.description(
Expand Down Expand Up @@ -390,6 +414,12 @@ program
collect,
[],
)
.option(
"--args <arg>",
"Argument for local stdio servers (repeatable)",
collect,
[],
)
.option("-y, --yes", "Skip confirmation prompts")
.option("--all", "Install to all agents")
.option("--gitignore", "Add generated project config files to .gitignore")
Expand Down Expand Up @@ -440,6 +470,10 @@ async function runFindCommand(
([key, value]) => `${key}: ${value}`,
)
: options.header,
env: installPlan.env
? Object.entries(installPlan.env).map(([key, value]) => `${key}=${value}`)
: options.env,
args: installPlan.args ?? options.args,
};

await main(installPlan.target, mergedOptions);
Expand Down Expand Up @@ -1258,6 +1292,52 @@ async function main(target: string | undefined, options: Options) {
);
}

const argsValues = options.args ?? [];
const hasArgsValues = argsValues.length > 0;
if (hasArgsValues && isRemote) {
p.log.warn(
"--args is only used for local/package/command installs, ignoring",
);
}

const promptTemplateVar = (varName: string) =>
p.text({
message: `Enter value for ${varName}`,
placeholder: `<${varName}>`,
});

if (
!options.yes &&
hasHeaderValues &&
hasTemplateVars(headerResult.headers)
) {
const result = await resolveRecordTemplates(
headerResult.headers,
promptTemplateVar,
);
if (result.cancelled) {
p.cancel("Cancelled");
process.exit(0);
}
for (const [key, value] of Object.entries(result.resolved)) {
headerResult.headers[key] = value;
}
}

if (!options.yes && hasEnvValues && hasTemplateVars(envResult.env)) {
const result = await resolveRecordTemplates(
envResult.env,
promptTemplateVar,
);
if (result.cancelled) {
p.cancel("Cancelled");
process.exit(0);
}
for (const [key, value] of Object.entries(result.resolved)) {
envResult.env[key] = value;
}
}

// Determine server name
const serverName = options.name || parsed.inferredName;
p.log.info(`Server name: ${chalk.cyan(serverName)}`);
Expand Down Expand Up @@ -1285,6 +1365,7 @@ async function main(target: string | undefined, options: Options) {
transport: resolvedTransport,
headers: isRemote && hasHeaderValues ? headerResult.headers : undefined,
env: !isRemote && hasEnvValues ? envResult.env : undefined,
args: !isRemote && hasArgsValues ? argsValues : undefined,
});

// Determine target agents
Expand Down
6 changes: 4 additions & 2 deletions src/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ export interface BuildServerConfigOptions {
headers?: Record<string, string>;
/** Environment variables for local stdio servers */
env?: Record<string, string>;
/** Extra command arguments for local stdio servers */
args?: string[];
}

export interface UpdateGitignoreOptions {
Expand Down Expand Up @@ -69,7 +71,7 @@ export function buildServerConfig(
if (parsed.type === "command") {
const parts = parsed.value.split(" ");
const command = parts[0]!;
const args = parts.slice(1);
const args = [...parts.slice(1), ...(options.args ?? [])];
const config: McpServerConfig = {
command,
args,
Expand All @@ -84,7 +86,7 @@ export function buildServerConfig(

const config: McpServerConfig = {
command: "npx",
args: ["-y", parsed.value],
args: ["-y", parsed.value, ...(options.args ?? [])],
};

if (options.env && Object.keys(options.env).length > 0) {
Expand Down
Loading