Let agents edit content over MCP
SunBird has an MCP server at /sunbird/mcp. An AI agent, such as a web factory agent porting an old site's content, uses
it to add pages from templates, fill in their slots, change fragments, upload images, save drafts, and publish, schedule,
roll back, unpublish or delete. Every action an editor can take in edit mode has a tool. It works as one editor account, through the same code as edit mode: the same rules, the same messages, and every change in
history under that editor's name. The agent never writes to the database itself.
The site's structure is still built in code: the pages, templates, slots and fragments in the Razor markup. The MCP server fills that structure with content.
1. Turn the server on
The server is off unless SunBird:Mcp:Enabled is true. Turn it on where agents work, in development and on the
staging slot, and leave it off on the live site:
{
"SunBird": {
"Mcp": { "Enabled": true }
}
}
On Azure App Service, set SunBird__Mcp__Enabled as a deployment slot setting on the staging slot, so it stays with
the slot and a swap never carries it to the live site. While it is off, /sunbird/mcp answers 404.
| Setting | Default | What it does |
|---|---|---|
Mcp:Enabled |
false |
Serves the MCP server at /sunbird/mcp. |
Mcp:AllowPublish |
true |
When false, agents can only save drafts: the tools that change what visitors see (publish, schedule, cancel a schedule, roll back, unpublish and delete a page) are not offered, and are refused if called. |
On a site in staging mode, those tools are never offered, whatever Mcp:AllowPublish
says, because staging shares the live database. Agents save drafts there, and an editor publishes them on the live site.
2. Make an access token
An admin makes a token on the user's page in Users (/sunbird/users), under Agent access: a name, such as
"Web factory", and how long it works (7 days to a year). The token is shown once, when it is made. Copy it then: only a
hash of it is kept, so it cannot be shown again.
The agent acts as that user. Make a separate editor account for agents, such as "Web factory", so history shows which changes an agent made. Revoke a token on the same page; it stops working at once. Suspending the account stops all its tokens too. A call with no token, an expired or revoked one, or one for a suspended account gets 401.
3. Connect the agent
The server speaks MCP over Streamable HTTP. Give the agent the address and the token as a bearer token. For Claude Code, for example:
claude mcp add --transport http sunbird https://staging.example.co.uk/sunbird/mcp --header "Authorization: Bearer sbmcp_..."
Keep the token out of source control, as you would a password.
4. Work on the content
| Tool | What it does |
|---|---|
whoami |
The account the token acts as, and whether publishing is allowed. |
search_icons |
Finds icons by name or tag for an sb-icon slot. |
list_templates |
The templates pages can be made from, and which templates may go under each. |
list_pages |
Every code page and created page (drafts included), with its address and the templates that may be added under it. |
get_page |
A page's saved slots and settings (the draft, or else what is live), its versions, and its stamp. |
create_page |
Adds a page from a template under a parent, as a draft. Returns its key and address. |
save_page_draft |
Saves slots, repeater entries, page settings and a created page's title and slug as the draft. |
publish_page |
Publishes the page's draft. |
schedule_page |
Saves any changes given as the draft and schedules it to go live at a time (UTC). |
cancel_page_schedule |
Cancels a waiting schedule; its content becomes the draft if there is none. |
roll_back_page |
Publishes a copy of an earlier version as the next version. |
unpublish_page |
Takes a created page off the public site, keeping everything. |
delete_page |
Deletes a created page (its history is kept). Refused while it has pages under it. |
list_fragments |
Every fragment, with the partial view that holds its slots. |
get_fragment |
A fragment's saved slots, versions and stamp. |
save_fragment_draft |
Saves a fragment's slots and repeater entries as its draft. |
publish_fragment |
Publishes the fragment's draft, for every page that shows it. |
schedule_fragment |
Schedules the fragment's draft to go live at a time (UTC). |
cancel_fragment_schedule |
Cancels the fragment's waiting schedule. |
roll_back_fragment |
Publishes a copy of an earlier version of the fragment. |
list_media |
The media library. |
upload_media |
Uploads an image (JPEG, PNG, WebP or GIF) or a document (PDF), sent as base64. Returns the item and its id. |
delete_media |
Deletes an item. Refused while a live version or draft uses it; if only older versions do, call again with confirm true. |
For example, to add a news article under a News page that allows the article template:
create_pagewithparentKeynewsandtitleOpening day. It returns the page's key and address (/news/opening-day).get_pagewith that key, for its stamp.save_page_draftwith the key, the stamp and the slots to fill in.publish_pagewith the key and the stamp the save returned.
Slots
get_page and get_fragment list only the slots that have been saved. A slot never saved shows its default from the
markup, so the agent takes slot keys from the page's markup. A slot's full key includes any sb-scope prefix and, inside
a repeater, the entry number: team.2.name. In a fragment, keys are the fragment's own.
A save sends only the slots that change, each with its key and kind:
[
{ "key": "headline", "kind": "text", "text": "We are open" },
{ "key": "photo", "kind": "image", "mediaItemId": "3f2b...", "alt": "The surgery's front desk" },
{ "key": "more", "kind": "link", "text": "Book now", "target": "page:p-abc123def456ghij" },
{ "key": "leaflet", "kind": "file", "text": "Price list (PDF)", "mediaItemId": "9c1d..." },
{ "key": "service", "kind": "icon", "text": "dentistry" },
{ "key": "intro", "kind": "richtext", "document": { "blocks": [
{ "type": "heading", "level": 2, "content": [ { "text": "Opening times" } ] },
{ "type": "paragraph", "content": [ { "text": "Open " }, { "text": "every day", "bold": true } ] }
] } }
]
Rich text is always an HCI.RichText document, never HTML. SunBird writes the HTML from it (see
Pages and slots), and a document SunBird does not accept is refused with a message
saying why. An empty alt marks a decorative image. A link's target is page: and a page key, so the link follows the
page when its address changes, or an address; empty keeps the markup's address.
To add, remove or move a repeater's entries, send repeaters: for each repeater, order gives, for each new position,
the entry number it had before, or null for a new entry. [1, 2, null] adds an entry at the end; [2, 1] swaps two.
Slot keys in the same save use the new entry numbers.
Page settings
settings replaces a page's title, description and share image for search and sharing as a whole:
{ "title": "...", "description": "...", "shareImageId": "..." }. An empty title or description means the page's
default from code. A created page's own title and address go in page: { "title": "...", "slug": "..." }.
Stamps and errors
Every page and fragment has a stamp. Send the one get_page, get_fragment or the last save gave you (null for a
page or fragment never saved). If someone else changed it since, the save is refused, as it would be for an editor, so
read it again and repeat the change.
A refused change is a tool error carrying the message an editor would see in edit mode: a stale stamp, an address that is taken, a template not allowed under that parent, a file that is not an image, a missing media item. A page or fragment key the site does not have is refused, so a typo cannot store content no page shows.