AgentStack
Browse Sign in
Browse Why AgentStack Sell Docs
Sign in
MCP verified MIT Self-run

Mcp Jdwp Java

mcp-fgforrest-mcp-jdwp-java · by FgForrest

MCP server giving AI agents full debugger control over running Java applications — inspect state, set breakpoints, evaluate expressions, and mutate values at runtime via JDWP/JDI

No reviews yet
0 installs
47 views
0.0% view→install

Install

$ agentstack add mcp-fgforrest-mcp-jdwp-java

✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

View the full security report →

Verified badge

Passed review? Show it. Paste this badge into your README, it links to the public security report.

AgentStack Verified badge Links to your public security report.
[![AgentStack Verified](https://agentstack.voostack.com/badges/verified.svg)](https://agentstack.voostack.com/security/report/mcp-fgforrest-mcp-jdwp-java)

Reliability & compatibility

Security review passed
0 installs to date
no reviews yet
3mo ago

Declared compatibility

Claude CodeClaude DesktopCursorWindsurf

Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.

Preview Execution monitoring

We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.

How agent discovery & health will work →
Are you the author of Mcp Jdwp Java? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

MCP JDWP Inspector

MCP server that gives AI agents full debugger control over running Java applications — inspect state, set breakpoints, evaluate expressions, and mutate values at runtime via JDWP/JDI.

> Release notes — see [CHANGELOG.md](CHANGELOG.md). 2.0.0 is a breaking release: several jdwp_clear_* tools were unified, and field watchpoints are new.

Built on the foundations of mcp-jdwp-java by Nicolas Vautrin — the original project that provided core JDI connectivity, thread/stack/variable inspection, stepping, and basic breakpoint management. Everything described below as "beyond standard JDWP" was built on top of that base.

What you get (TL;DR)

An MCP server that speaks JDWP — the same protocol IntelliJ / Eclipse / VS Code use to attach — and exposes the debugger to Claude Code as 47 tools + 2 MCP resources over STDIO. Once the agent attaches to your JVM, it can:

  • See runtime state, not stack traces — read locals, fields, threads, the entire object graph
  • Evaluate arbitrary Java in the suspended frame — compiled to real bytecode, full classpath, including private/package-private members
  • Mutate the running program — set locals, write fields, test "what if?" without restart
  • Stop on the bad case only — conditional breakpoints, exception breakpoints at the throw site, deferred BPs that arm before the class loads, chains that gate one BP on another
  • Trace without stopping — line/exception/field logpoints write to an event log, the thread runs on
  • Catch the writer of a mutating field — watchpoints suspend at the JVM-level store, including reflective Field.set that a line BP can never see
  • Pure STDIO — no daemon, no extra port, no GUI

The agent gets the same power as IntelliJ's debugger, with extras that exist specifically because an agent isn't a human staring at a UI (filtering, blocking resume, one-shot context dumps, recursion guards, reconnect after a wedge).

Security & trust

This MCP server runs entirely on your local machine:

  • No network calls — the server communicates with Claude Code over STDIO and with the target JVM over a local JDWP socket. It makes zero outbound HTTP/internet requests. No telemetry, no analytics, no phone-home.
  • Built from source — the JAR is compiled locally on your machine from the source code in this repository (either via the plugin auto-build hook or manually with ./mvnw clean package). No pre-built binaries are downloaded or distributed.
  • Auditable — the full source is here. The server is a standard Spring Boot application with no obfuscation or native code. Try asking Claude Code itself: "audit these sources for anything that touches network or filesystem beyond the JVM target".

Quick start

Prerequisites

  • JDK 17+ on PATH to run the server (must be a JDK, not a JRE — JDI lives in jdk.jdi).
  • JDK 21+ to build from source — the build toolchain pins to JDK ≥ 21 because Error Prone 2.48 ships Java-21 bytecode. The bytecode target stays at Java 17, so the resulting JAR still runs on a JDK 17 runtime.

No separate Maven install is required — the repository ships with the Maven Wrapper (./mvnw), which downloads a pinned Maven 3.9.x into ~/.m2/wrapper/ on first use. The SessionStart hook and every build command in this README use the wrapper.

1. Install the plugin

Option A: Plugin marketplace (recommended)

Installs the MCP server, the java-debug skill (debugging workflows, recipes, gotchas), and the .mcp.json configuration in one step:

/plugin marketplace add https://github.com/FgForrest/mcp-jdwp-java.git
/plugin install jdwp-debugging@mcp-jdwp-java

The server JAR is built automatically on first session start via the bundled ./mvnw wrapper (requires JDK 17+ on PATH — no Maven install needed). The hook is content-aware: it rebuilds when git HEAD moves, not just when the JAR is missing, so git pull + restart picks up updates without manual intervention. Restart Claude Code to pick up the plugin.

Alternative: manual MCP registration (without plugin)

If you prefer to register the MCP server directly without the plugin (no skill, no auto-build):

1. Build the JAR:

git clone https://github.com/FgForrest/mcp-jdwp-java.git
cd mcp-jdwp-java
./mvnw clean package -DskipTests   # use mvnw.cmd on Windows

2. Register with Claude Code:

claude mcp add jdwp-inspector -s user \
  -e MCP_TIMEOUT=30000 \
  -e MCP_TOOL_TIMEOUT=120000 \
  -- java --add-modules jdk.jdi,jdk.attach -jar /path/to/mcp-jdwp-java.jar

To change the JDWP port (default 5005), add -DJVM_JDWP_PORT=12345 before -jar.

The MCP_TIMEOUT and MCP_TOOL_TIMEOUT environment variables are important — JVM startup is not instant (class loading, Spring context initialization), so the default MCP timeouts will cause Claude Code to give up before the server is ready. MCP_TIMEOUT=30000 gives the server 30 seconds to start, and MCP_TOOL_TIMEOUT=120000 allows up to 2 minutes for long-running tools like first-time expression evaluation (which discovers the target's classpath and compiles bytecode).

Re-installing requires removing first: claude mcp remove jdwp-inspector -s user

Drop -s user to scope to the current project only.

.mcp.json:

{
  "mcpServers": {
    "jdwp-inspector": {
      "command": "java",
      "args": [
        "--add-modules", "jdk.jdi,jdk.attach",
        "-jar", "/path/to/mcp-jdwp-java.jar"
      ],
      "env": {
        "MCP_TIMEOUT": "30000",
        "MCP_TOOL_TIMEOUT": "120000"
      }
    }
  }
}

The plugin-managed install also passes -DLOG_PATH=${CLAUDE_PLUGIN_ROOT}/logs/mcp-jdwp-inspector.log so the server's SLF4J log lands inside the plugin directory; add the same flag if you want a fixed log path here.

2. Launch your Java application with JDWP

Maven Surefire (test debugging):

mvn test -Dmaven.surefire.debug

Starts the JVM with JDWP on port 5005, suspended until a debugger connects.

Any Java application:

-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005

3. Attach from Claude Code

From there, just tell the agent what to do — "attach to JDWP, set a breakpoint in OrderService.createOrder line 42, run the failing test, dump the context when it hits". The bundled java-debug skill teaches the agent the right tool sequence; the next section gives you a smoke test that exercises the surface.

Find the Bug — test flights

The jdwp-sandbox module ships 9 deliberately broken Java classes. Each one compiles fine, looks reasonable at first glance, and fails its test with a confusing message. Your job: attach with the JDWP MCP server and find the root cause.

This doubles as a setup verification — if you can solve these, everything works.

Each flight is built so that one tool group is the path of least resistance, and lists a par — the minimum tool calls that cleanly reveal the root cause. Hitting par is the elegant solve; the suite as a whole exercises the full tool surface (expression eval, exception breakpoints, field watchpoints, event history, marks, logpoints, runtime mutation, and multi-thread inspection).

Just installed the plugin? Grab the sandbox zip

The fastest path for fresh plugin users is a self-contained zip — no clone, no reactor build:

curl -L -o jdwp-sandbox.zip \
  https://github.com/FgForrest/mcp-jdwp-java/releases/latest/download/jdwp-sandbox.zip
unzip jdwp-sandbox.zip && cd jdwp-sandbox

Inside, you get a parentless Maven project with src/, a pom.xml, a flight-game README.md, and a CLAUDE.md that briefs the agent on the game's house rules. Open the folder in Claude Code and ask it to play flight #1 — the bundled CLAUDE.md keeps it honest (no peeking at source, no spoiler-fetching). Continue with the workflow below.

How to launch a test flight

Start Claude Code from the sandbox folder (or this repo's root, if you cloned) and type:

Use JDWP to debug  in the jdwp-sandbox module — the test is failing, find the root cause.

#1 The Phantom Session

Difficulty: Moderate | Test: SessionStoreTest | Package: session | Par: 4 | Exercises: expression eval

Symptom: retrieve() returned null — a session was stored, upgraded, and then... vanished from the map.

Hint: The session is still in the HashMap. The HashMap just can't find it anymore.

Reveal root cause

UserSession is used as a HashMap key, with both userId and role in hashCode(). upgradeUserRole() calls session.upgradeRole("PREMIUM"), which mutates role — changing the hashCode while the key is still in the map. The entry sits in the old hash bucket; lookups compute the new hash and search the wrong bucket.

Debug path: Breakpoint before and after upgradeRole(). Use jdwp_assert_expression("session.hashCode()", "") after the upgrade — MISMATCH confirms the hash drifted.


#2 The Swallowed Exception

Difficulty: Hard | Test: EventBusTest | Package: events | Par: 4 | Exercises: exception breakpoint + trigger gate

Symptom: `expected stock Reveal root cause

OrderEvent narrows the raw quantity through byte (200 → -56), so Inventory.reserve() throws IllegalStateException. EventBus.dispatch() runs each handler on a single-thread executor as a fire-and-forget task — the Future is never inspected, so the exception is captured inside java.util.concurrent.FutureTask.run (a JDK frame) and discarded. No sandbox frame ever holds the throwable, so a breakpoint-context dump has nothing to show, and getErrorSummary() stays empty.

Debug path: Because the exception is caught (by FutureTask), an uncaught-only breakpoint never fires. Set a line BP at the test/dispatch entry as a trigger, then jdwp_set_exception_breakpoint("java.lang.IllegalStateException", caught=true, triggerBreakpointId=) so bootstrap exceptions don't drown the signal. The throw site suspends with qty = -56 in the frame's locals.


#3 The Time Traveler's Config

Difficulty: Hard | Test: ConfigurationProviderTest | Package: config | Par: 4 | Exercises: field watchpoint + event history

Symptom: expected timeout=5000 but was 0 — the timeout was set during construction, yet it reads back as the default.

Hint: The value was set correctly. Something wrote over it afterward. There's no half-built object to inspect — the damage is in the order of writes.

Reveal root cause

ConfigurationProvider's constructor calls init(5000), so the timeout is correct. Then runMaintenanceSweep() starts a background config-reaper thread that wrongly treats the live instance as stale and calls resetToDefaults(), writing the timeout back to 0. By the time the test reads it, the heap shows 0 and no local holds 5000 — a single context dump looks like the value was never set.

Debug path: jdwp_set_field_breakpoint(className="…config.Configuration", fieldName="timeout", mode="modification"), then jdwp_get_events shows the two stores in order — 0→5000 on the main thread, then 5000→0 on config-reaper. jdwp_get_stack on the second event names the reaper as the clobberer.


#4 The Audit That Lies

Difficulty: Hard | Test: TransferServiceTest | Package: bank | Par: 4 | Exercises: field watchpoint + stack narration

Symptom: expected discrepancy=0 but was non-zero — money is neither created nor destroyed, yet the audit says the books don't balance.

Hint: The transfer moves money in two steps. The audit snapshot is taken between them.

Reveal root cause

TransferService.transfer() is not atomic: it calls source.withdraw(), then auditService.snapshotBalances(), then destination.deposit(). The snapshot captures the intermediate state where money has left the source but hasn't arrived at the destination — showing a total of 1500 instead of 2000.

Debug path: jdwp_set_field_breakpoint on AuditService.lastTotalSnapshot (modification). It fires mid-transfer with the stack at TransferService.transfer between the debit and the credit; jdwp_get_stack plus a live balance read confirm the captured dip.


#5 The Field That Lies

Difficulty: Hard | Test: UserProfileTest | Package: userprofile | Par: 3 | Exercises: event history as evidence

Symptom: expected: but was: — the welcome message rendered correctly, yet the user's stored display name has silently changed casing.

Hint: You will not find the write by ripgrep'ing for setDisplayName — the public setter is never called. A line BP on the setter never fires. Whatever path the write travels, the field's value still flips. Let the JVM tell you exactly when it changes, regardless of how the write reaches the field.

Reveal root cause

LoginNormalizer.welcomeMessage calls a private canonicalForm helper which delegates to a static DisplayNameMirror nested class. The mirror uses Field.setAccessible(true) + Field.set(profile, canonical) to write the lower-cased form straight into UserProfile.displayName — bypassing the public setter entirely. From the test's perspective the formatter is read-only; from JDI's perspective every reflective write is still a real field modification.

Debug path: jdwp_set_field_breakpoint(className="one.edee.jdwp.sandbox.userprofile.UserProfile", fieldName="displayName", mode="modification") — JDI watchpoints fire on every JVM-level field store, including stores issued through Field.set and Unsafe. The next write suspends the thread inside DisplayNameMirror.mirror; jdwp_get_stack walks back through canonicalForm to LoginNormalizer.welcomeMessage and names the culprit. A line BP on UserProfile.setDisplayName would never fire — the setter is genuinely unused.


#6 The Doppelgänger Cart

Difficulty: Moderate | Test: CheckoutTest | Package: cart | Par: 6 | Exercises: marked instances ($label)

Symptom: expected 45.0 but was 0.0 — the cart goes through pricing and discounting, yet its total never changes.

Hint: The object you get back from the pipeline is not the object you passed in. Prove it.

Reveal root cause

Checkout.process runs the cart through validate → price → discount. validate was changed to return a defensive snapshot — return new Cart(cart) — so every later stage mutates the copy. process reassigns its local to the copy as it threads the stages, so no single frame holds both the caller's cart and the copy at once. The test ignores the return value (assuming in-place mutation), so its cart stays at total 0. The copy's fields are identical to the original — only identity differs.

Debug path: Break in the test once the cart exists, jdwp_mark_instance(label="input", objectId=) to pin it, then set a breakpoint in price (or applyDiscount) with condition cart != $input. It fires — the stage is working on a doppelgänger, not the caller's cart.


#7 The Heisenbug Race

Difficulty: Hard | Test: RaceCounterTest | Package: race | Par: 3 | Exercises: logpoints (non-stopping)

Symptom: expected 2 but was 1 — two threads each increment a counter once, but one increment vanishes.

Hint: Two threads, one lost update. Suspending a thread to look changes the timing — watch the reads without stopping anything.

Reveal root cause

RaceCounter.increment reads the count, waits at a CyclicBarrier so both threads have read before either writes, then writes back read + 1. Both threads read 0 and both write 1 — one increment is lost. A suspending breakpoint serializes the threads and makes you juggle two parked threads to reconstruct what happened.

Debug path: Set a non-suspending `jdwp_

Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet, be the first.

Versions

  • v0.1.0 Imported from the upstream source.