{ lib, stdenv, fetchFromGitHub, makeWrapper, nodejs_20, pnpm_10, fetchPnpmDeps, pnpmConfigHook, postgresql, }: # MetaMCP — MCP aggregator/orchestrator. # # Upstream is a pnpm monorepo (Turbo) with two apps: # - apps/backend : Express/tRPC API, built with tsup -> dist/index.js # port 12009 (internal), runs drizzle-kit migrations at boot # - apps/frontend : Next.js 15, port 12008 (public), launched via `next start` # # Both require runtime node_modules (Next.js non-standalone; drizzle-kit is # a devDep invoked at runtime by docker-entrypoint.sh). We ship the whole # built tree under $out/lib/metamcp and provide launcher scripts. # # The upstream Dockerfile patches Next.js' proxy timeout (30s -> 600s) by # sed-editing two files inside node_modules/.pnpm/next@.../...; we replicate # that in postBuild so the behaviour matches the official image. # # Build is single-derivation: pnpm fetch -> pnpm build -> install. First # build prints the right pnpmDeps hash; paste it back here and rebuild. let pname = "metamcp"; version = "2.4.22"; src = fetchFromGitHub { owner = "metatool-ai"; repo = "metamcp"; rev = "v${version}"; hash = "sha256-EEb3RUjsaJ5ZSHSIkAxfdV/BAjZEAvw3rtjALM4RpSc="; }; in stdenv.mkDerivation (finalAttrs: { inherit pname version src; pnpmDeps = fetchPnpmDeps { inherit pname version src; fetcherVersion = 3; hash = "sha256-nHHLLu5wBzzP4i/oTnOkuIiPQvvvBAIIVtKdfpDiXQw="; }; nativeBuildInputs = [ nodejs_20 pnpm_10 pnpmConfigHook makeWrapper ]; # Upstream pins `packageManager: pnpm@9.0.0`; nixpkgs ships pnpm 10. The # lockfile is v9 (forward-compatible). Rewrite the pin to the pnpm we # actually have so pnpm 10 doesn't try to network-fetch pnpm@9 and Turbo # (which *requires* the field) still finds it. postPatch = '' ${nodejs_20}/bin/node -e ' const fs = require("fs"); const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")); pkg.packageManager = "pnpm@${pnpm_10.version}"; if (pkg.engines) delete pkg.engines.pnpm; fs.writeFileSync("package.json", JSON.stringify(pkg, null, 2) + "\n"); ' # cluster#49, cluster#50 — observability patch: W3C traceparent # propagation (HTTP header → params._meta.traceparent on stdio # children) + hand-rolled Prometheus metrics on /metrics, no new # dependency so pnpmDeps stays untouched. Two new source files # under apps/backend/src/lib/observability/ and minimal edits to # apps/backend/src/index.ts and process-managed-transport.ts. # Retire when this lands upstream. patch -p1 < ${./metamcp-observability.patch} ''; # pnpmConfigHook places node_modules; build the workspace with Turbo. buildPhase = '' runHook preBuild # Match the upstream Dockerfile sed-patch on Next.js proxy timeout. # Files live under the pnpm virtual store; glob to tolerate minor # next/react version bumps on later tags. for f in \ node_modules/.pnpm/next@*/node_modules/next/dist/server/lib/router-utils/proxy-request.js \ node_modules/.pnpm/next@*/node_modules/next/dist/esm/server/lib/router-utils/proxy-request.js; do if [ -f "$f" ]; then sed -i -e "s/30000/600000/" "$f" fi done pnpm build runHook postBuild ''; installPhase = '' runHook preInstall mkdir -p $out/lib/metamcp $out/bin # Ship the whole built workspace. This is bulky but reliable: # `next start` needs .next + node_modules + package.json side-by-side, # and the backend's runtime invokes `pnpm exec drizzle-kit migrate`. cp -r \ apps \ packages \ node_modules \ package.json \ pnpm-workspace.yaml \ pnpm-lock.yaml \ turbo.json \ $out/lib/metamcp/ # Next.js standalone post-processing: `output: "standalone"` produces # apps/frontend/.next/standalone/apps/frontend/server.js but does NOT # copy .next/static or public/ into the standalone tree. Do it here # so the runtime UI has its CSS, JS chunks and static assets. SA=$out/lib/metamcp/apps/frontend/.next/standalone/apps/frontend cp -r $out/lib/metamcp/apps/frontend/.next/static $SA/.next/static cp -r $out/lib/metamcp/apps/frontend/public $SA/public # Launcher: re-implementation of upstream's docker-entrypoint.sh. # Cleaner than substituteInPlace-ing the upstream script (the original # has overlapping `/app` substrings that break naive replacement) and # gives us a single place to keep the orchestration logic in sync. cat > $out/bin/metamcp </dev/null || { echo "Backend died"; exit 1; } echo "Starting frontend on :12008..." cd "\$ROOT/apps/frontend/.next/standalone/apps/frontend" # Next.js standalone uses \$HOSTNAME as the bind address. The shell # inherits HOSTNAME=, leaving the server unreachable # on 127.0.0.1 — force 0.0.0.0 unless overridden via METAMCP_HOSTNAME. PORT=12008 HOSTNAME="\''${METAMCP_HOSTNAME:-0.0.0.0}" node server.js & FRONTEND_PID=\$! sleep 3 kill -0 \$FRONTEND_PID 2>/dev/null || { kill \$BACKEND_PID 2>/dev/null; echo "Frontend died"; exit 1; } trap 'kill \$BACKEND_PID \$FRONTEND_PID 2>/dev/null || true' TERM INT wait \$BACKEND_PID \$FRONTEND_PID EOF chmod +x $out/bin/metamcp # Direct sub-launchers (handy for systemd "two-unit" deployments if # you ever want to skip the bundled orchestrator). makeWrapper ${nodejs_20}/bin/node $out/bin/metamcp-backend \ --add-flags "$out/lib/metamcp/apps/backend/dist/index.js" \ --set NODE_ENV production cat > $out/bin/metamcp-frontend <