fgforrest/mcp-jdwp-java/java-debug

Claude Code and agent skill by fgforrest/mcp-jdwp-java

Trust tier 2
  • community
LLM review pending
Source check pending

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-debug

SKILL.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, …).

-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:<port>

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:

  1. Launch-and-debug (tests, reproducers): start a fresh JVM yourself, usually via one of the quick-launch shortcuts below.
  2. 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:

  1. User-specified port — given in the conversation, or as the /java-debug $port argument — use it directly: jdwp_wait_for_attach(port=<that-port>).
  2. No port given — try the default once: jdwp_wait_for_attach() (= localhost:5005).
  3. 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 retry jdwp_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:

  1. 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.
  2. Attach following the port-resolution priority above. jdwp_wait_for_attach polls until the JVM is listening, then attaches.
  3. Set breakpoints at suspected bug locations: jdwp_set_breakpoint(className, lineNumber). Add exception breakpoints or logpoints as needed.
  4. 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.
  5. Inspect in one call: jdwp_get_breakpoint_context() — returns thread, top frames, locals (incl. this), and this field dump.
  6. Form a hypothesis, test it: step through, jdwp_assert_expression(...) to check invariants, jdwp_set_local/jdwp_set_field to mutate state and ask "would the test pass if X were Y?"
  7. Resume to next event (jdwp_resume_until_event) or disconnect when done. For sequential scenarios against the same target, use jdwp_reset between 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.

  1. BP at the call site, before the suspicious call.
  2. jdwp_evaluate_expression on the value -> correct.
  3. jdwp_step_over followed by jdwp_resume_until_event (the step resumes the thread; the latch fires when the STEP event lands).
  4. jdwp_evaluate_expression again -> wrong! The call mutates.
  5. Restart, BP at the same site, jdwp_step_into to land inside the suspicious method, then a small number of jdwp_step_over + jdwp_resume_until_event cycles, 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.

  1. BP at the read site (where the wrong value is observed).
  2. jdwp_get_locals -> find the broken object's ID.
  3. jdwp_get_fields(<id>) -> see the partially-initialized state.
  4. jdwp_set_field(<id>, "timeout", "5000") to fix it at runtime.
  5. jdwp_resume_until_event -> if the test passes now, the root cause is confirmed.
  6. 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.

  1. 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.
  2. If a cycle is reported, jdwp_suspend_thread(id) a member, then jdwp_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).
  3. No cycle but threads blocked? The hang is a lock held by a runnable thread that's slow/looping, or — if jdwp_dump_locks shows nothing — the threads are parked in Object.wait() or a java.util.concurrent Lock, which monitor dumps don't cover. Fall back to jdwp_get_threads for WAIT-status threads.
"Exception is buried under wrappers"

Test shows CompletionException("Async task failed"), but the real cause is 3 frames deeper.

  1. jdwp_set_exception_breakpoint("java.lang.IllegalStateException", caught=true, uncaught=false).
  2. 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.
  3. jdwp_resume_until_event.
  4. 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.
  5. jdwp_resume_until_event past the line BP.
  6. The exception BP catches the throw -> jdwp_get_stack shows 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.

  1. jdwp_set_exception_logpoint("java.sql.SQLException", expression="$exception.getSQLState() + \\\": \\\" + $exception.getMessage()") — $exception is bound to the thrown object; the listener auto-resumes after recording.
  2. Let the service run. Each throw produces an EXCEPTION_LOG entry (or EXCEPTION_LOG_ERROR if the expression fails).
  3. jdwp_get_events(50) to inspect throw locations + evaluated expression results in chronological order.
  4. For a pure suspending exception BP without expression evaluation, use jdwp_set_exception_breakpoint instead — 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.

  1. 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 / $mode bindings narrow the catch.
  2. jdwp_resume_until_event — the next write to the field suspends the thread at the write site.
  3. jdwp_get_stack — the caller frame is the culprit.
  4. Want to know the value AND keep going? Use jdwp_set_field_logpoint(..., expression="$oldValue + \" -> \" + $newValue") instead — every write records a FIELD_LOGPOINT event with the transition, no suspends. jdwp_get_events(50) shows the full history.
  5. Need only one instance? Pass objectFilterId=<instance-id from jdwp_get_locals or jdwp_get_fields> to filter to that one object. Pass threadFilterId=<thread uniqueID> to restrict by thread.
  6. 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.

  1. 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.
  2. On class load, the watchpoint promotes synchronously inside the ClassPrepareEvent — before the loading thread runs <clinit>. The first static-init write is caught.
  3. jdwp_get_stack shows 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.

  1. BP before AND after the suspected mutation point.
  2. At BP1: jdwp_evaluate_expression("session.hashCode()") — remember the value.
  3. jdwp_resume_until_event to advance to BP2.
  4. At BP2: jdwp_assert_expression("session.hashCode()", "<value-from-step-2>") — MISMATCH confirms the hash drifted.
  5. 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:

  1. jdwp_set_logpoint("MyClass", 42, "\"i=\" + i + \" v=\" + value")
  2. Run the test uninterrupted.
  3. jdwp_get_events(50) -> find the FIRST iteration where the value goes bad.
  4. 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

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
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/fgforrest/mcp-jdwp-java/java-debug/badge)](https://verified-skill.com/skills/fgforrest/mcp-jdwp-java/java-debug)

Something wrong with this skill or its page? Tell us.