MCP troubleshooting
Every failure the tool surface returns carries a stable code and a human-readable message. Branch on the code; the message text is free to change.
| Code | Meaning |
|---|---|
INVALID_ARGUMENT | The arguments cannot name a symbol, a page or a limit. |
SYMBOL_NOT_FOUND | Nothing in the published graph answers to that selector. |
AMBIGUOUS_SYMBOL | The qualified name matches more than one symbol. |
REPOSITORY_NOT_FOUND | The repository given is not in the published graph. |
CURSOR_INVALID | The cursor is malformed, or does not match the active query and sorting. |
CURSOR_SNAPSHOT_EXPIRED | The cursor belongs to an older snapshot. |
TRAVERSAL_LIMIT_REACHED | The traversal exceeded its deadline. |
SNAPSHOT_UNAVAILABLE | The published snapshot is inconsistent or could not be read. |
INDEX_NOT_READY | No graph is published. |
PERMISSION_REQUIRED | index_project was called without approval. |
PERMISSION_DENIED | The user refused the index_project elicitation. |
INDEXING_FAILED | index_project ran and the pass failed. |
The client sees no tools
Section titled “The client sees no tools”Symptom
The client lists one tool, index_project, and nothing else. The session instructions read:
Kivgraph has no published graph to answer from, so it exposes no query tools. Run "kivgraph index --full" to build one, then restart this server. Until then, use the host's own search and file tools.Cause
serve checks for a published generation before it registers anything. With none, it registers only index_project and returns. index_project is the exception because it is how a client without a graph builds one, and it needs no graph to run.
The handshake still completes. The client spawns this process itself, so exiting reads as a crash and says nothing; and publishing ten tools that would answer INDEX_NOT_READY to everything teaches the agent that the tools do not work.
Fix
Build a generation, then restart the server so it loads it:
kivgraph index --fullWhile the store is empty, kivgraph doctor says so without failing:
graph.store: PASS (no published generation)snapshot: PASS (no published generation)unresolved: PASS (no published generation)A query says the index is not ready
Section titled “A query says the index is not ready”Symptom
INDEX_NOT_READY: no graph is published yet: index a project with index_project, or run "kivgraph index --full"Cause
No graph is published, so nothing can answer. serve registers the query tools only when a generation exists, so this is what a query tool returns whenever the snapshot store it was given holds nothing — the state a freshly installed client is in. The code is stable; only the message carries the guidance.
Fix
Either of the two the message names. From a shell:
kivgraph index --fullOr, from the client, call index_project with the project and confirmed set. A configuration written outside the default location is self-contained: its state, its cache and its registry hang from its own directory, so a serve started with --config pointing elsewhere does not see the graph built against ~/.config/kivgraph/config.yaml.
A tool refuses the arguments
Section titled “A tool refuses the arguments”Every tool that takes a symbol accepts either a stable_key or the triple repository + path + qualified_name, and exactly one of the two. path is repository-relative. Two selectors can disagree, so passing both is refused instead of resolved quietly.
The selector is malformed
Section titled “The selector is malformed”Symptom
One of these, with code INVALID_ARGUMENT:
one of stable_key or qualified_name is requiredpass either stable_key or qualified_name, not bothrepository and path narrow a qualified_name; a stable_key already names one symbolpath is repository-relative, so it requires repositorystable_key must not carry surrounding whitespaceCause
The arguments do not name exactly one symbol.
Fix
Build the call out of the answer you already have. Every row the surface returns carries repository, file_path, qualified_name and a line range, so the next call is the triple copied from the previous response. Stable keys never have to enter the conversation.
The name matches more than one symbol
Section titled “The name matches more than one symbol”Symptom
With code AMBIGUOUS_SYMBOL. With no repository or path yet, the message names each candidate by where it is, because that is a narrowing you can express in the next call:
qualified name "<name>" names <n> symbols; narrow with repository and path: <repository> <path>:<start>-<end>, ...Once repository and path were both given and the name still matches more than once, only the key separates them, so the keys are listed instead:
qualified name "<name>" names <n> symbols in <repository> <path>, so only a stable_key separates them: <key>, <key>Cause
Kivgraph does not pick one for you. Choosing by name is the coincidence its edges exist to avoid.
Fix
Add repository, then path. When the message offers keys, pass one as stable_key and drop the triple.
The symbol is not found
Section titled “The symbol is not found”Symptom
A name the narrowing excluded says where it looked and how to widen:
SYMBOL_NOT_FOUND: qualified name "NoSuchThing" was not found under kivgraph; call it without repository and path to search the whole graphA name nobody declares says only that:
SYMBOL_NOT_FOUND: qualified name "<name>" was not foundAn unknown repository is its own code:
REPOSITORY_NOT_FOUND: repository "<name>" is not in the published graphCause
The two not-found cases need different fixes, so they read differently. The first is a narrowing problem; the second means the graph has no such qualified name.
Fix
Drop repository and path and call again. If it is still absent, search by name instead: find_symbol returns an empty result set rather than an error, so {"name": "ThisSymbolDoesNotExistAnywhere"} answers "total": 0 and proves the absence. Use list_repositories for the registered names.
index_project refuses to run
Section titled “index_project refuses to run”Symptom
PERMISSION_REQUIRED: user approval is required; confirm the operation before setting confirmed=trueOr, when the client supports elicitation and the user declines:
PERMISSION_DENIED: project indexing was not approvedCause
index_project registers projects and rebuilds the whole corpus. It is gated on explicit user approval, and it never infers approval from the call.
Fix
A client that declares the elicitation capability is prompted by the server, naming the projects the approval covers, and needs no argument. A client without it must ask the user itself and then send confirmed: true. Pass every project in one call: a rebuild costs the whole corpus.
If the pass itself fails, the code is INDEXING_FAILED and the message carries the reason the indexer gave.
Two rebuilds at once
Section titled “Two rebuilds at once”Symptom
another process is publishing into this generation storeReaching a client through index_project, this arrives as INDEXING_FAILED with that text inside the message.
Cause
Publishing a generation takes a flock on the store. One state directory is shared by an index --full, a client’s index_project and a server’s resynchroniser, so without the lock two passes overwrite each other.
The lock does not wait. A rebuild takes minutes and blocking would look like a hang, so the loser is told instead.
Fix
Let the winner finish, then retry. For the follower inside a running serve or ui this is not a failure and is not reported as one: it is exactly what the lock exists to produce, and the generation already published keeps answering.
The graph is behind the working tree
Section titled “The graph is behind the working tree”Symptom
Paths and line ranges name code that has moved. graph_status reports it in repository_freshness, and repositories_moved counts the entries whose working tree left the indexed commit. One entry, with path omitted:
{ "name": "mole", "languages": ["go", "typescript"], "indexed_commit": "4cc05cdc2c73cb7111b7b38447639c1444ab8410", "indexed_branch": "main", "indexed_dirty": true, "current_commit": "4cc05cdc2c73cb7111b7b38447639c1444ab8410", "current_branch": "main", "moved": false}Cause
A snapshot is immutable. It describes the commit it was built from, whatever the tree holds now.
Fix
Call graph_status first and read the entry for each repository:
| Field | What it says |
|---|---|
indexed_commit, indexed_branch | The tree the graph was built from. |
current_commit, current_branch | The tree on disk now. |
indexed_dirty | The indexed tree carried uncommitted changes, so matching commits do not prove matching files. |
moved | The two disagree. moved_detail names both positions. |
A repository whose HEAD could not be read is not counted as moved and is not counted as fresh either; its entry says why. A path that is not a checkout, and a graph built before the commit was recorded, are also not moved — and only a tree still holding the indexed commit means the results can be trusted. A derived entry is a provider Kivgraph built from the machine rather than from the registry; nothing checks it out and nothing can move it.
serve and ui follow the published generation and republish when the CURRENT pointer advances, so a rebuild in another terminal reaches a running server. To force the corpus back onto the current code:
kivgraph index --fullOne thing the fact cache cannot see: a lockfile is searched from the registered repository root upwards, and node_modules is never walked because that is the lockfile’s job. A lockfile that is not found leaves that dependency with no control at all — in a pnpm monorepo it lives above the registered repositories. This is a declared limitation. Where it applies, rebuild with the cache off or set indexing.fact_cache: verify, which analyses everything and aborts the pass when an entry disagrees with the analysis.
After a full clean the server serves nothing new
Section titled “After a full clean the server serves nothing new”Symptom
clean prints the warning:
clean: restart any running serve or ui before the next index --fullIgnored, a running server keeps answering from the graph that was removed, and its log says so once:
generation store was rewound to <id> while serving <id>: restart to follow it againCause
After a full clean the numbering returns to 000001, and SnapshotStore.Publish only accepts a strictly newer generation. A live server keeps the graph that no longer exists and installs no further one. The follower declares it once, and clean warns about it in advance.
Fix
Stop the long-running processes, rebuild, start them again:
kivgraph stopkivgraph index --fullkivgraph stop --dry-run lists what it would stop and stops nothing. It matches serve and ui only: an index in flight is left alone, because killing one throws away minutes of analysis. A client launches serve itself, so restarting the server means restarting that client, or reloading its MCP connection.
clean never removes registered repositories, only published generations, so rebuilding what is registered is kivgraph index --full and nothing has to be registered again. Without --yes it lists and changes nothing; with --keep-active it keeps exactly the generation currently published, and then rollback has nothing to restore.
Rust is not indexed
Section titled “Rust is not indexed”Rust is the one language Kivgraph does not analyse itself. rust-analyzer scip is the authority, invoked as an external process once per Cargo workspace, and it is a prerequisite like the Node runtime of the TypeScript worker.
The analyzer is not there, or is the wrong one
Section titled “The analyzer is not there, or is the wrong one”Symptom
toolchain.rust: FAIL (command "rust-analyzer" is unavailable)Or the binary resolves and fails to run:
toolchain.rust: FAIL (<path to rust-analyzer> --version failed: <error>)Run by hand, that binary prints:
Unknown binary 'rust-analyzer' in official toolchainCause
Being on the PATH is not being installed. rustup leaves a proxy named rust-analyzer for every toolchain whether or not the component exists, and the proxy fails with the line above.
Resolution is fixed. A rust.analyzer_command that contains a path separator is honoured exactly as written. A bare command name resolves first to the binary sitting beside the Kivgraph executable, and only then through the PATH: an installation that ships its own engine must use it, or two machines with the same bundle would index the same repository with different analyzers.
Fix
rustup component add rust-analyzerkivgraph doctor reports which binary answered and its source — bundled, explicit or path — and kivgraph version --json publishes its release.
cargo is missing
Section titled “cargo is missing”Symptom
toolchain.cargo: FAIL (cargo is unavailable, so no workspace can be loaded)Cause
The bundle carries bin/rust-analyzer but no Rust toolchain. Without cargo the analyzer cannot load a Cargo workspace, so the analyzer travelling inside the installation is not enough. doctor checks it separately and fails naming it.
Fix
Install a Rust toolchain on the machine that indexes.
derive macros, operators and ? produce no edges
Section titled “derive macros, operators and ? produce no edges”Symptom
find_references on a trait reached through #[derive(...)], an overloaded operator or the ? operator returns nothing, and calls into the standard library are absent from the graph.
Cause
core, std and alloc enter the graph with rust.index_sysroot, which is off by default. Four silences follow, all measured: #[derive(...)] produces no relation, operator overloading does not reach its trait, ? does not reach Try::branch, and every call into the standard library disappears.
Fix
Turn the standard library on:
rust: index_sysroot: trueWith it, all four become exact edges. It costs an order of magnitude in graph size — one toolchain is around 350.000 monikers and half a minute of indexing — and one cold pass per toolchain, since the fact-cache fingerprint includes rustc --version and a toolchain change invalidates every fact taken from it.
This is a declared limitation, never a failure: a machine with no toolchain, or without rust-src, indexes its own repositories and says why it did not index the library. rust.sysroot is discover, none, or a path; it says where the standard library is, not whether it enters the graph.
A Go package or module is missing
Section titled “A Go package or module is missing”The package is not buildable
Section titled “The package is not buildable”Symptom
The package contributes nothing, graph_status counts it under unresolved_by_reason, and a walk that would have crossed it downgrades its verdict. From a real get_blast_radius response, with the absolute path abridged:
{"completeness":{"verdict":"LOWER_BOUND","invisible_scopes":[{"reason":"PACKAGE_NOT_BUILDABLE","repository":"kivgraph","requested_package":"github.com/Luqueee/kivgraph/benchmarks/ladybug-recovery","detail":"LIST: build constraints exclude all Go files in /path/to/benchmarks/ladybug-recovery"}]}}Cause
Go is loaded with the tags in go.build_tags. A directory whose files those tags exclude is not an index failure: it is declared UNRESOLVED with reason PACKAGE_NOT_BUILDABLE and the pass continues. Every other loader diagnostic still aborts it.
Fix
Add the tag the package needs to go.build_tags and rebuild. Indexing the Kivgraph repository itself requires the ladybug tag.
The other reasons seen alongside it in a real graph are DECLARATION_NOT_RESOLVED, MODULE_PROVIDER_NOT_FOUND and PACKAGE_PROVIDER_NOT_FOUND. A package name nobody provides today is a dependency with an absent fingerprint, not the absence of a dependency, so it becomes the edge it should be once a provider appears.
The module was not loaded
Section titled “The module was not loaded”Symptom
The module’s symbols are absent and its references are declared MODULE_NOT_LOADED, with the diagnostics observed.
Cause
A module the loader cannot read publishes no facts, because they would not be reliable, and it does not bring down the pass. A repository whose dependencies nobody downloaded does not decide whether the others have a graph.
Fix
Download the module’s dependencies in its own checkout, then rebuild. Indexing is hermetic by default: a module the local cache does not hold is reported, not fetched. go.allow_network is the one declared way out, and a multi-repository workspace resolves one shared build list, so its selection can need a version no member downloaded on its own.
The module is above the language-version ceiling
Section titled “The module is above the language-version ceiling”Symptom
toolchain.typecheck: FAIL (registered Go module requires a newer Go language version than this build supports (this build type-checks with go <ceiling>): repository "<name>" module "<path>" requires go <version>; rebuild Kivgraph with that toolchain or drop "go" from the languages of that repository)A passing run reports both numbers, the ceiling and the highest registered module:
toolchain.typecheck: PASS (go <ceiling> (highest registered module: go <version>))Cause
go/types travels linked inside the binary, so Kivgraph type checks only up to the language version of the toolchain that compiled it. The go on your PATH is a different number and is not the one that decides whether a repository can be indexed. A module above the ceiling is rejected by name — repository, module and version — rather than being allowed to escalate the synthetic go.work and break the load of every other repository. The ceiling is major.minor.
Fix
The message names both ways out: rebuild Kivgraph with that toolchain, or drop go from the languages of that repository. kivgraph doctor is where the ceiling is stated.
A provider is ambiguous or absent
Section titled “A provider is ambiguous or absent”These three codes say something about the registry, not about your code. A name has to have exactly one owner for a stable key to mean one thing.
| Code | What it means |
|---|---|
AMBIGUOUS_PACKAGE_PROVIDER | A TypeScript package name is declared by several manifests. No manifest provides it, and both leave the registry. A Go module with several providers gets the same treatment. |
AMBIGUOUS_CRATE_PROVIDER | A crate name is declared by several registered repositories, or a registered repository declares a crate of the standard library, or two toolchains are present at once. None of them provides it. |
CRATE_PROVIDER_NOT_FOUND | Code the analyzer indexed that no manifest of the repository declares — a crate vendored through [patch.crates-io], for example. Its uses are declared as such rather than published against declarations that were discarded. |
Fix
Make the ownership unambiguous: remove the duplicate declaration, or register only one of the repositories that claims the name. A repository name is compared exactly and is an identifier, never a path component, so two names differing only in case are two repositories.
A reference that names core with a release is CRATE_VERSION_MISMATCH, and a version the analyzer does not know (.) identifies no code and never resolves.
The viewer will not start
Section titled “The viewer will not start”Symptom
ui: this binary carries no web bundle; build one with scripts/build-bundle.sh (without --mcp-only), or run the viewer from a source checkout with the webassets build tagkivgraph --help marks the command in the same build:
ui [--addr HOST:PORT] Serve the read-only graph viewer, every interface by default (unavailable: this build carries no web bundle)Cause
kivgraph ui refuses to start when the binary lacks the webassets build tag. Without it, every route would serve the page that says the bundle is missing, so the refusal costs one line instead of a browser tab.
Fix
The published release carries the viewer: the release workflow builds without --mcp-only and verifies both halves, that web/index.html is in the payload and that the help does not mark ui unavailable. If your binary refuses, it was built with --mcp-only or from a source checkout without the tag. Install the published release, or rebuild the bundle without --mcp-only.
The viewer is opt-in and serves only the published HotSnapshot over read-only HTTP. Its default bind is 0.0.0.0:7777, and it logs the address it bound, including the one a port 0 resolves to.
Where to look next
Section titled “Where to look next”kivgraph doctor answers the machine questions in one pass, one line per check: config, the state.* directories, repositories, the toolchain.* checks, graph.store, snapshot.digest, snapshot and unresolved, then doctor: PASS or doctor: FAIL. It is where the Go type-checking ceiling is stated and where you find out which rust-analyzer answered.
graph_status, through the client, answers the graph questions: status, which is ready or empty; snapshot_id, snapshot_built_at and snapshot_age_ms; the counts; edges_by_kind and unresolved_by_reason; and repository_freshness with repositories_moved. It reports nothing this process did not use or measure: serve answers from the published HotSnapshot, so it never opens the database and never runs the TypeScript worker, and declares them not_applicable saying why. Metrics nobody observed are omitted rather than reported as zero.
The logs. A one-shot command reports plain text when its stderr is a terminal and a JSON log record when it is not, so a pipeline gets records and a person gets sentences. serve and ui always log JSON, because a client reads their stderr. In both, progress is INFO and only a failure is ERROR, and the text of the line is the record’s msg, never a field buried inside a fixed message — a record you cannot filter by level or find by text reports nothing. On serve, stdout carries protocol framing and nothing else.