mcpreplay

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 19 Imported by: 0

README

LogRocket MCP Replay

Records the requests your MCP server handles as LogRocket sessions so you can see how users' agents interact with your product: which tools they call and with what arguments, which resources and prompts they fetch, what they got back, and where errors happen.

Works with MCP servers built on the official github.com/modelcontextprotocol/go-sdk (v1.x) over any transport (stdio, Streamable HTTP, SSE, in-memory).

Installation

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

Usage

import (
	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 receiving middleware that records every request the server handles - tool calls, resource reads, prompt gets, listings, and any tool or resource added afterwards - and uploads batched request events to LogRocket from a background goroutine. Errors returned by handlers will be surfaced as LogRocket issues.

Events are buffered and flushed when the buffer fills or on a timer. Go has no exit hook, so call recorder.Shutdown(ctx) before your process exits to upload the final batch. On platforms that freeze the process as soon as a response is sent (e.g., AWS Lambda), call it before returning from each invocation.

API key

Create an API key for your app in the LogRocket dashboard and pass it as APIKey. The key both authenticates uploads and determines which LogRocket app sessions are recorded to. There is no separate appID to configure.

HTTP transports

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

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 pass your own via Recorder.

Example

examples/streamable is a runnable server with tools that exercise each recorded outcome:

LOGROCKET_INGEST_KEY=pat:org:app:secret go run -C examples/streamable .

Sessions and identity

LogRocket assigns requests to sessions server-side, emulating web visits: one person using one MCP client is one session, ended by 30 minutes of inactivity. Requests are grouped per client app (name and version) by the best available identity signal:

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

Provide GetUser for the best results. It receives the request context and the mcp.Request; on HTTP transports req.GetExtra() carries the request headers and, when you use the SDK's auth.RequireBearerToken, the validated TokenInfo, so the user can be resolved from the OAuth subject, an API key owner, or whatever your auth middleware attached. RecordedUser.ID is required; a user returned without one is ignored (reported once via OnError) and the request falls through to the next signal.

W3C trace context (traceparent/baggage in _meta) is captured on every request and shown with the request's headers in the replay, so requests can be correlated with your tracing backend. It is not used for session grouping.

Options

Option Description
APIKey (required) LogRocket API key; identifies the app recorded to (see above)
ServerURL Ingest server origin override
Release Your server's release (e.g., version, git SHA) for release tracking
GetUser Resolve the LogRocket user from the request
DisableParamCapture Skip capturing request params
DisableResultCapture Skip capturing request results
DisableHeaderCapture Skip capturing HTTP request headers
RedactHeaders Additional header names to redact
MaxValueLength Max JSON-serialized length of captured values (default 100000)
SanitizeEvent Transform or drop (return nil) events before upload
Recorder Share one recorder across Instrument calls
MaxBatchSize Flush when the buffer reaches this many events (default 25)
FlushInterval Timer-based flush interval (default 5s)
UploadTimeout Abort an ingest upload after this long (default 10s)
HTTPClient *http.Client used for uploads (default http.DefaultClient)
OnError Callback for upload/serialization errors

Sanitization

HTTP request headers are captured so they appear alongside each request in the Network view. Credential-bearing headers (Authorization, Cookie, X-Api-Key, and similar) are always replaced with [REDACTED]. This can't be disabled. Use RedactHeaders to redact additional headers, or DisableHeaderCapture to skip header capture entirely.

Params and results are truncated to MaxValueLength by default. To handle requests containing sensitive data, use SanitizeEvent to redact fields or drop events entirely:

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
	},
})

Documentation

Overview

Package mcpreplay records the requests an MCP server built on github.com/modelcontextprotocol/go-sdk handles as LogRocket sessions.

Call Instrument on an *mcp.Server to record every request it handles, and Recorder.Shutdown before the process exits to upload the final batch. On HTTP transports, wrap the handler with HTTPMiddleware to also record the server URL and client peer address.

Index

Constants

View Source
const (
	SDKName    = "logrocket-mcp-replay-go"
	SDKVersion = "0.1.0"
)

SDKName and SDKVersion are reported to LogRocket with every batch. SDKVersion must match the module's release tag (without the leading v).

View Source
const TruncatedMarker = "[truncated by " + SDKName + "]"

TruncatedMarker is appended to captured values that exceeded MaxValueLength.

Variables

This section is empty.

Functions

func ClampString

func ClampString(value string, maxLength int) string

ClampString returns value cut to at most maxLength bytes.

func HTTPMiddleware

func HTTPMiddleware(next http.Handler) http.Handler

HTTPMiddleware wraps the HTTP handler serving your MCP server (e.g., an *mcp.StreamableHTTPHandler) so recorded requests include the server URL and the client's peer address. Without it, the client IP is available only from proxy headers (X-Forwarded-For and similar) and the server URL is omitted.

func MakeIngestPath

func MakeIngestPath(appID string) string

MakeIngestPath returns the ingest endpoint path for an app ID ("org/app").

func NewMiddleware

func NewMiddleware(recorder *Recorder, opts Options) mcp.Middleware

NewMiddleware returns receiving middleware that records every request to the given recorder. Instrument installs it for you; use it directly with server.AddReceivingMiddleware to control ordering relative to your own middleware.

func ParseAppID

func ParseAppID(apiKey string) string

ParseAppID extracts "org/app" from a LogRocket API key, or returns "" if the key is not in a recognized format.

func TruncateValue

func TruncateValue(value any, maxLength int) any

TruncateValue returns value unchanged if its JSON serialization fits in maxLength bytes, and a truncated string representation otherwise.

Types

type EventBatch

type EventBatch struct {
	AppID   string          `json:"appID"`
	SDK     SDKInfo         `json:"sdk"`
	Release string          `json:"release,omitempty"`
	Events  []*RequestEvent `json:"events"`
}

EventBatch is the payload uploaded to the LogRocket ingest server.

type Options

type Options struct {
	// APIKey is a LogRocket API key ("pat:org:app:secret", or the legacy
	// "org:app:secret" format). Authenticates with the ingest server and
	// identifies the app recorded to.
	APIKey string
	// ServerURL is the ingest server origin. Defaults to the production LogRocket ingest server.
	ServerURL string
	// Release is your MCP server's release identifier (e.g., a version or git
	// SHA). Shown on sessions and used by LogRocket's release filtering and comparison.
	Release string
	// DisableParamCapture skips capturing request params.
	DisableParamCapture bool
	// DisableResultCapture skips capturing request results.
	DisableResultCapture bool
	// DisableHeaderCapture skips capturing HTTP request headers. When
	// captured, sensitive values are always redacted.
	DisableHeaderCapture bool
	// RedactHeaders lists header names (case-insensitive) to redact in
	// addition to the built-in denylist.
	RedactHeaders []string
	// MaxValueLength is the maximum JSON-serialized length for captured
	// params/results. Defaults to 100000.
	MaxValueLength int
	// SanitizeEvent transforms or drops (by returning nil) an event before it is buffered.
	SanitizeEvent func(event *RequestEvent) *RequestEvent
	// MaxBatchSize flushes the buffer whenever it reaches this many events. Defaults to 25.
	MaxBatchSize int
	// FlushInterval flushes the buffer on this interval. Defaults to 5s.
	FlushInterval time.Duration
	// UploadTimeout aborts an ingest upload after this long. Defaults to 10s.
	UploadTimeout time.Duration
	// HTTPClient is used for ingest uploads. Defaults to http.DefaultClient.
	HTTPClient *http.Client
	// OnError is called with upload/serialization errors. These never
	// propagate to the host server.
	OnError func(err error)

	// Recorder shares one recorder (and its upload batching) across
	// Instrument calls. Used by Instrument only.
	Recorder *Recorder
	// GetUser resolves the LogRocket user for a request. req.GetExtra()
	// carries the HTTP headers and validated bearer token info on HTTP
	// transports (nil on stdio). Used by Instrument only.
	GetUser func(ctx context.Context, req mcp.Request) *RecordedUser
}

Options configure recording. The zero value of every field except APIKey selects the default.

type RecordedClient

type RecordedClient struct {
	Name    string `json:"name,omitempty"`
	Version string `json:"version,omitempty"`
}

RecordedClient identifies the MCP client application that sent a request.

type RecordedResult

type RecordedResult struct {
	// IsError is true when the handler returned an isError result (e.g., a failed tool call).
	IsError bool `json:"isError,omitempty"`
	// Value is the handler's result, truncated to MaxValueLength.
	Value any `json:"value,omitempty"`
}

RecordedResult is the outcome of a request that returned a result.

type RecordedUser

type RecordedUser struct {
	ID        string            `json:"id"`
	Email     string            `json:"email,omitempty"`
	Name      string            `json:"name,omitempty"`
	Anonymous bool              `json:"anonymous,omitempty"`
	Traits    map[string]string `json:"traits,omitempty"`
}

RecordedUser identifies the LogRocket user a request belongs to.

type Recorder

type Recorder struct {
	// contains filtered or unexported fields
}

Recorder buffers events and uploads them from a background goroutine so the host server's request handling is never blocked.

func Instrument

func Instrument(server *mcp.Server, opts Options) *Recorder

Instrument records every request the server handles (tool calls, resource reads, prompt gets, listings, etc.) to LogRocket, including handlers registered afterwards. Instrumenting the same server twice is a no-op.

The returned Recorder is the one events are buffered in; call its Shutdown before the process exits so the final batch is uploaded.

func NewRecorder

func NewRecorder(opts Options) *Recorder

NewRecorder creates a recorder. Recording is disabled (and OnError called) when an app ID cannot be parsed from opts.APIKey.

func (*Recorder) AppID

func (r *Recorder) AppID() string

AppID is the "org/app" parsed from the API key, or "" if recording is disabled.

func (*Recorder) Enqueue

func (r *Recorder) Enqueue(event *RequestEvent)

Enqueue buffers an event, flushing when the buffer reaches MaxBatchSize or scheduling a timer-based flush otherwise.

func (*Recorder) Flush

func (r *Recorder) Flush()

Flush queues buffered events for upload in the background and returns immediately. Use Shutdown to wait for uploads to finish.

func (*Recorder) InstanceID

func (r *Recorder) InstanceID() string

InstanceID is the random ID stamped on every event this recorder buffers.

func (*Recorder) NextSeq

func (r *Recorder) NextSeq() int64

NextSeq returns the next per-recorder event sequence number.

func (*Recorder) Shutdown

func (r *Recorder) Shutdown(ctx context.Context) error

Shutdown flushes buffered events and waits for all pending uploads to finish, or for ctx to be done, whichever comes first. Call it before your process exits (e.g., with defer in main) so the final batch is not lost.

type RequestEvent

type RequestEvent struct {
	Type string `json:"type"`
	// StartTime is epoch milliseconds when handling of the request started.
	StartTime  int64 `json:"startTime"`
	DurationMs int64 `json:"durationMs"`
	// Method is the JSON-RPC method (e.g., "tools/call", "resources/read", "initialize").
	Method string `json:"method"`
	// Target is the tool/prompt name or resource URI the request addressed, when applicable.
	Target string `json:"target,omitempty"`
	// RequestID is the JSON-RPC request ID. The Go SDK does not expose it to
	// middleware, so it is left unset.
	RequestID any `json:"requestID,omitempty"`
	// Params holds the request params (minus _meta), truncated to MaxValueLength.
	Params any             `json:"params,omitempty"`
	Result *RecordedResult `json:"result,omitempty"`
	// ErrorMessage is set when the handler returned an error rather than a result.
	ErrorMessage string `json:"errorMessage,omitempty"`
	// ErrorName is the error's Go type (e.g., "jsonrpc2.WireError"), used for issue grouping.
	ErrorName string `json:"errorName,omitempty"`
	// ErrorCode is the JSON-RPC error code carried by the returned error
	// (e.g., -32602 invalid params). Distinguishes client mistakes from server bugs.
	ErrorCode int64           `json:"errorCode,omitempty"`
	User      *RecordedUser   `json:"user,omitempty"`
	Client    *RecordedClient `json:"client,omitempty"`
	// UserAgent is the raw User-Agent header from the MCP client's HTTP request.
	UserAgent string `json:"userAgent,omitempty"`
	// Headers are the HTTP request headers, with sensitive values redacted.
	Headers map[string]string `json:"headers,omitempty"`
	// ServerURL is the URL of the MCP server endpoint that handled the request,
	// without query string. Requires HTTPMiddleware.
	ServerURL string `json:"serverUrl,omitempty"`
	// IPAddress is the client IP from standard proxy headers (or the peer
	// address when HTTPMiddleware is used), used for geo enrichment.
	IPAddress       string `json:"ipAddress,omitempty"`
	ProtocolVersion string `json:"protocolVersion,omitempty"`
	Traceparent     string `json:"traceparent,omitempty"`
	Tracestate      string `json:"tracestate,omitempty"`
	Baggage         string `json:"baggage,omitempty"`
	// OAuthClientID is the OAuth client ID from the validated access token, when available.
	OAuthClientID string `json:"oauthClientID,omitempty"`
	// AuthHash is the SHA-256 hex digest of the bearer token, when the request
	// was authenticated. Used server-side to group requests into per-user
	// sessions when GetUser is not configured. The token itself is never sent.
	AuthHash string `json:"authHash,omitempty"`
	// InstanceID is a random ID generated once per recorder (i.e., per server
	// process). Last-resort sessionization key for transports with no HTTP
	// context where one process serves one user (e.g., stdio).
	InstanceID string `json:"instanceID,omitempty"`
	Seq        int64  `json:"seq"`
}

RequestEvent is one recorded request. Field names match the LogRocket ingest schema.

type SDKInfo

type SDKInfo struct {
	Name    string `json:"name"`
	Version string `json:"version"`
}

SDKInfo identifies the SDK that produced a batch.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL