From 90b214b57ce4a321fb0bf7fc75c368eb233d80c1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 8 Jun 2026 20:02:50 +0100 Subject: [PATCH] fix(cli): fail unknown list targets --- CHANGELOG.md | 1 + src/cli/list-command.ts | 2 +- tests/cli-list-classification.test.ts | 20 ++++++++++++------- tests/cli-list-json.test.ts | 28 ++++++++++++++++----------- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ecbd3a4..8c40867 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Add `mcporter record` and `mcporter replay` helpers for capturing and replaying MCP JSON-RPC traffic, with server filters and daemon-safe manual env setup. (PR #192, thanks @LDMB123) - Prevent direct daemon starts from rebinding over an already-running healthy daemon, avoiding orphaned keep-alive processes during foreground or launch races. (PR #195, thanks @zm2231) +- Return a non-zero exit code for explicit `mcporter list ` failures while preserving aggregate list health checks by default. (Issue #203, thanks @theo674) - Reconcile keep-alive daemon metadata with the responding process and serialize daemon startup across parallel clients, preventing duplicate orphaned daemons. (Issue #191, thanks @dtmsyi) - Keep daemon-managed stdio servers warm across repeated `mcporter list` requests instead of treating non-interactive tool listing as a throwaway process. (Issue #188, thanks @robertoronderosjr) diff --git a/src/cli/list-command.ts b/src/cli/list-command.ts index 77610d4..825a03c 100644 --- a/src/cli/list-command.ts +++ b/src/cli/list-command.ts @@ -175,7 +175,7 @@ export async function handleList( const resolved = resolveServerDefinition(runtime, target, { quiet: flags.quiet }); if (!resolved) { - maybeSetListExitCode([{ status: 'error' }], flags); + process.exitCode = 1; return; } target = resolved.name; diff --git a/tests/cli-list-classification.test.ts b/tests/cli-list-classification.test.ts index bbd4bc9..58e1b8a 100644 --- a/tests/cli-list-classification.test.ts +++ b/tests/cli-list-classification.test.ts @@ -330,6 +330,8 @@ describe('CLI list classification and routing', () => { it('suggests a server name when the typo is large', async () => { const { handleList } = await cliModulePromise; + const previousExitCode = process.exitCode; + process.exitCode = undefined; const definition = linearDefinition; const listTools = vi.fn(); const runtime = { @@ -343,13 +345,17 @@ describe('CLI list classification and routing', () => { const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - await handleList(runtime, ['zzz']); + try { + await handleList(runtime, ['zzz']); - const errorLines = errorSpy.mock.calls.map((call) => call.join(' ')); - expect(errorLines.some((line) => line.includes('Did you mean linear?'))).toBe(true); - expect(listTools).not.toHaveBeenCalled(); - - errorSpy.mockRestore(); - logSpy.mockRestore(); + const errorLines = errorSpy.mock.calls.map((call) => call.join(' ')); + expect(errorLines.some((line) => line.includes('Did you mean linear?'))).toBe(true); + expect(listTools).not.toHaveBeenCalled(); + expect(process.exitCode).toBe(1); + } finally { + errorSpy.mockRestore(); + logSpy.mockRestore(); + process.exitCode = previousExitCode; + } }); }); diff --git a/tests/cli-list-json.test.ts b/tests/cli-list-json.test.ts index 9678f48..10d4dde 100644 --- a/tests/cli-list-json.test.ts +++ b/tests/cli-list-json.test.ts @@ -37,20 +37,26 @@ function createRuntime(): Runtime { describe('handleList JSON output', () => { it('emits aggregated status counts', async () => { const runtime = createRuntime(); + const previousExitCode = process.exitCode; + process.exitCode = undefined; const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - await runHandleList(runtime, ['--json']); + try { + await runHandleList(runtime, ['--json']); - const payload = JSON.parse(logSpy.mock.calls.at(-1)?.[0] ?? '{}'); - expect(payload.mode).toBe('list'); - expect(payload.counts.auth).toBe(1); - const healthyEntry = payload.servers.find((entry: { name: string }) => entry.name === 'healthy'); - expect(healthyEntry.status).toBe('ok'); - const authEntry = payload.servers.find((entry: { name: string }) => entry.name === 'auth-server'); - expect(authEntry.status).toBe('auth'); - expect(authEntry.issue.kind).toBe('auth'); - - logSpy.mockRestore(); + const payload = JSON.parse(logSpy.mock.calls.at(-1)?.[0] ?? '{}'); + expect(payload.mode).toBe('list'); + expect(payload.counts.auth).toBe(1); + const healthyEntry = payload.servers.find((entry: { name: string }) => entry.name === 'healthy'); + expect(healthyEntry.status).toBe('ok'); + const authEntry = payload.servers.find((entry: { name: string }) => entry.name === 'auth-server'); + expect(authEntry.status).toBe('auth'); + expect(authEntry.issue.kind).toBe('auth'); + expect(process.exitCode).toBeUndefined(); + } finally { + logSpy.mockRestore(); + process.exitCode = previousExitCode; + } }); it('sets a non-zero exit code for unhealthy multi-server checks when requested', async () => {