From 170128b7fbf21b8afe679aded2cb8712ecf1d51d Mon Sep 17 00:00:00 2001 From: haosenwang1018 Date: Sun, 22 Mar 2026 04:27:54 +0000 Subject: [PATCH 1/4] fix: publish linux arm64 release binary --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7bdc987..07eb62b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 @@ -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 From bb332993b8b85fca4e24a8e9e77bf854796bf1d3 Mon Sep 17 00:00:00 2001 From: haosenwang1018 Date: Sun, 22 Mar 2026 04:53:20 +0000 Subject: [PATCH 2/4] fix: scope env substitution to targeted servers --- src/commands/call.ts | 22 +++++++++++++++------- src/commands/info.ts | 9 ++++++--- src/config.ts | 36 +++++++++++++++++++++++++++++++----- tests/config.test.ts | 29 +++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/commands/call.ts b/src/commands/call.ts index 4579847..6040c52 100644 --- a/src/commands/call.ts +++ b/src/commands/call.ts @@ -110,13 +110,6 @@ async function parseArgs( export async function callCommand(options: CallOptions): Promise { 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; @@ -129,6 +122,21 @@ export async function callCommand(options: CallOptions): Promise { 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); diff --git a/src/commands/info.ts b/src/commands/info.ts index e88bea6..8e8c208 100644 --- a/src/commands/info.ts +++ b/src/commands/info.ts @@ -40,15 +40,18 @@ function parseTarget(target: string): { server: string; tool?: string } { export async function infoCommand(options: InfoOptions): Promise { 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); diff --git a/src/config.ts b/src/config.ts index 99a4e25..2f8ffa0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -56,6 +56,11 @@ export interface McpServersConfig { mcpServers: Record; } +export interface LoadConfigOptions { + explicitPath?: string; + serverNames?: string[]; +} + // ============================================================================ // Tool Filtering // ============================================================================ @@ -397,13 +402,16 @@ function getDefaultConfigPaths(): string[] { * Load and parse MCP servers configuration */ export async function loadConfig( - explicitPath?: string, + input?: string | LoadConfigOptions, ): Promise { + 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); } @@ -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 = {}; + + 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; } diff --git a/tests/config.test.ts b/tests/config.test.ts index a48602c..3253595 100644 --- a/tests/config.test.ts +++ b/tests/config.test.ts @@ -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( From 7c3fcf86cbe4af5dca8f35614aa9b0cf207d4848 Mon Sep 17 00:00:00 2001 From: haosenwang1018 Date: Sun, 22 Mar 2026 05:05:46 +0000 Subject: [PATCH 3/4] fix: only stream server stderr in debug mode --- src/client.ts | 24 +++++++++++++++++++----- tests/client.test.ts | 28 ++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/client.ts b/src/client.ts index 50731cf..b994c53 100644 --- a/src/client.ts +++ b/src/client.ts @@ -154,6 +154,16 @@ function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } +/** + * Whether server stderr should be streamed live to the terminal. + * + * Default is quiet to avoid noisy MCP/LSP server logs polluting stdout/stderr + * for normal CLI use and AI-agent pipelines. Use MCP_DEBUG=1 to see live logs. + */ +export function shouldStreamServerStderr(): boolean { + return Boolean(process.env.MCP_DEBUG); +} + /** * Execute a function with retry logic for transient failures * Respects overall timeout budget from MCP_TIMEOUT @@ -244,14 +254,16 @@ export async function connectToServer( transport = createStdioTransport(config); // Capture stderr for debugging - attach BEFORE connect - // Always stream stderr immediately so auth prompts are visible + // Only stream stderr live in debug mode to avoid noisy servers polluting output const stderrStream = transport.stderr; if (stderrStream) { stderrStream.on('data', (chunk: Buffer) => { const text = chunk.toString(); stderrChunks.push(text); - // Always stream stderr immediately so users can see auth prompts - process.stderr.write(`[${serverName}] ${text}`); + // In debug mode, show server stderr with server name prefix + if (shouldStreamServerStderr()) { + process.stderr.write(`[${serverName}] ${text}`); + } }); } } @@ -268,12 +280,14 @@ export async function connectToServer( throw error; } - // For successful connections, forward stderr to console + // For successful connections, forward stderr to console only in debug mode if (!isHttpServer(config)) { const stderrStream = (transport as StdioClientTransport).stderr; if (stderrStream) { stderrStream.on('data', (chunk: Buffer) => { - process.stderr.write(chunk); + if (shouldStreamServerStderr()) { + process.stderr.write(chunk); + } }); } } diff --git a/tests/client.test.ts b/tests/client.test.ts index 9c53200..042bce0 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -15,6 +15,7 @@ import { isTransientError, getTimeoutMs, getConcurrencyLimit, + shouldStreamServerStderr, } from '../src/client'; describe('client', () => { @@ -240,6 +241,33 @@ describe('client', () => { }); }); + describe('shouldStreamServerStderr', () => { + const originalEnv = process.env.MCP_DEBUG; + + afterEach(() => { + if (originalEnv !== undefined) { + process.env.MCP_DEBUG = originalEnv; + } else { + delete process.env.MCP_DEBUG; + } + }); + + test('returns false by default (quiet mode)', () => { + delete process.env.MCP_DEBUG; + expect(shouldStreamServerStderr()).toBe(false); + }); + + test('returns true when MCP_DEBUG=1', () => { + process.env.MCP_DEBUG = '1'; + expect(shouldStreamServerStderr()).toBe(true); + }); + + test('returns true when MCP_DEBUG is set to any value', () => { + process.env.MCP_DEBUG = 'true'; + expect(shouldStreamServerStderr()).toBe(true); + }); + }); + // Note: Actually testing connectToServer requires a real MCP server // Those tests are in the integration test suite }); From 3ce8ee148d79807412a01468e03e9ccd48c5a1f5 Mon Sep 17 00:00:00 2001 From: haosenwang1018 Date: Sun, 22 Mar 2026 05:57:56 +0000 Subject: [PATCH 4/4] docs: clarify debug stderr behavior --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4d1c6a3..c7552da 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ mcp-cli [options] call Call tool with JSON arguments | Stream | Content | |--------|---------| | **stdout** | Tool results and human-readable info | -| **stderr** | Errors and diagnostics | +| **stderr** | Errors and diagnostics (plus live server logs only when `MCP_DEBUG=1`) | ### Commands @@ -352,7 +352,7 @@ The CLI searches for configuration in this order: | Variable | Description | Default | |----------|-------------|---------| | `MCP_CONFIG_PATH` | Path to config file | (none) | -| `MCP_DEBUG` | Enable debug output | `false` | +| `MCP_DEBUG` | Enable debug output and live stdio server stderr streaming | `false` | | `MCP_TIMEOUT` | Request timeout (seconds) | `1800` (30 min) | | `MCP_CONCURRENCY` | Servers processed in parallel (not a limit on total) | `5` | | `MCP_MAX_RETRIES` | Retry attempts for transient errors (0 = disable) | `3` | @@ -374,6 +374,18 @@ Traditional MCP integration loads full tool schemas into the AI's context window - **Shell composable**: Chain with `jq`, pipes, and scripts - **Scriptable**: AI can write shell scripts for complex workflows +### Debug logging behavior + +By default, `mcp-cli` keeps successful stdio server stderr quiet so MCP/LSP startup banners do not pollute normal CLI output or agent pipelines. + +If you want to see live server logs while debugging a connection, enable `MCP_DEBUG`: + +```bash +MCP_DEBUG=1 mcp-cli info filesystem +``` + +Connection failures still include captured server stderr in the final error message, even when debug mode is off. + ### Option 1: System Prompt Integration Add this to your AI agent's system prompt for direct CLI access: