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 execution forward until a breakpoint, watchpoint, exception breakpoint, or end of
recording is reached. Must be followed by |
|
Continue execution backward until a breakpoint, watchpoint, exception breakpoint, or start
of recording is reached. Must be followed by |
|
Step into the next line of code. Optionally accepts class patterns to exclude (e.g.
|
|
Step over the current line. |
|
Step out of the current method. |
|
Step into the previous line in reverse. Optionally accepts class patterns to exclude. |
|
Step over the current line in reverse. |
|
Step out of the current method in reverse. |
|
Wait for the debuggee to stop after a continue or reverse continue operation. |
Breakpoints, Watchpoints, and Exception Breakpoints¶
Tool |
Description |
|---|---|
|
Set a breakpoint at a class and line number, or a class and method name. Optionally accepts a method signature for overloaded methods. |
|
Set a watchpoint on a field. Triggers when the field value changes. Particularly useful in reverse execution to find what modified a field. |
|
Set a breakpoint that triggers when a specific exception class is thrown, or for all exceptions. |
|
List all active breakpoints, watchpoints, and exception breakpoints. |
|
Clear all breakpoints, or a specific breakpoint by ID or location. |
|
Clear all watchpoints, or a specific watchpoint by ID or location. |
|
Clear all exception breakpoints, or a specific one by ID or class. |
State Inspection¶
Tool |
Description |
|---|---|
|
Get the current execution location (class, method, line number). |
|
Get the current session state: bbcount position, recording boundaries, UTC timestamps, pause state. |
|
Get the Java stack trace for the current thread or a specific thread. |
|
Get the names and values of all local variables in a given stack frame. |
|
Get the value of an instance field for a specific object. |
|
Get the value of a static field for a specific class. |
|
List all Java threads with their IDs, names, and states. |
|
Read the console output (stdout/stderr) from the recorded application. |
|
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 the native (C/C++) stack trace at the current position, with symbol resolution. |
|
Set a breakpoint on a native function. |
|
Clear native breakpoints. |
|
Read raw memory from the debuggee process at a specific address. |
|
Search for a string in the debuggee’s process memory. |
|
Get the values of CPU argument registers (x64: RDI, RSI, RDX, RCX, R8, R9; ARM64: X0-X7). |
|
Get the values of all general-purpose CPU registers. |
|
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 a breakpoint at a line number or method. Provide either |
|
Break when an exception of the given class is thrown. |
|
Break when the named field is modified. |
|
List all active breakpoints, watchpoints, and exception breakpoints. |
|
Clear all breakpoints. |
|
Clear all watchpoints. |
|
Clear all exception breakpoints. |
session.execution¶
Method |
Description |
|---|---|
|
Continue forward. Follow with |
|
Continue backward. Follow with |
|
Step into the next line. Pass |
|
Step over the current line. |
|
Step out of the current method. |
|
Step into the previous line in reverse. |
|
Step over the current line in reverse. |
|
Step out of the current method in reverse. |
|
Wait for execution to stop after |
session.inspection¶
Method |
Description |
|---|---|
|
Get local variables for a stack frame. |
|
Get the value of an instance field. |
|
Get the value of a static field. |
|
Get the Java stack trace for a thread. |
|
List all Java threads with their IDs, names, and states. |
|
Read stdout/stderr from the recorded application. |
|
Get the current position (bbcount, timestamps, recording boundaries). |
|
Get the next event in the Undo event log (system calls, thread switches, etc.). |
session.memory¶
Method |
Description |
|---|---|
|
Read raw bytes from the debuggee process. |
|
Search for a string in process memory. |
|
Get CPU argument registers (x64: RDI, RSI, RDX, RCX, R8, R9; ARM64: X0-X7). |
|
Get all general-purpose CPU registers. |
|
Get the native (C/C++) stack trace. |
|
Set a breakpoint on a native C/C++ function. |
|
Clear all native breakpoints. |
|
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
NullPointerExceptionoccurs 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_finishand 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.”