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 operationsBeyond 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:
replace-section replaces the content under a heading, matched by its
exact text (without the # characters), and leaves the heading line itself in
place. Everything under the heading goes, sub-sections included, up to the
next heading of the same or a higher level. content is the new body only, so
don’t repeat the heading. To rename a heading, use replace. A content of
only spaces, tabs or blank lines clears the section, the same as an empty one.delete-section removes a heading and everything under it, heading line
and sub-sections included. When the section is the last thing in the note, the
blank lines before it go too, and the note keeps its final newline if it had
one.toggle-checkbox flips, or explicitly sets, a - [ ] or - [x] line,
matched by its exact text.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.
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).
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:
edit-note append, prepend and replace-section convert the line
breaks of content to the note’s ending. A note with no line break (an empty
note, or a single line) has no ending to follow, and append and prepend
write content as sent.add-tags, remove-tags, rename-tag, set-frontmatter-field and
remove-frontmatter-field rewrite the frontmatter block in the note’s ending,
and add a block, or a tag line in the body, in it too. The rest of the note is
not touched.search-vault returns a matching line without its line ending.create-note, edit-note replace and create-file write exactly what
they are sent, because the caller supplies the whole file.
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.
create-file and fetch-file write the bytes they are given to a .md
destination only if they are valid UTF-8, otherwise they fail and write
nothing; upload-file and replace-file do the same.delete-file on a note does what delete-note does.move-file on a note does what move-note does, including rewriting the
wikilinks that point at it. It does not rename a note to another file type or
the reverse; use move-note for that.create-file and fetch-file never replace anything: they fail if the path
already holds a file or a link, even when two calls arrive at once.move-file, move-note, delete-file and delete-note refuse a folder, so
files cannot be carried out from under the deny rules; move or delete the
files one at a time.denyPaths applies to every file.
denyBinaryPaths
protects files that are not notes, and never a note.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 |
pdf, not .pdf or *.pdf. * matches
any run of characters and ? one character.jpg finds IMG_0001.JPG.gz finds backup.tar.gz. A
file with no extension is listed only by *.list-files adds the size in bytes as a third field
(list-notes lines have the path and timestamp only).
Fields are separated by tabs. path scopes the listing to a folder. Neither
tool reads a file’s content, so neither gives a hash: read-file does..obsidian and
.trash) and symbolic links are not listed. If path is itself a hidden
folder, only its notes are listed, whatever extension says (list-files).extension (list-files only) is at most 64 characters and has no spaces
at either end.denyPaths applies.
denyBinaryPaths does
not hide names: it only stops a file’s content being read.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: true returns the note’s headings instead of its content. The
first line gives the characters, lines and number of headings; each heading
then has its line range, the size of its section, which occurrence of that
text it is, and the heading itself:
18420 characters, 312 lines, 3 headings
lines 1-310 | 18390 chars | occurrence 1 of 1 | # Project
lines 12-140 | 7210 chars | occurrence 1 of 1 | ## Design
lines 141-310 | 10980 chars | occurrence 1 of 2 | ## Notes
heading, with occurrence if the heading repeats, returns that section:
the heading line and everything under it up to the next heading of the same or
a higher level, sub-sections included, without the blank lines that follow it.
This is the extent edit-note’s section operations work on, so a heading from
the outline can be passed straight to edit-note. A section longer than the
limit is refused too; read one of its sub-sections instead.
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.
expectedMtimeEvery 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-filecreate-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-statusReturns 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-fileupload-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:
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.)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" }
curl command, exactly as for an upload..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):
expectedSha256 and expectedMtime
must both match the file in the vault. They are checked when the URL is
reserved, and again when the file arrives, on the very file that is about to
be swapped out. If it changed in between, nothing is replaced and the answer is
409. A refusal says that the file has changed and to read it again, and does
not give its hash..trash under a name that
carries the time, such as report_1760812345678.pdf, as with a
delete-file that is not permanent, and the reply’s trashedAs says
which. move-file can put it back. The server deletes it for good
after trashRetentionDays (30 by default).
The new file keeps the old file’s permissions (but not setuid, setgid or
sticky bits, and not its owner).denyBinaryPaths
cannot be replaced, because the hash check would let a caller test guesses at
a file it is not allowed to read. A .md is not covered by that setting, and
is accepted only if it is valid UTF-8, as for an upload.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-fileThe 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-filecreate-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:
wc -c,
sha256sum), never by hand.It calls the tool:
upload-file -> { filename: "report.pdf", folder: "attachments", size: 810490, sha256: "<64 hexadecimal characters>" }
https://server:4001/up/<random token>,
when it expires, and a ready-to-run command.curl --fail-with-body -sS -T report.pdf "<url>".{"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:
Content-Length or a hash mismatch all kill it just as a good upload does. If
anything goes wrong, call the tool again for a new URL.curl often leave from different networks. With
uploadNetworkPrefix set, it must
come from the same address as the call that reserved the URL (32) or the same
IPv4 network (16 to 31). Behind a reverse proxy that check needs
trustedProxies; see
Configuration.uploadTtlSeconds (300 by default).replace-file), and it must pass
the denyPaths check, both when the URL is
reserved and again when the file is placed, after following any symbolic
links, so a link that leads out of the vault, or into a denied folder, is
refused. Missing folders are created..md destination is accepted. Use this for a large or generated
note that is already on disk, such as an Excalidraw drawing, so that its text
doesn’t pass through the model; use create-note for text the agent has
written itself. A note is placed only if the received file is valid UTF-8,
otherwise it is discarded with a 400 that says why. Everything else about
the upload is the same as for any other file.size can be at most uploadMaxBytes (50 MiB by
default). At most 16 URLs can be pending and 4 uploads in progress at once.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.