cb369d3be3
Adds a postPatch step that applies metamcp-observability.patch to
the upstream metatool-ai/metamcp v2.4.22 source before pnpm build.
The patch:
* Drops in apps/backend/src/lib/observability/{trace,metrics}.ts:
AsyncLocalStorage trace context + parser/synthesizer per the W3C
contract (Specs/mcp-request-id), plus a hand-rolled
Counter/Histogram so we don't have to touch pnpm-lock.yaml /
pnpmDeps hash (no new npm dependency).
* Wires a top-level express middleware in apps/backend/src/index.ts
that binds trace context, observes mcp_hop_duration_seconds on
response close, and counts mcp_cancellation_total when the
downstream client hangs up mid-response.
* Adds /metrics to the Express app and last-resort process traps
(unhandledRejection / uncaughtException) feeding
mcp_uncaught_throw_total — the smoking-gun signal from
cluster#44.
* Patches process-managed-transport.send() to inject
params._meta.traceparent on every outbound JSON-RPC bound for a
stdio child (MCP _meta convention, Specs/mcp-request-id).
Retire when this lands upstream.
210 lines
7.0 KiB
Nix
210 lines
7.0 KiB
Nix
{
|
|
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 <<EOF
|
|
#!${stdenv.shell}
|
|
set -e
|
|
|
|
export PATH=${
|
|
lib.makeBinPath [
|
|
nodejs_20
|
|
pnpm_10
|
|
postgresql # for pg_isready
|
|
]
|
|
}:\$PATH
|
|
|
|
ROOT=$out/lib/metamcp
|
|
: "\''${POSTGRES_HOST:=127.0.0.1}"
|
|
: "\''${POSTGRES_PORT:=5432}"
|
|
: "\''${POSTGRES_USER:=postgres}"
|
|
|
|
echo "Waiting for PostgreSQL at \$POSTGRES_HOST:\$POSTGRES_PORT..."
|
|
until pg_isready -h "\$POSTGRES_HOST" -p "\$POSTGRES_PORT" -U "\$POSTGRES_USER"; do
|
|
sleep 2
|
|
done
|
|
|
|
echo "Running drizzle migrations..."
|
|
cd "\$ROOT/apps/backend"
|
|
pnpm exec drizzle-kit migrate
|
|
|
|
echo "Starting backend on :12009..."
|
|
PORT=12009 node "\$ROOT/apps/backend/dist/index.js" &
|
|
BACKEND_PID=\$!
|
|
sleep 3
|
|
kill -0 \$BACKEND_PID 2>/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=<system-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 <<EOF
|
|
#!${stdenv.shell}
|
|
cd $out/lib/metamcp/apps/frontend/.next/standalone/apps/frontend
|
|
exec env HOSTNAME="\''${METAMCP_HOSTNAME:-0.0.0.0}" ${nodejs_20}/bin/node server.js "\$@"
|
|
EOF
|
|
chmod +x $out/bin/metamcp-frontend
|
|
|
|
runHook postInstall
|
|
'';
|
|
|
|
# The pnpm/turbo build writes into $HOME; give it a writable one.
|
|
preBuild = ''
|
|
export HOME=$TMPDIR
|
|
'';
|
|
|
|
meta = {
|
|
description = "MetaMCP — MCP aggregator, orchestrator, middleware, gateway";
|
|
homepage = "https://github.com/metatool-ai/metamcp";
|
|
license = lib.licenses.mit;
|
|
mainProgram = "metamcp";
|
|
platforms = [
|
|
"x86_64-linux"
|
|
"aarch64-linux"
|
|
];
|
|
};
|
|
})
|