Plone Dynamic Pages MCP Server
# Plone Dynamic Pages MCP Server
An MCP server that **extends the official [`@plone/mcp`](https://github.com/plone/plone-mcp) server** with seven additional tools for managing Plone sites built with the [cs_dynamicpages](https://github.com/codesyntax/cs_dynamicpages) product. Call `plone_configure` once per session, then use the full official toolset plus the dynamic-layout tools.
## 🚀 Key Features
* **Official toolset included**: All `@plone/mcp` tools (blocks, workflow, translations, users, navigation tree, vocabularies, search) are available as-is.
* **Dynamic Layout Management**: Analyze full page structures, including rows and featured items.
* **Full CRUD Operations**: Create, patch, move, and delete Plone content and layout components.
* **Universal Content Support**: Generic `plone_create_content` tool for any Plone content type (Folder, Document, Link, etc.).
* **Local File Integration**: `plone_upload_local_asset` uploads files straight from your filesystem.
---
## 🛠️ Dynamic Pages Tools (added on top of `@plone/mcp`)
| Tool | Description |
| :--- | :--- |
| `plone_get_site_definitions` | Fetches site-specific definitions (Schemas and Row Types) from Plone. |
| `plone_get_dynamic_page_content` | Returns the full JSON structure of a DynamicPage (Rows + Featured Items). Handles up to 1000 items. |
| `plone_create_dynamic_page_row` | Creates a new layout section (`DynamicPageRow`) in a page, optionally with nested featured items. |
| `plone_create_dynamic_page_row_featured` | Creates a featured item (`DynamicPageRowFeatured`) within an existing row. |
| `plone_move_dynamic_page_row` | Reorders layout rows (top, bottom, or a specific position). |
| `plone_upload_file` | Uploads images or files using Base64 data. |
| `plone_upload_local_asset` | Reads a file from the local filesystem and uploads it to Plone. |
Everything else comes from the official package: `plone_configure`, `plone_create_content`, `plone_update_content`, `plone_delete_content`, `plone_search`, `plone_get_navigation_tree`, `plone_get_block_schemas`, workflow and translations tools, etc.
---
## 📚 MCP Resources
Besides tools, the server exposes read-only MCP resources, discoverable by any MCP client:
| Resource | URI | Content |
| :--- | :--- | :--- |
| Architecture | `cs-dynamicpages://docs/architecture` | Markdown reference: hierarchy, row types, common fields and migration mapping. |
| Migration protocol | `cs-dynamicpages://docs/migration` | Markdown "Skeleton + Mapping" workflow for replicating pages. |
| Row types | `cs-dynamicpages://row-types` | JSON list of available row types, from the site registry. |
| DynamicPageRow schema | `cs-dynamicpages://schemas/row` | JSON schema of `DynamicPageRow`. |
| DynamicPageRowFeatured schema | `cs-dynamicpages://schemas/row-featured` | JSON schema of `DynamicPageRowFeatured`. |
The two documentation resources work without a Plone connection. The other three require `plone_configure`; until then they return a short message explaining how to configure the connection instead of failing.
The official `@plone/mcp` resources (`plone://site`, `plone://types`, `plone://content{+path}`) remain available.
---
## ⚠️ Differences from the standalone v1 server
Earlier versions of this server shipped their own `set_session_context` / `check_credentials_status` / `create_dynamic_page_row` tools and authenticated with a `__ac` cookie. As of v2 the server delegates to `@plone/mcp`:
- Connect with `plone_configure({ baseUrl, token })` or `plone_configure({})` using `PLONE_BASE_URL` / `PLONE_TOKEN` (or username/password). The client does **not** support cookie auth; use a token or basic auth.
- Tool arguments use site-relative **paths** (e.g. `/rows`, `/en/home`). Full `++api++` URLs are also accepted and normalized to site-relative paths automatically.
- `create_content`/`search_content`/`patch_content`/`delete_content` map to `plone_create_content`/`plone_search`/`plone_update_content`/`plone_delete_content`.
---
## đź’» Installation & Usage
The server runs over STDIO (no Plone-side changes required) and can be launched either directly from the GitHub repository via `npx` or from a local clone of this repo.
### Quick Start: run via npx from GitHub (no cloning required)
Add the following configuration to your **Opencode** (`opencode.json`):
```json
{
"mcp": {
"cs-dynamicpages-mcp": {
"type": "local",
"command": ["npx", "-y", "github:codesyntax/cs-dynamicpages-mcp"],
"enabled": true
}
}
}
```
`npx` fetches the repository from GitHub and launches the committed build automatically; nothing else needs to be installed. `github:` resolves to `git+ssh`, so it requires an SSH key for GitHub; without one, use the HTTPS form: `npx -y git+https://github.com/codesyntax/cs-dynamicpages-mcp.git`.
Authentication is provided through the server's `environment` block — see [Credentials & environment variables](#credentials--environment-variables). A `.env` file is not read.
### Running from a local clone
Clone the repository and install its dependencies:
```bash
git clone https://github.com/codesyntax/cs-dynamicpages-mcp
cd cs-dynamicpages-mcp
npm install
```
Then point your MCP client at the local entry point instead of the GitHub package:
```json
{
"mcp": {
"cs-dynamicpages-mcp": {
"type": "local",
"command": ["npx", "tsx", "src/local.ts"],
"cwd": "/absolute/path/to/cs-dynamicpages-mcp",
"enabled": true
}
}
}
```
The `cwd` is required because MCP clients launch the command from an arbitrary
directory; without it the relative `src/local.ts` will not resolve. You can also
run the server directly from the clone with `npm start` (after `npm run bundle`).
### Running from a local clone without `tsx`
The compiled entry point (`dist/local.js`) is committed to the repository, so it is
available without building:
```json
{
"mcp": {
"cs-dynamicpages-mcp": {
"type": "local",
"command": ["node", "/absolute/path/to/cs-dynamicpages-mcp/dist/local.js"],
"enabled": true
}
}
}
```
### Credentials & environment variables
`@plone/mcp` reads its configuration from the process environment. Supported variables:
| Variable | Description |
| :--- | :--- |
| `PLONE_BASE_URL` | Base URL of your Plone site (fallback for `plone_configure`). |
| `PLONE_TOKEN` | Bearer token for authentication (alternative to username/password). |
| `PLONE_USERNAME` / `PLONE_PASSWORD` | Basic auth credentials. |
| `ENABLED_TOOLS` | Optional comma-separated allow-list of tool names; applies to both the official tools and the dynamic pages tools. |
| `PLONE_SESSION_TTL` | Session TTL in milliseconds (from `@plone/mcp`). |
| `PLONE_PREPARED_BLOCKS_TTL` | Prepared-blocks TTL in milliseconds (from `@plone/mcp`). |
Provide them through the MCP server's `environment` block in `opencode.json`:
```json
{
"mcp": {
"cs-dynamicpages-mcp": {
"type": "local",
"command": ["npx", "-y", "github:codesyntax/cs-dynamicpages-mcp"],
"environment": {
"PLONE_BASE_URL": "https://your-plone",
"PLONE_TOKEN": "eyJhbGciOi..."
},
"enabled": true,
"timeout": 60000
}
}
}
```
Or with basic auth instead of a token:
```json
"environment": {
"PLONE_BASE_URL": "https://your-plone",
"PLONE_USERNAME": "admin",
"PLONE_PASSWORD": "secret"
}
```
Then call `plone_configure({})` once per session. The credentials live in the server process environment; the model never needs to read them.
> ⚠️ **A `.env` file is not loaded into the MCP server.** Verified with OpenCode: with a variable defined only in `.env`, the server process received `undefined`, and `{env:VAR}` resolved to an empty string (OpenCode runs a long-lived background service, so the MCP process inherits the service environment, not the environment of the shell that runs a single command).
If you prefer not to store secrets in `opencode.json`, use substitution (`"PLONE_TOKEN": "{env:PLONE_TOKEN}"`) and make sure the variable is present in the environment that starts the OpenCode **service** (then restart it, e.g. `opencode service restart`). Exporting it only in the shell of a one-off command does not reach the server.
*Note: credentials can also be passed at runtime with `plone_configure({ baseUrl, token })` (or username/password).*
Requires a Node.js version supported by `@plone/mcp`: `^20.19.0 || >=22.12.0` (Node.js 22+ recommended).
---
## 🏗️ Project Structure
* **`src/local.ts`**: Entry point for local `stdio` execution (the extended server).
* **`src/extended-server.ts`**: Creates the `@plone/mcp` server and registers the dynamic pages tools.
* **`src/plone-mcp.ts`**: Single adapter re-exporting the `@plone/mcp` internals the wrapper relies on.
* **`src/tools/`**: One file per dynamic pages tool (`plone_*.ts`), plus `shared.ts` (helpers and schemas) and `index.ts` (the `dynamicPagesTools` list and `registerDynamicPagesTools`), mirroring the `@plone/mcp` layout.
* **`src/dynamicPages/`**: Pure helpers (payload builders, ordering computation, hierarchy reassembly) shared by the tools.
* **`scripts/build.mjs`**: esbuild bundling of `src/local.ts` into `dist/local.js`.
---
## đź§Ş Development
```bash
npm install # .npmrc sets legacy-peer-deps for npm 10 + vitest
npm run bundle # rebuild dist/local.js (esbuild) — run before committing
npm test # vitest unit + handler seam tests
npm run typecheck # tsc --noEmit
npm start # run the compiled stdio server (node dist/local.js)
npm run dev # run from source with tsx (src/local.ts)
```
`dist/local.js` is intentionally committed: git installs (`npx github:…`) must not
run a build, because that would install devDependencies and npm 10.9.x crashes on
vitest's peer set. Remember to run `npm run bundle` and commit `dist/` after
changing the source.
> The build script is named `bundle` (not `build`) on purpose: npm/pacote treat a
> `build` script as a git-dependency preparation trigger, which would reinstall
> devDependencies on `npx github:…`.
This project pins `@plone/mcp` to the published `1.0.0-alpha.2` package. To track
unreleased changes from the official repository instead, point the dependency at
the branch (`git+https://github.com/plone/plone-mcp.git#main`). Note that a git
dependency requires **npm >= 11** (or `legacy-peer-deps` on npm 10), because
`@plone/mcp` compiles itself on install.
---
## đź”’ Security
This server handles credentials strictly within your local environment. The `@plone/mcp` code runs from `node_modules` on your machine, and your API tokens are only ever sent to the Plone site you configure. Ensure you trust the source before providing sensitive API tokens.
TDQS
Scored across 13 tools
Most tools have clear, distinct purposes (e.g., get_site_definitions vs. get_dynamic_page_content). However, overlap exists between create_content and the specialized row creation tools, and between upload_file and upload_local_asset, which could lead to confusion about which to use.
All tool names follow a consistent verb_noun pattern using lowercase with underscores (e.g., create_content, patch_content, search_content). The naming is uniform and predictable, with no mixing of conventions.
The server has 13 tools, which is within the ideal 3-15 range. Each tool serves a distinct functional need—configuration, CRUD, dynamic page operations, and file uploads—without being excessive or sparse.
The tool set covers the core domain of Dynamic Page management, including creation, retrieval, update, delete, search, and specialized row operations. A notable gap is the lack of a generic get_content tool for non-DynamicPage objects, but search_content can serve that purpose, so the gap is minor.