Tools

Every tool except list-vaults and get-setup-status takes a vault argument naming which configured vault the call applies to. Use list-vaults to see the names.

The first column is the tool’s function name, which is what an agent calls and what permission rules (mcp__<server>__<name>) and the audit log use. The second is its display title, the phrase a client can show a person instead.

Tool Title Description
list-notes List notes List the notes (.md files) in the vault, or scoped to a folder. For other files use list-files. Returns sorted vault-relative paths, each with a last-modified timestamp
list-files List files List every file in the vault, or scoped to a folder, whatever its type (scripts, PDFs, images, notes), optionally narrowed by extension. Returns sorted vault-relative paths, each with a last-modified timestamp and the size in bytes
list-tags List tags List all unique tags (YAML frontmatter) used across vault notes, optionally scoped to a subdirectory
search-tags Search tags Find notes that have ALL of the specified tags
new-notes Recently created notes List notes created in the last 7 days, or since a provided timestamp
changed-notes Recently changed notes List notes modified in the last 7 days, or since a provided timestamp
list-vaults List vaults List all configured vaults
get-setup-status Get setup status List the companion skills and agents, the server version, and how to install them on each Claude surface
read-note Read note Read a note’s content and last-modified timestamp
create-note Create note Create a new note. Fails if it already exists
edit-note Edit note Edit a note: append, prepend, replace, replace or delete the content under a heading, or toggle a checkbox
delete-note Delete note Delete a note, moving it to .trash by default (under a name that carries the time; see Emptying .trash)
move-note Move note Move or rename a note, rewriting all vault-wide wikilinks to the old path
create-file Create file Create a new file of any type (an image, a PDF, a .md note) from base64-encoded content. Fails if it already exists
fetch-file Fetch file Create a new file of any type by downloading a URL server-side, so the client sends only a URL
upload-file Upload file Reserve a one-time upload URL for a file of a declared size and SHA-256. The agent sends the file to it with curl, so its bytes never pass through the model
replace-file Replace file Reserve a one-time upload URL that replaces a file already in the vault, but only if it is still the version named by its SHA-256 and mtime. The old version moves to .trash
read-file Read file Return an existing file of any type, such as a PDF, to the client as an embedded base64 resource, so the agent can read it
move-file Move file Move or rename a file of any type, rewriting the wikilinks (for a note) or embeds (for any other file) that point at the old path
delete-file Delete file Delete a file of any type, a .md note included, moving it to .trash by default (under a name that carries the time)
find-backlinks Find backlinks Find all notes that link to or embed a given note or file
resolve-wikilink Resolve wikilink Resolve a wikilink target string to the vault-relative file or files it points to
create-folder Create folder Create a new folder, and any missing parents, in the vault
search-vault Search vault Search vault notes by content, filename, or both
add-tags Add tags Add tags to notes in frontmatter and/or inline body content
remove-tags Remove tags Remove tags from notes in frontmatter and/or inline body content
rename-tag Rename tag Rename a tag throughout the entire vault, in frontmatter and inline
set-frontmatter-field Set frontmatter field Set a single frontmatter field (not tags) to a scalar value, creating it if missing
remove-frontmatter-field Remove frontmatter field Remove a single frontmatter field (not tags) entirely
query-graph Query graph Ask a natural-language question against a vault’s graphify knowledge graph

edit-note operations

Beyond append, prepend and replace, edit-note supports targeted, section-scoped edits, so a large note doesn’t have to be resent in full for a small change:

If a heading or taskText matches more than once in the note, the call fails with a list of every match (line number, and heading level where relevant). Pass the 1-based occurrence from that list on a follow-up call to pick one.

Wrong arguments

A call that leaves out an argument the tool requires is answered before the tool runs, with a message that says what to send: the missing arguments, then the arguments the tool takes (a * marks the required ones), then any argument the tool does not take, which usually explains the miss:

filename is required. read-note takes: vault*, filename*, folder, outline,
heading, occurrence (* required). Unrecognised arguments: path.

A missing vault also lists the configured vaults, and so does a vault the server does not have: Unknown vault: x. Available vaults: a, b. An argument a tool does not take is not an error when the call is otherwise complete, so a client that adds fields of its own is unaffected. create-note on a note that already exists points to edit-note (append, prepend, replace, replace-section or delete-section).

Line endings

A note’s line ending is the ending of its first line break: CRLF if that is \r\n, otherwise LF (a note with no line break counts as LF). Every tool that changes a note writes the text it makes in that ending and leaves the other lines as they were:

create-note, edit-note replace and create-file write exactly what they are sent, because the caller supplies the whole file.

The file tools and notes

create-file, fetch-file, upload-file, replace-file, read-file, move-file and delete-file take a file of any type, a .md note included. (They used to be called *-binary-file and refused notes, except for the upload, replace and read tools.) The note tools (create-note, read-note, edit-note, delete-note, move-note) stay, because a note also needs text, sections and a Last-Modified: line; use them for text you write yourself.

Listing files

list-files lists every kind of file, so an agent can find the scripts, PDFs and images that read-file, replace-file, move-file and the other file tools work on. Give it an extension to narrow the list. list-notes lists the notes (.md files) only and takes no extension: a call that passes one, with any value, is refused with a message that names list-files.

extension Lists
(omitted) Every file
md The notes: files ending in .md, in any letter case (list-notes lists these without being asked)
pdf Every PDF
p* pdf, png and anything else starting with p
* Every file, whatever its extension

Reading part of a note

read-note returns a whole note, which fails for a note too long for the client to accept (a project spec can reach 100K characters). Rather than return something the client then rejects, read-note refuses a note longer than readNoteMaxChars (50,000 characters by default) and says how to read it in parts. It takes two extra parameters, named like edit-note’s:

outline and heading can’t be used together, occurrence needs a heading, and both need a .md note. The outline is not subject to the size limit. Both results carry the Last-Modified: timestamp, as a whole-note read does.

Headings inside fenced code blocks are ignored, and notes with Windows (CRLF) line endings work the same as any other. A line that is only # characters and white space has no text to name it by, so it is not listed and is not a heading: the section above it runs on through it, and replacing or deleting that section removes it too.

Avoiding overwrites with expectedMtime

Every tool that writes to an existing file accepts an optional expectedMtime: the ISO 8601 last-modified timestamp you got back earlier from read-note or list-notes. Pass it on a later write and the server refuses the write, changing nothing, if the file has been modified since. The error tells you both the timestamp you expected and the current one, so you know to read the note again.

It is a compare-and-swap check, not a lock. It catches “I read this note a while ago and something else has touched it since”. A client that reads, decides, then writes back should always pass it, otherwise an edit made in between is silently overwritten.

read-note  -> note content + Last-Modified: 2026-09-20T10:15:00.000Z
...decide what to change...
edit-note  -> { operation: "replace", content: "...", expectedMtime: "2026-09-20T10:15:00.000Z" }

It applies to edit-note (all operations), delete-note, move-note, set-frontmatter-field, remove-frontmatter-field, move-file and delete-file; for the two move tools and delete-file it is checked against the source file. add-tags and remove-tags work on a files[] array, so for them expectedMtime is an object mapping each vault-relative path to its expected timestamp, and it must include every file in the batch (a file missing from the map fails the call). Every file is checked before any file is written, so the batch either applies completely or not at all.

It doesn’t apply to create-note, create-file or fetch-file, which already fail if the destination exists, or to rename-tag, which sweeps the whole vault.

fetch-file

create-file needs the client to send the file as base64, which is costly through an LLM client because the bytes are read in and written out again. fetch-file has the server download a URL itself and write the result, so the client sends only the URL:

fetch-file -> { filename: "photo.jpg", folder: "attachments", url: "https://example.com/photo.jpg" }

The parameters are filename, folder (optional), url, and the optional maxBytes and timeoutMs, which must be positive integers and otherwise default to the config file’s fetchMaxBytes and fetchTimeoutMs. The filename must not end in .md; use create-note for notes. It fails if the destination already exists. Because the server makes the request itself, the URL is checked carefully first. See Security.

get-setup-status

Returns text for the agent: the companion skills and agents that ship with the server (each with what it is for and which tools it helps), the server’s version, how to tell which Claude surface the agent is on, and the install steps for each surface. It takes no parameters, touches no vault, and a read-only user may call it.

The server does not know what a client has installed or which surface it runs on. The agent compares the list with its own skills and agents, and a skill’s metadata.version with the server version, and installs or suggests an update. The result also says what to do without the skills: read-file shows only images on a chat surface, and upload-file needs a shell and curl, which Claude Code can refuse until the user allows the skill’s script with a permission rule. See Agent skills and agents.

replace-file

upload-file never overwrites. replace-file is the same upload with the opposite rule: the destination must already exist, and the caller must say which version of it is being replaced, so a file someone else changed in the meantime is never overwritten by mistake.

The flow:

  1. The agent reads the file in the vault with read-file. The result ends with a sha256: and a Last-Modified: line, which are the file’s current SHA-256 and mtime. (A file over readMaxBytes is refused, but the refusal gives both values.)
  2. It works out the new file’s size and SHA-256 with a tool, as for an upload.
  3. It calls the tool:

    replace-file -> { filename: "report.pdf", folder: "attachments", size: 912340, sha256: "<new file>", expectedSha256: "<file in the vault>", expectedMtime: "2026-05-01T09:30:00.123Z" }
    
  4. The result is a one-time URL and a curl command, exactly as for an upload.
  5. The server checks the received bytes against the declared size and hash. If both match, and the file in the vault is still the one named, it moves the old version to .trash and puts the new one in its place. It answers 200 with {"path", "bytes", "sha256", "replaced": true, "previousSha256", "trashedAs"}.

The rules, beyond those of upload-file (one request, short-lived, the same limits, address and note rules):

The checks guard against an edit made since the agent read the file, which is the realistic case. Just before the swap the server also checks that the destination is still the file it moved to .trash, so a second replacement that got in first is refused. A program that writes to the old file in place during the short time it takes to hash it cannot be stopped without locking the file; the server notices afterwards, and the reply then carries a warning saying that the copy in .trash may hold newer content than previousSha256.

While the new file is being copied, a hidden temporary file (.upload-<hex>.part) exists beside the destination; it is removed afterwards, but one left by a process that was killed is not cleaned up automatically.

What curl receives is the same as for an upload, except that a successful replacement is 200 (not 201) and 409 means that the file changed or no longer exists.

The repository includes an agent skill, skills/replace-file, with a script, scripts/replace-file.sh: prepare <file> (prints the new file’s name, size and SHA-256) and send <file> <url> <old sha256> (uploads, then compares the server’s reply with the local file and with the version that was expected). Install it as described for the upload skill; see Security.

read-file

The other file tools can create, move and delete a file but not hand its contents to the agent. read-file does that: it returns the file as a short text line (attachments/report.pdf (48213 bytes, application/pdf)), an MCP embedded resource carrying the file’s bytes as base64, its mimeType, and a file:/// URI built from the vault-relative path, and then a sha256: line and a Last-Modified: line that name the version that was read, which replace-file takes as expectedSha256 and expectedMtime.

read-file -> { filename: "report.pdf", folder: "attachments" }

The parameters are filename and folder (optional). A .md note is returned too, as its raw bytes with the type text/markdown, so a note can be read back after upload-file, or read whole when it is longer than read-note returns. read-note is still better for a note: it returns text, can read one section, and adds the Last-Modified: line. denyPaths applies to a note read this way, and denyBinaryPaths does not, as for read-note. The file’s size is checked before it is read, and a file larger than the config file’s readMaxBytes (10 MiB by default) is refused without being loaded. There is no per-call override.

A large file that has not been read recently, a scanned PDF of a few MB for example, can fail on the first call with a client-side timeout such as The operation timed out. The server received the request and was still reading the file; the response is the file plus a third again for base64, and the client’s own request timeout is the limit. A retry is then fast because the file is cached. The tool’s description tells an agent to retry up to twice before reporting a failure. If a client times out on every attempt, lower readMaxBytes so such files are refused outright instead.

The server does not interpret the file. It never parses a PDF, extracts text, renders pages or runs OCR, so it needs no extra software. The client does that work, which is why one tool covers a PDF with a real text layer and a scanned PDF whose text is only in page images: a client that can read a PDF handles both. The mimeType comes from the file extension (pdf, png, jpg/jpeg, gif, webp and svg are recognised), and anything else is application/octet-stream.

The path must pass the denyPaths check, and it must not be covered by denyBinaryPaths, which refuses with Reading is restricted for '<path>' while leaving the file visible to the other tools. To stop a protected file being read from somewhere else, move-file (and move-note, for a file that isn’t a .md file) refuses to move it to a path that is not protected. Moving it within the protected area, or moving an unprotected file in, is allowed. Symbolic links are resolved before the checks, and only regular files are returned.

How the file reaches the model depends on the client; see Reading PDFs and other binary files. Claude Code reads any file this way. Claude Desktop chat shows images but silently drops a PDF and rejects other types, so use a code mode chat there. The repository includes an agent skill, skills/read-file, that checks this before reading and tells the user what to do.

upload-file

create-file needs the model to write the whole file out as base64. That is impractical beyond a few tens of kilobytes (an 810 KB PDF is about a million tokens, more than one response can hold) and gives no way to tell whether the file arrived intact. upload-file gives the agent a one-time URL to send the file to directly, so the file’s bytes never pass through the model.

The flow:

  1. The agent works out the file’s size and SHA-256 with a tool (wc -c, sha256sum), never by hand.
  2. It calls the tool:

    upload-file -> { filename: "report.pdf", folder: "attachments", size: 810490, sha256: "<64 hexadecimal characters>" }
    
  3. The result holds a URL of the form https://server:4001/up/<random token>, when it expires, and a ready-to-run command.
  4. The agent runs it: curl --fail-with-body -sS -T report.pdf "<url>".
  5. The server checks the received bytes against the declared size and hash. If both match it places the file at the destination and answers with {"path", "bytes", "sha256"}. If not, it discards the file and says why.

The address part of the URL (https://server:4001 above) is the config file’s mcpBaseUrl; /up/ is always the same and only the token after it is secret and random.

The rules:

What curl receives:

Code Meaning
201 The file was received, matched, and placed. The body is {"path", "bytes", "sha256"}
400 The Content-Length was smaller than the declared size, or the destination is a note (.md) and the file is not valid UTF-8
403 The request carried an Origin header, came from a different address (or network) when uploadNetworkPrefix is set, or the destination is denied
404 The URL is unknown, already used or expired (the same answer in every case)
405 The request was not a PUT
409 The destination appeared after the URL was reserved
411 No Content-Length was sent (chunked uploads are refused)
413 The Content-Length was larger than the declared size
422 The received bytes did not match the declared SHA-256. The file was discarded
503 Too many uploads are already in progress. Request a new URL and try again shortly

Because of --fail-with-body, curl exits with code 22 on any of the error codes and prints the server’s explanation, which is what makes a failure readable.

This needs an agent with a shell on a machine that can reach the server’s address, such as Claude Code. A client without a shell, or whose network can’t reach the server, can’t use it, and falls back to create-file for small files. A client whose tool call and upload leave from different networks, such as Claude Code through the claude.ai connector, Claude Desktop chat or claude.ai, can upload: the address is not checked unless uploadNetworkPrefix is set.

The server writes one line to its log for every refused upload, with the status, the caller’s address and the reason, for example upload refused (403) from 203.0.113.7: the caller address 203.0.113.7 is not the same as the address that reserved the URL, 198.51.100.2 (only when uploadNetworkPrefix is set; with a number below 32 it says “is not in the same /21 network as” instead). The answer to the client is deliberately shorter. The URL is never logged.

The repository includes an agent skill, skills/upload-file, that does steps 1 and 4 for the agent. Its script, scripts/upload-file.sh, has prepare <file> (prints the file name, size and SHA-256) and send <file> <url> (uploads, then compares the server’s reply with the local file and never prints the token). It needs curl and one of sha256sum, shasum or openssl. The npm package contains only the server, so copy the skill folder into your agent’s skills directory (for Claude Code, ~/.claude/skills/) to use it, or install the plugin; see Agent skills and agents. The tool works without the skill. See Security for what the URL does and doesn’t protect.