github coddingtonbear/obsidian-local-rest-api 5.5.0

4 hours ago

This release makes every write able to say "only if nobody else changed this first," so two clients editing the same note can no longer silently overwrite each other, and puts limits on simple search so a query full of common words can't return megabytes of results into an MCP client's context.


Features

Conditional writes: two clients writing the same note could silently lose one of the writes

The problem: Only PATCH could be made conditional (through the instruction's ifMatch). PUT, POST, DELETE, MOVE and COPY always went through, so if two clients each read a note and wrote their edited copy back, the second write replaced the first one and nobody was told. The only workaround was to re-read the note right before writing and hope nothing changed in between. (#379)

What's new: Every write to a file now accepts If-Match and If-None-Match, on /vault/{path} and /active/, including writes aimed at a heading, block or frontmatter field. When the precondition doesn't hold, the response is a 412 (error code 41200) and nothing is written.

# Read the note; the ETag response header is its current version
curl -i -H "Authorization: Bearer $KEY" https://127.0.0.1:27124/vault/notes/todo.md
# => ETag: "a1b2c3"

# Write it back only if nobody has changed it since
curl -X PUT -H "Authorization: Bearer $KEY" -H 'If-Match: "a1b2c3"' \
     -H "Content-Type: text/markdown" --data-binary @todo.md \
     https://127.0.0.1:27124/vault/notes/todo.md
# => 204 with the new version in ETag, or 412 and nothing written

There's one version token for a file everywhere you see one: the ETag on a GET of the file (or a section of it), the version in a document map, the new version field in note JSON, and a PATCH instruction's ifMatch are all the same value. PUT, POST and PATCH answer with the new version, so you can chain edits without reading the note again, and If-None-Match: * makes a write create-only. A GET with a matching If-None-Match gets a 304.

Writes made through the API are now queued per file, so two clients holding the same version can't both pass the check before either one writes. That queue only covers writes made through this API. Edits made in Obsidian's editor or by sync plugins aren't part of it, but they do change the file's version, so a conditional write will still notice them.

Two limits to know about: a note's HTML rendering and its note JSON keep their old, non-matching ETag, because they also depend on other files through embeds and backlinks (read note JSON's version field instead). And on MOVE and COPY, the precondition applies to the source; whether the destination may be replaced is still decided by Allow-Overwrite.

Over MCP, vault_write, vault_append, vault_delete, vault_move and vault_copy take an ifMatch argument, vault_write and vault_append also take ifNoneMatch: "*", vault_read returns version, and the write tools return the new one. (#383; thanks @ErikEvenson for the proposal!)

Simple search limits: one search could return more than a model's whole context window

The problem: POST /search/simple/ and the search_simple MCP tool returned every match in every matching file. Obsidian's simple search matches each word of the query separately, so a query containing a common word like "the" matches almost everywhere. In the report that prompted this, one search_simple call returned 3,159 matches across just nine files, about 1.4 MB of JSON. That was larger than the model's entire context window, and because the result was saved in the conversation, every retry failed the same way. Lowering contextLength didn't help much, because the number of matches stayed the same. (#384)

What's new: Simple search takes two new limits:

  • maxResults keeps only the highest-scoring files.
  • maxMatchesPerFile keeps the first matches in each file, in document order.

Each result now has a totalMatches count, so you can compare it with the length of matches to see what was left out, and the REST response has a Total-Results header giving how many files matched in all.

On REST, there's no limit unless you ask for one, so existing scripts get the same results they always have. The new parameters are strict, though: anything other than a single positive integer is a 400 (error code 40090), because a typo that silently returned everything would be exactly the problem the limits are there to prevent.

Over MCP, search_simple now defaults to 20 files and 10 matches per file, and contextLength can be at most 500. When anything was left out, a second text block starting [Search results limited: says how much and how to see more. The first block is still the same JSON array. (#387; thanks @chagwood for the report!)

If you have an MCP client that needs every match, pass a larger maxResults or maxMatchesPerFile. For more than 500 characters around a match, use vault_read on the file that matched. And if your client joins all of a result's text blocks together before parsing it, read only the first block from search_simple. REST callers and the other MCP tools aren't affected.

Don't miss a new obsidian-local-rest-api release

NewReleases is sending notifications on new releases.