Skip to content

About

A Go-only code indexer that builds a queryable graph of packages, files, functions, types, and the call edges between them — with semantic search over doc comments and an agent-authoring layer for higher-level notes (`Learning`) and architectural concepts (`Concept`).

Resources

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

codebase-graph

A Go-only code indexer that builds a queryable graph of packages, files, functions, types, and the call edges between them — with semantic search over doc comments and an agent-authoring layer for higher-level notes (Learning) and architectural concepts (Concept).

Built on modusgraph, so the same code targets an embedded Badger directory or a Dgraph cluster without changing.

Two audiences:

  • Humans running ad-hoc CLI queries to navigate a codebase.
  • Coding agents — either Claude Code via the MCP server, or other Go programs that import the library.

Status

Phase 1. Go-only indexing (golang.org/x/tools/go/packages + go/types). Intra-module call edges resolve; cross-module CALLS are deferred. See pkg/store/types.go for the full schema.

Quick start

go build -o codebase-graph ./cmd/codebase-graph

# Index a Go module into an embedded Badger store (default).
./codebase-graph index /path/to/some-go-module

# Query.
./codebase-graph callers github.com/foo/bar.SomeFunction
./codebase-graph callees github.com/foo/bar.SomeFunction
./codebase-graph list-functions github.com/foo/bar

# Semantic search (requires an embedding provider — see below).
./codebase-graph search github.com/foo/bar "natural-language description"

Default store URI is file://$XDG_CACHE_HOME/codebase-graph. Override with --store or CBG_STORE.

CLI reference

subcommand what it does
index <path> Walk the go.mod at <path> and populate the store. Re-runs skip unchanged files via sha256 file-hash.
search <module> <query> Exact name match + similarity search over function doc comments.
callers <qn> Functions that call the given qualified name.
callees <qn> Functions called by the given qualified name.
list-files <module> All indexed files in a module.
list-functions <module> All indexed functions, paginated.
mcp Run the MCP server on stdio or Streamable HTTP.

Run ./codebase-graph with no args for the full flag listing.

MCP server (for coding agents)

Two transports:

  • stdio — default. One process per client. Wire into Claude Code via the command: form below.
  • Streamable HTTP — multi-tenant network server with per-user bearer-token auth. Deploy this when teammates should contribute to a shared graph from their own Claude Code instances.

Stdio (local / single-user)

{
  "mcpServers": {
    "codebase-graph": {
      "command": "/absolute/path/to/codebase-graph",
      "args": [
        "--store", "file:///absolute/path/to/store",
        "--embedding-url", "http://localhost:11434",
        "--embedding-model", "bge-m3:latest",
        "--embedding-dims", "1024",
        "mcp"
      ]
    }
  }
}

The store must be indexed first (codebase-graph index ...) — the MCP server doesn't re-index on demand. Index whenever the code changes.

HTTP (team deployment)

Run codebase-graph mcp --addr :8080 --tokens-file tokens.json on a host that can reach your Dgraph cluster. Each contributor gets their own bearer token; writes are auto-stamped with the token's user identity so you can attribute every Learning to the person who created it.

Tokens file (tokens.json) — JSON array of {token, user} pairs:

[
  {"token": "sk-cbg-alice-...", "user": "alice@team.com"},
  {"token": "sk-cbg-bob-...",   "user": "bob@team.com"}
]

Pick high-entropy tokens (32+ random bytes hex/base64); they're matched literally. The user field is what gets persisted on Learning.agent — use something stable and human-readable (email, github handle).

Server start:

./codebase-graph \
  --store dgraph://cluster.internal:9999 \
  mcp --addr :8080 --tokens-file /etc/codebase-graph/tokens.json

Refuses to start if the tokens file is empty (no implicit unauthenticated mode for HTTP — that's a footgun).

TLS: terminate at a reverse proxy (nginx, caddy, k8s ingress). The server speaks plain HTTP — cert lifecycle is the proxy's problem.

Client config (Claude Code). Two homes for this config, depending on whether the deployment is personal or shared via the repo:

  • ~/.claude.json (per user, all repos). Find or create the top-level mcpServers key and paste the "codebase-graph": { ... } block inside it. Best when the token is yours and you want the MCP server available in every Claude Code session.
  • .mcp.json at the repo root (committed, shared with the team). Claude Code picks this up automatically when launched inside the repo. Don't commit a literal token — use ${VAR} expansion and have each contributor export the variable in their shell rc.
{
  "mcpServers": {
    "codebase-graph": {
      "type": "http",
      "url": "https://codebase-graph.team.internal/mcp",
      "headers": {
        "Authorization": "Bearer ${CBG_TOKEN}"
      }
    }
  }
}

For a personal ~/.claude.json entry, swap ${CBG_TOKEN} for your literal token. For an open-source project served with --allow-anonymous-reads, omit the headers block entirely — visitors who clone the repo will connect read-only with no setup.

Endpoints:

  • POST /mcp — JSON-RPC requests. Requires Authorization: Bearer <token>.
  • GET /healthz — unauthenticated liveness probe for load balancers.

Identity rules:

  • add_learning ignores any client-supplied agent argument when running under HTTP — the auth-derived user always wins. (Spoofing-resistant.)
  • Under stdio (no auth), agent is still honored as before.

Rotating a token: edit tokens.json and restart the server. SIGHUP-reload isn't supported yet.

Anonymous read access (open-source deployments)

For projects that want their graph publicly browsable while keeping contributions gated, add --allow-anonymous-reads:

./codebase-graph \
  --store dgraph://cluster.internal:9999 \
  mcp --addr :8080 \
      --tokens-file /etc/codebase-graph/tokens.json \
      --allow-anonymous-reads

What changes:

  • initialize, tools/list, and every read-only tool (list_projects, search_functions, find_callers, find_callees, get_function, list_concepts / get_concept / list_concept_members / search_concepts, list_learnings_for, search_learnings, export_project_graph) are reachable with no Authorization header.
  • Write tools (add_learning, add_concept, tag_with_concept) still require a valid bearer; anonymous attempts get 401.
  • A bearer token is still validated when present — sending a bad token is 401 even on read endpoints (silently ignoring it would mask programming errors and weaken provenance).

You can also run with no tokens file at all for a fully public, read-only deployment:

./codebase-graph mcp --addr :8080 --allow-anonymous-reads

The server refuses to start with neither --tokens-file nor --allow-anonymous-reads — that combination would accept zero requests.

Using with Claude Code

The graph is a bidirectional context cache for coding agents. The win is two-sided:

  1. Read side: fewer round trips. Instead of grepping a repo, opening files, and scrolling through hundreds of functions to find the right one, Claude calls one MCP tool and gets a typed answer.
  2. Write side: durable learning. When Claude figures out something non-obvious during a task — a hidden constraint, a confusing convention, the reason a function exists — it calls add_learning and that note sticks to the function. The next agent (you, someone else, a future you) hits it on the next query.

To get Claude to actually use the graph, add a short section to your project's CLAUDE.md so it knows when to reach for which tool. Drop this in (or adapt it):

## Codebase navigation via codebase-graph

This project is indexed in the codebase-graph MCP server. **Prefer graph
queries over filesystem traversal** when navigating Go code — they're
faster, more reliable, and surface accumulated context from prior agents.

**Before grepping or opening files at random:**

- Looking for a function by name or behavior →
  `search_functions(module, query, limit?)`
- Tracing call relationships → `find_callers(qn)` / `find_callees(qn)`
- "How does this codebase handle X?" → `search_concepts(query)` /
  `search_learnings(query)`
- About to dive into a function → `list_learnings_for(target_kind, target)`
  first. Prior agents may have already noted the gotcha you're about to
  rediscover.

**After learning something non-obvious about the code, write it back.** If
you find a hidden invariant, a convention that isn't visible in the
source, a "this looks like a bug but isn't", a function that has no 
documentation and you've taken the time (spent tokens) to understand it,
 — call `add_learning` with `target_kind`, `target` (qualified name), 
`body` (state the rule, then a one-line "Why:"), and optionally 
`kind` (`gotcha` / `convention` / `note`). If the insight is architectural
 and spans many files, use `add_concept` instead and tag the relevant code 
 nodes with `tag_with_concept`.

Keep learning bodies tight — written for a future agent reading the
codebase cold, not for yourself today. Avoid restating what's obvious
from the source.

**Only write from `main` (or the repo's default branch).** Feature
branches get abandoned, force-pushed, or significantly reworked before
merging — learnings written from them often describe code that never
lands, leaving the graph full of claims about functions that no longer
exist. Before calling `add_learning` / `add_concept` /
`tag_with_concept`, run `git rev-parse --abbrev-ref HEAD` and verify
you're on the default branch. If you're on a feature branch and the
insight is about code *you're introducing in this branch*, hold the
note in conversation and write it after the PR merges. Insights about
*pre-existing* code you happened to read or review while on a branch
are fine to write from anywhere — the claim is about behavior that's
already on main.

A few notes:

  • Reads cost ~nothing. Tool calls are tens-of-milliseconds against the local cluster; far cheaper than re-tokenizing source. Encourage Claude to query liberally.
  • Writes are de-duped. add_learning derives a stable id from <agent>:<sha256(target+body)> — re-writing the same insight is a no-op upsert, not a duplicate.
  • Provenance is automatic under HTTP transport. With per-user bearer tokens, every learning auto-stamps the contributing teammate's identity. No client-side configuration needed.

Tools exposed

Code queries

  • search_functions(module, query, limit?) — exact-name + semantic.
  • find_callers(qualified_name) / find_callees(qualified_name).
  • get_function(qualified_name) — full detail.

Agent annotations — write durable context onto graph nodes.

  • add_learning(target_kind={function|type|package|file|concept}, target, agent, body, kind?, title?, learning_id?)
  • list_learnings_for(target_kind, target)
  • search_learnings(query, limit?) — semantic over Learning bodies.

Concepts — higher-level mechanisms that span code (e.g. "MVCC", "LSM").

  • add_concept(module, name, summary?, description?, related_to?)
  • tag_with_concept(concept_id, target_kind, targets[]) — attach code nodes.
  • list_concepts(module) / get_concept(concept_id) / list_concept_members(concept_id, kind?).
  • search_concepts(query, limit?) — semantic over Concept descriptions.

Visualization — bulk export for rendering.

  • export_project_graph(module, include_*: bool) — every node and edge for a module as D3-compatible JSON:

    {
      "module": "github.com/dgraph-io/badger/v4",
      "nodes": [
        {"id": "0x5", "label": "Open", "kind": "function", "qualified_name": "..."},
        ...
      ],
      "links": [
        {"source": "0x5", "target": "0x3", "kind": "in_file"},
        ...
      ]
    }

    Node id is the Dgraph uid; kind is one of project | package | file | function | type | concept | learning. Link kind is the graph predicate (calls, in_file, in_pkg, in_file_t, in_package, imports, includes_*, about_*, related_to). Use include_*: false flags to shrink the export — include_functions: false is the biggest lever for large modules (functions are usually the bulk).

    A single-page D3 visualization example app is checked into this repo at deploy/d3-viewer/index.html.

Library usage

Apps that already speak Go can skip the MCP transport and import directly:

// go.mod
require github.com/matthewmcneely/codebase-graph v0.0.0
replace github.com/matthewmcneely/codebase-graph => /path/to/codebase-graph
import (
    "context"
    "time"

    "github.com/matthewmcneely/codebase-graph/pkg/embed"
    "github.com/matthewmcneely/codebase-graph/pkg/index"
    "github.com/matthewmcneely/codebase-graph/pkg/store"
)

ctx := context.Background()

// Optional: gate startup on a remote cluster being reachable.
if err := store.WaitReady(ctx, "dgraph://localhost:9999", 60*time.Second); err != nil {
    log.Fatal(err)
}

prov, _ := embed.Build(embed.DefaultConfig())
st, err := store.Open(ctx, "dgraph://localhost:9999", store.WithEmbeddingProvider(prov))
if err != nil { log.Fatal(err) }
defer st.Close()

if _, err := index.Index(ctx, st, "/path/to/some/repo"); err != nil { log.Fatal(err) }

learnings, _ := st.LearningsForFunction(ctx, "github.com/foo/bar.SomeFunc")
similar, _  := st.SimilarFunctionsByDoc(ctx, "github.com/foo/bar", "rebalance shards", 10)

The internal/mcp package is not importable on purpose — it's a CLI-only JSON-RPC adapter, not part of the library contract.

Data model

Project ── owns ──> Package ── contains ──> File
                       │                     │
                       │                     ├── Function (with `calls` edges to other Functions)
                       │                     └── Type
                       │
                       └── imports another Package

Concept ── includes_{packages,files,functions,types} ──> code nodes
        ── related_to ──> other Concepts

Learning ── about_{package,file,function,type,concept} ──> any of the above

Reverse-edge convention: forward edges live on the "annotation/owner" side; code nodes find their annotations via ~about_* / ~includes_* DQL traversals.

Identity scheme (durable across machines and storage backends):

node identity
Project Go module path (from go.mod)
Package full import path
File <module_path>|<rel_path>
Function / Type qualified name (pkg.Name or pkg.Receiver.Method)
Learning agent-supplied learning_id (MCP auto-derives as <agent>:<sha256>)
Concept <module_path>::<slug>

Storage backends

--store accepts:

  • file:///abs/path — embedded Badger directory. Default. Fast, zero-config. Single-writer: only one process can hold the lock at a time. Use for CLI + single-agent setups.
  • dgraph://host:port — external Dgraph cluster. Multi-process safe. Use when multiple apps (e.g. oss-daily plus an MCP agent) share the graph.

A reference local Dgraph cluster is in deploy/local-cluster/docker-compose.yml. Ports are shifted out of Dgraph's defaults so this cluster coexists with others you may run for Dgraph core development:

docker compose -f deploy/local-cluster/docker-compose.yml up -d
./codebase-graph --store dgraph://localhost:9999 index /path/to/module
service port purpose
alpha gRPC 9999 client URI
alpha HTTP 9998 /health, admin, Ratel
zero gRPC 5999 coordination
zero HTTP 6999 zero admin

Embeddings

The store uses an OpenAI-compatible embedding provider. Defaults to Ollama:

./codebase-graph \
  --embedding-url http://localhost:11434 \
  --embedding-model bge-m3:latest \
  --embedding-dims 1024 \
  index /path/to/module

Also tested with LMStudio (text-embedding-embeddinggemma-300m-qat at 768 dim) and any OpenAI-compatible endpoint. Override via flags or env (CBG_EMBEDDING_URL, CBG_EMBEDDING_MODEL, CBG_EMBEDDING_DIMS, CBG_EMBEDDING_API_KEY).

--no-embeddings skips wiring entirely — faster indexing, no semantic search.

Note: embedding dims are baked into the Dgraph schema on first Open(). Switching models with a different dim count requires a fresh store (rm -rf the file:// dir or down -v the cluster).

Project layout

cmd/codebase-graph     CLI + MCP server entry point
pkg/store              public: typed wrapper over modusgraph Client
pkg/index              public: two-pass Go-module walker
pkg/harvest            public: mine ~/.claude/projects/ sessions into Learnings/Concepts
pkg/embed              public: embedding-provider config
internal/mcp           private: stdio + HTTP JSON-RPC adapter (CLI-only)
deploy/local-cluster   Dgraph cluster compose recipe
testdata/              fixtures used by tests

Testing

go test ./...

Tests use file:// stores under t.TempDir() — no external services required (Ollama / Dgraph cluster are not needed for the test suite).

Future

Tracked items that didn't make v1 but are worth picking up:

MCP HTTP transport.

  • SIGHUP-reload of tokens.json. Rotating a token currently requires a server restart. Trivial to add — watch for SIGHUP and re-read the file into the registry atomically.
  • Identity-stamping on add_concept / tag_with_concept. Today only add_learning honors the auth-derived user. Concepts and tags don't carry per-write attribution, so you can't tell who proposed a concept or who tagged what. Same plumbing pattern as add_learning.
  • In-process TLS. Intentionally out of scope — reverse proxies handle it better. Revisit if there's a deployment context where adding a proxy is friction (e.g. small teams running the binary directly on a laptop).
  • Per-token rate limits. Not needed for trusted-team deployments; worth doing if the server ever sits on a less-trusted network.
  • Read-only token role. Currently every token has full read+write access. A role flag on the token entry ("role": "read") would let teams give broad query access without write privileges.

Harvester.

  • Top-level const/var indexing. The Go indexer captures Functions and Types only. Behavior encoded in package-level constants (e.g. GraphQL SDL strings, regex tables, default-options structs) can't be a Learning target at function granularity — the harvester correctly falls back to file-level attachment, but a structural fix is to index those nodes too.
  • Non-Go indexers. The Ratel session in the v1 harvest hit 0/6 admit because Ratel is largely TypeScript/React, which the Go indexer doesn't see. A TS/JS indexer (using tsserver or tree-sitter-typescript) would unlock JS-heavy repos.
  • Function.Signature-aware source verification. The apply-stage source verifier reads the function's source from disk and checks for symbol mentions. It could also pull the indexed Signature field and do a tighter parameter-count / type-shape check against the body's claims. Catches more "describes an in-progress edit" failures without needing the repo checked out.
  • Provenance fields on Learning. Today agent is the only origin signal. Adding source_session_id / source_chunk_index / evidence_quote as typed predicates (not body suffix) would let reviewers query "show me everything from session X" or "amend this learning, here's the new evidence."
  • Semantic dedup at apply time. The pipeline currently dedups by <agent>:<sha256(target+body)> — only catches identical bodies. Wiring SearchLearnings(body) filtered to the same target with a cosine threshold would amend near-duplicates instead of inserting siblings.

Performance.

  • Tight projection on FindFunctionByQN. modusgraph's default Query expansion pulls the full Function struct including the Calls predicate. On hot functions with thousands of callers this generates very large responses (we already hit the 4 MB gRPC default once; bumped to 32 MB). A projected query that only fetches uid + line range would be more bandwidth-efficient.

About

A Go-only code indexer that builds a queryable graph of packages, files, functions, types, and the call edges between them — with semantic search over doc comments and an agent-authoring layer for higher-level notes (`Learning`) and architectural concepts (`Concept`).

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages