import { Command } from '@commander-js/extra-typings'; import { describe, expect, it, vi } from 'vitest'; import type { KtxCliCommandContext } from '../../src/cli-program.js'; import { registerMcpCommands } from '../../src/commands/mcp-commands.js'; function makeContext(overrides: Partial = {}): KtxCliCommandContext { let exitCode = 0; return { io: { stdout: { write: vi.fn() }, stderr: { write: vi.fn() }, }, deps: {}, packageInfo: { name: '@kaelio/ktx', version: '0.0.0-test' }, setExitCode: (code) => { exitCode = code; }, runInit: vi.fn(), writeDebug: vi.fn(), ...overrides, get exitCode() { return exitCode; }, } as KtxCliCommandContext; } describe('registerMcpCommands', () => { it('registers the public mcp lifecycle commands', () => { const program = new Command().exitOverride(); registerMcpCommands(program, makeContext()); const mcp = program.commands.find((command) => command.name() === 'mcp'); expect(mcp?.commands.map((command) => command.name()).sort()).toEqual([ 'logs', 'serve-internal', 'start', 'status', 'stdio', 'stop', ]); expect( (mcp?.commands.find((command) => command.name() === 'serve-internal') as { _hidden?: boolean } | undefined) ?._hidden, ).toBe(true); }); it('rejects non-loopback start without token before spawning', async () => { const program = new Command().exitOverride(); const startDaemon = vi.fn(); const context = makeContext({ deps: { mcp: { startDaemon } } }); registerMcpCommands(program, context); await expect(program.parseAsync(['mcp', 'start', '--host', '0.0.0.0'], { from: 'user' })).rejects.toThrow( 'Binding KTX MCP to 0.0.0.0 requires --token or KTX_MCP_TOKEN', ); expect(startDaemon).not.toHaveBeenCalled(); }); it('prints "already running" when startDaemon reports already-running', async () => { const program = new Command().exitOverride().option('--project-dir '); const startDaemon = vi.fn().mockResolvedValue({ status: 'already-running', url: 'http://127.0.0.1:7878/mcp', state: { schemaVersion: 1, pid: 4242, host: '127.0.0.1', port: 7878, tokenAuth: false, projectDir: '/tmp/ktx-already', startedAt: '2026-05-14T00:00:00.000Z', logPath: '/tmp/ktx-already/.ktx/logs/mcp.log', }, }); const context = makeContext({ deps: { mcp: { startDaemon } } }); registerMcpCommands(program, context); await program.parseAsync(['--project-dir', '/tmp/ktx-already', 'mcp', 'start'], { from: 'user' }); expect(startDaemon).toHaveBeenCalledTimes(1); expect(context.io.stdout.write).toHaveBeenCalledWith( [ 'KTX MCP daemon already running: http://127.0.0.1:7878/mcp', '', 'KTX is ready for configured agents.', 'Open your agent for this KTX project and ask a data question, for example:', ' "Use KTX to show me the available tables and metrics."', '', ].join('\n'), ); }); it('prints a friendly next step after starting the daemon', async () => { const program = new Command().exitOverride().option('--project-dir '); const startDaemon = vi.fn().mockResolvedValue({ status: 'started', url: 'http://127.0.0.1:7878/mcp', state: { schemaVersion: 1, pid: 4242, host: '127.0.0.1', port: 7878, tokenAuth: false, projectDir: '/tmp/ktx-started', startedAt: '2026-05-14T00:00:00.000Z', logPath: '/tmp/ktx-started/.ktx/logs/mcp.log', }, }); const context = makeContext({ deps: { mcp: { startDaemon } } }); registerMcpCommands(program, context); await program.parseAsync(['--project-dir', '/tmp/ktx-started', 'mcp', 'start'], { from: 'user' }); expect(context.io.stdout.write).toHaveBeenCalledWith( expect.stringContaining('KTX MCP daemon started: http://127.0.0.1:7878/mcp\n\nKTX is ready for configured agents.'), ); expect(context.io.stdout.write).toHaveBeenCalledWith( expect.stringContaining('"Use KTX to show me the available tables and metrics."'), ); }); it('runs the stdio server with the resolved project directory', async () => { const program = new Command().exitOverride().option('--project-dir '); const runStdioServer = vi.fn().mockResolvedValue(undefined); const context = makeContext({ deps: { mcp: { runStdioServer } } }); registerMcpCommands(program, context); await expect(program.parseAsync(['--project-dir', '/tmp/ktx6', 'mcp', 'stdio'], { from: 'user' })).resolves.toBe( program, ); expect(runStdioServer).toHaveBeenCalledWith({ projectDir: '/tmp/ktx6', cliVersion: '0.0.0-test', io: context.io, }); }); });