説明
WordPress.com has a built-in MCP server. Now self-hosted WordPress does too.
IATO MCP connects your WordPress site to Claude Desktop and other MCP-enabled AI clients. Once connected, you can ask Claude to audit your site and fix SEO issues, identify orphan pages, clean up broken links, and more, all in a single conversation.
How it works
- Install and activate the plugin
- Follow the setup wizard — copy the config into Claude Desktop, or use “Add Custom Connector” with your site URL
- Connect your IATO account for AI-powered analysis (free trial up to 500 pages)
What Claude can do
Without an IATO account (45 WordPress tools):
- Read and edit posts, pages, and media
- Create new posts and pages with excerpt support
- Update SEO titles and meta descriptions (Yoast SEO, RankMath, SEOPress)
- Update canonical URLs
- Update image alt text
- Upload new images to the media library (base64 by default; URL ingestion optional with admin allowlist + full SSRF protection)
- Set and clear the featured image on any post
- Read and write arbitrary post meta with a credential-key denylist and a known-safe theme/builder/SEO allowlist (force=true required outside the allowlist)
- Set per-post theme + builder page settings (hide title, sidebar layout, content layout, etc.) on Astra, Kadence, GeneratePress, and Elementor in one call
- Read and edit navigation menus
- Manage categories, tags, and taxonomy terms
- Manage JSON-LD structured data
- Manage redirect rules
- Read and write Elementor page builder data
- Widget-grained Elementor edits with optimistic concurrency, idempotency, and bulk operations
- Fix heading levels, text, links and image alt text on Elementor Atomic Editor (V4) elements (Elementor 4.3.0 or later), converted and validated with Elementor’s own code, each change with its own rollback
- Clone the styling of an existing Elementor post in one call via
update_elementor_data(..., inherit_settings_from: <id>) - Resolve URLs to their rendering post (Theme Builder shadowing detection)
- Search content across the site
- Read site info and settings
- Read and filter comments
- One-call rollback for any tracked write — every change emits a receipt with a stable
change_id; pass it back to therollbacktool and the original value is restored
With an IATO account (12 bridge tools — full analyze-and-fix pipeline):
- Start a new crawl of your site directly from Claude (admin only)
- Check crawl status and list recent crawl jobs
- Run a full SEO audit and fix title, meta description, and alt text issues automatically
- Identify orphan pages not linked from any navigation menu
- Audit navigation menus for gaps and missing sections
- Surface thin content with specific improvement recommendations
- Map broken links to source posts for direct editing
- Analyze site taxonomy and suggest consolidations
- Get AI-prioritized suggestions across all areas
- Flag slow pages with contributing performance factors
Supported SEO plugins
- Yoast SEO
- RankMath
- SEOPress
- Falls back to native WordPress title if none detected
Example prompts
“Crawl my site and fix all missing meta descriptions”
“Show me pages that aren’t in any navigation menu and add them to the right place”
“What are the most impactful improvements I can make to my site right now?”
“Find all broken links and tell me which posts contain them”
“Audit my categories and tags and suggest consolidations”
“Set every H2 heading in these Elementor posts to H1”
“Find all button widgets on the site and change their color to #ff0000”
External Services
This plugin connects to the following external service when configured:
IATO API (https://iato.ai) — When you enter an IATO API key in the plugin settings, the plugin sends requests to https://iato.ai/api to retrieve crawl data, SEO audit results, sitemap information, and AI-generated improvement suggestions. No data is sent to IATO until you configure an API key. Your public page URLs (as crawled by IATO) and crawl analysis results are transmitted.
The plugin also implements an OAuth 2.0 authorization server on your WordPress site so that MCP clients like Claude Desktop can authenticate via the standard “Add Custom Connector” flow. This communication stays between the MCP client and your WordPress site — no data is sent to third parties during authentication.
スクリーンショット




インストール
- Upload the plugin files to
/wp-content/plugins/iato-mcp/or install via the WordPress plugin directory - Activate the plugin via the Plugins menu in WordPress
- Follow the setup wizard that appears — it provides the JSON config for Claude Desktop
- In Claude Desktop, either paste the JSON config or use “Add Custom Connector” and enter your site URL
- Optionally, go to Settings > IATO MCP to enter your IATO API key for the full analysis pipeline. Use a separate IATO API key for each client site: a key gives access to every crawl in its IATO account, so a key shared between sites lets one site’s administrators see the others’ crawl data.
For detailed setup instructions, see the IATO MCP documentation.
FAQ
-
Do I need an IATO account?
-
No. The plugin works standalone for reading and editing WordPress content with 40 built-in tools. An IATO account (free trial up to 500 pages) unlocks 12 additional bridge tools: start/list/status crawl management, SEO audit, broken links, content gaps, orphan pages, navigation audit, taxonomy analysis, AI suggestions, and performance reports. They are available to administrators. If you manage several client sites, give each site its own IATO API key: a key can read every crawl in its account.
-
Which WordPress version is required?
-
WordPress 6.2 or higher with PHP 8.0+. The plugin uses the WordPress REST API and implements OAuth 2.0 for secure authentication with AI clients.
-
Yes. The plugin uses standard HTTP requests (one per MCP call) rather than long-lived connections, so it works on all hosting environments including shared hosting.
-
Which AI clients are supported?
-
Any MCP-enabled client: Claude Desktop, Cursor, VS Code with GitHub Copilot, and any client that supports the Streamable HTTP MCP transport.
-
How does authentication work?
-
The plugin generates a secure API key on activation. You can authenticate in two ways: paste the provided Bearer token config into your AI client, or use Claude’s “Add custom connector” flow, which handles OAuth 2.1 with PKCE automatically. Each OAuth connection gets its own expiring token, acts as the administrator who approved it, and can be revoked on its own under Settings > IATO MCP > Connected OAuth clients.
-
Why does the plugin support two auth methods?
-
AI clients like Claude Desktop authenticate via a WordPress Application Password (or the OAuth 2.0 / PKCE flow), which is the WordPress-native pattern most users will use. The plugin also accepts the plugin-generated Bearer token at the same MCP endpoint — that path is used by the IATO platform’s own integrations (for example, the dashboard’s “Sync pages, posts, menus, and taxonomy from WordPress” feature, which composes the plugin’s read tools to pull content into IATO). Both methods land at
/wp-json/iato-mcp/v1/messageand are validated byclass-auth.php. You don’t have to choose — paste your Bearer token into the IATO platform connection, generate an Application Password for Claude Desktop, and the same plugin handles both. -
Is my content sent to IATO or Anthropic?
-
WordPress content (post titles, meta descriptions, etc.) is never sent to IATO. IATO crawls your public URLs the same way a search engine would. Claude processes content within your AI client session only. The IATO API is only called when you use bridge tools, and only crawl analysis data (not your content) is transmitted.
-
A page lost an embed or script after an IATO edit. How do I repair it?
-
Open the page in Elementor, go to History, then Revisions, and apply the last revision saved by a person before the first IATO edit. Then re-apply the IATO changes you want to keep. If revisions are disabled on the site, there is no earlier copy to restore.
-
A page edited with IATO shows a missing link or image, or Elementor will not save it
-
IATO 1.13.0 and earlier could store a plain value on an Elementor Atomic Editor (V4) element, such as a heading, paragraph, image or button built with the Atomic Editor, where Elementor expects a typed value. Elementor then drops a link or image it cannot read, and saving the page in Elementor fails with “Settings validation failed”.
To find the affected elements, read the page with get_elementor_data, get_elementor_widget, list_elementor_widgets or find_elementor_widgets. Since 1.13.1 they flag the element with stored_value_invalid: true, and invalid_keys names the settings to repair; get_elementor_data also lists them under invalid_elements.
To repair an element, send each listed setting again with update_elementor_widget, set_widget_setting or set_heading_level. From 1.14.0 (with Elementor 4.3.0 or later) the plain value works, for example {“tag”: “h2”} or {“link”: {“destination”: “https://example.com/”}}: IATO stores it in Elementor’s format and the element is repaired. With 1.13.1, or on a site without Elementor 4.3.0, send it as a typed value, in the shape get_elementor_widget shows under settings for the element, for example {“tag”: {“$$type”: “string”, “value”: “h2”}}. When several elements on a page are affected, repair them one at a time; Elementor saves the page normally again once the last one is repaired. A damaged element cannot be copied (update_elementor_patch refuses the copy): repair the source first, then copy it. You can also restore a revision saved before the IATO edit, under History, then Revisions, in Elementor.
-
Can IATO edit Elementor Atomic Editor (V4) elements?
-
Yes, from 1.14.0, with Elementor 4.3.0 or later and the Atomic Editor turned on. set_heading_level, set_widget_setting, update_elementor_widget, update_elementor_widgets_bulk and update_elementor_patch take plain values for atomic headings, paragraphs, buttons, images and containers, and update_alt_text takes post_id and element_id for an atomic image. IATO converts and validates each value with Elementor’s own code and keeps every setting the call does not name as it was stored. Changes go live, like edits to classic widgets, and each one can be rolled back on its own.
The alt text of an image from the Media Library belongs to the attachment, so IATO changes it there, and the change applies everywhere that image is used. Global classes and component instances are not edited by IATO yet. On sites with an older Elementor, IATO accepts only typed values for atomic elements and says what is missing. While the Atomic Editor is off, IATO does not change atomic elements at all, because Elementor cannot check them; the rest of the page can still be edited.
-
Can I control which tools are available?
-
Yes. Go to Settings > IATO MCP to enable or disable individual tools. You can turn off any tool you don’t want AI clients to access.
-
Can AI clients upload arbitrary files to my media library?
-
Only images, and only when the calling user has the
upload_filescapability. Thecreate_mediatool enforces an image-only MIME allowlist (JPEG, PNG, GIF, WebP, AVIF) verified against actual file bytes — the claimed mime_type is never trusted. SVG uploads are not supported in this release. Files exceeding the size cap (default 10MB) or the dimension cap (default 8000×8000) are rejected, as are filenames containing.php,.phtml, or.htaccess. URL-source ingestion is disabled by default; admins who enable it must also configure a host allowlist, and private/loopback/cloud-metadata IPs are rejected even for allowlisted hosts. Each upload counts against a per-user rate limit (default 20/min) and emits achange_receipt— rolling back fully deletes the attachment file. All four limits are configurable from Settings > IATO MCP.
評価
このプラグインにはレビューがありません。
貢献者と開発者
変更履歴
1.14.0
- New: Heading level, text, link and external-image alt fixes now work on Elementor Atomic Editor (V4) elements through the existing tools (set_heading_level, set_widget_setting, update_elementor_widget, update_elementor_widgets_bulk, update_elementor_patch, update_alt_text). IATO converts and validates values with Elementor’s own code (Elementor 4.3.0 or later), and edits go live like classic edits.
- New: Each atomic change records a receipt per element and can be rolled back on its own, with the conflict check and dry_run.
- New: Results say how a change was applied (applied_via), that it is live (state), which values were sanitised, and which caches were cleared.
- New: update_alt_text accepts post_id and element_id to fix the alt text of an atomic image that uses an external URL. For a Media Library image it changes the attachment’s alt text, as before. update_alt_text also accepts dry_run.
- Change: Plain values sent to atomic elements, refused since 1.13.1, are now converted and validated with Elementor’s code and stored in Elementor’s format. On sites without Elementor 4.3.0 or later, the 1.13.1 rule stays: only typed values are accepted, and the refusal (now atomic_write_unavailable) says what is missing.
- Change: While Elementor cannot load an atomic element’s type (the Atomic Editor is off, or the plugin that provides it is inactive), IATO no longer changes that element, because nothing can validate it; typed values were stored unchecked until now. Other elements on the page can still be edited.
- Change: In update_elementor_widgets_bulk, atomic rows for the same page are applied in one save, so several rows that send the same if_revision now succeed together. Before this change, every row after the first failed with revision_conflict. Classic rows are unchanged.
- New: get_iato_seo_fixes lists issues it cannot act on under skipped, with a reason: a URL that is not a WordPress post on this site, or a URL on another domain.
- Fix: An error thrown inside a tool or another plugin now returns a clear error instead of a PHP fatal error.
- Fix: When Elementor’s own save fails and IATO stores the page itself, IATO clears that page’s CSS and element cache and says so in the result.
- Fix: Sending a setting again to an atomic element that an earlier IATO version stored as a plain value repairs that element.
- Tweak: Atomic edits clear only the edited page’s caches, not every page’s.
- Tweak: Wording of the plugin description.
1.13.1
- Fix: plain values sent to Elementor Atomic Editor (V4) elements are now refused instead of being stored in a form that could remove links or images and block later saves.
- Fix: IATO no longer stores new settings for an Atomic Editor (V4) element that Elementor’s own validation rejects, whichever credential is used, and dry runs report the same refusal. Settings an atomic element does not have (such as header_size on an atomic heading) are refused instead of being reported as applied.
- Fix: editing a page that contains elements from a deactivated addon, or atomic elements while the Atomic Editor is off, no longer removes those elements.
- New: the Elementor read tools (get_elementor_data, get_elementor_widget, list_elementor_widgets, find_elementor_widgets) flag an atomic element whose stored settings Elementor cannot read with stored_value_invalid and invalid_keys, and show those values as stored. See the FAQ for how to repair such an element.
1.13.0
- Security: OAuth connections (the Claude connector, Claude Code, Cursor and other MCP clients) now receive their own tokens. The authorization code is random and single use (5 minutes); the access token expires after 1 hour and is renewed with a rotating refresh token (30 days); a connection expires after 180 days unless it is approved again (filter: iato_mcp_oauth_grant_lifetime). Tokens are stored hashed. Before this release the OAuth code and the access token were the site’s API key itself.
- Security: each OAuth connection acts as the administrator who approved it and is re-checked on every request. Deleting or demoting that user revokes the connection. Approving a connection no longer assigns the site’s API key to the approving user.
- New: Settings > IATO MCP lists connected OAuth clients (application, approving user, connected, last used) with a Revoke button per connection and a Disconnect all button, and the plugin serves a token revocation endpoint at /oauth/revoke. Revoking connections does not affect the API key.
- Security: dynamic client registration accepts https redirect URIs only (plain http only to localhost, 127.0.0.1 or [::1], any port), stores the application name, is rate-limited (10 per IP per 10 minutes, 100 per site per hour; filter: iato_mcp_oauth_registration_limits), and removes registrations that never complete an authorization after 24 hours or have had no live connection for 30 days.
- Security: PKCE (S256) is required at /oauth/authorize; the resource parameter (RFC 8707) is checked; the authorization response carries iss (RFC 9207); the consent screen shows the application name, the host it will send you back to, and the user the connection will act as, and its approval is bound to that exact request. Logged-in users who are not administrators get a clear “administrators only” page. Follows the MCP authorization specification revision 2026-07-28. Client ID Metadata Documents (“Use Claude’s published identity”) are not supported yet; Claude falls back to dynamic registration on its own.
- Fix: database changes are now tracked by a schema version separate from the plugin version, so a site that ran a pre-release build still receives every later table change.
- Security: update_post_meta and inherit_keys no longer write Elementor builder data. Every elementor key is refused except _elementor_page_settings (whose values are sanitised), _elementor_edit_mode and _elementor_template_type; the error names the Elementor tool to use. Element trees stored as arrays by earlier versions are still read.
- Security: when Elementor’s own save refuses, throws (some addon packs) or Elementor is not loaded, the plugin now sanitises the values the request changed before storing the tree, by field type: URL fields through esc_url_raw, custom_css through sanitize_textarea_field, strings containing markup through wp_kses_post; untouched values, plain text, numbers and booleans are stored verbatim. Elementor’s save runs first and the raw tree is never written. Responses carry sanitized: true and the changed setting paths when a value was altered; dry runs report the same.
- Fix: the 1.12.2 fix (edits change only the values they set) now also holds when Elementor’s own save runs, which it does for a key bound to an owner and for OAuth connections; what a person authored elsewhere on the page is stored as it was.
- Security: content written through MCP with an Application Password is now filtered like a non-administrator’s (the same kses rules that already applied to the API key and OAuth), so scripts and embeds in Elementor widgets or post content are stripped even for administrators. A developer who needs raw HTML through MCP can opt out with the iato_mcp_allow_unfiltered_html filter (receives the user ID and the credential kind; honoured only for Application Password users who hold unfiltered_html).
- Security: every change receipt records who made it: the WordPress user, the credential (site key, OAuth connection or Application Password) and the OAuth application. Only that user or an administrator can roll the change back; changes recorded before 1.13.0 keep the earlier rules. Rollback responses and the Diagnostics call log show who made and who undid each change.
- New: rollback refuses to overwrite a value that has changed since the change was made (a later edit in WordPress or another change) and shows the current and expected values; pass force: true to roll back anyway, or dry_run: true to check first.
- Fix: Elementor edits can be rolled back exactly (the receipt now keeps the page as it was before the edit). Elementor receipts from earlier versions held only a revision hash; their rollback now says so instead of failing.
- New: those page copies are removed automatically after 90 days by a daily task; the receipt stays as a record of who changed what, and older edits are restored from Elementor’s revisions instead. Developers can change the period with the iato_mcp_receipt_retention_days filter (0 keeps them). Diagnostics shows when the cleanup last ran.
- Fix: backslashes are kept. Post and page titles, content and excerpts, post meta (including Elementor page settings such as custom CSS escapes like \201C), SEO titles and descriptions, alt text, media titles and captions, menu item titles and term names and descriptions written through MCP used to lose every backslash, and rollback could not restore them.
- Fix: rolling back a tag added to a post that had no tags no longer removes the post’s categories, and rolling back a term edit restores the term (it did nothing before).
- Security: the IATO bridge tools (crawls, SEO audit, broken links, content gaps, orphan pages, navigation audit, taxonomy, suggestions, performance, sitemap) now require an administrator (manage_options) through MCP, as they already did as WordPress abilities. They return the IATO account’s crawl data, which can include other sites. The iato_mcp_ability_capability filter lowers the requirement for both surfaces.
- Security: get_media requires upload_files (Author and above), the same as the Media Library.
- Security: get_comments returns only comments on posts the caller can read (password-protected posts need edit rights), and held, spam or trashed comments require moderate_comments. An unknown status is now an error instead of an empty list.
- Security: get_menu_items leaves out items that link to posts the caller cannot read, such as drafts and private posts, and reports how many it left out.
- Security: get_site_info returns the active plugin count and the Theme Builder template count to administrators only; other users see them listed under restricted_fields.
- Security: resolve_url answers with a 404 for posts the caller cannot read, as WordPress does for visitors.
- Security: writing _elementor_template_type with update_post_meta requires an administrator; update_elementor_data’s inherit step skips it for other users and says why.
- Security: in array values written to other post meta keys (custom fields, theme and SEO plugin settings), text containing HTML that the write changes is filtered with wp_kses_post, like Elementor values; values already stored are kept as they are.
- Note on upgrade: OAuth connections made before 1.13.0 keep working (they hold the API key) but cannot be revoked individually. An admin notice explains how to move them to their own tokens (remove and re-add the connector in the AI client) and that regenerating the API key disconnects all of them; Settings and Diagnostics keep pointing this out until the key is regenerated. An authorization that was in progress during the upgrade has to be started again.
1.12.2
- Fixed: editing an Elementor page with an IATO site key or OAuth connection removed iframes, scripts and other HTML from widgets the edit didn’t change, and turned & into & in text. Edits now change only the values they set. Affects 1.0.0 to 1.12.1. Connections using an Application Password of a user who may publish unfiltered HTML (administrators and editors on a single site, super admins on multisite) were not affected.
- Fixed: rolling back Elementor content now restores it exactly.
1.12.1
- Security: write tools now check permission on the specific post, page, term, or attachment being changed, and publishing requires publish rights.
- Security: read tools now follow WordPress permissions for drafts, private posts, password-protected content (including revisions), and Theme Builder templates. get_post_meta requires edit access to the post.
- Security: hardened structured data. update_structured_data now accepts only JSON objects or arrays, rejects script and comment markup, and stores values with HTML-sensitive characters escaped (for example \u003C). Code that reads the stored meta directly will see the escaped form.
- Security: the OAuth consent screen can no longer be embedded in other sites. Token exchange is stricter: only the S256 PKCE method is accepted, and a client that sent a code challenge must send the matching code verifier.
- Security: rollback requires the same permissions as the original change.
- Fix: base64 media uploads no longer fail on some sites.
- Note for integrations using an Application Password: requests now get exactly the access that user’s WordPress role allows. Contributors and Authors can no longer read or change other users’ unpublished content, and Contributors can no longer create pages. get_posts may report a hidden count. find_elementor_widgets lists explicitly requested posts it could not read in skipped_unreadable, reports only a count for posts found by its automatic scan, and requires administrator rights for include_templates. Connections using the site key or OAuth are unaffected.
1.12.0
- Elementor Atomic Editor (V4) read support. list_elementor_widgets, get_elementor_data (summary), get_elementor_widget and find_elementor_widgets now return correct content for atomic elements (e-heading, e-paragraph, e-image, e-button, e-flexbox, e-div-block, …) and for pages mixing classic and atomic elements, in document order. Every node carries schema: “classic” | “atomic”; atomic nodes are normalised to the classic peek keys (title, header_size, editor, text, link) plus tag, link_new_tab, image_url, image_id, image_alt, image_alt_source.
- get_elementor_widget returns settings_plain (typed envelopes unwrapped, unsaved schema defaults filled and listed in defaulted_keys) alongside the raw settings. get_page_builder returns elementor_schema: classic | atomic | mixed | empty. initialize advertises capabilities.elementor.atomic_read.
- find_elementor_widgets setting filters match atomic nodes on unwrapped values, so setting.title.contains and setting.header_size.eq work on both schemas; type accepts atomic names.
- Reads parse _elementor_data directly with no call into Elementor code, so they work on Elementor 3.x, 4.0-4.2, with the Elementor MCP module off, and with Elementor deactivated. Classic-only pages return the same output as 1.11.0 apart from the added schema key.
- Writes to atomic elements are not yet supported (planned next release).
1.10.0
- Security:
IATO_MCP_Auth::require_cap()now actually enforces the capability argument. Previously the function checked only whether the request was authenticated and returnedtrueregardless of the cap string passed — a documented but long-deferred limitation (the file’s own docblock acknowledged it as a v1.6 hardening item). Every existingrequire_cap()call site (edit_postson write tools,manage_optionsonget_site_settings,upload_filesoncreate_media, etc.) was cosmetic until this release. Now real. - Mechanism:
authenticate()carries the authenticatedWP_Userobject through torequire_cap()for the Application Password path.require_cap()callsuser_can($user, $cap)against that user. Bearer plugin-key authentication remains documented full-administrative-access (intentional — the key itself is the gate; security comes from key issuance being admin-only); the change applies to per-user auth paths only. - Observable contract change: any Application Password user below admin level who previously could call
manage_options-gated MCP tools will now be correctly rejected. This is the security boundary tightening — semver minor, not patch, even though the surface change is “tiny” by line count. Theedit_postsauth-time baseline inauthenticate()is preserved (Subscribers still rejected at auth time, not per-tool time). - Audit: every
require_cap()call site reviewed under enforcement. 21 sites confirmed-correct without code change. Two structural changes:- Menus (4 tools:
update_menu_item,create_menu_item,delete_menu_item,update_menu_item_details) — switched frommanage_optionstoedit_theme_options, the WP-canonical cap for nav-menu structure (matcheswp-admin/nav-menus.php). Functionally identical on default WP (both admin-only); observably different for sites that grantedit_theme_optionsto non-admin via a role-management plugin. - Rollback (
tool-rollback.php) — replaced the hand-maintained$elevated_typesswitch with a per-receipt-type cap map inIATO_MCP_Change_Receipt::cap_required_for(). Fail-closed default: unknown target_types are gated atmanage_optionsuntil they’re explicitly added to the map, so a new receipt type can’t silently inherit a lower cap. The map lives adjacent to the existing target_type docblock so future developers see the cap requirement at the receipt-type declaration site. The'taxonomy'entry is field-discriminated —assign/termsoperations stay atedit_posts(matchassign_term/update_taxonomycreate-time caps),create_term/update_term/delete_termgo tomanage_categories(match the create-time caps). Convention: rollback cap === create-time cap.
- Menus (4 tools:
- Known issue (not addressed in this release): the OAuth flow at
/oauth/tokenreturns the plugin’siato_mcp_key(admin-only MCP key) as the OAuthaccess_token— seeclass-oauth.php:486-489. This means OAuth-issued tokens are admin-key-equivalent regardless of which user originally authorized the OAuth flow, and OAuth-authenticated MCP requests flow through the Bearer plugin-key path inauthenticate()(which intentionally bypasses per-user cap checks). The OAuth/oauth/authorizegate atmanage_optionsis one-time at issuance, not ongoing per-request. Fixing this requires per-user OAuth token issuance (new token storage, per-user issuance with scoping, revocation, an alternate access-token format that encodes user identity) — substantial separate design tracked for a future cycle. Severity is bounded by the issuance-timemanage_optionsgate (only admins can ever obtain an OAuth token in the first place); the failure mode is “OAuth-authorized client retains admin-equivalent access even if the originally-authorizing admin’s role is later downgraded.”
1.11.0
- New:
list_elementor_templatestool. Enumerateselementor_libraryposts (Theme Builder templates) with their Display Conditions surfaced both as the raw stored string (e.g.include/archive/category/1) and a structured parsed shape with resolved target labels (e.g. the category named “Build”). Closes the discovery gap that motivated Layers 1 and 2 — without this, the only way to find a template you didn’t already know the ID of was to brute-force post IDs until a revision slug leaked the parent. Parameters:template_type(filter on_elementor_template_type),status,per_page(max 100),page,include_conditions(defaulttrue— conditions ARE the listing). Returns id, title, slug, status, template_type, modified, edit_url, and the conditions array. Gated behindmanage_options— enforced as of v1.10.0’s hardening (not cosmetic). - New:
find_elementor_widgetsgains aninclude_templatesboolean parameter (defaultfalse). Whentrue, the auto-scan expands from[post, page]to[post, page, elementor_library]. Opt-in not default to preserve v1.8.x BC — silently widening the scan would shift match counts unpredictably for every existing caller. Revision auto-resolution,post_statusfilter,containsoperator, and the 500-post cap all unchanged. Has no effect when explicitpost_idsare passed. - New:
get_site_infogains atheme_builder_template_countinteger field. Countselementor_libraryposts that are actually Theme Builder templates (have a non-empty_elementor_template_typemeta) — excludes saved sections, reusable blocks, and condition-less popups since those don’t shadow URLs. Returns 0 when Elementor isn’t installed. Field is always present (typed as int) so callers can rely on it. - Security:
list_elementor_templatesis gated behindmanage_options. Justification: the tool exposes the full structural map of every Theme Builder construct on the site and its targeting rules — comparable to or greater than whatget_site_settingsreturns, which is the existingmanage_options-gated structural read. Per-resource template questions (“what renders THIS URL”) are already answered byresolve_urlat the authenticated-only level; this tool exists specifically for the enumeration access pattern, which deserves a strictly higher gate than per-resource lookup. Thetheme_builder_template_countfield inget_site_infostays ungated — a bare count is comparable to the existingplugin_countand answers “do templates exist worth querying” without disclosing structure.find_elementor_widgets.include_templatesstays no-cap because it’s a filtered widget search keyed by user-supplied filters, not bare structural enumeration; conditions aren’t surfaced by it. - New: public
IATO_MCP_Elementor_Router::parse_condition_string()helper that returns the structured representation of a single Elementor condition string without applying it to any URL context. Added as a sibling to the v1.8.1 matcher rather than a refactor — keeps the verified matcher untouched. - Compat:
rendering_post_idsemantics still UNCHANGED from v1.7.x.resolve_url‘s v1.8.0 additive contract fully preserved.
1.8.2
- Fix:
get_site_settingsno longer corruptspermalink_structure,title, ortagline. The five-field tool was wrapping every value insanitize_text_field(), which calls_sanitize_text_fields()— a function that repeatedly strips%[a-f0-9]{2}octets as a transport-safety measure for URL-encoded strings. Applied to fields that legitimately carry literal%xxcontent, that’s actively destructive. Three field-level changes follow, with deliberately distinct framing because they’re different categories of fix:- (a)
permalink_structure— pure bug fix. The sanitized output was always wrong for this field. WordPress’s permalink structure legitimately carries%category%,%postname%,%year%,%monthnum%,%day%,%post_id%, and%author%as literal placeholder tokens;_sanitize_text_fieldsate the%xxprefixes of those tokens (e.g./%category%/%postname%/came back as/tegory%/%postname%/). Now returns the raw value as WordPress stores it — matching what WP core uses internally when generating URLs. - (b)
titleandtagline— broader behavior change. These now return the raw stored value, not thesanitize_text_field-processed form. Practical implications beyond%xx: HTML entities, leading/trailing whitespace, line breaks, and collapsed multiple spaces in the site title or tagline now surface to the read tool instead of being stripped or collapsed. Rationale: an admin read tool’s contract is to surface what’s stored, not a display-rendered form. Most sites won’t notice the change because typical site titles are plain text; sites with unusual characters in their title/tagline will see the raw form they actually stored. - (c)
admin_emailandtimezone— unchanged. Explicit decision: these value types don’t legitimately carry%xxin practice. PHP timezone identifiers are a controlled list with no%;admin_email‘s RFC percent-encoding form is exceedingly rare in single-mailbox use.sanitize_text_fieldis WP-canonical for these and stays.
- (a)
- Downstream: v1.8.1’s archive-detection path reads
category_base/tag_basedirectly viaget_option()inside the router (not via this MCP tool) and is unaffected. No internal callers ofget_site_settingsoutput exist in the codebase. External MCP callers now receive faithful DB values for the three fixed fields.
1.8.1
- Fix:
resolve_urlcorrectly identifies Elementor Theme Builder archive templates as the renderer for Yoast-stripped category URLs (e.g./build/instead of/category/build/). Previously such URLs returnedroute_type=404even though a Theme Builder template was rendering them. Root cause was two independent bugs:- Detect_archive_info gap. The URL classifier only recognised the default
/category/<slug>/,/tag/<slug>/, and/author/<slug>/patterns. Sites with Yoast’s “Remove the categories prefix” enabled — or with a customcategory_base/tag_baseconfigured in Settings > Permalinks — served archives at URLs that didn’t match those patterns, so the classifier returned null and the shadowing check had no archive context to evaluate against. A three-stage cascade now handles all three cases: default patterns first (zero new cost on standard sites), then configured-base patterns, then a bounded reverse lookup throughget_term_link()that goes through any activeterm_linkfilter (Yoast, RankMath, etc.) and matches the resulting URL against the input. - Shadowing-dispatch bug.
detect_theme_builder_shadowingtreated Elementor Pro’sfind_via_theme_builder_modulefalsereturn (Pro loaded butget_documents_for_locationmatched nothing) as authoritative, returning thatfalsedirectly and never reaching thefind_via_conditions_metafallback. In REST context — which is 100% of MCP traffic —get_documents_for_locationis bound to the current$wp_query(the REST endpoint, not the URL being asked about) so it consistently returns nothing; the dispatch bug meant the conditions-meta scan that DOES evaluate against our explicit URL context was unreachable on every Pro-installed site. Fix is a one-line change fromif ( null !== $found )toif ( is_array( $found ) )so bothfalseandnullfall through to the meta scan;array(a positive match) still returns immediately. Bug has existed since v1.7.x; v1.8.0’s archive plumbing landed correctly but the dispatch bug blocked it from firing.
- Detect_archive_info gap. The URL classifier only recognised the default
- Bonus: the dispatch fix also restores singular page shadowing detection on Pro-installed sites.
get_post(id, include_shadowing:true)now correctly surfacesis_shadowed_bywhen an Elementor Theme Builder single template overrides the slug-based render. Same dispatch bug had been blocking this since v1.7.x. rendering_post_idsemantics UNCHANGED from v1.7.x. v1.8.0’s additiveresolve_urlcontract fully preserved — all new fields (effective_render_id,template{},shadowed_route_type, etc.) populated correctly by v1.8.0 already; v1.8.1 just makes the shadowing detection that fills them actually fire in REST context.- Documented limitation: plugins that strip the category base via raw
.htaccessrewrites without using theterm_linkfilter won’t be detected by Stage 3 ofdetect_archive_info. Rare; rely on aterm_link-based stripper (Yoast, RankMath, etc.) until a workaround is needed.
1.8.0
- Fix:
resolve_urlnow resolves Elementor Theme Builder archive routes. Archive URLs served by a Theme Builder template (e.g. a category or CPT archive whose render is provided by anelementor_librarydocument) previously returnedroute_type=404because the conditions evaluator only matchedinclude/singular/...patterns againsturl_to_postid()‘s post ID — which is 0 on archives. The conditions parser now evaluatesinclude/archive/...patterns (taxonomies, terms, authors, CPT archives,in_taxonomy,post_archive) against URL-derived context, so archive shadowing is correctly detected.find_via_theme_builder_modulealso captures the matching location intotemplate_type. The condition string that fired is surfaced astemplate.condition_matchedfor callers who need to see the match logic. - New:
resolve_urlresponse gains four additive fields.rendering_post_idsemantics are UNCHANGED — still the canonical/slug-based post (now normalized tonullinstead of0on archives so=== nullchecks work). The new fields are:rendering_post_type,effective_render_id(single field answering “what actually renders” — template ID when shadowed, canonical post ID otherwise,nullonly on a true 404),effective_render_post_type,shadowed_route_type(route the URL would have had absent the template), and a structuredtemplate{template_id,template_type,condition_matched,builder}object present when shadowing applies.rendering_template_idcontinues to work exactly as documented; the new fields are additive only. - New:
find_elementor_widgetsauto-resolves revision IDs to their parent post. Passing a revision ID viapost_idspreviously returned matches tagged with the revision’s own ID, leaking the parent only through theNNN-revision-vNslug — the brute-force discovery path that motivated this work. Revision IDs are now mapped to their parent viawp_is_post_revision(), deduped, and each match scanned from a revision input carriesresolved_from_revision_idso callers can see the mapping. Default auto-scan also tightenspost_statusfromanyto[publish, draft, pending, private]— trash and auto-draft are no longer scanned by default. - New:
find_elementor_widgetsfilter gains acontainsoperator (case-insensitive substring match against scalar settings), alongside the existingeq|ne|in|nin|exists. Useful for finding a widget by its content rather than exact-match on a heading — e.g.setting.editor.contains="some phrase". Scalar-only by design; never recurses into nested settings arrays. Theregexoperator is deferred to a future release where it can get proper backtracking-DoS guards. - Docs:
get_posts,find_elementor_widgets, andresolve_urldescriptions now state the current scope honestly.get_postsnotes thatpost_type=anyexpands to[post, page]and does NOT returnelementor_library.find_elementor_widgetsnotes that templates are not yet scanned. Both point at the upcoming Layer 2 work. - Note on disclosure surface: the new fields (
condition_matched,template_type,effective_render_*) widen what an authenticated caller can learn about site structure on a per-URL basis.resolve_urlcontinues to require only authentication (no capability check), unchanged from v1.7.x — Theme Builder shadowing was already disclosed per-URL since v1.5. The forthcoming template-listing tool (Layer 2) will gate full enumeration behindedit_posts.
1.7.2
- Fix:
create_mediano longer hard-rejects payloads that omitsource.type. v1.7.1 began requiring an explicittypefield, breaking callers that worked under v1.6.x where the type was inferred from the presence ofsource.data(base64) orsource.url(URL ingestion). Restored that inference — supply either field and the right mode is picked automatically. Explicittypecontinues to work and remains the documented preferred form. - Fix: URL ingestion no longer rejects the site’s own host. Plugins installed on
example.comcould not ingesthttps://example.com/wp-content/uploads/foo.jpgwithout manually addingexample.comto the URL allowlist — the allowlist defends against fetching arbitrary external hosts, not the site’s own media library, and forcing every admin to allowlist their own domain was the most common papercut on this tool. The site’shome_url()andsite_url()host(s) are now implicitly trusted. The SSRF IP-resolution guard (check_host_resolves_publicly) still runs in both branches, so the bypass only skips the manual allowlist; private/loopback/link-local IPs continue to be rejected. - Improved:
create_mediatool description rewritten to reflect real-world transport behaviour. Base64 is documented as suitable ONLY for tiny assets (favicons, sprite icons, ~4 KB of decoded image); larger JSON-RPC payloads are truncated by the MCP transport before reaching the plugin, which presents as the call hanging silently. URL ingestion is named as the path for anything bigger, with explicit warnings that share/viewer pages (Google Drive share links, Dropbox?dl=0) return HTML rather than the file. The previous “~100 KB” guidance was wrong in practice and trained agents to attempt uploads that would silently hang. - Improved:
file_too_largeerrors on the base64 path now mention URL ingestion as the alternative, mirroring the helpful tone of the existingurl_source_disablederror. The agent gets a clear next step instead of an opaque size-cap message.
1.7.1
- Fix: the four Media Uploads settings shipped in 1.7.0 (
iato_mcp_media_url_source_enabled,iato_mcp_media_url_host_allowlist,iato_mcp_media_max_upload_size,iato_mcp_media_upload_rate_limit) silently failed to persist on Save. The UI rendered correctly and the fields were properly registered withregister_setting(), but the General-tab form is hijacked through an admin-ajax handler (some hosts 503 onoptions.phpPOSTs due to upstream WAF/timeout rules) and that handler hardcoded the keys it persisted — anything not explicitly listed fell off the floor. 1.7.1 extendsajax_save_settings()to call the matching sanitize-and-update path for each of the four media keys, mirroring the existingiato_mcp_api_key/iato_mcp_crawl_id/iato_mcp_toolslines. - Docs: CLAUDE.md gains a “Release Checklist (adding a new admin setting)” section calling out the three places a new option must land —
register_setting(), the rendered<input name="...">, and the AJAX persist handler. Same shape as the existing tool-release checklist; closes the structural failure mode that produced this 1.7.0 1.7.1 follow-up.
1.7.0
- New:
Settings > IATO MCPnow includes a Media Uploads card. The four media settings (iato_mcp_media_url_source_enabled,iato_mcp_media_url_host_allowlist,iato_mcp_media_max_upload_size,iato_mcp_media_upload_rate_limit) were registered and enforced at runtime in 1.6.0 but never surfaced in admin UI, so the only way to enable URL-source ingestion or configure the host allowlist was via WP-CLI or a direct database edit. Theurl_source_disablederror message returned bycreate_mediacontinues to point admins to “Settings > IATO MCP > Media uploads” — that path now exists. - New: Diagnostics page gains a “Recent media uploads” panel showing the last 100
create_mediacalls with their full per-phase trace, outcome badge, error code, attachment metadata (MIME, dimensions, size), and total duration. Each row expands inline via a native<details>element to show the phase-by-phase timing. Triage that previously required enablingWP_DEBUG_LOGmid-session and tailing the host’s PHP error log is now one click on the Diagnostics tab. The on-diskerror_log()mirror is preserved for environments that already aggregate logs centrally. - New: backing infrastructure —
IATO_MCP_Media_Phase_Logclass and{prefix}iato_mcp_media_phase_logtable store one ring-buffered row percreate_mediacall. The deferred-subsizes cron path threads itsreq_idthroughwp_schedule_single_eventand appends anasync-subsizes-donephase to the parent row on completion, so a single Diagnostics row captures the entire end-to-end timeline of an upload — including the async tick that lands minutes later. DB writes are wrapped in try/catch so an observability hiccup cannot regresscreate_mediaitself. …
