npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kealthas-dev/opencode-mcp-java-lsp

v1.1.0

Published

MCP server exposing jdtls (Java LSP) code-intelligence tools - see README.md for design and the config.json-driven config

Readme

java-lsp mcp server

An MCP server exposing real, semantic Java code-intelligence tools — java_definition, java_references, java_hover, java_implementation, java_document_symbols, java_workspace_symbols, java_diagnostics — by spawning and driving a real jdtls (Eclipse JDT Language Server) process over its native LSP stdio protocol. Written in TypeScript (src/server.ts + src/lsp-client.ts + src/types/config.ts, compiled to dist/ — see Run below), built directly against @modelcontextprotocol/sdk, same hand-rolled pattern as mcp-servers/oracle//mcp-servers/loki/. Speaks MCP over Streamable HTTP as a persistent process opencode connects to (type: "remote"), same shape as those two.

This exists because opencode's own built-in jdtls LSP integration only auto-detects a java on PATH (which("java") + a version check) with no way to point it at a different JDK for the actual server process. Owning the spawn logic here means this server decides exactly which java/jdtls to launch, independent of whatever's on PATH for the agent's own shell commands.

Design, and why it looks the way it does

  • A real LSP client, not a reimplementation of jdtls's semantics. src/lsp-client.ts is a minimal, spec-compliant LSP client over stdio (Content-Length framing, JSON-RPC request/response correlation, the initialize/initialized handshake, didOpen/didChange document sync). It does none of the actual Java analysis — that's entirely jdtls's job; this just drives the protocol.
  • One persistent jdtls process for the server's whole lifetime, not one per request. Unlike mcp-servers/oracle's/mcp-servers/loki's deliberately-stateless per-request design, LSP is a genuinely stateful session — project indexing alone takes real time, and jdtls doesn't support concurrent instances against the same -data directory. src/server.ts keeps one LspClient singleton (lazily started on the first tool call) at module scope; the outer MCP/HTTP layer is still stateless-per-request (fresh Server/transport pair per call, same as mcp-servers/oracle/mcp-servers/loki) — those are two independent layers, and only the inner one needed to change.
  • src/lsp-client.ts is duplicated into mcp-servers/spring-lsp/, not shared via a package dependency. Both packages need the same framing/handshake/sync engine, verbatim. Rather than introduce a cross-package file: dependency (which nothing else under mcp-servers/ does — each package there is independently installable), the file is copied. See mcp-servers/spring-lsp/README.md for the same note from that side.
  • File paths are resolved relative to JAVA_LSP_WORKSPACE_ROOT and checked against path traversal (resolveFile() in src/server.ts) — a path that escapes the configured workspace root is rejected before ever reaching jdtls or the filesystem.
  • Line/character positions are 0-indexed, per the LSP spec — not the 1-indexed line numbers most editors display. Documented on every tool's line/character argument, not just here.

Vendoring

jdtls (eclipse-jdtls/eclipse.jdt.ls, EPL-2.0) is vendored here — vendor/jdt-language-server-*.tar.gz (committed, ~49MB), Eclipse's own official milestone build from download.eclipse.org/jdtls/milestones/, the same distribution channel brew install jdtls itself pulls from (checksum verified against Homebrew's own formula at download time). src/server.ts extracts it automatically into a sibling directory on first run (gitignored — see the root .gitignore); nothing to do manually beyond npm install. bin/jdtls inside the extracted distribution is Eclipse's own Python launcher script, not a single binary — needs python3 on PATH in addition to the JDK below. On Windows, src/server.ts's resolveJdtlsCommand() spawns python3 on that script explicitly rather than executing it directly, since Windows spawn() has no shebang support and fails with ENOENT trying to run a shebang script as if it were a native executable — macOS/Linux need no such handling, since their spawn() reads the shebang itself. See fetch-jdtls.sh to reproduce or refresh the vendored build; JDTLS_COMMAND still overrides this entirely, e.g. to point at a separately-installed jdtls instead (brew install jdtls or otherwise) — on Windows that override is expected to already be something spawn() can execute directly (a .exe/.bat/.cmd), since the python3 wrapping only applies to the vendored default.

JDK version

jdtls itself needs a JDK 21+ runtime to launch — that's a property of whatever launches it (its JAVA_HOME), completely separate from what your actual project needs to compile/run against. If your project targets an older Java version, set JAVA_EXECUTABLE (jdtls's own --java-executable flag) to point jdtls at the JDK your project should be analyzed with, without touching the JDK that launches jdtls itself or your shell's default java on PATH — see the intro above for why that separation is the whole point of this package existing as a standalone MCP server.

This package is verified against the vendored jdtls 1.61.0 above (see Vendoring). Vendoring the jdtls distribution itself doesn't remove the JDK-21+-to-launch-it requirement — a system java still needs to be on PATH (or however the process supervisor provides one); see SETUP.md step 8.

Configuration

Config is file-based, not env-var-based — same two-file split as mcp-servers/oracle/ (see its README's Configuration section for the fullest writeup of the pattern):

  • $HOME/.config/kealthas-dev/opencode-mcp-java-lsp/server.json — the port to listen on. JAVA_LSP_MCP_PORT env var overrides it, for running more than one instance (one per project, say). Otherwise optional: if missing, defaults to 8092; if present, must be valid JSON or the server refuses to start. Shape (see server.example.json):
    { "JAVA_LSP_MCP_PORT": 8092 }
  • A config file, read once at startup (unlike mcp-servers/oracle/mcp-servers/loki, not re-read per call - jdtls holds a stateful session against one workspace, so switching config means restarting the process). The location it's read from is never user-supplied — only a short environment/project name is, via JAVA_LSP_CONFIG_ENV; see mcp-servers/oracle/README.md's Configuration section for why. With no JAVA_LSP_CONFIG_ENV set, it's read from config.json; with JAVA_LSP_CONFIG_ENV=my-project, from config-my-project.json instead. Required (one file or the other must exist) — the server prints a sample and exits if the resolved file doesn't exist or JAVA_LSP_WORKSPACE_ROOT/JDTLS_DATA_DIR is missing from it. Shape (see config.example.json):
    {
      "JAVA_LSP_WORKSPACE_ROOT": "/path/to/your/java/project",
      "JDTLS_DATA_DIR": "/path/to/a/scratch/dir/jdtls-data",
      "JDTLS_COMMAND": "/path/to/some/other/jdtls",
      "JAVA_EXECUTABLE": "/path/to/jdk8/bin/java"
    }
    JAVA_LSP_WORKSPACE_ROOT — absolute path to the Java project jdtls should analyze. JDTLS_DATA_DIR — jdtls's own workspace/index storage directory (its -data flag), not the project root; dedicate one per project — jdtls refuses to share a -data dir across concurrently-running instances for different projects. JDTLS_COMMAND — optional, the jdtls launcher, defaults to the vendored jdtls above. JAVA_EXECUTABLE — optional, see "JDK version" above.

Run

See docs/java-lsp-spring-lsp-quickstart.zh.md for a bare-minimum copy-paste version of the local-dev path below.

Published as @kealthas-dev/opencode-mcp-java-lsp — on a real deployment, install it globally and run the resulting binary:

npm install -g @kealthas-dev/opencode-mcp-java-lsp
mkdir -p ~/.config/kealthas-dev/opencode-mcp-java-lsp
# real config at ~/.config/kealthas-dev/opencode-mcp-java-lsp/config-my-project.json (see config.example.json for the shape)
JAVA_LSP_CONFIG_ENV=my-project opencode-mcp-java-lsp

For local dev/testing against this repo's own checkout (this directory, not the published package), same idea — drop a real config file at the default location, or a named config-<name>.json (see config.example.json for the shape):

npm install
npm run build
npm start   # reads ~/.config/kealthas-dev/opencode-mcp-java-lsp/config.json

npm run dev runs src/server.ts directly via tsx watch instead, for a compile-on-save loop.

Either way, point opencode at it with a type: "remote" entry (see deploy/opencode.json.example) — it needs to already be running and stay running, since opencode connects rather than spawns it.

Status

Verified end-to-end against a real jdtls, both manually and by java-lsp.test.ts. All seven tools were run against a real jdtls 1.61.0 process (the vendored one above) and a small real Java file, confirming: initialize handshake succeeds; java_document_symbols returns the real parsed class/methods (not text matches); java_hover returns the real resolved type signature; java_references finds the real declaration + call site (2 results, not a name-text grep's false positives); java_definition/java_implementation resolve real cross-references; java_workspace_symbols fuzzy-matches the real symbol index; java_diagnostics returns a real compiler diagnostic (a genuine package-mismatch warning from the test fixture, confirming this isn't a stubbed-empty response). java-lsp.test.ts covers four of these (java_document_symbols, java_hover, java_references, java_workspace_symbols) as automated assertions, plus a path-traversal rejection check — java_definition/java_implementation/java_diagnostics are only verified manually, above.

Not yet wired into tests/run-in-container.sh / the docker/ sandbox — the sandbox's base image has no JDK, and adding one needs explicit sign-off, same as any new download source (see mcp-servers/TODO.md). Run java-lsp.test.ts directly on a machine with a JDK 21+ java, python3, and (for now) a jdtls binary on PATH (e.g. brew install jdtls) — the test's own preflight check still requires one on PATH, even though the server process it spawns launches the vendored copy regardless of what that check finds. A real deployment (SETUP.md step 8) only needs the JDK and python3; the vendored jdtls handles the rest there.

Not tested: the Windows python3-wrapping spawn path (see Vendoring above) — verified by reading Node's spawn()/Windows shebang behavior and a tsc build, not by running a real jdtls on Windows; the target deployment machine (docs/deployment-environment.md) is Windows, so this is worth confirming there. A real multi-file/Maven/Gradle project (only a single loose .java file was used — jdtls's cross-file resolution across a real dependency graph should work the same way in principle, since that's exactly what jdtls itself is for, but wasn't specifically exercised here). Concurrent tool calls while jdtls is still indexing a large project. Behavior once JDTLS_DATA_DIR already has a populated index from a previous run.