This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
# Run from the reactor root. Builds both modules.
# Prefer ./mvnw (the bundled Maven Wrapper) — it pins the Maven version and
# does not require Maven to be installed system-wide. Plain `mvn` works too
# if it is on PATH.
./mvnw compile # Compile only
./mvnw clean package -DskipTests # Full build (produces jdwp-mcp-server/target/mcp-jdwp-java.jar)
# Build only the MCP server (skips the sandbox module entirely):
./mvnw -pl jdwp-mcp-server -am clean package -DskipTestsThe repository is a 2-module Maven reactor:
jdwp-mcp-server/— the real MCP server. Contains all genuine unit tests;./mvnw -pl jdwp-mcp-server testruns them.jdwp-sandbox/— deliberately broken Java classes used as JDWP debugging targets ("test flights"). Sandbox tests are expected-to-fail by design and are skipped by default so a normal./mvnw teststays green. Run them explicitly during a test flight with:./mvnw -pl jdwp-sandbox test -DskipTests=false.
The compiler requires --add-modules jdk.jdi (configured in jdwp-mcp-server/pom.xml). At runtime, the JAR must also be launched with --add-modules jdk.jdi.
This is a Spring Boot MCP Server that lets Claude Code inspect and control running Java applications via JDWP (Java Debug Wire Protocol). It connects directly to a target JVM's JDWP port (default 5005).
Claude Code ──MCP/STDIO──> Spring Boot MCP Server ──JDI──> Target JVM (port 5005)
- JDWPTools — Exposes ~30 MCP tools via
@McpToolannotations. This is the main interface: connection, thread/stack/variable inspection, breakpoints, stepping, and watcher evaluation. All tool methods return formattedStringresponses. - JDIConnectionService — Singleton holding the persistent
VirtualMachine(JDI) connection. Manages object caching (ConcurrentHashMap<Long, ObjectReference>) and smart collection rendering (ArrayList, HashMap, HashSet internals). Also drives classpath discovery for expression evaluation. - BreakpointTracker — Maintains a registry of breakpoints with synthetic integer IDs (since JDI
BreakpointRequesthas no user-facing ID). Tracks the last thread that hit a breakpoint. - JdiEventListener — Daemon thread consuming the JDI event queue. Updates
BreakpointTrackeron breakpoint/step events. Breakpoint events do NOT auto-resume — threads stay suspended for user inspection.
Evaluates arbitrary Java expressions at breakpoints by compiling and injecting bytecode into the target JVM:
- JdiExpressionEvaluator — Orchestrator. Analyzes the stack frame, generates a wrapper class with a unique UUID name, delegates compilation, and caches results.
- ClasspathDiscoverer — Walks the target JVM's classloader hierarchy (including Tomcat/container classloaders) to discover all JARs. Also uses JdkDiscoveryService to find a local JDK matching the target JVM version.
- InMemoryJavaCompiler — Compiles Java source to bytecode using Eclipse JDT (ECJ), entirely in memory.
- RemoteCodeExecutor — Injects compiled bytecode into the target JVM via
ClassLoader.defineClass()and invokes it.
- WatcherManager — CRUD for watchers, dual-indexed by watcher UUID and breakpoint ID.
- Watcher — Immutable model: id, label, breakpointId, expression.
Watchers are MCP-side only — they store expressions that get evaluated via the expression pipeline when a breakpoint is hit.
jdwp-mcp-server is @NullMarked at the package level — every type, field, parameter and return in the module is non-null by default. The small number of legitimately nullable slots are annotated with org.jspecify.annotations.Nullable. The convention is enforced at compile time by NullAway running as an Error Prone plugin in maven-compiler-plugin (see jdwp-mcp-server/pom.xml); a ./mvnw -pl jdwp-mcp-server compile that emits any [NullAway] diagnostic fails the build.
When adding new code, do not introduce un-annotated nullable slots — if a value can legitimately be null, annotate the declaration with @Nullable from org.jspecify.annotations. jdwp-sandbox is intentionally unmarked: its classes are deliberately broken debugging targets and annotations there would muddy the exercises.
MCP tool parameters: Every @McpToolParam(required = false) parameter is nullable at runtime — the MCP framework passes null when the client omits the value. These parameters are a system boundary, not internal code. Always null-guard them before dereferencing, even though @NullMarked makes them look non-null. Boxed types (Integer, Boolean, Long) will NPE on auto-unbox; String will NPE on .isBlank() or .toLowerCase(). The standard patterns are:
Integer:(param != null && param > 0) ? param : defaultValueBoolean:param != null && param(default false) orparam == null || param(default true)String:(param == null || param.isBlank()) ? "default" : paramLong(optional thread ID): null → fall back tobreakpointTracker.getLastBreakpointThread()
- MCP server type is
SYNCandweb-application-type=none— communication is JSON over STDIO, no HTTP server. JDIConnectionService.disconnect()callsvm.dispose()for clean JDWP teardown.- Object references are cached globally so users can navigate object graphs across multiple tool calls.
- Expression wrapper classes use UUID-based naming to avoid
LinkageErroracross server restarts. - The JDI event listener thread must consume all events to prevent the target JVM from blocking.