Ask an AI coding agent "where is handleAuth defined?" and most of them do the same thing you'd do at 2am with no editor: grep -r handleAuth .
That works, sort of. It also returns the comment mentioning handleAuth, the string "handleAuth" in a log line, the unrelated handleAuth in a vendored dependency, and the one in a test fixture. The agent picks one and hopes.
There's a better source of truth sitting right there in almost every project — the same one your editor uses when you hit "Go to Definition." We just shipped it in loop, our open-source terminal coding agent, and the results were more interesting than we expected.
The two problems with searching code as text
Text search doesn't know what a symbol is. grep matches characters. It cannot tell a definition from a mention, a type from a variable that shares its name, or live code from a comment. For a name like run, get, or Config, the signal-to-noise ratio collapses entirely.
Worse, some questions can't be phrased as a search at all. "What calls this function?" has no textual form — you'd have to find every reference, then work out which ones are calls rather than imports or re-exports, then repeat that for each alias. Agents mostly don't try.
The agent finds out it broke something far too late — or never. This is the bigger issue. An agent edits a file, moves on, edits three more, and only discovers the type error when someone finally runs the build. By then the mistake is buried under later changes, and the fix is more expensive than it should have been. Sometimes nobody runs the build at all and the error ships.
Both problems have the same answer, and it's been standardized since 2016.
What a language server actually gives you
The Language Server Protocol is how your editor knows things about your code. A language server holds a real model of the program — parsed, type-checked, cross-referenced — and answers structured questions about it: where is this defined, what references it, what type is it, what calls it, what does it call.
Two capabilities matter most for an agent.
Navigation that's actually correct. "Find references" returns the places a symbol is genuinely used, skipping comments, strings, and unrelated same-named symbols. "Incoming calls" gives you a real call graph — the question text search cannot express.
Diagnostics, delivered at the moment of the mistake. This is the one that changes agent behavior. When an edit introduces a type error, the error arrives attached to that edit:
LSP errors detected in this file, please fix:
<diagnostics file="src/auth.ts">
ERROR [42:11] Type 'string' is not assignable to type 'number'.
</diagnostics>
The agent doesn't have to remember to run the type checker. It doesn't have to decide the code is worth verifying. The feedback is simply there, in the result of the action that caused it, while the relevant context is still in front of it. In our testing this is the single highest-leverage change: the write → verify loop closes itself.
How we built it into loop
We shipped it as a built-in extension rather than baking it into the core. loop already had an extension system that could add tools and transform tool results, so the entire feature — 37 language servers, a navigation tool, and the diagnostics pipeline — plugs into public API surfaces. Nothing about language servers leaked into loop's core.
That was a deliberate constraint, and it paid off twice: the extension is opt-in (loop enable lsp), so nobody pays for a feature they don't want, and building it exposed exactly which extension APIs were missing.
Three things we learned
TypeScript 7 doesn't need a wrapper anymore. Every agent we looked at runs typescript-language-server — a Node process wrapping the TypeScript compiler. But TypeScript 7 is a native Go binary that speaks LSP itself: tsc --lsp --stdio handshakes and advertises hover, definitions, references, call hierarchy and diagnostics. So loop talks to the type checker directly. No wrapper, no Node process in between. If you're building something similar, check this before copying the older setup.
LSP has two diagnostic mechanisms, and picking one silently breaks you. The older model pushes diagnostics when analysis settles. The newer one — which TypeScript 7 uses — only answers when asked. We initially listened for pushes only, and against a pull-only server that means you wait out your timeout and conclude the file is clean. The feature looked like it was working and never fired once. If you implement this, support both and merge the results.
Don't auto-download compilers. It's tempting to make every language work with zero setup. We install servers automatically only where it's safe and cheap — TypeScript and ten npm-based servers, plus gopls if you already have Go. Everything else has to be on your PATH. rust-analyzer, clangd and jdtls belong to toolchains developers manage deliberately, and silently downloading a compiler's tooling is worse behavior than saying "not installed."
Details that turned out to matter
A monorepo needs one server per package, not one confused server for the whole tree — so root detection walks up from the edited file to the nearest project marker. A deno.json has to stand the TypeScript server down, or every diagnostic gets reported twice. And a single file is often served by several servers at once (a type checker and a linter say genuinely different things), so their output is merged and deduped rather than one winning.
The shape of the result
37 servers covering 75 file extensions: TypeScript, JavaScript, Vue, Svelte, Astro, Go, Rust, C/C++, Python, Ruby, PHP, Java, Kotlin, C#, Swift, Zig, Elixir, Haskell, OCaml, Clojure, Lua, Dart, Julia, Terraform, Nix, Typst and more.
Turning it on is one command:
loop enable lsp
From there the agent gets type errors with its edits and can ask real questions about your code instead of grepping for names.
Why this generalizes
The specific integration is loop's, but the principle isn't: give the model the same structured information a human developer has, at the moment they'd have it.
Developers don't grep for definitions — they jump to them. They don't discover type errors during code review — the editor underlines them as they type. Agents that work from raw text are doing the job with a significant handicap, and the fix is mostly a matter of plumbing tools that already exist on every developer's machine.
loop is open source and MIT licensed: github.com/notshekhar/loop.