Skip to main content

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:

  1. The target directory is INSIDE Bibliome's library root. Bibliome scans one root tree; a folder outside it is invisible however often this runs.
  2. Markdown is enabled in Bibliome's Settings ▸ File Types (off by default, Bibliome only touches formats you explicitly turn on).
  3. 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_alone in 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, run PRIVATENOTE_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)

Source distribution for notesmith-mcp 0.1.0
File Size Uploaded
notesmith_mcp-0.1.0.tar.gz 165.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for notesmith-mcp 0.1.0
File Interpreter ABI Platform
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}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page