/** * Agent Skill generator/installer for the RA3 Mod XML MCP tools. * * The Skill focuses on the functionality itself: when to use it and how to * use the RA3 Mod XML MCP query tools. It intentionally avoids referencing * any project-specific docs (e.g. docs/codebase-navigation-guide.md) so the * agent is not distracted by unrelated workspace guidance. * * Pure TypeScript: no VS Code dependency. */ import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import { dirname, join } from "node:path"; import { defaultAgentHome } from "./snapshot"; export const SKILL_NAME = "ra3-mod-xml"; export const SKILL_MARKER_FILE = ".ra3modxml-skill.json"; const SKILL_MD = `--- name: ra3-mod-xml description: Use the RA3 Mod XML semantic index to find asset definitions, references, outgoing references, active files, defines, and include sources in SAGE / BinaryAssetBuilder XML projects such as Command & Conquer: Red Alert 3 mods. Use this when you need exact facts about mod assets instead of guessing or full-text searching the XML tree. --- # RA3 Mod XML Index This skill provides access to the semantic index built by the RA3 Mod XML VS Code extension. ## When this skill applies Use these tools only for SAGE / BinaryAssetBuilder mod XML projects — the kind used by Command & Conquer: Red Alert 3 mods. Positive signals (any single one is enough to try the tools): - \`Data/Mod.xml\` exists. - \`Data/additionalmaps/mapmetadata_*.xml\` exists. - A \`*.babproj\` file exists. - XML whose root element is \`\`. - XML that uses \`\` or declares \`xmlns="uri:ea.com:eala:asset"\`. Do **not** use these tools for unrelated repositories. In particular, do not use them merely because a project contains XML, a build script, copied \`.xsd\` files, or a folder named \`Data\`. A file named \`CnC3Types.xsd\` refers to C&C3 (Tiberium Wars / Kane's Wrath) and is **not** by itself evidence of a Red Alert 3 mod. When the repository is not a SAGE / RA3 mod project, ignore this skill entirely. If you are unsure whether the current project is in scope, call \`get_status\` first: it is cheap and reports the \`projectDir\` the index belongs to. Stop using the index tools when: - the state is \`no_index\`, or - the reported \`projectDir\` does not match the workspace root you are working in. In those cases read the files directly instead. Never present index results from one project as if they belonged to another. ## When to use Use this skill when you need any of the following: - Find where an asset id is defined (GameObject, WeaponTemplate, Texture, etc.). - Find which files/positions reference an asset id. - Find which assets an asset references, and through which element/attribute. - List assets of a particular type. - Check whether a file is actually part of the active include graph (i.e. not a dead file). - Resolve an Include source string. - Look up a $DEFINE constant. - Get the current index status and statistics. Do not use full-text search over the XML tree when one of the MCP query tools can answer the question directly. ## Reaching the index Work down this list and stop at the first step that works. 1. **The query tools are already in your tool list** (names like \`find_asset\`, \`get_status\`). Use them directly. You do not need to configure anything. 2. **The tools are not available, but you can run commands.** The index is reachable without any MCP setup, because the MCP server speaks JSON-RPC over stdio. Read \`~/.ra3modxml/index.json\` first: it is a small, stable discovery manifest listing the live instances and their project roots. - If it does not exist, the extension has never been enabled for this project. Tell the user to open the project in VS Code and run "RA3 Mod XML: Enable AI Agent access…". Do not guess or search further. - If it exists, use the launcher at \`~/.ra3modxml/ra3-mod-xml-mcp.cmd\` (Windows) or \`~/.ra3modxml/ra3-mod-xml-mcp\` (elsewhere), or call the bundled server directly through that instance's runtime. Feed it one JSON-RPC request per line on stdin, for example: \`\`\` {"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"find_asset","arguments":{"id":"AthenaCannon","type":"GameObject"}}} \`\`\` A CLI with the same capabilities is bundled alongside the MCP server (\`cli.js\` next to \`mcpServer.js\` in the extension's \`dist/agent/\`). Run it with the same runtime as the launcher; \`cli.js help\` lists the commands. It answers from the live index when VS Code is running and falls back to the last exported snapshot otherwise. Commands that need element context (\`outgoing\`, \`projects\`) require the live index and will say so explicitly instead of returning an empty result. 3. **You can write configuration, if the user agrees.** You may add the MCP server to the your own harness' configuration. Check what your own client supports (e.g. if a restart is required) before promising otherwise. If none of the steps work, say the index is unavailable and read the XML files directly. ## How to use 1. Call \`get_status\` first when you are unsure whether an index is available, current, or belongs to the project you are working in. 2. Use narrow queries: - \`find_asset(id, type?)\` for definition locations. - \`find_references(id, type?)\` for incoming semantic references. - \`get_asset_references(id, type?, depth?, targetTypes?)\` for outgoing references (what this asset uses, and where that link is written). - \`list_assets_by_type(type, prefix?, limit?)\` for browsing assets. - \`is_file_active(path)\` to determine whether a file is included in an indexed stream. - \`find_define(name)\` for $DEFINE constants. - \`resolve_include(source)\` for Include source candidates. 3. Asset ids are case-insensitive. 4. If an id exists for multiple asset types, pass the type filter to avoid mixing definitions. 5. If the returned index state is \`stale\`, \`ready_xml\`, or \`building\`, treat results as provisional. 6. Never read or dump the whole index snapshot; query narrowly. ## Following references with get_asset_references Use \`get_asset_references\` to answer "which weapon / model / upgrade / die-object does this asset actually use" without reading source first. It returns **edges**, not a flat list, so provenance is preserved: - \`from\` is the asset you asked about. - \`to\` is the resolved definition (file + line), or null when unresolved. - \`via.element\` / \`via.parent\` / \`via.attribute\` say exactly which XML element and attribute created the link. - \`source\` is the file/line where that link is written. - \`definedIn\` is present when the link comes from an \`inheritFrom\` ancestor's XML rather than from the asset's own file. Inheritance is walked at the same depth, so a base asset's weapon slot configuration is reported together with the derived asset's own modules. Practical rules: - Start with the default \`depth: 1\` (the queried asset only). Raise it to 2 or 3 only for a specific node you already decided to follow. The maximum is 3. - Pass \`targetTypes\` to cut noise, e.g. \`["WeaponTemplate"]\`, or \`["GameObject"]\` for \`CreateObjectDie\`-style die-object links. - If \`truncated\` is true, read \`omittedByTargetType\` and narrow the query (smaller \`targetTypes\`, lower \`depth\`) instead of blindly retrying. - Call \`find_references\` in the opposite direction: it tells you who else would be affected by a change. - Merged/inherited *effective values* are not computed. When \`xai:joinAction\` (\`Replace\` / \`Remove\`) appears in the merge path, open the file and confirm the real result yourself. `; const QUERY_GUIDE_MD = `# RA3 Mod XML query tool reference The MCP server exposes these tools: - get_status() - find_asset(id, type?) - find_references(id, type?) - get_asset_references(id, type?, depth?, targetTypes?, maxEdges?, includeUnresolved?) - list_assets_by_type(type, prefix?, limit?) - is_file_active(path) - find_define(name) - resolve_include(source) - get_usage_guide() ## Result metadata Every query result includes an \`index\` object: \`\`\`json { "state": "ready", "projectDir": "D:/Mods/Example", "complete": true, "stale": false } \`\`\` Possible states: - no_index: no index snapshot exists. - building: a rebuild is in progress. - ready_xml: XML assets are available but art assets may be incomplete. - ready: complete index. - stale: index may be outdated. - error: last build failed. Always check \`index.projectDir\`. If it does not match the project you are working on, discard the result and read files directly instead. ## get_asset_references Returns outgoing reference edges with full provenance: \`\`\`json { "index": { "state": "ready", "projectDir": "D:/Mods/Example" }, "data": { "roots": [{ "type": "GameObject", "id": "AthenaCannon", "file": "...", "line": 12 }], "edges": [ { "depth": 1, "from": { "type": "GameObject", "id": "AthenaCannon" }, "to": { "type": "WeaponTemplate", "id": "AthenaCannonWeapon", "file": "...", "line": 88 }, "via": { "kind": "attribute", "element": "Weapon", "parent": "WeaponSlotHardpoint", "attribute": "Template" }, "source": { "file": "D:/Mods/Example/Data/Allied/Units/AthenaCannon.xml", "line": 40, "character": 24 } } ], "nodes": [], "truncated": false, "omittedByTargetType": {}, "warnings": [] } } \`\`\` \`via.kind\` is one of \`attribute\`, \`content\` or \`inheritFrom\`. Edges carrying \`definedIn\` come from an \`inheritFrom\` ancestor's XML. This tool requires a **live** index (VS Code open with the project indexed). It is not available from the on-disk snapshot because element context is not stored there. When live is unavailable the tool returns an explicit error instead of an empty result. `; export interface SkillInstallRecord { /** Skill directory (the directory containing SKILL.md). */ path: string; /** Extension version that installed/updated this copy. */ sourceVersion: string; } /** * Writes a managed copy of the Skill into `targetDir` (the directory that * should contain SKILL.md). */ export async function writeSkillTo( targetDir: string, sourceVersion: string, ): Promise { await mkdir(join(targetDir, "references"), { recursive: true }); await writeFile(join(targetDir, "SKILL.md"), SKILL_MD, "utf8"); await writeFile( join(targetDir, "references", "query-guide.md"), QUERY_GUIDE_MD, "utf8", ); const marker: SkillInstallRecord = { path: targetDir, sourceVersion, }; await writeFile( join(targetDir, SKILL_MARKER_FILE), JSON.stringify(marker, null, 2), "utf8", ); } /** Conventional ~/.agents/skills/ path. */ export function agentsSkillsDirForUser(home = homedir()): string { return join(home, ".agents", "skills", SKILL_NAME); } /** Conventional ~/.claude/skills/ path (Claude Code). */ export function claudeSkillsDirForUser(home = homedir()): string { return join(home, ".claude", "skills", SKILL_NAME); } /** Path to the managed-install record under the agent home. */ export function skillInstallRecordPath(agentHome = defaultAgentHome()): string { return join(agentHome, "skill-install.json"); } /** Reads the managed skill install record, or returns an empty list. */ export async function readSkillInstallRecord( agentHome = defaultAgentHome(), ): Promise { try { const text = await readFile(skillInstallRecordPath(agentHome), "utf8"); const parsed = JSON.parse(text) as { installed?: SkillInstallRecord[] }; return Array.isArray(parsed.installed) ? parsed.installed : []; } catch { return []; } } /** Writes the managed skill install record. */ export async function writeSkillInstallRecord( installed: SkillInstallRecord[], agentHome = defaultAgentHome(), ): Promise { const file = skillInstallRecordPath(agentHome); await mkdir(dirname(file), { recursive: true }); await writeFile( file, JSON.stringify({ installed }, null, 2), "utf8", ); } /** * Installs the Skill into several directories and records each managed copy. * Returns the directories successfully written. */ export async function installSkillToDirectories( directories: string[], sourceVersion: string, agentHome = defaultAgentHome(), ): Promise { const installed = await readSkillInstallRecord(agentHome); const succeeded: string[] = []; for (const dir of directories) { try { await writeSkillTo(dir, sourceVersion); if (!installed.some((r) => r.path === dir)) { installed.push({ path: dir, sourceVersion }); } else { const record = installed.find((r) => r.path === dir); if (record) record.sourceVersion = sourceVersion; } succeeded.push(dir); } catch { // Keep going; caller can surface per-directory failures. } } await writeSkillInstallRecord(installed, agentHome); return succeeded; } /** Removes one managed Skill directory and its install record entry. */ export async function uninstallSkillFromDirectory( directory: string, agentHome = defaultAgentHome(), ): Promise { await rm(directory, { recursive: true, force: true }); const installed = (await readSkillInstallRecord(agentHome)).filter( (r) => r.path !== directory, ); await writeSkillInstallRecord(installed, agentHome); } /** Re-writes every recorded Skill copy with the current extension version. */ export async function syncInstalledSkills( sourceVersion: string, agentHome = defaultAgentHome(), ): Promise { const installed = await readSkillInstallRecord(agentHome); const synced: SkillInstallRecord[] = []; for (const record of installed) { try { await writeSkillTo(record.path, sourceVersion); synced.push({ path: record.path, sourceVersion }); } catch { // Skip unreadable/missing targets; the record will be cleaned below. } } await writeSkillInstallRecord(synced, agentHome); return synced; }