Slimdex
About
Narrow code retrieval for coding agents — outlines, symbol bodies, dependency graphs, and persistent memory instead of whole-file reads.
Details
- Author
- siddhukaushik
- Categories
- Developer Tools
Jump to
Setup
Install Slimdex in your MCP client (Claude Desktop, Cursor, Windsurf, and others).
Repository: https://github.com/siddhukaushik/slimdex-mcp
Follow the installation instructions in the repository README, then restart your MCP client.
Config:<root>/.slimdex.json(optional)
{ "ignoreDirs": ["fixtures", "backend/src/main/resources/static/assets"], "extensions": [".astro", ".vue"], "suffixes": [".stories.mdx"], "exclude": ["generated/", "legacy/vendor"], "maxFileBytes": 2000000 }
suffixesmatches a filename ending, for file types an extension can't identify. Salesforce metadata sidecars ship as a built-in:AccountSvc.cls-meta.xml,panel.js-meta.xmlandAccount.object-meta.xmlare indexed, whilepom.xml,web.xmlandmanifest/package.xmlare not — adding.xmltoextensionswould have pulled in every config tree in the repo. Suffix-matched files are indexed for search and read reach, not symbols.
Merged on top of the built-in ignore list (node_modules,dist,.venv,.svelte-kit,Pods,.pytest_cache, …). AnignoreDirsentry is either a bare name, matching any directory so called at any depth, or a path containing/, anchored at the repo root and respecting directory boundaries (src/genwill not also ignoresrc/generated).index_repoechoes what it loaded and warns about unknown keys, wrong types, or invalid JSON, so a typo'd config isn't silently indistinguishable from none.
Build output usually needs no config at all.Beyond the directory list, any file whose lines run past ~5,000 characters is treated as minified build output and left out of the index — bundlers strip newlines, and hand-written source doesn't look like that. This catches what a name list structurally cannot: a hash-named bundle (index-B7xK2p9q.js) inside a directory calledassets.assets,publicandstaticare deliberatelynotignored by name, because real source lives in them;index_reporeports the count asskipped(minified build output): N.
There's no compression trick. The saving is behavioral: these tools let an agent retrieve outlines, ranges, and locations instead of whole files, and the persistent index means repeat lookups hit a cached query rather than a re-read.
Two later sessions, run by different models on different repo shapes, added real-world numbers to the original report:
Multi-file web app, bug-fix session (GPT-5.3-Codex).19 credits reported with slimdex; the model's own estimate for the same scope without it: 45–70 credits. Math: 19/45 → 19/70 ≈58–73% cheaper. The counterfactual is the model's estimate, not a measured A/B — directional.
Single giant file (folio-app: one 6,200-line, 313 KBapp.js).Slimdex's own stats: ~34,000 chars across 8 calls ≈ 9–10k tokens — one skeleton (213 signatures), then bodies of only ~12 relevant functions, 9 of them fetched in a singleget_symbol_context names:[...]call. The naive path: 313 KB ≈ 78–85k tokens across 3–4 forced full reads. Math: ~10k vs ~80k ≈~70k tokens saved, an 85–90% reductionon exploration. The bug's diagnosis (an export path with no matching import path) was visible from the skeleton's signatures before a single body was opened.
Together they sketch the scaling law:the saving scales with how much irrelevant code the naive path would drag in.One giant file is the best case; a normal repo lands around half to two-thirds cheaper; a repo of tiny files breaks even. Same standing caveats as everything here: stats count chars, not tokens (÷3.5–4), and single sessions are evidence, not benchmarks.
Both figures above measure reading only, which is the cheaper half.Output costs roughly 4–5× input, so an undisciplined edit wastes more than an undisciplined read: rewriting a whole function through a generic edit tool means re-sending the entire old body purely so the tool can locate it.replace_symboladdresses by name and that cost disappears.statsreports this alongside follow-through, because the leak is otherwise invisible — the expensive path still produces a correct edit, so nothing signals that you overpaid:
write discipline: replace_symbol: 0 call(s), 0 symbol(s) rewritten by name changed outside slimdex: 12 file(s) pre-edit checks (find_tests/dep_graph/get_context/changed_files): 0
External edits are inferred from content hashes moving between twoindex_reporuns, so the number is honest about its limits: it sees that bytes changed, never which tool changed them, and a human editing in another window counts too.
The figures above are single-scenarioexplorationnumbers — the best case, where the naive path would have dragged in the most irrelevant code. Averaged across a whole real workday, not just the exploration slice, the band settles lower:
- ~55–60%on navigation-heavy work — reading and understanding a codebase, where narrow retrieval replaces whole-file reads most often.
- ~45%on output-heavy work — churning out new code, where more of the cost is generation the server doesn't touch (thoughreplace_symbolnow shaves the write side too).
- ~50% averagedover regular day-to-day use. The saving compounds the more sessions run through it, becausebriefand memory mean each new chat starts informed instead of re-deriving the repo from zero.
Use it regularly across sessions in your IDE for the best of this.
Treat these as one data point, not a benchmark.Single repo, single task, one A/B run each, self-measured, no repetitions or variance. Your mileage depends heavily on whether your agent actually reaches for the narrow tools instead of falling back to reading files — which varies by client and model. The method is repeatable if you want to check it: run the same task in two fresh sessions, one instructed to use only Slimdex and one instructed to avoid it, and compare/statuscache-write.
Being explicit, since the rest of this README is easy to over-read.
Covered by the unit suite(npm testruns 224 tests across 23 files):
-
Symbol extraction across JS/TS (incl. class and object-literal methods), Python, Go, Rust, Java/C#, and comment skipping —symbols.test.ts
Import extraction for JSimport/require/export-from, Python, Rust
Block extraction, brace-scoped and indentation-scoped, with string/comment awareness (quotes, templates,//,/ /, full-line#) —extractBlock.test.ts
Import resolution, external-module classification, reverse-edge dependents, Mermaid emission, and root-BFS depth scoping —graph.test.ts
Search match format, pagination without overlap, per-line occurrence counting, exact totals, regex escaping/rejection —search.test.ts
Opaque cursor round-tripping and malformed-cursor rejection; parser-backend fallback —pagination.test.ts
Outline declaration detection vs. control flow —outline.test.ts
get_symbol_contextmaxLinesbudgeting and truncation notice
String/comment masking and brace-depth tracking —lexer.test.ts
Per-language extraction for all twelve supported languages —languages.test.ts
The index cache returns the same object until the index is rewritten
.slimdex.jsonloading: every key applied through a real index build, plus the failure modes (invalid JSON, unknown keys, wrong types) each producing a visible warning instead of silence —config.test.ts
changed_filesagainst a real temporary git repository: hunk→symbol attribution, untracked files, explicit base refs, and formatting; skips cleanly when git isn't installed —git.test.ts
The file watcher, with real fs events: a save is debounced, reindexed, and lands in the on-disk index —watch.test.ts
Graph edges beyond imports: name-reference edges for import-less code (class→used-class, interface→implementation via dependents, trigger→handler) and declarative-wiring edges from repo XML (metadata-binding→class), with comment/string mentions excluded and per-build caching —apexgraph.test.ts
The in-memory file cache serves repeats without re-reading and always serves fresh content after an on-disk change —fscache.test.ts
Test-file detection across JS/TS/Python/Go/Ruby/Java/C# conventions, with Windows separators normalized and ordinary source (latest.ts,Contest.java) not misflagged —testlink.test.ts
The write side: replacing a symbol's block, trailing code preserved, and CRLF vs LF line endings kept so an edit isn't reflowed into a whole-file diff —edit.test.ts
Memory staleness: a fact is marked live when it names a symbol/file that still exists, flagged stale only when every code mention is gone, and left unflagged for prose — plus brief composition —brief.test.ts
Intent search: camelCase/snake_case tokenization, and BM25 ranking that surfaces a differently-named symbol by its intent words while scoring an unrelated query to nothing —intent.test.ts
Freshness: a file newer than its indexed mtime reads as stale (line numbers may be off), a matching mtime reads as fresh, and a missing file never cries stale —freshness.test.ts
context_packassembly: header + ranked symbols + bodies in one bundle, the no-match message, char-budget gating that still guarantees the first body, and the symbols-limit cap —pack.test.ts
The architecture digest: covered files modified after the digest read as stale, a newer digest reads clean, coverage-scope and directory-prefix filtering, and the rendered fresh/stale verdict —digest.test.ts
Covered end to end, through the real MCP server(integration.test.tsspawns the server over stdio against a temporary fixture repo and asserts on output):index_repo,repo_map,read_lines,get_file_skeleton,outline_file,get_symbol_context,find_definition,find_references,find_tests(the hit and the no-coverage warning),search_intent(intent ranking),context_pack(one-call bundle),digest_save/digest_get(round trip with freshness verdict),get_context(including itsmaxCharscap),dep_graph(imports + mermaid),batch,search_code,search_symbols,stats,brief,replace_symbol(write-then-query round trip and the unknown-symbol refusal), thememory_save/search/list/deleteround trip, the path-escape guard, and the not-found paths.
CI runs the build and both suites on Ubuntu + Windows, Node 20 and 22.
Caveat on the watcher test:recursivefs.watchis platform-dependent, sowatch.test.tsdegrades to a logged skip on filesystems that never deliver an event — same behavior as the watcher itself. On Windows, macOS, and current Linux it asserts the full save→reindex path.
npm run smokestill exists but proves only that the pipeline is alive — the correctness assertions live inintegration.test.ts.
Verified by inspection:src/contains no network calls — no code leaves your machine. This one you can check yourself:grep -rE "fetch\(|https?://|axios|http\.request" src/.
- tool-guide.md— every tool explained twice (technically and in plain words) with an example each, the combined workflow, and how mtime-based persistence works
- tool-guide.html— the same guide as a styled, self-contained page for the browser
- token-savings-report.md— the original A/B measurement, its method, and how to repeat it
- agent-brain.md— the full operating discipline as a readable document
- agent-brain-slim.md—the one to drop into a repoas CLAUDE.md / AGENTS.md. Self-contained and one page: savings ladder, question→tool table, memory discipline, session hygiene, honest limits, env knobs. Same coverage as the full document at ~30% of the prose, because the tool rules are dense tables rather than paragraphs the server already injects.
Two measurements, because fixtures alone prove very little.
Fixtures— one per language, counting the declarations a developer would actually navigate to:65/65 found, 0 false positives, pinned bytest/languages.test.ts.
Real third-party code— extraction run over ~11,800 files from several hundred real packages (React, Babel, Remix, Socket.io, Playwright, Three.js, Emotion, zod, ajv …) and compared against an independently written heuristic for what counts as a declaration:95.9% recall. Reproduce it yourself:
npm run audit -- ./node_modules # or any directory of code you didn't write
That number is a floor, not a grade — the truth heuristic counts some non-declarations, so real recall is a little higher. What it's for is catching regressions and finding the next real gap.
Almost nothing that failed the audit was framework-specific. Frameworks add annotations, decorators and conventions; they rarely invent syntax. Handle the language and the frameworks come with it — fflib's Application/Domain/Selector/ Service/UnitOfWork layers extract completely (129 declarations) without a single fflib-aware rule.
The one genuine exception istest DSLs. A vitest/jest/mocha/RSpec file often has no top-level declarations at all, so entire test directories used to index to nothing.describe/it/testtitles are now indexed as kindtest, which is what you actually navigate to in a test file.
Frameworksemanticsare recovered wherever the reference exists somewhere in the repo, through two extra edge sources in the graph:
- Name-reference edges, for languages that have no import statement (e.g. Apex): if one file's code — comments and strings masked out — mentions a top-level type defined in another file, that's an edge. This is what makesimplementsanswerable as "who implements this interface", and links a trigger to the handler class it news up.
- Declarative-wiring edges: bindings that frameworks keep in configuration rather than code (custom-metadata records, flow definitions) usually live in the repo as XML with the type name as an element value. Repo XML is scanned for known type names — XML comments excluded — and each hit becomes ametadata-file → classedge, sodependentsanswers "what wires this up".
Both scans are cached per index build and cost nothing on repos without such files. Pinned byapexgraph.test.ts. What no static reader can see is a binding that existsonly in a live system— configured in a running org or database and never retrieved into the repo. If it's not in the repo in any form, there is no edge to draw; search the type name instead.
Cold index is a full parse; warm is an mtime check per file. Measured on Windows, Node 24.
The index is held in memory and invalidated by the index file's mtime. Without that cache every tool call re-read and re-parsed the whole index — about 20 ms of dead weight per call on the 5,000-file repo, and it grew with the repo.
find_referencesis the slowest tool at scale because it is a textual scan, not an index lookup — but a literal pre-filter now skips the line-split and per-line regex for any file whose raw source doesn't contain the searched name, which on a typical repo is most of them. Scope withpathPrefixto cut the remaining file reads when you know roughly where to look.
File contents are also served from a byte-bounded in-memory LRU (64 MB, validated by mtime+size per hit), so the second scan of a repo — and the skeleton→read_lines→context sequence agents actually perform on one file — costs astat()instead of a read.
memory_savewrites to<root>/.slimdex/memory.json, which outlives the process — a fact saved in one chat is readable in the next, by a different client, after a restart. Chat and editor share one store only when both point at the sameSLIMDEX_ROOT.
Nothing is captured automatically: the server never sees your conversation, so the agent has to decide what's worth keeping. The shippedinstructionstell it to read memory first in a new session and to save decisions, constraints and gotchas as it learns them — but that's guidance to the model, not a guarantee.
- Symbol extraction isregex-based and heuristic, not a parser or LSP. It can miss unusual declarations, andfind_referencesis atextualmatch that may include same-named but unrelated identifiers.
- Symbol and outline extraction now run against amaskedcopy of each line, with string and comment contents blanked out, so declaration-shaped prose inside a template literal is no longer indexed as code. Declarations are alsodepth-aware: aconst x = () => …ortype X = …counts only at top level, because locals inside a function body are not things anyone navigates to. Class methods are still indexed at their nesting depth.
- AninlinePython#comment containing a brace can still confuse block extraction (#is also the JS private-field sigil, so it can't be stripped blindly).
- changed_filesattributes a hunk to thenearest preceding declaration— right for a normal function body, approximate for code between declarations. Treat it as blast radius, not a call graph.
- search_codereports an exact total but stops at an internal scan cap on very large result sets, printingN+ (scan cap reached)rather than a confident wrong number.
- Language support is uneven: JS/TS is the best-covered. C-family and Ruby, formerly the thinnest, gained dedicated rules (free functions,Foo::bardefinitions, function-like macros,attr_*); the remaining soft spots are advanced C++ shapes — templates split across lines, operator overloads.
- For LSP-grade precision you'd swap the parser for tree-sitter or a language server.src/parser.tsis the seam: aParserinterface selected bySLIMDEX_PARSER, with the regex parser as the only implementation that ships. A tree-sitter backend would drop in there without touching any tool or the index format. It isnot built— per-language grammars trade away the "installs instantly, runs offline, zero config" property.
Ideas evaluated and rejected, with reasoning — these are design opinions, not measured results:
- Symbol-ID dictionaries (S42→ path)— MCP has no client-side expansion layer, so the model receives an opaque token it must spend another call to resolve.
- Token-budget managers / cost estimators—chars/4estimates are unreliable across tokenizers, and auto-compressing on a bad estimate can drop data the model needed.
- Delta / "already-sent, see response #5" caching— after context compaction the earlier payload is gone, so the reference resolves to nothing.
- Embeddings / semantic search— large dependency footprint; possible future optional flag, not a default.
- A tree-sitter parser backend— this is the one that would close the remaining ~4%, and it was costed rather than hand-waved:web-tree-sitteris WASM so it needs no native compilation, but the grammars (tree-sitter-wasms) are51.7 MBunpacked against ~4.5 MB for the whole current install. Evaluated and declined at 95.9% measured recall, because "installs in a second, runs offline, no configuration" is the property this server exists to have.src/parser.tsremains the seam if that calculus ever changes — a backend drops in there without touching a tool or the index format.
Published on npm asslimdex-mcp, and listed in theMCP Registryasio.github.Siddhukaushik/slimdex-mcp. Nothing to build — point your client at:
Or from source, if you want to hack on it:
git clone https://github.com/Siddhukaushik/slimdex-mcp cd slimdex-mcp npm install npm run build # produces dist/index.js npm test # vitest unit suite
Verify it runs end to end against a repo:
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.





