Skip to main content
Glama
sitharaj88

Symbra

by sitharaj88

Symbra

Local-first, live code intelligence for AI coding agents.

Symbra weaves a repository into a symbol graph — every function, class, method, route, test and config key with its exact range, signature, doc comment, callers, callees, imports and inheritance — stores it in SQLite, keeps it fresh as you edit, and serves it to Claude Code, Cursor, Codex, Windsurf, VS Code and Gemini over MCP. One tool call replaces a grep-and-read loop.

  • Zero LLM, zero cloud, zero native compilation. Parsing is tree-sitter compiled to WebAssembly; storage is Node's built-in SQLite. Nothing leaves your machine.

  • Live, not static. The index is a database, not a JSON dump. Changed files re-index in milliseconds; the MCP server refreshes itself on every call and flags stale symbols.

  • Symbols, not strings. Calls resolve through lexical scope, real module resolution (tsconfig paths, Python packages, re-exports), declared receiver types and inheritance. Ambiguous references are kept as candidate sets instead of being guessed.

  • Answers in one call. explore("how does connection pooling work") returns the ranked symbols with source, relations and the paths between them, packed under a token budget.

  • Blast radius from your diff. impact with no arguments reads git diff, walks reverse dependencies and lists the tests to run.

npx symbra index            # build the index for the current repo (seconds)
npx symbra install          # register the MCP server with Claude Code (+ Cursor, Codex, ... via --tool)
npx symbra explore "where are timeouts configured"

Why another code graph

Graphify popularised the idea of a knowledge graph for AI assistants, and its 114K stars show the demand. Symbra was built after reading graphify end to end (see docs/graphify-teardown.md) to fix what makes agents ignore such graphs in practice:

Graphify 0.9.53

Symbra

Node data

label, file, start line

kind, start/end line, byte range, signature, doc, modifiers, exported, declared type

Call resolution

corpus-wide name table with a hand-written builtin blocklist

scope → imports → receiver type → unique global; unresolved kept as candidates

Parallel edges

collapsed to one relation per pair

every call site is an edge with line and resolver provenance

Query

substring match over labels, fixed-depth BFS, advisory token budget

BM25 over names/split identifiers/signatures/docs + personalised PageRank, packed by relevance under a real budget, with source

Storage

pretty-printed JSON, 512 MiB cap, parsed whole on every read

SQLite with FTS5, incremental per-file upsert

Freshness

full re-extract on every watch event

only changed files and their dependents

Agent integration

700 KB of duplicated skill prompts, MCP server not registered at install

MCP server registered at install, 9 tools, short marker-fenced guidance

Visualisation

vis-network from a CDN, breaks above 5,000 nodes

self-contained offline map with search, filters, a details drawer and light/dark themes, semantic zoom from subsystems to symbols

Install

Python, graphifyy package, PATH setup, interpreter sniffing

npx symbra

Measured on the same machine (Apple M-series, 8 cores):

Repository

graphify extract

Symbra index

Notes

httpx (17.7K LOC Python)

6.3 s

0.6 s

Symbra resolves Client.request → Client.send through the receiver type; graphify's query for "connection pooling" returned 72 nodes without Limits or HTTPTransport

express (21.5K LOC JS)

1.4 s

0.7 s

graphify extracted 3 symbols from lib/application.js and 7 call edges repo-wide; Symbra finds all 20 app.x = function methods and 812 calls

graphify itself (764 files, 153K lines Python plus fixtures in 18 other languages)

21.8 s

8.0 s (4.5 s for the Python alone)

11,059 symbols, 22,758 edges

Django (2,375 files, about 1M lines)

not measured

20 s

59,850 symbols, 135,881 edges, 750 MB peak memory (best of four --full runs; 19.5–54 s depending on machine load)

Retrieval quality is measured, not asserted. npm run bench runs 132 hand-written questions over 11 indexed repositories (httpx, express, gin, gson, moshi, MediatR, Slim, sinatra, bytes, fmt, Alamofire — Python, JS, Go, Java, Kotlin, C#, PHP, Ruby, Rust, C++, Swift) across five question kinds, with the gold answer recorded as a symbol fqn:

Retrieval

recall@1

recall@5

found

MRR

lexical only (BM25 + PageRank)

43.9

61.4

74.2

0.514

+ semantic tier (symbra embed)

47.7

72.0

81.8

0.573

The graph questions (callers, impact, search) already find the gold symbol in 100% of cases; the movement is in free-form explore questions, where recall@5 goes from 30.9 to 51.5. Full harness, questions and per-repo results are in bench/.

Related MCP server: OpenCodeHub MCP Server

Install

Requires Node 22.5 or newer (for built-in SQLite). The launcher runs Node with --liftoff-only because V8's optimizing wasm compiler runs out of memory on the largest grammars; parsing speed is unaffected.

npm install -g symbra       # or use npx symbra ... without installing
cd your-repo
symbra index
symbra install --tool claude,cursor    # claude | cursor | codex | windsurf | vscode | gemini | arcturn | all

install writes the MCP server entry (.mcp.json, .cursor/mcp.json, ~/.codex/config.toml, .arcturn/mcp.json, …), a short guidance section in CLAUDE.md / AGENTS.md / GEMINI.md / ARCTURN.md between <!-- symbra:start --> markers, and adds .symbra/ to .gitignore. Add --hooks for a once-per-session Claude Code hint when the agent reaches for grep; it never blocks a tool call (Arcturn's hook protocol has no channel for a hint, so --hooks is a no-op there — its guidance comes from ARCTURN.md alone). Use --command "<cmd>" to register a different server command (e.g. a local checkout) instead of the default npx -y symbra.

Arcturn

symbra index
symbra install --tool arcturn
arcturn trust --allow

symbra install --tool arcturn writes .arcturn/mcp.json and also merges nine allow rules (one per read-only tool: explore, search, symbol, callers, callees, path, impact, overview, status) into .arcturn/config.json, since Arcturn's project MCP tools are permission-gated — a non-interactive run denies them without an explicit rule. The merge is additive and idempotent: it only appends rules that are missing and never touches unrelated config or permissions. Running install again does not duplicate rules. Arcturn also requires arcturn trust --allow once per repo before it will start project MCP servers at all. The Arcturn VS Code extension reads the same .arcturn/ configuration, so no separate setup is needed there.

For a local checkout of symbra (instead of the published npm package), point the server entry at your build directly:

symbra install --tool arcturn --command "node /path/to/symbra/bin/symbra.js"

The npm package ships wasm grammars for 21 core languages (Python, JS/TS/TSX, Go, Rust, Java, C#, C, C++, Ruby, PHP, Kotlin, Swift, Scala, Bash, JSON, YAML, TOML, CSS, HTML — plus Markdown, which needs no grammar at all). Everything else (Dart, Elixir, Lua, Zig, HCL/Terraform, Objective-C, Groovy, PowerShell, Haskell, OCaml, F#, Julia, Solidity, Svelte, Make, Vue) is fetched from npm on first use and cached under ~/.cache/symbra/grammars, verified against the registry's published sha512 before it's trusted — still zero native compilation, just a one-time download for the long tail. Prefetch before going offline with symbra grammars --all (or name specific languages, e.g. symbra grammars dart lua); symbra grammars --list shows what's shipped, cached, or missing. SYMBRA_OFFLINE=1 (or a network failure) makes indexing skip files in a not-yet-fetched language with a one-line warning instead of failing the run; SYMBRA_GRAMMARS=<dir> points at a directory of your own prebuilt grammars.

CLI

symbra index [path] [--full]        build or update the index (incremental)
symbra status                        freshness, counts, resolver statistics
symbra explore "<question>"          one-shot context pack (alias: query)
symbra search <terms>                ranked symbol search
symbra symbol <name>                 signature, doc, relations, tests, source (alias: explain)
symbra callers <name> [-d 2]         who calls or references it
symbra callees <name>                what it calls
symbra path <from> <to>              shortest structural path
symbra impact [name] [--base ref]    blast radius of a symbol or of the git diff, with tests to run
symbra overview                      subsystems, hubs, entry points, env vars, import cycles
symbra embed                         compute local semantic vectors (optional tier)
symbra import-scip <index.scip>      upgrade edges with compiler-accurate references
symbra viz [--open]                  self-contained HTML map at .symbra/map.html, with search, filters and a details drawer
symbra grammars [names…|--all|--list]  prefetch or show status of tree-sitter grammars
symbra watch                         re-index as files change
symbra serve                         MCP server over stdio (what the agents use)

explore understands intent: "who calls X", "what breaks if I change X", "how does X reach Y" and "where is X defined" dispatch to the graph tools; anything else runs hybrid retrieval.

Example, on httpx:

$ symbra path Client HTTPTransport
2 hops
  [class] Client  httpx/_client.py:594-1304
  --> [contains] [method private] Client._init_transport  httpx/_client.py:718-738
  --> [calls L731] [class] HTTPTransport  httpx/_transports/default.py:135-262

$ symbra impact Timeout -d 2
12 dependent symbols in 2 files · 29 tests
depth 1:
  [constructor] BaseClient.__init__  httpx/_client.py:189-221  calls Timeout
  [method] BaseClient.build_request  httpx/_client.py:340-389  calls Timeout
  ...
tests to run:
  tests/client/test_client.py::test_build_request
  tests/test_config.py::test_timeout_eq

MCP tools

Tool

What it returns

explore(question, budget?, source?)

ranked symbols with signatures, docs, source bodies and the relations among them; ranked symbols that did not fit the budget are counted, not silently dropped

symbol(name)

definition, supertypes and subtypes, members, callers, callees, tests, source

callers(name, depth?) / callees(name)

direct or transitive edges with call-site lines and resolver confidence; a reference the resolver could not narrow to one definition is labelled (candidate, conf 0.5) rather than reported as a certain edge; a callback or function pointer handed to another function shows as passes (passed as callback)

path(from, to)

shortest chain of contains/calls/extends/passes edges

impact(name?, depth?, base?)

reverse dependency closure grouped by depth plus tests to run; no name means the git diff

search(query, kind?, path?)

ranked symbol search

overview()

subsystems with labels, hubs, entry points, env vars, import cycles, index quality

status(reindex?)

freshness and counts

Every response starts with [symbra] index 3m ago, checked 2s ago @ 1a2b3c4d · 1802 symbols · 3779 edges — when the index was last written and when it was last verified against the working tree, which are the same clock only when the last check found a change. Symbols whose file changed after indexing are marked STALE.

Map

symbra viz --open builds a self-contained HTML file and opens it in your browser — no server, no network calls. It starts at a subsystem overview (gradient spheres sized by member count, edges weighted by call traffic) and drills into a symbol-level graph with importance-sized nodes, coloured edge kinds and a details drawer showing overview, relations and syntax-highlighted source, with a link to open the symbol in VS Code. Search with /, clear with Esc, fit the view with f, toggle light/dark with t, and move between nodes with the arrow keys and Enter.

BM25 finds a symbol when you know a word in its name. It does not find _redirect_headers from "what gets stripped when a 302 points at a different domain". An optional local embedding tier closes that gap; it fuses with BM25 and PageRank by reciprocal rank rather than replacing them.

npm i @huggingface/transformers   # optional, not installed by default
symbra embed                    # downloads Xenova/bge-small-en-v1.5 once (~32 MB), then vectorises

Vectors live in the same SQLite file and are incremental by text hash, so symbra embed after an edit only re-embeds what changed. Throughput is about 130 symbols/s on an M-series laptop (httpx: 929 symbols in 7 s). Nothing is sent anywhere: the model runs locally through onnxruntime-node, with a WASM fallback.

Variable

Effect

SYMBRA_EMBED=0

switch the tier off entirely — no model load, no fusion

SYMBRA_EMBED_BACKEND=wasm|node

pin the runtime (default: native first, WASM as fallback)

SYMBRA_MODEL_DIR=<dir>

move the model cache (default ~/.cache/symbra/models)

Without the package, without the model, or with no vectors computed, every query behaves exactly as it did before — the tier is additive. The MCP server preloads the model when the store has vectors, so the first question does not pay the session-creation cost.

Measured effect on the benchmark: overall recall@5 61.4 → 68.9, and 30.9 → 45.6 on free-form explore questions.

Compiler-accurate references (SCIP)

Symbra's resolver is a tiered approximation of a compiler. When you already have a real one, feed it in: SCIP indexes from scip-typescript, scip-python, scip-java, scip-go, rust-analyzer or scip-clang upgrade the graph in place.

scip-typescript index                 # or your language's indexer
symbra import-scip index.scip --dry-run   # report what would change
symbra import-scip index.scip

Heuristic call edges at a SCIP-resolved call site are replaced by edges with resolver = scip and confidence = 1, and the candidate sets they came from are collapsed. On express: 920 edges over 141/141 documents, 467 heuristic edges replaced, 259 candidate sets resolved, in 0.3 s.

A SCIP index names every binding, including the module-scope consts, destructured locals and re-export aliases that Symbra deliberately does not extract. By default those are not turned into new symbol rows — references to variables Symbra did extract still resolve. --include-variables opts in; on express it adds 1112 variable rows, growing the symbol count by 53%.

What gets indexed

35 languages (counting TypeScript/TSX as one and OCaml's .ml/.mli as one; 37 grammars are registered once those are split out). The core set ships with the package; the long tail downloads on first use — see Install. In monorepos, pnpm/npm workspace packages resolve by package name into their source (@scope/pkg, subpaths), and tsconfig/jsconfig paths are read from the nearest config up the tree, following extends.

Language

Definitions

Resolution

TypeScript, TSX, JavaScript

functions, classes, interfaces, types, enums, namespaces, methods, fields, CommonJS and prototype assignments (app.use = function), object-literal APIs, describe/it tests, Express/Fastify/Koa/NestJS routes

ES and CommonJS imports, re-exports and module.exports, tsconfig paths/baseUrl, this.x fields, new T() locals, constructor parameter properties

Python

functions, classes, methods, properties, dataclasses, enums, constants, pytest tests, FastAPI/Flask routes

relative and package imports, __init__ re-exports, TYPE_CHECKING imports, annotated parameters and fields, self.x = x injection

Go

functions, methods with receivers, structs and embedded fields, interfaces, consts and vars, Test* functions, net/http, gin, echo, chi and fiber routes

package-directory scope, module-path imports via go.mod, receiver types, x := T{} and NewT() locals

Rust

functions, impl blocks, structs, enums, traits, consts, type aliases, macros, #[test]

use trees with as/globs, mod files, crate::/self::/super:: paths, Foo::new() locals

Java, Kotlin, Scala

classes, interfaces, records, enums, annotations, methods, fields, JUnit tests, Spring and JAX-RS routes, Ktor routes, ScalaTest specs

declared package recorded per file; imports (import a.b.C, nested types, Kotlin top-level functions, wildcards, import static) resolve by package and symbol name, same-package files see each other across directories, source sets and modules, Kotlin Multiplatform source sets (commonMain, androidMain, iosMain, jvmMain, desktopMain, jsMain, wasmJsMain, *Test) recognised as source roots, inherited fields, declared and new T() locals

C#

namespaces (block and file-scoped), classes, records, structs, interfaces, enums, properties, events, primary constructors, xUnit/NUnit/MSTest tests, ASP.NET attribute and minimal-API routes

using directives and enclosing namespaces, receiver types, var x = new T()

C, C++

functions and prototypes, structs, unions, enums, typedefs, macros, classes, namespaces, templates, out-of-class methods, gtest and Catch2 tests

#include paths, using namespace, smart-pointer members, this->x

Ruby, PHP

modules, classes, mixins, attr_* properties, visibility sections, RSpec/minitest, Rails and Sinatra routes; namespaces, traits, enums, promoted constructor params, PHPUnit, Laravel and Symfony routes

require_relative, PSR-4 best effort, @ivar/$this->x typing, parent::

Swift, Dart, Elixir, Zig, Lua, Bash

classes, structs, protocols, extensions, mixins, defmodule/def, test blocks, Phoenix routes, Lua module tables, shell functions

relative imports, alias/import, require, source

Markdown

headings as sections, links as edges, backticked identifiers as references to code

relative links

Terraform, HCL

every block kind (resource, module, variable, output, …) as a symbol named after its terraform address, plain .tf/.hcl too

traversals as references addressed like the blocks they point at, local module sources resolved to the module's files

YAML shapes (GitHub Actions, GitLab CI, docker-compose, Kubernetes, OpenAPI)

jobs and steps, services with their dependencies and env keys, each Kubernetes document as Kind/name, OpenAPI routes and schemas — YAML of no recognised shape yields nothing, deliberately

secrets/env keys as config reads, compose service dependencies

Makefiles (Makefile, extension-less names detected by content)

targets and variables with docs and .PHONY flags

prerequisites, variable references, include directives as imports, sub-make calls

SQL (grammar-less scanner)

tables, columns, views, functions, triggers, with COMMENT ON as documentation

foreign keys and body table reads as references

Objective-C (.h claimed by content, not just extension)

@interface/@implementation/category/extension members reparented onto one symbol per type, NS_ENUM/NS_OPTIONS macros the grammar can't parse, XCTest methods

#import/#include with local headers resolved next to the source, message sends, allocations, receiver facts, config reads

Groovy, Gradle build scripts

classes, members, Spock specifications and feature methods; Gradle dependencies {}/plugins {} blocks and task definitions read as their own shape rather than through the (weaker) Gradle grammar

JVM-style imports, project dependency references resolved to the subproject build script, task dependsOn references

PowerShell (.ps1/.psm1/.psd1)

functions with comment-based help as docs, classes, members, enums, Pester blocks as nested tests

Import-Module and dot-sourcing as imports, parameter types, env reads

Haskell

modules, types, classes, functions (multiple equations collapsed into one symbol), instances modeled as impl blocks reparented onto their type, hspec describe/it tests

imports, qualified calls, config reads

OCaml (.ml/.mli)

types, modules, classes, let bindings, .mli signatures as prototypes, alcotest/test_* tests

open/include as namespace imports, qualified calls, type references, config reads

F#

namespaces, modules, types, members, attributed test functions

open, calls, member calls, config reads

Julia

modules, structs, functions and constants, macros, multiple methods collapsed into one symbol with a method count, @testset blocks

using/import/include, calls, type annotations, decorators, config reads

Solidity

contracts, interfaces, libraries, functions, constructors, modifiers, events, errors, structs, enums, state variables, Foundry tests

imports, calls, new, emit, modifiers as decorators, using, Foundry vm.env* as config reads

Vue, Svelte single-file components (grammar-less: script/template/style blocks scanned and the script parsed as JS/TS)

one component symbol named after the file, props (defineProps/options-API props/Svelte 5 $props()) as fields, emits as component metadata

imports and calls from the script block, template component usage as references (PascalCased for kebab-case tags), .vue/.svelte specifiers resolved through the JS module resolver

Environment variable reads (process.env.X, os.environ["X"], os.Getenv, …) become config_key symbols with reads_config edges, so "where is DATABASE_URL used" is a graph question.

Every edge records the resolver tier that produced it (scope, import, receiver, unique, heuristic, structural) and a confidence, so you can audit or filter.

How it works

scan (gitignore-aware) → parse (tree-sitter WASM, worker threads) → IR per file
  → SQLite (symbols, refs, imports, local types, FTS5)
  → resolve changed files and their importers (scope → import → receiver → unique)
  → PageRank over production code, Louvain communities with directory prior and hierarchical refinement
  → CLI · MCP · HTML map

Design notes: docs/DESIGN.md. Determinism is a requirement: same input, same IDs, same community numbering, on every machine.

Built output — hashed bundle names (index-DrLLgu_7.js, main.a1b2c3d4.js), known chunk names (chunk-*.js, vendor.js), and directories like site/assets/, public/assets/, static/js/, storybook-static/ — is skipped by name before extraction, and a plain-named file (any language) is still skipped at read time if it's minified (very long lines) or carries an @generated/DO NOT EDIT marker in its first few lines; both counts are reported as "N generated/minified files skipped" when you index. If a legitimate file gets caught by mistake, force it back in with a negation in .symbraignore (!site/assets/hand-written.js) — symbra already reads that file for every ignore rule, including these heuristics.

Development

npm install
npm run grammars     # vendor core grammars into ./grammars and long-tail ones into ./grammars-optional (already committed; the loader checks both locally, so a source checkout never needs network for any language)
npm run build
npm test             # vitest: extractor fixtures + an end-to-end index of tests/fixtures/repo
npm run bench        # retrieval quality over bench/questions.json (needs the repos in bench/repos.json indexed)
node bin/symbra.js index ../some-repo

Adding a language is one file in src/languages/ implementing LanguageSupport (definitions, imports, references, module resolution) plus a fixture and a test. node scripts/dump-node-types.mjs <grammar> prints the node and field names a grammar exposes.


👤 Author

Sitharaj Seenivasan

☕ Support

If this project helps you, consider buying me a coffee — it keeps the work going.

Buy Me A Coffee

📄 License

Licensed under the Apache License 2.0. © 2026 Sitharaj Seenivasan.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sitharaj88/Symbra'

If you have feedback or need assistance with the MCP directory API, please join our Discord server