@adsim/wordpress-mcp-server 4.4.0 → 4.5.0

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/README.md CHANGED
@@ -3,14 +3,14 @@
3
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
4
  [![Node.js](https://img.shields.io/badge/Node.js-%3E%3D18-green.svg)](https://nodejs.org/)
5
5
  [![MCP SDK](https://img.shields.io/badge/MCP-SDK-blue.svg)](https://github.com/anthropics/mcp)
6
- [![Tests](https://img.shields.io/badge/tests-613%20passing-brightgreen.svg)](https://github.com/GeorgesAdSim/wordpress-mcp-server/actions)
6
+ [![Tests](https://img.shields.io/badge/tests-674%20passing-brightgreen.svg)](https://github.com/GeorgesAdSim/wordpress-mcp-server/actions)
7
7
  [![npm](https://img.shields.io/npm/v/@adsim/wordpress-mcp-server.svg)](https://www.npmjs.com/package/@adsim/wordpress-mcp-server)
8
8
 
9
9
  **Enterprise Governance · Audit Trail · Multi-Site · Plugin-Free**
10
10
 
11
11
  The enterprise governance layer for Claude-to-WordPress integrations — secure, auditable, and multi-site.
12
12
 
13
- **v4.4.0 Enterprise** · 79 tools · 613 Vitest tests · GitHub Actions CI · HTTP Streamable transport · MCPB bundle · SEO metadata · SEO audit suite · Content intelligence · Plugin & theme management · Revision control · Editorial approval workflow · Destructive confirmation · Internal link analysis · WooCommerce (read + intelligence + write) · Execution controls · JSON audit trail · Multi-site targeting
13
+ **v4.5.0 Enterprise** · 85 tools · 674 Vitest tests · GitHub Actions CI · HTTP Streamable transport · MCPB bundle · SEO metadata · SEO audit suite · Content intelligence · Plugin intelligence · Plugin & theme management · Revision control · Editorial approval workflow · Destructive confirmation · Internal link analysis · WooCommerce (read + intelligence + write) · Execution controls · JSON audit trail · Multi-site targeting
14
14
 
15
15
  ---
16
16
 
@@ -716,7 +716,7 @@ WC_PRICE_GUARDRAIL_THRESHOLD=20 # percentage — changes above this require ex
716
716
 
717
717
  ## Testing
718
718
 
719
- 613 unit tests covering all 79 tools — zero network calls, fully mocked.
719
+ 674 unit tests covering all 85 tools — zero network calls, fully mocked.
720
720
  ```bash
721
721
  npm test # run all tests (vitest)
722
722
  npm run test:watch # watch mode
@@ -754,7 +754,9 @@ npm run test:coverage # coverage report
754
754
  | `contentIntelligence.test.js` | 16 content intelligence tools: brief, outline, readability, update frequency, link map, anchor texts, schema, structure, duplicates, gaps, FAQ, CTA, entities, velocity, revisions diff, word count | 125 |
755
755
  | `site.test.js` | site info, set target | 5 |
756
756
  | `transport/http.test.js` | HTTP transport, Bearer auth, sessions | 10 |
757
- | `dxt/manifest.test.js` | MCPB manifest validation, 79 tools declared | 10 |
757
+ | `pluginDetector.test.js` | SEO plugin detection, rendered head, HTML head parsing | 13 |
758
+ | `pluginIntelligence.test.js` | 6 plugin intelligence tools: rendered head, rendered SEO audit, pillar content, schema plugins, SEO score, Twitter meta | 48 |
759
+ | `dxt/manifest.test.js` | MCPB manifest validation, 85 tools declared | 10 |
758
760
 
759
761
  Each test verifies: success response shape, governance blocking (write tools), HTTP error handling (403/404), and audit log entries.
760
762
 
@@ -924,6 +926,25 @@ npx @modelcontextprotocol/inspector node index.js
924
926
 
925
927
  ## Changelog
926
928
 
929
+ ### v4.5.0 (2026-02-21) — Plugin Intelligence (RankMath + Yoast)
930
+
931
+ 6 new tools exploiting native RankMath and Yoast SEO API endpoints for rendered head analysis, schema validation, and social meta management.
932
+
933
+ **New shared module:**
934
+ - `src/pluginDetector.js` — SEO plugin auto-detection via REST API namespace discovery (cached), rendered head fetching, HTML head parsing
935
+
936
+ **Rendered SEO Analysis:**
937
+ - `wp_get_rendered_head` — fetch the real `<head>` HTML via RankMath `/rankmath/v1/getHead` or Yoast `/yoast/v1/get_head` endpoints, compare rendered vs stored meta
938
+ - `wp_audit_rendered_seo` — bulk audit rendered vs stored SEO meta divergences with per-post scoring (title/description/canonical/robots/schema mismatches)
939
+
940
+ **Plugin-Native Features:**
941
+ - `wp_get_pillar_content` — read/write RankMath `rank_math_pillar_content` cornerstone flag. Write mode blocked by `WP_READ_ONLY`
942
+ - `wp_audit_schema_plugins` — validate JSON-LD schemas from plugin native fields (`rank_math_schema` or Yoast `yoast_head_json`). Checks required fields per @type
943
+ - `wp_get_seo_score` — read RankMath native SEO score (0-100) with bulk mode distribution stats
944
+ - `wp_get_twitter_meta` — read/write Twitter Card meta (title, description, image) for RankMath, Yoast, and SEOPress. Write mode blocked by `WP_READ_ONLY`
945
+
946
+ 674 Vitest unit tests · 85 tools
947
+
927
948
  ### v4.4.0 (2026-02-21) — Content Intelligence
928
949
 
929
950
  16 new read-only analysis tools for deep content intelligence without any WordPress plugin.
package/dxt/manifest.json CHANGED
@@ -2,9 +2,9 @@
2
2
  "manifest_version": "0.3",
3
3
  "name": "wordpress-mcp-server",
4
4
  "display_name": "WordPress MCP Server",
5
- "version": "4.4.0",
6
- "description": "Manage your WordPress site from Claude Desktop — posts, pages, media, SEO audits, content intelligence, plugins, themes, revisions, WooCommerce, content compression, and more. 79 tools, no WordPress plugin required.",
7
- "long_description": "A full-featured MCP server for WordPress REST API integration. Manage posts, pages, media, categories, tags, comments, users, SEO metadata, plugins, themes, revisions, and WooCommerce products/orders/customers through 79 tools — all from Claude Desktop.\n\nNo WordPress plugin required. Uses the built-in WordPress REST API with Application Passwords for secure authentication.\n\nFeatures:\n- Content management: create, read, update, delete posts and pages\n- Content compression: field filtering, content_format (html/text/links_only), list modes (full/summary/ids_only) for LLM context optimization\n- Content intelligence: readability scoring, duplicate detection, entity extraction, publishing velocity, revision diff, content structure analysis, FAQ extraction, CTA detection, link mapping, anchor text audit, schema markup validation\n- Editorial approval workflow: submit for review, approve, reject\n- Media library: list, get details, upload from URL\n- Taxonomy: categories, tags, custom post types\n- SEO: auto-detects Yoast, RankMath, SEOPress, or All in One SEO\n- SEO audit suite: media alt text, orphan pages, heading structure, thin content, canonicals, E-E-A-T signals, broken links, keyword cannibalization, taxonomy bloat, outbound links\n- Plugins & themes: list, activate, deactivate\n- Revisions: list, view, restore, delete\n- WooCommerce read: products, orders, customers, price guardrail\n- WooCommerce intelligence: inventory alerts, order analytics, product SEO audit, product link suggestions\n- WooCommerce write: update products (with price guardrail), stock management, order status transitions\n- Enterprise controls: read-only mode, draft-only, disable-delete, require-approval (global and per-target)\n- Multi-site: target multiple WordPress installations with per-target controls",
5
+ "version": "4.5.0",
6
+ "description": "Manage your WordPress site from Claude Desktop — posts, pages, media, SEO audits, content intelligence, plugin intelligence, plugins, themes, revisions, WooCommerce, content compression, and more. 85 tools, no WordPress plugin required.",
7
+ "long_description": "A full-featured MCP server for WordPress REST API integration. Manage posts, pages, media, categories, tags, comments, users, SEO metadata, plugins, themes, revisions, and WooCommerce products/orders/customers through 85 tools — all from Claude Desktop.\n\nNo WordPress plugin required. Uses the built-in WordPress REST API with Application Passwords for secure authentication.\n\nFeatures:\n- Content management: create, read, update, delete posts and pages\n- Content compression: field filtering, content_format (html/text/links_only), list modes (full/summary/ids_only) for LLM context optimization\n- Content intelligence: readability scoring, duplicate detection, entity extraction, publishing velocity, revision diff, content structure analysis, FAQ extraction, CTA detection, link mapping, anchor text audit, schema markup validation\n- Plugin intelligence: SEO plugin detection (RankMath/Yoast/SEOPress), rendered head analysis, schema validation from plugin fields, SEO score reading, Twitter Card meta, pillar content management\n- Editorial approval workflow: submit for review, approve, reject\n- Media library: list, get details, upload from URL\n- Taxonomy: categories, tags, custom post types\n- SEO: auto-detects Yoast, RankMath, SEOPress, or All in One SEO\n- SEO audit suite: media alt text, orphan pages, heading structure, thin content, canonicals, E-E-A-T signals, broken links, keyword cannibalization, taxonomy bloat, outbound links\n- Plugins & themes: list, activate, deactivate\n- Revisions: list, view, restore, delete\n- WooCommerce read: products, orders, customers, price guardrail\n- WooCommerce intelligence: inventory alerts, order analytics, product SEO audit, product link suggestions\n- WooCommerce write: update products (with price guardrail), stock management, order status transitions\n- Enterprise controls: read-only mode, draft-only, disable-delete, require-approval (global and per-target)\n- Multi-site: target multiple WordPress installations with per-target controls",
8
8
  "author": {
9
9
  "name": "AdSim",
10
10
  "email": "georges@adsim.be",
@@ -112,7 +112,13 @@
112
112
  { "name": "wp_extract_entities", "description": "Regex/heuristic named entity extraction (brands, locations, persons, organizations)" },
113
113
  { "name": "wp_get_publishing_velocity", "description": "Publication cadence by author/category with trend detection" },
114
114
  { "name": "wp_compare_revisions_diff", "description": "Textual diff between revisions with amplitude scoring" },
115
- { "name": "wp_list_posts_by_word_count", "description": "Posts sorted by length with 6-tier segmentation" }
115
+ { "name": "wp_list_posts_by_word_count", "description": "Posts sorted by length with 6-tier segmentation" },
116
+ { "name": "wp_get_rendered_head", "description": "Fetch real rendered <head> HTML via RankMath or Yoast headless endpoint, compare rendered vs stored meta" },
117
+ { "name": "wp_audit_rendered_seo", "description": "Bulk audit rendered vs stored SEO meta divergences with per-post scoring" },
118
+ { "name": "wp_get_pillar_content", "description": "Read or set RankMath pillar/cornerstone content flag. Write mode blocked by WP_READ_ONLY" },
119
+ { "name": "wp_audit_schema_plugins", "description": "Audit JSON-LD schemas from SEO plugin native fields (rank_math_schema or Yoast head)" },
120
+ { "name": "wp_get_seo_score", "description": "Read RankMath native SEO score (0-100) with bulk mode distribution stats" },
121
+ { "name": "wp_get_twitter_meta", "description": "Read or write Twitter Card meta (title, description, image) from RankMath, Yoast, or SEOPress" }
116
122
  ],
117
123
  "keywords": [
118
124
  "wordpress",
@@ -129,7 +135,8 @@
129
135
  "woocommerce",
130
136
  "ecommerce",
131
137
  "content-compression",
132
- "content-intelligence"
138
+ "content-intelligence",
139
+ "plugin-intelligence"
133
140
  ],
134
141
  "license": "MIT",
135
142
  "user_config": {
package/index.js CHANGED
@@ -28,6 +28,7 @@ import { wcApiCall, getWcCredentials } from './src/woocommerceClient.js';
28
28
  import { parseImagesFromHtml, extractHeadings, extractInternalLinks as extractInternalLinksHtml, countWords } from './src/htmlParser.js';
29
29
  import { summarizePost, applyContentFormat } from './src/utils/contentCompressor.js';
30
30
  import { calculateReadabilityScore, extractHeadingsOutline, detectContentSections, extractTransitionWords, countPassiveSentences, buildTFIDFVectors, computeCosineSimilarity, findDuplicatePairs, extractEntities, computeTextDiff } from './src/contentAnalyzer.js';
31
+ import { detectSeoPlugin, getRenderedHead, parseRenderedHead } from './src/pluginDetector.js';
31
32
 
32
33
  // ============================================================
33
34
  // CONFIGURATION
@@ -435,7 +436,7 @@ const ORDERBY = ['date', 'relevance', 'id', 'title', 'slug', 'modified', 'author
435
436
  const ORDERS = ['asc', 'desc'];
436
437
  const MEDIA_TYPES = ['image', 'video', 'audio', 'application'];
437
438
  const COMMENT_STATUSES = ['approved', 'hold', 'spam', 'trash'];
438
- const TOOLS_COUNT = 79;
439
+ const TOOLS_COUNT = 85;
439
440
 
440
441
  function json(data) { return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }; }
441
442
  function strip(html) { return (html || '').replace(/<[^>]*>/g, '').trim(); }
@@ -632,7 +633,23 @@ const TOOLS_DEFINITIONS = [
632
633
  { name: 'wp_compare_revisions_diff', description: 'Diff between two post revisions: lines/words added/removed, headings diff, amplitude score. Read-only.',
633
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'] }},
634
635
  { name: 'wp_list_posts_by_word_count', description: 'List posts sorted by word count with automatic length segmentation and distribution stats. Read-only.',
635
- inputSchema: { type: 'object', properties: { limit: { type: 'number', description: 'Max posts to analyze (1-500, default 100)' }, post_type: { type: 'string', enum: ['post', 'page', 'both'], description: 'post, page, or both (default post)' }, order: { type: 'string', enum: ['asc', 'desc'], description: 'Sort order by word count (default desc)' }, category_id: { type: 'number', description: 'Optional: filter by category ID' } }}}
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' } }}},
637
+
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)' } }}},
645
+
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'] }}
636
653
  ];
637
654
 
638
655
  function registerHandlers(s) {
@@ -4419,6 +4436,457 @@ export async function handleToolCall(request) {
4419
4436
  break;
4420
4437
  }
4421
4438
 
4439
+ // ── PLUGIN INTELLIGENCE v4.5 ──
4440
+
4441
+ case 'wp_get_rendered_head': {
4442
+ validateInput(args, { post_id: { type: 'number', required: true }, post_type: { type: 'string', enum: ['post', 'page'] } });
4443
+ const grhPostId = args.post_id;
4444
+ const grhPostType = args.post_type || 'post';
4445
+ const { url: grhBaseUrl, auth: grhAuth } = getActiveAuth();
4446
+
4447
+ const grhPlugin = await detectSeoPlugin(grhBaseUrl, fetch);
4448
+ if (!grhPlugin) throw new Error('No supported SEO plugin detected. wp_get_rendered_head requires RankMath or Yoast.');
4449
+ if (grhPlugin !== 'rankmath' && grhPlugin !== 'yoast') throw new Error(`Rendered head requires RankMath or Yoast (detected: ${grhPlugin})`);
4450
+
4451
+ const grhEp = grhPostType === 'page' ? `/pages/${grhPostId}?_fields=id,title,link,slug,meta` : `/posts/${grhPostId}?_fields=id,title,link,slug,meta`;
4452
+ const grhPost = await wpApiCall(grhEp);
4453
+
4454
+ const grhHeadResult = await getRenderedHead(grhBaseUrl, grhPost.link, grhPlugin, fetch, grhAuth);
4455
+ if (!grhHeadResult.success) throw new Error(grhHeadResult.error);
4456
+
4457
+ const grhParsed = parseRenderedHead(grhHeadResult.head);
4458
+ const grhMeta = grhPost.meta || {};
4459
+
4460
+ // Extract stored SEO meta based on plugin
4461
+ let grhStoredTitle, grhStoredDesc, grhStoredKeyword, grhStoredCanonical, grhStoredRobots;
4462
+ if (grhPlugin === 'rankmath') {
4463
+ grhStoredTitle = grhMeta.rank_math_title || null;
4464
+ grhStoredDesc = grhMeta.rank_math_description || null;
4465
+ grhStoredKeyword = grhMeta.rank_math_focus_keyword || null;
4466
+ grhStoredCanonical = grhMeta.rank_math_canonical_url || null;
4467
+ const rm = grhMeta.rank_math_robots || [];
4468
+ grhStoredRobots = Array.isArray(rm) && rm.length > 0 ? rm.join(', ') : null;
4469
+ } else {
4470
+ grhStoredTitle = grhMeta._yoast_wpseo_title || null;
4471
+ grhStoredDesc = grhMeta._yoast_wpseo_metadesc || null;
4472
+ grhStoredKeyword = grhMeta._yoast_wpseo_focuskw || null;
4473
+ grhStoredCanonical = grhMeta._yoast_wpseo_canonical || null;
4474
+ grhStoredRobots = grhMeta._yoast_wpseo_meta_robots_noindex === '1' ? 'noindex' : null;
4475
+ }
4476
+
4477
+ result = json({
4478
+ post_id: grhPostId,
4479
+ post_url: grhPost.link,
4480
+ seo_plugin: grhPlugin,
4481
+ rendered: grhParsed,
4482
+ stored: {
4483
+ title: grhStoredTitle,
4484
+ description: grhStoredDesc,
4485
+ focus_keyword: grhStoredKeyword,
4486
+ canonical: grhStoredCanonical,
4487
+ robots: grhStoredRobots
4488
+ },
4489
+ raw_head_length: grhHeadResult.head.length,
4490
+ schemas_count: grhParsed.schema_json_ld.length
4491
+ });
4492
+ auditLog({ tool: name, action: 'get_rendered_head', status: 'success', latency_ms: Date.now() - t0, params: { post_id: grhPostId, post_type: grhPostType, plugin: grhPlugin } });
4493
+ break;
4494
+ }
4495
+
4496
+ case 'wp_audit_rendered_seo': {
4497
+ validateInput(args, { limit: { type: 'number', min: 1, max: 50 }, post_type: { type: 'string', enum: ['post', 'page'] } });
4498
+ const arsLimit = args.limit || 10;
4499
+ const arsPostType = args.post_type || 'post';
4500
+ const { url: arsBaseUrl, auth: arsAuth } = getActiveAuth();
4501
+
4502
+ const arsPlugin = await detectSeoPlugin(arsBaseUrl, fetch);
4503
+ if (!arsPlugin) throw new Error('No supported SEO plugin detected. wp_audit_rendered_seo requires RankMath or Yoast.');
4504
+ if (arsPlugin !== 'rankmath' && arsPlugin !== 'yoast') throw new Error(`Rendered SEO audit requires RankMath or Yoast (detected: ${arsPlugin})`);
4505
+
4506
+ const arsEp = `/${arsPostType}s?per_page=${arsLimit}&status=publish&_fields=id,title,link,slug,meta`;
4507
+ const arsPosts = await wpApiCall(arsEp);
4508
+
4509
+ const arsResults = [];
4510
+ const arsSummary = { title_mismatch: 0, description_mismatch: 0, canonical_mismatch: 0, missing_rendered_title: 0, missing_rendered_description: 0, robots_mismatch: 0, schema_missing: 0 };
4511
+
4512
+ for (const p of arsPosts) {
4513
+ const headRes = await getRenderedHead(arsBaseUrl, p.link, arsPlugin, fetch, arsAuth);
4514
+ if (!headRes.success) {
4515
+ arsResults.push({ id: p.id, title: strip(p.title?.rendered || ''), url: p.link, score: 0, issues: ['head_fetch_failed'], rendered: null, stored: null });
4516
+ continue;
4517
+ }
4518
+
4519
+ const parsed = parseRenderedHead(headRes.head);
4520
+ const meta = p.meta || {};
4521
+ const issues = [];
4522
+
4523
+ // Extract stored meta
4524
+ let storedTitle, storedDesc, storedCanonical, storedRobots;
4525
+ if (arsPlugin === 'rankmath') {
4526
+ storedTitle = meta.rank_math_title || null;
4527
+ storedDesc = meta.rank_math_description || null;
4528
+ storedCanonical = meta.rank_math_canonical_url || null;
4529
+ const rm = meta.rank_math_robots || [];
4530
+ storedRobots = Array.isArray(rm) && rm.length > 0 ? rm.join(', ') : null;
4531
+ } else {
4532
+ storedTitle = meta._yoast_wpseo_title || null;
4533
+ storedDesc = meta._yoast_wpseo_metadesc || null;
4534
+ storedCanonical = meta._yoast_wpseo_canonical || null;
4535
+ storedRobots = meta._yoast_wpseo_meta_robots_noindex === '1' ? 'noindex' : null;
4536
+ }
4537
+
4538
+ // Compare rendered vs stored
4539
+ if (!parsed.title) { issues.push('missing_rendered_title'); arsSummary.missing_rendered_title++; }
4540
+ else if (storedTitle && !parsed.title.includes(storedTitle)) { issues.push('title_mismatch'); arsSummary.title_mismatch++; }
4541
+
4542
+ if (!parsed.meta_description) { issues.push('missing_rendered_description'); arsSummary.missing_rendered_description++; }
4543
+ else if (storedDesc && parsed.meta_description !== storedDesc) { issues.push('description_mismatch'); arsSummary.description_mismatch++; }
4544
+
4545
+ if (parsed.canonical && parsed.canonical !== p.link && storedCanonical && parsed.canonical !== storedCanonical) { issues.push('canonical_mismatch'); arsSummary.canonical_mismatch++; }
4546
+
4547
+ if (parsed.robots && parsed.robots.includes('noindex') && (!storedRobots || !storedRobots.includes('noindex'))) { issues.push('robots_mismatch'); arsSummary.robots_mismatch++; }
4548
+
4549
+ if (parsed.schema_json_ld.length === 0) { issues.push('schema_missing'); arsSummary.schema_missing++; }
4550
+
4551
+ const score = Math.max(0, 100 - issues.length * 15);
4552
+
4553
+ arsResults.push({
4554
+ id: p.id, title: strip(p.title?.rendered || ''), url: p.link, score, issues,
4555
+ rendered: { title: parsed.title, description: parsed.meta_description, canonical: parsed.canonical, robots: parsed.robots, schemas_count: parsed.schema_json_ld.length },
4556
+ stored: { title: storedTitle, description: storedDesc, canonical: storedCanonical, robots: storedRobots }
4557
+ });
4558
+ }
4559
+
4560
+ const arsAvgScore = arsResults.length > 0 ? arsResults.reduce((s, r) => s + r.score, 0) / arsResults.length : 0;
4561
+
4562
+ result = json({
4563
+ seo_plugin: arsPlugin,
4564
+ total_audited: arsResults.length,
4565
+ avg_score: Math.round(arsAvgScore),
4566
+ issues_summary: arsSummary,
4567
+ posts: arsResults
4568
+ });
4569
+ auditLog({ tool: name, action: 'audit_rendered_seo', status: 'success', latency_ms: Date.now() - t0, params: { limit: arsLimit, post_type: arsPostType, plugin: arsPlugin } });
4570
+ break;
4571
+ }
4572
+
4573
+ case 'wp_get_pillar_content': {
4574
+ validateInput(args, {
4575
+ post_id: { type: 'number' },
4576
+ set_pillar: { type: 'boolean' },
4577
+ list_pillars: { type: 'boolean' },
4578
+ post_type: { type: 'string', enum: ['post', 'page'] },
4579
+ limit: { type: 'number', min: 1, max: 500 }
4580
+ });
4581
+ const pcPostType = args.post_type || 'post';
4582
+ const { url: pcBaseUrl } = getActiveAuth();
4583
+
4584
+ const pcPlugin = await detectSeoPlugin(pcBaseUrl, fetch);
4585
+ if (pcPlugin !== 'rankmath') throw new Error('Pillar content requires RankMath (detected: ' + (pcPlugin || 'none') + ')');
4586
+
4587
+ if (args.list_pillars) {
4588
+ // Mode: list all pillar posts
4589
+ const pcLimit = args.limit || 100;
4590
+ const pcPosts = await wpApiCall(`/${pcPostType}s?per_page=${pcLimit}&status=publish&_fields=id,title,link,slug,meta`);
4591
+ const pillars = pcPosts.filter(p => (p.meta || {}).rank_math_pillar_content === 'on').map(p => ({
4592
+ id: p.id, title: strip(p.title?.rendered || ''), slug: p.slug, link: p.link, post_type: pcPostType
4593
+ }));
4594
+ result = json({ mode: 'list_pillars', seo_plugin: pcPlugin, pillar_count: pillars.length, pillars });
4595
+ auditLog({ tool: name, action: 'read_pillar_content', status: 'success', latency_ms: Date.now() - t0, params: { list_pillars: true, post_type: pcPostType, limit: pcLimit } });
4596
+ } else if (args.post_id !== undefined && args.set_pillar !== undefined) {
4597
+ // Mode: write — set/unset pillar flag
4598
+ if (getActiveControls().read_only) throw new Error('Blocked: READ-ONLY mode. Cannot update pillar content flag.');
4599
+ const pcPost = await wpApiCall(`/${pcPostType}s/${args.post_id}?_fields=id,title,link,meta`);
4600
+ await wpApiCall(`/${pcPostType}s/${args.post_id}`, { method: 'POST', body: JSON.stringify({ meta: { rank_math_pillar_content: args.set_pillar ? 'on' : '' } }) });
4601
+ result = json({
4602
+ mode: 'write', post_id: args.post_id, title: strip(pcPost.title?.rendered || ''),
4603
+ is_pillar: args.set_pillar, action: args.set_pillar ? 'marked_as_pillar' : 'unmarked_as_pillar', seo_plugin: pcPlugin
4604
+ });
4605
+ auditLog({ tool: name, action: 'update_pillar_content', target: args.post_id, target_type: pcPostType, status: 'success', latency_ms: Date.now() - t0, params: { set_pillar: args.set_pillar } });
4606
+ } else if (args.post_id !== undefined) {
4607
+ // Mode: read single post
4608
+ const pcPost = await wpApiCall(`/${pcPostType}s/${args.post_id}?_fields=id,title,link,meta`);
4609
+ const isPillar = (pcPost.meta || {}).rank_math_pillar_content === 'on';
4610
+ result = json({ mode: 'read', post_id: args.post_id, title: strip(pcPost.title?.rendered || ''), is_pillar: isPillar, seo_plugin: pcPlugin });
4611
+ auditLog({ tool: name, action: 'read_pillar_content', status: 'success', latency_ms: Date.now() - t0, params: { post_id: args.post_id, post_type: pcPostType } });
4612
+ } else {
4613
+ throw new Error('Provide post_id (read/write) or list_pillars:true');
4614
+ }
4615
+ break;
4616
+ }
4617
+
4618
+ case 'wp_audit_schema_plugins': {
4619
+ validateInput(args, {
4620
+ limit: { type: 'number', min: 1, max: 100 },
4621
+ post_type: { type: 'string', enum: ['post', 'page', 'both'] }
4622
+ });
4623
+ const aspLimit = args.limit || 20;
4624
+ const aspPostType = args.post_type || 'post';
4625
+ const { url: aspBaseUrl, auth: aspAuth } = getActiveAuth();
4626
+
4627
+ const aspPlugin = await detectSeoPlugin(aspBaseUrl, fetch);
4628
+ if (!aspPlugin) throw new Error('No supported SEO plugin detected');
4629
+ if (aspPlugin !== 'rankmath' && aspPlugin !== 'yoast') throw new Error(`Schema plugin audit requires RankMath or Yoast (detected: ${aspPlugin})`);
4630
+
4631
+ const aspRequired = {
4632
+ 'Article': ['headline', 'datePublished', 'author'],
4633
+ 'BlogPosting': ['headline', 'datePublished', 'author'],
4634
+ 'NewsArticle': ['headline', 'datePublished', 'author'],
4635
+ 'FAQPage': ['mainEntity'],
4636
+ 'HowTo': ['name', 'step'],
4637
+ 'LocalBusiness': ['name', 'address'],
4638
+ 'BreadcrumbList': ['itemListElement'],
4639
+ 'Organization': ['name'],
4640
+ 'WebPage': ['name'],
4641
+ 'WebSite': ['name', 'url']
4642
+ };
4643
+
4644
+ let aspAllPosts = [];
4645
+ if (aspPostType === 'both') {
4646
+ const aspP = await wpApiCall(`/posts?per_page=${aspLimit}&status=publish&_fields=id,title,link,slug,meta`);
4647
+ const aspG = await wpApiCall(`/pages?per_page=${aspLimit}&status=publish&_fields=id,title,link,slug,meta`);
4648
+ aspAllPosts = [...aspP, ...aspG];
4649
+ } else {
4650
+ aspAllPosts = await wpApiCall(`/${aspPostType}s?per_page=${aspLimit}&status=publish&_fields=id,title,link,slug,meta`);
4651
+ }
4652
+
4653
+ const aspResults = [];
4654
+ const aspTypesCounts = {};
4655
+ const aspIssuesSummary = { no_plugin_schema: 0, invalid_schema_json: 0, missing_required_fields: 0, no_article_schema: 0 };
4656
+ let aspWithSchema = 0;
4657
+
4658
+ for (const p of aspAllPosts) {
4659
+ const postIssues = [];
4660
+ const postSchemas = [];
4661
+ let schemas = [];
4662
+
4663
+ if (aspPlugin === 'rankmath') {
4664
+ const rawSchema = (p.meta || {}).rank_math_schema;
4665
+ if (!rawSchema || rawSchema === '{}') {
4666
+ postIssues.push('no_plugin_schema');
4667
+ aspIssuesSummary.no_plugin_schema++;
4668
+ } else {
4669
+ try {
4670
+ const parsed = typeof rawSchema === 'string' ? JSON.parse(rawSchema) : rawSchema;
4671
+ if (parsed['@type']) {
4672
+ schemas = [parsed];
4673
+ } else if (parsed['@graph']) {
4674
+ schemas = parsed['@graph'];
4675
+ } else {
4676
+ schemas = Object.values(parsed).filter(v => v && typeof v === 'object' && v['@type']);
4677
+ }
4678
+ if (schemas.length === 0) { postIssues.push('no_plugin_schema'); aspIssuesSummary.no_plugin_schema++; }
4679
+ } catch {
4680
+ postIssues.push('invalid_schema_json');
4681
+ aspIssuesSummary.invalid_schema_json++;
4682
+ }
4683
+ }
4684
+ } else {
4685
+ const headRes = await getRenderedHead(aspBaseUrl, p.link, aspPlugin, fetch, aspAuth);
4686
+ if (headRes.success) {
4687
+ const parsed = parseRenderedHead(headRes.head);
4688
+ schemas = parsed.schema_json_ld || [];
4689
+ if (schemas.length === 0) { postIssues.push('no_plugin_schema'); aspIssuesSummary.no_plugin_schema++; }
4690
+ } else {
4691
+ postIssues.push('no_plugin_schema');
4692
+ aspIssuesSummary.no_plugin_schema++;
4693
+ }
4694
+ }
4695
+
4696
+ let hasArticleType = false;
4697
+ for (const schema of schemas) {
4698
+ const schemaType = schema['@type'] || 'Unknown';
4699
+ if (['Article', 'BlogPosting', 'NewsArticle'].includes(schemaType)) hasArticleType = true;
4700
+ aspTypesCounts[schemaType] = (aspTypesCounts[schemaType] || 0) + 1;
4701
+
4702
+ const requiredFields = aspRequired[schemaType];
4703
+ const missingFields = [];
4704
+ if (requiredFields) {
4705
+ for (const field of requiredFields) {
4706
+ if (!schema[field]) missingFields.push(field);
4707
+ }
4708
+ }
4709
+ if (missingFields.length > 0) { postIssues.push('missing_required_fields'); aspIssuesSummary.missing_required_fields++; }
4710
+ postSchemas.push({ type: schemaType, valid: missingFields.length === 0, missing_fields: missingFields });
4711
+ }
4712
+
4713
+ if (schemas.length > 0 && !hasArticleType && aspPostType === 'post') {
4714
+ postIssues.push('no_article_schema');
4715
+ aspIssuesSummary.no_article_schema++;
4716
+ }
4717
+
4718
+ if (schemas.length > 0) aspWithSchema++;
4719
+ const postScore = Math.max(0, 100 - postIssues.length * 15);
4720
+ aspResults.push({ id: p.id, title: strip(p.title?.rendered || ''), slug: p.slug, score: postScore, schemas: postSchemas, issues: postIssues });
4721
+ }
4722
+
4723
+ const aspAvg = aspResults.length > 0 ? aspResults.reduce((s, r) => s + r.score, 0) / aspResults.length : 0;
4724
+ const aspWithout = aspAllPosts.length - aspWithSchema;
4725
+
4726
+ result = json({
4727
+ seo_plugin: aspPlugin,
4728
+ total_audited: aspResults.length,
4729
+ avg_score: Math.round(aspAvg),
4730
+ schema_coverage: {
4731
+ posts_with_schema: aspWithSchema,
4732
+ posts_without_schema: aspWithout,
4733
+ coverage_percent: aspAllPosts.length > 0 ? Math.round(aspWithSchema / aspAllPosts.length * 100) : 0
4734
+ },
4735
+ schema_types_found: aspTypesCounts,
4736
+ issues_summary: aspIssuesSummary,
4737
+ posts: aspResults
4738
+ });
4739
+ auditLog({ tool: name, action: 'audit_schema_plugins', status: 'success', latency_ms: Date.now() - t0, params: { limit: aspLimit, post_type: aspPostType, plugin: aspPlugin } });
4740
+ break;
4741
+ }
4742
+
4743
+ case 'wp_get_seo_score': {
4744
+ validateInput(args, {
4745
+ post_id: { type: 'number' },
4746
+ limit: { type: 'number', min: 1, max: 100 },
4747
+ post_type: { type: 'string', enum: ['post', 'page'] },
4748
+ order: { type: 'string', enum: ['asc', 'desc'] }
4749
+ });
4750
+ const gssPostType = args.post_type || 'post';
4751
+ const { url: gssBaseUrl } = getActiveAuth();
4752
+
4753
+ const gssPlugin = await detectSeoPlugin(gssBaseUrl, fetch);
4754
+ if (gssPlugin !== 'rankmath') throw new Error('SEO score requires RankMath (detected: ' + (gssPlugin || 'none') + ')');
4755
+
4756
+ if (args.post_id !== undefined) {
4757
+ const gssPost = await wpApiCall(`/${gssPostType}s/${args.post_id}?_fields=id,title,link,slug,meta`);
4758
+ const gssMeta = gssPost.meta || {};
4759
+ const gssRaw = gssMeta.rank_math_seo_score;
4760
+ const gssScore = gssRaw !== undefined && gssRaw !== null && gssRaw !== '' ? parseInt(gssRaw, 10) : null;
4761
+ const gssKw = gssMeta.rank_math_focus_keyword || null;
4762
+ const gssRating = gssScore === null || gssScore === 0 ? 'no_score' : gssScore >= 80 ? 'excellent' : gssScore >= 60 ? 'good' : gssScore >= 40 ? 'average' : 'poor';
4763
+
4764
+ result = json({
4765
+ mode: 'single', post_id: args.post_id, title: strip(gssPost.title?.rendered || ''),
4766
+ link: gssPost.link, seo_score: gssScore, focus_keyword: gssKw, rating: gssRating
4767
+ });
4768
+ auditLog({ tool: name, action: 'get_seo_score', status: 'success', latency_ms: Date.now() - t0, params: { post_id: args.post_id, post_type: gssPostType } });
4769
+ } else {
4770
+ const gssLimit = args.limit || 20;
4771
+ const gssSortOrder = args.order || 'desc';
4772
+ const gssPosts = await wpApiCall(`/${gssPostType}s?per_page=${gssLimit}&status=publish&_fields=id,title,link,slug,meta`);
4773
+
4774
+ const gssItems = gssPosts.map(p => {
4775
+ const m = p.meta || {};
4776
+ const raw = m.rank_math_seo_score;
4777
+ const score = raw !== undefined && raw !== null && raw !== '' ? parseInt(raw, 10) : null;
4778
+ const kw = m.rank_math_focus_keyword || null;
4779
+ const rating = score === null || score === 0 ? 'no_score' : score >= 80 ? 'excellent' : score >= 60 ? 'good' : score >= 40 ? 'average' : 'poor';
4780
+ return { id: p.id, title: strip(p.title?.rendered || ''), slug: p.slug, link: p.link, seo_score: score, focus_keyword: kw, rating };
4781
+ });
4782
+
4783
+ gssItems.sort((a, b) => {
4784
+ const sa = a.seo_score === null ? -1 : a.seo_score;
4785
+ const sb = b.seo_score === null ? -1 : b.seo_score;
4786
+ return gssSortOrder === 'asc' ? sa - sb : sb - sa;
4787
+ });
4788
+
4789
+ const gssDist = { excellent: 0, good: 0, average: 0, poor: 0, no_score: 0 };
4790
+ const gssScores = [];
4791
+ for (const item of gssItems) {
4792
+ gssDist[item.rating]++;
4793
+ if (item.seo_score !== null && item.seo_score > 0) gssScores.push(item.seo_score);
4794
+ }
4795
+ const gssTotal = gssItems.length;
4796
+ const gssAvg = gssScores.length > 0 ? gssScores.reduce((a, b) => a + b, 0) / gssScores.length : 0;
4797
+ const gssSorted = [...gssScores].sort((a, b) => a - b);
4798
+ const gssMedian = gssSorted.length > 0 ? (gssSorted.length % 2 === 0 ? (gssSorted[gssSorted.length / 2 - 1] + gssSorted[gssSorted.length / 2]) / 2 : gssSorted[Math.floor(gssSorted.length / 2)]) : 0;
4799
+
4800
+ result = json({
4801
+ mode: 'bulk', total_analyzed: gssItems.length,
4802
+ avg_score: Math.round(gssAvg), median_score: gssMedian,
4803
+ distribution: {
4804
+ excellent: { count: gssDist.excellent, percent: gssTotal > 0 ? Math.round(gssDist.excellent / gssTotal * 100) : 0 },
4805
+ good: { count: gssDist.good, percent: gssTotal > 0 ? Math.round(gssDist.good / gssTotal * 100) : 0 },
4806
+ average: { count: gssDist.average, percent: gssTotal > 0 ? Math.round(gssDist.average / gssTotal * 100) : 0 },
4807
+ poor: { count: gssDist.poor, percent: gssTotal > 0 ? Math.round(gssDist.poor / gssTotal * 100) : 0 },
4808
+ no_score: { count: gssDist.no_score, percent: gssTotal > 0 ? Math.round(gssDist.no_score / gssTotal * 100) : 0 }
4809
+ },
4810
+ posts: gssItems
4811
+ });
4812
+ auditLog({ tool: name, action: 'get_seo_score', status: 'success', latency_ms: Date.now() - t0, params: { limit: gssLimit, post_type: gssPostType, order: gssSortOrder } });
4813
+ }
4814
+ break;
4815
+ }
4816
+
4817
+ case 'wp_get_twitter_meta': {
4818
+ validateInput(args, {
4819
+ post_id: { type: 'number', required: true },
4820
+ post_type: { type: 'string', enum: ['post', 'page'] },
4821
+ twitter_title: { type: 'string' },
4822
+ twitter_description: { type: 'string' },
4823
+ twitter_image: { type: 'string' }
4824
+ });
4825
+ const gtmPostId = args.post_id;
4826
+ const gtmPostType = args.post_type || 'post';
4827
+ const { url: gtmBaseUrl } = getActiveAuth();
4828
+ const gtmPlugin = await detectSeoPlugin(gtmBaseUrl, fetch);
4829
+ if (!gtmPlugin) throw new Error('No supported SEO plugin detected');
4830
+
4831
+ const gtmIsWrite = args.twitter_title !== undefined || args.twitter_description !== undefined || args.twitter_image !== undefined;
4832
+
4833
+ if (gtmIsWrite) {
4834
+ if (getActiveControls().read_only) throw new Error('Blocked: READ-ONLY mode. Cannot update Twitter meta.');
4835
+ if (gtmPlugin !== 'rankmath' && gtmPlugin !== 'yoast') throw new Error('Twitter meta write requires RankMath or Yoast (detected: ' + gtmPlugin + ')');
4836
+
4837
+ const gtmMeta = {};
4838
+ const gtmUpdated = [];
4839
+ if (gtmPlugin === 'rankmath') {
4840
+ if (args.twitter_title !== undefined) { gtmMeta.rank_math_twitter_title = args.twitter_title; gtmUpdated.push('twitter_title'); }
4841
+ if (args.twitter_description !== undefined) { gtmMeta.rank_math_twitter_description = args.twitter_description; gtmUpdated.push('twitter_description'); }
4842
+ if (args.twitter_image !== undefined) { gtmMeta.rank_math_twitter_image = args.twitter_image; gtmUpdated.push('twitter_image'); }
4843
+ } else {
4844
+ if (args.twitter_title !== undefined) { gtmMeta['_yoast_wpseo_twitter-title'] = args.twitter_title; gtmUpdated.push('twitter_title'); }
4845
+ if (args.twitter_description !== undefined) { gtmMeta['_yoast_wpseo_twitter-description'] = args.twitter_description; gtmUpdated.push('twitter_description'); }
4846
+ if (args.twitter_image !== undefined) { gtmMeta['_yoast_wpseo_twitter-image'] = args.twitter_image; gtmUpdated.push('twitter_image'); }
4847
+ }
4848
+
4849
+ const gtmPost = await wpApiCall(`/${gtmPostType}s/${gtmPostId}?_fields=id,title,link,meta`);
4850
+ await wpApiCall(`/${gtmPostType}s/${gtmPostId}`, { method: 'POST', body: JSON.stringify({ meta: gtmMeta }) });
4851
+
4852
+ result = json({
4853
+ mode: 'write', post_id: gtmPostId, title: strip(gtmPost.title?.rendered || ''),
4854
+ seo_plugin: gtmPlugin, updated_fields: gtmUpdated,
4855
+ twitter: { title: args.twitter_title || null, description: args.twitter_description || null, image: args.twitter_image || null }
4856
+ });
4857
+ auditLog({ tool: name, action: 'update_twitter_meta', target: gtmPostId, target_type: gtmPostType, status: 'success', latency_ms: Date.now() - t0, params: { updated_fields: gtmUpdated } });
4858
+ } else {
4859
+ const gtmPost = await wpApiCall(`/${gtmPostType}s/${gtmPostId}?_fields=id,title,link,meta`);
4860
+ const gtmM = gtmPost.meta || {};
4861
+
4862
+ let gtmTitle, gtmDesc, gtmImage, gtmCard;
4863
+ if (gtmPlugin === 'rankmath') {
4864
+ gtmTitle = gtmM.rank_math_twitter_title || null;
4865
+ gtmDesc = gtmM.rank_math_twitter_description || null;
4866
+ gtmImage = gtmM.rank_math_twitter_image || null;
4867
+ gtmCard = gtmM.rank_math_twitter_card_type || null;
4868
+ } else if (gtmPlugin === 'yoast') {
4869
+ gtmTitle = gtmM['_yoast_wpseo_twitter-title'] || null;
4870
+ gtmDesc = gtmM['_yoast_wpseo_twitter-description'] || null;
4871
+ gtmImage = gtmM['_yoast_wpseo_twitter-image'] || null;
4872
+ gtmCard = null;
4873
+ } else {
4874
+ gtmTitle = gtmM._seopress_social_twitter_title || null;
4875
+ gtmDesc = gtmM._seopress_social_twitter_desc || null;
4876
+ gtmImage = gtmM._seopress_social_twitter_img || null;
4877
+ gtmCard = null;
4878
+ }
4879
+
4880
+ result = json({
4881
+ mode: 'read', post_id: gtmPostId, title: strip(gtmPost.title?.rendered || ''),
4882
+ link: gtmPost.link, seo_plugin: gtmPlugin,
4883
+ twitter: { title: gtmTitle, description: gtmDesc, image: gtmImage, card_type: gtmCard }
4884
+ });
4885
+ auditLog({ tool: name, action: 'read_twitter_meta', status: 'success', latency_ms: Date.now() - t0, params: { post_id: gtmPostId, post_type: gtmPostType, plugin: gtmPlugin } });
4886
+ }
4887
+ break;
4888
+ }
4889
+
4422
4890
  default:
4423
4891
  throw new Error(`Unknown tool: "${name}".`);
4424
4892
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adsim/wordpress-mcp-server",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "description": "A Model Context Protocol (MCP) server for WordPress REST API integration. Manage posts, search content, and interact with your WordPress site through any MCP-compatible client.",
5
5
  "type": "module",
6
6
  "main": "index.js",