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:

  1. create_page with parentKey news and title Opening day. It returns the page's key and address (/news/opening-day).
  2. get_page with that key, for its stamp.
  3. save_page_draft with the key, the stamp and the slots to fill in.
  4. publish_page with 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.