Enables AI agents to perform full debugger control over running Java applications via JDWP/JDI, including inspecting state and mutating values.
Also known as FgForrest/mcp-jdwp-java
- Stars
- 34
- Tools
- 50
- Downloads, 30 days
- —
- Latest version
- —
- Last commit
- Nov 14, 2026
- License
- MIT
What is jdwp-java MCP?
jdwp-java enables AI agents to perform full debugger control over running Java applications via JDWP/JDI, including inspecting state and mutating values. It exposes 50 tools, such as jdwp_connect, jdwp_disconnect, jdwp_reconnect.
Summary generated from the project's README
About jdwp-java MCP server
Debugging running Java applications from within an AI agent is what this MCP server enables. It provides an interface for agents to perform deep inspection and runtime manipulation of a JVM using the JDWP and JDI protocols. Instead of just reading stack traces, an agent can explore the entire object graph, read local variables, and inspect fields or threads. This allows for a level of diagnostic depth similar to professional IDEs like IntelliJ or Eclipse.
The server operates locally via STDIO and connects to a target JVM through a local JDWP socket. It requires JDK 17 or higher to run, though JDK 21 is needed to build the server from source. Users can integrate it with Claude Code using a plugin or manual registration. Once attached, the agent can use over 50 tools to set conditional breakpoints, evaluate arbitrary Java expressions, and even mutate field values or local variables to test different scenarios without restarting the application.
What you can do with it
- 1Inspect runtime object stateAn agent can examine the entire object graph and read local variables to understand why a specific value is incorrect.
- 2Test logic via runtime mutationA developer can ask an AI to change a field value or a local variable while the program is paused to see how the application reacts.
- 3Monitor field changes with watchpointsA developer can track exactly when and where a specific field is being modified by the running application.
Written from the project's README. Every use case is checked against a quote from it.
jdwp-java MCP tools 50
Detected from the READMEjdwp_connectjdwp_disconnectjdwp_reconnectjdwp_wait_for_attachjdwp_get_versionjdwp_get_threadsjdwp_dump_locksjdwp_get_stackjdwp_get_localsjdwp_get_fieldsjdwp_to_stringjdwp_get_breakpoint_context
| jdwp_connect | — | |
| jdwp_disconnect | — | |
| jdwp_reconnect | — | |
| jdwp_wait_for_attach | — | |
| jdwp_get_version | — | |
| jdwp_get_threads | — | |
| jdwp_dump_locks | — | |
| jdwp_get_stack | — | |
| jdwp_get_locals | — | |
| jdwp_get_fields | — | |
| jdwp_to_string | — | |
| jdwp_get_breakpoint_context | — |
Show all 50 toolsShow fewer tools
jdwp_get_current_threadjdwp_resumejdwp_resume_threadjdwp_suspend_threadjdwp_resume_until_eventjdwp_step_overjdwp_step_intojdwp_step_outjdwp_set_breakpointjdwp_set_logpointjdwp_set_fast_logpointjdwp_set_fast_breakpointjdwp_restore_fast_logpointsjdwp_clear_breakpointjdwp_set_exception_breakpointjdwp_set_exception_logpointjdwp_set_field_breakpointjdwp_set_field_logpointjdwp_set_breakpoint_dependencyjdwp_clear_breakpoint_dependencyjdwp_disarm_until_triggerjdwp_evaluate_expressionjdwp_assert_expressionjdwp_set_localjdwp_set_fieldjdwp_get_eventsjdwp_clear_eventsjdwp_diagnosejdwp_attach_watcherjdwp_detach_watcherjdwp_list_watchers_for_breakpointjdwp_evaluate_watchersjdwp_mark_instancejdwp_unmark_instancejdwp_rename_markjdwp_overviewjdwp_clearjdwp_reset
| jdwp_get_current_thread | — | |
| jdwp_resume | — | |
| jdwp_resume_thread | — | |
| jdwp_suspend_thread | — | |
| jdwp_resume_until_event | — | |
| jdwp_step_over | — | |
| jdwp_step_into | — | |
| jdwp_step_out | — | |
| jdwp_set_breakpoint | — | |
| jdwp_set_logpoint | — | |
| jdwp_set_fast_logpoint | — | |
| jdwp_set_fast_breakpoint | — | |
| jdwp_restore_fast_logpoints | — | |
| jdwp_clear_breakpoint | — | |
| jdwp_set_exception_breakpoint | — | |
| jdwp_set_exception_logpoint | — | |
| jdwp_set_field_breakpoint | — | |
| jdwp_set_field_logpoint | — | |
| jdwp_set_breakpoint_dependency | — | |
| jdwp_clear_breakpoint_dependency | — | |
| jdwp_disarm_until_trigger | — | |
| jdwp_evaluate_expression | — | |
| jdwp_assert_expression | — | |
| jdwp_set_local | — | |
| jdwp_set_field | — | |
| jdwp_get_events | — | |
| jdwp_clear_events | — | |
| jdwp_diagnose | — | |
| jdwp_attach_watcher | — | |
| jdwp_detach_watcher | — | |
| jdwp_list_watchers_for_breakpoint | — | |
| jdwp_evaluate_watchers | — | |
| jdwp_mark_instance | — | |
| jdwp_unmark_instance | — | |
| jdwp_rename_mark | — | |
| jdwp_overview | — | |
| jdwp_clear | — | |
| jdwp_reset | — |
Configuration
MCP_TIMEOUTMCP_TOOL_TIMEOUT
| Name | Description | |
|---|---|---|
| MCP_TIMEOUT | — | |
| MCP_TOOL_TIMEOUT | — |
Automated checks
Ran Nov 13, 2026Full report →Passed our automated checks. That is not a security audit: read the code and give the server the narrowest access it needs. How checks work
- Source repository linked and reachablePass
- Open source license (OSI)Pass
- Commit or release in the last yearPass
- Package name not close to a popular packageNot checked
- Remote endpoint uses HTTPSNot checked
- Destructive tools are labelledPass
- No hidden instructions to the modelPass
- No critical advisory in the package or its dependenciesNot checked
README
View on GitHubMCP 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. 2.0.0 is a breaking release: severaljdwp_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 50 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 every bytecode store to the field, wherever it is; a write through reflection or
Unsafeis the one they cannot see, and the[VM_DEATH]reply's hit count shows when a watchpoint stayed silent - 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. Every build command in this README uses the wrapper; the SessionStart hook does too, and falls back to mvn on PATH when the wrapper cannot set itself up.
1. Install the plugin
Option A: Plugin marketplace (recommended)
Installs the MCP server, the java-debug skill (debugging workflows, recipes, gotchas), and the MCP server registration 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 21+ — no Maven install needed). The hook is content-aware: it rebuilds when the JAR is missing or, in a git checkout, when HEAD moves, so git pull + restart picks up updates without manual intervention. When the wrapper cannot set itself up (for example, ~/.m2 is read-only in a sandbox), the hook tries mvn -o and then mvn from PATH. If every attempt fails, a warning at session start names the build log and the exact command to build by hand. A build runs while Claude Code starts the MCP server and the JAR is absent until it finishes, so after a build (first install, update, moved HEAD) jdwp-inspector may show as failed in /mcp; reconnect it there once the build is done. Restart Claude Code to pick up the 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 10 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, multi-thread inspection, and in-target probes).
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 <TestClass> 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.
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()", "<value-before>") 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 < 100 but was 100 and an empty error summary — the order was supposed to reserve inventory, but nothing happened and nobody complained.
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=<trigger>) 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.
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.
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: field watchpoint blind spot + before/after bisect
Symptom: expected: <Alice> but was: <alice> — the welcome message rendered correctly, yet the user's stored display name has silently changed casing.
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. From the test's side the formatter looks read-only, and a field watchpoint cannot see the write either: JDI watchpoints fire only on the putfield/getfield bytecodes (and JNI field access). Field.set ends in an Unsafe store straight to memory, which trips no watchpoint, in modification mode or in access mode.
Debug path: The tell is a modification watchpoint that fires once, for the constructor's ordinary assignment, and never again, although the value flips before the assertion: the write does not go through a field store. The par path takes three calls:
jdwp_set_field_breakpoint(className="one.edee.jdwp.sandbox.userprofile.UserProfile", fieldName="displayName", mode="modification"). The class is not loaded yet, so the watchpoint waits for it.jdwp_resume_until_event: the watchpoint fires in theUserProfileconstructor, which stores"Alice".jdwp_resume_until_event: the test fails and its JVM exits. The[VM_DEATH]reply lists the watchpoint with one hit. The only store the JVM saw wrote"Alice", yet the test read"alice", so the write is reflective or goes throughUnsafe. Between the two, the test calls onlywelcomeMessage, on line 24.
Bisecting takes three calls as well and shows where the value flips, though not how: jdwp_set_breakpoint("one.edee.jdwp.sandbox.userprofile.UserProfileTest", 27), just after welcomeMessage, then jdwp_resume_until_event, then jdwp_evaluate_expression("profile.getDisplayName()"), which is already "alice". When the old and new strings could be equal, compare System.identityHashCode(profile.getDisplayName()) instead.
That is the evidence to read LoginNormalizer with: canonicalForm → DisplayNameMirror.mirror → Field.set. To name the writing call from the debugger alone, break at line 24 instead and keep bisecting inside welcomeMessage with jdwp_step_into / jdwp_step_over, comparing the identity after each line: it flips across DisplayNameMirror.mirror(profile, canonical) in canonicalForm. A line BP on UserProfile.setDisplayName never fires — 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.
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=<id>) 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.
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_set_logpoint at the read site logging the thread name and count (or a field logpoint on count). Let the test run, then jdwp_get_events — both racer-1 and racer-2 are recorded reading 0 before either writes, which is the lost update laid out in order.
#8 The Magic Patch
Difficulty: Warm-up | Test: DateParserTest | Package: parser | Par: 4 | Exercises: runtime mutation (set_local)
Symptom: NumberFormatException: For input string: "15 " — a date string that looks right fails to parse.
DateParser.parse splits YYYY-MM-DD and Integer.parseInts each part. The feed delivers "2026-05-15 " with a trailing space, so parseInt("15 ") throws. The parser never trims its input.
Debug path: Four calls. jdwp_set_breakpoint at the top of parse, jdwp_resume_until_event to stop there, jdwp_set_local("input", "2026-05-15") to drop the trailing space (or break on the test's line 19 and patch fromFeed instead), and jdwp_resume. The parse now succeeds and the test passes — confirming the trailing space is the whole story, with no rebuild. The exception message already shows the stray space, so the stop needs no inspection call; the confirming resume counts.
#9 The Polite Standoff
Difficulty: Hard | Test: TransferDeadlockTest | Package: deadlock | Par: 4 | Exercises: multi-thread inspection (no breakpoints)
Symptom: The test hangs and then fails its join — both transfers never complete.
Account.transfer locks the source account, pauses, then locks the destination. Two transfers in opposite directions (A→B and B→A) each grab their first lock and then wait forever for the other's — a classic lock-ordering deadlock.
Debug path: No breakpoint can fire — the threads execute nothing. jdwp_get_threads shows transfer-A-to-B and transfer-B-to-A in MONITOR state; jdwp_dump_locks prints the lock graph with both monitor holders and the inverse-order cycle; jdwp_get_stack on each thread shows the lock-ordering inversion. (Trying jdwp_evaluate_expression / jdwp_to_string on these threads is refused — invoking a method on a monitor-blocked thread would hang the debugger, which is itself the tell.)
#10 The Oversold House
Difficulty: Hard | Test: BoxOfficeTest | Package: boxoffice | Par: 4 | Exercises: in-target probes (fast logpoint / fast breakpoint, with the user's consent)
Symptom: [Tickets sold for 300 performances of 200000 seats] expected: 60000000L but was: 60001634L — the season ends oversold by a few tickets per performance.
Solved when: you name one specific double sale: the seat, the window that sold it first and the window that sold it again. Knowing that the house is oversold, or by how much, is not enough.
BoxOffice.trySell checks that the seat is free, prices it (reading the pricing desk's volatile surcharge), and only then records the buyer: a check-then-act race. Two windows that check the same free seat within the pricing step both sell it, and sold counts both. About six seats per performance are sold twice. Customer numbers are window numbers, and owners[] keeps only the last buyer, so counting seats against sold() after the fact shows the overselling but cannot name the window that sold a seat first. Only catching the collision can.
Debug path: On the sell line (BoxOffice.java:54, seatOwners[seat] = customer), the condition seatOwners[seat] != 0 is true when another window sold the seat after this one checked it. Both in-target tools rewrite the class's bytecode, so each needs the user's explicit consent: the first call, without userApproved, arms nothing and returns a risk briefing to relay word for word. BoxOffice is not loaded when the test JVM starts; the approved probe waits for the class to load and arms on its next hit.
- Fast logpoint, 4 calls (par):
jdwp_set_fast_logpointonone.edee.jdwp.sandbox.boxoffice.BoxOfficeline 54, with that condition and the expressionseat + " sold to " + seatOwners[seat] + ", resold to " + customer, withoutuserApproved: the risk briefing, relayed to the user.- The same call with
userApproved=true, once the user agrees. jdwp_resume_until_event: the test runs to its end. The reply is[VM_DEATH]and says how manyFAST_LOGPOINThits were recorded.jdwp_get_events: the double sales, each with its seat and the first and second window.
- Fast breakpoint, 5 calls: the briefing and the approved
jdwp_set_fast_breakpointcall with the same condition, thenjdwp_resume_until_event: the first double sale stops with every window parked.jdwp_get_breakpoint_contextshowsseatand its second buyer,customer.jdwp_evaluate_expression("seatOwners[seat]")gives the first buyer; array elements are not in the context dump.
The logpoint does not catch every double sale. Its condition reads a plain int[] without synchronization, so a window that reads the seat before the other window's write is visible logs nothing; one live run caught 1,460 of 2,472 double sales. One is enough to solve the flight. The probe also sits inside the race window, between the check and the write, so it widens the race rather than hiding it: runs with the probe sold more seats twice, not fewer.
An ordinary conditional breakpoint or logpoint on that line goes through the debugger on each of about 60 million passes, at 3–30 ms each. The box office closes first, and the serialized windows stop colliding.
jdwp-java MCP questions
What tools does jdwp-java MCP provide?
It exposes 50 tools, including jdwp_connect, jdwp_disconnect, jdwp_reconnect, jdwp_wait_for_attach, jdwp_get_version.
What can the AI agent do with this server?
The agent can inspect runtime state like locals and fields, evaluate arbitrary Java code in suspended frames, mutate running programs, and set conditional or exception breakpoints.
What are the system requirements to run the server?
You need JDK 17+ installed on your PATH to run the server, and JDK 21+ is required to build it from source.
How do I connect it to my Java application?
You must launch your Java application with JDWP enabled, for example using the argument -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005.