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
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ jobs:

- name: Build Linux x64
run: bun build --compile --minify --target=bun-linux-x64 src/index.ts --outfile dist/mcp-cli-linux-x64

- name: Build Linux ARM64
run: bun build --compile --minify --target=bun-linux-arm64 src/index.ts --outfile dist/mcp-cli-linux-arm64

- name: Build macOS x64
run: bun build --compile --minify --target=bun-darwin-x64 src/index.ts --outfile dist/mcp-cli-darwin-x64
Expand Down Expand Up @@ -95,6 +98,7 @@ jobs:
generate_release_notes: true
files: |
dist/mcp-cli-linux-x64
dist/mcp-cli-linux-arm64
dist/mcp-cli-darwin-x64
dist/mcp-cli-darwin-arm64
dist/checksums.txt
22 changes: 15 additions & 7 deletions src/commands/call.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,6 @@ async function parseArgs(
export async function callCommand(options: CallOptions): Promise<void> {
let config: McpServersConfig;

try {
config = await loadConfig(options.configPath);
} catch (error) {
console.error((error as Error).message);
process.exit(ErrorCode.CLIENT_ERROR);
}

let serverName: string;
let toolName: string;

Expand All @@ -129,6 +122,21 @@ export async function callCommand(options: CallOptions): Promise<void> {
process.exit(ErrorCode.CLIENT_ERROR);
}

try {
config = await loadConfig({
explicitPath: options.configPath,
serverNames: [serverName],
});
} catch (error) {
console.error((error as Error).message);
process.exit(ErrorCode.CLIENT_ERROR);
}

try {
console.error((error as Error).message);
process.exit(ErrorCode.CLIENT_ERROR);
}

let serverConfig: ServerConfig;
try {
serverConfig = getServerConfig(config, serverName);
Expand Down
9 changes: 6 additions & 3 deletions src/commands/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,18 @@ function parseTarget(target: string): { server: string; tool?: string } {
export async function infoCommand(options: InfoOptions): Promise<void> {
let config: McpServersConfig;

const { server: serverName, tool: toolName } = parseTarget(options.target);

try {
config = await loadConfig(options.configPath);
config = await loadConfig({
explicitPath: options.configPath,
serverNames: [serverName],
});
} catch (error) {
console.error((error as Error).message);
process.exit(ErrorCode.CLIENT_ERROR);
}

const { server: serverName, tool: toolName } = parseTarget(options.target);

let serverConfig: ServerConfig;
try {
serverConfig = getServerConfig(config, serverName);
Expand Down
36 changes: 31 additions & 5 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export interface McpServersConfig {
mcpServers: Record<string, ServerConfig>;
}

export interface LoadConfigOptions {
explicitPath?: string;
serverNames?: string[];
}

// ============================================================================
// Tool Filtering
// ============================================================================
Expand Down Expand Up @@ -397,13 +402,16 @@ function getDefaultConfigPaths(): string[] {
* Load and parse MCP servers configuration
*/
export async function loadConfig(
explicitPath?: string,
input?: string | LoadConfigOptions,
): Promise<McpServersConfig> {
const options: LoadConfigOptions =
typeof input === 'string' ? { explicitPath: input } : input ?? {};

let configPath: string | undefined;

// Check explicit path from argument or environment
if (explicitPath) {
configPath = resolve(explicitPath);
if (options.explicitPath) {
configPath = resolve(options.explicitPath);
} else if (process.env.MCP_CONFIG_PATH) {
configPath = resolve(process.env.MCP_CONFIG_PATH);
}
Expand Down Expand Up @@ -496,8 +504,26 @@ export async function loadConfig(
}
}

// Substitute environment variables
config = substituteEnvVarsInObject(config);
// Substitute environment variables only for the relevant servers.
// This avoids warning about unrelated missing variables when a command
// targets a single server (for example: `mcp-cli info chrome-devtools`).
if (options.serverNames && options.serverNames.length > 0) {
const targetServers = new Set(options.serverNames);
const substitutedServers: Record<string, ServerConfig> = {};

for (const [serverName, serverConfig] of Object.entries(config.mcpServers)) {
substitutedServers[serverName] = targetServers.has(serverName)
? substituteEnvVarsInObject(serverConfig)
: serverConfig;
}

config = {
...config,
mcpServers: substitutedServers,
};
} else {
config = substituteEnvVarsInObject(config);
}

return config;
}
Expand Down
29 changes: 29 additions & 0 deletions tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,35 @@ describe('config', () => {
await expect(loadConfig(configPath)).rejects.toThrow('MISSING_ENV_VAR');
});

test('only substitutes env vars for targeted servers', async () => {
delete process.env.MCP_STRICT_ENV;

const configPath = join(tempDir, 'targeted_env.json');
await writeFile(
configPath,
JSON.stringify({
mcpServers: {
chrome: {
command: 'echo',
args: ['ok'],
},
context7: {
command: 'echo',
env: { API_KEY: '${MISSING_CONTEXT7_API_KEY}' },
},
},
})
);

const config = await loadConfig({
explicitPath: configPath,
serverNames: ['chrome'],
});

expect((config.mcpServers.chrome as any).command).toBe('echo');
expect((config.mcpServers.context7 as any).env.API_KEY).toBe('${MISSING_CONTEXT7_API_KEY}');
});

test('throws error on empty server config', async () => {
const configPath = join(tempDir, 'empty_server.json');
await writeFile(
Expand Down