Skip to main content

Citewell

Citation-grounded document memory for AI assistants.
Offline · MCP-native · Turkish-ready

CI CodeQL codecov Python License: MIT Docs

Türkçe README · Documentation · Architecture decisions


Citewell is a headless RAG server that gives Claude Desktop, Claude Code, Cursor and any other MCP client access to your documents — and every passage it returns comes with file, page, chunk id and a verbatim quote, so answers can be checked.

It has no LLM of its own and needs no API key. It retrieves; your assistant writes the answer.

You ▸ Ankara'da otelde bir gece için en fazla ne kadar harcayabilirim?

Claude ▸ (calls citewell.search_documents)
         Ankara, İstanbul ve İzmir'de gecelik konaklama üst sınırı 3.500 TL'dir [1].

         Kaynaklar
         [1] calisan-el-kitabi.pdf, s. 4 — "İş seyahatlerinde konaklama için gecelik üst sınır
             İstanbul, Ankara ve İzmir'de 3.500 TL, diğer şehirlerde 2.500 TL'dir."

Why Citewell?

  • Cited, verifiable passages — page-aware chunking that never crosses page boundaries, verbatim quotes, stable chunk ids (ADR-0006), and a verify_quote tool so assistants can check a quotation against the source before presenting it. Printed page numbers from the PDF are shown when they differ from the physical page (s. 12 (PDF s. 17)).
  • MCP-native — stdio for desktop clients, Streamable HTTP at /mcp, structured tool output, resources and a citation prompt. Read-only by design (ADR-0007).
  • Hybrid search — pgvector (HNSW) + PostgreSQL full-text search fused with Reciprocal Rank Fusion, optional bge-reranker-v2-m3 cross-encoder (ADR-0004).
  • Turkish first — Turkish stemming (tsvector turkish config), correct I/ı – İ/i casing, search that works without Turkish characters (ogrenci izni finds öğrenci izni), cp1254 decoding, multilingual bge-m3 embeddings, Turkish eval sets.
  • Offline & private — local models; after a one-time download nothing leaves your machine.
  • Measured — built-in eval: Recall@k, MRR, hit rate, page hit rate, p50/p95 latency.
  • One command — docker compose up. PostgreSQL is the only dependency.

Quickstart (PyPI, no clone)

uv tool install "citewell[models]"      # or: pipx install "citewell[models]"

# PostgreSQL + pgvector (any PostgreSQL 15+ with pgvector works)
docker run -d --name citewell-db -p 127.0.0.1:5432:5432 \
  -e POSTGRES_USER=citewell -e POSTGRES_PASSWORD=citewell -e POSTGRES_DB=citewell \
  -v citewell-pgdata:/var/lib/postgresql/data pgvector/pgvector:pg17

citewell init
citewell models download                # once (~4–5 GB), needs internet; offline afterwards
citewell ingest ~/Documents/policies -c policies
citewell search "yıllık izin kaç gün?" -c policies

Then connect your assistant — e.g. Claude Code: claude mcp add -s user citewell -- citewell mcp

Quickstart (Docker)

git clone https://github.com/omerozkilic/citewell && cd citewell
docker compose up -d                                   # PostgreSQL + Citewell on localhost:8000

cp -r ~/Documents/handbooks/* data/                    # PDF, Markdown, TXT, HTML, DOCX
docker compose run --rm citewell models download       # once (~2–3 GB), needs internet
docker compose run --rm citewell ingest /data
docker compose run --rm citewell search "yıllık izin kaç gün?"

No documents at hand? Use the bundled sample corpus (a fictional Turkish company handbook):

docker compose run --rm -v ./examples:/examples:ro citewell ingest /examples/corpus
docker compose run --rm citewell search "Ankara'da otelde bir gece için en fazla ne kadar harcayabilirim?"

A database is tied to the embedding model it was initialised with (citewell doctor shows it). To switch models, use a fresh database volume and re-ingest.

Quickstart (local, uv)

uv sync --extra models
docker compose up -d db
uv run citewell init
uv run citewell ingest ./my-docs --collection handbook
uv run citewell search "parola en az kaç karakter olmalı?" -c handbook

Connect your assistant

Claude Code
# stdio, installed from PyPI (`uv tool install "citewell[models]"`)
claude mcp add -s user citewell -- citewell mcp

# HTTP (server running via docker compose or `citewell serve`)
claude mcp add -s user --transport http citewell http://localhost:8000/mcp

# stdio, from a local checkout
claude mcp add -s user citewell -- uv --directory /path/to/citewell run citewell mcp
Claude Desktop — claude_desktop_config.json

Installed from PyPI — Claude Desktop does not see your shell's PATH, so use the full path that which citewell prints (typically ~/.local/bin/citewell):

{
  "mcpServers": {
    "citewell": {
      "command": "/Users/you/.local/bin/citewell",
      "args": ["mcp"],
      "env": { "CITEWELL_DATABASE_URL": "postgresql://citewell:citewell@localhost:5432/citewell" }
    }
  }
}

With Docker Compose:

{
  "mcpServers": {
    "citewell": {
      "command": "docker",
      "args": ["exec", "-i", "citewell-citewell-1", "citewell", "mcp"]
    }
  }
}

Local checkout:

{
  "mcpServers": {
    "citewell": {
      "command": "uv",
      "args": ["--directory", "/path/to/citewell", "run", "citewell", "mcp"],
      "env": { "CITEWELL_DATABASE_URL": "postgresql://citewell:citewell@localhost:5432/citewell" }
    }
  }
}
Cursor — .cursor/mcp.json
{ "mcpServers": { "citewell": { "url": "http://localhost:8000/mcp" } } }

or stdio with the PyPI install: { "mcpServers": { "citewell": { "command": "citewell", "args": ["mcp"] } } }

MCP surface

Kind Name Purpose
tool search_documents(query, top_k, collection, mode, rerank, source_prefix) Cited passages (ref, citation, source, page, chunk_id, quote, text, score)
tool get_chunk(chunk_id, context) A chunk plus neighbouring chunks, to read around a quote
tool verify_quote(quote, chunk_id, source, collection) Checks that a quotation really occurs in the source; returns the exact wording and page, or the closest passage if it was paraphrased
tool list_collections() / list_documents(collection) What is indexed
resource citewell://collections, citewell://collections/{collection}/documents, citewell://chunks/{id} Same data as resources; collection names auto-complete
prompt answer_with_citations(question, collection) "Answer only from passages, cite with [n]"

CLI

citewell init                         create extension + schema (idempotent)
citewell ingest PATH... [-c COL] [-l turkish|english|simple] [--force] [--prune [-y]]
citewell search "query" [-k 5] [-m hybrid|vector|keyword] [-c COL|*] [--rerank] [--json]
citewell verify "quote" [--chunk ID] [--source FILE] [-c COL]   exit code 4 if not found
citewell serve [--init] [--host] [--port]      REST API + MCP at /mcp
citewell mcp [--transport stdio|http]          MCP server only
citewell eval DATASET.jsonl [-k 5] [-r vector,keyword,hybrid,hybrid+rerank] [-o report.json]
citewell collections list|create|delete · documents list|delete · models download · doctor

REST API

curl -s localhost:8000/v1/search -H 'content-type: application/json' \
  -d '{"query": "uzaktan çalışma kaç gün?", "top_k": 3, "mode": "hybrid"}'
curl -s -F file=@politika.pdf -F collection=handbook localhost:8000/v1/documents

GET /health · POST|GET /v1/search · POST /v1/verify · GET /v1/chunks/{id}?context=1 · GET|POST|DELETE /v1/collections · GET|POST|DELETE /v1/documents — full schema at /docs. Set CITEWELL_API_TOKEN to require a bearer token.

How it works

flowchart LR
  subgraph Ingest
    F[PDF / MD / TXT / HTML / DOCX] --> L[Loaders<br/>pages & sections]
    L --> C[Page-aware chunker<br/>verbatim slices]
    C --> E[bge-m3 embeddings<br/>+ title/section header]
  end
  E --> PG[(PostgreSQL<br/>pgvector HNSW<br/>tsvector GIN)]
  subgraph Query
    Q[query] --> V[vector top-40]
    Q --> K[keyword top-40<br/>turkish stemmer]
    V --> R[RRF k=60]
    K --> R
    R --> X[optional rerank<br/>bge-reranker-v2-m3]
    X --> O[passages + citations<br/>+ best quote]
  end
  PG --- V
  PG --- K
  O --> M[MCP · REST · CLI]

Evaluation

citewell eval examples/eval/tr-sample.jsonl -k 5 -r vector,keyword,hybrid,hybrid+rerank --markdown

Datasets are JSON Lines: {"id", "query", "relevant": [{"source", "page"?}], "collection"?}. Metrics: Recall@k, MRR@k, Hit@k, Page hit@k (exact page), p50/p95 latency. CI runs the sample set end-to-end on every push (with the lexical test backends) and fails below a recall floor; see docs/evaluation.md.

First results: Turkish Labour Law (İş Kanunu, 4857)

20 natural-language questions from examples/eval/mevzuat-questions.jsonl against the official PDF from mevzuat.gov.tr (121 articles). A hit counts only if it lands on a page of the right article. BAAI/bge-m3 + bge-reranker-v2-m3, default chunking, Apple Silicon (MPS), models already loaded:

Run Recall@5 MRR@5 Hit@5 Page hit@5 p50 ms p95 ms
vector 1.000 0.885 1.000 1.000 45 95
keyword 0.850 0.614 0.850 0.850 8 11
hybrid (default) 1.000 0.893 1.000 1.000 48 54
hybrid + rerank 1.000 0.950 1.000 1.000 3259 3771
  • Every question finds the right article and page in the top 5 with hybrid search; the right passage is usually ranked first or second (MRR 0.89).
  • Dense retrieval does the heavy lifting on paraphrased questions; keyword search alone misses 15 %.
  • The cross-encoder lifts MRR to 0.95 but costs ~3 s per query on a laptop, so it is opt-in (--rerank / rerank: true).

Small and single-domain (20 questions, one law, written by the maintainers) — a first data point, not a leaderboard. Reproduce it with docs/evaluation.md; larger independent Turkish eval sets are very welcome.

Configuration

All settings are environment variables prefixed with CITEWELL_ (or a .env file) — see .env.example and docs/configuration.md.

Variable Default
CITEWELL_DATABASE_URL postgresql://citewell:citewell@localhost:5432/citewell
CITEWELL_EMBEDDING_MODEL / _DIM BAAI/bge-m3 / 1024
CITEWELL_RERANKER_MODEL BAAI/bge-reranker-v2-m3
CITEWELL_DEFAULT_MODE / _TOP_K hybrid / 5
CITEWELL_CHUNK_SIZE / _OVERLAP 1000 / 150 characters
CITEWELL_API_TOKEN unset (no auth; bind to localhost)

Roadmap

  • 0.1 — ingest (PDF/MD/TXT/HTML/DOCX), hybrid search (pgvector + tsvector, RRF), rerank, cited passages over CLI / REST / MCP, collections, Docker, Turkish eval sets
  • 0.2 — PyPI install, verify_quote, printed page labels, search without Turkish characters, MCP completions, ingest --prune
  • 0.3 — OCR for scanned PDFs, better tables, citewell watch
  • 0.4 — OpenAI-compatible endpoint (Open WebUI), per-collection tokens
  • 1.0 — stable API, public Turkish benchmark, sparse/BM25 retriever, SQLite mode

Details: docs/roadmap.md · progress in the Roadmap issue — good first issue and help wanted items are open for contributors.

Contributing

See CONTRIBUTING.md. Security reports: SECURITY.md. Architecture decisions live in docs/adr.

License

MIT © Ömer Özkılıç

Metadata

Release files for citewell 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for citewell 0.2.0
File Size Uploaded Provenance Actions
citewell-0.2.0.tar.gz 53.4 kB With provenance Details for citewell-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for citewell 0.2.0
File Interpreter ABI Platform Actions
citewell-0.2.0-py3-none-any.whl Python 3 none any Details for citewell-0.2.0-py3-none-any.whl

Total release size: 115.7 kB

Release files / citewell-0.2.0.tar.gz

Download URL citewell-0.2.0.tar.gz
Size 53.4 kB
Tags Source
SHA-256 checksum
How to use checksums
0c65abee9945f280950b4032929a0dd5d84e75871007d711fbe99d630d2cf4b6
BLAKE2b-256 checksum
How to use checksums
c20a274a6210cafb66ceca52cbd0015e4725a2f1082d81c463e86857cc4180bb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

Release files / citewell-0.2.0-py3-none-any.whl

Download URL citewell-0.2.0-py3-none-any.whl
Size 62.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
017abf754898851bc6d439129f8a6d2418933e26af6627bb7fd5af331c68ada5
BLAKE2b-256 checksum
How to use checksums
b59e03de226800a62191106223e6fb2fcb968828719edb4dc5c07d19d579b567
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

Security for citewell 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for citewell-0.2.0.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 8, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page