# CLAUDE.md — ContentAI module

Guidance for working in `modules/ContentAI`. This is a Laravel-style module (registered as a service provider), not a legacy auto-discovered module.

## Purpose

Exposes the site's content blocks to external AI tools over the Model Context Protocol (MCP). An AI client (e.g. Claude) connects to the site's MCP endpoint and can read a block's field definitions and update the field values of a placed block instance. Editorial content stays editable through a standard, tool-driven interface instead of ad-hoc scripting.

The module also provides an admin **AI Settings** page (under Content, falling back to Administration) where a user toggles the MCP server on/off and views connection details.

## Registration

- Registered as `Mtc\Modules\ContentAI\Providers\ContentAIServiceProvider` in `config/app.php` (~line 188).
- The provider's `boot()` loads `routes/ai.php`, `routes/web.php`, and the module migrations. It does **not** register anything in `register()`.
- Depends on the `laravel/mcp` package (`^0.9.1`) and on the `BlockRegistry` module (`Mtc\Modules\BlockRegistry`) for block data.

## How it works

### MCP server exposure (`routes/ai.php`)
- The MCP web route is registered **only when enabled**: it checks `content_ai_settings` exists and `Setting::isMcpEnabled()` is true (wrapped in try/catch so a missing table before migration cannot fatal boot).
- When enabled, `Mcp::web('/mcp/block-registry', BlockRegistryServer::class)` mounts the server at that path over Streamable HTTP.
- Because exposure is decided at route-registration time, toggling the setting takes effect on the next request cycle / route cache rebuild, not mid-request.

### The MCP server (`Mcp/Servers/BlockRegistryServer.php`)
- `BlockRegistryServer` extends `Laravel\Mcp\Server`. Path constant `PATH = 'mcp/block-registry'`.
- Metadata (name, version, instructions) comes from PHP attributes (`#[Name]`, `#[Version]`, `#[Instructions]`).
- Registers two tools: `ReadBlockDataTool`, `UpdateBlockDataTool`.
- `metadata()` is a static helper that reflects over the class attributes and tool list to build the payload shown on the admin settings page (server name/version/instructions/path + each tool's name/title/description). Keep it in sync with the `$tools` array.

### Tools (`Mcp/Tools/`)
- **ReadBlockDataTool** — input `block` (numeric id or slug, e.g. `"faq"`). Resolves via `BlockRegistry::find()` or `->where('slug', ...)`. Returns the block's field definitions via `BlockSyncService::blockToArray()`. This reads the **block definition**, not a placed instance.
- **UpdateBlockDataTool** — input `id` (a placed `EntityBlock` instance id, the number in `/admin/blocks/data/{id}`), `fields` (map of field name => value), optional `status` (`draft`/`published`). Behaviour:
  - Loads the `EntityBlock` with `block.fields.subFields`.
  - Rejects any field name not defined on the block, and tells the caller to run the read tool for `{slug}` to see valid fields.
  - **Partial update**: starts from existing `data` and overlays only supplied fields, so omitted fields are preserved.
  - Coerces each value to its on-disk shape via `coerceValue()` per field type (checkbox → `'1'`/`'0'`, repeater → JSON array, image/file/link → JSON object or string).
  - `save()` fires the model saved event, which clears the block render cache (`EntityBlockService::clearCache`).
  - **Known limitation**: image crop / srcset processing (`ImageProcessingService`) is **not** applied — image values are stored as-is.

> Note the two id spaces: the read tool takes a **block definition** (id or slug); the update tool takes a **placed instance** (`EntityBlock` id). Don't conflate them.

### Admin settings

- **Controller** `Http/Controllers/Admin/AiSettingsController.php`
  - `index()` renders `templates/admin/ai_settings/index.twig` with enabled state, the MCP update URL, and `BlockRegistryServer::metadata()` merged with `endpoint` (full URL), `transport`, and `enabled`.
  - `updateMcp()` persists the toggle via `Setting::put(MCP_ENABLED, ...)` and returns JSON.
- **Routes** `routes/web.php` — `web` + `auth` middleware, `admin` prefix:
  - `GET  admin/ai-settings` → `admin-ai-settings`
  - `POST admin/ai-settings/mcp` → `admin-ai-settings-mcp`
- **Model** `Models/Setting.php` — key/value store on `content_ai_settings`. Constant `MCP_ENABLED = 'mcp_enabled'`. Static `get()`/`put()`/`isMcpEnabled()` helpers.
- **Frontend** — Twig mounts the Vue 2 component `<ai-content-settings>` inside `.js-vue-environment`, passing props as attributes (`:mcp-details="{{ mcp | json_encode }}"`). The component is registered globally in `admin/js/admin_app.js` and built by Vite (loaded via `vite_asset('modules/ContentAI/admin/js/admin_app.js')`).
  - `src/admin/components/AIContentSettings.vue` — tabbed UI (MCP + placeholder Agent tab). Toggling posts to the MCP update URL with optimistic UI and rollback on failure. Shows endpoint, server metadata, tool list, and a copy-ready client config JSON block.
  - Styles in `admin/css/ai_settings.less`.

## Database

Two migrations (`database/migrations/`):
- `..._create_content_ai_settings_table` — `content_ai_settings` (id, unique `key`, nullable `value`, timestamps).
- `..._add_ai_settings_admin_menu_item` — inserts the **AI Settings** admin menu item (icon `fa fa-magic`) under the Content section (falls back to Administration). Idempotent: skips if the path already exists; reversible in `down()`.

## Conventions / gotchas

- Laravel module conventions apply (ServiceProvider, Eloquent, standard routing), not the legacy hooks/loader pattern.
- Templates are Twig, rendered via the `template()` helper — not Blade.
- Adding a tool: create it under `Mcp/Tools/`, add it to `BlockRegistryServer::$tools`; it then appears automatically in the admin connection details via `metadata()`.
- The MCP endpoint is unauthenticated at the route level (exposure is gated only by the enabled toggle). Treat enabling it as opening a content-write surface; keep it off unless a client connection is actually needed.
