Configurar Crafty mediante el servidor MCP

Usa esto cuando un agente deba crear y configurar contenido en un tenant de Crafty: sitios, páginas/bloques, colecciones/entradas, formularios, medios y publicación.

URL del servidor

El endpoint MCP es por tenant y se encuentra en la ruta /mcp del propio dominio del tenant:

https://<your-tenant-domain>/mcp

El tenant se identifica por el host de la solicitud, así que cada tenant tiene su propio endpoint. Si usas un host incorrecto, no se resolverá el tenant. Cada solicitud debe estar autenticada (token Bearer).

Autenticación

Hay dos familias, ambas con un token Bearer en cada solicitud:

Personal Access Token (PAT): la opción más sencilla

Genera un Personal Access Token de larga duración desde tu perfil de Crafty, eligiendo los scopes que necesites, y luego pégalo en la configuración del editor como cabecera Authorization: Bearer …. Es ideal para CLIs y configuraciones locales. Si deja de funcionar, regénéralo y actualiza la configuración.

OAuth 2.0: mediante un cliente MCP

Crea un "cliente MCP" en tu perfil de Crafty y elige un tipo de concesión (grant type):

  • Authorization Code: flujo estándar con redirección del navegador para herramientas que abren un navegador (Claude.ai, Claude Code, Codex, Cursor, VS Code, ChatGPT). Selecciona la URI de redirección que corresponda a tu herramienta. La mayoría se registra sola (ver más abajo), así que un cliente manual solo es necesario cuando la herramienta pide un client_id.
  • Device Code (RFC 8628): para clientes que no pueden abrir un navegador y admiten esta concesión; el cliente muestra un código que apruebas en el navegador.

Al crear el cliente manualmente en tu perfil se te muestran el client_id y un client_secret de un solo uso. Cópialo de inmediato (los PAT también muestran el token una sola vez). Los clientes que se registran automáticamente mediante discovery (por ejemplo, Claude.ai, Claude Code, Codex, Cursor, ChatGPT) se registran como clientes públicos y no reciben client secret.

Los clientes OAuth que admiten el autodescubrimiento de MCP (Claude.ai, Claude Code, Codex, Cursor, ChatGPT) pueden encontrar todo solo con la URL del servidor: el discovery se publica en /.well-known/oauth-protected-resource y /.well-known/oauth-authorization-server, con authorization_endpoint /oauth/authorize, token_endpoint /oauth/token, registration_endpoint dinámico /oauth/register, PKCE (S256) y las concesiones authorization_code + refresh_token. Las URI de redirección registradas deben ser https, un esquema de aplicación personalizado o http en una dirección de loopback (así funcionan los callbacks de CLI locales). Estos endpoints OAuth están en la raíz del tenant, no bajo /mcp.

Los clientes que implementan la especificación de autorización MCP 2026-07-28 pueden omitir el registro y usar un Client ID Metadata Document (CIMD): el client_id es una URL https que sirve los metadatos del cliente en JSON (client_id igual a esa URL, redirect_uris, y client_name opcional). Crafty anuncia client_id_metadata_document_supported: true, obtiene el documento en /oauth/authorize (solo direcciones públicas, sin redirecciones, 5 KB como máximo, en caché según el max-age de su Cache-Control) y exige que la redirect_uri esté listada en él. Solo se aceptan de esta forma los clientes públicos (token_endpoint_auth_method: "none"). El registro dinámico sigue funcionando para los clientes que aún lo usan.

Scopes

Al crear un token (PAT o cliente OAuth) concedes uno o más scopes. Los scopes se aplican por herramienta: una herramienta solo se anuncia si tu token tiene el scope requerido, y si la invocas sin ese scope falla con un error de MCP (Insufficient OAuth scope). Concede los scopes que el agente necesite:

Scope Desbloquea Herramientas
crafty:read Leer sitios, páginas, tipos de bloque, colecciones, entradas, medios y definiciones de formularios todas las list_* / get_* excepto los envíos de formularios
crafty:write Crear, actualizar y eliminar sitios, páginas, colecciones, entradas, medios y formularios; configurar el editor visual, llms.txt y la caché de la API todas las create_* / update_* / delete_*, upload_media, configure_visual_editor, configure_llms, flush_api_cache, set_api_cache_status
crafty:publish Publicar, despublicar y programar páginas, entradas y formularios publish_page, unpublish_page, publish_entry, unpublish_entry, schedule_publication, publish_form, unpublish_form
crafty:submissions Leer los envíos de formularios (pueden contener datos personales) list_form_submissions, get_form_submission

Solicita los cuatro para tener acceso completo a la creación de contenido y a los envíos. Los scopes también se anuncian en el discovery de OAuth; el endpoint /mcp además exige un token autenticado válido en cada solicitud.

Conectar cada cliente

Reemplaza https://<tenant>/mcp por la URL de tu tenant y YOUR_PAT por un Personal Access Token.

Claude.ai (Authorization Code): ve a Customize → Connectors → + → Add custom connector, introduce la URL del servidor, haz clic en Connect y aprueba en el navegador (en planes Team/Enterprise, un Owner la añade primero en Organization settings → Connectors). Se registra automáticamente; si prefieres usar tu propio cliente, crea un cliente MCP Authorization Code con la URI de redirección https://claude.ai/api/mcp/auth_callback y pega su client_id/client_secret en Advanced settings del conector.

Claude Code (CLI):

# Opción A: PAT
claude mcp add --transport http crafty https://<tenant>/mcp \
  --header "Authorization: Bearer YOUR_PAT"

# Opción B: OAuth en el navegador (se registra automáticamente)
claude mcp add --transport http crafty https://<tenant>/mcp
# después, dentro de Claude Code: /mcp → crafty → Authenticate

Cursor (Authorization Code): añade lo siguiente a ~/.cursor/mcp.json y autoriza cuando se te pida (se registra automáticamente; para tu propio cliente usa la URI de redirección cursor://anysphere.cursor-deeplink/mcp/auth_callback):

{ "mcpServers": { "crafty": { "url": "https://<tenant>/mcp" } } }

VS Code (GitHub Copilot, PAT): añade lo siguiente a .vscode/mcp.json, luego abre Copilot Chat → modo Agent y selecciona las herramientas de Crafty. (Alternativa OAuth: cliente Authorization Code con la URI de redirección vscode://github.copilot-chat/mcp/auth_callback).

{ "servers": { "crafty": { "type": "http", "url": "https://<tenant>/mcp",
  "headers": { "Authorization": "Bearer YOUR_PAT" } } } }

Codex CLI (escribe en ~/.codex/config.toml):

# Opción A: PAT, leído desde una variable de entorno
export CRAFTY_MCP_TOKEN="YOUR_PAT"
codex mcp add crafty --url https://<tenant>/mcp --bearer-token-env-var CRAFTY_MCP_TOKEN

# Opción B: OAuth en el navegador (se registra automáticamente)
codex mcp add crafty --url https://<tenant>/mcp
codex mcp login crafty

ChatGPT (Authorization Code; Plus, Pro, Business, Enterprise, Education, web): activa Developer mode en Settings → Apps & Connectors → Advanced settings, haz clic en Create, pega la URL del servidor, elige OAuth y aprueba en el inicio de sesión de Crafty. Se registra automáticamente.

Catálogo de herramientas

Todas las herramientas devuelven una respuesta de texto JSON. Los IDs son enteros; los slugs son cadenas. La paginación es de 25 por página por defecto (per_page con máximo de 100 cuando se admite).

Lectura (crafty:read)

Herramienta Obligatorio Opcional Devuelve
list_sites — — todos los sitios (id, name, slug, domain, is_active)
get_site site_id — un sitio
list_pages site_id status (draft/published) páginas paginadas, ordenadas por título
get_page page_id — página con sus bloques (incl. tipos de bloque)
list_block_types — — tipos de bloque y sus esquemas (no se filtran por sitio)
get_block_type site_id, y block_type_id o slug — un tipo de bloque del sitio con su esquema completo (field_definitions)
list_collections — site_id colecciones activas (con su id, usado por update_collection / delete_collection)
list_entries collection_slug site_id, status, per_page entradas paginadas (primero el id más reciente), cada una con status y published_at
get_entry entry_id — una entrada con su colección
list_media site_id collection, per_page medios paginados del sitio; cada elemento tiene alt (null si falta)
list_forms — site_id, status, per_page formularios paginados, ordenados por nombre
get_form form_id — un formulario con sus definiciones de campos
list_redirects site_id environment (por defecto production) las redirecciones del entorno { source, destination, status_code }
get_assistant_settings site_id environment (por defecto production) la configuración del asistente del sitio, status (mode assistant/whatsapp/off, blocked_reason server/plan/disabled/quota, el usage de este mes), answers_count y las options permitidas
list_assistant_answers site_id environment (por defecto production) las respuestas fijas privadas del asistente { id, question, answer, author, updated_at }, de la más reciente a la más antigua
list_webhooks site_id environment (por defecto production) webhooks de contenido (solo administradores) con sus eventos y últimas entregas; upsert_webhook / delete_webhook los gestionan (crafty:write)

Envíos (crafty:submissions)

Los envíos de formularios pueden contener datos personales, por eso requieren su propio scope.

Herramienta Obligatorio Opcional Devuelve
list_form_submissions form_slug site_id, per_page envíos paginados, del más reciente al más antiguo
get_form_submission form_slug, submission_id site_id un envío con sus datos

Escritura (crafty:write)

Herramienta Obligatorio Opcional Notas
flush_api_cache site_id resource vacía las respuestas de API públicas en caché del sitio; resource = pages, entries, collections, media o all (por defecto all)
set_api_cache_status site_id, enabled — activa (true) o desactiva (false) la caché de respuestas de la API pública del sitio; es una anulación por sitio que conserva el resto de la configuración de caché (TTL, reglas de bypass)
create_site name slug el slug se genera automáticamente a partir del nombre; devuelve el sitio (con un api_token generado)
create_block_type site_id, name, fields slug, description fields: [{ key, type, label?, required?, model?, collection? }]; tipos string, text, longText, url, number (entero), decimal (cualquier número; min, max, precision opcionales = máximo de decimales), boolean, datetime, select (options: ["a", { value: "b", label: "B" }], multiple?: true para un array de valores), reference, multiReference, stringList, repeater. number también admite min/max opcionales. Un repeater recibe fields: [...] (subtipos string, text, longText, url, number, decimal, boolean, select, reference, stringList; sin repeater anidado) y min/max opcionales de número de elementos. Los campos url aceptan URLs http(s), mailto:, tel:, #anclas y rutas relativas al sitio que empiecen por / (mailto: admite un único destinatario, tel: necesita al menos 3 dígitos, los hosts IDN se convierten a punycode al validarse). Imágenes y archivos: reference / multiReference con model: "Media" (valor: id(s) de medios); entradas de una colección: collection: "<slug>". El slug se genera a partir del nombre y es único por sitio
update_block_type site_id, block_type_id name, slug, description, fields, force fields reemplaza todos los campos. Eliminar o cambiar el tipo de un campo (o subcampo de repeater) que los bloques existentes llenan se rechaza salvo que uses force: true; los datos de los bloques nunca se reescriben
delete_block_type site_id, block_type_id, confirm: true — se rechaza mientras haya bloques que usen el tipo
create_page site_id, title slug, blocks empieza como draft; blocks: [{ block_type_id, data }] se crean en orden; data se valida contra el tipo de bloque (errores como blocks.2.data.items.1.title), las claves no declaradas se descartan y se listan en warnings
update_page page_id title, slug, blocks si se envía blocks, se reemplazan TODOS los bloques existentes (envía cada bloque con su id para conservarlo); misma validación y warnings que create_page
create_collection site_id, name slug, description, fields slug generado automáticamente a partir del nombre; fields: [{ key, type, label, required?, model?, repository?, collection?, options?, multiple?, min?, max?, precision?, fields? }]; tipos string, text, longText, url, number (entero), decimal, boolean, datetime, select, json, reference, multiReference, stringList, repeater; decimal, select y repeater aceptan las mismas opciones que en create_block_type; las configuraciones no válidas (por ejemplo, un select sin opciones o con valores duplicados) se rechazan. Imágenes: { key: "cover", type: "reference", model: "Media", repository: "library" }; galerías: multiReference; evita json para contenido que mantengan los editores
update_collection collection_id name, description, is_active, fields fields es una lista de reemplazo completo en el orden de visualización; cada campo { key, type, label?, required?, model?, repository?, collection?, relation?, slug_source?, title_source?, options?, multiple?, min?, max?, precision?, fields? } (referencia a medios: { key: "image", type: "reference", model: "Media", repository: "library" })
delete_collection collection_id, confirm: true — elimina de forma permanente la colección y todas sus entradas
create_entry collection_slug, site_id, slug data, status status = draft (por defecto, oculta en la API pública hasta publish_entry) o published; data = pares clave/valor de campos, validados contra el esquema de la colección (number entero, decimal dentro de min/max/precision, select uno de sus valores de opción o un array de valores distintos cuando multiple, elementos de repeater según sus subcampos; los datos no válidos se rechazan como data.<field>: <message>; las claves no declaradas se descartan y se listan en warnings)
update_entry entry_id data, slug guarda los datos anteriores en una nueva revisión; data se valida como en create_entry (los campos obligatorios no se exigen, solo se comprueban los enviados) y los datos rechazados no crean revisión
bulk_upsert_entries site_id, collection_slug, entries dry_run, publish hasta 100 { slug, external_id?, data } por llamada; busca primero una entrada existente por external_id y luego por slug, y si no existe la crea; data se valida como en create_entry y reemplaza los datos guardados al actualizar (con una revisión); las entradas nuevas son borradores salvo que publish: true (requiere crafty:publish); los elementos fallan de forma individual; dry_run informa created / updated / failed por elemento sin guardar
upload_media url, site_id collection_name (por defecto library), file_name, sha256, alt alt = texto alternativo para imágenes (máximo 500 caracteres); URL http(s) o URI de datos base64 (máximo 256 KB decodificados); sha256 rechaza y descarta la subida si no coincide; la respuesta devuelve el sha256 almacenado, visibility y un warning cuando el archivo es privado
import_media_from_urls site_id, urls collection_name (por defecto library) hasta 50 URL http(s) públicas por llamada, con las mismas comprobaciones de SSRF, tamaño y tipo que upload_media; una URL ya importada al sitio (o repetida en el lote) no se vuelve a descargar; por URL: status imported / existing / duplicate / failed, media_id, original_url
request_media_upload site_id, file_name, sha256 collection_name (por defecto library), size, alt alt se guarda con el archivo una vez subido; devuelve una URL firmada de un solo uso válida durante 15 minutos y un comando curl -T listo para subir un archivo binario local sin enviar sus bytes a través del modelo; máximo 25 MB; se verifica contra sha256
manage_media_collection site_id, key name, description, is_public crea o actualiza un repositorio de medios; is_public: true da URL públicas permanentes (los archivos existentes se mueven en segundo plano); los repositorios nuevos son privados
update_media site_id, media_id, alt — establece el texto alternativo (máximo 500 caracteres) de un archivo multimedia; null o "" lo borra
delete_media site_id, media_id, confirm: true — elimina de forma permanente un archivo multimedia (ids de list_media)
create_form site_id, name, slug description, fields, captcha fields: [{ key, type, label, required? }]; empieza como draft; protegido con captcha por defecto cuando Crafty tiene uno configurado (captcha: false para desactivarlo), y el frontend debe mostrarlo con crafty.forms.captcha()
update_form form_id name, slug, description, fields, notification_emails solo cambian los campos enviados; fields/notification_emails se reemplazan
configure_visual_editor site_id environment (por defecto production), frontend_url, routes (page_pattern, home_slug, collections, fixed), render_mode (ssr/hybrid/static/unknown), upload_media_collection configuración del editor visual de un entorno; solo cambian las claves enviadas, null restablece el valor por defecto; solo con site_id únicamente lee; devuelve la configuración efectiva más llms_warnings (las advertencias del generador de llms.txt, ya que las rutas también le afectan)
configure_llms site_id environment (por defecto production), title, summary, language, excluded_collections (colecciones con ruta que se excluyen), included_collections (colecciones sin ruta que se incluyen), collection_languages solo administradores; misma semántica de lectura y actualización parcial que configure_visual_editor; solo cambian las claves enviadas, null restablece el valor por defecto; solo con site_id únicamente lee; devuelve la configuración efectiva, las advertencias de configuración y las primeras 40 líneas del llms.txt generado

| configure_assistant | site_id y al menos un ajuste | environment (por defecto production), enabled, whatsapp_enabled, whatsapp_number, whatsapp_label, contact_form, hours (weekdays/saturday/sunday → { open, close } o null), timezone, greeting, suggestions (máximo 3), tone (friendly/formal/technical), guidelines, privacy_url, appearance (name, color, font, corners, position, avatar_on_launcher) | solo cambian las claves enviadas, null restablece el valor por defecto; el avatar se sube desde el panel; devuelve la configuración efectiva y el status | | configure_store | site_id | environment (por defecto 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 | solo administradores; enabled: true requiere que la tienda esté en el plan y crea (o adopta) la colección productos y la URL de pago; solo cambian las claves enviadas; solo con site_id únicamente lee; devuelve la configuración, open, el resumen de productos y checkout_url. Consulta online-store.md | | list_orders | site_id | environment, status, payment_status, search, limit, page | requiere el permiso orders (lectura) | | get_order | site_id, number | environment | el pedido con sus líneas (id para update_order), fields, metadata, payment_url, next_statuses e historial | | create_order | site_id, customer, fulfillment, payment_method, items | environment, address, fields, metadata, coupon, locale, expected_total | permiso de escritura orders; origen mcp; devuelve el pedido y su payment_url | | update_order | site_id, number | environment, claves de edición (items, fulfillment, payment_method, customer, address, fields, metadata, coupon, coupon_below_minimum), notify, status, paid, payment_reference, internal_note | solo edita pedidos abiertos no pagados (los metadatos siempre); las líneas antiguas conservan su precio; dispara order.updated | | list_coupons | site_id | environment, status, search | cupones con status, uses, orders, sales, discounted; requiere el permiso coupons (lectura) | | get_coupon | site_id, code | environment | las reglas de un cupón (productos como slugs) y sus resultados | | 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 | crea o modifica por código; un cupón ya usado conserva su código y tipo; requiere escritura en coupons | | delete_coupon | site_id, code | environment | elimina un cupón no usado, pausa uno usado | | configure_bookings | site_id | environment (por defecto production), enabled, timezone, hours (combinadas por día de la semana), 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 | solo administradores; enabled: true requiere reservas en el plan y crea (o adopta) las colecciones servicios y profesionales; solo cambian las claves enviadas; solo con site_id únicamente lee; devuelve la configuración, open, los slugs de servicios y profesionales y checkout_url. Consulta online-bookings.md | | get_booking_availability | site_id | environment, service, professional, from, to | sin service: los servicios y profesionales reservables; con él: huecos libres por día local (hasta 31 días), cada uno con los profesionales libres | | list_bookings | site_id | environment, from, to, status, professional, search, metadata, limit, page | requiere el permiso de reservas (lectura) | | get_booking | site_id, number | environment | la reserva con next_statuses, internal_note, customer_url y events | | create_booking | site_id, service, start, customer | environment, professional, payment_method, fields, metadata, outside_hours, internal_note, locale | permiso de escritura de reservas; origen mcp; un hueco ocupado responde SLOT_UNAVAILABLE | | update_booking | site_id, number | environment, start, professional, outside_hours, status, paid, payment_reference, internal_note, metadata | mueve la reserva, cambia el estado o la marca como pagada; devuelve la reserva y applied | | set_professional_schedule | site_id, professional | environment, mode (shop/own), hours | sin mode solo lee; los horarios propios siempre quedan dentro de los de la tienda | | 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 (para eliminar) | bloquea tiempo para un profesional o para toda la tienda; create devuelve cuántas reservas ya caen dentro | | upsert_assistant_answer | site_id, question, answer | environment (por defecto production), answer_id | añade una respuesta fija o edita la que tiene answer_id; el asistente la usa desde el siguiente mensaje del visitante | | delete_assistant_answer | site_id, answer_id | environment (por defecto production) | elimina una respuesta fija | | upsert_redirects | site_id, redirects | environment (por defecto production), dry_run | hasta 500 { source, destination, status_code? } por llamada, emparejadas por source; source es una ruta que empieza por / (sin espacios, ? ni #; se admiten los splats * y los :placeholders de Cloudflare Pages), destination una ruta o una URL http(s) absoluta, status_code 301 (por defecto) / 302 / 307 / 308; los elementos no válidos aparecen en failed y se omiten. Crafty hosting las escribe en _redirects en el siguiente despliegue (después de las reglas propias del sitio; límite de 2000 estáticas + 100 dinámicas); los frontends personalizados leen GET /api/v1/redirects | | delete_redirect | site_id, source | environment (por defecto production) | elimina una redirección |

Las herramientas del asistente siguen la sección Asistente del panel: los administradores siempre, los editores con acceso de lectura pueden leer y los editores con acceso de escritura pueden modificar.

Las herramientas de redirección siguen el permiso Pages: leer requiere acceso a páginas, modificar requiere acceso de escritura.

Publicación (crafty:publish)

Herramienta Obligatorio Efecto
publish_page page_id estado → published, establece published_at
unpublish_page page_id estado → draft, borra published_at
publish_entry entry_id estado → published, establece published_at
unpublish_entry entry_id estado → draft, borra published_at; la entrada desaparece de la API pública y de las referencias
schedule_publication type (page/entry), id, y publish_at y/o unpublish_at define cuándo un borrador se publica solo y/o cuándo un elemento publicado vuelve a borrador; fechas ISO 8601 (sin desfase → la zona horaria regional del usuario), null borra una de ellas, unpublish_at debe ser posterior a publish_at; se aplica cada minuto
publish_form form_id estado → published (ahora acepta envíos públicos)
unpublish_form form_id estado → draft (deja de aceptar envíos)

Flujos de trabajo canónicos

Modelar contenido con tipos de bloque (recomendado)

  1. list_sites → elige un site_id (o create_site).
  2. Para cada sección del diseño, create_block_type (o get_block_type si ya existe). Usa un repeater para listas de elementos, stringList para listas de cadenas simples y reference / multiReference con model: "Media" para imágenes (súbelas primero); usa una colección solo para conjuntos grandes e independientes de registros.
  3. create_page en el idioma por defecto con blocks: [{ block_type_id, data }, …]; corrige los errores blocks.N.data.… y revisa warnings. Para otros idiomas, ejecuta configure_locales una vez y luego upsert_translation por página (bloques por id desde get_page), o translate_content para que la IA lo traduzca una vez que la traducción con IA esté habilitada para el workspace y el sitio (configure_translation).
  4. publish_page.
  5. Para cambiar un esquema más adelante, update_block_type con la lista COMPLETA de campos (usa force: true solo si aceptas dejar huérfanos los datos que los bloques ya contienen).

Crear y publicar una página

  1. list_sites → elige un site_id (o create_site).
  2. get_block_type / list_block_types → anota cada id y su esquema de datos.
  3. create_page con site_id, title y blocks: [{ block_type_id, data }, …].
  4. publish_page con el page_id devuelto.

Contenido estructurado

  1. list_collections (opcionalmente con site_id) → elige un collection_slug, o create_collection con sus fields (update_collection los cambia más adelante).
  2. create_entry con collection_slug, site_id, slug y data.
  3. update_entry para editar (cada edición guarda una revisión).

Formularios

  1. create_form con site_id, name, slug y fields.
  2. publish_form para que acepte envíos públicos.
  3. list_form_submissions / get_form_submission para leer lo que enviaron los visitantes.

Medios

  1. ¿Necesitas URL públicas permanentes? Primero manage_media_collection con site_id, key, is_public: true (si no, los archivos reciben URL firmadas con caducidad).
  2. Desde una URL o un archivo pequeño: upload_media con url, site_id y alt (opcionalmente collection_name, sha256). Desde un archivo local: calcula shasum -a 256 <file>, llama a request_media_upload (con alt) y luego ejecuta el comando curl -T devuelto.
  3. list_media para confirmar y obtener URL; asigna con update_media un texto alt a cada imagen de contenido que no lo tenga.

Migrar un sitio existente

Sigue migrate-to-crafty.md: rastrear → clasificar → modelar → import_media_from_urls → bulk_upsert_entries (borradores, primero dry_run) → upsert_redirects → revisión humana → publicar.

Editor visual

  1. Prepara el frontend (consulta visual-editor.md).
  2. configure_visual_editor con site_id, frontend_url, routes y render_mode.
  3. Pide al usuario que abra Editar sitio en el admin para confirmar que la edición al hacer clic funciona.

Gestión de la caché de respuestas de la API

La API pública tiene una caché de respuestas basada en etiquetas (habilitada por entorno). Las anulaciones por sitio tienen prioridad sobre la configuración global.

  • Desactiva la caché de un sitio antes de las ediciones masivas y vuelve a activarla al terminar:
    1. set_api_cache_status con site_id y enabled: false.
    2. Haz todos tus cambios de contenido (update_entry, update_page, etc.).
    3. set_api_cache_status con enabled: true.
    4. flush_api_cache con site_id (y opcionalmente resource) para borrar las entradas obsoletas.
  • Vacía solo un tipo de recurso después de cambios puntuales:
    • flush_api_cache con site_id y resource: "entries" tras actualizar entradas.
    • flush_api_cache con site_id y resource: "pages" tras actualizar páginas.
  • flush_api_cache sin resource (o con resource: "all") borra de una vez todas las respuestas en caché del sitio.

Solución de problemas

  • 401 / error de autenticación: el token Bearer falta, no es válido o ha caducado; vuelve a conectar o regenéralo. Cada solicitud a /mcp debe llevar un token válido.
  • Falta una herramienta o falla con Insufficient OAuth scope: el token no tiene el scope de esa herramienta. Las herramientas solo se anuncian si el token tiene el scope correspondiente (crafty:write para crear/editar, crafty:publish para publicar, crafty:submissions para leer envíos de formularios). Vuelve a emitir el token con los scopes que necesitas.
  • Tenant no encontrado: host incorrecto; usa el dominio exacto del tenant. Los endpoints de token de OAuth están en la raíz del tenant (/oauth/...), no bajo /mcp.
  • OAuth falla con un cliente creado manualmente: su tipo debe coincidir con el flujo (Authorization Code o Device Code), su URI de redirección debe ser exactamente la que usa la herramienta, y client_id/client_secret deben coincidir. Las herramientas que se registran automáticamente no necesitan cliente manual.
  • Token/secreto perdido: client_secret y los PAT se muestran solo una vez al crearlos; regénéralos si los pierdes.