diff --git a/src/bin/__tests__/command.line.ts b/src/bin/__tests__/command.line.ts index 2dea8289..17b21803 100644 --- a/src/bin/__tests__/command.line.ts +++ b/src/bin/__tests__/command.line.ts @@ -112,3 +112,58 @@ test('handles empty JAVA_OPTS', async () => { } } }); + +test('runs mcp server directly with inherited stdio', async () => { + mockSpawn.mockReturnValue(javaProcessMock); + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + + const originalExecutor = process.env.SPECMATIC_EXECUTOR; + delete process.env.SPECMATIC_EXECUTOR; + + const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined); + + try { + callSpecmaticCli(['mcp', 'server']); + const specmaticJarPath = path.resolve(__dirname, '..', '..', '..', specmaticCoreJarName); + + expect(mockSpawn.mock.calls[0][0]).toBe('java'); + expect(mockSpawn.mock.calls[0][1]).toEqual(['-jar', path.resolve(specmaticJarPath), 'mcp', 'server']); + expect(mockSpawn.mock.calls[0][2]).toMatchObject({ + stdio: 'inherit', + shell: false, + env: expect.objectContaining({ + SPECMATIC_EXECUTOR: 'npm', + }), + }); + expect(consoleLogSpy).not.toHaveBeenCalled(); + } finally { + consoleLogSpy.mockRestore(); + if (originalExecutor !== undefined) { + process.env.SPECMATIC_EXECUTOR = originalExecutor; + } else { + delete process.env.SPECMATIC_EXECUTOR; + } + } +}); + +test('runs mcp server directly when a root flag appears before it', async () => { + mockSpawn.mockReturnValue(javaProcessMock); + jest.spyOn(fs, 'existsSync').mockReturnValue(true); + + const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined); + + try { + callSpecmaticCli(['--verbose', 'mcp', 'server']); + const specmaticJarPath = path.resolve(__dirname, '..', '..', '..', specmaticCoreJarName); + + expect(mockSpawn.mock.calls[0][0]).toBe('java'); + expect(mockSpawn.mock.calls[0][1]).toEqual(['-jar', path.resolve(specmaticJarPath), '--verbose', 'mcp', 'server']); + expect(mockSpawn.mock.calls[0][2]).toMatchObject({ + stdio: 'inherit', + shell: false, + }); + expect(consoleLogSpy).not.toHaveBeenCalled(); + } finally { + consoleLogSpy.mockRestore(); + } +}); diff --git a/src/bin/command.line.ts b/src/bin/command.line.ts index d72ca78c..d09fde43 100644 --- a/src/bin/command.line.ts +++ b/src/bin/command.line.ts @@ -1,6 +1,37 @@ +import path from 'path'; +import { spawn } from 'child_process'; import { parse } from 'shell-quote'; import logger from '../common/logger' import { callCore} from '../common/runner' +import { specmaticCoreJarName } from '../config'; + +const getCommandPath = (args: string[]) => args.filter(arg => !arg.startsWith('-')); + +const isMcpServerCommand = (args: string[]) => { + const commandPath = getCommandPath(args); + return commandPath.length >= 2 && commandPath[0] === 'mcp' && commandPath[1] === 'server'; +}; + +const callMcpServerDirectly = (args: string[], jvmArgs: string[]) => { + const rootPath = path.resolve(__dirname, '..', '..'); + const specmaticJarPath = path.resolve(rootPath, specmaticCoreJarName); + const argsList: string[] = [...jvmArgs, '-jar', specmaticJarPath, ...args]; + const envVars: NodeJS.ProcessEnv = { ...process.env }; + + if (!envVars['SPECMATIC_EXECUTOR']) { + envVars['SPECMATIC_EXECUTOR'] = 'npm'; + } + + const javaProcess = spawn('java', argsList, { stdio: 'inherit', shell: false, env: envVars }); + + javaProcess.on('error', () => { + process.exitCode = 1; + }); + + javaProcess.on('close', (code: number | null) => { + process.exitCode = code ?? 1; + }); +}; const callSpecmaticCli = (argsv?: string[]) => { const args = argsv || process.argv.slice(2); @@ -17,6 +48,11 @@ const callSpecmaticCli = (argsv?: string[]) => { } } + if (isMcpServerCommand(args)) { + callMcpServerDirectly(args, jvmArgs); + return; + } + callCore( args, (err?: any) => {