Structure tools change the shape of your site rather than its words: which page types exist, what the navigation tree looks like, which labels group your content, what lives at which URL, what your funnel stages are called, and which phrases turn into links automatically. Fifteen tools cover that surface, and none of them puts anything on your live site. Every write lands in a draft state and waits for a person to publish it.
All fifteen tools at a glance
| Tool | Reads or writes | Also in the assistant? |
|---|---|---|
list_page_types | Read | Yes |
get_page_type_schema | Read | No |
list_menus | Read | Yes |
get_menu | Read | Yes |
update_menu_draft | Write — replaces the whole tree | Yes |
list_labels | Read | No |
get_label | Read | No |
update_label | Write | No |
list_label_users | Read | No |
list_paths | Read | No |
resolve_path | Read | No |
list_journey_stages | Read | No |
update_journey_stage | Write — name and description only | No |
list_autolinks | Read | No |
create_autolink | Write | No |
Page types
Two tools. A page type is the template-plus-schema that decides what fields a page has and where its URLs live, so both of these are things a model should call before it creates or edits a page, not after.
list_page_types
GET /api/ai/assistant/context/page_types · Returns { pageTypes: [{ pageTypeId, slug, name, type }] } · Also available in the Leed Assistant
Takes no parameters. type is one of documentation, api or posts, and is absent for a custom type. The tool’s own description instructs the model to call this before choosing a pageTypeId, and to ask you rather than guess when more than one page type plausibly matches — so “put this in the blog” resolves to the posts-kind entry and “add it to the docs” to the documentation-kind one.
get_page_type_schema
GET /api/pagetypes/:pageTypeId · Returns { pageType } · MCP only
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
pageTypeId | string | Yes | — | From list_page_types |
Returns the full page type, including its field schema and its required-field configuration. This is the call that tells a model that documentation pages must carry a summary before they can be published, so it is worth making before a bulk authoring run rather than discovering the requirement one rejected publish at a time.
Menus
Three tools, and the most consequential write on this page.
list_menus
GET /api/menu · Returns { menus } · Also available in the Leed Assistant
Takes no parameters. Each row carries menuId, name, isDirty and an isDocsMenu flag saying whether that menu is bound to a documentation page type as its left navigation — which matters, because the docs-menu rules below apply only to those.
get_menu
GET /api/menu/:menuId · Returns { menu, isDocsMenu } · Also available in the Leed Assistant
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
menuId | string | Yes | — | From list_menus |
Returns the menu’s draft state — the tree as it currently stands, including unpublished edits — alongside its dirty flag. Always read before you write: the update tool replaces what is there.
update_menu_draft
PUT /api/menu/:menuId · Returns { menu, isDocsMenu } · Also available in the Leed Assistant
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
menuId | string | Yes | — | Which menu to write |
items | array of menu items | No | unchanged | Replaces the whole tree. See below |
name | string | No | unchanged | The menu’s name — see the warning about renaming |
description | string | No | unchanged | Internal description only |
expanded | boolean | No | unchanged | Whether the menu renders expanded by default |
isDirty | boolean | No | set to true by the route | Do not send it; the save marks the menu dirty for you |
A menu save is a whole-tree replace
A menu item is a node, and a node with children is a folder. The shape is the same either way:
| Field | Type | Meaning | Docs-menu constraint |
|---|---|---|---|
menuItemId | string, required | Stable id for the node | You supply it; it is not generated for you |
name | string, required | The visible label in the nav | For a folder, this is what gets slugified into the URL segment |
title | string, required | The hover tooltip | Never appears in a URL |
href | string, required | pageid:<pageId> for a page; "" for a folder | Blanked automatically on folders — see below |
target | string, required | Link target; "" for a normal in-site link | — |
submenu | array, required | Child items; [] for a leaf | A page item may not have children |
icon + iconStyle | string, optional | Font Awesome icon on the item | Folders carry no icon |
expanded | boolean, optional | Folder open by default | — |
disabled | boolean, optional | Renders the item grayed out | — |
description | string, optional | Internal note | — |
Three rules are enforced by the route rather than by convention, and each returns an error rather than silently doing something else:
- A folder can never be a link. On every save to a docs-bound menu,
stripFolderHrefsblanks thehrefof any item that has children. Sending a folder with anhrefis not an error — the href is simply discarded. - A page item may not have children. Sending one is a
400with the message “A docs page item cannot have children — place pages inside folders instead”. This exists precisely because the previous rule would otherwise blank that page’s href and drop it out of the nav. - A documentation page may appear in exactly one docs left-nav menu, once. A duplicate inside the tree, or a page already carried by another docs menu, is a
409.
There is no order field on a menu item. Order is array order, top to bottom, at every level.
A two-level docs menu — one folder holding one page — looks like this. menuId travels as its own argument alongside items, not inside it:
{
"menuId": "docs-left-nav",
"items": [
{
"menuItemId": "folder-getting-started",
"name": "Getting Started",
"title": "Getting Started",
"href": "",
"target": "",
"expanded": true,
"submenu": [
{
"menuItemId": "page-install",
"name": "Install the CLI",
"title": "Install the CLI",
"href": "pageid:9f1c0f5e-6b1a-4b2e-9a77-0c2d5a1f8e10",
"target": "",
"icon": "fa-solid fa-download",
"iconStyle": "solid",
"submenu": []
}
]
}
]
}Publishing a menu is not an MCP action
The registry contains a publish_menu tool, but it is not exposed to MCP clients — an MCP connection is never shown it and cannot call it by name. Menus reach the live site through the deployments flow, which a person runs from the CMS. If you want the same tree edited by hand instead, Building and Editing a Menu is the screen, and Left Navigation Menu covers the docs-specific rules from the editor’s side.
Labels
Four tools. A label does three jobs at once in Leed — a taxonomy, a series, and an RBAC boundary — so read Labels and Series before reshaping them programmatically.
list_labels
GET /api/labels · Returns { labels, isDirty } · MCP only
Takes no parameters. isDirty is the whole label list’s flag, not per-label.
get_label
GET /api/labels/:labelId · Returns { label } · MCP only
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
labelId | string | Yes | — | From list_labels |
update_label
PUT /api/labels/:labelId · Returns { label } · MCP only
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
labelId | string | Yes | — | Which label to edit |
name | string | No | unchanged | Display name |
description | string | No | unchanged | — |
visible | boolean | No | unchanged | Whether the label is shown on the public site |
isSeries | boolean | No | unchanged | Turns the label into a multi-part series |
series | multipart | announcement | No | unchanged |
slug | string | No | unchanged | The label’s URL segment |
Only the fields you send change. There is no create_label and no delete_label on the MCP surface.
list_label_users
GET /api/labels/:labelId/users · Returns { users: [{ userId, role }] } · MCP only
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
labelId | string | Yes | — | Which label to inspect |
URL paths
Two read-only tools that answer “what lives where”.
list_paths
GET /api/paths · Returns { paths } · MCP only
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
q | string | No | — | Free-text filter over the path text |
t | string | No | all types | Comma-separated list of row types, e.g. alias or current,next |
Each row carries id, pageId, path, type, title, createdAt and a cmsManaged flag. The type is the field that matters:
| Type | Meaning | Created by |
|---|---|---|
current | The live URL of a published page | Publishing a page |
next | A placeholder reserving a URL for a page that is not published yet | Creating or moving an unpublished page, or a staged folder move |
alias | A custom path that redirects to a page | Adding an alias in the CMS, or a page whose URL changed |
A page that has moved typically owns several rows: one current at its new URL and one alias per old URL. URL Paths and Slugs is the full model.
resolve_path
GET /api/paths/lookup · Returns { pageId } (or { pageId: null }) · MCP only
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
url | string | Yes | — | A relative path or an absolute URL on your own domain |
The reverse of list_paths, and the tool a model should reach for whenever it has a URL and needs a page. Anything that is not on your domain returns null rather than an error. Use it before writing a cross-link: a body should carry pageid:<id>, never a literal path, so the link survives the page moving.
Journey stages
Journey stages are the funnel bands Engage scores contacts into. Two tools, and the write is deliberately narrow.
list_journey_stages
GET /api/journeys · Returns { journeys } · MCP only
Takes no parameters.
update_journey_stage
PUT /api/journeys/:journeyId · Returns { journeys } — the whole list, re-saved · MCP only
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
journeyId | string | Yes | — | Which stage to edit |
name | string | No | unchanged | Display name of the stage |
description | string | No | unchanged | — |
The patch is merged into the matching stage; an unknown journeyId is a 404. The set of stages is fixed — there is no tool to add one, remove one or reorder them, over MCP or in the assistant. Renaming a stage does not re-score anybody. Journey Stages covers what the stages mean and why the Engage journey is not editable.
Autolinks
An autolink turns a phrase into a link to a page everywhere that phrase appears, at build time.
list_autolinks
GET /api/autolinks · Returns { autolinks } · MCP only
Takes no parameters. Each entry carries autolinkId, text, an optional pageId and an optional pageCount — the number of pages the phrase matched on the last site build, which is a build statistic rather than something you set.
create_autolink
POST /api/autolinks · Returns { autolink, autolinks } · MCP only
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
text | string | Yes | — | The phrase to match |
pageId | string | No | — | The page to link it to |
The id is generated server-side. Autolinks are stored as one list per workspace, so a create reads the current list, appends, and re-saves the whole thing — which is why the response hands you both the new entry and the full list.
Autolinking is a Starter-and-up feature at build time, and phrase ordering and the per-page opt-out both change what actually gets linked — Autolinks covers both. If the tool name you came here for is not on this page, the Operator MCP Tool Index lists every tool alphabetically with the page that documents it.