@adsim/wordpress-mcp-server 4.5.1 → 4.6.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 +69 -11
- package/dxt/manifest.json +17 -6
- package/index.js +67 -6
- package/package.json +1 -1
- package/src/plugins/IPluginAdapter.js +95 -0
- package/src/plugins/adapters/acf/acfAdapter.js +181 -0
- package/src/plugins/adapters/elementor/elementorAdapter.js +176 -0
- package/src/plugins/contextGuard.js +57 -0
- package/src/plugins/registry.js +94 -0
- package/tests/unit/pluginLayer.test.js +151 -0
- package/tests/unit/plugins/acf/acfAdapter.test.js +205 -0
- package/tests/unit/plugins/acf/acfAdapter.write.test.js +157 -0
- package/tests/unit/plugins/contextGuard.test.js +51 -0
- package/tests/unit/plugins/elementor/elementorAdapter.test.js +206 -0
- package/tests/unit/plugins/iPluginAdapter.test.js +34 -0
- package/tests/unit/plugins/registry.test.js +84 -0
- package/tests/unit/tools/dynamicFiltering.test.js +4 -4
- package/tests/unit/tools/site.test.js +1 -1
- package/tests/unit/tools/siteOptions.test.js +101 -0
package/README.md
CHANGED
|
@@ -3,14 +3,14 @@
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
4
|
[](https://nodejs.org/)
|
|
5
5
|
[](https://github.com/anthropics/mcp)
|
|
6
|
-
[](https://github.com/GeorgesAdSim/wordpress-mcp-server/actions)
|
|
7
7
|
[](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.
|
|
13
|
+
**v4.6.0 Enterprise** · 92 tools · 767 Vitest tests · GitHub Actions CI · HTTP Streamable transport · MCPB bundle · SEO metadata · SEO audit suite · Content intelligence · Plugin intelligence · Plugin layer (ACF, Elementor) · 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
|
|
|
@@ -174,7 +174,7 @@ npx -y @adsim/wordpress-mcp-server
|
|
|
174
174
|
### Health check
|
|
175
175
|
```bash
|
|
176
176
|
curl http://localhost:3000/health
|
|
177
|
-
# → { "status": "ok", "version": "4.
|
|
177
|
+
# → { "status": "ok", "version": "4.6.0", "transport": "http" }
|
|
178
178
|
```
|
|
179
179
|
|
|
180
180
|
### Connect an MCP client via HTTP
|
|
@@ -214,7 +214,7 @@ Double-click `wordpress-mcp-server.mcpb` — Claude Desktop will prompt for:
|
|
|
214
214
|
|
|
215
215
|
---
|
|
216
216
|
|
|
217
|
-
## Available Tools (
|
|
217
|
+
## Available Tools (92)
|
|
218
218
|
|
|
219
219
|
### Content Management
|
|
220
220
|
|
|
@@ -311,6 +311,33 @@ All SEO audit tools are read-only and always allowed regardless of governance fl
|
|
|
311
311
|
|
|
312
312
|
All Content Intelligence tools are read-only and always allowed regardless of governance flags.
|
|
313
313
|
|
|
314
|
+
### Plugin Intelligence Layer
|
|
315
|
+
|
|
316
|
+
> New in v4.6.0 — Extensible adapter architecture for third-party WordPress plugins. Adapters activate only when the plugin is detected via REST API namespace discovery.
|
|
317
|
+
|
|
318
|
+
Disable all plugin tools: `WP_DISABLE_PLUGIN_LAYERS=true`
|
|
319
|
+
|
|
320
|
+
**ACF (Advanced Custom Fields)**
|
|
321
|
+
|
|
322
|
+
| Tool | Description |
|
|
323
|
+
|---|---|
|
|
324
|
+
| `acf_get_fields` | Get ACF custom fields for a post/page with key filtering and raw/compact/summary modes |
|
|
325
|
+
| `acf_list_field_groups` | List all configured ACF field groups |
|
|
326
|
+
| `acf_get_field_group` | Get full detail of an ACF field group by ID |
|
|
327
|
+
| `acf_update_fields` | Update ACF custom fields for a post/page. Write — blocked by `WP_READ_ONLY` |
|
|
328
|
+
|
|
329
|
+
Requires ACF Pro or ACF Free with REST API enabled (`/acf/v3` namespace).
|
|
330
|
+
|
|
331
|
+
**Elementor**
|
|
332
|
+
|
|
333
|
+
| Tool | Description |
|
|
334
|
+
|---|---|
|
|
335
|
+
| `elementor_list_templates` | List Elementor templates (page, section, block, popup) with type filtering |
|
|
336
|
+
| `elementor_get_template` | Get full Elementor template content and elements. Context-guarded at 50k chars |
|
|
337
|
+
| `elementor_get_page_data` | Get Elementor editor data for a post/page: widgets used, elements count |
|
|
338
|
+
|
|
339
|
+
Requires Elementor Free or Pro (`/elementor/v1` namespace).
|
|
340
|
+
|
|
314
341
|
### Plugins
|
|
315
342
|
|
|
316
343
|
| Tool | Description |
|
|
@@ -387,7 +414,7 @@ All WooCommerce write tools are blocked by `WP_READ_ONLY`. `wc_price_guardrail`
|
|
|
387
414
|
| Tool | Description |
|
|
388
415
|
|---|---|
|
|
389
416
|
| `wp_set_target` | Switch active WordPress site in multi-target mode |
|
|
390
|
-
| `wp_site_info` | Site info, current user, post types, enterprise controls,
|
|
417
|
+
| `wp_site_info` | Site info, current user, post types, enterprise controls, available targets, and `plugin_layer` (detected plugins, tools count) |
|
|
391
418
|
|
|
392
419
|
---
|
|
393
420
|
|
|
@@ -716,7 +743,7 @@ WC_PRICE_GUARDRAIL_THRESHOLD=20 # percentage — changes above this require ex
|
|
|
716
743
|
|
|
717
744
|
## Testing
|
|
718
745
|
|
|
719
|
-
|
|
746
|
+
767 unit tests covering all 92 tools — zero network calls, fully mocked.
|
|
720
747
|
```bash
|
|
721
748
|
npm test # run all tests (vitest)
|
|
722
749
|
npm run test:watch # watch mode
|
|
@@ -756,9 +783,17 @@ npm run test:coverage # coverage report
|
|
|
756
783
|
| `transport/http.test.js` | HTTP transport, Bearer auth, sessions | 10 |
|
|
757
784
|
| `pluginDetector.test.js` | SEO plugin detection, rendered head, HTML head parsing | 13 |
|
|
758
785
|
| `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,
|
|
786
|
+
| `dxt/manifest.test.js` | MCPB manifest validation, 86 tools declared | 10 |
|
|
760
787
|
| `dynamicFiltering.test.js` | WooCommerce/editorial/plugin-intelligence filtering, combined counts, callable when filtered | 9 |
|
|
761
788
|
| `outputCompression.test.js` | mode=full/summary/ids_only for 10 listing tools (pages, media, comments, categories, tags, users, custom posts, plugins, themes, revisions) | 30 |
|
|
789
|
+
| `siteOptions.test.js` | wp_get_site_options: all options, key filtering, 403, audit log, not blocked by WP_READ_ONLY | 5 |
|
|
790
|
+
| `plugins/registry.test.js` | PluginRegistry: ACF/Elementor detection, empty namespaces, WP_DISABLE_PLUGIN_LAYERS, getSummary | 6 |
|
|
791
|
+
| `plugins/contextGuard.test.js` | applyContextGuard: under threshold, truncation, raw bypass, stderr log | 4 |
|
|
792
|
+
| `plugins/iPluginAdapter.test.js` | validateAdapter: complete adapter, missing id, missing getTools | 3 |
|
|
793
|
+
| `plugins/acf/acfAdapter.test.js` | ACF read tools: get fields, filter, contextGuard, 404, list groups, get group, audit log | 10 |
|
|
794
|
+
| `plugins/acf/acfAdapter.write.test.js` | ACF write: update fields, WP_READ_ONLY blocking, validation, 404/403, audit log | 8 |
|
|
795
|
+
| `plugins/elementor/elementorAdapter.test.js` | Elementor adapter: list/get templates, page data, contextGuard, validation, namespace detection, audit log | 10 |
|
|
796
|
+
| `pluginLayer.test.js` | Plugin Layer integration: listTools, callTool routing, wp_site_info, WP_DISABLE_PLUGIN_LAYERS, no collisions | 8 |
|
|
762
797
|
|
|
763
798
|
Each test verifies: success response shape, governance blocking (write tools), HTTP error handling (403/404), and audit log entries.
|
|
764
799
|
|
|
@@ -855,7 +890,7 @@ The server performs a health check on startup: REST API connectivity, user authe
|
|
|
855
890
|
- Credentials never logged — audit trail sanitizes all sensitive data
|
|
856
891
|
- No credentials in code — `.env` or environment variables only
|
|
857
892
|
- Instant revocation — Application Passwords can be revoked from WordPress admin
|
|
858
|
-
- Traceable requests — custom `User-Agent: WordPress-MCP-Server/4.
|
|
893
|
+
- Traceable requests — custom `User-Agent: WordPress-MCP-Server/4.6.0`
|
|
859
894
|
- Bearer token auth in HTTP mode — timing-safe comparison, no token in logs
|
|
860
895
|
- Origin validation in HTTP mode — anti-DNS-rebinding protection
|
|
861
896
|
|
|
@@ -928,6 +963,29 @@ npx @modelcontextprotocol/inspector node index.js
|
|
|
928
963
|
|
|
929
964
|
## Changelog
|
|
930
965
|
|
|
966
|
+
### v4.6.0 (2026-02-22) — Plugin Intelligence Layer
|
|
967
|
+
|
|
968
|
+
Extensible adapter architecture for third-party WordPress plugins. Adapters activate only when their plugin is detected via REST API namespace discovery — zero overhead when plugins are absent.
|
|
969
|
+
|
|
970
|
+
**Architecture:**
|
|
971
|
+
- `src/plugins/registry.js` — PluginRegistry with automatic plugin detection via REST namespaces. `WP_DISABLE_PLUGIN_LAYERS=true` disables all plugin tools
|
|
972
|
+
- `src/plugins/contextGuard.js` — LLM context overflow protection: automatic truncation at 50k chars with truncation metadata
|
|
973
|
+
- `src/plugins/IPluginAdapter.js` — Adapter contract interface: id, namespace, riskLevel, contextConfig, getTools, handleTool
|
|
974
|
+
- `wp_site_info` now reports `plugin_layer` (detected plugins, available tools count)
|
|
975
|
+
|
|
976
|
+
**ACF Adapter:**
|
|
977
|
+
- `acf_get_fields` — ACF custom fields with key filtering, raw/compact/summary modes
|
|
978
|
+
- `acf_list_field_groups` — all configured field groups
|
|
979
|
+
- `acf_get_field_group` — field group detail by ID
|
|
980
|
+
- `acf_update_fields` — update custom fields. Blocked by `WP_READ_ONLY`. riskLevel: "medium"
|
|
981
|
+
|
|
982
|
+
**Elementor Adapter (read-only):**
|
|
983
|
+
- `elementor_list_templates` — templates with type filter (page/section/block/popup)
|
|
984
|
+
- `elementor_get_template` — full template content, context-guarded at 50k chars
|
|
985
|
+
- `elementor_get_page_data` — widgets used, elements count, Elementor status per post
|
|
986
|
+
|
|
987
|
+
767 Vitest unit tests · 92 tools
|
|
988
|
+
|
|
931
989
|
### v4.5.1 (2026-02-21) — Context Optimization
|
|
932
990
|
|
|
933
991
|
LLM context reduction across all 85 tools — zero breaking changes.
|
|
@@ -1114,16 +1172,16 @@ All Content Intelligence tools are read-only and always allowed regardless of go
|
|
|
1114
1172
|
|
|
1115
1173
|
## Roadmap
|
|
1116
1174
|
|
|
1117
|
-
### v4.
|
|
1175
|
+
### v4.7 — GSC Integration
|
|
1118
1176
|
- `wp_get_gsc_performance` — Google Search Console API (clicks, impressions, position, CTR per URL)
|
|
1119
1177
|
- `wp_find_quick_win_keywords` — surface keywords ranking positions 11–20 for targeted updates
|
|
1120
1178
|
- `wp_seo_content_decay` — cross-reference GSC traffic loss with content age to prioritize refresh candidates
|
|
1121
1179
|
|
|
1122
|
-
### v4.
|
|
1180
|
+
### v4.8 — Redirect Intelligence
|
|
1123
1181
|
- `wp_create_redirect` — create 301 redirects via Redirection plugin or RankMath/Yoast Redirects. Auto-triggered governance hook when `wp_update_post` changes a slug
|
|
1124
1182
|
- `wp_list_404_errors` — surface recent 404s from Redirection plugin log
|
|
1125
1183
|
|
|
1126
|
-
### v4.
|
|
1184
|
+
### v4.9 — OAuth & Registry
|
|
1127
1185
|
- OAuth 2.0 / JWT authentication
|
|
1128
1186
|
- MCP Registry submission
|
|
1129
1187
|
|
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.
|
|
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.
|
|
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
|
|
5
|
+
"version": "4.6.0",
|
|
6
|
+
"description": "Manage your WordPress site from Claude Desktop — posts, pages, media, SEO audits, content intelligence, plugin intelligence, plugin layer (ACF, Elementor), plugins, themes, revisions, WooCommerce, content compression, and more. 92 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 92 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- Plugin layer: extensible adapter architecture for third-party plugins (ACF, Elementor). Adapters activate only when plugin detected via REST namespace discovery\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",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
{ "name": "wp_list_custom_posts", "description": "List posts from any custom post type" },
|
|
60
60
|
{ "name": "wp_list_users", "description": "List users with roles" },
|
|
61
61
|
{ "name": "wp_set_target", "description": "Switch active WordPress site (multi-target mode)" },
|
|
62
|
-
{ "name": "wp_site_info", "description": "Get site info, current user, post types, and
|
|
62
|
+
{ "name": "wp_site_info", "description": "Get site info, current user, post types, enterprise controls, and plugin_layer (detected plugins, tools count)" },
|
|
63
63
|
{ "name": "wp_get_seo_meta", "description": "Get SEO metadata for a post or page" },
|
|
64
64
|
{ "name": "wp_update_seo_meta", "description": "Update SEO metadata for a post or page" },
|
|
65
65
|
{ "name": "wp_audit_seo", "description": "Audit SEO metadata across multiple posts/pages" },
|
|
@@ -118,7 +118,15 @@
|
|
|
118
118
|
{ "name": "wp_get_pillar_content", "description": "Read or set RankMath pillar/cornerstone content flag. Write mode blocked by WP_READ_ONLY" },
|
|
119
119
|
{ "name": "wp_audit_schema_plugins", "description": "Audit JSON-LD schemas from SEO plugin native fields (rank_math_schema or Yoast head)" },
|
|
120
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" }
|
|
121
|
+
{ "name": "wp_get_twitter_meta", "description": "Read or write Twitter Card meta (title, description, image) from RankMath, Yoast, or SEOPress" },
|
|
122
|
+
{ "name": "wp_get_site_options", "description": "Read WordPress site settings (title, tagline, language, timezone) via /wp/v2/settings. Requires manage_options" },
|
|
123
|
+
{ "name": "acf_get_fields", "description": "Get ACF custom fields for a post/page with key filtering and raw/compact/summary modes. Requires ACF plugin" },
|
|
124
|
+
{ "name": "acf_list_field_groups", "description": "List all configured ACF field groups. Requires ACF plugin" },
|
|
125
|
+
{ "name": "acf_get_field_group", "description": "Get full detail of an ACF field group by ID. Requires ACF plugin" },
|
|
126
|
+
{ "name": "acf_update_fields", "description": "Update ACF custom fields for a post/page. Write — blocked by WP_READ_ONLY. Requires ACF plugin" },
|
|
127
|
+
{ "name": "elementor_list_templates", "description": "List Elementor templates (page, section, block, popup) with type filtering. Requires Elementor" },
|
|
128
|
+
{ "name": "elementor_get_template", "description": "Get full Elementor template content and elements. Context-guarded at 50k chars. Requires Elementor" },
|
|
129
|
+
{ "name": "elementor_get_page_data", "description": "Get Elementor editor data for a post/page: widgets used, elements count. Requires Elementor" }
|
|
122
130
|
],
|
|
123
131
|
"keywords": [
|
|
124
132
|
"wordpress",
|
|
@@ -136,7 +144,10 @@
|
|
|
136
144
|
"ecommerce",
|
|
137
145
|
"content-compression",
|
|
138
146
|
"content-intelligence",
|
|
139
|
-
"plugin-intelligence"
|
|
147
|
+
"plugin-intelligence",
|
|
148
|
+
"plugin-layer",
|
|
149
|
+
"acf",
|
|
150
|
+
"elementor"
|
|
140
151
|
],
|
|
141
152
|
"license": "MIT",
|
|
142
153
|
"user_config": {
|
package/index.js
CHANGED
|
@@ -29,12 +29,14 @@ import { parseImagesFromHtml, extractHeadings, extractInternalLinks as extractIn
|
|
|
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
31
|
import { detectSeoPlugin, getRenderedHead, parseRenderedHead } from './src/pluginDetector.js';
|
|
32
|
+
import { PluginRegistry } from './src/plugins/registry.js';
|
|
33
|
+
import { acfAdapter } from './src/plugins/adapters/acf/acfAdapter.js';
|
|
32
34
|
|
|
33
35
|
// ============================================================
|
|
34
36
|
// CONFIGURATION
|
|
35
37
|
// ============================================================
|
|
36
38
|
|
|
37
|
-
const VERSION = '
|
|
39
|
+
const VERSION = '4.6.0';
|
|
38
40
|
const VERBOSE = process.env.WP_MCP_VERBOSE === 'true' || process.argv.includes('--verbose');
|
|
39
41
|
const MAX_RETRIES = parseInt(process.env.WP_MCP_MAX_RETRIES || '3', 10);
|
|
40
42
|
const TIMEOUT_MS = parseInt(process.env.WP_MCP_TIMEOUT || '30000', 10);
|
|
@@ -418,6 +420,28 @@ async function healthCheck() {
|
|
|
418
420
|
} catch (e) { log.warn(`Health check: ${e.message}`); }
|
|
419
421
|
}
|
|
420
422
|
|
|
423
|
+
// ============================================================
|
|
424
|
+
// PLUGIN LAYER
|
|
425
|
+
// ============================================================
|
|
426
|
+
|
|
427
|
+
const pluginRegistry = new PluginRegistry();
|
|
428
|
+
|
|
429
|
+
const PLUGIN_ADAPTERS = [acfAdapter];
|
|
430
|
+
|
|
431
|
+
async function initializePluginLayer() {
|
|
432
|
+
try {
|
|
433
|
+
await pluginRegistry.initialize(wpApiCall);
|
|
434
|
+
for (const adapter of PLUGIN_ADAPTERS) {
|
|
435
|
+
if (pluginRegistry.isActive(adapter.id)) {
|
|
436
|
+
pluginRegistry.registerTools(adapter.id, adapter.getTools());
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
log.info(JSON.stringify({ event: 'plugin_layer_initialized', summary: pluginRegistry.getSummary() }));
|
|
440
|
+
} catch (e) {
|
|
441
|
+
log.warn(`Plugin layer init failed (continuing without plugins): ${e.message}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
421
445
|
// ============================================================
|
|
422
446
|
// MCP SERVER
|
|
423
447
|
// ============================================================
|
|
@@ -436,7 +460,7 @@ const ORDERBY = ['date', 'relevance', 'id', 'title', 'slug', 'modified', 'author
|
|
|
436
460
|
const ORDERS = ['asc', 'desc'];
|
|
437
461
|
const MEDIA_TYPES = ['image', 'video', 'audio', 'application'];
|
|
438
462
|
const COMMENT_STATUSES = ['approved', 'hold', 'spam', 'trash'];
|
|
439
|
-
const TOOLS_COUNT =
|
|
463
|
+
const TOOLS_COUNT = 86;
|
|
440
464
|
|
|
441
465
|
function json(data) { return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] }; }
|
|
442
466
|
function strip(html) { return (html || '').replace(/<[^>]*>/g, '').trim(); }
|
|
@@ -493,9 +517,11 @@ const TOOLS_DEFINITIONS = [
|
|
|
493
517
|
{ name: 'wp_set_target', description: 'Use to switch active WordPress site in multi-target mode. Write.',
|
|
494
518
|
inputSchema: { type: 'object', properties: { site: { type: 'string', description: 'Site key from targets config' } }, required: ['site'] }},
|
|
495
519
|
|
|
496
|
-
// ── SITE INFO (
|
|
520
|
+
// ── SITE INFO (2) ──
|
|
497
521
|
{ name: 'wp_site_info', description: 'Use first to discover site config, user, post types, governance flags, and available targets. Read-only.',
|
|
498
522
|
inputSchema: { type: 'object', properties: {} }},
|
|
523
|
+
{ name: 'wp_get_site_options', description: 'Use to read WordPress site settings (title, tagline, language, timezone, etc.) via /wp/v2/settings. Requires manage_options. Read-only.',
|
|
524
|
+
inputSchema: { type: 'object', properties: { keys: { type: 'array', items: { type: 'string' }, description: 'Return only these option keys (returns all if omitted)' } }}},
|
|
499
525
|
|
|
500
526
|
// ── SEO METADATA (3) ──
|
|
501
527
|
{ 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.',
|
|
@@ -655,13 +681,15 @@ const TOOLS_DEFINITIONS = [
|
|
|
655
681
|
function getFilteredTools(allTools = TOOLS_DEFINITIONS) {
|
|
656
682
|
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
683
|
const editorialTools = ['wp_submit_for_review', 'wp_approve_post', 'wp_reject_post'];
|
|
658
|
-
|
|
684
|
+
const coreFiltered = allTools.filter(tool => {
|
|
659
685
|
const n = tool.name;
|
|
660
686
|
if (n.startsWith('wc_') && !process.env.WC_CONSUMER_KEY) return false;
|
|
661
687
|
if (editorialTools.includes(n) && process.env.WP_REQUIRE_APPROVAL !== 'true') return false;
|
|
662
688
|
if (pluginIntelTools.includes(n) && process.env.WP_ENABLE_PLUGIN_INTELLIGENCE !== 'true') return false;
|
|
663
689
|
return true;
|
|
664
690
|
});
|
|
691
|
+
const pluginTools = pluginRegistry.getAvailableTools();
|
|
692
|
+
return [...coreFiltered, ...pluginTools];
|
|
665
693
|
}
|
|
666
694
|
|
|
667
695
|
function registerHandlers(s) {
|
|
@@ -692,6 +720,21 @@ export async function handleToolCall(request) {
|
|
|
692
720
|
|
|
693
721
|
log.debug(`Tool: ${name}`, args);
|
|
694
722
|
|
|
723
|
+
// Plugin Layer routing — check adapters before core switch
|
|
724
|
+
const pluginTool = pluginRegistry.getAvailableTools().find(t => t.name === name);
|
|
725
|
+
if (pluginTool && pluginTool.handler) {
|
|
726
|
+
try {
|
|
727
|
+
const pluginResult = await pluginTool.handler(args, wpApiCall);
|
|
728
|
+
log.debug(`${name} done in ${Date.now() - t0}ms`);
|
|
729
|
+
return pluginResult;
|
|
730
|
+
} catch (error) {
|
|
731
|
+
const ms = Date.now() - t0;
|
|
732
|
+
log.error(`${name} failed (${ms}ms): ${error.message}`);
|
|
733
|
+
auditLog({ tool: name, target: args.id || null, status: 'error', latency_ms: ms, params: sanitizeParams(args), error: error.message });
|
|
734
|
+
return { content: [{ type: 'text', text: `Error: ${error.message}` }], isError: true };
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
695
738
|
try {
|
|
696
739
|
let result;
|
|
697
740
|
|
|
@@ -1097,12 +1140,29 @@ export async function handleToolCall(request) {
|
|
|
1097
1140
|
if (process.env.WP_REQUIRE_APPROVAL !== 'true') groups.push('editorial');
|
|
1098
1141
|
if (process.env.WP_ENABLE_PLUGIN_INTELLIGENCE !== 'true') groups.push('plugin_intelligence');
|
|
1099
1142
|
return { mcp_version: VERSION, tools_total: TOOLS_COUNT, tools_exposed: exposed, filtered_out: groups };
|
|
1100
|
-
})()
|
|
1143
|
+
})(),
|
|
1144
|
+
plugin_layer: pluginRegistry.getSummary()
|
|
1101
1145
|
});
|
|
1102
1146
|
auditLog({ tool: name, action: 'info', status: 'success', latency_ms: Date.now() - t0 });
|
|
1103
1147
|
break;
|
|
1104
1148
|
}
|
|
1105
1149
|
|
|
1150
|
+
case 'wp_get_site_options': {
|
|
1151
|
+
validateInput(args, { keys: { type: 'array' } });
|
|
1152
|
+
const { keys } = args;
|
|
1153
|
+
const settings = await wpApiCall('/settings');
|
|
1154
|
+
let data = settings;
|
|
1155
|
+
if (keys && keys.length > 0) {
|
|
1156
|
+
data = {};
|
|
1157
|
+
for (const k of keys) {
|
|
1158
|
+
if (k in settings) data[k] = settings[k];
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
result = json(data);
|
|
1162
|
+
auditLog({ tool: name, action: 'read_options', status: 'success', latency_ms: Date.now() - t0, params: { keys_requested: keys?.length || 'all', keys_returned: Object.keys(data).length } });
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1106
1166
|
// ── SEO METADATA ──
|
|
1107
1167
|
|
|
1108
1168
|
case 'wp_get_seo_meta': {
|
|
@@ -5023,6 +5083,7 @@ export function createMcpServer() {
|
|
|
5023
5083
|
|
|
5024
5084
|
async function main() {
|
|
5025
5085
|
await healthCheck();
|
|
5086
|
+
await initializePluginLayer();
|
|
5026
5087
|
|
|
5027
5088
|
const transportMode = (process.env.MCP_TRANSPORT || 'stdio').toLowerCase();
|
|
5028
5089
|
|
|
@@ -5054,4 +5115,4 @@ if (process.env.NODE_ENV !== 'test') {
|
|
|
5054
5115
|
main().catch((error) => { log.error(`Fatal: ${error.message}`); process.exit(1); });
|
|
5055
5116
|
}
|
|
5056
5117
|
|
|
5057
|
-
export { server, getActiveControls, getControlSources, _testSetTarget, getFilteredTools };
|
|
5118
|
+
export { server, getActiveControls, getControlSources, _testSetTarget, getFilteredTools, pluginRegistry, initializePluginLayer };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adsim/wordpress-mcp-server",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.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",
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plugin Adapter Interface — contract every adapter must implement.
|
|
3
|
+
*
|
|
4
|
+
* @typedef {Object} IPluginAdapter
|
|
5
|
+
* @property {string} id
|
|
6
|
+
* Unique identifier for the plugin (e.g. "acf", "elementor", "astra").
|
|
7
|
+
*
|
|
8
|
+
* @property {string} namespace
|
|
9
|
+
* REST API namespace used for detection (e.g. "acf/v3", "elementor/v1").
|
|
10
|
+
*
|
|
11
|
+
* @property {"low"|"medium"|"high"|"critical"} riskLevel
|
|
12
|
+
* Global risk level for audit logging.
|
|
13
|
+
* - low: read-only operations, zero side-effects
|
|
14
|
+
* - medium: modifies a single targeted resource
|
|
15
|
+
* - high: cascading modifications across multiple resources
|
|
16
|
+
* - critical: site-wide visual / structural impact
|
|
17
|
+
*
|
|
18
|
+
* @property {Object} contextConfig
|
|
19
|
+
* Context-size guardrails for this adapter.
|
|
20
|
+
* @property {number} contextConfig.maxChars
|
|
21
|
+
* Maximum serialised response size in characters (default 30 000).
|
|
22
|
+
* @property {string} contextConfig.defaultMode
|
|
23
|
+
* Default response mode if the caller omits it (typically "compact").
|
|
24
|
+
* @property {string[]} contextConfig.supportedModes
|
|
25
|
+
* Modes the adapter's tools accept (e.g. ["raw", "compact", "summary", "ids_only"]).
|
|
26
|
+
*
|
|
27
|
+
* @property {Function} getTools
|
|
28
|
+
* Returns an array of MCP tool definition objects
|
|
29
|
+
* ({ name, description, inputSchema, handler }).
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const VALID_RISK_LEVELS = ['low', 'medium', 'high', 'critical'];
|
|
33
|
+
|
|
34
|
+
const REQUIRED_FIELDS = [
|
|
35
|
+
{ key: 'id', type: 'string' },
|
|
36
|
+
{ key: 'namespace', type: 'string' },
|
|
37
|
+
{ key: 'riskLevel', type: 'string', enum: VALID_RISK_LEVELS },
|
|
38
|
+
{ key: 'contextConfig', type: 'object' },
|
|
39
|
+
{ key: 'getTools', type: 'function' },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate that an adapter object satisfies the IPluginAdapter contract.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} adapter Adapter object to validate
|
|
46
|
+
* @returns {{ valid: boolean, errors: string[] }}
|
|
47
|
+
*/
|
|
48
|
+
export function validateAdapter(adapter) {
|
|
49
|
+
const errors = [];
|
|
50
|
+
|
|
51
|
+
if (!adapter || typeof adapter !== 'object') {
|
|
52
|
+
return { valid: false, errors: ['adapter must be a non-null object'] };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const field of REQUIRED_FIELDS) {
|
|
56
|
+
const value = adapter[field.key];
|
|
57
|
+
|
|
58
|
+
if (value === undefined || value === null) {
|
|
59
|
+
errors.push(`missing required field: "${field.key}"`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (field.type === 'function') {
|
|
64
|
+
if (typeof value !== 'function') {
|
|
65
|
+
errors.push(`"${field.key}" must be a function, got ${typeof value}`);
|
|
66
|
+
}
|
|
67
|
+
} else if (field.type === 'object') {
|
|
68
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
69
|
+
errors.push(`"${field.key}" must be a plain object`);
|
|
70
|
+
}
|
|
71
|
+
} else if (typeof value !== field.type) {
|
|
72
|
+
errors.push(`"${field.key}" must be a ${field.type}, got ${typeof value}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (field.enum && !field.enum.includes(value)) {
|
|
76
|
+
errors.push(`"${field.key}" must be one of: ${field.enum.join(', ')}; got "${value}"`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// contextConfig sub-fields
|
|
81
|
+
if (adapter.contextConfig && typeof adapter.contextConfig === 'object') {
|
|
82
|
+
const cc = adapter.contextConfig;
|
|
83
|
+
if (cc.maxChars !== undefined && typeof cc.maxChars !== 'number') {
|
|
84
|
+
errors.push('"contextConfig.maxChars" must be a number');
|
|
85
|
+
}
|
|
86
|
+
if (cc.defaultMode !== undefined && typeof cc.defaultMode !== 'string') {
|
|
87
|
+
errors.push('"contextConfig.defaultMode" must be a string');
|
|
88
|
+
}
|
|
89
|
+
if (cc.supportedModes !== undefined && !Array.isArray(cc.supportedModes)) {
|
|
90
|
+
errors.push('"contextConfig.supportedModes" must be an array');
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { valid: errors.length === 0, errors };
|
|
95
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ACF (Advanced Custom Fields) Adapter — read + write.
|
|
3
|
+
*
|
|
4
|
+
* Provides 4 tools for ACF data via the ACF REST API (acf/v3):
|
|
5
|
+
* - acf_get_fields Read custom fields for a post or page
|
|
6
|
+
* - acf_list_field_groups List registered field groups
|
|
7
|
+
* - acf_get_field_group Get full details of a field group by ID
|
|
8
|
+
* - acf_update_fields Update custom fields (write — blocked by WP_READ_ONLY)
|
|
9
|
+
*
|
|
10
|
+
* Read tools are riskLevel "low". The write tool checks WP_READ_ONLY before
|
|
11
|
+
* making any API call. All responses pass through contextGuard.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { applyContextGuard } from '../../contextGuard.js';
|
|
15
|
+
|
|
16
|
+
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
function json(data) {
|
|
19
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function auditLog(entry) {
|
|
23
|
+
console.error(`[AUDIT] ${JSON.stringify({ timestamp: new Date().toISOString(), ...entry })}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ── Tool handlers ───────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {object} args
|
|
30
|
+
* @param {Function} apiRequest wpApiCall-compatible: (endpoint, options?) => JSON
|
|
31
|
+
*/
|
|
32
|
+
async function handleGetFields(args, apiRequest) {
|
|
33
|
+
const t0 = Date.now();
|
|
34
|
+
const { id, post_type = 'posts', fields, mode = 'compact' } = args;
|
|
35
|
+
|
|
36
|
+
const data = await apiRequest(`/${post_type}/${id}`, { basePath: '/wp-json/acf/v3' });
|
|
37
|
+
|
|
38
|
+
let acfData = data?.acf ?? data ?? {};
|
|
39
|
+
|
|
40
|
+
// Filter to requested keys
|
|
41
|
+
if (fields && fields.length > 0) {
|
|
42
|
+
const filtered = {};
|
|
43
|
+
for (const k of fields) {
|
|
44
|
+
if (k in acfData) filtered[k] = acfData[k];
|
|
45
|
+
}
|
|
46
|
+
acfData = filtered;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const guarded = applyContextGuard(
|
|
50
|
+
{ id, post_type, fields_count: Object.keys(acfData).length, acf: acfData },
|
|
51
|
+
{ toolName: 'acf_get_fields', mode, maxChars: 30000 }
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
auditLog({ tool: 'acf_get_fields', action: 'read_acf_fields', target: id, status: 'success', latency_ms: Date.now() - t0 });
|
|
55
|
+
return json(guarded);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function handleListFieldGroups(args, apiRequest) {
|
|
59
|
+
const t0 = Date.now();
|
|
60
|
+
|
|
61
|
+
const groups = await apiRequest('/field-groups', { basePath: '/wp-json/acf/v3' });
|
|
62
|
+
const list = Array.isArray(groups) ? groups : [];
|
|
63
|
+
|
|
64
|
+
const summary = list.map(g => ({
|
|
65
|
+
id: g.id,
|
|
66
|
+
title: g.title?.rendered ?? g.title ?? '',
|
|
67
|
+
fields: (g.fields ?? []).map(f => f.name ?? f.key ?? f.label ?? ''),
|
|
68
|
+
location: g.location ?? [],
|
|
69
|
+
}));
|
|
70
|
+
|
|
71
|
+
auditLog({ tool: 'acf_list_field_groups', action: 'list_field_groups', status: 'success', latency_ms: Date.now() - t0, params: { count: summary.length } });
|
|
72
|
+
return json({ total: summary.length, field_groups: summary });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function handleGetFieldGroup(args, apiRequest) {
|
|
76
|
+
const t0 = Date.now();
|
|
77
|
+
const { id } = args;
|
|
78
|
+
|
|
79
|
+
const group = await apiRequest(`/field-groups/${id}`, { basePath: '/wp-json/acf/v3' });
|
|
80
|
+
|
|
81
|
+
auditLog({ tool: 'acf_get_field_group', action: 'read_field_group', target: id, status: 'success', latency_ms: Date.now() - t0 });
|
|
82
|
+
return json(group);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function handleUpdateFields(args, apiRequest) {
|
|
86
|
+
const t0 = Date.now();
|
|
87
|
+
const { id, post_type = 'posts', fields } = args;
|
|
88
|
+
|
|
89
|
+
// Validate id is a positive integer
|
|
90
|
+
if (typeof id !== 'number' || !Number.isInteger(id) || id <= 0) {
|
|
91
|
+
throw new Error('Validation error: "id" must be a positive integer');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Validate fields is a non-empty object
|
|
95
|
+
if (!fields || typeof fields !== 'object' || Array.isArray(fields) || Object.keys(fields).length === 0) {
|
|
96
|
+
throw new Error('Validation error: "fields" must be a non-empty object of key/value pairs');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Governance: WP_READ_ONLY check
|
|
100
|
+
if (process.env.WP_READ_ONLY === 'true') {
|
|
101
|
+
auditLog({ tool: 'acf_update_fields', action: 'update_acf_fields', target: id, status: 'blocked', latency_ms: Date.now() - t0 });
|
|
102
|
+
return json({ error: 'Blocked: READ-ONLY mode' });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const data = await apiRequest(`/${post_type}/${id}`, {
|
|
106
|
+
basePath: '/wp-json/acf/v3',
|
|
107
|
+
method: 'POST',
|
|
108
|
+
body: { fields },
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const updatedFields = data?.acf ?? data ?? {};
|
|
112
|
+
|
|
113
|
+
auditLog({ tool: 'acf_update_fields', action: 'update_acf_fields', target: id, status: 'success', latency_ms: Date.now() - t0 });
|
|
114
|
+
return json({ id, post_type, updated_fields: updatedFields });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── Tool definitions (MCP format) ───────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
const TOOLS = [
|
|
120
|
+
{
|
|
121
|
+
name: 'acf_get_fields',
|
|
122
|
+
description: 'Use to read ACF custom fields for a post or page. Read-only.',
|
|
123
|
+
inputSchema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
properties: {
|
|
126
|
+
id: { type: 'number' },
|
|
127
|
+
post_type: { type: 'string', enum: ['posts', 'pages'], default: 'posts' },
|
|
128
|
+
fields: { type: 'array', items: { type: 'string' }, description: 'Return only these field keys (returns all if omitted)' },
|
|
129
|
+
mode: { type: 'string', enum: ['raw', 'compact', 'summary'], default: 'compact' },
|
|
130
|
+
},
|
|
131
|
+
required: ['id'],
|
|
132
|
+
},
|
|
133
|
+
handler: handleGetFields,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: 'acf_list_field_groups',
|
|
137
|
+
description: 'Use to list ACF field groups registered on the site. Read-only.',
|
|
138
|
+
inputSchema: { type: 'object', properties: {} },
|
|
139
|
+
handler: handleListFieldGroups,
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'acf_get_field_group',
|
|
143
|
+
description: 'Use to get full details of an ACF field group by ID. Read-only.',
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
id: { type: 'number' },
|
|
148
|
+
},
|
|
149
|
+
required: ['id'],
|
|
150
|
+
},
|
|
151
|
+
handler: handleGetFieldGroup,
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'acf_update_fields',
|
|
155
|
+
description: 'Use to update ACF custom fields for a post or page. Write — blocked by WP_READ_ONLY.',
|
|
156
|
+
inputSchema: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
properties: {
|
|
159
|
+
id: { type: 'number' },
|
|
160
|
+
post_type: { type: 'string', enum: ['posts', 'pages'], default: 'posts' },
|
|
161
|
+
fields: { type: 'object', additionalProperties: true, description: 'Key/value pairs of ACF fields to update' },
|
|
162
|
+
},
|
|
163
|
+
required: ['id', 'fields'],
|
|
164
|
+
},
|
|
165
|
+
handler: handleUpdateFields,
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
// ── Adapter export ──────────────────────────────────────────────────────
|
|
170
|
+
|
|
171
|
+
export const acfAdapter = {
|
|
172
|
+
id: 'acf',
|
|
173
|
+
namespace: 'acf/v3',
|
|
174
|
+
riskLevel: 'medium',
|
|
175
|
+
contextConfig: {
|
|
176
|
+
maxChars: 30000,
|
|
177
|
+
defaultMode: 'compact',
|
|
178
|
+
supportedModes: ['raw', 'compact', 'summary', 'ids_only'],
|
|
179
|
+
},
|
|
180
|
+
getTools: () => TOOLS,
|
|
181
|
+
};
|