Fix daemon implicit config handling and docs
This commit is contained in:
parent
f02a59bc89
commit
260fc2993b
@ -50,6 +50,7 @@ Usage: In repo copies, the shared content lives inside `<shared>…</shared>` an
|
||||
- Whenever doing a large refactor, track work in `docs/refactor/<title><date>.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.
|
||||
|
||||
@ -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 <command> --help” hint.
|
||||
|
||||
@ -90,6 +90,7 @@ export async function runCli(argv: string[]): Promise<void> {
|
||||
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<void> {
|
||||
.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());
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<string>;
|
||||
@ -32,7 +33,7 @@ interface ServerActivity {
|
||||
|
||||
export async function runDaemonHost(options: DaemonHostOptions): Promise<void> {
|
||||
const runtime = await createRuntime({
|
||||
configPath: options.configPath,
|
||||
configPath: options.configExplicit ? options.configPath : undefined,
|
||||
rootDir: options.rootDir,
|
||||
});
|
||||
const keepAliveDefinitions = runtime.getDefinitions().filter(isKeepAliveServer);
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user