ChaiBuilder Logo

MCP Server Setup

Available from chaipro v0.4.0. Upgrade with pnpm up chaipro@^0.4.0 before 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/mcp on 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 beforeChange hook on Users that only lets a super admin change enableAPIKey, apiKey and apiKeyIndex. 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.

© ChaiBuilder. All rights reserved.