Recording an SDK’s server
Capturing from your client records a session your own MCP host drives. This chapter is the other way round: you have a server built on an official SDK and want to know what it puts on the wire. It is worked through on the TypeScript (both of its lines) and Python SDKs’ own conformance servers — the servers each SDK runs the official suite against — but nothing here is specific to them: any Streamable HTTP server can stand in.
The commands are not copies. Each block below is included, by mdBook anchor,
from interop/matrix.sh,
the script the weekly scheduled job runs to keep the
cross-SDK matrix true; cargo xtask sdk-matrix --check fails
if this chapter stops including them, or the job stops running the script. What
you read is what the job executed.
Prerequisites
Build the recorder and the validator, then install the pinned SDKs:
cargo build -p mcp-trace-capture -p mcp-trace-validator
cd interop
./setup.sh
setup.sh installs each SDK at the version
interop/sdk-pins.env
names, every one with integrity: npm from package-lock.json (npm ci),
Python from the hash-locked requirements.txt (pip --require-hashes), Go
from go.sum, Rust from Cargo.lock. A conformance server that lives in an
SDK’s repository rather than its package is fetched from the exact commit the
pins name and refused unless its SHA-256 matches. It needs node 20+, Python 3.11+,
go, cargo and git.
Recording one session
Every session is the same four steps — start the server, put
mcp-trace-capture’s proxy in front of it, drive one session through the
proxy, stop the proxy so it writes the trace — followed by judging it:
# record NAME REVISION PORT -- SERVER...
#
# Starts SERVER (listening on 127.0.0.1:PORT), puts the capture proxy in
# front of it on PORT+100, drives one session through the proxy with the
# client for REVISION, stops the proxy (which writes the trace), and judges
# the trace. The ports are fixed rather than ephemeral because the proxy
# records the Host header, and a committed trace should not change with them.
record() {
local name=$1 revision=$2 port=$3 proxy=$(($3 + 100)) status=0
shift 4
"$@" > "$OUT/$name.server.log" 2>&1 & local server=$!
pids+=("$server")
wait_for_port "$port"
"$CAPTURE" -o "$OUT/$name.jsonl" http \
--upstream "http://127.0.0.1:$port" --listen "127.0.0.1:$proxy" \
2> "$OUT/$name.capture.log" & local capture=$!
pids+=("$capture")
wait_for_port "$proxy"
export INTEROP_ERRORS=record
case $revision in
2025-11-25) node ts-client.mjs http "http://127.0.0.1:$proxy/mcp" || status=$? ;;
2026-07-28) "$PYTHON" py-client.py http "http://127.0.0.1:$proxy/mcp" || status=$? ;;
esac
kill -INT "$capture"; wait "$capture" || true
kill "$server" 2>/dev/null || true; wait "$server" 2>/dev/null || true
judge "$name" "$revision" "$status"
}
The client is chosen by revision, and is the same for every server: the
TypeScript SDK’s v1 client (ts-client.mjs) speaks 2025-11-25, the Python
SDK’s (py-client.py) speaks 2026-07-28. Each lists what the server
advertises, calls a tool, calls one that does not exist, reads a resource and
gets a prompt. With INTEROP_ERRORS=record an error answer is logged and the
session carries on: the server’s answer is the evidence, so the client must not
stop at it.
The TypeScript SDK, v1
The v1 line is the single package @modelcontextprotocol/sdk, in maintenance.
The server is test/conformance/src/everythingServer.ts from the SDK’s
repository at the release tag. It imports the SDK by its path inside that
repository; setup.sh rewrites those imports to the published package, so it
runs on the pinned npm release, and tsx runs the TypeScript directly:
record sdk-typescript-2025-11-25 2025-11-25 38301 -- env PORT=38301 node_modules/.bin/tsx upstream/ts-everything-server.ts
record sdk-typescript-2026-07-28 2026-07-28 38301 -- env PORT=38301 node_modules/.bin/tsx upstream/ts-everything-server.ts
The second line is a session the server cannot serve: the TypeScript SDK’s 1.x
line implements the specification up to 2025-11-25 (its SUPPORTED_PROTOCOL_VERSIONS
stops there), so the Python client’s discovery falls back to the initialize
handshake. py-client.py exits 3 when that happens, and the script files the
recording as …not-served.jsonl rather than judging a session nobody asked for.
The TypeScript SDK, v2
The v2 line is a set of packages, @modelcontextprotocol/server and its
companions, and it does serve 2026-07-28: its server answers
server/discover with supportedVersions of ["2026-07-28"], and still answers
the initialize handshake for 2025-11-25, from one process. The server is the
same path in the SDK’s repository, test/conformance/src/everythingServer.ts,
at the release tag. Nothing is rewritten: the file imports the published packages by name, which
setup.sh installs from the lockfile (@modelcontextprotocol/server, node and
express, pinned together in sdk-pins.env). Both revisions are recorded:
# The v2 line's server answers both revisions from one process: the initialize
# handshake for 2025-11-25 and server/discover for 2026-07-28.
record sdk-typescript-v2-2025-11-25 2025-11-25 38305 -- env PORT=38305 node_modules/.bin/tsx upstream/ts2-everything-server.ts
record sdk-typescript-v2-2026-07-28 2026-07-28 38305 -- env PORT=38305 node_modules/.bin/tsx upstream/ts2-everything-server.ts
The Python SDK
The server is the SDK repository’s examples/servers/everything-server, run on
the hash-locked mcp release:
record sdk-python-2025-11-25 2025-11-25 38302 -- "$PYTHON" upstream/py_everything_server.py --port 38302
record sdk-python-2026-07-28 2026-07-28 38302 -- "$PYTHON" upstream/py_everything_server.py --port 38302
Judging, and what comes out
./matrix.sh ../target/debug
prints one line per session — the verdict and the totals — and leaves each
trace and its JSON report in interop/out-matrix/. Read any one in full the
usual way:
../target/debug/mcp-trace-validator validate out-matrix/sdk-python-2026-07-28.jsonl
A finding is a result, not a failure of the script: it exits non-zero only when a session could not be recorded at all. The same script records the Go and Rust SDKs’ servers, and the committed results of all of them are the cross-SDK matrix. To check a fresh run against them:
cargo xtask sdk-matrix --recaptured interop/out-matrix
Recording your own server
Replace the server command in a record line with yours, and give it a free
port. If your server speaks stdio rather than HTTP, record it the way
Getting started shows instead —
mcp-trace-capture -o session.jsonl stdio -- <server> — and point a client at
that command.