{"id":294616,"date":"2026-04-24T17:15:08","date_gmt":"2026-04-24T17:15:08","guid":{"rendered":"https:\/\/wordpress.org\/plugins\/iato-mcp\/"},"modified":"2026-05-15T18:05:31","modified_gmt":"2026-05-15T18:05:31","slug":"iato-mcp","status":"publish","type":"plugin","link":"https:\/\/ky.wordpress.org\/plugins\/iato-mcp\/","author":23472460,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_crdt_document":"","version":"1.10.0","stable_tag":"1.10.0","tested":"6.9.4","requires":"6.2","requires_php":"8.0","requires_plugins":null,"header_name":"IATO MCP","header_author":"IATO","header_description":"Exposes an MCP server from any self-hosted WordPress install, enabling IATO analyze-and-fix workflows via Claude Desktop and other AI clients.","assets_banners_color":"809d9a","last_updated":"2026-05-15 18:05:31","external_support_url":"","external_repository_url":"","donate_link":"","header_plugin_uri":"https:\/\/iato.ai\/wordpress-mcp","header_author_uri":"https:\/\/iato.ai","rating":0,"author_block_rating":0,"active_installs":0,"downloads":705,"num_ratings":0,"support_threads":0,"support_threads_resolved":0,"author_block_count":0,"sections":["description","installation","faq","changelog"],"tags":{"1.1.10":{"tag":"1.1.10","author":"iatoai","date":"2026-04-24 17:15:11"},"1.1.11":{"tag":"1.1.11","author":"iatoai","date":"2026-04-24 18:15:19"},"1.1.12":{"tag":"1.1.12","author":"iatoai","date":"2026-04-24 18:28:25"},"1.10.0":{"tag":"1.10.0","author":"iatoai","date":"2026-05-15 18:05:31"},"1.2.0":{"tag":"1.2.0","author":"iatoai","date":"2026-04-28 01:12:00"},"1.2.1":{"tag":"1.2.1","author":"iatoai","date":"2026-04-28 01:32:21"},"1.2.2":{"tag":"1.2.2","author":"iatoai","date":"2026-04-28 12:25:07"},"1.2.3":{"tag":"1.2.3","author":"iatoai","date":"2026-04-28 12:50:20"},"1.2.4":{"tag":"1.2.4","author":"iatoai","date":"2026-04-28 14:04:56"},"1.3.0":{"tag":"1.3.0","author":"iatoai","date":"2026-04-28 17:29:19"},"1.3.1":{"tag":"1.3.1","author":"iatoai","date":"2026-04-28 18:00:40"},"1.3.2":{"tag":"1.3.2","author":"iatoai","date":"2026-04-28 18:15:13"},"1.3.3":{"tag":"1.3.3","author":"iatoai","date":"2026-04-28 18:32:19"},"1.3.4":{"tag":"1.3.4","author":"iatoai","date":"2026-04-28 18:50:53"},"1.3.5":{"tag":"1.3.5","author":"iatoai","date":"2026-04-28 19:11:26"},"1.4.0":{"tag":"1.4.0","author":"iatoai","date":"2026-05-01 14:03:55"},"1.4.1":{"tag":"1.4.1","author":"iatoai","date":"2026-05-01 17:32:13"},"1.4.10":{"tag":"1.4.10","author":"iatoai","date":"2026-05-06 01:14:50"},"1.4.2":{"tag":"1.4.2","author":"iatoai","date":"2026-05-01 18:02:17"},"1.4.3":{"tag":"1.4.3","author":"iatoai","date":"2026-05-01 18:22:52"},"1.4.4":{"tag":"1.4.4","author":"iatoai","date":"2026-05-02 16:21:10"},"1.4.5":{"tag":"1.4.5","author":"iatoai","date":"2026-05-04 14:15:14"},"1.4.6":{"tag":"1.4.6","author":"iatoai","date":"2026-05-04 14:30:17"},"1.4.7":{"tag":"1.4.7","author":"iatoai","date":"2026-05-04 14:50:48"},"1.4.8":{"tag":"1.4.8","author":"iatoai","date":"2026-05-05 17:42:52"},"1.4.9":{"tag":"1.4.9","author":"iatoai","date":"2026-05-05 18:22:14"},"1.5.0":{"tag":"1.5.0","author":"iatoai","date":"2026-05-13 16:15:42"},"1.6.0":{"tag":"1.6.0","author":"iatoai","date":"2026-05-13 21:23:02"},"1.6.1":{"tag":"1.6.1","author":"iatoai","date":"2026-05-13 21:57:12"},"1.6.2":{"tag":"1.6.2","author":"iatoai","date":"2026-05-13 22:30:33"},"1.6.3":{"tag":"1.6.3","author":"iatoai","date":"2026-05-13 23:26:37"},"1.6.4":{"tag":"1.6.4","author":"iatoai","date":"2026-05-14 00:37:35"},"1.7.0":{"tag":"1.7.0","author":"iatoai","date":"2026-05-14 01:45:38"},"1.7.1":{"tag":"1.7.1","author":"iatoai","date":"2026-05-14 02:10:10"},"1.7.2":{"tag":"1.7.2","author":"iatoai","date":"2026-05-14 11:09:38"},"1.8.0":{"tag":"1.8.0","author":"iatoai","date":"2026-05-14 16:24:06"},"1.8.1":{"tag":"1.8.1","author":"iatoai","date":"2026-05-14 19:33:32"},"1.8.2":{"tag":"1.8.2","author":"iatoai","date":"2026-05-14 20:05:51"}},"upgrade_notice":{"1.4.10":"<p>The JSON config snippets the plugin emits now use a unique-per-site inner <code>mcpServers<\/code> key derived from the site&#039;s hostname (e.g. <code>iato-garennebigby-dev<\/code>) instead of the hardcoded <code>iato-wordpress<\/code>. Lets agencies paste config snippets from many WordPress installs into a single Claude Desktop config without silent overwrites. Existing connections keep working unchanged.<\/p>","1.4.9":"<p>Docs-only release: adds the plugin demo video to the WordPress.org plugin page Description. No code changes. Safe to skip if you&#039;ve already updated to 1.4.8.<\/p>","1.4.8":"<p>Adds page-builder-aware safety rails to the MCP <code>initialize<\/code> response: a dynamic instructions string telling the AI agent which write tools to use for which builder, with a mandatory check-first rule before any content edit. Closes a silent-failure class where <code>update_post<\/code> on Elementor-built posts succeeded in the database but never reached the frontend. Also adds Beaver Builder per-post detection.<\/p>","1.4.7":"<p>Fixes a misleading UX in Settings where IATO Platform and Crawl Management tool toggles appeared enabled even when no IATO API key was configured \u2014 making the checkboxes placebo. Toggles are now visually disabled with an inline hint until an API key is set. No backend or auth changes.<\/p>","1.4.6":"<p>Completes the rollback Settings UI fix from 1.4.5 by adding rollback to the second gating constant (<code>TOOL_CATEGORIES<\/code>) the rendering loop actually iterates \u2014 so the checkbox now actually appears under a new &quot;Safety&quot; category. Also unifies the inner server key in the Settings page config snippet from <code>wordpress<\/code> to <code>iato-wordpress<\/code> to match the dismissible notice.<\/p>","1.4.5":"<p>Fixes the rollback MCP tool being invisible on the Settings page and silently stripped from <code>iato_mcp_tools<\/code> on every Settings save (a bug present since v1.4.0 introduced rollback). One-shot migration auto-restores rollback for affected installs on upgrade. Also makes the <code>initialize<\/code> capability advertisement honest about whether rollback is actually registered.<\/p>","1.4.4":"<p>Fixes the OAuth flow: clicking Approve on the consent screen now correctly redirects back to the OAuth client (Claude, Cursor, etc.) with an authorization code instead of dumping users on \/wp-admin. The connector framework on the client side then transitions to &quot;Connected&quot; as expected. Required for anyone trying to connect via Claude.ai&#039;s Add Connector or Claude Desktop&#039;s Connectors UI.<\/p>","1.4.3":"<p>Restructures the dismissible &quot;Ready to Connect&quot; admin notice so its two connection methods (Connectors UI \/ Claude Desktop config file) are presented as mutually-exclusive options instead of a confusing three-step sequence. No code-path or auth-handler changes \u2014 purely a clarity fix in the onboarding notice.<\/p>","1.4.2":"<p>Makes the Application Password auth path documented in the v1.4.1 setup wizard actually work (the auth handler was hard-rejecting non-Bearer requests, so wizard Methods 2 and 3 were returning 401). Also fixes the dismissible setup notice to emit a Claude-Desktop-compatible stdio-bridge config, and relabels the Settings page hero-card so its audience (HTTP MCP clients) is unambiguous. Recommended upgrade for anyone on 1.4.1.<\/p>","1.4.1":"<p>Setup wizard restructured around the three real connection paths (Connectors UI \/ Direct HTTP \/ stdio bridge), and the stdio JSON snippet now references the actual <code>mcp-remote<\/code> npm package with an env-var credential pattern. Recommended for any new install; existing connections are unaffected.<\/p>","1.4.0":"<p>Adds a <code>rollback<\/code> MCP tool and change-receipt coverage for <code>update_post<\/code> \/ <code>create_post<\/code>, closing the gap that previously left the two highest-volume write tools without an audit trail. Claude can now undo any tracked change in a single tool call.<\/p>","1.3.5":"<p>Docs-only release: corrects a stale FAQ tool count and adds widget-flavored example prompts. No code changes; safe to skip if you&#039;ve already updated to 1.3.4.<\/p>","1.3.4":"<p>Drops <code>change_receipt<\/code> from <code>update_elementor_widgets_bulk<\/code> per-result rows (still persisted to the audit table; bulk callers query by post_id). Lands the 4-page H1-flip benchmark under the spec&#039;s<\/p>","1.3.3":"<p>Slims v2 write responses by ~93 bytes per result by eliding the previous_revision echo when the caller didn&#039;t pass if_revision. Lands the canonical 4-page bulk benchmark under the spec&#039;s<\/p>","1.3.2":"<p>Slims v2 write-tool responses by ~600 bytes per update by removing redundant change_receipt fields. Brings 4-page bulk sweeps under the 2 KB spec target. No API breakage \u2014 the slim receipt still carries the change_id for downstream lookup.<\/p>","1.3.1":"<p>Fixes the Elementor v2 bulk + find tools rejecting every request with auth_denied (capability-check mismatch with the bearer auth model). Adds an idempotent migration so existing installs see the new v2 tools enabled automatically on upgrade. Required for anyone on 1.3.0.<\/p>","1.3.0":"<p>Adds widget-grained Elementor tools (v2 surface) \u2014 patch a single widget without re-uploading the whole document, with optimistic concurrency and idempotency. Existing v1 tools are unchanged. Recommended upgrade for anyone editing Elementor pages from Claude.<\/p>","1.2.4":"<p>Fixes <code>list_iato_crawls<\/code> returning the wrong identifier (numeric DB id instead of the UUID), which broke the chain into the other bridge tools. Adds dual-key envelope resilience for the same endpoint. Recommended upgrade for anyone on 1.2.0\u20131.2.3.<\/p>","1.2.3":"<p>Sends workspace_id as a JSON integer so the platform&#039;s Pydantic binding accepts it. Required to make the crawl-control tools fully functional; recommended upgrade for anyone on 1.2.0\u20131.2.2.<\/p>","1.2.2":"<p>Completes the workspace_id scoping fix from 1.2.1. After upgrading, click Test connection in Settings &gt; IATO MCP once to populate the workspace_id, then crawl management will work end-to-end.<\/p>","1.2.1":"<p>Fixes workspace_id scoping for the new crawl-management tools and a PHP 8.2-only return type that broke installs on PHP 8.0\/8.1. Recommended upgrade for anyone on 1.2.0.<\/p>","1.2.0":"<p>Adds three crawl-control MCP tools so Claude can start, check, and list IATO crawls without leaving the conversation. Admin only for <code>start_iato_crawl<\/code>.<\/p>","1.1.12":"<p>Adds Plugin URI and contextual links to iato.ai throughout the listing. No code changes.<\/p>","1.1.11":"<p>Readme accuracy pass. No code changes.<\/p>","1.1.10":"<p>First stable release to the WordPress.org directory.<\/p>"},"ratings":[],"assets_icons":{"icon-128x128.png":{"filename":"icon-128x128.png","revision":3514829,"resolution":"128x128","location":"assets","locale":"","width":128,"height":128},"icon-256x256.png":{"filename":"icon-256x256.png","revision":3514829,"resolution":"256x256","location":"assets","locale":"","width":256,"height":256}},"assets_banners":{"banner-1544x500.png":{"filename":"banner-1544x500.png","revision":3514829,"resolution":"1544x500","location":"assets","locale":"","width":1544,"height":400},"banner-772x250.png":{"filename":"banner-772x250.png","revision":3514829,"resolution":"772x250","location":"assets","locale":"","width":772,"height":250}},"assets_blueprints":{},"all_blocks":[],"tagged_versions":["1.1.10","1.1.11","1.1.12","1.10.0","1.2.0","1.2.1","1.2.2","1.2.3","1.2.4","1.3.0","1.3.1","1.3.2","1.3.3","1.3.4","1.3.5","1.4.0","1.4.1","1.4.10","1.4.2","1.4.3","1.4.4","1.4.5","1.4.6","1.4.7","1.4.8","1.4.9","1.5.0","1.6.0","1.6.1","1.6.2","1.6.3","1.6.4","1.7.0","1.7.1","1.7.2","1.8.0","1.8.1","1.8.2"],"block_files":[],"assets_screenshots":{"screenshot-1.png":{"filename":"screenshot-1.png","revision":3514833,"resolution":"1","location":"assets","locale":"","width":1544,"height":898},"screenshot-2.png":{"filename":"screenshot-2.png","revision":3514833,"resolution":"2","location":"assets","locale":"","width":1544,"height":382},"screenshot-3.png":{"filename":"screenshot-3.png","revision":3514833,"resolution":"3","location":"assets","locale":"","width":1544,"height":1357},"screenshot-4.png":{"filename":"screenshot-4.png","revision":3514833,"resolution":"4","location":"assets","locale":"","width":1544,"height":1109}},"screenshots":{"1":"Settings page \u2014 MCP connection info with endpoint URL and API key","2":"Settings page \u2014 IATO Platform configuration and tool toggles","3":"Setup wizard \u2014 auto-generated Claude Desktop configuration","4":"OAuth authorization screen \u2014 approve AI client connections"},"jetpack_post_was_ever_published":false},"plugin_section":[],"plugin_tags":[2353,229563,242115,186,1557],"plugin_category":[55],"plugin_contributors":[261008],"plugin_business_model":[],"class_list":["post-294616","plugin","type-plugin","status-publish","hentry","plugin_tags-ai","plugin_tags-claude","plugin_tags-mcp","plugin_tags-seo","plugin_tags-sitemap","plugin_category-seo-and-marketing","plugin_contributors-iatoai","plugin_committers-iatoai"],"banners":{"banner":"https:\/\/ps.w.org\/iato-mcp\/assets\/banner-772x250.png?rev=3514829","banner_2x":"https:\/\/ps.w.org\/iato-mcp\/assets\/banner-1544x500.png?rev=3514829","banner_rtl":false,"banner_2x_rtl":false},"icons":{"svg":false,"icon":"https:\/\/ps.w.org\/iato-mcp\/assets\/icon-128x128.png?rev=3514829","icon_2x":"https:\/\/ps.w.org\/iato-mcp\/assets\/icon-256x256.png?rev=3514829","generated":false},"screenshots":[{"src":"https:\/\/ps.w.org\/iato-mcp\/assets\/screenshot-1.png?rev=3514833","caption":"Settings page \u2014 MCP connection info with endpoint URL and API key"},{"src":"https:\/\/ps.w.org\/iato-mcp\/assets\/screenshot-2.png?rev=3514833","caption":"Settings page \u2014 IATO Platform configuration and tool toggles"},{"src":"https:\/\/ps.w.org\/iato-mcp\/assets\/screenshot-3.png?rev=3514833","caption":"Setup wizard \u2014 auto-generated Claude Desktop configuration"},{"src":"https:\/\/ps.w.org\/iato-mcp\/assets\/screenshot-4.png?rev=3514833","caption":"OAuth authorization screen \u2014 approve AI client connections"}],"raw_content":"<!--section=description-->\n<p>WordPress.com has a built-in MCP server. Now self-hosted WordPress does too.<\/p>\n\n<p><a href=\"https:\/\/iato.ai\/wordpress-mcp\">IATO MCP<\/a> 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 \u2014 all in a single conversation.<\/p>\n\n<p>https:\/\/www.youtube.com\/watch?v=gSX6Vc9Yask<\/p>\n\n<h4>How it works<\/h4>\n\n<ol>\n<li>Install and activate the plugin<\/li>\n<li>Follow the setup wizard \u2014 copy the config into Claude Desktop, or use \"Add Custom Connector\" with your site URL<\/li>\n<li>Connect your <a href=\"https:\/\/iato.ai\">IATO account<\/a> for AI-powered analysis (<a href=\"https:\/\/iato.ai\">free trial<\/a> up to 500 pages)<\/li>\n<\/ol>\n\n<h4>What Claude can do<\/h4>\n\n<p><strong>Without an IATO account (45 WordPress tools):<\/strong><\/p>\n\n<ul>\n<li>Read and edit posts, pages, and media<\/li>\n<li>Create new posts and pages with excerpt support<\/li>\n<li>Update SEO titles and meta descriptions (Yoast SEO, RankMath, SEOPress)<\/li>\n<li>Update canonical URLs<\/li>\n<li>Update image alt text<\/li>\n<li>Upload new images to the media library (base64 by default; URL ingestion optional with admin allowlist + full SSRF protection)<\/li>\n<li>Set and clear the featured image on any post<\/li>\n<li>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)<\/li>\n<li>Set per-post theme + builder page settings (hide title, sidebar layout, content layout, etc.) on Astra, Kadence, GeneratePress, and Elementor in one call<\/li>\n<li>Read and edit navigation menus<\/li>\n<li>Manage categories, tags, and taxonomy terms<\/li>\n<li>Manage JSON-LD structured data<\/li>\n<li>Manage redirect rules<\/li>\n<li>Read and write Elementor page builder data<\/li>\n<li>Widget-grained Elementor edits with optimistic concurrency, idempotency, and bulk operations<\/li>\n<li>Clone the styling of an existing Elementor post in one call via <code>update_elementor_data(..., inherit_settings_from: &lt;id&gt;)<\/code><\/li>\n<li>Resolve URLs to their rendering post (Theme Builder shadowing detection)<\/li>\n<li>Search content across the site<\/li>\n<li>Read site info and settings<\/li>\n<li>Read and filter comments<\/li>\n<li>One-call rollback for any tracked write \u2014 every change emits a receipt with a stable <code>change_id<\/code>; pass it back to the <code>rollback<\/code> tool and the original value is restored<\/li>\n<\/ul>\n\n<p><strong>With an <a href=\"https:\/\/iato.ai\">IATO account<\/a> (12 bridge tools \u2014 full analyze-and-fix pipeline):<\/strong><\/p>\n\n<ul>\n<li>Start a new crawl of your site directly from Claude (admin only)<\/li>\n<li>Check crawl status and list recent crawl jobs<\/li>\n<li>Run a full SEO audit and fix title, meta description, and alt text issues automatically<\/li>\n<li>Identify orphan pages not linked from any navigation menu<\/li>\n<li>Audit navigation menus for gaps and missing sections<\/li>\n<li>Surface thin content with specific improvement recommendations<\/li>\n<li>Map broken links to source posts for direct editing<\/li>\n<li>Analyze site taxonomy and suggest consolidations<\/li>\n<li>Get AI-prioritized suggestions across all areas<\/li>\n<li>Flag slow pages with contributing performance factors<\/li>\n<\/ul>\n\n<h4>Supported SEO plugins<\/h4>\n\n<ul>\n<li>Yoast SEO<\/li>\n<li>RankMath<\/li>\n<li>SEOPress<\/li>\n<li>Falls back to native WordPress title if none detected<\/li>\n<\/ul>\n\n<h4>Example prompts<\/h4>\n\n<blockquote>\n  <p>\"Crawl my site and fix all missing meta descriptions\"<\/p>\n  \n  <p>\"Show me pages that aren't in any navigation menu and add them to the right place\"<\/p>\n  \n  <p>\"What are the most impactful improvements I can make to my site right now?\"<\/p>\n  \n  <p>\"Find all broken links and tell me which posts contain them\"<\/p>\n  \n  <p>\"Audit my categories and tags and suggest consolidations\"<\/p>\n  \n  <p>\"Set every H2 heading in these Elementor posts to H1\"<\/p>\n  \n  <p>\"Find all button widgets on the site and change their color to #ff0000\"<\/p>\n<\/blockquote>\n\n<h4>External Services<\/h4>\n\n<p>This plugin connects to the following external service when configured:<\/p>\n\n<p><strong>IATO API<\/strong> (<a href=\"https:\/\/iato.ai\">https:\/\/iato.ai<\/a>) \u2014 When you enter an IATO API key in the plugin settings, the plugin sends requests to <code>https:\/\/iato.ai\/api<\/code> 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.<\/p>\n\n<ul>\n<li><a href=\"https:\/\/iato.ai\/terms\">IATO Terms of Service<\/a><\/li>\n<li><a href=\"https:\/\/iato.ai\/privacy\">IATO Privacy Policy<\/a><\/li>\n<\/ul>\n\n<p>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 \u2014 no data is sent to third parties during authentication.<\/p>\n\n<!--section=installation-->\n<ol>\n<li>Upload the plugin files to <code>\/wp-content\/plugins\/iato-mcp\/<\/code> or install via the WordPress plugin directory<\/li>\n<li>Activate the plugin via the Plugins menu in WordPress<\/li>\n<li>Follow the setup wizard that appears \u2014 it provides the JSON config for Claude Desktop<\/li>\n<li>In Claude Desktop, either paste the JSON config or use \"Add Custom Connector\" and enter your site URL<\/li>\n<li>Optionally, go to Settings &gt; IATO MCP to enter your IATO API key for the full analysis pipeline<\/li>\n<\/ol>\n\n<p>For detailed setup instructions, see the <a href=\"https:\/\/iato.ai\/wordpress-mcp-docs\">IATO MCP documentation<\/a>.<\/p>\n\n<!--section=faq-->\n<dl>\n<dt id=\"do%20i%20need%20an%20iato%20account%3F\"><h3>Do I need an IATO account?<\/h3><\/dt>\n<dd><p>No. The plugin works standalone for reading and editing WordPress content with 40 built-in tools. An <a href=\"https:\/\/iato.ai\">IATO account<\/a> (<a href=\"https:\/\/iato.ai\">free trial<\/a> 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.<\/p><\/dd>\n<dt id=\"which%20wordpress%20version%20is%20required%3F\"><h3>Which WordPress version is required?<\/h3><\/dt>\n<dd><p>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.<\/p><\/dd>\n<dt id=\"does%20this%20work%20on%20shared%20hosting%3F\"><h3>Does this work on shared hosting?<\/h3><\/dt>\n<dd><p>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.<\/p><\/dd>\n<dt id=\"which%20ai%20clients%20are%20supported%3F\"><h3>Which AI clients are supported?<\/h3><\/dt>\n<dd><p>Any MCP-enabled client: Claude Desktop, Cursor, VS Code with GitHub Copilot, and any client that supports the Streamable HTTP MCP transport.<\/p><\/dd>\n<dt id=\"how%20does%20authentication%20work%3F\"><h3>How does authentication work?<\/h3><\/dt>\n<dd><p>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 Desktop's \"Add Custom Connector\" flow which handles OAuth 2.0 with PKCE automatically.<\/p><\/dd>\n<dt id=\"why%20does%20the%20plugin%20support%20two%20auth%20methods%3F\"><h3>Why does the plugin support two auth methods?<\/h3><\/dt>\n<dd><p>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 \u2014 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 <code>\/wp-json\/iato-mcp\/v1\/message<\/code> and are validated by <code>class-auth.php<\/code>. You don't have to choose \u2014 paste your Bearer token into the IATO platform connection, generate an Application Password for Claude Desktop, and the same plugin handles both.<\/p><\/dd>\n<dt id=\"is%20my%20content%20sent%20to%20iato%20or%20anthropic%3F\"><h3>Is my content sent to IATO or Anthropic?<\/h3><\/dt>\n<dd><p>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.<\/p><\/dd>\n<dt id=\"can%20i%20control%20which%20tools%20are%20available%3F\"><h3>Can I control which tools are available?<\/h3><\/dt>\n<dd><p>Yes. Go to Settings &gt; IATO MCP to enable or disable individual tools. You can turn off any tool you don't want AI clients to access.<\/p><\/dd>\n<dt id=\"can%20ai%20clients%20upload%20arbitrary%20files%20to%20my%20media%20library%3F\"><h3>Can AI clients upload arbitrary files to my media library?<\/h3><\/dt>\n<dd><p>Only images, and only when the calling user has the <code>upload_files<\/code> capability. The <code>create_media<\/code> tool enforces an image-only MIME allowlist (JPEG, PNG, GIF, WebP, AVIF) verified against actual file bytes \u2014 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\u00d78000) are rejected, as are filenames containing <code>.php<\/code>, <code>.phtml<\/code>, or <code>.htaccess<\/code>. 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 a <code>change_receipt<\/code> \u2014 rolling back fully deletes the attachment file. All four limits are configurable from Settings &gt; IATO MCP.<\/p><\/dd>\n\n<\/dl>\n\n<!--section=changelog-->\n<h4>1.10.0<\/h4>\n\n<ul>\n<li>Security: <code>IATO_MCP_Auth::require_cap()<\/code> now actually enforces the capability argument. Previously the function checked only whether the request was authenticated and returned <code>true<\/code> regardless of the cap string passed \u2014 a documented but long-deferred limitation (the file's own docblock acknowledged it as a v1.6 hardening item). Every existing <code>require_cap()<\/code> call site (<code>edit_posts<\/code> on write tools, <code>manage_options<\/code> on <code>get_site_settings<\/code>, <code>upload_files<\/code> on <code>create_media<\/code>, etc.) was cosmetic until this release. Now real.<\/li>\n<li>Mechanism: <code>authenticate()<\/code> carries the authenticated <code>WP_User<\/code> object through to <code>require_cap()<\/code> for the Application Password path. <code>require_cap()<\/code> calls <code>user_can($user, $cap)<\/code> against that user. Bearer plugin-key authentication remains documented full-administrative-access (intentional \u2014 the key itself is the gate; security comes from key issuance being admin-only); the change applies to per-user auth paths only.<\/li>\n<li>Observable contract change: any Application Password user below admin level who previously could call <code>manage_options<\/code>-gated MCP tools will now be correctly rejected. This is the security boundary tightening \u2014 semver minor, not patch, even though the surface change is \"tiny\" by line count. The <code>edit_posts<\/code> auth-time baseline in <code>authenticate()<\/code> is preserved (Subscribers still rejected at auth time, not per-tool time).<\/li>\n<li>Audit: every <code>require_cap()<\/code> call site reviewed under enforcement. 21 sites confirmed-correct without code change. Two structural changes:\n\n<ul>\n<li><strong>Menus (4 tools: <code>update_menu_item<\/code>, <code>create_menu_item<\/code>, <code>delete_menu_item<\/code>, <code>update_menu_item_details<\/code>)<\/strong> \u2014 switched from <code>manage_options<\/code> to <code>edit_theme_options<\/code>, the WP-canonical cap for nav-menu structure (matches <code>wp-admin\/nav-menus.php<\/code>). Functionally identical on default WP (both admin-only); observably different for sites that grant <code>edit_theme_options<\/code> to non-admin via a role-management plugin.<\/li>\n<li><strong>Rollback (<code>tool-rollback.php<\/code>)<\/strong> \u2014 replaced the hand-maintained <code>$elevated_types<\/code> switch with a per-receipt-type cap map in <code>IATO_MCP_Change_Receipt::cap_required_for()<\/code>. Fail-closed default: unknown target_types are gated at <code>manage_options<\/code> until 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 <code>'taxonomy'<\/code> entry is field-discriminated \u2014 <code>assign<\/code>\/<code>terms<\/code> operations stay at <code>edit_posts<\/code> (match <code>assign_term<\/code> \/ <code>update_taxonomy<\/code> create-time caps), <code>create_term<\/code>\/<code>update_term<\/code>\/<code>delete_term<\/code> go to <code>manage_categories<\/code> (match the create-time caps). Convention: rollback cap === create-time cap.<\/li>\n<\/ul><\/li>\n<li>Known issue (not addressed in this release): the OAuth flow at <code>\/oauth\/token<\/code> returns the plugin's <code>iato_mcp_key<\/code> (admin-only MCP key) as the OAuth <code>access_token<\/code> \u2014 see <code>class-oauth.php:486-489<\/code>. 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 in <code>authenticate()<\/code> (which intentionally bypasses per-user cap checks). The OAuth <code>\/oauth\/authorize<\/code> gate at <code>manage_options<\/code> is 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) \u2014 substantial separate design tracked for a future cycle. Severity is bounded by the issuance-time <code>manage_options<\/code> gate (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.\"<\/li>\n<\/ul>\n\n<h4>1.8.2<\/h4>\n\n<ul>\n<li>Fix: <code>get_site_settings<\/code> no longer corrupts <code>permalink_structure<\/code>, <code>title<\/code>, or <code>tagline<\/code>. The five-field tool was wrapping every value in <code>sanitize_text_field()<\/code>, which calls <code>_sanitize_text_fields()<\/code> \u2014 a function that repeatedly strips <code>%[a-f0-9]{2}<\/code> octets as a transport-safety measure for URL-encoded strings. Applied to fields that legitimately carry literal <code>%xx<\/code> content, that's actively destructive. Three field-level changes follow, with deliberately distinct framing because they're different categories of fix:\n\n<ul>\n<li><strong>(a) <code>permalink_structure<\/code> \u2014 pure bug fix.<\/strong> The sanitized output was always wrong for this field. WordPress's permalink structure legitimately carries <code>%category%<\/code>, <code>%postname%<\/code>, <code>%year%<\/code>, <code>%monthnum%<\/code>, <code>%day%<\/code>, <code>%post_id%<\/code>, and <code>%author%<\/code> as literal placeholder tokens; <code>_sanitize_text_fields<\/code> ate the <code>%xx<\/code> prefixes of those tokens (e.g. <code>\/%category%\/%postname%\/<\/code> came back as <code>\/tegory%\/%postname%\/<\/code>). Now returns the raw value as WordPress stores it \u2014 matching what WP core uses internally when generating URLs.<\/li>\n<li><strong>(b) <code>title<\/code> and <code>tagline<\/code> \u2014 broader behavior change.<\/strong> These now return the raw stored value, not the <code>sanitize_text_field<\/code>-processed form. Practical implications beyond <code>%xx<\/code>: 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.<\/li>\n<li><strong>(c) <code>admin_email<\/code> and <code>timezone<\/code> \u2014 unchanged.<\/strong> Explicit decision: these value types don't legitimately carry <code>%xx<\/code> in practice. PHP timezone identifiers are a controlled list with no <code>%<\/code>; <code>admin_email<\/code>'s RFC percent-encoding form is exceedingly rare in single-mailbox use. <code>sanitize_text_field<\/code> is WP-canonical for these and stays.<\/li>\n<\/ul><\/li>\n<li>Downstream: v1.8.1's archive-detection path reads <code>category_base<\/code>\/<code>tag_base<\/code> directly via <code>get_option()<\/code> inside the router (not via this MCP tool) and is unaffected. No internal callers of <code>get_site_settings<\/code> output exist in the codebase. External MCP callers now receive faithful DB values for the three fixed fields.<\/li>\n<\/ul>\n\n<h4>1.8.1<\/h4>\n\n<ul>\n<li>Fix: <code>resolve_url<\/code> correctly identifies Elementor Theme Builder archive templates as the renderer for Yoast-stripped category URLs (e.g. <code>\/build\/<\/code> instead of <code>\/category\/build\/<\/code>). Previously such URLs returned <code>route_type=404<\/code> even though a Theme Builder template was rendering them. Root cause was two independent bugs:\n\n<ul>\n<li><strong>Detect_archive_info gap.<\/strong> The URL classifier only recognised the default <code>\/category\/&lt;slug&gt;\/<\/code>, <code>\/tag\/&lt;slug&gt;\/<\/code>, and <code>\/author\/&lt;slug&gt;\/<\/code> patterns. Sites with Yoast's \"Remove the categories prefix\" enabled \u2014 or with a custom <code>category_base<\/code> \/ <code>tag_base<\/code> configured in Settings &gt; Permalinks \u2014 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 through <code>get_term_link()<\/code> that goes through any active <code>term_link<\/code> filter (Yoast, RankMath, etc.) and matches the resulting URL against the input.<\/li>\n<li><strong>Shadowing-dispatch bug.<\/strong> <code>detect_theme_builder_shadowing<\/code> treated Elementor Pro's <code>find_via_theme_builder_module<\/code> <code>false<\/code> return (Pro loaded but <code>get_documents_for_location<\/code> matched nothing) as authoritative, returning that <code>false<\/code> directly and never reaching the <code>find_via_conditions_meta<\/code> fallback. In REST context \u2014 which is 100% of MCP traffic \u2014 <code>get_documents_for_location<\/code> is bound to the current <code>$wp_query<\/code> (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 from <code>if ( null !== $found )<\/code> to <code>if ( is_array( $found ) )<\/code> so both <code>false<\/code> and <code>null<\/code> fall through to the meta scan; <code>array<\/code> (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.<\/li>\n<\/ul><\/li>\n<li>Bonus: the dispatch fix also restores singular page shadowing detection on Pro-installed sites. <code>get_post(id, include_shadowing:true)<\/code> now correctly surfaces <code>is_shadowed_by<\/code> when an Elementor Theme Builder single template overrides the slug-based render. Same dispatch bug had been blocking this since v1.7.x.<\/li>\n<li><code>rendering_post_id<\/code> semantics UNCHANGED from v1.7.x. v1.8.0's additive <code>resolve_url<\/code> contract fully preserved \u2014 all new fields (<code>effective_render_id<\/code>, <code>template{}<\/code>, <code>shadowed_route_type<\/code>, etc.) populated correctly by v1.8.0 already; v1.8.1 just makes the shadowing detection that fills them actually fire in REST context.<\/li>\n<li>Documented limitation: plugins that strip the category base via raw <code>.htaccess<\/code> rewrites without using the <code>term_link<\/code> filter won't be detected by Stage 3 of <code>detect_archive_info<\/code>. Rare; rely on a <code>term_link<\/code>-based stripper (Yoast, RankMath, etc.) until a workaround is needed.<\/li>\n<\/ul>\n\n<h4>1.8.0<\/h4>\n\n<ul>\n<li>Fix: <code>resolve_url<\/code> now 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 an <code>elementor_library<\/code> document) previously returned <code>route_type=404<\/code> because the conditions evaluator only matched <code>include\/singular\/...<\/code> patterns against <code>url_to_postid()<\/code>'s post ID \u2014 which is 0 on archives. The conditions parser now evaluates <code>include\/archive\/...<\/code> patterns (taxonomies, terms, authors, CPT archives, <code>in_taxonomy<\/code>, <code>post_archive<\/code>) against URL-derived context, so archive shadowing is correctly detected. <code>find_via_theme_builder_module<\/code> also captures the matching location into <code>template_type<\/code>. The condition string that fired is surfaced as <code>template.condition_matched<\/code> for callers who need to see the match logic.<\/li>\n<li>New: <code>resolve_url<\/code> response gains four additive fields. <code>rendering_post_id<\/code> semantics are UNCHANGED \u2014 still the canonical\/slug-based post (now normalized to <code>null<\/code> instead of <code>0<\/code> on archives so <code>=== null<\/code> checks work). The new fields are: <code>rendering_post_type<\/code>, <code>effective_render_id<\/code> (single field answering \"what actually renders\" \u2014 template ID when shadowed, canonical post ID otherwise, <code>null<\/code> only on a true 404), <code>effective_render_post_type<\/code>, <code>shadowed_route_type<\/code> (route the URL would have had absent the template), and a structured <code>template{template_id,template_type,condition_matched,builder}<\/code> object present when shadowing applies. <code>rendering_template_id<\/code> continues to work exactly as documented; the new fields are additive only.<\/li>\n<li>New: <code>find_elementor_widgets<\/code> auto-resolves revision IDs to their parent post. Passing a revision ID via <code>post_ids<\/code> previously returned matches tagged with the revision's own ID, leaking the parent only through the <code>NNN-revision-vN<\/code> slug \u2014 the brute-force discovery path that motivated this work. Revision IDs are now mapped to their parent via <code>wp_is_post_revision()<\/code>, deduped, and each match scanned from a revision input carries <code>resolved_from_revision_id<\/code> so callers can see the mapping. Default auto-scan also tightens <code>post_status<\/code> from <code>any<\/code> to <code>[publish, draft, pending, private]<\/code> \u2014 trash and auto-draft are no longer scanned by default.<\/li>\n<li>New: <code>find_elementor_widgets<\/code> filter gains a <code>contains<\/code> operator (case-insensitive substring match against scalar settings), alongside the existing <code>eq|ne|in|nin|exists<\/code>. Useful for finding a widget by its content rather than exact-match on a heading \u2014 e.g. <code>setting.editor.contains=\"some phrase\"<\/code>. Scalar-only by design; never recurses into nested settings arrays. The <code>regex<\/code> operator is deferred to a future release where it can get proper backtracking-DoS guards.<\/li>\n<li>Docs: <code>get_posts<\/code>, <code>find_elementor_widgets<\/code>, and <code>resolve_url<\/code> descriptions now state the current scope honestly. <code>get_posts<\/code> notes that <code>post_type=any<\/code> expands to <code>[post, page]<\/code> and does NOT return <code>elementor_library<\/code>. <code>find_elementor_widgets<\/code> notes that templates are not yet scanned. Both point at the upcoming Layer 2 work.<\/li>\n<li>Note on disclosure surface: the new fields (<code>condition_matched<\/code>, <code>template_type<\/code>, <code>effective_render_*<\/code>) widen what an authenticated caller can learn about site structure on a per-URL basis. <code>resolve_url<\/code> continues to require only authentication (no capability check), unchanged from v1.7.x \u2014 Theme Builder shadowing was already disclosed per-URL since v1.5. The forthcoming template-listing tool (Layer 2) will gate full enumeration behind <code>edit_posts<\/code>.<\/li>\n<\/ul>\n\n<h4>1.7.2<\/h4>\n\n<ul>\n<li>Fix: <code>create_media<\/code> no longer hard-rejects payloads that omit <code>source.type<\/code>. v1.7.1 began requiring an explicit <code>type<\/code> field, breaking callers that worked under v1.6.x where the type was inferred from the presence of <code>source.data<\/code> (base64) or <code>source.url<\/code> (URL ingestion). Restored that inference \u2014 supply either field and the right mode is picked automatically. Explicit <code>type<\/code> continues to work and remains the documented preferred form.<\/li>\n<li>Fix: URL ingestion no longer rejects the site's own host. Plugins installed on <code>example.com<\/code> could not ingest <code>https:\/\/example.com\/wp-content\/uploads\/foo.jpg<\/code> without manually adding <code>example.com<\/code> to the URL allowlist \u2014 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's <code>home_url()<\/code> and <code>site_url()<\/code> host(s) are now implicitly trusted. The SSRF IP-resolution guard (<code>check_host_resolves_publicly<\/code>) still runs in both branches, so the bypass only skips the manual allowlist; private\/loopback\/link-local IPs continue to be rejected.<\/li>\n<li>Improved: <code>create_media<\/code> tool 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 <code>?dl=0<\/code>) 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.<\/li>\n<li>Improved: <code>file_too_large<\/code> errors on the base64 path now mention URL ingestion as the alternative, mirroring the helpful tone of the existing <code>url_source_disabled<\/code> error. The agent gets a clear next step instead of an opaque size-cap message.<\/li>\n<\/ul>\n\n<h4>1.7.1<\/h4>\n\n<ul>\n<li>Fix: the four Media Uploads settings shipped in 1.7.0 (<code>iato_mcp_media_url_source_enabled<\/code>, <code>iato_mcp_media_url_host_allowlist<\/code>, <code>iato_mcp_media_max_upload_size<\/code>, <code>iato_mcp_media_upload_rate_limit<\/code>) silently failed to persist on Save. The UI rendered correctly and the fields were properly registered with <code>register_setting()<\/code>, but the General-tab form is hijacked through an admin-ajax handler (some hosts 503 on <code>options.php<\/code> POSTs due to upstream WAF\/timeout rules) and that handler hardcoded the keys it persisted \u2014 anything not explicitly listed fell off the floor. 1.7.1 extends <code>ajax_save_settings()<\/code> to call the matching sanitize-and-update path for each of the four media keys, mirroring the existing <code>iato_mcp_api_key<\/code> \/ <code>iato_mcp_crawl_id<\/code> \/ <code>iato_mcp_tools<\/code> lines.<\/li>\n<li>Docs: CLAUDE.md gains a \"Release Checklist (adding a new admin setting)\" section calling out the three places a new option must land \u2014 <code>register_setting()<\/code>, the rendered <code>&lt;input name=\"...\"&gt;<\/code>, and the AJAX persist handler. Same shape as the existing tool-release checklist; closes the structural failure mode that produced this 1.7.0 \u2192 1.7.1 follow-up.<\/li>\n<\/ul>\n\n<h4>1.7.0<\/h4>\n\n<ul>\n<li>New: <code>Settings &gt; IATO MCP<\/code> now includes a Media Uploads card. The four media settings (<code>iato_mcp_media_url_source_enabled<\/code>, <code>iato_mcp_media_url_host_allowlist<\/code>, <code>iato_mcp_media_max_upload_size<\/code>, <code>iato_mcp_media_upload_rate_limit<\/code>) 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. The <code>url_source_disabled<\/code> error message returned by <code>create_media<\/code> continues to point admins to \"Settings &gt; IATO MCP &gt; Media uploads\" \u2014 that path now exists.<\/li>\n<li>New: Diagnostics page gains a \"Recent media uploads\" panel showing the last 100 <code>create_media<\/code> calls 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 <code>&lt;details&gt;<\/code> element to show the phase-by-phase timing. Triage that previously required enabling <code>WP_DEBUG_LOG<\/code> mid-session and tailing the host's PHP error log is now one click on the Diagnostics tab. The on-disk <code>error_log()<\/code> mirror is preserved for environments that already aggregate logs centrally.<\/li>\n<li>New: backing infrastructure \u2014 <code>IATO_MCP_Media_Phase_Log<\/code> class and <code>{prefix}iato_mcp_media_phase_log<\/code> table store one ring-buffered row per <code>create_media<\/code> call. The deferred-subsizes cron path threads its <code>req_id<\/code> through <code>wp_schedule_single_event<\/code> and appends an <code>async-subsizes-done<\/code> phase to the parent row on completion, so a single Diagnostics row captures the entire end-to-end timeline of an upload \u2014 including the async tick that lands minutes later. DB writes are wrapped in try\/catch so an observability hiccup cannot regress <code>create_media<\/code> itself. Table creation is dbDelta-idempotent and runs from both the activation hook and the migration gate, so upgraded installs pick it up without a reactivation.<\/li>\n<li>Improved: <code>create_media<\/code> tool description now includes practical guidance on when to use base64 vs URL ingestion. Base64 is reliable for small assets (icons, badges, screenshots under ~100 KB); URL ingestion is the recommended path for production-scale photography and requires admin opt-in via the new Media Uploads settings card.<\/li>\n<li>Fix: <code>uninstall.php<\/code> now drops the <code>iato_mcp_media_phase_log<\/code> table on plugin delete and cleans up <code>iato_mcp_db_version<\/code> plus the four <code>iato_mcp_media_*<\/code> options that the 1.6.0 release introduced without matching uninstall coverage.<\/li>\n<li>Audit: the other four tools from the 1.6.0 batch (<code>set_featured_image<\/code>, <code>update_post_meta<\/code>, <code>get_post_meta<\/code>, <code>set_page_settings<\/code>) were audited for the same \"admin-controlled toggle without UI\" pattern that the <code>create_media<\/code> URL-source feature exhibited. None of them read any <code>iato_mcp_*<\/code> options at runtime \u2014 the missing-UI gap was isolated to <code>create_media<\/code>. No changes to those four were required.<\/li>\n<\/ul>\n\n<h4>1.6.4<\/h4>\n\n<ul>\n<li>Fix: <code>create_media<\/code> now actually accepts uploads under Bearer-token MCP auth. The v1.6.0 implementation gated the handler on <code>current_user_can('upload_files')<\/code> and <code>current_user_can('edit_post', $attach_to_post)<\/code>, but Bearer-authenticated MCP requests don't establish a logged-in WordPress user \u2014 <code>wp_get_current_user()<\/code> returns 0 and meta-cap checks against the empty user object always fail, so every call returned \"You do not have permission to upload files.\" regardless of who initiated it. Switched to <code>IATO_MCP_Auth::require_cap()<\/code>, which honors the documented \"plugin key grants full administrative access\" auth model \u2014 exactly the same fix shape v1.3.1 applied to <code>update_elementor_widgets_bulk<\/code> and <code>find_elementor_widgets<\/code> for the same bug class. Audited the other v1.6.0 tools (<code>get_post_meta<\/code>, <code>update_post_meta<\/code>, <code>set_page_settings<\/code>, <code>set_featured_image<\/code>) and confirmed they all use <code>require_cap()<\/code> correctly \u2014 <code>create_media<\/code> was the only regression.<\/li>\n<\/ul>\n\n<h4>1.6.3<\/h4>\n\n<ul>\n<li>New: <code>create_media<\/code> accepts <code>defer_subsizes: true<\/code> to skip the synchronous <code>wp_generate_attachment_metadata<\/code> call and schedule it via WP-Cron instead. The MCP response returns immediately with <code>attachment_id<\/code> and the canonical URL; intermediate sizes are generated on the next cron tick (typically within seconds). Recommended for any caller running through the Anthropic MCP gateway, which times out around 30 seconds \u2014 sites with image-optimisation pipelines (ShortPixel, Imagify, Smush, etc.) intercepting the metadata-generation hook routinely exceed that limit and produce silent hangs where the response never arrives but the attachment also never lands. The default remains synchronous so existing callers see no behaviour change.<\/li>\n<li>New: per-phase diagnostic logging in the <code>create_media<\/code> handler. Every call now writes <code>[iato-mcp create_media:&lt;req_id&gt;] phase=... elapsed=...s<\/code> lines to PHP's error log at each stage (entry, source resolve, MIME check, dimension check, sideload, attachment insert, subsize generation, return), including the byte counts and dimensions seen. When a call hangs or fails, the last line in <code>wp-content\/debug.log<\/code> (or the host's PHP log) identifies which phase stalled \u2014 turning the previously-silent failure mode into a one-line diagnosis. The async cron handler logs its own line on completion with attachment_id, subsize count, and duration.<\/li>\n<li>Fix: <code>update_elementor_data<\/code> with <code>inherit_settings_from<\/code> now copies empty-string values from the source post instead of skipping them. WordPress's <code>get_post_meta<\/code> returns <code>''<\/code> for both stored-empty and absent keys, so the prior skip-empty rule turned out to silently drop meaningful Astra layout state on real cloning workflows (Astra stores per-post overrides as empty strings in some configurations, and skipping them caused targets to retain the wrong layout). The contract of <code>inherit_settings_from<\/code> is \"make the target match the source\"; that now happens uniformly.<\/li>\n<li>Fix: the default <code>inherit_keys<\/code> list on <code>update_elementor_data<\/code> is widened from 8 keys to 14 to cover the full Astra per-post override family (<code>site-content-style<\/code>, <code>site-sidebar-style<\/code>, <code>ast-global-header-display<\/code>, <code>ast-banner-title-visibility<\/code>, <code>ast-breadcrumbs-content<\/code>, <code>ast-featured-img<\/code>). Cloning a styled post now transfers the complete layout state in a single call, not just the original 4 brief-flagged keys.<\/li>\n<li>Refactor: <code>inherited_skipped[]<\/code> in the <code>update_elementor_data<\/code> response semantics changed from \"source value was empty\" to \"source value matches target's existing value (no-op write)\". Each entry now uses <code>reason: 'noop'<\/code>. The new shape surfaces the case where inherit_settings_from would have written a value but the target already had it \u2014 useful diagnostic, no behavioural cost.<\/li>\n<\/ul>\n\n<h4>1.6.2<\/h4>\n\n<ul>\n<li>Refactor: the four hand-written <code>version_compare<\/code> migration blocks that backfill new tool names into the saved <code>iato_mcp_tools<\/code> option are replaced by a single declarative <code>TOOL_MIGRATION_BACKFILL<\/code> map on <code>IATO_MCP_Settings<\/code> plus a one-loop walker in <code>iato_mcp_maybe_run_migrations()<\/code>. Same behavior for every install that was already correctly migrated; the new shape eliminates the \"remembered to add a migration block\" failure mode that produced the 1.3.0 \u2192 1.3.1 fix, the 1.4.0 \u2192 1.4.5 fix, and the 1.6.0 \u2192 1.6.1 fix. Adding a new tool now requires appending one line to the map alongside the <code>TOOL_NAMES<\/code> edit \u2014 colocated, hard to miss at review time.<\/li>\n<li>Fix: backfills the three crawl-management tools (<code>start_iato_crawl<\/code>, <code>get_iato_crawl_status<\/code>, <code>list_iato_crawls<\/code>) on installs that originally upgraded from 1.1.x to 1.2.x with a saved <code>iato_mcp_tools<\/code> option. v1.2.0 introduced those tools but shipped without the migration to append them, so any user who had saved their per-tool toggles in 1.1.x and configured an IATO API key has had the crawl-management tools invisibly disabled for the entire 1.2 \u2192 1.6 interval. The 1.6.2 backfill catches them automatically on first request after upgrade. No-op for installs that already have the names in the saved option (idempotent), and no-op for fresh installs (the option starts empty and every tool is enabled by default).<\/li>\n<li>Fix: <code>update_elementor_data<\/code> with <code>inherit_settings_from<\/code> now returns an <code>inherited_skipped[]<\/code> array in the response listing keys that were in the configured inheritance list but absent on the source post (so the assistant can see which clone targets had no source value rather than silently getting fewer receipts than the default list implies). Each entry is <code>{ key, reason: 'source_empty' }<\/code>. The skip-empty behavior itself is unchanged \u2014 copying an explicit empty string from a source post that never set a key would stomp the target's existing value.<\/li>\n<\/ul>\n\n<h4>1.6.1<\/h4>\n\n<ul>\n<li>Fix: the five new MCP tools added in 1.6.0 (<code>get_post_meta<\/code>, <code>update_post_meta<\/code>, <code>set_page_settings<\/code>, <code>set_featured_image<\/code>, <code>create_media<\/code>) now register correctly on sites upgrading from a previous version. v1.6.0 added them to the <code>TOOL_NAMES<\/code> constant but forgot the idempotent migration that appends new tool names to the saved <code>iato_mcp_tools<\/code> per-tool toggle option \u2014 the same migration shape used for the Elementor v2 tools in 1.3.5 and for <code>rollback<\/code> in 1.4.0\/1.4.5. Without it, <code>is_tool_enabled()<\/code> filtered the new names out of the registry on every upgraded install, so the tools never appeared in <code>tools\/list<\/code> despite shipping in the plugin. New installs were unaffected (the option is empty on first activation and all tools are enabled by default). Single one-shot migration; no-op for installs that already have the tool names in the saved option.<\/li>\n<\/ul>\n\n<h4>1.6.0<\/h4>\n\n<ul>\n<li>New: <code>get_post_meta<\/code> and <code>update_post_meta<\/code> expose arbitrary post meta over MCP with a centralised security policy. A credential-shaped denylist (<code>*_token*<\/code>, <code>*_secret*<\/code>, <code>*_api_key*<\/code>, <code>*_password*<\/code>, <code>*_credential*<\/code>, <code>_oauth_*<\/code>, <code>_jwt_*<\/code>, <code>_refresh_token_*<\/code>, plus <code>wp_capabilities<\/code> and friends) is hard-rejected on writes and redacted on reads \u2014 <code>force=true<\/code> cannot override it. A known-safe allowlist of theme\/builder\/SEO prefixes (Astra <code>site-<\/code>\/<code>ast-<\/code>, Elementor <code>_elementor_<\/code>, Yoast\/RankMath\/SEOPress, Kadence, GeneratePress, Genesis, plus <code>_wp_page_template<\/code> and <code>_thumbnail_id<\/code>) lets the assistant write the common cases without ceremony; anything outside both lists requires <code>force=true<\/code>. Every write emits a <code>change_receipt<\/code> rollback-able under the new <code>target_type=post_meta<\/code>. Closes the long-standing gap that left the assistant unable to touch per-post theme settings on Astra and similar themes.<\/li>\n<li>New: <code>set_page_settings<\/code> is a one-call convenience wrapper for the most common page-level settings cluster on Astra + Elementor sites. Pass abstract names like <code>hide_title: true<\/code>, <code>sidebar_layout: \"no-sidebar\"<\/code>, <code>content_layout: \"page-builder\"<\/code>, <code>disable_header<\/code>, <code>disable_footer<\/code>, <code>page_template<\/code>, or <code>elementor_page_settings<\/code> and the tool maps each to the right concrete meta key for the active theme. Astra-specific keys are silently skipped on non-Astra themes and surfaced in <code>skipped[]<\/code> so the agent can report them back to the user. Returns one <code>change_receipt<\/code> per concrete meta key written, so the whole settings cluster is reversible.<\/li>\n<li>New: <code>set_featured_image<\/code> finally closes the \"create a post end-to-end\" loop \u2014 the assistant can now set or clear <code>_thumbnail_id<\/code> directly instead of bouncing the user to wp-admin. Validates that the supplied attachment is an image, captures the previous thumbnail ID in the receipt, and rolls back via the same <code>post_meta<\/code> target_type.<\/li>\n<li>New: <code>create_media<\/code> uploads new images to the media library. Two source modes: <code>base64<\/code> (default and recommended \u2014 the WordPress server never makes an outbound HTTP request on agent input) and <code>url<\/code> (default-disabled; admins must explicitly enable it and add hosts to an allowlist before any fetch is attempted). The URL path runs full SSRF guards: DNS resolution + private\/loopback\/link-local\/cloud-metadata IP rejection, hard timeout, redirect cap, and re-validation of every redirect destination's resolved IP. MIME is verified against actual file bytes (never the claimed mime_type), filenames containing <code>.php<\/code>, <code>.phtml<\/code>, <code>.phar<\/code>, or <code>.htaccess<\/code> are rejected, and SVG is hard-rejected this release regardless of how the upload is presented. Size (default 10MB) and dimension (default 8000\u00d78000) caps are configurable; per-user rate limit (default 20\/min) is enforced via a transient. Successful uploads return the attachment ID, public URL, generated intermediate sizes, and a <code>change_receipt<\/code> under the new <code>target_type=attachment<\/code> \u2014 rollback fully deletes the attachment file via <code>wp_delete_attachment(force=true)<\/code>.<\/li>\n<li>New: <code>update_elementor_data<\/code> gains <code>inherit_settings_from: &lt;post_id&gt;<\/code> and optional <code>inherit_keys<\/code> parameters. When set, the tool copies a curated default set of theme + Elementor page-level meta keys (<code>site-post-title<\/code>, <code>site-sidebar-layout<\/code>, <code>site-content-layout<\/code>, <code>ast-main-header-display<\/code>, <code>footer-sml-layout<\/code>, <code>_wp_page_template<\/code>, <code>_elementor_page_settings<\/code>, <code>_elementor_template_type<\/code>) from the source post to the target in the same MCP call, returning one <code>change_receipt<\/code> per inherited key in <code>change_receipts[]<\/code>. Collapses the \"clone the styling of an existing post\" workflow from four or more tool calls to one \u2014 and means the new post no longer renders with the wrong theme title bar above the Elementor content.<\/li>\n<li>New: four new admin settings under Settings &gt; IATO MCP control upload behaviour \u2014 <code>iato_mcp_media_url_source_enabled<\/code> (default off), <code>iato_mcp_media_url_host_allowlist<\/code> (one host per line), <code>iato_mcp_media_max_upload_size<\/code> (bytes; default 10MB), and <code>iato_mcp_media_upload_rate_limit<\/code> (per-user per-minute; default 20). Existing installs upgrade with secure defaults \u2014 URL ingestion stays off until explicitly enabled.<\/li>\n<\/ul>\n\n<h4>1.5.0<\/h4>\n\n<ul>\n<li>New: <code>update_post<\/code> accepts a <code>slug<\/code> parameter to rename a post's URL slug via MCP \u2014 previously the agent had no way to update a slug and the user had to do it manually in the WP editor. Input is strictly validated (lowercase a-z 0-9 and hyphens only, no leading\/trailing\/double hyphens, max 200 chars, must survive a <code>sanitize_title()<\/code> round-trip unchanged) and conflicts return a <code>slug_conflict<\/code> error with the colliding post's ID and title rather than silently appending <code>-2<\/code> like WordPress would. Changing the slug of a non-draft post additionally requires <code>confirm_url_break: true<\/code> since it breaks every inbound link \u2014 drafts are exempt. Slug changes are rollback-able the same way as title\/content\/status edits: each change emits a <code>change_receipt<\/code> and can be reversed via the <code>rollback<\/code> tool.<\/li>\n<li>New: <code>create_post<\/code> and <code>update_post<\/code> responses now include a <code>notice<\/code> field on page-builder-driven sites (Elementor, Divi, WPBakery, Beaver Builder) when the call would produce content that doesn't match the site's existing post format. On Elementor the notice tells the agent to fetch a reference post via <code>get_post<\/code> + <code>get_elementor_data<\/code> and apply its structure via <code>update_elementor_data<\/code>; on Divi\/WPBakery\/Beaver it tells the agent the layout must be finished in WP admin. On Gutenberg-only sites the field is absent \u2014 vanilla installs see no spurious warnings. Closes the gap that left agents creating posts with plain HTML on Elementor sites, producing structurally orphaned drafts that looked nothing like the rest of the site.<\/li>\n<li>New: the dynamic instructions injected into the MCP <code>initialize<\/code> response (added in 1.4.8) now include a NEW-POST WORKFLOW block whenever a non-Gutenberg builder is active. The block primes the agent to (1) ask the user for a reference post URL before calling <code>create_post<\/code>, (2) fetch the reference's structure, and (3) port that structure onto the new post \u2014 so the right path happens on the first call, not after the user notices the formatting problem.<\/li>\n<\/ul>\n\n<h4>1.4.10<\/h4>\n\n<ul>\n<li>Fix: the JSON config snippets emitted by the plugin (setup wizard Method 3, dismissible \"Ready to Connect\" notice, Settings hero card) now use a unique-per-site inner <code>mcpServers<\/code> key derived from the WordPress site's hostname (e.g. <code>iato-garennebigby-dev<\/code>, <code>iato-dynomapper-com<\/code>) instead of the hardcoded <code>iato-wordpress<\/code>. Agencies managing multiple WordPress installs from a single AI client (Claude Desktop, Claude Code, etc.) can now paste config snippets from many IATO MCP installs into the same client config file without one silently overwriting another (JSON object keys are unique, so two snippets sharing a key was a silent collision). Existing connections that were set up with the old <code>iato-wordpress<\/code> key continue to work \u2014 the inner key is a display name only, not part of any HTTP request \u2014 so no migration is needed.<\/li>\n<\/ul>\n\n<h4>1.4.9<\/h4>\n\n<ul>\n<li>Docs: added the plugin demo video to the top of the Description section on the WordPress.org plugin page (auto-embedded by WordPress.org's readme renderer when a YouTube URL is on its own line). No code changes; safe to skip if you've already updated to 1.4.8.<\/li>\n<\/ul>\n\n<h4>1.4.8<\/h4>\n\n<ul>\n<li>New: dynamic page-builder-aware server instructions injected into the MCP <code>initialize<\/code> response. The plugin now detects which page-builder plugins are active on the WordPress site (Elementor, Divi, WPBakery, Beaver Builder, Gutenberg) and emits a context-specific instruction string telling the AI agent which write tools are correct for which builder, with a mandatory <code>get_page_builder<\/code> check-first rule before any content edit. Closes a class of silent-failure bug where <code>update_post<\/code> on an Elementor-built post would succeed at the database level but never reach the frontend (because Elementor stores content in <code>_elementor_data<\/code>, not <code>post_content<\/code>). Detected-but-unsupported builders (Divi, WPBakery, Beaver Builder for writes) are explicitly flagged so the agent tells the user to edit in the WP admin instead of attempting a write that won't take effect. Uses the standard MCP <code>instructions<\/code> field added in spec rev 2025-03-26; older clients on 2024-11-05 cleanly ignore the unknown field.<\/li>\n<li>New: <code>get_page_builder<\/code> now detects Beaver Builder posts (via <code>_fl_builder_enabled<\/code> post meta) and returns <code>beaver-builder<\/code>. Previously these posts fell through to the <code>gutenberg<\/code> or <code>classic<\/code> branch, misleading the agent about how to handle them.<\/li>\n<\/ul>\n\n<h4>1.4.7<\/h4>\n\n<ul>\n<li>Fix: Settings \u2192 IATO MCP no longer presents the IATO Platform and Crawl Management tool toggles as functional when no IATO API key is configured. Previously the checkboxes appeared enabled and saveable, but bridge tool registration is gated by a separate condition at <code>iato-mcp.php:85<\/code> (the bridge tool files only <code>require_once<\/code> when the API key is non-empty), so the toggles were placebo \u2014 a user could check every box, save, and still get <code>Unknown tool: get_iato_sitemap<\/code> on every call with no UI signal explaining why. The toggle inputs in those two categories are now <code>disabled<\/code> when the API key is empty, the category card grays out (55% opacity), and an inline banner under the heading explains: \"These tools require an IATO API key. Add it under 'IATO Platform' above to enable them \u2014 until then, these toggles have no effect.\" When the user pastes an API key and saves, the categories become interactive again.<\/li>\n<\/ul>\n\n<h4>1.4.6<\/h4>\n\n<ul>\n<li>Fix: <code>rollback<\/code> now appears as a checkbox on the Settings \u2192 IATO MCP page (under a new \"Safety\" category). v1.4.5 added rollback to the <code>TOOL_NAMES<\/code> constant \u2014 which fixed the sanitize-strip behavior \u2014 but the Settings UI rendering loop iterates a separate constant, <code>TOOL_CATEGORIES<\/code>, which also needed rollback added. Without the category entry, the checkbox was never rendered. Adding <code>'Safety' =&gt; ['rollback']<\/code> closes the gap.<\/li>\n<li>Polish: unified the inner <code>mcpServers<\/code> server key shown in the Settings page hero card config snippet from <code>wordpress<\/code> to <code>iato-wordpress<\/code>, matching the dismissible setup notice. Cosmetic only \u2014 the inner key is a user-facing display name they can rename \u2014 but eliminates an unnecessary inconsistency between the two snippets.<\/li>\n<\/ul>\n\n<h4>1.4.5<\/h4>\n\n<ul>\n<li>Fix: <code>rollback<\/code> tool now appears in the Settings \u2192 IATO MCP per-tool toggle list, and the Settings save no longer silently strips it from <code>iato_mcp_tools<\/code>. When v1.4.0 added the rollback MCP tool, the developer forgot to add it to the <code>TOOL_NAMES<\/code> constant in <code>class-settings.php<\/code>. Consequence: no UI checkbox for it, and <code>sanitize_tools()<\/code> (which <code>array_intersect<\/code>s saved values against TOOL_NAMES) was stripping it from existing installs every time a user clicked Save Settings. Once stripped, <code>is_tool_enabled('rollback')<\/code> returned false and the tool stopped registering. Adding rollback to TOOL_NAMES fixes both the UI and the strip behavior.<\/li>\n<li>Fix:  &hellip;<\/li>\n<\/ul>","raw_excerpt":"Exposes an MCP server from any self-hosted WordPress site, enabling AI agents like Claude to audit and fix your site in a single workflow.","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/ky.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin\/294616","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ky.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin"}],"about":[{"href":"https:\/\/ky.wordpress.org\/plugins\/wp-json\/wp\/v2\/types\/plugin"}],"replies":[{"embeddable":true,"href":"https:\/\/ky.wordpress.org\/plugins\/wp-json\/wp\/v2\/comments?post=294616"}],"author":[{"embeddable":true,"href":"https:\/\/ky.wordpress.org\/plugins\/wp-json\/wporg\/v1\/users\/iatoai"}],"wp:attachment":[{"href":"https:\/\/ky.wordpress.org\/plugins\/wp-json\/wp\/v2\/media?parent=294616"}],"wp:term":[{"taxonomy":"plugin_section","embeddable":true,"href":"https:\/\/ky.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_section?post=294616"},{"taxonomy":"plugin_tags","embeddable":true,"href":"https:\/\/ky.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_tags?post=294616"},{"taxonomy":"plugin_category","embeddable":true,"href":"https:\/\/ky.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_category?post=294616"},{"taxonomy":"plugin_contributors","embeddable":true,"href":"https:\/\/ky.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_contributors?post=294616"},{"taxonomy":"plugin_business_model","embeddable":true,"href":"https:\/\/ky.wordpress.org\/plugins\/wp-json\/wp\/v2\/plugin_business_model?post=294616"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}