@adsim/wordpress-mcp-server 4.4.0 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -28,6 +28,7 @@ import { wcApiCall, getWcCredentials } from './src/woocommerceClient.js';
28
28
  import { parseImagesFromHtml, extractHeadings, extractInternalLinks as extractInternalLinksHtml, countWords } from './src/htmlParser.js';
29
29
  import { summarizePost, applyContentFormat } from './src/utils/contentCompressor.js';
30
30
  import { calculateReadabilityScore, extractHeadingsOutline, detectContentSections, extractTransitionWords, countPassiveSentences, buildTFIDFVectors, computeCosineSimilarity, findDuplicatePairs, extractEntities, computeTextDiff } from './src/contentAnalyzer.js';
31
+ import { detectSeoPlugin, getRenderedHead, parseRenderedHead } from './src/pluginDetector.js';
31
32
 
32
33
  // ============================================================
33
34
  // CONFIGURATION
@@ -435,7 +436,7 @@ const ORDERBY = ['date', 'relevance', 'id', 'title', 'slug', 'modified', 'author
435
436
  const ORDERS = ['asc', 'desc'];
436
437
  const MEDIA_TYPES = ['image', 'video', 'audio', 'application'];
437
438
  const COMMENT_STATUSES = ['approved', 'hold', 'spam', 'trash'];
438
- const TOOLS_COUNT = 79;
439
+ const TOOLS_COUNT = 85;
439
440
 
440
441
  function json(data) { return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }; }
441
442
  function strip(html) { return (html || '').replace(/<[^>]*>/g, '').trim(); }
@@ -446,197 +447,225 @@ function strip(html) { return (html || '').replace(/<[^>]*>/g, '').trim(); }
446
447
 
447
448
  const TOOLS_DEFINITIONS = [
448
449
  // ── POSTS (6) ──
449
- { name: 'wp_list_posts', description: 'List posts with filtering and search. Default "full" mode returns all fields including excerpts — use mode:"summary" for listing/inventory workflows (id, title, slug, date, status, link only), mode:"ids_only" when you only need IDs for a subsequent batch operation (e.g. then calling wp_get_post per ID). Use mode:"full" only when you need excerpts or category/tag details.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, status: { type: 'string', default: 'publish' }, orderby: { type: 'string', default: 'date' }, order: { type: 'string', default: 'desc' }, categories: { type: 'string' }, tags: { type: 'string' }, search: { type: 'string' }, author: { type: 'number' }, mode: { type: 'string', default: 'full', description: 'full=all fields, summary=id/title/slug/date/status/link only, ids_only=flat ID array' } }}},
450
- { name: 'wp_get_post', description: 'Get post by ID. WARNING: default returns full HTML content (50,000-187,000 chars on Elementor/page-builder sites) always specify content_format to avoid context overflow. Use content_format:"links_only" for internal linking workflows (~800 chars instead of 187,000). Use content_format:"text" for content audit/rewrite workflows (plain text truncated to 15,000 chars). Use content_format:"html" only when you need raw HTML for structure analysis. Combine with fields:["id","title","content"] for rewrite tasks or fields:["id","title","meta"] for SEO-only tasks. Prefer wp_get_seo_meta over wp_get_post when you only need SEO metadata.', inputSchema: { type: 'object', properties: { id: { type: 'number' }, fields: { type: 'array', items: { type: 'string' }, description: 'Return only specified fields (id,title,content,excerpt,slug,status,date,modified,categories,tags,author,featured_media,meta,link). Omit for all.' }, content_format: { type: 'string', default: 'html', description: 'html=raw HTML (truncated at WP_MAX_CONTENT_CHARS), text=plain text, links_only=extract internal links only' } }, required: ['id'] }},
451
- { name: 'wp_create_post', description: 'Create a post (default: draft).', inputSchema: { type: 'object', properties: { title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string', default: 'draft' }, excerpt: { type: 'string' }, categories: { type: 'array', items: { type: 'number' } }, tags: { type: 'array', items: { type: 'number' } }, slug: { type: 'string' }, featured_media: { type: 'number' }, meta: { type: 'object' }, author: { type: 'number' } }, required: ['title', 'content'] }},
452
- { name: 'wp_update_post', description: 'Update a post.', inputSchema: { type: 'object', properties: { id: { type: 'number' }, title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string' }, excerpt: { type: 'string' }, categories: { type: 'array', items: { type: 'number' } }, tags: { type: 'array', items: { type: 'number' } }, slug: { type: 'string' }, featured_media: { type: 'number' }, meta: { type: 'object' }, author: { type: 'number' } }, required: ['id'] }},
453
- { name: 'wp_delete_post', description: 'Delete a post (trash or permanent). When WP_CONFIRM_DESTRUCTIVE=true, requires a confirmation_token (call once without to get token, then again with token).', inputSchema: { type: 'object', properties: { id: { type: 'number' }, force: { type: 'boolean', default: false }, confirmation_token: { type: 'string', description: 'Confirmation token returned by the first call when WP_CONFIRM_DESTRUCTIVE=true' } }, required: ['id'] }},
454
- { name: 'wp_search', description: 'Full-text search across all content.', inputSchema: { type: 'object', properties: { search: { type: 'string' }, per_page: { type: 'number', default: 10 }, type: { type: 'string', default: '' } }, required: ['search'] }},
450
+ { name: 'wp_list_posts', description: 'Use to browse/filter posts by status, category, tag, author, or search. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, status: { type: 'string', default: 'publish' }, orderby: { type: 'string', default: 'date' }, order: { type: 'string', default: 'desc' }, categories: { type: 'string' }, tags: { type: 'string' }, search: { type: 'string' }, author: { type: 'number' }, mode: { type: 'string', default: 'full', description: 'full=all fields, summary=id/title/slug/date/status/link only, ids_only=flat ID array' } }}},
451
+ { name: 'wp_get_post', description: 'Use to read a single post with full content and meta. Read-only. Hint: use content_format=\'links_only\' for link audits (~800 chars vs 187k), \'text\' for rewrites, fields=[\'id\',\'title\',\'meta\'] for SEO-only.', inputSchema: { type: 'object', properties: { id: { type: 'number' }, fields: { type: 'array', items: { type: 'string' }, description: 'Return only specified fields (id,title,content,excerpt,slug,status,date,modified,categories,tags,author,featured_media,meta,link). Omit for all.' }, content_format: { type: 'string', default: 'html', description: 'html=raw HTML (truncated at WP_MAX_CONTENT_CHARS), text=plain text, links_only=extract internal links only' } }, required: ['id'] }},
452
+ { name: 'wp_create_post', description: 'Use to create a post (defaults to draft). Accepts title, HTML content, categories, tags, featured_media, meta. Write — blocked by WP_READ_ONLY.', inputSchema: { type: 'object', properties: { title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string', default: 'draft' }, excerpt: { type: 'string' }, categories: { type: 'array', items: { type: 'number' } }, tags: { type: 'array', items: { type: 'number' } }, slug: { type: 'string' }, featured_media: { type: 'number' }, meta: { type: 'object' }, author: { type: 'number' } }, required: ['title', 'content'] }},
453
+ { name: 'wp_update_post', description: 'Use to modify any post field — only provided fields change. Write — blocked by WP_READ_ONLY.', inputSchema: { type: 'object', properties: { id: { type: 'number' }, title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string' }, excerpt: { type: 'string' }, categories: { type: 'array', items: { type: 'number' } }, tags: { type: 'array', items: { type: 'number' } }, slug: { type: 'string' }, featured_media: { type: 'number' }, meta: { type: 'object' }, author: { type: 'number' } }, required: ['id'] }},
454
+ { name: 'wp_delete_post', description: 'Use to trash or permanently delete a post. Write blocked by WP_READ_ONLY, WP_DISABLE_DELETE.', inputSchema: { type: 'object', properties: { id: { type: 'number' }, force: { type: 'boolean', default: false }, confirmation_token: { type: 'string', description: 'Confirmation token returned by the first call when WP_CONFIRM_DESTRUCTIVE=true' } }, required: ['id'] }},
455
+ { name: 'wp_search', description: 'Use for full-text search across all content types. Returns id, title, url, type. Read-only.', inputSchema: { type: 'object', properties: { search: { type: 'string' }, per_page: { type: 'number', default: 10 }, type: { type: 'string', default: '' } }, required: ['search'] }},
455
456
 
456
457
  // ── APPROVAL WORKFLOW (3) ──
457
- { name: 'wp_submit_for_review', description: 'Submit a draft post for editorial review (draft pending). Blocked by WP_READ_ONLY.',
458
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Post ID' }, note: { type: 'string', description: 'Optional review note (stored as post meta _mcp_review_note)' } }, required: ['id'] }},
459
- { name: 'wp_approve_post', description: 'Approve a pending post for publication (pending → publish). Blocked by WP_READ_ONLY and WP_DRAFT_ONLY.',
460
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Post ID' } }, required: ['id'] }},
461
- { name: 'wp_reject_post', description: 'Reject a pending post back to draft with a reason (pending → draft). Stores rejection reason and increments rejection count. Blocked by WP_READ_ONLY.',
462
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Post ID' }, reason: { type: 'string', description: 'Reason for rejection (stored as post meta _mcp_rejection_reason)' } }, required: ['id', 'reason'] }},
458
+ { name: 'wp_submit_for_review', description: 'Use to transition a draft post to pending status for editorial review. Write blocked by WP_READ_ONLY.',
459
+ inputSchema: { type: 'object', properties: { id: { type: 'number' }, note: { type: 'string', description: 'Optional review note (stored as post meta _mcp_review_note)' } }, required: ['id'] }},
460
+ { name: 'wp_approve_post', description: 'Use to transition a pending post to published (editor/admin action). Write — blocked by WP_READ_ONLY, WP_DRAFT_ONLY.',
461
+ inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] }},
462
+ { name: 'wp_reject_post', description: 'Use to return a pending post to draft with a mandatory rejection reason. Write blocked by WP_READ_ONLY.',
463
+ inputSchema: { type: 'object', properties: { id: { type: 'number' }, reason: { type: 'string', description: 'Reason for rejection (stored as post meta _mcp_rejection_reason)' } }, required: ['id', 'reason'] }},
463
464
 
464
465
  // ── PAGES (4) ──
465
- { name: 'wp_list_pages', description: 'List pages with hierarchy.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, status: { type: 'string', default: 'publish' }, parent: { type: 'number' }, orderby: { type: 'string', default: 'menu_order' }, order: { type: 'string', default: 'asc' }, search: { type: 'string' } }}},
466
- { name: 'wp_get_page', description: 'Get page by ID with content and template.', inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] }},
467
- { name: 'wp_create_page', description: 'Create a page (default: draft).', inputSchema: { type: 'object', properties: { title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string', default: 'draft' }, parent: { type: 'number', default: 0 }, template: { type: 'string' }, menu_order: { type: 'number', default: 0 }, excerpt: { type: 'string' }, slug: { type: 'string' }, featured_media: { type: 'number' }, meta: { type: 'object' } }, required: ['title', 'content'] }},
468
- { name: 'wp_update_page', description: 'Update a page.', inputSchema: { type: 'object', properties: { id: { type: 'number' }, title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string' }, parent: { type: 'number' }, template: { type: 'string' }, menu_order: { type: 'number' }, excerpt: { type: 'string' }, slug: { type: 'string' }, featured_media: { type: 'number' }, meta: { type: 'object' } }, required: ['id'] }},
466
+ { name: 'wp_list_pages', description: 'Use to browse pages with hierarchy and templates. Supports parent filter, menu_order sort. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, status: { type: 'string', default: 'publish' }, parent: { type: 'number' }, orderby: { type: 'string', default: 'menu_order' }, order: { type: 'string', default: 'asc' }, search: { type: 'string' }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }}},
467
+ { name: 'wp_get_page', description: 'Use to read a single page with content and template. Read-only. Warning: Elementor pages can exceed 100k chars.', inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] }},
468
+ { name: 'wp_create_page', description: 'Use to create a page (defaults to draft). Supports parent, template, menu_order. Write — blocked by WP_READ_ONLY.', inputSchema: { type: 'object', properties: { title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string', default: 'draft' }, parent: { type: 'number', default: 0 }, template: { type: 'string' }, menu_order: { type: 'number', default: 0 }, excerpt: { type: 'string' }, slug: { type: 'string' }, featured_media: { type: 'number' }, meta: { type: 'object' } }, required: ['title', 'content'] }},
469
+ { name: 'wp_update_page', description: 'Use to modify any page field. Write — blocked by WP_READ_ONLY.', inputSchema: { type: 'object', properties: { id: { type: 'number' }, title: { type: 'string' }, content: { type: 'string' }, status: { type: 'string' }, parent: { type: 'number' }, template: { type: 'string' }, menu_order: { type: 'number' }, excerpt: { type: 'string' }, slug: { type: 'string' }, featured_media: { type: 'number' }, meta: { type: 'object' } }, required: ['id'] }},
469
470
 
470
471
  // ── MEDIA (3) ──
471
- { name: 'wp_list_media', description: 'List media files.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, media_type: { type: 'string' }, search: { type: 'string' }, orderby: { type: 'string', default: 'date' }, order: { type: 'string', default: 'desc' } }}},
472
- { name: 'wp_get_media', description: 'Get media details with all sizes.', inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] }},
473
- { name: 'wp_upload_media', description: 'Upload media from URL.', inputSchema: { type: 'object', properties: { url: { type: 'string' }, filename: { type: 'string' }, title: { type: 'string' }, alt_text: { type: 'string' }, caption: { type: 'string' }, description: { type: 'string' }, post_id: { type: 'number' } }, required: ['url'] }},
472
+ { name: 'wp_list_media', description: 'Use to browse media library by type (image/video/audio). Returns id, URL, alt_text, dimensions. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, media_type: { type: 'string' }, search: { type: 'string' }, orderby: { type: 'string', default: 'date' }, order: { type: 'string', default: 'desc' }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }}},
473
+ { name: 'wp_get_media', description: 'Use to get full media details with all available sizes. Read-only.', inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] }},
474
+ { name: 'wp_upload_media', description: 'Use to upload a file from a public URL to the media library. Set alt_text for image SEO. Write — blocked by WP_READ_ONLY.', inputSchema: { type: 'object', properties: { url: { type: 'string' }, filename: { type: 'string' }, title: { type: 'string' }, alt_text: { type: 'string' }, caption: { type: 'string' }, description: { type: 'string' }, post_id: { type: 'number' } }, required: ['url'] }},
474
475
 
475
476
  // ── TAXONOMIES (3) ──
476
- { name: 'wp_list_categories', description: 'List categories with hierarchy and post count.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 100 }, page: { type: 'number', default: 1 }, parent: { type: 'number' }, search: { type: 'string' }, orderby: { type: 'string', default: 'name' }, hide_empty: { type: 'boolean', default: false } }}},
477
- { name: 'wp_list_tags', description: 'List tags with post count.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 100 }, page: { type: 'number', default: 1 }, search: { type: 'string' }, orderby: { type: 'string', default: 'name' }, hide_empty: { type: 'boolean', default: false } }}},
478
- { name: 'wp_create_taxonomy_term', description: 'Create a category or tag.', inputSchema: { type: 'object', properties: { taxonomy: { type: 'string' }, name: { type: 'string' }, slug: { type: 'string' }, description: { type: 'string' }, parent: { type: 'number' } }, required: ['taxonomy', 'name'] }},
477
+ { name: 'wp_list_categories', description: 'Use to list categories with hierarchy, post count, and descriptions. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 100 }, page: { type: 'number', default: 1 }, parent: { type: 'number' }, search: { type: 'string' }, orderby: { type: 'string', default: 'name' }, hide_empty: { type: 'boolean', default: false }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }}},
478
+ { name: 'wp_list_tags', description: 'Use to list tags with post count. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 100 }, page: { type: 'number', default: 1 }, search: { type: 'string' }, orderby: { type: 'string', default: 'name' }, hide_empty: { type: 'boolean', default: false }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }}},
479
+ { name: 'wp_create_taxonomy_term', description: 'Use to create a new category or tag. Write — blocked by WP_READ_ONLY.', inputSchema: { type: 'object', properties: { taxonomy: { type: 'string' }, name: { type: 'string' }, slug: { type: 'string' }, description: { type: 'string' }, parent: { type: 'number' } }, required: ['taxonomy', 'name'] }},
479
480
 
480
481
  // ── COMMENTS (2) ──
481
- { name: 'wp_list_comments', description: 'List comments with filters.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, post: { type: 'number' }, status: { type: 'string' }, orderby: { type: 'string', default: 'date_gmt' }, order: { type: 'string', default: 'desc' }, search: { type: 'string' } }}},
482
- { name: 'wp_create_comment', description: 'Create a comment or reply.', inputSchema: { type: 'object', properties: { post: { type: 'number' }, content: { type: 'string' }, parent: { type: 'number', default: 0 }, author_name: { type: 'string' }, author_email: { type: 'string' }, status: { type: 'string', default: 'approved' } }, required: ['post', 'content'] }},
482
+ { name: 'wp_list_comments', description: 'Use to list comments filtered by post or status (approved/hold/spam). Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, post: { type: 'number' }, status: { type: 'string' }, orderby: { type: 'string', default: 'date_gmt' }, order: { type: 'string', default: 'desc' }, search: { type: 'string' }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }}},
483
+ { name: 'wp_create_comment', description: 'Use to post a comment or reply on any post. Write — blocked by WP_READ_ONLY.', inputSchema: { type: 'object', properties: { post: { type: 'number' }, content: { type: 'string' }, parent: { type: 'number', default: 0 }, author_name: { type: 'string' }, author_email: { type: 'string' }, status: { type: 'string', default: 'approved' } }, required: ['post', 'content'] }},
483
484
 
484
485
  // ── CUSTOM POST TYPES (2) ──
485
- { name: 'wp_list_post_types', description: 'Discover all registered post types (CPT).', inputSchema: { type: 'object', properties: {} }},
486
- { name: 'wp_list_custom_posts', description: 'List posts from any custom post type.', inputSchema: { type: 'object', properties: { post_type: { type: 'string' }, per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, status: { type: 'string', default: 'publish' }, orderby: { type: 'string', default: 'date' }, order: { type: 'string', default: 'desc' }, search: { type: 'string' } }, required: ['post_type'] }},
486
+ { name: 'wp_list_post_types', description: 'Use to discover all registered post types including custom ones (products, portfolio, events). Read-only.', inputSchema: { type: 'object', properties: {} }},
487
+ { name: 'wp_list_custom_posts', description: 'Use to list any custom post type (products, portfolio, events). Requires post_type slug. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.', inputSchema: { type: 'object', properties: { post_type: { type: 'string' }, per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, status: { type: 'string', default: 'publish' }, orderby: { type: 'string', default: 'date' }, order: { type: 'string', default: 'desc' }, search: { type: 'string' }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }, required: ['post_type'] }},
487
488
 
488
489
  // ── USERS (1) ──
489
- { name: 'wp_list_users', description: 'List users with roles.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, roles: { type: 'string' }, search: { type: 'string' }, orderby: { type: 'string', default: 'name' } }}},
490
+ { name: 'wp_list_users', description: 'Use to list site users with roles. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.', inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, roles: { type: 'string' }, search: { type: 'string' }, orderby: { type: 'string', default: 'name' }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }}},
490
491
 
491
492
  // ── MULTI-TARGET (1) ──
492
- { name: 'wp_set_target', description: 'Switch active WordPress site (multi-target mode). Use wp_list_targets to see available sites.',
493
+ { name: 'wp_set_target', description: 'Use to switch active WordPress site in multi-target mode. Write.',
493
494
  inputSchema: { type: 'object', properties: { site: { type: 'string', description: 'Site key from targets config' } }, required: ['site'] }},
494
495
 
495
496
  // ── SITE INFO (1) ──
496
- { name: 'wp_site_info', description: 'Get site info, current user, post types, enterprise controls, and available targets.',
497
+ { name: 'wp_site_info', description: 'Use first to discover site config, user, post types, governance flags, and available targets. Read-only.',
497
498
  inputSchema: { type: 'object', properties: {} }},
498
499
 
499
500
  // ── SEO METADATA (3) ──
500
- { name: 'wp_get_seo_meta', description: 'Get SEO metadata (title, description, focus keyword, robots, canonical, og) for a post or page. Auto-detects Yoast SEO, RankMath, SEOPress, or All in One SEO. Preferred over wp_get_post for SEO-only workflows — no HTML content loaded.',
501
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Post or page ID' }, post_type: { type: 'string', default: 'post', description: 'post or page' } }, required: ['id'] }},
502
- { name: 'wp_update_seo_meta', description: 'Update SEO metadata (title, description, focus keyword) for a post or page. Auto-detects installed SEO plugin.',
503
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Post or page ID' }, post_type: { type: 'string', default: 'post', description: 'post or page' }, title: { type: 'string', description: 'SEO title' }, description: { type: 'string', description: 'Meta description' }, focus_keyword: { type: 'string', description: 'Focus keyword' }, canonical_url: { type: 'string', description: 'Canonical URL' }, robots_noindex: { type: 'boolean', description: 'Set noindex' }, robots_nofollow: { type: 'boolean', description: 'Set nofollow' } }, required: ['id'] }},
504
- { name: 'wp_audit_seo', description: 'Audit SEO metadata across multiple posts/pages. Returns scores and issue flags only never loads post content, safe for bulk operations.',
501
+ { name: 'wp_get_seo_meta', description: 'Use to read SEO title, description, focus keyword, canonical, robots, OG for one post. Auto-detects Yoast/RankMath/SEOPress/AIOSEO. Read-only. Hint: prefer this over wp_get_post for SEO-only workflows.',
502
+ inputSchema: { type: 'object', properties: { id: { type: 'number' }, post_type: { type: 'string', default: 'post', description: 'post or page' } }, required: ['id'] }},
503
+ { name: 'wp_update_seo_meta', description: 'Use to update SEO title, description, focus keyword, canonical, or robots. Auto-detects SEO plugin. Write — blocked by WP_READ_ONLY.',
504
+ inputSchema: { type: 'object', properties: { id: { type: 'number' }, post_type: { type: 'string', default: 'post', description: 'post or page' }, title: { type: 'string', description: 'SEO title' }, description: { type: 'string', description: 'Meta description' }, focus_keyword: { type: 'string', description: 'Focus keyword' }, canonical_url: { type: 'string' }, robots_noindex: { type: 'boolean' }, robots_nofollow: { type: 'boolean' } }, required: ['id'] }},
505
+ { name: 'wp_audit_seo', description: 'Use for quick bulk SEO scoring (0-100) across posts or pages. Checks missing titles, descriptions, keywords, and length issues. Read-only. Hint: use wp_audit_rendered_seo for rendered-vs-stored comparison.',
505
506
  inputSchema: { type: 'object', properties: { post_type: { type: 'string', default: 'post', description: 'post or page' }, per_page: { type: 'number', default: 20, description: 'Number of posts to audit (max 100)' }, status: { type: 'string', default: 'publish' }, orderby: { type: 'string', default: 'date' }, order: { type: 'string', default: 'desc' } }}},
506
507
 
507
508
  // ── PLUGINS (3) ──
508
- { name: 'wp_list_plugins', description: 'List WordPress plugins with status filtering. Requires Administrator role (activate_plugins capability). Returns plugin name, version, status, author, and description.',
509
- inputSchema: { type: 'object', properties: { search: { type: 'string', description: 'Filter plugins by search term' }, status: { type: 'string', enum: ['active', 'inactive', 'all'], default: 'all', description: 'Filter by plugin status (active, inactive, all)' }, per_page: { type: 'number', default: 20, description: 'Number of plugins to return (1-100)' } }}},
510
- { name: 'wp_activate_plugin', description: 'Activate a WordPress plugin. Requires Administrator role (activate_plugins capability). Blocked by WP_READ_ONLY and WP_DISABLE_PLUGIN_MANAGEMENT.',
509
+ { name: 'wp_list_plugins', description: 'Use to list installed plugins with status and version. Requires Administrator role. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.',
510
+ inputSchema: { type: 'object', properties: { search: { type: 'string' }, status: { type: 'string', enum: ['active', 'inactive', 'all'], default: 'all' }, per_page: { type: 'number', default: 20 }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }}},
511
+ { name: 'wp_activate_plugin', description: 'Use to activate a plugin. Write blocked by WP_READ_ONLY, WP_DISABLE_PLUGIN_MANAGEMENT.',
511
512
  inputSchema: { type: 'object', properties: { plugin: { type: 'string', description: 'Plugin slug/file (e.g. "akismet/akismet.php"). Use wp_list_plugins to find the correct value.' } }, required: ['plugin'] }},
512
- { name: 'wp_deactivate_plugin', description: 'Deactivate a WordPress plugin. Requires Administrator role (activate_plugins capability). Blocked by WP_READ_ONLY and WP_DISABLE_PLUGIN_MANAGEMENT.',
513
+ { name: 'wp_deactivate_plugin', description: 'Use to deactivate a plugin. Write blocked by WP_READ_ONLY, WP_DISABLE_PLUGIN_MANAGEMENT.',
513
514
  inputSchema: { type: 'object', properties: { plugin: { type: 'string', description: 'Plugin slug/file (e.g. "akismet/akismet.php"). Use wp_list_plugins to find the correct value.' } }, required: ['plugin'] }},
514
515
 
515
516
  // ── THEMES (2) ──
516
- { name: 'wp_list_themes', description: 'List installed WordPress themes with status filtering. Requires switch_themes capability (Administrator or Editor role).',
517
- inputSchema: { type: 'object', properties: { status: { type: 'string', enum: ['active', 'inactive', 'all'], default: 'all', description: 'Filter by theme status' }, per_page: { type: 'number', default: 20, description: 'Number of themes to return (1-100)' } }}},
518
- { name: 'wp_get_theme', description: 'Get details of a specific WordPress theme by stylesheet slug. Requires switch_themes capability.',
517
+ { name: 'wp_list_themes', description: 'Use to list installed themes with active theme detection. Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.',
518
+ inputSchema: { type: 'object', properties: { status: { type: 'string', enum: ['active', 'inactive', 'all'], default: 'all' }, per_page: { type: 'number', default: 20 }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }}},
519
+ { name: 'wp_get_theme', description: 'Use to get theme details by stylesheet slug. Read-only.',
519
520
  inputSchema: { type: 'object', properties: { stylesheet: { type: 'string', description: 'Theme stylesheet slug (e.g. "twentytwentyfour"). Use wp_list_themes to find the correct value.' } }, required: ['stylesheet'] }},
520
521
 
521
522
  // ── REVISIONS (4) ──
522
- { name: 'wp_list_revisions', description: 'List revisions of a post or page. Returns metadata without content (use wp_get_revision for full content).',
523
- inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Post or page ID' }, post_type: { type: 'string', enum: ['post', 'page'], default: 'post', description: 'Post type' }, per_page: { type: 'number', default: 10, description: 'Number of revisions to return (1-100)' } }, required: ['post_id'] }},
524
- { name: 'wp_get_revision', description: 'Get a specific revision with full content.',
525
- inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Post or page ID' }, revision_id: { type: 'number', description: 'Revision ID' }, post_type: { type: 'string', enum: ['post', 'page'], default: 'post', description: 'Post type' } }, required: ['post_id', 'revision_id'] }},
526
- { name: 'wp_restore_revision', description: 'Restore a post or page to a previous revision. Copies revision content back to the post. Blocked by WP_READ_ONLY.',
527
- inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Post or page ID' }, revision_id: { type: 'number', description: 'Revision ID to restore' }, post_type: { type: 'string', enum: ['post', 'page'], default: 'post', description: 'Post type' } }, required: ['post_id', 'revision_id'] }},
528
- { name: 'wp_delete_revision', description: 'Permanently delete a revision. This action cannot be undone. Blocked by WP_READ_ONLY and WP_DISABLE_DELETE. When WP_CONFIRM_DESTRUCTIVE=true, requires a confirmation_token.',
529
- inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Post or page ID' }, revision_id: { type: 'number', description: 'Revision ID to delete' }, post_type: { type: 'string', enum: ['post', 'page'], default: 'post', description: 'Post type' }, confirmation_token: { type: 'string', description: 'Confirmation token returned by the first call when WP_CONFIRM_DESTRUCTIVE=true' } }, required: ['post_id', 'revision_id'] }},
523
+ { name: 'wp_list_revisions', description: 'Use to list revisions of a post or page (metadata only). Read-only. Hint: use mode=\'summary\' for listings, \'ids_only\' for batch ops.',
524
+ inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page'], default: 'post' }, per_page: { type: 'number', default: 10 }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'], default: 'full', description: 'full=all fields, summary=key fields only, ids_only=flat ID array' } }, required: ['post_id'] }},
525
+ { name: 'wp_get_revision', description: 'Use to read a specific revision with full content. Read-only.',
526
+ inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, revision_id: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page'], default: 'post' } }, required: ['post_id', 'revision_id'] }},
527
+ { name: 'wp_restore_revision', description: 'Use to restore a post to a previous revision. Write blocked by WP_READ_ONLY.',
528
+ inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, revision_id: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page'], default: 'post' } }, required: ['post_id', 'revision_id'] }},
529
+ { name: 'wp_delete_revision', description: 'Use to permanently delete a revision. Write blocked by WP_READ_ONLY, WP_DISABLE_DELETE, WP_CONFIRM_DESTRUCTIVE.',
530
+ inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, revision_id: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page'], default: 'post' }, confirmation_token: { type: 'string', description: 'Confirmation token returned by the first call when WP_CONFIRM_DESTRUCTIVE=true' } }, required: ['post_id', 'revision_id'] }},
530
531
 
531
532
  // ── LINK ANALYSIS (2) ──
532
- { name: 'wp_analyze_links', description: 'Analyze internal and external links in a post. Optionally checks for broken internal links via HEAD requests (read-only).',
533
- inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Post ID to analyze' }, check_broken: { type: 'boolean', default: true, description: 'Check broken internal links via HEAD request' }, timeout_ms: { type: 'number', default: 5000, description: 'Timeout per HEAD request in ms' } }, required: ['post_id'] }},
534
- { name: 'wp_suggest_internal_links', description: 'Suggest internal links for a post based on keyword relevance, shared categories, and content freshness (read-only). Use before wp_get_post with content_format:"links_only" to map existing links first.',
535
- inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Post ID to get suggestions for' }, max_suggestions: { type: 'number', default: 5, description: 'Number of suggestions (1-10)' }, focus_keywords: { type: 'array', items: { type: 'string' }, description: 'Additional keywords to match against' }, exclude_already_linked: { type: 'boolean', default: true, description: 'Exclude posts already linked from the current post' } }, required: ['post_id'] }},
533
+ { name: 'wp_analyze_links', description: 'Use to audit all internal and external links in a single post via HEAD requests. Returns broken/warning/ok status per link. Read-only.',
534
+ inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, check_broken: { type: 'boolean', default: true, description: 'Check broken internal links via HEAD request' }, timeout_ms: { type: 'number', default: 5000, description: 'Timeout per HEAD request in ms' } }, required: ['post_id'] }},
535
+ { name: 'wp_suggest_internal_links', description: 'Use to get scored internal link suggestions for a post based on keyword relevance, categories, and freshness. Read-only. Hint: call wp_get_post with content_format=\'links_only\' first to map existing links.',
536
+ inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, max_suggestions: { type: 'number', default: 5, description: 'Number of suggestions (1-10)' }, focus_keywords: { type: 'array', items: { type: 'string' }, description: 'Additional keywords to match against' }, exclude_already_linked: { type: 'boolean', default: true, description: 'Exclude posts already linked from the current post' } }, required: ['post_id'] }},
536
537
 
537
538
  // ── WOOCOMMERCE (6) ──
538
- { name: 'wc_list_products', description: 'List WooCommerce products with filtering and search. Requires WC_CONSUMER_KEY and WC_CONSUMER_SECRET. Blocked by WP_READ_ONLY.',
539
- inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, status: { type: 'string', default: 'any', description: 'any, draft, pending, private, publish' }, search: { type: 'string' }, category: { type: 'number', description: 'Category ID' }, orderby: { type: 'string', default: 'date', description: 'date, id, title, price, popularity' }, order: { type: 'string', default: 'desc', description: 'asc or desc' } }}},
540
- { name: 'wc_get_product', description: 'Get a WooCommerce product by ID with full details. Includes variations summary for variable products. Blocked by WP_READ_ONLY.',
541
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Product ID' } }, required: ['id'] }},
542
- { name: 'wc_list_orders', description: 'List WooCommerce orders with filtering. Blocked by WP_READ_ONLY.',
543
- inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, status: { type: 'string', default: 'any', description: 'any, pending, processing, on-hold, completed, cancelled, refunded, failed' }, customer: { type: 'number', description: 'Customer ID' }, orderby: { type: 'string', default: 'date' }, order: { type: 'string', default: 'desc' } }}},
544
- { name: 'wc_get_order', description: 'Get a WooCommerce order by ID with full details including line items, shipping, billing, and payment info. Blocked by WP_READ_ONLY.',
545
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Order ID' } }, required: ['id'] }},
546
- { name: 'wc_list_customers', description: 'List WooCommerce customers with search and filtering. Blocked by WP_READ_ONLY.',
539
+ { name: 'wc_list_products', description: 'Use to browse WooCommerce products. Filter by status, category, or search. Read-only.',
540
+ inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, status: { type: 'string', default: 'any', description: 'any, draft, pending, private, publish' }, search: { type: 'string' }, category: { type: 'number' }, orderby: { type: 'string', default: 'date', description: 'date, id, title, price, popularity' }, order: { type: 'string', default: 'desc' } }}},
541
+ { name: 'wc_get_product', description: 'Use to get full product details including variations summary. Read-only.',
542
+ inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] }},
543
+ { name: 'wc_list_orders', description: 'Use to browse WooCommerce orders. Filter by status or customer. Read-only.',
544
+ inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, status: { type: 'string', default: 'any', description: 'any, pending, processing, on-hold, completed, cancelled, refunded, failed' }, customer: { type: 'number' }, orderby: { type: 'string', default: 'date' }, order: { type: 'string', default: 'desc' } }}},
545
+ { name: 'wc_get_order', description: 'Use to get order details with line items, shipping, billing, and payment info. Read-only.',
546
+ inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] }},
547
+ { name: 'wc_list_customers', description: 'Use to list WooCommerce customers with search and role filtering. Read-only.',
547
548
  inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 10 }, page: { type: 'number', default: 1 }, search: { type: 'string' }, orderby: { type: 'string', default: 'date_created' }, order: { type: 'string', default: 'desc' }, role: { type: 'string', default: 'customer' } }}},
548
- { name: 'wc_price_guardrail', description: 'Analyze a product price change without modifying anything. Returns safe/unsafe assessment based on threshold percentage. Always allowed even in WP_READ_ONLY mode.',
549
- inputSchema: { type: 'object', properties: { product_id: { type: 'number', description: 'Product ID' }, new_price: { type: 'number', description: 'Proposed new price' }, threshold_percent: { type: 'number', default: 20, description: 'Maximum allowed change percentage (default 20)' } }, required: ['product_id', 'new_price'] }},
549
+ { name: 'wc_price_guardrail', description: 'Use BEFORE changing a product price. Analyzes safety of proposed price change. Read-only, always allowed.',
550
+ inputSchema: { type: 'object', properties: { product_id: { type: 'number' }, new_price: { type: 'number', description: 'Proposed new price' }, threshold_percent: { type: 'number', default: 20, description: 'Maximum allowed change percentage (default 20)' } }, required: ['product_id', 'new_price'] }},
550
551
 
551
552
  // ── WOOCOMMERCE INTELLIGENCE (4) ──
552
- { name: 'wc_inventory_alert', description: 'Identify low-stock and out-of-stock products below a threshold, sorted by urgency. Blocked by WP_READ_ONLY.',
553
- inputSchema: { type: 'object', properties: { threshold: { type: 'number', default: 5, description: 'Stock quantity threshold (default 5)' }, per_page: { type: 'number', default: 50, description: 'Products to scan (max 100)' }, include_out_of_stock: { type: 'boolean', default: true, description: 'Include out-of-stock products' } }}},
554
- { name: 'wc_order_intelligence', description: 'Analyze customer purchase history: lifetime value, average order, favourite products, order frequency, status breakdown. Blocked by WP_READ_ONLY.',
555
- inputSchema: { type: 'object', properties: { customer_id: { type: 'number', description: 'Customer ID' } }, required: ['customer_id'] }},
556
- { name: 'wc_seo_product_audit', description: 'Audit WooCommerce product listings for SEO issues (missing descriptions, images, alt text, generic slugs, missing price). Returns per-product scores. Blocked by WP_READ_ONLY.',
557
- inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 20, description: 'Products to audit (max 100)' }, page: { type: 'number', default: 1 } }}},
558
- { name: 'wc_suggest_product_links', description: 'Suggest WooCommerce products to link from a WordPress blog post based on SEO keyword relevance. Blocked by WP_READ_ONLY.',
559
- inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'WordPress post ID' }, max_suggestions: { type: 'number', default: 3, description: 'Maximum suggestions (1-5)' } }, required: ['post_id'] }},
553
+ { name: 'wc_inventory_alert', description: 'Use to identify low-stock and out-of-stock products below a threshold, sorted by urgency. Read-only.',
554
+ inputSchema: { type: 'object', properties: { threshold: { type: 'number', default: 5, description: 'Stock quantity threshold (default 5)' }, per_page: { type: 'number', default: 50 }, include_out_of_stock: { type: 'boolean', default: true, description: 'Include out-of-stock products' } }}},
555
+ { name: 'wc_order_intelligence', description: 'Use to analyze a customer\'s purchase history: lifetime value, average order, favorite products, frequency. Read-only.',
556
+ inputSchema: { type: 'object', properties: { customer_id: { type: 'number' } }, required: ['customer_id'] }},
557
+ { name: 'wc_seo_product_audit', description: 'Use to audit WooCommerce product listings for SEO issues (missing descriptions, images, alt text, generic slugs). Read-only.',
558
+ inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 20 }, page: { type: 'number', default: 1 } }}},
559
+ { name: 'wc_suggest_product_links', description: 'Use to suggest WooCommerce products to link from a blog post based on keyword relevance. Read-only.',
560
+ inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, max_suggestions: { type: 'number', default: 3, description: 'Maximum suggestions (1-5)' } }, required: ['post_id'] }},
560
561
 
561
562
  // ── WOOCOMMERCE WRITE (3) ──
562
- { name: 'wc_update_product', description: 'Update a WooCommerce product. Includes automatic price guardrail: price changes >20% require price_guardrail_confirmed=true. Blocked by WP_READ_ONLY.',
563
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Product ID' }, name: { type: 'string' }, description: { type: 'string' }, short_description: { type: 'string' }, regular_price: { type: 'string', description: 'Format "19.99"' }, sale_price: { type: 'string' }, status: { type: 'string', description: 'publish, draft, or private' }, price_guardrail_confirmed: { type: 'boolean', default: false, description: 'Set true to bypass price guardrail after calling wc_price_guardrail' } }, required: ['id'] }},
564
- { name: 'wc_update_stock', description: 'Update stock quantity of a WooCommerce product or variation. Blocked by WP_READ_ONLY.',
565
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Product ID' }, stock_quantity: { type: 'number', description: 'New stock quantity (>= 0)' }, variation_id: { type: 'number', description: 'Variation ID (for variable products)' } }, required: ['id', 'stock_quantity'] }},
566
- { name: 'wc_update_order_status', description: 'Update WooCommerce order status with transition validation. Blocked by WP_READ_ONLY.',
567
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Order ID' }, status: { type: 'string', description: 'New status (processing, completed, cancelled, refunded, on-hold, failed)' }, note: { type: 'string', description: 'Optional internal note added to the order' } }, required: ['id', 'status'] }},
563
+ { name: 'wc_update_product', description: 'Use to update product fields (title, description, price, stock, status). Write blocked by WP_READ_ONLY. Hint: call wc_price_guardrail first for price changes.',
564
+ inputSchema: { type: 'object', properties: { id: { type: 'number' }, name: { type: 'string' }, description: { type: 'string' }, short_description: { type: 'string' }, regular_price: { type: 'string', description: 'Format "19.99"' }, sale_price: { type: 'string' }, status: { type: 'string', description: 'publish, draft, or private' }, price_guardrail_confirmed: { type: 'boolean', default: false, description: 'Set true to bypass price guardrail after calling wc_price_guardrail' } }, required: ['id'] }},
565
+ { name: 'wc_update_stock', description: 'Use to update stock quantity of a product or variation. Write — blocked by WP_READ_ONLY.',
566
+ inputSchema: { type: 'object', properties: { id: { type: 'number' }, stock_quantity: { type: 'number' }, variation_id: { type: 'number', description: 'Variation ID (for variable products)' } }, required: ['id', 'stock_quantity'] }},
567
+ { name: 'wc_update_order_status', description: 'Use to transition order status (e.g. processing → completed). Write — blocked by WP_READ_ONLY.',
568
+ inputSchema: { type: 'object', properties: { id: { type: 'number' }, status: { type: 'string', description: 'processing, completed, cancelled, refunded, on-hold, failed' }, note: { type: 'string' } }, required: ['id', 'status'] }},
568
569
 
569
570
  // ── SEO ADVANCED (3) ──
570
- { name: 'wp_audit_media_seo', description: 'Audit media library images for SEO issues (missing alt text, filename-as-alt, alt too short). Optionally scans post content for inline images. Read-only.',
571
- inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 50, description: 'Media items to audit (max 100)' }, page: { type: 'number', default: 1 }, post_id: { type: 'number', description: 'Optional: also scan inline images from this post' } }}},
572
- { name: 'wp_find_orphan_pages', description: 'Find published pages with no internal links pointing to them from other pages. Read-only.',
573
- inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 100, description: 'Pages to scan (max 100)' }, exclude_ids: { type: 'array', items: { type: 'number' }, description: 'Page IDs to exclude from orphan check' }, min_words: { type: 'number', default: 0, description: 'Minimum word count to include in results' } }}},
574
- { name: 'wp_audit_heading_structure', description: 'Audit heading hierarchy (H1-H6) of a post or page for SEO issues: H1 in content, level skips, empty headings, keyword stuffing. Read-only.',
575
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Post or page ID' }, post_type: { type: 'string', default: 'post', description: 'post or page' }, focus_keyword: { type: 'string', description: 'Optional keyword to check in H2 headings' } }, required: ['id'] }},
571
+ { name: 'wp_audit_media_seo', description: 'Use when checking image SEO. Scans media library for missing/short alt text and bad filenames. Returns per-image scores + fix list. Read-only.',
572
+ inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 50 }, page: { type: 'number', default: 1 }, post_id: { type: 'number', description: 'Also scan inline images from this post' } }}},
573
+ { name: 'wp_find_orphan_pages', description: 'Use to find posts with zero inbound internal links, sorted by word count. Read-only. Hint: combine with wp_suggest_internal_links to fix orphans.',
574
+ inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 100 }, exclude_ids: { type: 'array', items: { type: 'number' }, description: 'Page IDs to exclude from orphan check' }, min_words: { type: 'number', default: 0, description: 'Minimum word count to include in results' } }}},
575
+ { name: 'wp_audit_heading_structure', description: 'Use to check H1-H6 hierarchy in a single post. Detects H1 in body, level skips, empty headings. Read-only.',
576
+ inputSchema: { type: 'object', properties: { id: { type: 'number' }, post_type: { type: 'string', default: 'post', description: 'post or page' }, focus_keyword: { type: 'string', description: 'Keyword to check in H2 headings' } }, required: ['id'] }},
576
577
 
577
578
  // ── SEO ADVANCED v4.1 (3) ──
578
- { name: 'wp_find_thin_content', description: 'Find thin/low-quality published posts: too short, outdated, uncategorized. Classifies severity and suggests actions. Read-only.',
579
- inputSchema: { type: 'object', properties: { limit: { type: 'number', default: 100, description: 'Max posts to scan (1-500)' }, min_words: { type: 'number', default: 300, description: 'Threshold for "too short"' }, critical_words: { type: 'number', default: 150, description: 'Threshold for "very short"' }, max_age_days: { type: 'number', default: 730, description: 'Days since update to flag as outdated' }, include_uncategorized: { type: 'boolean', default: true, description: 'Flag uncategorized posts' }, post_type: { type: 'string', default: 'post', description: 'post or page' } }}},
580
- { name: 'wp_audit_canonicals', description: 'Audit canonical URLs for SEO issues: missing, HTTP on HTTPS site, staging URLs, wrong domain, trailing slash mismatch. Auto-detects SEO plugin. Read-only.',
581
- inputSchema: { type: 'object', properties: { limit: { type: 'number', default: 50, description: 'Max posts to audit (1-200)' }, post_type: { type: 'string', default: 'post', description: 'post, page, or both' }, check_staging_patterns: { type: 'boolean', default: true, description: 'Detect staging/dev URLs' } }}},
582
- { name: 'wp_analyze_eeat_signals', description: 'Analyze E-E-A-T (Experience, Expertise, Authoritativeness, Trustworthiness) signals for posts. Scores 0-100 with actionable priority fixes. Read-only.',
583
- inputSchema: { type: 'object', properties: { post_ids: { type: 'array', items: { type: 'number' }, description: 'Specific post IDs to analyze (if empty, audits latest N)' }, limit: { type: 'number', default: 10, description: 'Number of latest posts to audit (1-50)' }, post_type: { type: 'string', default: 'post', description: 'post or page' }, authoritative_domains: { type: 'array', items: { type: 'string' }, default: ['wikipedia.org', 'gov', 'edu', 'who.int', 'pubmed'], description: 'Domains considered authoritative' } }}},
579
+ { name: 'wp_find_thin_content', description: 'Use to surface short/low-quality posts below a word count threshold. Classifies severity. Read-only.',
580
+ inputSchema: { type: 'object', properties: { limit: { type: 'number', default: 100 }, min_words: { type: 'number', default: 300, description: 'Threshold for "too short"' }, critical_words: { type: 'number', default: 150, description: 'Threshold for "very short"' }, max_age_days: { type: 'number', default: 730, description: 'Days since update to flag as outdated' }, include_uncategorized: { type: 'boolean', default: true, description: 'Flag uncategorized posts' }, post_type: { type: 'string', default: 'post', description: 'post or page' } }}},
581
+ { name: 'wp_audit_canonicals', description: 'Use to validate canonical URLs across posts/pages. Detects missing, mismatched, or staging URLs. Auto-detects SEO plugin. Read-only.',
582
+ inputSchema: { type: 'object', properties: { limit: { type: 'number', default: 50 }, post_type: { type: 'string', default: 'post', description: 'post, page, or both' }, check_staging_patterns: { type: 'boolean', default: true, description: 'Detect staging/dev URLs' } }}},
583
+ { name: 'wp_analyze_eeat_signals', description: 'Use to score E-E-A-T per post (0-100): author bio, dates, citations, word count, structured data. Read-only.',
584
+ inputSchema: { type: 'object', properties: { post_ids: { type: 'array', items: { type: 'number' }, description: 'Specific post IDs (if empty, audits latest N)' }, limit: { type: 'number', default: 10 }, post_type: { type: 'string', default: 'post', description: 'post or page' }, authoritative_domains: { type: 'array', items: { type: 'string' }, default: ['wikipedia.org', 'gov', 'edu', 'who.int', 'pubmed'], description: 'Domains considered authoritative' } }}},
584
585
 
585
586
  // ── SEO ADVANCED v4.2 (4) ──
586
- { name: 'wp_find_broken_internal_links', description: 'Scan published posts, extract all internal links and verify accessibility via HEAD requests. Detects 404s, 301/302 redirects, timeouts and network errors. Configurable batching to avoid overloading WordPress. Read-only.',
587
- inputSchema: { type: 'object', properties: { limit_posts: { type: 'number', default: 20, description: 'Max posts to scan (1-100)' }, batch_size: { type: 'number', default: 5, description: 'Links per batch (1-10)' }, timeout_ms: { type: 'number', default: 5000, description: 'Timeout per HEAD request (1000-30000)' }, delay_ms: { type: 'number', default: 200, description: 'Delay between batches (0-2000)' }, post_type: { type: 'string', default: 'post', description: 'post, page, or both' }, include_redirects: { type: 'boolean', default: true, description: 'Include 301/302 redirects in results' } }}},
588
- { name: 'wp_find_keyword_cannibalization', description: 'Detect articles targeting the same SEO focus keywords, creating internal competition that dilutes authority. Returns cannibalization groups with recommended actions. Read-only.',
589
- inputSchema: { type: 'object', properties: { limit: { type: 'number', default: 200, description: 'Max posts to analyze (1-500)' }, post_type: { type: 'string', default: 'post', description: 'post, page, or both' }, similarity_mode: { type: 'string', default: 'normalized', description: 'exact or normalized keyword matching' }, min_group_size: { type: 'number', default: 2, description: 'Minimum articles per group (min 2)' } }}},
590
- { name: 'wp_audit_taxonomies', description: 'Audit categories and tags for taxonomy bloat: empty tags, single-post tags, near-duplicate terms, categories without SEO description. These archive pages create crawl waste for Googlebot. Read-only.',
591
- inputSchema: { type: 'object', properties: { check_tags: { type: 'boolean', default: true, description: 'Audit tags' }, check_categories: { type: 'boolean', default: true, description: 'Audit categories' }, min_posts_threshold: { type: 'number', default: 2, description: 'Minimum posts per term' }, detect_duplicates: { type: 'boolean', default: true, description: 'Detect near-duplicate terms via Levenshtein' } }}},
592
- { name: 'wp_audit_outbound_links', description: 'Analyze outbound (external) links in published posts. Detects posts without external sources, posts with too many outbound links (dilution), and identifies most-cited domains. Good outbound link profile improves E-E-A-T credibility. Read-only.',
593
- inputSchema: { type: 'object', properties: { limit: { type: 'number', default: 30, description: 'Max posts to audit (1-100)' }, post_type: { type: 'string', default: 'post', description: 'post or page' }, min_outbound: { type: 'number', default: 1, description: 'Minimum outbound links threshold' }, max_outbound: { type: 'number', default: 15, description: 'Maximum outbound links before dilution warning' }, authoritative_domains: { type: 'array', items: { type: 'string' }, default: ['wikipedia.org', 'gov', 'edu', 'who.int', 'pubmed.ncbi'], description: 'Domains considered authoritative' } }}},
587
+ { name: 'wp_find_broken_internal_links', description: 'Use to check internal links via HEAD requests. Returns broken (4xx), redirected (3xx), and slow links. Read-only.',
588
+ inputSchema: { type: 'object', properties: { limit_posts: { type: 'number', default: 20 }, batch_size: { type: 'number', default: 5, description: 'Links per batch (1-10)' }, timeout_ms: { type: 'number', default: 5000, description: 'Timeout per HEAD request (1000-30000)' }, delay_ms: { type: 'number', default: 200, description: 'Delay between batches (0-2000)' }, post_type: { type: 'string', default: 'post', description: 'post, page, or both' }, include_redirects: { type: 'boolean', default: true, description: 'Include 301/302 redirects in results' } }}},
589
+ { name: 'wp_find_keyword_cannibalization', description: 'Use to detect posts competing on the same focus keyword. Groups conflicts, flags weakest post. Read-only.',
590
+ inputSchema: { type: 'object', properties: { limit: { type: 'number', default: 200 }, post_type: { type: 'string', default: 'post', description: 'post, page, or both' }, similarity_mode: { type: 'string', default: 'normalized', description: 'exact or normalized keyword matching' }, min_group_size: { type: 'number', default: 2, description: 'Minimum articles per group (min 2)' } }}},
591
+ { name: 'wp_audit_taxonomies', description: 'Use to detect taxonomy bloat: empty/single-post terms, near-duplicates, missing descriptions. Read-only.',
592
+ inputSchema: { type: 'object', properties: { check_tags: { type: 'boolean', default: true }, check_categories: { type: 'boolean', default: true }, min_posts_threshold: { type: 'number', default: 2, description: 'Minimum posts per term' }, detect_duplicates: { type: 'boolean', default: true, description: 'Detect near-duplicate terms via Levenshtein' } }}},
593
+ { name: 'wp_audit_outbound_links', description: 'Use to analyze external link profile per post. Detects over-linking, missing nofollow, broken external URLs. Read-only.',
594
+ inputSchema: { type: 'object', properties: { limit: { type: 'number', default: 30 }, post_type: { type: 'string', default: 'post', description: 'post or page' }, min_outbound: { type: 'number', default: 1, description: 'Minimum outbound links threshold' }, max_outbound: { type: 'number', default: 15, description: 'Maximum outbound links before dilution warning' }, authoritative_domains: { type: 'array', items: { type: 'string' }, default: ['wikipedia.org', 'gov', 'edu', 'who.int', 'pubmed.ncbi'], description: 'Domains considered authoritative' } }}},
594
595
 
595
596
  // ── CONTENT INTELLIGENCE v4.4 (2) ──
596
- { name: 'wp_get_content_brief', description: 'Aggregate a complete editorial/SEO brief for a single post or page in one call: word count, readability score, heading structure, internal/external links, SEO metadata, content sections (intro, conclusion, FAQ, lists, tables, images), categories and tags resolved to names. Read-only.',
597
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Post or page ID' }, post_type: { type: 'string', default: 'post', description: 'post or page' } }, required: ['id'] }},
598
- { name: 'wp_extract_post_outline', description: 'Extract heading structure (H1-H4) from N posts in a category to build a reference outline. Returns per-post outlines with word counts, aggregated stats, and common H2 patterns ranked by frequency. Read-only.',
599
- inputSchema: { type: 'object', properties: { category_id: { type: 'number', description: 'Category ID to analyze' }, post_type: { type: 'string', default: 'post', description: 'post or page' }, limit: { type: 'number', default: 10, description: 'Max posts to analyze (1-50)' } }, required: ['category_id'] }},
597
+ { name: 'wp_get_content_brief', description: 'Use to get a compact content brief in 1 call: title, SEO meta, headings, word count, links, categories. Read-only. Hint: start here before writing or rewriting content.',
598
+ inputSchema: { type: 'object', properties: { id: { type: 'number' }, post_type: { type: 'string', default: 'post', description: 'post or page' } }, required: ['id'] }},
599
+ { name: 'wp_extract_post_outline', description: 'Use to extract H1-H4 outline from N posts in a category as a reference template for new content. Read-only.',
600
+ inputSchema: { type: 'object', properties: { category_id: { type: 'number' }, post_type: { type: 'string', default: 'post', description: 'post or page' }, limit: { type: 'number', default: 10 } }, required: ['category_id'] }},
600
601
 
601
602
  // ── CONTENT INTELLIGENCE v4.4 Week 2 (3) ──
602
- { name: 'wp_audit_readability', description: 'Audit Flesch-Kincaid FR readability in bulk across N published posts. Returns per-post scores, transition word density, passive voice ratio, issues flagged, and aggregated distribution. Sorted worst-first. Read-only.',
603
- inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Max posts to analyze (1-200, default 50)' }, post_type: { type: 'string', enum: ['post', 'page'], description: 'post or page (default post)' }, category_id: { type: 'number', description: 'Optional: filter by category ID' }, min_words: { type: 'number', description: 'Minimum word count to include (default 100)' } }}},
604
- { name: 'wp_audit_update_frequency', description: 'Find posts not modified in X days, cross-referenced with SEO metadata quality to prioritize content refreshes. Sorted by priority (age x SEO gap). Read-only.',
605
- inputSchema: { type: 'object', properties: { days_threshold: { type: 'number', description: 'Flag posts not modified in X days (default 180)' }, limit: { type: 'number', description: 'Max posts to analyze (1-200, default 50)' }, post_type: { type: 'string', enum: ['post', 'page'], description: 'post or page (default post)' }, include_seo_score: { type: 'boolean', description: 'Cross-reference with SEO metadata quality (default true)' } }}},
606
- { name: 'wp_build_link_map', description: 'Build a complete internal linking matrix with simplified PageRank scoring. Identifies orphan posts, inbound/outbound counts, and generates a sparse adjacency matrix. Read-only.',
607
- inputSchema: { type: 'object', properties: { post_type: { type: 'string', enum: ['post', 'page', 'both'], description: 'post, page, or both (default post)' }, limit: { type: 'number', description: 'Max posts to analyze (1-200, default 50)' }, category_id: { type: 'number', description: 'Optional: filter by category ID' } }}},
603
+ { name: 'wp_audit_readability', description: 'Use to score text readability (Flesch-Kincaid adapted). Returns transition density and passive ratio. Read-only.',
604
+ inputSchema: { type: 'object', properties: { limit: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page'] }, category_id: { type: 'number' }, min_words: { type: 'number', description: 'Minimum word count to include (default 100)' } }}},
605
+ { name: 'wp_audit_update_frequency', description: 'Use to find stale posts not updated since N days, cross-referenced with SEO score. Read-only.',
606
+ inputSchema: { type: 'object', properties: { days_threshold: { type: 'number', description: 'Flag posts not modified in X days (default 180)' }, limit: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page'] }, include_seo_score: { type: 'boolean', description: 'Cross-reference with SEO metadata quality (default true)' } }}},
607
+ { name: 'wp_build_link_map', description: 'Use to generate full internal link matrix with simplified PageRank scores per post. Read-only.',
608
+ inputSchema: { type: 'object', properties: { post_type: { type: 'string', enum: ['post', 'page', 'both'] }, limit: { type: 'number' }, category_id: { type: 'number' } }}},
608
609
 
609
610
  // ── CONTENT INTELLIGENCE v4.4 Week 3 (3) ──
610
- { name: 'wp_audit_anchor_texts', description: 'Audit internal link anchor text diversity and relevance across N posts. Detects generic anchors, over-optimized anchors, image links without text. Sorted by diversity score ASC. Read-only.',
611
- inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Max posts to analyze (1-200, default 50)' }, post_type: { type: 'string', enum: ['post', 'page', 'both'], description: 'post, page, or both (default post)' } }}},
612
- { name: 'wp_audit_schema_markup', description: 'Detect and validate JSON-LD schema.org blocks in post HTML. Checks Article, FAQPage, HowTo, LocalBusiness, BreadcrumbList types for required fields. Read-only.',
613
- inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Max posts to analyze (1-200, default 50)' }, post_type: { type: 'string', enum: ['post', 'page', 'both'], description: 'post, page, or both (default post)' } }}},
614
- { name: 'wp_audit_content_structure', description: 'Analyze editorial structure: intro/conclusion/FAQ presence, heading density, lists, tables, images, paragraphs. Scores 0-100. Sorted by structure score ASC. Read-only.',
615
- inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Max posts to analyze (1-200, default 50)' }, post_type: { type: 'string', enum: ['post', 'page', 'both'], description: 'post, page, or both (default post)' }, category_id: { type: 'number', description: 'Optional: filter by category ID' } }}},
611
+ { name: 'wp_audit_anchor_texts', description: 'Use to check internal link anchor diversity. Detects generic (\'click here\') and over-optimized anchors. Read-only.',
612
+ inputSchema: { type: 'object', properties: { limit: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page', 'both'] } }}},
613
+ { name: 'wp_audit_schema_markup', description: 'Use to detect and validate JSON-LD in post HTML content (Article, FAQ, HowTo, LocalBusiness). Read-only. Hint: use wp_audit_schema_plugins for plugin-native schema instead.',
614
+ inputSchema: { type: 'object', properties: { limit: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page', 'both'] } }}},
615
+ { name: 'wp_audit_content_structure', description: 'Use to analyze post structure: intro/body/conclusion ratio, FAQ presence, TOC, lists, tables. Read-only.',
616
+ inputSchema: { type: 'object', properties: { limit: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page', 'both'] }, category_id: { type: 'number' } }}},
616
617
 
617
618
  // ── CONTENT INTELLIGENCE v4.4 Batch 4A (4) ──
618
- { name: 'wp_find_duplicate_content', description: 'Detect near-duplicate content via TF-IDF cosine similarity. Returns duplicate pairs with severity and clusters. Read-only.',
619
- inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Max posts to analyze (1-100, default 50)' }, post_type: { type: 'string', enum: ['post', 'page'], description: 'post or page (default post)' }, category_id: { type: 'number', description: 'Optional: filter by category ID' }, similarity_threshold: { type: 'number', description: 'Minimum similarity to flag (0.0-1.0, default 0.7)' } }}},
620
- { name: 'wp_find_content_gaps', description: 'Identify taxonomy terms (categories/tags) with too few posts content creation opportunities. Read-only.',
621
- inputSchema: { type: 'object', properties: { min_posts: { type: 'number', description: 'Minimum posts per term to NOT be flagged (default 3)' }, taxonomy: { type: 'string', enum: ['category', 'post_tag', 'both'], description: 'Taxonomy to analyze (default both)' }, exclude_empty: { type: 'boolean', description: 'Exclude terms with 0 posts (default false)' } }}},
622
- { name: 'wp_extract_faq_blocks', description: 'Inventory all FAQ blocks: JSON-LD FAQPage, Gutenberg Yoast/RankMath blocks, HTML Q&A patterns. Read-only.',
623
- inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Max posts to scan (1-200, default 50)' }, post_type: { type: 'string', enum: ['post', 'page', 'both'], description: 'post, page, or both (default post)' } }}},
624
- { name: 'wp_audit_cta_presence', description: 'Detect CTA presence per post: contact links, forms, buttons, phone links, quote requests, signup links. Scores 0-100. Read-only.',
625
- inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Max posts to analyze (1-200, default 50)' }, post_type: { type: 'string', enum: ['post', 'page', 'both'], description: 'post, page, or both (default post)' }, category_id: { type: 'number', description: 'Optional: filter by category ID' } }}},
619
+ { name: 'wp_find_duplicate_content', description: 'Use to detect near-duplicate posts via TF-IDF cosine similarity. Read-only.',
620
+ inputSchema: { type: 'object', properties: { limit: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page'] }, category_id: { type: 'number' }, similarity_threshold: { type: 'number', description: 'Minimum similarity to flag (0.0-1.0, default 0.7)' } }}},
621
+ { name: 'wp_find_content_gaps', description: 'Use to find under-represented taxonomy terms (< N posts) as content creation opportunities. Read-only.',
622
+ inputSchema: { type: 'object', properties: { min_posts: { type: 'number', description: 'Minimum posts per term to NOT be flagged (default 3)' }, taxonomy: { type: 'string', enum: ['category', 'post_tag', 'both'] }, exclude_empty: { type: 'boolean', description: 'Exclude terms with 0 posts (default false)' } }}},
623
+ { name: 'wp_extract_faq_blocks', description: 'Use to inventory all FAQ blocks (Gutenberg + schema JSON-LD) across the corpus. Read-only.',
624
+ inputSchema: { type: 'object', properties: { limit: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page', 'both'] } }}},
625
+ { name: 'wp_audit_cta_presence', description: 'Use to detect presence/absence of CTAs (contact links, forms, buttons) per post. Read-only.',
626
+ inputSchema: { type: 'object', properties: { limit: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page', 'both'] }, category_id: { type: 'number' } }}},
626
627
 
627
628
  // ── CONTENT INTELLIGENCE v4.4 Batch 4B (4) ──
628
- { name: 'wp_extract_entities', description: 'Extract named entities (brands, locations, persons, organizations) from posts using regex heuristics. Read-only.',
629
- inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Max posts to analyze (1-100, default 20)' }, post_type: { type: 'string', enum: ['post', 'page'], description: 'post or page (default post)' }, min_occurrences: { type: 'number', description: 'Minimum total occurrences across corpus (default 2)' } }}},
630
- { name: 'wp_get_publishing_velocity', description: 'Analyze publishing cadence by author and category over configurable periods. Read-only.',
631
- inputSchema: { type: 'object', properties: { periods: { type: 'string', description: "Comma-separated day periods (default '30,90,180')" }, post_type: { type: 'string', enum: ['post', 'page'], description: 'post or page (default post)' }, limit: { type: 'number', description: 'Max posts to fetch (1-500, default 200)' } }}},
632
- { name: 'wp_compare_revisions_diff', description: 'Diff between two post revisions: lines/words added/removed, headings diff, amplitude score. Read-only.',
633
- inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Post or page ID' }, revision_id_from: { type: 'number', description: 'Older revision ID (baseline)' }, revision_id_to: { type: 'number', description: 'Newer revision ID (omit for current post)' }, post_type: { type: 'string', enum: ['post', 'page'], description: 'post or page (default post)' } }, required: ['post_id', 'revision_id_from'] }},
634
- { name: 'wp_list_posts_by_word_count', description: 'List posts sorted by word count with automatic length segmentation and distribution stats. Read-only.',
635
- inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Max posts to analyze (1-500, default 100)' }, post_type: { type: 'string', enum: ['post', 'page', 'both'], description: 'post, page, or both (default post)' }, order: { type: 'string', enum: ['asc', 'desc'], description: 'Sort order by word count (default desc)' }, category_id: { type: 'number', description: 'Optional: filter by category ID' } }}}
629
+ { name: 'wp_extract_entities', description: 'Use to extract named entities (brands, places, people, organizations) from post content. Read-only.',
630
+ inputSchema: { type: 'object', properties: { limit: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page'] }, min_occurrences: { type: 'number', description: 'Minimum total occurrences across corpus (default 2)' } }}},
631
+ { name: 'wp_get_publishing_velocity', description: 'Use to measure publication cadence per author and category over 30/90/180 days. Read-only.',
632
+ inputSchema: { type: 'object', properties: { periods: { type: 'string', description: "Comma-separated day periods (default '30,90,180')" }, post_type: { type: 'string', enum: ['post', 'page'] }, limit: { type: 'number' } }}},
633
+ { name: 'wp_compare_revisions_diff', description: 'Use to diff two revisions and measure update amplitude. Read-only.',
634
+ inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, revision_id_from: { type: 'number', description: 'Older revision ID (baseline)' }, revision_id_to: { type: 'number', description: 'Newer revision ID (omit for current post)' }, post_type: { type: 'string', enum: ['post', 'page'] } }, required: ['post_id', 'revision_id_from'] }},
635
+ { name: 'wp_list_posts_by_word_count', description: 'Use to rank posts by length with auto-segmentation (<500, 500-1k, 1k-2k, 2k+). Read-only.',
636
+ inputSchema: { type: 'object', properties: { limit: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page', 'both'] }, order: { type: 'string', enum: ['asc', 'desc'] }, category_id: { type: 'number' } }}},
637
+
638
+ // ── PLUGIN INTELLIGENCE v4.5 (3) ──
639
+ { name: 'wp_get_rendered_head', description: 'Use to fetch the real <head> HTML Google sees via RankMath/Yoast headless endpoint. Compares rendered vs stored meta. Read-only. Requires WP_ENABLE_PLUGIN_INTELLIGENCE=true.',
640
+ inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page'] } }, required: ['post_id'] }},
641
+ { name: 'wp_audit_rendered_seo', description: 'Use for bulk rendered-vs-stored SEO divergence detection with per-post scoring. Read-only. Requires WP_ENABLE_PLUGIN_INTELLIGENCE=true.',
642
+ inputSchema: { type: 'object', properties: { limit: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page'] } }}},
643
+ { name: 'wp_get_pillar_content', description: 'Use to read or set RankMath cornerstone/pillar flag. Read always allowed. Write — blocked by WP_READ_ONLY. Requires WP_ENABLE_PLUGIN_INTELLIGENCE=true.',
644
+ inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, set_pillar: { type: 'boolean', description: 'Set pillar flag (true=pillar, false=not). Requires write access.' }, list_pillars: { type: 'boolean', description: 'List all pillar content posts (ignores post_id)' }, post_type: { type: 'string', enum: ['post', 'page'] }, limit: { type: 'number' } }}},
645
+
646
+ // ── PLUGIN INTELLIGENCE v4.5 batch 2 (3) ──
647
+ { name: 'wp_audit_schema_plugins', description: 'Use to validate JSON-LD from SEO plugin native fields (rank_math_schema or yoast_head_json). Read-only. Requires WP_ENABLE_PLUGIN_INTELLIGENCE=true.',
648
+ inputSchema: { type: 'object', properties: { limit: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page', 'both'] } }}},
649
+ { name: 'wp_get_seo_score', description: 'Use to read RankMath native SEO score (0-100). Bulk mode shows distribution stats. Read-only. Requires WP_ENABLE_PLUGIN_INTELLIGENCE=true.',
650
+ inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, limit: { type: 'number', description: 'Bulk mode: max posts (1-100, default 20)' }, post_type: { type: 'string', enum: ['post', 'page'] }, order: { type: 'string', enum: ['asc', 'desc'] } }}},
651
+ { name: 'wp_get_twitter_meta', description: 'Use to read or update Twitter Card meta (title, description, image) for RankMath/Yoast/SEOPress. Write — blocked by WP_READ_ONLY. Requires WP_ENABLE_PLUGIN_INTELLIGENCE=true.',
652
+ inputSchema: { type: 'object', properties: { post_id: { type: 'number' }, post_type: { type: 'string', enum: ['post', 'page'] }, twitter_title: { type: 'string', description: 'Set Twitter title (write mode)' }, twitter_description: { type: 'string', description: 'Set Twitter description (write mode)' }, twitter_image: { type: 'string', description: 'Set Twitter image URL (write mode)' } }, required: ['post_id'] }}
636
653
  ];
637
654
 
655
+ function getFilteredTools(allTools = TOOLS_DEFINITIONS) {
656
+ const pluginIntelTools = ['wp_get_rendered_head', 'wp_audit_rendered_seo', 'wp_get_pillar_content', 'wp_audit_schema_plugins', 'wp_get_seo_score', 'wp_get_twitter_meta'];
657
+ const editorialTools = ['wp_submit_for_review', 'wp_approve_post', 'wp_reject_post'];
658
+ return allTools.filter(tool => {
659
+ const n = tool.name;
660
+ if (n.startsWith('wc_') && !process.env.WC_CONSUMER_KEY) return false;
661
+ if (editorialTools.includes(n) && process.env.WP_REQUIRE_APPROVAL !== 'true') return false;
662
+ if (pluginIntelTools.includes(n) && process.env.WP_ENABLE_PLUGIN_INTELLIGENCE !== 'true') return false;
663
+ return true;
664
+ });
665
+ }
666
+
638
667
  function registerHandlers(s) {
639
- s.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS_DEFINITIONS }));
668
+ s.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: getFilteredTools() }));
640
669
  s.setRequestHandler(CallToolRequestSchema, handleToolCall);
641
670
  }
642
671
 
@@ -778,11 +807,19 @@ export async function handleToolCall(request) {
778
807
  // ── PAGES ──
779
808
 
780
809
  case 'wp_list_pages': {
781
- validateInput(args, { per_page: { type: 'number', min: 1, max: 100 }, page: { type: 'number', min: 1 }, status: { type: 'string', enum: STATUSES }, order: { type: 'string', enum: ORDERS } });
782
- const { per_page = 10, page = 1, status = 'publish', parent, orderby = 'menu_order', order = 'asc', search } = args;
810
+ validateInput(args, { per_page: { type: 'number', min: 1, max: 100 }, page: { type: 'number', min: 1 }, status: { type: 'string', enum: STATUSES }, order: { type: 'string', enum: ORDERS }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'] } });
811
+ const { per_page = 10, page = 1, status = 'publish', parent, orderby = 'menu_order', order = 'asc', search, mode = 'full' } = args;
783
812
  let ep = `/pages?per_page=${per_page}&page=${page}&status=${status}&orderby=${orderby}&order=${order}`;
784
813
  if (parent !== undefined) ep += `&parent=${parent}`; if (search) ep += `&search=${encodeURIComponent(search)}`;
785
814
  const pgs = await wpApiCall(ep);
815
+ if (mode === 'ids_only') {
816
+ result = json({ total: pgs.length, page, mode: 'ids_only', ids: pgs.map(p => p.id) });
817
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 }); break;
818
+ }
819
+ if (mode === 'summary') {
820
+ result = json({ total: pgs.length, page, mode: 'summary', pages: pgs.map(p => ({ id: p.id, title: p.title.rendered, slug: p.slug, status: p.status, link: p.link, parent: p.parent })) });
821
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 }); break;
822
+ }
786
823
  result = json({ total: pgs.length, page, pages: pgs.map(p => ({ id: p.id, title: p.title.rendered, status: p.status, date: p.date, link: p.link, parent: p.parent, menu_order: p.menu_order, template: p.template, excerpt: strip(p.excerpt.rendered).substring(0, 200) })) });
787
824
  auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
788
825
  break;
@@ -822,11 +859,19 @@ export async function handleToolCall(request) {
822
859
  // ── MEDIA ──
823
860
 
824
861
  case 'wp_list_media': {
825
- validateInput(args, { per_page: { type: 'number', min: 1, max: 100 }, page: { type: 'number', min: 1 }, media_type: { type: 'string', enum: MEDIA_TYPES }, order: { type: 'string', enum: ORDERS } });
826
- const { per_page = 10, page = 1, media_type, search, orderby = 'date', order = 'desc' } = args;
862
+ validateInput(args, { per_page: { type: 'number', min: 1, max: 100 }, page: { type: 'number', min: 1 }, media_type: { type: 'string', enum: MEDIA_TYPES }, order: { type: 'string', enum: ORDERS }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'] } });
863
+ const { per_page = 10, page = 1, media_type, search, orderby = 'date', order = 'desc', mode = 'full' } = args;
827
864
  let ep = `/media?per_page=${per_page}&page=${page}&orderby=${orderby}&order=${order}`;
828
865
  if (media_type) ep += `&media_type=${media_type}`; if (search) ep += `&search=${encodeURIComponent(search)}`;
829
866
  const media = await wpApiCall(ep);
867
+ if (mode === 'ids_only') {
868
+ result = json({ total: media.length, page, mode: 'ids_only', ids: media.map(m => m.id) });
869
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 }); break;
870
+ }
871
+ if (mode === 'summary') {
872
+ result = json({ total: media.length, page, mode: 'summary', media: media.map(m => ({ id: m.id, title: m.title.rendered, mime_type: m.mime_type, source_url: m.source_url, alt_text: m.alt_text })) });
873
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 }); break;
874
+ }
830
875
  result = json({ total: media.length, page, media: media.map(m => ({ id: m.id, title: m.title.rendered, date: m.date, mime_type: m.mime_type, source_url: m.source_url, alt_text: m.alt_text, width: m.media_details?.width, height: m.media_details?.height })) });
831
876
  auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
832
877
  break;
@@ -863,20 +908,36 @@ export async function handleToolCall(request) {
863
908
  // ── TAXONOMIES ──
864
909
 
865
910
  case 'wp_list_categories': {
866
- const { per_page = 100, page = 1, parent, search, orderby = 'name', hide_empty = false } = args;
911
+ const { per_page = 100, page = 1, parent, search, orderby = 'name', hide_empty = false, mode = 'full' } = args;
867
912
  let ep = `/categories?per_page=${per_page}&page=${page}&orderby=${orderby}&hide_empty=${hide_empty}`;
868
913
  if (parent !== undefined) ep += `&parent=${parent}`; if (search) ep += `&search=${encodeURIComponent(search)}`;
869
914
  const cats = await wpApiCall(ep);
915
+ if (mode === 'ids_only') {
916
+ result = json({ total: cats.length, mode: 'ids_only', ids: cats.map(c => c.id) });
917
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 }); break;
918
+ }
919
+ if (mode === 'summary') {
920
+ result = json({ total: cats.length, mode: 'summary', categories: cats.map(c => ({ id: c.id, name: c.name, slug: c.slug, count: c.count, parent: c.parent })) });
921
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 }); break;
922
+ }
870
923
  result = json({ total: cats.length, categories: cats.map(c => ({ id: c.id, name: c.name, slug: c.slug, description: c.description, parent: c.parent, count: c.count })) });
871
924
  auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
872
925
  break;
873
926
  }
874
927
 
875
928
  case 'wp_list_tags': {
876
- const { per_page = 100, page = 1, search, orderby = 'name', hide_empty = false } = args;
929
+ const { per_page = 100, page = 1, search, orderby = 'name', hide_empty = false, mode = 'full' } = args;
877
930
  let ep = `/tags?per_page=${per_page}&page=${page}&orderby=${orderby}&hide_empty=${hide_empty}`;
878
931
  if (search) ep += `&search=${encodeURIComponent(search)}`;
879
932
  const tags = await wpApiCall(ep);
933
+ if (mode === 'ids_only') {
934
+ result = json({ total: tags.length, mode: 'ids_only', ids: tags.map(t => t.id) });
935
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 }); break;
936
+ }
937
+ if (mode === 'summary') {
938
+ result = json({ total: tags.length, mode: 'summary', tags: tags.map(t => ({ id: t.id, name: t.name, slug: t.slug, count: t.count })) });
939
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 }); break;
940
+ }
880
941
  result = json({ total: tags.length, tags: tags.map(t => ({ id: t.id, name: t.name, slug: t.slug, count: t.count })) });
881
942
  auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
882
943
  break;
@@ -897,10 +958,18 @@ export async function handleToolCall(request) {
897
958
  // ── COMMENTS ──
898
959
 
899
960
  case 'wp_list_comments': {
900
- const { per_page = 10, page = 1, post, status, orderby = 'date_gmt', order = 'desc', search } = args;
961
+ const { per_page = 10, page = 1, post, status, orderby = 'date_gmt', order = 'desc', search, mode = 'full' } = args;
901
962
  let ep = `/comments?per_page=${per_page}&page=${page}&orderby=${orderby}&order=${order}`;
902
963
  if (post) ep += `&post=${post}`; if (status) ep += `&status=${status}`; if (search) ep += `&search=${encodeURIComponent(search)}`;
903
964
  const comments = await wpApiCall(ep);
965
+ if (mode === 'ids_only') {
966
+ result = json({ total: comments.length, page, mode: 'ids_only', ids: comments.map(c => c.id) });
967
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 }); break;
968
+ }
969
+ if (mode === 'summary') {
970
+ result = json({ total: comments.length, page, mode: 'summary', comments: comments.map(c => ({ id: c.id, post: c.post, author_name: c.author_name, date: c.date, status: c.status })) });
971
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 }); break;
972
+ }
904
973
  result = json({ total: comments.length, page, comments: comments.map(c => ({ id: c.id, post: c.post, parent: c.parent, author_name: c.author_name, date: c.date, status: c.status, content: strip(c.content.rendered).substring(0, 300), link: c.link })) });
905
974
  auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
906
975
  break;
@@ -927,9 +996,9 @@ export async function handleToolCall(request) {
927
996
  }
928
997
 
929
998
  case 'wp_list_custom_posts': {
930
- validateInput(args, { post_type: { type: 'string', required: true }, per_page: { type: 'number', min: 1, max: 100 }, page: { type: 'number', min: 1 }, order: { type: 'string', enum: ORDERS } });
999
+ validateInput(args, { post_type: { type: 'string', required: true }, per_page: { type: 'number', min: 1, max: 100 }, page: { type: 'number', min: 1 }, order: { type: 'string', enum: ORDERS }, mode: { type: 'string', enum: ['full', 'summary', 'ids_only'] } });
931
1000
  enforceAllowedTypes(args.post_type);
932
- const { post_type, per_page = 10, page = 1, status = 'publish', orderby = 'date', order = 'desc', search } = args;
1001
+ const { post_type, per_page = 10, page = 1, status = 'publish', orderby = 'date', order = 'desc', search, mode = 'full' } = args;
933
1002
  const types = await wpApiCall('/types');
934
1003
  const typeInfo = Object.values(types).find(t => t.slug === post_type || t.rest_base === post_type);
935
1004
  if (!typeInfo) throw new Error(`Post type "${post_type}" not found.`);
@@ -937,6 +1006,14 @@ export async function handleToolCall(request) {
937
1006
  let ep = `/${restBase}?per_page=${per_page}&page=${page}&status=${status}&orderby=${orderby}&order=${order}`;
938
1007
  if (search) ep += `&search=${encodeURIComponent(search)}`;
939
1008
  const posts = await wpApiCall(ep);
1009
+ if (mode === 'ids_only') {
1010
+ result = json({ post_type, total: posts.length, page, mode: 'ids_only', ids: posts.map(p => p.id) });
1011
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: { post_type } }); break;
1012
+ }
1013
+ if (mode === 'summary') {
1014
+ result = json({ post_type, total: posts.length, page, mode: 'summary', posts: posts.map(p => ({ id: p.id, title: p.title?.rendered || p.title, slug: p.slug, date: p.date, status: p.status, link: p.link })) });
1015
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: { post_type } }); break;
1016
+ }
940
1017
  result = json({ post_type, total: posts.length, page, posts: posts.map(p => ({ id: p.id, title: p.title?.rendered || p.title, status: p.status, date: p.date, link: p.link, slug: p.slug, type: p.type, meta: p.meta || {} })) });
941
1018
  auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: { post_type } });
942
1019
  break;
@@ -945,10 +1022,18 @@ export async function handleToolCall(request) {
945
1022
  // ── USERS ──
946
1023
 
947
1024
  case 'wp_list_users': {
948
- const { per_page = 10, page = 1, roles, search, orderby = 'name' } = args;
1025
+ const { per_page = 10, page = 1, roles, search, orderby = 'name', mode = 'full' } = args;
949
1026
  let ep = `/users?per_page=${per_page}&page=${page}&orderby=${orderby}`;
950
1027
  if (roles) ep += `&roles=${roles}`; if (search) ep += `&search=${encodeURIComponent(search)}`;
951
1028
  const users = await wpApiCall(ep);
1029
+ if (mode === 'ids_only') {
1030
+ result = json({ total: users.length, mode: 'ids_only', ids: users.map(u => u.id) });
1031
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 }); break;
1032
+ }
1033
+ if (mode === 'summary') {
1034
+ result = json({ total: users.length, mode: 'summary', users: users.map(u => ({ id: u.id, name: u.name, slug: u.slug, roles: u.roles || [] })) });
1035
+ auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 }); break;
1036
+ }
952
1037
  result = json({ total: users.length, users: users.map(u => ({ id: u.id, name: u.name, slug: u.slug, link: u.link, roles: u.roles, avatar: u.avatar_urls?.['96'] })) });
953
1038
  auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
954
1039
  break;
@@ -1005,7 +1090,14 @@ export async function handleToolCall(request) {
1005
1090
  enabled: isMultiTarget, active_site: currentTarget?.name || 'default',
1006
1091
  available_sites: Object.keys(targets)
1007
1092
  },
1008
- server: { mcp_version: VERSION, tools_count: TOOLS_COUNT }
1093
+ server: (() => {
1094
+ const exposed = getFilteredTools().length;
1095
+ const groups = [];
1096
+ if (!process.env.WC_CONSUMER_KEY) groups.push('woocommerce');
1097
+ if (process.env.WP_REQUIRE_APPROVAL !== 'true') groups.push('editorial');
1098
+ if (process.env.WP_ENABLE_PLUGIN_INTELLIGENCE !== 'true') groups.push('plugin_intelligence');
1099
+ return { mcp_version: VERSION, tools_total: TOOLS_COUNT, tools_exposed: exposed, filtered_out: groups };
1100
+ })()
1009
1101
  });
1010
1102
  auditLog({ tool: name, action: 'info', status: 'success', latency_ms: Date.now() - t0 });
1011
1103
  break;
@@ -1247,15 +1339,20 @@ export async function handleToolCall(request) {
1247
1339
  validateInput(args, {
1248
1340
  search: { type: 'string' },
1249
1341
  status: { type: 'string', enum: ['active', 'inactive', 'all'] },
1250
- per_page: { type: 'number', min: 1, max: 100 }
1342
+ per_page: { type: 'number', min: 1, max: 100 },
1343
+ mode: { type: 'string', enum: ['full', 'summary', 'ids_only'] }
1251
1344
  });
1252
- const { search, status = 'all', per_page = 20 } = args;
1345
+ const { search, status = 'all', per_page = 20, mode = 'full' } = args;
1253
1346
  let ep = `/plugins?per_page=${per_page}&context=edit`;
1254
1347
  if (search) ep += `&search=${encodeURIComponent(search)}`;
1255
1348
  if (status && status !== 'all') ep += `&status=${status}`;
1256
1349
 
1257
1350
  try {
1258
1351
  const plugins = await wpApiCall(ep);
1352
+ if (mode === 'ids_only') {
1353
+ result = json({ total: plugins.length, mode: 'ids_only', ids: plugins.map(p => p.plugin) });
1354
+ auditLog({ tool: name, action: 'list', target_type: 'plugin', status: 'success', latency_ms: Date.now() - t0, params: { search, status, per_page } }); break;
1355
+ }
1259
1356
  const mapped = plugins.map(p => ({
1260
1357
  plugin: p.plugin,
1261
1358
  name: p.name,
@@ -1269,6 +1366,10 @@ export async function handleToolCall(request) {
1269
1366
  network_only: p.network_only ?? false,
1270
1367
  textdomain: p.textdomain ?? ''
1271
1368
  }));
1369
+ if (mode === 'summary') {
1370
+ result = json({ total: mapped.length, mode: 'summary', plugins: mapped.map(p => ({ plugin: p.plugin, name: p.name, status: p.status, version: p.version })) });
1371
+ auditLog({ tool: name, action: 'list', target_type: 'plugin', status: 'success', latency_ms: Date.now() - t0, params: { search, status, per_page } }); break;
1372
+ }
1272
1373
  const activeCount = mapped.filter(p => p.status === 'active').length;
1273
1374
  const inactiveCount = mapped.filter(p => p.status === 'inactive').length;
1274
1375
  result = json({ total: mapped.length, active: activeCount, inactive: inactiveCount, plugins: mapped });
@@ -1329,13 +1430,18 @@ export async function handleToolCall(request) {
1329
1430
  case 'wp_list_themes': {
1330
1431
  validateInput(args, {
1331
1432
  status: { type: 'string', enum: ['active', 'inactive', 'all'] },
1332
- per_page: { type: 'number', min: 1, max: 100 }
1433
+ per_page: { type: 'number', min: 1, max: 100 },
1434
+ mode: { type: 'string', enum: ['full', 'summary', 'ids_only'] }
1333
1435
  });
1334
- const { status = 'all', per_page = 20 } = args;
1436
+ const { status = 'all', per_page = 20, mode = 'full' } = args;
1335
1437
  let ep = `/themes?per_page=${per_page}&context=edit`;
1336
1438
  if (status && status !== 'all') ep += `&status=${status}`;
1337
1439
  try {
1338
1440
  const themes = await wpApiCall(ep);
1441
+ if (mode === 'ids_only') {
1442
+ result = json({ total: themes.length, mode: 'ids_only', ids: themes.map(t => t.stylesheet) });
1443
+ auditLog({ tool: name, action: 'list', target_type: 'theme', status: 'success', latency_ms: Date.now() - t0, params: { status, per_page } }); break;
1444
+ }
1339
1445
  const mapped = themes.map(t => ({
1340
1446
  stylesheet: t.stylesheet,
1341
1447
  template: t.template,
@@ -1350,6 +1456,10 @@ export async function handleToolCall(request) {
1350
1456
  requires_php: t.requires_php ?? '',
1351
1457
  tags: t.tags?.rendered ?? t.tags ?? []
1352
1458
  }));
1459
+ if (mode === 'summary') {
1460
+ result = json({ total: mapped.length, mode: 'summary', themes: mapped.map(t => ({ stylesheet: t.stylesheet, name: t.name, status: t.status, version: t.version })) });
1461
+ auditLog({ tool: name, action: 'list', target_type: 'theme', status: 'success', latency_ms: Date.now() - t0, params: { status, per_page } }); break;
1462
+ }
1353
1463
  const activeTheme = mapped.find(t => t.status === 'active');
1354
1464
  result = json({ total: mapped.length, active_theme: activeTheme ? activeTheme.name : null, themes: mapped });
1355
1465
  auditLog({ tool: name, action: 'list', target_type: 'theme', status: 'success', latency_ms: Date.now() - t0, params: { status, per_page } });
@@ -1399,12 +1509,21 @@ export async function handleToolCall(request) {
1399
1509
  validateInput(args, {
1400
1510
  post_id: { type: 'number', required: true, min: 1 },
1401
1511
  post_type: { type: 'string', enum: ['post', 'page'] },
1402
- per_page: { type: 'number', min: 1, max: 100 }
1512
+ per_page: { type: 'number', min: 1, max: 100 },
1513
+ mode: { type: 'string', enum: ['full', 'summary', 'ids_only'] }
1403
1514
  });
1404
- const { post_id, post_type = 'post', per_page = 10 } = args;
1515
+ const { post_id, post_type = 'post', per_page = 10, mode = 'full' } = args;
1405
1516
  const base = post_type === 'page' ? 'pages' : 'posts';
1406
1517
  try {
1407
1518
  const revisions = await wpApiCall(`/${base}/${post_id}/revisions?per_page=${per_page}&context=edit`);
1519
+ if (mode === 'ids_only') {
1520
+ result = json({ total: revisions.length, post_id, post_type, mode: 'ids_only', ids: revisions.map(r => r.id) });
1521
+ auditLog({ tool: name, action: 'list', target: post_id, target_type: 'revision', status: 'success', latency_ms: Date.now() - t0, params: { post_type, per_page } }); break;
1522
+ }
1523
+ if (mode === 'summary') {
1524
+ result = json({ total: revisions.length, post_id, post_type, mode: 'summary', revisions: revisions.map(r => ({ id: r.id, date: r.date, author: r.author })) });
1525
+ auditLog({ tool: name, action: 'list', target: post_id, target_type: 'revision', status: 'success', latency_ms: Date.now() - t0, params: { post_type, per_page } }); break;
1526
+ }
1408
1527
  result = json({
1409
1528
  total: revisions.length,
1410
1529
  post_id,
@@ -4419,6 +4538,457 @@ export async function handleToolCall(request) {
4419
4538
  break;
4420
4539
  }
4421
4540
 
4541
+ // ── PLUGIN INTELLIGENCE v4.5 ──
4542
+
4543
+ case 'wp_get_rendered_head': {
4544
+ validateInput(args, { post_id: { type: 'number', required: true }, post_type: { type: 'string', enum: ['post', 'page'] } });
4545
+ const grhPostId = args.post_id;
4546
+ const grhPostType = args.post_type || 'post';
4547
+ const { url: grhBaseUrl, auth: grhAuth } = getActiveAuth();
4548
+
4549
+ const grhPlugin = await detectSeoPlugin(grhBaseUrl, fetch);
4550
+ if (!grhPlugin) throw new Error('No supported SEO plugin detected. wp_get_rendered_head requires RankMath or Yoast.');
4551
+ if (grhPlugin !== 'rankmath' && grhPlugin !== 'yoast') throw new Error(`Rendered head requires RankMath or Yoast (detected: ${grhPlugin})`);
4552
+
4553
+ const grhEp = grhPostType === 'page' ? `/pages/${grhPostId}?_fields=id,title,link,slug,meta` : `/posts/${grhPostId}?_fields=id,title,link,slug,meta`;
4554
+ const grhPost = await wpApiCall(grhEp);
4555
+
4556
+ const grhHeadResult = await getRenderedHead(grhBaseUrl, grhPost.link, grhPlugin, fetch, grhAuth);
4557
+ if (!grhHeadResult.success) throw new Error(grhHeadResult.error);
4558
+
4559
+ const grhParsed = parseRenderedHead(grhHeadResult.head);
4560
+ const grhMeta = grhPost.meta || {};
4561
+
4562
+ // Extract stored SEO meta based on plugin
4563
+ let grhStoredTitle, grhStoredDesc, grhStoredKeyword, grhStoredCanonical, grhStoredRobots;
4564
+ if (grhPlugin === 'rankmath') {
4565
+ grhStoredTitle = grhMeta.rank_math_title || null;
4566
+ grhStoredDesc = grhMeta.rank_math_description || null;
4567
+ grhStoredKeyword = grhMeta.rank_math_focus_keyword || null;
4568
+ grhStoredCanonical = grhMeta.rank_math_canonical_url || null;
4569
+ const rm = grhMeta.rank_math_robots || [];
4570
+ grhStoredRobots = Array.isArray(rm) && rm.length > 0 ? rm.join(', ') : null;
4571
+ } else {
4572
+ grhStoredTitle = grhMeta._yoast_wpseo_title || null;
4573
+ grhStoredDesc = grhMeta._yoast_wpseo_metadesc || null;
4574
+ grhStoredKeyword = grhMeta._yoast_wpseo_focuskw || null;
4575
+ grhStoredCanonical = grhMeta._yoast_wpseo_canonical || null;
4576
+ grhStoredRobots = grhMeta._yoast_wpseo_meta_robots_noindex === '1' ? 'noindex' : null;
4577
+ }
4578
+
4579
+ result = json({
4580
+ post_id: grhPostId,
4581
+ post_url: grhPost.link,
4582
+ seo_plugin: grhPlugin,
4583
+ rendered: grhParsed,
4584
+ stored: {
4585
+ title: grhStoredTitle,
4586
+ description: grhStoredDesc,
4587
+ focus_keyword: grhStoredKeyword,
4588
+ canonical: grhStoredCanonical,
4589
+ robots: grhStoredRobots
4590
+ },
4591
+ raw_head_length: grhHeadResult.head.length,
4592
+ schemas_count: grhParsed.schema_json_ld.length
4593
+ });
4594
+ auditLog({ tool: name, action: 'get_rendered_head', status: 'success', latency_ms: Date.now() - t0, params: { post_id: grhPostId, post_type: grhPostType, plugin: grhPlugin } });
4595
+ break;
4596
+ }
4597
+
4598
+ case 'wp_audit_rendered_seo': {
4599
+ validateInput(args, { limit: { type: 'number', min: 1, max: 50 }, post_type: { type: 'string', enum: ['post', 'page'] } });
4600
+ const arsLimit = args.limit || 10;
4601
+ const arsPostType = args.post_type || 'post';
4602
+ const { url: arsBaseUrl, auth: arsAuth } = getActiveAuth();
4603
+
4604
+ const arsPlugin = await detectSeoPlugin(arsBaseUrl, fetch);
4605
+ if (!arsPlugin) throw new Error('No supported SEO plugin detected. wp_audit_rendered_seo requires RankMath or Yoast.');
4606
+ if (arsPlugin !== 'rankmath' && arsPlugin !== 'yoast') throw new Error(`Rendered SEO audit requires RankMath or Yoast (detected: ${arsPlugin})`);
4607
+
4608
+ const arsEp = `/${arsPostType}s?per_page=${arsLimit}&status=publish&_fields=id,title,link,slug,meta`;
4609
+ const arsPosts = await wpApiCall(arsEp);
4610
+
4611
+ const arsResults = [];
4612
+ const arsSummary = { title_mismatch: 0, description_mismatch: 0, canonical_mismatch: 0, missing_rendered_title: 0, missing_rendered_description: 0, robots_mismatch: 0, schema_missing: 0 };
4613
+
4614
+ for (const p of arsPosts) {
4615
+ const headRes = await getRenderedHead(arsBaseUrl, p.link, arsPlugin, fetch, arsAuth);
4616
+ if (!headRes.success) {
4617
+ arsResults.push({ id: p.id, title: strip(p.title?.rendered || ''), url: p.link, score: 0, issues: ['head_fetch_failed'], rendered: null, stored: null });
4618
+ continue;
4619
+ }
4620
+
4621
+ const parsed = parseRenderedHead(headRes.head);
4622
+ const meta = p.meta || {};
4623
+ const issues = [];
4624
+
4625
+ // Extract stored meta
4626
+ let storedTitle, storedDesc, storedCanonical, storedRobots;
4627
+ if (arsPlugin === 'rankmath') {
4628
+ storedTitle = meta.rank_math_title || null;
4629
+ storedDesc = meta.rank_math_description || null;
4630
+ storedCanonical = meta.rank_math_canonical_url || null;
4631
+ const rm = meta.rank_math_robots || [];
4632
+ storedRobots = Array.isArray(rm) && rm.length > 0 ? rm.join(', ') : null;
4633
+ } else {
4634
+ storedTitle = meta._yoast_wpseo_title || null;
4635
+ storedDesc = meta._yoast_wpseo_metadesc || null;
4636
+ storedCanonical = meta._yoast_wpseo_canonical || null;
4637
+ storedRobots = meta._yoast_wpseo_meta_robots_noindex === '1' ? 'noindex' : null;
4638
+ }
4639
+
4640
+ // Compare rendered vs stored
4641
+ if (!parsed.title) { issues.push('missing_rendered_title'); arsSummary.missing_rendered_title++; }
4642
+ else if (storedTitle && !parsed.title.includes(storedTitle)) { issues.push('title_mismatch'); arsSummary.title_mismatch++; }
4643
+
4644
+ if (!parsed.meta_description) { issues.push('missing_rendered_description'); arsSummary.missing_rendered_description++; }
4645
+ else if (storedDesc && parsed.meta_description !== storedDesc) { issues.push('description_mismatch'); arsSummary.description_mismatch++; }
4646
+
4647
+ if (parsed.canonical && parsed.canonical !== p.link && storedCanonical && parsed.canonical !== storedCanonical) { issues.push('canonical_mismatch'); arsSummary.canonical_mismatch++; }
4648
+
4649
+ if (parsed.robots && parsed.robots.includes('noindex') && (!storedRobots || !storedRobots.includes('noindex'))) { issues.push('robots_mismatch'); arsSummary.robots_mismatch++; }
4650
+
4651
+ if (parsed.schema_json_ld.length === 0) { issues.push('schema_missing'); arsSummary.schema_missing++; }
4652
+
4653
+ const score = Math.max(0, 100 - issues.length * 15);
4654
+
4655
+ arsResults.push({
4656
+ id: p.id, title: strip(p.title?.rendered || ''), url: p.link, score, issues,
4657
+ rendered: { title: parsed.title, description: parsed.meta_description, canonical: parsed.canonical, robots: parsed.robots, schemas_count: parsed.schema_json_ld.length },
4658
+ stored: { title: storedTitle, description: storedDesc, canonical: storedCanonical, robots: storedRobots }
4659
+ });
4660
+ }
4661
+
4662
+ const arsAvgScore = arsResults.length > 0 ? arsResults.reduce((s, r) => s + r.score, 0) / arsResults.length : 0;
4663
+
4664
+ result = json({
4665
+ seo_plugin: arsPlugin,
4666
+ total_audited: arsResults.length,
4667
+ avg_score: Math.round(arsAvgScore),
4668
+ issues_summary: arsSummary,
4669
+ posts: arsResults
4670
+ });
4671
+ auditLog({ tool: name, action: 'audit_rendered_seo', status: 'success', latency_ms: Date.now() - t0, params: { limit: arsLimit, post_type: arsPostType, plugin: arsPlugin } });
4672
+ break;
4673
+ }
4674
+
4675
+ case 'wp_get_pillar_content': {
4676
+ validateInput(args, {
4677
+ post_id: { type: 'number' },
4678
+ set_pillar: { type: 'boolean' },
4679
+ list_pillars: { type: 'boolean' },
4680
+ post_type: { type: 'string', enum: ['post', 'page'] },
4681
+ limit: { type: 'number', min: 1, max: 500 }
4682
+ });
4683
+ const pcPostType = args.post_type || 'post';
4684
+ const { url: pcBaseUrl } = getActiveAuth();
4685
+
4686
+ const pcPlugin = await detectSeoPlugin(pcBaseUrl, fetch);
4687
+ if (pcPlugin !== 'rankmath') throw new Error('Pillar content requires RankMath (detected: ' + (pcPlugin || 'none') + ')');
4688
+
4689
+ if (args.list_pillars) {
4690
+ // Mode: list all pillar posts
4691
+ const pcLimit = args.limit || 100;
4692
+ const pcPosts = await wpApiCall(`/${pcPostType}s?per_page=${pcLimit}&status=publish&_fields=id,title,link,slug,meta`);
4693
+ const pillars = pcPosts.filter(p => (p.meta || {}).rank_math_pillar_content === 'on').map(p => ({
4694
+ id: p.id, title: strip(p.title?.rendered || ''), slug: p.slug, link: p.link, post_type: pcPostType
4695
+ }));
4696
+ result = json({ mode: 'list_pillars', seo_plugin: pcPlugin, pillar_count: pillars.length, pillars });
4697
+ auditLog({ tool: name, action: 'read_pillar_content', status: 'success', latency_ms: Date.now() - t0, params: { list_pillars: true, post_type: pcPostType, limit: pcLimit } });
4698
+ } else if (args.post_id !== undefined && args.set_pillar !== undefined) {
4699
+ // Mode: write — set/unset pillar flag
4700
+ if (getActiveControls().read_only) throw new Error('Blocked: READ-ONLY mode. Cannot update pillar content flag.');
4701
+ const pcPost = await wpApiCall(`/${pcPostType}s/${args.post_id}?_fields=id,title,link,meta`);
4702
+ await wpApiCall(`/${pcPostType}s/${args.post_id}`, { method: 'POST', body: JSON.stringify({ meta: { rank_math_pillar_content: args.set_pillar ? 'on' : '' } }) });
4703
+ result = json({
4704
+ mode: 'write', post_id: args.post_id, title: strip(pcPost.title?.rendered || ''),
4705
+ is_pillar: args.set_pillar, action: args.set_pillar ? 'marked_as_pillar' : 'unmarked_as_pillar', seo_plugin: pcPlugin
4706
+ });
4707
+ auditLog({ tool: name, action: 'update_pillar_content', target: args.post_id, target_type: pcPostType, status: 'success', latency_ms: Date.now() - t0, params: { set_pillar: args.set_pillar } });
4708
+ } else if (args.post_id !== undefined) {
4709
+ // Mode: read single post
4710
+ const pcPost = await wpApiCall(`/${pcPostType}s/${args.post_id}?_fields=id,title,link,meta`);
4711
+ const isPillar = (pcPost.meta || {}).rank_math_pillar_content === 'on';
4712
+ result = json({ mode: 'read', post_id: args.post_id, title: strip(pcPost.title?.rendered || ''), is_pillar: isPillar, seo_plugin: pcPlugin });
4713
+ auditLog({ tool: name, action: 'read_pillar_content', status: 'success', latency_ms: Date.now() - t0, params: { post_id: args.post_id, post_type: pcPostType } });
4714
+ } else {
4715
+ throw new Error('Provide post_id (read/write) or list_pillars:true');
4716
+ }
4717
+ break;
4718
+ }
4719
+
4720
+ case 'wp_audit_schema_plugins': {
4721
+ validateInput(args, {
4722
+ limit: { type: 'number', min: 1, max: 100 },
4723
+ post_type: { type: 'string', enum: ['post', 'page', 'both'] }
4724
+ });
4725
+ const aspLimit = args.limit || 20;
4726
+ const aspPostType = args.post_type || 'post';
4727
+ const { url: aspBaseUrl, auth: aspAuth } = getActiveAuth();
4728
+
4729
+ const aspPlugin = await detectSeoPlugin(aspBaseUrl, fetch);
4730
+ if (!aspPlugin) throw new Error('No supported SEO plugin detected');
4731
+ if (aspPlugin !== 'rankmath' && aspPlugin !== 'yoast') throw new Error(`Schema plugin audit requires RankMath or Yoast (detected: ${aspPlugin})`);
4732
+
4733
+ const aspRequired = {
4734
+ 'Article': ['headline', 'datePublished', 'author'],
4735
+ 'BlogPosting': ['headline', 'datePublished', 'author'],
4736
+ 'NewsArticle': ['headline', 'datePublished', 'author'],
4737
+ 'FAQPage': ['mainEntity'],
4738
+ 'HowTo': ['name', 'step'],
4739
+ 'LocalBusiness': ['name', 'address'],
4740
+ 'BreadcrumbList': ['itemListElement'],
4741
+ 'Organization': ['name'],
4742
+ 'WebPage': ['name'],
4743
+ 'WebSite': ['name', 'url']
4744
+ };
4745
+
4746
+ let aspAllPosts = [];
4747
+ if (aspPostType === 'both') {
4748
+ const aspP = await wpApiCall(`/posts?per_page=${aspLimit}&status=publish&_fields=id,title,link,slug,meta`);
4749
+ const aspG = await wpApiCall(`/pages?per_page=${aspLimit}&status=publish&_fields=id,title,link,slug,meta`);
4750
+ aspAllPosts = [...aspP, ...aspG];
4751
+ } else {
4752
+ aspAllPosts = await wpApiCall(`/${aspPostType}s?per_page=${aspLimit}&status=publish&_fields=id,title,link,slug,meta`);
4753
+ }
4754
+
4755
+ const aspResults = [];
4756
+ const aspTypesCounts = {};
4757
+ const aspIssuesSummary = { no_plugin_schema: 0, invalid_schema_json: 0, missing_required_fields: 0, no_article_schema: 0 };
4758
+ let aspWithSchema = 0;
4759
+
4760
+ for (const p of aspAllPosts) {
4761
+ const postIssues = [];
4762
+ const postSchemas = [];
4763
+ let schemas = [];
4764
+
4765
+ if (aspPlugin === 'rankmath') {
4766
+ const rawSchema = (p.meta || {}).rank_math_schema;
4767
+ if (!rawSchema || rawSchema === '{}') {
4768
+ postIssues.push('no_plugin_schema');
4769
+ aspIssuesSummary.no_plugin_schema++;
4770
+ } else {
4771
+ try {
4772
+ const parsed = typeof rawSchema === 'string' ? JSON.parse(rawSchema) : rawSchema;
4773
+ if (parsed['@type']) {
4774
+ schemas = [parsed];
4775
+ } else if (parsed['@graph']) {
4776
+ schemas = parsed['@graph'];
4777
+ } else {
4778
+ schemas = Object.values(parsed).filter(v => v && typeof v === 'object' && v['@type']);
4779
+ }
4780
+ if (schemas.length === 0) { postIssues.push('no_plugin_schema'); aspIssuesSummary.no_plugin_schema++; }
4781
+ } catch {
4782
+ postIssues.push('invalid_schema_json');
4783
+ aspIssuesSummary.invalid_schema_json++;
4784
+ }
4785
+ }
4786
+ } else {
4787
+ const headRes = await getRenderedHead(aspBaseUrl, p.link, aspPlugin, fetch, aspAuth);
4788
+ if (headRes.success) {
4789
+ const parsed = parseRenderedHead(headRes.head);
4790
+ schemas = parsed.schema_json_ld || [];
4791
+ if (schemas.length === 0) { postIssues.push('no_plugin_schema'); aspIssuesSummary.no_plugin_schema++; }
4792
+ } else {
4793
+ postIssues.push('no_plugin_schema');
4794
+ aspIssuesSummary.no_plugin_schema++;
4795
+ }
4796
+ }
4797
+
4798
+ let hasArticleType = false;
4799
+ for (const schema of schemas) {
4800
+ const schemaType = schema['@type'] || 'Unknown';
4801
+ if (['Article', 'BlogPosting', 'NewsArticle'].includes(schemaType)) hasArticleType = true;
4802
+ aspTypesCounts[schemaType] = (aspTypesCounts[schemaType] || 0) + 1;
4803
+
4804
+ const requiredFields = aspRequired[schemaType];
4805
+ const missingFields = [];
4806
+ if (requiredFields) {
4807
+ for (const field of requiredFields) {
4808
+ if (!schema[field]) missingFields.push(field);
4809
+ }
4810
+ }
4811
+ if (missingFields.length > 0) { postIssues.push('missing_required_fields'); aspIssuesSummary.missing_required_fields++; }
4812
+ postSchemas.push({ type: schemaType, valid: missingFields.length === 0, missing_fields: missingFields });
4813
+ }
4814
+
4815
+ if (schemas.length > 0 && !hasArticleType && aspPostType === 'post') {
4816
+ postIssues.push('no_article_schema');
4817
+ aspIssuesSummary.no_article_schema++;
4818
+ }
4819
+
4820
+ if (schemas.length > 0) aspWithSchema++;
4821
+ const postScore = Math.max(0, 100 - postIssues.length * 15);
4822
+ aspResults.push({ id: p.id, title: strip(p.title?.rendered || ''), slug: p.slug, score: postScore, schemas: postSchemas, issues: postIssues });
4823
+ }
4824
+
4825
+ const aspAvg = aspResults.length > 0 ? aspResults.reduce((s, r) => s + r.score, 0) / aspResults.length : 0;
4826
+ const aspWithout = aspAllPosts.length - aspWithSchema;
4827
+
4828
+ result = json({
4829
+ seo_plugin: aspPlugin,
4830
+ total_audited: aspResults.length,
4831
+ avg_score: Math.round(aspAvg),
4832
+ schema_coverage: {
4833
+ posts_with_schema: aspWithSchema,
4834
+ posts_without_schema: aspWithout,
4835
+ coverage_percent: aspAllPosts.length > 0 ? Math.round(aspWithSchema / aspAllPosts.length * 100) : 0
4836
+ },
4837
+ schema_types_found: aspTypesCounts,
4838
+ issues_summary: aspIssuesSummary,
4839
+ posts: aspResults
4840
+ });
4841
+ auditLog({ tool: name, action: 'audit_schema_plugins', status: 'success', latency_ms: Date.now() - t0, params: { limit: aspLimit, post_type: aspPostType, plugin: aspPlugin } });
4842
+ break;
4843
+ }
4844
+
4845
+ case 'wp_get_seo_score': {
4846
+ validateInput(args, {
4847
+ post_id: { type: 'number' },
4848
+ limit: { type: 'number', min: 1, max: 100 },
4849
+ post_type: { type: 'string', enum: ['post', 'page'] },
4850
+ order: { type: 'string', enum: ['asc', 'desc'] }
4851
+ });
4852
+ const gssPostType = args.post_type || 'post';
4853
+ const { url: gssBaseUrl } = getActiveAuth();
4854
+
4855
+ const gssPlugin = await detectSeoPlugin(gssBaseUrl, fetch);
4856
+ if (gssPlugin !== 'rankmath') throw new Error('SEO score requires RankMath (detected: ' + (gssPlugin || 'none') + ')');
4857
+
4858
+ if (args.post_id !== undefined) {
4859
+ const gssPost = await wpApiCall(`/${gssPostType}s/${args.post_id}?_fields=id,title,link,slug,meta`);
4860
+ const gssMeta = gssPost.meta || {};
4861
+ const gssRaw = gssMeta.rank_math_seo_score;
4862
+ const gssScore = gssRaw !== undefined && gssRaw !== null && gssRaw !== '' ? parseInt(gssRaw, 10) : null;
4863
+ const gssKw = gssMeta.rank_math_focus_keyword || null;
4864
+ const gssRating = gssScore === null || gssScore === 0 ? 'no_score' : gssScore >= 80 ? 'excellent' : gssScore >= 60 ? 'good' : gssScore >= 40 ? 'average' : 'poor';
4865
+
4866
+ result = json({
4867
+ mode: 'single', post_id: args.post_id, title: strip(gssPost.title?.rendered || ''),
4868
+ link: gssPost.link, seo_score: gssScore, focus_keyword: gssKw, rating: gssRating
4869
+ });
4870
+ auditLog({ tool: name, action: 'get_seo_score', status: 'success', latency_ms: Date.now() - t0, params: { post_id: args.post_id, post_type: gssPostType } });
4871
+ } else {
4872
+ const gssLimit = args.limit || 20;
4873
+ const gssSortOrder = args.order || 'desc';
4874
+ const gssPosts = await wpApiCall(`/${gssPostType}s?per_page=${gssLimit}&status=publish&_fields=id,title,link,slug,meta`);
4875
+
4876
+ const gssItems = gssPosts.map(p => {
4877
+ const m = p.meta || {};
4878
+ const raw = m.rank_math_seo_score;
4879
+ const score = raw !== undefined && raw !== null && raw !== '' ? parseInt(raw, 10) : null;
4880
+ const kw = m.rank_math_focus_keyword || null;
4881
+ const rating = score === null || score === 0 ? 'no_score' : score >= 80 ? 'excellent' : score >= 60 ? 'good' : score >= 40 ? 'average' : 'poor';
4882
+ return { id: p.id, title: strip(p.title?.rendered || ''), slug: p.slug, link: p.link, seo_score: score, focus_keyword: kw, rating };
4883
+ });
4884
+
4885
+ gssItems.sort((a, b) => {
4886
+ const sa = a.seo_score === null ? -1 : a.seo_score;
4887
+ const sb = b.seo_score === null ? -1 : b.seo_score;
4888
+ return gssSortOrder === 'asc' ? sa - sb : sb - sa;
4889
+ });
4890
+
4891
+ const gssDist = { excellent: 0, good: 0, average: 0, poor: 0, no_score: 0 };
4892
+ const gssScores = [];
4893
+ for (const item of gssItems) {
4894
+ gssDist[item.rating]++;
4895
+ if (item.seo_score !== null && item.seo_score > 0) gssScores.push(item.seo_score);
4896
+ }
4897
+ const gssTotal = gssItems.length;
4898
+ const gssAvg = gssScores.length > 0 ? gssScores.reduce((a, b) => a + b, 0) / gssScores.length : 0;
4899
+ const gssSorted = [...gssScores].sort((a, b) => a - b);
4900
+ const gssMedian = gssSorted.length > 0 ? (gssSorted.length % 2 === 0 ? (gssSorted[gssSorted.length / 2 - 1] + gssSorted[gssSorted.length / 2]) / 2 : gssSorted[Math.floor(gssSorted.length / 2)]) : 0;
4901
+
4902
+ result = json({
4903
+ mode: 'bulk', total_analyzed: gssItems.length,
4904
+ avg_score: Math.round(gssAvg), median_score: gssMedian,
4905
+ distribution: {
4906
+ excellent: { count: gssDist.excellent, percent: gssTotal > 0 ? Math.round(gssDist.excellent / gssTotal * 100) : 0 },
4907
+ good: { count: gssDist.good, percent: gssTotal > 0 ? Math.round(gssDist.good / gssTotal * 100) : 0 },
4908
+ average: { count: gssDist.average, percent: gssTotal > 0 ? Math.round(gssDist.average / gssTotal * 100) : 0 },
4909
+ poor: { count: gssDist.poor, percent: gssTotal > 0 ? Math.round(gssDist.poor / gssTotal * 100) : 0 },
4910
+ no_score: { count: gssDist.no_score, percent: gssTotal > 0 ? Math.round(gssDist.no_score / gssTotal * 100) : 0 }
4911
+ },
4912
+ posts: gssItems
4913
+ });
4914
+ auditLog({ tool: name, action: 'get_seo_score', status: 'success', latency_ms: Date.now() - t0, params: { limit: gssLimit, post_type: gssPostType, order: gssSortOrder } });
4915
+ }
4916
+ break;
4917
+ }
4918
+
4919
+ case 'wp_get_twitter_meta': {
4920
+ validateInput(args, {
4921
+ post_id: { type: 'number', required: true },
4922
+ post_type: { type: 'string', enum: ['post', 'page'] },
4923
+ twitter_title: { type: 'string' },
4924
+ twitter_description: { type: 'string' },
4925
+ twitter_image: { type: 'string' }
4926
+ });
4927
+ const gtmPostId = args.post_id;
4928
+ const gtmPostType = args.post_type || 'post';
4929
+ const { url: gtmBaseUrl } = getActiveAuth();
4930
+ const gtmPlugin = await detectSeoPlugin(gtmBaseUrl, fetch);
4931
+ if (!gtmPlugin) throw new Error('No supported SEO plugin detected');
4932
+
4933
+ const gtmIsWrite = args.twitter_title !== undefined || args.twitter_description !== undefined || args.twitter_image !== undefined;
4934
+
4935
+ if (gtmIsWrite) {
4936
+ if (getActiveControls().read_only) throw new Error('Blocked: READ-ONLY mode. Cannot update Twitter meta.');
4937
+ if (gtmPlugin !== 'rankmath' && gtmPlugin !== 'yoast') throw new Error('Twitter meta write requires RankMath or Yoast (detected: ' + gtmPlugin + ')');
4938
+
4939
+ const gtmMeta = {};
4940
+ const gtmUpdated = [];
4941
+ if (gtmPlugin === 'rankmath') {
4942
+ if (args.twitter_title !== undefined) { gtmMeta.rank_math_twitter_title = args.twitter_title; gtmUpdated.push('twitter_title'); }
4943
+ if (args.twitter_description !== undefined) { gtmMeta.rank_math_twitter_description = args.twitter_description; gtmUpdated.push('twitter_description'); }
4944
+ if (args.twitter_image !== undefined) { gtmMeta.rank_math_twitter_image = args.twitter_image; gtmUpdated.push('twitter_image'); }
4945
+ } else {
4946
+ if (args.twitter_title !== undefined) { gtmMeta['_yoast_wpseo_twitter-title'] = args.twitter_title; gtmUpdated.push('twitter_title'); }
4947
+ if (args.twitter_description !== undefined) { gtmMeta['_yoast_wpseo_twitter-description'] = args.twitter_description; gtmUpdated.push('twitter_description'); }
4948
+ if (args.twitter_image !== undefined) { gtmMeta['_yoast_wpseo_twitter-image'] = args.twitter_image; gtmUpdated.push('twitter_image'); }
4949
+ }
4950
+
4951
+ const gtmPost = await wpApiCall(`/${gtmPostType}s/${gtmPostId}?_fields=id,title,link,meta`);
4952
+ await wpApiCall(`/${gtmPostType}s/${gtmPostId}`, { method: 'POST', body: JSON.stringify({ meta: gtmMeta }) });
4953
+
4954
+ result = json({
4955
+ mode: 'write', post_id: gtmPostId, title: strip(gtmPost.title?.rendered || ''),
4956
+ seo_plugin: gtmPlugin, updated_fields: gtmUpdated,
4957
+ twitter: { title: args.twitter_title || null, description: args.twitter_description || null, image: args.twitter_image || null }
4958
+ });
4959
+ auditLog({ tool: name, action: 'update_twitter_meta', target: gtmPostId, target_type: gtmPostType, status: 'success', latency_ms: Date.now() - t0, params: { updated_fields: gtmUpdated } });
4960
+ } else {
4961
+ const gtmPost = await wpApiCall(`/${gtmPostType}s/${gtmPostId}?_fields=id,title,link,meta`);
4962
+ const gtmM = gtmPost.meta || {};
4963
+
4964
+ let gtmTitle, gtmDesc, gtmImage, gtmCard;
4965
+ if (gtmPlugin === 'rankmath') {
4966
+ gtmTitle = gtmM.rank_math_twitter_title || null;
4967
+ gtmDesc = gtmM.rank_math_twitter_description || null;
4968
+ gtmImage = gtmM.rank_math_twitter_image || null;
4969
+ gtmCard = gtmM.rank_math_twitter_card_type || null;
4970
+ } else if (gtmPlugin === 'yoast') {
4971
+ gtmTitle = gtmM['_yoast_wpseo_twitter-title'] || null;
4972
+ gtmDesc = gtmM['_yoast_wpseo_twitter-description'] || null;
4973
+ gtmImage = gtmM['_yoast_wpseo_twitter-image'] || null;
4974
+ gtmCard = null;
4975
+ } else {
4976
+ gtmTitle = gtmM._seopress_social_twitter_title || null;
4977
+ gtmDesc = gtmM._seopress_social_twitter_desc || null;
4978
+ gtmImage = gtmM._seopress_social_twitter_img || null;
4979
+ gtmCard = null;
4980
+ }
4981
+
4982
+ result = json({
4983
+ mode: 'read', post_id: gtmPostId, title: strip(gtmPost.title?.rendered || ''),
4984
+ link: gtmPost.link, seo_plugin: gtmPlugin,
4985
+ twitter: { title: gtmTitle, description: gtmDesc, image: gtmImage, card_type: gtmCard }
4986
+ });
4987
+ auditLog({ tool: name, action: 'read_twitter_meta', status: 'success', latency_ms: Date.now() - t0, params: { post_id: gtmPostId, post_type: gtmPostType, plugin: gtmPlugin } });
4988
+ }
4989
+ break;
4990
+ }
4991
+
4422
4992
  default:
4423
4993
  throw new Error(`Unknown tool: "${name}".`);
4424
4994
  }
@@ -4484,4 +5054,4 @@ if (process.env.NODE_ENV !== 'test') {
4484
5054
  main().catch((error) => { log.error(`Fatal: ${error.message}`); process.exit(1); });
4485
5055
  }
4486
5056
 
4487
- export { server, getActiveControls, getControlSources, _testSetTarget };
5057
+ export { server, getActiveControls, getControlSources, _testSetTarget, getFilteredTools };