Skip to content
Merged
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
# Windows runners whose Git defaults to CRLF conversion.
internal/remote/testdata/** text eol=lf
internal/vault/testdata/** text eol=lf
internal/mcp/testdata/** text eol=lf
69 changes: 67 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ vault too.
core of everything. Prefer a plain editor? Turn Vim off. Files, comments,
bulk actions, editable settings, themes, and per-vault sessions are included.
- **`zn mcp`.** The same tools the desktop's MCP server offers, for Claude
Code, Codex and any MCP client.
Code, Codex and any MCP client, on the whole vault or only the folders you
share, read-only if you like.
- **Local and self-hosted workspaces.** Keep several vaults and server
connections, switch between them, and use the same note commands against
either backend. Remote changes arrive over the live feed with polling fallback.
Expand Down Expand Up @@ -244,7 +245,10 @@ zn base pages "Reading list"
Use the stable ID returned by `task list` with `task toggle <id>`. Comment
replies and resolve/reopen operations take a thread ID. `base add` and
`base set` accept repeated `--set Field=Value` flags for existing fields;
`base add --body` or `--page` also creates a record page. `base convert`
`base add --body` or `--page` also creates a record page. When the view a
database opens on shows only picked rows, `base add` adds the new row to
them, as + Row does in the app, so the view still shows it; other views keep
their picks, and `--json` names the view in `pickedInView`. `base convert`
turns a loose CSV into a `.base` folder, and `base pages` gives every
record that has no page its record page, so a database converted from a
CSV can be linked right away. A page file already in the database folder
Expand Down Expand Up @@ -951,6 +955,67 @@ zn connect https://notes.example.com --no-default
`--no-default` saves the token without making the server zn's own default,
so zn keeps following the app.

### Scoped access

By default an MCP client sees the whole vault. Flags on `zn mcp` narrow what it
can see and change, and the server enforces them on every tool (listings,
search, tags, tasks, backlinks, assets, comments, error messages), so hidden
notes cannot be found indirectly:

```bash
zn mcp --allow-path Projects --allow-path Areas/Health --deny-path Projects/Clients --read-only
```

```json
{ "mcpServers": { "zennotes": { "command": "zn", "args": ["mcp", "--allow-path", "Projects", "--read-only"] } } }
```

| Flag | Meaning |
|------|---------|
| `--allow-path <path>` | Share only this folder or note, and what is inside it. Repeat for more. Without it, the whole vault is shared. |
| `--deny-path <path>` | Hide this folder or note even inside an allowed one. Repeat for more. |
| `--read-only` | Read tools only: no tool that creates, edits, moves or deletes is offered. Same as `--access read`. |
| `--access read\|comment\|write` | `comment` adds the comment tools to the read tools; `write` (the default) is everything the paths allow. |

Paths are vault-relative, as the tools report them (`Projects` in a root-mode
vault, `inbox/Projects` in an inbox-mode one, remapped system folders under
their own names), and a leading `/` is fine. A path matches the folder and
everything below it, never a sibling that merely starts with the same letters.

What the client gets:

- A hidden note answers exactly like a missing one (`Note not found: <path>`).
Paths through a symlink, case variants of a hidden folder and the internal
`.zennotes` directory do not exist for it, and its errors and `vault_info`
never show the vault's absolute location.
- Tools it cannot use are not listed at all. `empty_trash` is never offered
once paths are restricted, since it deletes every trashed note.
- Notes can be created, moved and restored only inside allowed folders, and
never through a symbolic link (which the listings skip, so it would otherwise
look like a new folder). Archiving and trashing a visible note are allowed
even when the archive or trash is not shared; the note then leaves what the
client sees.
- Renaming a note rewrites `[[links]]` only in notes the client can see. On a
ZenNotes server, which rewrites links in every note itself, a scoped client
cannot rename notes.
- A folder is renamed or deleted as a whole only when nothing inside it is
hidden.
- A `--deny-path` that matches nothing in the vault (renamed outside ZenNotes,
or meant for another vault) shares nothing at all until it is fixed, so a
rename can never quietly expose what it hid. The MCP server's stderr, which
clients keep in their logs, names the entry.

The desktop app's bundled MCP server takes the same flags and gives the same
answers (the shared fixture `internal/mcp/testdata/mcp-scope.json` holds both to
it), with one difference: it never rewrites inbound links on a rename, scoped or
not, so a rename through it leaves every other note as it was.

The scope governs the ZenNotes tools only. An assistant that can also run
commands or read files (Claude Code, Codex, OpenCode) can still read the vault
from disk. The design, including what comes next (per-vault profiles chosen
from the desktop app), is in
[`docs/ideas/mcp-scoped-access.md`](docs/ideas/mcp-scoped-access.md).

## Layout of the code

```
Expand Down
11 changes: 11 additions & 0 deletions docs/desktop-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,17 @@ again before every tool call and follows a desktop switch once `vault_info`
confirms it. Until then every other tool stops with a message naming the old and
new workspace, so a switch never redirects in-flight edits.

`zn mcp --allow-path`, `--deny-path`, `--read-only` and `--access` (ZenNotes/tui#21)
apply to whichever workspace the server follows. Entries are vault-relative, so a
switch applies the same paths to the new vault; a `--deny-path` the new vault does
not have shares nothing there. The desktop's bundled MCP server (`out/main/mcp.js`
and the desktop's own `zn mcp`) takes the same flags with the same rules, from
`packages/shared-domain/src/mcp-scope.ts` in ZenNotes/zennotes, and both engines
answer the shared fixture `internal/mcp/testdata/mcp-scope.json` the same way, so a
client is scoped whichever runtime Settings → MCP installed for it. Settings → MCP
shows a client with hand-added flags as needing an update; updating it there drops
them.

## Creation dates

Linux reads filesystem birth time using `statx` where supported; ctime is only a
Expand Down
Loading
Loading