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
401with aWWW-Authenticate: Bearerheader. The header'sresource_metadatapoints athttps://api.airogelcms.com/.well-known/oauth-protected-resource. - That metadata names the authorization server,
https://app.airogelcms.com. Its discovery document is athttps://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 (PKCES256required), 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
mcpscope. 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:
mcpgrants 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>:readand<resource>:writepairs for:entries,collections,blueprints,templates,assets,globals,navigations,forms,accounts,domains,events,contexts,subscriptions,supportandredirect_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_pathneedsentries:read,globals:readandtemplates: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_createplans_list,subscriptions_checkout,subscriptions_statussupport_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
| Tool | Scope | What it does |
|---|---|---|
accounts_list | accounts:read | List the accounts you can access |
accounts_create | accounts:write | Create a new account for the signed-in user and return its account_id |
plans_list | subscriptions:read | List subscription plans with pricing; optional interval filter |
subscriptions_checkout | subscriptions:write | Return a Stripe Checkout URL to subscribe an account to a plan |
subscriptions_status | subscriptions:read | Report whether an account has an active subscription, with plan details |
Entries and search
| Tool | Scope | What it does |
|---|---|---|
entries_list | entries:read | List entries in a collection. summary: true (the default) leaves out large rich-text fields |
entries_get | entries:read | Get one entry by handle or ID with all field values and an etag |
entries_save | entries:write | Create 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_field | entries:write | Edit one text, rich-text or raw-HTML field in place: str_replace (exact match, once unless replace_all) or append |
entries_delete | entries:write | Delete an entry by handle or ID |
entries_query | entries:read | Filter, 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_relation | entries:read | Find entries that reference another entry through an entity field, including reverse lookups |
search_query | entries:read | Full-text search across titles, handles and text fields, ranked by relevance (see Full-Text Search) |
Collections and blueprints
| Tool | Scope | What it does |
|---|---|---|
collections_list / collections_get | collections:read | List collections, or get one with its blueprints |
collections_save | collections:write | Create or update a collection by handle, including its routing pattern (see Routing) |
collections_delete | collections:write | Delete a collection and the blueprints it owns. Refused while the collection has entries or a form uses it |
blueprints_list / blueprints_get | blueprints:read | List blueprints, or get one with its field definitions |
blueprints_save | blueprints:write | Create or update a blueprint by handle. fields is the complete field list |
blueprints_delete | blueprints:write | Delete a blueprint. Refused while entries, forms or navigation items depend on it |
Templates, theme and preview
| Tool | Scope | What it does |
|---|---|---|
templates_list / templates_get | templates:read | List Liquid templates, or get one with its content |
templates_save | templates:write | Create or update a template by handle. Liquid syntax is checked before saving |
templates_delete | templates:write | Delete a template |
theme_generate_liquid_docs | blueprints, collections, globals, navigations (read) | Return a Markdown reference of every Liquid variable available on this account |
theme_validate | collections, 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_path | entries, 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
| Tool | Scope | What it does |
|---|---|---|
assets_list / assets_get | assets:read | List assets (filter by path, filename or content type), or get one by ID or full path |
assets_upload | assets:write | Upload base64-encoded content. For small files only; keyed on path and filename, with a replace flag |
assets_upload_from_url | assets:write | Import a file from a URL. The download runs in the background, and the asset comes back with status pending |
assets_initiate_direct_upload | assets:write | Start a large-file upload: returns a PUT URL, headers and a signed_blob_id |
assets_complete_direct_upload | assets:write | Turn the uploaded blob into an asset |
assets_upload_chunk | assets:write | Send one base64 chunk of a file, for clients that can't PUT to a URL |
assets_complete_chunked_upload | assets:write | Reassemble uploaded chunks into an asset |
assets_create_upload_session | assets:write | Create 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_session | assets:read | Check a session's status and get the assets uploaded through it |
assets_delete | assets:write | Delete an asset by ID or path |
Globals, navigations and forms
| Tool | Scope | What it does |
|---|---|---|
globals_list / globals_get | globals:read | List site globals, or get one with its values |
globals_save | globals:write | Create or update a global by handle. On update, fields replaces the whole field list |
globals_delete | globals:write | Delete a global, along with its blueprint and values |
navigations_list / navigations_get | navigations:read | List navigations, or get one with its items |
navigations_save | navigations:write | Create or update a navigation by handle |
navigations_delete | navigations:write | Delete a navigation and all its items |
navigation_items_list | navigations:read | List the items in a navigation |
navigation_items_save | navigations:write | Create or update an item. Matched on navigation, title and URL or linked entry |
navigation_items_delete | navigations:write | Delete an item by ID |
forms_list / forms_get | forms:read | List forms, or get one with template usage examples |
forms_save | forms:write | Create or update a form by handle |
forms_delete | forms:write | Delete a form. Entries it already submitted are kept |
Domains and redirects
| Tool | Scope | What it does |
|---|---|---|
domains_list / domains_get | domains:read | List custom domains, or get one with its verification status and DNS instructions |
domains_save | domains:write | Add or update a domain by name and return the DNS records to create (see Custom Domains) |
domains_verify | domains:write | Verify ownership through the TXT record (verify_ownership: true), or check email configuration |
domains_delete | domains:write | Deactivate a domain (the default), or delete it permanently with permanent: true |
redirect_rules_list / redirect_rules_get | redirect_rules:read | List 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_save | redirect_rules:write | Create 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_delete | redirect_rules:write | Delete a rule by ID |
Events
| Tool | Scope | What it does |
|---|---|---|
events_list / events_get | events:read | List events (filter by published or upcoming), or get one with its ticket types |
events_save | events:write | Create an event (omit event_id) or partially update one. Set published: true to show it on the site |
events_delete | events:write | Delete an event and its ticket types |
event_tickets_save | events:write | Create or update a ticket type. Set the price with dollar_cost, or with cost in cents |
event_tickets_delete | events:write | Delete 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.
| Tool | Scope | What it does |
|---|---|---|
contexts_list / contexts_get | contexts:read | List contexts, or get one by handle or ID |
contexts_save | contexts:write | Create or update a context by snake_case handle |
contexts_delete | contexts:write | Delete a context |
Support
| Tool | Scope | What it does |
|---|---|---|
support_contact | support:write | Send 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_saveandevent_tickets_saveuseevent_idandticket_id. Omit the ID to create a new record.navigation_items_savematches on navigation, title and URL or linked entry.redirect_rules_savematches on domain,from_pathandmatch_type.domains_savematches on the domain name, andassets_uploadon 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_uploadwith 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, thenassets_complete_direct_upload. - Clients that can only send JSON:
assets_upload_chunkrepeatedly, thenassets_complete_chunked_upload. - Files a person has on their computer:
assets_create_upload_sessionreturns a browser link. After the person uploads,assets_get_upload_sessionreturns 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"
}
]
}
}
}
| Code | Meaning |
|---|---|
validation_error | Invalid input. details.fields lists each problem with a path, a message and sometimes a suggested_value |
not_found | The account, record or path doesn't exist or isn't visible to you |
conflict | The if_match ETag no longer matches, or the change conflicts with existing data |
unauthorized | You don't have access to this account or action |
liquid_error | The Liquid template failed to parse or render |
payment_required | The account has no active subscription |
internal | Unexpected 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
accounts_listto get theaccount_id.contexts_listto read the site's purpose, content guidelines and brand voice before making content decisions.theme_generate_liquid_docsbefore writing or editing templates, to get the exact variable names for this account.- Make the change with the relevant
*_saveorentries_patch_field. Try it withdry_run: truefirst when you're unsure, and useif_matchwhen editing something others may be changing. theme_validateto catch missing templates, date-routing problems and duplicate paths.preview_render_pathto 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
mcpscope or the scope you need, and check the name against the catalogue above. payment_required: the target account has no active subscription. Useplans_listandsubscriptions_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_saveandblueprints_savereplace the wholefieldslist. Resend the complete list. validation_erroronpublished_at: the collection uses date routing. Includepublished_at.
For the REST API, see the API Overview. For template syntax, see Liquid Templates.