fix(plugin): provision externalised MCP runtime deps on first launch#3
Merged
Conversation
`claude plugin install` clones repository files but runs no install step, so a
fresh install had no node_modules — and the committed esbuild bundle
(mcp-server/index.js) intentionally externalises three runtime deps that must
resolve from disk at launch:
- ajv / ajv-formats — ajv's runtime-compiled validators require() their
helpers (ajv/dist/runtime/*) via specifiers esbuild cannot statically
inline. Static require => hard requirement at server load. (ajv-formats was
also a phantom dep — required by the bundle, declared nowhere.)
- better-sqlite3 — native addon; the platform-specific binary cannot be
bundled or committed cross-platform. Loaded via dynamic import() and guarded
by tryCreateEvidenceRepository => optional (absence only disables the
evidence-DB cache, it does not block startup).
Result: a clean `claude plugin install` could not launch the server
(`Cannot find module 'ajv'`).
Fix — provision the deps on first launch, mirroring automatised-pipeline's
bin/ensure-binary.sh ensure-then-exec pattern:
- mcp-server/package.json declares the externals: ajv + ajv-formats as
dependencies, better-sqlite3 as an optionalDependency (so a failed native
build is non-fatal).
- bin/ensure-deps.sh installs them into mcp-server/node_modules on first
launch (idempotent; no-ops once present) and execs node.
- .mcp.json launches via the script instead of node directly.
Versions match the workspace declarations (benchmark ajv ^8.17.1, core
better-sqlite3 ^11.7.0) and resolve to the bundled set (ajv 8.18.0,
ajv-formats 3.0.1, better-sqlite3 11.10.0).
Verification (clean clone, no node_modules):
- first launch: installs 44 pkgs (~19s) then answers MCP `initialize`
- second launch: no reinstall, answers `initialize` immediately
- ajv / ajv-formats / better-sqlite3 all resolve from mcp-server/node_modules
- --no-package-lock keeps the install dir free of a second lockfile
Note: as with automatised-pipeline, the FIRST launch pays the install cost
(~20s) before the server is ready; subsequent launches are instant.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Follow-up to #2. That PR fixed the startup deadlock; this one fixes the other half of "won't run after a clean install": the server's runtime dependencies aren't present.
Problem
claude plugin installclones repository files but runs no install step, so a fresh install has nonode_modules. The committed esbuild bundle (mcp-server/index.js) intentionally externalises three runtime deps that must resolve from disk at launch:ajv/ajv-formatsrequire()their helpers (ajv/dist/runtime/*) via specifiers esbuild can't statically inlinerequire→ needed at load)better-sqlite3import(), guarded bytryCreateEvidenceRepository)So a clean
claude plugin installfails to launch the server withCannot find module 'ajv'. (ajv-formatswas also a phantom dependency — required by the bundle but declared in nopackage.json.)Fix
Provision the externals on first launch — the same ensure-then-exec pattern
automatised-pipelineuses for its Rust binary (bin/ensure-binary.sh):mcp-server/package.jsondeclares the externals:ajv+ajv-formatsasdependencies,better-sqlite3as anoptionalDependency(a failed native build is non-fatal — the server still starts, just without the evidence-DB cache).bin/ensure-deps.shinstalls them intomcp-server/node_moduleson first launch (idempotent; no-ops once present), thenexec node mcp-server/index.js.--no-package-lockkeeps the install dir free of a second lockfile (the workspace pins viapnpm-lock.yaml)..mcp.jsonlaunches via the script instead ofnodedirectly.Declared ranges match the workspace (
benchmarkajv^8.17.1,corebetter-sqlite3^11.7.0) and resolve to the bundled set (ajv 8.18.0, ajv-formats 3.0.1, better-sqlite3 11.10.0).Verification (clean clone, no
node_modules)initialize.initializeimmediately.ajv/ajv-formats/better-sqlite3all resolve frommcp-server/node_modules.package-lock.jsonlitter.bash -nclean; script committed100755.Note
As with
automatised-pipeline, the first launch pays the install cost (~20s) before the server is ready; subsequent launches are instant. If Claude Code's MCP connect times out on that first attempt, a reconnect succeeds.🤖 Generated with Claude Code