From 0b40835bd168b3f6194139f092600d95a2d3ea30 Mon Sep 17 00:00:00 2001 From: cdeust Date: Mon, 1 Jun 2026 10:32:02 +0200 Subject: [PATCH] fix(plugin): provision externalised MCP runtime deps on first launch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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) --- .mcp.json | 7 ++++-- bin/ensure-deps.sh | 47 +++++++++++++++++++++++++++++++++++++++++ mcp-server/package.json | 11 ++++++++-- 3 files changed, 61 insertions(+), 4 deletions(-) create mode 100755 bin/ensure-deps.sh diff --git a/.mcp.json b/.mcp.json index b876c45..c95cbf8 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,8 +1,11 @@ { "mcpServers": { "prd-gen": { - "command": "node", - "args": ["${CLAUDE_PLUGIN_ROOT}/mcp-server/index.js"], + "command": "bash", + "args": [ + "${CLAUDE_PLUGIN_ROOT}/bin/ensure-deps.sh", + "${CLAUDE_PLUGIN_ROOT}" + ], "env": { "PRD_GEN_SKILL_CONFIG": "${CLAUDE_PLUGIN_ROOT}/packages/skill/skill-config.json", "PRD_GEN_EVIDENCE_DB": "${CLAUDE_PLUGIN_ROOT}/.prd-gen/evidence.db" diff --git a/bin/ensure-deps.sh b/bin/ensure-deps.sh new file mode 100755 index 0000000..916b701 --- /dev/null +++ b/bin/ensure-deps.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# +# ensure-deps.sh — provision the MCP server bundle's externalised runtime +# dependencies on first launch, then exec the server. +# +# The committed bundle (mcp-server/index.js) is produced by `pnpm bundle` +# (esbuild). Three runtime dependencies are intentionally *external* to that +# bundle and must resolve from node_modules at launch: +# +# - ajv, ajv-formats — ajv's runtime-compiled validators `require()` their +# helpers (ajv/dist/runtime/*) via specifiers esbuild cannot statically +# inline, so ajv must exist on disk. Static require => needed at load. +# - better-sqlite3 — native addon; the platform-specific binary cannot be +# bundled or committed cross-platform. Loaded via dynamic import() and +# guarded by tryCreateEvidenceRepository => OPTIONAL: its absence only +# disables the evidence-DB cache, it does not block startup. Declared as +# an optionalDependency so a failed native build is non-fatal. +# +# `claude plugin install` clones repository files but runs no install step, so +# this launcher provisions `mcp-server/node_modules` next to the bundle on the +# first run, then hands off to node. Idempotent: it no-ops once the deps are +# present, so steady-state launch cost is a single directory check. +# +# Mirrors automatised-pipeline's bin/ensure-binary.sh ensure-then-exec launcher. +# source: coding-standards.md §2.2 (composition-root provisioning); follow-up to +# the MCP-startup-deadlock fix (PR #2). +# +set -euo pipefail + +ROOT="${1:?usage: ensure-deps.sh }" +SERVER_DIR="${ROOT}/mcp-server" + +# ajv is a hard (static) dependency of the bundle; its presence is the +# provisioning sentinel. better-sqlite3 (optional) is installed in the same +# pass and, being an optionalDependency, will not fail the install if its +# native build is unavailable on the host. +if [[ ! -d "${SERVER_DIR}/node_modules/ajv" ]]; then + echo "prd-gen: first launch — installing MCP server runtime deps…" >&2 + # --no-package-lock: the workspace pins versions via pnpm-lock.yaml; this is a + # runtime provisioning step into an ephemeral install dir, so we don't litter + # it with a second (npm) lockfile. + npm install \ + --prefix "${SERVER_DIR}" \ + --omit=dev --no-audit --no-fund --no-package-lock --loglevel=error >&2 +fi + +exec node "${SERVER_DIR}/index.js" diff --git a/mcp-server/package.json b/mcp-server/package.json index 5773c4d..e684b42 100644 --- a/mcp-server/package.json +++ b/mcp-server/package.json @@ -1,8 +1,15 @@ { "name": "@prd-gen/mcp-server-bundle", "version": "0.2.0", - "description": "Self-contained ESM bundle of the prd-spec-generator MCP server. Built by `pnpm bundle` from the workspace TypeScript source. The marketplace .mcp.json runs index.js directly via `node`.", + "description": "Self-contained ESM bundle of the prd-spec-generator MCP server. Built by `pnpm bundle` from the workspace TypeScript source. `bin/ensure-deps.sh` provisions the externalised runtime deps below on first launch, then runs index.js via `node`.", "type": "module", "main": "./index.js", - "private": true + "private": true, + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1" + }, + "optionalDependencies": { + "better-sqlite3": "^11.7.0" + } }