# AgenticWP — WordPress Management via MCP

This project has an MCP server (AgenticWP) connected that provides 35+ tools for managing a WordPress site. Use these tools instead of curl, WP-CLI, or manual REST API calls.

## Available MCP tools

### Core
- `get_wp_status` — Site version, theme, plugins, PHP info
- `get_option` / `update_option` / `delete_option` — Read/write wp_options (returns `exists: false` for missing options)
- `get_astra_settings` / `update_astra_settings` — Astra theme customizer (merge-update). ⚠️ Schema-strict keys (see Gotchas) fatally crash the frontend on render — prefer `patch_css` for color/typography overrides.
- `flush_cache` — Object cache, Elementor CSS, rewrite rules

### Post meta
- `get_postmeta` — Get single key or all meta for a post/page
- `update_postmeta` / `delete_postmeta` — Single key CRUD
- `update_postmeta_bulk` — Set multiple keys on one post in one call (ideal for RankMath SEO)
- `update_postmeta_multi` — Set meta on MANY posts in one call (batch SEO updates across pages)

### Term meta (categories, tags, attributes)
- `get_termmeta` — Get single key or all meta for a taxonomy term
- `update_termmeta` / `delete_termmeta` — Single key CRUD
- `update_termmeta_bulk` — Set multiple keys on one term (ideal for RankMath category SEO)

### Media
- `media_upload` — Upload from local file path or URL (auto base64 for local files)
- `media_set_alt` / `media_set_alt_bulk` — Set alt text on attachments

### Elementor (prefer fine-grained tools to save tokens)
- `get_elementor_outline` — Lightweight page tree (use this first)
- `get_elementor_element` / `update_elementor_element` — Read/edit single element by ID
- `elementor_replace` — Find/replace text across all widgets on a page
- `get_elementor_page` / `update_elementor_page` — Full page read/write (large, use sparingly)
- `create_elementor_page` — Create new page with Elementor enabled
- `build_elementor_section` — Generate elements from templates (hero, CTA, products grid, etc.)
- `list_elementor_pages` — List pages (default), templates, or all. Use `type` param to filter.
- `list_elementor_widgets` / `get_elementor_components`

### Generic REST passthrough
- `wp_rest_call` — Any `/wp/v2/*` endpoint (pages, posts, media, users, taxonomies, menus)
- `wc_rest_call` — Any `/wc/v3/*` endpoint (products, orders, customers, coupons, shipping)

### Specialized helpers
- `find_pages` — Search pages by slug, title, parent, status
- `find_products` — Search WooCommerce products by name, SKU, category, brand, or product attribute (e.g., pa_znamka)
- `add_menu_item` — Add item to a WordPress nav menu
- `bulk_update_products` — Update many WooCommerce products in one call

## How to use these tools

1. **Always start with `get_wp_status`** to understand the site (theme, plugins, version).
2. **For Elementor pages**: use `get_elementor_outline` first, then `get_elementor_element` for specific widgets. Only use `get_elementor_page` if you need the entire page structure.
3. **For SEO**: use `update_postmeta_bulk` with keys like `rank_math_title`, `rank_math_description`, `rank_math_focus_keyword`.
4. **For Elementor edits**: `update_elementor_element` with `settings` merges into existing settings — you only need to send the keys you want to change.
5. **After Elementor updates**: the endpoint auto-renders HTML/CSS via `Document::save()`. No manual cache flush needed.
6. **For anything not covered**: use `wp_rest_call` or `wc_rest_call` as a generic passthrough.

## Working style — defaults, tools, anti-patterns

### Always on (defaults — apply these on every interaction)

1. **Verify before claiming.** Call the tool and show what came back; don't describe what an endpoint returns from training-data memory. AgenticWP ships fast — behavior may have changed since you were trained.
2. **Cheapest read first.** `get_elementor_outline` before `get_elementor_page`. `get_elementor_element` when you need one widget. The full-page read is 50–100× more context than the outline, and most tasks don't need it.

### Tools (activate only when the trigger matches — don't apply by default)

| Tool / pattern | Trigger | Don't activate when |
|---|---|---|
| `diff_elementor_element` before `update_elementor_element` | Change touches `widgetType`, container structure, or an unfamiliar widget | Updating a single known scalar setting (color, text) on a heading/text widget |
| Backup check before bulk write | Before `bulk_update_products` / `bulk_update_pages` / `update_postmeta_multi` on production data, or any call that touches >5 items | Scoped to a single test surface the user just created |
| `restore_elementor_revision` | After a destructive edit went wrong — call `list_elementor_revisions` first to find the pre-change `revision_id` | Element IDs misremembered (use `get_elementor_outline` to re-discover) |
| `patch_css` (named block, atomic) | ANY change to `astra-settings.custom-css`. Markers are `/* === BEGIN:name === */ … /* === END:name === */`. Modes: `replace_or_append` (default, idempotent), `append`, `replace`, `remove` | Never — never read-modify-write `custom-css` directly. That race-corrupts comma-joined selectors |
| Lock-aware sequencing | Multiple Elementor mutations on the same `page_id` (insert/update/replace/restore) | Different pages — those don't contend |
| `get_debug_log` | A save silently fails or returns an unexpected error. Filter by `level` (`error`, `warning`, `notice`) and optional `since` timestamp | The site doesn't have `WP_DEBUG_LOG = true` (response will be `exists: false` — that's not a tool failure, that's the answer) |
| `seo_audit` with `missing_only=true` | Before a bulk SEO sweep — returns only items with at least one missing field, so you don't touch what's already correct | One-off SEO read on a single known post |
| `update_elementor_page` | Restructuring the whole page (re-ordering top-level containers, large bulk-add). Payload limit 500KB | Editing a single widget — use `update_elementor_element`, 50–100× cheaper |

### Anti-patterns

- **Don't simulate API responses.** "This tool returns `{success: true, …}`" with no actual call is hallucinating. Call it.
- **Don't bulk-mutate production data without first scoping to a test surface.** Run the same operation on one item, verify the write shape, then scale.
- **Don't combine save + outbound HTTP in one WP `sanitize_callback`.** Known deadlock pattern from the licensing system — applies to any customization that does the same thing.
- **Don't try to work around the scope boundaries** (PHP edits / shell / raw SQL / plugin install) by squeezing them through a REST passthrough or chaining tool calls. Tell the user the right tool (Bash/SSH/wp-cli/Edit) and stop.
- **Don't add frontend hooks** in any AgenticWP customization (`wp_head`, `the_content`, `template_redirect`). The plugin is intentionally invisible to visitors — extensions should preserve that.

## Common tasks

```
"Set the RankMath SEO title on page 42 to 'About Us | Brand'"
→ update_postmeta(post_id=42, key="rank_math_title", value="About Us | Brand")

"Update the hero heading on the homepage"
→ find_pages(slug="home") → get_elementor_outline(page_id=X) → update_elementor_element(page_id=X, element_id=Y, settings={"title": "New Heading"})

"Bulk update alt text on product images"
→ media_set_alt_bulk(items=[{media_id: 123, alt_text: "..."}, ...])

"Find all draft pages"
→ find_pages(status="draft")

"Get all products in category 15"
→ find_products(category_id=15)

"Find all Honda products"
→ find_products(attribute="pa_znamka", attribute_term="honda")

"Set RankMath SEO title on product category 42"
→ update_termmeta(term_id=42, key="rank_math_title", value="Category Title | Brand")

"Bulk update SEO on 3 pages at once"
→ update_postmeta_multi(updates=[{post_id: 10, meta: {"rank_math_title": "..."}}, {post_id: 20, meta: {"rank_math_title": "..."}}, ...])
```

## Important

- The plugin has **zero frontend hooks** — it never modifies what visitors see.
- All endpoints require `manage_options` capability (admin user).
- Protected options (`siteurl`, `home`, `admin_email`, `active_plugins`, `template`, `stylesheet`) cannot be updated or deleted.

## What AgenticWP deliberately can't do (by design)

These are **intentional non-features** — part of the plugin's security posture and product scope. Don't try to work around them by wrapping shell/curl calls in your response; instead tell the user the right tool for the job.

| Out of scope | Use instead |
|---|---|
| Edit PHP files (`functions.php`, plugin files, theme files, `wp-config.php`) | Claude Code's `Edit` / `Write` tools + file path on the user's local dev machine |
| Install, activate, or delete plugins; switch themes | `wp-cli` over SSH, or WP Admin → Plugins |
| Write to the filesystem (except WP media library via `media_upload`) | Claude Code `Write` on local path, or SFTP |
| Host / server config (SSL, nginx / Apache, `.htaccess`, PHP version) | Host dashboard, SSH, or the user's DevOps |
| Raw SQL queries / direct DB access | `wp-cli db` over SSH (with user approval) |
| Create / promote admin users beyond the WP REST `/users` endpoint | WP Admin → Users, or `wp-cli user create` |
| Shell / SSH / cron jobs | Claude Code's `Bash` tool |
| Build tooling (bundlers, preprocessors, React/Vue theme generation) | Claude Code + local build scripts |

The plugin's tools are scoped to **WordPress runtime state**: options, post/term meta, Elementor data, Astra settings, media, menus, WooCommerce products, cache, debug log. That's deliberate. If the user asks for a feature on the left-hand column, recommend the right-hand column and don't try to squeeze it through a REST passthrough.

## Gotchas

### SVG `<use>` creates a shadow DOM — external CSS can't reach elements inside `<defs>` reliably
If you define an `<g id="thing">` in `<defs>` and reference it via `<use href="#thing"/>`, CSS animations targeting classes *inside* the symbol (`.inner-class { animation: ... }`) often silently fail in Chrome/Safari — the animation applies to the shadow instance, but browsers don't honor `transform-origin`, CSS `transform`, or `fill-opacity` keyframes on those elements consistently. Symptom: "my animation does nothing" on elements that look correct in DevTools.

**Fixes** (pick the cheapest that works):
1. **Inline the markup** — copy the group out of `<defs>` and paste it 8 times. Verbose but bulletproof.
2. **Extract just the animated layer** — keep the static part in `<defs>` as a symbol, but render the animated element (halo, needle, LED) as a standalone sibling `<circle>`/`<line>` in the main SVG. CSS reaches it directly.
3. **Wrap in translated `<g>` + put `<use>` at local (0,0)** — if the issue is `transform-origin: center` not honoring each instance's position. Structure: `<g transform="translate(x y)"><g class="spin"><use href="#sym"/></g></g>` — rotate the inner `<g>`, not the `<use>`.

### `<use x="200" y="130">` + CSS `transform-origin: center` rotates about SVG (0,0), not the use's center
`transform-box: fill-box` + `transform-origin: center` on `<use>` elements is unreliable. The rotation pivots about the SVG viewport origin, producing giant orbital sweeps instead of in-place spin. Use the wrapper-translate pattern (fix #3 above).

### Elementor widget inside flex container collapses to 0×0
If you insert a decorative HTML widget into a flex-column container (like a hero), the widget gets `width: auto; height: auto` and its absolutely-positioned children become 0×0. Fix: force the widget itself to `position: absolute; inset: 0; width: 100%; height: 100%;` so its children have a sized parent to inset against.

### Nested `.e-con .e-con` adds 10px side padding by default (Elementor)
When laying out page sections: top-level `.e-con` gets your intended `padding: 0 24px`, but any nested `.e-con` (columns, inner containers) adds its own default `padding: 0 10px` on top, creating a 34px combined gutter on mobile. Zero out nested container padding per-page when matching service-page gutter widths.

### `position: fixed` elements escape `body { overflow-x: hidden }` but NOT `html { overflow-x: clip }`
If a fixed-positioned element (off-canvas drawer, modal) renders off-screen right of the viewport via `transform: translateX(100%)`, `document.scrollWidth` inflates and a phantom white strip appears at narrow widths — `body { overflow-x: hidden }` clips the scrollbar but doesn't clip the width measurement. Fix on `<html>`: `overflow-x: clip; max-width: 100vw;` (with `overflow-x: hidden` fallback via `@supports not (overflow-x: clip)` for old Safari).

### `update_astra_settings` — schema-strict keys fatally crash the frontend on render
Some `astra-settings` keys have undocumented but **strict structural schemas**. Astra's frontend renderer iterates them with hard assumptions about shape (numeric arrays of strings, fixed nested zone names). If you write the wrong shape, save returns `200` — the crash happens on the **next page-render request**, when Astra's PHP either does `Array to string conversion`, calls a method on a non-object, or hits an undefined index in PHP 8 strict mode. Symptom: every visitor (including wp-admin) sees "Na spletišču je prišlo do kritične napake."

**Known dangerous keys** (do not write blind via `update_astra_settings`):

- `global-color-palette`
- `header-desktop-items`, `header-mobile-items`
- `footer-desktop-items`, `footer-mobile-items`
- `theme-color-meta`

**Recovery if you've already crashed the site:** SSH/SFTP `wp-config.php` → temporarily set `define('WP_DEBUG', false);` won't help. You must either (a) `wp option delete astra-settings` via wp-cli, then re-import, or (b) DB-edit `wp_options` row `astra-settings` back to a known-good serialized blob. There's no rollback from inside `update_astra_settings` because the crash is in a future request.

**Right way to make these changes:**

1. **Colors** → `patch_css` with CSS custom properties:
   ```css
   :root {
     --ast-global-color-0: #0895cc;
     --ast-global-color-1: #0678a8;
   }
   ```
   Astra reads these tokens at render time. Override-safe, no schema risk.

2. **Header/footer layout** → don't try to set the items map programmatically. Use WP Customizer (`/wp-admin/customize.php?autofocus[panel]=panel-header-builder`) once, then leave it alone.

3. **If you genuinely must write a strict-schema key**: first call `get_astra_settings()`, copy the **exact** existing shape, modify the value-not-the-shape, and write back. Compare the diff before saving.

### `global-color-palette` canonical shape (Astra 4.x)
For reference when reading/writing this specific key. Astra expects:

```php
[
  'currentPalette' => 'palette_1',
  'palettes' => [
    'palette_1' => [
      '#0274BE', '#3A3A3A', '#3A3A3A', '#3A3A3A',
      '#FFFFFF', '#F5F5F5', '#FFFFFF', '#777777', '#000000'
    ],
    'palette_2' => [...],
    'palette_3' => [...],
  ],
  'flat-palette' => [...],
  'presets' => [...],
]
```

`palettes[N]` is a **flat array of hex strings**, indexed 0..8 (slot positions: primary, secondary, text, headings, base, surface, content-bg, border, dark). It is NOT a list of `{color, slug, name}` objects — that shape will fatal Astra on render.

### Astra `.ast-builder-grid-row` has a max-width but doesn't auto-fill it — widens pop up wrong on non-Elementor pages
Astra Header/Footer Builder rows render as `display: grid` with `max-width: 1280px` on the inner `.ast-builder-grid-row` element, and have NO `width: 100%`. On Elementor full-width pages (homepage), some ambient rule stretches the row to the cap; on standard pages (single-post, blog archive, WP `the_content` pages) the row collapses to content-auto (~900px), so logo sits left, menu sits a third-of-the-way across, giant empty space on the right.

Three separate caps that all have to be handled:

1. **Outer `.site-above-header-wrap.ast-container` / `.site-primary-header-wrap.ast-container`** — has Astra's `.ast-container { max-width: 1380px }` by default. Removing this cap (via `max-width: none` scoped to the body class) gives the colored background edge-to-edge but does NOT widen the menu content.
2. **Inner `.ast-builder-grid-row`** — `max-width: 1280` but `width: auto`. Needs explicit `width: 100%` to fill its cap. This is what widens the menu content.
3. **Footer `.ast-builder-grid-row-container-inner` + `.ast-builder-grid-row`** — same pattern as #2 *and* the row has fixed-px `grid-template-columns: 307px 307px 307px` that don't stretch even when the parent is at full width. Override with `grid-template-columns: repeat(3, 1fr) !important` to get equal-stretch columns, otherwise the cols stay glued left and the 3rd section (often containing a second widget like "Kje smo") can render at zero width and vanish.

Debug workflow: measure `width` AND `max-width` AND `grid-template-columns` with `get_computed_style` on every `.ast-builder-grid-row` + `.ast-builder-grid-row-container-inner` in the header and footer. If `width < max-width`, you're looking at a non-stretching row; apply `width: 100%`. If columns are fixed-px, override to `1fr`. Never assume "the ast-container is too narrow" — it's almost always the inner grid row that's the culprit.
