loci-mcp
This MCP server lets an AI agent manage a local Markdown-based Loci knowledge vault: configure it, create/read/update/delete items, link and query them as a graph, manage ontology, assets, history, and inbox imports.
Configure or adopt the vault root and generate new item UIDs.
Create, read, patch, and permanently delete vault items stored as Markdown with YAML frontmatter, including duplicate checks.
Manage typed relations and validate types, relations, fields, and asset schemes against canonical and user registries.
List, search, filter, and resolve vault nodes, mentions, neighbors, related nodes, shortest paths, recent items, and cross-domain insights.
Inspect per-node history, show diffs, read old versions, restore nodes, and undo the last vault change.
Save and resolve binary/rich-file assets, find unregistered asset files, and scan directories for nodes to register.
Run staged inbox imports: scan, plan, approve, execute file/content batches, and update item statuses.
Read and write meta files and user skill additions; fetch URL metadata for bookmarks.
Define and validate new user types and relations that extend the vault ontology.
Lint the vault for schema violations, dangling links, duplicate UIDs, tag issues, and other warnings.
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., "@loci-mcp Save this article to my vault: https://example.com/post"
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.
Loci
A personal knowledge graph that you own. Loci keeps your notes, articles, books, tasks, people, and conversations as plain Markdown files in a folder that you choose. Your AI agent reads and writes these files through an MCP server. A local web app shows them as a graph.
The problem. Your knowledge is in many apps. The apps do not link to each other. Your AI assistant keeps its memory on the vendor's servers, in the vendor's format. When you search, you find only the note that you already remember.
What Loci does.
Keeps everything in one place, as files. Each item is one Markdown file with YAML frontmatter. You can open,
grep, and commit the files with no other software. If you delete Loci, the files stay.Links items with typed relations. Each item has a stable 8-character ID, a type (
article,task,entity, …), and typed links to other items (is-based-on,contradicts,supersedes, …). Links use IDs, so a rename does not break them.Holds your agent to your vocabulary. The types, fields, and relations are in versioned registries. Loci rejects an unknown type or relation on write. Your agent reads the registries before it writes.
Points at files. It does not move them. An item can record where a file is: on your disk, in Google Drive, at a URL, in Gmail, and more.
Records each change. Loci keeps a history of every write, also of edits that you make by hand.
Who it is for. You use an AI agent every day (Claude Code is fully supported). You want one place for what you read, write, and decide, in a format that no platform owns.
Ask questions that a folder cannot answer: what do I know about stoicism? returns the books, the reading logs, the notes, the people, and the links between them.

Status. Work in progress, and a personal project. The author uses the core every day. Some parts are not finished. Status and roadmap tells you which parts.
Quick start
Requirements: macOS, Python 3.13+ and uv. Loci supports macOS only for now. The author uses it on macOS. CI runs the tests on Linux, but Linux is not a supported platform, and Windows is not tested.
1. Install from PyPI
uv tool install loci-vaultThe PyPI package is
loci-vault, notloci. The namelocion PyPI is an unrelated project. Do not runuv tool install lociorpip install loci. What you install isloci-vault; the commands it gives you arelociandloci-mcp.
2. Connect it to your agent
loci setupThis installs the skills and registers the MCP server in one step. If Loci
finds one agent (Claude Code or Hermes), it uses it. If it finds several, it
asks you which. To skip the question, add --target claude,
--target hermes, or --target all. It is safe to run again.
Prefer the two halves? loci skills installs the skills and loci register
registers the server; loci setup is those two with one question.
For other agents, see Agent support.
3. Choose a vault folder
Restart Claude Code. Then tell it where the vault must be:
Point Loci at ~/loci/vaultUse a folder that is one level inside a folder of its own, as in this example.
Loci puts two more folders next to the vault: assets/ (files that you add)
and loci-stage/ (an inbox for batch import). With ~/loci/vault, all three
are in ~/loci/. With ~/vault, they go directly into your home folder.
4. Use it
Save this article to my loci vault: https://example.com/post
What does my loci vault know about stoicism?To browse the vault, start the web app:
loci # opens on http://127.0.0.1:8765Command | What it is |
| The web UI and its HTTP backend. Change the address with |
| The stdio MCP server. Your agent starts it. Do not start it yourself. |
For upgrades, a checkout install, and sync, see docs/setup.md.
Related MCP server: Zettelkasten MCP Server
Agent support
Loci is a standard MCP server. Any agent that supports MCP can use it.
Agent | Status |
Claude Code | Fully supported and tested. The steps above install everything. |
Claude Desktop (macOS) | Supported. |
Hermes | Supported. |
DeepSeek Harness and other agents | Untested. Manual setup, or ask the agent to set itself up. Steps. |
Codex and other MCP clients | Expected to work. Not tested. Do the steps below. |
For an agent other than Claude Code:
Register the MCP server with your agent. For Codex:
codex mcp add loci -- loci-mcp. For other agents, see their MCP documentation. The command to start isloci-mcp.If your agent supports skills (
SKILL.mdfolders), install the skills into its skills folder:loci skills --dir <folder>. For Codex, the folder is usually~/.codex/skills.If your agent does not support skills, add this line to its instructions: "Before you save to or read from Loci, call the
read_skilltool of thelociMCP server and follow the result."
Your data and the model. The vault stays on your machine. When your agent reads a note to answer you, the agent sends that note to its model provider, as with any other file that the agent reads.
Documentation
Use Loci
Why Loci exists, and why a Markdown folder or a PKM app alone is not sufficient | |
Install from PyPI, a release wheel, or a checkout. Upgrade. Sync and backup | |
One prompt for each task: save, find, update, extend the ontology | |
The mental model, saving, querying, the web UI, and the vault layout | |
Types, relations, schemes, and how the registries are versioned | |
What is stable, what is partial, what is next |
Work on Loci
Licence, CLA, and the rules for a pull request | |
Repo layout, daily commands, releases | |
The unit suite and the browser e2e suite |
Reference
How a file on disk becomes a vault node | |
What each install and upgrade path does, step by step | |
Components and data flow |
Licence
PolyForm Noncommercial 1.0.0. Noncommercial use is free: personal use, research, teaching, and charities, including changes and redistribution. Commercial use needs a separate licence. To get one, open an issue.
Loci is source-available, not open source. The licence covers this software, not the notes that you write with it. Your vault is yours.
Contributing
Read CONTRIBUTING.md. Each pull request needs a signed Contributor Licence Agreement. You sign one time, and you keep your copyright.
About the name
Loci /ˈloʊsaɪ/ (LOH-sy, or LOH-kee as a Roman said it) is Latin for "places," the plural of locus. The name comes from the method of loci: a classical memory technique. You put each thing that you want to remember in a place that you know well, such as a room or a road. To remember it, you walk back through that place. Loci gives your digital life the same kind of space: each item has a fixed place, and you find it by its links and its path.
Available Tools
39 toolsapprove_inbox_planA
Approve the pending inbox plan to enable execution. Must be called after scan_inbox() and before execute_inbox_file_batch() or get_inbox_content_batch(). Returns the approved plan summary.
Inputs: no named parameters.
check_duplicateA
Check whether a similar vault item already exists. Returns {"duplicate": false} or {"duplicate": true, "path": "...", "name": "..."}. write_vault runs this check itself and refuses on a hit, so a separate call is only needed to look before building the node — e.g. to decide between a new save and an update before drafting content.
Inputs: type (required), title (required), url.
define_relationA
Define a new user relation and append it to relation-dictionary-user.yaml. Returns {"defined": true, "name": str, "warnings": [...]} on success, or {"defined": false, "errors": [...], "warnings": [...]} if validation fails.
Rules enforced before writing:
name must not collide with any canonical or existing user relation
If inverse_of is provided and names an unknown relation, a warning is returned (not an error) — the inverse may be defined in a subsequent call.
Every relation needs an inverse or must be symmetric, or lint_vault warns. If inverse_of names a user relation with no inverse, that relation is updated to name this one back. A relation cannot be both symmetric and have inverse_of.
Args: name: New relation name (lowercase, hyphenated) description: What this relation means; include directionality in the wording group: Optional group label (e.g. "entity", "semantic", "custom") inverse_of: Optional name of the inverse relation symmetric: True when the relation is its own inverse (A relates B means B relates A)
Inputs: name (required), description (required), group, symmetric, +1 more.
define_typeA
Define a new user type and append it to ontology-user.yaml. Returns {"defined": true, "name": str, "extends": str} on success, or {"defined": false, "errors": [...]} if validation fails.
Rules enforced before writing:
name must not collide with any canonical or existing user type
extends must name a canonical root type (not a user type, and not
skill, which extendsprocedure)all user-defined fields are optional; inherited fields cannot be redefined
field type must be one of: string | array | datetime | enum | boolean
Args: name: New type name (lowercase, hyphenated) extends: Canonical root parent type (document, article, task, procedure, etc.) description: Short description of what this type represents fields: Optional extra fields: [{name, type, values?}]. Omit 'values' for non-enum fields.
Inputs: name (required), extends (required), description (required), fields.
delete_vaultA
Delete a vault item permanently.
Args: path: 8-character hex UID (e.g. "0b74b77c"), absolute path, or path relative to vault_root (e.g. "notes/my-note.md")
Always confirm with the user before calling this. Deletion is irreversible.
Inputs: path (required).
diff_vaultA
Show the change one commit made to a node, as a unified diff: lines starting with "-" were removed, "+" were added. Get the commit hash from history_vault. Returns {diff} or {error}.
Inputs: path (required), commit (required).
execute_inbox_file_batchA
Execute the next batch of file-category items from the approved plan. Processes up to budget_per_run.files items. Each item: reads file from loci-stage/, saves as an asset vault node (type=asset, subtype=book), deletes the staging file, updates plan status.
An item whose title already exists in the vault is not saved: it is parked
with status duplicate, its staging file kept, and reported under
duplicates with the existing node's uid and path. The plan stays open
until each is resolved with update_inbox_item: status="skipped" to drop
it, or status="pending" with allow_duplicate=True to save a second copy
on the next batch.
Returns {processed: [{path, uid, title, tag_warnings?}], failed: [{path, error}], duplicates: [{path, title, existing_uid, existing_path, existing_name}], remaining: int}. Plan must be approved before calling.
Inputs: no named parameters.
fetch_url_metadataA
Lightly fetch a URL and extract title, site name, and description. Use this for bookmark saves — does not return full page content. If the user wants full content extracted, use the article type instead.
Inputs: url (required).
find_nodesA
Find vault nodes matching any combination of filters. At least one should be provided. Each entry: {uid, title, type, subtype, path, state, status, due_date, language}.
Args: type: Node type (task, document, project, article, asset, …) tag: Tag value — node must include this tag status: Exact status match (todo, done, active, …) subtype: Exact subtype match — task: bug|feature|question|chore; document: meeting-notes|report|spec|plan|transcript|essay|journal|note; article: the medium (book, paper, …); entity: person|organization|place; asset: book|video|music|podcast|presentation|visual|physical state: Lifecycle state filter — "active" (default) excludes archived nodes; pass "archived" to see only archived nodes, or None to see all. due_before: ISO date (YYYY-MM-DD) — nodes whose due_date <= this value due_after: ISO date (YYYY-MM-DD) — nodes whose due_date >= this value created_after: ISO date — nodes created on or after this date updated_after: ISO date — nodes updated on or after this date related_to: UID — only return nodes connected to this node by any edge language: Language code (en, ru, de, …) has_asset: True to return only nodes with an asset; False for nodes without has_asset_scheme: scheme name (e.g. "google-drive") — return only nodes with a has_asset location using that scheme (case-insensitive)
Inputs: tag, type, state, status, +9 more. Returns: result
find_unregistered_assetsA
Find files in assets/ that have no registry entry. Returns a list of relative paths (e.g. ["assets/abc123.pdf"]). Use this to surface ghost files that were dropped into assets/ without going through save_asset.
Inputs: no named parameters. Returns: result
generate_uidA
Generate a random 8-character hex UID for a new vault item.
Inputs: no named parameters. Returns: result
get_inbox_content_batchA
Return the next batch of content-category items for Claude to process. Includes file content for text files. Claude should classify, summarize, and call loci__write_vault for each, then call update_inbox_item() to mark each as done.
Returns {items: [{path, type, content, ...}], remaining: int}. Plan must be approved before calling.
Inputs: no named parameters.
get_inbox_planA
Return the current inbox plan and progress. Returns {status: 'no-plan'} if no plan exists. Includes: status, budget_per_run, progress, next items in each category.
Inputs: no named parameters.
history_vaultA
List the commits that changed a node, newest first: {commits: [{commit, author, date, subject}]}. Every vault write is one commit.
Accepts a UID, absolute path or vault-relative path. A UID also works for a node that was deleted: history finds the file it last lived in.
Use this to find what happened to a node — for example when content is missing after a write — then diff_vault to see the change and restore_vault to undo it.
Inputs: path (required), limit.
lint_vaultA
Lint all vault files against the Loci schema. Returns errors (schema violations, unknown types, bad uids, duplicate uids) and warnings (dangling relations, orphaned nodes, empty summaries; a tag that names a project/entity/topic node and should be a relation; a tag that repeats the node's own type or subtype; vault-wide near-duplicate tag spellings). stats.tags carries the tag census: distinct count, uses, the tags used only once, and the near-duplicate groups. Run this after manual vault edits or when debugging unexpected behavior.
Inputs: no named parameters.
list_vaultA
List items in a vault type folder. Omit type to list all folders. Returns {type_name: [filename, ...], ...}. If meta files were auto-updated at startup, result includes a "_notice" key.
Inputs: type.
patch_vaultA
Patch frontmatter fields on an existing vault item. updated_at is always bumped.
Use this to correct or extend an existing item — e.g. add/remove relations, update tags, change status — without rewriting the whole file.
Workflow:
Call read_vault(path) to get the current content.
Parse the fields you want to change and construct their new values in full (e.g. the complete new relations list, not just the items to add).
Call patch_vault(path, updates={...}) with only the fields that changed.
Args: path: 8-character hex UID (e.g. "0b74b77c"), absolute path, or path relative to vault_root (e.g. "notes/my-note.md"). Prefer the UID when you have it — no lookup call is needed. updates: Dict of frontmatter fields to overwrite. Pass complete new values. Common uses: relations: [{uid: "a1b2c3d4", type: "belongs-to"}, ...] tags: ["tag-a", "tag-b"] status: "done" summary: "Updated summary." Do NOT put the body here — 'body' is not a frontmatter field. A 'body' key in updates is rerouted to the body argument and a warning is returned; a null value there clears the body. Any stale 'body' key already in the file's frontmatter is stripped on every patch. body: If provided, replaces the full body text. Always pass new body text here, never inside updates. Pass "" to clear it.
Returns {uid, path, type, title, ...}, plus tag_warnings when a tag names a project/entity/topic (belongs as a relation) or repeats the node's own type or subtype. The patch has already happened; surface the warning and offer the fix. Other warnings, all of which also arrive after the write: relation_warnings (unknown relation type), has_asset_warnings (unknown scheme or a missing scheme field in has_asset), and field_warnings (a field the patch touched now breaks its type's required fields or enum; a change to type or subtype checks the whole node).
Inputs: path (required), updates (required), body.
query_insightsA
Surface vault nodes that bridge different domains. Finds nodes connected to items from domains outside their own — these are the unexpected cross-domain links that reveal non-obvious patterns. Each entry: {uid, title, type, domains, connects_to_domains, degree}.
Args: limit: Maximum number of bridge nodes to return (default 10)
Inputs: limit. Returns: result
query_neighborsA
Return the direct neighbors of a vault node — one hop only. Includes both outgoing and incoming edges with their relation type and direction. Each entry: {uid, title, type, rel_type, direction} where direction is 'in' or 'out'.
Inputs: uid (required). Returns: result
query_pathB
Return the shortest path between two vault nodes as an ordered list. Each entry: {uid, title, type}. Returns an empty list if no path exists.
Inputs: uid_to (required), uid_from (required). Returns: result
query_recentA
Return the most recently updated vault nodes. Use for "what have I been thinking about lately?" queries. Each entry: {uid, title, type, path, updated_at, status}.
Args: days: Look-back window in days (default 30) limit: Maximum number of results to return (default 20) state: Lifecycle state filter — "active" (default) excludes archived nodes; pass "archived" to see only archived nodes, or None to see all.
Inputs: days, limit, state. Returns: result
read_metaA
Read a vault meta file. file: type-dictionary | type-decision | tag-dictionary | field-dictionary | relation-dictionary | relation-dictionary-user | foreign-definitions | ontology | ontology-user | scheme-dictionary | scheme-dictionary-user | ontology-guide | has-asset-guide | update-workflow | config
type-decision — the decision tree for picking a type when the content does not name one. Read with type-dictionary when classifying. ontology — machine-readable canonical type definitions (required fields, optional fields, enum values). Source of truth for structure. ontology-user — user-defined type extensions. Read before writing a node of a user-defined type to confirm the type is registered. relation-dictionary — machine-readable canonical relation types (name, group, description, inverse). Source of truth for relation types. relation-dictionary-user — user-defined relation additions. Read to include custom relation types alongside canonical ones. field-dictionary — behavioral guidance: field descriptions, complex object schemas, archiving patterns, temporal field rules (LLM-readable). scheme-dictionary — canonical location schemes for has_asset fields (scheme names, required/optional fields, capabilities). Read before constructing any has_asset entry to confirm valid schemes and required fields. scheme-dictionary-user — user-defined location scheme additions. ontology-guide — the user's prose guide to the ontology: what each type is for, how relations and schemes work, how to extend them. Read this when the user asks how the ontology works rather than what a specific type requires — for structure, read ontology instead. has-asset-guide — working guidance for has_asset: when to use each scheme, where its identifiers come from, adopt vs. reference. Read before writing or editing a has_asset entry. update-workflow — the procedure for changing an existing node: patch semantics, temporal-field archiving for entities, archive/unarchive, delete. Read when the user wants to update rather than save. config — where Loci is pointed and which version it runs. Answerable with no vault configured, so it also tells you why the other tools are refusing.
Inputs: file (required). Returns: result
read_skillA
Read a Loci skill: the packaged procedure with the user's own additions appended. Follow the returned document for the whole interaction. skill: loci-save | loci-query | loci-inbox | loci-ingest-directory
Answers without a vault too — the procedure is what tells the model to ask for one — but the additions only load once a vault is configured.
Inputs: skill. Returns: result
read_vaultA
Read a vault file. Accepts an 8-character hex UID (e.g. "0b74b77c"), an absolute path, or a path relative to vault_root (e.g. "my-note--0b74b77c.md"). Prefer passing the UID when you have it — the path will be resolved automatically.
With commit (a hash from history_vault), returns the file as it was right
after that commit — also for a node that has since been changed or deleted.
For a deleted node, pass the hash of the commit that deleted it with "1"
added (e.g. "a1b2c3d1") to get the last version before the deletion.
Inputs: path (required), commit. Returns: result
resolve_assetA
Resolve an asset UID to its file path and metadata. Returns {path, mime_type, format, size_bytes}. Use this when you need the physical file location for a vault node with has_asset: true.
Inputs: uid (required), format.
resolve_mentionsA
Match names mentioned in content — people, organisations, projects, topics — against existing vault nodes, so a save links to what is already there instead of creating a second "John Smith".
Pull every proper name out of the content first, then pass them all in one
call. Each name is compared against node titles and aliases, case- and
whitespace-insensitive. Returns:
{
found: [{name, uid, title, type, path, summary, via}, ...],
missing: [name, ...],
}
via is "title" or "alias" for an exact match, or "partial" when the name
only appears as a whole word inside a title or alias (e.g. "Loci" against
"Loci — knowledge vault") — treat partial hits as candidates to confirm, not
as matches. A name listed more than once in found matched several nodes;
ask the user which one they meant.
Args: names: Names to resolve, as they appear in the content. types: Optional list of node types to search (e.g. ["entity", "project"]). Omit to search every active node.
Inputs: names (required), types.
restore_vaultA
Put a node back as it was right after commit. Use it to repair a node
that a write damaged, or to bring back a deleted node.
You may call this without asking the user first: a restore is recorded as a new commit, nothing is lost, and it can itself be undone. But:
Read the old version first (read_vault with commit) and check it holds the content you want back.
If the node was changed after the damage, a restore also drops those newer changes. Ask the user first, or use read_vault with commit and patch_vault to put back only the missing part.
Tell the user what you restored: node, commit and why.
To restore a deleted node, pass its UID and the hash of the commit that
deleted it with "1" added (e.g. "a1b2c3d1").
Returns {restored, uid, path, from_commit, was_deleted, commit}.
Inputs: path (required), commit (required).
save_assetA
Save a new binary/rich-file asset with its vault proxy node. Executes the full crash-safe write sequence. Returns {uid, asset_path, vault_path}, plus tag_warnings when a tag names a project/entity/topic or repeats the node's own type or subtype — the same warning write_vault returns.
Checks for duplicates itself, like write_vault: if a node with the same title already exists it writes nothing and returns {duplicate: true, uid, path, name, message} describing the existing node. Ask the user whether to update that node or save a second copy anyway, and in the latter case call again with allow_duplicate=true.
Provide either source_path OR content — not both.
Choosing the right mode:
Use source_path whenever the file exists on disk. The server reads it directly; no file data passes through MCP. Works for any file size.
Use content only for material already present in the conversation context: text that Claude just generated, or base64 data returned by another tool in this same session. Never base64-encode a file yourself just to pass it here — use source_path instead.
Args: source_path: Absolute path to a file on disk. content: File content as a UTF-8 string (text) or base64-encoded string (binary). Use is_base64=True for binary content. is_base64: Set True when content is base64-encoded binary data. format: File extension: html, pdf, png, mp4, mp3, mov, etc. mime_type: MIME type: text/html, application/pdf, video/mp4, etc. title: Title for the vault proxy node. summary: One-paragraph summary. tags: 3-6 freeform lowercase hyphenated tags. domains: One or more domain taxonomy values. relations: List of {uid, type} dicts linking to other vault nodes. node_type: Vault node type: 'document' or 'asset'. subtype: asset → 'book', 'video', 'music', 'podcast', 'presentation', 'visual', 'physical'. document → 'report', 'transcript', 'spec', 'plan', 'meeting-notes', 'essay', 'journal', or 'note' (the default when omitted). author: Author or creator name. published_at: Publication or release date string. language: ISO 639-1 language code. allow_duplicate: Write even though a node with this title exists. Only after the user has chosen "save as new".
Inputs: title (required), format (required), mime_type (required), tags, +12 more.
scan_directoryA
Scan a directory for files and identify which already have vault nodes.
For each file found, checks whether any vault node has a has_asset location with scheme: local pointing to that file (by resolved absolute path).
Returns: { directory: str, total_files: int, unregistered: [{path, filename, format, suggested_type, suggested_subtype, suggested_title}, ...], registered: [{path, filename, format, node_uid}, ...], }
suggested_type is asset (with suggested_subtype book) for ebook formats
and document otherwise; suggested_title is the humanized filename. Both
come from the filename alone — confirm with the user when they look wrong.
Args: path: Absolute path to the directory to scan. recursive: Scan subdirectories recursively (default false).
Inputs: path (required), recursive.
scan_inboxA
Phase 1 of the inbox workflow: scan loci-stage/, classify all files, and write .inbox-plan.yaml. Read-only — no files are moved or vault nodes created.
Returns a summary: {status, total_files, total_content, runs_estimated, plan_path}.
Call this before approve_inbox_plan(). If a plan already exists with pending items, this overwrites it.
Inputs: no named parameters.
search_vaultA
Search vault nodes by title and summary text (case-insensitive substring match). Use this to find a node when you know a keyword but not the exact title or uid. Each entry: {uid, title, type, path, summary}.
Args: query: Keyword to search for (case-insensitive substring match) state: Lifecycle state filter — "active" (default) excludes archived nodes; pass "archived" to search only archived nodes, or None to search all.
Inputs: query (required), state. Returns: result
set_vault_rootA
Point Loci at the user's vault directory. Returns {vault_root, created,
configured_at}, plus notice when adopting the vault moved files the user
owns — pass it on to them verbatim.
Call this when a tool has failed with LOCI_NOT_CONFIGURED, or when the user asks to move Loci to a different vault. It records the path, creates and seeds the directory, and takes effect immediately — no restart, and the call that failed can simply be retried.
Ask the user for the path first. Never guess one and never offer a default:
the vault holds everything they save, and a path they did not choose is a
vault they will never find again. If they are unsure, ask whether they keep
notes in a synced folder (Dropbox, Nextcloud, iCloud) and suggest a vault
directory inside it — but let them confirm the full path.
Args: path: Absolute path to the vault directory (~ is expanded). create: Create and seed the directory. Pass False to adopt a vault that already exists and nothing else: any other path — missing, or there but not a vault — fails without writing anything, so you can confirm it with the user first.
Fails when the deployment sets LOCI_VAULT_ROOT: that is a managed install where the operator fixed the path, and it cannot be changed from chat.
Inputs: path (required), create.
undo_last_vault_changeA
Undo the newest vault commit: a node it created is removed, a node it changed or deleted is put back as before. Recorded as a new commit, so calling it again undoes the undo.
Use it right after a write that went wrong. If other writes came after the
mistake, use history_vault and restore_vault on the one node instead.
Pass commit (the hash the bad write returned, at least 7 characters) to
undo only that change: if a newer one has landed since, nothing is undone
and an error is returned.
Tell the user what you undid. Returns {undone, undone_commit, files, commit}.
Inputs: commit.
update_inbox_itemA
Update a plan item's status.
Content items: call after writing a vault node for the item.
File items parked as duplicate by execute_inbox_file_batch: pass
status="skipped" to drop the item, or status="pending" with
allow_duplicate=True to re-queue it so the next file batch saves a
second copy. The plan is not done while an item is still parked.
Args: path: Relative path of the item within loci-stage (as it appears in the plan) status: 'done' | 'failed' | 'skipped' | 'pending' uid: UID of the created vault node (required when status='done') allow_duplicate: when true, the file batch saves this item even if its title already exists in the vault
Inputs: path (required), status (required), uid, allow_duplicate.
validate_typeA
Validate a proposed user-defined type before writing it to ontology-user.yaml. Returns {"valid": bool, "errors": [...], "resolved_fields": {...}}.
Args: name: Proposed type name — must not collide with any canonical type. extends: A canonical type to inherit from (e.g. "procedure", "task"). fields: Optional list of additional fields: [{name, type, values?}]. 'type' is one of: string | array | datetime | enum | boolean. For enum fields, include 'values': [...].
Inputs: name (required), extends (required), fields.
write_metaA
Overwrite a vault meta file. file: tag-dictionary | foreign-definitions | ontology-user
ontology-user — write updated YAML to register a new user-defined type. Read the current content first with read_meta(file="ontology-user"), append the new type entry, then write the full updated content here.
Inputs: file (required), content (required). Returns: result
write_skillA
Write the user's additions to a Loci skill — the "Your additions" section read_skill returns, kept in vault/skills/.md. skill: loci-save | loci-query | loci-inbox | loci-ingest-directory
Replaces the whole file unless append=True, so read the current additions (the tail of read_skill) before rewriting them. The packaged procedure is not writable: it ships inside Loci and is replaced on upgrade, while the additions live in the vault and survive one. Nothing to reinstall — the next read_skill call returns the change.
Inputs: content (required), skill, append. Returns: result
write_vaultA
Write a new item to the Loci vault. Returns {uid, path, type, title}, plus tag_warnings when a tag names a project/entity/topic (belongs as a relation) or repeats the node's own type or subtype. The write has already happened; surface the warning and offer the fix.
Checks for duplicates itself: if a node with the same title or the same extra_fields.source_url already exists, nothing is written and the result is {duplicate: true, uid, path, name, message} describing the existing node. Ask the user whether to update that node (patch_vault) or save a new one anyway, and in the latter case call again with allow_duplicate=true.
Args: type: document | article | bookmark | conversation | project | task | event | entity | procedure | skill | topic | asset | log title: Concise descriptive title body: Main content body (markdown) tags: 3-6 freeform lowercase hyphenated tags domains: One or more from: technology-engineering, science-research, business-strategy, finance-economics, health-biology, philosophy-ethics, creativity-design, personal-development, current-events-society summary: One-paragraph summary — what this is, why it matters, key insight author: "self" for user-produced content; entity name/UID for external relations: List of {uid: str, type: str} dicts references: List of {url: str, title: str, source_type: str} dicts extra_fields: Type-specific frontmatter fields. Do NOT put the body here — 'body' is not a frontmatter field. A 'body' key in extra_fields is rerouted to the body argument and a warning is returned. A 'uid' key (from generate_uid) is honoured and reported back; one another node already holds is refused with {errors: [...]}. Omit it to have one generated. allow_duplicate: Write even though a similar node exists. Only after the user has chosen "save as new".
Inputs: body (required), type (required), title (required), tags, +7 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 39 tools
Most tools target a distinct resource+action (write/read/patch/delete/search on vault nodes, separate versioning, inbox, ontology, and asset tools). A few boundaries blur: search_vault vs find_nodes vs resolve_mentions all retrieve nodes, and define_type competes with write_meta(file="ontology-user") which is also described as the way to register a user type. Descriptions do help sort these out, but the registration overlap is a real source of misselection.
Nearly everything is snake_case verb_noun (write_vault, read_meta, scan_inbox, define_relation, execute_inbox_file_batch). The only wrinkle is inconsistent retrieval verbs — query_*, find_*, search_vault, get_inbox_plan, list_vault, read_vault — so similar read operations don't share a prefix. Still very readable and predictable overall.
39 tools is heavy for a single server and sits in the top band of the rubric. The domain is genuinely broad (vault CRUD, git-style history, ontology extension, assets, staged inbox, skills), so most tools earn their place, but several could be folded together (find_nodes/search_vault, define_type/write_meta).
Coverage is unusually thorough: full node lifecycle plus versioning/restore, filtered and keyword retrieval, graph queries, ontology/relation extension, asset registration, and a staged-inbox workflow. Minor gaps remain — search_vault only matches title and summary (no body full-text search), and there is no rename/move or delete operation for user-defined types/relations beyond overwriting ontology-user.
Maintenance
Related MCP Connectors
Notes and memory assistant: one Markdown vault that Claude, ChatGPT and any MCP client can share.
Persistent memory layer for AI tools. Save and recall notes across Claude and other MCP clients.
Search, read, and safely update Markdown notes in your connected Phasoric knowledge vaults.
Personal context for every AI: search, read, and write back to your private Markdown library.
Related MCP Servers
- FlicenseNot gradedqualityAmaintenanceEnables Claude and other MCP-compatible clients to create, link, tag, search, and update atomic notes stored as Markdown files, building a bidirectional knowledge graph with semantic relationships. Also supports exploring and synthesizing that graph through tools that surface linked, similar, central, and orphaned notes.-
- FlicenseNot gradedqualityAmaintenanceEnables creating, linking, tagging, searching and exploring atomic notes that form a networked knowledge graph, with bidirectional semantic link types and markdown files as the source of truth. Supports knowledge creation, exploration and synthesis through Claude and other MCP-compatible clients.-
- FlicenseNot gradedqualityAmaintenanceEnables creating, linking, searching, and synthesizing atomic notes stored as human-readable Markdown files with a rebuildable SQLite index, forming a navigable knowledge graph through typed semantic relationships, tags, and distinct note types. It lets Claude and other MCP-compatible clients capture fleeting ideas, explore linked notes across domains, and surface central, similar, or orphaned notes for further synthesis.-
- FlicenseNot gradedqualityAmaintenanceEnables Claude and other MCP-compatible clients to create, link, search, explore, and synthesize atomic Markdown notes organized as a connected knowledge graph with tags and semantic relationships.-