Files
Ra3ModXmlExt/src/agent/setup.ts
T
2026-09-10 17:03:10 +02:00

161 lines
4.8 KiB
TypeScript

/**
* 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<string> {
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<string, unknown> {
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<void> {
let config: Record<string, unknown> = {};
try {
config = JSON.parse(await readFile(filePath, "utf8")) as Record<string, unknown>;
} catch {
// File absent or malformed: start fresh.
}
const servers = (config.mcpServers as Record<string, unknown> | 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");
}