AI coding agents are good at writing JVM code. Reading it is another story.
Ask an agent what a method in one of your dependencies does and watch the transcript: it unzips the jar, runs javap, greps the output, and then reads an entire source file into its context window. javap gives it erased signatures and raw opcodes, so it fills the gaps by guessing.
In this tutorial we'll give the agent the answers an IDE gives a human, using jdx, an open-source CLI and MCP server I built for exactly this. Every output below comes from the jdx quickstart, which is generated by actually running the commands.
What you need
- A Java 21+ runtime (Linux, macOS or Windows)
- An MCP-capable agent if you want the integration step (Claude Code, Cursor, Codex CLI, GitHub Copilot CLI, OpenCode, Cline, Kilo Code, Antigravity, or anything else that speaks MCP)
1. Install
curl -fsSL https://raw.githubusercontent.com/MohammadMD1383/jdx/main/install-release.sh | bash
jdx version
It installs per user (no root) into ~/.local/share/jdx and links ~/.local/bin/jdx. On Windows, use the PowerShell installer from the installation guide. jdx doctor checks your environment if anything looks off.
Releases are unsigned, so macOS Gatekeeper and Windows SmartScreen will warn on first run. The installation guide shows how to clear it.
2. Ask the JDK something, with zero setup
The running JDK is always on the classpath:
$ jdx members java.util.HashMap --limit 5
members of java.util.HashMap
constructors declared on java.util.HashMap:
constructor public HashMap()
constructor public HashMap(int initialCapacity)
constructor public HashMap(int initialCapacity, float loadFactor)
constructor public HashMap(java.util.Map<? extends K, ? extends V> m)
methods declared on java.util.HashMap:
method public V compute(K key, java.util.function.BiFunction<? super K, ? super V, ? extends V> remappingFunction)
+ 7 from java.lang.Object (--from java.lang.Object to expand)
5 of 53 members shown (--limit 53 to see more)
source: java.base (jrt)
Two things matter here for an agent. The output is bounded, and the footer says exactly what was cut and how to get the rest. And the source: line says where the answer came from.
Now read one method, and only that method:
$ jdx body 'java.util.HashMap#get(java.lang.Object)'
java.util.HashMap#get(java.lang.Object)
source: src.zip · java.base/java/util/HashMap.java:562-565
public V get(Object key) {
Node<K,V> e;
return (e = getNode(key)) == null ? null : e.value;
}
next: jdx show java.util.HashMap
Quote references in your shell: #, ( and $ are shell syntax.
3. Ask about a library by its Maven coordinate
--coord resolves a library from your local Gradle and Maven caches. --fetch allows downloading it (and its -sources.jar) from Maven Central once:
$ jdx body 'com.google.gson.Gson#toJson(Object)' --coord com.google.code.gson:gson:2.14.0
com.google.gson.Gson#toJson(java.lang.Object)
source: gson-2.14.0-sources.jar · com/google/gson/Gson.java:542-547
public String toJson(Object src) {
if (src == null) {
return toJson(JsonNull.INSTANCE);
}
return toJson(src, src.getClass());
}
next: jdx show com.google.gson.Gson
(Gson is a multi-release jar, so on JDK 21 jdx also prints a warning MULTI_RELEASE_VARIANT line; I've trimmed it from the outputs in this article.)
If there is no sources jar, jdx decompiles the method with Vineflower and marks the output ⚠ reconstructed, so the agent knows it's reading a reconstruction. (--engine javap gives raw bytecode instead.)
4. Let it refuse to guess
toJson has eight overloads. jdx won't pick one:
$ jdx body 'com.google.gson.Gson#toJson' --coord com.google.code.gson:gson:2.14.0
ambiguous: 8 candidates for com.google.gson.Gson#toJson
com.google.gson.Gson#toJson(com.google.gson.JsonElement)
com.google.gson.Gson#toJson(com.google.gson.JsonElement, com.google.gson.stream.JsonWriter)
com.google.gson.Gson#toJson(com.google.gson.JsonElement, java.lang.Appendable)
com.google.gson.Gson#toJson(java.lang.Object)
com.google.gson.Gson#toJson(java.lang.Object, java.lang.Appendable)
com.google.gson.Gson#toJson(java.lang.Object, java.lang.reflect.Type)
com.google.gson.Gson#toJson(java.lang.Object, java.lang.reflect.Type, com.google.gson.stream.JsonWriter)
com.google.gson.Gson#toJson(java.lang.Object, java.lang.reflect.Type, java.lang.Appendable)
hint: re-run with one of the refs above
[exit 2]
Exit code 2 means "ambiguous", and every candidate is a paste-ready reference. An agent can branch on the exit code without parsing prose, and the next call is a copy-paste, not a reconstruction. Other outcomes (not found, no index, bad usage) have their own exit codes, and every command has --json.
5. Navigate: hierarchy and callers
$ jdx hierarchy com.google.gson.JsonElement --coord com.google.code.gson:gson:2.14.0
hierarchy of 'com.google.gson.JsonElement'
supertypes
extends java.lang.Object java.base
subtypes in workspace (4)
class com.google.gson.JsonArray gson-2.14.0.jar
class com.google.gson.JsonNull gson-2.14.0.jar
class com.google.gson.JsonObject gson-2.14.0.jar
class com.google.gson.JsonPrimitive gson-2.14.0.jar
$ jdx callers 'com.google.gson.JsonParser#parseReader(com.google.gson.stream.JsonReader)' --coord com.google.code.gson:gson:2.14.0
callers of 'com.google.gson.JsonParser#parseReader(com.google.gson.stream.JsonReader)'
call com.google.gson.JsonParser#parse(com.google.gson.stream.JsonReader) gson-2.14.0.jar
call com.google.gson.JsonParser#parseReader(java.io.Reader) gson-2.14.0.jar
--depth N follows the call graph transitively (cycle-safe). jdx usages and jdx implementors work across every jar on the classpath, not just one. More in Usages, hierarchy & calls.
6. Ask what a dependency upgrade breaks
$ jdx diff com.google.code.gson:gson:2.11.0 com.google.code.gson:gson:2.14.0 --fetch
api diff com.google.code.gson:gson:2.11.0 -> com.google.code.gson:gson:2.14.0 (public surface)
info TYPE_ADDED com.google.gson.internal.GsonTypes
...
info MEMBER_ADDED com.google.gson.stream.JsonReader#setNestingLimit(int)
info FIELD_CONSTANT_VALUE_CHANGED com.google.gson.internal.GsonBuildConfig#VERSION: "2.11.0" -> "2.14.0"
0 breaking · 0 suspicious · 17 informational · 2 types added · 0 types removed
Every finding names a rule (MEMBER_REMOVED, RETURN_TYPE_CHANGED, KOTLIN_NULLABILITY_CHANGED, …) at breaking, suspicious or info. Add --fail-on breaking and it becomes a CI gate. See API diff for upgrades.
7. Name your classpath once
jdx ws create gson --coord com.google.code.gson:gson:2.14.0
jdx -w gson usages com.google.gson.TypeAdapter
jdx ws use gson # make it the default
Inside a Gradle or Maven project you often need nothing: jdx discovers the project's sources and dependency jars from the local caches, without running the build.
8. Hand it to your agent
jdx setup --agent claude-code --scope project # then restart Claude Code
# or: cursor, codex, copilot, opencode, cline, kilo, antigravity, windsurf, aider
Any other MCP client takes one line of config:
{"mcpServers":{"jdx":{"command":"jdx","args":["mcp"]}}}
The MCP server exposes each query as a typed tool (jdx_members, jdx_body, jdx_usages, jdx_callers, jdx_diff, …) and keeps the index warm, so queries take about 20 ms instead of about 250 ms for a cold CLI call. If you'd rather use the CLI from your agent, jdx help --agent prints a compact block to paste into CLAUDE.md or AGENTS.md.
Per-agent setup pages: Claude Code, Cursor, Codex CLI, all integrations.
New in v1.7.0: the questions IDEs don't answer
Agents also ask things an IDE never made easy. v1.7.0 adds commands for them (see the command reference for every flag):
-
jdx literals— search string constants (LDCloads andConstantValuefields) and.propertieskeys/values across the workspace: "where does this log message / resource key come from?" -
jdx services—META-INF/servicesSPI providers: "who providescom.foo.Spi?" -
jdx modules— JPMSmodule-info(requires / exports / opens / uses / provides). -
jdx manifestandjdx resources—MANIFEST.MFattributes and non-class resources (--catprints a text resource). -
jdx annotated— classes and members carrying an annotation, with recipes such asdeprecated,forRemoval,testandbean;jdx usages --wherefilters on annotation values. -
callersis now override-aware by default (--exactfor invoke-owner matching), bytecode usages carryfile:line, andusages --contextshows inline snippets. - Kotlin nullability is rendered from
@Metadata, and there's a PowerShell installer for Windows.
Does it actually save context?
On one task from the README, "list Gson's API, then show only toJson(Object)" on gson-2.14.0:
| Approach | Calls | Output in context |
|---|---|---|
unzip + javap + grep + sed
|
5 | ~17.5 KB (~4.4k tokens), and head -200 missed the method entirely |
jdx members + jdx body
|
2 | ~4.6 KB (~1.2k tokens) |
The method body alone is 267 bytes, against 60,590 for javap -c and 58,251 for the full Gson.java. That's one task, not a benchmark suite, but you can rerun it yourself with sh docs/bench-compare.sh from the repo.
A note on safety
jdx parses bytecode with ASM and never loads the classes it inspects, so no static initialiser or other code from those jars runs. It only touches the network when you pass --fetch, run jdx upgrade, or run jdx kotlin install.
Kotlin
Kotlin binaries are read through @Metadata, so the agent sees nullability, suspend functions, properties, default arguments and @JvmName mappings instead of the JVM projection. Reading Kotlin sources needs a one-time jdx kotlin install. Details in Reading code.
Wrapping up
jdx is free and open source (Apache-2.0). Start with the quickstart, browse the docs, and if it gives your agent a wrong answer, please open an issue with the command and what you expected.
Source: github.com/MohammadMD1383/jdx
Top comments (0)