import { execFile } from 'node:child_process'; import fs from 'node:fs/promises'; import { createRequire } from 'node:module'; import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; const CLI_ENTRY = fileURLToPath(new URL('../dist/cli.js', import.meta.url)); const testRequire = createRequire(import.meta.url); const MCP_SERVER_MODULE = pathToFileURL(testRequire.resolve('@modelcontextprotocol/sdk/server/mcp.js')).href; const STDIO_SERVER_MODULE = pathToFileURL(testRequire.resolve('@modelcontextprotocol/sdk/server/stdio.js')).href; const ZOD_MODULE = pathToFileURL(path.join(process.cwd(), 'node_modules', 'zod', 'index.js')).href; async function ensureDistBuilt(): Promise { try { await fs.access(CLI_ENTRY); } catch { throw new Error('dist/cli.js is missing; run `pnpm build` before invoking this integration test directly.'); } } function runCli(args: string[], configPath: string): Promise<{ stdout: string; stderr: string; code: number }> { return new Promise((resolve) => { execFile( process.execPath, [CLI_ENTRY, '--config', configPath, ...args], { cwd: process.cwd(), env: process.env, maxBuffer: 1024 * 1024, }, (error, stdout, stderr) => { const code = typeof error?.code === 'number' ? error.code : 0; resolve({ stdout, stderr, code }); } ); }); } describe('mcporter forced exit behavior', () => { let tempDir: string; let configPath: string; beforeAll(async () => { await ensureDistBuilt(); tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mcporter-force-exit-')); const serverScriptPath = path.join(tempDir, 'force-exit-server.mjs'); configPath = path.join(tempDir, 'config.json'); const longDescription = 'Large schema tool description. '.repeat(30); await fs.writeFile( serverScriptPath, `import { McpServer } from ${JSON.stringify(MCP_SERVER_MODULE)}; import { StdioServerTransport } from ${JSON.stringify(STDIO_SERVER_MODULE)}; import { z } from ${JSON.stringify(ZOD_MODULE)}; const server = new McpServer({ name: 'force-exit', version: '1.0.0' }); server.registerTool( 'fail', { title: 'Fail', description: 'Return an MCP tool error result', inputSchema: {}, }, async () => ({ content: [{ type: 'text', text: 'expected failure' }], isError: true, }) ); for (let index = 0; index < 64; index += 1) { server.registerTool( \`tool_\${index}\`, { title: \`Tool \${index}\`, description: ${JSON.stringify(longDescription)} + index, inputSchema: { alpha: z.string().describe(${JSON.stringify(longDescription)}), beta: z.string().optional().describe(${JSON.stringify(longDescription)}), }, outputSchema: { ok: z.boolean(), }, }, async () => ({ content: [{ type: 'text', text: 'ok' }], structuredContent: { ok: true }, }) ); } const transport = new StdioServerTransport(); await server.connect(transport); `, 'utf8' ); await fs.writeFile( configPath, JSON.stringify( { mcpServers: { 'force-exit': { command: process.execPath, args: [serverScriptPath], }, }, }, null, 2 ), 'utf8' ); }); afterAll(async () => { await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {}); }); it('preserves non-zero exit codes for MCP isError tool results', async () => { const result = await runCli(['call', 'force-exit.fail'], configPath); expect(result.code).toBe(1); expect(result.stdout).toContain('expected failure'); expect(result.stderr).toBe(''); }, 20000); it('does not truncate large JSON output when force exit is enabled', async () => { const result = await runCli(['list', 'force-exit', '--schema', '--output', 'json'], configPath); expect(result.code).toBe(0); expect(result.stderr).toBe(''); expect(Buffer.byteLength(result.stdout)).toBeGreaterThan(8192); const payload = JSON.parse(result.stdout) as { tools: Array<{ name: string }> }; expect(payload.tools).toHaveLength(65); expect(payload.tools.at(-1)?.name).toBe('tool_63'); }, 20000); });