MCP Server Setup
Available from
chaiprov0.4.0. Upgrade withpnpm up chaipro@^0.4.0before following this page. See Releases & Upgrading.
ChaiBuilder Pro speaks MCP (Model Context Protocol). Point Claude Code, Claude Desktop, Cursor, VS Code or any other MCP client at your site and the agent can find pages, read and edit blocks, manage SEO, translate content, upload images, manage redirects, browse revisions and publish, all as you, with exactly the permissions your account already has.
This page covers turning the server on (a developer task, done once per project) and connecting a client (anyone with an account). To add tools of your own, see MCP Custom Tools.
How it works
- One endpoint. The server lives at
/api/mcpon your own site. There is no hosted service in between: the agent talks to your deployment, your database and your media storage. - API key auth. An MCP client has no browser session, so it sends a Payload API key as
Authorization: Bearer <key>. Cookies are never accepted on this route, so a page open in your browser cannot drive the endpoint on your behalf. - Your permissions, nothing more. The request runs as the key's user. Each tool is gated on the permission it needs, and tools you cannot use are not even listed to the agent.
- Drafts first. Edits land in the page draft. The agent is instructed to show you a preview link and ask before it publishes.
- No overwrites. While an agent is editing a page, the builder shows it as being edited by AI, and the agent refuses to write a page someone has open in the builder.
- Stateless. Every request carries everything it needs, so it runs on serverless hosts such as Vercel and Netlify with no sticky sessions.
Enable the server (developers)
The MCP server is opt-in. It is not part of the Pro preset, because it exposes an agent-driven write surface and that should be a deliberate decision. Four steps.
1. Install the MCP SDK
@modelcontextprotocol/sdk is an optional peer dependency of chaipro, only needed by the
project that mounts the route.
pnpm add @modelcontextprotocol/sdk
Any 1.x release from 1.26.0 on is supported.
2. Turn on API keys for users
Enable Payload API keys on your users collection. This adds an Enable API Key checkbox to every user record.
// src/collections/Users.ts
export const Users: CollectionConfig = {
slug: 'users',
auth: {
useAPIKey: true,
// ...your existing auth options
},
// ...
}
Then generate and run a migration, since Payload adds enableAPIKey, apiKey and
apiKeyIndex columns:
pnpm payload migrate:create
pnpm payload migrate
ChaiBuilder reads the key from a plain Authorization: Bearer <key> header and rewrites it
to the form Payload expects, so clients that can only send a bearer token work unchanged.
Payload's own users API-Key <key> form is accepted too. Keys are looked up in Payload's
admin user collection by default; pass apiKeyCollection to createPayloadChaiBuilder if
your keys live elsewhere.
Lock the key fields down. Payload lets users update their own record, so by default any user could turn on a key for themselves. The starter adds a
beforeChangehook onUsersthat only lets a super admin changeenableAPIKey,apiKeyandapiKeyIndex. Keep an equivalent hook if you built your users collection yourself.
3. Register the plugin
Add mcpPlugin to your server plugins. It registers the built-in tools and the workflow
instructions the agent receives.
// src/chaibuilder.plugins.ts
import { mcpPlugin } from 'chaipro/plugins/server'
export const chaiServerPlugins: ChaiServerPlugin[] = [
// ...your other plugins
mcpPlugin({
// Where the agent's preview links point. Must be absolute: the agent has no page to
// resolve a relative link against.
previewUrl: ({ baseUrl, slug }) => `${baseUrl}/next/preview?path=${encodeURIComponent(slug)}`,
}),
]
| Option | What it does |
|---|---|
previewUrl |
({ baseUrl, slug, pageId, lang }) => string. Builds the preview link page tools return. Defaults to ${baseUrl}/chai/preview?slug=.... Pass the preview route your site actually serves. |
exclude |
Tool names to leave out, for example ['delete_page', 'publish_page']. |
include |
When set, the only tools registered. |
getLivePageEditors |
Tells the tools who has a page open in the builder right now, from your realtime backend. Without it, the tools rely on the AI edit lease alone. Must answer fast: a slow lookup is treated as "nobody is editing". |
excludeInstructionSections |
Instruction section ids to drop when your app states its own rules, for example ['review-publish-gate']. |
Some tools only appear when the feature they depend on is registered:
| Tools | Needs |
|---|---|
get_page_revisions, get_revision_outline, compare_page_revisions, restore_page_revision |
revisionsPlugin |
list_redirects, create_redirect, update_redirect, delete_redirect |
redirectsPlugin |
upload_image |
A media backend (payloadMediaPlugin or mediaPlugin) |
start_page_edit, finish_page_edit |
The AI plugin (it owns the edit lease table) |
4. Mount the route
Create one route file. It has the same shape as the builder API route.
// src/app/api/mcp/route.ts
import { registerCustomBlocks } from '@/blocks'
import { getChaiBuilder } from '@/chaibuilder.server'
import { createChaiMcpRouteHandlers } from 'chaipro/mcp'
import { MCP_PERMISSIONS } from 'chaipro/plugins/server'
import { loadWebBlocks } from 'chaipro/web-blocks'
// Page tools convert blocks to and from HTML, which needs the full block registry.
loadWebBlocks()
registerCustomBlocks()
export const runtime = 'nodejs'
export const dynamic = 'force-dynamic'
// Publishing a large page can take a while.
export const maxDuration = 300
export const { GET, POST, DELETE } = createChaiMcpRouteHandlers({
getChaiBuilder: (request) => getChaiBuilder({}, request),
serverInfo: { name: 'my-site', version: '1.0.0' },
connectPermission: MCP_PERMISSIONS['mcp:use'],
onToolCall: (event) => {
console.log('[mcp]', event.toolName, event.outcome, `${event.durationMs}ms`, event.userId)
// Callers only ever see a sanitized message for server errors. The real one is here.
if (event.error) console.error('[mcp] tool error', event.toolName, event.error)
},
})
Keep it under /api. Do not put it under /admin: the admin prefix is redirected, and a
redirected POST loses its body.
onToolCall is your audit trail. It receives every settled call with the tool name,
outcome, duration, user, the page it touched and a size-capped copy of the arguments with
secret-looking keys redacted. A failure inside it never fails the call. See
MCP Custom Tools for the full list of route
options, including a read-only ceiling for the whole route.
Permissions
Connecting at all requires the mcp:use permission. It is deliberately separate from
the page permissions: someone can hold pages:update in the builder and still not be
allowed to drive an agent.
| Role | mcp:use by default |
|---|---|
| Owner, Admin | Yes (they hold *) |
| Editor, Designer | Yes |
| Viewer | No |
The defaults only apply to the built-in roles. If your project manages roles in the
database or passes its own role map, grant mcp:use explicitly to the roles that should
connect.
Beyond mcp:use, each tool needs the same permissions its builder action does. A key
without pages:publish is simply not offered publish_page, and a key without
redirects:* never sees the redirect tools or the instructions about them.
Connect a client
1. Create an API key
Open your user record in the admin (Collections → Users → your account), tick Enable API Key, save, and copy the key. In the starter only a super admin can enable a key, so ask one if the checkbox is locked.
The key acts as you. Anyone holding it can edit and publish anything you can. Keep it out of shared config files and git, and turn it off when you no longer need it.
The starter also ships a Connect an AI agent page at /admin/setup/mcp. It shows your
endpoint, whether your account is ready (membership, mcp:use, API key), and copy-ready
config for each client below with your endpoint already filled in.
2. Add the server to your client
Replace https://your-site.com with your site and YOUR_API_KEY with the key from step 1.
On a preview deployment, use that deployment's URL, not the production domain.
Claude Code
claude mcp add --transport http chaibuilder https://your-site.com/api/mcp \
--header "Authorization: Bearer YOUR_API_KEY"
That adds it for you in the current project. Add --scope user for every project, or
--scope project to write a .mcp.json the team shares. A committed .mcp.json should
reference the key from the environment rather than contain it:
{
"mcpServers": {
"chaibuilder": {
"type": "http",
"url": "https://your-site.com/api/mcp",
"headers": {
"Authorization": "Bearer ${CHAI_MCP_KEY}"
}
}
}
}
Use a variable name like CHAI_MCP_KEY. Claude Code leaves variables named like well known
credentials (for example ANTHROPIC_API_KEY) unexpanded, so they arrive empty. Run /mcp
inside Claude Code to check the connection.
Claude Desktop
Claude Desktop launches MCP servers as local commands, so a remote server goes through the
mcp-remote bridge. Edit the config file, then quit and reopen the app.
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json - Linux:
~/.config/Claude/claude_desktop_config.json
{
"mcpServers": {
"chaibuilder": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://your-site.com/api/mcp",
"--header",
"Authorization: Bearer YOUR_API_KEY"
]
}
}
}
- A
"type": "http"entry (the Claude Code and Cursor form) is rejected by Claude Desktop as "not valid MCP server configurations" and silently skipped. Use the command form. - Keep the
-y. Without it, npx waits for confirmation before installing the bridge and the server never starts. - The Connectors screen in Claude Desktop settings is built around OAuth and has no field for a bearer token, so add this server through the config file.
Cursor
In .cursor/mcp.json for one project, or ~/.cursor/mcp.json for all of them:
{
"mcpServers": {
"chaibuilder": {
"url": "https://your-site.com/api/mcp",
"headers": {
"Authorization": "Bearer ${env:CHAI_MCP_KEY}"
}
}
}
}
Cursor takes no type field and expands environment variables as ${env:NAME}. Paste the
key directly if you are not committing the file.
VS Code (Copilot agent mode)
VS Code prompts for the key the first time and keeps it in its own secret storage.
// .vscode/mcp.json
{
"servers": {
"chaibuilder": {
"type": "http",
"url": "https://your-site.com/api/mcp",
"headers": {
"Authorization": "Bearer ${input:chai-mcp-key}"
}
}
},
"inputs": [
{
"type": "promptString",
"id": "chai-mcp-key",
"description": "ChaiBuilder API key",
"password": true
}
]
}
The top-level key is servers, not mcpServers. If Copilot attempts an OAuth handshake
instead of sending the header, start the server explicitly from the MCP view.
Any other client
Use the Streamable HTTP transport with the endpoint URL and an
Authorization: Bearer YOUR_API_KEY header. SSE-only clients are not supported.
3. Check it works
Ask the agent to list your pages. It should call get_pages_list and come back with them.
To test without a client, list the tools your key can see:
curl -s -X POST https://your-site.com/api/mcp \
-H 'Content-Type: application/json' \
-H 'Accept: application/json, text/event-stream' \
-H 'Authorization: Bearer YOUR_API_KEY' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
The official inspector is the best way to see exactly what a key is offered and to call a
tool by hand. Drop --cli and --method to get its browser UI.
pnpm dlx @modelcontextprotocol/inspector --cli https://your-site.com/api/mcp \
--transport http \
--header "Authorization: Bearer YOUR_API_KEY" \
--method tools/list
What an agent can do
The built-in tools, and the permissions each one needs:
| Area | Tools | Permissions |
|---|---|---|
| Find pages | get_pages_list, get_page_outline, get_page_blocks, read_block_html, get_partial_blocks |
pages:read |
| Edit blocks | add_blocks, edit_block, remove_blocks, add_custom_block, bind_prop |
pages:read, pages:update |
| Manage pages | create_page, duplicate_page |
pages:read, pages:create |
update_page_name, change_page_slug, change_page_parent |
pages:read, pages:update |
|
delete_page |
pages:read, pages:delete |
|
| SEO | get_page_seo |
pages:read |
update_page_seo |
pages:read, pages:update, pages:edit_seo |
|
| Translations | get_block_translations |
pages:read |
update_block_translations |
pages:read, pages:update |
|
| Publishing | publish_page |
pages:publish |
unpublish_page |
pages:read, pages:unpublish |
|
| Revisions | get_page_revisions, get_revision_outline |
revisions:read |
compare_page_revisions |
revisions:read, pages:read |
|
restore_page_revision |
revisions:restore, pages:update, pages:edit_seo |
|
| Images | upload_image |
assets:create |
| Redirects | list_redirects |
redirects:read |
create_redirect, update_redirect, delete_redirect |
redirects:create, redirects:update, redirects:delete |
|
| Edit session | start_page_edit, finish_page_edit |
pages:read, pages:update |
Drafts, preview and publish
Block, SEO and translation edits save to the page draft. The agent returns a preview link
built from your previewUrl and is instructed to ask before calling publish_page.
A few tools change the live site the moment they return, because the thing they change has no draft: renaming a page, changing its slug or parent, unpublishing, and creating, updating or deleting redirects. The agent is told which ones these are.
Editing alongside people
- When the agent writes a page, it takes an AI edit lease on it. The builder shows the
page as being edited by AI, so nobody opens it and overwrites the agent's changes. The
lease ends when the agent calls
finish_page_edit, or five minutes after its last call. - If someone already has the page open in the builder, the agent's write is refused and it tells you the page is busy. It never names who holds the page, since that text goes to any credential that can edit it.
Troubleshooting
| Symptom | Cause |
|---|---|
401 |
The key is wrong, or Enable API Key is off on that user. |
403 |
The account lacks mcp:use, or has no membership in this site. |
405 |
The request was a GET. MCP requests use POST. |
| curl gets a transport error | The Accept header must list both application/json and text/event-stream. |
| Claude Desktop shows no server | The entry uses "type": "http", or -y is missing from the npx args. |
| A tool you expect is missing | Your account lacks its permission, the plugin it depends on is not registered, or it is in the plugin's exclude list. |
| Preview links 404 | previewUrl does not match your site's preview route. |
| An edit is refused as "being edited" | Someone has the page open in the builder, or another user's AI session holds the lease. Retry after they finish. |
| A tool error with a generic message | A server error. The real error reaches your onToolCall hook; check your logs. |
Related
- MCP Custom Tools - add your own tools and instructions.
- AI Setup - the in-builder AI assistant.
- Preview & Publish - drafts, previews and the live site.
- Revisions - what a restore brings back.
- Developing Plugins - how
mcpPluginfits in with the rest.

