@adsim/wordpress-mcp-server 1.0.0 → 3.0.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.
Files changed (64) hide show
  1. package/.env.example +8 -0
  2. package/.github/workflows/ci.yml +20 -0
  3. package/LICENSE +1 -1
  4. package/README.md +596 -135
  5. package/index.js +1367 -0
  6. package/package.json +21 -33
  7. package/src/auth/bearer.js +72 -0
  8. package/src/transport/http.js +264 -0
  9. package/tests/helpers/mockWpRequest.js +135 -0
  10. package/tests/unit/governance.test.js +260 -0
  11. package/tests/unit/tools/comments.test.js +170 -0
  12. package/tests/unit/tools/media.test.js +279 -0
  13. package/tests/unit/tools/pages.test.js +222 -0
  14. package/tests/unit/tools/plugins.test.js +268 -0
  15. package/tests/unit/tools/posts.test.js +310 -0
  16. package/tests/unit/tools/revisions.test.js +299 -0
  17. package/tests/unit/tools/search.test.js +190 -0
  18. package/tests/unit/tools/seo.test.js +248 -0
  19. package/tests/unit/tools/site.test.js +133 -0
  20. package/tests/unit/tools/taxonomies.test.js +220 -0
  21. package/tests/unit/tools/themes.test.js +163 -0
  22. package/tests/unit/tools/users.test.js +113 -0
  23. package/tests/unit/transport/http.test.js +300 -0
  24. package/vitest.config.js +12 -0
  25. package/dist/constants.d.ts +0 -13
  26. package/dist/constants.d.ts.map +0 -1
  27. package/dist/constants.js +0 -10
  28. package/dist/constants.js.map +0 -1
  29. package/dist/index.d.ts +0 -3
  30. package/dist/index.d.ts.map +0 -1
  31. package/dist/index.js +0 -33
  32. package/dist/index.js.map +0 -1
  33. package/dist/schemas/index.d.ts +0 -308
  34. package/dist/schemas/index.d.ts.map +0 -1
  35. package/dist/schemas/index.js +0 -191
  36. package/dist/schemas/index.js.map +0 -1
  37. package/dist/services/formatters.d.ts +0 -22
  38. package/dist/services/formatters.d.ts.map +0 -1
  39. package/dist/services/formatters.js +0 -52
  40. package/dist/services/formatters.js.map +0 -1
  41. package/dist/services/wp-client.d.ts +0 -38
  42. package/dist/services/wp-client.d.ts.map +0 -1
  43. package/dist/services/wp-client.js +0 -102
  44. package/dist/services/wp-client.js.map +0 -1
  45. package/dist/tools/content.d.ts +0 -4
  46. package/dist/tools/content.d.ts.map +0 -1
  47. package/dist/tools/content.js +0 -196
  48. package/dist/tools/content.js.map +0 -1
  49. package/dist/tools/posts.d.ts +0 -4
  50. package/dist/tools/posts.d.ts.map +0 -1
  51. package/dist/tools/posts.js +0 -179
  52. package/dist/tools/posts.js.map +0 -1
  53. package/dist/tools/seo.d.ts +0 -4
  54. package/dist/tools/seo.d.ts.map +0 -1
  55. package/dist/tools/seo.js +0 -241
  56. package/dist/tools/seo.js.map +0 -1
  57. package/dist/tools/taxonomy.d.ts +0 -4
  58. package/dist/tools/taxonomy.d.ts.map +0 -1
  59. package/dist/tools/taxonomy.js +0 -82
  60. package/dist/tools/taxonomy.js.map +0 -1
  61. package/dist/types.d.ts +0 -160
  62. package/dist/types.d.ts.map +0 -1
  63. package/dist/types.js +0 -3
  64. package/dist/types.js.map +0 -1
package/README.md CHANGED
@@ -1,53 +1,109 @@
1
1
  # WordPress MCP Server
2
2
 
3
- [![npm version](https://badge.fury.io/js/%40adsim%2Fwordpress-mcp-server.svg)](https://www.npmjs.com/package/@adsim/wordpress-mcp-server)
4
3
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![Node.js](https://img.shields.io/badge/Node.js-18%2B-green)](https://nodejs.org/)
5
+ [![MCP SDK](https://img.shields.io/badge/MCP_SDK-1.8%2B-blue)](https://modelcontextprotocol.io/)
6
+ [![Tests](https://img.shields.io/badge/Tests-171%20passed-brightgreen)]()
5
7
 
6
- A comprehensive **MCP (Model Context Protocol)** server for the **WordPress REST API**. Manage posts, pages, categories, tags, media, users, comments, and SEO metadata directly from Claude Desktop or any MCP-compatible client.
8
+ Enterprise Governance · Audit Trail · Multi-Site · Plugin-Free
7
9
 
8
- Built with TypeScript, Zod validation, and full support for **RankMath** and **Yoast SEO** metadata.
10
+ The enterprise governance layer for Claude-to-WordPress integrations secure, auditable, and multi-site.
11
+
12
+ **v2.2.0 Enterprise · 35 tools · 171 Vitest tests · GitHub Actions CI · SEO metadata · Plugin & theme management · Revision control · Execution controls · JSON audit trail · Multi-site targeting**
9
13
 
10
14
  ---
11
15
 
12
- ## Features
16
+ ## Architecture
17
+
18
+ ```
19
+ ┌─────────────────────────┐
20
+ │ Claude Client │ Claude Desktop · Claude Code · Any MCP client
21
+ └────────────┬────────────┘
22
+ │ MCP Protocol (stdio)
23
+ ┌────────────▼────────────┐
24
+ │ WordPress MCP Server │ Node.js · Standalone · No WordPress plugin
25
+ ├─────────────────────────┤
26
+ │ Execution Controls │ Read-only · Draft-only · Plugin mgmt · Type/status allowlists
27
+ ├─────────────────────────┤
28
+ │ Audit Logging │ JSON on stderr · 35 instrumentation points
29
+ ├─────────────────────────┤
30
+ │ Rate Limiting │ Client-side · Configurable per-minute cap
31
+ └────────────┬────────────┘
32
+ │ HTTPS + WordPress Application Password (Basic Auth over TLS)
33
+ ┌────────────▼────────────┐
34
+ │ WordPress REST API │ Single site or multi-target
35
+ └─────────────────────────┘
36
+ ```
37
+
38
+ ## Why This Server
39
+
40
+ Most WordPress MCP servers focus on what you *can* do. This one focuses on what you *should be allowed* to do — and who can verify it happened.
41
+
42
+ In regulated environments — financial services, healthcare, legal, government — AI-powered content operations need guardrails. This server provides them out of the box: read-only mode for monitoring, draft-only mode for review workflows, structured audit logs for compliance, and multi-site management for agencies operating across client portfolios.
43
+
44
+ No composer, no PHP build, no WordPress admin plugin. Point it at any WordPress site with an Application Password, configure your execution policy, and connect your Claude client.
45
+
46
+ ## Safety Model
47
+
48
+ This server is designed for safe operation in production environments:
49
+
50
+ - **Default non-destructive** — delete operations must be explicitly enabled
51
+ - **Configurable execution modes** — read-only, draft-only, or full access per deployment
52
+ - **Pre-flight enforcement** — all guardrails checked before any API call is made
53
+ - **Full audit trail** — every action logged with timestamp, target, outcome, and latency
54
+ - **Credential isolation** — secrets never appear in logs or error outputs
55
+ - **Multi-tenant ready** — independent auth and config per WordPress target
13
56
 
14
- | Category | Tools | Description |
15
- |----------|-------|-------------|
16
- | **Posts** | `wp_list_posts`, `wp_get_post`, `wp_create_post`, `wp_update_post`, `wp_delete_post` | Full CRUD for posts with filtering, pagination, search |
17
- | **Pages** | `wp_list_pages` | List and filter pages |
18
- | **Taxonomy** | `wp_list_categories`, `wp_list_tags` | Browse categories and tags |
19
- | **SEO** | `wp_get_seo_meta`, `wp_update_seo_meta`, `wp_audit_seo` | RankMath & Yoast support, SEO auditing with scoring |
20
- | **Media** | `wp_list_media` | Browse media library by type |
21
- | **Search** | `wp_search` | Cross-content search |
22
- | **Users** | `wp_list_users` | List users by role |
23
- | **Comments** | `wp_list_comments` | List and filter comments |
24
- | **Site** | `wp_site_info` | Get site name, URL, timezone |
57
+ ## Data Retention
58
+
59
+ The server does not store or persist WordPress content. All processing is stateless content flows through the server and is never cached, written to disk, or retained in memory beyond the scope of a single tool invocation. Audit logs are emitted to stderr in real-time and can be disabled (WP_AUDIT_LOG=off) or redirected to any logging pipeline based on deployment requirements. Zero data retention by design.
25
60
 
26
61
  ---
27
62
 
28
63
  ## Quick Start
29
64
 
30
- ### 1. Prerequisites
65
+ ### Requirements
66
+
67
+ - Node.js >= 18
68
+ - WordPress site with REST API enabled (default since WP 4.7)
69
+ - WordPress Application Password (WP 5.6+)
70
+ - HTTPS endpoint (required for production)
71
+
72
+ ### Install
73
+
74
+ ```bash
75
+ git clone https://github.com/GeorgesAdSim/wordpress-mcp-server.git
76
+ cd wordpress-mcp-server
77
+ npm install
78
+ ```
79
+
80
+ ### Configure
31
81
 
32
- - **Node.js 18+**
33
- - **WordPress site** with REST API enabled (enabled by default)
34
- - **Application Password** (WordPress → Users → Your Profile → Application Passwords)
82
+ Create a `.env` file:
35
83
 
36
- ### 2. Install & Configure
84
+ ```bash
85
+ WP_API_URL=https://yoursite.com
86
+ WP_API_USERNAME=your-username
87
+ WP_API_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx
88
+ ```
89
+
90
+ To generate an Application Password: WordPress Admin → Users → Profile → Application Passwords → Add New.
37
91
 
38
- Add to your Claude Desktop config file:
92
+ ### Connect to Claude Desktop
39
93
 
40
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
41
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
94
+ Add to `claude_desktop_config.json`:
95
+
96
+ **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
97
+ **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`
42
98
 
43
99
  ```json
44
100
  {
45
101
  "mcpServers": {
46
102
  "wordpress": {
47
- "command": "npx",
48
- "args": ["-y", "@adsim/wordpress-mcp-server"],
103
+ "command": "node",
104
+ "args": ["/path/to/wordpress-mcp-server/index.js"],
49
105
  "env": {
50
- "WP_API_URL": "https://your-site.com",
106
+ "WP_API_URL": "https://yoursite.com",
51
107
  "WP_API_USERNAME": "your-username",
52
108
  "WP_API_PASSWORD": "xxxx xxxx xxxx xxxx xxxx xxxx"
53
109
  }
@@ -56,166 +112,539 @@ Add to your Claude Desktop config file:
56
112
  }
57
113
  ```
58
114
 
59
- ### 3. Restart Claude Desktop
115
+ ### Connect to Claude Code
60
116
 
61
- That's it! Ask Claude: *"List my 10 latest WordPress posts"*
117
+ ```bash
118
+ claude mcp add wordpress \
119
+ -e WP_API_URL=https://yoursite.com \
120
+ -e WP_API_USERNAME=your-username \
121
+ -e WP_API_PASSWORD="xxxx xxxx xxxx xxxx xxxx xxxx" \
122
+ -- node /path/to/wordpress-mcp-server/index.js
123
+ ```
62
124
 
63
125
  ---
64
126
 
65
- ## Environment Variables
127
+ ## Available Tools (35)
128
+
129
+ ### Content Management
130
+
131
+ | Tool | Description |
132
+ |------|-------------|
133
+ | wp_list_posts | List posts with pagination, filtering by status/category/tag/author, and search |
134
+ | wp_get_post | Get a post by ID with full content, meta fields, and taxonomy info |
135
+ | wp_create_post | Create a post (defaults to draft). Supports HTML, categories, tags, featured image, meta |
136
+ | wp_update_post | Update any post field. Only provided fields are modified |
137
+ | wp_delete_post | Move to trash by default. Permanent deletion requires force=true |
138
+ | wp_search | Full-text search across all content types |
139
+ | wp_list_pages | List pages with hierarchy (parent/child), templates, and menu order |
140
+ | wp_get_page | Get page content, template, and hierarchy info |
141
+ | wp_create_page | Create a page with parent, template, and menu_order support |
142
+ | wp_update_page | Update any page field |
143
+
144
+ ### Media Library
145
+
146
+ | Tool | Description |
147
+ |------|-------------|
148
+ | wp_list_media | Browse media with type filtering (image/video/audio/document) |
149
+ | wp_get_media | Get URL, dimensions, alt text, caption, and all available sizes |
150
+ | wp_upload_media | Upload a file from a public URL to the WordPress media library |
151
+
152
+ ### Taxonomies & Structure
153
+
154
+ | Tool | Description |
155
+ |------|-------------|
156
+ | wp_list_categories | List categories with hierarchy, post count, and descriptions |
157
+ | wp_list_tags | List tags with post count |
158
+ | wp_create_taxonomy_term | Create a new category or tag |
159
+ | wp_list_post_types | Discover all registered post types (including custom ones) |
160
+ | wp_list_custom_posts | List content from any custom post type (products, portfolio, events) |
161
+
162
+ ### Engagement
163
+
164
+ | Tool | Description |
165
+ |------|-------------|
166
+ | wp_list_comments | List comments with filtering by post, status, and author |
167
+ | wp_create_comment | Create a comment or reply on any post |
168
+ | wp_list_users | List users with roles (read-only) |
169
+
170
+ ### SEO Metadata
171
+
172
+ | Tool | Description |
173
+ |------|-------------|
174
+ | wp_get_seo_meta | Read SEO title, description, focus keyword, canonical, robots, Open Graph. Auto-detects Yoast, RankMath, SEOPress, All in One SEO |
175
+ | wp_update_seo_meta | Update SEO metadata with automatic plugin detection |
176
+ | wp_audit_seo | Bulk audit SEO across posts/pages with quality scoring (0-100), missing fields detection, and length checks |
177
+
178
+ SEO metadata updates are subject to the same enterprise controls and execution policies as all other write operations.
179
+
180
+ ### Plugins
181
+
182
+ | Tool | Description |
183
+ |------|-------------|
184
+ | wp_list_plugins | List installed plugins with status, version, author. Requires Administrator (activate_plugins capability) |
185
+ | wp_activate_plugin | Activate a plugin. Blocked by `WP_READ_ONLY` and `WP_DISABLE_PLUGIN_MANAGEMENT` |
186
+ | wp_deactivate_plugin | Deactivate a plugin. Blocked by `WP_READ_ONLY` and `WP_DISABLE_PLUGIN_MANAGEMENT` |
187
+
188
+ ### Themes
189
+
190
+ | Tool | Description |
191
+ |------|-------------|
192
+ | wp_list_themes | List installed themes with active theme detection. Requires `switch_themes` capability |
193
+ | wp_get_theme | Get theme details by stylesheet slug |
66
194
 
67
- | Variable | Required | Description |
68
- |----------|----------|-------------|
69
- | `WP_API_URL` | | Your WordPress site URL (e.g., `https://example.com`) |
70
- | `WP_API_USERNAME` | ✅ | WordPress username |
71
- | `WP_API_PASSWORD` | | Application Password (Users Profile Application Passwords) |
195
+ ### Revisions
196
+
197
+ | Tool | Description |
198
+ |------|-------------|
199
+ | wp_list_revisions | List revisions of a post or page (metadata only) |
200
+ | wp_get_revision | Get a specific revision with full content |
201
+ | wp_restore_revision | Restore a post to a previous revision (plugin-free 2-step approach) |
202
+ | wp_delete_revision | Permanently delete a revision. Blocked by `WP_READ_ONLY` and `WP_DISABLE_DELETE` |
203
+
204
+ ### Operations
205
+
206
+ | Tool | Description |
207
+ |------|-------------|
208
+ | wp_set_target | Switch active WordPress site in multi-target mode |
209
+ | wp_site_info | Site info, current user, post types, enterprise controls, and available targets |
72
210
 
73
211
  ---
74
212
 
75
- ## SEO Support
213
+ ## Enterprise Controls
76
214
 
77
- ### Supported Plugins
215
+ Configure execution policy via environment variables. All restrictions are enforced before any API call is made — including SEO metadata and plugin operations.
78
216
 
79
- - **RankMath** Full support: read/write SEO title, description, focus keyword
80
- - **Yoast SEO** — Read support: title, description via `yoast_head_json`
217
+ | Control | Default | Effect |
218
+ |---------|---------|--------|
219
+ | WP_READ_ONLY | false | Blocks all write operations (create, update, delete, upload, SEO updates, plugin management) |
220
+ | WP_DRAFT_ONLY | false | Restricts to draft and pending statuses only |
221
+ | WP_DISABLE_DELETE | false | Blocks all delete operations (posts + revisions) |
222
+ | WP_DISABLE_PLUGIN_MANAGEMENT | false | Blocks plugin activate/deactivate (list still allowed) |
223
+ | WP_ALLOWED_TYPES | all | Restricts to specific post types (e.g., post,page) |
224
+ | WP_ALLOWED_STATUSES | all | Restricts to specific statuses (e.g., draft,pending) |
225
+ | WP_MAX_CALLS_PER_MINUTE | unlimited | Client-side rate limiting |
226
+ | WP_AUDIT_LOG | on | Structured JSON audit trail |
81
227
 
82
- ### Exposing SEO Meta Fields
228
+ ### Deployment profiles
229
+
230
+ **Agency content production** — writers can create and edit, but never publish or delete:
231
+
232
+ ```bash
233
+ WP_DRAFT_ONLY=true
234
+ WP_DISABLE_DELETE=true
235
+ WP_ALLOWED_STATUSES=draft,pending
236
+ WP_MAX_CALLS_PER_MINUTE=30
237
+ ```
238
+
239
+ **Compliance monitoring** — read-only access for auditing existing content:
240
+
241
+ ```bash
242
+ WP_READ_ONLY=true
243
+ WP_AUDIT_LOG=on
244
+ ```
83
245
 
84
- For the SEO tools to work, you need to expose your SEO plugin's meta fields via the REST API. Add this code to your theme's `functions.php` or a custom plugin:
246
+ **Regulated publishing** restrict to specific content types in a controlled environment:
247
+
248
+ ```bash
249
+ WP_ALLOWED_TYPES=post
250
+ WP_ALLOWED_STATUSES=draft,pending,publish
251
+ WP_DISABLE_DELETE=true
252
+ WP_AUDIT_LOG=on
253
+ ```
254
+
255
+ **Locked infrastructure** — content operations allowed, but no plugin/theme changes:
256
+
257
+ ```bash
258
+ WP_DISABLE_PLUGIN_MANAGEMENT=true
259
+ WP_DISABLE_DELETE=true
260
+ ```
261
+
262
+ Blocked actions return a clear error message explaining which control prevented execution, and are logged in the audit trail with status `blocked`.
263
+
264
+ ---
85
265
 
86
- #### RankMath
266
+ ## SEO Metadata
267
+
268
+ The SEO tools auto-detect which SEO plugin is installed on your WordPress site and use the correct meta fields automatically.
269
+
270
+ Supported plugins:
271
+
272
+ - **Yoast SEO** — _yoast_wpseo_title, _yoast_wpseo_metadesc, _yoast_wpseo_focuskw, plus yoast_head_json REST API extension
273
+ - **RankMath** — rank_math_title, rank_math_description, rank_math_focus_keyword
274
+ - **SEOPress** — _seopress_titles_title, _seopress_titles_desc, _seopress_analysis_target_kw
275
+ - **All in One SEO** — _aioseo_title, _aioseo_description, _aioseo_keywords
276
+
277
+ ### SEO Audit Scoring
278
+
279
+ `wp_audit_seo` scores each post on a 100-point scale:
280
+
281
+ | Check | Penalty |
282
+ |-------|---------|
283
+ | Missing SEO title | -30 |
284
+ | SEO title too short (< 30 chars) or too long (> 60 chars) | -10 |
285
+ | Missing meta description | -30 |
286
+ | Meta description too short (< 120 chars) or too long (> 160 chars) | -10 |
287
+ | Missing focus keyword | -20 |
288
+ | Focus keyword not in SEO title | -10 |
289
+
290
+ ### Exposing SEO Meta Fields (Required)
291
+
292
+ Most SEO plugins store their data in WordPress post meta fields that are **not exposed via the REST API by default**. Without this step, `wp_get_seo_meta` and `wp_audit_seo` will return empty results even though your SEO data exists in the database.
293
+
294
+ Add the following code to your theme's `functions.php` (Appearance → Theme File Editor → functions.php) or — preferably — create a custom mini-plugin (see below).
295
+
296
+ > **Warning:** When pasting code into functions.php, make sure the file starts with exactly `<?php` — no extra characters before it. A stray character (like `<<?php`) will break the WordPress REST API by injecting invalid output before JSON responses, causing `Unexpected token '<'` errors in MCP.
297
+
298
+ **RankMath:**
87
299
 
88
300
  ```php
89
- <?php
90
- // Expose RankMath SEO fields via REST API
91
- add_action('init', function() {
92
- $fields = [
301
+ add_action( 'init', function() {
302
+ $fields = array(
93
303
  'rank_math_title',
94
304
  'rank_math_description',
95
305
  'rank_math_focus_keyword',
306
+ 'rank_math_canonical_url',
96
307
  'rank_math_robots',
97
- ];
98
-
99
- foreach (['post', 'page'] as $post_type) {
100
- foreach ($fields as $field) {
101
- register_post_meta($post_type, $field, [
308
+ 'rank_math_facebook_title',
309
+ 'rank_math_facebook_description',
310
+ 'rank_math_facebook_image',
311
+ );
312
+ foreach ( $fields as $field ) {
313
+ foreach ( array( 'post', 'page' ) as $post_type ) {
314
+ register_post_meta( $post_type, $field, array(
315
+ 'show_in_rest' => true,
316
+ 'single' => true,
317
+ 'type' => 'string',
318
+ 'auth_callback' => function() {
319
+ return current_user_can( 'edit_posts' );
320
+ },
321
+ ) );
322
+ }
323
+ }
324
+ } );
325
+ ```
326
+
327
+ **Yoast SEO:**
328
+
329
+ ```php
330
+ add_action( 'init', function() {
331
+ $fields = array(
332
+ '_yoast_wpseo_title',
333
+ '_yoast_wpseo_metadesc',
334
+ '_yoast_wpseo_focuskw',
335
+ '_yoast_wpseo_canonical',
336
+ '_yoast_wpseo_meta-robots-noindex',
337
+ '_yoast_wpseo_meta-robots-nofollow',
338
+ '_yoast_wpseo_opengraph-title',
339
+ '_yoast_wpseo_opengraph-description',
340
+ '_yoast_wpseo_opengraph-image',
341
+ );
342
+ foreach ( $fields as $field ) {
343
+ foreach ( array( 'post', 'page' ) as $post_type ) {
344
+ register_post_meta( $post_type, $field, array(
345
+ 'show_in_rest' => true,
346
+ 'single' => true,
347
+ 'type' => 'string',
348
+ 'auth_callback' => function() {
349
+ return current_user_can( 'edit_posts' );
350
+ },
351
+ ) );
352
+ }
353
+ }
354
+ } );
355
+ ```
356
+
357
+ **SEOPress:**
358
+
359
+ ```php
360
+ add_action( 'init', function() {
361
+ $fields = array(
362
+ '_seopress_titles_title',
363
+ '_seopress_titles_desc',
364
+ '_seopress_analysis_target_kw',
365
+ '_seopress_robots_canonical',
366
+ '_seopress_robots_index',
367
+ '_seopress_social_fb_title',
368
+ '_seopress_social_fb_desc',
369
+ '_seopress_social_fb_img',
370
+ );
371
+ foreach ( $fields as $field ) {
372
+ foreach ( array( 'post', 'page' ) as $post_type ) {
373
+ register_post_meta( $post_type, $field, array(
374
+ 'show_in_rest' => true,
375
+ 'single' => true,
376
+ 'type' => 'string',
377
+ 'auth_callback' => function() {
378
+ return current_user_can( 'edit_posts' );
379
+ },
380
+ ) );
381
+ }
382
+ }
383
+ } );
384
+ ```
385
+
386
+ **All in One SEO:**
387
+
388
+ ```php
389
+ add_action( 'init', function() {
390
+ $fields = array(
391
+ '_aioseo_title',
392
+ '_aioseo_description',
393
+ '_aioseo_keywords',
394
+ '_aioseo_og_title',
395
+ '_aioseo_og_description',
396
+ '_aioseo_og_image_url',
397
+ );
398
+ foreach ( $fields as $field ) {
399
+ foreach ( array( 'post', 'page' ) as $post_type ) {
400
+ register_post_meta( $post_type, $field, array(
102
401
  'show_in_rest' => true,
103
402
  'single' => true,
104
403
  'type' => 'string',
105
404
  'auth_callback' => function() {
106
- return current_user_can('edit_posts');
405
+ return current_user_can( 'edit_posts' );
107
406
  },
108
- ]);
407
+ ) );
109
408
  }
110
409
  }
111
- });
410
+ } );
112
411
  ```
113
412
 
114
- #### Yoast SEO
413
+ ### Alternative: MCP SEO Bridge Plugin (Recommended)
115
414
 
116
- Yoast automatically exposes `yoast_head_json` in REST API responses no extra configuration needed.
415
+ > **Note:** Core content operations require no WordPress plugin. SEO metadata tools may require exposing meta fields via the REST API using either a theme snippet or this optional micro-plugin.
117
416
 
118
- #### MCP SEO Bridge Plugin (Auto-Detection)
417
+ Instead of modifying your theme's `functions.php` (which gets overwritten on theme updates), create a standalone micro-plugin.
119
418
 
120
- For a more robust solution that survives theme updates and auto-detects your SEO plugin:
419
+ Create the file `wp-content/plugins/mcp-seo-bridge.php`:
121
420
 
122
421
  ```php
123
422
  <?php
124
423
  /**
125
424
  * Plugin Name: MCP SEO Bridge
126
- * Description: Exposes SEO meta fields for MCP WordPress Server
425
+ * Description: Exposes SEO plugin meta fields via REST API for WordPress MCP Server
127
426
  * Version: 1.0.0
427
+ * Author: AdSim
428
+ * Author URI: https://adsim.be
128
429
  */
129
430
 
130
- add_action('init', function() {
131
- // RankMath
132
- if (class_exists('RankMath')) {
133
- $fields = ['rank_math_title', 'rank_math_description', 'rank_math_focus_keyword', 'rank_math_robots'];
134
- foreach (['post', 'page'] as $type) {
135
- foreach ($fields as $field) {
136
- register_post_meta($type, $field, ['show_in_rest' => true, 'single' => true, 'type' => 'string', 'auth_callback' => fn() => current_user_can('edit_posts')]);
137
- }
138
- }
431
+ if ( ! defined( 'ABSPATH' ) ) exit;
432
+
433
+ add_action( 'init', function() {
434
+ // Auto-detect SEO plugin and register appropriate fields
435
+ $fields = array();
436
+
437
+ if ( defined( 'RANK_MATH_VERSION' ) ) {
438
+ $fields = array(
439
+ 'rank_math_title', 'rank_math_description', 'rank_math_focus_keyword',
440
+ 'rank_math_canonical_url', 'rank_math_robots',
441
+ 'rank_math_facebook_title', 'rank_math_facebook_description', 'rank_math_facebook_image',
442
+ );
443
+ } elseif ( defined( 'WPSEO_VERSION' ) ) {
444
+ $fields = array(
445
+ '_yoast_wpseo_title', '_yoast_wpseo_metadesc', '_yoast_wpseo_focuskw',
446
+ '_yoast_wpseo_canonical', '_yoast_wpseo_meta-robots-noindex', '_yoast_wpseo_meta-robots-nofollow',
447
+ '_yoast_wpseo_opengraph-title', '_yoast_wpseo_opengraph-description', '_yoast_wpseo_opengraph-image',
448
+ );
449
+ } elseif ( defined( 'SEOPRESS_VERSION' ) ) {
450
+ $fields = array(
451
+ '_seopress_titles_title', '_seopress_titles_desc', '_seopress_analysis_target_kw',
452
+ '_seopress_robots_canonical', '_seopress_robots_index',
453
+ '_seopress_social_fb_title', '_seopress_social_fb_desc', '_seopress_social_fb_img',
454
+ );
455
+ } elseif ( defined( 'AIOSEO_VERSION' ) ) {
456
+ $fields = array(
457
+ '_aioseo_title', '_aioseo_description', '_aioseo_keywords',
458
+ '_aioseo_og_title', '_aioseo_og_description', '_aioseo_og_image_url',
459
+ );
139
460
  }
140
-
141
- // SEOPress
142
- if (function_exists('seopress_init')) {
143
- $fields = ['_seopress_titles_title', '_seopress_titles_desc'];
144
- foreach (['post', 'page'] as $type) {
145
- foreach ($fields as $field) {
146
- register_post_meta($type, $field, ['show_in_rest' => true, 'single' => true, 'type' => 'string', 'auth_callback' => fn() => current_user_can('edit_posts')]);
147
- }
461
+
462
+ foreach ( $fields as $field ) {
463
+ foreach ( array( 'post', 'page' ) as $post_type ) {
464
+ register_post_meta( $post_type, $field, array(
465
+ 'show_in_rest' => true,
466
+ 'single' => true,
467
+ 'type' => 'string',
468
+ 'auth_callback' => function() {
469
+ return current_user_can( 'edit_posts' );
470
+ },
471
+ ) );
148
472
  }
149
473
  }
150
- });
474
+ } );
151
475
  ```
152
476
 
153
- > **⚠️ Important**: A stray character before `<?php` in functions.php will break the REST API with "Unexpected token '<'" errors. Always verify the file starts exactly with `<?php`.
477
+ Activate it from WordPress Admin Plugins. This approach auto-detects your SEO plugin and survives theme updates.
478
+
479
+ ### Verifying SEO Fields Are Exposed
480
+
481
+ After adding the code, verify the fields are accessible:
482
+
483
+ ```bash
484
+ curl -s -u "username:application-password" \
485
+ "https://yoursite.com/wp-json/wp/v2/posts?per_page=1" | python3 -m json.tool | grep -E "rank_math|yoast|seopress|aioseo"
486
+ ```
487
+
488
+ If you see your SEO fields in the meta object, the configuration is working.
489
+
490
+ ### Troubleshooting SEO Fields
491
+
492
+ | Symptom | Cause | Fix |
493
+ |---------|-------|-----|
494
+ | wp_audit_seo returns empty SEO data | Meta fields not exposed via REST API | Add register_post_meta() code above |
495
+ | Unexpected token '<' on all MCP calls | Stray character before `<?php` in functions.php | Remove any characters before `<?php` |
496
+ | SEO fields visible but all null | SEO plugin not yet configured on those posts | Set titles/descriptions in RankMath/Yoast editor |
497
+ | No SEO plugin detected | Plugin constant not matched | Verify your SEO plugin is active |
498
+ | Fields lost after theme update | Code was in functions.php | Use the MCP SEO Bridge plugin instead |
154
499
 
155
500
  ---
156
501
 
157
- ## Usage Examples
502
+ ## Testing
158
503
 
159
- Once configured, ask Claude naturally:
504
+ 171 unit tests covering all 35 tools — zero network calls, fully mocked.
160
505
 
506
+ ```bash
507
+ npm test # run all tests (vitest)
508
+ npm run test:watch # watch mode
509
+ npm run test:coverage # coverage report
161
510
  ```
162
- "List my 10 latest posts"
163
- "Show me the SEO audit for all published posts"
164
- "Create a draft post about AI trends in 2025"
165
- "Update the meta description of post #42"
166
- "Search my WordPress content for 'analytics'"
167
- "Show me all categories with their post counts"
168
- "List media files uploaded this month"
511
+
512
+ | Test file | Scope | Tests |
513
+ |-----------|-------|-------|
514
+ | governance.test.js | 4 governance flags + combinations | 24 |
515
+ | posts.test.js | list, get, create, update, delete, search | 18 |
516
+ | pages.test.js | list, get, create, update | 12 |
517
+ | media.test.js | list, get, upload | 14 |
518
+ | taxonomies.test.js | categories, tags, create term | 16 |
519
+ | comments.test.js | list, create | 12 |
520
+ | users.test.js | list | 7 |
521
+ | search.test.js | search, post types, custom posts | 10 |
522
+ | seo.test.js | get, update, audit | 12 |
523
+ | plugins.test.js | list, activate, deactivate | 16 |
524
+ | themes.test.js | list, get | 8 |
525
+ | revisions.test.js | list, get, restore, delete | 17 |
526
+ | site.test.js | site info, set target | 5 |
527
+
528
+ Each test verifies: success response shape, governance blocking (write tools), HTTP error handling (403/404), and audit log entries.
529
+
530
+ ---
531
+
532
+ ## Structured Audit Log
533
+
534
+ Every tool invocation is recorded as a JSON event on stderr — ready for ingestion into Datadog, Splunk, CloudWatch, Langfuse, ELK, or any JSON-compatible pipeline.
535
+
536
+ ```json
537
+ {
538
+ "timestamp": "2026-02-16T18:42:00.000Z",
539
+ "tool": "wp_create_post",
540
+ "target": 1234,
541
+ "target_type": "post",
542
+ "action": "create",
543
+ "status": "success",
544
+ "latency_ms": 245,
545
+ "site": "production",
546
+ "params": { "title": "New Post", "status": "draft" },
547
+ "error": null
548
+ }
169
549
  ```
170
550
 
551
+ 35 instrumentation points across all tools. Three status types: `success`, `error`, `blocked`.
552
+
553
+ | Field | Description |
554
+ |-------|-------------|
555
+ | timestamp | ISO 8601 |
556
+ | tool | Tool name invoked |
557
+ | target | Resource ID when applicable |
558
+ | target_type | Resource type (post, page, media, comment, category, tag, plugin, theme, revision) |
559
+ | action | Operation: list, read, create, update, trash, permanent_delete, upload, search, switch_target, read_seo, update_seo, audit_seo, activate, deactivate, restore |
560
+ | status | success, error, or blocked |
561
+ | latency_ms | Execution time |
562
+ | site | Active target name |
563
+ | params | Sanitized parameters (content fields truncated) |
564
+ | error | Error detail or null |
565
+
171
566
  ---
172
567
 
173
- ## Tools Reference
568
+ ## Multi-Target
174
569
 
175
- ### Posts
570
+ Manage multiple WordPress sites from a single server instance. Designed for agencies and multi-brand organizations.
176
571
 
177
- #### `wp_list_posts`
178
- List posts with advanced filtering.
572
+ **Inline configuration:**
179
573
 
180
- | Param | Type | Default | Description |
181
- |-------|------|---------|-------------|
182
- | `per_page` | number | 10 | Items per page (1-100) |
183
- | `page` | number | 1 | Page number |
184
- | `status` | string | publish | publish/draft/pending/private/future/trash |
185
- | `orderby` | string | date | date/id/title/slug/modified/author/relevance |
186
- | `order` | string | desc | asc/desc |
187
- | `categories` | string | — | Comma-separated category IDs |
188
- | `tags` | string | — | Comma-separated tag IDs |
189
- | `search` | string | — | Search keyword |
574
+ ```bash
575
+ WP_TARGETS_JSON='{"production":{"url":"https://mysite.com","username":"admin","password":"xxxx"},"staging":{"url":"https://staging.mysite.com","username":"editor","password":"xxxx"}}'
576
+ ```
190
577
 
191
- #### `wp_get_post`
192
- Get full post data including SEO metadata.
578
+ **File-based configuration:**
193
579
 
194
- #### `wp_create_post`
195
- Create a new post (defaults to draft for safety).
580
+ ```bash
581
+ WP_TARGETS_FILE=/path/to/targets.json
582
+ ```
196
583
 
197
- #### `wp_update_post`
198
- Update any post field including SEO meta via the `meta` parameter.
584
+ ```json
585
+ {
586
+ "production": {
587
+ "url": "https://mysite.com",
588
+ "username": "admin",
589
+ "password": "xxxx xxxx xxxx xxxx xxxx xxxx"
590
+ },
591
+ "staging": {
592
+ "url": "https://staging.mysite.com",
593
+ "username": "editor",
594
+ "password": "xxxx xxxx xxxx xxxx xxxx xxxx"
595
+ },
596
+ "client-blog": {
597
+ "url": "https://client.com",
598
+ "username": "content-manager",
599
+ "password": "xxxx xxxx xxxx xxxx xxxx xxxx"
600
+ }
601
+ }
602
+ ```
199
603
 
200
- #### `wp_delete_post`
201
- Delete or trash a post.
604
+ Switch targets during a session with `wp_set_target`. All available sites and the active target are visible in `wp_site_info`.
202
605
 
203
- ### SEO
606
+ ---
204
607
 
205
- #### `wp_get_seo_meta`
206
- Read SEO metadata (RankMath/Yoast).
608
+ ## Health & Reliability
207
609
 
208
- #### `wp_update_seo_meta`
209
- Update RankMath SEO fields (title, description, focus keyword).
610
+ The server performs a health check on startup: REST API connectivity, user authentication, and role verification. During operation: automatic retry with exponential backoff (configurable, default 3 attempts), request timeout (default 30s), rate limit handling (respects 429 + retry-after), and contextual error messages with diagnosis guidance.
210
611
 
211
- #### `wp_audit_seo`
212
- Audit SEO for multiple posts/pages with scoring (0-100), issues, and recommendations.
612
+ | Setting | Default | Description |
613
+ |---------|---------|-------------|
614
+ | WP_MCP_VERBOSE | false | Debug-level logging |
615
+ | WP_MCP_TIMEOUT | 30000 | Request timeout (ms) |
616
+ | WP_MCP_MAX_RETRIES | 3 | Max retry attempts |
213
617
 
214
- ### Content
618
+ ---
215
619
 
216
- #### `wp_list_pages`, `wp_list_media`, `wp_search`, `wp_list_comments`, `wp_list_users`, `wp_site_info`
620
+ ## Security
217
621
 
218
- See tool descriptions in Claude for full parameter details.
622
+ - **HTTPS required** in production. HTTP only for localhost
623
+ - **Application Passwords only** — never use WordPress login credentials
624
+ - **Credentials never logged** — audit trail sanitizes all sensitive data
625
+ - **No credentials in code** — .env or environment variables only
626
+ - **Instant revocation** — Application Passwords can be revoked from WordPress admin
627
+ - **Traceable requests** — custom User-Agent: WordPress-MCP-Server/2.2.0
628
+
629
+ ---
630
+
631
+ ## Troubleshooting
632
+
633
+ | Issue | Solution |
634
+ |-------|----------|
635
+ | 401 Unauthorized | Verify username and Application Password |
636
+ | 403 Forbidden | Check WordPress user role and capabilities |
637
+ | 404 Not Found | Verify WP_API_URL and REST API availability |
638
+ | Unexpected token '<' | Stray character before `<?php` in functions.php — see [SEO Troubleshooting](#troubleshooting-seo-fields) |
639
+ | Blocked: READ-ONLY mode | Disable WP_READ_ONLY to allow writes |
640
+ | Blocked: DRAFT-ONLY mode | Only draft/pending allowed. Check WP_DRAFT_ONLY |
641
+ | Blocked: PLUGIN MANAGEMENT | Disable WP_DISABLE_PLUGIN_MANAGEMENT to allow activate/deactivate |
642
+ | Rate limit exceeded | Adjust WP_MAX_CALLS_PER_MINUTE |
643
+ | Timeout | Increase WP_MCP_TIMEOUT or check server |
644
+ | Site not found | Verify site key in WP_TARGETS_JSON or file |
645
+ | No SEO plugin detected | Install Yoast, RankMath, SEOPress, or AIOSEO |
646
+ | SEO meta fields empty | Add register_post_meta() code or install MCP SEO Bridge plugin — see [Exposing SEO Meta Fields](#exposing-seo-meta-fields-required) |
647
+ | Server not starting | Check Node.js 18+ is installed: `node --version` |
219
648
 
220
649
  ---
221
650
 
@@ -229,44 +658,76 @@ cd wordpress-mcp-server
229
658
  # Install dependencies
230
659
  npm install
231
660
 
232
- # Build
233
- npm run build
661
+ # Run tests
662
+ npm test
234
663
 
235
664
  # Run locally
236
665
  WP_API_URL="https://your-site.com" \
237
666
  WP_API_USERNAME="user" \
238
667
  WP_API_PASSWORD="xxxx xxxx xxxx xxxx" \
239
- node dist/index.js
668
+ node index.js
240
669
  ```
241
670
 
242
671
  ### Testing with MCP Inspector
243
672
 
244
673
  ```bash
245
- npx @modelcontextprotocol/inspector node dist/index.js
674
+ npx @modelcontextprotocol/inspector node index.js
246
675
  ```
247
676
 
248
677
  ---
249
678
 
250
- ## Troubleshooting
679
+ ## Changelog
251
680
 
252
- | Issue | Solution |
253
- |-------|----------|
254
- | "Unexpected token '<'" errors | Check `functions.php` — ensure no characters before `<?php` |
255
- | SEO fields return empty | Add the `register_post_meta()` code from the SEO section above |
256
- | 401 Unauthorized | Verify Application Password (not your login password) |
257
- | 403 Forbidden | Check user has `edit_posts` capability |
258
- | Connection timeout | Verify `WP_API_URL` is correct and accessible |
259
- | Server not starting | Check Node.js 18+ is installed: `node --version` |
681
+ ### v2.2.0 (2026-02-19) Enterprise Edition
682
+ - **9 new tools**: plugins (list/activate/deactivate), themes (list/get), revisions (list/get/restore/delete)
683
+ - **New governance flag**: `WP_DISABLE_PLUGIN_MANAGEMENT`
684
+ - **171 Vitest unit tests** covering all 35 tools (governance, success, 403/404, audit logs)
685
+ - **GitHub Actions CI** workflow
686
+ - Governance functions read env at call time for testability
687
+ - Exported `handleToolCall` for direct testing
688
+
689
+ ### v2.1.0 (2026-02-16)
690
+ - Enterprise governance controls (read-only, draft-only, type/status allowlists)
691
+ - Structured JSON audit trail (27 instrumentation points)
692
+ - Multi-target site management
693
+ - 27 MCP tools including pages CRUD, media upload, taxonomy creation, custom post types
694
+ - SEO auto-detection for 4 plugins (Yoast, RankMath, SEOPress, AIOSEO)
695
+ - Health checks, retry with backoff, rate limiting
696
+
697
+ ### v1.0.0 (2025-10-17)
698
+ - Initial release — JavaScript, 5 tools (list, get, create, update, search posts)
260
699
 
261
700
  ---
262
701
 
263
- ## Author
702
+ ## Roadmap
703
+
704
+ ### v2.3 — Governance Workflows
705
+ - Approval workflow: draft → human review → publish
706
+ - Confirmation step for destructive actions
707
+ - Per-target enterprise controls
708
+
709
+ ### v2.4 — Extended Integrations
710
+ - OAuth 2.0 / JWT authentication
711
+ - WooCommerce support (products, orders)
712
+ - Media management (bulk, transforms)
264
713
 
265
- **Georges Cordewiener**[AdSim SRL](https://adsim.be)
266
- CTO & Co-founder, digital marketing agency specializing in SEO/SEA for the financial sector.
714
+ ### v2.5Distribution
715
+ - npm: `npx @adsim/wordpress-mcp-server`
716
+ - MCP Registry submission
717
+ - TypeScript rewrite
267
718
 
268
719
  ---
269
720
 
721
+ ## Contributing
722
+
723
+ Contributions welcome. Open an issue or submit a pull request.
724
+
270
725
  ## License
271
726
 
272
- MIT © 2025 AdSim SRL
727
+ MIT see [LICENSE](LICENSE).
728
+
729
+ ## Credits
730
+
731
+ Built by [AdSim](https://adsim.be) — Digital Marketing & AI Agency, Liège, Belgium.
732
+
733
+ Building the governance layer for Claude-powered WordPress infrastructure in regulated environments.