Skip to content
Merged
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
26 changes: 26 additions & 0 deletions packages/email-mcp/src/cli.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
runCli,
runCliDirect,
parseCliArgs,
getNemoClawEgressDomains,
getAgentEmailHome,
Expand Down Expand Up @@ -294,6 +295,31 @@ describe('cli/Serve Subcommand', () => {
});
});

describe('cli/Direct entrypoint lifecycle', () => {
let originalExitCode: number | undefined;

beforeEach(() => {
originalExitCode = process.exitCode;
process.exitCode = undefined;
});

afterEach(() => {
process.exitCode = originalExitCode;
});

it('Scenario: Direct serve entrypoint does not force process exit after transport startup', async () => {
const exitSpy = vi.spyOn(process, 'exit').mockImplementation((() => undefined) as never);

await runCliDirect(['serve']);

expect(process.exitCode).toBe(0);
expect(exitSpy).not.toHaveBeenCalled();
expect(console.error).toHaveBeenCalledWith(
expect.stringContaining('MCP server started'),
);
});
});

describe('cli/Watch Subcommand', () => {
it('Scenario: Watch with wake URL', () => {
// WHEN email-agent-mcp watch --wake-url http://localhost:18789/hooks/wake is run
Expand Down
28 changes: 20 additions & 8 deletions packages/email-mcp/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1065,8 +1065,8 @@ async function runToken(opts: CliOptions): Promise<number> {
await auth.reconnect();
const token = await auth.getAccessToken();

// Await stdout flush — the CLI wrapper calls process.exit() after runCli()
// resolves, which can truncate piped output if the write hasn't drained.
// Await stdout flush so short-lived invocations can exit naturally only
// after the token has fully reached stdout.
await new Promise<void>((resolve, reject) => {
process.stdout.write(token, (err) => err ? reject(err) : resolve());
});
Expand Down Expand Up @@ -1118,13 +1118,25 @@ export function getNemoClawEgressDomains(): string[] {
return [...NEMOCLAW_EGRESS_DOMAINS];
}

/**
* Direct entrypoint adapter used by `node dist/cli.js ...`.
*
* Important: successful runs must not force `process.exit()`. `serve` resolves
* once stdio transport is connected, but the MCP process must stay alive for
* the client handshake and subsequent tool calls.
*/
export async function runCliDirect(args: string[]): Promise<void> {
try {
const code = await runCli(args);
process.exitCode = code;
} catch (err) {
console.error('Fatal:', err);
process.exit(1);
}
}

// Auto-execute when run directly (not imported as a module in tests)
const isDirectRun = process.argv[1]?.endsWith('cli.ts') || process.argv[1]?.endsWith('cli.js');
if (isDirectRun) {
runCli(process.argv.slice(2)).then(code => {
process.exit(code);
}).catch(err => {
console.error('Fatal:', err);
process.exit(1);
});
void runCliDirect(process.argv.slice(2));
}
Loading