/** * Helpers for creating the stable MCP launcher and MCP client configuration. * * The launcher lives outside the VS Code extension install directory (under * ~/.ra3modxml) so AI client configs do not break when the extension is * updated to a new version. The extension refreshes the launcher on every * activation/update. * * Pure TypeScript: no VS Code dependency. */ import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; import { defaultAgentHome } from "./snapshot"; export interface McpConfigTarget { id: string; label: string; path: string; } /** File name of the stable launcher on the current platform. */ export function launcherFileName(): string { return process.platform === "win32" ? "ra3-mod-xml-mcp.cmd" : "ra3-mod-xml-mcp"; } /** Absolute path to the stable launcher under the agent home. */ export function launcherPath(agentHome = defaultAgentHome()): string { return join(agentHome, launcherFileName()); } /** * Path to the bundled MCP server inside an extension install/dev directory. * The packaged extension ships this file under dist/agent/mcpServer.js. */ export function bundledMcpServerPath(extensionRoot: string): string { return join(extensionRoot, "dist", "agent", "mcpServer.js"); } /** * Creates the stable launcher script. It points to the current extension's * bundled MCP server and passes the project directory. */ export async function writeLauncher( extensionRoot: string, projectDir: string, agentHome = defaultAgentHome(), ): Promise { const server = bundledMcpServerPath(extensionRoot); const launcher = launcherPath(agentHome); await mkdir(dirname(launcher), { recursive: true }); if (process.platform === "win32") { const content = [ "@echo off", `node "${server}" --project "${projectDir}"`, "", ].join("\r\n"); await writeFile(launcher, content, "utf8"); } else { const content = [ "#!/usr/bin/env sh", `exec node "${server}" --project "${projectDir}"`, "", ].join("\n"); await writeFile(launcher, content, "utf8"); await chmod(launcher, 0o755); } return launcher; } /** MCP client config entry for one project. */ export function mcpServerConfig( launcher: string, projectDir: string, ): Record { return { mcpServers: { "ra3-mod-xml": { command: launcher, args: ["--project", projectDir], }, }, }; } /** Human-readable JSON config block users can paste into AI clients. */ export function mcpConfigJson( launcher: string, projectDir: string, ): string { return JSON.stringify(mcpServerConfig(launcher, projectDir), null, 2); } /** Claude Desktop config path (Windows/macOS/Linux common locations). */ export function claudeDesktopConfigPath(): string { if (process.env.APPDATA) return join(process.env.APPDATA, "Claude", "claude_desktop_config.json"); return join(homedir(), ".config", "Claude", "claude_desktop_config.json"); } /** Cursor's global MCP config path. */ export function cursorGlobalConfigPath(): string { return join(homedir(), ".cursor", "mcp.json"); } /** Cursor's project-scoped MCP config path. */ export function cursorProjectConfigPath(projectDir: string): string { return join(projectDir, ".cursor", "mcp.json"); } /** Common local MCP config files this extension can offer to update. */ export function commonMcpConfigTargets(projectDir: string): McpConfigTarget[] { return [ { id: "claude-desktop", label: "Claude Desktop", path: claudeDesktopConfigPath(), }, { id: "cursor-global", label: "Cursor (global)", path: cursorGlobalConfigPath(), }, { id: "cursor-project", label: "Cursor (current project)", path: cursorProjectConfigPath(projectDir), }, ]; } /** * Adds the RA3 Mod XML MCP server entry to a JSON config file, preserving any * existing keys and mcpServers. Creates the file when it does not exist. */ export async function addMcpServerToConfigFile( filePath: string, launcher: string, projectDir: string, ): Promise { let config: Record = {}; try { config = JSON.parse(await readFile(filePath, "utf8")) as Record; } catch { // File absent or malformed: start fresh. } const servers = (config.mcpServers as Record | undefined) ?? {}; servers["ra3-mod-xml"] = { command: launcher, args: ["--project", projectDir], }; config.mcpServers = servers; await mkdir(dirname(filePath), { recursive: true }); await writeFile(filePath, `${JSON.stringify(config, null, 2)}\n`, "utf8"); } /** Default path used for the agent home. */ export function defaultAgentHomeForSetup(): string { return join(homedir(), ".ra3modxml"); }