Skip to main content
Glama

Tests python 3.13+ interface: MCP + web UI tests: unit + browser e2e vault: plain Markdown licence: PolyForm Noncommercial 1.0.0

language: English language: ru-RU

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.

Loci topology view: force-directed graph coloured by node type

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-vault

The PyPI package is loci-vault, not loci. The name loci on PyPI is an unrelated project. Do not run uv tool install loci or pip install loci. What you install is loci-vault; the commands it gives you are loci and loci-mcp.

2. Connect it to your agent

loci setup

This 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/vault

Use 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:8765

Command

What it is

loci

The web UI and its HTTP backend. Change the address with --host / --port (or LOCI_UI_HOST / LOCI_UI_PORT). It stops with an error if you did not do step 3.

loci-mcp

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. loci register (or the checkout setup, uv run setup.py) registers it.

Hermes

Supported. loci register --target hermes and loci skills --target hermes.

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:

  1. 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 is loci-mcp.

  2. If your agent supports skills (SKILL.md folders), install the skills into its skills folder: loci skills --dir <folder>. For Codex, the folder is usually ~/.codex/skills.

  3. If your agent does not support skills, add this line to its instructions: "Before you save to or read from Loci, call the read_skill tool of the loci MCP 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

The problem

Why Loci exists, and why a Markdown folder or a PKM app alone is not sufficient

Setup

Install from PyPI, a release wheel, or a checkout. Upgrade. Sync and backup

Examples

One prompt for each task: save, find, update, extend the ontology

Using Loci

The mental model, saving, querying, the web UI, and the vault layout

The ontology

Types, relations, schemes, and how the registries are versioned

Status and roadmap

What is stable, what is partial, what is next

Work on Loci

CONTRIBUTING

Licence, CLA, and the rules for a pull request

Development

Repo layout, daily commands, releases

Testing

The unit suite and the browser e2e suite

Reference

File saving

How a file on disk becomes a vault node

Execution flows

What each install and upgrade path does, step by step

Diagrams

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 tools
approve_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 extends procedure)

  • 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:

  1. Call read_vault(path) to get the current content.

  2. 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).

  3. 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:

  1. Read the old version first (read_vault with commit) and check it holds the content you want back.

  2. 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.

  3. 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

    A3.8/5.0

    Scored across 39 tools

    Disambiguation4/5

    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.

    Naming Consistency4/5

    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.

    Tool Count3/5

    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).

    Completeness4/5

    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

    ActivityActive
    ResponsivenessNo recent community issues

    Related MCP Connectors

    Related MCP Servers

    • F
      license
      Not graded
      quality
      A
      maintenance
      Enables 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.
      -
    • F
      license
      Not graded
      quality
      A
      maintenance
      Enables 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.
      -
    • F
      license
      Not graded
      quality
      A
      maintenance
      Enables 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.
      -