claude-plugin-wordpress-manager 1.4.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/.claude-plugin/plugin.json +19 -0
- package/.mcp.json +19 -0
- package/CHANGELOG.md +62 -0
- package/LICENSE +69 -0
- package/README.md +213 -0
- package/agents/wp-content-strategist.md +148 -0
- package/agents/wp-deployment-engineer.md +93 -0
- package/agents/wp-performance-optimizer.md +198 -0
- package/agents/wp-security-auditor.md +161 -0
- package/agents/wp-site-manager.md +109 -0
- package/commands/wp-audit.md +37 -0
- package/commands/wp-backup.md +45 -0
- package/commands/wp-deploy.md +38 -0
- package/commands/wp-setup.md +64 -0
- package/commands/wp-status.md +53 -0
- package/docs/GUIDE.md +1190 -0
- package/hooks/hooks.json +57 -0
- package/hooks/scripts/backup-reminder.sh +29 -0
- package/hooks/scripts/pre-deploy-check.sh +49 -0
- package/package.json +46 -0
- package/scripts/health-check.sh +110 -0
- package/scripts/validate-wp-operation.sh +115 -0
- package/servers/wp-rest-bridge/build/server.d.ts +2 -0
- package/servers/wp-rest-bridge/build/server.js +74 -0
- package/servers/wp-rest-bridge/build/tools/comments.d.ts +227 -0
- package/servers/wp-rest-bridge/build/tools/comments.js +192 -0
- package/servers/wp-rest-bridge/build/tools/index.d.ts +919 -0
- package/servers/wp-rest-bridge/build/tools/index.js +30 -0
- package/servers/wp-rest-bridge/build/tools/media.d.ts +174 -0
- package/servers/wp-rest-bridge/build/tools/media.js +247 -0
- package/servers/wp-rest-bridge/build/tools/plugin-repository.d.ts +62 -0
- package/servers/wp-rest-bridge/build/tools/plugin-repository.js +149 -0
- package/servers/wp-rest-bridge/build/tools/plugins.d.ts +153 -0
- package/servers/wp-rest-bridge/build/tools/plugins.js +175 -0
- package/servers/wp-rest-bridge/build/tools/search.d.ts +44 -0
- package/servers/wp-rest-bridge/build/tools/search.js +44 -0
- package/servers/wp-rest-bridge/build/tools/unified-content.d.ts +328 -0
- package/servers/wp-rest-bridge/build/tools/unified-content.js +628 -0
- package/servers/wp-rest-bridge/build/tools/unified-taxonomies.d.ts +244 -0
- package/servers/wp-rest-bridge/build/tools/unified-taxonomies.js +492 -0
- package/servers/wp-rest-bridge/build/tools/users.d.ts +269 -0
- package/servers/wp-rest-bridge/build/tools/users.js +226 -0
- package/servers/wp-rest-bridge/build/types.d.ts +151 -0
- package/servers/wp-rest-bridge/build/types.js +2 -0
- package/servers/wp-rest-bridge/build/wordpress.d.ts +48 -0
- package/servers/wp-rest-bridge/build/wordpress.js +305 -0
- package/servers/wp-rest-bridge/package.json +27 -0
- package/skills/wordpress-router/SKILL.md +78 -0
- package/skills/wordpress-router/references/decision-tree.md +88 -0
- package/skills/wp-abilities-api/SKILL.md +97 -0
- package/skills/wp-abilities-api/references/php-registration.md +67 -0
- package/skills/wp-abilities-api/references/rest-api.md +13 -0
- package/skills/wp-audit/SKILL.md +114 -0
- package/skills/wp-audit/references/performance-checklist.md +113 -0
- package/skills/wp-audit/references/security-checklist.md +95 -0
- package/skills/wp-audit/references/seo-checklist.md +128 -0
- package/skills/wp-backup/SKILL.md +87 -0
- package/skills/wp-backup/references/backup-strategies.md +116 -0
- package/skills/wp-backup/references/restore-procedures.md +129 -0
- package/skills/wp-block-development/SKILL.md +176 -0
- package/skills/wp-block-development/references/attributes-and-serialization.md +22 -0
- package/skills/wp-block-development/references/block-json.md +49 -0
- package/skills/wp-block-development/references/creating-new-blocks.md +46 -0
- package/skills/wp-block-development/references/debugging.md +36 -0
- package/skills/wp-block-development/references/deprecations.md +24 -0
- package/skills/wp-block-development/references/dynamic-rendering.md +23 -0
- package/skills/wp-block-development/references/inner-blocks.md +25 -0
- package/skills/wp-block-development/references/registration.md +30 -0
- package/skills/wp-block-development/references/supports-and-wrappers.md +18 -0
- package/skills/wp-block-development/references/tooling-and-testing.md +21 -0
- package/skills/wp-block-development/scripts/list_blocks.mjs +121 -0
- package/skills/wp-block-themes/SKILL.md +118 -0
- package/skills/wp-block-themes/references/creating-new-block-theme.md +37 -0
- package/skills/wp-block-themes/references/debugging.md +24 -0
- package/skills/wp-block-themes/references/patterns.md +18 -0
- package/skills/wp-block-themes/references/style-variations.md +14 -0
- package/skills/wp-block-themes/references/templates-and-parts.md +16 -0
- package/skills/wp-block-themes/references/theme-json.md +59 -0
- package/skills/wp-block-themes/scripts/detect_block_themes.mjs +117 -0
- package/skills/wp-content/SKILL.md +103 -0
- package/skills/wp-content/references/content-templates.md +230 -0
- package/skills/wp-content/references/seo-optimization.md +169 -0
- package/skills/wp-deploy/SKILL.md +52 -0
- package/skills/wp-deploy/references/hostinger-deploy.md +51 -0
- package/skills/wp-deploy/references/ssh-deploy.md +63 -0
- package/skills/wp-interactivity-api/SKILL.md +181 -0
- package/skills/wp-interactivity-api/references/debugging.md +29 -0
- package/skills/wp-interactivity-api/references/directives-quickref.md +30 -0
- package/skills/wp-interactivity-api/references/server-side-rendering.md +310 -0
- package/skills/wp-migrate/SKILL.md +100 -0
- package/skills/wp-migrate/references/cross-platform.md +104 -0
- package/skills/wp-migrate/references/hostinger-migration.md +86 -0
- package/skills/wp-performance/SKILL.md +148 -0
- package/skills/wp-performance/references/autoload-options.md +24 -0
- package/skills/wp-performance/references/cron.md +20 -0
- package/skills/wp-performance/references/database.md +20 -0
- package/skills/wp-performance/references/http-api.md +15 -0
- package/skills/wp-performance/references/measurement.md +21 -0
- package/skills/wp-performance/references/object-cache.md +24 -0
- package/skills/wp-performance/references/query-monitor-headless.md +38 -0
- package/skills/wp-performance/references/server-timing.md +22 -0
- package/skills/wp-performance/references/wp-cli-doctor.md +24 -0
- package/skills/wp-performance/references/wp-cli-profile.md +32 -0
- package/skills/wp-performance/scripts/perf_inspect.mjs +128 -0
- package/skills/wp-phpstan/SKILL.md +99 -0
- package/skills/wp-phpstan/references/configuration.md +52 -0
- package/skills/wp-phpstan/references/third-party-classes.md +76 -0
- package/skills/wp-phpstan/references/wordpress-annotations.md +124 -0
- package/skills/wp-phpstan/scripts/phpstan_inspect.mjs +263 -0
- package/skills/wp-playground/SKILL.md +103 -0
- package/skills/wp-playground/references/blueprints.md +36 -0
- package/skills/wp-playground/references/cli-commands.md +39 -0
- package/skills/wp-playground/references/debugging.md +16 -0
- package/skills/wp-plugin-development/SKILL.md +114 -0
- package/skills/wp-plugin-development/references/data-and-cron.md +19 -0
- package/skills/wp-plugin-development/references/debugging.md +19 -0
- package/skills/wp-plugin-development/references/lifecycle.md +33 -0
- package/skills/wp-plugin-development/references/security.md +29 -0
- package/skills/wp-plugin-development/references/settings-api.md +22 -0
- package/skills/wp-plugin-development/references/structure.md +16 -0
- package/skills/wp-plugin-development/scripts/detect_plugins.mjs +122 -0
- package/skills/wp-project-triage/SKILL.md +40 -0
- package/skills/wp-project-triage/references/triage.schema.json +143 -0
- package/skills/wp-project-triage/scripts/detect_wp_project.mjs +592 -0
- package/skills/wp-rest-api/SKILL.md +116 -0
- package/skills/wp-rest-api/references/authentication.md +18 -0
- package/skills/wp-rest-api/references/custom-content-types.md +20 -0
- package/skills/wp-rest-api/references/discovery-and-params.md +20 -0
- package/skills/wp-rest-api/references/responses-and-fields.md +30 -0
- package/skills/wp-rest-api/references/routes-and-endpoints.md +36 -0
- package/skills/wp-rest-api/references/schema.md +22 -0
- package/skills/wp-wpcli-and-ops/SKILL.md +125 -0
- package/skills/wp-wpcli-and-ops/references/automation.md +30 -0
- package/skills/wp-wpcli-and-ops/references/cron-and-cache.md +23 -0
- package/skills/wp-wpcli-and-ops/references/debugging.md +17 -0
- package/skills/wp-wpcli-and-ops/references/multisite.md +22 -0
- package/skills/wp-wpcli-and-ops/references/packages-and-updates.md +22 -0
- package/skills/wp-wpcli-and-ops/references/safety.md +30 -0
- package/skills/wp-wpcli-and-ops/references/search-replace.md +40 -0
- package/skills/wp-wpcli-and-ops/scripts/wpcli_inspect.mjs +90 -0
- package/skills/wpds/SKILL.md +60 -0
- package/skills/wpds/references/wpds-mcp-setup.md +59 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
// src/wordpress.ts - Multi-site WordPress REST API client
|
|
2
|
+
import axios from 'axios';
|
|
3
|
+
// ── Concurrency Limiter ──────────────────────────────────────────────
|
|
4
|
+
// Simple FIFO semaphore to cap concurrent requests per site
|
|
5
|
+
class ConcurrencyLimiter {
|
|
6
|
+
maxConcurrent;
|
|
7
|
+
running = 0;
|
|
8
|
+
queue = [];
|
|
9
|
+
constructor(maxConcurrent) {
|
|
10
|
+
this.maxConcurrent = maxConcurrent;
|
|
11
|
+
}
|
|
12
|
+
async acquire() {
|
|
13
|
+
if (this.running < this.maxConcurrent) {
|
|
14
|
+
this.running++;
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
this.queue.push(() => {
|
|
19
|
+
this.running++;
|
|
20
|
+
resolve();
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
release() {
|
|
25
|
+
this.running--;
|
|
26
|
+
const next = this.queue.shift();
|
|
27
|
+
if (next)
|
|
28
|
+
next();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// ── Module State ─────────────────────────────────────────────────────
|
|
32
|
+
const siteClients = new Map();
|
|
33
|
+
const siteLimiters = new Map();
|
|
34
|
+
let activeSiteId = '';
|
|
35
|
+
const MAX_CONCURRENT_PER_SITE = 5;
|
|
36
|
+
const DEFAULT_TIMEOUT_MS = parseInt(process.env.WP_REQUEST_TIMEOUT_MS || '30000', 10);
|
|
37
|
+
const MAX_RETRIES = 3;
|
|
38
|
+
// HTTP status codes that are safe to retry
|
|
39
|
+
const RETRYABLE_STATUS_CODES = new Set([408, 429, 500, 502, 503, 504]);
|
|
40
|
+
// Network error codes that are safe to retry
|
|
41
|
+
const RETRYABLE_ERROR_CODES = new Set(['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND']);
|
|
42
|
+
// ── Initialization ───────────────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* Parse WP_SITES_CONFIG JSON and initialize all site clients
|
|
45
|
+
*/
|
|
46
|
+
export async function initWordPress() {
|
|
47
|
+
const sitesJson = process.env.WP_SITES_CONFIG;
|
|
48
|
+
const defaultSite = process.env.WP_DEFAULT_SITE;
|
|
49
|
+
if (!sitesJson) {
|
|
50
|
+
// Fallback to legacy single-site env vars
|
|
51
|
+
const url = process.env.WORDPRESS_API_URL;
|
|
52
|
+
const username = process.env.WORDPRESS_USERNAME;
|
|
53
|
+
const password = process.env.WORDPRESS_PASSWORD;
|
|
54
|
+
if (!url) {
|
|
55
|
+
throw new Error('No WordPress site configured. Set WP_SITES_CONFIG or WORDPRESS_API_URL.');
|
|
56
|
+
}
|
|
57
|
+
const siteId = 'default';
|
|
58
|
+
await initSiteClient(siteId, url, username || '', password || '');
|
|
59
|
+
activeSiteId = siteId;
|
|
60
|
+
logToStderr(`Initialized single site: ${url}`);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
let sites;
|
|
64
|
+
try {
|
|
65
|
+
sites = JSON.parse(sitesJson);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
throw new Error('Invalid WP_SITES_CONFIG JSON format');
|
|
69
|
+
}
|
|
70
|
+
if (sites.length === 0) {
|
|
71
|
+
throw new Error('WP_SITES_CONFIG contains no sites');
|
|
72
|
+
}
|
|
73
|
+
for (const site of sites) {
|
|
74
|
+
await initSiteClient(site.id, site.url, site.username, site.password);
|
|
75
|
+
logToStderr(`Initialized site: ${site.id} (${site.url})`);
|
|
76
|
+
}
|
|
77
|
+
activeSiteId = defaultSite || sites[0].id;
|
|
78
|
+
logToStderr(`Active site: ${activeSiteId}`);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Initialize a single site's Axios client.
|
|
82
|
+
* baseURL now points to {site}/wp-json/ so that tools can use any namespace.
|
|
83
|
+
*/
|
|
84
|
+
async function initSiteClient(id, url, username, password) {
|
|
85
|
+
let baseURL = url.endsWith('/') ? url : `${url}/`;
|
|
86
|
+
// Strip existing wp-json path variants so we normalize to just /wp-json/
|
|
87
|
+
const wpJsonIdx = baseURL.indexOf('/wp-json');
|
|
88
|
+
if (wpJsonIdx !== -1) {
|
|
89
|
+
baseURL = baseURL.substring(0, wpJsonIdx + 1);
|
|
90
|
+
}
|
|
91
|
+
baseURL = baseURL + 'wp-json/';
|
|
92
|
+
const config = {
|
|
93
|
+
baseURL,
|
|
94
|
+
headers: { 'Content-Type': 'application/json' },
|
|
95
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
96
|
+
};
|
|
97
|
+
if (username && password) {
|
|
98
|
+
const auth = Buffer.from(`${username}:${password}`).toString('base64');
|
|
99
|
+
config.headers = {
|
|
100
|
+
...config.headers,
|
|
101
|
+
'Authorization': `Basic ${auth}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const client = axios.create(config);
|
|
105
|
+
// Verify connection (using wp/v2 namespace)
|
|
106
|
+
try {
|
|
107
|
+
await client.get('wp/v2');
|
|
108
|
+
}
|
|
109
|
+
catch (error) {
|
|
110
|
+
logToStderr(`Warning: Could not verify connection to ${url}: ${error.message}`);
|
|
111
|
+
// Don't throw - the site might become available later
|
|
112
|
+
}
|
|
113
|
+
siteClients.set(id, client);
|
|
114
|
+
siteLimiters.set(id, new ConcurrencyLimiter(MAX_CONCURRENT_PER_SITE));
|
|
115
|
+
}
|
|
116
|
+
// ── Site Management ──────────────────────────────────────────────────
|
|
117
|
+
/**
|
|
118
|
+
* Get the active site's client, or a specific site's client
|
|
119
|
+
*/
|
|
120
|
+
function getClient(siteId) {
|
|
121
|
+
const id = siteId || activeSiteId;
|
|
122
|
+
const client = siteClients.get(id);
|
|
123
|
+
if (!client) {
|
|
124
|
+
const available = Array.from(siteClients.keys()).join(', ');
|
|
125
|
+
throw new Error(`Site "${id}" not found. Available sites: ${available}`);
|
|
126
|
+
}
|
|
127
|
+
return client;
|
|
128
|
+
}
|
|
129
|
+
function getLimiter(siteId) {
|
|
130
|
+
const id = siteId || activeSiteId;
|
|
131
|
+
let limiter = siteLimiters.get(id);
|
|
132
|
+
if (!limiter) {
|
|
133
|
+
limiter = new ConcurrencyLimiter(MAX_CONCURRENT_PER_SITE);
|
|
134
|
+
siteLimiters.set(id, limiter);
|
|
135
|
+
}
|
|
136
|
+
return limiter;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Switch the active site
|
|
140
|
+
*/
|
|
141
|
+
export function switchSite(siteId) {
|
|
142
|
+
if (!siteClients.has(siteId)) {
|
|
143
|
+
const available = Array.from(siteClients.keys()).join(', ');
|
|
144
|
+
throw new Error(`Site "${siteId}" not found. Available: ${available}`);
|
|
145
|
+
}
|
|
146
|
+
activeSiteId = siteId;
|
|
147
|
+
return activeSiteId;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* List all configured sites
|
|
151
|
+
*/
|
|
152
|
+
export function listSites() {
|
|
153
|
+
return Array.from(siteClients.keys());
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Get the active site ID
|
|
157
|
+
*/
|
|
158
|
+
export function getActiveSite() {
|
|
159
|
+
return activeSiteId;
|
|
160
|
+
}
|
|
161
|
+
// ── Logging ──────────────────────────────────────────────────────────
|
|
162
|
+
/**
|
|
163
|
+
* Log to stderr (safe for MCP stdio transport)
|
|
164
|
+
*/
|
|
165
|
+
export function logToStderr(message) {
|
|
166
|
+
const timestamp = new Date().toISOString();
|
|
167
|
+
process.stderr.write(`[${timestamp}] ${message}\n`);
|
|
168
|
+
}
|
|
169
|
+
// Keep backward-compatible alias
|
|
170
|
+
export const logToFile = logToStderr;
|
|
171
|
+
// ── Retry Logic ──────────────────────────────────────────────────────
|
|
172
|
+
function isRetryableError(error, method) {
|
|
173
|
+
// Network errors are always retryable
|
|
174
|
+
if (error.code && RETRYABLE_ERROR_CODES.has(error.code)) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
const status = error.response?.status;
|
|
178
|
+
if (!status)
|
|
179
|
+
return false;
|
|
180
|
+
// For non-GET (mutating) methods, only retry on 429 (rate limit) — not 5xx
|
|
181
|
+
// to avoid duplicate side-effects
|
|
182
|
+
if (method !== 'GET') {
|
|
183
|
+
return status === 429;
|
|
184
|
+
}
|
|
185
|
+
return RETRYABLE_STATUS_CODES.has(status);
|
|
186
|
+
}
|
|
187
|
+
function getRetryDelay(attempt, error) {
|
|
188
|
+
// Respect Retry-After header on 429
|
|
189
|
+
const retryAfter = error.response?.headers?.['retry-after'];
|
|
190
|
+
if (retryAfter) {
|
|
191
|
+
const seconds = parseInt(retryAfter, 10);
|
|
192
|
+
if (!isNaN(seconds))
|
|
193
|
+
return seconds * 1000;
|
|
194
|
+
}
|
|
195
|
+
// Exponential backoff: 1s, 2s, 4s + jitter (0–500ms)
|
|
196
|
+
const base = Math.pow(2, attempt) * 1000;
|
|
197
|
+
const jitter = Math.random() * 500;
|
|
198
|
+
return base + jitter;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Make a request to the WordPress API (active site or specific site).
|
|
202
|
+
* Includes automatic retry with exponential backoff and concurrency limiting.
|
|
203
|
+
*/
|
|
204
|
+
export async function makeWordPressRequest(method, endpoint, data, options) {
|
|
205
|
+
const siteId = options?.siteId || activeSiteId;
|
|
206
|
+
const client = getClient(siteId);
|
|
207
|
+
const limiter = getLimiter(siteId);
|
|
208
|
+
const namespace = options?.namespace || 'wp/v2';
|
|
209
|
+
// Build the full path: {namespace}/{endpoint}
|
|
210
|
+
const cleanEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
|
|
211
|
+
const path = `${namespace}/${cleanEndpoint}`;
|
|
212
|
+
await limiter.acquire();
|
|
213
|
+
try {
|
|
214
|
+
return await executeWithRetry(client, method, path, data, siteId, options);
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
limiter.release();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function executeWithRetry(client, method, path, data, siteId, options) {
|
|
221
|
+
let lastError;
|
|
222
|
+
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
223
|
+
try {
|
|
224
|
+
const requestConfig = {
|
|
225
|
+
method,
|
|
226
|
+
url: path,
|
|
227
|
+
headers: options?.headers || {},
|
|
228
|
+
};
|
|
229
|
+
if (options?.timeout) {
|
|
230
|
+
requestConfig.timeout = options.timeout;
|
|
231
|
+
}
|
|
232
|
+
if (method === 'GET') {
|
|
233
|
+
requestConfig.params = data;
|
|
234
|
+
}
|
|
235
|
+
else if (options?.isFormData) {
|
|
236
|
+
requestConfig.data = data;
|
|
237
|
+
}
|
|
238
|
+
else if (data) {
|
|
239
|
+
requestConfig.data = data;
|
|
240
|
+
}
|
|
241
|
+
const response = await client.request(requestConfig);
|
|
242
|
+
// Handle pagination metadata extraction
|
|
243
|
+
if (options?.includePagination && method === 'GET') {
|
|
244
|
+
const total = parseInt(response.headers['x-wp-total'] || '0', 10);
|
|
245
|
+
const totalPages = parseInt(response.headers['x-wp-totalpages'] || '0', 10);
|
|
246
|
+
const page = data?.page || 1;
|
|
247
|
+
const perPage = data?.per_page || 10;
|
|
248
|
+
return {
|
|
249
|
+
items: response.data,
|
|
250
|
+
pagination: { total, totalPages, page, perPage },
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
return options?.rawResponse ? response : response.data;
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
lastError = error;
|
|
257
|
+
if (attempt < MAX_RETRIES && isRetryableError(error, method)) {
|
|
258
|
+
const delay = getRetryDelay(attempt, error);
|
|
259
|
+
const status = error.response?.status || error.code || 'unknown';
|
|
260
|
+
logToStderr(`[${siteId}] Retry ${attempt + 1}/${MAX_RETRIES} for ${method} ${path} (${status}), waiting ${Math.round(delay)}ms`);
|
|
261
|
+
await sleep(delay);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// All retries exhausted or non-retryable error
|
|
268
|
+
const errorMessage = lastError.response?.data?.message || lastError.message;
|
|
269
|
+
logToStderr(`[${siteId}] Error ${method} ${path}: ${errorMessage}`);
|
|
270
|
+
throw lastError;
|
|
271
|
+
}
|
|
272
|
+
function sleep(ms) {
|
|
273
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
274
|
+
}
|
|
275
|
+
// ── Plugin Repository (External API) ────────────────────────────────
|
|
276
|
+
/**
|
|
277
|
+
* Search the WordPress.org Plugin Repository
|
|
278
|
+
*/
|
|
279
|
+
export async function searchWordPressPluginRepository(searchQuery, page = 1, perPage = 10) {
|
|
280
|
+
const apiUrl = 'https://api.wordpress.org/plugins/info/1.2/';
|
|
281
|
+
const requestData = {
|
|
282
|
+
action: 'query_plugins',
|
|
283
|
+
request: {
|
|
284
|
+
search: searchQuery,
|
|
285
|
+
page,
|
|
286
|
+
per_page: perPage,
|
|
287
|
+
fields: {
|
|
288
|
+
description: true,
|
|
289
|
+
sections: false,
|
|
290
|
+
tested: true,
|
|
291
|
+
requires: true,
|
|
292
|
+
rating: true,
|
|
293
|
+
downloaded: true,
|
|
294
|
+
downloadlink: true,
|
|
295
|
+
last_updated: true,
|
|
296
|
+
homepage: true,
|
|
297
|
+
tags: true,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
};
|
|
301
|
+
const response = await axios.post(apiUrl, requestData, {
|
|
302
|
+
headers: { 'Content-Type': 'application/json' },
|
|
303
|
+
});
|
|
304
|
+
return response.data;
|
|
305
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "wp-rest-bridge",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Lightweight MCP server bridging WordPress REST API for multi-site management",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./build/server.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc --project tsconfig.json",
|
|
9
|
+
"start": "node ./build/server.js",
|
|
10
|
+
"dev": "tsx watch src/server.ts",
|
|
11
|
+
"clean": "rm -rf build"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18.0.0"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
18
|
+
"axios": "^1.7.9",
|
|
19
|
+
"form-data": "^4.0.1",
|
|
20
|
+
"zod": "^3.24.2"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.10.0",
|
|
24
|
+
"tsx": "^4.19.0",
|
|
25
|
+
"typescript": "^5.7.0"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wordpress-router
|
|
3
|
+
description: “Use when the user asks about WordPress — whether development (plugins, themes,
|
|
4
|
+
blocks, REST API) or operations (deploy, audit, backup, migrate, content management).
|
|
5
|
+
Classifies the task and routes to the correct development or operational skill/agent.”
|
|
6
|
+
compatibility: “Targets WordPress 6.9+ (PHP 7.2.24+). Filesystem-based agent with bash + node. Some workflows require WP-CLI.”
|
|
7
|
+
version: 1.0.0
|
|
8
|
+
source: “WordPress/agent-skills (GPL-2.0-or-later) + wordpress-manager extensions”
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# WordPress Router
|
|
12
|
+
|
|
13
|
+
## When to use
|
|
14
|
+
|
|
15
|
+
Use this skill at the start of most WordPress tasks to:
|
|
16
|
+
|
|
17
|
+
- identify what kind of WordPress task this is (development vs operations),
|
|
18
|
+
- for development: classify the repo (plugin vs theme vs block theme vs WP core),
|
|
19
|
+
- for operations: identify the operation type (deploy, audit, backup, migrate, content),
|
|
20
|
+
- route to the most relevant skill(s) and/or agent(s).
|
|
21
|
+
|
|
22
|
+
## Inputs required
|
|
23
|
+
|
|
24
|
+
- Repo root (for development tasks) or site identifier (for operational tasks).
|
|
25
|
+
- The user’s intent and any constraints.
|
|
26
|
+
|
|
27
|
+
## Procedure
|
|
28
|
+
|
|
29
|
+
### 1) Determine task category
|
|
30
|
+
|
|
31
|
+
**Development tasks** (code changes to WordPress projects):
|
|
32
|
+
- Creating/modifying blocks, themes, plugins, REST endpoints
|
|
33
|
+
- Building UI with WordPress Design System (WPDS)
|
|
34
|
+
- Static analysis, build tooling, testing
|
|
35
|
+
- Sandbox testing via WordPress Playground
|
|
36
|
+
- Route via project triage → development skills
|
|
37
|
+
|
|
38
|
+
**Operational tasks** (managing live WordPress sites):
|
|
39
|
+
- Deploying, auditing, backing up, migrating, managing content
|
|
40
|
+
- Route directly → operational skills and agents
|
|
41
|
+
|
|
42
|
+
### 2) For Development tasks
|
|
43
|
+
|
|
44
|
+
1. Run the project triage script:
|
|
45
|
+
- `node skills/wp-project-triage/scripts/detect_wp_project.mjs`
|
|
46
|
+
2. Classify from triage output.
|
|
47
|
+
3. Route to development skills — see `references/decision-tree.md`.
|
|
48
|
+
|
|
49
|
+
### 3) For Operational tasks
|
|
50
|
+
|
|
51
|
+
Route by intent keywords:
|
|
52
|
+
|
|
53
|
+
- **Deploy / push / production** → `wp-deploy` skill + `wp-deployment-engineer` agent
|
|
54
|
+
- **Audit / security / health check / SEO** → `wp-audit` skill + `wp-security-auditor` agent
|
|
55
|
+
- **Backup / restore / snapshot** → `wp-backup` skill
|
|
56
|
+
- **Migrate / move / transfer / clone** → `wp-migrate` skill
|
|
57
|
+
- **Content / blog post / pages / categories** → `wp-content` skill + `wp-content-strategist` agent
|
|
58
|
+
- **Performance / slow / PageSpeed / optimize** → `wp-audit` skill + `wp-performance-optimizer` agent
|
|
59
|
+
- **Site status / plugins / users / multi-site** → `wp-site-manager` agent
|
|
60
|
+
|
|
61
|
+
### 4) Apply guardrails
|
|
62
|
+
|
|
63
|
+
- For development: prefer repo’s existing tooling and conventions.
|
|
64
|
+
- For operations: confirm target site, verify backups, get user confirmation for destructive ops.
|
|
65
|
+
|
|
66
|
+
## Verification
|
|
67
|
+
|
|
68
|
+
- Re-run triage after structural changes (development).
|
|
69
|
+
- Verify site connectivity before operations.
|
|
70
|
+
|
|
71
|
+
## Failure modes / debugging
|
|
72
|
+
|
|
73
|
+
- Development: If triage reports `kind: unknown`, inspect root files.
|
|
74
|
+
- Operations: If site unreachable, check `WP_SITES_CONFIG` and credentials.
|
|
75
|
+
|
|
76
|
+
## Escalation
|
|
77
|
+
|
|
78
|
+
- If routing is ambiguous, ask: “Are you looking to develop/modify WordPress code, or manage/operate a live WordPress site?”
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Router decision tree (v2 — unified development + operations)
|
|
2
|
+
|
|
3
|
+
This routing guide covers both WordPress **development** and **operations** workflows.
|
|
4
|
+
|
|
5
|
+
## Step 0: determine task category
|
|
6
|
+
|
|
7
|
+
Before repo triage, classify the user’s intent:
|
|
8
|
+
|
|
9
|
+
- **Development** (modifying code) → proceed to Step 1
|
|
10
|
+
- **Operations** (managing live sites) → skip to Step 2b
|
|
11
|
+
|
|
12
|
+
Keywords that indicate **operations**:
|
|
13
|
+
deploy, push to production, audit, security check, backup, restore, migrate, move site, create post, manage content, site status, check plugins, performance check, SEO audit
|
|
14
|
+
|
|
15
|
+
Keywords that indicate **development**:
|
|
16
|
+
create block, block.json, theme.json, register_rest_route, plugin development, hooks, PHPStan, build, test, scaffold
|
|
17
|
+
|
|
18
|
+
## Step 1: classify repo kind (from triage — development only)
|
|
19
|
+
|
|
20
|
+
Run `wp-project-triage` first, then use `triage.project.kind`:
|
|
21
|
+
|
|
22
|
+
- `wp-core` → WordPress core checkout work.
|
|
23
|
+
- `wp-site` → full site repo (wp-content present).
|
|
24
|
+
- `wp-block-theme` → theme.json/templates/patterns workflows.
|
|
25
|
+
- `wp-theme` → classic theme workflows.
|
|
26
|
+
- `wp-block-plugin` → Gutenberg block development in a plugin.
|
|
27
|
+
- `wp-plugin` / `wp-mu-plugin` → plugin workflows.
|
|
28
|
+
- `gutenberg` → Gutenberg monorepo workflows.
|
|
29
|
+
|
|
30
|
+
Priority: `gutenberg` > `wp-core` > `wp-site` > `wp-block-theme` > `wp-block-plugin` > `wp-theme` > `wp-plugin`.
|
|
31
|
+
|
|
32
|
+
## Step 2a: route by development intent (keywords)
|
|
33
|
+
|
|
34
|
+
- **Interactivity API / data-wp-* / @wordpress/interactivity / viewScriptModule**
|
|
35
|
+
→ `wp-interactivity-api`
|
|
36
|
+
- **Abilities API / wp_register_ability / wp-abilities/v1**
|
|
37
|
+
→ `wp-abilities-api`
|
|
38
|
+
- **Blocks / block.json / registerBlockType / attributes / save serialization**
|
|
39
|
+
→ `wp-block-development`
|
|
40
|
+
- **theme.json / Global Styles / templates/*.html / patterns/**
|
|
41
|
+
→ `wp-block-themes`
|
|
42
|
+
- **Plugin architecture / hooks / activation / Settings API / admin pages**
|
|
43
|
+
→ `wp-plugin-development`
|
|
44
|
+
- **REST endpoint / register_rest_route / permission_callback**
|
|
45
|
+
→ `wp-rest-api`
|
|
46
|
+
- **WP-CLI / wp-cli.yml / commands**
|
|
47
|
+
→ `wp-wpcli-and-ops`
|
|
48
|
+
- **PHPStan / static analysis / phpstan.neon**
|
|
49
|
+
→ `wp-phpstan`
|
|
50
|
+
- **Performance profiling / query optimization / editor slowness**
|
|
51
|
+
→ `wp-performance` (backend profiling with WP-CLI doctor/profile)
|
|
52
|
+
- **UI components / design tokens / @wordpress/components / @wordpress/ui / WPDS**
|
|
53
|
+
→ `wpds` (WordPress Design System)
|
|
54
|
+
- **Playground / disposable WP / blueprint / sandbox / test environment / version switching**
|
|
55
|
+
→ `wp-playground`
|
|
56
|
+
|
|
57
|
+
## Step 2b: route by operational intent (keywords)
|
|
58
|
+
|
|
59
|
+
- **Deploy / push / production / Hostinger**
|
|
60
|
+
→ `wp-deploy` skill + `wp-deployment-engineer` agent
|
|
61
|
+
- **Audit / security check / vulnerability / hacked / health check**
|
|
62
|
+
→ `wp-audit` skill + `wp-security-auditor` agent
|
|
63
|
+
- **Backup / snapshot / disaster recovery / restore**
|
|
64
|
+
→ `wp-backup` skill
|
|
65
|
+
- **Migrate / move / transfer / clone site / change hosting**
|
|
66
|
+
→ `wp-migrate` skill
|
|
67
|
+
- **Create post / write content / manage pages / categories / SEO content**
|
|
68
|
+
→ `wp-content` skill + `wp-content-strategist` agent
|
|
69
|
+
- **Slow site / PageSpeed / Core Web Vitals / optimize performance**
|
|
70
|
+
→ `wp-audit` skill (performance scope) + `wp-performance-optimizer` agent
|
|
71
|
+
- **Site status / list plugins / manage users / multi-site coordination**
|
|
72
|
+
→ `wp-site-manager` agent
|
|
73
|
+
- **DNS / domain / SSL / hosting configuration**
|
|
74
|
+
→ `wp-site-manager` agent (Hostinger MCP tools)
|
|
75
|
+
|
|
76
|
+
## Step 3: guardrails checklist (always)
|
|
77
|
+
|
|
78
|
+
### Development guardrails
|
|
79
|
+
- Verify detected tooling before suggesting commands (Composer vs npm/yarn/pnpm).
|
|
80
|
+
- Prefer existing lint/test scripts if present.
|
|
81
|
+
- If version constraints aren’t detectable, ask for target WP core and PHP versions.
|
|
82
|
+
|
|
83
|
+
### Operations guardrails
|
|
84
|
+
- Confirm target site before any operation.
|
|
85
|
+
- Verify backups exist before destructive operations (deploy, migrate, restore).
|
|
86
|
+
- Get explicit user confirmation before publishing, deleting, or modifying live content.
|
|
87
|
+
- Never deactivate plugins or delete content without listing dependencies first.
|
|
88
|
+
- For multi-site operations, announce which site you’re operating on.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: wp-abilities-api
|
|
3
|
+
description: "Use when working with the WordPress Abilities API (wp_register_ability, wp_register_ability_category, /wp-json/wp-abilities/v1/*, @wordpress/abilities) including defining abilities, categories, meta, REST exposure, and permissions checks for clients."
|
|
4
|
+
compatibility: "Targets WordPress 6.9+ (PHP 7.2.24+). Filesystem-based agent with bash + node. Some workflows require WP-CLI."
|
|
5
|
+
version: 1.0.0
|
|
6
|
+
source: "WordPress/agent-skills (GPL-2.0-or-later)"
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# WP Abilities API
|
|
10
|
+
|
|
11
|
+
## When to use
|
|
12
|
+
|
|
13
|
+
Use this skill when the task involves:
|
|
14
|
+
|
|
15
|
+
- registering abilities or ability categories in PHP,
|
|
16
|
+
- exposing abilities to clients via REST (`wp-abilities/v1`),
|
|
17
|
+
- consuming abilities in JS (notably `@wordpress/abilities`),
|
|
18
|
+
- diagnosing “ability doesn’t show up” / “client can’t see ability” / “REST returns empty”.
|
|
19
|
+
|
|
20
|
+
## Inputs required
|
|
21
|
+
|
|
22
|
+
- Repo root (run `wp-project-triage` first if you haven’t).
|
|
23
|
+
- Target WordPress version(s) and whether this is WP core or a plugin/theme.
|
|
24
|
+
- Where the change should live (plugin vs theme vs mu-plugin).
|
|
25
|
+
|
|
26
|
+
## Procedure
|
|
27
|
+
|
|
28
|
+
### 1) Confirm availability and version constraints
|
|
29
|
+
|
|
30
|
+
- If this is WP core work, check `signals.isWpCoreCheckout` and `versions.wordpress.core`.
|
|
31
|
+
- If the project targets WP < 6.9, you may need the Abilities API plugin/package rather than relying on core.
|
|
32
|
+
|
|
33
|
+
### 2) Find existing Abilities usage
|
|
34
|
+
|
|
35
|
+
Search for these in the repo:
|
|
36
|
+
|
|
37
|
+
- `wp_register_ability(`
|
|
38
|
+
- `wp_register_ability_category(`
|
|
39
|
+
- `wp_abilities_api_init`
|
|
40
|
+
- `wp_abilities_api_categories_init`
|
|
41
|
+
- `wp-abilities/v1`
|
|
42
|
+
- `@wordpress/abilities`
|
|
43
|
+
|
|
44
|
+
If none exist, decide whether you’re introducing Abilities API fresh (new registrations + client consumption) or only consuming.
|
|
45
|
+
|
|
46
|
+
### 3) Register categories (optional)
|
|
47
|
+
|
|
48
|
+
If you need a logical grouping, register an ability category early (see `references/php-registration.md`).
|
|
49
|
+
|
|
50
|
+
### 4) Register abilities (PHP)
|
|
51
|
+
|
|
52
|
+
Implement the ability in PHP registration with:
|
|
53
|
+
|
|
54
|
+
- stable `id` (namespaced),
|
|
55
|
+
- `label`/`description`,
|
|
56
|
+
- `category`,
|
|
57
|
+
- `meta`:
|
|
58
|
+
- add `readonly: true` when the ability is informational,
|
|
59
|
+
- set `show_in_rest: true` for abilities you want visible to clients.
|
|
60
|
+
|
|
61
|
+
Use the documented init hooks for Abilities API registration so they load at the right time (see `references/php-registration.md`).
|
|
62
|
+
|
|
63
|
+
### 5) Confirm REST exposure
|
|
64
|
+
|
|
65
|
+
- Verify the REST endpoints exist and return expected results (see `references/rest-api.md`).
|
|
66
|
+
- If the client still can’t see the ability, confirm `meta.show_in_rest` is enabled and you’re querying the right endpoint.
|
|
67
|
+
|
|
68
|
+
### 6) Consume from JS (if needed)
|
|
69
|
+
|
|
70
|
+
- Prefer `@wordpress/abilities` APIs for client-side access and checks.
|
|
71
|
+
- Ensure build tooling includes the dependency and the project’s build pipeline bundles it.
|
|
72
|
+
|
|
73
|
+
## Verification
|
|
74
|
+
|
|
75
|
+
- `wp-project-triage` indicates `signals.usesAbilitiesApi: true` after your change (if applicable).
|
|
76
|
+
- REST check (in a WP environment): endpoints under `wp-abilities/v1` return your ability and category when expected.
|
|
77
|
+
- If the repo has tests, add/update coverage near:
|
|
78
|
+
- PHP: ability registration and meta exposure
|
|
79
|
+
- JS: ability consumption and UI gating
|
|
80
|
+
|
|
81
|
+
## Failure modes / debugging
|
|
82
|
+
|
|
83
|
+
- Ability never appears:
|
|
84
|
+
- registration code not running (wrong hook / file not loaded),
|
|
85
|
+
- missing `meta.show_in_rest`,
|
|
86
|
+
- incorrect category/ID mismatch.
|
|
87
|
+
- REST shows ability but JS doesn’t:
|
|
88
|
+
- wrong REST base/namespace,
|
|
89
|
+
- JS dependency not bundled,
|
|
90
|
+
- caching (object/page caches) masking changes.
|
|
91
|
+
|
|
92
|
+
## Escalation
|
|
93
|
+
|
|
94
|
+
- If you’re uncertain about version support, confirm target WP core versions and whether Abilities API is expected from core or as a plugin.
|
|
95
|
+
- For canonical details, consult:
|
|
96
|
+
- `references/rest-api.md`
|
|
97
|
+
- `references/php-registration.md`
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# PHP registration quick guide
|
|
2
|
+
|
|
3
|
+
Key concepts and entrypoints for the WordPress Abilities API:
|
|
4
|
+
|
|
5
|
+
- Register ability categories and abilities in PHP.
|
|
6
|
+
- Use the Abilities API init hooks to ensure registration occurs at the right lifecycle time.
|
|
7
|
+
|
|
8
|
+
## Hook order (critical)
|
|
9
|
+
|
|
10
|
+
**Categories must be registered before abilities.** Use the correct hooks:
|
|
11
|
+
|
|
12
|
+
1. `wp_abilities_api_categories_init` — Register categories here first.
|
|
13
|
+
2. `wp_abilities_api_init` — Register abilities here (after categories exist).
|
|
14
|
+
|
|
15
|
+
**Warning:** Registering abilities outside `wp_abilities_api_init` triggers `_doing_it_wrong()` and the registration will fail.
|
|
16
|
+
|
|
17
|
+
```php
|
|
18
|
+
// 1. Register category first
|
|
19
|
+
add_action( 'wp_abilities_api_categories_init', function() {
|
|
20
|
+
wp_register_ability_category( 'my-plugin', [
|
|
21
|
+
'label' => __( 'My Plugin', 'my-plugin' ),
|
|
22
|
+
] );
|
|
23
|
+
} );
|
|
24
|
+
|
|
25
|
+
// 2. Then register abilities
|
|
26
|
+
add_action( 'wp_abilities_api_init', function() {
|
|
27
|
+
wp_register_ability( 'my-plugin/get-info', [
|
|
28
|
+
'label' => __( 'Get Site Info', 'my-plugin' ),
|
|
29
|
+
'description' => __( 'Returns basic site information.', 'my-plugin' ),
|
|
30
|
+
'category' => 'my-plugin',
|
|
31
|
+
'callback' => 'my_plugin_get_info_callback',
|
|
32
|
+
'meta' => [ 'show_in_rest' => true ],
|
|
33
|
+
] );
|
|
34
|
+
} );
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Common primitives
|
|
38
|
+
|
|
39
|
+
- `wp_register_ability_category( $category_id, $args )`
|
|
40
|
+
- `wp_register_ability( $ability_id, $args )`
|
|
41
|
+
|
|
42
|
+
## Key arguments for `wp_register_ability()`
|
|
43
|
+
|
|
44
|
+
| Argument | Description |
|
|
45
|
+
|----------|-------------|
|
|
46
|
+
| `label` | Human-readable name for UI (e.g., command palette) |
|
|
47
|
+
| `description` | What the ability does |
|
|
48
|
+
| `category` | Category ID (must be registered first) |
|
|
49
|
+
| `callback` | Function that executes the ability |
|
|
50
|
+
| `input_schema` | JSON Schema for expected input (enables validation) |
|
|
51
|
+
| `output_schema` | JSON Schema for returned output |
|
|
52
|
+
| `permission_callback` | Optional function to check if current user can execute |
|
|
53
|
+
| `meta.show_in_rest` | Set `true` to expose via REST API |
|
|
54
|
+
| `meta.readonly` | Set `true` if ability is informational only |
|
|
55
|
+
|
|
56
|
+
## Recommended patterns
|
|
57
|
+
|
|
58
|
+
- Namespace IDs (e.g. `my-plugin:feature.edit`).
|
|
59
|
+
- Treat IDs as stable API; changing IDs is a breaking change.
|
|
60
|
+
- Use `input_schema` and `output_schema` for validation and to help AI agents understand usage.
|
|
61
|
+
- Always include a `permission_callback` for abilities that modify data.
|
|
62
|
+
|
|
63
|
+
## References
|
|
64
|
+
|
|
65
|
+
- Abilities API handbook: https://developer.wordpress.org/apis/abilities-api/
|
|
66
|
+
- Dev note: https://make.wordpress.org/core/2025/11/10/abilities-api-in-wordpress-6-9/
|
|
67
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# REST API quick guide (`wp-abilities/v1`)
|
|
2
|
+
|
|
3
|
+
The Abilities API exposes endpoints under the REST namespace:
|
|
4
|
+
|
|
5
|
+
- `wp-abilities/v1/abilities`
|
|
6
|
+
- `wp-abilities/v1/categories`
|
|
7
|
+
|
|
8
|
+
Debug checklist:
|
|
9
|
+
|
|
10
|
+
- Confirm the route exists under `wp-json/wp-abilities/v1/...`.
|
|
11
|
+
- Verify the ability/category shows in REST responses.
|
|
12
|
+
- If missing, confirm `meta.show_in_rest` is enabled for that ability.
|
|
13
|
+
|