AI Agents (MCP)
TentoCMS runs a Model Context Protocol server, so an AI agent can work with a project directly. Once connected, an agent can:
- read the project's content model: page types, components and collection types
- look at published examples and search existing content before it writes anything
- create and update pages, publish them, and create collection items
- upload media from a URL
The server is part of the API and is reached with an ordinary API key. It sees exactly one project, the one that owns the key, and cannot read or change any other.
Before you start
You need a secret API key (tento_sk_*). A project admin creates one in the admin panel
under Settings → API Keys (see Authentication for
key types).
Any secret key can use the read-only tools, which see drafts as well as published content. Writing needs a permission. Grant only the ones the agent needs:
| Permission | Unlocks |
|---|---|
content:write | Creating, updating and publishing pages, and creating collection items |
media:write | Uploading media |
A secret key must hold at least one permission. For an agent that should only read, grant
ai:use: no MCP tool checks it, so the key can read everything and write nothing.
The endpoint refuses public (tento_pk_*) and preview (preview_*) keys with 403. The
read tools return unpublished content, and those keys are made to be shared: a public key
sits in a site's browser code, and a preview key is given to preview sites.
content:write includes publishing. There is no permission for "drafts only". If a
person should review everything before it goes live, ask the agent to leave pages as drafts,
but be aware that the key itself allows publishing.
Keep the key out of source control. Each example below shows a way to keep it out of a committed file.
The endpoint
https://your-api-host/api/v1/mcp
It uses the Streamable HTTP transport, statelessly: there is no session to keep alive,
and each request stands alone. Send the key in the X-API-Key header. The API also accepts
an ?api_key= query parameter, but never put a secret key in a URL (see
Authentication).
Connect a client
Claude Code
claude mcp add --transport http tentocms https://your-api-host/api/v1/mcp \
--header "X-API-Key: tento_sk_your_key_here"
This stores the key in your local Claude Code configuration, not in the repository. To share
the server with a team, commit a .mcp.json that reads the key from each person's
environment instead:
{
"mcpServers": {
"tentocms": {
"type": "http",
"url": "https://your-api-host/api/v1/mcp",
"headers": { "X-API-Key": "${TENTO_API_KEY}" }
}
}
}
Run /mcp inside Claude Code to confirm the server is connected and see its tools.
Cursor
Add the server to .cursor/mcp.json in the project, or ~/.cursor/mcp.json for every project:
{
"mcpServers": {
"tentocms": {
"url": "https://your-api-host/api/v1/mcp",
"headers": { "X-API-Key": "${env:TENTO_API_KEY}" }
}
}
}
VS Code (GitHub Copilot)
Add the server to .vscode/mcp.json. The inputs block makes VS Code prompt for the key
once and store it securely, rather than keeping it in the file:
{
"inputs": [
{
"type": "promptString",
"id": "tento-api-key",
"description": "TentoCMS secret API key",
"password": true
}
],
"servers": {
"tentocms": {
"type": "http",
"url": "https://your-api-host/api/v1/mcp",
"headers": { "X-API-Key": "${input:tento-api-key}" }
}
}
}
Claude Desktop
Claude Desktop's built-in connectors cannot send a custom header, so connect through the
mcp-remote bridge, which needs Node.js. Add
this to claude_desktop_config.json and restart the app:
{
"mcpServers": {
"tentocms": {
"command": "npx",
"args": [
"-y",
"mcp-remote",
"https://your-api-host/api/v1/mcp",
"--header",
"X-API-Key:${TENTO_API_KEY}"
],
"env": { "TENTO_API_KEY": "tento_sk_your_key_here" }
}
}
}
Leave out the space after the colon in X-API-Key:${TENTO_API_KEY}. Some platforms split
arguments on spaces, which would break the header.
Clients that only support OAuth
Connectors that can only sign in with OAuth, such as custom connectors on claude.ai, cannot connect yet. The endpoint accepts an API key and nothing else.
Check the connection without a client
Ask the server for its tool list with curl:
curl https://your-api-host/api/v1/mcp \
-H "X-API-Key: tento_sk_your_key_here" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
The Accept header must list both types, or the server answers 406 Not Acceptable. MCP
clients set it for you.
Tools
Every tool works within the key's project. You never pass a project id.
Content model (read-only)
| Tool | Arguments | Returns |
|---|---|---|
tento_list_page_types | none | Every page type's name, slug and field count |
tento_get_page_type | slug | The page type's full field schema. Component fields include the schemas of the components they allow, and reference fields include the referenced collection type's schema |
tento_list_components | none | Every component's name, slug and field count |
tento_get_component | slug | The component's field schema |
tento_list_collection_types | none | Every collection type's name, slug and field count |
tento_get_collection_type | slug | The collection type's field schema |
Context (read-only)
| Tool | Arguments | Returns |
|---|---|---|
tento_get_page | slug | The page's current draft: its fields exactly as stored, its SEO and status, and hasUnpublishedChanges |
tento_get_example_pages | pageTypeSlug, limit (1–5, default 3) | The most recently published pages of that type, with their fields and SEO |
tento_search_content | query, type (pages, collections or all), limit (1–20, default 10) | Pages and collection items whose name or slug contains the query, drafts included. Names, slugs and status only, not content |
tento_get_content_stats | none | Page counts by status, the project's page and collection types, and the number of media files |
tento_generate_types | none | TypeScript interfaces for every page type, component and collection type |
tento_generate_types uses the same generator as the @tentocms/typegen CLI, so its output
matches what the CLI writes for the same project (Nuxt,
Next.js, Astro).
Writing
| Tool | Needs | Arguments |
|---|---|---|
tento_create_page | content:write | pageTypeSlug, name, slug, fields, seo, status (draft by default, or published) |
tento_update_page | content:write | slug (the page's current slug), and any of name, newSlug, fields, seo |
tento_publish_page | content:write | slug |
tento_create_collection_item | content:write | collectionTypeSlug, name, slug, fields, status (draft by default, or published) |
tento_upload_media | media:write | url, and optionally filename, altText, folder |
A typical session
An agent does its best work when it reads before it writes:
tento_list_page_typesto see what kinds of page exist.tento_get_page_typefor the type it is about to write, to learn the fields.tento_get_example_pagesto match the tone and structure of published pages.tento_search_contentto check the page doesn't already exist.tento_create_pageas a draft.- A person reviews the draft in the admin and publishes it, or the agent calls
tento_publish_page.
To change an existing page, the agent calls tento_get_page first and then
tento_update_page with the fields it is changing.
You don't have to spell this out. The tool descriptions steer agents towards it, so a prompt like this is enough:
Using TentoCMS, draft a new case study page about our work with Acme. Match the style of our existing case studies and leave it as a draft.
Writing content
Fields
fields is an object keyed by field name, shaped the same way as fields in the
REST write API and checked against the page type's schema.
Image and file fields take a media id: use the id that tento_upload_media returns (see
Field Types).
- Drafts can be incomplete. Required fields are only enforced when content is published:
when the page is created with
status: "published", updated while already published, or published withtento_publish_page. - Rich text is sanitised. Markup that isn't allowed is removed before saving, and the
result includes a
warningslist saying what was removed. - Content may be nested up to 15 levels deep. It is also subject to the content size limit in Rate Limits & Errors.
Slugs must be at least three characters of lowercase letters, numbers and single hyphens, and cannot start or end with a hyphen.
Updating a page
Send only the fields you are changing. Each field in fields replaces that field's value,
and every field you leave out keeps its current value. To clear a field, set it to null. A
field's value is replaced whole: to change one item in a repeater, send the full repeater.
name, newSlug and seo are independent of fields. Leave any of them out and it stays
as it is.
Read the current draft with tento_get_page. Its fields are in the same shape
tento_update_page takes. Don't rebuild them from a public API read instead: even in
preview mode that substitutes variables, resolves media and
references, and flattens components, so sending its fields back would replace variable
placeholders such as #{KEY} with their current values, and media ids with resolved objects.
Updating a published page changes its draft content, not the live content. Visitors keep
seeing the published fields until the page is published again, and tento_get_page reports
hasUnpublishedChanges: true until it is.
name, newSlug and seo are not staged. They are stored once, for the draft and the live
page alike, so an update changes them on the live page without a publish, and
hasUnpublishedChanges does not count them. The page's cached copy is cleared when the
update is saved, so visitors see the change straight away. A new slug moves the live page to
the new URL.
Unlike tento_update_page, the REST API's PUT /api/v1/pages/:slug replaces the whole draft
with the fields it is sent.
SEO
seo takes the same object as the REST API: metaTitle, metaDescription, metaRobots,
ogTitle, ogDescription, ogImageId and canonicalUrl, with the same length limits. On an
update, only the properties you send change. Set one to null to clear it. A new page is
index unless you send metaRobots: "noindex".
Uploading media
tento_upload_media fetches a file from a URL and adds it to the media library. The URL
must:
- use HTTPS
- point at a public address. Private and internal network addresses are refused
- serve the file directly. Redirects are not followed
File types and sizes are limited exactly as for uploads through the REST API; see
Rate Limits & Errors. So is the number of
uploads: each tento_upload_media call counts against the same per-key media upload limit as
POST /api/v1/media.
Results and errors
A tool returns its result as JSON text. When a call fails, the tool result is marked as an
error and its JSON carries a code, a message, and usually a suggestion aimed at helping
the agent correct itself:
{
"error": {
"code": "FORBIDDEN",
"message": "This API key does not have the 'content:write' permission.",
"suggestion": "Create a secret API key (tento_sk_*) with the 'content:write' permission in the TentoCMS admin panel."
}
}
| Code | Meaning |
|---|---|
FORBIDDEN | The key isn't a secret key, or doesn't hold the permission the tool needs |
NOT_FOUND | No page type, component, collection type or page has that slug |
CONFLICT | The slug is taken, the page changed while the call ran, or the same call is already running (see Repeated calls) |
INVALID_CONTENT | fields doesn't match the schema, or a required field is empty when publishing |
PAYLOAD_TOO_LARGE | The content or the uploaded file is over its size limit |
INVALID_URL, FORBIDDEN_URL, FETCH_FAILED | The upload URL isn't HTTPS, points at a private address, or couldn't be downloaded |
VALIDATION_ERROR, INVALID_FILE_CONTENT | The uploaded file's name, folder or type was refused |
INVALID_CONTENT also carries details.errors, one entry per problem, each with the path
of the field at fault and a message:
{
"error": {
"code": "INVALID_CONTENT",
"message": "Content validation failed",
"suggestion": "Content validation failed. Use tento_get_page_type or tento_get_collection_type to see the required field schema.",
"details": {
"errors": [{ "path": "pageTitle", "message": "Title is required", "type": "required" }]
}
}
}
Arguments that break a tool's own rules, such as a malformed slug or a meta title over its length limit, are rejected before the tool runs, with a message naming the argument.
Some failures happen before any tool runs, and come back as plain HTTP errors:
| Status | Cause |
|---|---|
401 | The key is missing, invalid, revoked or expired |
403 | The key is a public or preview key; the endpoint needs a secret key |
406 | The request's Accept header doesn't list both application/json and text/event-stream |
429 | The key's rate limit is exhausted (see Limits) |
Repeated calls
Each tool handles a repeated call in the way that makes a retry after a dropped connection safe:
- Creates are deduplicated. Call
tento_create_pageortento_create_collection_itemagain with exactly the same arguments, from any key in the same project, and you get the first result back instead of a second page. A call that arrives while the first is still running getsCONFLICT. Only a successful create is remembered, for 24 hours: if a create fails, retrying it runs it again, so it can succeed once the cause is fixed. - Updates always apply. Sending the same update twice leaves the page as one update
would, and it is applied even if someone has edited the page since. An update that
overlaps someone else's edit to the same page returns
CONFLICTinstead of undoing it: read the page again withtento_get_pageand retry. - Publishing checks the page, not the call. If the page's current draft is already live,
tento_publish_pagepublishes nothing and returnsalreadyPublished: true. Otherwise it publishes, so publishing again after an edit, or after the page was unpublished, works. Two publishes of the same page that overlap publish it once: the other returnsalreadyPublished: trueorCONFLICT. - Uploads are not deduplicated. Retrying
tento_upload_mediauploads a second copy.
Limits
Every request to the MCP endpoint counts against the API key's write rate limit. That
includes read-only tools, and the requests a client makes when it connects and lists the
tools. The allowance is shared with the same key's REST write requests. Uploads also count
against the media upload limit. See Rate Limits & Errors
for both figures. An agent working quickly through a long task can reach them. When it does,
calls fail with 429 until the window resets.
Use a separate key for each agent or integration, so one cannot use up another's allowance.
What gets recorded
- Audit log. Changes made over MCP are recorded in the project's audit log against the API key that made them.
- Webhooks. Writes fire the same webhooks as the REST API:
page.created,page.updated,page.published,collection_item.created,collection_item.publishedandmedia.uploaded. See Webhooks. - API usage. Requests appear in the key's usage alongside its REST requests.
To cut an agent off, revoke its key under Settings → API Keys. Revocation can take up to a minute to reach every region.
Not available over MCP
The following need the REST API or the admin:
- reading one collection item's full content
- updating, publishing or deleting collection items
- unpublishing or deleting pages
- blog posts, categories and tags
- listing or editing existing media

