{
  "markdown": "# Paparats MCP\n\n<img src=\"docs/paparats-kvetka.png\" alt=\"Paparats-kvetka (fern flower)\" width=\"200\" align=\"right\">\n\n[![npm version](https://img.shields.io/npm/v/@paparats/cli?color=01696f)](https://www.npmjs.com/package/@paparats/cli)\n[![License: MIT](https://img.shields.io/badge/License-MIT-01696f.svg)](LICENSE)\n[![PulseMCP](https://img.shields.io/badge/PulseMCP-listed-01696f)](https://www.pulsemcp.com/servers/paparats-mcp)\n[![MCP Badge](https://lobehub.com/badge/mcp/ibazylchuk-paparats-mcp)](https://lobehub.com/mcp/ibazylchuk-paparats-mcp)\n\n[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/IBazylchuk/paparats-mcp) &nbsp;<sub>← try the full stack in your browser, no install ([details](#try-it-in-the-browser-no-install))</sub>\n\n**Paparats-kvetka** — a magical flower from Slavic folklore that blooms on Kupala Night\nand grants whoever finds it the power to see hidden things. Likewise, paparats-mcp\nhelps your agent see the right code across a sea of repositories.\n\n> 🌿 Works with **Claude Code · Cursor · Windsurf · Copilot · Codex · Antigravity** · any MCP-compatible agent\n\n**Give your AI coding assistant deep, real understanding of your entire workspace.**\nPaparats indexes every repo you care about — semantically, with AST-aware chunking and\na cross-chunk symbol graph — and exposes it through the Model Context Protocol. Search\nby meaning, follow `who-uses-what` through real symbol edges, see who last touched a\nchunk and which ticket it came from — all without your code ever leaving your machine.\n\n<a href=\"docs/dashboard.png\"><img src=\"docs/dashboard.png\" alt=\"Paparats operator console — ROI, top queries, cross-project usage, indexer health, embedding latency (synthetic data)\" width=\"100%\"></a>\n\n<sub>📊 The built-in `/ui` operator console — ROI, query quality, cross-project usage, per-user activity, indexer health. <em>Screenshot uses synthetic data (<code>?demo=1</code>) — no real queries, users, or project names.</em></sub>\n\n- ⚡ **One install, one config.** `paparats install` → `paparats add ~/code/repo` → done.\n- 🌳 **AST-aware chunking and symbol extraction.** Tree-sitter parses every supported\n  file once and feeds both chunking and the cross-chunk symbol graph (calls /\n  called_by / references / referenced_by) — 11 languages including TypeScript, Python,\n  Go, Rust, Java, Ruby, C, C++, C#.\n- 🧠 **Architectural memory that the agent maintains itself.** A second vector store\n  per group holds **components, decisions (ADRs) and lessons learned** — your agent\n  writes them as it works and reads them before answering. Bootstrap on day one with\n  the `init_arch_memory` MCP prompt (the `/init` of architectural memory). Server-side\n  similarity gate prevents duplicates, supersedes links replace stale decisions, a\n  `min_score` threshold gates low-confidence reads, every card carries an\n  \"updated N ago\" stamp, and Prometheus metrics tell you whether your memory is\n  actually being used.\n- 💸 **Saves tokens.** Returns only the chunks that matter, with token-savings telemetry\n  to prove it (per-query, per-user, per-anchor-project).\n- 🔭 **Production-ready observability.** Prometheus `/metrics`, OpenTelemetry traces\n  (Tempo, Jaeger, Honeycomb, Datadog, Grafana Cloud, **Elastic APM**), local SQLite\n  analytics, and a built-in `/ui` operator console that visualises ROI, query quality,\n  cross-project usage and indexer health in one screen.\n- 🏠 **100% local by default.** Qdrant + a local embed server (llama.cpp llama-server +\n  llama-swap) on your machine. No cloud, no API keys, no telemetry leaving the box. Bring\n  your own Qdrant Cloud / embed server URL if you want.\n\n---\n\n## Table of Contents\n\n- [Why Paparats?](#why-paparats)\n- [Quick Start](#quick-start)\n- [How the install works](#how-the-install-works)\n- [Install variants](#install-variants)\n- [Migrating from a v1 install](#migrating-from-a-v1-install)\n- [Support agent setup](#support-agent-setup)\n- [How It Works](#how-it-works)\n- [Key Features](#key-features)\n- [Use Cases](#use-cases)\n- [Architectural memory](#architectural-memory-agent-maintained-adrs-components-lessons)\n- [Configuration](#configuration)\n- [MCP Tools Reference](#mcp-tools-reference)\n- [Connecting MCP](#connecting-mcp)\n- [CLI Commands](#cli-commands)\n- [Monitoring](#monitoring)\n- [Analytics & Observability](#analytics--observability)\n- [Architecture](#architecture)\n- [Embedding Model Setup](#embedding-model-setup)\n- [Comparison with Alternatives](#comparison-with-alternatives)\n- [Token Savings Metrics](#token-savings-metrics)\n- [Contributing](#contributing)\n- [Links](#links)\n\n---\n\n## Why Paparats?\n\nAI coding assistants are smart, but they can only see files you open. They don't know your codebase structure, where the authentication logic lives, or how services connect. **Paparats fixes that.**\n\n### What you get\n\n- **Semantic code search** — ask \"where is the rate limiting logic?\" and get exact code ranked by meaning, not grep matches\n- **Real-time sync** — edit a file, and 2 seconds later it's re-indexed. No manual re-runs\n- **Cross-chunk symbol graph** — `find_usages` walks AST-derived edges (calls, called_by, references, referenced_by) so the agent can trace dependencies without re-grepping\n- **Token savings** — return only relevant chunks instead of full files to reduce context size\n- **Multi-project workspaces** — search across backend, frontend, infra repos in one query\n- **100% local & private** — Qdrant vector database + local llama-server embeddings. Nothing leaves your laptop\n- **AST-aware chunking** — code split by AST nodes (functions/classes) via tree-sitter, not arbitrary character counts (TypeScript, JavaScript, TSX, Python, Go, Rust, Java, Ruby, C, C++, C#; regex fallback for Terraform)\n- **Rich metadata** — each chunk knows its symbol name (from tree-sitter AST), service, domain context, and tags from directory structure\n- **Git history per chunk** — see who last modified a chunk, when, and which tickets (Jira, GitHub) are linked to it\n- **Architectural memory** — a living knowledge base of components, decisions (ADRs) and lessons learned, written by the agent as it learns, deduplicated server-side by vector similarity, and consulted on every support query so the agent stays consistent across sessions\n\n### Who benefits\n\n| Use Case                    | How Paparats Helps                                                                                     |\n| --------------------------- | ------------------------------------------------------------------------------------------------------ |\n| **Solo developers**         | Quickly navigate unfamiliar codebases, find examples of patterns, reduce context-switching             |\n| **Multi-repo teams**        | Cross-project search (backend + frontend + infra), consistent patterns, faster onboarding              |\n| **AI agents**               | Foundation for product support bots, QA automation, dev assistants — any agent that needs code context |\n| **Legacy modernization**    | Find all usages of deprecated APIs, identify migration patterns, discover hidden dependencies          |\n| **Contractors/consultants** | Accelerate ramp-up on client codebases, reduce \"where is X?\" questions                                 |\n\n---\n\n## Quick Start\n\n### Try it in the browser (no install)\n\n[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/IBazylchuk/paparats-mcp)\n\nSpin up a full Qdrant + embed server + paparats stack in a Codespace.\nA small slice of the repo (`packages/shared/src`) is auto-indexed on first start so\nyou can run\n\n```bash\npaparats search -g demo 'gitignore filter'\n```\n\nwithin a few minutes. Codespace forwards port 9876 for MCP — point Cursor/Claude Code\nat it via the URL VS Code shows in the Ports panel.\n\n> Note: Codespaces is for demo only. With CPU embedding the full repo\n> would take 15+ minutes and can hit batch timeouts on large files. For real\n> workloads run locally — or set `OPENAI_API_KEY` (or `VOYAGE_API_KEY`) as a\n> Codespaces user secret and indexing drops to a couple of seconds; see the\n> Embedding providers section below.\n\n### Run locally\n\nYou need **Docker** and **Docker Compose v2**. On macOS, also install the **embed server\nnatively** — running it inside Docker on macOS is significantly slower because the Docker\nVM cannot use Apple Silicon GPU (Metal) acceleration.\n\n```bash\n# 1. Install the CLI.\nnpm install -g @paparats/cli\n\n# 2. macOS only — install the native embed server (Linux uses the Docker embed\n#    image by default). Metal-accelerated.\nbrew install llama.cpp mostlygeek/llama-swap/llama-swap\n\n# 3. One-time bootstrap. Generates ~/.paparats/{docker-compose.yml,projects.yml},\n#    starts the stack, downloads the embedding model, wires Cursor/Claude Code MCP.\npaparats install\n\n# 4. Add the projects you want indexed. Local paths bind-mount read-only into the\n#    indexer; git URLs and owner/repo shorthand get cloned.\npaparats add ~/code/my-project\npaparats add git@github.com:acme/billing.git\npaparats add acme/widgets\n\n# 5. Watch it work.\npaparats list\n```\n\nThat's it. Your IDE is already wired (`~/.cursor/mcp.json`, `~/.claude/mcp.json`) to\n`http://localhost:9876/mcp`. Open Cursor or Claude Code and ask:\n\n> \"Search this workspace for the auth middleware and show me everything that calls it.\"\n\n### Existing v1 user?\n\nJust run `paparats install` again. The installer detects the legacy per-project\ncompose, asks once before swapping it for the new global setup, and **preserves your\nindexed data** (Qdrant collections, SQLite metadata, embedding cache). Your in-repo\n`.paparats.yml` files keep working as per-project overrides.\n\n---\n\n## How the install works\n\n`paparats install` is the only setup command. It creates a single global home at\n`~/.paparats/`, brings up a Docker stack, and wires your MCP clients. Re-run it any time\nto reconfigure — it diffs the existing compose and asks before overwriting hand edits.\n\n```\n~/.paparats/\n├── docker-compose.yml          generated; hand-editable; install asks before overwriting\n├── projects.yml        project list (CLI rewrites it; comments survive your manual edits)\n├── install.json                install flags persisted so add/remove can regenerate compose\n├── .env                        secrets — Qdrant API key, GitHub token; chmod 600\n├── models/                     bge-code-v1 + qwen3-embedding-0.6b GGUF (native embed mode)\n└── data/                       Docker volumes (mounted by name from compose)\n    ├── qdrant/                 vector index\n    ├── sqlite/                 metadata.db, embeddings.db, analytics.db\n    └── repos/                  cloned remote projects\n```\n\nInside the Docker stack:\n\n| Service            | Image                          | Port  | Role                                                     |\n| ------------------ | ------------------------------ | ----- | -------------------------------------------------------- |\n| `paparats-mcp`     | `ibaz/paparats-server:latest`  | 9876  | MCP HTTP/SSE endpoints, search, metadata API             |\n| `paparats-indexer` | `ibaz/paparats-indexer:latest` | 9877  | Cron + on-demand indexing, hot-reload of project list    |\n| `qdrant`           | `qdrant/qdrant:latest`         | 6333  | Vector DB (skipped when you pass `--qdrant-url`)         |\n| `embed`            | `ibaz/paparats-embed:latest`   | 11434 | Embed server — llama-server + llama-swap, `bge-code-v1` + `qwen3-embedding-0.6b` pre-baked (Linux default; macOS uses native embed server). llama-swap listens on 8080 inside the container |\n\nThe indexer hot-reloads `projects.yml`. Edits that **change project metadata\nonly** (group, language, indexing tweaks) reindex in place. Edits that **add or remove\nlocal-path projects** require a stack restart so Docker picks up the new bind-mount —\nthe CLI does this for you on `paparats add` and `paparats remove`.\n\n---\n\n## Install variants\n\n### Default (recommended)\n\n```bash\npaparats install\n```\n\nOn macOS prefers the native embed server and dockerized Qdrant. On Linux defaults to\nDocker for both.\n\n### Bring your own Qdrant\n\n```bash\npaparats install --qdrant-url https://qdrant.example.com\n# Asks for an API key after; stored in ~/.paparats/.env as QDRANT_API_KEY.\n```\n\nWhen `--qdrant-url` is set the Qdrant container is omitted from the stack entirely.\n\n### Bring your own embed server\n\n```bash\npaparats install --embed-url http://10.0.0.5:11434\n```\n\nSkips both the native and Docker embed server.\n\n> **The remote endpoint must serve the `bge-code-v1` model (and `qwen3-embedding-0.6b` for the\n> arch-memory layer) over an OpenAI-style `/v1/embeddings` API.** The installer will not\n> touch a remote instance. The simplest way is to run the pre-baked image on that host —\n> no model registration needed, llama-swap loads GGUF by name on first request:\n>\n> ```bash\n> docker run -d -p 11434:8080 ibaz/paparats-embed:latest\n> ```\n>\n> Then `paparats install --embed-url http://that-host:11434` and Paparats will use it.\n\n### Force Docker embed server on macOS\n\n```bash\npaparats install --embed-mode docker\n```\n\nSlower on Apple Silicon (no Metal GPU), but useful for parity testing or laptops without\nbrew.\n\n### Scripted / CI\n\n```bash\npaparats install --non-interactive --force\n```\n\nFails on any prompt; `--force` answers Y to compose-overwrite and migration prompts.\n\n---\n\n## Migrating from a v1 install\n\nWhen `paparats install` finds a legacy `~/.paparats/docker-compose.yml` (the one from the\nold per-project flow with no `paparats-indexer` service), it prints a one-screen\nmigration notice and asks before tearing the legacy stack down.\n\n**What survives:** Qdrant collections, SQLite metadata, indexer repos, and any\n`.paparats.yml` files inside your repos (those still take precedence over\n`projects.yml` overrides).\n\n**What's deleted:** the legacy `docker-compose.yml` and `.env`. They are regenerated on\nthe spot under the new schema.\n\n**No re-indexing needed** — the data volumes are referenced by the same names in the new\ncompose. Add your projects with `paparats add` and they re-appear in `paparats list` with\ntheir existing chunks.\n\nIf your install predates the `paparats-indexer.yml` → `projects.yml` rename, the\ninstaller migrates the file in place on first run and prints a one-line notice.\nThe indexer also reads the legacy name as a fallback, so nothing breaks if you\nroll out the indexer before re-running `paparats install`.\n\nPass `--force` to skip the migration prompt in scripts.\n\n---\n\n## Support agent setup\n\nFor bots and support teams that consume an existing Paparats server — no Docker, no\nembed server needed on this side.\n\n```bash\n# Connect to a running server (default: localhost:9876)\npaparats install --mode support\n\n# Connect to a remote server\npaparats install --mode support --server http://prod-server:9876\n```\n\nThe installer verifies the server is reachable, then wires Cursor MCP\n(`~/.cursor/mcp.json`) and Claude Code MCP (`~/.claude/mcp.json`) to the support\nendpoint. Tools available on `/support/mcp`: `search_code`, `get_chunk`, `find_usages`,\n`list_projects`, `health_check`, `get_chunk_meta`, `search_changes`, `explain_feature`,\n`recent_changes`, `impact_analysis`, **`arch_context`**, **`arch_record_component`**,\n**`arch_record_decision`**, **`arch_record_lesson`** (architectural memory — see\n[Key Features](#architectural-memory-agent-maintained-adrs-components-lessons)), plus\nthe analytics tools described in **Observability** below.\n\n---\n\n## How It Works\n\n```\nYour projects                   Paparats                       AI assistant\n                                                               (Claude Code / Cursor)\n  backend/                 ┌──────────────────────┐\n    .paparats.yml ────────►│  Indexer              │\n  frontend/                │   - chunks code       │          ┌──────────────┐\n    .paparats.yml ────────►│   - embeds via llama  │─────────►│ MCP search   │\n  infra/                   │   - stores in Qdrant  │          │ tool call    │\n    .paparats.yml ────────►│   - watches changes   │          └──────────────┘\n                           └──────────────────────┘\n```\n\n### Indexing Pipeline\n\nDuring each indexer cycle (cron-driven, on-demand via `paparats add`, or triggered by\nthe indexer's chokidar file watcher), every file in scope flows through this pipeline:\n\n```\n Source file\n     │\n     ▼\n ┌─────────────────┐\n │ 1. File discovery│  Collect files from indexing.paths, apply\n │    & filtering   │  gitignore + exclude patterns, skip binary\n └────────┬────────┘\n          ▼\n ┌─────────────────┐\n │ 2. Content hash  │  SHA-256 of file content → compare with\n │    check         │  existing Qdrant chunks → skip unchanged\n └────────┬────────┘\n          ▼\n ┌─────────────────┐\n │ 3. AST parsing   │  tree-sitter parses the file once (WASM)\n │    (single pass) │  → reused for chunking AND symbol extraction\n └────────┬────────┘\n          ▼\n ┌─────────────────┐\n │ 4. Chunking      │  AST nodes → chunks at function/class\n │                  │  boundaries. Regex fallback for unsupported\n │                  │  languages (brace/indent/block strategies)\n └────────┬────────┘\n          ▼\n ┌─────────────────┐\n │ 5. Symbol        │  AST queries extract module-level defines\n │    extraction    │  (function/class/variable names) and uses\n │                  │  (calls, references) per chunk. 11 languages\n └────────┬────────┘\n          ▼\n ┌─────────────────┐\n │ 6. Metadata      │  Service name, bounded_context, tags from\n │    enrichment    │  config + auto-detected directory tags\n └────────┬────────┘\n          ▼\n ┌─────────────────┐\n │ 7. Embedding     │  Jina Code Embeddings 1.5B via llama-server\n │                  │  SQLite cache (content-hash key) → skip\n │                  │  already-embedded content\n └────────┬────────┘\n          ▼\n ┌─────────────────┐\n │ 8. Qdrant upsert │  Vectors + payload (content, file, lines,\n │                  │  symbols, metadata) → batched upsert\n └────────┬────────┘\n          ▼\n ┌─────────────────┐\n │ 9. Git history   │  git log per file → diff hunks → map\n │    (post-index)  │  commits to chunks by line overlap →\n │                  │  extract ticket refs → store in SQLite\n └────────┬────────┘\n          ▼\n ┌─────────────────┐\n │10. Symbol graph  │  Cross-chunk edges: calls ↔ called_by,\n │    (post-index)  │  references ↔ referenced_by → SQLite\n └─────────────────┘\n```\n\nStep 5's symbol extractor only emits **module-level** definitions — locals declared\ninside function bodies, callback args, and hook closures stay out of the graph because\nthey're not addressable from another chunk anyway.\n\n### Search Flow\n\nAI assistant queries via MCP → server detects query type (nl2code / code2code / techqa) → expands query (abbreviations, case variants, plurals) → all variants searched in parallel against Qdrant → results merged by max score → only relevant chunks returned with confidence scores and symbol info.\n\n### Watching\n\nThe indexer container watches the projects mounted into it via chokidar with debouncing\n(1s default). On change, only the affected file re-enters the pipeline. Unchanged content\nis never re-embedded thanks to the content-hash cache. The indexer also hot-reloads\n`~/.paparats/projects.yml` itself: metadata-only edits reindex in place;\nadd/remove of local-path projects triggers a stack restart through the CLI.\n\n---\n\n## Key Features\n\n### Better Search Quality\n\n**Task-specific embeddings** — Jina Code Embeddings supports 3 query types (nl2code, code2code, techqa) with different prefixes for better relevance:\n\n- `\"find authentication middleware\"` → `nl2code` prefix (natural language → code)\n- `\"function validateUser(req, res)\"` → `code2code` prefix (code → similar code)\n- `\"how does OAuth work in this app?\"` → `techqa` prefix (technical questions)\n\n**Query expansion** — every search generates 2-3 variations server-side:\n\n- Abbreviations: `auth` ↔ `authentication`, `db` ↔ `database`\n- Case variants: `userAuth` → `user_auth` → `UserAuth`\n- Plurals: `users` → `user`, `dependencies` → `dependency`\n- Filler removal: `\"how does auth work\"` → `\"auth\"`\n\nAll variants searched in parallel, results merged by max score.\n\n**Confidence scores** — each result includes a percentage score (≥60% high, 40–60% partial, <40% low) to guide AI next steps.\n\n### Performance\n\n**Embedding cache** — SQLite cache with content-hash keys + Float32 vectors. Unchanged code never re-embedded. LRU cleanup at 100k entries.\n\n**AST-aware chunking** — tree-sitter AST nodes define natural chunk boundaries for 11 languages. Falls back to regex strategies (block-based for Ruby, brace-based for JS/TS, indent-based for Python, fixed-size) for unsupported languages.\n\n**Real-time watching** — the indexer's `chokidar` watcher reindexes a project on file\nchanges with debouncing (1s default). For local-path projects bind-mounted into the\nindexer, edits on your host show up in MCP queries within seconds.\n\n### Cross-chunk symbol graph\n\nThe post-index pass walks every chunk's `defines_symbols` / `uses_symbols` lists and\nmaterializes edges into SQLite — `calls`, `called_by`, `references`, `referenced_by`.\n`find_usages` returns those edges grouped by direction so the agent can traverse the\ngraph without re-searching. Because extraction is AST-driven, function locals don't\npollute the graph.\n\n### Architectural memory (agent-maintained ADRs, components, lessons)\n\nCode search tells the agent **what the code does**. Architectural memory tells it\n**why** — and the agent maintains that knowledge itself, across sessions, without you\nauthoring a single doc.\n\n**Three card kinds, structured by design:**\n\n| Kind          | Captures                                                                                  | Fields                                                            |\n| :------------ | :---------------------------------------------------------------------------------------- | :---------------------------------------------------------------- |\n| **Component** | A unit with a clear responsibility (service, module, subsystem)                           | `name`, `summary` with `Does / Owns / Does not / Touched when`    |\n| **Decision**  | An architectural choice (ADR-style)                                                       | `title`, `context`, `decision`, `alternatives_rejected`, `consequences` |\n| **Lesson**    | A rule learned from an incident, a code review, a bug, or a user correction (Reflexion-style) | `rule`, `why`, `when`                                             |\n\nThe agent reads them via **`arch_context`** before any architectural answer, and\nwrites them via **`arch_record_component`**, **`arch_record_decision`**, and\n**`arch_record_lesson`** whenever it discovers something new or learns from a\ncorrection. Each card carries an `updated N ago` stamp in the read tool so the agent\ncan spot stale memory and verify against current code.\n\n**Server-side similarity gate** (cosine over [Qwen3-Embedding-0.6B](https://huggingface.co/Qwen/Qwen3-Embedding-0.6B)\ntext embeddings, 1024d):\n\n- `≥ 0.85` is a **duplicate** — decisions are refused (the agent must reconcile or\n  supersede); lessons bump `updatedAt` (Reflexion-style \"rule confirmed\").\n- `0.70 – 0.85` is **similar** — surfaced to the agent so it can refine the wording\n  or chain a supersede.\n- `< 0.70` is **new** — accepted as a fresh card.\n\n`supersedes` links bypass the gate and mark the prior decision as `status=superseded`\nso it disappears from default search but remains in history.\n\n**Why this matters:**\n\n- 🧠 **Cross-session continuity** — what the agent learned last week, today's agent\n  still knows.\n- 📝 **ADRs without the ceremony** — no markdown files to maintain, no review process,\n  no doc drift. The agent writes when it learns.\n- 🔄 **Reflexion built in** — corrections become lessons; repeated mistakes get caught.\n- 🚦 **No memory rot** — similarity gate kills duplicates, supersedes link replaces\n  stale decisions, age stamps trigger verification against code.\n\nLives in a separate Qdrant collection per group (`paparats_<group>_arch`). Reading\n(`arch_context`) is available on **both** endpoints — coding agents need to know\nabout prior decisions before refactoring. Writing (`arch_record_*`) is **support-only**:\nrecording belongs to the architectural-review workflow, not to every line edit.\n\n**`arch_context` accepts a `min_score` parameter** (default `0.45`, cosine over\n[Qwen3-Embedding-0.6B](https://huggingface.co/Qwen/Qwen3-Embedding-0.6B)). Lower it to broaden recall on a sparse\narch memory; raise it to demand only high-confidence cards. The tool also emits an\nexplicit low-confidence hint when the question matched nothing above the threshold,\nso the agent knows to either rephrase or lower `min_score` instead of inventing\ncontext.\n\n**Initialise the arch layer on day one.** Two purpose-built MCP **workflow prompts**\nmake the boring scaffolding work disappear:\n\n- **`init_arch_memory`** — the `/init` of architectural memory. Walks the repo,\n  identifies 8-20 components by domain boundary, writes them, and captures any\n  obvious decisions inferable from comments or README. Run it once per group, right\n  after installing.\n- **`audit_architecture`** — sweeps the memory of one group, flags cards older than\n  90 days, verifies anchors against the live code, and surfaces a punch list of\n  updates / supersedes for your approval.\n- **`record_lesson_from_correction`** — converts a user correction into a structured\n  lesson card (rule / why / when) without overrecording typos.\n\n**MCP resources for live introspection:**\n\n- **`arch://schema`** — the full card-schema reference (fields, similarity-gate\n  thresholds, write semantics). Cite it from the agent when explaining the model.\n- **`arch://stats/{group}`** — live counts (total / by kind / by status) and the\n  oldest/newest `updatedAt` per group. The same numbers are also pushed to\n  Prometheus.\n\n**Observability built in.** When `PAPARATS_METRICS=true`, every read/write hits a\ncounter and the cosine score of returned cards lands in a histogram:\n\n- `paparats_arch_context_calls_total{group}` — calls per group\n- `paparats_arch_write_total{kind, status}` — writes by card kind and gate outcome\n- `paparats_arch_search_score` — histogram of cosine scores in `arch_context`\n  results (post `min_score`)\n- `paparats_arch_collection_size{group, kind, status}` — gauge updated whenever\n  `arch://stats/{group}` is read\n\nThese let you spot a memory that's not being written to, a similarity gate that's\ntoo aggressive, or a sparse group where every query returns low-confidence hits.\n\n---\n\n## Use Cases\n\n### For Developers (Coding)\n\nConnect via the **coding endpoint** (`/mcp`):\n\n| Use Case                     | How                                                                    |\n| ---------------------------- | ---------------------------------------------------------------------- |\n| **Navigate unfamiliar code** | `search_code \"authentication middleware\"` → exact locations            |\n| **Find similar patterns**    | `search_code \"retry with exponential backoff\"` → examples              |\n| **Trace dependencies**       | `find_usages {chunk_id, direction: \"incoming\"}` → callers via the graph |\n| **Explore context**          | `get_chunk <chunk_id> --radius_lines 50` → expand around               |\n| **Manage projects**          | `list_projects` and `delete_project` for index hygiene                 |\n\n### For Support Teams\n\nConnect via the **support endpoint** (`/support/mcp`):\n\n| Use Case                       | How                                                                    |\n| ------------------------------ | ---------------------------------------------------------------------- |\n| **Explain a feature**          | `explain_feature \"rate limiting\"` → code locations + changes          |\n| **Recent changes**             | `recent_changes \"auth\" --since 2024-01-01` → timeline with tickets     |\n| **Trace usages**               | `find_usages {chunk_id}` → who calls/references this chunk             |\n| **Change history**             | `get_chunk_meta <chunk_id>` → authors, dates, linked tickets           |\n| **Blast radius**               | `impact_analysis <chunk_id>` → cross-chunk + cross-project impact      |\n| **Architectural Q&A**          | `arch_context \"why X\"` → components / decisions / lessons (with age)   |\n| **Capture decisions & lessons**| `arch_record_decision` / `arch_record_lesson` — agent writes as it learns, server-side dedup |\n\n**Support chatbot example:**\n\n```\nUser: \"How do I configure rate limiting?\"\n\nBot workflow (via /support/mcp):\n1. explain_feature(\"rate limiting\", group=\"my-app\")\n   → returns code locations + recent changes + related modules\n2. get_chunk_meta(<chunk_id>)\n   → returns who last modified it, when, linked tickets\n3. Bot synthesizes response in plain language with ticket references\n```\n\n---\n\n## Configuration\n\nPaparats uses two config files. Both are optional — defaults work for the common case.\n\n### `~/.paparats/projects.yml` — global project list\n\nLives outside your repos. Edited by `paparats add` / `paparats remove` or by hand via\n`paparats edit projects`. Every entry has either `path:` (local bind-mount) or `url:`\n(remote git, cloned by the indexer), never both.\n\n```yaml\ndefaults:\n  cron: '0 */6 * * *' # global indexer schedule\n  group: workspace # default group when an entry doesn't specify one\n\nrepos:\n  - path: /Users/alice/code/billing # local bind-mount\n    group: dev\n    language: typescript\n\n  - url: org/widgets # remote git, cloned by the indexer\n    group: prod\n    language: ruby\n\n  - url: git@github.com:acme/billing.git\n    name: billing # override the auto-derived name\n    group: prod\n```\n\nThe indexer hot-reloads this file. Adding/removing **local-path** entries causes the CLI\nto restart the stack so Docker picks up the new bind-mount; metadata-only edits reindex\nin place.\n\n### `.paparats.yml` in your repo — per-project overrides\n\nDrop one at the project root to override anything from the global file.\n\n```yaml\ngroup: my-app\nlanguage: typescript\n\n# Indexing tuning (all optional)\nindexing:\n  paths: [src, packages] # restrict to these subdirectories\n  exclude: [node_modules, dist, '**/*.test.ts']\n  exclude_extra: ['**/__fixtures__/**'] # added on top of language defaults\n  chunkSize: 1500 # characters per chunk (default: 1200)\n  overlap: 100 # chunk overlap (default: 100)\n  concurrency: 4 # parallel embedding requests\n  batchSize: 8 # embeddings per llama-server call\n\n# Metadata\nmetadata:\n  service: billing\n  bounded_context: payments\n  tags: [backend, critical]\n  directory_tags:\n    src/api: [public-api]\n    src/internal: [internal]\n\n  # Git history per chunk (Jira / GitHub ticket extraction included)\n  git:\n    enabled: true\n    maxCommitsPerFile: 50\n    ticketPatterns:\n      - '\\b([A-Z]+-\\d+)\\b' # Jira-style PROJ-123\n      - '#(\\d+)' # GitHub-style #123\n```\n\nIn-repo `.paparats.yml` always wins over `projects.yml`. The CLI never\noverwrites it.\n\n### Groups\n\nA **group** is a Qdrant collection (`paparats_<group>`). Multiple projects can share a\ngroup to enable cross-project search; each project lives as a `project:` field in the\nchunk payload. By default `group` defaults to the project name (one project, one\ncollection). Set the same `group:` on multiple entries to consolidate them.\n\n### Git history per chunk\n\nWhen `metadata.git.enabled: true` (default), the indexer maps each chunk to the commits\nthat touched its line range using diff-hunk overlap. Tickets are extracted from commit\nmessages using `metadata.git.ticketPatterns` (built-in: Jira `PROJ-123`, GitHub `#42`,\ncross-repo `org/repo#99`). Surfaced through MCP tools `get_chunk_meta`, `search_changes`,\n`recent_changes`, `explain_feature`. Non-fatal: non-git projects index normally.\n\n---\n\n## MCP Tools Reference\n\nPaparats serves the Model Context Protocol on **two separate endpoints**, each with its\nown tool set and system instructions.\n\n### Coding endpoint (`/mcp`)\n\nFor developers using Claude Code, Cursor, etc. Focus: search code, read chunks, follow\nthe cross-chunk symbol graph, manage projects.\n\n| Tool             | Description                                                                                                                  |\n| :--------------- | :--------------------------------------------------------------------------------------------------------------------------- |\n| `search_code`    | Semantic search across indexed projects. Returns chunks with symbol info and confidence scores.                              |\n| `get_chunk`      | Retrieve a chunk by ID with optional surrounding context.                                                                    |\n| `find_usages`    | Walk the symbol graph from a `chunk_id` — `incoming` (callers/references in), `outgoing` (calls/references out), or `both`.  |\n| `list_projects`  | List indexed projects with chunk counts and detected languages.                                                              |\n| `delete_project` | Wipe Qdrant chunks + SQLite metadata for a project (CLI's `paparats remove` calls it).                                       |\n| `health_check`   | Indexing status, chunks per group, running jobs.                                                                             |\n| `arch_context`   | Read-only architectural memory. Returns components, decisions, and lessons relevant to the query with `updated N ago` stamps and a `min_score` cutoff. |\n\n### Support endpoint (`/support/mcp`)\n\nFor support teams and bots without direct code access. Focus: feature explanations,\nchange history, cost reporting — all in plain language.\n\n| Tool                   | Description                                                                            |\n| :--------------------- | :------------------------------------------------------------------------------------- |\n| `search_code`          | Same as coding endpoint.                                                               |\n| `get_chunk`            | Same.                                                                                  |\n| `find_usages`          | Same.                                                                                  |\n| `list_projects`        | Same.                                                                                  |\n| `health_check`         | Same.                                                                                  |\n| `get_chunk_meta`       | Git history and ticket references for a chunk — commits, authors, dates. No code.     |\n| `search_changes`       | Semantic search filtered by last-commit date. Each result shows when it last changed. |\n| `explain_feature`      | Comprehensive feature analysis: locations + recent changes for a question.            |\n| `recent_changes`       | Timeline grouped by date with commits, tickets, affected files. `since` filter.       |\n| `impact_analysis`      | Cross-chunk impact for a `chunk_id` — symbol graph traversal + cross-project blast radius. |\n| `arch_context`         | Read the architectural memory for a group — top-matching components, decisions and lessons, each stamped with \"updated N ago\" and a cosine score. Accepts a `min_score` parameter (default 0.45) to gate low-confidence hits. Call before any architectural answer. **Also available on `/mcp`.** |\n| `arch_record_component` | Record a component with `Does / Owns / Does not / Touched when` fields. Idempotent by `name`. |\n| `arch_record_decision` | Record an ADR-style decision (`context / decision / alternatives_rejected / consequences`). Server-side similarity gate refuses duplicates and surfaces near-matches; `supersedes` links replace prior decisions. |\n| `arch_record_lesson`   | Record a lesson as `rule / why / when`. Duplicates bump `updatedAt` (Reflexion confirmation) instead of overwriting. |\n| `token_savings_report` | Aggregate token-savings stats (naive baseline vs search-only vs actually consumed).   |\n| `top_queries`          | Most frequent queries by user/session/project anchor.                                  |\n| `slowest_searches`     | Top-N slowest searches with timing + chunk counts.                                    |\n| `cross_project_share`  | Off-anchor result share per user — indicator of search noise.                         |\n| `retry_rate`           | Tool-call retry rate per user — indicator of unhelpful results.                       |\n| `failed_chunks`        | AST parse failures, regex fallbacks, zero-chunk files, binary skips.                  |\n\n### Typical workflows\n\n**Drill-down (coding agent):**\n\n```\n1. search_code \"authentication middleware\"           → relevant chunks with symbols\n2. get_chunk <chunk_id> --radius_lines 50            → expand context around a hit\n3. find_usages {chunk_id, direction: \"incoming\"}     → who calls / references this chunk\n```\n\n**Single-call (support agent):**\n\n```\n1. explain_feature \"How does authentication work?\"   → locations + recent changes\n2. recent_changes \"auth\" --since 2024-01-01          → timeline with tickets\n3. token_savings_report                              → cost report for the last 7 days\n```\n\n**Architectural memory (support agent):**\n\n```\n1. arch_context \"why do we use qwen3-embedding-0.6b for the arch layer?\"\n                                                     → top components / decisions / lessons,\n                                                       each with an \"updated N ago\" stamp\n2. arch_record_decision { title, context, decision, alternatives_rejected, consequences }\n                                                     → status=created | duplicate | similar\n                                                       (gate refuses duplicates server-side)\n3. arch_record_lesson   { rule, why, when }          → status=created | updated (Reflexion bump)\n```\n\n---\n\n## Connecting MCP\n\n`paparats install` already wires Cursor (`~/.cursor/mcp.json`) and Claude Code\n(`~/.claude/mcp.json`) to `http://localhost:9876/mcp`. The sections below are for\nmanual setup or for adding the **support** endpoint alongside the default coding one.\n\n### Cursor\n\nCreate or edit `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` (project):\n\n```json\n{\n  \"mcpServers\": {\n    \"paparats\": {\n      \"type\": \"http\",\n      \"url\": \"http://localhost:9876/mcp\"\n    }\n  }\n}\n```\n\nFor support use case (feature explanations, change history, impact analysis):\n\n```json\n{\n  \"mcpServers\": {\n    \"paparats-support\": {\n      \"type\": \"http\",\n      \"url\": \"http://localhost:9876/support/mcp\"\n    }\n  }\n}\n```\n\nRestart Cursor after changing config.\n\n### Claude Code\n\n```bash\n# Coding endpoint (default)\nclaude mcp add --transport http paparats http://localhost:9876/mcp\n\n# Support endpoint (for support bots/agents)\nclaude mcp add --transport http paparats-support http://localhost:9876/support/mcp\n```\n\nOr add to `.mcp.json` in project root:\n\n```json\n{\n  \"mcpServers\": {\n    \"paparats\": {\n      \"type\": \"http\",\n      \"url\": \"http://localhost:9876/mcp\"\n    }\n  }\n}\n```\n\n### Verify\n\n- `paparats status` — check stack is up\n- **Coding endpoint** (`/mcp`): `search_code`, `get_chunk`, `find_usages`,\n  `list_projects`, `delete_project`, `health_check`\n- **Support endpoint** (`/support/mcp`): `search_code`, `get_chunk`, `find_usages`,\n  `health_check`, `list_projects`, plus the support-specific tools `get_chunk_meta`,\n  `search_changes`, `explain_feature`, `recent_changes`, `impact_analysis`, and the\n  analytics tools listed in **Observability** (`token_savings_report`, `top_queries`,\n  `slowest_searches`, `cross_project_share`, `retry_rate`, `failed_chunks`)\n- Ask the AI: _\"Search this workspace for the auth middleware\"_\n\n---\n\n## CLI Commands\n\n```text\npaparats install [flags]                Bootstrap or reconfigure the global stack.\npaparats add <path-or-repo> [flags]     Add a project (local path or git URL/shorthand).\npaparats list [--json] [--group g]      Show indexed projects with status from the indexer.\npaparats remove <name> [--yes]          Remove a project — deletes Qdrant + SQLite data.\n\npaparats start [--logs]                 Start the Docker stack (with `--logs` follows them).\npaparats stop                           Stop the stack (preserves data volumes).\npaparats restart                        Recreate containers (applies new compose changes).\npaparats edit compose|projects          Open the file in $EDITOR; on save, validate +\n                                          regenerate compose + restart + reindex (projects).\n\npaparats search <query> [flags]         Semantic search from the terminal.\npaparats status                         Stack health: Docker, embed server, server, indexer.\npaparats groups [--json]                List groups and their projects.\npaparats doctor                         Diagnostic checks (Docker, embed server, ports, configs).\npaparats update                         Update CLI from npm + pull latest Docker images.\n```\n\nThe legacy per-project commands (`paparats init`, `paparats index`, `paparats watch`) are\ngone — adding a project is now `paparats add`, indexing is automatic in the indexer\ncontainer, watching is the `chokidar` watcher inside the indexer.\n\n### Common flags\n\n**`paparats install`**\n\n- `--embed-mode <native|docker>` — force embed server mode (default: native on macOS, docker on Linux)\n- `--embed-url <url>` — external embed server; skips both native and docker embed server\n- `--qdrant-url <url>` — external Qdrant; skips the Qdrant container\n- `--qdrant-api-key <key>` — for authenticated Qdrant (e.g. Qdrant Cloud); written to `~/.paparats/.env`\n- `--mode support` — wire MCP clients only, no Docker stack\n- `--server <url>` — server URL for support mode (default: `http://localhost:9876`)\n- `--force` — skip overwrite/migration prompts\n- `--non-interactive` — fail on any prompt instead of asking\n- `-v, --verbose` — stream Docker output\n\n**`paparats add <path-or-repo>`**\n\n- `--name <name>` — override the auto-derived project name (basename of path / repo)\n- `--group <group>` — override group (default: project name)\n- `--language <lang>` — override language (default: auto-detect)\n- `--no-restart` — skip the Docker restart for local-path adds (useful in scripts)\n- `--no-reindex` — skip the per-project reindex trigger\n- `--force` — drop the project's existing chunks before reindexing (destructive, use after schema/config changes)\n\n**`paparats remove <name>`**\n\n- `--yes` — skip the confirmation prompt\n\n**`paparats search <query>`**\n\n- `-n, --limit <n>` — max results (default: 5)\n- `-p, --project <name>` — filter by project\n- `-g, --group <name>` — restrict to a group\n- `--json` — machine-readable output\n\n### Environment overrides\n\n| Var                    | Default                 | What                                       |\n| ---------------------- | ----------------------- | ------------------------------------------ |\n| `PAPARATS_SERVER_URL`  | `http://localhost:9876` | MCP server base URL (used by CLI commands) |\n| `PAPARATS_INDEXER_URL` | `http://localhost:9877` | Indexer base URL (`add`, `list`, `edit`)   |\n\n---\n\n## Monitoring\n\nPaparats exposes Prometheus metrics for operational visibility. Opt in by setting `PAPARATS_METRICS=true` in the server's environment:\n\n```yaml\n# In ~/.paparats/docker-compose.yml, under paparats service:\nenvironment:\n  PAPARATS_METRICS: 'true'\n```\n\n### Metrics endpoint\n\n```bash\ncurl http://localhost:9876/metrics\n```\n\n### Key metrics\n\n| Metric                              | Type      | Description                         |\n| ----------------------------------- | --------- | ----------------------------------- |\n| `paparats_search_total`             | Counter   | Search requests by group and method |\n| `paparats_search_duration_seconds`  | Histogram | Search latency                      |\n| `paparats_index_files_total`        | Counter   | Files indexed                       |\n| `paparats_index_chunks_total`       | Counter   | Chunks indexed                      |\n| `paparats_query_cache_hit_rate`     | Gauge     | Query result cache hit rate         |\n| `paparats_embedding_cache_hit_rate` | Gauge     | Embedding cache hit rate            |\n| `paparats_watcher_events_total`     | Counter   | File watcher events                 |\n\n### Prometheus scrape config\n\n```yaml\nscrape_configs:\n  - job_name: paparats\n    scrape_interval: 15s\n    static_configs:\n      - targets: ['localhost:9876']\n```\n\n### Query cache\n\nSearch results are cached in-memory (LRU, default 1000 entries, 5-minute TTL). The cache is automatically invalidated when files change. Configure via environment variables:\n\n- `QUERY_CACHE_MAX_ENTRIES` — max cached queries (default: 1000)\n- `QUERY_CACHE_TTL_MS` — TTL in milliseconds (default: 300000)\n\nCache stats are included in `GET /api/stats` under the `queryCache` field.\n\n---\n\n## Analytics & Observability\n\nPaparats ships with three observability layers that work together:\n\n1. **Prometheus** (`PAPARATS_METRICS=true`, see above) — scrape `/metrics`.\n2. **Local SQLite analytics store** at `~/.paparats/analytics.db` (default ON) — raw search/tool/indexing events. Six MCP tools query it directly: `token_savings_report`, `top_queries`, `cross_project_share`, `retry_rate`, `slowest_searches`, `failed_chunks`.\n3. **OpenTelemetry** (`PAPARATS_OTEL_ENABLED=true` + `OTEL_EXPORTER_OTLP_ENDPOINT`) — spans for every search, MCP tool call, embedding, indexing run, chunking error. Works with Tempo, Jaeger, Honeycomb, Datadog, Grafana Cloud — anything that speaks OTLP/HTTP.\n\n### Operator console (`/ui`)\n\nOpen `http://localhost:9876/ui` for a single-screen dashboard ([see screenshot at top of README](#paparats-mcp)) that visualises the analytics store above: ROI, top / slowest queries, cross-project usage, per-user activity, indexer status, embedding p95/p99, and recent failures. Polls every 5 s, no extra services to run.\n\n- Protect it (optional): `PAPARATS_UI_BASIC_AUTH=user:pass` — applies to `/ui` and `/api/analytics` only; `/mcp` and `/api/search` stay open so agents keep working.\n- Show the screenshot view to anyone without touching real data: `PAPARATS_UI_DEMO=true` (or append `?demo=1` to the URL once).\n\n### Pre-built Grafana dashboard\n\nThe built-in `/ui` covers the current snapshot. For history (latency p99 over weeks, GC trends, CPU under indexing bursts) wire `/metrics` to Prometheus and import [`docs/grafana/paparats.json`](docs/grafana/paparats.json) — 15 panels across four rows: **Traffic & latency**, **Embeddings**, **Indexing**, **Process health**.\n\n```bash\n# 1. Enable Prometheus surface on the server.\nPAPARATS_METRICS=true paparats up   # or set in your docker-compose.yml\n\n# 2. Point your Prometheus at http://<server>:9876/metrics.\n\n# 3. In Grafana: Dashboards → Import → upload docs/grafana/paparats.json\n#    → pick your Prometheus datasource → Import.\n```\n\nThe dashboard uses a `${DS_PROMETHEUS}` variable, so it works with any Prometheus instance (local, Grafana Cloud, Mimir, VictoriaMetrics).\n\n### Sending traces to Elastic APM (or any OTLP backend)\n\nElastic APM Server accepts OpenTelemetry natively since 7.14 — no agent install, no SDK injection. Set four env vars on the paparats container and restart:\n\n```bash\nPAPARATS_OTEL_ENABLED=true\nOTEL_EXPORTER_OTLP_ENDPOINT=https://your-apm-server:8200\nOTEL_EXPORTER_OTLP_HEADERS=Authorization=Bearer <apm-secret-token>\nOTEL_SERVICE_NAME=paparats-mcp\n```\n\nWithin a minute a new service `paparats-mcp` appears in APM → Services. The same env vars work for Tempo, Jaeger, Honeycomb, Datadog, Grafana Cloud Traces — change the endpoint and auth header.\n\n**What gets recorded** — one span per event, with paparats-specific attributes for filtering and grouping:\n\n| Span name                         | Key attributes                                                                       | When                                  |\n| --------------------------------- | ------------------------------------------------------------------------------------ | ------------------------------------- |\n| `paparats.search`                 | `tool`, `group`, `query.hash`, `query.length`, `search.duration_ms`, `result_count`, `cache_hit` | every `search_code` / `find_usages`   |\n| `paparats.get_chunk`              | `chunk_id`, `fetch.radius_lines`, `fetch.duration_ms`, `fetch.found`                 | every `get_chunk` call                |\n| `paparats.mcp.tool`               | `tool`, `tool.duration_ms`, `tool.ok`                                                | every MCP tool invocation             |\n| `paparats.embedding`              | `kind`, `batch_size`, `cache_hits`, `cache_miss`, `duration_ms`, `timeout`           | every embedding request               |\n| `paparats.indexing.run`           | `group`, `project`, `trigger`, `status`, `files_total`, `chunks_total`, `errors_total` | every indexer cycle                   |\n| `paparats.indexing.chunking_error`| `group`, `project`, `file`, `language`, `error_class`                                | per-file chunking failure             |\n\nEvery span also carries `paparats.user`, `paparats.session`, `paparats.client`, `paparats.request_id`, and (when present) `paparats.anchor_project` from the identity headers above — so you can filter APM by user or correlate spans across a single MCP session.\n\n**What this is good for in Elastic APM:**\n\n- **Errors view** — chunking and embedding failures with stacktrace + file/language/error_class context, aggregated by error class.\n- **Transactions** — `paparats.search` becomes a transaction type. Sort by p95/p99/error rate to find the slow workloads. Filter by `paparats.tool=search_code` or `paparats.group=…` to slice by repo.\n- **Custom queries / metrics** — every paparats attribute is indexed. Build APM queries like `paparats.embedding.cache_miss:true AND duration_ms>500` to find slow cache-miss embeddings, or aggregate `paparats.search.result_count` per `paparats.group`.\n- **Log correlation** — if you ship paparats stdout to Elastic via Filebeat, the `trace.id` field links a log line back to its span.\n\n**What this is _not_ — honest caveats:**\n\n- Spans are flat (one event = one span), not parented. Service Map will show `paparats-mcp` as an isolated node; you won't see a \"search → embedding → Qdrant\" waterfall. Use the per-span `duration_ms` attributes for stage timing instead.\n- Outbound HTTP to Qdrant / the embed server is not auto-instrumented — to see those as separate dependencies in APM you'd need to enable `@opentelemetry/instrumentation-http` (planned, not shipped). For now, embedding and search latency live on the existing spans.\n- Per-request token-savings, top queries, and cross-project usage stay in the local SQLite store — they're aggregations, not events. View them in the built-in `/ui` console, not in APM.\n\nFor pure metrics (CPU, GC, RSS, request rates) Elastic Metricbeat or our Prometheus exporter (above) is a better fit than APM.\n\n### Identity attribution\n\nClients (IDE plugins, CLI) can set `X-Paparats-User`, `X-Paparats-Session`, `X-Paparats-Client`, `X-Paparats-Anchor-Project` headers. The header name for `user` is configurable via `PAPARATS_IDENTITY_HEADER` (default `X-Paparats-User`). Missing header → events are attributed to `anonymous`. There is no cryptographic verification — this is for attribution, not access control.\n\n`GET /api/stats` echoes the resolved identity, useful for verifying header propagation:\n\n```bash\ncurl -H 'X-Paparats-User: alice' http://localhost:9876/api/stats | jq .identity\n```\n\n### Token-savings estimators\n\nThree levels, computed from raw events at query-time:\n\n- **Naive baseline** — what a model would have read if it pulled the whole file for each result.\n- **Search-only** — tokens actually returned by `search_code`.\n- **Actually consumed** — tokens that the client subsequently fetched via `get_chunk`. The most honest signal, since it discounts noisy results that were never used.\n\nRun `token_savings_report` from any MCP client connected to `/support/mcp`.\n\n### Cross-project noise\n\nWhen a client passes `X-Paparats-Anchor-Project` (or specifies a single project in the search call), the share of results from _other_ projects in the same group is recorded. Use `cross_project_share` to see how noisy your group's index is for each user.\n\n### Indexer-pipeline visibility\n\n`failed_chunks` aggregates AST parse failures, regex fallbacks, zero-chunk files, and binary skips. `slowest_searches` ranks individual searches by latency.\n\n### Configuration matrix\n\n| Env var                                 | Default                    | Purpose                                                     |\n| --------------------------------------- | -------------------------- | ----------------------------------------------------------- |\n| `PAPARATS_METRICS`                      | `false`                    | Prometheus surface (existing, unchanged)                    |\n| `PAPARATS_ANALYTICS_ENABLED`            | `true`                     | Local SQLite analytics writes                               |\n| `PAPARATS_ANALYTICS_DB_PATH`            | `~/.paparats/analytics.db` | Analytics DB file                                           |\n| `PAPARATS_ANALYTICS_RETENTION_DAYS`     | `90`                       | Daily prune cutoff                                          |\n| `PAPARATS_ANALYTICS_RETENTION_RUN_HOUR` | `3`                        | Hour-of-day for prune (local time)                          |\n| `PAPARATS_IDENTITY_HEADER`              | `X-Paparats-User`          | Header name for user attribution                            |\n| `PAPARATS_LOG_RESULT_FILES`             | `true`                     | If `false`, store NULL for `search_results.file`            |\n| `PAPARATS_LOG_QUERY_TEXT`               | `true`                     | If `false`, store NULL for `search_events.query_text`       |\n| `PAPARATS_REFORMULATION_WINDOW_MS`      | `90000`                    | Reformulation detection window                              |\n| `PAPARATS_TELEMETRY_SAMPLE_RATE`        | `1.0`                      | Sampling rate (errors are always kept)                      |\n| `PAPARATS_OTEL_ENABLED`                 | `false`                    | Enable OTel SDK + OTLP exporter                             |\n| `OTEL_EXPORTER_OTLP_ENDPOINT`           | unset                      | OTLP HTTP endpoint (e.g. `http://localhost:4318/v1/traces`) |\n| `OTEL_EXPORTER_OTLP_HEADERS`            | unset                      | OTLP auth headers (`key=value,key2=value2`)                 |\n| `OTEL_SERVICE_NAME`                     | `paparats-mcp`             | OTel resource attribute                                     |\n| `OTEL_RESOURCE_ATTRIBUTES`              | unset                      | Extra resource attrs (`key=value,key2=value2`)              |\n\n### PII guidance\n\n- File paths and query text are stored locally by default. For shared deployments where paths could leak sensitive info, set `PAPARATS_LOG_RESULT_FILES=false` and/or `PAPARATS_LOG_QUERY_TEXT=false`.\n- OTel spans never carry full query text by default — only `paparats.query.hash` and length.\n\n---\n\n## Architecture\n\n```\npaparats-mcp/\n├── packages/\n│   ├── server/          # MCP server (Docker image: ibaz/paparats-server)\n│   │   ├── src/\n│   │   │   ├── lib.ts                # Public library exports (for programmatic use)\n│   │   │   ├── index.ts              # HTTP server bootstrap + graceful shutdown\n│   │   │   ├── app.ts                # Express app + HTTP API routes\n│   │   │   ├── indexer.ts            # Group-aware indexing, single-parse chunkFile()\n│   │   │   ├── searcher.ts           # Search with query expansion, cache, metrics\n│   │   │   ├── query-expansion.ts    # Abbreviation, case, plural expansion\n│   │   │   ├── task-prefixes.ts      # Jina task prefix detection\n│   │   │   ├── query-cache.ts        # In-memory LRU search result cache\n│   │   │   ├── metrics.ts            # Prometheus metrics (opt-in)\n│   │   │   ├── ast-chunker.ts        # AST-based code chunking (tree-sitter, primary strategy)\n│   │   │   ├── chunker.ts            # Regex-based code chunking (fallback for unsupported languages)\n│   │   │   ├── ast-symbol-extractor.ts # AST-based symbol extraction (module-level only, 11 languages)\n│   │   │   ├── ast-queries.ts        # Tree-sitter S-expression queries per language\n│   │   │   ├── tree-sitter-parser.ts # WASM tree-sitter manager\n│   │   │   ├── symbol-graph.ts       # Cross-chunk symbol edges (calls/called_by/refs)\n│   │   │   ├── embeddings.ts         # llama-server provider + SQLite cache\n│   │   │   ├── config.ts             # .paparats.yml reader + validation\n│   │   │   ├── metadata.ts           # Tag resolution + auto-detection\n│   │   │   ├── metadata-db.ts        # SQLite store for git commits + tickets + symbol edges\n│   │   │   ├── git-metadata.ts       # Git history extraction + chunk mapping\n│   │   │   ├── ticket-extractor.ts   # Jira/GitHub/custom ticket parsing\n│   │   │   ├── mcp-handler.ts        # MCP protocol — dual-mode (coding /mcp + support /support/mcp)\n│   │   │   ├── watcher.ts            # File watcher (chokidar)\n│   │   │   ├── arch/                 # Architectural memory layer (components, decisions, lessons)\n│   │   │   │   ├── types.ts          # ArchComponent, ArchDecision, ArchLesson, ArchWriteResult\n│   │   │   │   ├── collection.ts     # Per-group Qdrant collection (`paparats_<group>_arch`) lifecycle\n│   │   │   │   ├── text-embeddings.ts # qwen3-embedding-0.6b text embedder (1024d, last-pooled, llama-server)\n│   │   │   │   ├── store.ts          # CRUD + server-side similarity gate (cosine 0.85 / 0.70)\n│   │   │   │   └── context.ts        # `arch_context` query — top-N across kinds with age stamps\n│   │   │   └── types.ts              # Shared types\n│   │   └── Dockerfile\n│   ├── indexer/         # Automated repo indexer (Docker image: ibaz/paparats-indexer)\n│   │   ├── src/\n│   │   │   ├── index.ts              # Entry: Express mini-server + cron scheduler\n│   │   │   ├── config-loader.ts      # projects.yml parser + per-repo overrides\n│   │   │   ├── config-watcher.ts     # chokidar watcher for hot-reloading the project list\n│   │   │   ├── repo-manager.ts       # parseReposEnv(), cloneOrPull() using simple-git\n│   │   │   ├── scheduler.ts          # node-cron wrapper\n│   │   │   └── types.ts              # IndexerConfig, RepoConfig, RepoOverrides, IndexerFileConfig\n│   │   └── Dockerfile\n│   ├── embed/           # llama.cpp llama-server + llama-swap, models pre-baked (Docker image: ibaz/paparats-embed)\n│   │   └── Dockerfile\n│   ├── cli/             # CLI tool (npm package: @paparats/cli)\n│   │   └── src/\n│   │       ├── index.ts                    # Commander entry\n│   │       ├── docker-compose-generator.ts # Programmatic YAML generation\n│   │       ├── projects-yml.ts             # projects.yml + install.json read/write\n│   │       └── commands/                   # install, projects (add/remove/list), lifecycle, edit, etc.\n│   └── shared/          # Shared utilities (npm package: @paparats/shared)\n│       └── src/\n│           ├── path-validation.ts    # Path validation\n│           ├── gitignore.ts          # Gitignore parsing\n│           ├── exclude-patterns.ts   # Glob exclude normalization\n│           └── language-excludes.ts  # Language-specific exclude defaults\n└── examples/\n    └── paparats.yml.*   # Config examples per language\n```\n\n---\n\n## Stack\n\n- **Qdrant** — vector database (1 collection per group with `paparats_` prefix for code, plus a separate `paparats_<group>_arch` collection per group for the architectural memory layer; cosine similarity, payload filtering)\n- **Embed server** — llama.cpp `llama-server` + `llama-swap` serving local embeddings via `bge-code-v1` for code (1536d, task-specific prefixes) **and `qwen3-embedding-0.6b` for the architectural memory layer** (1024d, last-pooled). Both Apache-2.0. llama-swap routes by model name; models stay resident by default (`EMBED_TTL=0`), with op",
  "bytes": 60000,
  "sha": "fe4017fa6ed4175821ad6987d27009f9e9a6313f4960412eddf250022f9312d1",
  "repo_slug": "ibazylchuk/paparats-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_ibazylchuk_paparats_mcp_ebd14880/readme"
}