@cloudhal/mcp-server
v0.6.2
Published
Cloudhal MCP server: semantic site tools (pages, articles, snapshots, preview, publish) governed by the policy engine
Readme
@cloudhal/mcp-server
The Model Context Protocol surface of Cloudhal (spec 2026-07-28, @modelcontextprotocol/server 2.3). ADR-0004 and ADR-0011.
Tools: every tool, with its scopes and whether a person approves it, is in the tool reference below, generated from the definitions in src/tools.ts (node scripts/mcp-tools-doc.ts; CI fails when it is stale). The server lists them in a fixed order. Every tool has JSON Schema input/output (built from the contract schemas), honest annotations and structuredContent. Updates need the current rev (optimistic locking). Every mutation snapshots automatically (exactly one snapshot per call; governance snapshots carry cloudhal.opId).
Taxonomy (RFC-0002): create_entry / update_entry take terms: { [taxonomy]: { set?, add?, remove?, primary? } } with values by id, slug, old slug, name, synonym or singular/plural, resolved by the store; an unknown or ambiguous value is an error with suggestions (unknown_term, ambiguous_term), never an implicit create. tags is a deprecated alias for terms.tag.add. list_entries takes the RFC §5 filter. Agents add terms only through propose_term (allowNewTerms: none → denied, propose → proposed, free → active; an existing match is returned). Structural tools (site:write) always wait for a person; their prompts list the articles touched and every redirect. Agents never redirect off-site, never reclaim slugs and never approve their own proposals (checked before anyone is asked). Store invariant errors map to stable codes (required_term, too_many_terms, …) with the next step in the text. Assignment deltas and governance results (opId, counts, redirects) go into the audit row's input.effect.
Approval: publish, rollback, delete_* and the structural taxonomy/redirect tools return MCP's multi-round-trip input_required with an elicitation (the message carries the diff summary) and an HMAC-signed requestState bound to the tool, the arguments and the principal. The person answers in their client; the call completes only on an explicit accept. Clients without elicitation get a confirmToken to pass on a second identical call (confirmTokenFallback: false turns that off).
Skills: skill://<name>/SKILL.md and skill://<name>/references/<file> resources. A client that follows a skill sends _meta["cloudhal.skill"] = "<name>"; the policy engine then limits the call to that skill's declared scopes.
import { createCloudhalMcpServer } from "@cloudhal/mcp-server"; // McpServer for one principal
import { serveCloudhalStdio } from "@cloudhal/mcp-server/stdio"; // what `cloudhal mcp` runs
import { createCloudhalMcpHandler } from "@cloudhal/mcp-server/http"; // fetch(request, principal) — mounted at /mcp by @cloudhal/apitools.ts is SDK-independent (defineTools(), runTool() = policy → audit → handler) and is what the tests exercise first; test/e2e.test.ts runs a real client ↔ server pair over the in-memory transport and proves the Sprint 5 exit criterion.
Tool reference
110 tools, in the order the server lists them.
| Tool | Scopes | Kind | Approval | What it does |
|---|---|---|---|---|
| list_sites | site:read | read | — | Sites this principal can operate. |
| get_site | site:read | read | — | A site with its page order, locales, theme, published snapshot and per-collection settings (collections.<key>.permalink, e.g. /blog/{slug}; a collection without one lives at /<c… |
| list_pages | content:read | read | — | Pages of a site in navigation order (all statuses). |
| get_page | content:read | read | — | One page with its sections and current revision (send rev back when updating). |
| create_page | content:write | write | by the key's mode | Create a draft page and append it to the site's navigation. |
| update_page | content:write | write | by the key's mode | Replace a page's title, path, sections or seo, or set its own motion level and chrome (RFC-0025: header default|transparent|hidden, footer default|minimal|hidden; null goes back t… |
| get_catalog | content:read | read | — | The components a page surface may use on this site (RFC-0020): Cloudhal's layout, content and interactive primitives plus the theme's block types, with their props, child rules, b… |
| patch_draft | content:write | write | by the key's mode | Build or change a page step by step with RFC 6902 JSON Patch operations on /title, /sections and /seo, e.g. {"op":"add","path":"/sections/-","value":{…a surface…}}. |
| import_surface | content:write | write | by the key's mode | Add (or replace) a page surface from another generative-UI format: a2ui takes A2UI v0.9 messages (createSurface, updateComponents, updateDataModel; basic catalog), json-render… |
| export_surface | content:read | read | — | A page's surface section in another generative-UI format: a2ui (A2UI v0.9 messages for the basic catalog, images as absolute URLs when the site has a primary domain) or json-re… |
| delete_page | content:write | write (destructive) | by the key's mode | Delete a page and remove it from navigation. |
| list_entries | content:read | read | — | Content entries, newest first. |
| get_entry | content:read | read | — | One content entry with its body and current revision. |
| create_entry | content:write | write | by the key's mode | Create a draft content entry (markdown body) in an existing collection (list_collections; a new one needs create_collection first), optionally filed under existing terms (terms;… |
| update_entry | content:write | write | by the key's mode | Update title, summary, body, slug, status (draft → review → published), terms (terms: add, remove or set categories and tags by id, slug or name) or typed custom fields (fields… |
| add_correction | content:write | write | by the key's mode | Append a correction, clarification or update notice to an article (RFC-0019 §2) when a published story changed in substance. |
| delete_entry | content:write | write (destructive) | by the key's mode | Delete a content entry. |
| list_translations | content:read | read | — | Every language version of a page or entry (its translation group): locale, status, method (original, human, machine, machine-reviewed), whether it is stale (its source changed sin… |
| list_stale_translations | content:read | read | — | Translations whose source changed since they were made (or whose source is gone), optionally in one locale: the work queue for keeping languages in step. |
| get_translation_source | content:read | read | — | What you need to translate a page or entry into locale: the source (translate its title, text, summary and SEO; keep section ids, kinds, links, numbers, images and bindings), fo… |
| save_translation | content:write | write | by the key's mode | Save your translation of a page or entry into locale as a draft linked to the source's current revision, with your model as its provenance. |
| search_site | content:read | read | — | Find nodes by words instead of listing everything. |
| get_theme_settings | content:read | read | — | The theme's design settings (RFC-0017): their schema (colors as #rrggbb, enums, numbers; per-locale titles), the contrast rules colors must meet, and the site's current values. |
| set_theme_settings | site:write | write | always a person | Change the site's design settings (colors, type, spacing: get_theme_settings lists them). |
| list_block_types | content:read | read | — | The typed blocks this site can use (RFC-0015): callouts, buttons, video embeds, FAQs and the theme's own. |
| get_body | content:read | read | — | Read a markdown body (an article's, or a page's rich-text section) as blocks: each with its id, hash, type, markdown and who last changed it. |
| edit_body | content:write | write | depends on the call | Change a markdown body by block instead of rewriting it: replace a block, replace text inside one, insert, move or delete blocks, insert a typed block (insertBlock: a callout, a b… |
| diff_node | content:read | read | — | Compare one page or article between two versions: the fields that changed, and its body (or each rich-text section) block by block, with the words that changed inside edited block… |
| get_design | site:read | read | — | Read this first when you build or redesign pages (RFC-0021). |
| get_design_tokens | site:read | read | — | The site's design tokens as a W3C Design Tokens (DTCG 2025.10) file, for Figma (Tokens Studio), Style Dictionary or a designer: the color roles for light and dark, corner radii, s… |
| set_design | site:write | write | always a person | Replace the site's design rules (DESIGN.md, Markdown, at most 24,000 characters; null removes them). |
| get_chrome | site:read | read | — | The site's header, footer and brand (RFC-0025): what is set, what pages show (the defaults filled in), the form schemas with every allowed value, and the menu keys the header and… |
| set_chrome | site:write | write | always a person | Change the site's header and footer (get_chrome lists the fields and allowed values). |
| set_brand | site:write | write | always a person | Change the site's brand (get_chrome lists the fields): logo and logoDark as an image asset of the site (asset:<id>, from list_assets or upload_asset; the site's name shows without… |
| render_page | preview:create | read | — | Look at a page's draft as people will see it: renders it in a real browser at desktop (1440), tablet (768) and phone (390) widths and returns screenshots plus automatic checks (si… |
| list_taxonomies | site:read | read | — | The site's taxonomies (e.g. category, tag) and their rules: required (review/published articles need a primary term), maxPerEntry, allowNewTerms (none: only people add terms… |
| list_terms | content:read | read | — | Terms of one taxonomy with names per locale, synonyms (altNames), old slugs (slugHistory), parent, status, visibility and usage (articles in total and published, last used). |
| resolve_terms | content:read | read | — | Which existing terms of a taxonomy the given ids, slugs, names or synonyms mean (the same rules as assigning terms: id → slug → old slug → name → synonym → singular/plural). |
| propose_term | content:draft:own | write | by the key's mode | Suggest a new term when no existing one fits (check list_terms / resolve_terms first). |
| update_term | content:write | write | by the key's mode | Change a term's text only: names, synonyms, archive intro (description) and SEO per locale, and its image. |
| rename_term | site:write | write | always a person | Change a term's slug (its archive URL). |
| merge_terms | site:write | write | always a person | Merge duplicate terms into one target term of the same taxonomy: every article is re-filed under the target (primary terms follow), the sources retire and their names and slugs be… |
| retire_term | site:write | write (destructive) | always a person | Retire a term: it is removed from every article, its sub-terms move up, and its archive redirects (301) to redirectToId (or, for categories, to its parent) — without a redirect… |
| set_term_parent | site:write | write | always a person | Move a term under another term of the same hierarchical taxonomy (null: to the top level). |
| set_term_visibility | site:write | write | always a person | Make a term public (archive, feed and sitemap entry; shown on articles) or internal (no public URL; themes and filters may still use it). |
| review_term | site:write | write | always a person | Decide on a proposed term: approve (it becomes active and, if public, gets its archive), reject (retired and removed from articles) or merge (into an existing term, mergeIntoId). |
| taxonomy_report | content:read | read | — | Findings about the vocabulary: single-use and unused terms, near-duplicates (edit distance ≤ 2, singular/plural) and synonym collisions, empty or oversized categories, archives wi… |
| create_taxonomy | site:write | write | always a person | Create a taxonomy (e.g. series, region) with its rules and archive path. |
| update_taxonomy | site:write | write | always a person | Change a taxonomy's rules, labels or archive (the key never changes). |
| list_redirects | site:read | read | — | The site's redirects (from → to, 301/302, exact/prefix, reason, source node). |
| create_redirect | site:write | write | always a person | Redirect a path the site no longer serves to another path of this site (agents cannot redirect to other hosts). |
| delete_redirect | site:write | write (destructive) | always a person | Delete a redirect; its old path then answers 404 unless something serves it. |
| list_collections | content:read | read | — | The site's collections (RFC-0040): declared ones, ones that already have entries, and posts. |
| create_collection | site:write | write | always a person | Declare a new collection (RFC-0040), e.g. events, team or jobs, with its names in the site's default locale (labels), an optional icon, permalink (/<prefix>/{slug}, default /<key>… |
| update_collection | site:write | write | always a person | Change a collection's names (merged per locale), icon, feed, sitemap, default sort (RFC-0040) or its typed custom fields (fields, the whole list; RFC-0041), list fields, date-dr… |
| preview_structured_data | content:read | read | — | The JSON-LD document an entry's page emits under its collection's structured-data preset (RFC-0042: Event, JobPosting, Product, Recipe), built exactly as the site builds it, with… |
| rename_field | site:write, content:write | write | always a person | Rename a typed custom field's key (RFC-0041 §6): every entry's value moves from from to to; nothing is converted. |
| change_field_type | site:write, content:write | write (destructive) | always a person | Change a typed custom field's type, or a select's multiple (RFC-0041 §6), converting every value by a closed table (text↔richText, text↔number, text↔select, text↔url, date↔datetim… |
| set_field_localized | site:write, content:write | write | always a person | Make a typed custom field localized (each language version has its own value; translations fall back to the original's) or shared (only the original holds it; translations' own co… |
| remove_field | site:write, content:write | write (destructive) | always a person | Remove a typed custom field (RFC-0041 §6): its value is deleted from every entry, then its definition. |
| set_collection_permalink | site:write | write | always a person | Move where a collection's entries live (RFC-0038): permalink "/blog/{slug}" puts collection "posts" at /blog/<slug>; null goes back to the default /<collection>/{slug}. |
| list_menus | site:read | read | — | The site's navigation menus (primary = header, footer, sidebar, custom keys) with id, rev, maxDepth, item count and how many items are hidden on the site. |
| get_menu | site:read | read | — | One menu: the stored tree (send its rev back when writing) and, per item for a locale, whether the site shows it (state), why not (reason: missing, unpublished, other-locale,… |
| update_menu | site:write | write | always a person | Edit a menu with operations, applied in order and all or nothing: add {item, parent?, index?}, remove {itemId} (with its subtree), move {itemId, parent?, index?}, update {itemId,… |
| set_menu | site:write | write | always a person | Replace a menu's whole tree (or create the menu: omit rev). |
| delete_menu | site:write | write (destructive) | always a person | Delete a menu; the site falls back to the theme's derived default for that key (e.g. top-level pages in the header). |
| list_assets | content:read | read | — | Files of a site's media library, newest first: images, video, audio, documents and data. |
| get_asset | content:read | read | — | One file of the media library with its URL, checksum file and every place that uses it (node, field and title). |
| upload_asset | asset:upload | write | by the key's mode | Add a file to the site's media library from base64 data (at most 4 MB decoded). |
| fetch_asset_url | asset:upload | write | by the key's mode | Have the server download a public https file (at most 25 MB) into the site's media library: the server fetches it under strict rules (https only, no internal hosts or IP addresses… |
| update_asset | asset:upload | write | by the key's mode | Change a file's alt text, title or caption (in locale), credit, license, decorative flag, URL slug or an image's focal point (focal: where the subject is, x and y from 0 to 1… |
| retire_asset | asset:manage | write | by the key's mode | Hide a file from the library and pickers so nothing new uses it. |
| restore_asset | asset:manage | write | by the key's mode | Make a retired file available again. |
| delete_asset | asset:manage | write (destructive) | by the key's mode | Delete a file from the media library. |
| create_snapshot | content:draft:own | write | by the key's mode | Freeze the current state of the site as an immutable, labelled snapshot. |
| list_snapshots | site:read | read | — | Snapshots of a site, newest first, and which one is live. |
| diff_snapshots | site:read | read | — | Added, changed and removed nodes between two snapshots. |
| preview_page | preview:create | read | — | Render a page (or an article by entryId) to HTML as the site would show it, plus a preview link (signed, to the deployed site, or the cloudhal dev URL). |
| publish | site:publish | write (destructive) | always a person | Snapshot the current state and make it live. |
| rollback | site:rollback | write (destructive) | always a person | Point production at an earlier snapshot (must be an ancestor of the live one). |
| get_analytics | analytics:read | read | — | How the site is doing over a range: page views, visits (entrances from other sites or typed in), unique visitors when the site counts them, the estimated cache-hit ratio (on Cloud… |
| top_content | analytics:read | read | — | The most viewed pages, articles and archives in a range, with their current title and path (renamed articles show their new ones; deleted ones read as gone) and the change of page… |
| content_stats | analytics:read | read | — | Readers of one page, article or archive (by id, so the history survives slug changes): page views and visits with the previous period, a series, visits by channel, referrer hosts,… |
| broken_links | analytics:read | read | — | Requests that ended in 404 or 410 in a range, most hits first, with the same-site pages that link to them, when they were last seen and a suggested target (an existing redirect, o… |
| list_comments | comments:read | read | — | Reader comments of a site, newest first (status pending, approved, hidden, spam, deleted, or reported for open reports). |
| get_comment_thread | comments:read | read | — | One article's comments in display order (top level oldest first, each followed by its replies), with the thread's state and open reports' details. |
| comments_summary | comments:read | read | — | Counts for a site's comments: by status, pending ages, open reports, the busiest threads, labels, unanswered questions and the daily cap. |
| label_comments | comments:moderate | write | depends on the call | Add or remove advisory labels on up to 50 comments (spam-suspect, toxic, off-topic, question, needs-reply, praise, personal-data, …) for the people who review them. |
| moderate_comments | comments:moderate | write | depends on the call | Change the status of up to 50 comments. |
| draft_comment_reply | comments:reply | write | depends on the call | Draft a reply on the site's behalf to an approved comment (plain text, 2–5,000 characters). |
| set_comment_thread | comments:moderate | write | depends on the call | Set an article's comment thread: closed (no new comments, existing ones stay) and hidden (nothing shown) reduce visibility; open, default and a closesAt date wait for a person in… |
| list_schedule | schedules:read | read | — | What happens on the site between from and to (at most 62 days): releases (scheduled publishes), time gates (embargoes and expiries on articles and pages) and automation runs, with… |
| get_schedule | schedules:read | read | — | One item from list_schedule by its id (rel_…, gate:<nodeId>:from|until, occ:<automationId>:<at>), with detail. |
| list_releases | schedules:read | read | — | The site's releases (scheduled publishes) in time order, by state: draft, awaiting_approval, scheduled, running, published, conflict, missed, expired, failed, canceled. |
| get_release | schedules:read | read | — | A release with its pinned items (titles, paths, pinned and live versions), what the dependency closure added, overlaps with other releases, locked nodes, the approval and the wait… |
| propose_release | schedules:write | write | depends on the call | Leave a draft release for a person: the named articles and pages are pinned at their current versions (plus what they need that is not live yet), optionally with a time. |
| schedule_release | schedules:write | write | depends on the call | Publish a release (or the named articles and pages, pinned now) at a time, or "on-approval". |
| reschedule | schedules:write | write | depends on the call | Move a release to another time. |
| cancel_schedule | schedules:write | write | depends on the call | Withdraw a release: nothing in it is published. |
| set_embargo | schedules:write, content:write | write | depends on the call | Keep an article or page off the live site until from (an instant): it is absent from every page, list, feed and sitemap until then, enforced by the site itself. |
| lift_embargo | schedules:write | write | depends on the call | Ask a person to publish an article or page before its embargo (remove from, or move it earlier). |
| schedule_unpublish | schedules:write, content:write | write | depends on the call | Take an article or page off the live site at a time (it disappears from every page, list, feed and sitemap from then on, enforced by the site itself). |
| list_automations | schedules:read | read | — | The site's automations: built-in jobs and scheduled agent tasks, with their recurrence, state and next run. |
| list_runs | schedules:read | read | — | What happened at each occurrence of an automation or release, newest first: state (ran or not) and outcome (did something or not), usage, taint and the summary. |
| get_run | schedules:read | read | — | One run with the audit rows, snapshots and approval requests it produced. |
| begin_run | | read | — | Start this automation's run while its window is open (its key works only inside a run). |
| end_run | | read | — | Close your run with a short plain-text summary (what you did, what a person should look at) and its outcome: did-work, nothing-to-do, proposed (you left proposals) or waiting-appr… |
| pause_automation | | read | — | Stop an automation from running again (your own automation's key, or an automation of the person behind your key). |
| get_mode | | read | — | Your current mode, your key's own mode, any time-boxed elevation in force (and when it ends), and the status of your latest elevation request. |
| request_elevation | site:read | read | — | Ask a person for a more autonomous mode for a limited time (5–1440 minutes), e.g. AUTO for an hour to publish a series of small article updates. |
