The checked-in .tool-versions selects Erlang/OTP 27.2.3 and Elixir
1.18.4-otp-27. The package supports Elixir ~> 1.17; CI exercises the oldest
supported line, the pinned development line, and the current Elixir/OTP line.
.tool-versions is a version declaration, not an installer. A fresh checkout
must be bootstrapped once before any Mix gate is expected to work:
bin/setupThe setup script requires mise, git, and jq; it fails immediately with a
specific error when one is missing. It idempotently installs the pinned runtimes
with mise, installs Hex and Rebar for that runtime, and fetches the project
dependencies. Mise activation is not needed to run bin/setup itself. Activate
mise afterward so its shims resolve mix and elixir, or prefix commands with
mise exec --. This avoids silently using an incompatible host Elixir or
treating a missing runtime as a repository test failure.
Run these from the repository root:
mix deps.get
mix precommitmix precommit is the canonical local gate. It runs the following checks in
the same deterministic order on every invocation; CI keeps them as separate
steps so a hosted failure names the failed check directly:
mix format --check-formatted
mix compile --warnings-as-errors
mix test --seed 0
mix credo --strict
mix dialyzer
mix docs --warnings-as-errors
elixir scripts/package_smoke.exs
bash -n bin/setup
test -x bin/setup
mix hex.audit
mix deps.unlock --check-unused
git diff --check
jq empty conformance/scenarios.json
jq empty conformance/compatibility-2025-11-25.json
jq empty conformance/mcp-apps-2026-01-26.jsonDo not use sleeps to coordinate process tests. Start owned processes with
start_supervised!/1, synchronize mailboxes with _ = :sys.get_state(pid),
and assert termination with monitors.
The harness is pinned to @modelcontextprotocol/conformance@0.2.0-alpha.11.
Select the exact revision and start the server adapter:
MCP_CONFORMANCE_PROTOCOL_VERSION=2025-11-25 \
mix run --no-halt conformance/server_adapter.exs 43001Then run both server release denominators:
npm ci --ignore-scripts
npx --no-install conformance server \
--url http://127.0.0.1:43001/mcp \
--requirements 2026-07-28
npx --no-install conformance server \
--url http://127.0.0.1:43001/mcp \
--requirements 2025-11-25Client scenarios invoke the checked-in adapter, for example:
npx --no-install conformance client \
--command 'mix run conformance/client_adapter.exs' \
--scenario sep-2322-client-request-state \
--spec-version 2026-07-28For the frozen November requirement set, explicitly pass the wire revision to
the adapter because harness 0.2.0-alpha.11 does not forward the requirement
revision to child-process environments:
npx --no-install conformance client \
--command 'env MCP_CONFORMANCE_PROTOCOL_VERSION=2025-11-25 ERL_LIBS=_build/dev/lib elixir conformance/client_adapter.exs' \
--requirements 2025-11-25conformance/scenarios.json records the 2026 release matrix and
conformance/compatibility-2025-11-25.json records the legacy evidence and
limitations. Authorization-profile scenarios remain outside the SDK transport
scope. Do not replace the pin with latest in release evidence.
The legacy sse-retry scenario remains a partial, release-blocking client
result: pinned harness 0.2.0-alpha.11 negotiates 2025-03-26, which is
outside the SDK's supported revision set. elixir scripts/validate_conformance_ledgers.exs enforces the exact scenario status
and release-blocker reason. CI wraps server denominators in ten-
minute limits and client scenarios in five-minute limits, then uploads all
per-scenario output plus the adapter log as the 14-day
mcp-core-conformance artifact, including on failure.
The declared optional Req range is tested at both supported boundaries in CI:
0.6.1 and 0.7.2. Each matrix job rewrites the dependency to an exact version
with scripts/select_req_compat.exs, resolves a fresh lock, compiles with
warnings as errors, and runs the Streamable HTTP client, response-bound, and
subscription integration suites. The script is CI evidence tooling and changes
mix.exs; use it only in a disposable checkout or restore that file afterward.
All primary CI jobs have finite job-level timeouts (30 minutes; Req boundary jobs use 20 minutes), so an adapter, test, or network hang cannot consume an unbounded runner allocation.
elixir scripts/package_smoke.exs builds and unpacks the Hex archive into a
new temporary directory, checks the release documents, conformance ledger, and
quick-start example are present, fetches production dependencies, compiles the
extracted project with warnings as errors, and executes the packaged quick
start. The temporary extraction is always removed afterward, so no untracked
checkout file can satisfy this gate accidentally.
CI also runs the full suite with two BEAM schedulers, two async threads, eight ExUnit cases, and an OS process limit of 1024. This is the regression gate for subprocess-heavy stdio tests that can pass in isolation but fail when the full suite competes for host resources.
The release probe is test/runtime/unraid_stdio_cleanup.exs, run as:
mix run --no-start test/runtime/unraid_stdio_cleanup.exs -- test/support/adversarial_stdio_server.exs--no-start is required when MCP_ERLEXEC_ALLOW_ROOT=1: erlexec reads its root
configuration at application start, so the probe must configure it before the
application tree comes up.
On 2026-08-10 it was run from the built Hex archive on an Unraid development
host, Linux 6.18.38-Unraid, inside a disposable elixir:1.18.4 container with Docker
--init. The host has no native Mix installation.
The first run exposed that /proc/<pid>/task/<pid>/children alone did not find
a descendant that created its own process group. The internal stdio process
owner now cross-checks the complete /proc/*/stat parent table before shutdown,
capturing each descendant's parent and start time from a single read so a PID
recycled mid-scan cannot be mistaken for a descendant. After rebuilding the
archive, the probe reported:
unraid stdio cleanup passed root_pid=335 root_pgid=335 descendant_pid=415 descendant_pgid=415
The separate process-group IDs are significant: cleanup did not pass merely because erlexec signalled the root command's process group. The temporary container, package directory, and images pulled solely for the probe were removed afterward.
test/runtime/microsandbox_stdio_roundtrip.exs runs a real third-party MCP
server inside a microsandbox microVM and
drives it through this SDK's stdio transport:
mix run --no-start test/runtime/microsandbox_stdio_roundtrip.exsRequires msb on PATH (or MCP_MSB_BIN) and a KVM-capable host. The first
run needs network access: it creates the sandbox and installs
@modelcontextprotocol/server-everything into it. Later runs reuse the warm
sandbox. MCP_MSB_SANDBOX and MCP_MSB_IMAGE override the defaults.
The probe exists because three properties cannot be shown by the unit suite:
- A sandbox is only a different
:command/:args. The transport is unchanged, which is what makesSecurityPolicy's absolute-executable-plus-argv rule compatible with containment wrappers. - Normal close reaps the host launcher and the process inside the guest,
which the host-side
/procdescendant sweep cannot see. - A SIGKILLed BEAM — where
terminate/2never runs — also leaves no orphan on either side.unraid_stdio_cleanup.exsonly covers the close path.
It also pins two facts that are easy to regress into:
- The shared
exec-portmust survive one upstream closing. erlexec registers one per BEAM (deps/erlexec/src/exec.erl:544), so tearing it down with a single upstream would kill every sibling upstream. - The microVM itself is not reclaimed by any teardown path.
msb createis persistent by design, so a consumer must stop it explicitly and use--idle-timeoutas the backstop for the SIGKILL case. The probe asserts the VM is still running at the end so the obligation stays visible.
On 2026-08-10 a run on Linux 6.17 with nested KVM reported:
roundtrip passed server=mcp-servers/everything v2.0.0 era=2025-11-25 tools=13 sum="The sum of 17 and 25 is 42."
normal close passed launcher=3781715 reaped, shared exec_port=3781658 survived
sigkill cleanup passed beam=3781818 launcher=3781990 exec_port=3781936
microvm survived both teardowns; consumers must stop it explicitly
The negotiated 2025-11-25 is incidental evidence for the fallback ladder: the
reference server has no 2026-07-28 support, so the session was only reachable
by walking down to a shared revision. Host teardown after SIGKILL is not
instantaneous — exec-port observes a broken pipe rather than a signal — so the
orphan assertions allow 60s.
Every buffered msb call in the probe redirects stdin from /dev/null.
Buffered msb exec drains stdin to EOF before running anything and an Erlang
port never closes the child's stdin, so the pair deadlocks; the MCP launch path
avoids this by using msb exec --stream.