Skip to content

Configuration ​

All configuration is done through environment variables. Variables are validated at startup; the server fails fast with a clear message if a required variable is missing.

Environment variables ​

VariableDefaultDescription
MCP_API_KEY(optional — no auth without it)Bearer token for REST + MCP over HTTP. Ignored in stdio mode. When unset the server starts without authentication (all requests accepted).
MCP_SESSION_TTL_SECONDS1800Idle timeout (seconds) before an orphaned MCP session is closed. HTTP mode only.
MCP_SESSION_MAX100Maximum concurrent MCP sessions (503 when exceeded). HTTP mode only.
MCP_SESSION_CREATE_RATE_PER_MINUTE30New-session creation rate per minute per IP (POST /mcp without a session id). HTTP mode only.
EMBEDDINGS_BASE_URLhttp://localhost:11434/v1OpenAI-compatible /v1/embeddings endpoint.
EMBEDDINGS_API_KEY(none)Bearer token for the embeddings API.
EMBEDDINGS_MODELbge-m3Embedding model name.
EMBEDDINGS_DIMENSION1024Fixed vector dimension for the embedding column (bge-m3 = 1024). Used by the PostgreSQL backend to size the pgvector column.
EMBEDDINGS_TIMEOUT_MS60000Abort the embeddings HTTP call after this delay (ms) instead of hanging.
CHUNK_MAX_CHARS3200Maximum characters per text chunk. Lower it for small-token embedding models. See Chunk sizing below.
SEARCH_SNIPPET_MAX_CHARS1000Maximum characters of chunk content returned per rag_search / GET /search result. Independent from CHUNK_MAX_CHARS; use rag_read for the full document content.
CONTEXTUAL_CHUNKING_ENABLEDfalseAt indexing time, generate a short context sentence per chunk (via the configured LLM) that is prepended to the chunk before embedding. See Contextual chunking below.
CONTEXTUAL_CHUNKING_BASE_URLEMBEDDINGS_BASE_URLOpenAI-compatible /v1/chat/completions endpoint used to generate chunk context. Defaults to the embeddings base URL.
CONTEXTUAL_CHUNKING_MODELphi3:minimalLightweight LLM that writes the per-chunk context sentence.
CONTEXTUAL_CHUNKING_TIMEOUT_MS60000Abort a single context-generation call after this delay (ms).
CONTEXTUAL_CHUNKING_CONCURRENCY4Max concurrent /chat/completions calls per document while enriching chunks.
KB_ROOT./kbs (stdio) / /data/kbs (http)Root directory for knowledge base folders.
SCAN_INTERVAL300Scan interval in seconds (0 = disabled). HTTP mode only.
INDEXER_CONCURRENCY4Number of index jobs processed in parallel by the async worker.
INDEXER_RETRY_MAX3Attempts before a failed job is parked (manual retry via /admin/jobs/:id/retry).
INDEXER_STALE_TIMEOUT1800Seconds before a processing job orphaned by a crash is reclaimed as pending. Keep it above INDEXER_JOB_TIMEOUT_MS/1000 so a live job is never reclaimed.
INDEXER_JOB_TIMEOUT_MS600000Hard wall-clock deadline for a single job (ms). On expiry the job is aborted (signal propagated to the embeddings / contextual-chunking calls) and failed, instead of spinning forever.
INDEXER_RETRY_BASE_MS5000Base delay of the exponential retry backoff: base * 2^(attempts-1). 0 disables backoff.
INDEXER_RETRY_MAX_MS300000Upper bound of the retry backoff delay (ms).
SEARCH_RATE_PER_MINUTE60Rate limit on GET /search per minute per IP (HTTP mode only).
REINDEX_RATE_PER_MINUTE60Rate limit on POST /admin/reindex per minute per IP (HTTP mode only).
PORT8000HTTP listen port. HTTP mode only.
DB_PATH./rag.db (stdio) / /data/index/rag.db (http)SQLite database path.
STORE_BACKENDsqlitesqlite (default, standalone) or postgres (requires a reachable Postgres).
DATABASE_URL(none)Postgres connection string. Takes precedence over the individual PG_* vars.
PG_HOSTlocalhostPostgres host.
PG_PORT5432Postgres port.
PG_DATABASEraghubPostgres database name.
PG_USER(none)Postgres user.
PG_PASSWORD(none)Postgres password.
PG_SSLfalseEnable TLS for the Postgres connection.
CORS_ORIGINS(none)Allowed CORS origins (comma-separated). Empty = disables the CORS restriction.
TEXT_EXTENSIONS(none)Additional text extensions to index (comma-separated, e.g. .kt,.java,.go). Merged with the built-in list, not replacing it. Unknown extensions are also auto-detected as text via magic bytes.
RAG_TRANSPORTstdiostdio or http. The --http flag wins.
VERSIONpackage.jsonReported version. Defaults to the version field of package.json.
LOG_LEVELinfopino log level (trace, debug, info, warn, error).
NODE_ENVproductiondevelopment enables pretty log output; otherwise JSON on stderr.
ModelNotes
bge-m3Multilingual (FR/EN), 1024d, best open-source retrieval, CPU-friendly. Recommended.
nomic-embed-textLighter, English-focused.
text-embedding-3-smallOpenAI API.

Any OpenAI-compatible /v1/embeddings endpoint works — Ollama, Bifrost, OpenAI, LM Studio…

Chunk sizing ​

CHUNK_MAX_CHARS caps the size of each chunk fed to the embedding model. If a chunk exceeds the model's token limit, the embeddings call fails. Estimate a safe value for your model with:

text
CHUNK_MAX_CHARS ≈ chars-per-token × token limit

The chars-per-token ratio depends on the script. Dense scripts tokenize heavily, so a character budget that works in English can overflow a small-token model in another language.

ScriptChars/tokenCHUNK_MAX_CHARS for a 512-token model
Latin (EN, FR…)~4~2000
Cyrillic (RU…)~2.5~1200

Lower CHUNK_MAX_CHARS when you use a small-token model (e.g. 512) or dense scripts. The 400-character overlap is preserved independently of this value.

Contextual chunking ​

When enabled, a lightweight LLM writes a short context sentence for each chunk at indexing time. That sentence is prepended to the chunk only when computing the embedding — the stored content stays unchanged. This anchors the vector to its place in the document, improving search precision at zero runtime latency (the cost is paid once, per chunk, when re-indexing).

bash
CONTEXTUAL_CHUNKING_ENABLED=true \
CONTEXTUAL_CHUNKING_BASE_URL=http://localhost:11434/v1 \
CONTEXTUAL_CHUNKING_MODEL=phi3:minimal

Opt-in and default-off. CONTEXTUAL_CHUNKING_BASE_URL defaults to EMBEDDINGS_BASE_URL; pick a light model (phi3:minimal, qwen2.5:0.5b, …) since it is called once per chunk at index time. Concurrent calls are bounded per document by CONTEXTUAL_CHUNKING_CONCURRENCY and each call is aborted after CONTEXTUAL_CHUNKING_TIMEOUT_MS. When the LLM is unreachable or too slow, the chunk is embedded raw so indexing never fails.

Note — enabling contextual chunking changes embeddings, so rebuild the index from scratch (rag_reindex { force: true } via MCP, or POST /admin/reindex?force=true via REST) for previously indexed KBs to benefit. A plain incremental scan skips unchanged files.

Storage backends ​

Two backends implement the same Store interface. SQLite is the default and runs standalone; PostgreSQL is opt-in.

SQLite (default) ​

A single file (DB_PATH) with FTS5 full-text search. No database server required — nothing else to start.

PostgreSQL (pgvector) ​

Opt-in for deployments that prefer a server-side store. Embeds the pgvector extension (vector column + ts_rank full-text search). Point the server at a reachable Postgres with the vector extension:

bash
STORE_BACKEND=postgres \
DATABASE_URL=postgres://raghub:raghub@localhost:5432/raghub \
node dist/index.js

The schema and the vector extension are created idempotently on first connect. Set EMBEDDINGS_DIMENSION to match your model before ingesting — the column is sized once at migration time.

Tests : the PostgreSQL backend is exercised without any server via PGlite, a real Postgres engine compiled to WASM with the pgvector extension bundled, instantiated in-memory for the duration of the suite (see src/test/e2e/pg-store.e2e.test.ts).

Transport modes ​

  • stdio (default): serve MCP tools on stdin/stdout for a local agent. No scan loop, no REST.
  • http (--http / RAG_TRANSPORT=http): REST API + streamable-http MCP on PORT.

In stdio mode, logs go to stderr (stdout is reserved for JSON-RPC); there is no periodic scan.

Scan behavior ​

Indexing is asynchronous. The producer (scanAll) discovers files on disk, computes their SHA-256, compares with known records, and enqueues index jobs. A background worker picks up jobs, processes them with configurable concurrency (INDEXER_CONCURRENCY), and handles retries.

  • Sync fallback for tests — e2e tests use a createSyncTestQueue helper that processes jobs immediately on enqueue, preserving the synchronous test pattern without a running worker.
  • File records are not created by the producer — they appear only once the worker has successfully indexed the chunks. Until then, the file is unknown to the index and will be re-enqueued on the next scan.
  • Skipped vs excluded — the scan tally (+added ~modified -deleted =skipped xexcluded) distinguishes files that are unchanged (skipped) from files that were scanned but not indexed because they have no extractable text (excluded, i.e. binary or empty). Run with LOG_LEVEL=debug to see the per-file reason.
  • Self-healing vectors — if the embeddings endpoint is down during a scan, chunks are stored without a vector. On the next scan any file with null-vector chunks is automatically re-indexed (content unchanged is not enough to skip it), so no manual table wipe is needed to recover. The scan log reports these as re-indexing (null embeddings).
  • Failed jobs — after INDEXER_RETRY_MAX failed attempts a job is parked with status=failed. It stays visible in GET /admin/jobs and can be retried manually via POST /admin/jobs/:id/retry. The scan never re-enqueues a parked job (no infinite retry loop).
  • Retry backoff — a failed attempt is not retried immediately: the job is parked as pending with a retry_at timestamp computed as INDEXER_RETRY_BASE_MS * 2^(attempts-1), capped at INDEXER_RETRY_MAX_MS. The worker only claims jobs whose retry_at has elapsed, so a struggling embeddings endpoint is not hammered.
  • Per-job deadline — every job runs under an INDEXER_JOB_TIMEOUT_MS wall-clock deadline. On expiry the job's AbortSignal is propagated to the embeddings and contextual-chunking HTTP calls (which are aborted) and the job is failed, rather than hanging a worker slot forever.
  • Crash recovery — the reclaimStale mechanism runs every poll cycle: any job stuck in processing for longer than INDEXER_STALE_TIMEOUT seconds is reclaimed. Reclaiming counts as an attempt, so a job that never completes eventually parks as failed instead of looping between reclaim and claim forever. Keep INDEXER_STALE_TIMEOUT above INDEXER_JOB_TIMEOUT_MS/1000 (a startup warning flags the opposite) so a live job always finishes before it is considered stale. A worker also never starts a second execution of a job whose previous attempt is still in flight, even if it is re-claimed.
  • Progress visibility — GET /admin/status / rag_status report the number of documents with unsearchable (null-embedding) chunks and list the jobs currently processing with their per-job chunk progress (done/total); GET /admin/jobs / rag_jobs additionally report each in-flight job's running time.

Released under the MIT License.