DEV Community

Cover image for How I reduced AI coding context by 95%
Alex
Alex

Posted on

How I reduced AI coding context by 95%

Every time I watched an AI coding assistant work on a large TypeScript project, I noticed the same pattern.

It wanted to answer something simple like:

  • "Where is this hook defined?"
  • "Who calls this function?"
  • "What's the type of this value?"

Instead of reading a single symbol, it often opened an entire file.

Sometimes that meant loading 50 KB... sometimes 100 KB... just to extract a 30-line function.

Multiply that by dozens of requests during a coding session, and you quickly end up wasting thousands of tokens on code the model never actually needed.

So I built SymbolPeek.

What it does

SymbolPeek is an MCP server that gives AI coding agents symbol-level access to a codebase.

Instead of asking for a whole file, an agent can ask for exactly what it needs:

  • read a single function
  • find all references
  • navigate to definitions
  • inspect callers and callees
  • resolve inferred TypeScript types
  • inspect call hierarchies
  • read only the surrounding context of a symbol

The result is dramatically smaller context with much richer information than plain text search.

Example

Imagine a file with nearly 1,800 lines.

Instead of this:

Open the entire file.

the model can simply ask:

read_symbol(
  path: ".../worker.js",
  symbol: "createProject.collectImports"
)
Enter fullscreen mode Exit fullscreen mode

and receive only the function it requested.

In one real example from the project itself, the response was about 2 KB instead of reading a 65 KB source file.

The model gets exactly what it asked for—and nothing else.

Real usage

I wanted to know whether semantic navigation actually reduces context consumption for LLMs.

So I added lifetime statistics.

After normal day-to-day development, these are my current numbers:

Requests:                         162
Files avoided:                    163
Lines avoided:                    352,910
Bytes avoided:                    6.4 MB

Estimated tokens saved:           ~1.61M
Average context reduction:        95.7%
Enter fullscreen mode Exit fullscreen mode

These aren't synthetic benchmarks.

They're collected during real development while working with AI coding agents.

Each request compares the semantic response against the counterfactual of reading the complete source files involved.

The result surprised me more than I expected.

More than 95% of the source context simply wasn't necessary.

Why not just use grep?

Text search is fantastic.

I still use grep every day.

But grep doesn't understand:

  • import aliases
  • barrel exports
  • inferred generic types
  • call graphs
  • definitions
  • semantic references

The TypeScript compiler already knows all of this.

Instead of parsing text again, SymbolPeek simply exposes the compiler's knowledge through MCP.

Multi-language support

The project started as a TypeScript tool because that's where semantic navigation provides the biggest payoff.

Today it supports:

  • TypeScript
  • JavaScript
  • Rust
  • Python
  • Java
  • Go
  • JSON
  • Markdown

TypeScript and JavaScript use the official TypeScript Compiler API for semantic analysis.

Rust, Python, Java, Go, JSON and Markdown currently use Tree-sitter for fast syntax-aware navigation.

That means the same MCP server can be useful across mixed-language repositories instead of only TypeScript projects.

What makes it different?

One thing I wanted to avoid was building "grep over MCP."

If a language already has a production-grade compiler capable of answering semantic questions, why ignore it?

For TypeScript, SymbolPeek uses the compiler itself to provide:

  • compiler-resolved references
  • module resolution
  • path aliases
  • barrel re-exports
  • inferred generic types
  • diagnostics
  • call hierarchy

The information already exists.

The MCP server simply exposes it to AI coding agents.

Current capabilities

For TypeScript and JavaScript:

  • symbol navigation
  • references
  • callers
  • callees
  • definitions
  • diagnostics
  • call hierarchy
  • type inspection
  • document outline

Other supported languages currently provide syntax-aware navigation through Tree-sitter.

The goal

The goal isn't to replace grep.

It isn't to replace reading source files.

It's to eliminate one of the most expensive workflows AI agents perform every day:

Open a huge file just to inspect one declaration.

Less context.

Fewer tokens.

Better signal.

I'd love feedback

I'm especially interested in hearing from people building AI coding tools or working with:

  • Codex
  • Claude Code
  • Cursor
  • Cline
  • Roo Code
  • Windsurf
  • other MCP-enabled agents

Have you ever watched an AI spend thousands of tokens reading files just to answer a simple navigation question?

I'd love to hear how you're solving that today.

GitHub:

https://github.com/pioner92/symbolpeek-mcp

Top comments (11)

Collapse
 
merbayerp profile image
Mustafa ERBAY

I really like the direction here. Treating the compiler as the source of truth instead of asking an LLM to rediscover semantic information from raw text feels like the right abstraction.

One thing I’d be careful about is generalizing the 95% reduction. It makes perfect sense for navigation tasks (definitions, references, call hierarchy), but architecture-level reasoning often benefits from broader context than a single symbol.

One idea that could be interesting is making the MCP server context-aware over time. Instead of returning only symbols, it could expose a semantic dependency graph and support incremental queries based on what the agent already knows. At that point, you’re no longer optimizing file reads—you’re optimizing knowledge acquisition.

Collapse
 
pioner92 profile image
Alex

Yeah, the 95% is fair to poke at. It's just the average over the requests I've actually run, and those are almost all navigation stuff — "where's this defined", "who calls this", that kind of thing. So of course it comes out high. It was never meant as "every task shrinks 95%." When I'm actually trying to understand how a chunk of the system fits together I still read the whole file or grep around like normal. The tool isn't trying to win that case. It's just trying to not dump a 2k-line file when all the agent wanted was one function.

The graph thing is interesting because part of it already sort of exists-

  • find_references
  • find_callers
  • find_callees call hierarchy / dependencies basically let the agent walk the graph one edge at a time (real semantic info for TS/JS through the compiler, syntax-level for the tree-sitter languages)

But you're pointing at the harder part it doesn't do: it's completely stateless. Every call starts from zero and it has no clue what the agent already pulled, so it'll happily hand back the same subtree it gave you two calls ago.

Making it remember that within a session and only return the delta is honestly not how I'd been thinking about it, and it's a better framing than "save file reads." No idea when I'd get to it, but it's going to be stuck in my head now

Collapse
 
merbayerp profile image
Mustafa ERBAY

I think that’s where it starts becoming less like a symbol server and more like a session-aware knowledge layer.

Instead of remembering “the agent already read this symbol,” it could remember what semantic facts have already been established during the session.

For example:

  • this type hierarchy is already known
  • this call path has already been traversed
  • this dependency subtree is already in context

Then future queries become requests for new information, not repeated navigation. At that point, context reduction becomes a consequence of knowledge reuse rather than smaller responses.

Thread Thread
 
pioner92 profile image
Alex

but LLM already knows everything received and keeps this info during the session, right ?

Thread Thread
 
merbayerp profile image
Mustafa ERBAY

True, the LLM remembers it within the current context. I was thinking more about the MCP side. The server still treats every request as independent and may return the same semantic information multiple times. A session-aware layer could return only what’s new, reducing redundant tool calls and making long sessions more efficient—especially once older context starts dropping out of the window.

Collapse
 
skillselion profile image
Skillselion

Whole-file reads are what push long sessions over the auto-compaction threshold, and compaction is where the agent quietly loses reasoning it did an hour earlier. Symbol-level reads delay that cliff, so the payoff shows up as better late-session answers, not just a smaller token bill. One caveat from running similar setups: for refactors that change a function's contract, the callers are part of the context the model genuinely needs, and narrow reads tend to force a second round trip to fetch them. Curious whether read_symbol's surrounding-context mode can pull call sites in the same response - if it can, that closes the main case where I have seen symbol-level access underperform a plain file read.

Collapse
 
pioner92 profile image
Alex

Yeah, you're right, that's a real gap. read_symbol_context only looks at the same file- helpers, local types, that kind of thing. It won't grab callers from other files.

get_call_hierarchy gets you closer since it does cross files, but it only gives you locations (file + line range for each caller), not their actual source. So for a signature change you still end up calling read_symbol again for each caller to see what you're actually dealing with. Not a full file read, but yeah, it's a second round trip. Your instinct on this one was correct

Collapse
 
mia_keller_ffd2584c046ecb profile image
Mia Keller

That 95% token reduction for navigation tasks is massive! How does SymbolPeek handle cases where the agent needs to perform multi-file refactoring? Do you still rely on full file reads once the agent transitions from exploration to writing code?

Collapse
 
pioner92 profile image
Alex

Short renames go through rename_symbol — server-side, writes to disk, zero file reads. For actual code changes, find_references/find_callers pinpoint the exact files/lines and read_symbol pulls just that function, so the agent edits a known symbol instead of reading whole files — the savings taper off only once a change stops being symbol-scoped (e.g. restructuring a module)

Collapse
 
hannune profile image
Tae Kim

The split between TypeScript getting compiler-level semantics and everything else getting Tree-sitter's syntax-level analysis is the main correctness gap to watch — Tree-sitter can't resolve import aliases, track type inference across files, or answer who actually calls this at runtime for Python or Rust, which are questions those languages' compilers can answer if you wire up LSP clients. The architecture already points in the right direction since you're exposing symbol access through MCP; replacing Tree-sitter with per-language LSP backends would give the other languages the same semantic depth TypeScript gets. Whether the startup latency and process-management overhead of LSP servers is worth it depends on which non-TS languages show up most in your user base. The 95% token reduction is almost certainly driven by TypeScript navigation, and I'd expect meaningfully lower numbers once you measure non-TS codebases with Tree-sitter answering the same queries.

Collapse
 
pioner92 profile image
Alex

I agree that the gap between TypeScript and the other languages is quite large - that’s true.

I originally started building this tool for myself, specifically for TypeScript, and later decided to expand support to other file formats and make it public. Another goal was to keep the MCP as fast and lightweight as possible.

I also came across a project that supports many languages through LSP (Serena), so that’s definitely a promising direction.