Local cache, incremental sync and persistent memory for Plone — over the Model Context Protocol (MCP).
plone-brain gives AI assistants a fast, cheap, local view of any Plone 6+ site. It mirrors the
content tree into a local SQLite database (with full-text search), keeps it fresh with an
incremental sync driven by the modified watermark, and answers reads without hitting the
server — reducing tokens burned and HTTP requests while enabling offline reads and cross-session
memory.
It is a read/context MCP server: it never writes to Plone. Pair it with a write MCP server such as @plone/mcp or cs-dynamicpages-mcp to get the best of both worlds.
Exploring a Plone site through live API calls is expensive: every question is a network round-trip
that returns large JSON payloads (tokens) and adds latency. plone-brain amortizes that cost:
- Reads are local — answers come from SQLite: 0 network requests, compact output.
- Sync is incremental — only objects changed since the last watermark are fetched.
- Context is small — a ~1 KB "brief" replaces large static context files.
- Memory persists — standing rules/decisions survive across sessions.
- Local-first: cache the content tree in SQLite with FTS5 full-text search.
- Incremental sync with deletion reconciliation (stale objects are removed).
- Progressive disclosure: compact responses by default, full detail on demand.
- Persistent memory:
brain_remember/brain_recallacross sessions. - Product overlays: auto-detects
cs_dynamicpagesand adds dynamic-page tools. - STDIO transport: works with Opencode, Claude Desktop, and any MCP client.
- Portable: one generic core for any Plone site; no per-site configuration required.
- Node.js >= 22
- A Plone 6+ site with
plone.restapienabled (default on Plone 6). - Build tooling for the
better-sqlite3native module (prebuilt binaries are usually available; if not, a C/C++ toolchain may be needed).
Add the server to your opencode.json:
Then, in a session, ask the assistant to use the plone-brain tools (e.g. brain_status,
brain_refresh).
claude_desktop_config.json:
{
"mcpServers": {
"plone-brain": {
"command": "npx",
"args": ["-y", "github:codesyntax/plone-brain"],
"env": {
"PLONE_BASE_URL": "https://your-plone.example",
"PLONE_TOKEN": "your-jwt-token"
}
}
}
}git clone https://github.com/codesyntax/plone-brain.git
cd plone-brain
npm install
node bin/plone-brain.js # or: npm startThe launcher runs the TypeScript sources through tsx, so there is no build step required.
Ask the assistant to run:
brain_status()— verify connectivity and cache state.brain_refresh({ full: true })— populate the local cache (subsequent refreshes are incremental).
Configuration uses the same environment variables as @plone/mcp and cs-dynamicpages-mcp, so
one set of credentials can drive both the read and write servers.
| Variable | Required | Description |
|---|---|---|
PLONE_BASE_URL |
yes | Base URL of the Plone site (e.g. https://plone.example). |
PLONE_TOKEN |
yes* | JWT bearer token. |
PLONE_USERNAME |
yes* | Basic-auth username (alternative to PLONE_TOKEN). |
PLONE_PASSWORD |
yes* | Basic-auth password (alternative to PLONE_TOKEN). |
BRAIN_DB |
no | SQLite path (default: ./.plone-brain/cache.sqlite). |
BRAIN_LANGUAGE |
no | Accept-Language for Plone REST (default: en). |
* Use either PLONE_TOKEN or PLONE_USERNAME/PLONE_PASSWORD.
There are three ways to provide configuration:
Pass variables in the MCP server definition (see the Opencode / Claude Desktop examples above).
Opencode supports {env:VAR} substitution, so secrets can stay in your shell environment.
plone-brain loads a .env file from the current working directory:
PLONE_BASE_URL=https://your-plone.example
PLONE_TOKEN=your-jwt-token
BRAIN_DB=./.plone-brain/cache.sqlite
BRAIN_LANGUAGE=enUseful when connecting to different sites per session:
brain_configure({ "baseUrl": "https://plone.example", "token": "..." })
// or
brain_configure({ "baseUrl": "https://plone.example", "username": "admin", "password": "secret" })| Tool | Description |
|---|---|
brain_configure({ baseUrl, token, username, password }) |
Set connection credentials at runtime. |
brain_status({ checkConnection }) |
Cache size, watermark, and last-known auth. Cache-only by default; checkConnection: true adds a live check. |
brain_brief() |
Standing conventions and product notes (~few tokens). |
brain_refresh({ full, portal_types, pathPrefix, reconcile }) |
Sync from Plone. Incremental by default; reconcile (default true) removes deleted objects. |
brain_search({ query, portal_type, pathPrefix, fields, limit, offset }) |
Full-text search + filters over the cache. |
brain_get({ idOrPath, fields, raw }) |
Fetch a single cached object by UID or path. |
brain_children({ parent, fields }) |
List children of a container (by path or UID). |
brain_remember({ key, value, tags, namespace }) |
Persist a note/rule across sessions. |
brain_recall({ query, namespace }) |
Search persisted notes/rules. |
Resources: brain://brief, brain://status.
When a site uses a supported add-on, plone-brain auto-detects it and adds tools.
Activated automatically when DynamicPageRow content exists. Adds:
| Tool | Description |
|---|---|
brain_dynamic_page({ page, summaryOnly }) |
Assemble a DynamicPage's rows + featured items from cache. |
brain_dynamic_rows({ pagePath, row_type, limit }) |
Query DynamicPageRows across pages. |
plone-brain is read-only and uses the brain_* namespace, so it never collides with the
plone_* tools of the write servers. All servers can share the same PLONE_* environment.
Rule of thumb: read/query with
brain_*(local, cheap), write withplone_*(live), then runbrain_refresh()to keep the cache consistent.
Read-only context, search, and memory. No CMS modifications.
{
"mcp": {
"plone-brain": {
"type": "local",
"command": ["npx", "-y", "github:codesyntax/plone-brain"],
"environment": { "PLONE_BASE_URL": "https://plone.example", "PLONE_TOKEN": "{env:PLONE_TOKEN}" }
}
}
}Read with brain_*, write (CRUD, blocks, workflow, translations) with plone_*.
{
"mcp": {
"plone": {
"type": "local",
"command": ["npx", "-y", "@plone/mcp"],
"environment": { "PLONE_BASE_URL": "https://plone.example", "PLONE_TOKEN": "{env:PLONE_TOKEN}" }
},
"plone-brain": {
"type": "local",
"command": ["npx", "-y", "github:codesyntax/plone-brain"],
"environment": { "PLONE_BASE_URL": "https://plone.example", "PLONE_TOKEN": "{env:PLONE_TOKEN}" }
}
}
}Same as above, plus DynamicPage layout tools.
cs-dynamicpages-mcpalready includes the full@plone/mcptoolset (it extends it with the dynamic-page tools). Do not also add@plone/mcpseparately — you would register theplone_*tools twice.
{
"mcp": {
"cs-dynamicpages": {
"type": "local",
"command": ["npx", "-y", "github:codesyntax/cs-dynamicpages-mcp"],
"environment": {
"PLONE_BASE_URL": "https://plone.example",
"PLONE_USERNAME": "admin",
"PLONE_PASSWORD": "{env:PLONE_PASSWORD}"
}
},
"plone-brain": {
"type": "local",
"command": ["npx", "-y", "github:codesyntax/plone-brain"],
"environment": { "PLONE_BASE_URL": "https://plone.example", "PLONE_TOKEN": "{env:PLONE_TOKEN}" }
}
}
}- Full sync (
brain_refresh({ full: true })): fetches every object in scope (withfullobjects) and stores a normalized row per object:uid,path,portal_type,review_state,modified,parent_path, plus the raw JSON and an FTS index. A whole small site can be cached in one request. - Incremental sync (
brain_refresh()): fetches only objects withmodifiednewer than the last watermark. - Deletion reconciliation: after an incremental sync, a metadata-only scan yields the current path list; cached paths that no longer exist upstream are removed. (Full syncs reconcile for free.)
- Reads (
brain_get,brain_search,brain_children,brain_dynamic_page) are served entirely from SQLite: 0 network requests. - Memory (
brain_remember/brain_recall) is stored in the same SQLite database, namespaced, and persists across sessions.
The SQLite file lives at BRAIN_DB (default ./.plone-brain/cache.sqlite) and is safe to delete to
force a cold rebuild.
npm install
npm run typecheck # tsc --noEmit
npm test # vitest
npm run build # compile to dist/ (for npm publishing)
npm start # run the server via tsxsrc/
stdio-server.ts # MCP entry point (STDIO)
config.ts # env + .env configuration
plone/
client.ts # REST client (auth, retries, request counter)
search.ts # @search helpers (pagination, metadata-only)
store/
db.ts # SQLite open + migrations
schema.sql # reference schema
repository.ts # objects, FTS, memory
sync/engine.ts # full/incremental sync + deletion reconciliation
products/
registry.ts # product overlay detection/registration
cs_dynamicpages/ # cs_dynamicpages overlay
tools/register.ts # brain_* tools
output.ts # compact output formatting
bin/plone-brain.js # tsx launcher (no build required)
MIT © 2026 CodeSyntax
{ "$schema": "https://opencode.ai/config.json", "mcp": { "plone-brain": { "type": "local", "command": ["npx", "-y", "github:codesyntax/plone-brain"], "environment": { "PLONE_BASE_URL": "https://your-plone.example", "PLONE_TOKEN": "{env:PLONE_TOKEN}" }, "enabled": true } } }