cavenine

Field note ·

ctx++: Codebase Intelligence for AI Coding Agents

How ctx++ uses tree-sitter parsing, SQLite, and vector search to give AI coding agents fast, accurate access to large codebases via MCP.


If you’ve used an AI coding agent on a large codebase, you’ve probably hit the wall: the agent reads five files, loses the thread, reads five more, and still can’t piece together how a feature actually works. Or it searches for a keyword, gets back forty results, and floods your context window with noise.

We built ctx++ to fix this. It’s an MCP server that gives AI agents precise, structured access to a codebase — extracting symbols, indexing them with both keyword and vector search, and tracing call graphs so the agent can follow how code connects across files. No hand-maintained documentation. No cloud services. No timeouts.

Quick glossary:

  • MCP: Model Context Protocol — the standard for connecting tools to AI agents (Claude, Cursor, Windsurf, etc.)
  • Tree-sitter: A parsing library that understands the grammar of programming languages, letting you extract real symbols instead of doing text search
  • FTS5 / BM25: SQLite’s built-in full-text search engine with BM25 ranking — the same algorithm search engines use
  • Embeddings / vector search: Turning text into numbers that capture meaning, so “authentication flow” can match a function called HandleLogin
  • Call graph: The map of which functions call which other functions

Three Principles

ctx++ is built around three constraints we wouldn’t compromise on:

Never time out. Every query hits an indexed SQLite database. The server starts in under two seconds. No lazy initialization, no “still indexing” failures mid-session.

Return the right code, not just matching code. Full-text search finds identifiers. Vector search finds concepts. Call graph traversal finds everything involved in a feature by walking real call relationships — the same way a senior engineer reads an unfamiliar codebase.

Minimal setup. A single Go binary and Ollama for embeddings. No Docker, no cloud accounts, no configuration files beyond pointing it at your project.

How Indexing Works

When you run ctxpp index, it walks your project and builds a local SQLite database at .ctxpp/index.db. The pipeline has five stages:

  1. Walk — scan the directory tree, skip node_modules, vendor directories, and anything in .gitignore
  2. Parse — use tree-sitter to extract actual symbols (functions, types, methods, interfaces) from each file
  3. Store — write symbols, call edges, and import edges to SQLite; track file hashes so unchanged files are skipped on re-runs
  4. Embed — send each symbol’s text to an embedding model to generate a vector
  5. Write — store vectors in the database alongside the symbol metadata

The key design decision is indexing at the symbol level, not the file level or chunk level. A 2,000-line authentication file contains functions about login, token refresh, session management, and password reset. If you embed the whole file as one blob, a search for “token refresh” gets a blurry match against everything in that file. If you embed each function separately, “token refresh” matches the right function precisely.

Subsequent runs are incremental: only files whose content has changed get re-parsed and re-embedded. After the initial index, a file watcher monitors the project and automatically re-indexes changed files in the background.

How Search Works

ctx++ supports three search modes:

Keyword search uses SQLite FTS5 with BM25 ranking. Fast (sub-millisecond) and precise for exact identifier lookups — great when you know the name of what you’re looking for.

Semantic search sends your query to the same embedding model used during indexing, then finds the stored symbols whose vectors are closest in meaning. This finds conceptually related symbols even when the query uses different words than the code — “authentication flow” can match HandleLogin.

Hybrid search (the default) runs both simultaneously and merges the results using Reciprocal Rank Fusion. Instead of trying to normalize scores across two different systems, RRF works with ranks: a symbol that appears near the top of both lists scores higher than one that only appears in one. The result is that exact matches don’t get buried under semantic matches, and conceptual queries don’t miss relevant identifiers.

On top of this, results get a small boost if they’re connected to each other in the call graph. If the top results include three functions that call each other, they’re probably part of the same feature and should rank together.

The Five Tools

ctx++ exposes five tools to the AI agent:

Tool What it does
ctxpp_index Index or reindex the codebase. Run once after install; incremental updates happen automatically via file watching.
ctxpp_search Search by identifier name (keyword) or natural language (semantic). Returns symbol definitions with file paths and line numbers.
ctxpp_file_skeleton Return all symbols in a file with signatures and line ranges, without reading the full body. A cheap way to understand a file’s API surface.
ctxpp_feature_traverse Given a symbol name, walk the call graph outward — what does this function call? What do those functions call?
ctxpp_blast_radius Given a symbol, find every location in the codebase that references it. Answers “what breaks if I change this?”

feature_traverse and blast_radius are complementary. Traverse walks forward: “show me everything this function orchestrates.” Blast radius walks backward: “show me everything that depends on this function.” Together they let an agent map a feature without reading files at all.

Example prompts:

use ctxpp to show me everything involved in account authentication
use ctxpp to find where FetchAccount is defined and what calls it
use ctxpp_blast_radius to tell me what breaks if I change the Account struct

Supported Languages

Language Symbols extracted
Go Functions, methods, structs, interfaces, types, constants, variables
Java Classes, interfaces, enums, methods, constructors, fields
JavaScript / TypeScript Functions, classes, interfaces, type aliases, enums, arrow functions
Rust Functions, structs, enums, traits, impl methods, type aliases
C / C++ Functions, structs, enums, typedefs, classes, templates
SQL Tables, views, indexes, functions, procedures, triggers
Protobuf Messages, services, RPCs, enums
Shell Functions
Markdown / HTML Headings and sections
HTTP Named requests

Getting Started

1. Install

go install github.com/cavenine/ctxpp@latest

Or build from source:

git clone https://github.com/cavenine/ctxpp
cd ctxpp
make build

2. Set up Ollama

ctx++ uses Ollama for embedding generation. Install it, then pull the default model:

ollama pull bge-m3

If you don’t have a GPU, Ollama will fall back to CPU — indexing and search will work fine, just slower. If Ollama isn’t running at all, ctx++ still works but provides keyword search only, with no semantic or hybrid mode.

3. Index your project

ctxpp index --path /path/to/your/project

This creates .ctxpp/index.db in the project root. Add it to .gitignore:

.ctxpp/

4. Configure your MCP client

OpenCode (opencode.json in your project root):

{
  "mcp": {
    "ctxpp": {
      "type": "local",
      "command": ["ctxpp", "mcp"],
      "enabled": true,
      "environment": {
        "CTXPP_PROJECT": "/path/to/your/project",
        "CTXPP_OLLAMA_URL": "http://localhost:11434",
        "CTXPP_OLLAMA_MODEL": "bge-m3"
      }
    }
  }
}

Claude Code (.mcp.json):

{
  "mcpServers": {
    "ctxpp": {
      "command": "ctxpp",
      "args": ["mcp"],
      "env": {
        "CTXPP_PROJECT": "/path/to/your/project",
        "CTXPP_OLLAMA_URL": "http://localhost:11434",
        "CTXPP_OLLAMA_MODEL": "bge-m3"
      }
    }
  }
}

Cursor / Windsurf (.cursor/mcp.json or .windsurf/mcp.json):

{
  "mcpServers": {
    "ctxpp": {
      "command": "ctxpp",
      "args": ["mcp"],
      "env": {
        "CTXPP_PROJECT": "/path/to/your/project",
        "CTXPP_OLLAMA_URL": "http://localhost:11434",
        "CTXPP_OLLAMA_MODEL": "bge-m3"
      }
    }
  }
}

Embedding Backends

The default is Ollama running locally. If you’re working in a cloud environment or on a machine without a GPU, ctx++ also supports AWS Bedrock (Amazon Titan Text Embeddings V2) and Text Embeddings Inference (TEI).

Ollama (local) Bedrock (AWS)
GPU required No — falls back to CPU No
Cost Free AWS API pricing
Embed latency per query ~25ms (GPU), slower on CPU 100–460ms
Index time (Kubernetes, 318k symbols) 47m (GPU) ~7.5h
Search quality 4.8/5 4.7/5
Privacy Code stays on your machine Code sent to AWS

For interactive development with a GPU, Ollama is the better experience. Without a GPU, Ollama on CPU is still usable for moderate-sized codebases. For CI/CD pipelines or cloud-hosted agents without local GPU access, Bedrock is the practical choice.

To use Bedrock, set CTXPP_EMBED_BACKEND=bedrock and add the region, model, and concurrency settings to your MCP config.

Performance

Measured on a Ryzen 9 9950X with 128 GiB RAM and local Ollama (GPU):

Private monorepo (9k files) Kubernetes (28k files)
Index time 56 seconds 47 minutes
Symbols indexed ~52,000 ~318,000
Database size 94.5 MiB 1.9 GiB
Search latency (p50) < 1ms ~640ms

For codebases under 100k symbols — which covers most projects — search is well under 10ms. The Kubernetes numbers are an extreme case. The per-query Ollama embedding call adds ~25ms regardless of corpus size, which dominates latency for small codebases.

When to Use This (and When Not To)

ctx++ is a good fit if:

  • Your codebase is large enough that asking an AI agent to “read the relevant files” produces noisy, expensive results
  • You need the agent to understand cross-cutting features — authentication, billing, data pipelines — that span many files
  • You’re using an AI agent regularly and want it to build understanding incrementally rather than cold-reading files each session

It’s probably overkill if:

  • Your project is small enough that the agent can read all the relevant files directly without hitting context limits
  • You only occasionally ask one-off questions about the codebase

Resources:

← All field notes