Content
# KnowledgeOS
> A knowledge system for AI agents that gets smarter as it works.
Documentation is primary. Code is an artifact.
## Idea
Traditional development looks like this:
```
Thought → Code
```
KnowledgeOS changes this order:
```
Thought → Document (source of truth) → AI Agent → Code
```
The AI agent doesn't just perform tasks - it reads project documentation before each action and updates it after completing the task. If it encounters a non-standard case, it writes a guideline. Next time, it will find it itself.
If you want to change behavior, edit the document, not the code.
## Architecture
```
┌──────────────────────────────────────────────────────────┐
│ Operator of AI Agent │
│ sets tasks · reviews · edits docs │
└──────────┬──────────────────────────────┬────────────────┘
│ task │ review
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Markdown Vault │◄────────────│ AI Agent │
│ │ reads / │ (OpenCode) │
│ any .md │ edits │ │
│ any structure │ │ 1. search_docs │
│ any YAML │ │ 2. reads docs │
│ frontmatter │ │ 3. writes code │
│ │ │ 4. write_doc │
│ [[wikilinks]] │ │ 5. update_doc │
└────────┬─────────┘ └────────┬─────────┘
│ files │ search_docs()
▼ ▼
┌──────────────────────────────────────────────────────────┐
│ MCP Server (Kotlin) │
│ │
│ [Metadata filter] │
│ BM25 ──────┐ │
│ Vector ────┼──► RRF Fusion ──► Wikilinks ──► Rerank ──► chunks │
│ │ │
└──────────────────────────────────────────────────────────┘
│
▼
┌─────────────┐
│ Codebase │
│ (artifact) │
└─────────────┘
```
### Three Components
**Vault** — the brain of the system. The single source of truth. An arbitrary hierarchy of `.md` files with arbitrary YAML frontmatter, linked through `[[wikilinks]]`. KnowledgeOS does not prescribe folder structure or frontmatter scheme. Optional recommendations are in [docs/STRUCTURE-RECOMMENDATIONS.md](docs/STRUCTURE-RECOMMENDATIONS.md).
**MCP Server** — the nervous system. Indexes the vault, processes agent requests. The BM25 index is persisted to the local disk, and the vector index is stored in ChromaDB. The server can be restarted without full re-indexing. The graph of wikilinks is maintained in memory and updated incrementally through a file watcher; wikilinks are expanded on-demand when a document is retrieved — if a chunk contains [[links]], the server automatically loads related documents. Implemented in Kotlin.
**AI Agent (OpenCode)** — the executor. The only one who writes code. Reads documentation before each action. After — updates documents and creates new ones through tools: `search_docs`, `write_doc`, `update_doc`, `get_doc`, `list_docs`.
## Vault Structure
KnowledgeOS does not prescribe a vault structure — an arbitrary hierarchy of `.md` files with any YAML frontmatter is indexed. Any frontmatter fields are indexed as filterable metadata (`fm.<key>`), and any folder is a valid location.
### Optional Recommended Structure
If you want a ready-made template optimized for AI agents, see [docs/STRUCTURE-RECOMMENDATIONS.md](docs/STRUCTURE-RECOMMENDATIONS.md). A brief example:
```
vault/
├── _index.md # entry point (optional)
├── rules/ # short rules: "agent MUST ..."
├── patterns/ # reusable how-to with code examples
├── decisions/ # ADR — why we chose X
├── reference/ # API/schemes/configs
└── domain/ # business knowledge
```
Minimal useful frontmatter:
```yaml
---
title: Database transaction rules
description: When and how to wrap DB mutations in transactions.
kind: rule
tags: [db, sql, transactions]
updated: 2026-05
---
```
The current vault in the repository (`vault/`) is an example of a different structure (Diátaxis with `concepts/`, `guidelines/`, etc.); it works, but this is not the "only correct" way. Any other structure also works.
### Wikilinks as a Knowledge Graph
Connections between documents are explicitly defined through `[[document-title]]`. This is the knowledge graph — without LLM extraction, without errors, with full control.
```markdown
# Transaction Work Rules
Always wrap mutations in a transaction. See [[db-schema]].
On errors → [[error-handling]].
Repository pattern: [[repository-pattern]].
```
When searching for `guidelines/database.md`, the server automatically pulls in all linked documents — the agent receives a related cluster, not an isolated chunk.
## MCP Server
### Agent Tools
The agent sees five tools:
```
search_docs(
query: String, // what we're looking for
filters: Map<String, String>? = null, // optional filter by any frontmatter fields
// e.g., {"kind": "rule", "tags": "db"}
) → List<Chunk>
write_doc(
path: String, // path relative to vault, must end with .md
content: String, // markdown (without frontmatter)
frontmatter: Map<String, JsonElement>? = null, // optional arbitrary YAML
) → { status, path }
update_doc(
path: String, // path to existing document
content: String, // new content
preserve_frontmatter: Boolean = false,// preserve old frontmatter if new one is absent
) → { status, path }
get_doc(path: String) → markdown
list_docs(directory: String? = null) → List<String>
```
**Filters are arbitrary.** `filters` compares values with any frontmatter fields of your documents. If you have `kind: rule` — filter by `{"kind": "rule"}`. If `genre: concept` (old scheme) — `{"genre": "concept"}`. KnowledgeOS does not know in advance what fields you have.
### Retrieval Pipeline
```
query
│
├─► [Metadata filter] optional narrowing by any frontmatter fields
│
├─► [BM25] exact terms, function names, libs
│
├─► [Vector search] semantically similar chunks
│
├─► [RRF Fusion] combine results of both retrievers
│
├─► [Wikilink expansion] pull in [[related]] documents
│
└─► [Reranker] final selection of top-K
```
**BM25** — exact match, works without GPU, fast, good for code.
**Vector** — semantics, catches synonyms and rephrasings.
**Wikilinks** — graph expansion, pulls in related context automatically.
**Reranker** — cross-encoder re-ranks the combined result, leaves only relevant.
### Indexing
File watcher monitors the vault. When a file changes — incremental update of BM25, vector index, and wikilinks graph in memory. The wikilinks graph is not persisted to disk: on each document request, the server expands [[links]] on-demand and loads related documents. Contextual enrichment: before indexing, each chunk is enriched with context through LLM — increases retrieval accuracy.
## How the Agent Gets Smarter
```
1. Agent receives task
↓
2. search_docs("task") → reads relevant documents
↓
3. Performs task according to rules
↓
4. Encounters something new or non-standard
↓
5. write_doc("rules/new-rule.md", ...) → new .md along the chosen path
update_doc(...) → updates existing document, adds [[link]]
↓
6. Next similar task → agent finds this knowledge itself
```
The loop is closed. The agent accumulates knowledge automatically; a person focuses on concepts, architectural decisions, and reviews.
## Technical Stack
| Component | Technology |
|---|---|
| MCP Server | [kotlin-sdk](https://github.com/modelcontextprotocol/kotlin-sdk) |
| BM25 | Apache Lucene (Kotlin DSL) |
| Vector store | ChromaDB |
| Deployment | Docker Compose / Docker Desktop |
| Embeddings + Enrichment | DeepSeek `deepseek/deepseek-v4-flash` |
| Reranker | `cross-encoder/ms-marco-MiniLM-L-6-v2` (ONNX) |
| File watching | Java WatchService |
| Frontmatter | Kaml (Kotlin YAML) |
| Wikilink parser | Regex + `[[...]]` |
| AI Agent | OpenCode |
| Vault | Obsidian |
## Quick Start
Without cloning the repository — only Docker.
### 1. Create Files
```
my-project/
├── knowledge-docker-compose.yml
├── .env
└── vault/ ← put documentation here
```
**`knowledge-docker-compose.yml`**
```yaml
services:
chromadb:
image: chromadb/chroma
volumes:
- chroma-data:/chroma/chroma
restart: unless-stopped
environment:
- ALLOW_RESET=true
mcp:
image: aequicor/knowledgeos:latest
ports:
- "8081:8080"
env_file: .env
environment:
- CHROMA_URL=http://chromadb:8000
- CHROMA_COLLECTION=vault_my_project
- BM25_INDEX_PATH=/app/index
- VAULT_PATH=/vault
volumes:
- ./vault:/vault
- bm25-index:/app/index
- bm25-models:/app/model # ONNX models (download once, persist across rebuilds)
depends_on:
chromadb:
condition: service_started
restart: unless-stopped
volumes:
chroma-data:
bm25-index:
bm25-models:
```
**`.env`**
```dotenv
LLM_API_KEY=sk-...
LLM_BASE_URL=https://openrouter.ai/api/v1
```
> **Important:** add `.env` to `.gitignore` to prevent the key from being committed to the repository.
### 2. Run
```bash
docker compose -f knowledge-docker-compose.yml up -d
```
The MCP server will be available at `http://localhost:8081/mcp`.
### 3. Update to New Version
```bash
docker compose -f knowledge-docker-compose.yml pull
docker compose -f knowledge-docker-compose.yml up -d
```
---
## Multiple Projects
Each project is a separate MCP container with its own vault, indexes, and port.
Only ChromaDB is shared (one instance, different collections).
### Structure
```
knowledgeos/
├── docker-compose.yml # in git — only ChromaDB
├── docker-compose.local.example.yml # in git — template for copying
├── docker-compose.local.yml # in .gitignore — your project settings
├── .env # in .gitignore — common variables (API keys, ...)
├── vaults/ # vault for each project
│ ├── my-app/ # vault for project "my-app"
│ │ ├── _INDEX.md
│ │ ├── concepts/
│ │ ├── guidelines/
│ │ └── ...
│ └── another-project/ # vault for project "another-project"
│ └── ...
```
### Configure New Project
Add a block to `docker-compose.local.yml`:
```yaml
services:
mcp-new-project:
image: aequicor/knowledgeos:latest
ports:
- "8083:8080" # unique port
env_file: .env
environment:
- CHROMA_URL=http://chromadb:8000
- CHROMA_COLLECTION=vault_new_project # unique collection
- BM25_INDEX_PATH=/app/index
- VAULT_PATH=/vault
volumes:
- ./vaults/new-project:/vault # unique vault folder
- bm25-new-project:/app/index # unique volume for BM25
- bm25-models:/app/model # ONNX models
depends_on:
chromadb:
condition: service_started
restart: unless-stopped
volumes:
bm25-new-project:
bm25-models:
```
### What Should be Unique for Each Project
| Parameter | Why |
|---|---|
| `ports` (host port) | So each MCP is available on its own port |
| `CHROMA_COLLECTION` | Project vector indexes are not mixed |
| `volumes` (vault path) | Each project has its own vault |
| `volumes` (BM25 named volume) | BM25 indexes do not intersect |
| `service name` | Unique Docker service name |
### Connecting Multiple Projects in OpenCode
```json
{
"mcpServers": {
"knowledge-my-app": {
"type": "remote",
"url": "http://localhost:8081/mcp"
}
}
}
```
---
## Configuration
### `.env` — Common Settings for All Projects
```dotenv
# .env.example
# OpenAI-compatible LLM (enrichment only — OpenRouter, DeepSeek, OpenAI, etc.)
LLM_API_KEY=sk-...
LLM_BASE_URL=https://openrouter.ai/api/v1
EMBEDDINGS_MODEL=deepseek/deepseek-v4-flash
ENRICHMENT_MODEL=deepseek/deepseek-v4-flash
ENRICHMENT_ENABLED=true
# Retrieval
RETRIEVAL_TOP_K=5
RETRIEVAL_CHUNK_SIZE=512
RETRIEVAL_CHUNK_OVERLAP=50
RETRIEVAL_WIKILINKS_HOPS=1
RETRIEVAL_RERANKER_ENABLED=true
RETRIEVAL_RERANKER_MODEL=cross-encoder/ms-marco-MiniLM-L-6-v2
# Vault watching
VAULT_WATCH=true
# ChromaDB
CHROMA_URL=http://chromadb:8000
CHROMA_PORT=8000
```
> **Important:** add `.env` to `.gitignore` to prevent the `LLM_API_KEY` from being committed to the repository.
### Logging
By default, the server outputs logs at the `INFO` level. For diagnostics, the `DEBUG` level is available, which prints the parameters of each tool call and the truncated result (up to 500 characters).
```dotenv
LOG_LEVEL=DEBUG
```
Locally:
```bash
LOG_LEVEL=DEBUG ./gradlew :mcp-api:run
```
In `docker-compose.local.yml`:
```yaml
services:
mcp-my-app:
environment:
- LOG_LEVEL=DEBUG
```
Example output:
```
12:34:56.789 [eventLoop] DEBUG i.k.server.Routes - search_docs called: query='transaction rollback' genre=guideline topic=null
12:34:56.923 [eventLoop] DEBUG i.k.server.Routes - search_docs result: [{"chunkId":"guidelines/database.md#0","docPath":…
```
---
### `docker-compose.local.yml` — Specific Project Parameters
Variables unique to each project are set in the `environment` block:
| Variable | Description |
|---|---|
| `CHROMA_COLLECTION` | Unique collection name in ChromaDB (`vault_<project>`) |
| `VAULT_PATH` | Path to vault inside container (usually `/vault`) |
| `BM25_INDEX_PATH` | Path to Lucene index inside container (`/app/index`) |
### Volumes and Bind Mounts
```yaml
# docker-compose.local.yml (fragment)
services:
mcp-my-app:
volumes:
- ./vaults/my-app:/vault # bind mount — project vault
- bm25-my-app:/app/index # named volume — persistent BM25 index
- bm25-models:/app/model # named volume — ONNX models (downloaded once)
chromadb:
volumes:
- chroma-data:/chroma/chroma # named volume — all vector indexes
volumes:
bm25-my-app:
bm25-models:
chroma-data:
```
---
## Project Structure
```
knowledgeos/
├── build.gradle.kts
├── settings.gradle.kts
├── Dockerfile
├── docker-compose.yml # ChromaDB (common, in git)
├── docker-compose.local.example.yml # template for projects (in git)
├── docker-compose.local.yml # your project settings (.gitignore)
├── .env.example
├── .env # common variables (.gitignore)
├── vaults/ # project vaults
│ ├── my-app/ # vault for project "my-app"
│ └── another-project/ # vault for project "another-project"
│
├── buildSrc/ # convention plugins (kotlin-jvm)
│
├── mcp-api/ # contracts and entry point
│ └── src/main/kotlin/
│ ├── server/
│ │ ├── McpServer.kt # MCP protocol handler
│ │ └── Routes.kt # HTTP endpoints
│ │
│ └── tools/
│ ├── SearchDocsTool.kt # MCP tool: search_docs
│ ├── WriteDocTool.kt # MCP tool: write_doc
│ ├── UpdateDocTool.kt # MCP tool: update_doc
│ ├── GetDocTool.kt # MCP tool: get_doc
│ └── ListDocsTool.kt # MCP tool: list_docs
│
└── mcp-imp/ # implementation
└── src/main/kotlin/
├── vault/
│ ├── VaultWatcher.kt # file watcher
│ ├── FrontmatterParser.kt
│ └── WikilinkParser.kt
│
├── indexing/
│ ├── Chunker.kt
│ ├── ContextualEnricher.kt
│ └── IndexPipeline.kt
│
└── retrieval/
├── Bm25Retriever.kt
├── VectorRetriever.kt
├── WikilinkRetriever.kt
├── RrfFusion.kt # Reciprocal Rank Fusion
└── Reranker.kt
```
---
## Philosophy
> Code ceases to be the source of truth — it becomes its consequence.
The developer no longer argues over code — they argue over the formulation in the document. The agent's error is an error in the documentation, not in the implementation. Debugging becomes linguistic.
The new skill — the ability to write precise, unambiguous documents — is more valuable than knowledge of syntax.
---
## License
MIT
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
markitdown
MarkItDown-MCP is a lightweight server for converting URIs to Markdown.
markitdown
Python tool for converting files and office documents to Markdown.
Filesystem
Node.js MCP Server for filesystem operations with dynamic access control.
TrendRadar
TrendRadar: Your hotspot assistant for real news in just 30 seconds.
mempalace
The highest-scoring AI memory system ever benchmarked. And it's free.
mempalace
The highest-scoring AI memory system ever benchmarked. And it's free.