MCP server for AI agents to explore large codebases via symbol-level search and semantic similarity. Supports 305 languages via tree-sitter-language-pack.
# Homebrew (macOS β Apple Silicon)
brew tap sdkks/tap && brew install sdkks/tap/symbol-index
# Cargo (any platform β requires Rust 1.86+)
cargo install --git https://github.com/sdkks/symbol-index
# macOS: if the build fails with a protobuf-related linker error, run:
# brew install protobuf
# then retry the cargo install command.
# From source
git clone https://github.com/sdkks/symbol-index
cd symbol-index
make install # builds release and installs to ~/.cargo/bin# Index a repository (cd first so project ID is stable regardless of path form)
cd /path/to/your/repo && symbol-index index .
# Search for symbols by name (case-insensitive, FTS5)
symbol-index search "handle_request"
# Search by meaning (vector similarity)
symbol-index search-semantic "function that handles HTTP requests"
# Filter by kind (struct, fn, class, etc. β aliases accepted)
symbol-index search "store" --kind struct
# Look up a specific symbol (dot notation, not ::)
symbol-index get "MyStruct.method_name"
# Check index status
symbol-index status| Invocation | Mode | Description |
|---|---|---|
symbol-index (no args) |
MCP server | Listens on stdio for MCP JSON-RPC requests from AI agents |
symbol-index index <path> |
CLI | One-shot indexing with JSON output |
symbol-index search <query> |
CLI | Search symbols by name or signature (FTS5) |
symbol-index search-semantic <query> |
CLI | Search symbols by meaning (vector similarity) |
symbol-index get <name> |
CLI | Look up a symbol by qualified name |
symbol-index status |
CLI | Show index statistics |
symbol-index flush |
CLI | Clear all index data for current project |
Wire it into your MCP client by adding to mcp.json:
{
"mcpServers": {
"symbol-index": {
"command": "symbol-index",
"args": []
}
}
}Or install with Claude Code:
# Use full path to binary if it's not in your $PATH
# Validate which command `which symbol-index`
claude mcp add symbol-index -- symbol-index # project scope
claude mcp add symbol-index --scope user -- symbol-index # user scope (global)
claude mcp add symbol-index --scope user -- /some/path/symbol-index # user scope if binary not in $PATHsymbol-index index <path> Index a repository (auto: incremental if previously indexed, full otherwise)
symbol-index index <path> --incremental
Index only changed files (Merkle tree)
symbol-index index <path> --diff Index only git-changed files (since last index)
symbol-index index <path> --force Force full re-index (override auto-default)
symbol-index search <query> Search symbols by name/signature (FTS5, case-insensitive)
--kind <type> Filter by kind (function, struct, class, etc.)
--language <lang> Filter by language (rust, typescript, python, etc.)
--limit <N> Max results (default 20)
--project <id> Filter by project ID
--format <json|pretty> Output format (default pretty)
symbol-index search-semantic <query>
Search by meaning (vector similarity)
--limit <N> Max results (default 10)
--threshold <F> Min similarity 0.0-1.0 (default 0.3)
--project <id> Filter by project ID
--format <json|pretty> Output format (default pretty)
symbol-index get <qualified_name> Look up a symbol by qualified name (dot notation)
--project <id> Filter by project ID
--format <json|pretty> Output format (default pretty)
symbol-index status Show index statistics
--path <path> Repository path (default: current directory)
--format <json|pretty> Output format (default pretty)
symbol-index flush Clear index data (SQLite + LanceDB) for current project
symbol-index --version Show version and git SHA
symbol-index help Show help message
Qualified names always use dot notation β not language-specific separators. This is a deliberate design choice: since symbol-index supports 305 languages (each with its own path separator), dot notation provides a single universal syntax.
| Language | Native syntax | symbol-index QN |
|---|---|---|
| Rust | std::io::Read |
std.io.Read |
| Python | os.path.join |
os.path.join |
| Java | java.util.List |
java.util.List |
| TypeScript | React.useState |
React.useState |
Methods use dot notation from their parent type:
# Rust: impl SqliteStore { fn open(...) }
symbol-index get "SqliteStore.open"
# Python: class MyClass { def method(self) }
symbol-index get "MyClass.method"
# TypeScript: class UserService { login() }
symbol-index get "UserService.login"Top-level symbols (functions, classes not inside a module) use bare names:
symbol-index get "discover_files"
symbol-index get "main"Why not ::? Rust uses ::, Python uses ., Java uses . β there's no universal separator. Rather than requiring language-specific knowledge to look up symbols, dot notation works identically for all 305 languages. The search command (case-insensitive FTS5) is the recommended way to discover exact qualified names.
The --kind filter accepts common aliases β use whatever feels natural:
| Input | Maps to | Examples |
|---|---|---|
struct, impl, type |
type | Rust structs, impl blocks |
fn, func, function |
function | Top-level functions |
class |
class | Python/Java/TS classes |
method, meth |
method | Methods on types |
trait, interface, iface |
interface | Rust traits, TS interfaces |
enum |
enum | Enumerations |
mod, module |
module | Module declarations |
const, constant |
constant | Constants |
var, variable |
variable | Variables |
field, member |
field | Struct/class fields |
When running in MCP mode, 16 tools are exposed to the AI agent:
| Tool | Description |
|---|---|
fff_find_files |
FFF-backed fuzzy file/path discovery across repository files |
fff_grep |
FFF-backed broad text search across source, docs, config, tests, and prose |
fff_multi_grep |
FFF-backed multi-query text search with grouped and annotated results |
search_symbol |
FTS5 text search by name, signature, or docstring |
search_semantic |
Vector similarity search with natural language queries |
explore |
Unified search: parallel FTS5 + semantic, merged with cross-path boosting |
get_symbol |
Exact lookup by qualified name |
get_related |
Cross-linked discovery: same-file + same-module + semantic neighbors |
get_callers |
Call graph: find all callers of a symbol (impact analysis) |
get_callees |
Call graph: find all symbols called by a symbol (dependency tracking) |
trace_path |
Call graph: BFS path finding between two symbols |
index_repo |
Full/incremental/diff indexing (auto-detects mode, embeddings always generated) |
flush_repo |
Delete all index data (SQLite + LanceDB) for a fresh start |
index_status |
Index stats: symbol count, embedded count, file count, languages, model |
status |
Alias for index_status |
version |
Server version, git SHA, and build target |
Use FFF tools for broad repository discovery when the symbol index is stale, indexing, not indexed, unavailable, or when symbol search returns poor first results. They are also the right first choice for docs, ADRs, configuration, prose, scripts, tests, examples, fixtures, and other non-symbol content. Use symbol tools for precise structured navigation once candidate symbols or files are known. See FFF Discovery Tools for repo_path, state, limits, and safety defaults.
# First run β full index (embeddings auto-generated for semantic search)
cd /path/to/repo && symbol-index index .
# Daily development β fast incremental updates (auto-detects existing index)
symbol-index index /path/to/repo --diff
# After major refactoring β flush stale data, rebuild fresh
symbol-index flush
symbol-index index /path/to/repo --forceFor repositories with 100,000+ symbols, always index from the CLI β the MCP index_repo tool may timeout. The CLI has no time limit and shows real-time progress.
# Build the index manually β the agent will use it once it's ready
cd /path/to/large-repo && symbol-index index .Once indexed, you can verify with symbol-index status. The MCP server serves any existing index β the agent can immediately search and explore without re-indexing.
In MCP mode, the agent can call flush_repo then index_repo with mode full to get a fully fresh view of smaller codebases after major changes.
- Full (
--mode full): Parses every file, builds complete index. Use for first run. - Incremental (
--incremental): Compares file mtime/size against stored metadata. Only re-parses changed files. - Diff (
--diff): Git-aware. Compares last indexed commit against HEAD. Includes committed changes, staged changes, and untracked files. Fastest for active development.
flowchart TB
subgraph Input["π Codebase"]
src["Source Files<br/>305 languages"]
end
subgraph Index["βοΈ symbol-index"]
direction TB
parse["tree-sitter Parse<br/>Extract symbols, signatures, docstrings"]
store["SQLite FTS5<br/>Full-text search + metadata"]
vectors["LanceDB Vectors<br/>768-dim embeddings"]
parse --> store
parse --> vectors
end
subgraph Query["π AI Agent"]
direction TB
q1["search_symbol<br/>Find by name/signature"]
q2["search_semantic<br/>Find by meaning"]
q3["get_related<br/>Cross-linked discovery"]
end
subgraph Benefit["π° Token Savings"]
direction TB
t1["Skip reading 1000s of files"]
t2["Jump directly to relevant code"]
t3["Understand codebase structure<br/>without exploration"]
end
src -->|"Index once"| Index
Index -->|"Query many"| Query
Query --> Benefit
style Input fill:#e1f5fe
style Index fill:#fff3e0
style Query fill:#e8f5e9
style Benefit fill:#fce4ec
Without symbol-index, an AI agent exploring a large codebase must:
- Read entire files to find relevant functions
- Search through grep results with no semantic understanding
- Spend 10-50k tokens on exploration before doing useful work
With symbol-index:
- Index once β parse the entire codebase into a compact symbol database
- Query precisely β find exact symbols in milliseconds, not minutes
- Semantic search β describe what you need in natural language
- Cross-linking β discover related symbols without reading source files
Result: 90%+ reduction in tokens spent on codebase exploration.
symbol-index
|
+---------------+---------------+
| | |
tree-sitter SQLite FTS5 LanceDB vectors
(parsing) (text search) (semantic search)
| | |
305 languages BM25 ranking cosine similarity
| | |
+-------+-------+-------+-------+
| |
symbol lookup related symbols
(exact match) (cross-linked)
- Parse: tree-sitter-language-pack extracts symbols (functions, classes, methods, types, etc.) from source files
- Store: Symbols go into SQLite with FTS5 full-text search; embeddings go into LanceDB for vector search
- Search: Agents can search by name (FTS5), by meaning (vector similarity), or find related symbols (cross-linked)
- Index: Supports full, incremental (mtime/size), and diff (git-aware) indexing modes
SQLite and LanceDB are linked via a shared lance_id ({project_id}:{file_path}:{qualified_name}). This enables the get_related tool to combine both stores on the fly:
- Textual path β SQLite finds symbols in the same file or module (no LanceDB needed)
- Semantic path β LanceDB finds embedding neighbors by cosine similarity, then resolves back to full symbol records in SQLite via
lance_id
There is no stored graph β relationships are computed fresh on each query. This means the cross-links always reflect the current index state without needing maintenance.
The first time you index, symbol-index downloads the jina-embeddings-v2-base-code ONNX model (~350 MB). Embeddings are always generated automatically. By default the model is stored at ~/.cache/fastembed so all projects share a single copy.
To store the model elsewhere, set the FASTEMBED_CACHE_DIR environment variable:
export FASTEMBED_CACHE_DIR="/path/to/models"symbol-index only needs network access once β to download the embedding model (~350 MB ONNX file) on first indexing run. After that, it works entirely offline.
If you see TLS errors during model download (e.g. UnknownIssuer), your machine
is likely behind a corporate TLS-inspecting proxy (Netskope, Zscaler, etc.) that
re-signs HTTPS traffic with a root CA not visible to fastembed's HTTP layer.
The workaround is to route model downloads through a local HuggingFace mirror that proxies over plain HTTP. See docs/tls-ca-issue.md for the full guide.
symbol-index uses tree-sitter-language-pack which provides grammars for 305 languages. The file walker currently recognizes these extensions for indexing:
Systems & compiled: Rust (.rs), C (.c, .h), C++ (.cpp, .cc, .cxx, .hpp), Go (.go), Zig (.zig), Swift (.swift), Assembly (.asm, .s)
JVM: Java (.java), Kotlin (.kt, .kts), Scala (.scala, .sc)
Scripting & web: Python (.py, .pyi), JavaScript (.js, .jsx, .mjs, .cjs), TypeScript (.ts, .tsx, .mts, .cts), PHP (.php), Ruby (.rb, .erb), Lua (.lua), Perl (.pl, .pm), Elixir (.ex, .exs), Dart (.dart)
Functional: Haskell (.hs, .lhs), OCaml (.ml, .mli)
Data & scientific: R (.r), Julia (.jl), SQL (.sql)
Shell & config: Bash/Zsh (.sh, .bash, .zsh), TOML (.toml), YAML (.yaml, .yml), JSON (.json, .jsonc), XML (.xml, .svg)
Web & markup: HTML (.html, .htm), CSS (.css, .scss, .sass, .less), Markdown (.md, .mdx)
Build & infra: Dockerfile (.dockerfile), Makefile (.mk), Protocol Buffers (.proto)
Also supported but not yet mapped: The underlying parser handles 305 languages β if a language isn't listed above, its files are silently skipped during indexing. To add support for a new extension, edit detect_language() in src/indexer/walker.rs.
This project follows Conventional Commits for automated versioning. Every push to main triggers a semantic version bump based on commit messages:
| Commit type | Release | Example |
|---|---|---|
feat!: or BREAKING CHANGE: footer |
major | feat!: drop support for Python 3.8 |
feat: |
minor | feat: add incremental embedding support |
fix:, perf:, refactor:, revert: |
patch | fix: canonicalize repo path in index command |
docs:, style:, chore:, ci:, test:, build: |
none | docs: update install instructions |
Breaking changes are detected via either the ! notation (feat!:, fix(api)!:) or a BREAKING CHANGE: footer in the commit body.
The profiling Cargo profile inherits release optimization but keeps full debug info and symbols.
cargo build --profile profilingTwo tools are documented below. They produce different artifacts and serve different purposes β pick based on what you need.
Uses xctrace under the hood. Produces a .trace file that opens in Instruments.app with a full-featured timeline view. This is the most comprehensive option: you can inspect CPU usage over time, drill into specific time ranges, view per-thread activity, and use multiple analysis modes (Time Profiler, Allocations, Leaks, System Trace).
cargo install cargo-instruments
cargo instruments --profile profiling -t "Time Profiler" --bin symbol-index -- index "$PWD"# Trace opens automatically in Instruments.app
# Use File > Export to save a flamegraph from the selected time rangeWraps the platform profiler and produces a single interactive SVG. Shows aggregated stack traces (width = time spent) without a time axis. Best for quick hotspot identification and sharing results β the SVG opens in any browser with click-to-zoom.
# Linux (uses perf)
cargo install flamegraph || cargo install --git https://github.com/flamegraph-rs/flamegraph.git
cargo flamegraph --profile profiling --bin symbol-index -- index /path/to/repo
# macOS (uses xctrace under the hood, outputs SVG directly)
cargo install flamegraph || cargo install --git https://github.com/flamegraph-rs/flamegraph.git
cargo flamegraph --profile profiling --bin symbol-index -- index "$PWD"```
#### Which one?
| | Instruments.app | flamegraph |
|---|---|---|
| **Output** | `.trace` file | `.svg` file |
| **View** | Timeline + call trees | Aggregated flame graph |
| **Time axis** | Yes β zoom into spikes | No β all samples merged |
| **Thread analysis** | Per-thread breakdown | Merged across threads |
| **Other modes** | Allocations, Leaks, GPU, etc. | CPU time only |
| **Sharing** | Requires Instruments.app | Any browser |
| **Best for** | Deep investigations | Quick hotspot checks |
The profiling profile settings:
- `debug = 2` β full DWARF debug info
- `strip = "none"` β all symbols preserved
- `lto = false` β no link-time optimization (cleaner call stacks)
- `opt-level = 2` β near-release speed, not size-optimized
## Build
```bash
cargo build --release # release build (stripped, LTO optimized)
cargo test # run tests
make lint # format + clippy
make fix # auto-fix formatting and clippy suggestions
make install # install to ~/.cargo/bin
make coverage # run coverage (requires cargo-llvm-cov)MIT