jar-inspector-mcp
# jar-inspector-mcp
An MCP server that lets Claude Code read **inside Java archives** — classes, APIs,
sources, resources, bytecode — without extracting them.
## The problem
When Claude Code works on a JVM project and needs to know what is inside a jar, it
falls back on shell tools:
```
unzip -l target/lib/jackson-databind-2.16.1.jar # 780 lines of entry listing
unzip -o app.jar -d /tmp/x && cat /tmp/x/…/Foo.java # a whole file to answer one question
javap -p -c -cp app.jar com.acme.Foo # 300 KB of bytecode
```
All of it lands in the context window, most of it is noise, and some of it writes
temporary files into the working tree.
## The solution
Ten focused tools that answer the *actual* questions — "what is in this jar", "what
is this class's API", "where is this string set" — and return only that.
Measured on `jackson-databind-2.16.1.jar` (779 classes, 4.1 MB extracted):
| Instead of | | jar-inspector | | |
|---|--:|---|--:|--:|
| `unzip -l <jar>` | 79,674 chars | `jar_overview` | 2,632 chars | **97% less** |
| `javap <class>` | 36,008 chars | `jar_class_outline` | 18,061 chars | **50% less** |
| `javap -p -c <class>` | 303,105 chars | `jar_class_outline` | 21,409 chars | **93% less** |
Nothing is ever written to disk, and **no JDK is required** — the class file parser
is pure Python. (`jar_disassemble` is the one exception; it shells out to `javap`.)
## Install
Requires [uv](https://docs.astral.sh/uv/) and Python 3.10+.
```bash
# from GitHub, nothing to clone
claude mcp add jar-inspector -- uvx --from git+https://github.com/pgatzka/jar-inspector-mcp jar-inspector-mcp
# or from a local clone
git clone https://github.com/pgatzka/jar-inspector-mcp && cd jar-inspector-mcp
claude mcp add jar-inspector -- uv run --directory "$PWD" jar-inspector-mcp
```
Add `--scope project` to share it with a repo through `.mcp.json`, or `--scope user`
to enable it everywhere. To wire it up by hand instead:
```jsonc
// .mcp.json
{
"mcpServers": {
"jar-inspector": {
"command": "uvx",
"args": ["--from", "git+https://github.com/pgatzka/jar-inspector-mcp", "jar-inspector-mcp"]
}
}
}
```
Verify with `claude mcp list`, or `/mcp` inside Claude Code.
Other MCP clients: the server speaks stdio, so `uvx --from … jar-inspector-mcp` works
as the command anywhere.
## Tools
| Tool | Answers |
|---|---|
| `jar_overview` | What is this artifact? Manifest, packages, class count, bytecode level, are sources attached |
| `jar_list` | What entries are in it? Filtered by glob and kind, paged |
| `jar_find_class` | Where does `HttpClient` live? Simple name, FQCN or glob → class names |
| `jar_class_outline` | What is this class's API? Declaration, annotations, fields, method signatures |
| `jar_read_source` | What does it actually do? Attached `.java`/`.kt` source, by line window |
| `jar_read_entry` | What is in this config? One text entry: MANIFEST, `META-INF/services/…`, XML |
| `jar_search` | Where is this string? Grep across text entries and compiled string constants |
| `jar_class_dependencies` | What does this class touch? Internal vs external references |
| `jar_disassemble` | What does this method compile to? One method's bytecode via `javap` |
| `jar_find` | Where is the jar? Searches build output, the Maven repo and the Gradle cache |
Every tool is read-only and takes a `jar` argument that accepts:
- an archive — `.jar`, `.war`, `.ear`, `.aar`, `.zip`, `.jmod`
- an exploded classes directory — `target/classes`, `build/classes/java/main`
- a **nested** archive — `app.jar!BOOT-INF/lib/dep-1.0.jar` (Spring Boot fat jars)
## What the output looks like
`jar_class_outline`, verbatim (this is the exact output the tests assert on):
```
com.acme.demo.UserService [class, Java 21, UserService.java]
// source is attached -- jar_read_source(jar, 'com.acme.demo.UserService') shows it
@Deprecated
@Marker(enabled=true, code='z', tags={"alpha", "beta"})
public class UserService implements Repository<String, Integer>
// fields (2)
public static final String CACHE_KEY = "user.cache"
protected volatile boolean dirty
// constructors (1)
public UserService(Map<Integer, String> users)
// methods (5)
public long count()
public List<String> findAll()
public Optional<String> findById(Integer id) throws IllegalStateException
public <R extends Comparable<R>> List<R> mapAll(Function<String, R> fn)
public void rename(int id, String newName, String... aliases)
// nested (1)
UserService.Builder
// 4 more member(s) hidden (visibility below 'protected', synthetic or bridge)
```
Generic signatures, parameter names (when compiled with `-g`), varargs, declared
exceptions, constant values, records, enums and `default` methods all survive.
Method bodies, bridge methods and constant pools do not.
## Configuration
| Environment variable | Effect |
|---|---|
| `JAR_INSPECTOR_ALLOW` | Path-separated roots the server may read. Unset means no restriction |
| `JAR_INSPECTOR_SEARCH_PATH` | Extra directories `jar_find` scans first |
| `MAVEN_REPO_LOCAL` | Maven repository location, if not `~/.m2/repository` |
`jar_find` searches, in order: `./target`, `./build/libs`, `./libs`, `./lib`, the
Maven repository, the Gradle module cache, the Ivy cache and the Coursier cache.
To confine the server to one tree:
```bash
claude mcp add jar-inspector --env JAR_INSPECTOR_ALLOW="$HOME/work:$HOME/.m2" \
-- uvx --from git+https://github.com/pgatzka/jar-inspector-mcp jar-inspector-mcp
```
## Nudging Claude to use it
The server ships MCP `instructions` telling clients these tools replace `unzip`,
`jar xf` and `javap`. If you still see Claude reaching for the shell, add a line to
your project's `CLAUDE.md`:
```markdown
To inspect jars, use the jar-inspector MCP tools instead of unzip/jar/javap.
```
## How it works
- **Class parsing** is a dependency-free reader for the class file format: constant
pool, access flags, `Signature`, `MethodParameters`, `LocalVariableTable`,
`Exceptions`, `Record`, `InnerClasses` and the runtime annotation attributes.
Method bodies are skipped unless `jar_disassemble` asks for them.
- **Archives** are read through `zipfile` in memory. Nested jars are opened from the
bytes of the outer entry, so a Spring Boot fat jar needs no unpacking.
- **Sources** are resolved from the archive itself, then from the sibling
`-sources.jar` in Maven layout, then from the neighbouring hash directory that
Gradle's cache uses.
- **Output** is always capped and always says when it was truncated and how to page.
## Development
```bash
uv sync
uv run pytest # 49 tests
```
The tests compile the Java in `tests/java` with `javac` and assert against real
bytecode, so generics, bridge methods and records are exercised for real; they skip
when no JDK is present. The parser is additionally checked against every class in
every jar shipped with Maven and Gradle — 65,573 classes, no failures.
## License
MIT
TDQS
Scored across 10 tools
Each tool targets a distinct inspection task: overview, listing, class lookup, API outline, source reading, entry reading, search, dependency analysis, disassembly, and jar discovery. There is no meaningful overlap that would make an agent likely to pick the wrong tool.
All tools share the jar_ prefix, which helps, but the grammatical pattern is mixed: jar_list and jar_search are bare verbs, jar_find_class and jar_read_source are verb_noun, while jar_overview and jar_class_outline are noun phrases. The names are readable but not consistently verb_noun.
Ten tools is well within the ideal range and each tool covers a distinct facet of archive inspection. The count feels neither bloated nor thin for the stated purpose.
The read-only jar inspection domain is covered thoroughly: overview, listing, class lookup, API outline, source reading, arbitrary text entries, grep-like search, class dependency analysis, bytecode disassembly, and locating jars on disk. An agent can move from finding a jar to reading its source or bytecode without obvious dead ends.