↑ ↓ to navigate
↵ to select
esc to close
API Reference Advanced

MCP Integration

Connect Claude, Cursor and other AI assistants to Airogel CMS through the hosted MCP server: OAuth or token setup, scopes, the complete tool catalogue, key behaviours and error codes.

What is MCP?

Model Context Protocol (MCP) is an open protocol that lets AI assistants call tools exposed by other services. Airogel CMS runs a hosted MCP server, so Claude, Cursor and other MCP clients can read and change your site's content, structure, templates and settings for you.

You can ask an assistant to "add a blog post about our spring fundraiser", "create a Team collection with a bio field" or "fix the broken link in the footer navigation", and it will use the tools described on this page.

Endpoint and transport

https://api.airogelcms.com/v1/mcp
  • Transport: MCP Streamable HTTP. JSON-RPC requests are sent with POST.
  • Stateless: the server keeps no session between requests. It does not support server-initiated streams or resource subscriptions.
  • Host: the server only accepts requests addressed to api.airogelcms.com.
  • Server name: airogel-cms. The server also sends usage instructions that your assistant receives when it connects.

Connecting a client

Claude (claude.ai and Claude Desktop)

Add Airogel CMS as a custom connector and enter the URL https://api.airogelcms.com/v1/mcp. You don't need to paste a token. Claude opens an Airogel CMS sign-in window, you approve access, and the connector is ready.

Behind the scenes this uses OAuth 2.0 with PKCE and dynamic client registration:

  • An unauthenticated request returns 401 with a WWW-Authenticate: Bearer header. The header's resource_metadata points at https://api.airogelcms.com/.well-known/oauth-protected-resource.
  • That metadata names the authorization server, https://app.airogelcms.com. Its discovery document is at https://app.airogelcms.com/.well-known/oauth-authorization-server.
  • The client registers itself at https://app.airogelcms.com/oauth/register, sends you through the authorization-code flow (PKCE S256 required), and receives an access token (valid for one hour) and a refresh token.

Any MCP client that supports OAuth discovery can connect the same way.

Claude Code, Cursor and other header-based clients

If a client takes a static header instead of running OAuth, first create a token in your Airogel CMS account settings:

  • OAuth Logins: creates a personal access token with the mcp scope. It doesn't expire. This is the recommended option.
  • API Tokens: a standard API token. It also works with MCP and has full access.

Then send it as a bearer token. For Claude Code:

claude mcp add --transport http airogel-cms https://api.airogelcms.com/v1/mcp \
  --header "Authorization: Bearer <token>"

For Cursor, add this to mcp.json:

{
  "mcpServers": {
    "airogel-cms": {
      "url": "https://api.airogelcms.com/v1/mcp",
      "headers": {
        "Authorization": "Bearer <token>"
      }
    }
  }
}

Authorization: Bearer is the only supported way to send a token. Tokens in query strings or request bodies are not accepted.

Scopes

OAuth tokens carry scopes that decide which tools the client can see:

  • mcp grants every tool. Personal tokens from OAuth Logins use this scope, and so do clients that request it during sign-in.
  • Granular scopes come in <resource>:read and <resource>:write pairs for: entries, collections, blueprints, templates, assets, globals, navigations, forms, accounts, domains, events, contexts, subscriptions, support and redirect_rules.
  • Search, preview and theme tools have no scope of their own. They require the read scopes for the data they use. For example, preview_render_path needs entries:read, globals:read and templates:read.

Scopes are checked when the tool list is built. If a token lacks a tool's scopes, that tool doesn't appear in tools/list, and calling it returns "Tool not found" rather than a permission error. API tokens from the API Tokens page are not scope-limited.

Subscription requirement

Account-scoped tools need an active subscription on the account they act on. Without one they return the error code payment_required. These tools work without a subscription, so an assistant can create a new account and pay for it from start to finish:

  • accounts_list, accounts_create
  • plans_list, subscriptions_checkout, subscriptions_status
  • support_contact

Any active plan includes MCP. No tools are reserved for higher plan tiers, and the endpoint has no published rate limits.

Tool catalogue

Every tool except accounts_list, accounts_create, plans_list and support_contact takes an account_id. Call accounts_list first to find it. List tools accept page and per_page (at most 100).

Accounts and billing

ToolScopeWhat it does
accounts_listaccounts:readList the accounts you can access
accounts_createaccounts:writeCreate a new account for the signed-in user and return its account_id
plans_listsubscriptions:readList subscription plans with pricing; optional interval filter
subscriptions_checkoutsubscriptions:writeReturn a Stripe Checkout URL to subscribe an account to a plan
subscriptions_statussubscriptions:readReport whether an account has an active subscription, with plan details

Entries and search

ToolScopeWhat it does
entries_listentries:readList entries in a collection. summary: true (the default) leaves out large rich-text fields
entries_getentries:readGet one entry by handle or ID with all field values and an etag
entries_saveentries:writeCreate or update an entry by collection and handle. Fields you omit are left unchanged. Supports published, published_at, if_match and dry_run
entries_patch_fieldentries:writeEdit one text, rich-text or raw-HTML field in place: str_replace (exact match, once unless replace_all) or append
entries_deleteentries:writeDelete an entry by handle or ID
entries_queryentries:readFilter, sort and paginate entries with operators eq, ne, contains, starts_with, ends_with, gt, gte, lt, lte, in, nin, exists, is_empty
entries_query_by_relationentries:readFind entries that reference another entry through an entity field, including reverse lookups
search_queryentries:readFull-text search across titles, handles and text fields, ranked by relevance (see Full-Text Search)

Collections and blueprints

ToolScopeWhat it does
collections_list / collections_getcollections:readList collections, or get one with its blueprints
collections_savecollections:writeCreate or update a collection by handle, including its routing pattern (see Routing)
collections_deletecollections:writeDelete a collection and the blueprints it owns. Refused while the collection has entries or a form uses it
blueprints_list / blueprints_getblueprints:readList blueprints, or get one with its field definitions
blueprints_saveblueprints:writeCreate or update a blueprint by handle. fields is the complete field list
blueprints_deleteblueprints:writeDelete a blueprint. Refused while entries, forms or navigation items depend on it

Templates, theme and preview

ToolScopeWhat it does
templates_list / templates_gettemplates:readList Liquid templates, or get one with its content
templates_savetemplates:writeCreate or update a template by handle. Liquid syntax is checked before saving
templates_deletetemplates:writeDelete a template
theme_generate_liquid_docsblueprints, collections, globals, navigations (read)Return a Markdown reference of every Liquid variable available on this account
theme_validatecollections, entries, templates (read)Check for missing templates, orphaned entries, date routing without published_at, missing index configuration, empty collections and duplicate content paths
preview_render_pathentries, globals, templates (read)Render a URL path and return the HTML and how the path was resolved. Optional entry_overrides, template_overrides and global_overrides preview changes before you save them

Assets

ToolScopeWhat it does
assets_list / assets_getassets:readList assets (filter by path, filename or content type), or get one by ID or full path
assets_uploadassets:writeUpload base64-encoded content. For small files only; keyed on path and filename, with a replace flag
assets_upload_from_urlassets:writeImport a file from a URL. The download runs in the background, and the asset comes back with status pending
assets_initiate_direct_uploadassets:writeStart a large-file upload: returns a PUT URL, headers and a signed_blob_id
assets_complete_direct_uploadassets:writeTurn the uploaded blob into an asset
assets_upload_chunkassets:writeSend one base64 chunk of a file, for clients that can't PUT to a URL
assets_complete_chunked_uploadassets:writeReassemble uploaded chunks into an asset
assets_create_upload_sessionassets:writeCreate a browser upload link for a person. expires_in is 1 to 1440 minutes (default 60); max_files and allowed_content_types are optional
assets_get_upload_sessionassets:readCheck a session's status and get the assets uploaded through it
assets_deleteassets:writeDelete an asset by ID or path

Globals, navigations and forms

ToolScopeWhat it does
globals_list / globals_getglobals:readList site globals, or get one with its values
globals_saveglobals:writeCreate or update a global by handle. On update, fields replaces the whole field list
globals_deleteglobals:writeDelete a global, along with its blueprint and values
navigations_list / navigations_getnavigations:readList navigations, or get one with its items
navigations_savenavigations:writeCreate or update a navigation by handle
navigations_deletenavigations:writeDelete a navigation and all its items
navigation_items_listnavigations:readList the items in a navigation
navigation_items_savenavigations:writeCreate or update an item. Matched on navigation, title and URL or linked entry
navigation_items_deletenavigations:writeDelete an item by ID
forms_list / forms_getforms:readList forms, or get one with template usage examples
forms_saveforms:writeCreate or update a form by handle
forms_deleteforms:writeDelete a form. Entries it already submitted are kept

Domains and redirects

ToolScopeWhat it does
domains_list / domains_getdomains:readList custom domains, or get one with its verification status and DNS instructions
domains_savedomains:writeAdd or update a domain by name and return the DNS records to create (see Custom Domains)
domains_verifydomains:writeVerify ownership through the TXT record (verify_ownership: true), or check email configuration
domains_deletedomains:writeDeactivate a domain (the default), or delete it permanently with permanent: true
redirect_rules_list / redirect_rules_getredirect_rules:readList the redirect rules you've created (automatic redirects from handle changes are not included), or get one, including any rule that overrides it (shadowed_by)
redirect_rules_saveredirect_rules:writeCreate or update a rule, matched on domain, from_path and match_type (exact or prefix). A rule applies only when the request would otherwise return a 404
redirect_rules_deleteredirect_rules:writeDelete a rule by ID

Events

ToolScopeWhat it does
events_list / events_getevents:readList events (filter by published or upcoming), or get one with its ticket types
events_saveevents:writeCreate an event (omit event_id) or partially update one. Set published: true to show it on the site
events_deleteevents:writeDelete an event and its ticket types
event_tickets_saveevents:writeCreate or update a ticket type. Set the price with dollar_cost, or with cost in cents
event_tickets_deleteevents:writeDelete a ticket type

See Events & Registrations for every parameter. Constituent, CRM and registration data is intentionally not available over MCP (see Constituents & CRM).

Account contexts

Contexts are short notes stored on an account, such as site_purpose, content_guidelines, brand_voice or technical_stack. Assistants read them to learn about a site and can write new ones for future sessions.

ToolScopeWhat it does
contexts_list / contexts_getcontexts:readList contexts, or get one by handle or ID
contexts_savecontexts:writeCreate or update a context by snake_case handle
contexts_deletecontexts:writeDelete a context

Support

ToolScopeWhat it does
support_contactsupport:writeSend a message to the Airogel CMS support team (support@airogelcms.com)

Key behaviours

Idempotent saves

Every *_save tool creates the record if it doesn't exist and updates it if it does, so running the same call twice is safe. Most are keyed on handle. The exceptions:

  • events_save and event_tickets_save use event_id and ticket_id. Omit the ID to create a new record.
  • navigation_items_save matches on navigation, title and URL or linked entry.
  • redirect_rules_save matches on domain, from_path and match_type.
  • domains_save matches on the domain name, and assets_upload on path and filename.

Handles are snake_case

Handles for collections, blueprints, globals, contexts and other structural records must be snake_case, such as blog_posts, not blog-posts. An invalid handle returns a validation_error with a suggested_value.

Dry runs

Pass dry_run: true to validate a change and see its result without saving. This works on every *_save tool except navigation_items_save, and on entries_patch_field.

Optimistic concurrency with ETags

Get responses include an etag. Pass it back as if_match on the next save. If someone else changed the record in the meantime, the save fails with conflict instead of overwriting their work. if_match works on entries_save, entries_patch_field, collections_save, blueprints_save, templates_save, globals_save, navigations_save, forms_save, contexts_save and redirect_rules_save.

Date routing needs published_at

If a collection's routing contains :year, :month or :day, entries_save requires a published_at date so it can build the URL. Entry pages are linked by content_path. Entry objects have no url property.

fields is a full replacement

On globals_save and blueprints_save, the fields array replaces the whole field list. Any field you leave out is removed. Fetch the current definition first and send every field you want to keep, or omit fields entirely to leave them unchanged. entries_save is different: it updates only the field values you send.

Rich-text wrapper normalisation

entries_get returns rich-text fields wrapped in a single outer <div class="lexxy-content">. When you save, that outer wrapper is removed, so reading an entry, editing it and saving it back won't nest wrappers. entries_patch_field matches old_str against the unwrapped content.

Small edits with entries_patch_field

To change one sentence in a long article, use entries_patch_field instead of resending the whole field. With operation: "str_replace", old_str must match exactly once, including whitespace and markup, unless you set replace_all: true. With operation: "append", content is added at the end of the field exactly as sent, with no separator.

Choosing an asset upload method

  • Small files: assets_upload with base64 content, roughly a few MB at most. It can't fetch a URL.
  • Files already online: assets_upload_from_url.
  • Large files: assets_initiate_direct_upload, then PUT the bytes to the returned URL, then assets_complete_direct_upload.
  • Clients that can only send JSON: assets_upload_chunk repeatedly, then assets_complete_chunked_upload.
  • Files a person has on their computer: assets_create_upload_session returns a browser link. After the person uploads, assets_get_upload_session returns the new asset IDs.

Legacy tool names

Older tool names still work when called: dotted forms such as entries.list, and *_upsert forms such as entries_upsert. They are not shown in tools/list, and they only resolve if your token's scopes cover the tool. Use the current names in new work.

Responses and errors

Successful calls return "ok": true along with the result. List tools also return pagination:

{
  "ok": true,
  "page": 1,
  "per_page": 25,
  "total": 42,
  "total_pages": 2,
  "items": [ ... ]
}

Failed calls return "ok": false and an error with a code, a message and sometimes details:

{
  "ok": false,
  "error": {
    "code": "validation_error",
    "message": "handle Invalid handle format 'blog-posts'",
    "details": {
      "fields": [
        {
          "path": "handle",
          "message": "Use snake_case (e.g., 'blog_posts')",
          "suggested_value": "blog_posts"
        }
      ]
    }
  }
}
CodeMeaning
validation_errorInvalid input. details.fields lists each problem with a path, a message and sometimes a suggested_value
not_foundThe account, record or path doesn't exist or isn't visible to you
conflictThe if_match ETag no longer matches, or the change conflicts with existing data
unauthorizedYou don't have access to this account or action
liquid_errorThe Liquid template failed to parse or render
payment_requiredThe account has no active subscription
internalUnexpected server error. Retry, or contact support if it continues

A missing or invalid token is rejected before any tool runs. The server returns HTTP 401 with {"error": "unauthorized", "error_description": "Authentication required"} and the WWW-Authenticate challenge described above.

Recommended workflow

  1. accounts_list to get the account_id.
  2. contexts_list to read the site's purpose, content guidelines and brand voice before making content decisions.
  3. theme_generate_liquid_docs before writing or editing templates, to get the exact variable names for this account.
  4. Make the change with the relevant *_save or entries_patch_field. Try it with dry_run: true first when you're unsure, and use if_match when editing something others may be changing.
  5. theme_validate to catch missing templates, date-routing problems and duplicate paths.
  6. preview_render_path to confirm the page renders as expected.

When styling templates, only use CSS classes that already exist in the site's compiled stylesheet. Classes that appear only in stored content may not be included in the site's CSS build.

Security

  • Every request is tied to your user and token, and a tool can only reach accounts that user belongs to.
  • Scoped OAuth tokens only see the tools their scopes allow.
  • You can revoke personal tokens and API tokens from your account settings at any time.
  • Constituent and CRM data is not available over MCP.

Troubleshooting

  • "Tool not found": either your token's scopes don't include that tool (such tools are hidden, not refused), or the client is using an old name. Reconnect with the mcp scope or the scope you need, and check the name against the catalogue above.
  • payment_required: the target account has no active subscription. Use plans_list and subscriptions_checkout, or subscribe from the dashboard (see Billing).
  • HTTP 401: the token is missing, expired or revoked. OAuth clients should sign in again automatically. For header-based clients, check that the header reads exactly Authorization: Bearer <token>, or create a new token.
  • Fields disappeared after a save: globals_save and blueprints_save replace the whole fields list. Resend the complete list.
  • validation_error on published_at: the collection uses date routing. Include published_at.

For the REST API, see the API Overview. For template syntax, see Liquid Templates.