AI-Assisted Debugging with MCP

Overview

Undo for Java includes an MCP (Model Context Protocol) server that enables AI assistants to programmatically debug Java recordings. Instead of manually stepping through code in IntelliJ, you can ask an AI assistant such as Claude to investigate a recording, diagnose root causes, and report findings in natural language.

The AI assistant connects to the MCP server, which provides a full set of debugging tools: breakpoints, watchpoints, forward and reverse execution, variable inspection, time travel navigation, and more. The assistant can use these tools to autonomously explore a recording, following the same debugging strategies a human developer would - but faster and without requiring you to drive each step.

This is particularly powerful for:

  • Root cause analysis: Ask the AI to find the cause of an exception, working backwards through the execution history automatically.

  • Batch analysis: Ask the AI to collect data across an entire recording, such as profiling method calls or tracking field modifications.

  • Exploratory debugging: Ask the AI to investigate a symptom when you’re not sure where to start.

  • Native and Java mixed debugging: The MCP server provides access to both Java-level and native (C/C++) debugging tools, allowing the AI to investigate issues that cross the Java/native boundary.

Getting Started

The MCP server is provided as lr4j_mcp in the LR4J-Record-*.zip archive.

Launching the MCP server

$LR4J_HOME/bin/lr4j_mcp --input /path/to/recording.undo --key /path/to/keyfile

The server communicates over stdin/stdout using the Model Context Protocol (JSON-RPC). It is designed to be launched by an AI assistant’s MCP client, not invoked directly.

Options:

-i, –input <filename>

The recording file to debug.

-k, –key <keyfile>

Path to the license key file.

-p, –port <nnn>

Port for the internal JDWP connection (default: 9000).

-cp <classpath>

Additional classpath, if needed for class resolution.

-Xmx<size>

JVM heap size (default: 1g). Increase if you encounter OutOfMemoryError.

Configuring Claude Code

To use the MCP server with Claude Code, add it to your project’s .claude/commands/ directory or configure it as an MCP server in your Claude Code settings. Claude Code will automatically discover the available debugging tools and use them to investigate recordings.

A typical Claude Code MCP server configuration:

{
  "mcpServers": {
    "undo-java": {
      "command": "/absolute/path/to/lr4j/bin/lr4j_mcp",
      "args": ["--input", "/path/to/recording.undo", "--key", "/path/to/keyfile"]
    }
  }
}

Once configured, you can ask Claude natural-language questions about the recording and it will use the MCP tools to investigate.

Note

AI assistants that support MCP can automatically discover the available tools and their descriptions. You do not need to teach the assistant which tools to use; it will select the appropriate tools based on your request.

MCP Tools Reference

The MCP server exposes the following tools, organized by category. Each tool is callable by an MCP-compatible AI assistant.

Execution Control

Tool

Description

continue

Continue execution forward until a breakpoint, watchpoint, exception breakpoint, or end of recording is reached. Must be followed by wait_for_stop.

reverse_continue

Continue execution backward until a breakpoint, watchpoint, exception breakpoint, or start of recording is reached. Must be followed by wait_for_stop.

step_into

Step into the next line of code. Optionally accepts class patterns to exclude (e.g. java.*, sun.*).

step_over

Step over the current line.

step_out

Step out of the current method.

reverse_step_into

Step into the previous line in reverse. Optionally accepts class patterns to exclude.

reverse_step_over

Step over the current line in reverse.

reverse_step_out

Step out of the current method in reverse.

wait_for_stop

Wait for the debuggee to stop after a continue or reverse continue operation.

Breakpoints, Watchpoints, and Exception Breakpoints

Tool

Description

set_breakpoint

Set a breakpoint at a class and line number, or a class and method name. Optionally accepts a method signature for overloaded methods.

set_watchpoint

Set a watchpoint on a field. Triggers when the field value changes. Particularly useful in reverse execution to find what modified a field.

set_exception_breakpoint

Set a breakpoint that triggers when a specific exception class is thrown, or for all exceptions.

list_breakpoints

List all active breakpoints, watchpoints, and exception breakpoints.

clear_breakpoints

Clear all breakpoints, or a specific breakpoint by ID or location.

clear_watchpoints

Clear all watchpoints, or a specific watchpoint by ID or location.

clear_exception_breakpoints

Clear all exception breakpoints, or a specific one by ID or class.

Time Travel Navigation

Tool

Description

goto_bbcount

Navigate to a specific basic block count position in the recording.

goto_start

Navigate to the beginning of the recording.

goto_end

Navigate to the end of the recording.

go_to_epoch_millis

Navigate to a specific UTC timestamp (milliseconds since epoch).

go_to_wall_clock_time

Navigate to a specific wall-clock time, with an optional offset in milliseconds.

goto_object_create

Navigate to the point where a specific object was created, accounting for GC relocations.

goto_native_bbcount

Navigate to a specific bbcount in native code. Used for low-level native debugging.

State Inspection

Tool

Description

get_current_location

Get the current execution location (class, method, line number).

get_session_state

Get the current session state: bbcount position, recording boundaries, UTC timestamps, pause state.

get_thread_stack

Get the Java stack trace for the current thread or a specific thread.

get_local_variables

Get the names and values of all local variables in a given stack frame.

get_field_value

Get the value of an instance field for a specific object.

get_static_field_value

Get the value of a static field for a specific class.

list_threads

List all Java threads with their IDs, names, and states.

read_console_output

Read the console output (stdout/stderr) from the recorded application.

get_next_undo_event

Get the next event in the Undo event log (e.g. system calls, thread switches). Supports filtering by event type and value range.

Native Debugging

These tools provide access to the native (C/C++) layer beneath the JVM, useful for debugging JNI code, JVM internals, or mixed Java/native issues.

Tool

Description

get_native_stack_trace

Get the native (C/C++) stack trace at the current position, with symbol resolution.

set_native_breakpoint

Set a breakpoint on a native function.

clear_native_breakpoints

Clear native breakpoints.

read_memory

Read raw memory from the debuggee process at a specific address.

search_process_memory

Search for a string in the debuggee’s process memory.

get_argument_registers

Get the values of CPU argument registers (x64: RDI, RSI, RDX, RCX, R8, R9; ARM64: X0-X7).

get_all_registers

Get the values of all general-purpose CPU registers.

addr_to_symbol

Look up the nearest symbol for a memory address using DWARF debug information.

Python API

The MCP server ships with a typed Python API for writing analysis scripts that run against recordings. This is the recommended approach for tasks that require iteration, batch processing, or collecting data across an entire recording.

The Python API is located in the demos/python/ directory within the replay tools archive.

Setup

import os, sys
LR4J_HOME = os.environ['LR4J_HOME']
sys.path.insert(0, f'{LR4J_HOME}/demos/python')
from undo_mcp import Session

with Session(
    mcp_path=f'{LR4J_HOME}/bin/lr4j_mcp',
    input_path='/path/to/recording.undo',
    key_path='/path/to/keyfile'
) as session:
    # Use session.breakpoints, session.execution, session.navigation,
    # session.inspection, and session.memory to interact with the recording.
    pass

The Session object provides access to five manager objects:

  • session.breakpoints - Set and clear breakpoints, watchpoints, and exception breakpoints.

  • session.execution - Continue, step, reverse continue, and wait for stop.

  • session.navigation - Navigate by bbcount, timestamp, or object creation.

  • session.inspection - Inspect variables, stack traces, threads, console output, and events.

  • session.memory - Read memory, search memory, inspect registers, and access native stacks.

Python API reference

The Session manager objects correspond to the MCP tool categories described in MCP Tools Reference. Each Python method maps to one MCP tool and accepts the same parameters. The source code in demos/python/undo_mcp/ contains detailed docstrings for every method, including parameter descriptions, return types, and usage examples.

The following table summarizes the most commonly used methods.

session.breakpoints

Method

Description

set(className, line=None, method=None, signature=None)

Set a breakpoint at a line number or method. Provide either line or method (not both). Use signature to disambiguate overloaded methods (e.g. "(Ljava/lang/String;)V").

set_exception(className)

Break when an exception of the given class is thrown.

set_watchpoint(className, fieldName)

Break when the named field is modified.

list()

List all active breakpoints, watchpoints, and exception breakpoints.

clear_all()

Clear all breakpoints.

clear_watchpoints()

Clear all watchpoints.

clear_exception_breakpoints()

Clear all exception breakpoints.

session.execution

Method

Description

continue_()

Continue forward. Follow with wait_for_stop().

reverse_continue()

Continue backward. Follow with wait_for_stop().

step_into(classPatterns=None)

Step into the next line. Pass classPatterns (e.g. ["java.*"]) to skip classes.

step_over()

Step over the current line.

step_out()

Step out of the current method.

reverse_step_into(classPatterns=None)

Step into the previous line in reverse.

reverse_step_over()

Step over the current line in reverse.

reverse_step_out()

Step out of the current method in reverse.

wait_for_stop()

Wait for execution to stop after continue_() or reverse_continue(). Returns a dict with reason (e.g. "breakpoint", "exception", "terminus"), threadId, and optional location.

session.navigation

Method

Description

goto_start() / goto_end()

Jump to the beginning or end of the recording.

goto_bbcount(bbcount)

Jump to a Java basic block count position.

goto_time(epochMillis)

Jump to a UTC timestamp (milliseconds since epoch).

goto_wall_clock_time(timestamp, offset_millis=0)

Jump to a wall-clock time string, with an optional offset.

goto_object_create(address)

Jump to where the object at the given address was created.

goto_native_bbcount(bbcount)

Jump to a native basic block count. Use with bbcount values from get_next_undo_event().

session.inspection

Method

Description

get_variables(frameId=0, threadId=None)

Get local variables for a stack frame.

get_field_value(objectId, fieldName)

Get the value of an instance field. objectId is a hex address string (e.g. "0x6317ba350").

get_static_field_value(className, fieldName)

Get the value of a static field.

get_thread_stack(threadId=None)

Get the Java stack trace for a thread.

list_threads()

List all Java threads with their IDs, names, and states.

read_console_output()

Read stdout/stderr from the recorded application.

get_session_state()

Get the current position (bbcount, timestamps, recording boundaries).

get_next_undo_event()

Get the next event in the Undo event log (system calls, thread switches, etc.).

session.memory

Method

Description

read_memory(address, length)

Read raw bytes from the debuggee process.

search_process_memory(search_string)

Search for a string in process memory.

get_argument_registers()

Get CPU argument registers (x64: RDI, RSI, RDX, RCX, R8, R9; ARM64: X0-X7).

get_all_registers()

Get all general-purpose CPU registers.

get_native_stack_trace(maxFrames=256)

Get the native (C/C++) stack trace.

set_native_breakpoint(function)

Set a breakpoint on a native C/C++ function.

clear_native_breakpoints()

Clear all native breakpoints.

addr_to_symbol(address)

Look up the nearest symbol for a memory address.

Example: Diagnosing a ConcurrentModificationException

This script demonstrates the power of reverse debugging using the ConcurrentModificationExceptionTest demo program shipped in demos/java/ in the replay tools archive. The test has a main thread iterating over an ArrayList while a MutatorThread adds and removes elements concurrently, causing a ConcurrentModificationException.

This is a notoriously difficult problem to debug with conventional tools because the exception is thrown in the victim thread (the iterator), not the culprit thread (the mutator). The script finds the exception, then travels backwards in time to discover which thread modified the collection.

First, record the demo:

java -XX:-Inline -XX:TieredStopAtLevel=1 -XX:UseAVX=2 \
    -agentpath:$LR4J_HOME/agent/lr4j_agent_<arch>.so=save_on=always,output=cme.undo \
    -cp $LR4J_HOME/demos/java \
    io.undo.test.ConcurrentModificationExceptionTest

Then run the analysis script:

import os, sys
LR4J_HOME = os.environ['LR4J_HOME']
sys.path.insert(0, f'{LR4J_HOME}/demos/python')
from undo_mcp import Session

with Session(
    mcp_path=f'{LR4J_HOME}/bin/lr4j_mcp',
    input_path='cme.undo',
    key_path='/path/to/keyfile'
) as session:
    # Step 1: Find the ConcurrentModificationException.
    session.breakpoints.set_exception(
        "java.util.ConcurrentModificationException"
    )
    session.execution.continue_()
    stop = session.execution.wait_for_stop()

    print("Exception hit!")
    stack = session.inspection.get_thread_stack()
    for frame in stack['frames']:
        print(f"  {frame['className']}.{frame['methodName']}"
              f":{frame['lineNumber']}")

    # Step 2: Set a watchpoint on the modCount field that tracks
    # structural modifications to the collection.
    session.breakpoints.clear_all()
    session.breakpoints.set_watchpoint(
        "java.util.AbstractList", "modCount"
    )

    # Step 3: Travel backwards to find who modified the collection.
    session.execution.reverse_continue()
    stop = session.execution.wait_for_stop()

    # Look up the thread name from the thread ID.
    threads = session.inspection.list_threads()
    thread_id = stop['threadId']
    thread_name = next(
        t['threadName'] for t in threads['threads']
        if t['threadId'] == thread_id
    )

    print(f"\nCollection modified by thread: {thread_name}")
    stack = session.inspection.get_thread_stack(threadId=thread_id)
    for frame in stack['frames']:
        print(f"  {frame['className']}.{frame['methodName']}"
              f":{frame['lineNumber']}")

This produces output like:

Exception hit!
  java.util.ArrayList$Itr.checkForComodification:1043
  java.util.ArrayList$Itr.next:997
  io.undo.test.ConcurrentModificationExceptionTest.main:18

Collection modified by thread: MutatorThread
  java.util.ArrayList.fastRemove:670
  java.util.ArrayList.remove:540
  io.undo.test.ConcurrentModificationExceptionTest$MutatorThread.run:62
  java.lang.Thread.run:829

The script found the exception in the main thread (the victim iterating the list), then traveled backwards in time and identified MutatorThread as the culprit - it was removing an element from the list via ArrayList.remove while the main thread was iterating.

Note

The demos/python/examples/ directory in the replay archive contains additional example scripts including a more detailed version of this analysis.

What you can ask AI to do

Here are examples of the kinds of tasks you can ask an AI assistant to perform on a recording. These are natural-language requests; the AI will select the appropriate MCP tools to carry them out.

Finding Root Causes

  • “Read the console output and find the root cause of the exception.”

  • “There’s a ConcurrentModificationException - find which thread modified the collection and show me the stack trace.”

  • “Find where this NullPointerException occurs and trace back to where the null value originated.”

  • “Why did this HTTP request fail? Go to the error and work backwards to find the cause.”

Inspecting Objects and State

  • “Find where this object (address 0x7f1234) was created.”

  • “Read the console messages and find the one that mentions the instance address, then go to where that object was created.”

  • “Show me the local variables at the point where the exception was thrown.”

Profiling and Analysis

  • “Sample stack traces at 30 points across the recording and show me which methods are consuming the most time.”

  • “Set a native breakpoint on undo_gc_finish and collect the free memory and total memory at each GC, then show me the results as a table.”

  • “Find all write system calls with length greater than 4 and show what was written.”

Cross-Service Debugging

  • “In the API gateway recording, read the console and find the errors. Then go to that timestamp in the customers-service recording and find where the call is handled.”

Native-Level Debugging

  • “Show the native stack traces at 30 points across the recording and show me which native methods are consuming the most time.”

  • “Find the precise native bbcount where a specific string was created and print the native stack there.”