Skip to content

feat(record): capture MCP call streams to NDJSON and replay deterministically#175

Open
mvanhorn wants to merge 2 commits into
openclaw:mainfrom
mvanhorn:feat/mcporter-record-replay
Open

feat(record): capture MCP call streams to NDJSON and replay deterministically#175
mvanhorn wants to merge 2 commits into
openclaw:mainfrom
mvanhorn:feat/mcporter-record-replay

Conversation

@mvanhorn
Copy link
Copy Markdown
Contributor

Summary

  • Adds mcporter record <session> and mcporter 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.
  • Plain JSON-RPC over NDJSON with a small _meta field (direction, server name, ISO timestamp). No proprietary blob, no streaming archive library, no new runtime deps.
  • Env-var passthrough: MCPORTER_RECORD=<name> and MCPORTER_REPLAY=<name> let any existing mcporter invocation participate (the runtime constructor wraps each server's transport when set).
  • Replay matching is strict by design. A request that has no matching recv in the recording fails with a clear error naming the request and the next expected recv. 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:

  • Offline bug reproduction. Record once when the bug happens. Replay it on a laptop without the agent or the live server.
  • Test fixtures from real call sequences. mcporter record session-foo → commit session-foo.ndjson to your test suite. Replay it in CI without network.
  • Postmortem sharing without credentials. Share a recording, not the OAuth tokens that produced it.

Sibling project openclaw/acpx already 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:

record/replay 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 _meta direction + server fields the replay transport matches on.

Testing

  • corepack pnpm typecheck
  • corepack pnpm lint (oxlint clean, oxfmt clean)
  • corepack pnpm test — 646 tests pass; new tests/record-replay.test.ts covers:
    • recording writes one NDJSON line per send/recv with _meta.dir and _meta.server populated
    • replaying matches requests by method + params and returns the recorded response
    • mismatch requests throw with a clear error naming the request and the next expected recv
    • multi-server sessions keep streams separated by _meta.server
    • lifecycle events (start, close) are recorded for completeness but ignored on replay

…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.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant