These are the tools that can change your live site, and every one that does stops to ask first. Here are publishing and scheduling pages, publishing your pending settings changes, and watching, promoting and retrying deployments — together with everything that surrounds a publish: the channels a publish fans out to, the calendar the whole workspace is scheduled on, 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.
Publishing and scheduling pages
Three tools change when a page goes live: schedule_pages_by_id for a chosen set of pages, schedule_pages_by_source_id for every page tagged with a release’s source ids, and publish_page_now for one page immediately. All three are confirm-gated and need page:publish.
schedule_pages_by_id
POST /api/page/schedule-bulk · Operator MCP and the assistant · 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. A past timestamp is allowed: the pages publish on the next run, within a minute, stamped with the date you gave — use it to back-date content. |
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_metadata, 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_by_id 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 route mode differs from local mode, is in how Leed’s AI is guarded.
schedule_pages_by_source_id
POST /api/page/schedule-by-source · Operator MCP only · confirm-gated (route mode) · requires page:publish · Growth and up to execute
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
sourceIds | string[] | yes | — | Up to 1,000 source ids — whatever strings were passed as sourceId when pages were written, usually git commit SHAs |
publishedAt | string (ISO-8601) | yes | — | One shared slot; a past timestamp is allowed, as above |
minimumStatus | draft | in_revision | approved | yes | none | The least-reviewed state a matched page may be in. There is no default |
confirm | boolean | no | false | Omit for a preview |
This is the release tool. A page matches when its current revision carries one of the ids, and only the most recent tag on a revision counts, so pass every id written since the last release — for git SHAs, git log <last-release-tag>..HEAD --format=%H — rather than a hand-picked subset. Matches are processed one by one like schedule_pages_by_id. A matched page that is already published with the id on its live revision is counted in alreadyPublished and needs no action. The response also reports matched, unmatchedSourceIds and, when the match set exceeds 300 pages, remaining — repeat the identical call to schedule the next batch. The preview is free on every plan; executing below Growth returns upgrade_required.
minimumStatus is required because a page’s sourceId can be set by anyone who can edit it, so an unreviewed draft can carry a release commit. draft suits docs-as-code, where the review happened in code review. A CI pipeline calls the same route with an OIDC token from a trust registered under Settings → CI trust; API keys are refused on it.
publish_page_now
POST /api/page/:pageId/schedule · Operator MCP and the assistant · confirm-gated (local mode) · requires page:publish
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
pageId | string | yes | — | The page to publish |
confirm | boolean | no | false | Omit for a preview |
Schedules the page at the current instant, and the publication cron builds it within a minute. For several pages, or a chosen time, use schedule_pages_by_id.
Publishing pending changes
list_pending_changes
GET /api/deployments/pending · Operator MCP and the assistant · 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": "Label: Release Notes", "targetBranch": "main" },
{ "type": "menus", "id": "menu-leeddocs", "label": "Menu: Docs Navigation", "targetBranch": "main" },
{ "type": "company_settings", "id": "company_settings-1223cf", "label": "Settings (General)", "targetBranch": "main" }
]
}type is the kind of resource — labels, page_types, autolinks, search_indexes, menus, forms, users or company_settings — and targetBranch says whether publishing it would touch the live site or the preview. Each kind publishes as its own unit.
It shows exactly what a publish would commit. The same list is what the Your-edits card shows in the CMS — publishing changes is the human version of this call.
publish_pending_changes
POST /api/deployments/publish · Operator MCP and the assistant · confirm-gated (local mode) · high risk
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
items | array of { type, id } | yes | — | Exactly as list_pending_changes reports them; ids are prefixed by kind, such as menu-<id> |
confirm | boolean | no | false | Omit for a preview |
Publishes the selected resources straight to the live production site — they do not pass through staging. Each item publishes independently, and the response is { deployments, unmatched, failed }. An item that matches no pending entry is skipped and returned in unmatched; an item whose kind could not be read is returned in failed with a reason. Always check both. This tool does not publish pages, and it does not publish templates or managed files, which live on staging.
Deployments
Three tools, on both the Operator MCP and the assistant.
list_deployments
GET /api/deployments · read-only · requires deployment:read
Returns the recent preview (staging) and public (production) deployments, plus promotableToPublic and promotionBlockedReason. Each row’s retryable is true only for a failed build that re-running could fix, and its id is the deploymentId that retry_deployment takes.
promote_staging_to_production
POST /api/deployments/promote · confirm-gated (local mode) · requires deployment:promote
Merges staging into main and triggers a production build. Staging carries templates, managed files and CLI pushes; CMS-managed content never passes through it. The route refuses with a 409 while the preview site’s latest deployment has failed or is still building. It is idempotent: already_promoted and nothing_to_promote both return 200 and change nothing.
retry_deployment
POST /api/deployments/:id/retry · confirm-gated (local mode) · requires deployment:publish
Takes deploymentId and re-runs that failed build against the same commit. A deployment that is not failed, whose failure a re-run would hit again, or whose branch has moved is refused with a 400. A retry already taken returns 200 with already_retried.
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.
set_page_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 | discord | yes | — | A page has at most one row per channel; a second call for the same pair updates it |
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_by_id
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 available to the in-CMS assistant, where the first nine park for your approval.
| Tool | Class | Where this is actually done |
|---|---|---|
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 |
delete_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_chart | Assistant chat affordance | Renders a chart inside the chat; MCP clients get rows from get_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 |
The first nine rows are deletes, an approval and a permanent create, which the Leed Assistant parks as pending actions until you press Approve. The tools on this page that do change the live site — publishing, scheduling, promoting and retrying — are on the MCP surface only behind two-phase confirmation, so the model has to show you the plan before it can act.
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_overview, which returns the tier name and nothing else. |
| User creation and invitation | Inviting people |
| Role and email changes, and CI trusts | Managing team members, or the Leed CLI’s leed settings user and leed settings oidctrust |
| The site repository’s contents | Your site repository and the Leed CLI. promote_staging_to_production merges it, but no tool reads or writes a file in it |
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.