Session replay · MCP server included

Session replay your AI agent can read

One script tag, and a bug report stops being a sentence. It arrives with the session attached: the network calls with their bodies, the console, every click, and the DOM replay or the screen video. Watch it yourself — or point an AI agent at it over MCP and let it read the whole thing.

Free plan, no card. 6 KB gzip on your page, and nothing uploads until someone reports a problem.

network (with bodies), console, clicks, navigation — 6 KB gzip

<script src="https://app.espejo.dev/sdk/espejo.js"
        data-key="pk_live_..."></script>

Then, whenever a user hits something:

await espejo.report('Can’t save the order')
// → the URL of the uploaded session

One recording, four tracks, one clock

The replay, the network log, the console and the interactions share a timeline. A 500 stops being a line in a log and becomes the button that was pressed a second before it.

espejo · session 4f2a…c81
Network Console Clicks
  • 0:04 click — “Save”
  • 0:04 POST /v1/orders — 500
  • 0:04 Uncaught TypeError
  • 0:06 GET /v1/session — 200
0:00 Replay 0:09
Schematic of the replay view — an illustration, not a recording.
The difference

Your agent opens the recording. Not you.

Espejo runs an MCP server in the same process as the API. Claude Code, claude.ai or any MCP client connects to POST /mcp over OAuth 2.1 with PKCE; a person approves it on a consent screen and picks which projects it may see; and from then on the agent has five read-only tools. It can find the session that failed and read what happened inside it without anyone opening a console.

Connect an agent

$ claude mcp add --transport http espejo https://app.espejo.dev/mcp

Read-only, by policy and not by habit

All five sit behind a single mcp:read scope. A tool with no entry in the policy table does not run, so adding a handler is never enough to expose one — the authority has to be declared too, in another file, on purpose.

No machine key gets in

The platform admin key sees every account, so it is refused at the MCP door. Every connection carries an account, a consent and a scope behind it.

Revocable from the console

Each connection is a grant you can see and revoke, with rotating refresh tokens and RFC 7009 revocation. Take the consent away and the agent is out.

How the MCP server works →

The five tools

  • list_recordings

    The recordings this connection can see, newest first. Filter by project, by date range, by free text, or only the ones with errors.

  • get_recording

    Everything Espejo knows about one: when it ran, how long, on what page and release, its error counts, and what it stored.

  • recording_events

    What happened inside. The summary first — requests, 4xx/5xx with the click that caused them, console errors — then network, console, interactions and navigations.

  • recording_frames

    Signed, short-lived links to what can be watched: the screen video, the rrweb DOM replay. Espejo URLs, never bucket URLs, and they expire.

  • recording_transcript

    The stored narration transcript of a recording, when it has one. It never starts a transcription — Espejo does not produce them yet.

The install is a script tag

Two bundles exist. Pick one — that is the whole integration.

By default nothing is uploaded. The script keeps a capped, in-memory ring buffer and sends that slice only when report() is called. data-mode="always" uploads every session — it exists, but it is not the default. That is the difference between something you can leave running in production and something that burns your bucket in a week.

network (with bodies), console, clicks, navigation — 6 KB gzip

<script src="https://app.espejo.dev/sdk/espejo.js"
        data-key="pk_live_..."></script>

everything above, plus DOM replay via rrweb — 86 KB gzip

<script src="https://app.espejo.dev/sdk/espejo.dom.js"
        data-key="pk_live_..."></script>

What lands in the recording

All of it captured from inside the page, so there is nothing to install and nothing to approve.

Network, with the bodies

fetch and XHR are patched from inside the page, so request and response bodies are actually there. No debugger banner, no extension, no half a log.

Console, clicks and navigation

Every log and error, every interaction described — what was clicked, never what was typed — and every URL change, on the same clock as everything else.

DOM replay or screen video

rrweb reconstructs the page exactly as it was. Or record the real screen, with the microphone, from a click handler — a user gesture is required, so it is a button in your UI.

Multi-tenant from the first table

Accounts, projects, and a public key per project that goes straight into your HTML, the way a Sentry DSN does. The bucket path is always built by the server, never proposed by the browser.

Our bucket, or yours

The default is our bucket, so a project starts in one click. Pointing it at your own bucket with your own credential is a setting, for whoever needs recordings never to leave their account. R2 today, S3 next.

A report button, already there

A floating “report a problem” button mounts itself unless you turn it off with data-button="off". Or call report() from your own UI and get the session URL back.

What never reaches the bucket

A recording ends up in object storage and in the context window of a model. A secret that leaks here is not something you delete later — so the redactor runs in the browser, before anything is sent.

  • Auth headers

    Authorization, Cookie, Set-Cookie, x-api-key and proxy-authorization — plus any header whose name carries “token” or “secret”, which catches the ones nobody listed.

  • Secrets inside URLs

    Query and fragment values named like a token, password, signature, code, key or credential; user:pass@ in the host; and anything JWT-shaped, recognised by its form even sitting unnamed in the middle of a path.

  • Whatever people type

    An input records that it changed, never to what. In the DOM replay every input is masked, not just the password ones — a card number in a plain text field is not less private for not being a password.

  • Whole regions, when you say so

    data-espejo-block leaves a hole in the replay and silences the clicks inside it. data-espejo-mask keeps the shape and hides the text.

Redaction runs before truncation, on purpose: a JSON cut in half no longer parses, and a redactor that cannot parse cannot redact.

Pricing

One free plan with the whole product in it, and caps instead of a trial clock.

Team

Coming soon

Higher caps, your own bucket, more people in the account.

  • Your own R2 or S3 bucket
  • Raised caps and retention
  • More seats per account
Read the docs

Not priced yet. We would rather say nothing than put a number here we have not decided.

Put it on a page and break something

Create a project, paste the script tag, reproduce the bug. Then ask your agent what happened.