---
title: KMS agent tools — expose your Markdown vault to spawned agents
---

# KMS agent tools — expose your Markdown vault to spawned agents

## What it is

KMS is Omniscio's built-in Markdown vault editor (described in [kms.md](kms.md)) — point Omniscio at a folder of `.md` notes, and an indexer + chokidar watcher keep an in-app TipTap editor in sync with the files on disk. **KMS agent tools** are an opt-in extension that lets every Claude Code session Omniscio spawns interact with that vault through MCP tools. There are two tiers: **five read tools** (search, fetch, list, traverse) and **four write tools** (create, append, update properties, insert at position). Read tools are gated by one toggle; write tools have their own separate toggle plus rate limiting and folder restrictions. The user-visible surface is at **Settings → Workflow → Features**, off by default.

Once on, every new Claude CLI session Omniscio starts has an extra MCP server in its curated `.mcp.json` — a private per-session file Omniscio hands to the CLI, not one written into your project folder. The agent calling `kms_search` or any of the other tools reaches an out-of-process Node subprocess (no UI, no API spend, no network) that reads and writes the same SQLite database the in-app editor uses, applies the same hidden-note filter, and returns JSON.

## Where to find it

**Settings → Workflow → Features**, where the read and write halves are two separate toggles, both off by default. The tools themselves are invisible in normal use — they appear to the agent, not to you.

## How it behaves

### How to use it

1. **Turn on KMS first.** Settings → Workflow → Features → **Enable KMS**. Pick a vault root if you haven't already (the vault root is the folder full of `.md` files). This step lights up the editor; the agent tools toggle is hidden until KMS is on.
2. **Turn on read tools.** Same panel, the row labeled **"Expose KMS vault to spawned agents"**. Off by default. Flipping this on does **not** retroactively re-spawn live sessions — it applies to the next session Omniscio starts.
3. **Turn on write tools (optional).** A separate toggle labeled **"Allow agents to write to KMS vault"** appears once read tools are on. This enables `kms_create`, `kms_append`, `kms_update_properties`, and `kms_insert_at`. Write tools have additional safety controls (see below).
4. **Use Claude normally.** When the agent asks itself a question your vault can answer ("what did I write about the kestrel migration?"), it can now call `kms_search` and read the matching note bodies directly via `kms_read`. With write tools on, the agent can also create and edit notes. There is no UI surface for the call itself — it happens inside the spawned session's tool-use stream, the same way file reads and bash commands do.
5. **Verify the wiring (optional).** In a new session, paste: `Use your kms_list_recent tool to show me the 5 most recently updated notes.` If the tool is wired, the agent responds with the list; if not, it replies that the tool isn't available.
6. **Turn it off any time.** Same toggle. Already-running sessions keep the tools until they end; new sessions don't get the `.mcp.json` block.

### The five read tools

Every read tool excludes notes whose `hidden_at` column is set (the "hide from tree" stamp you can apply in the in-app editor — agent visibility tracks UI visibility). Every tool's output is capped at **100 KB of serialized JSON** per call; over-cap responses return `{ truncated: true, cursor: "<last-id>" }` so the agent can narrow its query.

- `kms_search(query, limit?)` — FTS5 search across every vault's notes. Returns BM25-ranked hits with title, excerpt (~200 chars with the match highlighted), positive-scaled score (the underlying bm25 is negated so "higher = better" matches LLM intuition), tags, and updated-at timestamp. Default limit 10, max 50. Empty query returns `[]` without hitting the DB. Malformed FTS5 syntax (unbalanced quotes, stray colons) returns `{ error: 'invalid-input' }` so the agent can retry with a cleaner query.
- `kms_read(noteId)` — Fetch one note by id. Returns title, body (with the AI summary marker block stripped), tags, the structured summary (state + hash + extracted text when a marker is present), and updated-at. Unknown id or hidden id returns `{ error: 'not-found' }` — hidden notes are not surfaced through direct read either.
- `kms_list_tags()` — Every distinct tag in every vault with its note count. Sorted by count DESC, tag ASC. Tags whose only carriers are hidden notes don't surface.
- `kms_list_recent(limit?)` — N most recently updated notes across every vault. Sorted by `updated_at DESC`, then `id ASC` for stable ordering. Default limit 10, max 50.
- `kms_get_backlinks(noteId, limit?)` — Every note that wiki-links to the given target via `[[Title]]` syntax. Returns source title + id + a short snippet of context around the link. Self-links are excluded; hidden source notes are excluded; a hidden target returns `not-found` (same intent as `kms_read`). Default limit 25, max 50.

- `kms_semantic_search(query, limit?)` — **Hybrid semantic + keyword search** across every vault's notes. Embeds the query with the `all-MiniLM-L6-v2` model and combines FTS5 BM25 keyword hits with embedding cosine similarity via Reciprocal Rank Fusion (RRF). Finds notes by meaning — e.g. "error handling in auth" finds relevant notes even if they use different words. Each hit includes a `matchType` field (`'keyword'`, `'semantic'`, or `'both'`) so the agent knows WHY it matched. Falls back gracefully to keyword-only search when the embedding model isn't ready. Default limit 10, max 50.

- `kms_inventory(cursor?, limit?)` — **Paginated inventory of every visible note** across every vault. Each entry carries `{ noteId, title, tags, updatedAt, summary: { state, text }, tokenEstimate, sizeBytes }`. Summary text is decoded from the body HEAD only — no full bodies are read and no summaries are generated. Pages via an opaque cursor: when more rows remain the result carries `nextCursor` — re-call with it to continue. Order is newest-first (`updatedAt DESC, noteId ASC`). Default limit 100, max 500. Zero cost (local SQLite only). On a rare payload-cap overflow returns `{ truncated: true, cursor }` where `cursor` is the SAME input cursor so the agent re-calls with a smaller `limit` without skipping notes. See [kms-vault-overview.md](kms-vault-overview.md) for the equivalent user-facing panel.

Cross-vault scope: none of these take a `vaultId`. The agent sees the union of every registered vault. If you have one vault (the common case), this is a non-issue; if you have many, results from different vaults can mix and the agent uses the snippet/title to disambiguate.

### The four write tools

Write tools require a **three-flag progressive opt-in**: `nothariEnabled` + `nothariAgentToolsEnabled` + `nothariAgentWriteToolsEnabled` — all three must be on. They are subject to rate limiting and an optional folder allowlist (see Safety model below). Every write is recorded in the `nothari_tool_invocations` provenance table (see Provenance tracking below).

- `kms_create(title, content?, tags?, folder?)` — Create a new note. `title` becomes the filename (auto-sanitized: colons → dashes, slashes → dashes, capped at 200 chars). `content` is optional Markdown body (max 100 KB). `tags` is an optional array of hashtags (validated against `^[a-zA-Z0-9][a-zA-Z0-9_-]*$`, max 50 per note). `folder` places the note inside a vault subfolder (validated against the folder allowlist if one is configured). Returns `{ noteId, title, path }`. Duplicate titles in the same folder return `{ error: 'already-exists' }`.
- `kms_append(noteId, content, heading?, separator?)` — Append content to an existing note by id. `content` is the Markdown to add (max 50 KB). `heading` (optional) targets a specific heading — the content is inserted after the heading's last line rather than at the end of the note. `separator` controls how the appended block is separated from existing content (default `'\n\n'`). Uses **optimistic concurrency**: the tool captures the note's `updated_at` timestamp, and if the note was modified between read and write, it retries once automatically; a second conflict returns `{ error: 'conflict' }`.
- `kms_update_properties(noteId, set, remove?)` — Update YAML frontmatter properties on a note. `set` is a record of key-value pairs to add or overwrite (values can be strings, numbers, booleans, or arrays). `remove` is an optional array of keys to delete. The `kms_provenance` key is reserved and cannot be set. Tags are reconciled: setting a `tags` property in the frontmatter merges with inline `#hashtags`; removing a tag from frontmatter removes it from the note. This tool is **exempt from the folder allowlist** (it modifies metadata only, not file location).
- `kms_insert_at(noteId, content, position)` — Insert content at a specific position in a note. `position` is one of four targeting modes: `{ afterHeading: "## Section" }` inserts after the named heading's content, `{ beforeHeading: "## Section" }` inserts before the heading line, `{ afterBlockContaining: "search text" }` inserts after the paragraph containing the given text, or `{ atLine: N }` inserts at an exact line number (1-based). Max 50 KB content. Same optimistic concurrency as `kms_append`.

All four write tools return a result envelope on success or an error envelope (`not-found`, `feature-disabled`, `rate-limited`, `conflict`, `folder-denied`, `already-exists`, `invalid-input`). A `rate-limited` error includes a `retryAfterMs` hint.

### Safety model (write tools)

Write tools have three layers of protection against runaway agents:

- **Rate limiting** — a per-session sliding-window counter. Default: **10 writes per minute** and **200 writes per session** (both configurable in Settings). When either limit is hit, the tool returns `{ error: 'rate-limited', retryAfterMs }`. The limiter resets when the session ends. Rate limits are enforced in the MCP server process, not in Omniscio main.
- **Folder allowlist** — an optional list of vault subfolders the agent is allowed to write into (configured in Settings as a list of folder paths, supports single-segment globs like `drafts/*`). When configured, `kms_create`, `kms_append`, and `kms_insert_at` check the target note's folder against the allowlist and return `{ error: 'folder-denied' }` if it doesn't match. An empty allowlist means "all folders allowed." `kms_update_properties` is always exempt (metadata-only).
- **No destructive operations** — there is no `kms_delete`, `kms_rename`, or `kms_move` tool. Agents cannot delete, rename, or relocate notes. The `kms_append` and `kms_insert_at` tools add content; they do not replace or remove existing content. `kms_update_properties` can remove frontmatter keys but cannot touch the note body.

### Provenance tracking

Every write tool invocation is recorded in the `nothari_tool_invocations` table with: tool name, session id, note id, a summary of the input, result status (`success` / `error` / `conflict`), line count added, and timestamp. This table is **FTS5-indexed** for search integration — provenance records surface as a fourth result type in the unified KMS search.

Read-tool access tracking (`nothari_tool_reads` table) was designed but the write path was never wired, so no read-access data is currently recorded.

### What the agent does and doesn't see

- **Sees (read tools)**: every note body (excluding hidden), every tag, every wiki-link, the AI summary text (when present), updated-at timestamps. The same content the in-app editor renders.
- **Can do (write tools, when enabled)**: create new notes, append content, update frontmatter properties, insert at specific positions. Every write is provenance-tracked and rate-limited.
- **Cannot do**: delete, rename, or move notes. There is no `kms_delete`, `kms_rename`, or `kms_move` tool. Agents also cannot overwrite existing content — `kms_append` and `kms_insert_at` are additive only. Destructive file operations remain available as bash file-system tools if needed.
- **Doesn't pay**: tools hit local SQLite only. Zero API spend. No network. The agent's per-turn input/output cost goes up by the bytes of the returned JSON because the agent has to read the response, but no Anthropic-billed call is made on Omniscio's side.

### Known limitations

- **No destructive tools.** There is no delete, rename, or move tool. Agents can only add content. Destructive operations remain available as bash file-system tools.
- **Write tools are additive only.** `kms_append` and `kms_insert_at` add content; there is no "replace" or "overwrite" mode.
- **Cross-vault aggregation.** Multi-vault users get unified results; if you need to scope, the agent can search for `vaultName:` patterns in the snippet text, but a dedicated `vaultId` parameter isn't there yet.
- **Payload cap is 100 KB hard** (read tools). Tools that exceed it return `{ truncated, cursor }` rather than a partial result — the agent must narrow its query.
- **Flipping the toggle off mid-session** doesn't tear down the spawned MCP subprocess. The toggle change applies to **new** sessions.
- **No Settings UI for write tool configuration yet.** The four new settings (`nothariAgentWriteToolsEnabled`, rate limits, folder allowlist) work via the existing settings system and are configurable through the CLI, but the Settings panel section is planned for a follow-on PR.
- **Session-to-note capture** (structured capture of decisions/findings at session end, auto-capture rules) is designed but not yet implemented (Phase 2b/2c).

## For agents

### How it works

The toggle `nothariAgentToolsEnabled` lives in [/src/shared/types.ts](/src/shared/types.ts) (default `false`) and is surfaced at [/src/renderer/src/features/settings/sections/features/KmsFeatureSettings.tsx](/src/renderer/src/features/settings/sections/features/KmsFeatureSettings.tsx) under the existing KMS Features section. The toggle is gated by the parent `nothariEnabled` flag — both must be on for tools to appear.

When Omniscio spawns a new Claude CLI session, the process manager runs [/src/main/services/mcp/mcp-config-orchestrator.ts](/src/main/services/mcp/mcp-config-orchestrator.ts) which composes a single `.mcp.json` file at a private per-session path (not the session's working directory; handed to the CLI via `--mcp-config` — see [backend-spawn-contract.md](/.claude/memory/contracts/backend-spawn-contract.md) §12). The orchestrator composes the enabled servers — **Zapier**, **Playwright**, **Google Drive**, **Mobbin**, **Canva**, plus any user-configured custom MCP servers (`ResolvedCustomMcpServer` rows). Each built-in server entry is built by a tiny pure-function builder (for example [/src/main/services/zapier/mcp-config-entry.ts](/src/main/services/zapier/mcp-config-entry.ts) and [/src/main/services/drive-mcp/mcp-config-entry.ts](/src/main/services/drive-mcp/mcp-config-entry.ts)) so the orchestrator can mix-and-match without any feature owning the file write. **MemPalace, KMS, Google Workspace, Fathom, JLS Image Studio and Webapp Verify were retired from composition on 2026-09-19** (see the note on `McpOrchestratorOpts` in [/src/main/services/mcp/mcp-compose.ts](/src/main/services/mcp/mcp-compose.ts)) — and the KMS vault keeps serving agents through the control server's `/kms/*` routes and its skill instead of a spawned per-session process. The KMS and memory per-session entry builders were deleted with the retirement; the Google Workspace one is deliberately retained as the written-down shape a revival would restore, and its own comment says not to read it as a live path.

The KMS MCP server itself lives at [/src/main/services/kms-mcp-server/](/src/main/services/kms-mcp-server/) and is bundled to `out/mcp/nothari-server.js` by [/scripts/build-kms-mcp.js](/scripts/build-kms-mcp.js) (esbuild, externalizes `better-sqlite3` + `electron` + `electron-log`). On spawn, the orchestrator launches it via `ELECTRON_RUN_AS_NODE=1` + `process.execPath` — the same pure-Node-via-Electron pattern MemPalace uses to avoid bundling a separate Node runtime. The server reads three env vars from its process environment: `KMS_DB_PATH` (absolute path to `mission-control.db`), `KMS_ENABLED` (`'1'`/`'0'` — parent feature flag), and `KMS_AGENT_TOOLS_ENABLED` (`'1'`/`'0'` — agent-tools sub-feature flag). It opens the database in WAL mode and builds a frozen feature gate from the two flag vars; the gate is checked on every tool call but never re-read, so flipping a toggle mid-session takes effect on the next session spawn. The server then serves the tools through the standard MCP `ListToolsRequestSchema` + `CallToolRequestSchema` handler pair.

The tool functions themselves are pure: they take a `Database.Database` handle, the parsed input, and a gate object, and return either a result envelope, a `{ error }` envelope, or a `{ truncated: true, cursor }` envelope. Read tools live in [/src/main/services/kms/agent-tools-read.ts](../../src/main/services/kms/agent-tools/agent-tools-read.ts); write tools live in [/src/main/services/kms/agent-tools-write.ts](../../src/main/services/kms/agent-tools/agent-tools-write.ts). The write tools additionally receive a `WriteRateLimiter` instance (sliding-window counter), the folder allowlist, and a `WriteSessionContext` (session id + vault id for provenance recording).

Write tools use **direct DB writes** with optimistic concurrency (CAS via `WHERE updated_at = ?`) rather than going through `note-crud-service.ts`'s file-based path, since the MCP server doesn't have filesystem access to the vault. The `kms_create` tool uses `gray-matter` for frontmatter serialization (same library the in-app editor uses). Every successful write records a row in `nothari_tool_invocations` for provenance. Read tools now record aggregated access in `nothari_tool_reads` (one row per note+session pair, UPSERT on each read).

Sub-feature gating is enforced **in the spawned server** (defense in depth) — even if Omniscio's main process wrote the `.mcp.json` block while the toggle was on, flipping the toggle off mid-session would cause new calls to return `feature-disabled` because the env var is read once on subprocess start. The same hidden-note + payload-cap contract is enforced at the tool function level, not by the calling MCP adapter. The write gate checks all three flags independently: `nothariEnabled`, `nothariAgentToolsEnabled`, and `nothariAgentWriteToolsEnabled`.

Force-disable: Search-window and Ask-Omniscio sessions are isolated from the user's data by design, so the orchestrator's `mempalaceForceDisabled` / `kmsForceDisabled` overrides are set when those sessions spawn ([/src/main/process/process-manager.ts](/src/main/process/process-manager.ts)) regardless of the user's settings. No `.mcp.json` is written if every server is disabled.

The integration registry entry for KMS ([/src/shared/integration-registry.ts](/src/shared/integration-registry.ts)) declares `agentToolPrefixes: ['kms_']` so the integration completeness lint covers the new tool surface; the per-feature lint at [/tests/unit/lint/kms-agent-tool-registry.test.ts](/tests/unit/lint/kms-agent-tool-registry.test.ts) cross-checks that every name in `READ_ONLY_TOOLS` has a matching Zod schema export and handler function. Read tools are covered by [/tests/unit/services/kms/agent-tools-read.test.ts](/tests/unit/services/kms/agent-tools-read.test.ts) (39 tests); write tools are covered by [/tests/unit/services/kms/agent-tools-write.test.ts](/tests/unit/services/kms/agent-tools-write.test.ts) (30 tests) — both against a real `:memory:` SQLite with full migration.

## Related

- [kms.md](kms.md) — the parent Markdown vault feature
- [kms-summaries.md](kms-summaries.md) — the AI summary marker codec the tools' `summary` field surfaces
- [mempalace-memory.md](mempalace-memory.md) — the other MCP server Omniscio injects into spawned sessions; uses the same orchestrator
