The closest the Operator MCP gets to publishing is scheduling, and even that stops to ask. There is no tool that makes a page live now, no tool that pushes settings to your repository, and no tool that deletes anything. What is here is everything that surrounds a publish: the one tool that can put pages in the queue, the channels a publish fans out to, the calendar the whole workspace is scheduled on, a read-only view of everything you have changed but not published, your short links, and your team’s profiles.
This page ends with the definitive list of what MCP is never allowed to do, and where each of those things is actually done.
Scheduling
schedule_pages is the only tool on the MCP surface that can change when content goes live, and it is one of two tools anywhere on the surface that require an explicit confirmation.
schedule_pages
POST /api/page/schedule-bulk · Operator MCP only · confirm-gated (route mode) · requires page:publish
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
pageIds | string[] | yes | — | 1–100 page ids. Duplicates are removed before processing. |
publishedAt | string (ISO-8601) | yes | — | One shared slot for the whole batch. Must be in the future — a past timestamp is 400 publishedAt must be in the future. |
confirm | boolean | no | false | Omit for a non-mutating preview. Pass true only after you have approved the previewed plan. |
Returns a per-page result set: total, wouldSchedule/wouldFail on a preview or scheduled/failed on execution, and a results array with a row for every id.
Per page, never all-or-nothing
Every id in the batch is evaluated independently against four checks: that the page exists in your workspace, that you hold page:publish on it, that its current status can be scheduled, and that the page type’s required fields are filled. Pages that pass are scheduled even when others fail, and the result explains each failure in place.
| Reason | What it means | Fix |
|---|---|---|
Page <id> not found | No page with that id in this workspace | Resolve the id with list_pages or resolve_path rather than guessing |
Missing page:publish permission | Your role and overrides do not allow publishing this page | Ask for the privilege, or hand the batch to someone who has it |
Page is PUBLISHED and cannot be scheduled — revise it first | The page is already live | Create a revision, then schedule the revision |
Missing required fields: … | The page type requires a feature image, form, summary or keywords that this page does not have | Fill the named fields with update_page_draft, then re-run |
Titles and current statuses appear in the result only after the permission check passes, so a failed permission check does not disclose what the page is.
Two-phase confirmation, route mode
The first call — no confirm, or confirm: false — is a dry run performed by the backing route itself. That matters: the preview is not a guess assembled in the MCP layer, it is the real validation run against real pages, permissions and page types, with nothing written.
{
"dryRun": true,
"publishedAt": "2026-09-15T14:00:00.000Z",
"total": 3,
"wouldSchedule": 2,
"wouldFail": 1,
"results": [
{ "pageId": "abc123", "title": "Release 4.10", "currentStatus": "DRAFT", "wouldSchedule": true },
{ "pageId": "def456", "title": "Changelog", "currentStatus": "IN_REVISION", "wouldSchedule": true },
{
"pageId": "ghi789",
"title": "Pricing",
"currentStatus": "PUBLISHED",
"reason": "Page is PUBLISHED and cannot be scheduled — revise it first",
"wouldSchedule": false
}
],
"instruction": "This was a preview — nothing was changed. Present this plan to the user and ask for approval; only call schedule_pages again with the same arguments plus confirm: true after the user explicitly approves."
}Approving means the same call again with the same arguments plus the flag:
{ "pageIds": ["abc123", "def456"], "publishedAt": "2026-09-15T14:00:00.000Z", "confirm": true }{
"dryRun": false,
"publishedAt": "2026-09-15T14:00:00.000Z",
"total": 2,
"scheduled": 2,
"failed": 0,
"results": [
{ "pageId": "abc123", "title": "Release 4.10", "currentStatus": "DRAFT", "scheduled": true },
{ "pageId": "def456", "title": "Changelog", "currentStatus": "IN_REVISION", "scheduled": true }
]
}Scheduled pages go live on their slot through a per-minute cron, not through anything your client does — scheduling and automatic publishing describes what happens at the slot. Why confirmation exists at all, and how the other confirm-gated tool differs, is in how Leed’s AI is guarded.
Seeing what is unpublished
list_pending_settings_changes
GET /api/deployments/pending · Operator MCP only · read-only · requires deployment:read
No parameters. Returns { items: [...] }, one entry per dirty resource anywhere in the CMS:
{
"items": [
{ "type": "labels", "id": "label-release-notes", "label": "Release Notes", "targetBranch": "main" },
{ "type": "menus", "id": "menu-leeddocs", "label": "Docs Navigation", "targetBranch": "main" },
{ "type": "company_settings", "id": "company", "label": "Company Settings", "targetBranch": "main" }
]
}type is the deployment reason — labels, page_types, autolinks, menus, forms, users, dynamic_ctas, company_settings and the rest — and targetBranch says whether publishing it would touch the live site or the preview.
This is the only publishing-adjacent visibility the Operator MCP has, and it is deliberately read-only: it shows exactly what a publish from the CMS would commit, and stops there. The matching action, publish_pending_settings_changes, is not on this surface. The same list is what the Your-edits card shows in the CMS — publishing changes is the human version of this call.
Distribution channels
Three tools manage the per-page channel rows that a publish fans out to. They sit with the publishing tools rather than with campaign planning because that is what they attach to: a page, and what happens when it goes live.
list_distributions
GET /api/distributions · Operator MCP and the assistant · read-only · requires contact:read
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
pageId | string | yes | — | The page whose channel rows you want |
Returns { distributions: [...] } — one row per armed channel with its enabled flag, config and on-page behaviors. Reads are never plan-gated: a workspace can always see its own rows, including on a tier that could not create them.
upsert_distribution
POST /api/distributions · Operator MCP and the assistant · requires contact:write
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
pageId | string | yes | — | The page this channel is armed for |
channel | email | x | linkedin | bluesky |
enabled | boolean | no | unchanged | Whether the channel is armed for the next publish |
config | object | no | unchanged | Channel-specific settings, validated per channel at the API boundary |
behaviors | object | no | unchanged | autoShareOnPublish, showShareButtons, includeFeatureImage |
update_distribution
PATCH /api/distributions/:id · Operator MCP and the assistant · requires contact:write
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
id | integer | yes | — | The distribution row id, from list_distributions |
enabled | boolean | no | unchanged | |
config | object | no | unchanged | |
behaviors | object | no | unchanged |
An unknown id is a 404 before any plan check, so a wrong id never masquerades as an upgrade prompt.
What the channels are, and what each one costs
| Channel | Minimum plan to arm or configure | What actually sends |
|---|---|---|
email | Starter (releaseNotesEmail) | Nothing yet — the setting is stored |
x | Growth (leadWorkflows + autoSocialPosting) | Nothing yet |
linkedin | Growth | Nothing yet |
bluesky | Growth | Nothing yet |
discord | Growth | Nothing yet |
Three deliberate asymmetries are worth knowing before you script against these tools. Reads are never gated. A pure disable — { "enabled": false } with no other field — is never gated either, so a workspace that downgrades can always switch its leftover channels off. And deleting a row is not available over MCP at all; delete_distribution is assistant-only.
Permissions are the other surprise: these tools are guarded by contact:read and contact:write, the same privileges as the Engage surface, rather than by a page privilege. That is because the email channel reaches your contact list.
The same rows are edited by hand on the page editor’s Workflows tab, described in page workflows, and which plan carries which feature key is in feature availability by plan.
The plan calendar
get_plan_calendar
GET /api/plan/calendar · Operator MCP and the assistant · read-only · requires contact:read
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
from | integer | yes | — | Window start, milliseconds since the epoch — not an ISO string |
to | integer | yes | — | Window end, milliseconds since the epoch |
Returns { from, to, items }, merging three feeds that are otherwise queried separately: scheduled and planned page revisions, deployments, and campaign deliverables. It is the only tool that shows all three on one timeline, which makes it the right first call for “what is going out next week”.
It is not plan-gated even though the campaign tools around it are — the schedule view is core. The screen it backs is the Plan calendar, and the deliverables it merges in have their own tools in MCP tools: campaigns and personas.
Short links
Two read tools. Creating a branded short link is not on this surface, because a shortcode is permanent once created.
list_shortcodes
GET /api/shortcodes · Operator MCP only · read-only · requires shortcode:read · Growth and up
No parameters. Returns { shortcodes: [...] } — the raw metadata for every UTM-tagged short link in the workspace.
get_shortcode
GET /api/shortcodes/:code · Operator MCP only · read-only · requires shortcode:read · Growth and up
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
code | string | yes | — | The short code itself, not the full URL |
Returns { shortcode } resolved to its destination URL and its short link. An unknown code is a 404.
Users
Three tools, all scoped to profile data. None of them can change who a person is or what they may do.
list_users
GET /api/user · Operator MCP only · read-only · requires user:read
No parameters. Returns { users: [...], isDirty: boolean } — every user in the workspace, plus whether the user set has unpublished changes.
get_user
GET /api/user/:userId · Operator MCP only · read-only · requires user:read
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
userId | string | yes | — | The CMS user id |
Returns { user, rbacOverrides } — the profile plus the resource-level overrides granted in this workspace.
update_user
PUT /api/user/:userId · Operator MCP only · requires user:write for anyone but yourself
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
userId | string | yes | — | Which user to update |
firstName, lastName | string | no | unchanged | Cannot be set to an empty string |
jobTitle, department, biography, location, country | string | no | unchanged | Profile fields as shown on the team screen |
phone, language, timezone | string | no | unchanged | |
image, imageAssetId | string | no | unchanged | Avatar URL, or the id of an asset already in the library |
slug | string | no | unchanged | The author page’s URL segment |
visible | boolean | no | unchanged | Whether the person appears as an author on the public site |
externalAccounts | object | no | unchanged | Linked social profiles |
Updating your own profile needs no special privilege. Updating anyone else’s requires user:write — Content Publisher or above.
What update_user cannot do
role and email are stripped out of the tool’s schema entirely, so a client cannot send them even by accident, and the route rejects an email change with 400 Unable to change email regardless of who asks. There is no tool on any surface that creates a user, deletes a user, or sends an invitation.
That is a deliberate line, not an oversight: changing what someone may do, or letting someone new in, is a decision a person makes on the team screen. Managing team members is where roles and the Billing and Developer overrides are granted, and inviting people covers bringing someone new into the workspace.
What MCP is never allowed to do
The tools below are not restricted-but-available. They are absent from the tool surface — never listed to a client, and refused by name with unknown tool if a client tries anyway. Every one of them is either available to the in-CMS assistant, where it parks for your approval, or is a CMS action only.
| Tool | Class | Where this is actually done |
|---|---|---|
publish_page | Publish | The page editor’s publish and schedule controls — publishing and scheduling a page. Over MCP, use schedule_pages for a future slot. |
publish_form | Publish | The form builder — building a form |
publish_menu | Publish | Menus go live through the deployments flow, not a per-menu publish button — publishing changes |
publish_pending_settings_changes | Publish | The Your-edits card in the CMS — publishing changes |
delete_page | Delete | The page list — managing pages |
delete_form | Delete | The form builder; blocked with a 409 while any page references the form or it holds submissions — building a form |
delete_distribution | Delete | The page editor’s Workflows tab — page workflows |
remove_autolink | Delete | Settings → Autolinks — autolinks |
delete_persona | Delete | The Engage personas screen — personas |
delete_campaign | Delete | The Engage campaign screen — campaigns and deliverables |
delete_campaign_deliverable | Delete | The same campaign screen — campaigns and deliverables |
approve_campaign_deliverable | Approve | The Plan calendar’s approval overlay — Plan calendar |
create_shortcode | Create, permanent | A short code cannot be changed or removed once minted — short links and attribution |
get_analytics | Assistant chat affordance | Renders a chart inside the chat; MCP clients get rows from query_analytics instead — MCP tools: analytics and introspection |
get_current_resource | Assistant chat affordance | Reads whatever you are looking at in the CMS. An MCP client has no such context — Leed Assistant |
suggest_next_steps | Assistant chat affordance | Renders the clickable chips under a chat reply — Leed Assistant |
Everything in the first thirteen rows can be run by the Leed Assistant, which parks each one as a pending action and waits for you to press Approve. That is the same list from the other direction: the assistant has an approval step, MCP clients do not have a uniform one, so the destructive tools stay off the MCP surface entirely rather than relying on a client to ask.
Four whole domains with no tools at all
Beyond the tool-by-tool exclusions, four areas of the product have no MCP representation whatsoever — not a read tool, not a write tool.
| Domain | Where it is done |
|---|---|
| Billing and subscription state | Managing your subscription. Nothing here reads your plan except get_company, which returns the tier name and nothing else. |
| User creation and invitation | Inviting people |
| Role and email changes | Managing team members |
| The site repository | Your site repository and the Leed CLI |
Why these boundaries hold even when a client is persistent — and what a refusal looks like at each layer — is how Leed’s AI is guarded. The complete alphabetical index of every tool, with the page that documents it, is the Operator MCP tool index.