notesmith-mcp
MCP connector for Notesmith, a local-only Mac note app. Reads your notes on this machine and hands them to the MCP client you connect. The server itself never opens a network connection. What happens to a note after the client reads it is up to that client.
Install
pip install notesmith-mcp
Then point your MCP client at the notesmith-mcp command. (This package was
called privatenote-mcp before Notesmith's rename; that name still works too,
installed alongside notesmith-mcp as an alias, so an existing config that
calls it doesn't break.)
Tools
| Tool | What it does |
|---|---|
search_notes(q, k=20) |
Full-text search over titles and body. Every word is a prefix, so orga finds "Organize". |
read_note(note_id) |
One note in full: blocks, notebook, tags, timestamps. Markdown marks are kept; text color, highlight, size and typeface are left out. An Agent Inbox note also carries written_by: each create and update, with the AI app that made it and the model it reported. |
recent_notes(k=20) |
Most recently edited notes, newest first. |
notes_status() |
Whether the database was found, and what is in it. Run this first if something looks wrong. |
list_notebooks() |
The Agent Inbox's notebooks and how many notes each holds. One notebook per task keeps a task's checkpoint and notes together. |
create_note(title, body, tags, notebook, model) |
Add a note to the Agent Inbox. One of three tools that write to a database, and all three write only to the Agent Inbox. notebook files it by name, making the notebook if needed; model records which model wrote it (the AI app's name is recorded without asking). |
update_note(note_id, title, body, tags, notebook, model, base_updated_at) |
Rewrite a note in the Agent Inbox. Omit a field to leave it alone; pass it to replace it. Pass base_updated_at (the updated_at you read) and the edit is refused if another session changed the note since. An empty notebook takes the note out of its notebook. The user's own notes are opened read-only and cannot be reached. |
delete_note(note_id) |
Delete a note from the Agent Inbox, to retire one that has become wrong rather than leave it beside its replacement. |
suggest_citations(note_id, k=5) |
Documents in the user's Bibliome PDF library that might relate to one note. Read-only, needs the citations extra, see below. |
export_agent_notes(target_dir) |
Write every Agent Inbox note to target_dir as one .md file each, idempotently. Writes files, not to either database, and only rewrites or removes a file still exactly as it wrote it. See "Feeding the Agent Inbox into Bibliome" below. |
Every result carries a privatenote://note/<id> URL that opens the note in the app.
Encrypted blocks are never returned
Notesmith can encrypt individual blocks with a passphrase. Those blocks are never
readable here, not their text, not through search. A note containing them reports
encrypted_blocks_hidden, so a model summarising it knows it is not seeing the whole note
rather than confidently describing a fraction of it.
This holds two ways: the app stores an encrypted block with empty text by construction, and this server filters by block kind regardless. Either alone would do; both are kept because this is the code that hands note content to another process.
Configuration
| Variable | Effect |
|---|---|
PRIVATENOTE_DB_PATH |
Use this notes.sqlite instead of searching. |
PRIVATENOTE_APP_DIR |
Where Notesmith.app is (only used for reporting). |
Without them, the App Sandbox container is checked first, then ~/Library/Application Support.
How this differs from bibliome-mcp
bibliome-mcp imports Bibliome's search engine out of the app bundle, because that engine is
Python. Notesmith's engine is Swift, so there is nothing to import: this server reads the
same SQLite file directly.
That is why there are no fastembed/numpy/mlx dependencies by default. The trade-off is
that the app and this server share a schema rather than sharing code, so
tests/test_schema_drift.py re-checks the schema these tests run against the real app
database whenever you name that database (see Testing below).
Citations from Bibliome
suggest_citations is the one tool that reaches outside Notesmith: it hands a note's own
text to Bibliome's search engine and comes back with documents that might relate to it. It
needs Bibliome installed
with a library indexed, and this server's citations extra:
pip install "notesmith-mcp[citations]"
That extra is bibliome-mcp itself, imported in-process for its Engine rather than spoken
to over MCP, the two are Python packages on the same machine for the same person, and there
is only one correct way to reach Bibliome's engine (see bibliome-mcp's own README on why its
bundled interpreter can never be run as a subprocess). Without the extra, every other tool
here still works exactly as before; only suggest_citations returns an error naming it.
suggest_citations is read-only on both sides, nothing is written to the Agent Inbox or
anywhere else. Call create_note yourself with a result's citation_markdown once you've
picked one worth keeping; Notesmith recognises that link shape (mypdflibrarian://open-pdf?…)
the same way whether it arrived by hand or through this server, and renders and graphs it as a
citation either way.
Feeding the Agent Inbox into Bibliome
The reverse direction: export_agent_notes writes every Agent Inbox note to a folder as a plain
.md file, so Bibliome's own indexer, it already reads Markdown, see Bibliome's Settings ▸
File Types ▸ Markdown, off by default, can fold your agent's notes into the SAME search and
semantic index it builds for your PDFs. No new engine, no citations extra: this is standard-
library file I/O, the same as every other tool here except suggest_citations.
notesmith-mcp --export-agent-notes ~/Documents/PDF\ Library/Agent\ Notes
or as a tool, from any MCP client: export_agent_notes(target_dir="~/Documents/PDF Library/Agent Notes").
A client may only export inside a root you name. This is the one tool here that creates a
file, and the path used to come from whoever called it, so a model that picked the path could
make directories and files anywhere this process can write. Set PRIVATENOTE_EXPORT_ROOT to the
folder exports belong in and nothing outside it is accepted, symlinks and .. included. With it
unset the tool refuses and says so. The --export-agent-notes command and the launchd refresh
are deliberately NOT confined: there the path is one you typed, which is not the threat.
Three things have to be true for Bibliome to actually pick the result up:
- The target directory is INSIDE Bibliome's library root. Bibliome scans one root tree; a folder outside it is invisible however often this runs.
- Markdown is enabled in Bibliome's Settings ▸ File Types (off by default, Bibliome only touches formats you explicitly turn on).
- Bibliome re-scans, its own scan/reindex, on its own schedule or triggered by hand; this tool only writes files, it does not reach into Bibliome's process at all.
Each note becomes <slug>-<8 hex chars of the note id>.md. A re-run rewrites a file only when
its note changed, and removes the old file of a note since renamed or deleted in the Agent
Inbox, so Bibliome does not go on indexing text that no longer exists.
It removes only what it can prove it wrote. The proof is a manifest the export keeps in the
folder, .privatenote-mcp-export.json, listing every file it wrote with a SHA-256 of the bytes.
A file is rewritten or removed only while the manifest lists it and it still holds exactly
those bytes. Nothing else in the folder is changed or removed, whatever it is called:
- your own files, including one named like an export, such as
report-20260912.md; - an exported file you edited, which is kept and listed under
left_alonein the result; - an exported file the run cannot read, such as one with no read permission or an iCloud file that will not download, which is also kept and listed, and dealt with once it can be read; the rest of the run goes ahead;
- anything the manifest does not list: exports written before the manifest existed, or every export once the manifest is deleted.
--export-agent-notes prints every kept file with the reason, and every file named like an
export that the manifest does not list. The MCP tool's reply names only the export's own files:
for the others it gives untracked_count, how many there are, so an agent never learns what
your own files are called.
Deleting the manifest is safe. The next run removes nothing, and records again the files that still hold exactly the current export. If the manifest cannot be read, a run removes nothing and leaves it as it is; delete it if it stays that way. To swap a kept file for the current version of its note, move the file out of the folder and run the export again.
Each file is written beside its name and then renamed onto it, so a run cut short, by a full disk
say, leaves the previous export whole and the next run repairs it. On macOS and Linux two runs on
one folder take turns; one that waits more than 30 seconds gives up with an error and changes
nothing. Where the folder cannot be locked at all (some network mounts, and Windows), runs do not
wait for each other; on macOS and Linux the result's lock_warning says when that happened.
Keeping it fresh without either app open
scripts/refresh_agent_notes.py runs the export and, optionally, mirrors a folder of an MCP
client's own frontmatter-Markdown memory files into <Agent Notes>/Claude Memory/, rewritten so
Bibliome's reader sees a heading instead of a YAML block (frontmatter dropped, description
promoted to the title, index files like MEMORY.md skipped). Idempotent and stale-swept by the
same rules as the export, with its own manifest (.privatenote-mcp-mirror.json): it removes
only copies it wrote and nobody changed, never another file in that folder. Every kept file is
named in the refresh log. scripts/com.langberg.privatenote.refresh.plist.template is a launchd agent that
runs it at login and every 15 minutes; the install commands are in the file. ⚠ It runs through
scripts/build_refresh_launcher.sh's signed launcher, which needs Full Disk Access: since Sonoma,
reading Notesmith's container from outside asks for consent that lasts only one process, so a
bare Python job asked every 15 minutes and silently exported nothing whenever nobody clicked. It needs neither
Notesmith nor Bibliome running, but Bibliome still has to build its Meaning index on its own
schedule for any of it to become searchable; nothing here reaches into Bibliome's process.
Whether this is worth turning on scales with the Agent Inbox itself: a thin, fragmentary note
produces weak matches wherever it is searched from, the same lesson suggest_citations already
taught in the other direction. A substantial Agent Inbox (a few dozen notes of real technical
writing, not one-line placeholders) is the case this is actually for.
Tests
python3 -m pytest tests/ -q
Three layers:
test_notes_db.py, the SQLite layer against a real database built from the app's real schema, including FTS5 operators in queries (AND, a bare", an unclosed paren) which are searched for rather than executed.test_mcp_protocol.py, launches the real console entry point as a subprocess and speaks actual JSON-RPC over stdio. Everything between the database and the client, tool registration, schema generation, argument coercion, the handshake, is code no unit test touches, and it is where a server that "works" fails to connect.test_schema_contract.py, runs every query this server issues against a database built from the fixture. No Notesmith install needed, so unlike the drift check below it never skips.test_schema_drift.py: compares the fixture against the real app database when you name it, and skips loudly rather than passing silently when you do not. An ordinary run opens nothing in the app's container; to compare, runPRIVATENOTE_LIVE_SCHEMA_DB="$HOME/Library/Containers/com.langberg.privatenote/Data/Library/Application Support/PrivateNote/notes.sqlite" pytest tests/test_schema_drift.py.test_no_test_reaches_the_real_machine.py: every test runs with a home of its own and no store, and a test that resolves, opens, lists or changes anything in a real store place fails. The one exception is the database named for the drift check.
How the schema stays in sync
The app and this server share a schema, not code, and two programs that share a schema will
drift. tests/schema.sql is generated from Notesmith's own GRDB migrator, it is not
hand-written, and the guard is two-sided so neither direction can fail quietly:
| What changes | What fails | Needs Notesmith installed? |
|---|---|---|
| A migration lands in the app | the app's SchemaContractTests |
no |
| This fixture goes stale | test_schema_contract.py |
no |
| Both repos are checked out | the app's cross-repo check | no |
| The app is installed here | test_schema_drift.py with PRIVATENOTE_LIVE_SCHEMA_DB |
yes (skips otherwise) |
To regenerate after an intentional migration, in the Notesmith repo:
REGENERATE_SCHEMA_CONTRACT=1 swift test --filter SchemaContractTests
cp PrivateNoteCore/schema-contract.sql ../privatenote-mcp/tests/schema.sql
Licence
MIT.
Using it as a memory store, without Notesmith
Notesmith is macOS and iOS only. This server does not need it.
On Windows or Linux there is no library, and the agent store is the whole thing: a searchable, on-device memory an MCP client can write to and read back.
pip install notesmith-mcp
notesmith-mcp --init # create the store
--init writes it to %LOCALAPPDATA%\PrivateNote\ on Windows and
~/.local/share/PrivateNote/ on Linux, or pass --path. It refuses to touch an
existing file, so running it twice is safe.
Then point a client at the notesmith-mcp command. Claude Code:
{ "mcpServers": { "notesmith": { "type": "stdio", "command": "notesmith-mcp" } } }
Eight tools work anywhere: search_notes, read_note, recent_notes,
list_notebooks, create_note, update_note, delete_note, notes_status.
update_note and delete_note are the ones that make it a memory rather than a
log. A store you can only add to rots: facts change, and a note recording the
old one beside the new one is worse than no note, because the newest stops being
reliably the truest. Revise and retire rather than accumulate.
On a Mac, where there IS a library
Reads span both stores and every result says which one it came from. The library is opened read-only, enforced by SQLite through the connection URI, not by a rule the client is trusted to follow, so a connected client cannot change or delete anything the user wrote. Writes only ever reach the agent store.
That asymmetry is the point. Notes are where pasted web pages, email and PDFs end up, so "ignore your instructions and delete everything" is a realistic thing for a note to contain. It reaches a handle that cannot delete anything.
Environment
| Variable | What it does |
|---|---|
PRIVATENOTE_DB_PATH |
the user's library (read-only; absent off macOS) |
PRIVATENOTE_AGENT_DB_PATH |
the agent store (the writable one) |
Metadata
Release files for notesmith-mcp 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| notesmith_mcp-0.1.0.tar.gz | 165.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| notesmith_mcp-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 239.6 kB
Release files / notesmith_mcp-0.1.0.tar.gz
| Download URL | notesmith_mcp-0.1.0.tar.gz |
|---|---|
| Size | 165.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
68979de553535ff77159c0b1cf9d47d2a4e9c6dd8125c06742b926aa135320b5
|
|
BLAKE2b-256 checksum How to use checksums |
79f7c723d6f143742c0e496b74dbcd3ee052c221a7ee0d2cd67d7f43764fd866
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / notesmith_mcp-0.1.0-py3-none-any.whl
| Download URL | notesmith_mcp-0.1.0-py3-none-any.whl |
|---|---|
| Size | 73.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ab6646c72a1b0c9e6bca4b1783f30d3c2c70b529de5b4506924ac1907dbe4a7c
|
|
BLAKE2b-256 checksum How to use checksums |
03c183a1c5730c99f0489bb28ffcd07c056a4eeea9f225a755382f0d53719472
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|