@wickedevolutions/abilities-mcp 1.3.1

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.
@@ -0,0 +1,595 @@
1
+ 'use strict';
2
+
3
+ const { spawn, execSync, execFileSync } = require('child_process');
4
+
5
+ function shellQuote(s) {
6
+ return "'" + String(s).replace(/'/g, "'\\''") + "'";
7
+ }
8
+
9
+ const MAX_PENDING = 100;
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Constants (matching mcp-ssh-bridge.js v2.3.0)
13
+ // ---------------------------------------------------------------------------
14
+
15
+ const RECONNECT_MAX_RETRIES = 10;
16
+ const RECONNECT_BASE_DELAY = 1000; // 1 second
17
+ const RECONNECT_MAX_DELAY = 30000; // 30 seconds
18
+ const HEALTHCHECK_INTERVAL = 45000; // 45 seconds
19
+ const HEALTHCHECK_TIMEOUT = 10000; // 10 seconds to respond to ping
20
+ const REQUEST_TIMEOUT = 120000; // 2 minutes per request
21
+
22
+ /**
23
+ * SSH Transport — connects to a remote WordPress site via SSH + WP-CLI STDIO.
24
+ *
25
+ * Refactored from mcp-ssh-bridge.js v2.3.0. Same resilience features:
26
+ * auto-reconnect, handshake replay, request queuing, healthcheck pings,
27
+ * orphan cleanup, exponential backoff.
28
+ *
29
+ * @fires onMessage(parsedMsg) — when a JSON-RPC response arrives from server
30
+ *
31
+ * Copyright (C) 2026 Influencentricity | Wicked Evolutions
32
+ * @license GPL-2.0-or-later
33
+ */
34
+ class SshTransport {
35
+
36
+ constructor({ host, path, user, mcpServer, subsiteUrl, logger }) {
37
+ this.host = host;
38
+ this.wpPath = path;
39
+ this.wpUser = user || '';
40
+ this.mcpServer = mcpServer || 'mcp-adapter-default-server';
41
+ this.subsiteUrl = subsiteUrl || null;
42
+ this.log = logger || function noop() {};
43
+
44
+ // Build WP-CLI command
45
+ this.wpCmd = this._buildWpCmd();
46
+
47
+ // SSH child process
48
+ this.child = null;
49
+ this.childReady = false;
50
+ this.reconnecting = false;
51
+ this.reconnectAttempts = 0;
52
+ this.outputBuffer = '';
53
+
54
+ // Handshake cache (set externally by connection pool)
55
+ this.cachedInitializeRequest = null;
56
+ this.cachedInitializedNotification = null;
57
+ this.clientProtocolVersion = null;
58
+
59
+ // Request queue + in-flight tracking
60
+ this.pendingRequests = [];
61
+ this.inflightRequests = new Map();
62
+ this.reconnectWaiters = {};
63
+
64
+ // Healthcheck
65
+ this.healthcheckTimer = null;
66
+ this.healthcheckPendingId = null;
67
+ this.healthcheckTimeout = null;
68
+
69
+ // Shutdown flag
70
+ this.shuttingDown = false;
71
+
72
+ // Callback: (parsedMsg: object) => void
73
+ this.onMessage = null;
74
+ }
75
+
76
+ // ---------------------------------------------------------------------------
77
+ // Public API
78
+ // ---------------------------------------------------------------------------
79
+
80
+ /**
81
+ * Spawn SSH child and optionally replay handshake.
82
+ */
83
+ async connect() {
84
+ this._remoteCleanup();
85
+ this.child = this._spawnChild();
86
+ this.log(`[${this.host}] SSH child spawned`);
87
+ }
88
+
89
+ /**
90
+ * Send a JSON-RPC line to this transport's SSH child.
91
+ */
92
+ send(line) {
93
+ if (this.reconnecting) {
94
+ // Queue during reconnection
95
+ try {
96
+ const msg = JSON.parse(line);
97
+ if (msg.id !== undefined && msg.method) {
98
+ this.log(`[${this.host}] Queuing request ${msg.id} (${msg.method}) — reconnecting`);
99
+ this._queueOrReject(line);
100
+ }
101
+ } catch (e) {
102
+ this._queueOrReject(line);
103
+ }
104
+ return;
105
+ }
106
+
107
+ if (this.child && this.childReady && !this.child.killed) {
108
+ try {
109
+ this.child.stdin.write(line + '\n');
110
+ this._trackInflight(line);
111
+ } catch (e) {
112
+ this.log(`[${this.host}] Write failed: ${e.message}`);
113
+ this._queueOrReject(line);
114
+ }
115
+ } else if (this.child && !this.child.killed && !this.childReady) {
116
+ // Child exists but not ready — queue non-init messages
117
+ try {
118
+ const msg = JSON.parse(line);
119
+ if (msg.method === 'initialize') {
120
+ this.child.stdin.write(line + '\n');
121
+ } else if (msg.method === 'initialized' || msg.method === 'notifications/initialized') {
122
+ this.child.stdin.write(line + '\n');
123
+ this.childReady = true;
124
+ this.log(`[${this.host}] childReady set via client-driven handshake`);
125
+ this._drainPendingRequests();
126
+ this._startHealthcheck();
127
+ } else if (msg.id !== undefined && msg.method) {
128
+ this._queueOrReject(line);
129
+ }
130
+ } catch (e) {
131
+ this._queueOrReject(line);
132
+ }
133
+ } else {
134
+ // No child — queue
135
+ this._queueOrReject(line);
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Graceful shutdown — kill child, remote cleanup.
141
+ */
142
+ async shutdown() {
143
+ this.shuttingDown = true;
144
+ this._stopHealthcheck();
145
+ this._safeKillChild();
146
+ this._remoteCleanup();
147
+ }
148
+
149
+ isReady() {
150
+ return this.childReady && !this.reconnecting && !this.shuttingDown;
151
+ }
152
+
153
+ /**
154
+ * Perform MCP initialize handshake on this transport.
155
+ * Called by the connection pool for lazy-connected sites.
156
+ * Returns the initialize response (parsed JSON-RPC object).
157
+ */
158
+ async performHandshake(initReq, initializedNotif) {
159
+ this.cachedInitializeRequest = initReq;
160
+ this.cachedInitializedNotification = initializedNotif;
161
+ if (initReq.params && initReq.params.protocolVersion) {
162
+ this.clientProtocolVersion = initReq.params.protocolVersion;
163
+ }
164
+
165
+ // Send initialize request
166
+ const initLine = JSON.stringify(initReq) + '\n';
167
+ this.child.stdin.write(initLine);
168
+ this.log(`[${this.host}] HANDSHAKE > SERVER: initialize (id=${initReq.id})`);
169
+
170
+ // Wait for response
171
+ const response = await this._waitForResponse(initReq.id, 15000);
172
+ if (!response) {
173
+ throw new Error(`No initialize response from ${this.host} within 15s`);
174
+ }
175
+
176
+ // Protocol version negotiation is handled server-side (InitializeHandler v1.0.7+).
177
+
178
+ // Send initialized notification
179
+ if (initializedNotif) {
180
+ const notifLine = JSON.stringify(initializedNotif) + '\n';
181
+ this.child.stdin.write(notifLine);
182
+ this.log(`[${this.host}] HANDSHAKE > SERVER: initialized`);
183
+ }
184
+
185
+ this.childReady = true;
186
+ this._drainPendingRequests();
187
+ this._startHealthcheck();
188
+
189
+ return response;
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // SSH child process management
194
+ // ---------------------------------------------------------------------------
195
+
196
+ _buildWpCmd() {
197
+ let cmd = `cd ${this.wpPath} && timeout 2h wp mcp-adapter serve --server=${this.mcpServer}`;
198
+ if (this.subsiteUrl) {
199
+ cmd += ` --url=${this.subsiteUrl}`;
200
+ }
201
+ if (this.wpUser) {
202
+ cmd += ` --user=${this.wpUser}`;
203
+ }
204
+ cmd += ' 2>/dev/null';
205
+ return cmd;
206
+ }
207
+
208
+ _spawnChild() {
209
+ this.log(`[${this.host}] Spawning: ssh -T ${this.host} '${this.wpCmd}'`);
210
+
211
+ const proc = spawn('ssh', [
212
+ '-T',
213
+ '-o', 'BatchMode=yes',
214
+ '-o', 'ServerAliveInterval=30',
215
+ '-o', 'ServerAliveCountMax=3',
216
+ this.host,
217
+ this.wpCmd,
218
+ ], {
219
+ stdio: ['pipe', 'pipe', 'pipe'],
220
+ });
221
+
222
+ proc.stderr.on('data', (chunk) => {
223
+ this.log(`[${this.host}] SSH STDERR: ${chunk.toString().trim()}`);
224
+ });
225
+
226
+ proc.on('error', (err) => {
227
+ this.log(`[${this.host}] Child spawn error: ${err.message}`);
228
+ this._handleChildDeath(-1);
229
+ });
230
+
231
+ proc.on('close', (code) => {
232
+ this.log(`[${this.host}] Child exited with code ${code}`);
233
+ if (proc === this.child) {
234
+ this._handleChildDeath(code);
235
+ }
236
+ });
237
+
238
+ proc.stdout.on('data', (chunk) => {
239
+ this._handleChildStdout(chunk);
240
+ });
241
+
242
+ return proc;
243
+ }
244
+
245
+ _handleChildDeath(code) {
246
+ this.childReady = false;
247
+ this._stopHealthcheck();
248
+
249
+ // Fail any in-flight requests
250
+ for (const [id, timer] of this.inflightRequests) {
251
+ clearTimeout(timer);
252
+ this._sendError(id, -32603, 'Server connection lost, reconnecting...');
253
+ }
254
+ this.inflightRequests.clear();
255
+
256
+ if (this.shuttingDown) return;
257
+ if (this.reconnecting) return;
258
+
259
+ this.log(`[${this.host}] Child died (code ${code}), reconnecting`);
260
+ this._attemptReconnect();
261
+ }
262
+
263
+ // ---------------------------------------------------------------------------
264
+ // Reconnection with exponential backoff
265
+ // ---------------------------------------------------------------------------
266
+
267
+ async _attemptReconnect() {
268
+ this.reconnecting = true;
269
+
270
+ while (this.reconnectAttempts < RECONNECT_MAX_RETRIES && !this.shuttingDown) {
271
+ this.reconnectAttempts++;
272
+ const delay = Math.min(
273
+ RECONNECT_BASE_DELAY * Math.pow(2, this.reconnectAttempts - 1),
274
+ RECONNECT_MAX_DELAY
275
+ );
276
+ this.log(`[${this.host}] Reconnect ${this.reconnectAttempts}/${RECONNECT_MAX_RETRIES} after ${delay}ms`);
277
+
278
+ await this._sleep(delay);
279
+ if (this.shuttingDown) break;
280
+
281
+ try {
282
+ this.child = this._spawnChild();
283
+ this.outputBuffer = '';
284
+
285
+ if (this.cachedInitializeRequest) {
286
+ this.log(`[${this.host}] Replaying handshake`);
287
+
288
+ const initReq = JSON.stringify(this.cachedInitializeRequest) + '\n';
289
+ this.child.stdin.write(initReq);
290
+
291
+ const initResponse = await this._waitForResponse(this.cachedInitializeRequest.id, 15000);
292
+ if (!initResponse) {
293
+ this.log(`[${this.host}] No init response during reconnect`);
294
+ this._safeKillChild();
295
+ continue;
296
+ }
297
+
298
+ // Protocol version negotiation is handled server-side (InitializeHandler v1.0.7+).
299
+
300
+ if (this.cachedInitializedNotification) {
301
+ const notif = JSON.stringify(this.cachedInitializedNotification) + '\n';
302
+ this.child.stdin.write(notif);
303
+ }
304
+
305
+ this.childReady = true;
306
+ this.reconnectAttempts = 0;
307
+ this.reconnecting = false;
308
+ this.log(`[${this.host}] Reconnect successful`);
309
+
310
+ this._drainPendingRequests();
311
+ this._startHealthcheck();
312
+ return;
313
+ } else {
314
+ this.childReady = true;
315
+ this.reconnectAttempts = 0;
316
+ this.reconnecting = false;
317
+ this.log(`[${this.host}] Reconnect successful (pre-init)`);
318
+ return;
319
+ }
320
+ } catch (err) {
321
+ this.log(`[${this.host}] Reconnect attempt failed: ${err.message}`);
322
+ this._safeKillChild();
323
+ }
324
+ }
325
+
326
+ if (!this.shuttingDown) {
327
+ this.log(`[${this.host}] All reconnect attempts exhausted`);
328
+ // Fail pending requests
329
+ for (const req of this.pendingRequests) {
330
+ try {
331
+ const msg = JSON.parse(req.raw);
332
+ if (msg.id !== undefined) {
333
+ this._sendError(msg.id, -32603, 'Server connection lost after all reconnect attempts');
334
+ }
335
+ } catch (e) { /* ignore */ }
336
+ }
337
+ this.pendingRequests = [];
338
+ this.reconnecting = false;
339
+ }
340
+ }
341
+
342
+ _waitForResponse(id, timeoutMs) {
343
+ return new Promise((resolve) => {
344
+ const timer = setTimeout(() => {
345
+ delete this.reconnectWaiters[id];
346
+ resolve(null);
347
+ }, timeoutMs);
348
+
349
+ this.reconnectWaiters[id] = (msg) => {
350
+ clearTimeout(timer);
351
+ delete this.reconnectWaiters[id];
352
+ resolve(msg);
353
+ };
354
+ });
355
+ }
356
+
357
+ _safeKillChild() {
358
+ try {
359
+ if (this.child && !this.child.killed) {
360
+ this.child.kill('SIGTERM');
361
+ }
362
+ } catch (e) {
363
+ this.log(`[${this.host}] Error killing child: ${e.message}`);
364
+ }
365
+ this.child = null;
366
+ this.childReady = false;
367
+ }
368
+
369
+ _remoteCleanup() {
370
+ try {
371
+ let killPattern = this.wpPath + '.*mcp-adapter serve --server=' + this.mcpServer;
372
+ if (this.subsiteUrl) {
373
+ killPattern += '.*--url=' + this.subsiteUrl;
374
+ }
375
+ const remoteCmd = `pkill -f ${shellQuote(killPattern)}`;
376
+ this.log(`[${this.host}] Remote cleanup: ${remoteCmd}`);
377
+ execFileSync('ssh', [
378
+ '-o', 'BatchMode=yes',
379
+ '-o', 'ConnectTimeout=5',
380
+ this.host,
381
+ remoteCmd,
382
+ ], {
383
+ timeout: 10000,
384
+ stdio: 'ignore',
385
+ });
386
+ } catch (e) {
387
+ this.log(`[${this.host}] Remote cleanup finished (exit: ${e.status || 'unknown'})`);
388
+ }
389
+ }
390
+
391
+ _sleep(ms) {
392
+ return new Promise(resolve => setTimeout(resolve, ms));
393
+ }
394
+
395
+ // ---------------------------------------------------------------------------
396
+ // Pending request queue
397
+ // ---------------------------------------------------------------------------
398
+
399
+ _queueOrReject(line) {
400
+ if (this.pendingRequests.length >= MAX_PENDING) {
401
+ this.log(`[${this.host}] Pending queue full — rejecting`);
402
+ try {
403
+ const msg = JSON.parse(line);
404
+ if (msg.id !== undefined && this.onMessage) {
405
+ const errorMsg = {
406
+ jsonrpc: '2.0', id: msg.id,
407
+ error: { code: -32603, message: 'Transport queue full' }
408
+ };
409
+ this.onMessage(errorMsg, JSON.stringify(errorMsg));
410
+ }
411
+ } catch (e) { /* ignore */ }
412
+ return;
413
+ }
414
+ this.pendingRequests.push({ raw: line });
415
+ }
416
+
417
+ _drainPendingRequests() {
418
+ if (this.pendingRequests.length === 0) return;
419
+ this.log(`[${this.host}] Draining ${this.pendingRequests.length} pending request(s)`);
420
+
421
+ const queued = this.pendingRequests.slice();
422
+ this.pendingRequests = [];
423
+
424
+ for (const req of queued) {
425
+ if (this.child && this.childReady) {
426
+ this.child.stdin.write(req.raw + '\n');
427
+ this._trackInflight(req.raw);
428
+ } else {
429
+ this.pendingRequests.push(req);
430
+ }
431
+ }
432
+ }
433
+
434
+ // ---------------------------------------------------------------------------
435
+ // In-flight request tracking
436
+ // ---------------------------------------------------------------------------
437
+
438
+ _trackInflight(rawLine) {
439
+ try {
440
+ const msg = JSON.parse(rawLine);
441
+ if (msg.id !== undefined && msg.method) {
442
+ const timer = setTimeout(() => {
443
+ this.log(`[${this.host}] Request ${msg.id} timed out after ${REQUEST_TIMEOUT}ms`);
444
+ this.inflightRequests.delete(msg.id);
445
+ this._sendError(msg.id, -32603, `Request timed out after ${REQUEST_TIMEOUT / 1000}s`);
446
+ }, REQUEST_TIMEOUT);
447
+ this.inflightRequests.set(msg.id, timer);
448
+ }
449
+ } catch (e) { /* not JSON */ }
450
+ }
451
+
452
+ _resolveInflight(id) {
453
+ const timer = this.inflightRequests.get(id);
454
+ if (timer) {
455
+ clearTimeout(timer);
456
+ this.inflightRequests.delete(id);
457
+ }
458
+ }
459
+
460
+ // ---------------------------------------------------------------------------
461
+ // Healthcheck
462
+ // ---------------------------------------------------------------------------
463
+
464
+ _startHealthcheck() {
465
+ this._stopHealthcheck();
466
+ this.healthcheckTimer = setInterval(() => {
467
+ if (!this.child || !this.childReady || this.reconnecting) return;
468
+ this._sendPing();
469
+ }, HEALTHCHECK_INTERVAL);
470
+ }
471
+
472
+ _stopHealthcheck() {
473
+ if (this.healthcheckTimer) { clearInterval(this.healthcheckTimer); this.healthcheckTimer = null; }
474
+ if (this.healthcheckTimeout) { clearTimeout(this.healthcheckTimeout); this.healthcheckTimeout = null; }
475
+ this.healthcheckPendingId = null;
476
+ }
477
+
478
+ _sendPing() {
479
+ this.healthcheckPendingId = `__bridge_ping_${Date.now()}`;
480
+ const ping = JSON.stringify({ jsonrpc: '2.0', id: this.healthcheckPendingId, method: 'ping' }) + '\n';
481
+
482
+ try {
483
+ this.child.stdin.write(ping);
484
+ } catch (e) {
485
+ this.log(`[${this.host}] Ping write failed: ${e.message}`);
486
+ this._handleChildDeath(-1);
487
+ return;
488
+ }
489
+
490
+ this.healthcheckTimeout = setTimeout(() => {
491
+ this.log(`[${this.host}] Healthcheck timed out`);
492
+ this.healthcheckPendingId = null;
493
+ this._safeKillChild();
494
+ this._handleChildDeath(-1);
495
+ }, HEALTHCHECK_TIMEOUT);
496
+ }
497
+
498
+ // ---------------------------------------------------------------------------
499
+ // Child stdout processing
500
+ // ---------------------------------------------------------------------------
501
+
502
+ _handleChildStdout(chunk) {
503
+ this.outputBuffer += chunk.toString();
504
+
505
+ let newlineIdx;
506
+ while ((newlineIdx = this.outputBuffer.indexOf('\n')) !== -1) {
507
+ const line = this.outputBuffer.slice(0, newlineIdx);
508
+ this.outputBuffer = this.outputBuffer.slice(newlineIdx + 1);
509
+
510
+ if (!line.trim()) continue;
511
+
512
+ let msg = null;
513
+ try {
514
+ msg = JSON.parse(line);
515
+ } catch (e) {
516
+ // Not valid JSON — forward as-is
517
+ if (this.onMessage) this.onMessage(null, line);
518
+ continue;
519
+ }
520
+
521
+ // Internal: healthcheck pong
522
+ if (msg.id && msg.id === this.healthcheckPendingId) {
523
+ this.log(`[${this.host}] Healthcheck pong received`);
524
+ if (this.healthcheckTimeout) { clearTimeout(this.healthcheckTimeout); this.healthcheckTimeout = null; }
525
+ this.healthcheckPendingId = null;
526
+ continue;
527
+ }
528
+
529
+ // Internal: reconnect handshake waiter
530
+ if (msg.id !== undefined && this.reconnectWaiters[msg.id]) {
531
+ this.reconnectWaiters[msg.id](msg);
532
+ continue;
533
+ }
534
+
535
+ // Resolve in-flight tracking
536
+ if (msg.id !== undefined && !msg.method) {
537
+ this._resolveInflight(msg.id);
538
+ }
539
+
540
+ // Protocol version negotiation is handled server-side (InitializeHandler v1.0.7+).
541
+
542
+ // Fold _metadata.input_schema into error text content for client visibility
543
+ if (msg.result && msg.result.isError && msg.result._metadata && msg.result._metadata.input_schema) {
544
+ const content = msg.result.content;
545
+ if (Array.isArray(content) && content.length > 0 && content[0].type === 'text') {
546
+ const schema = msg.result._metadata.input_schema;
547
+ const required = schema.required || [];
548
+ const props = schema.properties || {};
549
+ const paramList = Object.entries(props).map(([k, v]) => {
550
+ const req = required.includes(k) ? ' (required)' : '';
551
+ return ` ${k}: ${v.type || 'any'}${req} — ${v.description || ''}`;
552
+ }).join('\n');
553
+ content[0].text += `\n\nExpected parameters:\n${paramList}`;
554
+ }
555
+ }
556
+
557
+ // Forward to callback
558
+ if (this.onMessage) {
559
+ this.onMessage(msg, JSON.stringify(msg));
560
+ }
561
+ }
562
+ }
563
+
564
+ // ---------------------------------------------------------------------------
565
+ // Error helper
566
+ // ---------------------------------------------------------------------------
567
+
568
+ _sendError(id, code, message) {
569
+ if (this.onMessage) {
570
+ this.onMessage({
571
+ jsonrpc: '2.0',
572
+ id: id,
573
+ error: { code, message },
574
+ });
575
+ }
576
+ }
577
+
578
+ // ---------------------------------------------------------------------------
579
+ // Static: SSH_AUTH_SOCK discovery (call once from main)
580
+ // ---------------------------------------------------------------------------
581
+
582
+ static ensureSshAuthSock() {
583
+ if (process.env.SSH_AUTH_SOCK) return;
584
+ try {
585
+ const sock = execSync('launchctl getenv SSH_AUTH_SOCK', { encoding: 'utf8' }).trim();
586
+ if (sock) {
587
+ process.env.SSH_AUTH_SOCK = sock;
588
+ }
589
+ } catch (e) {
590
+ // Not on macOS or launchd not available — fine
591
+ }
592
+ }
593
+ }
594
+
595
+ module.exports = { SshTransport };
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@wickedevolutions/abilities-mcp",
3
+ "version": "1.3.1",
4
+ "description": "Open-source MCP bridge connecting AI clients to WordPress through the Abilities API — multi-site routing, zero dependencies",
5
+ "main": "abilities-mcp.js",
6
+ "bin": {
7
+ "abilities-mcp": "./abilities-mcp.js"
8
+ },
9
+ "files": ["abilities-mcp.js", "lib/", "wp-sites.example.json", "LICENSE", "README.md", "CHANGELOG.md"],
10
+ "keywords": ["mcp", "wordpress", "bridge", "abilities", "ai", "open-source", "multi-site", "model-context-protocol"],
11
+ "license": "GPL-2.0-or-later",
12
+ "author": "Wicked Evolutions",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/Wicked-Evolutions/abilities-mcp.git"
16
+ },
17
+ "scripts": {
18
+ "test": "node --test test/*.test.js"
19
+ },
20
+ "engines": {
21
+ "node": ">=18.0.0"
22
+ }
23
+ }
@@ -0,0 +1,49 @@
1
+ {
2
+ "defaultSite": "mysite",
3
+ "sites": {
4
+ "mysite": {
5
+ "label": "My WordPress Site",
6
+ "url": "https://example.com",
7
+ "transport": "http",
8
+ "http": {
9
+ "endpoint": "https://example.com/wp-json/mcp/mcp-adapter-default-server",
10
+ "username": "mcp-agent",
11
+ "passwordCommand": "security find-generic-password -a mcp-agent -s example.com -w"
12
+ }
13
+ },
14
+ "staging": {
15
+ "label": "Staging Site",
16
+ "url": "https://staging.example.com",
17
+ "transport": "http",
18
+ "http": {
19
+ "endpoint": "https://staging.example.com/wp-json/mcp/mcp-adapter-default-server",
20
+ "username": "mcp-agent",
21
+ "password": "xxxx xxxx xxxx xxxx"
22
+ }
23
+ },
24
+ "legacy-ssh": {
25
+ "label": "SSH Site (legacy transport)",
26
+ "transport": "ssh",
27
+ "ssh": {
28
+ "host": "my-ssh-host",
29
+ "path": "~/public_html",
30
+ "user": "wpaiagent"
31
+ }
32
+ },
33
+ "network": {
34
+ "label": "WordPress Multisite",
35
+ "url": "https://network.example.com",
36
+ "transport": "http",
37
+ "http": {
38
+ "endpoint": "https://network.example.com/wp-json/mcp/mcp-adapter-default-server",
39
+ "username": "mcp-agent",
40
+ "passwordEnv": "WP_NETWORK_PASSWORD"
41
+ },
42
+ "multisite": {
43
+ "main": "https://network.example.com/",
44
+ "blog": "https://blog.network.example.com/",
45
+ "shop": "https://shop.network.example.com/"
46
+ }
47
+ }
48
+ }
49
+ }