Add Vortex extension scaffold + MCP stdio client (Unit E) - #15
Merged
Conversation
New vortex-extension/ top-level folder: a separate TypeScript/Node toolchain (package.json, tsconfig strict mode, webpack+ts-loader build, eslint flat config, vitest) fully isolated from WitcherScriptMerger.sln - confirmed via dotnet build/dotnet format whitespace before and after, unchanged. - src/index.ts: the init(context) entry point (verified against the real @nexusmods/vortex-api 2.4.2 typings, not the activate(context) name an earlier, unmerged design doc assumed) - gating only, no feature registration, per this unit's scope. - src/gating.ts: shared isWitcher3Active() helper + WITCHER3_GAME_ID, for every later unit's own registrations to gate on. - src/mcpClient.ts: hand-rolled MCP stdio client (child_process.spawn + newline-delimited JSON-RPC 2.0 framing) - api.runExecutable's IRunOptions has no stdio/pipe access, confirmed against the published typings, so it can't carry MCP's frames. Spawn-per-workflow lifecycle, typed wrappers for all four WSM MCP tools (scan_conflicts/merge_conflicts/get_status/ list_merges), no dependency on vortex-api itself (pure Node, independently testable). - test/mcpClient.integration.test.ts: real, no-mocks integration test - spawns the actual compiled WitcherScriptMerger.Headless.exe in mcp mode against a scratch mods folder and drives a full initialize -> tools/list -> tools/call round trip. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Foundation scaffold for a Vortex (Nexus Mods) companion extension for WitcherScriptMerger, plus the shared MCP stdio client later units (tool acquisition, conflict scanning, merge panel, dashlets) will build on. This unit is scaffold + plumbing only — no actual Vortex-facing features are registered here.
vortex-extension/folder: an entirely separate TypeScript/Node toolchain (npm + webpack/ts-loader + eslint flat config + vitest), fully isolated fromWitcherScriptMerger.sln. Confirmed viadotnet build/dotnet format whitespace --verify-no-changesbefore and after this change — identical (5 pre-existingCA1823warnings, 0 errors; format check clean).src/index.ts— the extension entry point. Does only game-activity gating (context.once(...), logs whether Witcher 3 is active) — noregisterAction/registerGame/etc. calls, per this unit's scope. Never callscontext.registerGame('witcher3', ...)— Vortex's own built-ingame-witcher3extension already owns that registration.src/gating.ts— small shared helper (isWitcher3Active,WITCHER3_GAME_ID) every later unit's own registrations should gate on.src/mcpClient.ts— the hand-rolled MCP stdio client (this unit's main deliverable).test/mcpClient.integration.test.ts— a real, no-mocks integration test that spawns the actual compiledWitcherScriptMerger.Headless.exeand drives a fullinitialize->tools/list->tools/callround trip.Why a hand-rolled MCP client
Verified against
@nexusmods/vortex-api's actual publishedlib/api.d.ts(not assumed):IRunOptions(the type ofrunExecutable's third argument) is{ cwd?, env?, suggestDeploy?, shell?, detach?, expectSuccess?, onSpawned?, onExit? }— no stdio/pipe access at all, so it cannot carry MCP's JSON-RPC frames.src/mcpClient.tsinstead uses rawchild_process.spawn(exePath, ['mcp'], { stdio: 'pipe' })and hand-rolls the framing: MCP's stdio transport is newline-delimited JSON-RPC 2.0 (confirmed against the current MCP spec — not LSP-styleContent-Lengthframing).mcpClient.tsAPI surface (for later units)Plus
ScanConflictsResult,MergeConflictsArgs/MergeConflictsResult,GetStatusResult,ListMergesResult(and their nested types), transcribed 1:1 fromWitcherScriptMerger.Core/Mcp/WsmMcpTools.cs's actual anonymous-object shapes — andWsmMcpProcessError/WsmMcpToolErrorfor error handling.Process lifecycle policy (documented in the file's header comment): spawn per user-initiated workflow,
close()when done — not a long-lived singleton. Every WSM MCP tool call already re-scans/re-loads state server-side, so a persistent process would only save the handshake cost, not worth the added crash/restart/orphan-process bookkeeping for v1.mcpClient.tshas zero dependency onvortex-api— pure Nodechild_process, independently testable and reusable regardless of which Vortex UI surface a later unit builds.Notable findings from actually running this (not assumed)
@nexusmods/vortex-apinpm package is types-only (itspackage.jsonexportsmap has only a"types"condition, no runtime target). Real extension code imports from the bare specifier'vortex-api'(confirmed in the package's own README and in a real third-party extension), which Vortex's own loader injects at runtime.tsconfig.json'spathsalias andwebpack.config.cjs'sexternals(both documented in-file) exist specifically to bridge this.@nexusmods/vortex-apiis listed underdevDependencies, notdependencies— it contributes zero runtime code (marked external, never bundled) and exists purely for types + the peer-dependency list used to build the webpack externals set. This matches real precedent found during research (the package's owndocs/MIGRATION.md, and a real hand-written third-party extension'spackage.json).tools/callresult shape:{ content: [{ type: "text", text: "<json>" }] }, with nostructuredContentfield at all, andisErrorsimply omitted when false.callTool()'s dual-path handling (preferstructuredContent, fall back to parsingcontent[0].text) was written defensively before this was known; the fallback path is the one actually exercised against WSM's currentModelContextProtocolSDK version.init(context), notactivate(context)— the not-yet-mergedchore/vortex-extension-design-docbranch's design doc gets this wrong; this PR follows the verified name.npm installneeded an explicitoverrides(pinningreact/react-domto16.14.0, matching@nexusmods/vortex-api's own peer pin) rather than--legacy-peer-deps— the latter was tried first and produced a genuinely brokenajv/ajv-keywordsresolution (ts-loader/webpack failed withCannot find module 'ajv/dist/compile/codegen') that a strict, override-guided resolution avoids.Code review
Ran the
code-reviewskill against this diff; three real findings were fixed:child.stdinhad no'error'listener — a write racing an already-exited WSM process (e.g.Environment.Exit(1)on missingApp.config) could crash the host process (Vortex) with an unhandled stream error instead of surfacing as a rejected promise. Fixed with both an'error'listener and atry/catcharound the write itself.App.configbuilder escaped backslashes (not XML-special) but not actual XML metacharacters (&,<,>,",') in the interpolated mods-directory path. Fixed with a properescapeXmlAttributehelper.npm testran both the fast unit test and the slow, .NET-toolchain-dependent integration test together, which would hard-fail on a Node-only machine/CI runner with nodotnetonPATH. Split intonpm test(fast, Node-only,src/**/*.test.ts) andnpm run test:integration(the real spawned-process test, buildsWitcherScriptMerger.Headlessitself if needed).Test plan
cd vortex-extension && npm install && npm run build— typecheck + webpack bundle succeed.npm run lint— clean.npm test— 3/3 fast unit tests pass (no .NET SDK needed).npm run test:integration— 2/2 tests pass, exercising the realinitialize->tools/list->tools/callround trip against the actual compiledWitcherScriptMerger.Headless.exe(get_status,scan_conflicts,list_mergesagainst a scratch empty mods folder).dotnet build WitcherScriptMerger.slnanddotnet format whitespace WitcherScriptMerger.sln --verify-no-changesfrom the repo root — unaffected, identical to before this change.AI-assisted development disclosure
This PR was substantially produced by Claude Code (an AI coding agent), per this repo's
CONTRIBUTING.mddisclosure convention. Commits carryCo-Authored-By/Claude-Sessiontrailers.🤖 Generated with Claude Code
https://claude.ai/code/session_01GXAuGMLB44T5Zv5o5ZzKah