java-index MCP server
Commit Nov 12, 2026Provides lightweight Java code navigation and indexing for both editors via LSP and AI agents via MCP.
Also known as slgobinath/java-index
- Stars
- 8
- Tools
- —
- Downloads, 30 days
- —
- Latest version
- —
- Last commit
- Nov 12, 2026
- License
- Apache-2.0
What is java-index MCP?
java-index provides lightweight Java code navigation and indexing for both editors via LSP and AI agents via MCP.
Summary generated from the project's README
About java-index MCP server
Developers reviewing Java code written by AI agents can use java-index to navigate complex codebases without the heavy resource requirements of a full language server. It provides a lightweight way to perform essential reading tasks such as finding definitions, checking implementations, and viewing call hierarchies. Unlike traditional tools that compile entire projects into a target folder, this server parses sources using turbine to create declaration summaries, ensuring it never writes to your project files.
Users can access the index through an editor via the Language Server Protocol or through AI agents like Claude Code using the Model Context Protocol. A single daemon serves every worktree of a repository, allowing multiple editor windows and agent sessions to share the same index. This setup is particularly useful for Spring Boot and Quarkus developers, as it links configuration keys in properties or YAML files directly to the Java code that reads them.
What you can do with it
- 1Navigate Spring configurationsJump from a configuration key in a properties file to the Java code that reads it.
- 2Review agent-generated codeProvide an AI agent with the ability to understand the real API of a class from source or dependency jars.
Written from the project's README. Every use case is checked against a quote from it.
Configuration
JAVA_HOME
| Name | Description | |
|---|---|---|
| JAVA_HOME | — |
Automated checks
Ran Nov 14, 2026Full report →Not verified yet. A server needs a score of 80, no serious finding, and enough data for us to check. How checks work · What to fix for more points
- Source repository linked and reachableslgobinath/java-index is public and reachablegithub.com/slgobinath/java-index ↗Pass
- Package names this repository as its sourceNot checked
- Build provenance (Sigstore attestation)Not checked
- Open source license (OSI)Apache-2.0spdx.org/licenses/Apache-2.0.html ↗Pass
- Commit or release in the last yearLast commit or release 2026-11-12github.com/slgobinath/java-index ↗Pass
- OpenSSF ScorecardNot checked
- Package name not close to a popular packageNot checked
- No critical advisory in the package or its dependenciesNot checked
- No hidden instructions to the model0 tool descriptions and the README read; no override phrases, credential paths or hidden charactersPass
- Destructive tools are labelledNot checked
- Remote endpoint answers the MCP handshakeNot checked
README
View on GitHubjava-index
Fast, lightweight Java code navigation, mainly for reviewing code that AI agents write.
It handles the everyday questions you have when reading Java: where is this defined, who calls it,
what implements it, what does this library method actually look like, which config key does this
@Value read. Your editor talks to it over
LSP, and agents like Claude Code talk to
it over MCP. Both share the same index.
https://github.com/user-attachments/assets/9984c749-106f-4f31-ac43-a0c9f10f1f90
Contents
- Purpose
- How it's different
- java-index vs jdtls
- Benchmark results — projects, spring-petclinic, jackson-databind, Hibernate ORM
- What you get — in your editor (LSP), for AI agents (MCP)
- Install
- Set up
- How it works
- Classpath
- Configuration
- Limitations
- Reporting problems
- License
Purpose
More and more of our Java code is written by agents, and we spend more time reviewing it than
writing it. Reviewing needs basic navigation: go to definition, find callers, check
implementations. Full language servers like jdtls can do that, but they're built for writing code.
They're heavy, they start over for every git worktree, and they compile the project into the same
target/ folder the agent's build uses.
java-index only does the reading part. One small daemon serves every worktree of a repository, agents get the same index as tools, and it never builds or writes anything in your project. It's not a replacement for jdtls or your IDE. If you write Java by hand, keep using those.
How it's different
- No compiling. Sources are parsed with turbine (Google's header compiler) into small declaration summaries. Method bodies are only looked at when you ask about something inside them.
- One daemon per repo. All your editor windows, agent sessions and worktrees share one process. Files that are the same across worktrees are stored once; each worktree only keeps what it changed.
- Same answers for you and your agents. The editor gets definitions, references, hover and
hierarchies. Agents get the same information as simple tools that return text with
file:linelocations.
java-index vs jdtls
jdtls is a full language server for writing Java. java-index only covers reading it.
| java-index | jdtls | jdtls light | |
|---|---|---|---|
| Definition, references, hover, implementations, type and call hierarchy, symbols | ✓ | ✓ | partial (definition, hover and symbols in project sources; no references, implementations or hierarchies) |
| Compiler errors and warnings | — | ✓ | syntax errors only |
| Completion | — | ✓ | partial (no classpath) |
| Refactoring, quick fixes (beyond imports), formatting | — | ✓ | formatting only |
| Debugging, running tests | — | ✓ (with extensions) | — |
| Lombok and other annotation processors | — | ✓ | — |
| Spring Boot / Quarkus config keys linked to Java | ✓ | — (separate Spring Tools) | — |
| Tools for agents (MCP) | ✓ | — | — |
| One shared process for every worktree of a repository | ✓ | — (one per window) | — (one per window) |
Writes into your project (target/) |
never | yes, builds alongside yours | never |
Measured numbers on three open-source projects are in Benchmark results.
jdtls does a lot more work than java-index, since it compiles the project. Where jdtls compiles, java-index has to approximate, which means:
- No Lombok or annotation processors. turbine doesn't run them, so anything generated by
@Getter,@Data,@Builderand friends doesn't exist for java-index, and calls to it won't resolve. - Generated sources need a build first. Code in
target/generated-sources(MapStruct, protobuf, OpenAPI) gets indexed once it's there, but on a fresh clone it isn't. - Resolution isn't perfect. Some generic inference falls back to bounds, and reflection or framework wiring is invisible. When it isn't sure, it returns nothing instead of guessing. See Limitations.
- No compile errors. It can't tell you the code doesn't compile. Your build or CI does that.
So: if you write Java by hand, use jdtls or an IDE. If you mostly review what agents write, especially across several worktrees, java-index gets you the same navigation for a lot less time and memory.
Benchmark results
The benchmark opens each project in a fresh editor-like LSP client, waits
until the server answers a probe query correctly, then runs a fixed set of queries and checks each
answer. Every server is started cold (no caches) and warm (its caches from the previous run) three
times; the numbers are medians. Machine: Apple M5 Pro, 64 GB. Servers: java-index with the native
relay, jdtls 1.63.0 with VS Code's default settings, and the same jdtls in VS Code's lightweight
mode (java.server.launchMode: LightWeight, "jdtls light"), which only parses sources and doesn't
import the build.
Projects
| Project | Version | Size | Build |
|---|---|---|---|
| spring-petclinic | commit 500158f |
50 Java files, 4k lines | Maven, one module |
| jackson-databind | 2.23.0 | 1,300 Java files, 267k lines | Maven, one module |
| Hibernate ORM | 7.4.15 | 22,800 Java files, 2.7M lines | Gradle 9.5, about 25 modules |
Dependencies are downloaded before any run, and Hibernate is built once (./gradlew testClasses)
so its generated parser sources exist. The queries are in benchmark/cases.py,
cases_jackson.py and
cases_hibernate.py: definitions into the project, libraries and
the JDK, hover, references, implementations, type and call hierarchy, signature help and symbols.
spring-petclinic
A small Spring Boot app.
| java-index | jdtls | jdtls light | |
|---|---|---|---|
| Ready to answer, cold / warm start | 1.5 s / 0.4 s | 2.9 s / 1.9 s | 1.6 s / 1.3 s |
| Memory, one window | 256 MB | 1.8 GB | 337 MB |
| CPU time to open one window, cold | 7 s | 57 s | 10 s |
| Disk | 64 KB | 164 MB | 2 MB |
| Query latency (queries it answers) | 0.2–7.6 ms | 0.7–395 ms | 1.1–8.0 ms |
| Find references on a common method | 2 ms | 395 ms | not supported |
| 8 worktrees open at once: all ready | 1.6 s | 8.3 s | 2.7 s |
| 8 worktrees open at once: memory | 418 MB | 12.8 GB | 3.1 GB |
| 8 worktrees open at once: CPU / disk | 11 s / < 1 MB | 434 s / 1.3 GB | 66 s / 18 MB |
| Correct answers (17 queries per window) | 17 / 17 | 17 / 17 | 5 / 17 |
| References and implementations, location by location | same as jdtls | (baseline) | not supported |
jackson-databind
A larger library, about 60 times petclinic.
| java-index | jdtls | jdtls light | |
|---|---|---|---|
| Ready to answer, cold / warm start | 1.7 s / 0.5 s | 3.1 s / 2.2 s | 2.3 s / 2.0 s |
| Memory, one window | 328 MB | 1.8 GB | 619 MB |
| CPU time to open one window, cold | 23 s | 91 s | 56 s |
| Disk | 4 MB | 77 MB | 41 MB |
| Query latency (queries it answers) | 0.6–200 ms | 3.8–505 ms | 8.9–383 ms |
| Find references on a common method (383 callers) | 12 ms | 161 ms | not supported |
| Correct answers (17 queries) | 17 / 17 | 17 / 17 | 4 / 17 |
| References and implementations, location by location | same as jdtls | (baseline) | not supported |
Hibernate ORM
A large multi-module Gradle build, about ten times jackson-databind.
| java-index | jdtls | jdtls light | |
|---|---|---|---|
| Ready to answer, cold / warm start | 3.7 s / 2.8 s | 14.6 s / 3.7 s | not ready after 18 min |
| Memory, one window | 960 MB | 2.0 GB | |
| CPU time to open one window, cold | 46 s | 534 s | |
| Disk | 46 MB | 492 MB | |
| Query latency | 1.0–95 ms | 4.4–1,474 ms | |
| Find references on a common method (196 callers) | 52 ms | 1,474 ms | |
| Correct answers (18 queries) | 18 / 18 | 18 / 18 warm; references empty on every cold run | |
| References and implementations, location by location | same as jdtls | (baseline) |
jdtls needed one setting to import Hibernate at all: its Gradle import fails on Gradle 9 while
annotation processing is on (the default), so the benchmark sets
java.import.gradle.annotationProcessing.enabled to false for Gradle projects.
What the numbers say
Lightweight mode makes jdtls almost as cheap to start as java-index on a small project, but without
the build it has no classpath, and it takes none: java.project.referencedLibraries is ignored in
that mode. It can't do references, implementations, type or call hierarchy, signature help or
workspace symbols, and anything that depends on a library type (definitions and hover into
libraries, and on jackson-databind even picking the right overload of a project method) comes back
empty or wrong. On Hibernate it never became ready.
On a warm start java-index reads its saved file summaries and parses only files that changed, so most of its warm start on Hibernate is walking and hashing 22,800 files. That cache is the disk column: 64 KB to 46 MB, depending on project size.
To be fair about these numbers: it's three projects and 17–18 queries per project that we picked ourselves. We also ran the same kind of queries on a private multi-module Maven project (19 queries, all correct), but can't publish it.
What you get
In your editor (LSP)
| Feature | What you get |
|---|---|
| Go to definition | into your sources, dependency sources jars, the JDK's src.zip, or generated stubs |
| Find references | exact: occurrences that resolve to the same declaration, not just the same name |
| Hover | signature and javadoc, overrides and implementations, usage counts, deprecation with replacement |
| Implementations, type and call hierarchy | subtypes, overrides, supertypes, incoming and outgoing calls |
| Signature help and inlay hints | overloads while typing; parameter names, var and lambda types, member-chain types |
| Code actions | add a missing import, organize imports |
| Diagnostics | uses of @Deprecated APIs, with the suggested replacement |
| Symbols | file outline; classes, methods and fields across sources and libraries |
Spring Boot and Quarkus configuration. Keys in application*.properties and .yml are linked
both ways with the Java that uses them (@Value, @ConfigurationProperties, @ConfigProperty,
@ConfigMapping, getProperty("…")). Jump from a key in Java to where it is set in every profile,
or from a config file to the code that reads it. Hover shows the value per profile and, for library
keys such as server.port, the documented type, default and description. Spring's relaxed binding
works too.
For AI agents (MCP)
| Tool | What it answers |
|---|---|
java_api |
The real API of a class or member as this project compiles against it, from source, dependency jars or the JDK |
java_callers |
Usages of a method, constructor or field, with receiver types resolved so same-named methods on other types are excluded |
java_implementations |
All subtypes of a type or all overrides of a method |
java_outline |
A file's types and members with resolved signatures and line numbers |
java_find_type |
Classes by name, camel-case humps or qualified suffix |
java_config |
A Spring Boot / Quarkus config key: its value per profile, the Java that uses it, and library docs |
Every tool takes an optional worktree path to query another worktree of the same repository.
Grep is still fine for plain text search. java-index helps most with library APIs (no more
guessing signatures), common method names like save, get or execute, and type hierarchies.
Treat its answers as strong hints, not proof, since it can't see reflection or framework wiring.
Install
macOS (Apple Silicon) and Linux, with Homebrew. This repository is its own tap:
brew tap slgobinath/java-index https://github.com/slgobinath/java-index
brew trust --formula slgobinath/java-index/java-index
brew install java-index
jindex --version
Homebrew refuses formulae from third-party taps until you trust them, hence brew trust. Later
releases install with brew update && brew upgrade java-index.
This installs the jindex command: a small native relay, plus the daemon it starts, which runs on
Homebrew's OpenJDK (set JINDEX_JAVA to use another JDK 21+).
Anywhere else (Intel Macs, Windows, or without Homebrew): download java-index-<version>.jar
from the releases and use
java -jar /path/to/java-index-<version>.jar (Java 21+) wherever this README says jindex.
From source (JDK 21+):
mvn package # target/java-index.jar, runs the tests
JAVA_HOME=/path/to/graalvm-25 mvn -Pnative package # also target/jindex-relay, the native relay
target/jindex-relay works like jindex: it finds java-index.jar next to itself (or in
../target, ../lib) and starts the daemon with $JINDEX_JAVA, $JAVA_HOME/bin/java or java
from the PATH. Each editor window or agent session runs one relay; the native one uses about
12 MB instead of a JVM's 60–80 MB.
Set up
jindex starts (or connects to) the repository's daemon:
jindex # LSP over stdio, for editors (same as: jindex lsp)
jindex mcp # MCP over stdio, for agents (--root DIR to override the working directory)
jindex --help # usage; --version prints the version
If you run plain jindex in a terminal it will look stuck. It's just waiting for an editor to
talk to it. Press Ctrl-D to exit.
Claude Code (run in your repository):
claude mcp add java-index -- jindex mcp
Neovim (0.11+):
vim.lsp.config('java-index', {
cmd = { 'jindex' },
filetypes = { 'java' },
root_markers = { '.git', 'pom.xml', 'build.gradle', 'build.gradle.kts' },
})
vim.lsp.enable('java-index')
Helix (languages.toml):
[language-server.java-index]
command = "jindex"
[[language]]
name = "java"
language-servers = ["java-index"]
Emacs (eglot): (add-to-list 'eglot-server-programs '(java-mode "jindex"))
Zed: Zed's Java extension hard-wires jdtls, so editors/zed holds a small
extension that adds java-index as a Java server. Keep the Java extension for syntax highlighting,
install editors/zed with zed: install dev extension, then in settings.json:
"languages": {
"Java": { "language_servers": ["java-index", "!jdtls", "..."] }
}
The extension runs jindex from the PATH; to use the jar instead, add
"lsp": { "java-index": { "binary": { "path": "java", "arguments": ["-jar", "/path/to/java-index.jar"] } } }.
VS Code: Install it from the Visual Studio Marketplace. Java 21 is required to run the daemon. Disable the Red Hat Java extension in workspaces you review with java-index, or you get every answer twice. Settings are in its README.
Sublime LSP / Kate: register a custom stdio server running jindex for Java files.
How it works
editor ──stdio──► jindex (relay) ──unix socket──► daemon (one per repository)
editor ──stdio──► jindex (relay) ─────────────┤
agent ──stdio──► jindex mcp (relay) ─────────────┘
- Routing. Each client runs a small relay. It finds the repository's git common dir (shared by all its worktrees) and connects to that repository's daemon, starting it if needed.
- Shared index. Each source file becomes a declaration summary keyed by content hash. The first worktree indexed becomes the shared base snapshot; other worktrees store only the files that differ, plus unsaved editor buffers.
- Restarts. Summaries are saved per worktree under
JINDEX_HOME, keyed by content hash, so a restarted daemon only parses files that changed since. The cache is dropped when java-index is upgraded. - Dependencies. Jars are read lazily with turbine's bytecode binder, shared across worktrees
with the same build files. Definitions in libraries open their
-sources.jar, the JDK'ssrc.zip, or a stub generated from the class file. - Method bodies. Turbine skips method bodies, so the cursor is resolved from tokens: locals,
var, lambdas, anonymous classes and member chains with generic substitution, overload selection and lambda-based inference. - References. Each file summary carries a Bloom filter of its identifiers, so a search reads only files that may mention the name, then keeps the occurrences that resolve to the same declaration.
- Resources. The relay only forwards bytes; the daemon does the work. It uses a shrinkable heap, compacts when quiet and exits after 15 idle minutes.
Classpath
Resolved per worktree, in this order:
initializationOptions.classpath: a list of jars or jar directories..java-index/classpathin the worktree: one entry per line.- Maven (
pom.xml, uses./mvnwif present):dependency:build-classpath. - Gradle (
build.gradle[.kts], uses./gradlew): an init script that prints the compile classpaths.
Results are cached, keyed by the content of the build files, and recomputed when a build file changes.
Configuration
| Variable | Default | Purpose |
|---|---|---|
JINDEX_HOME |
$XDG_CACHE_HOME/java-index or ~/.cache/java-index |
sockets, daemon logs (<key>.log), extracted sources, cached classpaths and file summaries |
JINDEX_DAEMON_OPTS |
-Xmx1g |
extra JVM flags for the daemon |
JINDEX_IDLE_MINUTES |
15 |
daemon exits after this long with no connections |
JINDEX_JAVA |
java |
JDK (21+) the relay starts the daemon with |
LSP initialization options:
| Option | Default | |
|---|---|---|
classpath |
build tool | see Classpath |
inlayHints.parameterNames |
"literals" |
"all" or "none" |
inlayHints.types |
true |
var, lambda parameter and chain type hints |
deprecations |
true |
mark uses of deprecated APIs |
java -jar java-index.jar lsp --no-daemon and … mcp --no-daemon serve in-process without a daemon (the jar only; the native relay always relays), which is
handy for debugging.
Limitations
Resolution is approximate on purpose. Generic methods are inferred from their arguments and
lambda parameters from the expected functional interface, so stream().map(...).filter(...) and
forEach(item -> item.save()) resolve. Inference that needs the assignment target
(collect(Collectors.toList())) falls back to the type variable's bound. When java-index isn't sure,
it returns nothing instead of guessing. References are exact matches on the resolved declaration:
a call through a subclass that overrides a method counts for the override (the MCP java_callers
tool reports both, in separate groups).
Reporting problems
Missing and wrong answers are the most useful reports: most of the resolver's fixes so far came from real code it got wrong. Please open an issue with:
- the query (go to definition, references, …) and the line of code it was made on, or a small snippet that reproduces it;
- what you expected and what you got (nothing, or the wrong place);
jindex --version, the build tool, and whether the project uses Lombok or other annotation processors (those aren't supported);- the daemon log,
~/.cache/java-index/<key>.log(underJINDEX_HOMEif set). The log of the repository you were in is the one that mentions its path:grep -l "indexed /path/to/repo" ~/.cache/java-index/*.log.
License
Licensed under the Apache License, Version 2.0. The built jar bundles third-party libraries under Apache 2.0, EPL 2.0 and MIT; see NOTICE.
java-index MCP questions
Is java-index a replacement for jdtls or an IDE?
No, it is not a replacement for jdtls or your IDE. It is designed specifically for the reading and reviewing part of Java development, especially for AI agents.
Does it support Lombok or other annotation processors?
No, it does not support Lombok or annotation processors because turbine does not run them.
Can it help with Spring Boot or Quarkus configuration?
Yes, it links configuration keys in properties and YAML files with the Java code that uses them.