Context Toolkit
БесплатноНе проверенGeneric MCP server: file-scoped rules + frecency-ranked memory recall from plain markdown stores. Two tiers, deterministic, read-only. Pull the right context on
Описание
Generic MCP server: file-scoped rules + frecency-ranked memory recall from plain markdown stores. Two tiers, deterministic, read-only. Pull the right context on demand into any MCP client.
README
A generic MCP server that feeds any MCP client the right context at the right time — file-scoped rules, frecency-ranked memory, an evidence-backed work store (goals/steps with reuse-search baked into the schema), and a navigable link-graph, loaded on demand from plain markdown + YAML-frontmatter stores (OKF-compatible).
By Othmar Atzmüller and
TALOS
(the AI coding harness this toolkit was built inside). MIT-licensed — fork
freely; a credit back is appreciated.
Project boundary
TALOS is where this package was extracted from and stays its reference implementation, but it is not a runtime dependency. Concretely:
- The package core is host-independent. It reads plain files under
.context/(or a configured equivalent) and needs nothing else to run — no TALOS checkout, no TALOS file, no TALOS environment variable. - TALOS is the origin and reference application, not a prerequisite. Anything TALOS-specific referenced anywhere in this repo (file paths, themes, review-process names, symbol resolvers) never sits in the default code path a new project hits — it is either inert unless discovered (see test_talos_adapter.py, which skips entirely outside a TALOS checkout) or, going forward, will live behind an explicit, optional host adapter (tracked, not yet built — see the "known gaps" list in the changelog).
- A fresh project needs none of TALOS's files or conventions to use
rules, memory, work items, or docs search —
examples/generic-project/is the TALOS-free starting point; copy it and go. - Optional host adapters may add extra checks (symbol resolution, additional docs exclusions, project-specific themes, additional lint rules) without the core importing any host-specific code to do so.
If you read this section and still can't tell whether something belongs to the general package or is a TALOS-only example, that's a bug in this README — please open an issue.
Why no database or embeddings?
mcp-context-toolkit is deliberately zero-infrastructure and Git-native:
- Zero ops: Starts as a local stdio MCP process. No Docker containers, no background daemons, no database setup, no API keys needed.
- Deterministic BM25F + Frecency: Exact, latar-fast symbol and path matching without vector hallucination or near-miss noise.
- Git-native: Plain Markdown and YAML files stored in your repository — 100% diffable, audit-friendly, and version-controlled.
| Feature | mcp-context-toolkit |
Typical Vector-DB / Heavy RAG |
|---|---|---|
| Database required? | No (Plain Text / Git) | Yes (Postgres/Qdrant/Milvus) |
| Embedding model required? | No (BM25F + Frecency) | Yes (Ollama/OpenAI) |
| Background daemon / Docker? | No (stdio process) | Yes (Container / Daemon) |
| Git-versionable? | Yes (Markdown & YAML) | No (Stored in DB) |
| Startup time / Latency | < 1 second / < 110ms | Multiple seconds / Min setup |
| File-scoped Rules? | Yes (Glob-matching) | No |
| Typed Work Store? | Yes (Goals, Steps, Decisions) | No |
| Custom Domains & Views? | Yes (domain.yaml Manifests) |
Rare / Complex |
| Telemetry & Conflict Linter? | Yes (JSONL & CLI stats) |
No |
| Section-based Docs? | Yes (Section-split) | Basic chunking |
The idea
Long-lived AI coding/agent sessions accumulate context: coding standards, security rules, architectural decisions, hard-won lessons. Stuffing all of it into one monolithic system prompt is wasteful and noisy — a frontend task should never carry backend security rules, and a memory you haven't needed in weeks shouldn't crowd the ones you use daily.
mcp-context-toolkit keeps that knowledge as a directory of small markdown files and
serves it over MCP so the client pulls only what's relevant:
- Rules match by file-path glob — open
api/users.py, get back the security + db rules that apply to it, nothing else. - Memory matches by relevance, frecency (frequency + recency) and backlink structure — recall surfaces the most-used, most-recently-used, most-cited notes first.
Both are just markdown with frontmatter. Git is the storage, history and backup. The engine is read-only; you (or a consolidation pass) own the writes.
The problem
Long agent sessions accumulate context. Rules get crowded out. The agent forgets constraints it saw three hours ago.
The solution
Don't load everything upfront. Load the right thing at the right moment:
- Rules inject when you touch a matching file (PreToolUse hook)
- Decisions inject alongside the rules for that same file (the why behind it)
- Memory injects when the prompt matches (UserPromptSubmit hook)
- Nothing else enters context until it's needed
Four domains
| Domain | Leading question | Query model | Use it for |
|---|---|---|---|
| Rules | What must hold? | file-path glob → matching rules, by priority | standards, security policies, review gates — anything tied to which file you touch |
| Memory | What was learned? | keyword relevance × frecency (hot/cold) + backlink boost | lessons, user preferences, context — anything worth recalling later |
| Work | What's planned, active, or done? | BM25F keyword search, or direct key lookup | goals/steps/notes/decisions/changelog entries — a plan that stays a record, not a stale markdown file (see Work store below) |
| Docs | What's officially documented? | section-indexed BM25F keyword search (docs_recall) |
prose documentation (docs/**/*.md), returned as cited sections rather than whole files |
Decisions (ADRs) are not a fifth store: on disk they are kind: decision
records inside the Work store. query_decisions_for_file stays as a
compatible, file-path-glob query over that same data for existing callers
that don't want BM25F search — same underlying records, a second query
model, not a second store.
Decisions injection is cut by default (query_decisions_for_file): only the newest
DECISION_TOP_K (8) decisions with an allowed status (accepted) are returned for a
path, since decisions accumulate unbounded with no lifecycle pruning. Pass
statuses=None, top_k=None for the raw, unfiltered match set (audits, tooling) — the
default injection path (hooks, query_rules_for_file) always uses the cut.
Stability
| Component | Stability | What that means here |
|---|---|---|
| Rules / Memory / Work / Docs (the four domains above), the CLI, the MCP tool surface | public | Schema and tool signatures are the compatibility contract — a breaking change gets a BREAKING changelog entry and a version bump, never a silent shape change. |
Custom Domains (.context/domains/) |
experimental | "Ausbaustufe 1" — manifest discovery + query_domain only. The CLI scaffold and prompt/path-based auto-routing are not built yet; the manifest shape may still change. |
Host adapters (host_adapters.py's HostAdapter Protocol) |
public (the Protocol) / host-specific (concrete adapters) | The Protocol itself (has_symbol_index/resolve_symbol/path_to_id) is a stable contract; TalosDiscoveryAdapter is TALOS's own implementation of it and inert outside a TALOS checkout (see Project boundary). A third-party adapter for a different host is expected to implement the same Protocol, not extend TALOS's. |
Two-plus tiers
Both content types load from multiple roots tagged by tier — tier names differ by content type:
- Memory:
project(this repo) →user(~/..., cross-project) → an optionalcoreroot (institutional/org-wide notes). A singlerecallspans every loaded tier, so a session sees its repo's notes and your global ones in one ranked list. - Rules:
project(this repo's own rules) + an optionalsharedtier — an org-wide "grundregeln" floor (CONTEXT_SHARED_RULES_DIR) for files a repo's own rule globs don't cover.
On a name/key collision the more specific (project) tier wins for both content types.
query_for_file_tiered(file_path) — used by the query_rules_for_file MCP tool
and the CLI's --format bundle — prefers project-tier matches; the shared tier
only surfaces as a generic discipline floor when a file matches zero project
rules (a top-level script, a config file, a greenfield repo before its own rules
exist). This avoids duplicating a shared rule alongside an already-matching, more
specific project rule. The untiered query_for_file still returns the raw match
set across all loaded tiers.
Hot / cold memory (frecency)
Every recall / get_memory hit is counted in a per-machine sidecar (_usage.json,
gitignored). The score is frequency-dominant and log-damped — it does not decay
with wall-clock time, so a weekend (or three-week) pause never cools a heavily-used
memory. On top of frecency, recall adds a small backlink boost (`log1p(inbound_count)
- 0.1
per memory) so notes cited by many others rise without explicit usage.memory_usagereports the hot→cold ranking; a consolidation step can use it to surface hot notes first.memory_dream_status` tells you when such a consolidation pass is due (files changed since last dream + lint issues against configurable thresholds).
Work store
A work item is goal / step / note / decision / changelog, with a
status (open → active → done/dropped/superseded) and a theme —
a closed vocabulary (WorkTheme in models.py), shipped here with this
project's own set. Fork/edit that Literal for your project's themes —
deliberately, one at a time: an unlisted value is a load error, not a
silent misc. Steps point at their goal via part_of; a
goal's open/active/done counts are computed from its children,
never stored, so a goal can't silently claim progress its children no
longer show.
Two fields, easy to conflate, deliberately kept apart:
evidence(commits/files) — backward-looking, filled when closing an item: what this actually touched.reuse— forward-looking, filled when planning: what already exists that this item uses, extends, or will build. The load-bearing sub-field issearched_for(what was searched for) — it is what separates "never searched" (noreuseblock at all) from "searched, found nothing" (searched_forset, everything else empty).founddeliberately includes rejected hits, because "mixin pattern found, discarded — no class here" is the most valuable part of a reuse record and fits in none ofuses/extends/creates.
work_lint surfaces goals whose children are all closed but the goal
itself is not — a real failure mode, not a hypothetical one: a plan that
gets marked done by consensus while the record itself sits open is
invisible to anything that only reads the record.
Glossary
- Work Store — the
goal/step/note/decision/changelogrecords under.context/work/**, served byWorkEngine. Each is one markdown file with YAML frontmatter; there is no database, so the store IS the directory. - Reuse Record — the
reuse:block on a work item, filled BEFORE building (forward-looking), answering "what already does this?" — see theevidencevs.reusedistinction above. - Evidence — the
evidence:block (commits/files), filled WHEN CLOSING an item (backward-looking): what the finished work actually touched. Astatus: doneitem with empty evidence is a claim without a receipt, andwork_lintflags it. - Rollup — a goal's
{total, open, active, done, dropped, superseded}child-status counts, returned byget_work/list_work. Always computed on the fly from the children's current status, never stored on the goal itself — the same reasoning aswork_lint's closeable-goals check just above.
Install
git clone https://github.com/othmaratzmueller-bit/mcp-context-toolkit
cd mcp-context-toolkit
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest # optional
Zero heavy deps: Python stdlib + pydantic, pyyaml, mcp.
Register with any MCP client
It's a stdio MCP server — works with any MCP-capable host (editors, agents, custom clients). Point it at your stores via env:
{
"mcpServers": {
"context": {
"command": "context-toolkit-mcp",
"env": {
"CONTEXT_RULES_DIR": "/abs/path/to/repo/.context/rules",
"CONTEXT_MEMORY_DIR": "/abs/path/to/repo/.context/memory",
"CONTEXT_USER_MEMORY_DIR": "/home/you/.context/memory",
"CONTEXT_WORK_DIR": "/abs/path/to/repo/.context/work"
}
}
}
}
Any unset dir is auto-discovered by walking up from the working directory, looking for
<dir>/.context/rules (and …/memory, …/work) first, then <dir>/.claude/rules as a
fallback for existing Claude Code repos. The convention list itself is configurable via
CONTEXT_STORE_CONVENTIONS (comma-separated, e.g. .talos,.context,.claude to prefer a
.talos store first). Memory tools register only when a memory store is found, work
tools only when a work store is found — rules-only setups keep working untouched.
Freshness / reloading
The server loads its stores once at startup, then reloads automatically when the files
change. Every tool call does a cheap mtime scan over the rule and memory trees and
rebuilds only when something actually changed — so an edited rule, or a memory store
re-bundled by a separate consolidation pass, is picked up at the next call, no restart
needed. The frecency sidecar is re-read on each recall, so several server processes
pointing at one store share a single hot/cold signal (and a file lock keeps their writes
from clobbering each other). Nothing is served staler than your last edit.
Architecture
flowchart LR
subgraph write["✏️ Write side — human or agent"]
Human["Human / Agent<br/>edits content directly"]
end
subgraph read["📖 Read side — your content is read-only"]
Files["Markdown + YAML<br/>.context/rules, memory, work, docs"]
Loader["Loader<br/>core.py: parse_frontmatter,<br/>iter_files, BM25 index"]
Engines["Engines<br/>RulesEngine · MemoryEngine ·<br/>WorkEngine · DocsEngine"]
Reloader["Reloader<br/>mtime-watch, rebuild on change"]
Tools["MCP Tools / CLI<br/>context-toolkit-query"]
Files --> Loader --> Engines --> Reloader --> Tools
end
Human -->|"edits content<br/>(the only content-write path)"| Files
Tools -->|serves| Client["MCP client<br/>Claude Code, etc."]
Your rule/memory/work/docs content is never created, edited, or deleted by the engine — the only way it changes is a human or an agent editing it directly; the Reloader picks that up on the next tool call (see "Freshness / reloading" above), it never writes it back. This is deliberate, not an oversight: the store format IS the file format (plain markdown + YAML frontmatter, see "Store format" below), so "write" is just "edit a file" — no second API, no drift between what a file says and what the tool returns.
This is narrower than "the engines never write" — they do write a small
set of their OWN sidecar files (never your content): a frecency usage
sidecar on every recall, a lock file alongside it, RulesEngine's
fallback-markdown snapshot on every server start, and --export-studio's
opt-in viewer export. All of them are listed in full, with exactly when
each fires, under What it writes to disk —
this diagram is about the content boundary, that section is the
complete inventory.
Tools
Rules & Decisions
| Tool | Purpose |
|---|---|
query_rules_for_file(file_path) |
codebase intelligence context (Rules, Decisions, Dependencies) for the path |
query_rules(type?, scope?, priority?, module?) |
bulk fetch rules by metadata |
get_rule(key) |
full body of one rule |
list_rule_keys(type?, scope?) |
enumerate rule keys |
validate_rules() |
dry-run validate the rule directory |
Memory
| Tool | Purpose |
|---|---|
recall(query, limit?) |
top memories across both tiers — frecency- and backlink-ranked |
get_memory(name) |
full body + metadata of one memory, plus cited_by (inbound [[links]]) |
list_memories(type?, tier?) |
enumerate, filterable |
memory_dream_status(files_threshold?, lint_threshold?) |
consolidation trigger: changed files + lint issues → "dream fällig?" |
memory_lint() |
hygiene: broken [[links]], index orphans, stale pointers |
memory_usage(limit?) |
hot→cold usage report (opens, recalls, heat) |
Work store
| Tool | Purpose |
|---|---|
work_recall(query, limit?) |
find work items by keyword, BM25F over title + body |
get_work(key) |
one work item, with its computed open/active/done rollup if it has children |
list_work(theme?, status?, kind?, include_archived?) |
bulk fetch by metadata — a call with NO theme/status/kind returns a per-theme topic overview instead of every item (no natural size bound otherwise); narrow with theme= to get the actual items. Every item-list response, filtered or not, is also capped at a total response budget with a truncated/omitted marker once exceeded |
work_children(key) |
direct children of a goal via part_of |
work_status() |
closeable-goal count, dangling references, records that failed to load — the work-store counterpart to memory_dream_status |
work_lint(check_symbols?) |
hygiene report: dangling refs, closeable goals, (optionally) reuse anchors that no longer resolve to a real symbol |
Docs & code structure (optional — registers only when the relevant files exist; see Register with any MCP client)
| Tool | Purpose |
|---|---|
docs_recall(query, limit?) |
find sections of prose documentation (docs/**/*.md) by keyword, same BM25F ranking as memory/work recall |
get_baustein_doc(pfad) |
purpose entry for a whole file — what it does, why it exists, what it's coupled to (project-specific data file, path via CONTEXT_BAUSTEIN_DOCS_FILE) |
get_function_doc(anchor) |
one-sentence purpose entry for a single function/method, keyed by path/to/file.py::Symbol (CONTEXT_FUNCTION_DOCS_FILE) |
Custom domains (optional — registers only when .context/domains/ exists; see Domains below)
| Tool | Purpose |
|---|---|
list_domains() |
configured domains — id, title, profile, physical store vs. named view, routing keywords/paths |
get_domain(domain_id) |
full manifest for one domain, including its filter and attributes |
query_domain(domain_id, query, limit?) |
keyword-search one domain — same BM25F ranking as the plain recall tools, with the domain's declared filter applied first for a view |
Domains
Stability: experimental (see Stability) — manifest shape and CLI surface may still change.
Beyond the four built-in domains above, a project can configure its OWN
named domains without writing code — a physical store (its own
directory, its own records) or a named view (a filtered slice of a
domain this server already loads — no second copy on disk). Both are
just a .context/domains/<id>/domain.yaml:
# .context/domains/critical_security/domain.yaml — a VIEW: filters the
# rules store this server already loads, no separate directory of its own.
id: critical_security
title: "Critical Security Rules"
profile: rules
source: rules # <- makes this a view; a physical store omits `source`
filter:
types: [security]
priorities: [non_negotiable]
# .context/domains/honey_bunny/domain.yaml — a PHYSICAL store: its own
# directory of memory-shaped records, `root:` resolved relative to this
# domain.yaml's own directory.
id: honey_bunny
title: "Honey Bunny Notes"
profile: memory
root: ./data # -> .context/domains/honey_bunny/data/
trigger:
keywords: [bunny, honey]
profile picks which engine answers query_domain (rules/memory/
work/docs) — for a view, source names the SAME profile being
filtered. filter is profile-shaped: types/priorities for a rules
view, themes/statuses for a work view; an empty filter matches
everything the source profile has. attributes is a free-form block for
whatever metadata your project wants to attach — it does not go through
the strict field validation the rest of the manifest does.
A rules-profile domain has no free-text body to rank on its own
(rules match by file-path glob, not keyword) — narrow it with
filter.types/filter.priorities first, then query_domain ranks the
narrowed set by keyword.
Current scope (Ausbaustufe 1): manifest discovery + the three tools
above. Not yet built: the context-toolkit-query domain init/list/ validate/... CLI scaffold, and path/prompt-based automatic routing
(match_domains_for_path/match_domains_for_prompt) — a domain today is
always queried explicitly by domain_id, never auto-selected.
CLI
A second entry point, context-toolkit-query, exposes the engine on the command line —
used by editor/agent hooks to inject the right rules + memory automatically:
# Rules for one file (glob match) — JSON bundle {fingerprint, markdown, rule_count}
context-toolkit-query path/to/file.py --format bundle
# Memory recall for a prompt — JSON {names, markdown, count}; --exclude dedups
context-toolkit-query --recall "how do I anonymize PII?" --limit 6 --exclude name1,name2
# All memories of a tier (e.g. always-load the user tier at session start)
context-toolkit-query --memory-tier user --with-bodies
# Maintenance
context-toolkit-query --validate # validate the rule set
context-toolkit-query --validate-all # validate rules + memory + work in one call, exit 1 on any error
context-toolkit-query --doctor # discovered store paths, record counts, load errors — no server needed
context-toolkit-query --export-studio ./studio # Context Studio snapshot + viewer (incl. Graph tab)
context-toolkit-query --method-block # print the always-on working-method block
Migrating from a legacy decision store
If you have an older .context/decisions/*.yaml ADR store (pre-dating the
Work Store's kind: decision), --migrate-decisions converts each file
into a Work Card:
# Preview first — writes nothing, reports what WOULD move
context-toolkit-query --migrate-decisions \
--from .context/decisions --to .context/work/decisions --dry-run
# Then for real
context-toolkit-query --migrate-decisions \
--from .context/decisions --to .context/work/decisions --theme migrations
A worked example (source YAML + the Work Cards it produces) lives under examples/migrations/.
What it does and does not do:
- Field mapping:
key(falling back to the filename stem if absent), ALWAYS sanitized againstWorkItem.key's^[a-z][a-z0-9_]*$pattern — never the raw value, since both a date-prefixed filename (2026-01-01_x) and a hand-writtenkey:field (ADR-007: Cache!) can violate it —title,date→openedalways, →closedonly when the mapped status is one Work considers closed (done/dropped/superseded— anopencard with aclosed:date is a lint violation the store itself flags),applies_to.files→evidence.files,supersedessanitized through the SAME rule askey(not carried through verbatim — a same-run reference has to resolve to the target's actual, possibly-transformed key, not the raw source value),statusmapped through the legacydraft/accepted/rejected/superseded/deprecatedvocabulary onto Work'sopen/done/dropped/superseded(never ontodonefor a rejected or deprecated decision; an unrecognized status defaults toopen, reported underunmapped_status),reason→ body (a rejected/deprecated card also gets a one-line "migrated from legacy status: …" note, so the original distinction survives the merge intodropped). - Not carried over: anything else the source YAML has (e.g.
applies_to.modules) has no Work Card equivalent — the run reports it per item underdropped_fieldsin its JSON result rather than losing it silently. - Repeatable: the same source file always produces the same target
key/filename, so running it again replaces rather than duplicates —
the result's
overwrittenlist names what got replaced. Two source files sanitizing to the SAME key within one run are reported underconflicts(including in--dry-run), since the second write would silently replace the first. - The source directory is never touched — nothing is deleted, so it stays its own backup until you remove it yourself.
The Context Studio viewer (--export-studio) has four tabs: Review (accept/reject a
consolidation diff), Browse (packages by tier + frecency heat), Graph (the resolved
[[link]] graph rendered with a vendored Cytoscape.js, MIT —
node colour = tier, size = heat, click a node to open it) and Pending (the flag ledger).
The viewer defaults to a dark theme with a one-click light/dark toggle (persisted in
localStorage); the Graph tab respects the active theme.
Wiring auto-injection (hooks)
The engine gives you the data; your agent/host decides when to inject it. The whole point is deterministic injection — beats hoping the model remembers to query. Five hooks cover it — three inject (rules + decisions + memory), two keep the store healthy.
Dual-Compatibility: Claude Code & Google Antigravity
MCP clients have slightly different hook output requirements. To run hooks that work seamlessly across both Claude Code and Google Antigravity, your hook scripts should return a dual-compatible JSON payload:
- Claude Code expects nested context under
hookSpecificOutput. - Google Antigravity expects a flat JSON with
decisionandadditionalContextat the root.
Here is the dual-compatible JSON format that your hooks should output:
For PreToolUse Hooks (e.g. file edit):
{
"decision": "allow",
"additionalContext": "[Markdown content here]",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": "[Markdown content here]"
}
}
For SessionStart & UserPromptSubmit Hooks:
{
"additionalContext": "[Markdown content here]",
"hookSpecificOutput": {
"hookEventName": "SessionStart", // or "UserPromptSubmit"
"additionalContext": "[Markdown content here]"
}
}
The hook scripts themselves live in the consuming repo, not in the engine (it stays host-agnostic):
1. Rules — per file touched (e.g. PreToolUse on Edit/Read/Write). Inject the rules
matching the file you are about to change; dedup by fingerprint so an unchanged set
stays silent:
BUNDLE=$(context-toolkit-query "$REL_PATH" --format bundle)
# inject $(jq -r .markdown <<<"$BUNDLE") as context IF its .fingerprint differs from
# what you last injected for this path (store per-path fingerprints in session state).
2. Memory — user tier at session start (SessionStart). The user tier is always relevant but rarely keyword-matches a prompt, so load it unconditionally, up front:
context-toolkit-query --memory-tier user --with-bodies # -> {markdown} -> context
# also seed your session "already-injected" set with the returned .names
3. Memory — relevant recall per prompt (UserPromptSubmit). Recall what matches the
prompt; inject only what you have not injected yet this session (track the names, pass
them back as --exclude):
context-toolkit-query --recall "$PROMPT" --limit 6 --exclude "$ALREADY_INJECTED"
# -> {names, markdown}: inject .markdown, then add .names to your session set
4. Memory — re-index on write (PostToolUse on Edit/Write). When a memory file is
written, regenerate the flat catalog so a freshly-added note is catalogued immediately —
mechanical + deterministic, no LLM, no /dream needed for a note to be findable:
context-toolkit-query --reindex # rebuilds _descriptions.md from ALL memory files (incl. loose)
5. Memory — staleness nudge at session start (SessionStart). Show, once, how many memory files changed since the last consolidation run so the user can decide to run one — silent when nothing is loose. No auto-run, no daemon, no monitoring:
# memory files newer than the package hot-index = touched since the last consolidation
find <memory-dir> -name '*.md' ! -name '_*' ! -name 'MEMORY.md' -newer <memory-dir>/MEMORY.md
# >0 -> inject "N new/changed -> run the consolidation skill"; 0 -> stay silent
The consolidation skill (/dream) itself runs incremental by default: it processes only
that loose set (newer-than-index) and asks at start whether to do a full sweep instead — an
already-curated store should not be re-swept just because three notes were added. Hooks 4+5
split the work cleanly: the re-index keeps the catalog current at write-time (mechanical),
while bundling + hot-index curation stay an explicit, gated /dream step.
Each emits a ready-to-inject markdown field (or runs a mechanical maintenance step) plus
the identifiers (fingerprint / names) you need for dedup. With these wired, the model
always has the right rules for the file it touches and the right memories for the topic —
and the store stays current without a manual chore — all without being asked.
Directory structure
Create a .context (or .claude) directory in your project root — auto-discovered, with the convention list configurable via CONTEXT_STORE_CONVENTIONS — with the following structure:
your-project/
├── .context/ # Core intelligence folder (rules, decisions, memories, work)
│ ├── rules/ # Rule files (*.yaml)
│ │ ├── security/ # Organized by domain (optional structure)
│ │ ├── frontend/
│ │ └── backend/
│ ├── graph/ # Optional dependency graph (see "Dependency graph" below)
│ │ └── reference-index.json
│ ├── memory/ # Project-tier memories (*.md)
│ │ ├── MEMORY.md # Human-curated index
│ │ └── _descriptions.md # Auto-generated catalog index (run --reindex to update)
│ └── work/ # Work items — goals/steps/notes/decisions/changelog (*.md)
│ ├── decisions/*.md # kind: decision cards — architectural decisions / ADRs
│ └── <theme>/*.md # Everything else: recursive rglob, any nesting works
For global user-specific memories that apply across all of your projects, place them in ~/.context/memory/ (user-tier).
Dependency graph (optional)
Drop a graph/reference-index.json next to your rules and every per-file query
also returns that file's coupling — what it imports and what imports it — so the
agent sees the blast radius before it edits:
// query_rules_for_file("backend/app/services/pipeline.py") →
{
"rules": [ /* … */ ],
"decisions": [ /* … */ ],
"dependencies": {
"imports": ["py:services.auth", "py:services.rate_limiter"],
"imported_by": ["py:api.routes.orders", "py:tasks.notify"]
}
}
The index is a flat map you generate however you like — the engine only reads it and
ships no graph builder, so it stays language-agnostic. Keys are py:<dotted.module>
or js:<path>; a queried file matches the entry whose key is a suffix of its path:
{
"py:services.pipeline": { "imports": ["py:services.auth"], "imported_by": ["py:api.routes.orders"] },
"js:web/order-view.js": { "imports": ["js:web/order-api.js"], "imported_by": [] }
}
No graph file, or no matching entry, simply means dependencies comes back empty — the
feature is purely additive.
Store format
A rule (.context/rules/**/*.yaml):
key: no_hardcoded_secrets
title: No hardcoded secrets
type: security # security | workflow | code_quality | frontend | architecture | infrastructure | module
scope: backend # backend | frontend | database | infrastructure | docs | all
priority: non_negotiable # non_negotiable | mandatory | recommended
modules: [all]
applies_to:
files: ["**/*.py", "**/*.js"]
summary: One-liner shown in query results.
content: |
## Full markdown body, fetched via get_rule.
created: 2026-01-01
A decision (.context/work/decisions/*.md) — a Work-Store card with
kind: decision, translated into the Decision shape query_rules_for_file
serves: opened → date, evidence.files → applies_to.files, the body →
reason, and status mapped open/active → draft, done → accepted,
dropped → rejected, superseded → superseded:
---
key: graph_injection
title: Code Graph Context Injection
kind: decision
status: done # -> accepted
theme: engine
opened: 2026-07-05
closed: 2026-07-05
evidence:
files: ["backend/app/**/*.py"]
---
Decision: Need to automatically inject graph coupling into the prompt context via MCP.
Why: Without element context, the agent answers generically.
A memory (.context/memory/**/*.md):
---
name: prefer_composition
description: One-line summary used for recall ranking.
metadata:
type: feedback # user | feedback | project | reference | misc
tags: [design]
resource: file:///abs/path/or/uri # optional (OKF): the asset this note describes
timestamp: 2026-05-28T14:30:00Z # optional (OKF): ISO 8601 last meaningful change
---
The note itself. Link related notes with [[their-name]].
type/tier/members/tags/resource/timestamp may live top-level or nested under
metadata: — both are read (top-level wins). MEMORY.md in the memory dir is the
human-curated index (skipped as a record); memory_lint checks it against the actual files.
A work item (.context/work/**/*.md):
---
key: rate_limit_retry_backoff
title: Exponential backoff on the rate-limit retry path
kind: step # goal | step | note | decision | changelog
status: done # open | active | done | dropped | superseded
theme: infra # your project's own closed vocabulary, see WorkTheme
opened: 2026-07-01
closed: 2026-07-03
part_of: api_reliability_hardening # the goal this step belongs to
evidence:
commits: [a1b2c3d]
files: [src/http/retry.py]
reuse:
searched_for: ["existing retry helper", "backoff jitter"]
found: ["a fixed-delay retry in http/client.py — rejected, no jitter, would thunder-herd"]
conclusion: "New exponential-backoff-with-jitter helper; the fixed-delay one stays for its one caller."
uses: ["http/client.py::Session"]
creates: ["http/retry.py::exponential_backoff"]
---
Body: full prose, same rules as a memory note (`[[links]]` resolve the same way).
The original (2026-07 to 2026-08) German field names — kanten/gesucht/
gefunden/entscheidung/nutzt/erweitert/baut/modul and a few
others (braucht_owner, entblockt_durch, aus_migration) — are still
accepted on read as aliases for existing stores, but the schema above is
what new records and new examples should use. MCP responses and the
Python API surface only the English names.
kind: goal and kind: step form the hierarchy via part_of (a goal has no
part_of; a step's part_of names its goal). kind: note/decision/changelog
are typically leaves. See Work store above for what evidence
vs. reuse are each for.
OKF interoperability
The store format is deliberately close to the Open Knowledge Format (OKF) — both are markdown + YAML-frontmatter + markdown-link graph, versioned in git, no RDF. Two conventions arrived at the same shape independently, which makes interop nearly free:
resource+timestampare the OKF optional fields (URI of the described asset, ISO 8601 last-change). Unquoted ISO datetimes are normalized to ISO 8601 (Z→+00:00); quote to keep a value verbatim.- Links are a graph.
[[name]]edges resolve through package bundling (a link to an absorbed member is credited to its package);get_memoryreturns the reverse edges ascited_by, and the Studio Graph tab renders the whole directed graph. - Portable by construction. A store is plain files in git — readable in Obsidian/MkDocs, diffable in PRs, consumable without this engine. What the engine adds on top of the format is the part OKF leaves to consumers: frecency ranking, lossless bundling, and the reverse-edge/graph views.
Where the engine goes beyond OKF: a live usage model (recall is ranked by
frequency+recency, not just keyword) and a consolidation mechanism (packages with a
verified-lossless merge), neither of which the format itself prescribes.
Examples — copy to start from zero
examples/ ships a runnable starting point so you don't face an empty store:
examples/rules/— a small, generic starter pack (8 rules acrosssecurity/,code_quality/,frontend/,workflow/). Copy it and adapt the globs:cp -r examples/rules/* /path/to/your/.context/rules/examples/memory/— the 3-file memory layout (MEMORY.mdindex,_descriptions.mdcatalog, one example package undercore/) showing the structurerecallexpects.examples/decisions/— two linked ADRs (onesupersedesthe other) showing the decision schema and status lifecycle.examples/graph/— a smallreference-index.jsonshowing the dependency-graph format (py:/js:keys,imports/imported_by).
These are illustrative defaults, not production policy — see each directory's
README.md. They are inert: auto-discovery only ever loads <dir>/.context/rules,
or <dir>/.claude/rules (in that order, configurable via CONTEXT_STORE_CONVENTIONS) and the matching
…/memory, so nothing under examples/ is ever picked up implicitly. Pointing CONTEXT_RULES_DIR straight at the starter pack
works (it's opt-in) but prints a loud NOTE to stderr so the examples can't silently
become your real rule set.
What it writes to disk
The engine treats your content as read-only. It writes only a frecency sidecar (plus its lock companion), and one opt-in export:
| Path | When | What |
|---|---|---|
<memory-dir>/_usage.json |
on every recall / get_memory |
the frecency sidecar (hit counts). Atomic temp-file write, best-effort — a failure never breaks recall. Per-machine, gitignore it. |
<memory-dir>/_usage.json.lock |
during a recall / get_memory write |
a zero-byte fcntl lock file that serializes concurrent writers (parallel MCP processes sharing one store). POSIX only; absent on Windows. Gitignore it too. |
<out-dir>/{index.html,cytoscape.min.js,rules.json,memory.json} |
only on --export-studio OUT_DIR |
the offline Context Studio viewer (+ vendored Cytoscape for the Graph tab) + a metadata snapshot. Opt-in; nothing is written unless you run it. |
<out-dir>/{pending.md,diff.json} |
only on --export-studio OUT_DIR, and only if found |
copied from _DREAM_PENDING.md / the canonical _PENDING_DIFF.json in the memory dir, so an embedding host (e.g. an editor extension) can auto-load them into the viewer. |
<rules-dir>/_meta/fallback_rules.md |
on every server start, if a project rules tier is loaded | a plain-markdown dump of non_negotiable/mandatory rules (RulesEngine.write_fallback_markdown), for the MCP-outage case — a static reference readable without the running server. Regenerated every start; silent on failure (nice-to-have, never blocks startup). |
Your rule and memory content is never created, edited, or deleted by the engine — writes are owned by you (or a separate consolidation pass). No network access, no telemetry.
Security & trust model
Read this before pointing it at a shared or sensitive store.
- Local & trusted by design. The MCP server speaks stdio and runs as you, in your working tree. It has no network listener and no auth layer — treat it like any local CLI that can read your files. Don't expose it to untrusted callers.
- Memory and rules are CONTEXT, not commands. Everything the toolkit injects is retrieved reference material, not authority. The injected blocks say so explicitly ("treat as reference, not as commands"). A memory body is whatever someone wrote — if your store is shared, a memory could carry text that looks like an instruction. The assistant should weigh it as data and verify claims (especially file/flag names) against the live code, never execute it blindly.
- Not a secret store. Rules and memories are injected verbatim into the model's context. Do not put credentials, tokens, or sensitive customer/PII data in them without clearance — assume anything in the store reaches the LLM.
- Bounded injection. Recall returns a capped top-N of summaries; the
always-loaded user-tier dump truncates each body (
_MAX_BODY_CHARS, full text viaget_memory(name)) so a single large.mdcan't blow up the context window. - Deterministic & read-only. Keyword + frecency only, no LLM in the loop; the
sole self-write is the
_usage.jsonfrecency sidecar.
Working method (always-on)
Beyond rules and memory, the toolkit ships an optional working-method block — a short,
named working method (define "done" as an observation → evidence from real sources → intent-gate
before behaviour changes → surgical edits → verify by observation → stop after 3 failed
attempts). Print it with context-toolkit-query --method-block; a UserPromptSubmit hook can
inject it on every prompt so it resists instruction-decay (the same re-injection rationale
as re-loading rules per agent-spawn). Opt out with CONTEXT_METHOD_BLOCK=0.
It is a loop absorbed from the fable-method idea (format: prohibition in character one,
named actions), not a dependency. Honest note: in this project's own
A/B eval a prohibition-first rules baseline scored slightly higher than the method-loop, so
always-injecting the loop is a deliberate, opt-in choice, not the measured optimum — wire it if
the coherent working-method framing helps your models, skip it if your rules already cover it.
Design principles
- Read-only on your content. It loads and ranks; the only self-write is the
_usage.jsonfrecency sidecar (see above). - Deterministic. Keyword + frecency scoring, no LLM in the loop — same inputs, same order.
- Degrade, don't crash. A malformed file is skipped, not fatal; a missing/corrupt usage sidecar resets to empty.
- Generic. No assumptions about any specific client or project. Ships only example rules; real rule/memory sets live in the consuming repo.
License
MIT — see LICENSE.
Установить Context Toolkit в Claude Desktop, Claude Code, Cursor
unyly install context-toolkitСтавит в Claude Desktop, Claude Code, Cursor и VS Code — сам разбирается с npx, uvx и сборкой из исходников.
Впервые? Поставь CLI: curl -fsSL https://unyly.org/install | sh
Или настроить вручную
Выполни в терминале:
claude mcp add context-toolkit -- uvx --from git+https://github.com/othmaratzmueller-bit/mcp-context-toolkit mcp-context-toolkitПошаговые гайды: как установить Context Toolkit
FAQ
Context Toolkit MCP бесплатный?
Да, Context Toolkit MCP бесплатный — установка в пару кликов через Unyly без оплаты.
Нужен ли API-ключ для Context Toolkit?
Нет, Context Toolkit работает без API-ключей и переменных окружения.
Context Toolkit — hosted или self-hosted?
Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.
Как установить Context Toolkit в Claude Desktop, Claude Code или Cursor?
Открой Context Toolkit на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.
Похожие MCP
GitHub
PRs, issues, code search, CI status
автор: GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
автор: mcpdotdirectAmap Maps Mcp Server
MCP server for using the AMap Maps API
автор: duxiaohuiSupabase
Database, auth and storage
автор: SupabaseEverything
Reference / test server with prompts, resources, and tools.
Git
Tools to read, search, and manipulate Git repositories.
Sequential Thinking
Dynamic and reflective problem-solving through thought sequences.
Time
Time and timezone conversion capabilities.
Compare Context Toolkit with
Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории development
