Generate RESTForge Launcher Script
runtime_generate_launcherProduces server-start and server-stop scripts (.bat/.sh) for RESTForge based on OS and runtime mode. The user must run the script manually to keep the server alive.
Instructions
Generate a launcher script for the RESTForge Server in the project root. This tool does NOT run the server — it produces a .bat or .sh script that the user must execute themselves. This is intentional: a server launched by the AI session would terminate when the session ends; a script executed by the user lives in the user's terminal/session and persists independently.
Files produced (depending on os + mode) — file names are FIXED, not user-customisable:
windows + host: server-start.bat, server-stop.bat
windows + pm2: server-start.bat (calls 'pm2 start ecosystem.config.js'), server-stop.bat, ecosystem.config.js
linux + host: server-start.sh, server-stop.sh
linux + pm2: server-start.sh (calls 'pm2 start ecosystem.config.js'), server-stop.sh, ecosystem.config.js
For host mode the start script runs 'npx restforge ...' in the foreground (or 'exec' on Linux). The stop script kills the process by port via netstat/taskkill on Windows or lsof/fuser+kill on Linux — no PID file is required. For PM2 mode the ecosystem points 'script' directly at './node_modules/restforgejs/server.js' to bypass the npx shim and avoid PM2's default node interpreter mis-parsing a .cmd file.
USE WHEN:
The user asks "run the server", "jalankan server", "start RESTForge"
After confirming OS, mode, project, config, and port with the user (typically via 'runtime_detect_project', 'runtime_detect_config', and direct questions)
After 'runtime_check_launcher_exists' returned no conflicts, OR the user confirmed overwrite
DO NOT USE FOR:
Actually starting or stopping the server -> the user runs the generated script themselves
Modifying database schema or payload files -> out of scope
Running one-off commands -> out of scope; this tool is launcher-scaffolding only
Preconditions:
The cwd must exist.
'cluster' and 'workers' are mutually exclusive.
'watch' is not allowed with mode=pm2.
PRESENTATION GUIDANCE:
Match the user's language.
Never mention internal tool names. Refer to the produced files by their visible names (e.g. 'server-start.bat').
Tell the user clearly that the AI does NOT execute the script — they must run it themselves so the server keeps running after the AI session ends.
For PM2 mode: warn the user that PM2 must be installed globally first (npm install -g pm2). Do not auto-install.
After generation, summarise: location, files produced, how to start, how to stop. Do not paste the JSON envelope unless explicitly asked.
If the user asks for a different mode (Windows Service, Docker, systemd) — those are out of scope V1; suggest using host or pm2 instead.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| cwd | Yes | Absolute path of the project folder root (output goes here) | |
| os | Yes | Target OS: windows produces .bat, linux produces .sh | |
| mode | Yes | Runtime mode: host runs npx restforge directly; pm2 uses ecosystem.config.js | |
| project | Yes | Project name (filename in src/modules/ without .js). Passed as --project=<name> | |
| config | Yes | Config file name in config/ folder (e.g. db-connection.env). Passed as --config=<filename> | |
| port | Yes | Server port | |
| overwrite | No | If true, overwrite existing files. If false, skip existing. | |
| cluster | No | If true, append --cluster flag. Mutually exclusive with workers. | |
| workers | No | If set, append --workers=N flag. Mutually exclusive with cluster. | |
| watch | No | If true, append --watch flag. Not allowed with mode=pm2 (PM2 has its own watch via ecosystem). |
Implementation Reference
- Main handler: registerRuntimeGenerateLauncher registers the 'runtime_generate_launcher' tool via server.registerTool(), containing the full async handler that validates inputs, builds launcher scripts from templates, writes files to disk, and returns a structured JSON result.
export function registerRuntimeGenerateLauncher(server: McpServer): void { server.registerTool( 'runtime_generate_launcher', { title: 'Generate RESTForge Launcher Script', description: `Generate a launcher script for the RESTForge Server in the project root. This tool does NOT run the server — it produces a .bat or .sh script that the user must execute themselves. This is intentional: a server launched by the AI session would terminate when the session ends; a script executed by the user lives in the user's terminal/session and persists independently. Files produced (depending on os + mode) — file names are FIXED, not user-customisable: - windows + host: server-start.bat, server-stop.bat - windows + pm2: server-start.bat (calls 'pm2 start ecosystem.config.js'), server-stop.bat, ecosystem.config.js - linux + host: server-start.sh, server-stop.sh - linux + pm2: server-start.sh (calls 'pm2 start ecosystem.config.js'), server-stop.sh, ecosystem.config.js For host mode the start script runs 'npx restforge ...' in the foreground (or 'exec' on Linux). The stop script kills the process by port via netstat/taskkill on Windows or lsof/fuser+kill on Linux — no PID file is required. For PM2 mode the ecosystem points 'script' directly at './node_modules/restforgejs/server.js' to bypass the npx shim and avoid PM2's default node interpreter mis-parsing a .cmd file. USE WHEN: - The user asks "run the server", "jalankan server", "start RESTForge" - After confirming OS, mode, project, config, and port with the user (typically via 'runtime_detect_project', 'runtime_detect_config', and direct questions) - After 'runtime_check_launcher_exists' returned no conflicts, OR the user confirmed overwrite DO NOT USE FOR: - Actually starting or stopping the server -> the user runs the generated script themselves - Modifying database schema or payload files -> out of scope - Running one-off commands -> out of scope; this tool is launcher-scaffolding only Preconditions: - The cwd must exist. - 'cluster' and 'workers' are mutually exclusive. - 'watch' is not allowed with mode=pm2. PRESENTATION GUIDANCE: - Match the user's language. - Never mention internal tool names. Refer to the produced files by their visible names (e.g. 'server-start.bat'). - Tell the user clearly that the AI does NOT execute the script — they must run it themselves so the server keeps running after the AI session ends. - For PM2 mode: warn the user that PM2 must be installed globally first (npm install -g pm2). Do not auto-install. - After generation, summarise: location, files produced, how to start, how to stop. Do not paste the JSON envelope unless explicitly asked. - If the user asks for a different mode (Windows Service, Docker, systemd) — those are out of scope V1; suggest using host or pm2 instead.`, inputSchema: { cwd: z .string() .min(1) .describe('Absolute path of the project folder root (output goes here)'), os: z .enum(['windows', 'linux']) .describe('Target OS: windows produces .bat, linux produces .sh'), mode: z .enum(['host', 'pm2']) .describe('Runtime mode: host runs npx restforge directly; pm2 uses ecosystem.config.js'), project: z .string() .min(1) .describe( 'Project name (filename in src/modules/ without .js). Passed as --project=<name>' ), config: z .string() .min(1) .describe( 'Config file name in config/ folder (e.g. db-connection.env). Passed as --config=<filename>' ), port: z.number().int().min(1).max(65535).describe('Server port'), overwrite: z .boolean() .default(false) .describe('If true, overwrite existing files. If false, skip existing.'), cluster: z .boolean() .optional() .describe('If true, append --cluster flag. Mutually exclusive with workers.'), workers: z .number() .int() .min(1) .max(64) .optional() .describe('If set, append --workers=N flag. Mutually exclusive with cluster.'), watch: z .boolean() .optional() .describe( 'If true, append --watch flag. Not allowed with mode=pm2 (PM2 has its own watch via ecosystem).' ), }, annotations: { title: 'Generate Launcher Script', readOnlyHint: false, idempotentHint: false, destructiveHint: true, }, }, async (input) => { const { cwd, os, mode, project, config, port, overwrite, cluster, workers, watch } = input; if (cluster && workers !== undefined) { return { content: [ { type: 'text', text: `Conflicting options: 'cluster' and 'workers' cannot both be set. For the assistant: - Ask the user to pick one. Cluster auto-detects CPU count; workers=N is explicit.`, }, ], isError: false, }; } if (mode === 'pm2' && watch) { return { content: [ { type: 'text', text: `Conflicting options: 'watch' is not allowed when mode=pm2 (PM2 manages watch internally via ecosystem.config.js). For the assistant: - Suggest setting watch only with mode=host.`, }, ], isError: false, }; } const projectCwd = resolve(cwd); try { await access(projectCwd); } catch { return { content: [ { type: 'text', text: `Precondition not met: cwd does not exist: ${projectCwd} For the assistant: - The target folder is missing. Suggest creating it first or verifying the path. - Match the user's language. Do not mention internal tool names.`, }, ], isError: false, }; } const extra = buildExtraArgs({ cluster, workers, watch }); const vars: Record<string, string> = { PROJECT: project, CONFIG: config, PORT: String(port), EXTRA_ARGS_SH: extra, }; const startFile = LAUNCHER_FILES[os].start; const stopFile = LAUNCHER_FILES[os].stop; type FileSpec = { fileName: string; content: string }; const filesToWrite: FileSpec[] = []; if (os === 'windows' && mode === 'host') { filesToWrite.push( { fileName: startFile, content: applyTemplate(TEMPLATE_WINDOWS_HOST_START, vars) }, { fileName: stopFile, content: applyTemplate(TEMPLATE_WINDOWS_HOST_STOP, vars) } ); } else if (os === 'windows' && mode === 'pm2') { filesToWrite.push( { fileName: startFile, content: applyTemplate(TEMPLATE_WINDOWS_PM2_START, vars) }, { fileName: stopFile, content: applyTemplate(TEMPLATE_WINDOWS_PM2_STOP, vars) }, { fileName: ECOSYSTEM_FILE, content: applyTemplate(TEMPLATE_PM2_ECOSYSTEM, vars) } ); } else if (os === 'linux' && mode === 'host') { filesToWrite.push( { fileName: startFile, content: applyTemplate(TEMPLATE_LINUX_HOST_START, vars) }, { fileName: stopFile, content: applyTemplate(TEMPLATE_LINUX_HOST_STOP, vars) } ); } else if (os === 'linux' && mode === 'pm2') { filesToWrite.push( { fileName: startFile, content: applyTemplate(TEMPLATE_LINUX_PM2_START, vars) }, { fileName: stopFile, content: applyTemplate(TEMPLATE_LINUX_PM2_STOP, vars) }, { fileName: ECOSYSTEM_FILE, content: applyTemplate(TEMPLATE_PM2_ECOSYSTEM, vars) } ); } const generated: { path: string; size_bytes: number; overwritten: boolean }[] = []; const skipped: { path: string; reason: string }[] = []; for (const f of filesToWrite) { const fullPath = join(projectCwd, f.fileName); let existed = false; try { await stat(fullPath); existed = true; } catch { existed = false; } if (existed && !overwrite) { skipped.push({ path: fullPath, reason: 'already exists, overwrite=false' }); continue; } try { await writeFile(fullPath, f.content, 'utf8'); generated.push({ path: fullPath, size_bytes: Buffer.byteLength(f.content, 'utf8'), overwritten: existed, }); } catch (err) { const msg = err instanceof Error ? err.message : String(err); return { content: [ { type: 'text', text: `Failed to write ${fullPath}: ${msg}` }, ], isError: true, }; } } const pm2Warning = mode === 'pm2' ? 'PM2 must be installed globally on the user machine (npm install -g pm2). This tool does not auto-install. If PM2 is missing, the launcher will fail at runtime.' : null; const stopMethod = mode === 'pm2' ? `pm2 stop ecosystem.config.js (invoked by ${stopFile})` : `kill the process listening on port ${port} (invoked by ${stopFile})`; const envelope = { cwd: projectCwd, os, mode, project, config, port, generated_files: generated, skipped_files: skipped, stop_method: stopMethod, pm2_warning: pm2Warning, }; const prettyJson = JSON.stringify(envelope, null, 2); const summary = generated.length === filesToWrite.length ? `Generated ${generated.length} launcher file${generated.length === 1 ? '' : 's'}.` : `Generated ${generated.length}; skipped ${skipped.length} (already exist, overwrite=false).`; const startCommand = os === 'windows' ? `.\\${startFile}` : `./${startFile}`; const stopCommand = os === 'windows' ? stopFile : `./${stopFile}`; return { content: [ { type: 'text', text: `${summary} Project path: ${projectCwd} OS: ${os} Mode: ${mode} Project: ${project} Config: ${config} Port: ${port} Generated: ${generated.length} Skipped: ${skipped.length} PM2 warning: ${pm2Warning ?? 'n/a'} --- Generation Result (JSON) --- ${prettyJson} --- end Generation Result (JSON) --- For the assistant: - ${ generated.length > 0 ? `Tell the user the launcher was generated at the project root. To start: ${startCommand}. To stop: ${stopCommand}.` : `Nothing was generated because all files already exist and overwrite=false. Ask the user whether to overwrite (call again with overwrite=true) or pick a different base name.` } - ${ pm2Warning ? `Warn the user that PM2 must be installed globally on their machine before running the launcher.` : `` } - The server will run in the user's terminal — when launched manually, the process belongs to the user's session, NOT to this AI session. - For host mode the stop script kills the process listening on port ${port} (cross-platform port-based lookup). For PM2 mode the stop script delegates to pm2. - Match the user's language. Do not mention internal tool names.`, }, ], isError: false, }; } ); } - src/tools/runtime/index.ts:6-16 (registration)Registration: import and call of registerRuntimeGenerateLauncher inside registerRuntimeTools, which is called from src/server.ts line 280.
import { registerRuntimeGenerateLauncher } from './generate-launcher.js'; import { registerRuntimeCheckStatus } from './check-status.js'; export function registerRuntimeTools(server: McpServer): void { registerRuntimeDetectProject(server); registerRuntimeDetectConfig(server); registerRuntimeValidatePreflight(server); registerRuntimeCheckLauncherExists(server); registerRuntimeGenerateLauncher(server); registerRuntimeCheckStatus(server); } - src/tools/runtime/index.ts:9-16 (registration)Registration: registerRuntimeTools function that aggregates all runtime tool registrations, including registerRuntimeGenerateLauncher at line 14.
export function registerRuntimeTools(server: McpServer): void { registerRuntimeDetectProject(server); registerRuntimeDetectConfig(server); registerRuntimeValidatePreflight(server); registerRuntimeCheckLauncherExists(server); registerRuntimeGenerateLauncher(server); registerRuntimeCheckStatus(server); } - Helper functions: buildExtraArgs (constructs CLI flags like --cluster, --workers=N, --watch) and applyTemplate (replaces {{VAR}} placeholders in template strings with provided variables).
interface ExtraArgsOptions { cluster?: boolean; workers?: number; watch?: boolean; } function buildExtraArgs(opts: ExtraArgsOptions): string { const parts: string[] = []; if (opts.cluster) { parts.push('--cluster'); } else if (opts.workers !== undefined) { parts.push(`--workers=${opts.workers}`); } if (opts.watch) { parts.push('--watch'); } return parts.length > 0 ? ' ' + parts.join(' ') : ''; } function applyTemplate(template: string, vars: Record<string, string>): string { return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? ''); } - Schema: Zod-based input schema for the tool defining parameters: cwd, os, mode, project, config, port, overwrite, cluster, workers, watch.
cwd: z .string() .min(1) .describe('Absolute path of the project folder root (output goes here)'), os: z .enum(['windows', 'linux']) .describe('Target OS: windows produces .bat, linux produces .sh'), mode: z .enum(['host', 'pm2']) .describe('Runtime mode: host runs npx restforge directly; pm2 uses ecosystem.config.js'), project: z .string() .min(1) .describe( 'Project name (filename in src/modules/ without .js). Passed as --project=<name>' ), config: z .string() .min(1) .describe( 'Config file name in config/ folder (e.g. db-connection.env). Passed as --config=<filename>' ), port: z.number().int().min(1).max(65535).describe('Server port'), overwrite: z .boolean() .default(false) .describe('If true, overwrite existing files. If false, skip existing.'), cluster: z .boolean() .optional() .describe('If true, append --cluster flag. Mutually exclusive with workers.'), workers: z .number() .int() .min(1) .max(64) .optional() .describe('If set, append --workers=N flag. Mutually exclusive with cluster.'), watch: z .boolean() .optional() .describe( 'If true, append --watch flag. Not allowed with mode=pm2 (PM2 has its own watch via ecosystem).' ), },