diff --git a/AGENTS.md b/AGENTS.md
index 15c0a56..4cac6d9 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -50,6 +50,7 @@ Usage: In repo copies, the shared content lives inside `…` an
- Whenever doing a large refactor, track work in `docs/refactor/
.md`, update it as you go, and delete it when the work is finished.
- Only create new documentation when the user or local instructions explicitly request it; otherwise, edit the canonical file in place.
- When you uncover a reproducible tooling or CI issue, record the repro steps and workaround in the designated troubleshooting doc for that repo.
+- Routine test additions don’t require changelog entries; reserve changelog lines for user-visible behavior changes.
### Troubleshooting & Observability
- Design workflows so they are observable without constant babysitting: use tmux panes, CI logs, log-tail scripts, MCP/browser helpers, and similar tooling to surface progress.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30f19dc..532eeb7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@
### CLI
- Nothing yet.
+## [0.6.6] - 2025-11-28
+### CLI
+- Prevented ENOENT crashes when no config file exists anywhere by only passing an explicit `--config`/`MCPORTER_CONFIG` path to the runtime; implicit defaults now fall back cleanly across list/config/daemon flows.
+
## [0.6.5] - 2025-11-26
### CLI
- `mcporter call|auth|list help/--help` now print the command-specific usage text instead of attempting to run a server, matching the footer’s “mcporter --help” hint.
diff --git a/src/cli.ts b/src/cli.ts
index 40a13f0..19e49ed 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -90,6 +90,7 @@ export async function runCli(argv: string[]): Promise {
if (command === 'daemon') {
await handleDaemonCli(args, {
configPath: configPathResolved,
+ configExplicit: configResolution.explicit,
rootDir: rootOverride,
});
return;
@@ -124,7 +125,13 @@ export async function runCli(argv: string[]): Promise {
.map((entry) => entry.name)
);
const daemonClient =
- keepAliveServers.size > 0 ? new DaemonClient({ configPath: configResolution.path, rootDir: rootOverride }) : null;
+ keepAliveServers.size > 0
+ ? new DaemonClient({
+ configPath: configResolution.path,
+ configExplicit: configResolution.explicit,
+ rootDir: rootOverride,
+ })
+ : null;
const runtime = createKeepAliveRuntime(baseRuntime, { daemonClient, keepAliveServers });
const inference = inferCommandRouting(command, args, runtime.getDefinitions());
diff --git a/src/cli/daemon-command.ts b/src/cli/daemon-command.ts
index 2e24f9f..af77fed 100644
--- a/src/cli/daemon-command.ts
+++ b/src/cli/daemon-command.ts
@@ -10,6 +10,9 @@ import { createRuntime } from '../runtime.js';
interface DaemonCliOptions {
readonly configPath: string;
+ // Whether the config path was explicitly provided (flag/env). If false, runtime should
+ // treat config as implicit and allow missing files without throwing ENOENT.
+ readonly configExplicit?: boolean;
readonly rootDir?: string;
}
@@ -29,6 +32,7 @@ export async function handleDaemonCli(args: string[], options: DaemonCliOptions)
const client = new DaemonClient({
configPath: options.configPath,
+ configExplicit: options.configExplicit,
rootDir: options.rootDir,
});
@@ -80,7 +84,7 @@ async function handleDaemonStart(args: string[], options: DaemonCliOptions, clie
const logging = await resolveDaemonLoggingOptions(args, paths.key);
const runtime = await createRuntime({
- configPath: options.configPath,
+ configPath: options.configExplicit ? options.configPath : undefined,
rootDir: options.rootDir,
});
const keepAlive = runtime.getDefinitions().filter(isKeepAliveServer);
@@ -95,6 +99,7 @@ async function handleDaemonStart(args: string[], options: DaemonCliOptions, clie
socketPath,
metadataPath,
configPath: options.configPath,
+ configExplicit: options.configExplicit,
rootDir: options.rootDir,
logPath: logging.enabled ? logging.logPath : undefined,
logServers: logging.serverFilter,
@@ -119,6 +124,7 @@ async function handleDaemonStart(args: string[], options: DaemonCliOptions, clie
launchDaemonDetached({
configPath: options.configPath,
+ configExplicit: options.configExplicit,
rootDir: options.rootDir,
metadataPath,
socketPath,
diff --git a/src/daemon/client.ts b/src/daemon/client.ts
index 7bbc6db..23f2a94 100644
--- a/src/daemon/client.ts
+++ b/src/daemon/client.ts
@@ -16,6 +16,7 @@ import type {
export interface DaemonClientOptions {
readonly configPath: string;
+ readonly configExplicit?: boolean;
readonly rootDir?: string;
}
@@ -121,6 +122,7 @@ export class DaemonClient {
.then(() => {
launchDaemonDetached({
configPath: this.options.configPath,
+ configExplicit: this.options.configExplicit,
rootDir: this.options.rootDir,
metadataPath: this.metadataPath,
socketPath: this.socketPath,
diff --git a/src/daemon/host.ts b/src/daemon/host.ts
index 1091af0..010b215 100644
--- a/src/daemon/host.ts
+++ b/src/daemon/host.ts
@@ -19,6 +19,7 @@ interface DaemonHostOptions {
readonly socketPath: string;
readonly metadataPath: string;
readonly configPath: string;
+ readonly configExplicit?: boolean;
readonly rootDir?: string;
readonly logPath?: string;
readonly logServers?: Set;
@@ -32,7 +33,7 @@ interface ServerActivity {
export async function runDaemonHost(options: DaemonHostOptions): Promise {
const runtime = await createRuntime({
- configPath: options.configPath,
+ configPath: options.configExplicit ? options.configPath : undefined,
rootDir: options.rootDir,
});
const keepAliveDefinitions = runtime.getDefinitions().filter(isKeepAliveServer);
diff --git a/src/daemon/launch.ts b/src/daemon/launch.ts
index 0b4a351..504f54f 100644
--- a/src/daemon/launch.ts
+++ b/src/daemon/launch.ts
@@ -3,6 +3,7 @@ import path from 'node:path';
export interface DaemonLaunchOptions {
readonly configPath: string;
+ readonly configExplicit?: boolean;
readonly rootDir?: string;
readonly socketPath: string;
readonly metadataPath: string;
@@ -11,11 +12,11 @@ export interface DaemonLaunchOptions {
export function launchDaemonDetached(options: DaemonLaunchOptions): void {
const cliEntry = resolveCliEntry();
+ const configArgs = options.configExplicit ? ['--config', options.configPath] : [];
const args = [
...process.execArgv,
cliEntry,
- '--config',
- options.configPath,
+ ...configArgs,
...(options.rootDir ? ['--root', options.rootDir] : []),
'daemon',
'start',
diff --git a/tests/cli-config-fallback.test.ts b/tests/cli-config-fallback.test.ts
index 6cac7a6..ce6cf6b 100644
--- a/tests/cli-config-fallback.test.ts
+++ b/tests/cli-config-fallback.test.ts
@@ -238,4 +238,22 @@ describe('mcporter CLI with completely empty environment (ENOENT regression)', (
logSpy.mockRestore();
warnSpy.mockRestore();
});
+
+ it('daemon start no-ops gracefully with no config files anywhere', async () => {
+ const { runCli } = await cliModulePromise;
+ const logs: string[] = [];
+ const logSpy = vi.spyOn(console, 'log').mockImplementation((value?: unknown) => {
+ if (typeof value === 'string') {
+ logs.push(value);
+ }
+ });
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+
+ await expect(runCli(['daemon', 'start'])).resolves.not.toThrow();
+ expect(warnSpy).not.toHaveBeenCalled();
+ expect(logs.join('\n')).toContain('No MCP servers are configured for keep-alive; daemon not started.');
+
+ logSpy.mockRestore();
+ warnSpy.mockRestore();
+ });
});
diff --git a/tests/daemon-cli-command.test.ts b/tests/daemon-cli-command.test.ts
index 39cfca9..dae0040 100644
--- a/tests/daemon-cli-command.test.ts
+++ b/tests/daemon-cli-command.test.ts
@@ -83,15 +83,30 @@ describe('daemon CLI restart', () => {
.mockResolvedValueOnce(null) // waitFor: daemon not ready yet
.mockResolvedValueOnce({ pid: 420, socketPath: '/tmp/socket', servers: [], logPath: '/tmp/mock-daemon.log' });
- await handleDaemonCli(['restart', '--log'], { configPath: '/tmp/config.json' });
+ await handleDaemonCli(['restart', '--log'], { configPath: '/tmp/config.json', configExplicit: true });
expect(stopMock).toHaveBeenCalledTimes(1);
expect(launchDaemonDetachedMock).toHaveBeenCalledWith({
configPath: '/tmp/config.json',
+ configExplicit: true,
rootDir: undefined,
metadataPath: '/tmp/meta',
socketPath: '/tmp/socket',
extraArgs: ['--log-file', '/tmp/mock-daemon.log'],
});
});
+
+ it('uses implicit config when no explicit path is provided, avoiding ENOENT', async () => {
+ statusMock
+ .mockResolvedValueOnce(null) // restart wait sees daemon already stopped
+ .mockResolvedValueOnce(null) // handleDaemonStart: no existing daemon
+ .mockResolvedValueOnce({ pid: 321, socketPath: '/tmp/socket', servers: [], logPath: undefined }); // waitFor ready
+
+ await handleDaemonCli(['restart'], { configPath: '/tmp/config.json', configExplicit: false });
+
+ expect(createRuntimeMock).toHaveBeenCalledWith({
+ configPath: undefined,
+ rootDir: undefined,
+ });
+ });
});