TentoCMS
Guides

AI Agents (MCP)

Connect Claude Code, Cursor, VS Code or another MCP client to a TentoCMS project, so an agent can read its schemas and write content.

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:

PermissionUnlocks
content:writeCreating, updating and publishing pages, and creating collection items
media:writeUploading 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)

ToolArgumentsReturns
tento_list_page_typesnoneEvery page type's name, slug and field count
tento_get_page_typeslugThe 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_componentsnoneEvery component's name, slug and field count
tento_get_componentslugThe component's field schema
tento_list_collection_typesnoneEvery collection type's name, slug and field count
tento_get_collection_typeslugThe collection type's field schema

Context (read-only)

ToolArgumentsReturns
tento_get_pageslugThe page's current draft: its fields exactly as stored, its SEO and status, and hasUnpublishedChanges
tento_get_example_pagespageTypeSlug, limit (1–5, default 3)The most recently published pages of that type, with their fields and SEO
tento_search_contentquery, 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_statsnonePage counts by status, the project's page and collection types, and the number of media files
tento_generate_typesnoneTypeScript 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

ToolNeedsArguments
tento_create_pagecontent:writepageTypeSlug, name, slug, fields, seo, status (draft by default, or published)
tento_update_pagecontent:writeslug (the page's current slug), and any of name, newSlug, fields, seo
tento_publish_pagecontent:writeslug
tento_create_collection_itemcontent:writecollectionTypeSlug, name, slug, fields, status (draft by default, or published)
tento_upload_mediamedia:writeurl, and optionally filename, altText, folder

A typical session

An agent does its best work when it reads before it writes:

  1. tento_list_page_types to see what kinds of page exist.
  2. tento_get_page_type for the type it is about to write, to learn the fields.
  3. tento_get_example_pages to match the tone and structure of published pages.
  4. tento_search_content to check the page doesn't already exist.
  5. tento_create_page as a draft.
  6. 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 with tento_publish_page.
  • Rich text is sanitised. Markup that isn't allowed is removed before saving, and the result includes a warnings list 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."
  }
}
CodeMeaning
FORBIDDENThe key isn't a secret key, or doesn't hold the permission the tool needs
NOT_FOUNDNo page type, component, collection type or page has that slug
CONFLICTThe slug is taken, the page changed while the call ran, or the same call is already running (see Repeated calls)
INVALID_CONTENTfields doesn't match the schema, or a required field is empty when publishing
PAYLOAD_TOO_LARGEThe content or the uploaded file is over its size limit
INVALID_URL, FORBIDDEN_URL, FETCH_FAILEDThe upload URL isn't HTTPS, points at a private address, or couldn't be downloaded
VALIDATION_ERROR, INVALID_FILE_CONTENTThe 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:

StatusCause
401The key is missing, invalid, revoked or expired
403The key is a public or preview key; the endpoint needs a secret key
406The request's Accept header doesn't list both application/json and text/event-stream
429The 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_page or tento_create_collection_item again 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 gets CONFLICT. 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 CONFLICT instead of undoing it: read the page again with tento_get_page and retry.
  • Publishing checks the page, not the call. If the page's current draft is already live, tento_publish_page publishes nothing and returns alreadyPublished: 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 returns alreadyPublished: true or CONFLICT.
  • Uploads are not deduplicated. Retrying tento_upload_media uploads 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.published and media.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
Copyright © 2026