jarvis
Local-first code intelligence for coding agents. Precomputed SCIP navigation(go-to-definition, find-references, call/type hierarchy, document symbols), Zoektlexical search, natural-language semantic search, and cross-repo blast radius —exposed as nine MCP tools for Claude Code, Cursor, or any MCP client.
One indexing CLI writes up, one stdio runtime reads down — the storage seam in~/.jarvis is the only contract between them. No server, no auth, no network,nothing leaves your machine.
What is it? · How it works · Quick start · MCP tools · Requirements and limits · Indexing · Configuration · Documentation
What is it?
Without jarvis, asking your agent "where is AuthService used?" meansgrepping for the string, re-reading whole files to filter false positives, andguessing at call sites — burning context window on search instead ofreasoning.
With jarvis, the agent calls findReferences and gets exact file-and-rangeoccurrences from a precomputed SCIP index, callHierarchy for the call graph,and semanticSearch for questions like "where is token refresh handled?" inplain language.
Think of it as grep, but matching symbols, definitions, and references —indexed once per repo, answered in milliseconds.
- Declaration-level navigation without any indexer — a Tree-sitter syntaxbaseline (17 languages) is built on every
jarvis indexrun from pip-installedgrammars, no compiler or build system required — on top of a precomputedSCIP index for full precise navigation(TypeScript/TSX, Python, Java/Kotlin, Swift), Zoekt lexical search, andoptional vector search, all from local SQLite/LanceDB files. - Read-only by design. jarvis never edits code; it is the retrieval half.If you want an agent that performs semantic renames and refactors, you wantSerena — the two are complementary.
jarvis is deliberately narrow: one language per repo, macOS/Linux only, andindexing is an explicit step — see Requirements and limitsbefore installing.
How it works
1. **Index.** `jarvis index /repo` builds a Tree-sitter syntax baseline for every supported file first, then optionally runs the language's SCIP indexer and converts the result to SQLite, builds Zoekt shards (plus optional embeddings), and publishes everything **atomically** into `~/.jarvis` as one immutable snapshot selected by a single `current` pointer. SCIP tooling missing or failing degrades the run to exit-0 — the baseline still publishes. 2. **Serve.** `jarvis-server` speaks MCP over stdio and exposes nine tools, backed by lazy singletons; a `zoekt-webserver` is spawned on first search and shared across processes via pidfile. 3. **Ask.** Your agent calls tools. Every query opens the published `index--.db` read-only (`mode=ro&immutable=1`) — the runtime path never writes.
Storage is the seam. The runtime half only ever reads down into it; theindexing half only ever writes up into it; the two share no other contract.Three load-bearing consequences:
- The runtime path never writes. Index files are never mutated in place.
- Publishing is atomic. A reindex writes a new versioned
.db, populatesthe package graph, and runszoekt-index— only once all of that succeedsdoesos.replace(POSIXrename(2)) flip the smallcurrentpointer. Aquery already reading the old file keeps working; there is no downtimewindow, and a failure anywhere leaves the previously published index live. - The package graph is rebuilt, not accumulated. Each reindex clears thatrepo's own outgoing edges before recomputing them, so
blastRadiusalwaysreflects each repo's last index run.
Layer-by-layer detail, the full index pipeline, and the semantic path are indocs/system-architecture.md. Core query/searchlogic is ported from an internal reference implementation; the enterprise shell(FastAPI, Postgres, hosted-git auth, Cloud Build) is dropped in favor of asingle stdio process reading local SQLite files.
Quick start
1. Install the external indexer binaries (only needed for optional SCIPnavigation and Zoekt search — the Tree-sitter syntax baseline ships inside thepip package and needs no external binary): scip, zoekt, per-language indexers:
curl -fsSL https://raw.githubusercontent.com/jarvis-intelligence/jarvis-index/main/setup.sh | sh
2. Install jarvis:
uv tool install jarvis-mcp
3. Index a repo (slug defaults to the directory name):
jarvis index /path/to/your/repo
4. Register the MCP server. Using Claude Code, install the plugin and itregisters itself:
/plugin marketplace add jarvis-intelligence/jarvis-index
/plugin install jarvis@jarvis
Any other MCP client (or Claude Code without the plugin) registers manually:
claude mcp add jarvis --scope user -- jarvis-server
That's it — ask your agent "find all references to AuthService" and it willcall findReferences instead of grepping.
{
"mcpServers": {
"jarvis": {
"command": "jarvis-server"
}
}
}
If your client can't find jarvis-server on PATH (GUI apps often don'tinherit your shell's), use the absolute path from which jarvis-server.
git clone https://github.com/phuongddx/jarvis && cd jarvis
uv sync
claude mcp add jarvis --scope user -- uv --directory "$(pwd)" run jarvis-server
Optional extras
uv tool install "jarvis-mcp[watch]" # + watchdog, for `jarvis watch`
uv tool install "jarvis-mcp[semantic]" # + lancedb/sentence-transformers, for semanticSearch
MCP tools
| goToDefinition | Resolve a symbol to its defining file and range — SCIP when the file has SCIP definition coverage, otherwise the syntax baseline's declaration; each location carries source ("scip" or "tree-sitter") and positionEncoding || findReferences | Every occurrence of a symbol across the indexed repo — SCIP-only: without usable SCIP occurrence data it returns requiredCapability/reason/recovery, never an empty list || callHierarchy | Incoming/outgoing calls for a symbol — SCIP-only (same contract as findReferences) || typeHierarchy | Supertypes/subtypes — SCIP-only; needs an index built with the bundled scip, see limitations || documentSymbols | Outline of every symbol defined in one file — routed per file: the SCIP outline when usable, otherwise Tree-sitter declarations; a syntax-served response carries a coverage object (parsed/partial/failed counts and reason) || searchCode | Zoekt lexical/regex search, optionally filtered to one repo || semanticSearch | Natural-language search — vector hits fused with Zoekt lexical hits and SCIP symbol-definition matches via reciprocal rank fusion || blastRadius | Which other indexed repos depend on a package, up to 2 hops || getIndexStatus | Published commit, freshness, staleness vs. a working tree; capabilities.tools reports per-tool providers, capabilities.syntax reports extraction counts, and freshness names the snapshot generation |
documentSymbols/goToDefinition are per-file provider routed: a file withusable SCIP coverage is answered by SCIP (full identifiers, references,hierarchies); a file without it is answered by the syntax baseline's realTree-sitter declarations, whose opaque syntax: identifiers round-trip throughgoToDefinition. Bare or qualified names search both providers, so anambiguous name returns combined candidates from both.
Every nav tool takes repo (the slug from jarvis index) plus atool-specific symbol or path. All tools report failure the same way — a{"error": "..."} payload rather than a transport-level error, so a query bugnever kills the stdio server.
Requirements and limits
Read this before installing — jarvis is deliberately narrow.
macOS and Linux only. Windows is not supported.
One language per repo. Language is detected by extension plurality acrossgit-tracked files; a polyglot monorepo gets indexed as whichever language hasthe most files. Multi-language merge is out of scope. Override with
--language.Build-free syntax baseline covers 17 languages — Python, JavaScript,TypeScript/TSX, Java, Kotlin, Swift, Go, Ruby, Rust, C, C++, C#, PHP, Scala,Bash, and SQL — served by
documentSymbols/goToDefinitionas declarationoutlines. Grammars are pip-installed dependencies of the package itself(no external binary, no download at index time); a repo with none of thesestill gets Zoekt search.Precise SCIP navigation (
findReferences,callHierarchy,typeHierarchy) covers four language families: TypeScript/TSX, Python,Java/Kotlin, Swift. These tools require real SCIP data — without it theyexplain what is missing and how to retry rather than returning emptyresults.Navigation and search only — jarvis never edits code. If you want anagent that can perform semantic renames and refactors, you wantSerena; the two are complementary.
Indexing is a separate, explicit step. Nothing is live-analyzed. Run
jarvis index(orjarvis watch) to publish an index before querying.Optional SCIP/Zoekt enrichment requires external binaries that
setup.shinstalls (the syntax baseline itself ships in the wheel):Purpose Binary Source SCIP → SQLite conversion scipprebuilt, pinned v0.9.0(minimum — older versions silently drop occurrence ranges)Lexical search zoekt-git-index·zoekt-webservercross-compiled by our CI — upstream publishes no binaries Zoekt symbol queries ( sym:)universal-ctagssystem package manager via setup.sh — without it sym:silently returns nothingTypeScript indexing scip-typescriptnpm install -gPython indexing scip-pythonnpm install -gSwift indexing scip-swiftprebuilt, macOS arm64 only Java/Kotlin indexing scip-javadetect-only — Docker image, asks before pulling Options:
--only <name>to install one dependency,--forceto reinstall,--helpfor usage. Re-running is safe: anything already present is skipped.
Indexing a repo
jarvis index /path/to/your/repo # slug defaults to the directory name
jarvis index /path/to/your/repo --slug foo # or pick one explicitly
jarvis index /path/to/your/repo --scheme MyScheme # Swift repo with an ambiguous Xcode scheme
jarvis index /path/to/your/repo --language python # force the language instead of detecting it from git-tracked files
jarvis index /path/to/your/repo --semantic-include vendor/generated # force-include a path the generated-file filter would otherwise skip
jarvis index /path/to/your/repo --no-scip # skip optional SCIP enrichment; the syntax baseline + Zoekt still publish (exit 0)
jarvis index /path/to/your/repo --scip # re-enable SCIP enrichment (both flags persist per repo)
jarvis list
jarvis status foo
jarvis reindex foo
jarvis forget foo
status (as shown by both list and status) is one of indexing (run inprogress), indexed (baseline published, SCIP usable/disabled/unsupported),partial (published, but the snapshot has documented syntax/SCIP extractiongaps), degraded (published, but the enabled SCIP stage failed, is missing, oris watch-suppressed — exit 0, cause and jarvis reindex <slug> --scip recoveryrecorded), or failed (a required stage/storage/publication failure — nothingnew published; the previous snapshot stays live).
--semantic-include is repeatable — pass it once per path prefix toforce-include several. Like --scheme and --language, once set there is no flag to clearit; change it by re-running jarvis index with the new value(s).
Language detection counts source files by extension across git-tracked files and picks the winner —one language per index:
| Extensions | Indexer |
|---|---|
.ts .tsx |
scip-typescript |
.py |
scip-python |
.java .kt |
scip-java |
.swift |
scip-swift |
Ties break by fixed priority (.ts → .tsx → .py → .java → .kt → .swift)..git, node_modules, .venv, __pycache__, dist, and build areskipped. Reading git rather than walking the filesystem is deliberate: a walkalso counts gitignored scratch directories, which can outnumber a repo's owncode and pick a language it doesn't use.
The pipeline then runs in stages: capture tracked files and build the syntaxbaseline (scratch) → optional SCIP indexer + scip expt-convert (failuresdegrade to exit-0, never blocking the baseline) → zoekt-index into~/.jarvis/.zoekt (its failure fails the run) → optional semantic embeddings →graph edge update → publish everything as one immutable~/.jarvis/scip/_/<slug>/_/index-<sha>-<generation>.db snapshot → atomiccurrent pointer flip → registry update → old snapshots retired.
The
scip/_/<slug>/_/path shape reuses the vendoredIndexConnectionCache's(project, repo, branch)3-tuple layout with the outer two pinned to_(seesrc/jarvis/config.py). It is not a user-facingcontract — only<slug>matters when calling tools.
Swift indexing works end-to-end. It requires scip >= v0.9.0: older converterscannot read scip.proto's typed_range oneof, which is the only range encodingscip-swift emits, and silently produce an index with no navigable positions.jarvis index refuses an older scip rather than publishing one.
Indexing a Swift repo with code-signed app-extension targets additionally requiresscip-swift >= v0.1.2: earlier versions pass no code-signing overrides to xcodebuild, whichthen fails provisioning for every signed target before compiling anything. Because setup.shskips any dependency that is merely present, an existing install is not upgraded byre-running it — use sh ./setup.sh --only scip-swift --force.
Watching a repo (auto-reindex)
jarvis watch /path/to/your/repo # debounce defaults to 5s
jarvis watch /path/to/your/repo --debounce 3
jarvis watch /path/to/your/repo --scheme MyScheme
jarvis watch /path/to/your/repo --language python
jarvis watch /path/to/your/repo --no-scip # persist SCIP-off for this repo
Each debounced reindex runs the same staged pipeline as jarvis index. When aSCIP attempt already failed at the current commit, a watch run skips only thatSCIP retry (the syntax baseline still publishes); a new commit, an explicitjarvis reindex, or an explicit --scip retries enrichment.
Runs in the foreground (not a daemon) using watchdog — install it with thewatch extra. A burst of file changes (e.g. an editor's atomic save touchingseveral files) coalesces into exactly one reindex. The reindex fires once--debounce seconds (default 5) have passed since the last file change —this prevents thrashing on rapid edits. .git, node_modules, .venv,__pycache__, dist, and build are ignored.
Tool details
getIndexStatustakes an optionalrepo_path(the repo's local gitworking directory) to compare the published commit againstgit rev-parse HEAD. Omitted, freshness is reported without a stalenesscheck — neverstale: truewithout evidence.searchCodetakesqueryplus an optionalrepofilter. On first callit lazy-spawns an embeddedzoekt-webserver(pidfile'd so a second jarvisprocess reuses it instead of spawning a duplicate; killed on clean exit viaatexit).blastRadiustakesrepoplussymbol_or_package(e.g."npm:@scope/ name", the same"{manager}:{name}"stringjarvis indexderives fromeach repo's SCIP symbols). Returns every other indexed repo whose packagedepends on it, up to 2 hops, each tagged with its hop distance. The packagegraph has no per-node timestamp, sofreshnessis always"unknown"here —an honest limitation of the schema, not a bug. Cross-repo edges resolve byexact package name against whatever has already been indexed: index thedependency first, or re-runjarvis index/reindexafter indexing it,for an edge to appear. Each reindex retracts that repo's own stale edgesbefore recomputing them, so a removed dependency's edge disappears too —the graph always reflects each repo's last index run, not anaccumulation of every run it's ever had.semanticSearchtakesrepoplus a natural-languagequery. Requires theoptionalsemanticextra. Results fuse a LanceDB vector search overtree-sitter-chunked code withsearchCode's Zoekt hits via reciprocal rankfusion. Raises a clear error if the repo has never been indexed with the extrainstalled (jarvis reindex <slug>after installing it builds the missingtable); indexing itself is non-fatal — a failure there never blocks the restofjarvis index. Semantic indexing also respects.gitignore(on top ofthe hardcoded ignore-directory list) and skips any file over 1 MB, in additionto the existing generated-file banner/long-line detection —--semantic-includeoverrides all three.
Known upstream limitations
These are real behaviors of the underlying SCIP tooling (scip expt-convertas of v0.9.0, scip-java, scip-kotlinc), not jarvis bugs:
typeHierarchyreturns an explicit{"error": ...}, not empty arrays, onindexes built with an unpatched upstreamscip— that converter declaresglobal_symbols.relationshipsin its schema but never writes it. An emptyresult would wrongly assert "no supertypes"; the error says "cannot tell"instead. setup.sh installs a fork build carrying the fix, so a freshjarvis reindex <slug>makes the tool work. Reported upstream:scip-code/scip#464, fixscip-code/scip#465 (open, CI green).displayName/kindare backfilled from the symbol string. The converter never populatesglobal_symbols.display_name/.kind, soquery.py's_display_and_kindparses both from theSCIP symbol string whenever the database columns are empty (which they still normally are) —documentSymbolsreturns real values in practice; only a genuinely unparseable symbol fallsthrough tonull.searchCode'srepofilter matches Zoekt's own repository name, whichjarvis indexnow names after the slug viazoekt-index -meta— so thisno longer diverges for repos indexed with current code. Shards published byan older jarvis still carry their old directory-derived name until youjarvis reindex <slug>.scip-javacan't index Android/Gradle repos at all — its Gradle pluginkeys off Gradle's standard source sets, which AGP replaces with its variantmodel, so the build emits zero SCIP shards(scip-java#177).- Kotlin indexing requires an exact Kotlin version match —
scip-kotlincis compiled against one pinned Kotlin release (SCIP_JAVA_KOTLINinsetup.sh, currently2.2.0); its compiler-plugin API is internal andunstable even across patch releases, so any other version fails.Both cases are detected automatically from the indexer's own failure outputand degrade to exit-0degraded(SCIP skipped, syntax baseline stillpublished) rather than failing outright. - Maven-built Java repos need bash >= 4.4 on macOS — scip-java's generated
javacwrapper (#!/usr/bin/env bash,set -eu) expands"${LAUNCHER_ARGS[@]}"unguarded, which errors on bash < 4.4; macOS shipsonly 3.2, so the build dies atdefault-compilewithLAUNCHER_ARGS[@]: unbound variable.setup.shworks around it by linkingfirst onPATHfor the indexer. If no bash >= 4.4 is installed, indexingfails with the remedy rather than degrading — unlike the two cases above,this one is fixable (brew install bash).
Configuration
Data directory (default ~/.jarvis):
JARVIS_DATA_DIR=/custom/path jarvis index /path/to/repo
Environment variables:
JARVIS_DATA_DIR— override default~/.jarvisfor all indexes and registryJARVIS_FALLBACK_SEARCH_ONLY— removed. No longer read; jarvis prints aone-line note if your shell still exports it. Replaced by the reversiblepersisted--scip/--no-scipflags onindex/reindex/watch.JARVIS_EMBEDDING_QUERY_PREFIX/JARVIS_EMBEDDING_DOC_PREFIX— override thequery/document instruction prefix applied before embedding. Auto-detected for bge-m3,e5, and nomic-embed; set these if using a different model that needs one —semanticSearchwarns when an unlisted model has no prefix configured.
Agent skills
Three agent skills ship in the Claude Code plugin, under plugin/skills/:
jarvis-setup— install, register, index, verify.jarvis-use— prefer jarvis for structural queries (find references, go-to-definition, hierarchy).jarvis-issues— file jarvis bugs/features viagh.
Install them, and register the MCP server, with:
/plugin marketplace add jarvis-intelligence/jarvis-index
/plugin install jarvis@jarvis
See Quick start above for the manual registration alternative.
Standards
Blob decoding follows the SCIP protocol:scip_pb2.py is generated from scip.proto at scip-code/scip tagv0.9.0 (regenerated up from v0.7.0, which lacked the typed_range oneofscip-swift requires), and occurrence/relationship blobs are decoded as realscip.Document / scip.SymbolInformation messages.
The SQLite layer (documents, chunks, global_symbols, mentions,defn_enclosing_ranges) is not part of that published spec — it is theoutput shape of the experimental scip expt-convert sub-command, verified byhand against a real index. Treat it as a moving target across scip releases.
Tests
uv run pytest
Integration tests that shell out to the real scip-python / scip /zoekt-index binaries are marked integration:
uv run pytest -m "not integration" # unit only
uv run pytest -m integration # real-binary pipeline
Documentation
docs/project-overview-pdr.md— scope, value prop, out-of-scope itemsdocs/system-architecture.md— architectural guarantees, storage layout, query pathsdocs/codebase-summary.md— module map, test coveragedocs/code-standards.md— code patterns and conventionsdocs/project-roadmap.md— all phases complete, future ideas
All 4 planned phases are shipped — seeplans/0724-2316-jarvis-mcp-implementation/plan.md.
License
MIT