MCP and agent access
How to give an agent safe access to a brand, and what it can and cannot do.
5 min read
What an agent token is
An agent token is a credential for an outside program, such as an AI assistant, that works on one brand. It starts with atl_agent_, ATLAS shows it once when it is created and keeps only a hash.
Behind each token sits a hidden agent user with a fixed editor role. That user never appears in the member list. Because the role is editor, the same rules that apply to a person apply to the agent: content types with a higher min_role are invisible to it, and a record from another brand is a 404.
An agent token carries scopes. You pick at least one when you create it.
| Scope | What it allows on the agent door today |
|---|---|
read | Read the brand, content types, block schemas, entries (drafts included), revisions, terms, menus and a small dashboard. |
draft:write | Create a draft entry, save a draft, and set the terms of a draft entry. |
media:write, translate, review:request, settings:read | Can be minted, but no route on the agent door uses them yet. |
The publish and schedule scopes cannot be minted. Asking for them answers 422.
Agents never publish
Everything an agent writes is a draft revision. A person opens it in the dashboard, reviews it and publishes it. There is no publish, schedule, rollback or delete route on the agent door and no such tool on the MCP endpoint.
Create an agent token
An admin turns agent access on for the brand first. The brand setting agent.enabled is off by default. While it is off, creating an agent token answers 403 agent_disabled.
/v1/manage/api-tokensThis is a dashboard call, so it needs a signed in admin session and the X-Brand-Id header.
{
"name": "Content assistant",
"kind": "agent",
"scopes": ["read", "draft:write"],
"expires_in_minutes": 43200
}expires_in_minutes is optional. It must be between 15 minutes and 129600 minutes (90 days), and it defaults to 43200 (30 days). The answer is 201 with the token row and the plain token value. Store it in a secret manager right away.
Revoking the token (DELETE /v1/manage/api-tokens/{token}) disables the hidden agent user at once. Expired agent tokens are disabled by an hourly sweep as well.
The agent door
The agent door lives under https://api.atlascms.id/v1/agent. The token names its brand, so you send no X-Brand-Id and no cookie.
curl -s "https://api.atlascms.id/v1/agent/entries?per_page=10" \
-H "Authorization: Bearer $ATLAS_AGENT_TOKEN"| Method | Path | Scope |
|---|---|---|
| GET | /v1/agent/brand | read |
| GET | /v1/agent/content-types, /v1/agent/content-types/{key} | read |
| GET | /v1/agent/block-schemas | read |
| GET | /v1/agent/entries, /v1/agent/entries/{entry}, /v1/agent/entries/{entry}/revisions | read |
| GET | /v1/agent/terms, /v1/agent/menus, /v1/agent/dashboard | read |
| POST | /v1/agent/entries | draft:write |
| PUT | /v1/agent/entries/{entry}/draft | draft:write |
| PUT | /v1/agent/entries/{entry}/terms | draft:write |
Lists take per_page (20 by default, 50 at most). Media upload, menu writes, term creation, translations and reviews are not on this door yet.
Writes need an idempotency key
Every write must carry an Idempotency-Key header of 16 to 64 letters, digits, dots, colons, dashes or underscores. Without it the call is 422 idempotency_key_required.
curl -s -X POST "https://api.atlascms.id/v1/agent/entries" \
-H "Authorization: Bearer $ATLAS_AGENT_TOKEN" \
-H "Idempotency-Key: 01JD6N9V0Q8KZC3F5H7M2T4W6Y" \
-H "Content-Type: application/json" \
-d '{"type":"page","locale":"en","path":"/about","title":"About","blocks":[]}'- Retrying with the same key and the same body replays the first answer (header
Idempotent-Replay: true) and writes nothing. - The same key with a different body is 422
idempotency_key_reuse. A call while the first is still running is 409idempotency_in_progress. - Keys are per token and are kept for 24 hours.
- Saving a draft needs
base_revision_id, the head revision the agent read. If a person saved in the meantime the answer is 409revision_changedand nothing is written. - Saving a draft of a published entry never changes the live page.
Limits and quotas
| Limit | Value |
|---|---|
| Reads | 120 calls a minute per token and 600 a minute per brand |
| Writes | 30 a minute per token and 150 a minute per brand |
| Daily writes | 500 per token, then 429 agent_quota_exceeded |
| Body size | 256 KiB, then 413 payload_too_large |
Other limits answer 429 with a Retry-After header.
Remote MCP endpoint
MCP is a standard way for an AI client to call tools. POST /mcp exposes the agent door as a remote MCP server, for clients that cannot run a local package.
- It is off by default. The installation turns it on with
ATLAS_MCP_REMOTE=true. While it is off the path answers 404. - It speaks Streamable HTTP in stateless mode: one JSON-RPC request per
POST, one JSON answer.GET /mcpis 405. - Auth is
Authorization: Bearer atl_agent_...only. A token in the query string is refused. - A request with an
Originheader must come from an origin listed inATLAS_MCP_ALLOWED_ORIGINS(empty by default), else 403. Plain HTTP is refused in production. - The brand switch
agent.enabledstill applies.
tools/list returns 13 tools. Ten read tools need the read scope: get_brand, list_content_types, get_content_type, list_block_schemas, list_entries, get_entry, list_revisions, list_terms, list_menus and get_dashboard. Three write tools need draft:write: create_entry, save_draft and sync_entry_terms.
A tool call runs the matching agent route in process, so scopes, limits, quotas and audit rows are the same as on the door. A write tool takes its key in params._meta.idempotencyKey.
{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {
"name": "create_entry",
"arguments": {"type": "page", "locale": "en", "path": "/about", "title": "About", "blocks": []},
"_meta": {"idempotencyKey": "01JD6N9V0Q8KZC3F5H7M2T4W6Y"}
}}A client that takes headers can be configured like this:
{
"mcpServers": {
"atlas": {
"type": "http",
"url": "https://api.atlascms.id/mcp",
"headers": { "Authorization": "Bearer ${ATLAS_AGENT_TOKEN}" }
}
}
}Let the client read the token from the environment or a secret store. Never commit it.
Safety and control
- Content is data, not instructions. Free text in answers sits under
content, and the answers carrymeta.trust: "untrusted_content". An MCP result repeats that warning. Treat page text as untrusted input in your agent. - Token-shaped strings are redacted. Any value that looks like an ATLAS token is replaced by
[redacted]in answers. - Kill switches. Turn off
agent.enabledto stop new agent tokens. To stop a token you already issued, revoke it. The installation also has platform switches that refuse all agent reads or all agent writes at once. - Audit rows. Every agent call writes an
agent.callaudit row with the tool, scope, outcome and status, never a body or title. Rows are kept for 90 days. Admins can read them withGET /v1/manage/audit-logs?action=agent.call. - Least privilege. Mint the shortest-lived token with the fewest scopes the job needs, one token per agent, and rotate it by minting a new one before revoking the old one.
The sync token is not an agent token
A separate sync token kind exists only for the project's own CI to publish its documentation and changelog. You cannot use it as an agent token, and you do not need it.
For roles, token kinds and who may create them, see roles and permissions. For the full list of token kinds, see tokens and security.
Was this page helpful?