@mangerik/wordpress-mcp 0.1.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/.env.example +32 -0
- package/CHANGELOG.md +23 -0
- package/LICENSE +21 -0
- package/README.md +256 -0
- package/dist/config.d.ts +80 -0
- package/dist/config.js +84 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +145 -0
- package/dist/prompts.d.ts +7 -0
- package/dist/prompts.js +104 -0
- package/dist/resources.d.ts +13 -0
- package/dist/resources.js +64 -0
- package/dist/tools/batch.d.ts +14 -0
- package/dist/tools/batch.js +49 -0
- package/dist/tools/blocks.d.ts +4 -0
- package/dist/tools/blocks.js +202 -0
- package/dist/tools/comments.d.ts +4 -0
- package/dist/tools/comments.js +80 -0
- package/dist/tools/cpt.d.ts +16 -0
- package/dist/tools/cpt.js +97 -0
- package/dist/tools/jwt.d.ts +9 -0
- package/dist/tools/jwt.js +17 -0
- package/dist/tools/media.d.ts +4 -0
- package/dist/tools/media.js +101 -0
- package/dist/tools/multisite.d.ts +17 -0
- package/dist/tools/multisite.js +111 -0
- package/dist/tools/pages.d.ts +4 -0
- package/dist/tools/pages.js +101 -0
- package/dist/tools/posts.d.ts +4 -0
- package/dist/tools/posts.js +160 -0
- package/dist/tools/seo.d.ts +4 -0
- package/dist/tools/seo.js +269 -0
- package/dist/tools/site.d.ts +4 -0
- package/dist/tools/site.js +96 -0
- package/dist/tools/taxonomy.d.ts +4 -0
- package/dist/tools/taxonomy.js +147 -0
- package/dist/tools/users.d.ts +4 -0
- package/dist/tools/users.js +99 -0
- package/dist/tools/woocommerce.d.ts +4 -0
- package/dist/tools/woocommerce.js +400 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.js +2 -0
- package/dist/wordpress-client.d.ts +223 -0
- package/dist/wordpress-client.js +519 -0
- package/package.json +67 -0
package/.env.example
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# ── Required: where to connect ───────────────────────────────────────────
|
|
2
|
+
WP_URL=https://example.com
|
|
3
|
+
|
|
4
|
+
# ── Auth mode: 'application_password' (default) or 'jwt' ─────────────────
|
|
5
|
+
WP_AUTH_MODE=application_password
|
|
6
|
+
|
|
7
|
+
# Application Password mode (recommended; built into WP core)
|
|
8
|
+
WP_USERNAME=your-wp-username
|
|
9
|
+
# Application Password from: WP Admin → Users → Profile → Application Passwords
|
|
10
|
+
# Spaces are fine and should be preserved.
|
|
11
|
+
WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx
|
|
12
|
+
|
|
13
|
+
# JWT mode (requires the "JWT Authentication for WP REST API" plugin or compatible)
|
|
14
|
+
# Either supply WP_USERNAME + WP_PASSWORD (server fetches a token at startup),
|
|
15
|
+
# or supply WP_JWT_TOKEN directly.
|
|
16
|
+
# WP_PASSWORD=
|
|
17
|
+
# WP_JWT_TOKEN=
|
|
18
|
+
# WP_JWT_NAMESPACE=jwt-auth/v1
|
|
19
|
+
|
|
20
|
+
# ── Optional: tuning ─────────────────────────────────────────────────────
|
|
21
|
+
WP_TIMEOUT_MS=30000
|
|
22
|
+
WP_MAX_RETRIES=3
|
|
23
|
+
# Set to "false" only for local dev with self-signed certificates
|
|
24
|
+
WP_VERIFY_SSL=true
|
|
25
|
+
WP_USER_AGENT=WordPress-MCP-Server/1.0
|
|
26
|
+
|
|
27
|
+
# ── Optional: WooCommerce dedicated credentials ──────────────────────────
|
|
28
|
+
# Generate at: WC Admin → Settings → Advanced → REST API → Add key.
|
|
29
|
+
# When set, /wc/* and /wc-analytics/* requests use these instead of the
|
|
30
|
+
# Application Password (the WP user no longer needs `manage_woocommerce`).
|
|
31
|
+
WC_CONSUMER_KEY=
|
|
32
|
+
WC_CONSUMER_SECRET=
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-05-17
|
|
9
|
+
|
|
10
|
+
Initial public preview. Tagged `beta` on npm while we collect feedback from
|
|
11
|
+
real WordPress installations. API surface may evolve before 1.0.0.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- Initial release.
|
|
15
|
+
- Application Password and JWT auth modes.
|
|
16
|
+
- 95+ MCP tools spanning posts, pages, media, comments, taxonomies, users,
|
|
17
|
+
settings, search, custom post types, WooCommerce, Yoast / Rank Math SEO,
|
|
18
|
+
block themes (templates, parts, patterns, global styles, menus), multisite,
|
|
19
|
+
the WP 5.6+ batch endpoint, and JWT diagnostics.
|
|
20
|
+
- Resources: `wp://site`, `wp://post/{id}`, `wp://page/{id}`, `wp://media/{id}`.
|
|
21
|
+
- Prompts: `summarize_post`, `seo_rewrite`, `translate_page`, `draft_post`.
|
|
22
|
+
- Auto-retry on 408/425/429/5xx with `Retry-After` honored.
|
|
23
|
+
- WooCommerce consumer key/secret auth path for `/wc/*` routes.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 mangerik
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# WordPress MCP Server
|
|
2
|
+
|
|
3
|
+
A [Model Context Protocol](https://modelcontextprotocol.io) server that lets any MCP-compatible AI client (Claude Desktop, Kiro, Cursor, Continue, etc.) read and manage a WordPress site through the official REST API.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **30+ tools** covering posts, pages, media, comments, categories, tags, users, settings, search, and revisions.
|
|
8
|
+
- **Generic CPT tools** (`wp_list_items`, `wp_get_item`, `wp_create_item`, `wp_update_item`, `wp_delete_item`) for any custom post type, custom taxonomy, or third-party namespace (WooCommerce, ACF, Yoast, etc.).
|
|
9
|
+
- **Resources** so the LLM can attach a post/page as context: `wp://post/{id}`, `wp://page/{id}`, `wp://media/{id}`, `wp://site`.
|
|
10
|
+
- **Prompts** for common workflows: summarize, SEO rewrite, translate, draft.
|
|
11
|
+
- **Application Password** auth (recommended by WordPress core).
|
|
12
|
+
- **Auto-retry** on 429/5xx with `Retry-After` honored.
|
|
13
|
+
- **Token-saving knobs**: `_fields`, `_embed`, `context=view|edit`.
|
|
14
|
+
- **Safety**: destructive tool annotations, drafts default to `status=draft`.
|
|
15
|
+
|
|
16
|
+
## Requirements
|
|
17
|
+
|
|
18
|
+
- Node.js ≥ 18
|
|
19
|
+
- A WordPress site with the REST API enabled (default since WP 4.7).
|
|
20
|
+
- A user with an Application Password (WP Admin → Users → Profile → Application Passwords).
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
### From npm (when published)
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g @mangerik/wordpress-mcp
|
|
28
|
+
# the `wordpress-mcp` binary is now on your PATH
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or run on demand without installing:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx -y @mangerik/wordpress-mcp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### From source
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git clone https://github.com/mangerik/wordpress-mcp.git
|
|
41
|
+
cd wordpress-mcp
|
|
42
|
+
npm install
|
|
43
|
+
npm run build
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Configure
|
|
47
|
+
|
|
48
|
+
Two auth modes are supported.
|
|
49
|
+
|
|
50
|
+
### A. Application Password (default, recommended)
|
|
51
|
+
|
|
52
|
+
Copy `.env.example` to `.env` and fill in:
|
|
53
|
+
|
|
54
|
+
```env
|
|
55
|
+
WP_URL=https://example.com
|
|
56
|
+
WP_USERNAME=your-wp-username
|
|
57
|
+
WP_APP_PASSWORD=xxxx xxxx xxxx xxxx xxxx xxxx
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### B. JWT (when Application Passwords are blocked or you need short-lived tokens)
|
|
61
|
+
|
|
62
|
+
Install the "JWT Authentication for WP REST API" plugin (Tmeister) or compatible. Add to `wp-config.php`:
|
|
63
|
+
|
|
64
|
+
```php
|
|
65
|
+
define('JWT_AUTH_SECRET_KEY', 'a long random string');
|
|
66
|
+
define('JWT_AUTH_CORS_ENABLE', true);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Then in `.env`:
|
|
70
|
+
|
|
71
|
+
```env
|
|
72
|
+
WP_AUTH_MODE=jwt
|
|
73
|
+
WP_URL=https://example.com
|
|
74
|
+
WP_USERNAME=your-wp-username
|
|
75
|
+
WP_PASSWORD=your-wp-password
|
|
76
|
+
# Optional override if your plugin uses a different namespace:
|
|
77
|
+
# WP_JWT_NAMESPACE=jwt-auth/v1
|
|
78
|
+
|
|
79
|
+
# Or skip credentials and supply a pre-issued token:
|
|
80
|
+
# WP_JWT_TOKEN=eyJhbGciOiJIUzI1NiIs...
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
The server fetches a token at startup, attaches it as `Authorization: Bearer …`, and you can validate it any time via the `wp_jwt_validate` tool.
|
|
84
|
+
|
|
85
|
+
Optional:
|
|
86
|
+
|
|
87
|
+
| Variable | Default | Purpose |
|
|
88
|
+
|----------|---------|---------|
|
|
89
|
+
| `WP_TIMEOUT_MS` | `30000` | Per-request timeout |
|
|
90
|
+
| `WP_MAX_RETRIES` | `3` | Retries on 408/425/429/5xx |
|
|
91
|
+
| `WP_VERIFY_SSL` | `true` | Set `false` only for local self-signed certs |
|
|
92
|
+
| `WP_USER_AGENT` | `WordPress-MCP-Server/1.0` | UA header |
|
|
93
|
+
|
|
94
|
+
## Use it from an MCP client
|
|
95
|
+
|
|
96
|
+
### Claude Desktop / Kiro
|
|
97
|
+
|
|
98
|
+
Add to your MCP config (`~/.kiro/settings/mcp.json` or `~/Library/Application Support/Claude/claude_desktop_config.json`):
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"mcpServers": {
|
|
103
|
+
"wordpress": {
|
|
104
|
+
"command": "npx",
|
|
105
|
+
"args": ["-y", "@mangerik/wordpress-mcp"],
|
|
106
|
+
"env": {
|
|
107
|
+
"WP_URL": "https://example.com",
|
|
108
|
+
"WP_USERNAME": "your-wp-username",
|
|
109
|
+
"WP_APP_PASSWORD": "xxxx xxxx xxxx xxxx xxxx xxxx"
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
For local development (running from source):
|
|
117
|
+
|
|
118
|
+
```json
|
|
119
|
+
{
|
|
120
|
+
"command": "node",
|
|
121
|
+
"args": ["/absolute/path/to/wordpress-mcp/dist/index.js"],
|
|
122
|
+
"env": { "WP_URL": "...", "WP_USERNAME": "...", "WP_APP_PASSWORD": "..." }
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Test from the terminal
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
WP_URL=https://example.com \
|
|
130
|
+
WP_USERNAME=admin \
|
|
131
|
+
WP_APP_PASSWORD="xxxx xxxx xxxx xxxx xxxx xxxx" \
|
|
132
|
+
npm run dev
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The server speaks JSON-RPC over stdio. Use the [MCP Inspector](https://github.com/modelcontextprotocol/inspector) to drive it interactively:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npx @modelcontextprotocol/inspector node dist/index.js
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Tool catalogue
|
|
142
|
+
|
|
143
|
+
### Site / discovery
|
|
144
|
+
- `wp_site_info` · `wp_get_settings` · `wp_update_settings`
|
|
145
|
+
- `wp_get_post_types` · `wp_get_taxonomies` · `wp_search`
|
|
146
|
+
|
|
147
|
+
### Posts
|
|
148
|
+
- `wp_get_posts` · `wp_get_post` · `wp_create_post` · `wp_update_post` · `wp_delete_post` · `wp_get_post_revisions`
|
|
149
|
+
|
|
150
|
+
### Pages
|
|
151
|
+
- `wp_get_pages` · `wp_get_page` · `wp_create_page` · `wp_update_page` · `wp_delete_page`
|
|
152
|
+
|
|
153
|
+
### Media
|
|
154
|
+
- `wp_get_media` · `wp_get_media_item` · `wp_upload_media_file` · `wp_upload_media_url` · `wp_update_media` · `wp_delete_media`
|
|
155
|
+
|
|
156
|
+
### Comments
|
|
157
|
+
- `wp_get_comments` · `wp_get_comment` · `wp_create_comment` · `wp_update_comment` · `wp_delete_comment`
|
|
158
|
+
|
|
159
|
+
### Taxonomies
|
|
160
|
+
- `wp_get_categories` · `wp_get_category` · `wp_create_category` · `wp_update_category` · `wp_delete_category`
|
|
161
|
+
- `wp_get_tags` · `wp_get_tag` · `wp_create_tag` · `wp_update_tag` · `wp_delete_tag`
|
|
162
|
+
|
|
163
|
+
### Users
|
|
164
|
+
- `wp_get_users` · `wp_get_user` · `wp_get_current_user` · `wp_create_user` · `wp_update_user` · `wp_delete_user`
|
|
165
|
+
|
|
166
|
+
### Generic (CPT, custom taxonomies, plugin namespaces)
|
|
167
|
+
- `wp_list_items` · `wp_get_item` · `wp_create_item` · `wp_update_item` · `wp_delete_item`
|
|
168
|
+
|
|
169
|
+
Use these with any REST route, e.g. `wc/v3/products`, `wp/v2/movie`, `acf/v3/posts/123`.
|
|
170
|
+
|
|
171
|
+
### WooCommerce (`wc/v3`)
|
|
172
|
+
Auth: set `WC_CONSUMER_KEY` + `WC_CONSUMER_SECRET` (recommended) or rely on the WP user having `manage_woocommerce`.
|
|
173
|
+
|
|
174
|
+
- Products: `wc_list_products` · `wc_get_product` · `wc_create_product` · `wc_update_product` · `wc_delete_product`
|
|
175
|
+
- Variations: `wc_list_variations` · `wc_create_variation`
|
|
176
|
+
- Categories: `wc_list_product_categories`
|
|
177
|
+
- Orders: `wc_list_orders` · `wc_get_order` · `wc_update_order` · `wc_create_order_note` · `wc_create_refund`
|
|
178
|
+
- Customers: `wc_list_customers` · `wc_get_customer`
|
|
179
|
+
- Coupons: `wc_list_coupons` · `wc_create_coupon`
|
|
180
|
+
- Reports: `wc_get_sales_report` · `wc_get_top_sellers`
|
|
181
|
+
|
|
182
|
+
### SEO (Yoast & Rank Math)
|
|
183
|
+
Both plugins store SEO data in post meta with different keys; one uniform tool reads/writes either.
|
|
184
|
+
|
|
185
|
+
- `seo_get_meta` — read SEO meta (`plugin: 'yoast' | 'rank_math'`)
|
|
186
|
+
- `seo_set_meta` — write SEO meta
|
|
187
|
+
- `yoast_get_head` — render Yoast `<head>` for a URL (SERP preview)
|
|
188
|
+
- `yoast_indexable_for_post` — Yoast computed indexable record
|
|
189
|
+
- `rankmath_list_redirections` · `rankmath_create_redirection` (Rank Math Pro)
|
|
190
|
+
|
|
191
|
+
> Yoast exposes its meta via REST automatically. **Rank Math** may require enabling "REST API" in the plugin settings, and meta keys must be marked `show_in_rest`.
|
|
192
|
+
|
|
193
|
+
### Block themes (WP 5.9+)
|
|
194
|
+
- Templates: `wp_list_templates` · `wp_get_template` · `wp_update_template`
|
|
195
|
+
- Template parts: `wp_list_template_parts` · `wp_get_template_part` · `wp_update_template_part`
|
|
196
|
+
- Patterns: `wp_list_block_patterns` · `wp_list_block_pattern_categories`
|
|
197
|
+
- Block types: `wp_list_block_types`
|
|
198
|
+
- Global styles: `wp_get_global_styles`
|
|
199
|
+
- Menus: `wp_list_menus` · `wp_list_menu_items`
|
|
200
|
+
|
|
201
|
+
> Template IDs are strings shaped like `theme//slug`, e.g. `twentytwentyfour//single`.
|
|
202
|
+
|
|
203
|
+
### Multisite
|
|
204
|
+
- `ms_list_sites` · `ms_get_site` · `ms_create_site` · `ms_update_site` · `ms_delete_site`
|
|
205
|
+
- `ms_list_networks` · `ms_get_network`
|
|
206
|
+
|
|
207
|
+
> Core WP does **not yet** ship `/wp/v2/sites`. These tools assume a Multisite REST plugin is installed (e.g. `brettkrueger/multisite-rest-api`). Without one you will get `rest_no_route`.
|
|
208
|
+
|
|
209
|
+
### Batch operations (WP 5.6+, `/batch/v1`)
|
|
210
|
+
- `wp_batch_options` — discover the per-batch limit (default 25, filterable).
|
|
211
|
+
- `wp_batch` — execute up to N writes in one request. GET is **not** supported by core; only `POST/PUT/PATCH/DELETE`.
|
|
212
|
+
|
|
213
|
+
```jsonc
|
|
214
|
+
// Example call to wp_batch
|
|
215
|
+
{
|
|
216
|
+
"validation": "require-all-validate",
|
|
217
|
+
"requests": [
|
|
218
|
+
{ "method": "POST", "path": "/wp/v2/posts", "body": { "title": "First", "content": "...", "status": "draft" } },
|
|
219
|
+
{ "method": "POST", "path": "/wp/v2/posts", "body": { "title": "Second", "content": "...", "status": "draft" } },
|
|
220
|
+
{ "method": "POST", "path": "/wp/v2/posts/123", "body": { "status": "publish" } }
|
|
221
|
+
]
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### JWT diagnostics (only in JWT mode)
|
|
226
|
+
- `wp_jwt_validate` — validate the active token via `/jwt-auth/v1/token/validate`.
|
|
227
|
+
|
|
228
|
+
## Resources
|
|
229
|
+
|
|
230
|
+
| URI | What |
|
|
231
|
+
|-----|------|
|
|
232
|
+
| `wp://site` | REST root summary (name, namespaces, etc.) |
|
|
233
|
+
| `wp://post/{id}` | Single post (rendered + embedded relations) |
|
|
234
|
+
| `wp://page/{id}` | Single page |
|
|
235
|
+
| `wp://media/{id}` | Single media item |
|
|
236
|
+
|
|
237
|
+
## Prompts
|
|
238
|
+
|
|
239
|
+
| Name | Args |
|
|
240
|
+
|------|------|
|
|
241
|
+
| `summarize_post` | `post_id`, `length` |
|
|
242
|
+
| `seo_rewrite` | `post_id`, `primary_keyword`, `target_audience?` |
|
|
243
|
+
| `translate_page` | `page_id`, `target_language`, `create_new?` |
|
|
244
|
+
| `draft_post` | `topic`, `tone?`, `word_count?` |
|
|
245
|
+
|
|
246
|
+
## Notes & gotchas
|
|
247
|
+
|
|
248
|
+
- **`status` defaults to `draft`** for `wp_create_post` / `wp_create_page`. Override explicitly if you really want to publish.
|
|
249
|
+
- **Custom fields (`meta`)** must be registered server-side with `register_post_meta(..., 'show_in_rest' => true)` to be writable through REST.
|
|
250
|
+
- **Use `_fields`** to slim responses — e.g. `_fields: "id,title,slug"` cuts response size by ~80% for list calls.
|
|
251
|
+
- **`context=edit`** is required to receive raw (unfiltered) content for round-tripping through `wp_update_post`.
|
|
252
|
+
- The server logs to **stderr only**. Stdout is reserved for JSON-RPC framing.
|
|
253
|
+
|
|
254
|
+
## License
|
|
255
|
+
|
|
256
|
+
MIT
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Server configuration loaded from environment variables.
|
|
4
|
+
* Validated with zod so we fail fast at startup.
|
|
5
|
+
*/
|
|
6
|
+
declare const ConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
7
|
+
WP_URL: z.ZodString;
|
|
8
|
+
WP_AUTH_MODE: z.ZodDefault<z.ZodEnum<["application_password", "jwt"]>>;
|
|
9
|
+
WP_USERNAME: z.ZodOptional<z.ZodString>;
|
|
10
|
+
WP_APP_PASSWORD: z.ZodOptional<z.ZodString>;
|
|
11
|
+
WP_PASSWORD: z.ZodOptional<z.ZodString>;
|
|
12
|
+
WP_JWT_TOKEN: z.ZodOptional<z.ZodString>;
|
|
13
|
+
WP_JWT_NAMESPACE: z.ZodDefault<z.ZodString>;
|
|
14
|
+
WP_TIMEOUT_MS: z.ZodDefault<z.ZodNumber>;
|
|
15
|
+
WP_MAX_RETRIES: z.ZodDefault<z.ZodNumber>;
|
|
16
|
+
WP_VERIFY_SSL: z.ZodEffects<z.ZodDefault<z.ZodUnion<[z.ZodLiteral<"true">, z.ZodLiteral<"false">]>>, boolean, "true" | "false" | undefined>;
|
|
17
|
+
WP_USER_AGENT: z.ZodDefault<z.ZodString>;
|
|
18
|
+
WC_CONSUMER_KEY: z.ZodOptional<z.ZodString>;
|
|
19
|
+
WC_CONSUMER_SECRET: z.ZodOptional<z.ZodString>;
|
|
20
|
+
}, "strip", z.ZodTypeAny, {
|
|
21
|
+
WP_URL: string;
|
|
22
|
+
WP_AUTH_MODE: "application_password" | "jwt";
|
|
23
|
+
WP_JWT_NAMESPACE: string;
|
|
24
|
+
WP_TIMEOUT_MS: number;
|
|
25
|
+
WP_MAX_RETRIES: number;
|
|
26
|
+
WP_VERIFY_SSL: boolean;
|
|
27
|
+
WP_USER_AGENT: string;
|
|
28
|
+
WP_USERNAME?: string | undefined;
|
|
29
|
+
WP_APP_PASSWORD?: string | undefined;
|
|
30
|
+
WP_PASSWORD?: string | undefined;
|
|
31
|
+
WP_JWT_TOKEN?: string | undefined;
|
|
32
|
+
WC_CONSUMER_KEY?: string | undefined;
|
|
33
|
+
WC_CONSUMER_SECRET?: string | undefined;
|
|
34
|
+
}, {
|
|
35
|
+
WP_URL: string;
|
|
36
|
+
WP_AUTH_MODE?: "application_password" | "jwt" | undefined;
|
|
37
|
+
WP_USERNAME?: string | undefined;
|
|
38
|
+
WP_APP_PASSWORD?: string | undefined;
|
|
39
|
+
WP_PASSWORD?: string | undefined;
|
|
40
|
+
WP_JWT_TOKEN?: string | undefined;
|
|
41
|
+
WP_JWT_NAMESPACE?: string | undefined;
|
|
42
|
+
WP_TIMEOUT_MS?: number | undefined;
|
|
43
|
+
WP_MAX_RETRIES?: number | undefined;
|
|
44
|
+
WP_VERIFY_SSL?: "true" | "false" | undefined;
|
|
45
|
+
WP_USER_AGENT?: string | undefined;
|
|
46
|
+
WC_CONSUMER_KEY?: string | undefined;
|
|
47
|
+
WC_CONSUMER_SECRET?: string | undefined;
|
|
48
|
+
}>, {
|
|
49
|
+
WP_URL: string;
|
|
50
|
+
WP_AUTH_MODE: "application_password" | "jwt";
|
|
51
|
+
WP_JWT_NAMESPACE: string;
|
|
52
|
+
WP_TIMEOUT_MS: number;
|
|
53
|
+
WP_MAX_RETRIES: number;
|
|
54
|
+
WP_VERIFY_SSL: boolean;
|
|
55
|
+
WP_USER_AGENT: string;
|
|
56
|
+
WP_USERNAME?: string | undefined;
|
|
57
|
+
WP_APP_PASSWORD?: string | undefined;
|
|
58
|
+
WP_PASSWORD?: string | undefined;
|
|
59
|
+
WP_JWT_TOKEN?: string | undefined;
|
|
60
|
+
WC_CONSUMER_KEY?: string | undefined;
|
|
61
|
+
WC_CONSUMER_SECRET?: string | undefined;
|
|
62
|
+
}, {
|
|
63
|
+
WP_URL: string;
|
|
64
|
+
WP_AUTH_MODE?: "application_password" | "jwt" | undefined;
|
|
65
|
+
WP_USERNAME?: string | undefined;
|
|
66
|
+
WP_APP_PASSWORD?: string | undefined;
|
|
67
|
+
WP_PASSWORD?: string | undefined;
|
|
68
|
+
WP_JWT_TOKEN?: string | undefined;
|
|
69
|
+
WP_JWT_NAMESPACE?: string | undefined;
|
|
70
|
+
WP_TIMEOUT_MS?: number | undefined;
|
|
71
|
+
WP_MAX_RETRIES?: number | undefined;
|
|
72
|
+
WP_VERIFY_SSL?: "true" | "false" | undefined;
|
|
73
|
+
WP_USER_AGENT?: string | undefined;
|
|
74
|
+
WC_CONSUMER_KEY?: string | undefined;
|
|
75
|
+
WC_CONSUMER_SECRET?: string | undefined;
|
|
76
|
+
}>;
|
|
77
|
+
export type ServerConfig = z.infer<typeof ConfigSchema>;
|
|
78
|
+
export declare function loadConfig(env?: NodeJS.ProcessEnv): ServerConfig;
|
|
79
|
+
export {};
|
|
80
|
+
//# sourceMappingURL=config.d.ts.map
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/**
|
|
3
|
+
* Server configuration loaded from environment variables.
|
|
4
|
+
* Validated with zod so we fail fast at startup.
|
|
5
|
+
*/
|
|
6
|
+
const ConfigSchema = z
|
|
7
|
+
.object({
|
|
8
|
+
WP_URL: z
|
|
9
|
+
.string()
|
|
10
|
+
.url()
|
|
11
|
+
.describe("Base URL of the WordPress site, e.g. https://example.com"),
|
|
12
|
+
WP_AUTH_MODE: z
|
|
13
|
+
.enum(["application_password", "jwt"])
|
|
14
|
+
.default("application_password"),
|
|
15
|
+
// ── Application Password mode ───────────────────────────────────────
|
|
16
|
+
WP_USERNAME: z.string().min(1).optional(),
|
|
17
|
+
WP_APP_PASSWORD: z.string().min(1).optional(),
|
|
18
|
+
// ── JWT mode (Tmeister "JWT Authentication for WP REST API") ────────
|
|
19
|
+
// Either supply username + password and we'll fetch a token at startup,
|
|
20
|
+
// or supply WP_JWT_TOKEN directly (e.g. issued by another auth plugin).
|
|
21
|
+
WP_PASSWORD: z.string().min(1).optional(),
|
|
22
|
+
WP_JWT_TOKEN: z.string().min(10).optional(),
|
|
23
|
+
WP_JWT_NAMESPACE: z.string().default("jwt-auth/v1"),
|
|
24
|
+
// ── Common ──────────────────────────────────────────────────────────
|
|
25
|
+
WP_TIMEOUT_MS: z.coerce.number().int().positive().default(30_000),
|
|
26
|
+
WP_MAX_RETRIES: z.coerce.number().int().min(0).max(10).default(3),
|
|
27
|
+
WP_VERIFY_SSL: z
|
|
28
|
+
.union([z.literal("true"), z.literal("false")])
|
|
29
|
+
.default("true")
|
|
30
|
+
.transform((v) => v === "true"),
|
|
31
|
+
WP_USER_AGENT: z.string().default("WordPress-MCP-Server/1.0"),
|
|
32
|
+
// ── WooCommerce (optional) ──────────────────────────────────────────
|
|
33
|
+
WC_CONSUMER_KEY: z.string().optional(),
|
|
34
|
+
WC_CONSUMER_SECRET: z.string().optional(),
|
|
35
|
+
})
|
|
36
|
+
.superRefine((c, ctx) => {
|
|
37
|
+
if (c.WP_AUTH_MODE === "application_password") {
|
|
38
|
+
if (!c.WP_USERNAME)
|
|
39
|
+
ctx.addIssue({ code: "custom", path: ["WP_USERNAME"], message: "Required for application_password mode" });
|
|
40
|
+
if (!c.WP_APP_PASSWORD)
|
|
41
|
+
ctx.addIssue({ code: "custom", path: ["WP_APP_PASSWORD"], message: "Required for application_password mode" });
|
|
42
|
+
}
|
|
43
|
+
if (c.WP_AUTH_MODE === "jwt") {
|
|
44
|
+
const hasToken = !!c.WP_JWT_TOKEN;
|
|
45
|
+
const hasUserPass = !!c.WP_USERNAME && !!c.WP_PASSWORD;
|
|
46
|
+
if (!hasToken && !hasUserPass) {
|
|
47
|
+
ctx.addIssue({
|
|
48
|
+
code: "custom",
|
|
49
|
+
path: ["WP_JWT_TOKEN"],
|
|
50
|
+
message: "JWT mode requires either WP_JWT_TOKEN, or WP_USERNAME + WP_PASSWORD to obtain a token",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
export function loadConfig(env = process.env) {
|
|
56
|
+
const parsed = ConfigSchema.safeParse(env);
|
|
57
|
+
if (!parsed.success) {
|
|
58
|
+
const issues = parsed.error.issues
|
|
59
|
+
.map((i) => ` • ${i.path.join(".")}: ${i.message}`)
|
|
60
|
+
.join("\n");
|
|
61
|
+
throw new Error(`Invalid configuration. Check your environment variables:\n${issues}\n\n` +
|
|
62
|
+
`Required (application_password mode, default):\n` +
|
|
63
|
+
` WP_URL (e.g. https://example.com)\n` +
|
|
64
|
+
` WP_USERNAME (your WordPress username)\n` +
|
|
65
|
+
` WP_APP_PASSWORD (Application Password from WP profile)\n\n` +
|
|
66
|
+
`Required (jwt mode):\n` +
|
|
67
|
+
` WP_AUTH_MODE=jwt\n` +
|
|
68
|
+
` WP_URL\n` +
|
|
69
|
+
` Either:\n` +
|
|
70
|
+
` WP_USERNAME + WP_PASSWORD (server fetches token)\n` +
|
|
71
|
+
` or:\n` +
|
|
72
|
+
` WP_JWT_TOKEN (pre-issued token)\n` +
|
|
73
|
+
` WP_JWT_NAMESPACE (default jwt-auth/v1; change if your plugin differs)\n\n` +
|
|
74
|
+
`Optional:\n` +
|
|
75
|
+
` WP_TIMEOUT_MS (default 30000)\n` +
|
|
76
|
+
` WP_MAX_RETRIES (default 3)\n` +
|
|
77
|
+
` WP_VERIFY_SSL (default true)\n` +
|
|
78
|
+
` WP_USER_AGENT (default WordPress-MCP-Server/1.0)\n` +
|
|
79
|
+
` WC_CONSUMER_KEY (optional, WooCommerce consumer key)\n` +
|
|
80
|
+
` WC_CONSUMER_SECRET(optional, WooCommerce consumer secret)`);
|
|
81
|
+
}
|
|
82
|
+
return parsed.data;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=config.js.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
+
import { loadConfig } from "./config.js";
|
|
5
|
+
import { WordPressClient, WPError } from "./wordpress-client.js";
|
|
6
|
+
import { postTools } from "./tools/posts.js";
|
|
7
|
+
import { pageTools } from "./tools/pages.js";
|
|
8
|
+
import { mediaTools } from "./tools/media.js";
|
|
9
|
+
import { commentTools } from "./tools/comments.js";
|
|
10
|
+
import { taxonomyTools } from "./tools/taxonomy.js";
|
|
11
|
+
import { userTools } from "./tools/users.js";
|
|
12
|
+
import { siteTools } from "./tools/site.js";
|
|
13
|
+
import { cptTools } from "./tools/cpt.js";
|
|
14
|
+
import { woocommerceTools } from "./tools/woocommerce.js";
|
|
15
|
+
import { seoTools } from "./tools/seo.js";
|
|
16
|
+
import { blockTools } from "./tools/blocks.js";
|
|
17
|
+
import { multisiteTools } from "./tools/multisite.js";
|
|
18
|
+
import { batchTools } from "./tools/batch.js";
|
|
19
|
+
import { jwtTools } from "./tools/jwt.js";
|
|
20
|
+
import { registerResources } from "./resources.js";
|
|
21
|
+
import { registerPrompts } from "./prompts.js";
|
|
22
|
+
const NAME = "wordpress-mcp";
|
|
23
|
+
const VERSION = "1.0.0";
|
|
24
|
+
/**
|
|
25
|
+
* Wrap a tool handler so its return value becomes a proper `CallToolResult`
|
|
26
|
+
* and any thrown error becomes an `isError: true` result the LLM can see.
|
|
27
|
+
*
|
|
28
|
+
* Important for stdio: never write to stdout outside the MCP framing — all
|
|
29
|
+
* logs go to stderr.
|
|
30
|
+
*/
|
|
31
|
+
function wrap(tool) {
|
|
32
|
+
return async (input) => {
|
|
33
|
+
try {
|
|
34
|
+
const data = await tool.handler(input);
|
|
35
|
+
const text = typeof data === "string" ? data : JSON.stringify(data, null, 2);
|
|
36
|
+
const result = {
|
|
37
|
+
content: [{ type: "text", text }],
|
|
38
|
+
};
|
|
39
|
+
// Attach structured output when it's a JSON-compatible object.
|
|
40
|
+
if (data && typeof data === "object") {
|
|
41
|
+
result.structuredContent = data;
|
|
42
|
+
}
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const message = formatError(err);
|
|
47
|
+
// Log full error to stderr for debugging without breaking stdio framing.
|
|
48
|
+
console.error(`[${tool.name}]`, err);
|
|
49
|
+
return {
|
|
50
|
+
isError: true,
|
|
51
|
+
content: [{ type: "text", text: message }],
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function formatError(err) {
|
|
57
|
+
if (err instanceof WPError) {
|
|
58
|
+
const parts = [
|
|
59
|
+
`WordPress error: ${err.message}`,
|
|
60
|
+
`code: ${err.code}`,
|
|
61
|
+
`status: ${err.status}`,
|
|
62
|
+
];
|
|
63
|
+
if (err.details && typeof err.details === "object") {
|
|
64
|
+
parts.push(`details: ${JSON.stringify(err.details)}`);
|
|
65
|
+
}
|
|
66
|
+
return parts.join(" | ");
|
|
67
|
+
}
|
|
68
|
+
if (err instanceof Error)
|
|
69
|
+
return `${err.name}: ${err.message}`;
|
|
70
|
+
return `Unknown error: ${String(err)}`;
|
|
71
|
+
}
|
|
72
|
+
async function main() {
|
|
73
|
+
const config = loadConfig();
|
|
74
|
+
const wp = new WordPressClient({
|
|
75
|
+
baseUrl: config.WP_URL,
|
|
76
|
+
authMode: config.WP_AUTH_MODE,
|
|
77
|
+
username: config.WP_USERNAME,
|
|
78
|
+
appPassword: config.WP_APP_PASSWORD,
|
|
79
|
+
password: config.WP_PASSWORD,
|
|
80
|
+
jwtToken: config.WP_JWT_TOKEN,
|
|
81
|
+
jwtNamespace: config.WP_JWT_NAMESPACE,
|
|
82
|
+
timeoutMs: config.WP_TIMEOUT_MS,
|
|
83
|
+
maxRetries: config.WP_MAX_RETRIES,
|
|
84
|
+
verifySsl: config.WP_VERIFY_SSL,
|
|
85
|
+
userAgent: config.WP_USER_AGENT,
|
|
86
|
+
wcConsumerKey: config.WC_CONSUMER_KEY,
|
|
87
|
+
wcConsumerSecret: config.WC_CONSUMER_SECRET,
|
|
88
|
+
});
|
|
89
|
+
// Bootstrap JWT if needed (no-op for application_password mode).
|
|
90
|
+
await wp.ensureJwt();
|
|
91
|
+
const server = new McpServer({ name: NAME, version: VERSION }, {
|
|
92
|
+
capabilities: {
|
|
93
|
+
tools: {},
|
|
94
|
+
resources: {},
|
|
95
|
+
prompts: {},
|
|
96
|
+
logging: {},
|
|
97
|
+
},
|
|
98
|
+
instructions: "WordPress MCP server. Authenticated via Application Password. " +
|
|
99
|
+
"Discover the site shape with wp_site_info / wp_get_post_types / wp_get_taxonomies " +
|
|
100
|
+
"before performing CRUD on custom content. Destructive tools (delete, force=true) " +
|
|
101
|
+
"are irreversible — confirm with the user first.",
|
|
102
|
+
});
|
|
103
|
+
// Collect all tools from feature modules.
|
|
104
|
+
const tools = [
|
|
105
|
+
...siteTools(wp),
|
|
106
|
+
...postTools(wp),
|
|
107
|
+
...pageTools(wp),
|
|
108
|
+
...mediaTools(wp),
|
|
109
|
+
...commentTools(wp),
|
|
110
|
+
...taxonomyTools(wp),
|
|
111
|
+
...userTools(wp),
|
|
112
|
+
...cptTools(wp),
|
|
113
|
+
...woocommerceTools(wp),
|
|
114
|
+
...seoTools(wp),
|
|
115
|
+
...blockTools(wp),
|
|
116
|
+
...multisiteTools(wp),
|
|
117
|
+
...batchTools(wp),
|
|
118
|
+
...(config.WP_AUTH_MODE === "jwt" ? jwtTools(wp) : []),
|
|
119
|
+
];
|
|
120
|
+
// Sanity check: tool names must be unique.
|
|
121
|
+
const seen = new Set();
|
|
122
|
+
for (const t of tools) {
|
|
123
|
+
if (seen.has(t.name))
|
|
124
|
+
throw new Error(`Duplicate tool name: ${t.name}`);
|
|
125
|
+
seen.add(t.name);
|
|
126
|
+
}
|
|
127
|
+
for (const tool of tools) {
|
|
128
|
+
server.registerTool(tool.name, {
|
|
129
|
+
title: tool.title,
|
|
130
|
+
description: tool.description,
|
|
131
|
+
inputSchema: tool.inputSchema,
|
|
132
|
+
annotations: tool.annotations,
|
|
133
|
+
}, wrap(tool));
|
|
134
|
+
}
|
|
135
|
+
registerResources(server, wp);
|
|
136
|
+
registerPrompts(server);
|
|
137
|
+
const transport = new StdioServerTransport();
|
|
138
|
+
await server.connect(transport);
|
|
139
|
+
console.error(`[${NAME}] connected · ${tools.length} tools · auth: ${config.WP_AUTH_MODE} · target: ${config.WP_URL}`);
|
|
140
|
+
}
|
|
141
|
+
main().catch((err) => {
|
|
142
|
+
console.error(`[${NAME}] fatal:`, err instanceof Error ? err.message : err);
|
|
143
|
+
process.exit(1);
|
|
144
|
+
});
|
|
145
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reusable prompt templates that turn common WordPress workflows into one-click
|
|
4
|
+
* actions for the LLM client.
|
|
5
|
+
*/
|
|
6
|
+
export declare function registerPrompts(server: McpServer): void;
|
|
7
|
+
//# sourceMappingURL=prompts.d.ts.map
|