Skip to content
kivgraph

semantic code intelligence for coding agents

Know what breaks before your coding agent changes it.

Kivgraph gives Claude Code, Codex and other coding agents exact symbol references, dependency paths and cross-repository change impact — from a graph on your own machine, over MCP.

Supported MCP clients: Claude CodeCodexOpenCodeClaude DesktopOh My Pi

install the latest release
$ curl -fsSL https://github.com/Luqueee/kivgraph/releases/latest/download/install.sh | bash
  • local only
  • MCP over stdio
  • no model, no API key
  • Apache-2.0

ask without a name

The one tool that does not need you to know what it is called.

find_by_intenttakes a sentence, not a symbol
intent

which code refuses to publish a generation when the disk is nearly full

files to openranked 1 of 10
  • internal/storage/generation/store.go
  • internal/storage/generation/types.go
grep -l diskranked 25 of 34

Not one identifier in that question. 4 of its words match nothing in the graph — which, the, disk, nearly — and the rest carried it. The rows are text that looked alike, not edges a checker resolved, so they are where to start reading rather than the answer.

what find_by_intent returns, and where it loses

what your agent can ask

One symbol, three questions, and the answer your agent gets back. Every file count, hop, edge kind, line range and token figure below is from one run of the published benchmark, and each panel names the question it answers.

The corpus behind it is 37 private repositories, so the repository, file and symbol names are substituted. The structure is not: same repositories, same row in each, same language, same numbers. Every figure here can be checked against results-all.json in the repository.

Choose a question to see the answer

Which call sites use the withBackoff declared in platform-lib/src/util/retry.ts?

find_referencesplatform-lib src/util/retry.ts:135withBackoff
    • runtime-coresrc/queue/dispatcher.tswithBackoff@2
    • runtime-coresrc/queue/worker.tswithBackoff@13
    • runtime-coresrc/queue/partitioning.tswithBackoff@7
    • edge-servicesrc/rpc/server.tswithBackoff@20
    • client-sdksrc/client/session.tswithBackoff@20
kivgraph
332 tokens, 2 calls
grep + reading
10,054 tokens, 8 calls

Both arms named all five files, in three repositories. Ours cost 30x less, and every edge is EXACT_TYPECHECKED by the TypeScript checker.

question R1_ts_xrepo of results-all.json, commit 954b9eb

Which files hold something that reaches jitterFor within two hops?

get_blast_radiusdata-service internal/storage/retry.go:25jitterFor
  • one hop — reached from jitterFor

    • data-serviceinternal/storage/retry.gowithBackoff@49-78
    • data-serviceinternal/storage/retry_test.goTestJitterForWithinWindow@94-103
  • two hops — reached from withBackoff

    • data-serviceinternal/storage/client.goOpen@104-144
    • data-serviceinternal/storage/retry_test.goTestWithBackoffRetriesThenSucceeds@24-43
kivgraph
2,380 tokens, 1 call
grep + reading
897 tokens, 2 calls

Nine affected nodes over two hops, all CALLS_DIRECT. Both arms were exact and grep was cheaper here — a two-hop chain inside one package is a chain grep can still walk by hand.

question I1_go_depth2 of results-all.json, commit 954b9eb

Which files outside platform-lib consume the StatusCode it declares?

find_cross_repo_consumersplatform-lib src/errors/status.ts:29StatusCode
    • edge-cachesrc/http/session-guard.tsIMPORTS_SYMBOL
    • api-servicesrc/http/routes.tsIMPORTS_SYMBOL
    • client-sdksrc/index.tsREEXPORTS
kivgraph
530 tokens, 1 call
grep + reading
12,200 tokens, 7 calls

Three of three. Grep found two: the re-export in client-sdk never spells StatusCode, so no text search reaches it. It is grep's only miss in the twenty-nine questions.

question X1_ts_shared_enum of results-all.json, commit 954b9eb

the whole benchmark, method and losses included

less context, same exact answer

The alternative to a code graph is not being wrong. It is spending the context. So the benchmark's sixth arm is grep and reading the files — what an agent already does — and both arms are scored against a ground truth written by hand, question by question.

35,961
tokens — Kivgraph graph queries
267,980
tokens — grep + file reading
28 / 29
exact answers, both arms

context saved

~87% fewer tokens

Kivgraph graph queries35,961
tokens
grep + file reading267,980
tokens

Twenty-nine questions over a 37-repository corpus, one pass. Both arms answered 28 of them exactly; the answers were scored against a hand-written ground truth, not against each other. See the method.

questions
29 across six sets
corpus
37 git repositories
version
kivgraph 0.5.0
commit
954b9eb
tokenizer
o200k_base
  1. Precision was 1.00 on both arms. Kivgraph missed one TypeScript test file; grep missed one cross-repository consumer that never spells the symbol.
  2. The median question cost 5.95x less through the graph, with the widest margin at 86.8x. grep was cheaper on five of the twenty-nine, between 0.26x and 0.53x, and every one of those five was a question both arms answered fully.
  3. One corpus, one machine, one tokenizer. o200k_base is a proxy for the tokenizer an agent actually pays, so the ratio between the rows is the claim and the absolute values are not.

The harness, the hand-written ground truth and the verbatim response of every call are published beside the numbers inbenchmarks/graph-tools-comparison/, so any figure here can be checked against the bytes it came from — including the five where we lose. The five-way comparison against four other code graphs is a separate pass and is reported separately: explore the benchmark.

grep finds text, Kivgraph resolves symbols

Kivgraph does not replace grep. It answers the structural questions grep cannot answer reliably — and it says so on the two rows below where a text search is simply the better tool.

The difference is not thoroughness, it is evidence. An edge in this graph comes from the language's own checker, so an empty list of references is a claim: nobody calls it. A search that returns nothing has not made that claim, and cannot.

how the edges are resolved

What a text search and a resolved code graph can each answer, with the benchmark question that decided each row
questiongrepkivgraph
Find a string anywhereNot what the graph is for; it indexes declarations, not lines.yesno
Answer a rare name in a small treeT1_go_trivial — two occurrences in the corpus, and grep at 0.53x.yes, cheaperyes, dearer
Tell two homonyms apartH1–H5 — a method, an interface, a type, an alias and a Rust trait impl, each with a namesake elsewhere.noyes
Follow callers without opening the filesR1_ts_xrepo — grep needed eight calls and 10,054 tokens; one query needed two calls and 332.manuallyyes
Reach transitive impactI1–I3 — two and three hops. Inside one package grep is still cheaper; across packages it stops being.one hop at a timeyes, bounded by depth
Find consumers in another repositoryX1_ts_shared_enum — grep found two of three consumers and missed the re-export.only if they spell the nameyes
Prove that nothing calls itA1–A3 — grep is three to four times cheaper on these and still cannot distinguish `nobody calls it` from `my pattern missed it`.noyes

a name is not a symbol

withBackoff is declared seven times in the benchmark corpus, in five repositories and two languages — two of them the same file path in two different repositories. The graph holds twenty-two symbols with that name — the other fifteen are TypeScript barrel re-exports — and the identifier appears in twenty-two files. Identical text, seven different symbols.

That corpus is private, so the repository, file and symbol names below are substituted; the seven declarations, the five repositories, the language split and the line numbers are the captured ones. The 129-token cost of the refusal was measured against the real identifiers — a token count is a function of the strings it encodes. The two transcripts on the right are literal: they come from Kivgraph's own repository.

one name, as a text search sees it

  • withBackoffconfig-libsrc/retry.ts:41ts
  • withBackoffmedia-serviceinternal/secrets/provider.go:240go
  • withBackoffdata-serviceinternal/secrets/provider.go:143go
  • withBackoffclient-sdksrc/managers/command.ts:19ts
  • withBackoffdata-serviceinternal/storage/retry.go:49go
  • withBackoffplatform-libsrc/util/retry.ts:135ts
  • withBackoffclient-sdksrc/types/result.ts:91ts

seven declarations, one spelling — a search cannot rank them, and choosing the likeliest is how a graph invents an edge

the server refuses to choose, and says what to ask instead
tools/call find_references{"name":"withBackoff"} AMBIGUOUS_SYMBOL: name "withBackoff" declares 7symbols; repeat with the repository and path ofthe one you mean: config-lib:src/retry.ts:41, ... # 129 tokens, and the answer is the next call
an absence, captured on Kivgraph's own repository
tools/call find_cross_repo_consumers{"repository":"kivgraph","path":"internal/facts/facts.go", "qualified_name":"MergeAll"} # total 0   returned 0   consumers nullguidance: no repository in the published graph consumes this          symbol. Check graph_status if a consumer is          registered but was not indexed, and          find_references for uses inside its own          repository
  1. Edges come from go/types, the TypeScript checker, rust-analyzer and the Dart analysis server. Never from matching a name, a path, an alias or a single plausible candidate.
  2. Asked by a name that means seven things, the server refuses and names the seven. It costs 129 tokens and the next call is a copy of one row.
  3. Two homonymous symbols are two symbols, with different kinds, different packages and different ranges.
  4. A reference that cannot be resolved is published as UNRESOLVED with its reason, repository and language rather than dropped, and CANDIDATE is a different result from EXACT.

how resolution works, per language

your repositories are not isolated

An agent reads one repository at a time. The change it is about to make does not stop there. Kivgraph registers every repository of a workspace into one graph, keeps each one's identity inside the keys it publishes, and answers who consumes a symbol from outside the repository that declares it.

This is the question with no cheap substitute. A text search finds a consumer only if that consumer spells the name, and a language server stops at the edge of its own workspace.

how the cross-repository graph is built

platform-lib · @workspace/platform

declares

StatusCode

src/errors/status.ts:29

client-sdk · @workspace/sdk

consumes

export *

src/index.ts:1

One edge, crossing a repository boundary. Grep found the other two consumers of this symbol and missed this one. The corpus is private, so the names are substituted; the three consumers, the 22 package-level rows and the edge kind are the measured ones.
the edge
REEXPORTS, EXACT_TYPECHECKED, resolved by the TypeScript checker of the providing package — not by the two packages sharing a name.
what grep saw
Nothing. The consuming file never spells the symbol, so no text search reaches it. Two of the three consumers were found; this was the third.
what a language server sees
Its own workspace. A cross-repository consumer is outside it, so the question cannot be asked there at all.
kept apart on purpose
The same answer carried 22 package-level rows. They are reported separately and never counted as uses: a package depending on a package is not a symbol using a symbol.

works where you already code

One MCP server over stdio, and the same structural context in every client that speaks the protocol. kivgraph mcp install detects the agents on the machine, lets you pick, and writes the entry — with a backup beside the file it edits and --dry-run to see the change first.

register the client
$ kivgraph mcp install --scope user

The scope is user or project. Nothing is installed inside a repository you registered, and the command neither initialises Kivgraph nor indexes anything.

Eleven tools are registered once a generation is published. Before that the server registers only index_project and says so, rather than offering ten tools that would have nothing to answer from.

every client, with its configuration path

start with your workspace

The installer puts the bundle in ~/.local/opt/kivgraph with launchers in ~/.local/bin, verifies the release checksum and then the bundle's own, and stops there: it configures no client, writes no configuration and indexes nothing. These four commands are the rest.

Register, check, index, connect. Nothing answers until a generation exists, so that order is the whole setup.

  1. register a repository

    $ kivgraph init \
      --repository project=/absolute/path/to/project \
      --languages go,typescript,rust

    --repository NAME=PATH may be repeated. The name is compared exactly, and it travels inside the stable keys of everything the repository declares.

  2. check the machine

    $ kivgraph doctor

    doctor reports the configuration, the toolchains it found and the state of the published graph. It names the language version ceiling this binary type-checks Go with, which is not the go on your PATH.

  3. index and publish

    $ kivgraph index --full

    The pass analyses every registered repository, validates the canonical graph and publishes it as a new generation. Publication is atomic: a candidate that fails integrity or validation never becomes CURRENT, and the previous generation keeps serving.

  4. register the client

    $ kivgraph mcp install --scope user

    Detects the coding agents on this machine and writes the entry after you select them. Supported targets: claude-code, claude-desktop, codex, opencode and oh-my-pi.

or wire a client by hand

{
  "mcpServers": {
    "kivgraph": {
      "command": "/home/user/.local/bin/kivgraph",
      "args": [
        "serve",
        "--config",
        "/home/user/.config/kivgraph/config.yaml"
      ]
    }
  }
}

serve is stdio and opens no port. It writes MCP framing to stdout and logs to stderr, and it follows the published generation: an index --full in another terminal cannot leave it answering from a graph that no longer exists on disk.

before you start

  • Runtime for the installed bundle: Bash, Node.js 22 or later, curl, tar, and sha256sum or shasum. Published platforms are linux/amd64 and darwin/arm64; the installer refuses an Intel Mac by name rather than installing a bundle that cannot run.
  • Per language, and only for the languages you register: indexing Rust needs cargo on the PATH — the bundled rust-analyzer cannot load a Cargo workspace without it — indexing Python needs Python 3.10 or later, and indexing Dart needs the Dart or Flutter SDK.
  • index_project is the only tool that writes. Without confirmed=true it refuses, and it never writes inside the source projects.
captured: index_project, isError true
tools/call index_project        # confirmed unset PERMISSION_REQUIRED: user approval is required; confirm theoperation before setting confirmed=true

languages

One authority per language, and it is the language's own. An edge is never created from a name — but the five are not equal, and the difference is printed on each row rather than left for you to discover after indexing.

  • Go

    type-checked

    go/types, linked into the binary

    Packages are loaded through go/packages with the build tags you configure, and resolution comes from a full type universe. The unit of analysis is the module.

    The binary type-checks only up to the language version of the toolchain that built it. A module above that ceiling is rejected by name.

  • TypeScript

    type-checked

    the @kivgraph/ts-worker tsserver process

    Each package.json project is analysed by the TypeScript checker itself. A cross-package symbol is placed by the provider's declaration map, or by asking the provider's own checker what its module exports.

  • Rust

    type-checked

    rust-analyzer scip, bundled and pinned

    The authority is rust-analyzer, invoked once per Cargo workspace. Tree-sitter contributes the syntactic class of a use and the declared visibility, never identity.

    core, std and alloc are not indexed, so derive, operator overloading, ? and standard-library calls produce no edges.

  • Dart

    analyzer-resolved

    the Dart or Flutter Analysis Server

    Dart and Flutter source is indexed through the SDK analysis server. Local declarations, navigation references and calls are published with analyzer evidence, while external packages are reported as unresolved unless their source is indexed.

    Tests and generated files are excluded by default and can be enabled in configuration.

  • Python

    exact only with a configured analyzer

    the configured semantic worker or bundled AST worker

    A configured semantic provider — a Pyright-compatible language server — can publish exact declarations and references. Without one, the bundled worker covers Python and stub files without guessing dynamic names, and everything it infers stays CANDIDATE rather than becoming EXACT.

    Dynamic imports, monkey patching and unresolved runtime attributes remain explicit candidates or unresolved facts.

how resolution works

see what the agent sees

Every answer on this page comes out of one immutable snapshot, published atomically and never queried from the database behind it. That snapshot is inspectable two ways: as the numbers below, and as a read-only 3D view of the same structure. It is a debugging surface, not the product — but it is the fastest way to see whether the graph your agent is answering from is the graph you meant to build.

kivgraph ui serves that view. It binds 0.0.0.0:7777 by default and carries no authentication, so on a shared network restrict it with --addr 127.0.0.1:7777; what it exposes is repository paths, file paths, symbol names and signatures. Its palette is the one the transcripts on this page borrow: one colour per node kind, plus one for an exact, type-checked edge.

  • repository
  • package
  • file
  • symbol
  • exact dependency

graph_status

  • statusready
  • snapshot_id30
  • snapshot_built_at2026-08-15T11:14:10Z
  • schema_version2
  • resolver_version0.5.0
  • repositories2
  • packages57
  • files311
  • symbols10957
  • edges40125
  • package_edges123
  • unresolved1642

edges_by_kind

  • CALLS_DIRECT6198
  • EXPORTS312
  • PASSES_AS_CALLBACK75
  • ASSIGNS_FUNCTION22
  • IMPLEMENTS9
  • EMBEDS8
  • EXTENDS4

list_repositories

namelanguagesindexed_branchindexed_commitstate
kivgraphgo, typescriptmaind67bc0emoved false
molego, typescriptmain4cc05cdindexed_dirty true

The 1642 unresolved references are published with their reason, repository and language rather than dropped, and they are counted apart from the 40125 edges. graph_status also reports whether a repository has moved since it was indexed, which is the answer when a result looks stale.

the viewer guide

frequently asked questions

Does Kivgraph replace grep?

No, and the benchmark says where it should not. grep is cheaper for a rare name in one small repository: over the 29 published questions it cost fewer tokens on five of them, between 0.26x and 0.53x of ours, and both arms were fully correct on all five. Kivgraph is for type-checked references, transitive impact, consumers in another repository, and empty answers you can rely on.

Does my code leave my machine?

No. The server is a local process that speaks MCP over stdio, and no query path makes a network call. One thing to know: kivgraph ui, the optional viewer, opens a local HTTP listener that binds 0.0.0.0:7777 with no authentication and exposes repository paths, file paths, symbol names and signatures. On a shared network, start it as kivgraph ui --addr 127.0.0.1:7777.

Does Kivgraph need an LLM, an API key or an embedding step?

None of the three. The edges are resolved by go/types, the TypeScript checker, rust-analyzer and the Dart analysis server, all running locally. There is no model in the indexing path and no key to configure.

How is this different from a vector database over my code?

A vector index answers what looks similar; this answers what is connected, and each edge records the analyzer that resolved it. The difference shows up in an empty answer: a similarity search always returns its nearest neighbours, whereas an empty reference list here is a claim that nothing calls the symbol.

How does cross-repository analysis work?

Every registered repository is indexed into one graph, and each keeps its identity inside the keys it publishes, so a symbol used from outside the repository that declares it is a cross-repository consumer with its own evidence. In the published benchmark this found three of three consumers of one symbol where grep found two: the third re-exports it and never spells its name, so no text search reaches it.

Which languages are supported, and are they equal?

Go, TypeScript, Rust, Python and Dart, and no, they are not equal. Go, TypeScript and Rust edges are type-checked. Dart edges are resolved by the Dart Analysis Server. Python publishes exact semantic facts only when a configured Pyright-compatible analyzer provides them; with the bundled AST fallback its inferred references stay CANDIDATE and never become EXACT.

What happens when Kivgraph finds no references?

It says so, and that is the answer rather than a failure. A reference that could not be resolved is published as UNRESOLVED with its reason, repository and language instead of being dropped, and CANDIDATE is reported as a different result from EXACT — so an empty list means nobody calls the symbol, not that nothing was found.

How much does indexing cost, and how often must I re-index?

A full rebuild is the only path; there is no incremental mode. Measured cold on the benchmark corpus of 37 repositories and 96,482 published symbols, one pass took 37.6 seconds and left 1,423 MB of on-disk state — the slowest and heaviest of the five tools compared. Re-index when a repository has moved; graph_status reports whether one has.

What can it answer before I have indexed anything?

Nothing, deliberately. With no published generation the server registers only index_project and reports that it has no graph to answer from, rather than offering ten tools whose answers would be empty for the wrong reason.

More answers in the Kivgraph FAQ, the limits and on GitHub.

Give your coding agent structural context.

One binary, one index, one MCP entry. Nothing leaves the machine, and every answer carries the evidence it was resolved from.

install the latest release
$ curl -fsSL https://github.com/Luqueee/kivgraph/releases/latest/download/install.sh | bash