MCP Replay

Record the requests your MCP server handles as LogRocket sessions to see how AI agents use your product. SDKs for TypeScript, Python, and Go.

🚧

Beta

MCP Replay is in beta. The SDKs and how MCP sessions appear in LogRocket may change before general availability. Contact [email protected] or your CSM to share feedback or get help.

Summary

If you run an MCP (Model Context Protocol) server, LogRocket can record the requests it handles as LogRocket sessions. Add one of the MCP Replay SDKs to your server to see how users' AI agents interact with your product, just like session replay shows how people use your app. This lets you:

  • See what agents do with your tools. Tool calls, resource reads, prompt gets, and listings are recorded with arguments, results, and latency data.
  • Find and fix failures. Errors thrown by your handlers show up as LogRocket issues, allowing MCP failures to be tracked and alerted on like other errors.
  • Understand who's using your server. Sessions are tied to your users and to the MCP client they used (e.g., Cursor, Claude, ChatGPT), with the same user, release, and location filters as your other sessions.

This page is about recording your MCP server. To connect AI tools to LogRocket's own MCP server and query your LogRocket data, see MCP.

What it looks like

MCP sessions are listed alongside your other LogRocket sessions. They have no visual replay. Each request your server handled appears in the session's Network tab, in the order it happened.

An MCP session in the Network tab. Each tool call is a request, with the tool's arguments in the request body and its result in the response body.

Each request shows:

  • URL: the MCP method and target, appended to your server's URL. For example, a call to a tool named search_orders appears as https://example.com/mcp/tools/call/search_orders. Servers that don't record their URL (e.g., stdio servers) use mcp://<your_organization_id>/<your_project_id> instead.
  • Request: the params the client sent (for a tool call, the tool's name and arguments), and the HTTP request headers with credentials redacted (see Data and privacy).
  • Response: the result your server returned, under value. If the handler threw an error, the response contains its errorMessage and, for JSON-RPC protocol errors, its errorCode.
  • Duration: how long your handler took.
  • Status: an HTTP-style status that summarizes the outcome:
StatusMeaning
200The request succeeded.
400The handler threw a JSON-RPC protocol error for a bad request (parse error, invalid request, or invalid params).
404The handler threw a JSON-RPC "method not found" error.
500The handler threw any other error, or returned a result with isError: true (e.g., a failed tool call).

Errors your handlers throw (or return, in Go) are also recorded as LogRocket issues. Tool results with isError: true are how MCP reports expected failures (e.g., a tool given bad input), so they're shown with a 500 status but don't create issues.

In place of a browser, each session records the MCP client's User-Agent header, or its MCP client name and version (e.g., Cursor/1.2.0) if it didn't send one. Location is derived from the client's IP address when your server is reachable over HTTP.

Galileo can analyze MCP sessions too, using the recorded requests in place of video. For example, you can ask the LogRocket MCP server to watch the MCP sessions of a user who reported a problem.

Supported SDKs

LanguagePackageSupported MCP SDKs
TypeScript/JavaScript@logrocket/mcp-replay@modelcontextprotocol/server 2.x, on any JS runtime with fetch (Node, Cloudflare Workers, Bun, Deno, etc.)
Pythonlogrocket-mcp-replaymcp 1.x (FastMCP, low-level Server) and 2.x (MCPServer, low-level Server), and fastmcp 2.x through 4.x. Python 3.10+
Gogithub.com/LogRocket/logrocket-mcp-replay-gogithub.com/modelcontextprotocol/go-sdk 1.x. Go 1.25+

Every SDK works over any MCP transport (stdio, Streamable HTTP, and SSE).

Setup

Before you start

Create an API key in the LogRocket dashboard under Settings > API Keys, in the project you want your MCP sessions recorded to. The API key both authenticates uploads and determines which project sessions are recorded to, so there's no separate app ID to configure. Every example below reads the key from a LOGROCKET_INGEST_KEY environment variable.

TypeScript/JavaScript

Install the SDK:

npm install @logrocket/mcp-replay

Then call instrument on your server:

import { instrument } from '@logrocket/mcp-replay';

const server = buildMyMcpServer();

instrument(server, {
  apiKey: process.env.LOGROCKET_INGEST_KEY,
  getUser: ctx => {
    const sub = ctx.http?.authInfo?.extra?.sub;
    return sub ? { id: String(sub) } : undefined;
  },
});

instrument wraps every request handler on the server, including handlers registered after it's called.

Cloudflare Workers

Pass waitUntil so uploads complete after the response is returned:

export default {
  async fetch(request, env, ctx) {
    const createServer = () =>
      instrument(buildMyMcpServer(), {
        apiKey: env.LOGROCKET_INGEST_KEY,
        waitUntil: ctx.waitUntil.bind(ctx),
      });

    return createMcpHandler(createServer)(request, env, ctx);
  },
};

Short-lived processes

On platforms that freeze the process as soon as a response is sent and don't provide waitUntil (e.g., AWS Lambda), create a recorder, pass it to instrument, and flush it before returning:

import { createRecorder, instrument } from '@logrocket/mcp-replay';

const recorder = createRecorder({ apiKey: process.env.LOGROCKET_INGEST_KEY });
instrument(server, { apiKey: process.env.LOGROCKET_INGEST_KEY, recorder });

export const handler = async event => {
  try {
    return await runServer(event);
  } finally {
    await recorder.flush();
  }
};

Python

Install the SDK:

pip install logrocket-mcp-replay
# or: uv add logrocket-mcp-replay

Then call instrument on your server:

import os

from logrocket_mcp_replay import instrument

server = build_my_mcp_server()

instrument(
    server,
    api_key=os.environ["LOGROCKET_INGEST_KEY"],
    get_user=lambda ctx: {"id": ctx.request.headers["x-user-id"]} if ctx.request else None,
)

instrument records every request the server handles, including handlers registered after it's called, and uploads events from a background thread. Options can be passed as keyword arguments or as an InstrumentOptions instance.

Short-lived processes

Buffered events are uploaded when the buffer fills, on a timer, and when the interpreter exits. On platforms that freeze the process as soon as a response is sent (e.g., AWS Lambda), create a recorder, pass it to instrument, and shut it down before returning:

import os

from logrocket_mcp_replay import InstrumentOptions, RecorderOptions, create_recorder, instrument

recorder = create_recorder(RecorderOptions(api_key=os.environ["LOGROCKET_INGEST_KEY"]))
instrument(server, InstrumentOptions(api_key=os.environ["LOGROCKET_INGEST_KEY"], recorder=recorder))


def handler(event, context):
    try:
        return run_server(event)
    finally:
        recorder.shutdown()

Go

Install the SDK:

go get github.com/LogRocket/logrocket-mcp-replay-go

Then call Instrument on your server, and shut the recorder down before your process exits to upload the final batch:

import (
	"context"
	"os"

	mcpreplay "github.com/LogRocket/logrocket-mcp-replay-go"
	"github.com/modelcontextprotocol/go-sdk/mcp"
)

server := buildMyMcpServer()

recorder := mcpreplay.Instrument(server, mcpreplay.Options{
	APIKey: os.Getenv("LOGROCKET_INGEST_KEY"),
	GetUser: func(ctx context.Context, req mcp.Request) *mcpreplay.RecordedUser {
		if extra := req.GetExtra(); extra != nil && extra.TokenInfo != nil {
			return &mcpreplay.RecordedUser{ID: extra.TokenInfo.UserID}
		}
		return nil
	},
})
defer recorder.Shutdown(context.Background())

Instrument installs middleware that records every request the server handles, including tools and resources added after it's called, and uploads events from a background goroutine.

HTTP transports

The Go MCP SDK only exposes request headers to middleware. To also record your server's URL and the client's IP address (used when there's no proxy header like X-Forwarded-For), wrap your HTTP handler with HTTPMiddleware:

handler := mcp.NewStreamableHTTPHandler(func(*http.Request) *mcp.Server { return server }, nil)
http.Handle("/mcp", mcpreplay.HTTPMiddleware(handler))

If you create a server per request, instrument each one. Instrument shares a single recorder per API key, or you can pass your own with the Recorder option.

Serverless

On platforms that freeze the process as soon as a response is sent (e.g., AWS Lambda), create the recorder once at init so warm invocations reuse it, and call recorder.Shutdown(ctx) before returning from each invocation. Shutdown is safe to call repeatedly, and the recorder keeps working afterwards.

Other MCP frameworks

Instrument requires the official Go MCP SDK. For servers built on something else (e.g., a custom JSON-RPC dispatcher), create a recorder with NewRecorder, build a RequestEvent for each request, and pass it to recorder.Enqueue:

recorder := mcpreplay.NewRecorder(mcpreplay.Options{APIKey: os.Getenv("LOGROCKET_INGEST_KEY")})

recorder.Enqueue(&mcpreplay.RequestEvent{
	StartTime:  start.UnixMilli(),
	DurationMs: time.Since(start).Milliseconds(),
	Method:     "tools/call",
	Target:     toolName,
	Params:     params,
	Result:     &mcpreplay.RecordedResult{Value: result},
	User:       &mcpreplay.RecordedUser{ID: userID},
})

Enqueue applies the recorder's capture, redaction, and SanitizeEvent options, and drops events without a Method or StartTime. You're responsible for filling in anything else you want recorded, like client info, headers, and trace context.

Sessions and identity

LogRocket groups requests into sessions the way it does for web or mobile sessions: one person using one MCP client is one session, which ends after 30 minutes of inactivity. Sessions are also capped at 4 hours. Requests are grouped by MCP client (name and version) and by the best identity signal available:

  1. The identified user, when you provide getUser.
  2. A SHA-256 hash of the bearer token, for OAuth-protected servers. The token itself is never sent to LogRocket.
  3. The client's IP address and user agent, for unauthenticated HTTP servers.
  4. A random per-process ID, for transports with no HTTP request (e.g., stdio), where one server process serves one user.

Identifying users

We recommend providing getUser (get_user in Python, GetUser in Go). It ties sessions and user analytics to your authenticated user (e.g., an OAuth subject or API key owner) instead of a token or network fingerprint, and keeps a user's requests in one session if their token is refreshed. Return an object with a required id and, optionally, name, email, and traits, just like the web SDK's identify method. Return nothing to fall back to the next signal. Errors thrown by getUser are reported to onError and don't affect the request.

Here's what getUser receives in each SDK:

  • TypeScript: the MCP SDK's request context. On HTTP transports, ctx.http?.authInfo holds the validated OAuth token info.
  • Python: the MCP SDK's request context (ServerRequestContext in mcp 2.x, RequestContext in 1.x). On HTTP transports, ctx.request is the Starlette Request (it's None on stdio), so you can read headers, the validated OAuth token (ctx.request.scope["user"].access_token), or anything your auth middleware attached.
  • Go: the request context.Context and the mcp.Request. On HTTP transports, req.GetExtra() holds the request headers and, if you use the SDK's auth.RequireBearerToken, the validated TokenInfo.

Splitting sessions by conversation

If your app has its own notion of a session, like a conversation or chat ID, return it from getSessionID (get_session_id in Python, GetSessionID in Go). Requests that would otherwise share a session are then split by that ID, so each conversation gets its own LogRocket session. A session ID never merges requests that the signals above would keep separate, so provide getUser too. Without it, a conversation splits into two sessions if the client's bearer token changes partway through. Sessions still end after 30 minutes of inactivity.

Trace context

W3C trace context (traceparent, tracestate, and baggage in a request's _meta) is recorded on every request and shown with the request's headers, so you can correlate MCP requests with your tracing backend. It isn't used to group sessions.

Options

Option names follow each language's conventions. Defaults are the same across SDKs.

TypeScriptPythonGoDescription
apiKey (required)api_key (required)APIKey (required)LogRocket API key. Determines the project sessions are recorded to.
releasereleaseReleaseYour server's release (e.g., a version or git SHA), for release filtering and comparison.
getUserget_userGetUserResolve the LogRocket user from a request. See Identifying users.
getSessionIDget_session_idGetSessionIDResolve an app-defined session ID. See Splitting sessions by conversation.
captureParamscapture_paramsDisableParamCaptureRecord request params. On by default.
captureResultscapture_resultsDisableResultCaptureRecord request results. On by default.
captureHeaderscapture_headersDisableHeaderCaptureRecord HTTP request headers. On by default. Credentials are always redacted.
redactHeadersredact_headersRedactHeadersAdditional header names to redact.
maxValueLengthmax_value_lengthMaxValueLengthMaximum JSON-serialized length of recorded params and results. Defaults to 100,000.
sanitizeEventsanitize_eventSanitizeEventChange or drop events before upload. See Data and privacy.
recorderrecorderRecorderShare one recorder across instrument calls.
waitUntilKeep the runtime alive until uploads finish (Cloudflare Workers and similar runtimes).
maxBatchSizemax_batch_sizeMaxBatchSizeUpload when the buffer reaches this many events. Defaults to 25.
flushIntervalMsflush_interval_msFlushIntervalUpload buffered events on this interval. Defaults to 5 seconds.
uploadTimeoutMsupload_timeout_msUploadTimeoutAbort an upload after this long. Defaults to 10 seconds.
HTTPClientThe *http.Client used for uploads. Defaults to http.DefaultClient.
onErroron_errorOnErrorCalled with upload and serialization errors. These never propagate to your server.

Data and privacy

  • Params and results are recorded by default, truncated to 100,000 characters each. Use maxValueLength to change the limit, or turn off captureParams or captureResults to skip them entirely.
  • HTTP request headers are recorded by default. Credential headers (Authorization, Proxy-Authorization, Cookie, Set-Cookie, X-Api-Key, Api-Key, and X-Auth-Token) are always replaced with [REDACTED], and this can't be turned off. Use redactHeaders to redact more headers, or turn off captureHeaders to skip headers entirely.
  • Bearer tokens are never sent to LogRocket. Only a SHA-256 hash is sent, which is used to group a user's requests into a session.

To keep sensitive data out of LogRocket, use sanitizeEvent to remove fields from an event or drop it entirely. These examples remove the params of every call to a create_payment tool:

instrument(server, {
  apiKey: process.env.LOGROCKET_INGEST_KEY,
  sanitizeEvent: event => {
    if (event.method === 'tools/call' && event.target === 'create_payment') {
      return { ...event, params: undefined };
    }
    return event; // or return null to drop the event
  },
});
def sanitize(event):
    if event["method"] == "tools/call" and event.get("target") == "create_payment":
        return {**event, "params": None}
    return event  # or return None to drop the event


instrument(server, api_key=os.environ["LOGROCKET_INGEST_KEY"], sanitize_event=sanitize)
mcpreplay.Instrument(server, mcpreplay.Options{
	APIKey: os.Getenv("LOGROCKET_INGEST_KEY"),
	SanitizeEvent: func(event *mcpreplay.RequestEvent) *mcpreplay.RequestEvent {
		if event.Method == "tools/call" && event.Target == "create_payment" {
			event.Params = nil
		}
		return event // or return nil to drop the event
	},
})

Check that it's working

  1. Start your instrumented server and connect to it from an MCP client.
  2. Call a tool.
  3. Open your project in LogRocket. A new session for your MCP client should appear in the session list within a few minutes, with each request in its Network tab.

If no session appears, pass an onError callback that logs errors. Uploads fail silently otherwise. On short-lived platforms, also make sure you flush or shut down the recorder before each invocation returns (see the setup section for your language).