fgforrest/mcp-jdwp-java/java-debug
Claude Code and agent skill by fgforrest/mcp-jdwp-java
- community
Checking that the SKILL.md file is still in the repository.
Debug a live Java application via the jdwp-inspector MCP server — breakpoints, runtime state, expression eval, variable mutation, exception-throw-site catches, non-intrusive line/exception logpoints, and field watchpoints (suspend or log on every read/write of a specific field).
Install
npx vskill@latest install fgforrest/mcp-jdwp-java/java-debugSKILL.md
Java Debug
Live debugging of a running JVM via JDWP. Replaces "add a println, re-run, repeat" with: set breakpoint -> hit it -> inspect everything -> mutate state -> resume.
Use when:
- A test fails and the assertion message doesn't tell you why
- An exception is buried under several layers of wrapping
- A value is wrong but you can't tell where it changes
- A bug only happens under specific conditions (race, off-by-one, edge case)
- Stepping through code in your head doesn't match what the runtime does
Don't use when:
- The bug is clear from code review alone
- You already know the fix and just need to write it
- It's a build/compile failure (not a runtime bug)
Prerequisites
The target JVM must be running with the JDWP agent. The port is whatever the developer (or their deployment) chose — port 5005 is only the convention for build-system test shortcuts. Long-running services very often expose JDWP on a different port (8003, 8000, 9009, …).
suspend=y blocks the JVM at startup until you attach — use for tests and early-startup bugs. suspend=n lets the JVM run freely — use for long-running services where you attach on demand.
Two attach scenarios:
- Launch-and-debug (tests, reproducers): start a fresh JVM yourself, usually via one of the quick-launch shortcuts below.
- Attach-to-running (services already up): skip the launch step entirely — go straight to attach. The port follows the resolution priority in the next section.
Quick launch shortcuts (these all default to port 5005):
- Maven Surefire:
mvn test -Dtest=<TestClass> -Dmaven.surefire.debug - Gradle:
./gradlew test --tests "com.example.MyTest" --debug-jvm - Standalone:
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 -jar app.jar
For build-system-specific gotchas (Surefire <argLine> overrides, Gradle maxParallelForks, bootRun) and the already-running-service workflow: see references/prerequisites.md.
Attach: port resolution
Follow this priority — do not skip steps:
- User-specified port — given in the conversation, or as the
/java-debug $portargument — use it directly:jdwp_wait_for_attach(port=<that-port>). - No port given — try the default once:
jdwp_wait_for_attach()(=localhost:5005). - Default failed / nothing listening on 5005 — call
jdwp_diagnose(). It returns the list of local JVMs with their JDWP ports. Pick the right one (ask the user if more than one is plausible) and retryjdwp_wait_for_attach(port=<discovered>).
Never silently fall back to 5005 if the user specified a port — that's a bug, not a default.
Core Workflow
Every debug session follows this sequence:
- Launch the target in a separate shell with
suspend=y— or skip this step if the JVM is already running with JDWP open. The JVM blocks until step 2. - Attach following the port-resolution priority above.
jdwp_wait_for_attachpolls until the JVM is listening, then attaches. - Set breakpoints at suspected bug locations:
jdwp_set_breakpoint(className, lineNumber). Add exception breakpoints or logpoints as needed. - Resume and wait:
jdwp_resume_until_event()— releases the JVM and BLOCKS until the next BP/step/exception fires (30s default). Returns the suspended thread info. - Inspect in one call:
jdwp_get_breakpoint_context()— returns thread, top frames, locals (incl.this), andthisfield dump. - Form a hypothesis, test it: step through,
jdwp_assert_expression(...)to check invariants,jdwp_set_local/jdwp_set_fieldto mutate state and ask "would the test pass if X were Y?" - Resume to next event (
jdwp_resume_until_event) or disconnect when done. For sequential scenarios against the same target, usejdwp_resetbetween flights to clear state without dropping the connection.
On `[TIMEOUT]`: the response includes a structured diagnostic — read it. If your breakpoints are PENDING (target class not loaded), the code path is not executing and a larger timeout will not help — verify the entry point or class name. If a pending BP shows [FAILED], the line/class is invalid. If recent events show LOGPOINT or BREAKPOINT_SUPPRESSED hits, your BP is firing but auto-resuming (logpoint or false condition). Do not blindly retry with a bigger timeout. Call jdwp_diagnose() any time for the same snapshot without resuming — useful as a sanity check before waiting on a long path.
For follow-up investigations against the same target: jdwp_reset + new breakpoints, no need to reconnect.
Debugging Recipes
"The function shouldn't have changed that"
A value looks correct before a method call and wrong after. The method appears to only do reads.
- BP at the call site, before the suspicious call.
jdwp_evaluate_expressionon the value -> correct.jdwp_step_overfollowed byjdwp_resume_until_event(the step resumes the thread; the latch fires when the STEP event lands).jdwp_evaluate_expressionagain -> wrong! The call mutates.- Restart, BP at the same site,
jdwp_step_intoto land inside the suspicious method, then a small number ofjdwp_step_over+jdwp_resume_until_eventcycles, eval after each — find the exact mutation point. If you find yourself stepping more than ~3 lines, set a breakpoint at the suspect line and resume to it instead.
"Race / partial init / observable intermediate state"
A field has a value at one read site that doesn't match what was written, or a thread reads a half-built object.
- BP at the read site (where the wrong value is observed).
jdwp_get_locals-> find the broken object's ID.jdwp_get_fields(<id>)-> see the partially-initialized state.jdwp_set_field(<id>, "timeout", "5000")to fix it at runtime.jdwp_resume_until_event-> if the test passes now, the root cause is confirmed.- Find and fix the actual write order in the source.
"Everything's stuck — is it a deadlock?"
Threads hang, the test never completes, or jdwp_get_threads shows several threads in MONITOR status and you need to know what they're each waiting on.
jdwp_dump_locks()— one call takes a transient VM-wide snapshot and prints which threads are blocked on a monitor, who holds each one, and any deadlock cycle (e.g.transfer-A-to-B → transfer-B-to-A → transfer-A-to-B). The suspend/resume is balanced, so a genuine deadlock stays put and a non-deadlocked VM is undisturbed.- If a cycle is reported,
jdwp_suspend_thread(id)a member, thenjdwp_get_stack(id)to see the exact lock-acquisition line (the two members usually contend the same line in mirror order — the AB-BA signature). - No cycle but threads blocked? The hang is a lock held by a runnable thread that's slow/looping, or — if
jdwp_dump_locksshows nothing — the threads are parked inObject.wait()or ajava.util.concurrentLock, which monitor dumps don't cover. Fall back tojdwp_get_threadsforWAIT-status threads.
"Exception is buried under wrappers"
Test shows CompletionException("Async task failed"), but the real cause is 3 frames deeper.
jdwp_set_exception_breakpoint("java.lang.IllegalStateException", caught=true, uncaught=false).- If it returns "deferred" (class not yet loaded), also set a regular line BP somewhere upstream of the throw. Both BPs must be in place before resuming.
jdwp_resume_until_event.- The line BP hits -> call any inspection tool (e.g.
jdwp_get_locals) -> this triggers class loading -> exception BP self-promotes from[PENDING]to active. jdwp_resume_until_eventpast the line BP.- The exception BP catches the throw ->
jdwp_get_stackshows the real root frame, not the wrapper.
"Trace exceptions without stopping the app"
A long-running service throws something occasionally and you want to see when/where without halting traffic.
jdwp_set_exception_logpoint("java.sql.SQLException", expression="$exception.getSQLState() + \\\": \\\" + $exception.getMessage()")—$exceptionis bound to the thrown object; the listener auto-resumes after recording.- Let the service run. Each throw produces an
EXCEPTION_LOGentry (orEXCEPTION_LOG_ERRORif the expression fails). jdwp_get_events(50)to inspect throw locations + evaluated expression results in chronological order.- For a pure suspending exception BP without expression evaluation, use
jdwp_set_exception_breakpointinstead — that tool no longer carries log-only flags.
"Who is overwriting this field?"
A field has the wrong value at read time and you can't tell which of many code paths wrote it.
jdwp_set_field_breakpoint(className="com.example.OrderState", fieldName="status", mode="modification")— suspends on every write of the field. Conditions and the$oldValue/$newValue/$object/$fieldName/$modebindings narrow the catch.jdwp_resume_until_event— the next write to the field suspends the thread at the write site.jdwp_get_stack— the caller frame is the culprit.- Want to know the value AND keep going? Use
jdwp_set_field_logpoint(..., expression="$oldValue + \" -> \" + $newValue")instead — every write records aFIELD_LOGPOINTevent with the transition, no suspends.jdwp_get_events(50)shows the full history. - Need only one instance? Pass
objectFilterId=<instance-id from jdwp_get_locals or jdwp_get_fields>to filter to that one object. PassthreadFilterId=<thread uniqueID>to restrict by thread. - Drowning in constructor-storm writes before the interesting mutation? Pass
excludeConstructors=true— writes inside the declaring class's<init>/<clinit>are silently dropped (no event, no chain trigger, no suspend), so the BP only fires on post-construction mutations. Use when a field is set by many constructors and you only care about later changes.
"Field is mutated during <clinit> but my BP misses the first write"
Static initializers run the moment the class loads. A line BP fires on the first event after load, but a static-init write inside the class itself happens before the class is fully visible.
jdwp_set_field_breakpoint(className="com.example.Config", fieldName="DEFAULTS", mode="modification")— even when the class hasn't loaded yet, the watchpoint is registered as PENDING.- On class load, the watchpoint promotes synchronously inside the ClassPrepareEvent — before the loading thread runs
<clinit>. The first static-init write is caught. jdwp_get_stackshows whether the write came from<clinit>(static initializer) or a normal call site.
"Object inside a HashMap is no longer findable"
map.put(k, v) then map.get(k) -> null even though k looks identical.
- BP before AND after the suspected mutation point.
- At BP1:
jdwp_evaluate_expression("session.hashCode()")— remember the value. jdwp_resume_until_eventto advance to BP2.- At BP2:
jdwp_assert_expression("session.hashCode()", "<value-from-step-2>")—MISMATCHconfirms the hash drifted. - Fix: use immutable keys, or
remove+ re-insert around the mutation.
"Bug only at large input / specific value"
Test fails at one input, passes at another. Stepping through every iteration is impractical.
Approach A — conditional breakpoint: jdwp_set_breakpoint("MyClass", 42, "all", condition="i > 100 && items.size() > 50")
Approach B — logpoint then conditional:
jdwp_set_logpoint("MyClass", 42, "\"i=\" + i + \" v=\" + value")- Run the test uninterrupted.
jdwp_get_events(50)-> find the FIRST iteration where the value goes bad.- Set a conditional BP for that exact iteration.
Approach C — conditional logpoint (best of both): jdwp_set_logpoint("MyClass", 42, "\"i=\" + i + \" v=\" + value", condition="value < 0") — logs only when the suspicious shape appears.
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 100 of 100Pattern checks on the skill's files.
- Model reviewNot runA model review is scheduled for the next rescan.
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/java-debug/SKILL.md
Popularity
- 7-day trend score
- 0
Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/fgforrest/mcp-jdwp-java/java-debug)