Running a test server
test-servers/ provides composable MCP servers so tests and manual checks
exercise a real server over a real transport instead of mocks. A server is
assembled from presets (fixture factories in
test-servers/src/preset-registry.ts) and configured declaratively with a JSON
file under test-servers/configs/.
The full catalogue of showcase configs — one per feature, each with what to click
and what the broken build did — is
docs/test-servers.md. This skill is how to
run one.
Build first
The servers are spawned as real subprocesses, so the build output must exist:
cd clients/web && npm run test-servers:build # tsc -p test-servers → test-servers/build/
Scripts reach this through scripts/ensure-test-servers.mjs, which builds
unconditionally (once per process per repo root).
⚠️ Unconditional emit is not a clean. A deleted source file leaves its
stale .js behind, existence checks pass against it, and anything still
importing that module silently runs the old code — reported not as staleness but
as a product failure in whatever was being tested. So after deleting or renaming
a source file:
rm -rf test-servers/build
The .tsbuildinfo is pinned inside build/ so that clean actually invalidates
the cache.
Run one
Two processes: the test server, then the Inspector.
# 1. The server, from the repo root, with the config you picked:
node test-servers/build/server-composable.js --config test-servers/configs/<name>.json
# 2. The Inspector, in another terminal (needs a built launcher — `npm run build`):
node clients/launcher/build/index.js --web
Then add the server in the Inspector using the URL the first process announced.
Two mechanics that bite:
- The server announces its URL on stderr, not stdout (
console.errorinserver-composable.ts). Watching stdout alone looks like a server that never started. - The bound port is not necessarily the config's.
createTestServerHttpresolves throughfindAvailablePort(), which walks upward when the configured port is taken — so read the announced URL rather than assuming.
Pick the right protocol era
Each config in docs/test-servers.md says
which era to connect with. The default is legacy; configs setting
transport.modern need Protocol Era = Modern. Connecting with the wrong era
usually looks like a missing capability rather than an error.
Common starting points
| Want to see | Config |
|---|---|
| An MCP App in the Apps tab | mcp-app-http.json (legacy) |
| An App-rendered elicitation | app-elicitation-http.json (legacy) |
Mcp-* headers + the modern error taxonomy |
modern-network-http.json |
A tool result's structuredContent section |
structured-output-http.json (legacy) |
| RFC 6570 resource-template expansion | rfc6570-templates-http.json |
| OAuth token revocation on clear | oauth-revocation-http.json (legacy) |
| Cancelling a call mid-flight | cancellation-modern-http.json (modern) |
Adding a config or preset
- Presets live in
test-servers/src/preset-registry.ts; configs intest-servers/configs/*.json. - A new showcase config gets a row in
docs/test-servers.mdsaying what to do and what the broken build did — the "what it looked like broken" half is what makes the fixture reproducible later. - ⚠️ An
outputSchemaoverride must ride a tool that returns structured content. A conforming client validates the result against the advertised schema, so an override on a preset returning none makes every call fail with "declares an output schema but returned no structured content".