Skip to content
Conduit

mcp server

pinakes-rs

Self-hosted knowledge service for AI agents: an LLM-maintained wiki and a business graph, searched in milliseconds, far faster than RAG, with no LLM or database on the read path. HTTP and MCP.

chenkezhen480/pinakes-rscommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

This project does not publish an install method we can turn into a config. Follow the README instead.

README

synced Oct 17, 2026

Pinakes

A self-hosted knowledge service for AI agents: a wiki an LLM librarian keeps current, a business graph beside it, and one search entry that answers in milliseconds.

CI License: MIT OR Apache-2.0 Rust 1.98+ MCP

English · 简体中文

Pinakes is named after the catalogue Callimachus wrote for the Library of Alexandria. It works like a library: the wiki is the collection, and an LLM acts as the librarian that files, merges and checks everything that comes in. It is built around two things.

1. Retrieval in milliseconds

The expensive thinking happens once, when knowledge is added (the LLM wiki pattern): the librarian writes the synthesis into pages ahead of time. So a read is only a lookup. No generative model and no database query on the read path: name matching, keyword search, graph walks and, when the agent asks, a vector search (whose only model is the small query encoder), all over an in-memory snapshot. A classic RAG retriever is quick too, but it hands back raw chunks that an LLM has to synthesize on every question; here that synthesis is already in the pages, and the agent answers from them.

Measured on a laptop with 4 compute threads (medians; method and every number in docs/performance.md):

Wiki sizeKeyword searchVector searchSemantic search, query embedding includedOne page changed (write)
10,000 pages2.3 ms6 ms (exact scan)20 ms0.16 s
100,000 pages13.3 ms¹about 3 ms (HNSW)37.6 ms¹1.29 s

¹ Measured while Windows Defender kept two to three cores busy scanning the files the benchmark had just written; keyword search measured 10 to 13 ms across runs. With the query embedding timed alone (14 to 16 ms), a semantic search on an idle machine would take about 18 to 20 ms: an estimate, not a measured run.

The benchmark's page vectors are stand-ins, so these rows measure speed, not search quality. Quality was measured separately on real vectors from the service's 8-bit model: the HNSW index finds what the exact scan finds, recall@10 0.993 on 10,881 rows (3,093 pages). Graph walks take microseconds, and name matching under 0.3 ms at every size. Most of a semantic search is embedding the query on the CPU: 14 ms with the model's weights in 8 bits, which the service quantizes itself after the download (same answers on the test set as the full model, at half the time and memory). What makes it fast:

  • Compiled at write time, looked up at read time: one search returns answer-shaped pages, related memories and real graph edges together, in three slots, with routes to follow.
  • Everything a search touches is in memory: the tantivy index (memory-mapped), page vectors as int8 (a quarter of the memory, compared with AVX2), the graph as adjacency lists.
  • Our own HNSW index over the int8 vectors of large tenants, one per search slot, updated on every write and deletion.
  • Writes change only what changed: incremental snapshots, so a 100,000-page wiki takes a new page in about a second.
  • Rust, with one bounded compute pool: no garbage-collection pauses, predictable latency.

Compared end to end through an agent

Pinakes was compared with Mem0, Graphiti, LightRAG and Hindsight end to end through the Pi agent: Pi 1.1 with its own MCP client, each system behind its own read tools, DeepSeek V4.1 Flash as the model of the agent and of every system, the 212 questions of the six-scenario test set (Chinese and English, with users, groups and hidden graph spaces), each answer graded blind by an LLM judge (2 answered, 1 partly, 0 not).

SystemSearch time (median)Answered fullyMean score (0–2)Text handed to the agent per questionTime per question
Pinakes3.4 ms (keywords), 18.9 ms (with meaning)99.1%1.993.4k characters3.9 s
Hindsight 0.10.31,911 ms95.3%1.9331.9k characters15.7 s
Mem0 2.2.1254 ms92.9%1.884.0k characters4.3 s
Files + grep (Pi's own tools)–92.5%1.863.0k characters4.5 s
LightRAG 1.5.71,076 ms86.3%1.7637.9k characters5.7 s
Graphiti 0.30.2186 ms62.7%1.465.0k characters3.9 s

The other systems embed queries with Qwen3-Embedding-0.6B here (about 120 ms a query on this CPU); with jina-embeddings-v2-base-zh for every system, as Pinakes uses, they searched in 80 ms (Graphiti), 106 ms (Mem0) and 854 ms (LightRAG, which calls the LLM on every search). At 10,000 pages Pinakes still answers in 4.3 ms by keywords and 25.5 ms with meaning, while Mem0 took 491 ms and Graphiti 263 ms. No system leaked another user's, group's or hidden space's content.

2. Knowledge that hosts and manages itself

Self-hosted. Pinakes is one Rust binary, with its web console built in, on your own machine or server; it reads Word, Excel, PowerPoint, PDF and web pages itself (an optional converter container adds the old binary Office formats). Your knowledge stays in files you can read: the wiki is plain Markdown, each tenant has one SQLite file, and graph data sits in SQLite or Neo4j. The embedding model runs locally. The only outside call is the librarian's model at write time, and that can be any Anthropic-compatible or OpenAI-compatible endpoint, a local model too (Ollama, vLLM). Tenants, groups and users are isolated; deletion is hard, checked byte by byte.

Self-managing. Nobody has to curate the wiki by hand; people step in only to approve:

flowchart LR
    In["Agents and systems add knowledge<br/>(5 kinds, documents)"] --> Lib["Librarian files it:<br/>merges into existing pages,<br/>verifies every change"]
    Lib --> Wiki[("Wiki + graph")]
    Wiki --> Search["Agents search<br/>(traces kept)"]
    Search --> Lint["Lint: contradictions, stale claims,<br/>duplicates, broken links, weak searches"]
    Lint --> Fix["Fixes and gap-filling proposals<br/>(acquisition from trusted sites,<br/>agents' graph drafts)"]
    Fix --> Review{"A person approves"}
    Review --> Lib
  • Filing, not piling up: new content updates the pages it belongs to instead of adding another document; a document is read like a person reads it, one concept at a time, and a new version updates only what changed. Edits the evidence doesn't support are held for review.
  • Documents of any length: the librarian reads a document window by window, as a person works through a book, and files each concept as it goes, so a manual of thousands of pages is read as fully as a one-page note. The model's context window never limits it; the reading is saved after every window, and one that stops resumes where it was (uploads up to 20 MB).
  • Upkeep: lint finds contradictions, stale claims, duplicates, broken links, unanchored memories and keywords searches keep missing, and proposes fixes.
  • Growing on its own: acquisition reads the sites a tenant trusts (its allowlist only, no search engine), matches pages to the gaps lint found, screens them for prompt injection, and proposes them; a person approves before anything is filed.
  • Agents take part: they record experience, decisions and preferences as they work, and propose changes to the graph as one reviewable draft per space.

How it works

Pinakes keeps three layers apart, and only the librarian writes the middle one:

LayerWhat it isWho writes it
Raw sourcesTask records, decision records, documents, schema definitions, preference notes; kept as they arrived, never changedThe agents and systems that call Pinakes
The wikiMarkdown pages per kind and audience, linked to each other and anchored to graph entitiesThe librarian (an LLM): it plans each change, writes it, and another step verifies it against the sources
Graph dataNodes and edges in graph spaces, checked against their schemaThe host system, or agents' drafts once a person approves them; never an LLM

Knowledge comes in through one entry per kind, and goes out through one search entry:

KindAdded withComes back in
Agent experience: what worked, pitfalls and fixes/ingest/experiencememories
User decisions and their outcomes/ingest/decisionmemories
A decision, a lesson or both, as an agent recorded it (split by the librarian: decisions for the user, lessons for the team)/ingest/note, MCP record_decision_or_lessonmemories
User preferences/ingest/preference, MCP record_preferencememories
Vertical expertise: documents and manuals/ingest/expertise (JSON or a file)knowledge
The business graph: schemas, spaces, nodes, edges/ingest/graph/schema, /space, /dataknowledge (the schema's description) and relations (the edges)

A search (/search, MCP search) brings back three slots at once: memories, knowledge and relations. Each hit says how it matched and carries a route: read the page, open the source it cites, walk the graph from a node. The service returns signals, not decisions: the agent chooses keywords or meaning, what to open and what to follow (A-RAG), and writes the answer itself.

Classic RAGPinakes
When the thinking happenson every questiononce, when knowledge is added
What a search findsraw text chunkssynthesized pages, related memories, real graph edges
Model work per questionan LLM synthesizes the retrieved chunksthe agent answers from pages already synthesized; the search runs no generative model
Who decides how to searchthe pipelinethe agent
How knowledge stays currentre-index the documentsthe librarian merges, lint checks, people approve

Also

  • A business graph described by its schema: node and relation types, node properties, any number of graph spaces (shared, private or restricted).
  • Chinese and English from day one: jieba tokenizing, a name matcher, and a local Chinese–English embedding model for cross-language search.
  • Built for agents: an MCP server with seven tools whose descriptions teach the retrieval strategy, beside the HTTP API.

Architecture

flowchart LR
    Host[Host system] -- "ingest: 5 kinds" --> API
    Agent[AI agent] -- "MCP / HTTP" --> API
    subgraph Pinakes
        API[HTTP API + MCP server] --> Search["Search: names, BM25, vectors, graph walk"]
        API --> Queue[Per-tenant write queue]
        Queue --> Librarian[LLM librarian: plan, write, verify]
        Librarian --> Wiki[(Wiki: Markdown pages)]
        Queue --> Graph[(Graph: SQLite or Neo4j)]
        Wiki --> Mem[In-memory snapshot: tantivy, vectors, HNSW]
        Graph --> Mem
        Search --> Mem
    end
    Librarian -. "writes only" .-> LLM[(LLM: Anthropic API or OpenAI-compatible)]

Reads never call the LLM or query a database: they search the in-memory snapshot. Writes go through each tenant's queue, where the librarian files them.

Running modes

Each is one line of config.toml (or the compose profile named); the defaults are marked.

ChoiceOptionsWhen
Deployment ([identity] deployment)multi_tenant (default)A platform: an admin creates tenants (POST /admin/tenants), each gets an API key; the host sends the user in X-User-Id, the groups in X-User-Groups, and who a write is for in X-Audience
personalOne person: the default tenant 000000 is created on first start, its key written to <data>/personal-api-key.txt; calls without a user act as admin
Identity ([identity] mode)api_key (default)Calls come from a trusted backend, which names the user
jwtCalls come straight from users' machines (an agent with MCP): the host signs a token holding tenant, user and groups; Pinakes checks it with the host's public key
Graph store ([graph] backend)sqlite (default)graph.sqlite in each tenant's folder: nothing else to run
neo4jThe compose profile neo4j; move data between the two with pinakes-migrate-graph
Meaning search ([embedding] enabled)off (default)Keyword search only: nothing is downloaded; semantic and both say how to turn it on
on (the console's switch, or the config line)Downloads jina-embeddings-v2-base-zh (about 640 MB) into <data>/models once, in the background, keeps an 8-bit copy of it (316 MB) in its place, and embeds every page; no restart. HF_ENDPOINT names a mirror
Default search mode ([search] default_mode)keyword (default), semantic, both, autoWhen a search doesn't say; auto tries keywords first and falls back to meaning when coverage is weak
Document conversionbuilt in (default)Markdown, text, HTML, Word (.docx), spreadsheets (.xlsx, .csv), PowerPoint (.pptx) and PDF (its text layer, read in a process of its own; a scan is transcribed page by page by the model, which must take images, such as DeepSeek's deepseek-flash), read by the service itself
plus MarkItDown ([converter] url, compose profile markitdown)The old binary Office formats (.doc, .xls, .ppt); without it they are refused with what to do
Acquisition ([acquisition] enabled)off (default)On for the deployment, then per tenant with its allowlist (PUT /admin/tenants/{id}/acquisition)
Test chat (PINAKES_DEV=1)off (default)The console's Search page gets a chat tab that answers with the read tools an agent has

How to run it: Docker Compose (docker compose up -d --build starts the service alone; add --profile markitdown for the old Office formats and --profile neo4j for Neo4j), or from source (cargo run --release). Either way the web console is at http://127.0.0.1:8080/ui: search, the wiki rendered, adding knowledge, the graph drawn, review, lint, acquisition, tenants and every setting explained, laid out as a 3D hall of the Library of Alexandria whose sky follows your clock (or as a plain list, a click away). Sign in as root with the password 1234 and change it in Settings, Environment (or set PINAKES_ADMIN_PASSWORD) before others can reach the service. The browser stays signed in until you sign out. Secrets never go in config.toml: PINAKES_ADMIN_PASSWORD (the console), PINAKES_ADMIN_KEY (scripts calling the admin routes), NEO4J_PASSWORD, and the model's key in model.conf (git-ignored) or the variable it names.

Quick start

With Docker:

cp model.conf.example model.conf      # set the librarian's model and key (DeepSeek's Anthropic-compatible endpoint by default)
export PINAKES_ADMIN_KEY=$(openssl rand -hex 24)
export PINAKES_ADMIN_PASSWORD='choose one'   # the console's sign-in as root
docker compose up -d --build          # the console: http://127.0.0.1:8080/ui

# create a tenant and get its API key
curl -s -X POST http://127.0.0.1:8080/admin/tenants \
  -H "Authorization: Bearer $PINAKES_ADMIN_KEY" -H "Content-Type: application/json" \
  -d '{"tenant_id": "acme"}'

From source (Rust 1.98 or later):

cp model.conf.example model.conf
cargo run --release                   # reads config.toml; binds 127.0.0.1:8080

From crates.io (the console is built in):

cargo install pinakes                 # pinakes, pinakes-pdf, pinakes-replay, pinakes-migrate-graph
mkdir pinakes && cd pinakes           # the service keeps its data in ./data
# write model.conf here, as model.conf.example in this repository shows
pinakes                               # without a config.toml it runs on the defaults

For one person, set deployment = "personal" in config.toml: the service creates a default tenant on first start and writes its key to <data>/personal-api-key.txt. To try it with data, cargo run --example load_testset -- ./data loads six sample business scenarios.

Connect an agent over MCP

Point any MCP client that speaks Streamable HTTP at http://127.0.0.1:8080/mcp with the tenant's key in Authorization: Bearer <key> (and the user in X-User-Id). The tools: search, read, graph, path, record_decision_or_lesson, record_preference and propose_graph_change.

Search over HTTP

curl -s -X POST http://127.0.0.1:8080/search \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{"query": "订单1042的下单客户", "keywords": ["订单1042", "下单"]}'

Documentation

  • docs/guide.md: how it works and how to use it: concepts, identity, adding knowledge, the graph, searching, the librarian, lint and acquisition, storage, the HTTP API and the MCP tools.
  • docs/configuration.md: every configuration field.
  • docs/performance.md: speed and memory measurements, and how to take them.
  • CONTRIBUTING.md: building, testing and the working rules.
  • CHANGELOG.md: what each milestone brought.

Status

Milestones M1 to M10 are built: isolation, search, the graph, the librarian, meaning search, speed, MCP and packaging, lint and deletion, acquisition and the HNSW index, and the web console with built-in document conversion and vector search off by default. Pinakes is pre-1.0: APIs may still change.

Contributing

Issues and pull requests are welcome; please read CONTRIBUTING.md and the Code of Conduct. Report security issues privately, as SECURITY.md describes.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

Third-party components

Besides the Rust crates it is built from, Pinakes downloads or runs beside these components. None of them is part of this repository; each keeps its own license.

ComponentUsed forHow it arrivesLicense
jina-embeddings-v2-base-zhpage and query vectorsdownloaded from Hugging Face on first use into <data>/modelsApache-2.0
ONNX Runtimerunning the embedding modelprebuilt library linked into the binary at build time (through ort)MIT
MarkItDownconverting old Office files (optional)the converter container image (markitdown compose profile)MIT
Pythonthe optional converter's runtimethe python:3.12-slim base imagePSF-2.0
Svelte and d3-forcethe web console and its graph layoutcompiled into ui/dist, embedded in the binaryMIT, ISC
three.jsthe console's 3D hallcompiled into ui/dist (its own chunk), embedded in the binaryMIT
jieba dictionaryChinese word segmentationbundled in the jieba-rs crateMIT
Neo4j Community Editionthe optional graph backendthe official neo4j image (neo4j compose profile)GPL-3.0 (a separate server, not linked)
Debianthe runtime imagethe debian:trixie-slim base imagevarious free licenses

The Rust crates, with their versions in Cargo.lock, are all under permissive licenses (MIT, Apache-2.0, BSD, ISC, Zlib, Unicode-3.0, CDLA-Permissive-2.0, and option-ext under MPL-2.0); cargo deny (see deny.toml) checks this in CI.

↑↓ navigate openesc close