Skip to main content
Glama
RaviIITk
by RaviIITk
README.md
# code-index

Structural, queryable understanding of a Python codebase for a coding agent —
built as an AST-derived RDF graph, stored in an embedded triple store, and
exposed as MCP tools.

Instead of an agent re-discovering "who calls this function" or "what does
this class inherit from" via repeated grep/read cycles, `code-index` parses
the codebase once into a graph of facts and answers those questions as
direct lookups, in milliseconds.

## Why

Grep and file reads are the default way an LLM coding agent explores a
repository, but they're the wrong tool for structural questions:

- "What calls this function, anywhere in the repo, directly or
  transitively?" — grep finds text matches, not resolved call edges.
- "What does this class inherit from, or what inherits from it?" — requires
  actually resolving imports across files.
- "Give me a cheap overview of this folder before I decide what to read in
  full." — grep/read either gives you everything or nothing.

`code-index` answers these by parsing structure once and storing it as
facts, so the agent can query instead of re-deriving.

## Concept

1. **Parse** — Python's stdlib `ast` walks each file and extracts structural
   facts: modules, classes, functions, signatures, line ranges, docstrings,
   decorators, variable reads/writes, and *unresolved* call/import/inherit
   targets (by name only, not yet a link to the actual defining symbol).
2. **Resolve** — a real [Pyright](https://github.com/microsoft/pyright)
   language server runs as a managed subprocess for the duration of a build.
   It supplies return-type inference (`hover`) and cross-file symbol
   resolution (`definition`), which get attached onto the AST-built nodes —
   `ast` owns identity and structure, Pyright only annotates it.
3. **Store** — every fact becomes an RDF triple (subject-predicate-object) in
   an embedded [pyoxigraph](https://github.com/oxigraph/oxigraph) store, one
   named graph per source file. This makes incremental re-indexing a
   drop-and-replace of a single file's graph, and gives the whole thing a
   real SPARQL 1.1 query engine for free.
4. **Materialize closures** — `calls`, `imports`, and `inherits` are
   transitive relationships. Rather than walking property paths at query
   time, the full transitive closure of each is precomputed at index time
   and written into a dedicated `urn:code:graph:inferred` graph. "Every
   caller of X, however deep" becomes a direct lookup, not a graph traversal.
5. **Serve** — an [MCP](https://modelcontextprotocol.io/) server exposes six
   tools over the store: a token-cheap structural browser
   (`get_context`), a full-detail single-node lookup (`get_details`), three
   curated graph queries (`get_callers`, `get_dependencies`,
   `get_class_hierarchy`), and a raw SPARQL escape hatch (`query_sparql`)
   for anything the curated tools don't cover.

Everything downstream of parsing is just facts and queries over those facts
— no LLM involved in indexing itself, so results are exact, not guessed.

### Example: what actually gets stored

For a class like this:

```python
class FeatureScaler:
    """Scales numeric features into [0, 1]."""

    def transform(self, values: list[float]) -> list[float]:
        """Scale values into [0, 1]."""
        return [self._normalize(v) for v in values]
```

parsing produces triples roughly equivalent to:

```
urn:code:src/scaler.py::FeatureScaler            a               code:Class
urn:code:src/scaler.py::FeatureScaler            code:defines    urn:code:src/scaler.py::FeatureScaler.transform
urn:code:src/scaler.py::FeatureScaler.transform  a               code:Function
urn:code:src/scaler.py::FeatureScaler.transform  code:signature  "transform(self, values: list[float]) -> list[float]"
urn:code:src/scaler.py::FeatureScaler.transform  code:description "Scale values into [0, 1]."
urn:code:src/scaler.py::FeatureScaler.transform  code:calls      urn:code:src/scaler.py::FeatureScaler._normalize
urn:code:src/scaler.py::FeatureScaler.transform  code:startLine  "5"
```

`get_context("src/scaler.py")` returns the cheap structural summary (names,
signatures, line ranges — no docstrings). `get_details(node_id)` returns
everything for one specific node, including its docstring. `get_callers`
and `get_class_hierarchy` are direct lookups against the precomputed
transitive closure, not live traversals.

## Install

Requires Python 3.13+, [uv](https://docs.astral.sh/uv/), and Node.js (used
only to vendor `pyright-langserver` as an internal subprocess — never a
user-facing server).

```bash
git clone https://github.com/RaviIITk/code-index.git
cd code-index
uv sync
npm install       # vendors pyright-langserver into node_modules/
```

## Usage

### CLI

```bash
# Build (or incrementally rebuild) the index for a repo
uv run code-index build /path/to/repo

# Check what's currently indexed
uv run code-index status /path/to/repo

# Run a raw SPARQL query against the index
uv run code-index query "SELECT ?fn WHERE { ?fn a <http://example.org/code-ontology#Function> }" /path/to/repo
```

The index itself is stored outside the repo, in
`~/.cache/code-index/<repo-slug>-<hash>/`, keyed by the repo's canonical
path — nothing is written into the working tree.

### As a Python library

Everything the CLI and MCP server do is just calls into the library —
useful for scripting or embedding in something else:

```python
from pathlib import Path

from code_index.cache.location import store_path
from code_index.incremental.build import run_incremental_build
from code_index.mcp_server import tools
from code_index.store.triple_store import TripleStore

repo = Path("/path/to/repo")
store = TripleStore(store_path(repo))

# Build (or incrementally refresh) the index
report = run_incremental_build(store, repo, pyright_bin="pyright-langserver")
print(report.added, report.changed, report.deleted, report.parse_failures)

# Query it with the same functions the MCP tools delegate to
print(tools.get_context(store, "src/scaler.py", depth=1))

fn_id = "urn:code:src/scaler.py::FeatureScaler.transform"
print(tools.get_details(store, fn_id))
print(tools.get_callers(store, fn_id))
print(tools.get_dependencies(store, "src/scaler.py"))
print(tools.get_class_hierarchy(store, "urn:code:src/scaler.py::FeatureScaler"))

# Or drop to raw SPARQL 1.1
print(tools.query_sparql(store, "SELECT ?fn WHERE { ?fn a <http://example.org/code-ontology#Function> }"))
```

`node` ids are the same `urn:code:<file_path>::<qualified.name>` IRI strings
returned in every `get_context`/`get_details` result's `"id"` field, so you
can chain a broad query into a specific one without hand-building IRIs.

### As an MCP server

Register it with an MCP-speaking agent host by adding to that host's MCP
config (e.g. `.mcp.json`):

```json
{
  "mcpServers": {
    "code-index": {
      "command": "uv",
      "args": ["run", "--directory", "/path/to/code-index", "code-index-mcp", "/path/to/repo"]
    }
  }
}
```

This starts a resident server (one per repo, guarded by an exclusive lock)
exposing six tools:

| Tool | Signature | Purpose |
|---|---|---|
| `get_context` | `(path: str, depth: int = 1) -> dict` | Token-cheap structural summary rooted at a file or folder; increase `depth` to drill from folder → file → function/class. |
| `get_details` | `(node: str) -> dict` | Full structural facts for one node, plus its docstring if the source has one. |
| `get_callers` | `(fn: str) -> list[str]` | Every function that calls `fn`, directly or transitively. |
| `get_dependencies` | `(file: str) -> list[str]` | Every file that `file` transitively imports. |
| `get_class_hierarchy` | `(cls: str) -> dict` | Transitive ancestors and descendants of a class. |
| `query_sparql` | `(query: str) -> list[dict]` | Raw SPARQL 1.1 escape hatch; the tool description embeds an ontology cheat-sheet so the agent has schema context without an extra call. |

A typical agent workflow: call `get_context` on the repo root to see the
folder tree, drill into a file of interest, grab a function/class `id` from
that summary, then call `get_details` or `get_callers` with that id for the
full picture.

Note: the MCP server only ever serves whatever is already in the store when
it starts — run `code-index build` (or wire a session-start hook to do so)
before pointing an agent at a repo whose index may be stale.

## Development

```bash
uv run pytest    # 44 tests
uv run ruff check .
uv run ruff format .
```

## Scope

v1 targets small-to-medium Python repos (hundreds of files). Deliberately
out of scope for now: other languages, LLM-generated summaries/purpose
fields, graph-centrality ranking, and transparent multi-session server
sharing (each repo currently gets one resident server; a second concurrent
session refuses to start rather than silently sharing state).

## License

Dual-licensed under either of

- [MIT license](LICENSE-MIT)
- [Apache License, Version 2.0](LICENSE-APACHE)

at your option.

TDQS

A4/5.0

Scored across 6 tools

Disambiguation5/5

Each tool has a distinct purpose: callers, class hierarchy, structural context, dependencies, detailed node info, and SPARQL queries. There is no overlap or ambiguity between them.

Naming Consistency4/5

Most tools follow the 'get_' prefix pattern (get_callers, get_class_hierarchy, etc.), but query_sparql deviates slightly by using 'query' instead. The naming is still clear and predictable overall.

Tool Count5/5

With 6 tools, the set is well-scoped for a code indexing server. Each tool provides essential functionality without redundancy or excess.

Completeness4/5

The tools cover structural analysis (context, details) and relational queries (callers, hierarchy, dependencies). The SPARQL endpoint allows arbitrary queries, filling most gaps. Minor missing direct tool for listing all modules, but query_sparql can handle that.

Maintenance

ActivitySlowing
ResponsivenessNo issues