feat(v3.13.0): settings live DB reads, remove Directus/Appwrite, admin stats

Settings fixes:
- MAX_SITES_PER_USER, USER_RATE_LIMIT_PER_MIN/HR now read from DB settings
  table (DB > ENV > default), so dashboard/settings changes apply without
  restart. Sync cache refreshed on every save or delete.
- /api/me reports the live DB value for max_sites_per_user.

Admin improvements:
- Admin users bypass per-user rate limiting entirely (role=admin or ADMIN_EMAILS).
- Admin Overview now shows platform stats: registered users, new users (7d),
  total user sites, available tools.

Plugin cleanup:
- Appwrite and Directus plugins removed from the active registry (8 plugins
  now: WordPress, WooCommerce, WordPress Specialist, Gitea, n8n, Supabase,
  OpenPanel, Coolify). Plugin code is retained for future re-enabling.
- Settings page plugin visibility list updated to match.

Mobile onboarding:
- Stepper steps on narrow viewports stack vertically with correct full border
  and rounded corners on each step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 23:33:20 +02:00
parent f203ca88de
commit 43fd2201a0
223 changed files with 36183 additions and 4115 deletions

View File

@@ -1,6 +1,6 @@
# Airano MCP Bridge
**Version:** 2.9.0
**Version:** 2.18.0
**Requires WordPress:** 5.0+
**Tested up to:** 6.9
**Requires PHP:** 7.4+
@@ -26,6 +26,14 @@ Installing **Airano MCP Bridge** unlocks a second tier of capabilities that Word
- **Capability probe** — tells MCP Hub what the calling application password can actually do
- **Audit hook** — forwards WordPress action events to MCP Hub with HMAC-signed webhooks
- **SEO meta routes** for Rank Math and Yoast
- **WordPress Specialist admin namespace** (v2.11.0+) — read-only inventory of plugins, themes, users, options, cron, maintenance status. Powers the `wordpress_specialist` MCP plugin so admins on shared hosting can use the same tools that previously required Docker socket + WP-CLI.
- **System diagnostics** (v2.12.0) — `system-info`, `phpinfo` (curated, not the full HTML dump), `disk-usage` (bounded directory walks).
- **Page editing** (v2.13.0) — Gutenberg block read/replace/insert/remove, Elementor detect/get/set/regen-css/template-list/template-apply, Classic-editor html replace. Writes are double-gated: `manage_options` route-level + per-post `edit_post` enforcement; raw HTML requires `unfiltered_html`; Elementor JSON capped at 5,000 nodes per call.
- **Theme dev surface** (v2.14.0) — install (from URL or base64 zip) / activate / delete + theme file CRUD (list, read, write, delete). All theme paths canonicalise via `realpath()` and reject any escape from the slug directory. PHP file writes additionally require `!DISALLOW_FILE_EDIT`. Optimistic concurrency via `expected_sha256` on writes. Per-call caps 5 MB/file, 1000 files/list, 50 MB/install zip. Refuses to delete the active theme (or its parent) and `style.css` of the active theme.
- **Plugin write management** (v2.15.0) — install (from wp.org slug, URL, or base64 zip) / activate / deactivate / update / delete. Per-route capability checks (`install_plugins`, `activate_plugins`, `delete_plugins`). Refuses to deactivate or delete the companion plugin itself (would brick the MCP connection) and refuses to delete active plugins. Update route returns `up_to_date:true` cleanly when no update is available.
- **Site config** (v2.16.0) — Settings → General + Reading + Permalinks panels via typed REST. `GET/POST /admin/site/identity` (title / tagline / site_icon / custom_logo), `GET/POST /admin/site/reading` (show_on_front / page_on_front / page_for_posts / posts_per_page / blog_public), `GET/POST /admin/permalinks` (structure + category_base + tag_base, with `flush_rewrite_rules()` after each write).
- **Site layout** (v2.17.0) — Settings → Menus + Appearance → Widgets + Customizer surfaces. Menus: `GET /admin/menus`, `GET /admin/menus/{id}`, `PUT /admin/menus/{id}` (full-replace items + optional rename, slug stays frozen, S-22 per-item cap dispatch). Widgets: `GET /admin/widgets/areas`, `GET /admin/widgets/{area_id}`, `PUT /admin/widgets/{area_id}` (block-kind areas accept any block raw HTML; legacy areas accept `text` widget settings only this round). Customizer: `POST /admin/customizer/changeset` with `{action: get|apply|discard}` — single-tool wrapper around the changeset queue; `apply` requires the `customize` cap (S-24).
- **Database inspection** (v2.18.0) — read-only DB introspection via typed routes that NEVER expose raw SQL. `GET /admin/db/size` (single `information_schema.TABLES` aggregation: database_bytes + table_count + row_count_estimate), `GET /admin/db/tables` (per-table breakdown), `POST /admin/db/search` (`WP_Query` wrapper with `s=$query`; sanitised + length-capped at 200 chars; S-25 keeps non-readable posts out via `WP_Query`'s own `posts_clauses`). Bundled with the wordpress_advanced sunset on the MCP Hub side: the deprecated WP-CLI/Docker-socket plugin was removed in F.19.3.2-.3; `wordpress_specialist` is now the only WP-management surface and has no Docker socket dependency.
All operations remain guarded by WordPress's own capability system — this plugin never bypasses auth.
@@ -63,6 +71,66 @@ The plugin works out of the box. There are no settings pages to configure; behav
| `/audit-hook` | GET / POST / DELETE | 2.7.0 | Configure a webhook that forwards WP action events to MCP Hub. HMAC-SHA256-signed, non-blocking |
| `/regenerate-thumbnails` | POST | 2.8.0 | Rebuild attachment sub-sizes via `wp_generate_attachment_metadata()` for a list of IDs or in paged batch mode |
### WordPress Specialist admin namespace (`airano-mcp/v1/admin/*`)
All routes require `manage_options`. They power the `wordpress_specialist` MCP plugin (companion-backed advanced WordPress management — no Docker socket required).
| Route | Method | Introduced | Purpose |
|---|---|---|---|
| `/admin/plugins` | GET | 2.11.0 | Every plugin known to WP with active/network-active/version/author/update flags |
| `/admin/themes` | GET | 2.11.0 | Installed themes with stylesheet/template/parent/block-theme/active flags |
| `/admin/users` | GET | 2.11.0 | Paginated user list (cap 200/call) with optional `role` and `search` filters |
| `/admin/options/{name}` | GET | 2.11.0 | Single option fetch with deny-list of credential-shaped keys (`*_secret`, `*_password`, `*_api_key`, `*_token`, `auth_*_key`, `auth_*_salt`) |
| `/admin/cron` | GET | 2.11.0 | Full cron table with epoch + ISO 8601 next_run, schedule slug, interval, args |
| `/admin/maintenance` | GET | 2.11.0 | `.maintenance` sentinel inspection (`enabled` / `started_at` / `stale`, 10-min stale threshold matches WP) |
| `/admin/system-info` | GET | 2.12.0 | WP/PHP/MySQL/server versions + memory + paths in one envelope |
| `/admin/phpinfo` | GET | 2.12.0 | Sorted extension list + curated ini whitelist + disabled functions + opcache state (structured JSON, not the full HTML dump) |
| `/admin/disk-usage` | GET | 2.12.0 | Bytes for uploads/plugins/themes + filesystem `disk_total/free/used` (bounded 200k files / 5s per tree, with `truncated:true` flag) |
| `/admin/blocks/replace` | POST | 2.13.0 | Replace post block tree (companion runs `serialize_blocks()`; max 200 blocks; `wp_kses_post` unless caller has `unfiltered_html`) |
| `/admin/blocks/insert` | POST | 2.13.0 | Insert one block at index N |
| `/admin/blocks/remove` | POST | 2.13.0 | Remove block at index N (returns the removed block for rollback) |
| `/admin/elementor/status` | GET | 2.13.0 | Elementor presence + version + Pro flag + supported post types; `installed:false` cleanly when absent |
| `/admin/elementor/{post_id}` | GET | 2.13.0 | Parsed `_elementor_data` (slash-stripped, JSON-decoded) |
| `/admin/elementor/{post_id}` | POST | 2.13.0 | Replace `_elementor_data`; validates id/elType/settings; capped at 5,000 nodes; fires `elementor/document/after_save` |
| `/admin/elementor/{post_id}/regen-css` | POST | 2.13.0 | Trigger per-post Elementor CSS regeneration |
| `/admin/elementor/templates` | GET | 2.13.0 | Saved Elementor templates (id/title/type/modified) |
| `/admin/elementor/templates/apply` | POST | 2.13.0 | Copy a saved template's `_elementor_data` into a target post |
| `/admin/classic/{post_id}/replace` | POST | 2.13.0 | Pure post_content swap for classic-editor sites (`wp_kses_post` unless caller has `unfiltered_html`) |
| `/admin/themes/install` | POST | 2.14.0 | Install theme from `zip_url` or base64 `zip_base64`; supports `activate`, `overwrite`. Runs WP core's `Theme_Upgrader`. Requires `install_themes` (+ `switch_themes` if `activate`). 50 MB cap. |
| `/admin/themes/{slug}/activate` | POST | 2.14.0 | Switch the active theme. Requires `switch_themes`. |
| `/admin/themes/{slug}` | DELETE | 2.14.0 | Delete an installed theme. Refuses if active or active parent. Requires `delete_themes`. |
| `/admin/themes/files/{slug}` | GET | 2.14.0 | Theme directory walk: each file's relative path, size, mime, sha256, modified_at. Optional `glob` + `max_files` (cap 1000). |
| `/admin/themes/files/{slug}/{path}` | GET | 2.14.0 | Read a theme file as base64 + sha256 + mime + modified_at. 5 MB cap. |
| `/admin/themes/files/{slug}/{path}` | PUT | 2.14.0 | Write a theme file (base64). Optional `expected_sha256` for optimistic concurrency. PHP writes require `!DISALLOW_FILE_EDIT`. Requires `edit_themes`. 5 MB cap. |
| `/admin/themes/files/{slug}/{path}` | DELETE | 2.14.0 | Delete a theme file. Refuses to delete `style.css` of the active theme. Requires `edit_themes`. |
| `/admin/plugins/install` | POST | 2.15.0 | Install via wp.org `slug`, OR via `zip_url`, OR via inline `zip_base64`. Supports `activate` + `overwrite`. Runs WP core's `Plugin_Upgrader`. Requires `install_plugins`. 50 MB zip cap. |
| `/admin/plugins/{slug}/activate` | POST | 2.15.0 | Activate an installed plugin. Optional `network_wide` for multisite. Requires `activate_plugins`. |
| `/admin/plugins/{slug}/deactivate` | POST | 2.15.0 | Deactivate. Refuses companion (S-20) and `Required` plugins (S-21). Requires `activate_plugins`. |
| `/admin/plugins/{slug}/update` | POST | 2.15.0 | Update to latest wp.org version. Returns `up_to_date:true` cleanly when no update available. Requires `update_plugins`. |
| `/admin/plugins/{slug}` | DELETE | 2.15.0 | Delete an installed plugin. Refuses active plugins, the companion, and `Required` plugins. Requires `delete_plugins`. |
| `/admin/site/identity` | GET | 2.16.0 | Site title / tagline / site_icon / custom_logo / charset / WP version / admin_email / language / siteurl. |
| `/admin/site/identity` | POST | 2.16.0 | Update title / tagline / site_icon_id / custom_logo_id (any subset). Attachment ids validated against the media library. |
| `/admin/site/reading` | GET | 2.16.0 | show_on_front / page_on_front / page_for_posts / posts_per_page / posts_per_rss / blog_public. |
| `/admin/site/reading` | POST | 2.16.0 | Update reading settings; page_on_front + page_for_posts must reference published Pages. |
| `/admin/permalinks` | GET | 2.16.0 | permalink_structure + category_base + tag_base. |
| `/admin/permalinks` | POST | 2.16.0 | Update permalink structure (empty string = plain). Calls `flush_rewrite_rules()` after the write. |
| `/admin/menus` | GET | 2.17.0 | Every nav menu with id / name / slug / theme locations / item count. |
| `/admin/menus/{menu_id}` | GET | 2.17.0 | One menu's items: `{id, title, type, object, object_id, parent, order, url, target, classes, xfn}`. |
| `/admin/menus/{menu_id}` | PUT | 2.17.0 | Full-replace items + optional rename. Slug frozen. Two-pass: every item validated against S-22 before any mutation; refusals return `forbidden_object_id` 403/404 and leave the menu untouched. |
| `/admin/widgets/areas` | GET | 2.17.0 | Every registered sidebar with id / name / theme_location / widget_count / kind (`block` or `legacy`). |
| `/admin/widgets/{area_id}` | GET | 2.17.0 | One area's widgets. Block-kind: `{id, type:'block', blocks:[parsed], raw}`. Legacy-kind: `{id, type, settings}`. |
| `/admin/widgets/{area_id}` | PUT | 2.17.0 | Full replace. Block areas accept any block raw HTML; legacy areas accept `text` widget settings only this round (other legacy types remain read-only). HTML sanitised via `wp_kses_post` unless caller has `unfiltered_html` (S-23). |
| `/admin/customizer/changeset` | POST | 2.17.0 | `{action: get|apply|discard}`. `apply` requires the `customize` cap on top of `manage_options` (S-24). Empty changeset returns `{status:'empty'}` 200. |
| `/admin/db/size` | GET | 2.18.0 | Aggregate database size from `information_schema.TABLES`: `{database_bytes, table_count, row_count_estimate, database_name, table_prefix}`. No SQL exposure. |
| `/admin/db/tables` | GET | 2.18.0 | Per-table breakdown: `{name, engine, rows, data_bytes, index_bytes, total_bytes, collation}` ordered by total_bytes desc. |
| `/admin/db/search` | POST | 2.18.0 | `WP_Query` wrapper. Body `{query, post_type?, status?, limit?}` (limit default 20, max 100). Sanitises + length-caps query at 200 chars (S-25). Non-readable posts filtered out via WP's own `posts_clauses`. Never raw SQL. |
Writes added in 2.13.0 require **two** capabilities: route-level `manage_options` AND per-post `edit_post` on the target (S-12 in MCP Hub's security ruleset). Raw-HTML pass-through requires the calling user to also hold `unfiltered_html`. Elementor JSON node count is capped at 5,000 per call (S-14); oversized payloads return `elementor_too_large` — switch to `/admin/elementor/templates/apply` instead.
Theme dev routes added in 2.14.0 layer per-route capability checks on top of `manage_options`: `install_themes`, `switch_themes`, `delete_themes`, `edit_themes`. PHP file writes additionally require `!DISALLOW_FILE_EDIT` (S-17). All file paths canonicalise via `realpath()` and must resolve under `wp-content/themes/{slug}` (S-16); attempts to escape return `path_invalid`. Optimistic concurrency via `expected_sha256` returns `sha_mismatch` (409) if the on-disk sha256 differs (S-19).
Plugin write routes added in 2.15.0 layer per-route capability checks on `manage_options`: `install_plugins` (install / update), `activate_plugins` (activate / deactivate), `delete_plugins` (delete). New rules: S-20 refuses to deactivate / delete the Airano MCP Bridge companion itself (returns `companion_self` 409 — would brick the MCP connection); S-21 refuses to deactivate / delete plugins whose header sets `Required: yes`. The single `POST /admin/plugins/install` route accepts three mutually-exclusive body shapes — `{slug}` for wp.org install, `{zip_url}` for download, `{zip_base64}` for inline — with the same 50 MB cap (S-18) on zip payloads. Delete refuses any plugin currently active (`plugin_active` 409 — caller must deactivate first).
### Authentication
All routes require **HTTP Basic** authentication with a WordPress **Application Password** (Users → Profile → Application Passwords). Per-route capability requirements:
@@ -74,6 +142,7 @@ All routes require **HTTP Basic** authentication with a WordPress **Application
| Capabilities probe | `read` (any logged-in user) |
| Bulk meta, Export | `edit_posts` (per-item `edit_post` check) |
| Cache purge, Transient flush, Site health, Audit hook, Regenerate thumbnails | `manage_options` |
| Admin namespace (`/admin/*`) | `manage_options` |
## Privacy
@@ -119,6 +188,38 @@ The `/upload-chunk` route bypasses PHP's `upload_max_filesize` (typically 264
## Changelog
### 2.18.0 — 2026-05-04
- **F.19.3.2-.3: Database inspection (read) + bulk fan-out (write) + wordpress_advanced sunset.** Three new admin routes — `GET /admin/db/size` (single `information_schema.TABLES` aggregation, returns `{database_bytes, table_count, row_count_estimate, database_name, table_prefix}` — no SQL exposure), `GET /admin/db/tables` (per-table breakdown with name / engine / rows / data_bytes / index_bytes / total_bytes / collation, ordered by total_bytes desc), `POST /admin/db/search` (body `{query, post_type?, status?, limit?}` — wraps `WP_Query` with `s=$query`, sanitises via `sanitize_text_field` and length-caps at 200 chars server-side, `limit` capped at 100; returns `{query, limit, count, hits}`). New security rule **S-25**: db/search wraps `WP_Query`, NEVER raw SQL; `WP_Query`'s own `posts_clauses` filter (the same gate the WP search page uses) keeps non-readable posts (private/draft from other authors) out of the result set. Bulk write tools on the MCP Hub side (`wp_bulk_post_update`, `wp_bulk_term_update`) ride stock REST `wp/v2/posts/{id}` and `wp/v2/{taxonomy}/{id}` — no companion routes needed for those, but the cap of 50 items per call is documented under **S-26** in the MCP Hub spec. All admin routes gated on `manage_options` route-level. **Bundled with the wordpress_advanced sunset on the MCP Hub side**: the deprecated WP-CLI/Docker-socket plugin was removed in F.19.3.2-.3 (2026-05-04); `wordpress_specialist` is now the only WP-management surface and has no Docker socket dependency.
### 2.17.0 — 2026-05-03
- **F.19.6.B: Site layout (menus + widgets + customizer).** Seven new admin routes — three for menus, three for widgets, one for the customizer changeset queue. Menus: `GET /admin/menus`, `GET /admin/menus/{id}`, `PUT /admin/menus/{id}` (body `{items:[...], name?}` — full replace, items not in the array are deleted, slug stays frozen so `theme_location` mapping survives). Widgets: `GET /admin/widgets/areas` (each area carries `kind: 'block'|'legacy'`), `GET /admin/widgets/{area_id}` (block-kind returns parsed block tree + roundtrip-safe `raw`; legacy-kind returns option-keyed settings), `PUT /admin/widgets/{area_id}` (block areas accept any block raw HTML; legacy areas accept `text` widget settings only this round — other legacy types remain read-only and return `unsupported_legacy_widget`; caller-side `kind` is ignored, area kind is determined by the area itself). Customizer: `POST /admin/customizer/changeset` with `{action: get|apply|discard}` — single-tool wrapper around the changeset queue, intentionally limited because most modern themes use FSE/site-editor instead of the customizer. New security rules: **S-22** dispatches per nav-menu item type — `post_type` checks `current_user_can('read_post', id)`; `taxonomy` allows public taxonomies and otherwise requires the taxonomy's `assign_terms` cap (deliberately NOT `manage_categories` which is a write cap and would refuse routine "add public Category X to footer" flows for editors who don't manage taxonomies); `custom` URL items skip the object validation. Refusals surface as `forbidden_object_id` 403/404. **S-23** sanitises widget HTML via `wp_kses_post` unless the caller has `unfiltered_html` (mirrors S-13 from F.19.5). **S-24** customizer apply requires the `customize` cap on top of `manage_options` — same bar as `/wp-admin/customize.php`. All routes gated on `manage_options` route-level. Pre-validates every menu item before mutating so a partial S-22 failure leaves the menu untouched. Customizer apply is racy with concurrent edits via the customizer UI — intentional, mirrors WP behaviour.
### 2.16.0 — 2026-05-03
- **F.19.6.A: Site config surface.** Six new admin routes covering Settings → General + Reading + Permalinks: `GET/POST /admin/site/identity` (title / tagline / site_icon / custom_logo), `GET/POST /admin/site/reading` (show_on_front / page_on_front / page_for_posts / posts_per_page / posts_per_rss / blog_public), `GET/POST /admin/permalinks` (permalink_structure + category_base + tag_base). All gated on `manage_options`. POST routes validate attachment ids against the media library, reject non-published page ids for page_on_front / page_for_posts, and route every option write through WP's own `sanitize_option_*` hooks. After a permalink_structure write `flush_rewrite_rules()` runs so the new structure takes effect immediately. First consumer of the `settings` tier introduced by F.19.2.0.
### 2.15.0 — 2026-05-02
- **F.19.2.1: Plugin write management.** Five new admin routes — `POST /admin/plugins/install` (accepts `{slug}` for wp.org install OR `{zip_url|zip_base64}` for arbitrary zip; supports `activate` + `overwrite`), `POST /admin/plugins/{slug}/activate`, `POST /admin/plugins/{slug}/deactivate`, `POST /admin/plugins/{slug}/update`, `DELETE /admin/plugins/{slug}`. Per-route capability checks: `install_plugins` (install / update), `activate_plugins` (activate / deactivate), `delete_plugins` (delete). New security rules: S-20 refuses to deactivate / delete the Airano MCP Bridge companion itself (returns `companion_self` 409 — operator must use WP-Admin → Plugins instead); S-21 refuses to deactivate / delete plugins marked `Required: yes` in their header. Reuses S-15 (slug whitelist via `get_plugins()`) + S-18 (50 MB cap on install zip). Refuses to delete an active plugin (caller must deactivate first). Update route returns `up_to_date:true` cleanly when no update is available. Multisite supports `network_wide` activation/deactivation (requires `manage_network_plugins` for activate).
### 2.14.0 — 2026-05-02
- **F.19.7: Theme dev surface (install + activate + delete + file CRUD).** Three theme management routes — `POST /admin/themes/install` (accepts either `zip_url` or inline `zip_base64`; runs WP core's `Theme_Upgrader`; supports `activate` + `overwrite`), `POST /admin/themes/{slug}/activate`, `DELETE /admin/themes/{slug}` — and four file CRUD routes — `GET /admin/themes/files/{slug}` (glob walk capped at 1000 files, returns `path/size/mime/sha256/modified_at` per entry), `GET /admin/themes/files/{slug}/{path}` (read as base64), `PUT /admin/themes/files/{slug}/{path}` (write with optional `expected_sha256` for optimistic concurrency and `create_dirs`), `DELETE /admin/themes/files/{slug}/{path}`. Per-route capability checks: `install_themes`, `switch_themes`, `delete_themes`, `edit_themes`. PHP file writes additionally require `!DISALLOW_FILE_EDIT` (S-17). All file paths canonicalise via `realpath()` and must stay under `wp-content/themes/{slug}` (S-16). Per-call caps: 5 MB/file, 1000 files/list, 50 MB/install zip (S-18). Optimistic concurrency on `expected_sha256` returns `sha_mismatch` (409) on drift (S-19). Refuses to delete the active theme, the active parent theme, or `style.css` of the active theme.
### 2.13.0 — 2026-05-01
- **F.19.5: Page editing surface (Gutenberg + Elementor + Classic).** Seven new write routes — `POST /admin/blocks/{replace,insert,remove}`, `POST /admin/elementor/{post_id}` (set), `POST /admin/elementor/{post_id}/regen-css`, `POST /admin/elementor/templates/apply`, `POST /admin/classic/{post_id}/replace` — and three new read routes — `GET /admin/elementor/status`, `GET /admin/elementor/{post_id}`, `GET /admin/elementor/templates`. Writes require `manage_options` AND per-post `edit_post` (S-12); block + classic content sanitised via `wp_kses_post` unless caller has `unfiltered_html` (S-13); Elementor JSON node count capped at 5,000 per call (S-14). All Elementor writes fire `elementor/document/after_save` so caches and CSS regenerate cleanly.
### 2.12.0 — 2026-05-01
- **F.19.3.1: System diagnostic ports from `wordpress_advanced`.** Three more read-only admin routes — `GET /admin/system-info`, `GET /admin/phpinfo`, `GET /admin/disk-usage` — replace the legacy WP-CLI / Docker-socket flow so admins on shared hosting can use those tools too. All gated on `manage_options`. `phpinfo` returns curated structured JSON (extension list + ini whitelist + opcache state), never the full `phpinfo()` HTML dump (which would leak server internals). `disk-usage` walks bounded at 200k files / 5s per tree.
### 2.11.0 — 2026-05-01
- **F.19.1: Read-only admin namespace.** Six new routes under `airano-mcp/v1/admin/*``plugins`, `themes`, `users`, `options/{name}`, `cron`, `maintenance` — gated on `manage_options`. `options` route uses a deny-list of credential-shaped suffixes on top of the cap check. `users` route paginates up to 200/call with optional `role` + `search` filters. No state changes; write operations land in F.19.2.
### 2.9.0 — 2026-04-16
- New: `POST /airano-mcp/v1/upload-and-attach` — raw-body upload + metadata (title / alt / caption / description) + attach-to-post + set-featured in a single REST round-trip. Saves 23 calls for every hero-image or product-image workflow. Per-target permission enforced via `current_user_can('edit_post', attach_to_post)`.
@@ -184,6 +285,22 @@ The `/upload-chunk` route bypasses PHP's `upload_max_filesize` (typically 264
## Upgrade notice
### 2.16.0
Adds the F.19.6.A site config surface (6 new admin routes — identity / reading / permalinks). All gated on `manage_options`. Permalink writes flush rewrite rules automatically. No breaking changes; existing routes unchanged.
### 2.15.0
Adds the F.19.2.1 plugin write management surface (5 new admin routes — install / activate / deactivate / update / delete). Per-route checks for `install_plugins` / `activate_plugins` / `delete_plugins`. Refuses to deactivate or delete the companion plugin itself. No breaking changes; existing routes unchanged.
### 2.14.0
Adds the F.19.7 theme dev surface (7 new admin routes — install / activate / delete + file CRUD). Per-route checks for `install_themes` / `switch_themes` / `delete_themes` / `edit_themes`; PHP file writes additionally require `!DISALLOW_FILE_EDIT`. No breaking changes; existing routes unchanged.
### 2.13.0
Adds the F.19.5 page editing surface (10 new admin routes — Gutenberg blocks, Elementor read/write/template, Classic html replace). Writes require `manage_options` AND per-post `edit_post`; raw-HTML pass-through is gated on `unfiltered_html`. No breaking changes; existing routes unchanged.
### 2.9.0
Adds `POST /airano-mcp/v1/upload-and-attach` — upload + metadata + attach + featured in one REST round-trip. No breaking changes to existing routes.

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,11 @@ Tags: mcp, ai, rest-api, seo, media
Requires at least: 5.0
Tested up to: 6.9
Requires PHP: 7.4
Stable tag: 2.9.0
Stable tag: 2.18.1
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/gpl-2.0.html
Companion plugin for MCP Hub. REST API routes for SEO meta and raw-binary media uploads that bypass upload_max_filesize.
Companion plugin for MCP Hub. REST API routes for SEO meta, raw-binary media uploads that bypass upload_max_filesize, and the WordPress Specialist admin namespace.
== Description ==
@@ -51,6 +51,78 @@ Companion plugin for MCP Hub. REST API routes for SEO meta and raw-binary media
* `POST /wp-json/airano-mcp/v1/regenerate-thumbnails` — Regenerate attachment sub-sizes via `wp_generate_attachment_metadata` for a list of IDs or in paged batch mode (added in 2.8.0)
* `POST /wp-json/airano-mcp/v1/upload-and-attach` — Raw-body upload + metadata + attach-to-post + set-featured in a single REST call (added in 2.9.0). Query params: `attach_to_post`, `set_featured`, `title`, `alt_text`, `caption`, `description`. Permission: `upload_files` + `edit_post` on the target post.
*WordPress Specialist admin namespace (`airano-mcp/v1/admin/*`, added in 2.11.02.18.0)*
All admin routes require `manage_options`. They power the `wordpress_specialist` MCP plugin (companion-backed advanced WordPress management — no Docker socket required).
Read routes (2.11.02.14.0):
* `GET /wp-json/airano-mcp/v1/admin/plugins` — Every plugin known to WP with active/network-active/version/author/update flags (2.11.0)
* `GET /wp-json/airano-mcp/v1/admin/themes` — Installed themes with stylesheet/template/parent/block-theme/active flags (2.11.0)
* `GET /wp-json/airano-mcp/v1/admin/users?role=&search=&page=&per_page=` — Paginated user list (cap 200/call) (2.11.0)
* `GET /wp-json/airano-mcp/v1/admin/options/{name}` — Single option fetch with deny-list of credential-shaped keys (`*_secret`, `*_password`, `*_api_key`, `auth_*_key`, etc.) (2.11.0)
* `GET /wp-json/airano-mcp/v1/admin/cron` — Full cron table with epoch + ISO 8601 next_run, schedule slug, interval, args (2.11.0)
* `GET /wp-json/airano-mcp/v1/admin/maintenance` — `.maintenance` sentinel inspection (`enabled` / `started_at` / `stale`) (2.11.0)
* `GET /wp-json/airano-mcp/v1/admin/system-info` — WP/PHP/MySQL/server versions + memory + paths in one envelope (2.12.0)
* `GET /wp-json/airano-mcp/v1/admin/phpinfo` — Sorted extension list + curated ini whitelist + disabled functions + opcache state (2.12.0)
* `GET /wp-json/airano-mcp/v1/admin/disk-usage` — Bytes for uploads/plugins/themes + filesystem `disk_total/free/used` (bounded 200k files/5s per tree) (2.12.0)
* `GET /wp-json/airano-mcp/v1/admin/elementor/status` — Elementor presence + version + Pro flag + supported post types; `installed:false` cleanly when absent (2.13.0)
* `GET /wp-json/airano-mcp/v1/admin/elementor/{post_id}` — Parsed `_elementor_data` (slash-stripped, JSON-decoded) (2.13.0)
* `GET /wp-json/airano-mcp/v1/admin/elementor/templates` — Saved Elementor templates (id/title/type/modified) (2.13.0)
* `GET /wp-json/airano-mcp/v1/admin/themes/files/{slug}?glob=&max_files=` — Theme directory walk (path/size/mime/sha256/modified per file; capped at 1000 files/call) (2.14.0)
* `GET /wp-json/airano-mcp/v1/admin/themes/files/{slug}/{path}` — Single theme file as base64 + sha256 + mime (5 MB cap) (2.14.0)
Page-editing write routes (2.13.0). All require `manage_options` AND `edit_post` on the target post (S-12); block + classic content is sanitised via `wp_kses_post` unless the caller has `unfiltered_html` (S-13); Elementor JSON node count capped at 5,000 per call (S-14).
* `POST /wp-json/airano-mcp/v1/admin/blocks/replace` — Replace post block tree; companion runs `serialize_blocks()` server-side (max 200 blocks)
* `POST /wp-json/airano-mcp/v1/admin/blocks/insert` — Insert one block at index N
* `POST /wp-json/airano-mcp/v1/admin/blocks/remove` — Remove block at index N (returns the removed block for rollback)
* `POST /wp-json/airano-mcp/v1/admin/elementor/{post_id}` — Replace `_elementor_data`; fires `elementor/document/after_save`
* `POST /wp-json/airano-mcp/v1/admin/elementor/{post_id}/regen-css` — Trigger per-post Elementor CSS regeneration
* `POST /wp-json/airano-mcp/v1/admin/elementor/templates/apply` — Copy a saved template into a target post
* `POST /wp-json/airano-mcp/v1/admin/classic/{post_id}/replace` — Pure post_content swap for classic-editor sites
Theme dev write routes (2.14.0). Per-route capability checks layer on top of `manage_options`: `install_themes` for install, `switch_themes` for activate, `delete_themes` for theme delete, `edit_themes` for file CRUD. PHP file writes additionally require `!DISALLOW_FILE_EDIT` (S-17). theme_slug must match `wp_get_themes()` (S-15). File paths canonicalise via `realpath()` and must stay under `wp-content/themes/{slug}` (S-16). Caps: 5 MB/file, 50 MB/install zip (S-18). Optimistic concurrency on `expected_sha256` returns `sha_mismatch` (409) on drift (S-19).
* `POST /wp-json/airano-mcp/v1/admin/themes/install` — Install theme from `zip_url` or inline `zip_base64`; supports `activate` + `overwrite`. Companion runs WP core's `Theme_Upgrader`.
* `POST /wp-json/airano-mcp/v1/admin/themes/{slug}/activate` — Switch the active theme.
* `DELETE /wp-json/airano-mcp/v1/admin/themes/{slug}` — Delete an installed theme (refuses if active or active parent).
* `PUT /wp-json/airano-mcp/v1/admin/themes/files/{slug}/{path}` — Write a theme file (base64 body, optional `expected_sha256`, optional `create_dirs`).
* `DELETE /wp-json/airano-mcp/v1/admin/themes/files/{slug}/{path}` — Delete a theme file (refuses to delete `style.css` of the active theme).
Plugin write management routes (2.15.0). Per-route capability checks layer on `manage_options`: `install_plugins` for install / update, `activate_plugins` for activate / deactivate, `delete_plugins` for delete. New security rules: S-20 refuses to deactivate / delete the Airano MCP Bridge companion itself (would brick the MCP connection); S-21 refuses to deactivate / delete plugins marked `Required: yes` in their header. Reuses S-15 (slug whitelist via `get_plugins()`) + S-18 (50 MB cap on install zip).
* `POST /wp-json/airano-mcp/v1/admin/plugins/install` — Install via wp.org `slug`, OR via `zip_url`, OR via inline `zip_base64` (mutually exclusive). Supports `activate` + `overwrite`. Companion runs WP core's `Plugin_Upgrader`.
* `POST /wp-json/airano-mcp/v1/admin/plugins/{slug}/activate` — Activate an installed plugin. Optional `network_wide` for multisite.
* `POST /wp-json/airano-mcp/v1/admin/plugins/{slug}/deactivate` — Deactivate. Refuses companion (S-20) and `Required` plugins (S-21).
* `POST /wp-json/airano-mcp/v1/admin/plugins/{slug}/update` — Update to latest wp.org version. Returns `up_to_date:true` cleanly when no update available.
* `DELETE /wp-json/airano-mcp/v1/admin/plugins/{slug}` — Delete. Refuses active plugins, the companion, and `Required` plugins.
Site config routes (2.16.0). All gated on `manage_options`. Writes route every option through WP's own `sanitize_option_*` hooks; permalink writes additionally call `flush_rewrite_rules()`.
* `GET /wp-json/airano-mcp/v1/admin/site/identity` — title / tagline / site_icon / custom_logo / charset / WP version / admin_email / language / siteurl
* `POST /wp-json/airano-mcp/v1/admin/site/identity` — Update title / tagline / site_icon_id / custom_logo_id (any subset). Attachment ids are validated against the media library.
* `GET /wp-json/airano-mcp/v1/admin/site/reading` — show_on_front / page_on_front / page_for_posts / posts_per_page / posts_per_rss / blog_public
* `POST /wp-json/airano-mcp/v1/admin/site/reading` — Update reading settings. page_on_front / page_for_posts must reference published Pages.
* `GET /wp-json/airano-mcp/v1/admin/permalinks` — permalink_structure / category_base / tag_base
* `POST /wp-json/airano-mcp/v1/admin/permalinks` — Update permalink structure (empty string = plain). Calls `flush_rewrite_rules()` after the write so the new structure takes effect immediately.
Site layout routes (2.17.0). All gated on `manage_options`. Per-request capability checks layer on top per the new security rules: S-22 dispatches per nav-menu item type (`post_type` checks `read_post`; `taxonomy` allows public taxonomies and otherwise requires `assign_terms`; `custom` URL items skip the object check); S-23 sanitises widget HTML via `wp_kses_post` unless caller has `unfiltered_html`; S-24 customizer apply requires the `customize` cap (not just `manage_options`).
* `GET /wp-json/airano-mcp/v1/admin/menus` — Every nav menu with id / name / slug / theme locations / item count.
* `GET /wp-json/airano-mcp/v1/admin/menus/{menu_id}` — One menu's items: `{id, title, type, object, object_id, parent, order, url, target, classes, xfn}`.
* `PUT /wp-json/airano-mcp/v1/admin/menus/{menu_id}` — Full-replace items + optional rename. Slug stays frozen so theme_location mapping survives. Two-pass: every item validated against S-22 before any mutation.
* `GET /wp-json/airano-mcp/v1/admin/widgets/areas` — Every registered sidebar with id / name / theme_location / widget_count / kind (`block` or `legacy`).
* `GET /wp-json/airano-mcp/v1/admin/widgets/{area_id}` — One area's widgets. Block-kind returns `{id, type:'block', blocks: [...parsed], raw: '<!-- wp:... -->'}`; legacy-kind returns `{id, type, settings: {...}}` keyed by the widget's option store.
* `PUT /wp-json/airano-mcp/v1/admin/widgets/{area_id}` — Full replace. Block areas accept `{raw}` (or `{blocks}` server-serialised); legacy areas accept `text` widget settings only this round (other legacy types remain read-only). Caller-side `kind` is ignored — area kind is determined by the area itself.
* `POST /wp-json/airano-mcp/v1/admin/customizer/changeset` — `{action: get|apply|discard}`. `get` returns the pending changeset; `apply` publishes it; `discard` trashes it. Empty changeset returns `{status: 'empty'}` 200.
Database inspection routes (2.18.0). All gated on `manage_options`. New security rule S-25: db/search wraps `WP_Query` with `s=$query` (NEVER raw SQL); query is sanitised via `sanitize_text_field` and length-capped at 200 chars; `WP_Query`'s own `posts_clauses` filter (the same gate the WP search page uses) keeps non-readable posts out of the result set.
* `GET /wp-json/airano-mcp/v1/admin/db/size` — Aggregate database size. Single `information_schema.TABLES` aggregation scoped to the WP table prefix; returns `{database_bytes, table_count, row_count_estimate, database_name, table_prefix}`. No SQL exposure: caller doesn't pick the query.
* `GET /wp-json/airano-mcp/v1/admin/db/tables` — Per-table breakdown. Same source as `db/size`, one row per WP table; returns `{database_name, table_prefix, tables: [{name, engine, rows, data_bytes, index_bytes, total_bytes, collation}]}` ordered by total_bytes descending.
* `POST /wp-json/airano-mcp/v1/admin/db/search` — Full-text search wrapper around `WP_Query`. Body `{query, post_type?, status?, limit?}` (limit default 20, max 100). Returns `{query, limit, count, hits: [{id, post_type, status, title, snippet, url, modified}]}`.
**Designed for [MCP Hub](https://github.com/airano-ir/mcphub)** — the AI-native management hub for WordPress and self-hosted services.
== Installation ==
@@ -92,6 +164,33 @@ Yes. All REST API endpoints work with any application that can make authenticate
== Changelog ==
= 2.18.1 =
* WordPress.org review fixes: drop the unused `wp-admin/includes/media.php` include from `/upload-chunk` and `/upload-and-attach` (only helpers from `file.php` and `image.php` are actually used). Split the `/upload-and-attach` permission_callback into its own method (`require_upload_and_attach_capability`) that explicitly checks `current_user_can('edit_post', $attach_to_post)` at the route gate when `attach_to_post` is supplied, and rejects `set_featured` without a target. The per-target check is no longer hidden inside the callback body.
= 2.18.0 =
* F.19.3.2-.3 — Database inspection (read) + bulk fan-out (write). Three new admin routes: `GET /admin/db/size` (single `information_schema.TABLES` aggregation: database_bytes + table_count + row_count_estimate, no SQL exposure), `GET /admin/db/tables` (per-table breakdown: name, engine, rows, data_bytes, index_bytes, total_bytes, collation), `POST /admin/db/search` body `{query, post_type?, status?, limit?}` wraps `WP_Query` with `s=$query` (NEVER raw SQL), `query` sanitised via `sanitize_text_field` and capped at 200 chars server-side, `limit` capped at 100. New security rule S-25 — db/search wraps `WP_Query`, never raw SQL; `posts_clauses` filter (same as the WP search page) keeps non-readable posts (private/draft from other authors) out of the result set. The bulk write tools (`wp_bulk_post_update`, `wp_bulk_term_update` on MCP Hub side) ride stock REST `wp/v2/posts/{id}` and `wp/v2/{taxonomy}/{id}` — no companion routes needed for those, but the cap of 50 items per call is documented under S-26 in the MCP Hub spec. All admin routes gated on `manage_options`. This release is bundled with the wordpress_advanced sunset on the MCP Hub side: the deprecated WP-CLI/Docker-socket plugin is removed in F.19.3.2-.3 (2026-05-04); `wordpress_specialist` is now the only WP-management surface.
= 2.17.0 =
* F.19.6.B — Site layout (menus + widgets + customizer). Adds seven new admin routes: `GET /admin/menus`, `GET /admin/menus/{id}`, `PUT /admin/menus/{id}` (full-replace items + optional rename, slug stays frozen), `GET /admin/widgets/areas`, `GET /admin/widgets/{area_id}`, `PUT /admin/widgets/{area_id}` (block areas accept any block raw HTML; legacy areas accept `text` widget settings only — other legacy types remain read-only this round), and `POST /admin/customizer/changeset` (single-tool wrapper around the changeset queue with `{action: get|apply|discard}`). New security rules: S-22 dispatches per nav-menu item type — `post_type` checks `current_user_can('read_post', id)`, `taxonomy` allows public taxonomies and otherwise requires the taxonomy's `assign_terms` cap (deliberately NOT `manage_categories` which is a write cap), `custom` URL items skip the object validation; refusals surface as `forbidden_object_id` 403/404. S-23 sanitises widget HTML via `wp_kses_post` unless the caller has `unfiltered_html` (mirrors S-13 from F.19.5). S-24 customizer apply requires the `customize` cap (same bar as `/wp-admin/customize.php`); `manage_options` alone is not enough. All routes gated on `manage_options`. Pre-validates every menu item before mutating so a partial S-22 failure leaves the menu untouched. Customizer apply is racy with concurrent edits via the customizer UI — intentional, mirrors WP behaviour.
= 2.16.0 =
* F.19.6.A — Site config surface. Six new admin routes covering Settings → General + Reading + Permalinks: `GET/POST /admin/site/identity` (title / tagline / site_icon / custom_logo), `GET/POST /admin/site/reading` (show_on_front / page_on_front / page_for_posts / posts_per_page / posts_per_rss / blog_public), `GET/POST /admin/permalinks` (permalink_structure / category_base / tag_base). All gated on `manage_options`. POST routes validate attachment ids against the media library, reject non-published page ids for page_on_front / page_for_posts, and route every option write through WP's own `sanitize_option_*` hooks. After a permalink_structure write `flush_rewrite_rules()` runs so the new structure takes effect immediately. First consumer of the `settings` tier introduced by F.19.2.0.
= 2.15.0 =
* F.19.2.1 — Plugin write management. Adds five new admin routes — `POST /admin/plugins/install` (accepts `{slug}` for wp.org install OR `{zip_url|zip_base64}` for arbitrary zip), `POST /admin/plugins/{slug}/activate`, `POST /admin/plugins/{slug}/deactivate`, `POST /admin/plugins/{slug}/update`, `DELETE /admin/plugins/{slug}`. Per-route capability checks: `install_plugins` (install / update), `activate_plugins` (activate / deactivate), `delete_plugins` (delete). New security rules: S-20 refuses to deactivate or delete the Airano MCP Bridge companion itself (would brick the MCP connection — operators must use WP-Admin → Plugins instead); S-21 refuses to deactivate / delete plugins marked `Required: yes` in their header (must-use plugins shipped by some managed hosts). Reuses S-15 (slug whitelist via `get_plugins()`) + S-18 (50 MB cap on install zip). Refuses to delete an active plugin (caller must deactivate first). Update route returns `up_to_date:true` cleanly when no update is available.
= 2.14.0 =
* F.19.7 — Theme dev surface (install + activate + delete + file CRUD). Adds three theme management routes — `POST /admin/themes/install` (accepts either `zip_url` or `zip_base64`; runs WP core's `Theme_Upgrader`), `POST /admin/themes/{slug}/activate`, `DELETE /admin/themes/{slug}` — and four file CRUD routes — `GET /admin/themes/files/{slug}` (glob walk capped at 1000 files), `GET /admin/themes/files/{slug}/{path}` (file read as base64), `PUT /admin/themes/files/{slug}/{path}` (file write with optional `expected_sha256` for optimistic concurrency), `DELETE /admin/themes/files/{slug}/{path}`. New security rules: `theme_slug` must match `wp_get_themes()` (S-15); paths canonicalise via `realpath()` and must stay under `wp-content/themes/{slug}`, blocking `..`, symlinks, absolute paths, null bytes (S-16); PHP file writes additionally require `!DISALLOW_FILE_EDIT` (S-17); per-call caps 5 MB/file, 1000 files/list, 50 MB/zip (S-18); optimistic concurrency via `expected_sha256` returns `sha_mismatch` (409) on drift (S-19). All routes gated on `manage_options` route-side; per-handler capability checks add `install_themes` / `switch_themes` / `delete_themes` / `edit_themes`. Refuses to delete the active theme, the active parent theme, or `style.css` of the active theme.
= 2.13.0 =
* F.19.5 — Page editing surface (Gutenberg + Elementor + Classic). Adds seven write routes — `POST /admin/blocks/{replace,insert,remove}`, `POST /admin/elementor/{post_id}` (set), `POST /admin/elementor/{post_id}/regen-css`, `POST /admin/elementor/templates/apply`, `POST /admin/classic/{post_id}/replace` — and three new read routes — `GET /admin/elementor/status`, `GET /admin/elementor/{post_id}`, `GET /admin/elementor/templates`. Writes require `manage_options` AND `edit_post` on the target post (S-12). Block + classic content is sanitised via `wp_kses_post` unless the caller has `unfiltered_html` (S-13). Elementor JSON node count capped at 5,000 per call; oversized payloads return `elementor_too_large` (S-14). All Elementor writes fire `elementor/document/after_save` so caches and CSS regenerate cleanly.
= 2.12.0 =
* F.19.3.1 — Three more read-only admin routes ported from the legacy `wordpress_advanced` WP-CLI tools so they can run via the companion instead of `docker exec`. `GET /admin/system-info` (PHP/MySQL/WordPress versions + server software + memory limits), `GET /admin/phpinfo` (extension list + curated ini snapshot — never the full `phpinfo()` HTML which would leak server internals), `GET /admin/disk-usage` (uploads/plugins/themes/total bytes + `disk_free_space`, bounded at 200k files / 5s wall clock per tree). All gated on `manage_options`.
= 2.11.0 =
* F.19.1 — New read-only admin namespace `airano-mcp/v1/admin/*` for the WordPress Specialist tool surface. Six new routes — `plugins`, `themes`, `users`, `options/{name}`, `cron`, `maintenance` — all gated on `manage_options`. `options` route uses a deny-list of credential-shaped suffixes (`*_secret`, `*_password`, `*_api_key`, `*_token`, `auth_*_key`, `auth_*_salt`) on top of the cap check. `users` route paginates up to 200/call with optional `role` + `search` filters. No state changes in this version; write operations land in F.19.2.
= 2.9.0 =
* Added `POST /wp-json/airano-mcp/v1/upload-and-attach` — same raw-body semantics as `/upload-chunk`, but accepts query params (`attach_to_post`, `set_featured`, `title`, `alt_text`, `caption`, `description`) and applies them in one PHP round-trip. Saves 2-3 REST calls per upload. Per-target permission enforced via `current_user_can('edit_post', attach_to_post)`.
* `status` + `capabilities` routes now advertise `upload_and_attach: true` alongside the existing capability flags.
@@ -146,6 +245,33 @@ Yes. All REST API endpoints work with any application that can make authenticate
== Upgrade Notice ==
= 2.18.1 =
WordPress.org review fixes only — no behaviour changes. Drops a redundant core file include and tightens the `/upload-and-attach` permission gate so the per-target `edit_post` check runs before the callback (visible to static analysis).
= 2.18.0 =
Adds the F.19.3.2-.3 database inspection surface (3 new admin read routes — db/size / db/tables / db/search) wrapping `information_schema.TABLES` + `WP_Query`. NEVER exposes raw SQL (S-25 — query is sanitised and length-capped). All gated on `manage_options`. No breaking changes; existing routes unchanged.
= 2.17.0 =
Adds the F.19.6.B site layout surface (7 new admin routes — menus / widgets / customizer changeset). New security rules S-22 (per-item caps for nav-menu object refs), S-23 (`wp_kses_post` on widget HTML unless `unfiltered_html`), S-24 (customizer apply requires `customize` cap). No breaking changes; existing routes unchanged.
= 2.16.0 =
Adds the F.19.6.A site config surface (6 new admin routes — identity / reading / permalinks). All gated on `manage_options`. Permalink writes flush rewrite rules automatically. No breaking changes; existing routes unchanged.
= 2.15.0 =
Adds the F.19.2.1 plugin write management surface (5 new admin routes — install / activate / deactivate / update / delete). Per-route checks for `install_plugins` / `activate_plugins` / `delete_plugins`. Refuses to deactivate or delete the companion plugin itself. No breaking changes; existing routes unchanged.
= 2.14.0 =
Adds the F.19.7 theme dev surface (7 new admin routes — install / activate / delete + file CRUD). Per-route checks for `install_themes` / `switch_themes` / `delete_themes` / `edit_themes`; PHP file writes additionally require `!DISALLOW_FILE_EDIT`. No breaking changes; existing routes unchanged.
= 2.13.0 =
Adds the F.19.5 page editing surface (10 new admin routes — Gutenberg blocks, Elementor read/write/template, Classic html replace). Writes require `manage_options` AND `edit_post` on the target post; raw HTML is gated on the caller also holding `unfiltered_html`. No breaking changes; existing routes unchanged.
= 2.12.0 =
Adds three more read-only admin routes (`/admin/system-info`, `/admin/phpinfo`, `/admin/disk-usage`). They replace the legacy WP-CLI / Docker-socket flow used by the `wordpress_advanced` MCP plugin so admins on shared hosting can use those tools too. No breaking changes.
= 2.11.0 =
Adds the `airano-mcp/v1/admin/*` namespace (six read routes) for the new WordPress Specialist MCP tool surface. No breaking changes; existing routes unchanged. Activate the plugin and any admin Application Password gets the new routes.
= 2.9.0 =
Adds `POST /airano-mcp/v1/upload-and-attach` — upload + metadata + attach + featured in one REST round-trip. No breaking changes.