The Compiler Should Be Part of the AI Context, Not Just the Build Pipeline

For a while, when we talked about making developer tools “AI-native,” we mostly meant making them easier for an agent to call. Expose an API. Add an MCP server. Give the agent access to the repository. Let it run tests, execute commands, inspect logs, call external tools.

All of that is useful. But it’s still only half the problem.

A modern coding agent still spends a surprising amount of its time doing something fairly primitive: reading source code as text, forming an assumption, changing something, running the compiler, reading the error message, going back to the source, forming another assumption, trying again. It works, often impressively well, but it’s a wasteful way to work.

The compiler already knows more than the agent does

A compiler or a language server doesn’t see your code as a pile of text files. It knows about symbols, types, overload resolution, references, inheritance, nullability, accessibility, generic constraints, diagnostics, project dependencies, and a lot more.

Take a simple example: an agent wants to change the signature of a method. Today it will typically search for the method name, read a handful of files, guess which references are relevant, modify the method, run the build, get five compiler errors, read those errors, open five more files, fix them, build again, and sometimes repeat that loop more than once.

A developer working inside a good IDE does something different. When you rename a symbol in GoLand, RustRover, or Visual Studio, the IDE doesn’t grep the repository and hope for the best. It asks the semantic model what that symbol actually is and where it’s used. That’s the gap I want to talk about.

To be fair, this gap isn’t a secret, and it’s worth being precise about who’s already working on it, because the landscape has moved fast this year.

The Language Server Protocol has offered semantic access (go-to-definition, find-references, safe rename, diagnostics) since 2016, but it was built for IDEs talking to humans. What’s new is connecting that to agents through MCP, and the maturity here varies a lot by language.

Go is the most advanced case I’ve found: gopls, the official Go language server, now ships a built-in MCP server directly from the Go team. It runs either attached to a live gopls session (so it can see unsaved buffer state, not just files on disk) or detached against a headless session. That’s not a third-party wrapper, it’s the language’s own tooling exposing itself.

Rust, C#, and TypeScript aren’t there yet at the official level, but the community has filled the gap with more than one serious attempt each. For Rust, there are several MCP bridges around rust-analyzer, some fairly thin (hover, definitions, references), others more ambitious, like a 19-tool server that also does refactoring and Clippy suggestions. For C#, there’s a similar spread: lightweight Roslyn-based servers for validation and find-usages, and at least one with 41 tools covering refactoring, navigation, and code generation, with preview mode and rollback on every write operation, which is a detail worth remembering for later in this piece. For TypeScript, the more interesting recent work is servers built directly on tsc –lsp (TypeScript’s native Go-based compiler) instead of the older Node-based tsserver, specifically because it’s faster on large projects, which tells you performance is already a live design concern for whoever is building these bridges, not just something I’m speculating about.

So the raw capability isn’t new, and it isn’t hypothetical. What’s still missing, even in the gopls case, is treating it as a first-class part of how an agent reasons, and building an interface shaped for a model rather than reusing the same shape built for a human clicking through an IDE.

We’re wasting intelligence rediscovering what the compiler already knows

LLMs are good at reasoning over incomplete information. That’s useful when the information really is incomplete. But asking a model to infer things Roslyn, gopls, or rust-analyzer already know with certainty isn’t a smart use of that ability.

There’s a practical cost too. Every cycle of read source, reason, change, compile, parse error, read more source, reason again means more latency, more tool calls, more tokens. At small scale nobody notices. At the scale of thousands of developers and millions of agent runs a day, those loops turn into real infrastructure spend. There’s also a cost we don’t talk about enough in developer tooling: compute isn’t free from an energy standpoint either. If an agent can skip two or three model round-trips because the compiler answers a semantic question directly instead of through trial and error, that’s not a minor efficiency win. Over enough repetitions it adds up to a meaningfully different cost profile. Not because every token needs optimizing, but because it’s wasteful to spend probabilistic reasoning on deterministic information that already exists somewhere in the toolchain.

AI-native should mean more than “callable by an agent”

This is where I think the definition needs to move. An AI-native compiler isn’t a compiler with an MCP endpoint bolted onto it. An AI-native language server isn’t just something an agent can invoke. The real question is what semantic capabilities should the compiler expose so an AI system can reason about the program directly, in a shape that’s actually useful for a model rather than for a human clicking through an IDE.

It helps to be honest that not all of these capabilities are equally hard to provide. Roughly:

Already solved, mostly a plumbing problem. “What symbol does this expression resolve to?” “Which overload will be selected here?” “Where is this symbol referenced?” Language servers answer these today, reliably, and as the examples above show, several of them are already reachable by an agent right now. The remaining work is exposing them in a token-efficient shape, not inventing new analysis.

Solvable but expensive at scale. “What will break if I change this public contract?” “Which projects depend on this type?” “What’s the call graph around this method?” These require whole-program or cross-project analysis. Language servers can compute them, but on a large monorepo this can be slow enough that a naive compile-and-read-the-error loop is sometimes actually cheaper. This is a real trade-off the “just ask the compiler” framing tends to skip over. Incremental analysis and caching help, but they don’t make the cost disappear, and it’s exactly why some of the community bridges above are already optimizing for raw query speed.

Only approximately answerable, and that’s fine. “Is this refactoring semantically safe?” is the interesting one. In general this question is not fully decidable: reflection, dynamic dispatch, code generation, and serialization by field name can all make a “safe” rename unsafe in ways no static analysis will catch. I don’t think that’s a reason to drop the question, but an agent (and the person prompting it) should understand it’s getting a confidence estimate, not a guarantee, the same way a human refactoring tool gives you “should be safe, but check these N spots” rather than a proof. This is likely why at least one of the C# bridges I mentioned builds in preview mode and rollback for every write: not as a nice-to-have, but as an acknowledgment that “safe” is a probability, not a certainty.

Mixing all of these into one flat list of questions an agent “should be able to ask,” as if they were equally certain, undersells how much of this is genuinely hard.

From text context to semantic context

Most AI coding systems today are good at building text context: picking files, diffs, terminal output, docs, and stuffing them into the prompt. That’s not going away. But the next step is semantic context, and I think this is where the real differentiation from plain LSP access lives, and also where today’s bridges (gopls included) still fall short.

LSP was designed to answer a human clicking on a symbol: show me one definition, one hover tooltip, one list of references to scroll through. An agent doesn’t want that shape of answer. It wants something like “here is the minimal type hierarchy relevant to this change” or “here is a structured explanation of why these five errors are actually one root cause,” summarized and filtered for what fits in a context window and what’s actually relevant to the diff at hand, not a raw dump of everything the language server knows. As far as I can tell, none of the servers I looked at reshape their output this way yet. Almost all of them are wrapping the same LSP-shaped responses (positions, ranges, symbol lists) that an IDE would render on screen. That’s the gap that’s still open.

So instead of giving an agent ten files and asking it to reconstruct a type hierarchy, give it the relevant slice of that hierarchy. Instead of two hundred lines of compiler output, let the compiler return a structured explanation of the dependency that caused the errors. Instead of grepping for the string CustomerId, ask the language server for references to the exact CustomerId symbol.

That sounds like a small implementation detail. I don’t think it is. It changes the interaction from “here’s some text, figure out what the program probably means” to “here’s what the program means, now reason about what to change.”

The compiler as part of the reasoning loop, not just the judge at the end

I don’t expect AI to replace compilers, static analyzers, or language servers. Quite the opposite, I think it makes them more important. A good agent should combine probabilistic reasoning with deterministic tools:

Article content

Today we mostly use the compiler as: “I changed something, tell me if I broke it.” A more AI-native workflow uses it as: “I want to make this change, tell me what this symbol means, what depends on it, and what constraints I need to preserve, before I write the diff.” That alone could remove a lot of unnecessary trial-and-error cycles, though as above, only for the class of questions the compiler can actually answer cheaply and with certainty. For the rest, it should still shorten the loop, just not eliminate it.

MCP is useful, but MCP is not the architecture

MCP made tool integration much easier, and that’s genuinely good. But exposing a tool protocol isn’t the same as designing an AI-native development environment. The protocol is plumbing, the interesting design question is what sits behind it.

A compiler that only exposes build() is technically callable by an agent. A compiler that exposes its semantic model, dependency graph, diagnostics, and refactoring capabilities, in a shape built for a model to consume cheaply, is something else. The gopls MCP server is a good example of where the industry actually is right now: it’s honest that it exposes “a subset” of gopls’s functionality, and it’s explicitly a wrapper around the same operations an ordinary IDE session would perform. That’s a reasonable first step, and probably the right way to ship something like this incrementally, but it’s still step one, not the destination.

It’s also worth pausing on something the gopls documentation is unusually upfront about: security. Their own docs list what the MCP server can actually do underneath, including reading files from disk, executing the go command (which can reach the network to fetch modules), and writing to local caches and config. None of that is new behavior compared to a normal IDE session, but handing those same capabilities to an autonomous agent instead of a human clicking buttons is a different risk profile, and it’s a dimension this piece hasn’t touched on until now. The more semantic and “deep” we make this access, the more we also need to be explicit about what an agent could do with it if a prompt or a piece of untrusted code steers it somewhere it shouldn’t go. That’s a design question for AI-native tooling just as much as latency or token cost is.

This also changes how we think about programming languages, a little

For years we compared languages and ecosystems on things like runtime performance, type systems, memory safety, tooling, package ecosystems, build speed, IDE support. I think another dimension is becoming relevant: how well can the language’s tooling collaborate with software agents. Not how much AI-generated code exists for it, and not whether some IDE has a chat window bolted on, but whether the compiler, language server, and runtime expose structured information an agent can use to understand and safely change a real system.

I’ll admit this is more of a hunch than something I can back with hard numbers yet, though the current landscape gives a small hint: Go currently has the most direct answer to this question of any language I looked at, precisely because the tooling ships the bridge itself instead of leaving it to the community. Statically typed languages with mature tooling (C#, TypeScript, Rust, Go) probably have a real head start in general, since so much of what an agent would want to ask is already computable from their compilers. Dynamically typed languages don’t lose this by definition, but they likely need more work (and more runtime information, not just static analysis) to get to the same place. I’d rather flag this as an open question than claim it’s settled.

The goal isn’t to let AI write more code

We already know AI can generate a lot of code. I’m more interested in whether it can make better changes with fewer guesses, and that needs a different kind of integration than “give the agent a terminal and a build command.”

The compiler shouldn’t be something the agent discovers after it’s already made a mistake. It should be part of the context the agent uses before making the change. We spent decades building compilers and language servers that understand our programs with real precision. It would be a strange waste if the next generation of coding tools kept treating those programs as nothing more than text.


I also prepared a small demo showing how an AI model can interact directly with the .NET Compiler Platform (Roslyn).

https://github.com/aminmesbahi/roslyn-agent-demo

Leave a Comment