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" + } }