A well-behaved MCP client does not guess. Before it writes a page it asks for the page schema; before it charts your traffic it asks which metrics exist; and when a call is refused it asks what the connection is actually allowed to do. This page covers those tools — the two introspection calls that answer “what shape is this”, the two context calls that answer “who am I and where am I”, and get_analytics, which answers “how did that page do” — plus report_blocked_request, which a client calls when Cloudflare’s firewall stops a request before it reaches Leed.
None of these tools changes anything in your workspace, and none is gated by your plan — though the data one of them returns is bounded by your analytics retention, which is. The only one that writes at all is report_blocked_request, and what it writes is a report to Leed, not a change to your content.
Analytics
Two tools, and they are meant to be used in that order: discover the metric names, then query one.
get_analytics
GET /api/analytics · Operator MCP only · read-only · requires analytics:read
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
metric | string | yes | — | A metric name from list_analytics_metrics. Validated by the route, not by the tool schema — see the warning below. |
start | string (ISO-8601 date-time) | yes | — | Start of the window. |
end | string (ISO-8601 date-time) | yes | — | End of the window. Must be strictly after start. |
type | "count" | "line" | no | both | Number metrics only. count returns the single value, line the time series. Omit for both. Ignored by list metrics. |
pageId | string | no | site-wide | The page’s internal id, never its title. Resolve a title through list_pages first. |
Returns the metric result — a value plus trend for a number metric, a ranked table for a list metric.
list_analytics_metrics
In-process · Operator MCP only · read-only · no arguments
Returns { numberMetrics: [...], listMetrics: [...] } — the two arrays of valid metric values. This is the only correct way to discover them. It has no backing HTTP route: the answer is a static catalog, so it is served in the MCP layer itself and never touches your data.
{
"numberMetrics": [
"averagePageViewsPerSessionMetric",
"distinctPagesViewedPerHourMetric",
"pageViewsMetric",
"totalSessionsMetric",
"uniqueSessionsMetric",
"uniqueVisitorsMetric"
],
"listMetrics": ["browsersUsedMetric", "referrersMetric", "sessionsByCountryMetric", "…"]
}The two shapes of metric
| Shape | Count | What it returns | Does type apply? |
|---|---|---|---|
| Number metrics | 6 | One counted value for the window, plus a time series | Yes — count, line, or both |
| List metrics | 22 | A ranked table of rows | No |
The definitions belong with the analytics screens that display them, not here: the six counted metrics are defined in the metrics reference, and the twenty-two ranked tables, with their columns, in the list metrics reference. A metric means the same thing over MCP as it does on the Know dashboard — there is one implementation behind both.
A handful of list metrics are gated by plan rather than by MCP: the video and audio engagement metrics, the internal-click metrics and hidden-UTM inbound attribution are Growth features. Querying one below Growth returns a 402 with an upgrade_required body, which reaches your client as a tool error naming the feature. Which metric needs which plan is in feature availability by plan; capture is never gated, only reporting, so the data is waiting when you upgrade.
Where your data stops
Analytics retention is a quota, not a feature switch. Free keeps 30 days, Starter 365, Growth 730, and Enterprise is unlimited. Nothing is ever deleted — retention is applied at query time as a cutoff.
That produces two different outcomes, and it is worth knowing which one you hit:
- A window that straddles the cutoff is silently clamped. Ask a Free workspace for 90 days and you get the most recent 30, with no error and no warning. The response echoes the window that was actually queried, so read it rather than assuming your requested bounds.
- A window entirely older than the cutoff is refused. You get a
402with{"error":"upgrade_required","reason":"quota_exceeded","feature":"analyticsRetention"}, carrying your current tier, the retention quota in days, and how far back the request reached.
Date ranges and retention covers the same clamp from the dashboard’s side, where the date picker enforces it before you can ask.
MCP returns data, the assistant draws a chart
There are two analytics tools in Leed and they are deliberately not the same one. get_analytics is Operator-MCP-only and returns rows for a model to reason over. get_analytics_chart is assistant-only and renders a chart inline in the chat — a count, a line, a ranked list, or a US and world map for the two geographic metrics. Neither surface can see the other’s tool. If you connected a client and cannot find a tool that draws pictures, that is why; ask the Leed Assistant instead.
Calls made over MCP are also counted separately from human traffic in your own analytics — analytics for AI and MCP clients explains the three audience buckets.
Introspection
Two tools with no backing HTTP route. They are answered in the MCP layer itself, are read-only, and are the same for every workspace — they describe shapes and catalogs, not your content. They are still recorded in the audit trail like any other call.
describe_schema
In-process · Operator MCP only · read-only
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
target | "page" | "page_type" | "form" | "menu" | "leed_markdown" | yes | — | Which schema to return. |
The first four targets return JSON Schema, so a model can construct a valid input for the matching create or update tool without trial and error. The fifth returns prose.
| Target | Returns | Call it before |
|---|---|---|
page | JSON Schema for a page input | reserve_page, create_page_from_markdown, update_page_metadata |
page_type | JSON Schema for a page type input | create_page_type, update_page_type, or reasoning about a page type’s fields |
form | JSON Schema for a form input | create_form, update_form_draft |
menu | JSON Schema for a menu input | create_menu |
leed_markdown | The full Leed Markdown syntax guide, as text | Writing any page body |
The leed_markdown target is worth calling out. It returns the same syntax guide the page-writing tools embed in their own descriptions and the same one authors read at the Leed Markdown cheat sheet — alerts, collapsibles, tabs, embeds, attribute syntax, the lot. A client that fetches it once writes markdown that survives the round trip through the editor. Authoring pages over MCP shows what that looks like in practice.
{
"target": "leed_markdown",
"guide": "# Leed Markdown Format\n\nAlerts:\n:::note\n…"
}list_analytics_metrics
The second introspection tool, covered under Analytics above because that is where you will use it. It is listed here so the pair is not surprising: both are in-process, both are argument-light, and both are cheap enough to call at the start of every session.
Workspace context
Two tools that answer “what is this workspace, and what am I allowed to do in it”.
describe_capabilities
GET /api/ai/context/capabilities · Operator MCP and the assistant · read-only · requires rbac:read
No parameters. Returns your effective role, the full privilege list with the minimum role each one requires and whether you are granted it, and your resource-level overrides grouped by resource type.
{
"role": "Write",
"privileges": [
{ "privilege": "page:write", "minimumRole": "Write", "granted": true },
{ "privilege": "page:publish", "minimumRole": "Publish", "granted": false }
],
"overrides": {
"pagetype": [{ "resourceId": "u05nxr", "role": "Publish" }]
}
}The overrides matter as much as the role. A Content Writer with a Content Publisher override on one page type can publish there and nowhere else, and this tool is the only way a client can see that before it tries. The privilege matrix is the same grid in human form, and resource-level access overrides explains how one gets granted.
rbac:read itself requires the Read Only role or higher, so a Restricted user cannot call this tool — or get_analytics, which needs analytics:read on the same floor.
get_company_overview
GET /api/ai/context/company · Operator MCP only · read-only
No parameters. Returns a deliberately narrow summary of the workspace:
| Field | Meaning |
|---|---|
siteTitle | The site’s display title |
public.domain / public.customDomainActive | Your live domain and whether the custom hostname is active |
preview.domain / preview.customDomainActive | The same for the preview site |
tier | The entitlement tier: free, starter, growth or enterprise |
languages | The languages configured for API documentation code samples |
It is a safe subset by design and does not expose provisioning internals — no DNS or DKIM records, no Cloudflare project ids, no Turnstile keys, no repository credentials. A client that wants to build a link to a published page combines this with resolve_path and list_paths, which live with the rest of the URL tools in MCP tools: site structure.
Reporting a blocked request
Cloudflare’s firewall sits in front of Leed and now and then mistakes legitimate content for an attack. When it does, the request never reaches Leed: the tool call comes back as a 403 with an HTML page (“Just a moment…” or “Attention Required”) or a cf-ray value, and no JSON error. That is not a permissions problem, and signing in again will not fix it.
report_blocked_request
POST /api/feedback/blocked-request · Operator MCP only · any signed-in role, Restricted included
| Parameter | Type | Required | Default | Notes |
|---|---|---|---|---|
rayId | string | yes | — | The Cloudflare ray id from the cf-ray value, such as a405c2a089ca7ce0-ORD |
command | string | yes | — | The tool name and its argument names written as name= with no values, such as update_page_body pageId= ops= mode=. Every other word after the tool name is treated as a value and dropped |
occurredAt | string (ISO-8601) | no | the time of the report | When the request was blocked |
method | string | no | — | The HTTP method of the blocked request |
path | string | no | — | Its URL path, without the query string |
httpStatus | integer | no | — | The status Cloudflare answered with |
bodyBytes | integer | no | — | The size of the blocked body |
bodySha256 | string | no | — | The lowercase hex SHA-256 of the blocked body |
excerptBase64 | string | no | — | At most the first 2 KB of the blocked body, base64-encoded |
note | string | no | — | What was being done, in at most 2,000 characters |
Returns { feedback: { id, createdAt } }. Leed uses the ray id to find the firewall rule that fired and fix the false positive. Reports share the feedback limit of 30 per hour.
The tool is not offered to the Leed Assistant: the assistant calls Leed from inside the CMS and never passes through the firewall, so it has nothing to report. A client should retry a blocked write at most once, never reword or encode your content to get past the filter, and tell you what was blocked along with the ray id. The Leed CLI reports the same failure with leed feedback blocked.