This guide covers everything you need to contribute to ContextBuilder — building from source, running tests, understanding the codebase, and adding new features.
- Getting Started
- Build System
- Running Tests
- Code Organization
- Rust Conventions
- TypeScript Conventions
- Adding a Platform Adapter
- Adding an Artifact Type
- Adding an MCP Tool
- Cross-Language Schemas
- CI/CD Pipeline
- Contributing Workflow
| Tool | Version | Purpose |
|---|---|---|
| Rust | 1.85+ | Core pipeline, CLI, TUI |
| Bun | 1.3+ | MCP server, LLM bridge, schemas |
| make | Any | Build orchestration |
# Clone
git clone https://github.com/PerkyZZ999/ContextBuilder.git
cd ContextBuilder
# Install TS dependencies
bun install
# Build everything
make build
# Run all tests
make test
# Run linters
make lintAll orchestration is via the root Makefile:
| Target | What It Does |
|---|---|
make build |
cargo build --workspace + bun run build (MCP server) |
make test |
cargo test --workspace + bun test |
make lint |
cargo clippy -D warnings + bunx biome check . |
make fmt |
cargo fmt --all + bunx biome format --write . |
make check |
make lint + make test |
make clean |
Remove target/ and build artifacts |
make release |
Release build with optimizations |
The Rust workspace is defined in the root Cargo.toml:
[workspace]
resolver = "3"
members = [
"apps/cli",
"apps/tui",
"apps/llm-enrich",
"packages/rust/core",
"packages/rust/shared",
"packages/rust/discovery",
"packages/rust/crawler",
"packages/rust/markdown",
"packages/rust/artifacts",
"packages/rust/storage",
]TypeScript packages use Bun workspaces defined in root package.json:
{
"workspaces": [
"apps/mcp-server",
"packages/ts/*",
"packages/schemas/*"
]
}make testcargo test --workspacebun test# Specific Rust crate
cargo test -p contextbuilder-crawler
cargo test -p contextbuilder-storage
# Specific TS package
bun test packages/ts/kb-reader/
bun test apps/mcp-server/| Suite | Location | Tests | Framework |
|---|---|---|---|
| Rust unit + integration | packages/rust/*/src/ |
146 | cargo test |
| KB Reader | packages/ts/kb-reader/ |
19 | bun test |
| MCP Server | apps/mcp-server/ |
15 | bun test |
| E2E Pipeline | apps/mcp-server/tests/ |
29 | bun test |
| OpenRouter Provider | packages/ts/openrouter-provider/ |
18 | bun test |
| Shared Schemas | packages/ts/shared/ |
24 | bun test |
| Total | 251 |
Test fixtures live in fixtures/:
| Path | Purpose |
|---|---|
fixtures/html/ |
HTML pages for platform adapter testing |
fixtures/markdown/ |
Expected Markdown output (golden files) |
fixtures/llms/ |
Sample llms.txt files for discovery testing |
Golden file tests: Markdown conversion tests compare output against checked-in golden files. To update golden files after intentional changes:
# Run tests with UPDATE_GOLDEN=1 to regenerate
UPDATE_GOLDEN=1 cargo test -p contextbuilder-markdownThe Rust code follows a layered architecture:
apps/cli/ → Clap CLI, calls into core
apps/tui/ → Ratatui TUI, calls into core + storage
apps/llm-enrich/ → LLM enrichment orchestration
packages/rust/
shared/ → Types, config, errors (used by all crates)
discovery/ → llms.txt detection and parsing
crawler/ → HTTP crawling with concurrency control
markdown/ → HTML → Markdown conversion + platform adapters
artifacts/ → Artifact generation (6 types)
storage/ → SQLite/libSQL database layer
core/ → Pipeline orchestration (ties everything together)
Dependency flow: apps → core → {discovery, crawler, markdown, artifacts, storage} → shared
apps/mcp-server/ → MCP server (stdio + HTTP)
packages/ts/
shared/ → Constants, types, zod schemas
kb-reader/ → Read-only KB access (files + SQLite)
openrouter-provider/ → LLM bridge subprocess (OpenRouter via Vercel AI SDK)
packages/schemas/
manifest/ → manifest.json schema
toc/ → toc.json schema
artifacts/ → Artifact schemas
mcp/ → MCP message schemas
- Rust Edition 2024 — All crates use
edition = "2024" unsafeis explicit —std::env::remove_varis unsafe in Edition 2024; use with justification
| Context | Library | Pattern |
|---|---|---|
| Library crates | thiserror |
Define typed errors with #[derive(Error)] |
| App crates (CLI/TUI) | color-eyre |
Use eyre::Result and .wrap_err() |
// In library crate (e.g., packages/rust/crawler/)
#[derive(Debug, thiserror::Error)]
pub enum CrawlError {
#[error("HTTP request failed: {0}")]
HttpError(#[from] reqwest::Error),
#[error("Max page limit reached: {limit}")]
PageLimitExceeded { limit: usize },
}
// In app crate (e.g., apps/cli/)
fn main() -> eyre::Result<()> {
color_eyre::install()?;
// ...
}Use tracing with structured spans:
use tracing::{info, instrument, warn};
#[instrument(skip(content), fields(url = %url))]
pub async fn convert_page(url: &Url, content: &str) -> Result<Page> {
info!("Converting page");
// ...
}- Default to
pub(crate), notpub - Re-export public API via
pub useinlib.rs - Only make items
pubwhen they're part of the crate's external API
- Runtime:
tokio(multi-threaded) - Use
tokio::Semaphorefor concurrency caps - All I/O-bound operations are async
- Use UUID v7 (time-sortable) for KBs, pages, and crawl jobs
- Content hashing uses SHA-256 (
sha2crate)
- Runtime: Bun (never Node.js)
- Linting/formatting: BiomeJS (never ESLint/Prettier)
- Config:
biome.json— 2-space indent, 100-char width, double quotes, semicolons, trailing commas ES5
Use evlog for structured logging:
import { logger } from "@contextbuilder/shared";
logger.info("Loading knowledge base", { kbId, pageCount });Never use console.log in production code.
Use zod for all runtime schema validation:
import { z } from "zod";
const KbManifestSchema = z.object({
schema_version: z.literal(1),
id: z.string().uuid(),
name: z.string(),
source_url: z.string().url(),
// ...
});- No
any— Useunknown+ type narrowing - Suppress with
// biome-ignore lint/suspicious/noExplicitAny: <reason>only when justified - All function parameters and return types must be explicitly typed
The MCP server uses @modelcontextprotocol/sdk (protocol revision 2025-11-25):
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";Platform adapters teach ContextBuilder how to extract content from specific documentation frameworks.
In packages/rust/markdown/src/adapters/:
use crate::adapter::PlatformAdapter;
use scraper::Html;
use url::Url;
pub struct MyPlatformAdapter;
impl PlatformAdapter for MyPlatformAdapter {
fn detect(doc: &Html, url: &Url) -> Option<Self>
where
Self: Sized,
{
// Return Some(Self) if this doc matches your platform
// Check for unique meta tags, class names, or URL patterns
if doc.select(&selector("meta[name='generator'][content*='MyPlatform']")).next().is_some() {
Some(Self)
} else {
None
}
}
fn extract_toc(&self, doc: &Html) -> Vec<TocEntry> {
// Extract table of contents / navigation
todo!()
}
fn extract_content(&self, doc: &Html) -> String {
// Extract the main documentation content as HTML
// (This HTML will then be converted to Markdown)
todo!()
}
fn extract_metadata(&self, doc: &Html) -> PageMeta {
// Extract page title, description, etc.
todo!()
}
fn name(&self) -> &str {
"MyPlatform"
}
}Add it to the adapter registry in packages/rust/markdown/src/adapters/mod.rs with a priority. The registry tries adapters in order; GenericAdapter is always last.
Add representative HTML pages to fixtures/html/myplatform/ and expected Markdown output to fixtures/markdown/myplatform/.
#[test]
fn test_myplatform_detection() {
let html = include_str!("../../../../fixtures/html/myplatform/basic.html");
let doc = Html::parse_document(html);
let url = Url::parse("https://myplatform.example.com/docs").unwrap();
assert!(MyPlatformAdapter::detect(&doc, &url).is_some());
}Add the schema to packages/schemas/artifacts/.
In packages/rust/artifacts/src/generators/:
pub struct MyArtifactGenerator;
impl ArtifactGenerator for MyArtifactGenerator {
fn artifact_name(&self) -> &str {
"my_artifact.md"
}
fn generate(&self, pages: &[Page], enrichments: &[Enrichment]) -> Result<String> {
// Combine page content and LLM enrichments into the artifact
todo!()
}
}If your artifact needs a new type of LLM enrichment, add it to the bridge protocol in packages/ts/openrouter-provider/src/tasks/.
Register the generator and add tests with expected output.
In apps/mcp-server/src/tools/:
import { z } from "zod";
export const myToolSchema = z.object({
kb_id: z.string().describe("Knowledge base ID"),
// ... parameters
});
export async function handleMyTool(
params: z.infer<typeof myToolSchema>,
reader: KbReader
) {
// Implementation
}Add it to the MCP server's tool registry in apps/mcp-server/src/index.ts:
server.tool("my_tool", myToolSchema, async (params) => {
return handleMyTool(params, reader);
});Add integration tests in apps/mcp-server/tests/.
Schemas in packages/schemas/ define the contract between Rust and TypeScript:
| Schema | Purpose | Files |
|---|---|---|
manifest/ |
KB manifest format | JSON Schema + zod |
toc/ |
Table of contents | JSON Schema + zod |
artifacts/ |
Artifact metadata | JSON Schema + zod |
mcp/ |
MCP message formats | JSON Schema + zod |
Rule: If you change a schema, update both the JSON Schema and the zod definition. Run make test to verify cross-language compatibility.
The CI pipeline runs on every push and PR via GitHub Actions:
| Job | What It Runs |
|---|---|
rust |
cargo clippy -D warnings, cargo test --workspace, cargo fmt --check |
typescript |
bunx biome check ., bun test |
graph LR
A[Push / PR] --> B[rust job]
A --> C[typescript job]
B --> D[clippy]
B --> E[cargo test]
B --> F[cargo fmt --check]
C --> G[biome check]
C --> H[bun test]
Both jobs run in parallel. A PR must pass both to be mergeable.
git clone https://github.com/your-username/ContextBuilder.git
cd ContextBuilder
git checkout -b feature/my-feature- Follow the Rust and TypeScript conventions documented above
- Add tests for new functionality
- Update schemas if changing data formats
# Run everything
make check
# Or step by step:
make fmt # Auto-format
make lint # Lint check
make test # All tests- Use conventional commit messages:
feat: add VuePress platform adapterfix: handle empty TOC in crawlerdocs: update user guide with TUI sectiontest: add golden files for GitBook adapterrefactor: extract enrichment cache into module
- Describe what changed and why
- Reference any relevant issues
- Ensure CI passes
- Tests added/updated for changes
- No
unwrap()without justification comment (Rust) - No
anytypes without suppression comment (TypeScript) - Schemas updated if data formats changed
- Documentation updated if user-facing behavior changed
-
make checkpasses locally
- Architecture Guide — Deep dive into system design
- API Reference — Full API documentation
- Technical Specification — Detailed specs