Manent
Allows AI agents to search, read, and explore neighbors within an Obsidian-compatible Markdown vault, with retrieval ranking and git-based versioning.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@Manentsearch my vault for notes about the Q3 roadmap"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Manent
Verba volant, scripta manent. Spoken words fly away, written words remain.
File-first, git-versioned memory for AI agents. A specification plus a toolchain that turns a plain Markdown vault (Obsidian-compatible) into a queryable, lintable, MCP-served brain that any agent can mount, and that several agents with different clearances can share.
Context windows evaporate: verba volant. Manent is the written memory that remains: plain files you can read, diff, and own.
Why
Agent memory today is either a proprietary vector-DB dump (lock-in, no audit trail, no human curation) or an unstructured pile of notes (no schema, no retrieval quality, no guarantees). Manent takes a third path:
Markdown files are the source of truth. Everything else (search indexes, graphs, embeddings) is derived and rebuildable from scratch.
Git is the sync and audit backbone. Every memory write is a commit: who, when, why.
A closed, versioned schema (JSON Schema 2020-12) for note types and typed edges. Linted in CI, so a malformed note never lands.
MCP is the access layer. Any MCP client (Claude, ChatGPT, VS Code, Cursor, your own agent) mounts the brain with a URL, no custom SDK.
Identity decides visibility, before ranking. Each agent reads the vault through a view built from the notes it may see; a note that says nothing about its audience is private.
Related MCP server: loreweave
Packages
Package | What |
| The vault specification (v0.2): note types, frontmatter schemas, audience, typed edges, layout |
| Parser (frontmatter + wikilinks), vault loader, graph builder, audience filter, PII/injection scanner |
| Ranking: BM25 lexical, local dense embeddings, graph expansion, RRF fusion, status demotion |
| Eval harness: golden sets, recall@k / MRR / nDCG, regression gate |
| Rule engine: schema, links, duplicates, orphans, personal data, model-directed text, audience labels |
| Curation: near-duplicate pairs, declared contradictions, link communities and the maps of content they lack |
| MCP server over a vault: read tools, gated write tools, gap register, identities, audit, hot reload; see Protocol eras |
| The CLI: `init |
Quickstart
npm install
npm run build
# scaffold a new vault
node packages/cli/dist/index.js init my-vault
# lint it
node packages/cli/dist/index.js lint my-vault
# serve it over MCP (stdio); re-indexes on every edit
node packages/cli/dist/index.js serve my-vault
# or over Streamable HTTP with bearer auth (for remote clients / claude.ai)
node packages/cli/dist/index.js serve my-vault --http 3939 --token <long-random-token>
# with the gap register (needs Node 22.5 or newer), an audit log and agent identities
node packages/cli/dist/index.js serve my-vault --http 3939 --token <token> \
--gaps ~/manent/gaps.sqlite --audit ~/manent/audit.jsonl --agents agents.json --writableRegister with Claude Code:
claude mcp add mybrain -- node <repo>/packages/cli/dist/index.js serve <vault>Use from claude.ai (web)
Verified working. claude.ai will not connect to a remote MCP server that has no discoverable OAuth metadata, so the HTTP server ships its own single-owner authorization server: your vault token is the login password, or an agent's token, to connect as that agent.
Serve over HTTP and expose it: a tunnel (
cloudflared tunnel --url http://127.0.0.1:3939) or a reverse proxy. HTTPS is required by OAuth for non-localhost redirects.claude.ai, Settings, Connectors, Add custom connector, URL
https://<your-host>/mcp. Leave the OAuth Client ID empty; discovery and registration are automatic.claude.ai opens the consent page. Paste your token, approve, done.
Access tokens are HMAC-derived from the vault token rather than stored, so a connected client survives server restarts. Rotating the vault token invalidates every issued token. Tokens issued to an agent carry the agent's name; tokens issued before identities existed keep working as the owner's.
Three details worth knowing:
The endpoint binds
127.0.0.1by default and refuses to start without a token: a vault never reaches the network unauthenticated by accident.Redirect URIs are restricted to an allowlist (
claude.ai,claude.com, localhost) and must be HTTPS. PKCE S256 is mandatory; authorization codes are single-use and expire in five minutes./t/<token>/mcpalso works for clients that can neither set headers nor do OAuth. That URL is a credential; treat it like a password.
Run npm run test:oauth to exercise the whole flow, including wrong token, failed PKCE, code
replay, forged token and disallowed redirect.
Tools
Tool | What |
| ranked search, returns |
| a note, parsed or verbatim |
| the notes linked to a note, up to 3 hops |
| enumerate, or regex over bodies |
| "this answer was wrong / outdated / incomplete / helpful"; see The gap register |
| near-duplicates, contradictions and unmapped communities over the whole vault; comes back as a task on the modern path, see Long-running calls |
| the review queue, the gap register and the link neighbourhood, each with a page to read it in; see MCP Apps |
| listed only with |
Every tool runs on the caller's view of the vault. That is the whole access model, and it is
worth stating once: a view is built from the notes the caller may read before any index exists
over it, so brain_grep and brain_read_raw, which never touch the ranker, cannot reach a note
the ranker would have hidden. Filtering after ranking would leave them wide open.
One vault, several agents
A vault read by one person needs no identities: the token is the password. A vault read by a customer-care agent, a coding agent and the owner's own sessions needs to know who is asking.
Identities live in a JSON file passed with --agents (HTTP only; stdio is the owner's own
session):
{
"customer-care": { "token": "<long random>", "read": ["product", "business"], "write": "quarantine/customer-care" },
"coding": { "token": "<long random>", "read": ["tech", "product"] }
}readis the agent's scope: the audience labels it may see.*is the owner's scope and sees everything, private notes included.privateis never granted; it is the absence of a grant.writeis the one directory the agent may write into. Nowrite, no writing.The vault token is the owner. The
--agentsfile, the vault token, and the OAuth tokens minted for either all resolve to an identity that travels with every call.
Visibility is a property of the note, in its frontmatter, not of the folder, because the same note often serves two audiences:
audience: [tech, product] # who may read itAbsent or empty means private: the most restrictive reading, on purpose. A note written
without thinking about it cannot become visible by accident, the same principle as secrets/.
public is the one reserved label that may leave the organisation; every other label is the
vault's own, and manent lint --audiences tech,business,product closes the set: an unknown
label is reported, and until fixed it makes a note less visible, never more, because no scope
names it.
The index is a note too, and it is where a per-note perimeter leaks. An index or a MOC
carries the title and the one-line description of everything it points at, which for a memory
note is most of the content: a reader who cannot open the target still gets the map of it. Lint
reports it (audience-leak) — a document that summarizes others inherits the narrowest audience
among them. An index serving two audiences splits; it does not widen.
Audit: --audit <file> appends one JSONL line per tool call: timestamp, identity, tool,
redacted arguments, result names, elapsed. Enough to reconstruct an incident; not enough to
reconstruct a customer.
npm run test:acl exercises all of it: scoped search / list / grep / raw read / neighbours,
quarantine writes, the gate, the approval round-trip, OAuth as an agent, and both protocol eras.
Writes: the gate, quarantine and approval
Write tools are off unless the server starts with --writable, and not even advertised
otherwise. When they are on:
The gate. Every write is scanned before anything touches the disk. Text that carries personal data (email, phone, IBAN, card, national id) or reads as an instruction aimed at a model ("ignore previous instructions", hidden HTML directives, zero-width characters) is refused with the reason. A vault lives in git and git history is forever, so the check is before storage, not a cleanup afterwards; and a knowledge base that agents with tools read must not be a place where an outsider's words can become instructions.
Quarantine. An agent's write lands in the directory it was granted and nowhere else, stamped
status: quarantine,author: <agent>,audience: [private], whatever the call asked for. Quarantined notes rank below active ones (score halved) and are visible to the owner only, who promotes them withmanent promote(below): a commit with a name on it. The owner's writes keep their folder and audience, and getauthor: owner.Approval. On MCP 2026-07-28 the write does not complete on the first call: it answers
resultType: "input_required"with an elicitation form that shows the note, and completes on the retry that carries the person's confirmation (inputResponses). The request state is a fingerprint of what was proposed, so an altered retry is asked again instead of trusted. The agent proposes, the person confirms, in the standard's own primitive. Clients that cannot ask (the handshake eras, clients without the elicitation capability) fall through: the owner's write goes straight through as before, an agent's goes to quarantine.
Every write is a commit: --git
The principle above — git as the audit backbone, every memory write a commit — was, until
--git, kept by a person: the one who ran manent promote --commit on their own machine. A brain
that lives on a server and is written through MCP has no such person, and what an agent wrote was
a file nobody had committed: gone with the next checkout, sync or disk.
manent serve <vault> --writable --git # each write is committed, authored by its identity
manent serve <vault> --writable --git-push # …and pushed to origin, fast-forward onlyThe author of the commit is the identity that wrote (tech <tech@manent>, owner <owner@manent>),
the committer is whoever runs the server — the repository's own user.name, required at
startup. So git log --author=tech answers "what did tech write this week", and a quarantine write
is a commit whose message says it waits for promotion. A write that changed nothing commits
nothing. A push finds the remote moved on — someone committed elsewhere — and reports it instead
of forcing; the next push after a pull --rebase goes through. A failed commit never undoes a
write that reached the disk: it is reported to the caller, the audit and stderr, and the next
commit sweeps the file up. npm run test:git is the invariant: nothing untracked, ever.
Promotion: the review queue
Quarantine is only half a design: something has to take notes out of it, and if that something is
three YAML edits by hand (status, audience, folder) the one that gets skipped is the audience, so
the note either stays invisible or becomes readable by everyone. manent promote is the other
half — one move, one commit message:
manent promote <vault> # the queue: what is waiting, oldest first
manent promote <vault> --author customer-care # only what one agent proposed
manent promote <vault> --note <name> --dry-run # the whole move, nothing touched
manent promote <vault> --note <name> --audience tech,product --to memory --commitThe queue prints one line per quarantined note: age in days (from created, or the file's mtime
when it has none), name, author, current audience, path, description. Promotion sets
status: active, sets the audience it is given (keeps the one it has when given none), moves the
file out of quarantine/<agent>/ when --to says where, stamps updated, and prints the commit
message a person would have written by hand:
promote(cache-warmup): out of quarantine
Written by tech on 2026-09-01, promoted 2026-09-05.
status: quarantine, now active
audience: private, now tech and product
moved from quarantine/tech/cache-warmup.md to memory/cache-warmup.md--commit stages exactly those paths and commits them in the vault repository; without it the
message is printed for the person to use. What promotion never does is decide: no note leaves
quarantine because a heuristic liked it. It refuses an unknown or ambiguous name, a note that is
not in quarantine, a destination already taken or outside the vault, an audience label that is
not a slug, and private alongside another label — private is the absence of an audience, not
one more of them, and a note carrying both reads as private while being served to everybody
holding the other label. Every refusal happens before anything is written.
Curation: what a vault accumulates
A brain that is written to keeps two things nobody added on purpose: the same thing said twice, and
two notes that disagree. Both cost the reader, and both are judgements about meaning — so
manent curate reports and never resolves.
manent curate <vault> # all three reports
manent curate <vault> --duplicates --dense # by meaning, not by shared words
manent curate <vault> --communities --gaps gaps.sqlite # groups without a map, by demand
manent curate <vault> --contradictions --jsonNear-duplicates. Two notes saying the same thing rank lower than either would alone: the ranker
splits the evidence between them, and the reader who finds one never learns the other exists. The
default comparison needs no model — word triples, scored by the larger of Jaccard and containment
(how much of the smaller note sits inside the bigger one), with containment allowed to speak only
when the two are within a factor of four in size, and pairs of very short notes dropped, because a
one-line note shares a sentence with anything. --dense compares meaning instead, using the same
embedding model as the ranker.
Both thresholds are measured on a 488-note vault, not guessed:
median | p99.9 | max | default | pairs reported | |
lexical (word triples) | 0.005 | 0.214 | 0.40 | 0.25 | 6 |
dense (whole-note cosine) | 0.851 | 0.933 | 0.973 | 0.95 | 35 |
The dense numbers say something worth saying out loud: this model puts everything one person wrote about one job in a narrow band, so at those scores the dense list is topic twins — a handoff and the retro of the same day, a project note and its retro — while the lexical list is where the actual copies are. Read them as two different questions.
Communities, and the maps that are missing. A vault's folders are one taxonomy, chosen once;
its links are another, made a note at a time by whoever was writing, and it is the honest one.
--communities finds the groups the links form (Louvain modularity optimisation, plus the guarantee
that motivated Leiden: a community whose subgraph is disconnected is split into its components) and
says which of them already has a map of content, so the ones without are where writing one would
pay. Nothing is random and node order is fixed: two runs give the same answer, because a suggestion
that moves between runs is one nobody can act on.
Two things had to be measured rather than assumed:
Index and MOC notes are held out of the graph. An index links every note by design. Leaving it in gave one community of 437 notes and a modularity of 0.000 on the reference vault. Held out, the same vault yields 15 communities, modularity 0.775, and the maps come back at the end to say which community is covered.
What "covered" means. A real MOC links a small share of a community — 12 members out of 45 in the best case here — so "covers a third of it" reports nothing. What separates a map of a subject from a general one is where its own links land: a MOC belongs to the community holding most of them, provided that is at least a quarter of what it links. The vault's personal MOC, 22 links spread over five communities at four apiece, is a map of none of them. On the reference vault that leaves 10 of 15 communities with no map — the work list.
With --gaps <register> the report is ordered by demand instead of size: each community carries the
number of open gaps whose best answers landed inside it, so the suggestion at the top is the subject
people ask about and the vault fails to answer. That is the roadmap's "fed by the gap register's
numbers, not by intuition", and it is the difference between a clustering toy and a work list.
Contradictions. Only what is checkable without understanding the notes: a pair where both
declare contradicts (a work item until one is deprecated or a note reconciles them); a one-sided
declaration, where the reader who arrives at the other note never learns there is an argument; and a
note that supersedes another which is still active — the decision was made and the vault was not
told, so the replaced note still ranks and still answers. Semantic contradiction detection is
deliberately absent: it would be a guess dressed as a finding, and the duplicate report already
surfaces the pairs worth looking at.
The gap register
To learn from the people asking, the brain does not need their text; it needs to know which
questions it could not answer. --gaps <sqlite file> records every brain_search, redacted,
into a file outside the vault (a gap is not a fact; and unlike the embedding cache it is
observed, not derived; losing it loses weeks of real questions). Two signals carry the weight:
followed: a search that no
brain_readof one of its results follows, from the same identity within ten minutes, is a search that did not help. The server sees that alone.count: rows group by meaning through the ranker's own embedding model, so paraphrases collapse into one line; by normalized words until the model is warm. Threshold 0.9, measured on
multilingual-e5-small: paraphrases scored 0.908 to 0.943, unrelated questions 0.752 to 0.835 (--gaps-thresholdto tune).
The caller recorded is the agent, never a person: the register is free of personal data by construction, not by discipline. It is a queue, not a memory; its job is to be emptied:
manent gaps <vault> --gaps gaps.sqlite # open gaps, by asked − read
manent gaps <vault> --gaps gaps.sqlite --show g_<id> # the searches behind one
manent gaps <vault> --gaps gaps.sqlite --close g_<id> --note <name> --golden eval/golden.json
manent gaps <vault> --gaps gaps.sqlite --dismiss g_<id> # not a real question
manent gaps <vault> --gaps gaps.sqlite --feedback # what agents reportedClosing a gap with the note that answers it emits a golden-set entry: the question as the
asker phrased it, the note the curator wrote: an oblique query by construction. That is the
set the eval is weakest on (MRR 0.208) and the one that had to be written by hand imagining how
someone else would ask. The register manufactures it from real use, and the regression gate then
keeps every closed gap reachable when the ranker changes.
The register sees what was missing. It cannot see "there, but wrong": a confident wrong answer
arrives with a high score and a read, and looks like a success. Only the agent or the person can
say so; that is brain_feedback (verdict, note, searchId, outcome), filed next to the question
it came from.
Hot reload
manent serve watches the vault and re-indexes what changed: lexical index and graph rebuilt,
dense vectors re-embedded only for notes whose content hash moved, per-identity views
invalidated. Edits are coalesced (a git checkout touching hundreds of files costs one reload)
and reloads are serialized. Measured: a new file is searchable ~530 ms after the write. A
post-receive hook that checks out the shared branch is all a git-backed brain needs to serve
what was just pushed. --no-watch turns it off.
Deploy: a git-backed brain
The vault is a git repository, so deploying it is a push. deploy/ carries the layout that has
been running, and the reasoning: a bare repo whose pre-receive hook is the lint gate (a note
with personal data or model-directed text is refused at the push, not found later in a history that
is forever), a post-receive that checks the tree out and restarts nothing — the watcher
re-indexes it in about half a second, and a restart would throw away the dense index and every warm
view — a systemd unit, and an agents.json example.
The one line worth repeating here: the gap register and the audit log live outside the vault, under
/var/lib/manent, and they are the part no git push restores. Back that directory up
separately. Details in deploy/README.md.
Retrieval, measured
Ranking changes are decided by an eval harness, not by intuition. manent eval scores a
retriever against three kinds of query, and each kind answers a different question:
Source | How it's built | What it measures |
| hand written, wording close to the note | lexical recall |
| asks for the concept without the note's words | semantic recall, the hard case |
| derived from each note's own description | broad regression signal, no labelling |
manent eval <vault> --golden eval/golden-aios.json # bm25 vs hybrid side by side
manent eval <vault> --golden ... --save baseline.json # record a baseline (metrics only: safe to commit)
manent eval <vault> --golden ... --baseline baseline.json # exits 1 if a metric dropped
manent eval <vault> --golden ... --save full.json --save-full # with per-query results: names every note, keep it privateResults on a real 305-note vault (298 queries), in the order they were measured:
Ranker | curated hit@1 | curated MRR | oblique MRR | auto hit@1 |
BM25, naive tokenizer | 45.0% | 0.621 | n/a | 97.8% |
BM25 + stopwords, length-gated prefix/fuzzy | 75.0% | 0.863 | 0.099 | 97.8% |
Hybrid (graph expansion + recency + centrality) | 75.0% | 0.863 | 0.104 | 93.0% |
Dense only (multilingual-e5-small, local) | 95.0% | 0.975 | 0.131 | 94.4% |
Fused, lexical 1 : dense 2 (RRF) | 100.0% | 1.000 | 0.208 | 95.9% |
Five findings worth keeping:
Tokenization was the first big win. Dropping stopwords and allowing prefix/fuzzy matching only on longer terms moved curated hit@1 by 30 points. With prefix matching on,
dimatches diritto, disposizione, documento; long notes then win on accumulated noise.Graph expansion did not pay. Once retrieval is lexically sound, Personalized PageRank over wikilinks adds nothing measurable and the recency/centrality multipliers cost ~5 points on the auto set.
hybridstays available for vaults with a much denser link structure. PPR amplifies a good seed; it cannot create one.Lexical and dense fail in opposite directions, so fusing them beats both. Dense alone found the notes BM25 missed but blurred exact slugs and identifiers; at equal RRF weights the lexical list pulled correct answers off the top spot. Weighting dense twice reached 100% hit@1 on hand-written queries, trading ~2 points on the synthetic set.
Vocabulary mismatch is improved, not solved:
obliqueMRR went from 0.099 to 0.208 and recall@5 from 25% to 37.5%. A question whose wording shares nothing with its note is still often unreachable, which is what the gap register is for.Chunking made it worse here, and that is informative. Splitting notes into passages was the obvious next step; measured, it cost 10 to 15 points of curated hit@1. With ~2400 passages instead of 307 notes, max-scoring gives a long note one chance per passage to match by luck, so retros and legal texts float up, the same length bias BM25 normalizes away. Damping by passage count (
max-norm) recovers oblique recall (MRR 0.221) but still trades away curated and auto accuracy.
Passages | curated hit@1 | oblique MRR | auto hit@1 | Configuration |
307 | 95.0% | 0.133 | 95.9% | one passage per note, full body |
2419 | 90.0% | 0.096 | 96.3% | 1000-char passages, best-passage scoring |
2419 | 85.0% | 0.221 | 91.9% | 1000-char passages, length-damped |
1181 | 75.0% | 0.013 | 95.9% | 2000-char passages, best-passage |
1181 | 55 to 75% | at most 0.19 | 73 to 79% | any size, without the contextual prefix |
Two things to keep from that: the contextual prefix is not optional: a passage stripped of
its note's name and description loses the subject and everything collapses; and truncation
beats completeness on this corpus (1400-char single passage scored 100%, full body 95%).
Notes here are atomic and front-loaded, one fact each, stated at the top, so the tail is
elaboration that only blurs the vector. Raise maxPassages for vaults of long, multi-topic
documents, where the answer can sit in the middle of a note.
Every ranker is wrapped by status demotion: quarantine and deprecated notes keep half their score,
archived a quarter, on the server and in the eval alike, so what is measured is what ships. On a
vault with no such notes the wrapper is a no-op (regression gate: no change).
The September 2026 re-measurement on the grown vault (479 notes) is in paper/; it moved some of
these numbers, and the paper says how.
Reproduce any sweep: scripts/tune-retrieval.mjs (graph/scoring params), scripts/tune-fusion.mjs
(lexical/dense balance), scripts/tune-chunking.mjs (passage size, prefix, aggregation).
Dense retrieval setup
Embeddings run locally: no API key, nothing leaves the machine, and query time needs no
network. The model is an optional dependency, so bm25 keeps working without it:
npm install @huggingface/transformers # ~120 MB model, downloaded on first use
manent serve <vault> --retriever fused # or --retriever dense
manent eval <vault> --golden ... --retriever allVectors are cached in <vault>/.manent/embeddings.json, keyed by content hash: editing one note
re-embeds one note. Changing the model invalidates the cache. manent init gitignores that
directory; it is derived data, rebuildable from the notes.
Lint as the gate
manent lint <vault> # what a human should look at
manent lint <vault> --strict-content --audiences tech,business,product # what CI refusesBeyond schema, links, duplicates and orphans, lint reports personal data in a note, text that
reads as an instruction to a model, audience labels outside the vault's set, and an
index that names notes its own readers cannot open. They are
warnings for a person and, with --strict-content, errors for a pipeline. Run the strict form as
a pre-commit hook or in CI on the brain repository: a note that fails it never lands in the
shared branch, which is the only place the server reads from.
MCP Apps: three views on the vault
Three of this server's answers are ones a person works through rather than reads: the notes agents
wrote that nobody has promoted, the questions the brain could not answer, and the shape of the
links around a note. As text in a transcript the first two are a wall to skim and the third cannot
be written down at all. So the modern path implements io.modelcontextprotocol/ui (MCP Apps, final
2026-01-26) and ships three pages:
Resource | Tool it lays out |
|
|
|
|
|
|
They are served as text/html;profile=mcp-app from resources/list / resources/read, and each
tool points at its page with _meta.ui.resourceUri. A host without the extension ignores the
metadata and shows the same JSON — the page is a way to read the answer, never the only way to get
it.
Two properties make them safe to render inside somebody's chat, and both are tested:
Nothing is loaded from anywhere. No script
src, no stylesheet, no font, nofetch. The page is the resource; its declared CSP allows no connect domains at all, because it needs none. Data arrives from the host: the tool result that opened the page, or atools/callthe page asks the host to make when you press Refresh.The pages decide nothing. They read and they re-ask. Promoting a note and closing a gap are writes, and writes go through the gate and the person's confirmation — never through a button a page could press on its own. Each row instead carries the command that would do it.
brain_gaps is the owner's: one agent does not get to enumerate what the others asked. The
quarantine queue needs no such rule — quarantined notes are private, so an agent's view never
contains them.
The graph page draws itself: a force layout in about sixty lines, no library, no randomness — the same neighbourhood produces the same picture every time, because a drawing that moves between runs teaches nothing. Two numbers made it a graph instead of a rim of dots: repulsion is cut off past two and a half ideal spacings (without that, 40 of 60 nodes ended clamped on the border; with it, 6), and linked notes land a mean 59 pixels apart against 266 for any pair. Clicking a node opens it, "Centre on it" walks the neighbourhood, and "Ask about it" hands the conversation a question — the only thing a page here may do besides read.
Long-running calls: the tasks extension
Every brain tool answers in milliseconds except the one that reads the whole vault at once:
brain_curate compares every note against every other, so its cost grows with the vault and not
with the question. A vault of a few hundred notes finishes in a second; a few thousand does not,
and no client holds a request open that long.
So on the modern path Manent implements io.modelcontextprotocol/tasks (SEP-2663). A client that
declares the extension in _meta:
{ "io.modelcontextprotocol/clientCapabilities": { "extensions": { "io.modelcontextprotocol/tasks": {} } } }gets a handle instead of a held connection — {"resultType": "task", "taskId": …, "status": "working", "ttlMs": …, "pollIntervalMs": …} — and polls tasks/get until the status is terminal, at which
point the task carries the same result the inline call would have returned. tasks/cancel ends one;
tasks/update exists and says plainly that it does not apply here, because this server's only
question — confirming a write — is answered on the call itself. A client that declares nothing waits
for the answer exactly as before: the extension is an option the client takes, not a change to the
contract.
Three properties, all tested in npm run test:tasks:
A task belongs to the identity that created it. Another identity asking is told it does not exist. That is the same reason the extension has no
tasks/list: without a session there is no safe way to enumerate what someone else started.A task runs on its creator's view of the vault. An agent curating sees only the notes its audience allows, so a duplicate pair it may not read is not a pair it is told about.
Cancellation is cooperative and never invents a result. A task that had already finished stays finished, and work that completes after a cancel does not resurrect it.
Protocol eras
MCP revision 2026-07-28 removed the initialize handshake and sessions; every shipping client
still speaks the older, handshake-based revisions. Manent serves both, as two separate
implementations rather than one blended path:
Era | Revisions | How | File |
legacy |
| official SDK, |
|
modern |
| native: no handshake, |
|
Both adapters expose the same tools because the tool definitions live in one place
(src/tools.ts) and every call goes through one dispatcher (audit included); the eras cannot
drift in capability. src/http.ts only routes.
manent serve <vault> --http 3939 # auto: routes each request to its era
manent serve <vault> --http 3939 --era legacy # pin: modern requests get the fallback signal
manent serve <vault> --http 3939 --era modern # pin: 2026-07-28 onlyAuto-detection keys on the RPC itself (server/discover, subscriptions/listen) or a declared
2026+ protocol version, never on the Mcp-Method transport header alone, since dual-era clients
send it with a legacy initialize too. npm run test:era exercises all three modes.
Vault layout (see packages/spec/SPEC.md)
vault/
├─ MEMORY.md # global index, loaded every session (type: index)
├─ HOME.md # human navigation hub (type: moc)
├─ memory/ # global lessons: feedback_*.md, reference_*.md
├─ projects/<p>/ # per-project knowledge, handoffs, retros
├─ moc/ # one map-of-content per branch
├─ people/ # person notes (type: persona)
├─ library/ # raw dated sources: YYYY-MM-DD-<slug>.md
├─ wiki/ # compounding wiki: entities/ concepts/ queries/
├─ knowledge/ # read-only mirrors from external repos
├─ quarantine/ # agents' proposals, private until a person promotes them
└─ secrets/ # NEVER synced, NEVER indexed (enforced)Tests
npm run smoke # init, lint, search
npm run probe # stdio: initialize, tools/list, brain_search
npm run test:era # both protocol eras, pinned and auto-routed
npm run test:oauth # the whole OAuth flow and its refusals
npm run test:write # write tools: gate and path containment
npm run test:links # wikilink resolution by name, path and file name
npm run test:gaps # gap register: redaction, grouping, follow, close, feedback, CLI
npm run test:acl # identities, visibility at load, quarantine, approval, audit
npm run test:reload # hot reload: add, edit, delete, bursts
npm run test:promote # promotion: the queue, the refusals, the move, the commit
npm run test:curate # curation: duplicates, contradictions, communities, and the vault it must not touch
npm run test:tasks # tasks extension: handle, polling, ownership, cancel, ttl
npm run test:apps # MCP Apps: declaration, linkage, self-contained pages, the layout, what they are shown
npm run test:warmup # dense ranker warms up in the background
npm run lint:fixture # the lint gate on eval/fixture-vault, content rules strict
npm run eval:fixture # retrieval regression gate on eval/fixture-vault
npm run check:published # nothing tracked here names a private note, a disk or a credential
MANENT_VAULT=<vault> npm run eval:gate # the same gate on your own vault; baselines are metrics-only filesA golden set is written against a real vault, so the names it expects are an index of what that
vault holds. eval/golden-aios.json therefore publishes them by hash (note:<sha256 truncated>):
the queries stay readable, because they are the measurement, and the names resolve only against the
vault that has those notes — where the run and its miss list read exactly as before. npm run check:published holds the line in CI: no private note name, no absolute path from anyone's disk, no
credential, and no personal data outside the few files that exist to test for it.
CI (.github/workflows/ci.yml) runs all of it on node 20 and 22 except test:warmup, which would
download the embedding model, and npm pack --dry-run on every package, so a release ships what it
means to. The gates run on eval/fixture-vault: nineteen invented notes — an invented hosting
provider, an invented project — because a corpus a public CI can read is a corpus nobody wrote
their real memory into. Its numbers have the shape of the real thing (curated hit@1 91.7%, oblique
0%, eval/baseline-fixture-bm25.json), so a change that breaks retrieval breaks the gate.
Roadmap
Done, in the order it was built:
Spec v0.1 + lint + graph + BM25 search + MCP stdio server
Eval harness: three query kinds, recall@k / MRR / nDCG, regression gate
Lexical retrieval done properly (stopwords, length-gated prefix/fuzzy): +30 pts hit@1
Graph expansion (Personalized PageRank) + RRF fusion: built, measured, not default
Local dense embeddings + RRF fusion: curated hit@1 from 75% to 100%, oblique MRR from 0.10 to 0.21
Chunk-level embeddings: implemented, measured, not default; worse on atomic notes
Streamable HTTP transport, stateless, bearer-token auth
OAuth 2.1 (RFC 9728 metadata, dynamic registration, PKCE); connects from claude.ai
Two protocol eras as separate implementations: legacy handshake (SDK) and native 2026-07-28
Read tools beyond search:
brain_list,brain_read_raw,brain_grepWrite tools, off unless the operator opts in
Wikilinks resolved by name, path and file name, so the vault reads the same in Obsidian and here
Gap register: unanswered searches become a work list by frequency, then
obliquegolden-set entries on closureSpec v0.2:
audience,author,status: quarantine;statusconsumed by rankingIdentities (
--agents), visibility filtered at load, per-identity views on every toolWrite gate (personal data, model-directed text), quarantine for agents,
--auditWrite path behind approval:
resultType: "input_required"on 2026-07-28, fingerprinted retryVault hot reload: watch, coalesce, re-embed only what changed
brain_feedback: "there, but wrong", filed next to the questionLint gate for CI:
pii,injection,audience-unknown,audience-leak,--strict-content--git: every write is a commit authored by its identity, committed by the server's user;--git-pushpushes fast-forward only and reports a remote that moved on instead of forcingPromotion tooling:
manent promote: out of quarantine with status, audience, folder and a commit message in one move; a review queue of quarantined notes by age and authorGitHub Actions: build, every test, the lint gate and the retrieval gate on a public fixture vault, on two node versions;
npm pack --dry-runon every packageGit-backed deploy recipe (
deploy/): the lint gate as apre-receivehook, checkout onpost-receivewith no restart, systemd unit, identities and audit paths as a server layoutCuration (
manent curate): near-duplicate pairs, lexical or dense, thresholds measured on a real vault; contradictions surfaced (declared, one-sided, a superseded note left active); link communities with the maps of content they are missing, ordered by the gap register's numbers. Reported, never resolvedTasks extension (
io.modelcontextprotocol/tasks, SEP-2663) on the modern path: a call whose cost grows with the vault comes back as a task to poll, owned by the identity that started itMCP Apps (
io.modelcontextprotocol/ui): the review queue, the gap register and a graph explorer — self-contained pages that load nothing and write nothing, each bound to its tool
Next, in the order it should be built:
npm publish (the packages carry their publish metadata and the CLI is named
manent; the release itself is a person's gesture, with their own token)MCP spec 2026-07-28 wire upgrade on the legacy path, when the official SDK ships it
Open questions the code does not settle: who owns the brain infrastructure once four agents
depend on it; which audience labels a given organisation wants (the spec reserves private and
public and leaves the rest to the vault, on purpose).
License
Apache-2.0; see LICENSE.
This server cannot be deployed
Maintenance
Related MCP Connectors
An MCP server that gives your AI access to the source code and docs of all public github repos
Markdown-based note-taking with a hosted MCP server. Your notes serve you and your AI.
One memory, every AI. A shared, user-owned markdown memory your AI clients read and write over MCP.
Token-efficient MCP memory for Markdown vaults. Tiered search, GraphRAG, AI memories.
Related MCP Servers
- AlicenseAqualityAmaintenanceA self-hosted MCP server that gives AI agents shared, long-term memory over a git-backed folder of markdown, enabling persistent knowledge search, read, and write without a database.1626 npm11MIT
- AlicenseNot gradedqualityAmaintenanceAn MCP server that gives AI agents durable, temporal memory over local markdown vaults, with tools for searching, asserting facts, querying point-in-time state, and reinforcing useful knowledge.380 npmMIT
- AlicenseAqualityBmaintenanceMCP server that exposes an Obsidian-style markdown vault as a shared memory for AI agents, with tools for searching, reading, writing, and querying notes and wiki-links.123MIT
- AlicenseNot gradedqualityCmaintenanceAn MCP server that exposes an Obsidian-style markdown vault as agent-readable memory. It provides tools for search, read, graph, and guarded capture without using embeddings.1MIT