Hi! This is a follow-up to two threads.
In my thread about AI as a separate module @Toplin wrote: "Even a disabled interface is still an available interface. It's not as if AI always follows the rules and never breaks them." Nobody answered, but I think it is the most reasonable part of the whole anti-AI reaction. And it can be solved by design.
In Per-note MCP access @laurent agreed that a notebook allow-list is the way, and asked if there is a standard way to implement it in MCP servers. I went to look (with Claude's help, including reading the code of Joplin and of joplin-mcp - so please correct me where I'm wrong). First the answer, then the idea.
Is there a standard way? No. But there is a common pattern
The MCP spec covers authentication (OAuth, for the HTTP transport). "Which data may this client touch" is left to each server. Two things from the spec are still useful:
- The client name is self-declared, the server can't verify it (the spec itself says not to rely on it for security decisions). So the only reliable way to tell "Claude" from "Codex" is a separate token per client.
- The tools spec allows the tool list to differ depending on the credential presented. So per-token rules are in the spirit of the protocol.
Everybody who has this built it themselves, and always the same way - a scoped credential. GitHub fine-grained tokens: selected repos × permissions. Notion: a page must be shared with each integration. Home Assistant: entities are exposed per assistant.
And for Joplin it already exists: joplin-mcp by @shikuz has notebook_allowlist, per-tool switches and content_exposure. I use it every day. Below I tried to separate what core could take from it as is, and where core can do better.
The problem
Today there is one set of tool toggles for everything (chat and MCP), one chat provider at a time, and one token for all MCP clients - the same one the Web Clipper uses. But I don't trust every model equally:
- local Qwen on my own machine - all my notes
- cloud Claude - only work notebooks, read-only
- Codex over MCP - one project notebook, and maybe only titles from another one
"Please don't go to notebook X" in a prompt is not a restriction. Models sometimes ignore instructions, and a web-clipped note can contain text that tells the model what to do (prompt injection). OWASP says the same about LLM apps: implement authorization in downstream systems rather than relying on an LLM to decide. If the software can reach a note, sooner or later the model will.
One more thing. "Allow remote AI providers" is a real hard switch, and I like it. But it protects only the built-in chat. With MCP the cloud model sits on the client side, and Joplin can't know if the client is local or cloud. There the only protection is what the token can reach.
The idea: connectors
A connector = one way an AI reaches my notes. It is either a saved chat provider (LM Studio, Ollama, Joplin Cloud AI, OpenAI, Anthropic) or an MCP client with its own token. Each connector has three settings, all enforced in code, below the model:
- Where it can go - notebook allow-list. Default: nothing. Sub-notebooks inherit from the parent. New top-level notebooks are not added automatically (unless I choose "all notebooks, including future ones").
- What it can do - the tool toggles that already exist, but per connector: search, read, create, update, delete, manage tags...
- What it sees - which fields of a note go to the model: title / body (nothing, snippet, full) / tags / notebook name / dates / images. One default per connector, with an optional override per notebook. This is @Grrruk's idea from the other thread (hidden / title only / full), moved from notes to notebooks, since per-note rules were declined. joplin-mcp has the same thing as one global setting (
content_exposure: none / preview / full).
Example:
| Connector | Notebooks | Can do | Sees |
|---|---|---|---|
| Local Qwen (LM Studio) | all, including future | search, read, create, update | everything |
| Local Ollama | all, "Family" unchecked | search, read | everything |
| Cloud Claude (chat) | Work, Projects | search, read | title, body, tags |
| Cloud Codex (MCP, own token) | Projects/Plugins | search, read, update | title, body |
| Work | search, read | title only |
In Settings it could be a list of connectors. Inside each one: the notebook tree with checkboxes, the same tool toggles as today, and the field checkboxes. Presets would help: "Trusted local - everything", "Cloud - selected notebooks, read-only". Saved chat providers are useful on their own too: I could switch between a local and a cloud model without retyping URL, key and model. And with a token per MCP client I can revoke one client without breaking the Web Clipper.
What to take from joplin-mcp
Its allowlist is in real use since May (v0.8.0), and the bugs found on the way are the best checklist I know:
- Empty list means "nothing", not "no restriction". One code path once read it the other way and leaked notebook names through "did you mean" suggestions (#47).
- Deny wins, and mistakes fail closed - in the author's words, "over-deny rather than over-allow".
- Filtering the notebook list is not enough (also his words, in #13). Every tool that takes a note ID checks the notebook of that note. Every search across notebooks is filtered, snippets included.
- One generic error - "Notebook not accessible", no names, no IDs. Name suggestions come only from allowed notebooks.
- The model can't widen its own scope: no new top-level notebooks, no moving a notebook to the root, a move checks both source and destination.
- 48 end-to-end tests only about allowlist bypasses (test_e2e_allowlist.py) - a ready test plan.
- Exposure levels -
none / preview / full, separately for search results and for a single note, plusmax_preview_length.
Where core can do better
joplin-mcp sits outside and talks to Joplin over the REST API. Core has the data model, so:
- rules by notebook ID from a checkbox tree, not by name patterns. A rename doesn't break the rule, and "Work" can't accidentally match another "Work" deeper in the tree
- the filter goes inside the query, not over results that are already loaded, and it reacts at once when a notebook is moved (no cache)
- tags cross notebooks, so the tag list and tag counts need their own rule: only tags from notes in scope
- semantic search: filter results and chunk text by the allowed notebooks, not by the
notebook_idparameter that the model chooses. The embeddings index can stay one local index for everything - "title only" must not leak the body through a search snippet
- locked notes, trash and conflicts: always out. And if restore from trash or import ever become AI tools, they need the same check - they are easy to forget
Two more things from joplin-mcp (about tokens, not about access)
- Long notes: return the table of contents first, then let the model read one section (
smart_toc_threshold,section). Less tokens, and also less exposure - the model gets only the part it needs. Core hasoffset/max_charspaging, but it is blind. search_noteshaslimit(max 100) but nooffset, so the model can't page, it has to repeat the search with a bigger limit.
What it doesn't solve
An agent with shell access on the same machine (Claude Code, Codex CLI) can read the profile database directly, without any tool. That is a job for OS sandboxing and note lock, not for the tool layer. Connectors protect the usual case: the built-in chat and MCP clients that can only talk through the tools.
Small steps
- One global notebook allow-list, enforced in the tool layer, with the semantics above - what was already agreed in the other thread. But built as a policy object, not as a single setting.
- Several MCP tokens and several saved chat providers, each with its own policy and tool toggles. The current token keeps working as the default connector, so nothing breaks.
- The "sees" fields. Later maybe a log of tool calls per connector.
Technical notes from reading the core code (dev branch)
- Chat and MCP already share one tool registry (
ToolIndex), and both calltool.handler(input, context).ToolContextexists already (today it only carriesselectedFolderId; MCP passes an empty one). A policy object could travel there.ToolIndex.isEnabled()is where the toggles are checked for both, so a per-connector toggle map fits there. - There are 11 global tools. Notebook rules touch the read side in five of them (
search_notes,semantic_search_notes,read_note,read_image,list_notebooks) and the destination check in the write ones (update_notecan move a note vianotebook_id). search_notessearches the whole profile and returns a 240-character snippet.semantic_search_noteshas anotebook_idparameter, but the model chooses it, so it is not a boundary.- Each tool builds its own result, there is no shared "note to output" function. One small shared serializer would be the place for the "sees" filter. What tools return today: title, body / snippet / chunk text, tags, notebook id and title, todo state, created and updated time.
- MCP uses the same single
api.tokenas the Web Clipper API, andclientInfofrominitializeis not read. So today Joplin can't tell one MCP client from another. - The MCP spec doc already expects this: "There is no scope/permission system on the auth token — for v1, the per-tool toggles are the granularity. Token scopes could be added later without breaking existing setups." And in out of scope: "Token scopes. Per-tool toggles are enough until a real need surfaces." I think this is the real need

- Note lock (still behind a feature flag): locked notes are excluded from the keyword index and from the embeddings index, and the body is encrypted. But
read_notedoesn't checkis_locked, so by note ID it looks like it would still return the title, tags and notebook name. Maybe worth a look before note lock ships.
Questions
- One policy per MCP token and per saved provider - is it the right shape? Or better named profiles that can be attached to several connectors?
- Should a locked note be out of reach for every AI tool, including
read_note? - "Title only" still shows titles. For some notebooks only "hidden" is safe - is it fine to have both?
@shikuz - I hope it's OK that I use joplin-mcp as the reference here. Your experience with the allowlist would be very valuable in this discussion.
I can't write this in core myself, but I can help with testing and UX. What do you think?