@adsim/wordpress-mcp-server 4.5.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
@@ -447,213 +447,225 @@ function strip(html) { return (html || '').replace(/<[^>]*>/g, '').trim(); }
447
447
 
448
448
  const TOOLS_DEFINITIONS = [
449
449
  // ── POSTS (6) ──
450
- { 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' } }}},
451
- { 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'] }},
452
- { 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'] }},
453
- { 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'] }},
454
- { 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'] }},
455
- { 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'] }},
456
456
 
457
457
  // ── APPROVAL WORKFLOW (3) ──
458
- { name: 'wp_submit_for_review', description: 'Submit a draft post for editorial review (draft pending). Blocked by WP_READ_ONLY.',
459
- 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'] }},
460
- { name: 'wp_approve_post', description: 'Approve a pending post for publication (pending → publish). Blocked by WP_READ_ONLY and WP_DRAFT_ONLY.',
461
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Post ID' } }, required: ['id'] }},
462
- { 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.',
463
- 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'] }},
464
464
 
465
465
  // ── PAGES (4) ──
466
- { 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' } }}},
467
- { name: 'wp_get_page', description: 'Get page by ID with content and template.', inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] }},
468
- { 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'] }},
469
- { 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'] }},
470
470
 
471
471
  // ── MEDIA (3) ──
472
- { 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' } }}},
473
- { name: 'wp_get_media', description: 'Get media details with all sizes.', inputSchema: { type: 'object', properties: { id: { type: 'number' } }, required: ['id'] }},
474
- { 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'] }},
475
475
 
476
476
  // ── TAXONOMIES (3) ──
477
- { 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 } }}},
478
- { 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 } }}},
479
- { 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'] }},
480
480
 
481
481
  // ── COMMENTS (2) ──
482
- { 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' } }}},
483
- { 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'] }},
484
484
 
485
485
  // ── CUSTOM POST TYPES (2) ──
486
- { name: 'wp_list_post_types', description: 'Discover all registered post types (CPT).', inputSchema: { type: 'object', properties: {} }},
487
- { 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'] }},
488
488
 
489
489
  // ── USERS (1) ──
490
- { 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' } }}},
491
491
 
492
492
  // ── MULTI-TARGET (1) ──
493
- { 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.',
494
494
  inputSchema: { type: 'object', properties: { site: { type: 'string', description: 'Site key from targets config' } }, required: ['site'] }},
495
495
 
496
496
  // ── SITE INFO (1) ──
497
- { 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.',
498
498
  inputSchema: { type: 'object', properties: {} }},
499
499
 
500
500
  // ── SEO METADATA (3) ──
501
- { 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.',
502
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Post or page ID' }, post_type: { type: 'string', default: 'post', description: 'post or page' } }, required: ['id'] }},
503
- { name: 'wp_update_seo_meta', description: 'Update SEO metadata (title, description, focus keyword) for a post or page. Auto-detects installed SEO plugin.',
504
- 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'] }},
505
- { 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.',
506
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' } }}},
507
507
 
508
508
  // ── PLUGINS (3) ──
509
- { 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.',
510
- 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)' } }}},
511
- { 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.',
512
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'] }},
513
- { 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.',
514
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'] }},
515
515
 
516
516
  // ── THEMES (2) ──
517
- { name: 'wp_list_themes', description: 'List installed WordPress themes with status filtering. Requires switch_themes capability (Administrator or Editor role).',
518
- 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)' } }}},
519
- { 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.',
520
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'] }},
521
521
 
522
522
  // ── REVISIONS (4) ──
523
- { name: 'wp_list_revisions', description: 'List revisions of a post or page. Returns metadata without content (use wp_get_revision for full content).',
524
- 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'] }},
525
- { name: 'wp_get_revision', description: 'Get a specific revision with full content.',
526
- 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'] }},
527
- { 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.',
528
- 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'] }},
529
- { 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.',
530
- 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'] }},
531
531
 
532
532
  // ── LINK ANALYSIS (2) ──
533
- { name: 'wp_analyze_links', description: 'Analyze internal and external links in a post. Optionally checks for broken internal links via HEAD requests (read-only).',
534
- 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'] }},
535
- { 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.',
536
- 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'] }},
537
537
 
538
538
  // ── WOOCOMMERCE (6) ──
539
- { 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.',
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', description: 'Category ID' }, orderby: { type: 'string', default: 'date', description: 'date, id, title, price, popularity' }, order: { type: 'string', default: 'desc', description: 'asc or desc' } }}},
541
- { 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.',
542
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Product ID' } }, required: ['id'] }},
543
- { name: 'wc_list_orders', description: 'List WooCommerce orders with filtering. Blocked by WP_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', description: 'Customer ID' }, orderby: { type: 'string', default: 'date' }, order: { type: 'string', default: 'desc' } }}},
545
- { 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.',
546
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Order ID' } }, required: ['id'] }},
547
- { 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.',
548
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' } }}},
549
- { 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.',
550
- 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'] }},
551
551
 
552
552
  // ── WOOCOMMERCE INTELLIGENCE (4) ──
553
- { name: 'wc_inventory_alert', description: 'Identify low-stock and out-of-stock products below a threshold, sorted by urgency. Blocked by WP_READ_ONLY.',
554
- 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' } }}},
555
- { name: 'wc_order_intelligence', description: 'Analyze customer purchase history: lifetime value, average order, favourite products, order frequency, status breakdown. Blocked by WP_READ_ONLY.',
556
- inputSchema: { type: 'object', properties: { customer_id: { type: 'number', description: 'Customer ID' } }, required: ['customer_id'] }},
557
- { 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.',
558
- inputSchema: { type: 'object', properties: { per_page: { type: 'number', default: 20, description: 'Products to audit (max 100)' }, page: { type: 'number', default: 1 } }}},
559
- { 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.',
560
- 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'] }},
561
561
 
562
562
  // ── WOOCOMMERCE WRITE (3) ──
563
- { 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.',
564
- 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'] }},
565
- { name: 'wc_update_stock', description: 'Update stock quantity of a WooCommerce product or variation. Blocked by WP_READ_ONLY.',
566
- 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'] }},
567
- { name: 'wc_update_order_status', description: 'Update WooCommerce order status with transition validation. Blocked by WP_READ_ONLY.',
568
- 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'] }},
569
569
 
570
570
  // ── SEO ADVANCED (3) ──
571
- { 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.',
572
- 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' } }}},
573
- { name: 'wp_find_orphan_pages', description: 'Find published pages with no internal links pointing to them from other pages. Read-only.',
574
- 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' } }}},
575
- { 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.',
576
- 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'] }},
577
577
 
578
578
  // ── SEO ADVANCED v4.1 (3) ──
579
- { name: 'wp_find_thin_content', description: 'Find thin/low-quality published posts: too short, outdated, uncategorized. Classifies severity and suggests actions. Read-only.',
580
- 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' } }}},
581
- { 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.',
582
- 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' } }}},
583
- { 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.',
584
- 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' } }}},
585
585
 
586
586
  // ── SEO ADVANCED v4.2 (4) ──
587
- { 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.',
588
- 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' } }}},
589
- { 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.',
590
- 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)' } }}},
591
- { 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.',
592
- 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' } }}},
593
- { 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.',
594
- 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' } }}},
595
595
 
596
596
  // ── CONTENT INTELLIGENCE v4.4 (2) ──
597
- { 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.',
598
- inputSchema: { type: 'object', properties: { id: { type: 'number', description: 'Post or page ID' }, post_type: { type: 'string', default: 'post', description: 'post or page' } }, required: ['id'] }},
599
- { 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.',
600
- 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'] }},
601
601
 
602
602
  // ── CONTENT INTELLIGENCE v4.4 Week 2 (3) ──
603
- { 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.',
604
- 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)' } }}},
605
- { 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.',
606
- 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)' } }}},
607
- { 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.',
608
- 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' } }}},
609
609
 
610
610
  // ── CONTENT INTELLIGENCE v4.4 Week 3 (3) ──
611
- { 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.',
612
- 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)' } }}},
613
- { 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.',
614
- 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)' } }}},
615
- { 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.',
616
- 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' } }}},
617
617
 
618
618
  // ── CONTENT INTELLIGENCE v4.4 Batch 4A (4) ──
619
- { name: 'wp_find_duplicate_content', description: 'Detect near-duplicate content via TF-IDF cosine similarity. Returns duplicate pairs with severity and clusters. Read-only.',
620
- 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)' } }}},
621
- { name: 'wp_find_content_gaps', description: 'Identify taxonomy terms (categories/tags) with too few posts 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'], description: 'Taxonomy to analyze (default both)' }, exclude_empty: { type: 'boolean', description: 'Exclude terms with 0 posts (default false)' } }}},
623
- { name: 'wp_extract_faq_blocks', description: 'Inventory all FAQ blocks: JSON-LD FAQPage, Gutenberg Yoast/RankMath blocks, HTML Q&A patterns. Read-only.',
624
- 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)' } }}},
625
- { 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.',
626
- 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' } }}},
627
627
 
628
628
  // ── CONTENT INTELLIGENCE v4.4 Batch 4B (4) ──
629
- { name: 'wp_extract_entities', description: 'Extract named entities (brands, locations, persons, organizations) from posts using regex heuristics. Read-only.',
630
- 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)' } }}},
631
- { name: 'wp_get_publishing_velocity', description: 'Analyze publishing cadence by author and category over configurable periods. 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'], description: 'post or page (default post)' }, limit: { type: 'number', description: 'Max posts to fetch (1-500, default 200)' } }}},
633
- { name: 'wp_compare_revisions_diff', description: 'Diff between two post revisions: lines/words added/removed, headings diff, amplitude score. Read-only.',
634
- 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'] }},
635
- { name: 'wp_list_posts_by_word_count', description: 'List posts sorted by word count with automatic length segmentation and distribution stats. Read-only.',
636
- 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
637
 
638
638
  // ── PLUGIN INTELLIGENCE v4.5 (3) ──
639
- { name: 'wp_get_rendered_head', description: 'Retrieve the real rendered <head> HTML as seen by Google via RankMath or Yoast headless endpoint. Compares rendered meta with stored meta. Read-only.',
640
- inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Post or page ID' }, post_type: { type: 'string', enum: ['post', 'page'], description: 'post or page (default post)' } }, required: ['post_id'] }},
641
- { name: 'wp_audit_rendered_seo', description: 'Bulk audit: compare rendered <head> vs stored SEO meta for divergences (title mismatch, missing description, canonical issues, missing schema). Requires RankMath or Yoast. Read-only.',
642
- inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Max posts to audit (1-50, default 10)' }, post_type: { type: 'string', enum: ['post', 'page'], description: 'post or page (default post)' } }}},
643
- { name: 'wp_get_pillar_content', description: 'Read or set the RankMath pillar/cornerstone content flag. List all pillar posts, read a single post flag, or mark/unmark a post as pillar. Write mode blocked by WP_READ_ONLY.',
644
- inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Post or page ID (read or write mode)' }, 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'], description: 'post or page (default post)' }, limit: { type: 'number', description: 'Max posts to scan for list_pillars mode (default 100)' } }}},
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
645
 
646
646
  // ── PLUGIN INTELLIGENCE v4.5 batch 2 (3) ──
647
- { name: 'wp_audit_schema_plugins', description: 'Audit schema JSON-LD from SEO plugin native fields (rank_math_schema or Yoast head). Validates presence and required fields per @type. Read-only.',
648
- inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Max posts to audit (1-100, default 20)' }, post_type: { type: 'string', enum: ['post', 'page', 'both'], description: 'post, page, or both (default post)' } }}},
649
- { name: 'wp_get_seo_score', description: 'Read RankMath native SEO score (0-100) for a single post or bulk-list posts sorted by score with distribution stats. Read-only.',
650
- inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Single post ID to get score for' }, limit: { type: 'number', description: 'Bulk mode: max posts (1-100, default 20)' }, post_type: { type: 'string', enum: ['post', 'page'], description: 'post or page (default post)' }, order: { type: 'string', enum: ['asc', 'desc'], description: 'Sort by score (default desc)' } }}},
651
- { name: 'wp_get_twitter_meta', description: 'Read or write Twitter Card meta (title, description, image) from RankMath, Yoast, or SEOPress. Write mode blocked by WP_READ_ONLY.',
652
- inputSchema: { type: 'object', properties: { post_id: { type: 'number', description: 'Post or page ID' }, post_type: { type: 'string', enum: ['post', 'page'], description: 'post or page (default post)' }, 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'] }}
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'] }}
653
653
  ];
654
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
+
655
667
  function registerHandlers(s) {
656
- s.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS_DEFINITIONS }));
668
+ s.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: getFilteredTools() }));
657
669
  s.setRequestHandler(CallToolRequestSchema, handleToolCall);
658
670
  }
659
671
 
@@ -795,11 +807,19 @@ export async function handleToolCall(request) {
795
807
  // ── PAGES ──
796
808
 
797
809
  case 'wp_list_pages': {
798
- 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 } });
799
- 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;
800
812
  let ep = `/pages?per_page=${per_page}&page=${page}&status=${status}&orderby=${orderby}&order=${order}`;
801
813
  if (parent !== undefined) ep += `&parent=${parent}`; if (search) ep += `&search=${encodeURIComponent(search)}`;
802
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
+ }
803
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) })) });
804
824
  auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
805
825
  break;
@@ -839,11 +859,19 @@ export async function handleToolCall(request) {
839
859
  // ── MEDIA ──
840
860
 
841
861
  case 'wp_list_media': {
842
- 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 } });
843
- 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;
844
864
  let ep = `/media?per_page=${per_page}&page=${page}&orderby=${orderby}&order=${order}`;
845
865
  if (media_type) ep += `&media_type=${media_type}`; if (search) ep += `&search=${encodeURIComponent(search)}`;
846
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
+ }
847
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 })) });
848
876
  auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
849
877
  break;
@@ -880,20 +908,36 @@ export async function handleToolCall(request) {
880
908
  // ── TAXONOMIES ──
881
909
 
882
910
  case 'wp_list_categories': {
883
- 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;
884
912
  let ep = `/categories?per_page=${per_page}&page=${page}&orderby=${orderby}&hide_empty=${hide_empty}`;
885
913
  if (parent !== undefined) ep += `&parent=${parent}`; if (search) ep += `&search=${encodeURIComponent(search)}`;
886
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
+ }
887
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 })) });
888
924
  auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
889
925
  break;
890
926
  }
891
927
 
892
928
  case 'wp_list_tags': {
893
- 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;
894
930
  let ep = `/tags?per_page=${per_page}&page=${page}&orderby=${orderby}&hide_empty=${hide_empty}`;
895
931
  if (search) ep += `&search=${encodeURIComponent(search)}`;
896
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
+ }
897
941
  result = json({ total: tags.length, tags: tags.map(t => ({ id: t.id, name: t.name, slug: t.slug, count: t.count })) });
898
942
  auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
899
943
  break;
@@ -914,10 +958,18 @@ export async function handleToolCall(request) {
914
958
  // ── COMMENTS ──
915
959
 
916
960
  case 'wp_list_comments': {
917
- 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;
918
962
  let ep = `/comments?per_page=${per_page}&page=${page}&orderby=${orderby}&order=${order}`;
919
963
  if (post) ep += `&post=${post}`; if (status) ep += `&status=${status}`; if (search) ep += `&search=${encodeURIComponent(search)}`;
920
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
+ }
921
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 })) });
922
974
  auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
923
975
  break;
@@ -944,9 +996,9 @@ export async function handleToolCall(request) {
944
996
  }
945
997
 
946
998
  case 'wp_list_custom_posts': {
947
- 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'] } });
948
1000
  enforceAllowedTypes(args.post_type);
949
- 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;
950
1002
  const types = await wpApiCall('/types');
951
1003
  const typeInfo = Object.values(types).find(t => t.slug === post_type || t.rest_base === post_type);
952
1004
  if (!typeInfo) throw new Error(`Post type "${post_type}" not found.`);
@@ -954,6 +1006,14 @@ export async function handleToolCall(request) {
954
1006
  let ep = `/${restBase}?per_page=${per_page}&page=${page}&status=${status}&orderby=${orderby}&order=${order}`;
955
1007
  if (search) ep += `&search=${encodeURIComponent(search)}`;
956
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
+ }
957
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 || {} })) });
958
1018
  auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0, params: { post_type } });
959
1019
  break;
@@ -962,10 +1022,18 @@ export async function handleToolCall(request) {
962
1022
  // ── USERS ──
963
1023
 
964
1024
  case 'wp_list_users': {
965
- 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;
966
1026
  let ep = `/users?per_page=${per_page}&page=${page}&orderby=${orderby}`;
967
1027
  if (roles) ep += `&roles=${roles}`; if (search) ep += `&search=${encodeURIComponent(search)}`;
968
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
+ }
969
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'] })) });
970
1038
  auditLog({ tool: name, action: 'list', status: 'success', latency_ms: Date.now() - t0 });
971
1039
  break;
@@ -1022,7 +1090,14 @@ export async function handleToolCall(request) {
1022
1090
  enabled: isMultiTarget, active_site: currentTarget?.name || 'default',
1023
1091
  available_sites: Object.keys(targets)
1024
1092
  },
1025
- 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
+ })()
1026
1101
  });
1027
1102
  auditLog({ tool: name, action: 'info', status: 'success', latency_ms: Date.now() - t0 });
1028
1103
  break;
@@ -1264,15 +1339,20 @@ export async function handleToolCall(request) {
1264
1339
  validateInput(args, {
1265
1340
  search: { type: 'string' },
1266
1341
  status: { type: 'string', enum: ['active', 'inactive', 'all'] },
1267
- 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'] }
1268
1344
  });
1269
- const { search, status = 'all', per_page = 20 } = args;
1345
+ const { search, status = 'all', per_page = 20, mode = 'full' } = args;
1270
1346
  let ep = `/plugins?per_page=${per_page}&context=edit`;
1271
1347
  if (search) ep += `&search=${encodeURIComponent(search)}`;
1272
1348
  if (status && status !== 'all') ep += `&status=${status}`;
1273
1349
 
1274
1350
  try {
1275
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
+ }
1276
1356
  const mapped = plugins.map(p => ({
1277
1357
  plugin: p.plugin,
1278
1358
  name: p.name,
@@ -1286,6 +1366,10 @@ export async function handleToolCall(request) {
1286
1366
  network_only: p.network_only ?? false,
1287
1367
  textdomain: p.textdomain ?? ''
1288
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
+ }
1289
1373
  const activeCount = mapped.filter(p => p.status === 'active').length;
1290
1374
  const inactiveCount = mapped.filter(p => p.status === 'inactive').length;
1291
1375
  result = json({ total: mapped.length, active: activeCount, inactive: inactiveCount, plugins: mapped });
@@ -1346,13 +1430,18 @@ export async function handleToolCall(request) {
1346
1430
  case 'wp_list_themes': {
1347
1431
  validateInput(args, {
1348
1432
  status: { type: 'string', enum: ['active', 'inactive', 'all'] },
1349
- 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'] }
1350
1435
  });
1351
- const { status = 'all', per_page = 20 } = args;
1436
+ const { status = 'all', per_page = 20, mode = 'full' } = args;
1352
1437
  let ep = `/themes?per_page=${per_page}&context=edit`;
1353
1438
  if (status && status !== 'all') ep += `&status=${status}`;
1354
1439
  try {
1355
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
+ }
1356
1445
  const mapped = themes.map(t => ({
1357
1446
  stylesheet: t.stylesheet,
1358
1447
  template: t.template,
@@ -1367,6 +1456,10 @@ export async function handleToolCall(request) {
1367
1456
  requires_php: t.requires_php ?? '',
1368
1457
  tags: t.tags?.rendered ?? t.tags ?? []
1369
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
+ }
1370
1463
  const activeTheme = mapped.find(t => t.status === 'active');
1371
1464
  result = json({ total: mapped.length, active_theme: activeTheme ? activeTheme.name : null, themes: mapped });
1372
1465
  auditLog({ tool: name, action: 'list', target_type: 'theme', status: 'success', latency_ms: Date.now() - t0, params: { status, per_page } });
@@ -1416,12 +1509,21 @@ export async function handleToolCall(request) {
1416
1509
  validateInput(args, {
1417
1510
  post_id: { type: 'number', required: true, min: 1 },
1418
1511
  post_type: { type: 'string', enum: ['post', 'page'] },
1419
- 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'] }
1420
1514
  });
1421
- const { post_id, post_type = 'post', per_page = 10 } = args;
1515
+ const { post_id, post_type = 'post', per_page = 10, mode = 'full' } = args;
1422
1516
  const base = post_type === 'page' ? 'pages' : 'posts';
1423
1517
  try {
1424
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
+ }
1425
1527
  result = json({
1426
1528
  total: revisions.length,
1427
1529
  post_id,
@@ -4952,4 +5054,4 @@ if (process.env.NODE_ENV !== 'test') {
4952
5054
  main().catch((error) => { log.error(`Fatal: ${error.message}`); process.exit(1); });
4953
5055
  }
4954
5056
 
4955
- export { server, getActiveControls, getControlSources, _testSetTarget };
5057
+ export { server, getActiveControls, getControlSources, _testSetTarget, getFilteredTools };