Paparats MCP: Self-Hosted Semantic Code Search
LocalAbout Paparats MCP: Self-Hosted Semantic Code Search
Paparats MCP is a self-hosted semantic code-search server for teams working across multiple repositories. AST-aware indexing, symbol relationships, Git history, and agent-maintained architectural memory provide code and design context.
Key features
- 01AST-aware indexing across 11 languages
- 02Cross-chunk symbol graph
- 03Git history and ticket metadata
- 04Semantic search with query expansion
- 05Agent-maintained architectural memory
Use cases
- 01Search code across multiple repositories
- 02Trace symbol relationships between code chunks
- 03Review architectural decisions and lessons
- 04Search curated documentation through a scoped endpoint
MCP support
Exposes coding and support tools through separate MCP endpoints, plus an optional documentation-only endpoint. MCP clients can connect over Streamable HTTP; legacy SSE routes are also available.
Configuration
Local server config is the same for every client. Copy it into your client’s MCP config file.
Tools
Local servers expose tools only once running on your machine, and the maintainer has not provided a tool list.
Details
From the project README@paparats/server
The MCP server for Paparats MCP — semantic code search over multiple repos with AST-aware chunking, a cross-chunk symbol graph, git-history per chunk, and an agent-maintained architectural memory layer.
Not published to npm. This package is consumed as a library by
@paparats/indexerand shipped as a Docker image:ibaz/paparats-server. End users install through@paparats/cli.
What this package does
Three jobs in one process:
- Indexer. AST-aware chunking (tree-sitter, 11 languages), per-chunk symbol extraction, single-parse pipeline, llama-server embedding with SQLite cache, batched Qdrant upsert. Post-index passes attach git history, ticket references, and cross-chunk symbol edges (calls / called_by / references / referenced_by).
- Searcher. Task-prefixed embedding (
nl2code/code2code/techqa), query expansion for the support tools (abbreviations, case variants, plurals, filler removal), in-memory LRU query cache with per-group invalidation, confidence scoring, token-savings telemetry. - MCP server. Three endpoints —
/mcp(coding),/support/mcp(support) and the opt-in, docs-only/knowledge/mcp(knowledge) — each with its own tool set and system instructions. Streamable HTTP for every client, with legacy SSE routes kept. Plus an agent-maintained architectural memory layer.
Endpoints
http://<host>:9876/health – liveness probe
http://<host>:9876/metrics – Prometheus (PAPARATS_METRICS=true)
http://<host>:9876/ui – operator console
http://<host>:9876/api/analytics/... – analytics REST API (UI backend)
http://<host>:9876/mcp – MCP, coding mode (Streamable HTTP)
http://<host>:9876/sse – MCP, coding mode (SSE / Cursor)
http://<host>:9876/messages – MCP, coding mode (SSE message channel)
http://<host>:9876/support/mcp – MCP, support mode
http://<host>:9876/support/sse – MCP, support mode (SSE)
http://<host>:9876/support/messages – MCP, support mode (SSE message channel)
http://<host>:9876/knowledge/mcp – MCP, knowledge mode (stateless, bearer auth,
POST only; mounted only when configured)
POST /api/index registers a project; POST /api/file-changed and POST /api/file-deleted
update single files.
See docs/tools.md
for the full tool inventory per endpoint.
Knowledge mode
A docs-only MCP endpoint for a consumer in another network — an agent backend behind a
load balancer that may search curated documentation and nothing else. Off unless
PAPARATS_KNOWLEDGE_TOKENS and PAPARATS_KNOWLEDGE_AUDIENCE are both set; one without
the other stops the server at startup.
- Tools:
search_docsandget_documentonly. No glossary block, no resources, no workflow prompts.search_docsreturnsstructuredContentwith anoutputSchema, so a wrapping service can build citations without parsing markdown. - Auth:
Authorization: Bearer <token>, compared in constant time, checked before the request body is read.401+WWW-Authenticate: Bearerotherwise. - Scope: documents labelled with an audience in
PAPARATS_KNOWLEDGE_AUDIENCE(neverinternal), in the projects ofPAPARATS_KNOWLEDGE_PROJECTSwhen set. Both are intersected withPAPARATS_DOCS_AUDIENCE/PAPARATS_PROJECTSwhen those are set. Out-of-scope documents read exactly like missing ones. - Transport: stateless Streamable HTTP — a fresh server and transport per
POST, no session id;GET/DELETEanswer405. 64 kB body limit, 1000-character query cap.
Documents get their audience at index time: frontmatter audience: first, then
docs.audience from .paparats.yml / projects.yml, otherwise internal. An invalid
frontmatter value is logged and stored as internal. See the
documentation search guide.
Set PAPARATS_KNOWLEDGE_PROJECTS — otherwise a stray audience: in some code
repository's README is enough to widen what the endpoint serves.
Expose only that path, POST only. For example with nginx:
location = /knowledge/mcp {
limit_except POST { deny all; }
client_max_body_size 64k;
proxy_pass http://paparats:9876;
proxy_http_version 1.1;
proxy_buffering off; # responses stream as text/event-stream
}
location / {
return 404;
}
Architectural memory
Code search tells the agent what the code does. The arch-memory layer tells it why — and the agent maintains that knowledge itself, across sessions, without you authoring a single doc.
Three card kinds in a separate Qdrant collection per group (paparats_<group>_arch),
embedded with qwen3-embedding-0.6b (1024d, last-pooled):
| Kind | Fields | Idempotency |
|---|---|---|
| Component | name, summary (markdown: Does / Owns / Does not / Touched when), files, neighbours, anchors | By name (structural) |
| Decision | title, context, decision, alternatives_rejected, consequences, scope, supersedes | Server-side similarity gate |
| Lesson | rule, why, when, scope, severity, evidence | Server-side similarity gate |
Similarity gate (cosine on qwen3-embedding-0.6b):
>= 0.85→ duplicate. Decisions are refused so the agent must reconcile or supersede. Lessons bumpupdatedAton the existing card (Reflexion "rule confirmed").0.70 – 0.85→ similar. Nothing is written; the agent should refine wording or passsupersedes.< 0.70→ accepted as a new card.
supersedes bypasses the gate and marks the prior decision status=superseded so
it disappears from default search but stays in history.
Reading via arch_context accepts min_score (default 0.45). Each returned
card carries an updated N ago stamp and the cosine score so the agent can detect
stale or low-confidence hits.
Bootstrap with init_arch_memory — a workflow prompt analogous to /init in
Claude Code. Walks the repo, identifies 8-20 components by domain boundary, writes
the cards. Followed by audit_architecture (sweep stale cards) and
record_lesson_from_correction (turn user corrections into structured lessons).
MCP resources:
arch://schema— full card-schema reference.arch://stats/{group}— live counts (total / by kind / by status) and oldest/newestupdatedAt.
Prometheus metrics (PAPARATS_METRICS=true):
paparats_arch_context_calls_total{group}— counterpaparats_arch_write_total{kind, status}— counter labelled by gate outcomepaparats_arch_search_score— histogram of cosine scores fromarch_contextpaparats_arch_collection_size{group, kind, status}— gauge
arch_context is read-only on both MCP endpoints. arch_record_* is
support-only — recording belongs to the architectural-review workflow.
Module map
| Module | Responsibility |
|---|---|
types.ts | Shared interfaces |
lib.ts | Public library entry point (re-exports for programmatic use) |
config.ts | .paparats.yml reader, 11 language profiles, autoProjectConfig() |
app.ts | Express app factory, HTTP API routes, request wiring |
index.ts | Server bootstrap — starts HTTP server, wires components, graceful shutdown |
ast-chunker.ts | AST-based chunking via tree-sitter |
chunker.ts | Regex chunking fallback (4 strategies) |
ast-symbol-extractor.ts | AST-based per-chunk symbol extraction |
ast-queries.ts | Tree-sitter S-expression queries per language |
tree-sitter-parser.ts | WASM tree-sitter manager — lazy grammar loading |
symbol-graph.ts | Cross-chunk symbol edges |
embeddings.ts | llama-server provider + SQLite cache + cached wrapper |
indexer.ts | Group-aware Qdrant indexing — createQdrantClient(), toCollectionName(), single-parse chunkFile() |
searcher.ts | Vector search with query expansion, cache, metrics |
query-expansion.ts | Abbreviation / case / plural / filler expansion |
task-prefixes.ts | Query-type detection + bge-code-v1 / Qwen3 query instructions |
query-cache.ts | In-memory LRU cache with TTL and per-group invalidation |
metrics.ts | Prometheus metrics (prom-client) + NoOpMetrics fallback |
metadata.ts | Tag resolution + auto-detection from directory structure |
metadata-db.ts | SQLite store for git commits, tickets, symbol edges |
git-metadata.ts | Git history extraction — diff-hunk mapping to chunks |
ticket-extractor.ts | Jira / GitHub / custom ticket parsing |
mcp-handler.ts | MCP protocol — coding, support and knowledge endpoints with isolated tool sets and instructions |
knowledge-mode.ts | Knowledge mode — env parsing, bearer auth, body limit, scope intersection |
arch/types.ts | Architectural-memory types |
arch/collection.ts | Per-group paparats_<group>_arch collection lifecycle |
arch/text-embeddings.ts | qwen3-embedding-0.6b text embedder via llama-server |
arch/store.ts | CRUD + server-side similarity gate + stats() aggregation |
arch/context.ts | arch_context query — top-N across kinds with min_score and age stamps |
Configuration (env vars)
| Variable | Default | Notes |
|---|---|---|
PORT | 9876 | HTTP port |
QDRANT_URL | http://localhost:6333 | Qdrant endpoint (https → port 443, http → 6333) |
QDRANT_API_KEY | — | Qdrant Cloud API key |
EMBED_URL | http://127.0.0.1:18434 | Embed server endpoint (llama-swap; internal port 8080, host port 18434 in the generated stack) |
EMBED_BATCH_SIZE | — | Embeddings per llama-server call |
EMBED_TTL | 0 | Idle seconds before a model unloads; 0 = never (default) |
EMBEDDING_PROVIDER | llama | Code embedding provider: llama | openai | voyage |
EMBEDDING_MODEL | bge-code-v1 | Code embedding model |
EMBEDDING_DIMENSIONS | 1536 | Code embedding dimensions |
TEXT_EMBEDDING_PROVIDER | llama | llama or openai for the arch layer |
TEXT_EMBEDDING_MODEL | qwen3-embedding-0.6b | Arch-layer text embedding model |
TEXT_EMBEDDING_DIMENSIONS | 1024 | Arch-layer text embedding dimensions |
PAPARATS_METRICS | false | Set to true to expose /metrics |
PAPARATS_PROJECTS | — | Comma-separated allow-list of project names |
PAPARATS_DOCS_AUDIENCE | — | Comma-separated audience ceiling for search_docs / get_document on the coding and support endpoints |
PAPARATS_KNOWLEDGE_TOKENS | — | Comma-separated bearer tokens (≥ 16 characters) for /knowledge/mcp. Set together with PAPARATS_KNOWLEDGE_AUDIENCE, or neither |
PAPARATS_KNOWLEDGE_AUDIENCE | — | Audience ceiling of /knowledge/mcp; internal is refused |
PAPARATS_KNOWLEDGE_PROJECTS | — | Optional project allowlist of /knowledge/mcp — recommended |
PAPARATS_UI_BASIC_AUTH | — | user:pass for /ui and /api/analytics/...; a malformed value stops startup |
PAPARATS_CORS_ORIGINS | — | Comma-separated exact origins (e.g. https://app.example.com) browsers may call the server from. Unset = no CORS headers, and any request whose Origin is neither this server nor listed gets 403. Requests without Origin (MCP clients, curl) are unaffected |
OTEL_* | — | Standard OpenTelemetry env vars (Tempo / Honeycomb / Datadog / Elastic APM) |
Programmatic use (library)
The package.json exports map points at dist/lib.js, not the server bootstrap.
Importing @paparats/server does not start the HTTP server — @paparats/indexer
uses this to embed the Indexer class as a library:
import { Indexer, createQdrantClient } from '@paparats/server';
const qdrant = createQdrantClient({ url: process.env.QDRANT_URL! });
const indexer = new Indexer({ qdrant, /* ... */ });
await indexer.indexProject(group, project);
Docker
docker pull ibaz/paparats-server:latest
docker run --rm -p 9876:9876 \
-e QDRANT_URL=http://qdrant:6333 \
-e EMBED_URL=http://embed:8080 \
ibaz/paparats-server:latest
See examples/docker-compose for a complete
stack (Qdrant + embed server + server + indexer).
Development
yarn install
yarn workspace @paparats/server build
yarn workspace @paparats/server test
yarn workspace @paparats/server typecheck
Related packages
| Package | Purpose |
|---|---|
| @paparats/cli | One-command installer, project management, search |
| @paparats/indexer | Automated multi-repo indexer (uses this as a library) |
| @paparats/shared | Shared utilities (path validation, gitignore, excludes) |
| ibaz/paparats-embed | llama.cpp llama-server + llama-swap with pre-baked bge-code-v1 and qwen3-embedding-0.6b |
License
MIT
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.