Configuring Crafty via the MCP server
Use this when an agent should author and configure content in a Crafty tenant: sites, pages/blocks, collections/entries, forms, media, and publishing.
Server URL
The MCP endpoint is per-tenant and lives at the /mcp path on the tenant's own domain:
https://<your-tenant-domain>/mcp
The tenant is identified by the request host, so each tenant has its own endpoint. Hitting the wrong host fails to resolve the tenant. Every request must be authenticated (Bearer token).
Authentication
Two families, both carrying a Bearer token on every request:
Personal Access Token (PAT) — simplest
Generate a long-lived Personal Access Token from your Crafty profile, choosing the scopes you need, then paste it into the editor config as an Authorization: Bearer … header. Best for CLIs and local setups. Regenerate and update the config if it stops working.
OAuth 2.0 — via an MCP client
Create an "MCP client" in your Crafty profile and pick a grant type:
- Authorization Code — standard browser-redirect flow for tools that open a browser (Claude.ai, Claude Code, Codex, Cursor, VS Code, ChatGPT). Select the redirect URI matching your tool. Most of these register themselves (see below), so a manual client is only needed when the tool asks for a
client_id. - Device Code (RFC 8628) — for clients that can't open a browser and support this grant; the client shows a code that you approve in the browser.
When you create the client manually in your profile you are shown the client_id and a one-time client_secret — copy it immediately (PATs likewise show the token once). Clients that auto-register through discovery (e.g. Claude.ai, Claude Code, Codex, Cursor, ChatGPT) are registered as public clients and receive no client secret.
OAuth clients that support MCP auto-discovery (Claude.ai, Claude Code, Codex, Cursor, ChatGPT) can find everything from the server URL alone: discovery is published at /.well-known/oauth-protected-resource and /.well-known/oauth-authorization-server, with authorization_endpoint /oauth/authorize, token_endpoint /oauth/token, dynamic registration_endpoint /oauth/register, PKCE (S256), and grants authorization_code + refresh_token. Registered redirect URIs must be https, a custom app scheme, or http on a loopback address (so local CLI callbacks work). These OAuth endpoints live at the tenant root, not under /mcp.
Clients that implement the MCP 2026-07-28 authorization spec can skip registration and use a Client ID Metadata Document (CIMD): the client_id is an https URL serving the client's metadata as JSON (client_id equal to that URL, redirect_uris, optional client_name). Crafty advertises client_id_metadata_document_supported: true, fetches the document on /oauth/authorize (public addresses only, no redirects, 5 KB max, cached per its Cache-Control max-age), and requires the redirect_uri to be listed in it. Only public clients (token_endpoint_auth_method: "none") are accepted this way. Dynamic registration keeps working for clients that still use it.
Scopes
When you create a token (PAT or OAuth client) you grant one or more scopes. Scopes are enforced per tool: a tool is only advertised when your token carries its required scope, and invoking one without the scope fails with an MCP error (Insufficient OAuth scope). Grant the scopes the agent needs:
| Scope | Unlocks | Tools |
|---|---|---|
crafty:read |
Read sites, pages, block types, collections, entries, media, and form definitions | all list_* / get_* except form submissions |
crafty:write |
Create, update and delete sites, pages, collections, entries, media, and forms; configure the visual editor, llms.txt, and the API cache |
all create_* / update_* / delete_*, upload_media, configure_visual_editor, configure_llms, flush_api_cache, set_api_cache_status |
crafty:publish |
Publish, unpublish and schedule pages, entries and forms | publish_page, unpublish_page, publish_entry, unpublish_entry, schedule_publication, publish_form, unpublish_form |
crafty:submissions |
Read form submissions (may contain personal data) | list_form_submissions, get_form_submission |
Request all four for full authoring plus submission access. Scopes are also advertised in OAuth discovery; the /mcp endpoint additionally requires a valid authenticated token on every request.
Connecting each client
Replace https://<tenant>/mcp with your tenant URL and YOUR_PAT with a Personal Access Token.
Claude.ai (Authorization Code): go to Customize → Connectors → + → Add custom connector, enter the server URL, click Connect and approve in the browser (on Team/Enterprise an Owner adds it first under Organization settings → Connectors). It auto-registers; to use your own client instead, create an Authorization Code MCP client with redirect URI https://claude.ai/api/mcp/auth_callback and paste its client_id/client_secret in the connector's Advanced settings.
Claude Code (CLI):
# Option A — PAT
claude mcp add --transport http crafty https://<tenant>/mcp \
--header "Authorization: Bearer YOUR_PAT"
# Option B — OAuth in the browser (auto-registers)
claude mcp add --transport http crafty https://<tenant>/mcp
# then, inside Claude Code: /mcp → crafty → Authenticate
Cursor (Authorization Code): add to ~/.cursor/mcp.json and authorize when prompted (it auto-registers; for your own client use redirect URI cursor://anysphere.cursor-deeplink/mcp/auth_callback):
{ "mcpServers": { "crafty": { "url": "https://<tenant>/mcp" } } }
VS Code (GitHub Copilot, PAT): add to .vscode/mcp.json, then open Copilot Chat → Agent mode and select the Crafty tools. (OAuth alternative: Authorization Code client with redirect URI vscode://github.copilot-chat/mcp/auth_callback.)
{ "servers": { "crafty": { "type": "http", "url": "https://<tenant>/mcp",
"headers": { "Authorization": "Bearer YOUR_PAT" } } } }
Codex CLI (writes ~/.codex/config.toml):
# Option A — PAT, read from an environment variable
export CRAFTY_MCP_TOKEN="YOUR_PAT"
codex mcp add crafty --url https://<tenant>/mcp --bearer-token-env-var CRAFTY_MCP_TOKEN
# Option B — OAuth in the browser (auto-registers)
codex mcp add crafty --url https://<tenant>/mcp
codex mcp login crafty
ChatGPT (Authorization Code; Plus, Pro, Business, Enterprise, Education, web): turn on Developer mode in Settings → Apps & Connectors → Advanced settings, click Create, paste the server URL, choose OAuth, and approve in the Crafty login. It auto-registers.
Tool catalog
All tools return a JSON text response. IDs are integers; slugs are strings. Pagination defaults to 25 per page (per_page max 100 where supported).
Read (crafty:read)
| Tool | Required | Optional | Returns |
|---|---|---|---|
list_sites |
— | — | all sites (id, name, slug, domain, is_active) |
get_site |
site_id |
— | one site |
list_pages |
site_id |
status (draft/published) |
paginated pages, ordered by title |
get_page |
page_id |
— | page with its blocks (incl. block types) |
list_block_types |
— | — | block types and their schemas (not filtered by site) |
get_block_type |
site_id, and block_type_id or slug |
— | one block type of the site with its full schema (field_definitions) |
list_collections |
— | site_id |
active collections (with their id, used by update_collection / delete_collection) |
list_entries |
collection_slug |
site_id, status, q, filter, per_page |
paginated entries (newest id first), each with status and published_at; filter works as in the public API (see below) |
get_entry |
entry_id |
— | one entry with its collection |
list_media |
site_id |
collection, per_page |
paginated media for the site; each item has alt (null when missing) |
list_forms |
— | site_id, status, per_page |
paginated forms, ordered by name |
get_form |
form_id |
— | one form with its field definitions |
list_redirects |
site_id |
environment (default production) |
the environment's redirects { source, destination, status_code } |
get_assistant_settings |
site_id |
environment (default production) |
the site assistant's settings, status (mode assistant/whatsapp/off, blocked_reason server/plan/disabled/quota, this month's usage), answers_count and the allowed options |
list_assistant_answers |
site_id |
environment (default production) |
the assistant's private fixed answers { id, question, answer, author, updated_at }, newest first |
list_webhooks |
site_id |
environment (default production) |
content webhooks (admins only) with their events and last deliveries; upsert_webhook / delete_webhook manage them (crafty:write) |
Filtering entries by field value
list_entries (MCP) and GET /api/v1/collections/{slug}/entries (public API, crafty.entries.list(slug, { filter }) in the SDK) take the same filter, keyed by field name. A bare value means eq:
{ "precio": { "gte": 150000, "lte": 300000 }, "tipo": "apartamento", "sector": { "in": ["piantini", "naco"] }, "amenidades": { "contains": "piscina" } }
In a URL: ?filter[precio][gte]=150000&filter[tipo]=apartamento&filter[sector][in]=piantini,naco.
| Field type | Operators |
|---|---|
number, decimal |
eq, gt, gte, lt, lte (numeric) |
datetime |
eq, gt, gte, lt, lte (ISO 8601 text, e.g. 2026-12-01 or 2026-12-01T10:00) |
boolean |
eq (true/false/1/0; false also matches entries without the field) |
string, url, select, reference |
eq, in (reference by id) |
multiple select, multiReference, stringList |
contains |
text, longText, repeater, json |
not filterable (use q for text) |
An unknown field, a non-filterable type or a disallowed operator is a 422 (an error from the MCP tool). At most 10 conditions per request and 50 values per in. Filters combine with q, sorting, pagination, locale and preview; the public API still returns only published entries. When designing a collection that visitors will filter (a catalog, a directory), give the filterable attributes their own typed fields (number for price, select for type) rather than burying them in text.
Submissions (crafty:submissions)
Form submissions may contain personal data, so they require their own scope.
| Tool | Required | Optional | Returns |
|---|---|---|---|
list_form_submissions |
form_slug |
site_id, per_page |
paginated submissions, newest first |
get_form_submission |
form_slug, submission_id |
site_id |
one submission with its data |
Write (crafty:write)
| Tool | Required | Optional | Notes |
|---|---|---|---|
flush_api_cache |
site_id |
resource |
flushes cached public API responses for the site; resource = pages, entries, collections, media, or all (default all) |
set_api_cache_status |
site_id, enabled |
— | enables (true) or disables (false) the public API response cache for the site; per-site override, preserves other cache config (TTLs, bypass rules) |
create_site |
name |
slug |
slug auto-generated from name; returns the site (with a generated api_token) |
create_block_type |
site_id, name, fields |
slug, description |
fields: [{ key, type, label?, required?, model?, collection? }]; types string, text, longText, url, number (integer), decimal (any number; optional min, max, precision = max decimal places), boolean, datetime, select (options: ["a", { value: "b", label: "B" }], multiple?: true for an array of values), reference, multiReference, stringList, repeater. number also takes optional min/max. A repeater takes fields: [...] (subtypes string, text, longText, url, number, decimal, boolean, select, reference, stringList; no nested repeater) and optional min/max item counts. url fields accept http(s) URLs, mailto:, tel:, #anchors and site-relative paths starting with / (mailto: allows one recipient, tel: needs at least 3 digits, IDN hosts are converted to punycode when validated). Images and files: reference / multiReference with model: "Media" (value: media id(s)); entries of a collection: collection: "<slug>". Slug auto-generated from name, unique per site |
update_block_type |
site_id, block_type_id |
name, slug, description, fields, force |
fields replaces all fields. Removing or retyping a field (or repeater subfield) that existing blocks fill is rejected unless force: true; block data is never rewritten |
delete_block_type |
site_id, block_type_id, confirm: true |
— | refused while blocks use the type |
create_page |
site_id, title |
slug, blocks |
starts as draft; blocks: [{ block_type_id, data }] created in order; data is validated against the block type (errors like blocks.2.data.items.1.title), undeclared keys are dropped and listed in warnings |
update_page |
page_id |
title, slug, blocks |
if blocks is supplied, ALL existing blocks are replaced (send each block with its id to keep it); same validation and warnings as create_page |
create_collection |
site_id, name |
slug, description, fields |
slug auto-generated from name; fields: [{ key, type, label, required?, model?, repository?, collection?, options?, multiple?, min?, max?, precision?, fields? }]; types string, text, longText, url, number (integer), decimal, boolean, datetime, select, json, reference, multiReference, stringList, repeater — decimal, select and repeater take the same settings as in create_block_type; invalid settings (e.g. a select without options or with duplicate values) are rejected. Images: { key: "cover", type: "reference", model: "Media", repository: "library" }, galleries: multiReference; avoid json for content editors maintain |
update_collection |
collection_id |
name, description, is_active, fields |
fields is a full replacement list in display order; each field { key, type, label?, required?, model?, repository?, collection?, relation?, slug_source?, title_source?, options?, multiple?, min?, max?, precision?, fields? } (media reference: { key: "image", type: "reference", model: "Media", repository: "library" }) |
delete_collection |
collection_id, confirm: true |
— | permanently deletes the collection and all its entries |
create_entry |
collection_slug, site_id, slug |
data, status |
status = draft (default, hidden from the public API until publish_entry) or published; data = field key/value pairs, validated against the collection schema (number integer, decimal within min/max/precision, select one of its option values or an array of distinct values when multiple, repeater items against their subfields; invalid data is rejected as data.<field>: <message>; undeclared keys are dropped and listed in warnings) |
update_entry |
entry_id |
data, slug |
snapshots the prior data into a new revision; data is validated like create_entry (required fields are not enforced, only provided ones are checked) and rejected data creates no revision |
bulk_upsert_entries |
site_id, collection_slug, entries |
dry_run, publish |
up to 100 { slug, external_id?, data } per call; matches an existing entry by external_id first, then by slug, else creates it; data is validated like create_entry and replaces the stored data on update (with a revision); new entries are drafts unless publish: true (needs crafty:publish); items fail individually; dry_run reports created / updated / failed per item without saving |
upload_media |
url, site_id |
collection_name (default library), file_name, sha256, alt |
alt = alt text for images (max 500 chars); http(s) URL or base64 data URI (max 256 KB decoded); sha256 rejects and discards the upload on mismatch; the response returns the stored sha256, visibility and a warning when the file is private |
import_media_from_urls |
site_id, urls |
collection_name (default library) |
up to 50 public http(s) URLs per call, with the same SSRF, size and type checks as upload_media; a URL already imported to the site (or repeated in the batch) is not downloaded again; per URL: status imported / existing / duplicate / failed, media_id, original_url |
request_media_upload |
site_id, file_name, sha256 |
collection_name (default library), size, alt |
alt is stored with the file once uploaded; returns a 15-minute single-use signed URL and a ready curl -T command for uploading a local binary file without sending its bytes through the model; max 25 MB; verified against sha256 |
manage_media_collection |
site_id, key |
name, description, is_public |
creates or updates a media repository; is_public: true gives permanent public URLs (existing files are moved in the background); new repositories are private |
update_media |
site_id, media_id, alt |
— | sets the alt text (max 500 chars) of one media file; null or "" clears it |
delete_media |
site_id, media_id, confirm: true |
— | permanently deletes one media file (ids from list_media) |
create_form |
site_id, name, slug |
description, fields, captcha |
fields: [{ key, type, label, required? }]; starts as draft; protected by a captcha by default when Crafty has one configured (captcha: false to opt out), and the frontend must then render it with crafty.forms.captcha() |
update_form |
form_id |
name, slug, description, fields, notification_emails |
only supplied fields change; fields/notification_emails replace |
configure_visual_editor |
site_id |
environment (default production), frontend_url, routes (page_pattern, home_slug, collections, fixed), render_mode (ssr/hybrid/static/unknown), upload_media_collection |
visual editor settings of one environment; only sent keys change, null resets to the fallback; with only site_id it just reads; returns the effective settings plus llms_warnings (the llms.txt generator's warnings, since routes affect it too) |
configure_llms |
site_id |
environment (default production), title, summary, language, excluded_collections (routed collections to drop), included_collections (routeless collections to opt in), collection_languages |
admins only; same read/partial-update semantics as configure_visual_editor; only sent keys change, null resets to the fallback; with only site_id it just reads; returns the effective settings, the configuration warnings, and the first 40 lines of the generated llms.txt |
| configure_assistant | site_id and at least one setting | environment (default production), enabled, whatsapp_enabled, whatsapp_number, whatsapp_label, contact_form, hours (weekdays/saturday/sunday → { open, close } or null), timezone, greeting, suggestions (max 3), tone (friendly/formal/technical), guidelines, privacy_url, appearance (name, color, font, corners, position, avatar_on_launcher) | only sent keys change, null resets to the default; the avatar is uploaded in the panel; returns the effective settings and status |
| configure_store | site_id | environment (default production), enabled, currency, order_prefix, prices_include_tax, taxes, default_tax, delivery (enabled, fee, free_from, tax, zone), pickup (enabled, address), payment_methods (cash_on_delivery, pay_at_pickup, transfer), transfer_accounts, notify_emails, customer_emails, return_url, color, logo_media_id, font, corners, checkout_fields, checkout_analytics, captcha | admins only; enabled: true needs the store in the plan and creates (or adopts) the productos collection and the checkout URL; only sent keys change; with only site_id it just reads; returns the settings, open, the products summary and checkout_url. See online-store.md |
| list_orders | site_id | environment, status, payment_status, search, limit, page | needs the orders permission (read) |
| get_order | site_id, number | environment | the order with its lines (ids for update_order), fields, metadata, payment_url, next_statuses and history |
| create_order | site_id, customer, fulfillment, payment_method, items | environment, address, fields, metadata, coupon, locale, expected_total | orders write permission; source mcp; returns the order and its payment_url |
| update_order | site_id, number | environment, edit keys (items, fulfillment, payment_method, customer, address, fields, metadata, coupon, coupon_below_minimum), notify, status, paid, payment_reference, internal_note | edits only unpaid open orders (metadata always); old lines keep their price; fires order.updated |
| list_coupons | site_id | environment, status, search | coupons with status, uses, orders, sales, discounted; needs the coupons permission (read) |
| get_coupon | site_id, code | environment | one coupon's rules (products as slugs) and results |
| upsert_coupon | site_id, code | environment, name, type, value, max_discount, min_subtotal, applies_to, products, max_uses, max_uses_per_email, starts_at, ends_at, is_active | creates or changes by code; a used coupon keeps its code and type; needs coupons write |
| delete_coupon | site_id, code | environment | deletes an unused coupon, pauses a used one |
| configure_bookings | site_id | environment (default production), enabled, timezone, hours (merged by weekday), closed_dates, min_notice_minutes, max_days_ahead, slot_interval, cancel_notice_minutes, auto_confirm, reminder_hours, payment_methods (pay_at_venue, transfer), address, phone, checkout_fields | admins only; enabled: true needs bookings in the plan and creates (or adopts) the servicios and profesionales collections; only sent keys change; with only site_id it just reads; returns the settings, open, the service and professional slugs and checkout_url. See online-bookings.md |
| get_booking_availability | site_id | environment, service, professional, from, to | without service: the bookable services and professionals; with it: free slots per local day (up to 31 days), each with the professionals free |
| list_bookings | site_id | environment, from, to, status, professional, search, metadata, limit, page | needs the bookings permission (read) |
| get_booking | site_id, number | environment | the booking with next_statuses, internal_note, customer_url and events |
| create_booking | site_id, service, start, customer | environment, professional, payment_method, fields, metadata, outside_hours, internal_note, locale | bookings write permission; source mcp; a taken slot answers SLOT_UNAVAILABLE |
| update_booking | site_id, number | environment, start, professional, outside_hours, status, paid, payment_reference, internal_note, metadata | moves, changes the status, marks paid; returns the booking and applied |
| set_professional_schedule | site_id, professional | environment, mode (shop/own), hours | without mode it reads; own hours always fall inside the shop's |
| manage_booking_blocks | site_id, action (list/create/delete) | environment, professional, repeat (once/weekly), from_date, to_date, all_day, start_time, end_time, weekdays, reason, id (to delete) | blocks time for one professional or the whole shop; create returns how many bookings already fall inside |
| upsert_assistant_answer | site_id, question, answer | environment (default production), answer_id | adds a fixed answer, or edits the one with answer_id; the assistant uses it from the next visitor message |
| delete_assistant_answer | site_id, answer_id | environment (default production) | deletes one fixed answer |
| upsert_redirects | site_id, redirects | environment (default production), dry_run | up to 500 { source, destination, status_code? } per call, matched by source; source is a path starting with / (no spaces, ? or #; Cloudflare Pages * splats and :placeholders allowed), destination a path or absolute http(s) URL, status_code 301 (default) / 302 / 307 / 308; invalid items are listed in failed and skipped. Crafty hosting writes them to _redirects on the next deploy (after the site's own rules; limit 2000 static + 100 dynamic); custom frontends read GET /api/v1/redirects |
| delete_redirect | site_id, source | environment (default production) | deletes one redirect |
Assistant tools follow the panel's Asistente section: admins always, editors with read access can read and editors with write access can change.
Redirect tools follow the Pages permission: reading needs access to pages, changing needs write access.
Publish (crafty:publish)
| Tool | Required | Effect |
|---|---|---|
publish_page |
page_id |
status → published, sets published_at |
unpublish_page |
page_id |
status → draft, clears published_at |
publish_entry |
entry_id |
status → published, sets published_at |
unpublish_entry |
entry_id |
status → draft, clears published_at; the entry disappears from the public API and from references |
schedule_publication |
type (page/entry), id, and publish_at and/or unpublish_at |
sets when a draft publishes itself and/or when a published item returns to draft; ISO 8601 dates (no offset → the user's regional timezone), null clears one, unpublish_at must be after publish_at; applied every minute |
publish_form |
form_id |
status → published (now accepts public submissions) |
unpublish_form |
form_id |
status → draft (stops accepting submissions) |
Canonical workflows
Model content with block types (preferred)
list_sites→ choose asite_id(orcreate_site).- For each section of the design,
create_block_type(orget_block_typeif it exists). Use arepeaterfor lists of items,stringListfor lists of plain strings, andreference/multiReferencewithmodel: "Media"for images (upload them first); only use a collection for large independent record sets. create_pagein the default locale withblocks: [{ block_type_id, data }, …]; fix anyblocks.N.data.…errors and checkwarnings. For other languages,configure_localesonce and thenupsert_translationper page (blocks by id fromget_page), ortranslate_contentto let AI translate it once AI translation is enabled for the workspace and the site (configure_translation).publish_page.- To change a schema later,
update_block_typewith the FULL field list (useforce: trueonly if you accept orphaning data that blocks already hold).
Build and publish a page
list_sites→ choose asite_id(orcreate_site).get_block_type/list_block_types→ note eachidand its data schema.create_pagewithsite_id,title, andblocks: [{ block_type_id, data }, …].publish_pagewith the returnedpage_id.
Structured content
list_collections(optionallysite_id) → pick acollection_slug, orcreate_collectionwith itsfields(update_collectionchanges them later).create_entrywithcollection_slug,site_id,slug, anddata.update_entryto edit (each edit stores a revision).
Forms
create_formwithsite_id,name,slug, andfields.publish_formso it accepts public submissions.list_form_submissions/get_form_submissionto read what visitors sent.
Media
- Need permanent public URLs?
manage_media_collectionwithsite_id,key,is_public: truefirst (otherwise files get expiring signed URLs). - From a URL or a tiny file:
upload_mediawithurl,site_idandalt(optionallycollection_name,sha256). From a local file: computeshasum -a 256 <file>, callrequest_media_upload(withalt), then run the returnedcurl -Tcommand. list_mediato confirm and retrieve URLs; give every content image withoutaltone withupdate_media.
Migrate an existing site
Follow migrate-to-crafty.md: crawl → classify → model → import_media_from_urls → bulk_upsert_entries (drafts, dry_run first) → upsert_redirects → human review → publish.
Visual editor
- Prepare the frontend (see
visual-editor.md). configure_visual_editorwithsite_id,frontend_url,routes, andrender_mode.- Ask the user to open Editar sitio in the admin to confirm click-to-edit works.
API response cache management
The public API has a tag-based response cache (enabled per environment). Per-site overrides take priority over the global setting.
- Disable cache for a site before bulk edits, then re-enable when done:
set_api_cache_statuswithsite_idandenabled: false.- Make all your content changes (
update_entry,update_page, etc.). set_api_cache_statuswithenabled: true.flush_api_cachewithsite_id(and optionallyresource) to clear stale entries.
- Flush only a specific resource type after targeted changes:
flush_api_cachewithsite_idandresource: "entries"after updating entries.flush_api_cachewithsite_idandresource: "pages"after updating pages.
flush_api_cachewith noresource(orresource: "all") clears all cached responses for the site at once.
Troubleshooting
- 401 / authentication error — the Bearer token is missing, invalid, or expired; reconnect or regenerate it. Every
/mcprequest must carry a valid token. - A tool is missing, or fails with
Insufficient OAuth scope— the token lacks that tool's scope. Tools are only advertised when the token carries the matching scope (crafty:writeto create/edit,crafty:publishto publish,crafty:submissionsto read form submissions). Re-issue the token with the scopes you need. - Tenant not found — wrong host; use the exact tenant domain. OAuth token endpoints are at the tenant root (
/oauth/...), not under/mcp. - OAuth fails with a manually created client — its type must match the flow (Authorization Code or Device Code), its redirect URI must be exactly the one the tool uses, and
client_id/client_secretmust match. Tools that auto-register need no manual client. - Token/secret lost —
client_secretand PATs are shown only once at creation; regenerate if lost.