feat(record): capture MCP call streams to NDJSON and replay deterministically#175
feat(record): capture MCP call streams to NDJSON and replay deterministically#175mvanhorn wants to merge 2 commits into
Conversation
…stically mcporter record <session> wraps the runtime transport and appends every JSON-RPC request, response, and notification to a per-session NDJSON file under ~/.mcporter/recordings/. mcporter replay <session> reconstructs an in-memory transport from the recording and matches requests by method + deep-equal params, returning the recorded response without contacting the live server. Use cases: - Reproduce MCP-backed agent bugs offline (no live Linear quota, no Vercel API rate limits) - Build test fixtures from real call sequences - Share a session for a postmortem without sharing credentials The format is plain JSON-RPC over NDJSON with a small _meta field (direction, server, timestamp). No proprietary blob. Env-var passthrough (MCPORTER_RECORD=<name>, MCPORTER_REPLAY=<name>) lets the existing runtime constructor wrap any transport when set.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e2460611af
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| this.expectedSends.shift(); | ||
| if (expected.response) { | ||
| queueMicrotask(() => this.onmessage?.(expected.response as JSONRPCMessage)); |
There was a problem hiding this comment.
Rewrite replayed response ids to the active request
When the replayed client’s JSON-RPC id counter differs from the original recording (for example, a fixture captured after earlier daemon traffic or replayed from a different call sequence), this emits the recorded response id instead of the id from the request that just matched. JSON-RPC clients correlate responses by id, so the client will ignore the message and the call will hang or time out even though method/params matched; clone the recorded response with the current request id before delivering it.
Useful? React with 👍 / 👎.
Summary
mcporter record <session>andmcporter replay <session>. Record wraps the runtime transport and appends every JSON-RPC request, response, and notification to a per-session NDJSON file under~/.mcporter/recordings/<session>.ndjson. Replay reconstructs an in-memory transport from the recording and matches requests by method + deep-equal params, returning the recorded response without contacting the live server._metafield (direction, server name, ISO timestamp). No proprietary blob, no streaming archive library, no new runtime deps.MCPORTER_RECORD=<name>andMCPORTER_REPLAY=<name>let any existingmcporterinvocation participate (the runtime constructor wraps each server's transport when set).recvin the recording fails with a clear error naming the request and the next expectedrecv. No fuzzy matching, no auto-fallback to live — replay is for reproducing exact runs.Why this matters
When an MCP-backed workflow breaks in production, reproducing the bug means re-running the live MCP server with the same inputs — which is often expensive (Linear quota, Vercel API rate limits) or impossible (the server's state has changed). Today mcporter exposes MCP servers as TypeScript APIs and CLIs but has no way to capture what an agent actually called and what came back.
Three concrete use cases this unlocks:
mcporter record session-foo→ commitsession-foo.ndjsonto your test suite. Replay it in CI without network.Sibling project
openclaw/acpxalready has a flow trace replay (docs/2026-03-26-acpx-flow-trace-replay.md); this PR brings the same shape to MCP transport.Demo
Simulated demo:
The demo shows the full loop: record a Linear MCP call, then replay it deterministically even after the live server becomes unreachable. The NDJSON envelopes carry the
_metadirection + server fields the replay transport matches on.Testing
corepack pnpm typecheckcorepack pnpm lint(oxlint clean, oxfmt clean)corepack pnpm test— 646 tests pass; newtests/record-replay.test.tscovers:_meta.dirand_meta.serverpopulatedrecv_meta.server