# SEO Agency Suite

> The 11 SEO Agency Suite tools in Royal MCP Pro: portfolio-scale SEO audit and bulk meta writers for Yoast, Rank Math, SEOPress, AIOSEO and SEObolt.

- Canonical: <https://royalplugins.com/support/royal-mcp-pro/seo-agency-suite/>
- Last updated: 2026-09-20
- HTML version: <https://royalplugins.com/support/royal-mcp-pro/seo-agency-suite/>

---

Complete reference for the 11 SEO Agency Suite tools in Royal MCP Pro. Portfolio-scale SEO audit + bulk meta writers for all five supported SEO plugins (Yoast, Rank Math, SEOPress, AIOSEO, SEObolt) + widget bulk ops + Redirection plugin bulk import/export/rule-set apply. All bulk writers use the safer 2-step commit envelope with drift detection and a longer 168-hour undo TTL.

## The two-step commit envelope

Every bulk write in this suite uses a two-step commit pattern — different from the direct-write pattern the Divi / Elementor / WC bulk tools use. This is a deliberate safety layer: SEO meta and widget content live in surfaces users often edit outside your MCP session, so racing an editor is a real risk. The two-step envelope catches drift between preview and commit.

### Step 1 — Preview (no `commit_token`)

Call the tool with your update payload but no `commit_token`. The response is a **dry-run preview**: what would change per row, plus a fresh `commit_token` string. Nothing is written.

### Step 2 — Commit (echo the `commit_token` back)

Call the tool a second time with the same update payload PLUS the `commit_token` from step 1. The token has a **5-minute TTL and is one-shot** — consumed on successful commit, or on any conflict / drift. Successful commit issues a **168-hour undo token**.

### Drift detection

Between step 1 and step 2, the tool re-reads the target rows. If any target's state changed — someone edited Yoast meta in wp-admin, a scheduled cron rewrote a widget, another MCP session touched the row — step 2 **rejects with a `conflict` error + `drifted_post_ids` array** naming the rows that moved. Nothing is written. You call step 1 again with a fresh preview and try again.

Why 168 hours (7 days) instead of 72?

SEO meta changes often surface in Search Console rankings on a 3–7 day lag. If a bulk meta rewrite tanks rankings, you may not know until day 5 or 6. The 168-hour window gives you time to notice and roll back before the tokens age out.

## SEO tools (1 audit + 5 bulk writers)

### wp\_audit\_seo\_bulk ⭐

Bulk SEO audit across a post type. Returns per-page findings (title/description length + issue, noindex, H1 count, word count, image count, images missing alt, featured image, issues list) plus an optional aggregated rollup (issues by type, worst offenders). Auto-detects Yoast SEO, Rank Math, SEOPress, AIOSEO, or SEObolt; falls back to WordPress-native fields (`post_title`, `post_excerpt`) when none is active. Read-only.

#### Arguments

| Name | Type | Description |
| --- | --- | --- |
| `post_type` | `string` | Post type to audit (must be a registered CPT slug). Default: `post`. |
| `paginate` | `object` | `{page, per_page}`. Defaults: page 1, per\_page 100 (max 500). |
| `rollup` | `boolean` | Include the aggregated rollup summary alongside per-page results. Default `true`. |
| `snapshot` | `boolean` | When `true` and SiteVault is installed, kick off a pre-audit backup. Default `false`. Snapshot failure never aborts the audit. |

#### Example

```
Run wp_audit_seo_bulk on my page post type with rollup=true. Then tell me the top 3 issues by count and the 5 worst-offender post IDs.
```

No undo (read-only). Requires `manage_options`.

Shared arguments across the 5 SEO plugin bulk writers

All five bulk-meta writers (`yoast_bulk_update_meta`, `rankmath_bulk_update_meta`, `seopress_bulk_update_meta`, `aioseo_bulk_update_meta`, `seobolt_bulk_update_meta`) accept the same input shape:

| Name | Type | Description |
| --- | --- | --- |
| `updates` required | `array` | Up to 100 per-post update rows. Each row must include `post_id` and at least one field. |
| `dry_run` | `boolean` | Force a step-1 preview even if you already have a `commit_token`. Default `false`. |
| `commit_token` | `string` | Required for the actual write (step 2). Get it from the step-1 dry-run response. TTL: 5 minutes. One-shot. |

**Per-update-row 3-state field model** — OMIT the key to leave the field UNCHANGED; pass `""` or `null` to CLEAR (delete meta / revert to plugin default); pass a value to SET.

| Field | Type | Description |
| --- | --- | --- |
| `post_id` required | `integer` | Target post ID. |
| `title` | `string` | SEO title (browser tab / SERP display). |
| `description` | `string` | Meta description (SERP snippet). |
| `focus_keyword` | `string` | Primary focus keyword for SEO scoring. |
| `noindex` | `boolean` | `true` = force noindex. `false` = force explicit index (overrides site default) — EXCEPT SEOPress, see per-tool notes. `null` = revert to site default on all plugins. |
| `canonical` | `string` | Canonical URL (or clear). |

### yoast\_bulk\_update\_meta

Bulk-update Yoast SEO meta across up to 100 posts. Routes to Yoast's meta keys. Two-step commit + drift detection + 168h undo.

Requires Yoast SEO to be the active SEO plugin. Rank Math / SEOPress / AIOSEO / SEObolt have separate bulk-write tools. See shared arguments above.

Undo: 168h token — restores each affected post's Yoast meta exactly.

### rankmath\_bulk\_update\_meta

Bulk-update Rank Math SEO meta across up to 100 posts. Routes to Rank Math's meta keys (`rank_math_title`, `rank_math_description`, `rank_math_focus_keyword`, `rank_math_robots` array, `rank_math_canonical_url`).

**Noindex handling:** noindex toggles preserve other robots tokens (`nofollow`, `noimageindex`) already on the post. Two-step commit + drift detection + 168h undo. Requires Rank Math active.

Undo: 168h token — restores each affected post's Rank Math meta exactly.

### seopress\_bulk\_update\_meta

Bulk-update SEOPress meta across up to 100 posts. Routes to SEOPress's meta keys (`_seopress_titles_title`, `_seopress_titles_desc`, `_seopress_analysis_target_kw`, `_seopress_robots_index`, `_seopress_robots_canonical`).

SEOPress noindex quirk

SEOPress has no explicit-index concept — `noindex: false` and `noindex: null` both delete the meta row (revert to site default). If you want to force "always index" on Yoast/RankMath/AIOSEO you'd use `noindex: false`; on SEOPress there's no equivalent action.

Two-step commit + drift detection + 168h undo. Requires SEOPress active.

Undo: 168h token — restores each affected post's SEOPress meta exactly.

### aioseo\_bulk\_update\_meta

Bulk-update AIOSEO (All in One SEO) meta across up to 100 posts. Dual-writes to legacy `post_meta` AND to the `wp_aioseo_posts` custom table on modern versions so both surfaces stay coherent.

Never-touched posts show as warnings

Posts that AIOSEO has never touched (no `wp_aioseo_posts` row) surface in the response's warnings array. Open the post once in the AIOSEO editor to seed a row, then re-run the bulk update.

Two-step commit + drift detection + 168h undo. Requires AIOSEO active.

Undo: 168h token — restores each affected post's AIOSEO meta exactly.

### seobolt\_bulk\_update\_meta

Bulk-update SEObolt meta across up to 100 posts. Routes to SEObolt's `_seobolt_meta_*` post-meta keys. Two-step commit + drift detection + 168h undo. Requires SEObolt active.

SEObolt is Royal Plugins' own SEO plugin — treated exactly the same way as Yoast, Rank Math, SEOPress, and AIOSEO by the auto-detect layer. If SEObolt is the active SEO plugin on the site, this writer is what runs when you call bulk-meta updates.

Undo: 168h token — restores each affected post's SEObolt meta exactly.

## Widget bulk tools (2)

Both widget tools require `edit_theme_options` capability **plus** the Royal MCP settings toggle "Allow AI to modify theme appearance" — a deliberate second gate because widget changes affect site-wide chrome and can't be undone visually as easily as post content.

### wp\_bulk\_update\_widgets

Bulk-update widget instance settings across up to 100 widgets. Settings are partial-merged: keys you pass overwrite, keys you omit stay unchanged.

#### Arguments

| Name | Type | Description |
| --- | --- | --- |
| `updates` required | `array` | Up to 100 `{widget_id, settings}` rows. `widget_id` examples: `"text-2"`, `"custom_html-5"`, `"block-15"`. |
| `dry_run` | `boolean` | Force step-1 preview even with a `commit_token` present. |
| `commit_token` | `string` | Required for the actual write (step 2). TTL 5 min. One-shot. |

Widgets not found are surfaced in the response's `skipped` array, not raised as errors.

Undo: 168h token — restores prior widget instances exactly.

### wp\_bulk\_replace\_widget\_content

Literal find/replace across every stored instance of a widget type (e.g. update the copyright year on every `text` widget site-wide, or fix a URL across every `custom_html` widget). Walks all STRING values in each instance's settings — non-string values (numeric IDs, booleans) are left untouched.

Regex NOT supported

Literal match only. This is deliberate for safety — regex on user-typed widget content is a foot-gun class of bug that has no good fit inside an AI-driven workflow. If you need regex, do the find/replace client-side and use `wp_bulk_update_widgets` to write the transformed settings back.

#### Arguments

| Name | Type | Description |
| --- | --- | --- |
| `widget_type` required | `string` | Widget `id_base` (the piece before the `-N`), e.g. `text`, `custom_html`, `block`, `nav_menu`. |
| `find` required | `string` | Literal string to find. |
| `replace` required | `string` | Replacement (may be empty to remove all occurrences). |
| `case_insensitive` | `boolean` | Default `false`. When `true`, uses `str_ireplace`. |
| `dry_run` | `boolean` | Force step-1 preview. |
| `commit_token` | `string` | Required for step 2. TTL 5 min. |

Undo: 168h token — restores prior widget instances exactly.

## Redirection plugin bulk tools (3)

All three tools require the [Redirection plugin](https://wordpress.org/plugins/redirection/) by John Godley active. They wrap the plugin's storage layer with two-step commit + drift detection.

### redirection\_bulk\_import

Bulk-import up to 100 redirects from a CSV. Header row required with at least `source` and `destination` columns; optional `group`, `type`, `match_type`.

#### Arguments

| Name | Type | Description |
| --- | --- | --- |
| `csv_content` required | `string` | CSV body. First line = header; header must include `source` + `destination`. Columns: `source, destination, group, type, match_type`. |
| `overwrite` | `boolean` | When `true`, existing redirects with the same source are updated in-place. Default `false` (existing sources skipped and surfaced in the skipped list). |
| `dry_run` | `boolean` | Force step-1 preview. |
| `commit_token` | `string` | Required for step 2. TTL 5 min. |

Undo: 168h token — deletes only the redirects this batch CREATED and restores the ones it UPDATED. Never touches pre-existing redirects that weren't part of the batch.

### redirection\_bulk\_export

Export existing Redirection entries as CSV. Read-only.

#### Arguments

| Name | Type | Description |
| --- | --- | --- |
| `group_id` | `integer` | Filter to redirects in this group only. Use the Free-tier `redirection_list_groups` to discover IDs. |
| `limit` | `integer` | Default 500, min 1, max 5000. |

No undo (read-only, no-op token returned for API consistency).

### redirection\_apply\_rule\_set

Apply a JSON-defined rule set (up to 100 rules) — same shape + semantics as `redirection_bulk_import` but takes a `rules` array directly instead of CSV. Useful for saved templates like "301 old blog URLs to new structure."

#### Arguments

| Name | Type | Description |
| --- | --- | --- |
| `rules` required | `array` | Up to 100 rule objects. Each: `{source, destination, group?, type?, match_type?}`. `type` is HTTP status (301 / 302 / 307; default 301). |
| `overwrite` | `boolean` | When `true`, existing redirects with the same source are updated in-place. |
| `dry_run` | `boolean` | Force step-1 preview. |
| `commit_token` | `string` | Required for step 2. TTL 5 min. |

Undo: 168h token — same shape as `redirection_bulk_import` (deletes newly-created, restores updated, ignores pre-existing).

## Related documentation

- [**SEO Audit → Fix → Report workflow**](https://royalplugins.com/support/royal-mcp-pro/seo-audit-fix-report-workflow/) — end-to-end flagship guide for portfolio SEO cleanup
- [Undo Tokens explainer](https://royalplugins.com/support/royal-mcp-pro/#undo-tokens)
- [Cross-plugin Composers](https://royalplugins.com/support/royal-mcp-pro/cross-plugin-composers/) — `wp_prepare_for_launch` uses `wp_audit_seo_bulk` internally

## Still Stuck? Two-Step Support Path

If a SEO Suite tool isn't behaving the way you expected, work through these two steps in order.

### Step 1: Start with the Royal MCP Troubleshooting Guide

[**Royal MCP Troubleshooting — Start Here**](https://royalplugins.com/support/royal-mcp/troubleshooting-start-here/) covers MCP-layer issues.

For Pro-specific patterns:

- [Undo token errors](https://royalplugins.com/support/royal-mcp-pro/undo-token-errors/) — expired, consumed, or handler missing
- [Pro tool returned upgrade prompt with valid license](https://royalplugins.com/support/royal-mcp-pro/pro-tool-returned-upgrade-prompt/)

### Step 2: Email priority support

If you've worked through Start Here and the relevant Pro-specific doc and the issue still isn't resolved, email priority support from your purchase email address at **[support@royalplugins.com](mailto:support@royalplugins.com)**. Priority email support is included with your license — typical response within 24 hours. **Never include your license key in email**; we look it up from your purchase address.

What to include in your email

- **Your hosting provider** (SiteGround, Cloudways, Hostinger, Kinsta, WP Engine, Liquid Web, self-hosted VPS, etc.)
- **Royal MCP Pro version** from WP Admin → Plugins
- **Which SEO plugin is active** and its version (Yoast SEO / Rank Math / SEOPress / AIOSEO / SEObolt / none) — if using a bulk SEO writer
- **Redirection plugin version** if using a Redirection tool
- **Active caching plugins** (LiteSpeed Cache, WP Rocket, W3 Total Cache, SpeedyCache, Hummingbird, none)
- **Which MCP client** — claude.ai web custom connector, Claude Desktop with `mcp-remote`, ChatGPT MCP, Cursor, or another MCP-compatible client
- **The exact tool name and args** you called (redact any customer PII from the `updates` array if present)
- **Which step failed** — step 1 preview, or step 2 commit? If step 2 rejected with `conflict`, include the `drifted_post_ids` array
- **Screenshot of the most recent `oauth:` row in Activity Log** AND **the most recent Pro tool row in Audit Log** with View Details expanded

[← Cross-plugin Composers](https://royalplugins.com/support/royal-mcp-pro/cross-plugin-composers/)
[Documentation Hub →](https://royalplugins.com/support/royal-mcp-pro/)
