Stop Letting AI Grep Through JAR Files
Last Tuesday I watched Copilot CLI unzip a JAR file into /tmp and grep through .class files for a method signature. The thing is, that is what passes for code understanding when your AI agent is flying blind. The agent was not being lazy. It just did not have a language server, so text patterns were all it had.
Here is the part that took me a while to internalize. Without a language server, Copilot CLI is not actually a code assistant. It is a fancy grep wrapper with autocomplete bolted on. It sees your code as text. It does not know that T extends Comparable<T> is a type bound. It does not know which overload of execute() your code is calling. It does not know that the import you removed last week was actually re-exported by a transitive dependency. It is doing archaeology when it should be doing engineering.
GitHub’s answer, quietly rolled out in mid-2026, is the LSP Setup skill for Copilot CLI. Instead of teaching the agent to grok bytecode, the skill installs and configures Language Server Protocol (LSP) servers for fourteen languages. Same intelligence your IDE uses for jump-to-definition and find-references, now wired into your terminal agent. Once the server is up, the agent stops guessing and starts knowing.
What Your Terminal Agent Is Actually Doing Right Now
Without an LSP server configured, Copilot CLI defaults to heuristics. The pattern looks something like this when you ask it to understand a Java method:
# Find the dependency JAR
find ~/.m2/repository -name "httpclient.jar"
# Extract it somewhere temporary
mkdir /tmp/httpclient && cd /tmp/httpclient
jar xf ~/.m2/repository/org/apache/httpcomponents/httpclient/4.5.14/httpclient-4.5.14.jar
# Hope grepping binary finds what you need
grep -r "execute" --include="*.class" .
For Python, the agent cats files in site-packages. For TypeScript, it walks node_modules. For Rust, it greps through target/. These text-based hacks work for toy examples, but they miss every interesting thing about a real codebase:
- Generic type bounds and type parameter resolution. The agent sees
Comparable<T>and treats it as a string. It cannot tell you whetherTisStringorLocalDateTimein the call site. - Method overload disambiguation. Three methods named
executeexist. The agent does not know which one your code calls. - Transitive dependencies that are not directly imported. The class you use came from a JAR that came from another JAR. The agent does not see the chain.
- Compiled bytecode at all. Anything shipped as
.classfiles,.wasm, or native binaries is opaque. Most enterprise Java code lives here.
Every request burns CPU on extraction and scanning. Every request also has a high chance of returning the wrong answer in a way that looks plausible. The agent is confidently incorrect, which is worse than honestly lost.
Why Language Servers Change the Equation
The Language Server Protocol was standardized years ago for VS Code and friends. It is a client-server specification for editor tooling. An LSP server maintains a semantic model of your codebase in memory. When the client asks “what is this symbol,” the server returns the exact definition site, the fully resolved type, the inheritance hierarchy, and every cross-reference.
Copilot CLI is now a client.
When the agent needs to understand a Java interface, it sends a textDocument/definition request over stdio. The Eclipse JDT language server (the same engine Eclipse has used for two decades) responds with:
- The precise file and line where the interface is declared.
- All generic type parameters bound to concrete types in this call site.
- Which overloaded variant of the method applies in this context.
- References across the project, including transitive dependencies in the classpath.
That is the difference between “here is a string that looks like a method name” and “here is the actual typed signature with its contract.” Once the agent has the typed signature, its suggestions stop being plausible and start being correct. The grep wrapper becomes a code assistant.
The LSP Setup Skill, Step by Step
An agent skill in Copilot CLI is a reusable instruction set written in Markdown with YAML frontmatter. It defines triggers, workflow steps, reference data, and constraints. The LSP Setup skill automates a seven-step workflow that used to be a weekend of yak-shaving.
Step 1: Language selection. The agent prompts with a choice list of supported languages. Your pick drives every subsequent step. Java, Python, TypeScript, Go, Rust, C/C++, C#, Ruby, PHP, Kotlin, Swift, Scala, and Lua are all in.
Step 2: OS detection. Runs uname -s or checks $env:OS on Windows. The commands differ fundamentally between macOS, Linux, and Windows, and getting this wrong is the most common failure mode for manual setup.
Step 3: Package manager detection. Figures out what you actually have. Homebrew on Mac? apt on Debian? choco on Windows? winget? The skill maps the OS-language pair to the right installer without you having to read any README.
Step 4: Installation command execution. Generates the exact install string for that language server on that OS. No guesswork, no manual copy-paste from a five-year-old Stack Overflow answer.
Step 5: Configuration generation. Writes a JSON config file in the standard location Copilot CLI expects. Usually ~/.copilot/skills/lsp-setup.json. The format is plain JSON and easy to inspect if you want to see what the agent decided.
Step 6: Path verification. Confirms the binary is discoverable in $PATH or wherever the agent needs to invoke it. If the install put the binary somewhere unusual, the skill catches it here.
Step 7: Health check. Sends a basic LSP initialize request to confirm the server responds correctly. If the server is broken or misconfigured, you find out now, not in the middle of a refactor.
The entire process is deterministic and reproducible. Compare that to the previous behavior of “extract JAR, grep classes, hope for the best.”
What the Config Actually Looks Like
After the skill runs, Copilot CLI stores a lightweight mapping between language identifiers and server launch commands. A typical generated config for a polyglot workspace looks like this:
{
"servers": {
"java": {
"command": "jdtls",
"args": ["-data", "/workspace/jdt-data"],
"rootUri": "${workspaceFolder}"
},
"python": {
"command": "pylsp",
"rootUri": "${workspaceFolder}"
},
"typescript": {
"command": "typescript-language-server",
"args": ["--stdio"],
"rootUri": "${workspaceFolder}"
},
"go": {
"command": "gopls",
"rootUri": "${workspaceFolder}"
},
"rust": {
"command": "rust-analyzer",
"rootUri": "${workspaceFolder}"
}
}
}
Each entry is minimal because LSP itself handles the heavy lifting. The agent just needs to know which binary to spawn and how to talk to it over stdio.
When the agent encounters an unknown symbol, it checks this config, starts the relevant server if not already running, and fires off the appropriate LSP request. The server lives for the session, caching the project model in RAM for fast responses. When the session ends, the server is reaped. There is no persistent background daemon.
Languages That Work Today
As of the latest release, the LSP Setup skill covers fourteen languages, and the list is growing. Java gets JDT, the same engine Eclipse uses. TypeScript and JavaScript share typescript-language-server, which is the most polished server in the list. Rust gets rust-analyzer, which is widely considered the best Rust tooling available anywhere. C and C++ get clangd, LLVM’s official server, battle-tested at Google scale. Go gets gopls. Python gets python-lsp-server. C# gets OmniSharp. Ruby gets solargraph. PHP gets phpactor or intelephense. Kotlin, Swift, Scala, and Lua round out the list with their respective community-maintained servers.
For languages not yet covered, you can manually add a config entry pointing at any LSP-compatible server binary you have installed. The skill also writes a sample config for unsupported languages when it finishes, so you can copy and modify.
Trade-Offs Worth Knowing
Nothing is free. Adding LSP to Copilot CLI introduces real overhead.
Memory footprint. Each language server holds your project’s semantic model in RAM. A large Java codebase with JDT can consume 2 to 4 GB. If you are on a machine with 8 GB total, running multiple servers simultaneously will hurt. On a 16 GB machine, it is fine. On a 32 GB machine, you stop noticing.
Startup latency. The first LSP request for a session triggers server initialization. For big projects, this can take 10 to 30 seconds. Subsequent requests are instant, but the initial wait is real. Plan for it by triggering an LSP request early in the session, not when you are mid-thought.
Dependency on external binaries. If the LSP server is not installed or not in PATH, Copilot CLI falls back to the old heuristic behavior. You get degraded accuracy without a clear warning unless you check the logs. The skill’s health check at the end of setup catches this, but only if you let it run.
Not a silver bullet. LSP servers understand static code semantics. They do not know your runtime environment, your database schema, or your business logic. The agent gets smarter about types but is still genuinely dumb about context. Asking it to “find every place we charge a customer” still requires you to point it at the right domain module.
If your project fits in a single language and you have a beefy machine, the value is huge. If you work across five languages on an 8 GB laptop, you will need to pick which servers to keep loaded.
Setting It Up Yourself
Trigger the skill from Copilot CLI with a natural language command. Phrasings like “set up language server for Java” or “configure LSP for Python” all work. The skill recognizes the trigger from its frontmatter.
From there, it is mostly automated. You get prompted for the language choice, and the skill handles the rest. On some systems you may need to install the language server binary separately beforehand if your package manager does not carry it. The skill tells you exactly what is missing.
To verify it is working, ask Copilot CLI to explain the type of a complex generic expression in your code. With LSP active, it will resolve the concrete type parameters. Without, it will paraphrase the text and often get the nesting wrong. The difference is obvious once you see it.
If you want to inspect what the agent decided, the config lives in ~/.copilot/skills/lsp-setup.json. It is plain JSON and safe to edit by hand. Adding a custom language is just adding a new entry with a command and an optional args list.
What I Would Tell Past Me
Three things, in order of how much pain they would have saved:
- Set up LSP on day one of using Copilot CLI for a non-trivial codebase. The setup takes two minutes once the skill exists. The grep-wrapper behavior wastes hours per week on bad answers, and you do not realize how bad the answers are until you turn it on and see the difference.
- Trust the typed signature over the textual paraphrase. When the agent gives you a method signature with full generic resolution, that is the real answer. When it gives you a sentence describing what the code “seems to do,” that is a guess. If the session feels like the agent is being verbose, the LSP server probably is not running.
- Read the health-check output before trusting the setup. A language server that starts but cannot resolve your project root is worse than no server, because the agent does not fall back gracefully. The skill’s health check catches this. Run it.
Bottom line: Copilot CLI without a language server is clever but blind. The LSP Setup skill fixes this at the architecture level by wiring the agent into the same semantic tooling your IDE already uses. It costs you RAM and ten seconds of startup. It gives you back correct answers instead of plausible ones. For anyone doing serious terminal-driven development, that trade is obviously worth it.