claude-plugin-wordpress-manager 1.8.0 → 1.9.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 (28) hide show
  1. package/.claude-plugin/plugin.json +5 -3
  2. package/CHANGELOG.md +27 -0
  3. package/agents/wp-site-manager.md +26 -0
  4. package/docs/plans/2026-02-28-multisite-v1.9.0-design.md +258 -0
  5. package/docs/plans/2026-02-28-multisite-v1.9.0.md +1604 -0
  6. package/package.json +5 -3
  7. package/servers/wp-rest-bridge/build/tools/index.d.ts +260 -0
  8. package/servers/wp-rest-bridge/build/tools/index.js +6 -0
  9. package/servers/wp-rest-bridge/build/tools/multisite-network.d.ts +132 -0
  10. package/servers/wp-rest-bridge/build/tools/multisite-network.js +157 -0
  11. package/servers/wp-rest-bridge/build/tools/multisite-sites.d.ts +150 -0
  12. package/servers/wp-rest-bridge/build/tools/multisite-sites.js +160 -0
  13. package/servers/wp-rest-bridge/build/types.d.ts +13 -0
  14. package/servers/wp-rest-bridge/build/wordpress.d.ts +19 -0
  15. package/servers/wp-rest-bridge/build/wordpress.js +10 -0
  16. package/servers/wp-rest-bridge/build/wpcli.d.ts +23 -0
  17. package/servers/wp-rest-bridge/build/wpcli.js +72 -0
  18. package/skills/wordpress-router/references/decision-tree.md +4 -2
  19. package/skills/wp-multisite/SKILL.md +92 -0
  20. package/skills/wp-multisite/references/domain-mapping.md +70 -0
  21. package/skills/wp-multisite/references/migration-multisite.md +76 -0
  22. package/skills/wp-multisite/references/network-plugins.md +66 -0
  23. package/skills/wp-multisite/references/network-setup.md +69 -0
  24. package/skills/wp-multisite/references/site-management.md +67 -0
  25. package/skills/wp-multisite/references/user-roles.md +73 -0
  26. package/skills/wp-multisite/scripts/multisite_inspect.mjs +160 -0
  27. package/skills/wp-security/SKILL.md +4 -0
  28. package/skills/wp-wpcli-and-ops/SKILL.md +4 -0
@@ -0,0 +1,1604 @@
1
+ # v1.9.0 Multisite Implementation Plan
2
+
3
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
4
+
5
+ **Goal:** Add WordPress Multisite network management to the plugin via a new WP-CLI execution module, 10 new MCP tools, a skill with 6 references, and a detection script.
6
+
7
+ **Architecture:** New `wpcli.ts` module handles command execution (local or SSH), two tool files provide 10 multisite MCP tools (wp-cli for network-only, REST for plugin listing), `wp-site-manager` agent gets a multisite section, router becomes v6.
8
+
9
+ **Tech Stack:** TypeScript (ESM), Node.js child_process, Zod schemas, MCP SDK
10
+
11
+ ---
12
+
13
+ ## Task 1: Extend types.ts with Multisite types and SiteConfig fields
14
+
15
+ **Files:**
16
+ - Modify: `servers/wp-rest-bridge/src/types.ts`
17
+
18
+ **Step 1: Add WP-CLI/Multisite fields to SiteConfig comment and WPNetworkSite type**
19
+
20
+ Add at the end of `types.ts` (after the WCCoupon interface):
21
+
22
+ ```typescript
23
+ // ── WordPress Multisite Types ────────────────────────────────────────
24
+
25
+ export interface WPNetworkSite {
26
+ blog_id: number;
27
+ url: string;
28
+ domain: string;
29
+ path: string;
30
+ registered: string;
31
+ last_updated: string;
32
+ public: boolean;
33
+ archived: boolean;
34
+ mature: boolean;
35
+ spam: boolean;
36
+ deleted: boolean;
37
+ }
38
+ ```
39
+
40
+ **Step 2: Verify compilation**
41
+
42
+ Run: `cd servers/wp-rest-bridge && npx tsc --noEmit`
43
+ Expected: no errors (the new type is just an interface export, no runtime impact)
44
+
45
+ ---
46
+
47
+ ## Task 2: Extend SiteConfig and create wpcli.ts module
48
+
49
+ **Files:**
50
+ - Modify: `servers/wp-rest-bridge/src/wordpress.ts` (SiteConfig interface)
51
+ - Create: `servers/wp-rest-bridge/src/wpcli.ts`
52
+
53
+ **Step 1: Add WP-CLI fields to SiteConfig in wordpress.ts**
54
+
55
+ In `servers/wp-rest-bridge/src/wordpress.ts`, extend the `SiteConfig` interface (lines 4-11) to:
56
+
57
+ ```typescript
58
+ interface SiteConfig {
59
+ id: string;
60
+ url: string;
61
+ username: string;
62
+ password: string;
63
+ wc_consumer_key?: string;
64
+ wc_consumer_secret?: string;
65
+ // WP-CLI access (optional, for multisite and CLI operations)
66
+ wp_path?: string; // Local WP installation path
67
+ ssh_host?: string; // SSH hostname for remote wp-cli
68
+ ssh_user?: string; // SSH username
69
+ ssh_key?: string; // Path to SSH private key
70
+ ssh_port?: number; // SSH port (default: 22)
71
+ is_multisite?: boolean; // Flag: this site is a multisite network
72
+ }
73
+ ```
74
+
75
+ Also export a function to retrieve the raw SiteConfig. Add after the `getActiveSite()` function (after line 232):
76
+
77
+ ```typescript
78
+ /**
79
+ * Get the SiteConfig for a given site (needed by wpcli module).
80
+ */
81
+ export function getSiteConfig(siteId?: string): SiteConfig | undefined {
82
+ const id = siteId || activeSiteId;
83
+ return parsedSiteConfigs.get(id);
84
+ }
85
+ ```
86
+
87
+ And add a module-level Map to store parsed configs. After `let activeSiteId: string = '';` (line 45), add:
88
+
89
+ ```typescript
90
+ const parsedSiteConfigs = new Map<string, SiteConfig>();
91
+ ```
92
+
93
+ In the `initWordPress()` function, store each site config. In the for loop (line 93-96), add after `logToStderr`:
94
+
95
+ ```typescript
96
+ parsedSiteConfigs.set(site.id, site);
97
+ ```
98
+
99
+ For the legacy single-site fallback (lines 75-78), also store:
100
+
101
+ ```typescript
102
+ parsedSiteConfigs.set(siteId, { id: siteId, url, username: username || '', password: password || '' });
103
+ ```
104
+
105
+ **Step 2: Create wpcli.ts**
106
+
107
+ Create `servers/wp-rest-bridge/src/wpcli.ts`:
108
+
109
+ ```typescript
110
+ // src/wpcli.ts - WP-CLI execution module (local + SSH)
111
+ import { exec } from 'node:child_process';
112
+ import { getSiteConfig, getActiveSite, logToStderr } from './wordpress.js';
113
+
114
+ const WPCLI_TIMEOUT_MS = 30000;
115
+
116
+ /**
117
+ * Check if a site has WP-CLI access configured (wp_path and optionally ssh_host).
118
+ */
119
+ export function hasWpCli(siteId?: string): boolean {
120
+ const config = getSiteConfig(siteId || getActiveSite());
121
+ if (!config) return false;
122
+ return !!config.wp_path;
123
+ }
124
+
125
+ /**
126
+ * Check if a site is configured as multisite.
127
+ */
128
+ export function isMultisite(siteId?: string): boolean {
129
+ const config = getSiteConfig(siteId || getActiveSite());
130
+ if (!config) return false;
131
+ return !!config.is_multisite;
132
+ }
133
+
134
+ /**
135
+ * Execute a WP-CLI command for a given site.
136
+ *
137
+ * - If ssh_host is set: runs via SSH
138
+ * - If only wp_path is set: runs locally
139
+ * - Appends --format=json by default for structured output
140
+ *
141
+ * @param command WP-CLI command without the leading "wp " (e.g., "site list", "plugin activate hello --network")
142
+ * @param siteId Site ID (defaults to active site)
143
+ * @param options.skipJson Don't append --format=json (for commands that don't support it)
144
+ * @returns stdout as string
145
+ */
146
+ export async function executeWpCli(
147
+ command: string,
148
+ siteId?: string,
149
+ options?: { skipJson?: boolean }
150
+ ): Promise<string> {
151
+ const id = siteId || getActiveSite();
152
+ const config = getSiteConfig(id);
153
+
154
+ if (!config) {
155
+ throw new Error(`Site "${id}" not found in configuration.`);
156
+ }
157
+
158
+ if (!config.wp_path) {
159
+ throw new Error(
160
+ `WP-CLI not configured for site "${id}". ` +
161
+ `Add wp_path to WP_SITES_CONFIG for this site.`
162
+ );
163
+ }
164
+
165
+ const formatFlag = options?.skipJson ? '' : ' --format=json';
166
+ const wpCommand = `wp ${command}${formatFlag}`;
167
+
168
+ let shellCommand: string;
169
+
170
+ if (config.ssh_host) {
171
+ // Remote execution via SSH
172
+ const sshUser = config.ssh_user || 'root';
173
+ const sshPort = config.ssh_port || 22;
174
+ const sshKeyFlag = config.ssh_key ? `-i ${config.ssh_key} ` : '';
175
+ const escapedCommand = `cd ${config.wp_path} && ${wpCommand}`;
176
+ shellCommand = `ssh ${sshKeyFlag}-p ${sshPort} -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 ${sshUser}@${config.ssh_host} '${escapedCommand}'`;
177
+ } else {
178
+ // Local execution
179
+ shellCommand = `cd ${config.wp_path} && ${wpCommand}`;
180
+ }
181
+
182
+ logToStderr(`[${id}] WP-CLI: ${wpCommand}`);
183
+
184
+ return new Promise<string>((resolve, reject) => {
185
+ exec(shellCommand, { timeout: WPCLI_TIMEOUT_MS }, (error, stdout, stderr) => {
186
+ if (error) {
187
+ const msg = stderr?.trim() || error.message;
188
+ logToStderr(`[${id}] WP-CLI error: ${msg}`);
189
+ reject(new Error(`WP-CLI error on site "${id}": ${msg}`));
190
+ return;
191
+ }
192
+ resolve(stdout.trim());
193
+ });
194
+ });
195
+ }
196
+ ```
197
+
198
+ **Step 3: Verify compilation**
199
+
200
+ Run: `cd servers/wp-rest-bridge && npx tsc --noEmit`
201
+ Expected: no errors
202
+
203
+ ---
204
+
205
+ ## Task 3: Create multisite-sites.ts (5 tools)
206
+
207
+ **Files:**
208
+ - Create: `servers/wp-rest-bridge/src/tools/multisite-sites.ts`
209
+
210
+ **Step 1: Create the tool file**
211
+
212
+ Create `servers/wp-rest-bridge/src/tools/multisite-sites.ts`:
213
+
214
+ ```typescript
215
+ // src/tools/multisite-sites.ts — Multisite sub-site management (WP-CLI)
216
+ import { Tool } from '@modelcontextprotocol/sdk/types.js';
217
+ import { executeWpCli, isMultisite } from '../wpcli.js';
218
+ import { z } from 'zod';
219
+
220
+ // ── Schemas ──────────────────────────────────────────────────────────
221
+
222
+ const msListSitesSchema = z.object({
223
+ site_id: z.string().optional().describe('Target site ID (defaults to active site)')
224
+ }).strict();
225
+
226
+ const msGetSiteSchema = z.object({
227
+ blog_id: z.number().describe('Blog ID of the sub-site to retrieve'),
228
+ site_id: z.string().optional().describe('Target site ID (defaults to active site)')
229
+ }).strict();
230
+
231
+ const msCreateSiteSchema = z.object({
232
+ slug: z.string().describe('URL slug for the new sub-site (e.g., "blog", "shop")'),
233
+ title: z.string().describe('Title of the new sub-site'),
234
+ email: z.string().describe('Admin email for the new sub-site'),
235
+ site_id: z.string().optional().describe('Target site ID (defaults to active site)')
236
+ }).strict();
237
+
238
+ const msActivateSiteSchema = z.object({
239
+ blog_id: z.number().describe('Blog ID of the sub-site'),
240
+ active: z.boolean().describe('true to activate, false to deactivate'),
241
+ site_id: z.string().optional().describe('Target site ID (defaults to active site)')
242
+ }).strict();
243
+
244
+ const msDeleteSiteSchema = z.object({
245
+ blog_id: z.number().describe('Blog ID of the sub-site to delete'),
246
+ confirm: z.literal(true).describe('Must be true to confirm deletion (safety gate)'),
247
+ site_id: z.string().optional().describe('Target site ID (defaults to active site)')
248
+ }).strict();
249
+
250
+ // ── Tools ────────────────────────────────────────────────────────────
251
+
252
+ export const multisiteSiteTools: Tool[] = [
253
+ {
254
+ name: 'ms_list_sites',
255
+ description: 'Lists all sub-sites in a WordPress Multisite network. Requires wp-cli and is_multisite configuration.',
256
+ inputSchema: { type: 'object', properties: msListSitesSchema.shape }
257
+ },
258
+ {
259
+ name: 'ms_get_site',
260
+ description: 'Gets details of a specific sub-site by blog ID.',
261
+ inputSchema: { type: 'object', properties: msGetSiteSchema.shape }
262
+ },
263
+ {
264
+ name: 'ms_create_site',
265
+ description: 'Creates a new sub-site in the multisite network.',
266
+ inputSchema: { type: 'object', properties: msCreateSiteSchema.shape }
267
+ },
268
+ {
269
+ name: 'ms_activate_site',
270
+ description: 'Activates or deactivates a sub-site in the multisite network.',
271
+ inputSchema: { type: 'object', properties: msActivateSiteSchema.shape }
272
+ },
273
+ {
274
+ name: 'ms_delete_site',
275
+ description: 'Permanently deletes a sub-site. Requires confirm: true as safety gate.',
276
+ inputSchema: { type: 'object', properties: msDeleteSiteSchema.shape }
277
+ }
278
+ ];
279
+
280
+ // ── Helpers ──────────────────────────────────────────────────────────
281
+
282
+ function requireMultisite(siteId?: string): void {
283
+ if (!isMultisite(siteId)) {
284
+ throw new Error(
285
+ `Site is not configured as multisite. ` +
286
+ `Set is_multisite: true in WP_SITES_CONFIG.`
287
+ );
288
+ }
289
+ }
290
+
291
+ // ── Handlers ─────────────────────────────────────────────────────────
292
+
293
+ export const multisiteSiteHandlers = {
294
+ ms_list_sites: async (params: z.infer<typeof msListSitesSchema>) => {
295
+ try {
296
+ requireMultisite(params.site_id);
297
+ const result = await executeWpCli('site list', params.site_id);
298
+ return {
299
+ toolResult: {
300
+ content: [{ type: 'text', text: result }]
301
+ }
302
+ };
303
+ } catch (error: any) {
304
+ return {
305
+ toolResult: {
306
+ isError: true,
307
+ content: [{ type: 'text', text: `Error listing sites: ${error.message}` }]
308
+ }
309
+ };
310
+ }
311
+ },
312
+
313
+ ms_get_site: async (params: z.infer<typeof msGetSiteSchema>) => {
314
+ try {
315
+ requireMultisite(params.site_id);
316
+ const result = await executeWpCli(`site list --blog_id=${params.blog_id}`, params.site_id);
317
+ return {
318
+ toolResult: {
319
+ content: [{ type: 'text', text: result }]
320
+ }
321
+ };
322
+ } catch (error: any) {
323
+ return {
324
+ toolResult: {
325
+ isError: true,
326
+ content: [{ type: 'text', text: `Error getting site: ${error.message}` }]
327
+ }
328
+ };
329
+ }
330
+ },
331
+
332
+ ms_create_site: async (params: z.infer<typeof msCreateSiteSchema>) => {
333
+ try {
334
+ requireMultisite(params.site_id);
335
+ const result = await executeWpCli(
336
+ `site create --slug=${params.slug} --title="${params.title}" --email=${params.email}`,
337
+ params.site_id,
338
+ { skipJson: true }
339
+ );
340
+ return {
341
+ toolResult: {
342
+ content: [{ type: 'text', text: result }]
343
+ }
344
+ };
345
+ } catch (error: any) {
346
+ return {
347
+ toolResult: {
348
+ isError: true,
349
+ content: [{ type: 'text', text: `Error creating site: ${error.message}` }]
350
+ }
351
+ };
352
+ }
353
+ },
354
+
355
+ ms_activate_site: async (params: z.infer<typeof msActivateSiteSchema>) => {
356
+ try {
357
+ requireMultisite(params.site_id);
358
+ const action = params.active ? 'activate' : 'deactivate';
359
+ const result = await executeWpCli(
360
+ `site ${action} ${params.blog_id}`,
361
+ params.site_id,
362
+ { skipJson: true }
363
+ );
364
+ return {
365
+ toolResult: {
366
+ content: [{ type: 'text', text: result || `Site ${params.blog_id} ${action}d successfully.` }]
367
+ }
368
+ };
369
+ } catch (error: any) {
370
+ return {
371
+ toolResult: {
372
+ isError: true,
373
+ content: [{ type: 'text', text: `Error ${params.active ? 'activating' : 'deactivating'} site: ${error.message}` }]
374
+ }
375
+ };
376
+ }
377
+ },
378
+
379
+ ms_delete_site: async (params: z.infer<typeof msDeleteSiteSchema>) => {
380
+ try {
381
+ requireMultisite(params.site_id);
382
+ const result = await executeWpCli(
383
+ `site delete ${params.blog_id} --yes`,
384
+ params.site_id,
385
+ { skipJson: true }
386
+ );
387
+ return {
388
+ toolResult: {
389
+ content: [{ type: 'text', text: result || `Site ${params.blog_id} deleted successfully.` }]
390
+ }
391
+ };
392
+ } catch (error: any) {
393
+ return {
394
+ toolResult: {
395
+ isError: true,
396
+ content: [{ type: 'text', text: `Error deleting site: ${error.message}` }]
397
+ }
398
+ };
399
+ }
400
+ }
401
+ };
402
+ ```
403
+
404
+ **Step 2: Verify compilation**
405
+
406
+ Run: `cd servers/wp-rest-bridge && npx tsc --noEmit`
407
+ Expected: no errors
408
+
409
+ ---
410
+
411
+ ## Task 4: Create multisite-network.ts (5 tools)
412
+
413
+ **Files:**
414
+ - Create: `servers/wp-rest-bridge/src/tools/multisite-network.ts`
415
+
416
+ **Step 1: Create the tool file**
417
+
418
+ Create `servers/wp-rest-bridge/src/tools/multisite-network.ts`:
419
+
420
+ ```typescript
421
+ // src/tools/multisite-network.ts — Multisite network admin (REST + WP-CLI hybrid)
422
+ import { Tool } from '@modelcontextprotocol/sdk/types.js';
423
+ import { makeWordPressRequest } from '../wordpress.js';
424
+ import { executeWpCli, isMultisite } from '../wpcli.js';
425
+ import { z } from 'zod';
426
+
427
+ // ── Schemas ──────────────────────────────────────────────────────────
428
+
429
+ const msListNetworkPluginsSchema = z.object({
430
+ site_id: z.string().optional().describe('Target site ID (defaults to active site)')
431
+ }).strict();
432
+
433
+ const msNetworkActivatePluginSchema = z.object({
434
+ plugin_slug: z.string().describe('Plugin slug to network-activate (e.g., "akismet", "jetpack")'),
435
+ site_id: z.string().optional().describe('Target site ID (defaults to active site)')
436
+ }).strict();
437
+
438
+ const msNetworkDeactivatePluginSchema = z.object({
439
+ plugin_slug: z.string().describe('Plugin slug to network-deactivate'),
440
+ site_id: z.string().optional().describe('Target site ID (defaults to active site)')
441
+ }).strict();
442
+
443
+ const msListSuperAdminsSchema = z.object({
444
+ site_id: z.string().optional().describe('Target site ID (defaults to active site)')
445
+ }).strict();
446
+
447
+ const msGetNetworkSettingsSchema = z.object({
448
+ site_id: z.string().optional().describe('Target site ID (defaults to active site)')
449
+ }).strict();
450
+
451
+ // ── Tools ────────────────────────────────────────────────────────────
452
+
453
+ export const multisiteNetworkTools: Tool[] = [
454
+ {
455
+ name: 'ms_list_network_plugins',
456
+ description: 'Lists all plugins on the multisite network with their activation status (uses REST API).',
457
+ inputSchema: { type: 'object', properties: msListNetworkPluginsSchema.shape }
458
+ },
459
+ {
460
+ name: 'ms_network_activate_plugin',
461
+ description: 'Network-activates a plugin across all sites in the multisite network (uses wp-cli).',
462
+ inputSchema: { type: 'object', properties: msNetworkActivatePluginSchema.shape }
463
+ },
464
+ {
465
+ name: 'ms_network_deactivate_plugin',
466
+ description: 'Network-deactivates a plugin from all sites in the multisite network (uses wp-cli).',
467
+ inputSchema: { type: 'object', properties: msNetworkDeactivatePluginSchema.shape }
468
+ },
469
+ {
470
+ name: 'ms_list_super_admins',
471
+ description: 'Lists all Super Admin users in the multisite network (uses wp-cli).',
472
+ inputSchema: { type: 'object', properties: msListSuperAdminsSchema.shape }
473
+ },
474
+ {
475
+ name: 'ms_get_network_settings',
476
+ description: 'Gets network-wide settings (site name, admin email, registration policy) via wp-cli.',
477
+ inputSchema: { type: 'object', properties: msGetNetworkSettingsSchema.shape }
478
+ }
479
+ ];
480
+
481
+ // ── Helpers ──────────────────────────────────────────────────────────
482
+
483
+ function requireMultisite(siteId?: string): void {
484
+ if (!isMultisite(siteId)) {
485
+ throw new Error(
486
+ `Site is not configured as multisite. ` +
487
+ `Set is_multisite: true in WP_SITES_CONFIG.`
488
+ );
489
+ }
490
+ }
491
+
492
+ // ── Handlers ─────────────────────────────────────────────────────────
493
+
494
+ export const multisiteNetworkHandlers = {
495
+ ms_list_network_plugins: async (params: z.infer<typeof msListNetworkPluginsSchema>) => {
496
+ try {
497
+ requireMultisite(params.site_id);
498
+ // Use REST API — list_plugins works on multisite, returns network_only field
499
+ const result = await makeWordPressRequest('GET', 'plugins', undefined, {
500
+ siteId: params.site_id
501
+ });
502
+ return {
503
+ toolResult: {
504
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
505
+ }
506
+ };
507
+ } catch (error: any) {
508
+ const errorMessage = error.response?.data?.message || error.message;
509
+ return {
510
+ toolResult: {
511
+ isError: true,
512
+ content: [{ type: 'text', text: `Error listing network plugins: ${errorMessage}` }]
513
+ }
514
+ };
515
+ }
516
+ },
517
+
518
+ ms_network_activate_plugin: async (params: z.infer<typeof msNetworkActivatePluginSchema>) => {
519
+ try {
520
+ requireMultisite(params.site_id);
521
+ const result = await executeWpCli(
522
+ `plugin activate ${params.plugin_slug} --network`,
523
+ params.site_id,
524
+ { skipJson: true }
525
+ );
526
+ return {
527
+ toolResult: {
528
+ content: [{ type: 'text', text: result || `Plugin "${params.plugin_slug}" network-activated successfully.` }]
529
+ }
530
+ };
531
+ } catch (error: any) {
532
+ return {
533
+ toolResult: {
534
+ isError: true,
535
+ content: [{ type: 'text', text: `Error network-activating plugin: ${error.message}` }]
536
+ }
537
+ };
538
+ }
539
+ },
540
+
541
+ ms_network_deactivate_plugin: async (params: z.infer<typeof msNetworkDeactivatePluginSchema>) => {
542
+ try {
543
+ requireMultisite(params.site_id);
544
+ const result = await executeWpCli(
545
+ `plugin deactivate ${params.plugin_slug} --network`,
546
+ params.site_id,
547
+ { skipJson: true }
548
+ );
549
+ return {
550
+ toolResult: {
551
+ content: [{ type: 'text', text: result || `Plugin "${params.plugin_slug}" network-deactivated successfully.` }]
552
+ }
553
+ };
554
+ } catch (error: any) {
555
+ return {
556
+ toolResult: {
557
+ isError: true,
558
+ content: [{ type: 'text', text: `Error network-deactivating plugin: ${error.message}` }]
559
+ }
560
+ };
561
+ }
562
+ },
563
+
564
+ ms_list_super_admins: async (params: z.infer<typeof msListSuperAdminsSchema>) => {
565
+ try {
566
+ requireMultisite(params.site_id);
567
+ const result = await executeWpCli('super-admin list', params.site_id, { skipJson: true });
568
+ return {
569
+ toolResult: {
570
+ content: [{ type: 'text', text: result }]
571
+ }
572
+ };
573
+ } catch (error: any) {
574
+ return {
575
+ toolResult: {
576
+ isError: true,
577
+ content: [{ type: 'text', text: `Error listing super admins: ${error.message}` }]
578
+ }
579
+ };
580
+ }
581
+ },
582
+
583
+ ms_get_network_settings: async (params: z.infer<typeof msGetNetworkSettingsSchema>) => {
584
+ try {
585
+ requireMultisite(params.site_id);
586
+ const result = await executeWpCli('network meta list 1', params.site_id);
587
+ return {
588
+ toolResult: {
589
+ content: [{ type: 'text', text: result }]
590
+ }
591
+ };
592
+ } catch (error: any) {
593
+ return {
594
+ toolResult: {
595
+ isError: true,
596
+ content: [{ type: 'text', text: `Error getting network settings: ${error.message}` }]
597
+ }
598
+ };
599
+ }
600
+ }
601
+ };
602
+ ```
603
+
604
+ **Step 2: Verify compilation**
605
+
606
+ Run: `cd servers/wp-rest-bridge && npx tsc --noEmit`
607
+ Expected: no errors
608
+
609
+ ---
610
+
611
+ ## Task 5: Register multisite tools in index.ts
612
+
613
+ **Files:**
614
+ - Modify: `servers/wp-rest-bridge/src/tools/index.ts`
615
+
616
+ **Step 1: Add imports and spread into allTools/toolHandlers**
617
+
618
+ Add after the `wc-settings.js` import (line 16):
619
+
620
+ ```typescript
621
+ import { multisiteSiteTools, multisiteSiteHandlers } from './multisite-sites.js';
622
+ import { multisiteNetworkTools, multisiteNetworkHandlers } from './multisite-network.js';
623
+ ```
624
+
625
+ Add to `allTools` array (after `...wcSettingTools`):
626
+
627
+ ```typescript
628
+ ...multisiteSiteTools, // 5 tools
629
+ ...multisiteNetworkTools, // 5 tools
630
+ ```
631
+
632
+ Add to `toolHandlers` object (after `...wcSettingHandlers`):
633
+
634
+ ```typescript
635
+ ...multisiteSiteHandlers,
636
+ ...multisiteNetworkHandlers,
637
+ ```
638
+
639
+ **Step 2: Build and verify tool count**
640
+
641
+ Run: `cd servers/wp-rest-bridge && npx tsc`
642
+ Expected: build succeeds
643
+
644
+ Run: `node -e "const {allTools} = require('./servers/wp-rest-bridge/build/tools/index.js'); console.log('Total tools:', allTools.length)"`
645
+ Expected: `Total tools: 81` (71 existing + 10 multisite)
646
+
647
+ Note: If `require` fails due to ESM, use:
648
+ ```bash
649
+ node --input-type=module -e "import {allTools} from './servers/wp-rest-bridge/build/tools/index.js'; console.log('Total tools:', allTools.length)"
650
+ ```
651
+
652
+ ---
653
+
654
+ ## Task 6: Create detection script multisite_inspect.mjs
655
+
656
+ **Files:**
657
+ - Create: `skills/wp-multisite/scripts/multisite_inspect.mjs`
658
+
659
+ **Step 1: Create the detection script**
660
+
661
+ First create directory: `mkdir -p skills/wp-multisite/scripts`
662
+
663
+ Create `skills/wp-multisite/scripts/multisite_inspect.mjs`:
664
+
665
+ ```javascript
666
+ /**
667
+ * multisite_inspect.mjs — Detect WordPress Multisite configuration.
668
+ *
669
+ * Scans for multisite indicators: wp-config.php constants, WP_SITES_CONFIG flags,
670
+ * sunrise.php (domain mapping), .htaccess multisite rewrite rules.
671
+ *
672
+ * Usage:
673
+ * node multisite_inspect.mjs [--cwd=/path/to/check]
674
+ *
675
+ * Exit codes:
676
+ * 0 — multisite indicators found
677
+ * 1 — no multisite indicators found
678
+ */
679
+
680
+ import { readFileSync, existsSync } from 'node:fs';
681
+ import { join, resolve } from 'node:path';
682
+ import { argv, env, stdout, exit } from 'node:process';
683
+
684
+ // ---------------------------------------------------------------------------
685
+ // Helpers
686
+ // ---------------------------------------------------------------------------
687
+
688
+ function readFileSafe(filePath) {
689
+ try { return readFileSync(filePath, 'utf-8'); } catch { return null; }
690
+ }
691
+
692
+ function existsSafe(filePath) {
693
+ try { return existsSync(filePath); } catch { return false; }
694
+ }
695
+
696
+ // ---------------------------------------------------------------------------
697
+ // Detectors
698
+ // ---------------------------------------------------------------------------
699
+
700
+ function detectWpConfig(cwd) {
701
+ const paths = [
702
+ join(cwd, 'wp-config.php'),
703
+ join(cwd, '../wp-config.php'), // wp-config one level up (common setup)
704
+ ];
705
+
706
+ for (const p of paths) {
707
+ const content = readFileSafe(p);
708
+ if (!content) continue;
709
+
710
+ const multisite = /define\s*\(\s*['"]MULTISITE['"]\s*,\s*true\s*\)/i.test(content);
711
+ const subdomain = content.match(/define\s*\(\s*['"]SUBDOMAIN_INSTALL['"]\s*,\s*(true|false)\s*\)/i);
712
+ const domain = content.match(/define\s*\(\s*['"]DOMAIN_CURRENT_SITE['"]\s*,\s*['"]([^'"]+)['"]\s*\)/i);
713
+ const pathMatch = content.match(/define\s*\(\s*['"]PATH_CURRENT_SITE['"]\s*,\s*['"]([^'"]+)['"]\s*\)/i);
714
+
715
+ if (multisite) {
716
+ return {
717
+ found: true,
718
+ path: p,
719
+ subdomain_install: subdomain ? subdomain[1] === 'true' : null,
720
+ domain_current_site: domain ? domain[1] : null,
721
+ path_current_site: pathMatch ? pathMatch[1] : null,
722
+ };
723
+ }
724
+ }
725
+ return null;
726
+ }
727
+
728
+ function detectSitesConfig() {
729
+ const sitesJson = env.WP_SITES_CONFIG;
730
+ if (!sitesJson) return null;
731
+ try {
732
+ const sites = JSON.parse(sitesJson);
733
+ const msSites = sites.filter(s => s.is_multisite === true);
734
+ const cliSites = sites.filter(s => s.wp_path);
735
+ return {
736
+ multisite_sites: msSites.map(s => ({
737
+ id: s.id,
738
+ wp_path: s.wp_path || null,
739
+ ssh_host: s.ssh_host || null,
740
+ has_wpcli: !!s.wp_path,
741
+ })),
742
+ cli_ready_sites: cliSites.map(s => s.id),
743
+ count: msSites.length,
744
+ };
745
+ } catch { return null; }
746
+ }
747
+
748
+ function detectSunrise(cwd) {
749
+ const paths = [
750
+ join(cwd, 'wp-content/sunrise.php'),
751
+ join(cwd, 'sunrise.php'),
752
+ ];
753
+ for (const p of paths) {
754
+ if (existsSafe(p)) {
755
+ return { found: true, path: p };
756
+ }
757
+ }
758
+ return null;
759
+ }
760
+
761
+ function detectHtaccessMultisite(cwd) {
762
+ const content = readFileSafe(join(cwd, '.htaccess'));
763
+ if (!content) return null;
764
+
765
+ // WordPress multisite .htaccess has specific rewrite rules
766
+ const hasMultisiteRules = /RewriteRule\s+\.\s+index\.php/i.test(content) &&
767
+ (/upload/.test(content) || /files/.test(content) || /blogs\.dir/.test(content));
768
+
769
+ return hasMultisiteRules ? { found: true } : null;
770
+ }
771
+
772
+ // ---------------------------------------------------------------------------
773
+ // Main
774
+ // ---------------------------------------------------------------------------
775
+
776
+ function main() {
777
+ const cwdArg = argv.find(a => a.startsWith('--cwd='));
778
+ const cwd = cwdArg ? resolve(cwdArg.split('=')[1]) : process.cwd();
779
+
780
+ const wpConfig = detectWpConfig(cwd);
781
+ const sitesConfig = detectSitesConfig();
782
+ const sunrise = detectSunrise(cwd);
783
+ const htaccess = detectHtaccessMultisite(cwd);
784
+
785
+ const signals = [];
786
+ if (wpConfig) signals.push('wp_config_multisite');
787
+ if (sitesConfig?.count > 0) signals.push('sites_config_multisite');
788
+ if (sunrise) signals.push('sunrise_domain_mapping');
789
+ if (htaccess) signals.push('htaccess_multisite_rules');
790
+
791
+ const report = {
792
+ tool: 'multisite_inspect',
793
+ version: '1.0.0',
794
+ timestamp: new Date().toISOString(),
795
+ cwd,
796
+ found: signals.length > 0,
797
+ signals,
798
+ details: {
799
+ wp_config: wpConfig || undefined,
800
+ sites_config: sitesConfig || undefined,
801
+ sunrise: sunrise || undefined,
802
+ htaccess: htaccess || undefined,
803
+ },
804
+ recommendations: [],
805
+ };
806
+
807
+ if (wpConfig && !sitesConfig?.count) {
808
+ report.recommendations.push('Multisite detected in wp-config.php but no site in WP_SITES_CONFIG has is_multisite: true');
809
+ }
810
+ if (sitesConfig?.count > 0) {
811
+ const noCli = sitesConfig.multisite_sites.filter(s => !s.has_wpcli);
812
+ if (noCli.length > 0) {
813
+ report.recommendations.push(`Sites without wp_path (no wp-cli access): ${noCli.map(s => s.id).join(', ')}`);
814
+ }
815
+ report.recommendations.push(`${sitesConfig.count} multisite network(s) configured — 10 ms_* tools available`);
816
+ }
817
+ if (sunrise) {
818
+ report.recommendations.push('sunrise.php detected — domain mapping is active');
819
+ }
820
+
821
+ stdout.write(JSON.stringify(report, null, 2) + '\n');
822
+ exit(signals.length > 0 ? 0 : 1);
823
+ }
824
+
825
+ main();
826
+ ```
827
+
828
+ **Step 2: Test detection script**
829
+
830
+ Run: `node skills/wp-multisite/scripts/multisite_inspect.mjs`
831
+ Expected: exit code 1 (no multisite indicators in plugin dev directory), JSON output with `found: false`
832
+
833
+ ---
834
+
835
+ ## Task 7: Create wp-multisite SKILL.md
836
+
837
+ **Files:**
838
+ - Create: `skills/wp-multisite/SKILL.md`
839
+
840
+ **Step 1: Create SKILL.md**
841
+
842
+ Create `skills/wp-multisite/SKILL.md`:
843
+
844
+ ```markdown
845
+ ---
846
+ name: wp-multisite
847
+ description: |
848
+ This skill should be used when the user asks about "multisite", "network admin",
849
+ "sub-sites", "domain mapping", "super admin", "network activate",
850
+ "WordPress Multisite network", or any multisite network management operations.
851
+ version: 1.0.0
852
+ ---
853
+
854
+ ## Overview
855
+
856
+ WordPress Multisite network management via WP-CLI (10 MCP tools). Covers sub-site CRUD, network plugin management, Super Admin listing, network settings, and domain mapping guidance. Uses a hybrid approach: REST API where available, WP-CLI for network-only operations.
857
+
858
+ ## When to Use
859
+
860
+ - User mentions multisite, network, sub-sites, or domain mapping
861
+ - User needs to create, activate, deactivate, or delete sub-sites
862
+ - User wants to network-activate or network-deactivate plugins
863
+ - User needs Super Admin listing or network settings
864
+ - User asks about migrating single-site to multisite or vice versa
865
+
866
+ ## Prerequisites
867
+
868
+ WP-CLI access and multisite flag must be configured in `WP_SITES_CONFIG`:
869
+
870
+ ```json
871
+ {
872
+ "id": "mynetwork",
873
+ "url": "https://network.example.com",
874
+ "username": "superadmin",
875
+ "password": "xxxx xxxx xxxx xxxx",
876
+ "wp_path": "/var/www/wordpress",
877
+ "ssh_host": "network.example.com",
878
+ "ssh_user": "deploy",
879
+ "ssh_key": "~/.ssh/id_rsa",
880
+ "is_multisite": true
881
+ }
882
+ ```
883
+
884
+ - `wp_path` — required for all wp-cli operations
885
+ - `ssh_host` / `ssh_user` — required for remote sites (omit for local)
886
+ - `is_multisite: true` — required flag to enable ms_* tools
887
+
888
+ ## Detection
889
+
890
+ Run the detection script to check multisite presence:
891
+
892
+ ```bash
893
+ node skills/wp-multisite/scripts/multisite_inspect.mjs
894
+ ```
895
+
896
+ ## Multisite Operations Decision Tree
897
+
898
+ 1. **Sub-site management?**
899
+ - List all sub-sites → `ms_list_sites`
900
+ - Get sub-site details → `ms_get_site`
901
+ - Create new sub-site → `ms_create_site`
902
+ - Activate/deactivate → `ms_activate_site`
903
+ - Delete sub-site → `ms_delete_site`
904
+
905
+ 2. **Network plugin management?**
906
+ - List all plugins (with network status) → `ms_list_network_plugins`
907
+ - Network-activate plugin → `ms_network_activate_plugin`
908
+ - Network-deactivate plugin → `ms_network_deactivate_plugin`
909
+
910
+ 3. **Network administration?**
911
+ - List Super Admins → `ms_list_super_admins`
912
+ - Get network settings → `ms_get_network_settings`
913
+
914
+ 4. **Domain mapping / network setup / migration?**
915
+ - See reference files below (no dedicated MCP tool — use wp-cli via Bash)
916
+
917
+ ## Recommended Agent
918
+
919
+ For complex multi-step multisite operations, use the `wp-site-manager` agent (which has a dedicated Multisite Network Management section).
920
+
921
+ ## Additional Resources
922
+
923
+ ### Reference Files
924
+
925
+ - **`references/network-setup.md`** — Sub-directory vs sub-domain, wp-config constants, installation
926
+ - **`references/site-management.md`** — CRUD sub-sites, templates, bulk operations
927
+ - **`references/domain-mapping.md`** — Custom domains, SSL, DNS CNAME, sunrise.php
928
+ - **`references/network-plugins.md`** — Network-activated vs per-site plugins, must-use plugins
929
+ - **`references/user-roles.md`** — Super Admin capabilities, site-level roles
930
+ - **`references/migration-multisite.md`** — Single to multisite and back, database tables
931
+
932
+ ### Related Skills
933
+
934
+ - `wp-wpcli-and-ops` — WP-CLI command reference and multisite flags
935
+ - `wp-security` — Super Admin capabilities and multisite security
936
+ - `wp-deploy` — Deploy to multisite network
937
+ ```
938
+
939
+ ---
940
+
941
+ ## Task 8: Create 6 reference files
942
+
943
+ **Files:**
944
+ - Create: `skills/wp-multisite/references/network-setup.md`
945
+ - Create: `skills/wp-multisite/references/site-management.md`
946
+ - Create: `skills/wp-multisite/references/domain-mapping.md`
947
+ - Create: `skills/wp-multisite/references/network-plugins.md`
948
+ - Create: `skills/wp-multisite/references/user-roles.md`
949
+ - Create: `skills/wp-multisite/references/migration-multisite.md`
950
+
951
+ **Step 1: Create directory and all 6 files**
952
+
953
+ First: `mkdir -p skills/wp-multisite/references`
954
+
955
+ Create each file with content specified in the substeps below.
956
+
957
+ **Step 1a: network-setup.md**
958
+
959
+ ```markdown
960
+ # Network Setup
961
+
962
+ WordPress Multisite allows a single WordPress installation to host multiple websites (sub-sites) sharing the same codebase and database. Understanding the setup options is critical for architecture decisions.
963
+
964
+ ## MCP Tools
965
+
966
+ | Tool | Usage |
967
+ |------|-------|
968
+ | `ms_get_network_settings` | View current network configuration |
969
+
970
+ ## Sub-directory vs Sub-domain
971
+
972
+ | Mode | URL Pattern | Example | Requirements |
973
+ |------|------------|---------|-------------|
974
+ | Sub-directory | `example.com/site1/` | `example.com/blog/` | Default, works everywhere |
975
+ | Sub-domain | `site1.example.com` | `blog.example.com` | Wildcard DNS (`*.example.com`), wildcard SSL |
976
+
977
+ Decision factors:
978
+ - **Sub-directory**: simpler DNS, single SSL cert, better for related sites
979
+ - **Sub-domain**: each site feels independent, better for unrelated brands
980
+
981
+ ## wp-config.php Constants
982
+
983
+ Required constants for multisite (set during network installation):
984
+
985
+ ```php
986
+ define('WP_ALLOW_MULTISITE', true); // Step 1: enables Network Setup menu
987
+ define('MULTISITE', true); // Step 2: after network creation
988
+ define('SUBDOMAIN_INSTALL', false); // true for sub-domain, false for sub-directory
989
+ define('DOMAIN_CURRENT_SITE', 'example.com');
990
+ define('PATH_CURRENT_SITE', '/');
991
+ define('SITE_ID_CURRENT_SITE', 1);
992
+ define('BLOG_ID_CURRENT_SITE', 1);
993
+ ```
994
+
995
+ ## Installation Procedure
996
+
997
+ 1. Start with a fresh single-site WordPress installation
998
+ 2. Add `define('WP_ALLOW_MULTISITE', true);` to wp-config.php
999
+ 3. Navigate to Tools > Network Setup in wp-admin
1000
+ 4. Choose sub-directory or sub-domain
1001
+ 5. WordPress generates the remaining constants and .htaccess rules
1002
+ 6. Add the generated code to wp-config.php and .htaccess
1003
+ 7. Log in again — Network Admin menu appears
1004
+
1005
+ ## .htaccess Rules (sub-directory mode)
1006
+
1007
+ ```apache
1008
+ RewriteEngine On
1009
+ RewriteBase /
1010
+ RewriteRule ^index\.php$ - [L]
1011
+
1012
+ # add a trailing slash to /wp-admin
1013
+ RewriteRule ^([_0-9a-zA-Z-]+/)?wp-admin$ $1wp-admin/ [R=301,L]
1014
+
1015
+ RewriteCond %{REQUEST_FILENAME} -f [OR]
1016
+ RewriteCond %{REQUEST_FILENAME} -d
1017
+ RewriteRule ^ - [L]
1018
+ RewriteRule ^([_0-9a-zA-Z-]+/)?(wp-(content|admin|includes).*) $2 [L]
1019
+ RewriteRule ^([_0-9a-zA-Z-]+/)?(.*\.php)$ $2 [L]
1020
+ RewriteRule . index.php [L]
1021
+ ```
1022
+
1023
+ ## Tips and Gotchas
1024
+
1025
+ - **Cannot switch modes**: You cannot change from sub-directory to sub-domain (or vice versa) after network creation without a fresh install or complex migration.
1026
+ - **Existing content**: If the single site already has content, sub-directory mode may conflict with existing page slugs.
1027
+ - **SSL**: Sub-domain mode requires wildcard SSL (`*.example.com`). Let's Encrypt supports wildcard via DNS-01 challenge.
1028
+ - **WP_ALLOW_MULTISITE vs MULTISITE**: `WP_ALLOW_MULTISITE` enables the setup UI; `MULTISITE` activates the network. They are different constants.
1029
+ ```
1030
+
1031
+ **Step 1b: site-management.md**
1032
+
1033
+ ```markdown
1034
+ # Site Management
1035
+
1036
+ Sub-site lifecycle in a WordPress Multisite network: creating, configuring, activating/deactivating, and deleting sites.
1037
+
1038
+ ## MCP Tools
1039
+
1040
+ | Tool | Usage |
1041
+ |------|-------|
1042
+ | `ms_list_sites` | List all sub-sites with status |
1043
+ | `ms_get_site` | Get details of a specific sub-site |
1044
+ | `ms_create_site` | Create a new sub-site |
1045
+ | `ms_activate_site` | Activate or deactivate a sub-site |
1046
+ | `ms_delete_site` | Permanently delete a sub-site |
1047
+
1048
+ ## Sub-site Lifecycle
1049
+
1050
+ ```
1051
+ Create → Active → [Deactivate → Archived/Spam/Deleted]
1052
+ → [Delete permanently]
1053
+ ```
1054
+
1055
+ ## Common Procedures
1056
+
1057
+ ### List All Sub-sites
1058
+
1059
+ 1. `ms_list_sites` — returns blog_id, url, registered date, status for all sites
1060
+ 2. Review the `archived`, `spam`, `deleted` flags for each
1061
+
1062
+ ### Create a New Sub-site
1063
+
1064
+ 1. `ms_create_site` with slug, title, admin email
1065
+ 2. WordPress creates the sub-site with default theme and plugins
1066
+ 3. The specified email becomes the sub-site admin
1067
+
1068
+ ### Deactivate a Sub-site
1069
+
1070
+ 1. `ms_activate_site` with `active: false` and the target blog_id
1071
+ 2. Deactivated sites return a "This site has been archived" message to visitors
1072
+ 3. Content and settings are preserved
1073
+
1074
+ ### Delete a Sub-site
1075
+
1076
+ 1. `ms_delete_site` with blog_id and `confirm: true`
1077
+ 2. **Permanent**: removes all content, settings, and uploads for that sub-site
1078
+ 3. Database tables for the sub-site are dropped
1079
+
1080
+ ## Site Properties
1081
+
1082
+ | Property | Description |
1083
+ |----------|-------------|
1084
+ | `blog_id` | Unique numeric identifier |
1085
+ | `domain` | Domain name of the sub-site |
1086
+ | `path` | URL path (e.g., `/blog/` in sub-directory mode) |
1087
+ | `registered` | Creation timestamp |
1088
+ | `last_updated` | Last modification timestamp |
1089
+ | `public` | Whether the site appears in search results |
1090
+ | `archived` | Manually archived by network admin |
1091
+ | `spam` | Marked as spam |
1092
+ | `deleted` | Soft-deleted (not permanently removed) |
1093
+
1094
+ ## Tips and Gotchas
1095
+
1096
+ - **Blog ID 1**: The main site always has `blog_id: 1`. Do not delete it.
1097
+ - **Uploads**: Each sub-site has its own uploads directory under `wp-content/uploads/sites/{blog_id}/`.
1098
+ - **Database tables**: Each sub-site gets its own set of tables with prefix `wp_{blog_id}_` (e.g., `wp_2_posts`, `wp_2_options`).
1099
+ - **Default content**: New sub-sites get a "Hello World" post and sample page, similar to a fresh WordPress install.
1100
+ - **Themes**: Sub-sites can only use themes that are network-enabled or network-activated. See `network-plugins.md`.
1101
+ ```
1102
+
1103
+ **Step 1c: domain-mapping.md**
1104
+
1105
+ ```markdown
1106
+ # Domain Mapping
1107
+
1108
+ Domain mapping allows each sub-site in a WordPress Multisite network to use its own custom domain instead of the default sub-directory or sub-domain URL.
1109
+
1110
+ ## Overview
1111
+
1112
+ | Default URL | Mapped Domain |
1113
+ |-------------|---------------|
1114
+ | `network.com/shopA/` | `shopA.com` |
1115
+ | `shopB.network.com` | `shopB.com` |
1116
+
1117
+ Since WordPress 4.5+, domain mapping is built into core (no plugin required for basic mapping).
1118
+
1119
+ ## Setup Procedure
1120
+
1121
+ ### 1. DNS Configuration
1122
+
1123
+ For each custom domain, create a DNS record pointing to the network server:
1124
+
1125
+ | Record Type | Name | Value |
1126
+ |-------------|------|-------|
1127
+ | A | `shopA.com` | `<server-ip>` |
1128
+ | CNAME | `www.shopA.com` | `shopA.com` |
1129
+
1130
+ ### 2. WordPress Configuration
1131
+
1132
+ In Network Admin > Sites > Edit Site > Domain:
1133
+ - Change the site URL to the custom domain
1134
+
1135
+ Or via WP-CLI:
1136
+ ```bash
1137
+ wp site list # find the blog_id
1138
+ wp option update home 'https://shopA.com' --url=network.com/shopA/
1139
+ wp option update siteurl 'https://shopA.com' --url=network.com/shopA/
1140
+ ```
1141
+
1142
+ ### 3. SSL Certificate
1143
+
1144
+ Each mapped domain needs its own SSL certificate:
1145
+ - **Let's Encrypt**: Use Certbot with `--domains shopA.com,shopB.com`
1146
+ - **Wildcard**: Only covers `*.network.com`, NOT custom domains
1147
+ - **Multi-domain SAN cert**: Can cover all mapped domains in one cert
1148
+
1149
+ ### 4. Web Server Configuration
1150
+
1151
+ The web server must accept requests for all mapped domains. In Nginx:
1152
+
1153
+ ```nginx
1154
+ server {
1155
+ server_name shopA.com shopB.com network.com *.network.com;
1156
+ # ... standard WordPress config
1157
+ }
1158
+ ```
1159
+
1160
+ ## sunrise.php (Advanced)
1161
+
1162
+ For complex domain mapping logic, WordPress supports a `sunrise.php` drop-in:
1163
+
1164
+ - Location: `wp-content/sunrise.php`
1165
+ - Loaded very early in the WordPress bootstrap (before plugins)
1166
+ - Must be enabled: `define('SUNRISE', true);` in wp-config.php
1167
+ - Used by plugins like "WordPress MU Domain Mapping" (legacy) or "Mercator"
1168
+
1169
+ ## Tips and Gotchas
1170
+
1171
+ - **Cookie domain**: After mapping, update `COOKIE_DOMAIN` if login issues occur.
1172
+ - **Mixed content**: Ensure all mapped domains use HTTPS to avoid mixed content warnings.
1173
+ - **Caching**: Flush caches after domain mapping changes — both server-side and CDN.
1174
+ - **Search Console**: Register each mapped domain separately in Google Search Console.
1175
+ - **Reverse proxy**: If using Cloudflare or similar, configure the DNS to point to the origin server's IP, not the CDN.
1176
+ ```
1177
+
1178
+ **Step 1d: network-plugins.md**
1179
+
1180
+ ```markdown
1181
+ # Network Plugins and Themes
1182
+
1183
+ In WordPress Multisite, plugins and themes can be managed at the network level (Super Admin) or at the individual site level (Site Admin). Understanding the activation modes prevents conflicts.
1184
+
1185
+ ## MCP Tools
1186
+
1187
+ | Tool | Usage |
1188
+ |------|-------|
1189
+ | `ms_list_network_plugins` | List all plugins with network activation status |
1190
+ | `ms_network_activate_plugin` | Activate a plugin across the entire network |
1191
+ | `ms_network_deactivate_plugin` | Deactivate a plugin from the entire network |
1192
+
1193
+ ## Plugin Activation Modes
1194
+
1195
+ | Mode | Who Controls | Scope | Use Case |
1196
+ |------|-------------|-------|----------|
1197
+ | Network-activated | Super Admin | All sites | Security plugins, caching, essential functionality |
1198
+ | Per-site activated | Site Admin | One site | Site-specific features |
1199
+ | Must-use (mu-plugins) | Developer | All sites, always on | Core business logic, cannot be deactivated |
1200
+
1201
+ ## Procedures
1202
+
1203
+ ### Network-Activate a Plugin
1204
+
1205
+ 1. `ms_network_activate_plugin` with the plugin slug
1206
+ 2. The plugin immediately activates on ALL sub-sites
1207
+ 3. Site Admins cannot deactivate a network-activated plugin
1208
+
1209
+ ### Network-Deactivate a Plugin
1210
+
1211
+ 1. `ms_network_deactivate_plugin` with the plugin slug
1212
+ 2. The plugin deactivates on ALL sub-sites simultaneously
1213
+ 3. Per-site activation state is lost
1214
+
1215
+ ### Check Plugin Status
1216
+
1217
+ 1. `ms_list_network_plugins` — returns all plugins with their status
1218
+ 2. Look for `network_only: true` in the response for network-activated plugins
1219
+
1220
+ ## Theme Management
1221
+
1222
+ Themes in multisite work differently from plugins:
1223
+
1224
+ | Action | Level | Effect |
1225
+ |--------|-------|--------|
1226
+ | Network Enable | Super Admin | Theme becomes available for site admins to activate |
1227
+ | Network Disable | Super Admin | Theme removed from site admin's theme list |
1228
+ | Activate | Site Admin | Theme becomes active for that specific site |
1229
+
1230
+ A theme must be **network-enabled** before any site admin can use it.
1231
+
1232
+ ## Must-Use Plugins
1233
+
1234
+ - Location: `wp-content/mu-plugins/`
1235
+ - Always active on ALL sites — cannot be deactivated via UI
1236
+ - Loaded before regular plugins
1237
+ - No activation hooks (code runs immediately)
1238
+ - Useful for: custom login, security rules, performance optimizations
1239
+
1240
+ ## Tips and Gotchas
1241
+
1242
+ - **Network activation is immediate**: No confirmation dialog. All sites are affected instantly.
1243
+ - **Plugin conflicts**: A network-activated plugin may conflict with per-site plugins. Test thoroughly.
1244
+ - **Updates**: Plugin updates on multisite affect all sites. Test in staging first.
1245
+ - **Memory**: Each network-activated plugin increases memory usage across all sites.
1246
+ - **Drop-in replacements**: `object-cache.php`, `advanced-cache.php`, `db.php` are shared across all sites.
1247
+ ```
1248
+
1249
+ **Step 1e: user-roles.md**
1250
+
1251
+ ```markdown
1252
+ # User Roles in Multisite
1253
+
1254
+ WordPress Multisite adds a Super Admin role above the standard role hierarchy. Users can have different roles on different sub-sites within the same network.
1255
+
1256
+ ## MCP Tools
1257
+
1258
+ | Tool | Usage |
1259
+ |------|-------|
1260
+ | `ms_list_super_admins` | List all Super Admin users in the network |
1261
+
1262
+ ## Role Hierarchy
1263
+
1264
+ | Role | Scope | Key Capabilities |
1265
+ |------|-------|-----------------|
1266
+ | Super Admin | Entire network | All capabilities on all sites, network settings, site CRUD |
1267
+ | Administrator | Single site | Full control of one sub-site (cannot install plugins/themes) |
1268
+ | Editor | Single site | Manage and publish all posts on one site |
1269
+ | Author | Single site | Publish own posts |
1270
+ | Contributor | Single site | Write drafts, cannot publish |
1271
+ | Subscriber | Single site | Read-only access |
1272
+
1273
+ ## Super Admin vs Administrator (Multisite)
1274
+
1275
+ | Capability | Super Admin | Site Administrator |
1276
+ |-----------|-------------|-------------------|
1277
+ | Install plugins | Yes | No |
1278
+ | Install themes | Yes | No |
1279
+ | Create/delete sub-sites | Yes | No |
1280
+ | Network activate plugins | Yes | No |
1281
+ | Edit wp-config.php | Yes | No |
1282
+ | Manage network settings | Yes | No |
1283
+ | Edit files (theme/plugin editor) | Yes | No (disabled by default) |
1284
+ | Manage site users | Yes | Yes (own site only) |
1285
+ | Manage site options | Yes | Yes (own site only) |
1286
+
1287
+ ## User Registration Modes
1288
+
1289
+ Network-wide setting (Network Admin > Settings):
1290
+
1291
+ | Mode | Description |
1292
+ |------|-------------|
1293
+ | Registration disabled | No one can register |
1294
+ | User accounts may be registered | Users can register but not create sites |
1295
+ | Logged-in users may register new sites | Existing users can create sub-sites |
1296
+ | Both user accounts and sites can be registered | Open registration for users and sites |
1297
+
1298
+ ## Common Operations
1299
+
1300
+ ### List Super Admins
1301
+ 1. `ms_list_super_admins` — returns usernames with super admin status
1302
+
1303
+ ### Add Super Admin (via wp-cli)
1304
+ ```bash
1305
+ wp super-admin add username
1306
+ ```
1307
+
1308
+ ### Remove Super Admin (via wp-cli)
1309
+ ```bash
1310
+ wp super-admin remove username
1311
+ ```
1312
+
1313
+ ### Add User to Sub-site (via wp-cli)
1314
+ ```bash
1315
+ wp user set-role username editor --url=site1.example.com
1316
+ ```
1317
+
1318
+ ## Tips and Gotchas
1319
+
1320
+ - **Super Admin bypass**: Super Admins bypass all capability checks. Use this role sparingly.
1321
+ - **User exists once**: A user account exists once in the network but can have different roles on different sub-sites.
1322
+ - **Cannot demote yourself**: The last Super Admin cannot remove their own super admin status.
1323
+ - **wp-admin vs network-admin**: Super Admins see both site-level wp-admin and network-level wp-admin/network/.
1324
+ - **Plugin capability checks**: Plugins using `current_user_can()` should work correctly with multisite, but some older plugins may not distinguish Super Admin from site Administrator.
1325
+ ```
1326
+
1327
+ **Step 1f: migration-multisite.md**
1328
+
1329
+ ```markdown
1330
+ # Migration: Single-site to Multisite and Back
1331
+
1332
+ Migrating between single-site and multisite WordPress installations requires careful planning due to database structure differences.
1333
+
1334
+ ## Single-site to Multisite
1335
+
1336
+ ### Prerequisites
1337
+ - WordPress installed at domain root (not a subdirectory)
1338
+ - All plugins deactivated
1339
+ - Permalink structure using "pretty permalinks" (not plain)
1340
+ - Full database and file backup
1341
+
1342
+ ### Procedure
1343
+
1344
+ 1. **Backup**: Full database dump + wp-content directory
1345
+ 2. **Deactivate all plugins** via wp-admin or wp-cli
1346
+ 3. **Enable multisite**: Add `define('WP_ALLOW_MULTISITE', true);` to wp-config.php
1347
+ 4. **Network Setup**: Navigate to Tools > Network Setup, choose sub-directory or sub-domain
1348
+ 5. **Apply configuration**: Copy generated code to wp-config.php and .htaccess
1349
+ 6. **Re-login**: WordPress redirects to login — sign in as Super Admin
1350
+ 7. **Re-activate plugins**: One by one, test each plugin for multisite compatibility
1351
+ 8. **Verify**: Check permalink structure, media uploads, and user roles
1352
+
1353
+ ### What Changes in the Database
1354
+
1355
+ | Component | Before | After |
1356
+ |-----------|--------|-------|
1357
+ | Tables | `wp_posts`, `wp_options`, ... | Same (become site 1) |
1358
+ | New tables | — | `wp_blogs`, `wp_site`, `wp_sitemeta`, `wp_registration_log`, `wp_signups` |
1359
+ | Options | `wp_options` | `wp_options` (site 1) + `wp_sitemeta` (network) |
1360
+
1361
+ ## Multisite to Single-site
1362
+
1363
+ This migration is more complex because you need to extract one sub-site from the network.
1364
+
1365
+ ### Procedure (extract sub-site)
1366
+
1367
+ 1. **Backup**: Full database dump + wp-content directory
1368
+ 2. **Export content**: Use WordPress Export (Tools > Export) on the target sub-site
1369
+ 3. **Fresh WordPress install**: Install a clean single-site WordPress
1370
+ 4. **Import content**: Use WordPress Importer plugin
1371
+ 5. **Copy uploads**: Copy `wp-content/uploads/sites/{blog_id}/` to `wp-content/uploads/`
1372
+ 6. **Activate theme and plugins**: Install and activate the same theme and plugins
1373
+ 7. **Verify**: Check media URLs, internal links, shortcodes
1374
+
1375
+ ### Alternative: Direct Database Extraction
1376
+
1377
+ For large sites where export/import is impractical:
1378
+
1379
+ 1. Export tables with prefix `wp_{blog_id}_` (e.g., `wp_2_posts`, `wp_2_options`)
1380
+ 2. Rename tables to standard prefix (e.g., `wp_2_posts` → `wp_posts`)
1381
+ 3. Update `siteurl` and `home` in `wp_options`
1382
+ 4. Search-replace old URLs in content
1383
+ 5. Remove multisite constants from wp-config.php
1384
+ 6. Update .htaccess to standard WordPress rules
1385
+
1386
+ ## WP-CLI Migration Commands
1387
+
1388
+ ```bash
1389
+ # Export single site from multisite
1390
+ wp db export site-backup.sql --url=subsite.example.com
1391
+
1392
+ # Search-replace URLs after migration
1393
+ wp search-replace 'subsite.example.com' 'newdomain.com' --all-tables
1394
+
1395
+ # Export content as WXR
1396
+ wp export --url=subsite.example.com --dir=/tmp/exports/
1397
+ ```
1398
+
1399
+ ## Tips and Gotchas
1400
+
1401
+ - **Media paths**: Multisite stores uploads in `uploads/sites/{blog_id}/`. After migration to single-site, media URLs need search-replace.
1402
+ - **User roles**: Users may have different roles on different sub-sites. When extracting, only the target site's role assignments transfer.
1403
+ - **Plugins**: Some plugins store network-wide options in `wp_sitemeta`. These are lost when extracting to single-site.
1404
+ - **Test first**: Always perform migration on a staging environment before production.
1405
+ - **Backup twice**: Keep backups of both the source (multisite) and target (single-site) before starting.
1406
+ ```
1407
+
1408
+ ---
1409
+
1410
+ ## Task 9: Update wp-site-manager agent
1411
+
1412
+ **Files:**
1413
+ - Modify: `agents/wp-site-manager.md`
1414
+
1415
+ **Step 1: Add Multisite section and update delegation table**
1416
+
1417
+ After the "### Multi-Site Operations" section (after line 103), add a new section:
1418
+
1419
+ ```markdown
1420
+
1421
+ ### Multisite Network Management
1422
+ For WordPress Multisite networks (sites with `is_multisite: true` in WP_SITES_CONFIG):
1423
+
1424
+ **Prerequisites check:**
1425
+ 1. Verify the site is multisite: `ms_list_sites` (will error if not multisite)
1426
+ 2. Verify wp-cli access is configured (`wp_path` in config)
1427
+
1428
+ **Sub-site operations:**
1429
+ - List sub-sites → `ms_list_sites`
1430
+ - Create sub-site → `ms_create_site` (slug, title, admin email)
1431
+ - Activate/deactivate → `ms_activate_site`
1432
+ - Delete → `ms_delete_site` (requires `confirm: true`)
1433
+
1434
+ **Network administration:**
1435
+ - List plugins with network status → `ms_list_network_plugins`
1436
+ - Network-activate → `ms_network_activate_plugin`
1437
+ - Network-deactivate → `ms_network_deactivate_plugin`
1438
+ - List Super Admins → `ms_list_super_admins`
1439
+ - Network settings → `ms_get_network_settings`
1440
+
1441
+ **Safety rules for multisite:**
1442
+ - NEVER delete blog_id 1 (main site)
1443
+ - ALWAYS confirm before network-activating plugins (affects ALL sites)
1444
+ - Announce which network you're operating on when multiple multisite networks are configured
1445
+ ```
1446
+
1447
+ Also update the Specialized Agents delegation table to add multisite cross-reference. In the table at the end (line 124), add a row:
1448
+
1449
+ ```markdown
1450
+ | Multisite network management | `wp-site-manager` (this agent) | Sub-sites, network plugins, Super Admin — see section above |
1451
+ ```
1452
+
1453
+ ---
1454
+
1455
+ ## Task 10: Update router decision-tree.md to v6
1456
+
1457
+ **Files:**
1458
+ - Modify: `skills/wordpress-router/references/decision-tree.md`
1459
+
1460
+ **Step 1: Update version header**
1461
+
1462
+ Change line 1 from:
1463
+ ```
1464
+ # Router decision tree (v4 — development + local environment + operations)
1465
+ ```
1466
+ to:
1467
+ ```
1468
+ # Router decision tree (v6 — development + local environment + operations + multisite)
1469
+ ```
1470
+
1471
+ **Step 2: Add multisite keywords to Step 0**
1472
+
1473
+ In the "Keywords that indicate **operations**:" section (line 17), add multisite keywords:
1474
+
1475
+ Change:
1476
+ ```
1477
+ deploy, push to production, audit, security check, backup, restore, migrate, move site, create post, manage content, site status, check plugins, performance check, SEO audit, WooCommerce, prodotto, ordine, coupon, negozio, catalogo, inventario, vendite, carrello
1478
+ ```
1479
+ to:
1480
+ ```
1481
+ deploy, push to production, audit, security check, backup, restore, migrate, move site, create post, manage content, site status, check plugins, performance check, SEO audit, WooCommerce, prodotto, ordine, coupon, negozio, catalogo, inventario, vendite, carrello, multisite, network, sub-site, sub-sito, domain mapping, super admin, network activate
1482
+ ```
1483
+
1484
+ **Step 3: Add multisite routing entry to Step 2b**
1485
+
1486
+ After the WooCommerce entry (line 90), add:
1487
+
1488
+ ```markdown
1489
+ - **Multisite / network / sub-sites / domain mapping / super admin / network activate**
1490
+ → `wp-multisite` skill + `wp-site-manager` agent
1491
+ ```
1492
+
1493
+ ---
1494
+
1495
+ ## Task 11: Add cross-references to existing skills
1496
+
1497
+ **Files:**
1498
+ - Modify: `skills/wp-wpcli-and-ops/SKILL.md`
1499
+ - Modify: `skills/wp-security/SKILL.md`
1500
+
1501
+ **Step 1: Add cross-ref to wp-wpcli-and-ops SKILL.md**
1502
+
1503
+ Add at the end of the file:
1504
+
1505
+ ```markdown
1506
+
1507
+ ### Multisite Operations
1508
+
1509
+ For WordPress Multisite network management (sub-sites, network plugins, Super Admin), see the `wp-multisite` skill which provides 10 dedicated MCP tools.
1510
+ ```
1511
+
1512
+ **Step 2: Add cross-ref to wp-security SKILL.md**
1513
+
1514
+ Add at the end of the file:
1515
+
1516
+ ```markdown
1517
+
1518
+ ### Multisite Security
1519
+
1520
+ For Super Admin capabilities and multisite-specific security considerations, see the `wp-multisite` skill (`references/user-roles.md`).
1521
+ ```
1522
+
1523
+ ---
1524
+
1525
+ ## Task 12: Version bump + CHANGELOG
1526
+
1527
+ **Files:**
1528
+ - Modify: `.claude-plugin/plugin.json`
1529
+ - Modify: `package.json`
1530
+ - Modify: `CHANGELOG.md`
1531
+
1532
+ **Step 1: plugin.json — version 1.9.0**
1533
+
1534
+ Update `version` from `"1.8.0"` to `"1.9.0"`.
1535
+ Update `description` to mention 26 skills, 81+ MCP tools, multisite.
1536
+
1537
+ **Step 2: package.json — version 1.9.0**
1538
+
1539
+ Update `version` from `"1.8.0"` to `"1.9.0"`.
1540
+ Add `"multisite"` and `"network"` to `keywords` array.
1541
+
1542
+ **Step 3: CHANGELOG.md — add v1.9.0 entry**
1543
+
1544
+ Add at the top (after the header):
1545
+
1546
+ ```markdown
1547
+ ## [1.9.0] — 2026-02-28
1548
+
1549
+ ### Added
1550
+ - **WordPress Multisite support** — 10 new MCP tools for network management
1551
+ - **WP-CLI execution module** (`wpcli.ts`) — local and SSH remote command execution
1552
+ - **New skill**: `wp-multisite` with 6 reference files (network setup, site management, domain mapping, network plugins, user roles, migration)
1553
+ - **Detection script**: `multisite_inspect.mjs` — detects multisite configuration
1554
+ - **SiteConfig extended**: `wp_path`, `ssh_host`, `ssh_user`, `ssh_key`, `ssh_port`, `is_multisite` fields
1555
+ - **Router v6**: multisite keywords and routing to wp-multisite skill + wp-site-manager agent
1556
+
1557
+ ### New MCP Tools (10)
1558
+ - `ms_list_sites` — List all sub-sites in the network
1559
+ - `ms_get_site` — Get sub-site details
1560
+ - `ms_create_site` — Create a new sub-site
1561
+ - `ms_activate_site` — Activate or deactivate a sub-site
1562
+ - `ms_delete_site` — Delete a sub-site (with safety gate)
1563
+ - `ms_list_network_plugins` — List plugins with network activation status (REST)
1564
+ - `ms_network_activate_plugin` — Network-activate a plugin (wp-cli)
1565
+ - `ms_network_deactivate_plugin` — Network-deactivate a plugin (wp-cli)
1566
+ - `ms_list_super_admins` — List Super Admin users (wp-cli)
1567
+ - `ms_get_network_settings` — Get network-wide settings (wp-cli)
1568
+
1569
+ ### Changed
1570
+ - `wp-site-manager` agent: added Multisite Network Management section
1571
+ - Router decision-tree.md upgraded to v6 with multisite keywords
1572
+ - `wp-wpcli-and-ops` and `wp-security` skills: added multisite cross-references
1573
+ ```
1574
+
1575
+ **Step 4: Build final**
1576
+
1577
+ Run: `cd servers/wp-rest-bridge && npx tsc`
1578
+ Expected: clean build, 81 total tools
1579
+
1580
+ ---
1581
+
1582
+ ## Execution Order
1583
+
1584
+ ```
1585
+ Phase 1 — TypeScript (sequential, dependencies):
1586
+ Task 1: types.ts (WPNetworkSite)
1587
+ Task 2: wordpress.ts (SiteConfig) + wpcli.ts (NEW)
1588
+ Task 3: multisite-sites.ts (5 tools)
1589
+ Task 4: multisite-network.ts (5 tools)
1590
+ Task 5: index.ts (register 10 tools) + build
1591
+
1592
+ Phase 2 — Skill & Detection (parallelizable):
1593
+ Task 6: multisite_inspect.mjs
1594
+ Task 7: SKILL.md
1595
+ Task 8: 6 reference files
1596
+
1597
+ Phase 3 — Integration (parallelizable):
1598
+ Task 9: wp-site-manager.md update
1599
+ Task 10: decision-tree.md → v6
1600
+ Task 11: cross-references (2 SKILL.md)
1601
+
1602
+ Phase 4 — Finalize:
1603
+ Task 12: version bump + CHANGELOG + final build
1604
+ ```