@wp-playground/mcp 3.1.5 → 3.1.8
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/LICENSE +339 -0
- package/bridge-client.d.ts +27 -0
- package/bridge-server.d.ts +42 -0
- package/client.cjs +2 -0
- package/client.cjs.map +1 -0
- package/client.js +104 -0
- package/client.js.map +1 -0
- package/index.cjs +183 -0
- package/index.cjs.map +1 -0
- package/index.d.ts +1 -0
- package/index.js +12267 -0
- package/index.js.map +1 -0
- package/mcp-server.d.ts +2 -0
- package/package.json +79 -45
- package/tool-executors-ecXfLzk7.cjs +9 -0
- package/tool-executors-ecXfLzk7.cjs.map +1 -0
- package/tool-executors-pMxJ1GXT.js +100 -0
- package/tool-executors-pMxJ1GXT.js.map +1 -0
- package/tools/register-mcp-server-tools.d.ts +3 -0
- package/tools/tool-definitions.d.ts +42 -0
- package/tools/tool-executors.d.ts +63 -0
- package/.eslintrc.json +0 -24
- package/README.md +0 -96
- package/e2e/mcp-tools.spec.ts +0 -679
- package/playwright.config.ts +0 -35
- package/project.json +0 -64
- package/src/bridge-client.ts +0 -196
- package/src/bridge-server.spec.ts +0 -228
- package/src/bridge-server.ts +0 -485
- package/src/index.ts +0 -28
- package/src/mcp-server.ts +0 -34
- package/src/tools/register-mcp-server-tools.ts +0 -347
- package/src/tools/tool-definitions.ts +0 -527
- package/src/tools/tool-executors.ts +0 -205
- package/tsconfig.json +0 -15
- package/tsconfig.lib.json +0 -10
- package/vite.config.ts +0 -49
- /package/{src/client.ts → client.d.ts} +0 -0
package/src/bridge-server.ts
DELETED
|
@@ -1,485 +0,0 @@
|
|
|
1
|
-
import { WebSocketServer } from 'ws';
|
|
2
|
-
import type { WebSocket } from 'ws';
|
|
3
|
-
import { createServer as createHttpServer } from 'node:http';
|
|
4
|
-
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
5
|
-
import type { AddressInfo } from 'node:net';
|
|
6
|
-
import { randomUUID } from 'node:crypto';
|
|
7
|
-
import { presentStorage } from './tools/tool-definitions';
|
|
8
|
-
|
|
9
|
-
export interface SiteRegistration {
|
|
10
|
-
slug: string;
|
|
11
|
-
name: string;
|
|
12
|
-
storage: string;
|
|
13
|
-
isActive: boolean;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface BridgeSiteInfo {
|
|
17
|
-
siteId: string;
|
|
18
|
-
name: string;
|
|
19
|
-
storage: string;
|
|
20
|
-
isActive: boolean;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface RegisterMessage {
|
|
24
|
-
type: 'register';
|
|
25
|
-
tabId: string;
|
|
26
|
-
sites: SiteRegistration[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
interface ResponseMessage {
|
|
30
|
-
type: 'response';
|
|
31
|
-
id: string;
|
|
32
|
-
value?: unknown;
|
|
33
|
-
error?: unknown;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface SiteEntry {
|
|
37
|
-
siteSlug: string;
|
|
38
|
-
siteName: string;
|
|
39
|
-
storage: string;
|
|
40
|
-
reportedByTabs: Set<string>;
|
|
41
|
-
activeInTabs: string[];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Origins allowed to connect to the WebSocket bridge.
|
|
46
|
-
* Browser-based WebSocket connections include an Origin header
|
|
47
|
-
* that cannot be spoofed by JavaScript, so this prevents
|
|
48
|
-
* drive-by attacks from arbitrary web pages.
|
|
49
|
-
*/
|
|
50
|
-
const ALLOWED_ORIGIN_PATTERNS = [
|
|
51
|
-
/^https?:\/\/localhost(:\d+)?$/,
|
|
52
|
-
/^https?:\/\/127\.0\.0\.1(:\d+)?$/,
|
|
53
|
-
/^https?:\/\/playground\.wordpress\.net$/,
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
function isAllowedOrigin(origin: string | undefined): boolean {
|
|
57
|
-
// Non-browser clients (e.g. Node.js MCP clients) don't send
|
|
58
|
-
// an Origin header. Allow them — they're local processes.
|
|
59
|
-
if (!origin) {
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
return ALLOWED_ORIGIN_PATTERNS.some((pattern) => pattern.test(origin));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
type SiteActivatedListener = (siteId: string) => void;
|
|
66
|
-
|
|
67
|
-
export class PlaygroundBridge {
|
|
68
|
-
private connections = new Map<string, WebSocket>();
|
|
69
|
-
private sites = new Map<string, SiteEntry>();
|
|
70
|
-
private pendingRequests = new Map<
|
|
71
|
-
string,
|
|
72
|
-
{
|
|
73
|
-
resolve: (value: unknown) => void;
|
|
74
|
-
reject: (error: Error) => void;
|
|
75
|
-
tabId: string;
|
|
76
|
-
}
|
|
77
|
-
>();
|
|
78
|
-
private requestId = 0;
|
|
79
|
-
private wss: WebSocketServer | undefined;
|
|
80
|
-
private httpServer: ReturnType<typeof createHttpServer> | undefined;
|
|
81
|
-
private sessionToken = randomUUID();
|
|
82
|
-
private siteActivatedListeners: SiteActivatedListener[] = [];
|
|
83
|
-
|
|
84
|
-
getPort(): number {
|
|
85
|
-
const addr = this.httpServer?.address() as AddressInfo | null;
|
|
86
|
-
if (!addr) {
|
|
87
|
-
throw new Error('WebSocket server is not running');
|
|
88
|
-
}
|
|
89
|
-
return addr.port;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
startWebSocketServer(port = 0): Promise<WebSocketServer> {
|
|
93
|
-
return new Promise((resolve, reject) => {
|
|
94
|
-
const httpServer = createHttpServer((req, res) => {
|
|
95
|
-
this.handleHttpRequest(req, res);
|
|
96
|
-
});
|
|
97
|
-
this.httpServer = httpServer;
|
|
98
|
-
|
|
99
|
-
const wss = new WebSocketServer({
|
|
100
|
-
server: httpServer,
|
|
101
|
-
verifyClient: (
|
|
102
|
-
info: { origin: string; req: IncomingMessage },
|
|
103
|
-
callback: (
|
|
104
|
-
result: boolean,
|
|
105
|
-
code?: number,
|
|
106
|
-
message?: string
|
|
107
|
-
) => void
|
|
108
|
-
) => {
|
|
109
|
-
if (!isAllowedOrigin(info.origin)) {
|
|
110
|
-
console.error(
|
|
111
|
-
`[MCP] Rejected WebSocket connection ` +
|
|
112
|
-
`from origin: ${info.origin}`
|
|
113
|
-
);
|
|
114
|
-
callback(false, 403, 'Forbidden');
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const url = new URL(
|
|
119
|
-
info.req.url ?? '/',
|
|
120
|
-
`http://${info.req.headers.host}`
|
|
121
|
-
);
|
|
122
|
-
const token = url.searchParams.get('token');
|
|
123
|
-
if (token !== this.sessionToken) {
|
|
124
|
-
console.error(
|
|
125
|
-
'[MCP] Rejected WebSocket connection: ' +
|
|
126
|
-
'invalid token'
|
|
127
|
-
);
|
|
128
|
-
callback(false, 401, 'Invalid token');
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
callback(true);
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
this.wss = wss;
|
|
136
|
-
|
|
137
|
-
httpServer.on('error', (error: NodeJS.ErrnoException) => {
|
|
138
|
-
if (error.code === 'EADDRINUSE') {
|
|
139
|
-
console.error(
|
|
140
|
-
`[MCP] Port ${port} is already in use. ` +
|
|
141
|
-
`Kill the other process ` +
|
|
142
|
-
`(lsof -i :${port}).`
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
reject(error);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
httpServer.listen(port, '127.0.0.1', () => {
|
|
149
|
-
const actualPort = this.getPort();
|
|
150
|
-
console.error(
|
|
151
|
-
`[MCP] WebSocket server listening on ` +
|
|
152
|
-
`ws://127.0.0.1:${actualPort}`
|
|
153
|
-
);
|
|
154
|
-
resolve(wss);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
wss.on('connection', (ws) => {
|
|
158
|
-
this.handleConnection(ws);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
private handleHttpRequest(req: IncomingMessage, res: ServerResponse) {
|
|
164
|
-
if (req.url === '/bridge-token') {
|
|
165
|
-
const origin = req.headers.origin;
|
|
166
|
-
if (!origin || !isAllowedOrigin(origin)) {
|
|
167
|
-
res.writeHead(403);
|
|
168
|
-
res.end('Forbidden');
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
172
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET');
|
|
173
|
-
|
|
174
|
-
if (req.method === 'OPTIONS') {
|
|
175
|
-
res.writeHead(204);
|
|
176
|
-
res.end();
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
181
|
-
res.end(JSON.stringify({ token: this.sessionToken }));
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
res.writeHead(404);
|
|
185
|
-
res.end();
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
private handleConnection(ws: WebSocket) {
|
|
189
|
-
let tabId: string | undefined;
|
|
190
|
-
|
|
191
|
-
ws.on('message', (data) => {
|
|
192
|
-
let message: RegisterMessage | ResponseMessage;
|
|
193
|
-
try {
|
|
194
|
-
message = JSON.parse(data.toString());
|
|
195
|
-
} catch {
|
|
196
|
-
console.error('[MCP] Failed to parse message');
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
if (message.type === 'register') {
|
|
202
|
-
const isNew = !tabId;
|
|
203
|
-
tabId = message.tabId;
|
|
204
|
-
this.connections.set(tabId, ws);
|
|
205
|
-
this.updateSitesForTab(tabId, message.sites);
|
|
206
|
-
if (isNew) {
|
|
207
|
-
console.error(
|
|
208
|
-
`[MCP] Tab registered: ${tabId} ` +
|
|
209
|
-
`(${message.sites.length} sites)`
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
if (message.type === 'response') {
|
|
216
|
-
const pending = this.pendingRequests.get(message.id);
|
|
217
|
-
if (pending) {
|
|
218
|
-
this.pendingRequests.delete(message.id);
|
|
219
|
-
if (message.error) {
|
|
220
|
-
const errorMsg =
|
|
221
|
-
typeof message.error === 'string'
|
|
222
|
-
? message.error
|
|
223
|
-
: JSON.stringify(message.error);
|
|
224
|
-
pending.reject(new Error(errorMsg));
|
|
225
|
-
} else {
|
|
226
|
-
pending.resolve(message.value);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
} catch (error) {
|
|
231
|
-
console.error('[MCP] Error handling message:', error);
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
ws.on('close', () => {
|
|
236
|
-
if (!tabId) {
|
|
237
|
-
return;
|
|
238
|
-
}
|
|
239
|
-
console.error(`[MCP] Tab disconnected: ${tabId}`);
|
|
240
|
-
|
|
241
|
-
// Reject pending requests for this tab
|
|
242
|
-
for (const [id, pending] of this.pendingRequests) {
|
|
243
|
-
if (pending.tabId === tabId) {
|
|
244
|
-
pending.reject(new Error('Browser tab disconnected'));
|
|
245
|
-
this.pendingRequests.delete(id);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
this.connections.delete(tabId);
|
|
250
|
-
|
|
251
|
-
// Remove tab from all sites and clean up orphans
|
|
252
|
-
for (const [siteId, site] of this.sites) {
|
|
253
|
-
site.reportedByTabs.delete(tabId);
|
|
254
|
-
const idx = site.activeInTabs.indexOf(tabId);
|
|
255
|
-
if (idx !== -1) {
|
|
256
|
-
site.activeInTabs.splice(idx, 1);
|
|
257
|
-
}
|
|
258
|
-
if (site.reportedByTabs.size === 0) {
|
|
259
|
-
this.sites.delete(siteId);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
private updateSitesForTab(
|
|
266
|
-
tabId: string,
|
|
267
|
-
registeredSites: SiteRegistration[]
|
|
268
|
-
) {
|
|
269
|
-
const tabSiteSlugs = new Set(registeredSites.map((s) => s.slug));
|
|
270
|
-
|
|
271
|
-
// Remove this tab from sites it no longer reports
|
|
272
|
-
for (const [siteId, site] of this.sites) {
|
|
273
|
-
if (!tabSiteSlugs.has(site.siteSlug)) {
|
|
274
|
-
site.reportedByTabs.delete(tabId);
|
|
275
|
-
const idx = site.activeInTabs.indexOf(tabId);
|
|
276
|
-
if (idx !== -1) {
|
|
277
|
-
site.activeInTabs.splice(idx, 1);
|
|
278
|
-
}
|
|
279
|
-
if (site.reportedByTabs.size === 0) {
|
|
280
|
-
this.sites.delete(siteId);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Add/update sites from this tab's registration
|
|
286
|
-
for (const reg of registeredSites) {
|
|
287
|
-
const siteId = reg.slug;
|
|
288
|
-
|
|
289
|
-
let site = this.sites.get(siteId);
|
|
290
|
-
if (!site) {
|
|
291
|
-
site = {
|
|
292
|
-
siteSlug: reg.slug,
|
|
293
|
-
siteName: reg.name,
|
|
294
|
-
storage: reg.storage,
|
|
295
|
-
reportedByTabs: new Set(),
|
|
296
|
-
activeInTabs: [],
|
|
297
|
-
};
|
|
298
|
-
this.sites.set(siteId, site);
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Update name and storage in case they changed
|
|
302
|
-
site.siteName = reg.name;
|
|
303
|
-
site.storage = reg.storage;
|
|
304
|
-
site.reportedByTabs.add(tabId);
|
|
305
|
-
|
|
306
|
-
if (reg.isActive) {
|
|
307
|
-
const wasActive = site.activeInTabs.length > 0;
|
|
308
|
-
|
|
309
|
-
// activeInTabs is ordered most-recently-active first.
|
|
310
|
-
// sendCommand() always targets activeInTabs[0],
|
|
311
|
-
// so move this tab to the front.
|
|
312
|
-
const idx = site.activeInTabs.indexOf(tabId);
|
|
313
|
-
if (idx !== -1) {
|
|
314
|
-
site.activeInTabs.splice(idx, 1);
|
|
315
|
-
}
|
|
316
|
-
site.activeInTabs.unshift(tabId);
|
|
317
|
-
|
|
318
|
-
if (!wasActive) {
|
|
319
|
-
for (const listener of this.siteActivatedListeners) {
|
|
320
|
-
listener(siteId);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
} else {
|
|
324
|
-
// Remove this tab from activeInTabs if it was there
|
|
325
|
-
const idx = site.activeInTabs.indexOf(tabId);
|
|
326
|
-
if (idx !== -1) {
|
|
327
|
-
site.activeInTabs.splice(idx, 1);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
sendCommand(
|
|
334
|
-
siteId: string,
|
|
335
|
-
method: string,
|
|
336
|
-
args: unknown[] = []
|
|
337
|
-
): Promise<unknown> {
|
|
338
|
-
const site = this.sites.get(siteId);
|
|
339
|
-
if (!site) {
|
|
340
|
-
return Promise.reject(new Error(`Unknown site: ${siteId}`));
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const isBrowserCommand = method.startsWith('__');
|
|
344
|
-
let targetTabId: string;
|
|
345
|
-
|
|
346
|
-
if (isBrowserCommand) {
|
|
347
|
-
// Browser-level commands (e.g. __open_site, __rename_site)
|
|
348
|
-
// don't require the site to be active — just any connected
|
|
349
|
-
// tab, preferring one that reported this site.
|
|
350
|
-
if (this.connections.size === 0) {
|
|
351
|
-
return Promise.reject(new Error('No browser tabs connected'));
|
|
352
|
-
}
|
|
353
|
-
const reportingTabId = [...site.reportedByTabs].find((id) =>
|
|
354
|
-
this.connections.has(id)
|
|
355
|
-
);
|
|
356
|
-
targetTabId =
|
|
357
|
-
reportingTabId ?? this.connections.keys().next().value!;
|
|
358
|
-
} else {
|
|
359
|
-
// Site-level commands target the Playground client inside
|
|
360
|
-
// the iframe, so the site must be active in a tab.
|
|
361
|
-
if (site.activeInTabs.length === 0) {
|
|
362
|
-
return Promise.reject(
|
|
363
|
-
new Error(
|
|
364
|
-
`Site "${site.siteName}" (${siteId}) is not ` +
|
|
365
|
-
`active in any tab. Use open_site to ` +
|
|
366
|
-
`activate it.`
|
|
367
|
-
)
|
|
368
|
-
);
|
|
369
|
-
}
|
|
370
|
-
targetTabId = site.activeInTabs[0];
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
const ws = this.connections.get(targetTabId);
|
|
374
|
-
if (!ws) {
|
|
375
|
-
return Promise.reject(new Error('Target browser tab disconnected'));
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const id = String(++this.requestId);
|
|
379
|
-
return new Promise((resolve, reject) => {
|
|
380
|
-
const timeoutMs = 300_000;
|
|
381
|
-
const timeout = setTimeout(() => {
|
|
382
|
-
this.pendingRequests.delete(id);
|
|
383
|
-
reject(
|
|
384
|
-
new Error(
|
|
385
|
-
`Command "${method}" timed out after ${timeoutMs / 1000} seconds`
|
|
386
|
-
)
|
|
387
|
-
);
|
|
388
|
-
}, timeoutMs);
|
|
389
|
-
this.pendingRequests.set(id, {
|
|
390
|
-
resolve: (value: unknown) => {
|
|
391
|
-
clearTimeout(timeout);
|
|
392
|
-
resolve(value);
|
|
393
|
-
},
|
|
394
|
-
reject: (error: Error) => {
|
|
395
|
-
clearTimeout(timeout);
|
|
396
|
-
reject(error);
|
|
397
|
-
},
|
|
398
|
-
tabId: targetTabId,
|
|
399
|
-
});
|
|
400
|
-
ws.send(
|
|
401
|
-
JSON.stringify({
|
|
402
|
-
id,
|
|
403
|
-
type: 'command',
|
|
404
|
-
method,
|
|
405
|
-
args,
|
|
406
|
-
siteSlug: site.siteSlug,
|
|
407
|
-
})
|
|
408
|
-
);
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
waitForSiteActive(siteId: string, timeoutMs: number): Promise<SiteEntry> {
|
|
413
|
-
const site = this.sites.get(siteId);
|
|
414
|
-
if (site && site.activeInTabs.length > 0) {
|
|
415
|
-
return Promise.resolve(site);
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
return new Promise((resolve, reject) => {
|
|
419
|
-
const timeout = setTimeout(() => {
|
|
420
|
-
this.removeSiteActivatedListener(handler);
|
|
421
|
-
reject(
|
|
422
|
-
new Error(
|
|
423
|
-
`Timed out waiting for site ${siteId} to become active`
|
|
424
|
-
)
|
|
425
|
-
);
|
|
426
|
-
}, timeoutMs);
|
|
427
|
-
|
|
428
|
-
const handler = (activatedSiteId: string) => {
|
|
429
|
-
if (activatedSiteId === siteId) {
|
|
430
|
-
clearTimeout(timeout);
|
|
431
|
-
this.removeSiteActivatedListener(handler);
|
|
432
|
-
resolve(this.sites.get(siteId)!);
|
|
433
|
-
}
|
|
434
|
-
};
|
|
435
|
-
|
|
436
|
-
this.siteActivatedListeners.push(handler);
|
|
437
|
-
});
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
private removeSiteActivatedListener(listener: SiteActivatedListener) {
|
|
441
|
-
const idx = this.siteActivatedListeners.indexOf(listener);
|
|
442
|
-
if (idx !== -1) {
|
|
443
|
-
this.siteActivatedListeners.splice(idx, 1);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
listSites(): BridgeSiteInfo[] {
|
|
448
|
-
return [...this.sites.entries()].map(([siteId, site]) => ({
|
|
449
|
-
siteId,
|
|
450
|
-
name: site.siteName,
|
|
451
|
-
storage: presentStorage(site.storage),
|
|
452
|
-
isActive: site.activeInTabs.length > 0,
|
|
453
|
-
}));
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
getTabCount(): number {
|
|
457
|
-
return this.connections.size;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
isConnected(): boolean {
|
|
461
|
-
return this.connections.size > 0;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
async close(): Promise<void> {
|
|
465
|
-
if (this.wss) {
|
|
466
|
-
for (const client of this.wss.clients) {
|
|
467
|
-
client.close();
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
return new Promise<void>((resolve) => {
|
|
471
|
-
const closeHttp = () => {
|
|
472
|
-
if (this.httpServer) {
|
|
473
|
-
this.httpServer.close(() => resolve());
|
|
474
|
-
} else {
|
|
475
|
-
resolve();
|
|
476
|
-
}
|
|
477
|
-
};
|
|
478
|
-
if (this.wss) {
|
|
479
|
-
this.wss.close(() => closeHttp());
|
|
480
|
-
} else {
|
|
481
|
-
closeHttp();
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
2
|
-
import { PlaygroundBridge } from './bridge-server';
|
|
3
|
-
import { createServer } from './mcp-server';
|
|
4
|
-
import { registerMcpServerTools } from './tools/register-mcp-server-tools';
|
|
5
|
-
|
|
6
|
-
function getPortFromArgs(): number {
|
|
7
|
-
const portArg = process.argv.find((a) => a.startsWith('--port='));
|
|
8
|
-
if (portArg) {
|
|
9
|
-
return Number(portArg.split('=')[1]);
|
|
10
|
-
}
|
|
11
|
-
return 0;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function main() {
|
|
15
|
-
const bridge = new PlaygroundBridge();
|
|
16
|
-
await bridge.startWebSocketServer(getPortFromArgs());
|
|
17
|
-
const port = bridge.getPort();
|
|
18
|
-
const server = createServer(port);
|
|
19
|
-
registerMcpServerTools(server, bridge, port);
|
|
20
|
-
const transport = new StdioServerTransport();
|
|
21
|
-
await server.connect(transport);
|
|
22
|
-
console.error('[MCP] WordPress Playground MCP server running on stdio');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
main().catch((error) => {
|
|
26
|
-
console.error('Fatal error:', error);
|
|
27
|
-
process.exit(1);
|
|
28
|
-
});
|
package/src/mcp-server.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
-
import { createRequire } from 'module';
|
|
3
|
-
import { playgroundUrl } from './tools/tool-definitions';
|
|
4
|
-
|
|
5
|
-
const require = createRequire(import.meta.url);
|
|
6
|
-
let packageVersion: string;
|
|
7
|
-
try {
|
|
8
|
-
packageVersion = require('./package.json').version;
|
|
9
|
-
} catch {
|
|
10
|
-
// In the development environment, the package.json file is located in the parent directory.
|
|
11
|
-
packageVersion = require('../package.json').version;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function createServer(port: number): McpServer {
|
|
15
|
-
const url = playgroundUrl(port);
|
|
16
|
-
return new McpServer({
|
|
17
|
-
name: 'wordpress-playground',
|
|
18
|
-
version: packageVersion,
|
|
19
|
-
description: `Use this server when you need a live WordPress environment without any local setup. \
|
|
20
|
-
WordPress Playground runs entirely in the user's browser tab via WebAssembly — no PHP, MySQL, \
|
|
21
|
-
or server required. You are automatically authenticated as an admin user.\n\n\
|
|
22
|
-
PREREQUISITE: The user must have WordPress Playground open in their browser at \
|
|
23
|
-
${url} . Ask the user to open this URL if it is not already open.\n\n\
|
|
24
|
-
Typical workflow: playground_list_sites → playground_save_site → filesystem/PHP operations \
|
|
25
|
-
→ playground_navigate to verify results.\n\n\
|
|
26
|
-
Capabilities: execute arbitrary PHP with full WordPress access, read/write files in the virtual filesystem \
|
|
27
|
-
(WordPress root: /wordpress/), make HTTP requests to the site, navigate the browser, \
|
|
28
|
-
and manage multiple Playground sites simultaneously.\n\n\
|
|
29
|
-
Important: sites are temporary by default and not persisted between sessions. \
|
|
30
|
-
Call playground_save_site early in any multi-step workflow where losing progress would be costly.\n\n\
|
|
31
|
-
Error handling: tool failures are returned as thrown exceptions with descriptive messages, \
|
|
32
|
-
not as silent failures.`,
|
|
33
|
-
});
|
|
34
|
-
}
|