@wp-playground/mcp 3.1.8 → 3.1.9

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/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @wp-playground/mcp
2
+
3
+ MCP server that connects AI providers to a WordPress Playground running in the browser.
4
+
5
+ ## Usage
6
+
7
+ ### 1. Configure your MCP client
8
+
9
+ Pick the configuration for your AI tool:
10
+
11
+ #### Claude Code / Claude Desktop
12
+
13
+ Add to your Claude Code `.mcp.json` or Claude Desktop `claude_desktop_config.json`:
14
+
15
+ ```json
16
+ {
17
+ "mcpServers": {
18
+ "wordpress-playground": {
19
+ "type": "stdio",
20
+ "command": "npx",
21
+ "args": ["-y", "@wp-playground/mcp"]
22
+ }
23
+ }
24
+ }
25
+ ```
26
+
27
+ #### Gemini CLI
28
+
29
+ Add to `~/.gemini/settings.json` (or `.gemini/settings.json` in your project):
30
+
31
+ ```json
32
+ {
33
+ "mcpServers": {
34
+ "wordpress-playground": {
35
+ "command": "npx",
36
+ "args": ["-y", "@wp-playground/mcp"]
37
+ }
38
+ }
39
+ }
40
+ ```
41
+
42
+ ### 2. Open the Playground website
43
+
44
+ Your AI assistant will ask you to open the Playground website and provide the exact URL. You can also ask it: _"What's the Playground website URL?"_
45
+
46
+ ## How it works
47
+
48
+ ```
49
+ AI Client (stdio) → MCP Server (Node.js) → WebSocket (port 7999) → Browser (Playground website)
50
+ ```
51
+
52
+ The MCP server communicates with AI clients via stdio and with the browser via WebSocket. A bridge client (`bridge-client.ts`) integrated into the Playground website via Redux middleware auto-connects to the WebSocket server and proxies commands to the PlaygroundClient API.
53
+
54
+ ## Security
55
+
56
+ The MCP bridge runs locally and is only accessible from your machine — connections are origin-restricted and require a token generated at server startup, preventing other websites from hijacking it.
57
+
58
+ **Note:** A compromised WordPress site could attempt prompt injection by embedding instructions in its content (e.g. in a page, post, or PHP output). Use a capable model — larger models are generally better at detecting these attempts.
59
+
60
+ ## Available tools
61
+
62
+ **Site management**: `playground_get_website_url`, `playground_list_sites`, `playground_open_site`, `playground_rename_site`, `playground_save_site`
63
+
64
+ **Code execution**: `playground_execute_php`, `playground_request`
65
+
66
+ **Navigation & info**: `playground_navigate`, `playground_get_current_url`, `playground_get_site_info`
67
+
68
+ **Filesystem**: `playground_read_file`, `playground_write_file`, `playground_list_files`, `playground_mkdir`, `playground_delete_file`, `playground_delete_directory`, `playground_file_exists`
69
+
70
+ ## Development
71
+
72
+ When working on the MCP server or the Playground codebase, run from source instead:
73
+
74
+ ### 1. Start the Playground dev server
75
+
76
+ ```bash
77
+ npm run dev
78
+ ```
79
+
80
+ ### 2. Configure your MCP client
81
+
82
+ > **Note:** Your default `node` must be Node 22+. If it isn't, replace `node` in the command below with the full path to Node 22+ (e.g. `/Users/ME/.nvm/versions/node/v22.22.0/bin/node`).
83
+
84
+ Add to your MCP client config (e.g. Claude Code `.mcp.json` or Claude Desktop `claude_desktop_config.json`):
85
+
86
+ ```json
87
+ {
88
+ "mcpServers": {
89
+ "wordpress-playground": {
90
+ "type": "stdio",
91
+ "command": "node",
92
+ "args": ["--experimental-strip-types", "--experimental-transform-types", "--import", "ABS_PATH_TO_PLAYGROUND/packages/meta/src/node-es-module-loader/register.mts", "ABS_PATH_TO_PLAYGROUND/packages/playground/mcp/src/index.ts"]
93
+ }
94
+ }
95
+ }
96
+ ```
97
+
98
+ Replace `ABS_PATH_TO_PLAYGROUND` with the absolute path to your local checkout of this repository.
99
+
100
+ ### 3. Open the Playground website
101
+
102
+ Navigate to http://127.0.0.1:5400/website-server/?mcp=yes in your browser. The MCP bridge connects automatically.
package/client.cjs CHANGED
@@ -1,2 +1,2 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const E=require("./tool-executors-ecXfLzk7.cjs"),h=5e3;function N(t,r){const d=crypto.randomUUID();let e=null,s="",a=null,c=!1;function i(n){const o=t.getSites(),l=JSON.stringify(o);l!==s&&(s=l,n.send(JSON.stringify({type:"register",tabId:d,sites:o})))}async function f(){try{const n=await fetch(`http://127.0.0.1:${r}/bridge-token`);if(!n.ok){p();return}const{token:o}=await n.json();e=new WebSocket(`ws://127.0.0.1:${r}?token=${o}`)}catch{p();return}e.addEventListener("open",()=>{var n;s="",i(e),(n=t.onConnect)==null||n.call(t)}),e.addEventListener("message",async n=>{let o;try{o=JSON.parse(n.data)}catch{return}if(o.type!=="command")return;const{id:l,method:w,args:y,siteSlug:S}=o;try{const u=await b(t,w,y||[],S);(e==null?void 0:e.readyState)===WebSocket.OPEN&&e.send(JSON.stringify({id:l,type:"response",value:u}))}catch(u){const m=u instanceof Error?u.message:String(u);(e==null?void 0:e.readyState)===WebSocket.OPEN&&e.send(JSON.stringify({id:l,type:"response",error:m}))}}),e.addEventListener("close",()=>{e=null,p()}),e.addEventListener("error",()=>{})}function p(){c||(a=setTimeout(f,h))}return f(),{notifySitesChanged:()=>{(e==null?void 0:e.readyState)===WebSocket.OPEN&&i(e)},stop:()=>{c=!0,a!==null&&(clearTimeout(a),a=null),e&&(e.close(),e=null)}}}async function b(t,r,d,e){if(r==="__open_site"){const i=new URL(window.location.href);if(i.searchParams.set("site-slug",e),!window.open(i.toString(),"_blank"))throw new Error("Pop-up blocked by browser. The user must allow pop-ups for this site.");return!0}if(r==="__rename_site"){if(!t.renameSite)throw new Error("renameSite not configured");const[i]=d;return await t.renameSite(e,i),!0}if(r==="__save_site"){if(!t.saveSite)throw new Error("saveSite not configured");return await t.saveSite(e)}const s=t.getPlaygroundClient(e);if(!s)throw new Error(`No active client for site: ${e}`);const c=E.createToolClient(s)[r];if(typeof c!="function")throw new Error(`Unknown method: ${r}`);return await c(...d)}exports.startMcpBridge=N;
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const h=require("./tool-executors-ecXfLzk7.cjs"),E=5e3;function g(t,r){const d=crypto.randomUUID();let e=null,a="",i=null,p=!1;function c(o){const s=t.getSites(),l=JSON.stringify(s);l!==a&&(a=l,o.send(JSON.stringify({type:"register",tabId:d,sites:s})))}async function n(){try{const o=await fetch(`http://127.0.0.1:${r}/bridge-token`);if(!o.ok){f();return}const{token:s}=await o.json();e=new WebSocket(`ws://127.0.0.1:${r}?token=${s}`)}catch{f();return}e.addEventListener("open",()=>{var o;a="",c(e),(o=t.onConnect)==null||o.call(t)}),e.addEventListener("message",async o=>{let s;try{s=JSON.parse(o.data)}catch{return}if(s.type!=="command")return;const{id:l,method:w,args:y,siteSlug:S}=s;try{const u=await N(t,w,y||[],S,r);(e==null?void 0:e.readyState)===WebSocket.OPEN&&e.send(JSON.stringify({id:l,type:"response",value:u}))}catch(u){const m=u instanceof Error?u.message:String(u);(e==null?void 0:e.readyState)===WebSocket.OPEN&&e.send(JSON.stringify({id:l,type:"response",error:m}))}}),e.addEventListener("close",()=>{e=null,f()}),e.addEventListener("error",()=>{})}function f(){p||(i=setTimeout(n,E))}return n(),{notifySitesChanged:()=>{(e==null?void 0:e.readyState)===WebSocket.OPEN&&c(e)},stop:()=>{p=!0,i!==null&&(clearTimeout(i),i=null),e&&(e.close(),e=null)}}}async function N(t,r,d,e,a){if(r==="__open_site"){const n=new URL(window.location.href);if(n.searchParams.set("mcp","yes"),n.searchParams.set("mcp-port",String(a)),n.searchParams.set("site-slug",e),!window.open(n.toString(),"_blank"))throw new Error("Pop-up blocked by browser. The user must allow pop-ups for this site.");return!0}if(r==="__rename_site"){if(!t.renameSite)throw new Error("renameSite not configured");const[n]=d;return await t.renameSite(e,n),!0}if(r==="__save_site"){if(!t.saveSite)throw new Error("saveSite not configured");return await t.saveSite(e)}const i=t.getPlaygroundClient(e);if(!i)throw new Error(`No active client for site: ${e}`);const c=h.createToolClient(i)[r];if(typeof c!="function")throw new Error(`Unknown method: ${r}`);return await c(...d)}exports.startMcpBridge=g;
2
2
  //# sourceMappingURL=client.cjs.map
package/client.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.cjs","sources":["../../../../packages/playground/mcp/src/bridge-client.ts"],"sourcesContent":["import type { PlaygroundClient } from '@wp-playground/remote';\nimport { createToolClient } from './tools/tool-executors';\nimport type { ToolClient } from './tools/tool-executors';\n\n/**\n * Shared configuration for the MCP bridge client and WebMCP.\n *\n * Both transports need the same callbacks to interact with\n * the Playground site list and active client.\n */\nexport interface PlaygroundConfig {\n\tgetSites: () => Array<{\n\t\tslug: string;\n\t\tname: string;\n\t\tstorage: string;\n\t\tisActive: boolean;\n\t}>;\n\tgetPlaygroundClient: (siteSlug: string) => PlaygroundClient | undefined;\n\trenameSite?: (siteSlug: string, newName: string) => Promise<void>;\n\tsaveSite?: (siteSlug: string) => Promise<{ slug: string; storage: string }>;\n\tonConnect?: () => void;\n}\n\nexport interface McpBridgeHandle {\n\tnotifySitesChanged: () => void;\n\tstop: () => void;\n}\n\nconst RECONNECT_INTERVAL_MS = 5000;\n\nexport function startMcpBridge(\n\tconfig: PlaygroundConfig,\n\tport: number\n): McpBridgeHandle {\n\tconst tabId = crypto.randomUUID();\n\tlet ws: WebSocket | null = null;\n\tlet previousSitesSerialized = '';\n\tlet reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\tlet stopped = false;\n\n\tfunction sendSitesRegistration(socket: WebSocket) {\n\t\tconst sites = config.getSites();\n\t\tconst serialized = JSON.stringify(sites);\n\t\tif (serialized === previousSitesSerialized) {\n\t\t\treturn;\n\t\t}\n\t\tpreviousSitesSerialized = serialized;\n\t\tsocket.send(JSON.stringify({ type: 'register', tabId, sites }));\n\t}\n\n\tasync function connect() {\n\t\ttry {\n\t\t\tconst response = await fetch(\n\t\t\t\t`http://127.0.0.1:${port}/bridge-token`\n\t\t\t);\n\t\t\tif (!response.ok) {\n\t\t\t\tscheduleReconnect();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst { token } = await response.json();\n\t\t\tws = new WebSocket(`ws://127.0.0.1:${port}?token=${token}`);\n\t\t} catch {\n\t\t\tscheduleReconnect();\n\t\t\treturn;\n\t\t}\n\n\t\tws.addEventListener('open', () => {\n\t\t\tpreviousSitesSerialized = '';\n\t\t\tsendSitesRegistration(ws!);\n\t\t\tconfig.onConnect?.();\n\t\t});\n\n\t\tws.addEventListener('message', async (event) => {\n\t\t\tlet message;\n\t\t\ttry {\n\t\t\t\tmessage = JSON.parse(event.data as string);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (message.type !== 'command') {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst { id, method, args, siteSlug } = message;\n\t\t\ttry {\n\t\t\t\tconst value = await handleCommand(\n\t\t\t\t\tconfig,\n\t\t\t\t\tmethod,\n\t\t\t\t\targs || [],\n\t\t\t\t\tsiteSlug\n\t\t\t\t);\n\t\t\t\tif (ws?.readyState === WebSocket.OPEN) {\n\t\t\t\t\tws.send(JSON.stringify({ id, type: 'response', value }));\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconst errorMsg =\n\t\t\t\t\terror instanceof Error ? error.message : String(error);\n\t\t\t\tif (ws?.readyState === WebSocket.OPEN) {\n\t\t\t\t\tws.send(\n\t\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\ttype: 'response',\n\t\t\t\t\t\t\terror: errorMsg,\n\t\t\t\t\t\t})\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tws.addEventListener('close', () => {\n\t\t\tws = null;\n\t\t\tscheduleReconnect();\n\t\t});\n\n\t\tws.addEventListener('error', () => {\n\t\t\t// Error will be followed by close event,\n\t\t\t// which handles reconnect\n\t\t});\n\t}\n\n\tfunction scheduleReconnect() {\n\t\tif (stopped) {\n\t\t\treturn;\n\t\t}\n\t\treconnectTimer = setTimeout(connect, RECONNECT_INTERVAL_MS);\n\t}\n\n\tconnect();\n\n\treturn {\n\t\tnotifySitesChanged: () => {\n\t\t\tif (ws?.readyState === WebSocket.OPEN) {\n\t\t\t\tsendSitesRegistration(ws);\n\t\t\t}\n\t\t},\n\t\tstop: () => {\n\t\t\tstopped = true;\n\t\t\tif (reconnectTimer !== null) {\n\t\t\t\tclearTimeout(reconnectTimer);\n\t\t\t\treconnectTimer = null;\n\t\t\t}\n\t\t\tif (ws) {\n\t\t\t\tws.close();\n\t\t\t\tws = null;\n\t\t\t}\n\t\t},\n\t};\n}\n\nasync function handleCommand(\n\tconfig: PlaygroundConfig,\n\tmethod: string,\n\targs: unknown[],\n\tsiteSlug: string\n): Promise<unknown> {\n\tif (method === '__open_site') {\n\t\tconst url = new URL(window.location.href);\n\t\turl.searchParams.set('site-slug', siteSlug);\n\t\tconst newWindow = window.open(url.toString(), '_blank');\n\t\tif (!newWindow) {\n\t\t\tthrow new Error(\n\t\t\t\t'Pop-up blocked by browser. The user ' +\n\t\t\t\t\t'must allow pop-ups for this site.'\n\t\t\t);\n\t\t}\n\t\treturn true;\n\t}\n\n\tif (method === '__rename_site') {\n\t\tif (!config.renameSite) {\n\t\t\tthrow new Error('renameSite not configured');\n\t\t}\n\t\tconst [newName] = args as [string];\n\t\tawait config.renameSite(siteSlug, newName);\n\t\treturn true;\n\t}\n\n\tif (method === '__save_site') {\n\t\tif (!config.saveSite) {\n\t\t\tthrow new Error('saveSite not configured');\n\t\t}\n\t\treturn await config.saveSite(siteSlug);\n\t}\n\n\tconst playgroundClient = config.getPlaygroundClient(siteSlug);\n\tif (!playgroundClient) {\n\t\tthrow new Error(`No active client for site: ${siteSlug}`);\n\t}\n\n\tconst client = createToolClient(playgroundClient);\n\tconst fn = client[method as keyof ToolClient];\n\tif (typeof fn !== 'function') {\n\t\tthrow new Error(`Unknown method: ${method}`);\n\t}\n\treturn await (fn as (...a: unknown[]) => Promise<unknown>)(...args);\n}\n"],"names":["RECONNECT_INTERVAL_MS","startMcpBridge","config","port","tabId","ws","previousSitesSerialized","reconnectTimer","stopped","sendSitesRegistration","socket","sites","serialized","connect","response","scheduleReconnect","token","_a","event","message","id","method","args","siteSlug","value","handleCommand","error","errorMsg","url","newName","playgroundClient","fn","createToolClient"],"mappings":"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"}
1
+ {"version":3,"file":"client.cjs","sources":["../../../../packages/playground/mcp/src/bridge-client.ts"],"sourcesContent":["import type { PlaygroundClient } from '@wp-playground/remote';\nimport { createToolClient } from './tools/tool-executors';\nimport type { ToolClient } from './tools/tool-executors';\n\n/**\n * Shared configuration for the MCP bridge client and WebMCP.\n *\n * Both transports need the same callbacks to interact with\n * the Playground site list and active client.\n */\nexport interface PlaygroundConfig {\n\tgetSites: () => Array<{\n\t\tslug: string;\n\t\tname: string;\n\t\tstorage: string;\n\t\tisActive: boolean;\n\t}>;\n\tgetPlaygroundClient: (siteSlug: string) => PlaygroundClient | undefined;\n\trenameSite?: (siteSlug: string, newName: string) => Promise<void>;\n\tsaveSite?: (siteSlug: string) => Promise<{ slug: string; storage: string }>;\n\tonConnect?: () => void;\n}\n\nexport interface McpBridgeHandle {\n\tnotifySitesChanged: () => void;\n\tstop: () => void;\n}\n\nconst RECONNECT_INTERVAL_MS = 5000;\n\nexport function startMcpBridge(\n\tconfig: PlaygroundConfig,\n\tport: number\n): McpBridgeHandle {\n\tconst tabId = crypto.randomUUID();\n\tlet ws: WebSocket | null = null;\n\tlet previousSitesSerialized = '';\n\tlet reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\tlet stopped = false;\n\n\tfunction sendSitesRegistration(socket: WebSocket) {\n\t\tconst sites = config.getSites();\n\t\tconst serialized = JSON.stringify(sites);\n\t\tif (serialized === previousSitesSerialized) {\n\t\t\treturn;\n\t\t}\n\t\tpreviousSitesSerialized = serialized;\n\t\tsocket.send(JSON.stringify({ type: 'register', tabId, sites }));\n\t}\n\n\tasync function connect() {\n\t\ttry {\n\t\t\tconst response = await fetch(\n\t\t\t\t`http://127.0.0.1:${port}/bridge-token`\n\t\t\t);\n\t\t\tif (!response.ok) {\n\t\t\t\tscheduleReconnect();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst { token } = await response.json();\n\t\t\tws = new WebSocket(`ws://127.0.0.1:${port}?token=${token}`);\n\t\t} catch {\n\t\t\tscheduleReconnect();\n\t\t\treturn;\n\t\t}\n\n\t\tws.addEventListener('open', () => {\n\t\t\tpreviousSitesSerialized = '';\n\t\t\tsendSitesRegistration(ws!);\n\t\t\tconfig.onConnect?.();\n\t\t});\n\n\t\tws.addEventListener('message', async (event) => {\n\t\t\tlet message;\n\t\t\ttry {\n\t\t\t\tmessage = JSON.parse(event.data as string);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (message.type !== 'command') {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst { id, method, args, siteSlug } = message;\n\t\t\ttry {\n\t\t\t\tconst value = await handleCommand(\n\t\t\t\t\tconfig,\n\t\t\t\t\tmethod,\n\t\t\t\t\targs || [],\n\t\t\t\t\tsiteSlug,\n\t\t\t\t\tport\n\t\t\t\t);\n\t\t\t\tif (ws?.readyState === WebSocket.OPEN) {\n\t\t\t\t\tws.send(JSON.stringify({ id, type: 'response', value }));\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconst errorMsg =\n\t\t\t\t\terror instanceof Error ? error.message : String(error);\n\t\t\t\tif (ws?.readyState === WebSocket.OPEN) {\n\t\t\t\t\tws.send(\n\t\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\ttype: 'response',\n\t\t\t\t\t\t\terror: errorMsg,\n\t\t\t\t\t\t})\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tws.addEventListener('close', () => {\n\t\t\tws = null;\n\t\t\tscheduleReconnect();\n\t\t});\n\n\t\tws.addEventListener('error', () => {\n\t\t\t// Error will be followed by close event,\n\t\t\t// which handles reconnect\n\t\t});\n\t}\n\n\tfunction scheduleReconnect() {\n\t\tif (stopped) {\n\t\t\treturn;\n\t\t}\n\t\treconnectTimer = setTimeout(connect, RECONNECT_INTERVAL_MS);\n\t}\n\n\tconnect();\n\n\treturn {\n\t\tnotifySitesChanged: () => {\n\t\t\tif (ws?.readyState === WebSocket.OPEN) {\n\t\t\t\tsendSitesRegistration(ws);\n\t\t\t}\n\t\t},\n\t\tstop: () => {\n\t\t\tstopped = true;\n\t\t\tif (reconnectTimer !== null) {\n\t\t\t\tclearTimeout(reconnectTimer);\n\t\t\t\treconnectTimer = null;\n\t\t\t}\n\t\t\tif (ws) {\n\t\t\t\tws.close();\n\t\t\t\tws = null;\n\t\t\t}\n\t\t},\n\t};\n}\n\nasync function handleCommand(\n\tconfig: PlaygroundConfig,\n\tmethod: string,\n\targs: unknown[],\n\tsiteSlug: string,\n\tport: number\n): Promise<unknown> {\n\tif (method === '__open_site') {\n\t\tconst url = new URL(window.location.href);\n\t\turl.searchParams.set('mcp', 'yes');\n\t\turl.searchParams.set('mcp-port', String(port));\n\t\turl.searchParams.set('site-slug', siteSlug);\n\t\tconst newWindow = window.open(url.toString(), '_blank');\n\t\tif (!newWindow) {\n\t\t\tthrow new Error(\n\t\t\t\t'Pop-up blocked by browser. The user ' +\n\t\t\t\t\t'must allow pop-ups for this site.'\n\t\t\t);\n\t\t}\n\t\treturn true;\n\t}\n\n\tif (method === '__rename_site') {\n\t\tif (!config.renameSite) {\n\t\t\tthrow new Error('renameSite not configured');\n\t\t}\n\t\tconst [newName] = args as [string];\n\t\tawait config.renameSite(siteSlug, newName);\n\t\treturn true;\n\t}\n\n\tif (method === '__save_site') {\n\t\tif (!config.saveSite) {\n\t\t\tthrow new Error('saveSite not configured');\n\t\t}\n\t\treturn await config.saveSite(siteSlug);\n\t}\n\n\tconst playgroundClient = config.getPlaygroundClient(siteSlug);\n\tif (!playgroundClient) {\n\t\tthrow new Error(`No active client for site: ${siteSlug}`);\n\t}\n\n\tconst client = createToolClient(playgroundClient);\n\tconst fn = client[method as keyof ToolClient];\n\tif (typeof fn !== 'function') {\n\t\tthrow new Error(`Unknown method: ${method}`);\n\t}\n\treturn await (fn as (...a: unknown[]) => Promise<unknown>)(...args);\n}\n"],"names":["RECONNECT_INTERVAL_MS","startMcpBridge","config","port","tabId","ws","previousSitesSerialized","reconnectTimer","stopped","sendSitesRegistration","socket","sites","serialized","connect","response","scheduleReconnect","token","_a","event","message","id","method","args","siteSlug","value","handleCommand","error","errorMsg","url","newName","playgroundClient","fn","createToolClient"],"mappings":"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"}
package/client.js CHANGED
@@ -1,46 +1,47 @@
1
- import { c as E } from "./tool-executors-pMxJ1GXT.js";
2
- const h = 5e3;
3
- function k(t, r) {
1
+ import { c as h } from "./tool-executors-pMxJ1GXT.js";
2
+ const E = 5e3;
3
+ function k(t, n) {
4
4
  const d = crypto.randomUUID();
5
- let e = null, s = "", a = null, c = !1;
6
- function i(n) {
7
- const o = t.getSites(), l = JSON.stringify(o);
8
- l !== s && (s = l, n.send(JSON.stringify({ type: "register", tabId: d, sites: o })));
5
+ let e = null, a = "", i = null, p = !1;
6
+ function c(o) {
7
+ const s = t.getSites(), l = JSON.stringify(s);
8
+ l !== a && (a = l, o.send(JSON.stringify({ type: "register", tabId: d, sites: s })));
9
9
  }
10
- async function f() {
10
+ async function r() {
11
11
  try {
12
- const n = await fetch(
13
- `http://127.0.0.1:${r}/bridge-token`
12
+ const o = await fetch(
13
+ `http://127.0.0.1:${n}/bridge-token`
14
14
  );
15
- if (!n.ok) {
16
- p();
15
+ if (!o.ok) {
16
+ f();
17
17
  return;
18
18
  }
19
- const { token: o } = await n.json();
20
- e = new WebSocket(`ws://127.0.0.1:${r}?token=${o}`);
19
+ const { token: s } = await o.json();
20
+ e = new WebSocket(`ws://127.0.0.1:${n}?token=${s}`);
21
21
  } catch {
22
- p();
22
+ f();
23
23
  return;
24
24
  }
25
25
  e.addEventListener("open", () => {
26
- var n;
27
- s = "", i(e), (n = t.onConnect) == null || n.call(t);
28
- }), e.addEventListener("message", async (n) => {
29
- let o;
26
+ var o;
27
+ a = "", c(e), (o = t.onConnect) == null || o.call(t);
28
+ }), e.addEventListener("message", async (o) => {
29
+ let s;
30
30
  try {
31
- o = JSON.parse(n.data);
31
+ s = JSON.parse(o.data);
32
32
  } catch {
33
33
  return;
34
34
  }
35
- if (o.type !== "command")
35
+ if (s.type !== "command")
36
36
  return;
37
- const { id: l, method: w, args: y, siteSlug: S } = o;
37
+ const { id: l, method: w, args: y, siteSlug: S } = s;
38
38
  try {
39
39
  const u = await N(
40
40
  t,
41
41
  w,
42
42
  y || [],
43
- S
43
+ S,
44
+ n
44
45
  );
45
46
  (e == null ? void 0 : e.readyState) === WebSocket.OPEN && e.send(JSON.stringify({ id: l, type: "response", value: u }));
46
47
  } catch (u) {
@@ -54,48 +55,48 @@ function k(t, r) {
54
55
  );
55
56
  }
56
57
  }), e.addEventListener("close", () => {
57
- e = null, p();
58
+ e = null, f();
58
59
  }), e.addEventListener("error", () => {
59
60
  });
60
61
  }
61
- function p() {
62
- c || (a = setTimeout(f, h));
62
+ function f() {
63
+ p || (i = setTimeout(r, E));
63
64
  }
64
- return f(), {
65
+ return r(), {
65
66
  notifySitesChanged: () => {
66
- (e == null ? void 0 : e.readyState) === WebSocket.OPEN && i(e);
67
+ (e == null ? void 0 : e.readyState) === WebSocket.OPEN && c(e);
67
68
  },
68
69
  stop: () => {
69
- c = !0, a !== null && (clearTimeout(a), a = null), e && (e.close(), e = null);
70
+ p = !0, i !== null && (clearTimeout(i), i = null), e && (e.close(), e = null);
70
71
  }
71
72
  };
72
73
  }
73
- async function N(t, r, d, e) {
74
- if (r === "__open_site") {
75
- const i = new URL(window.location.href);
76
- if (i.searchParams.set("site-slug", e), !window.open(i.toString(), "_blank"))
74
+ async function N(t, n, d, e, a) {
75
+ if (n === "__open_site") {
76
+ const r = new URL(window.location.href);
77
+ if (r.searchParams.set("mcp", "yes"), r.searchParams.set("mcp-port", String(a)), r.searchParams.set("site-slug", e), !window.open(r.toString(), "_blank"))
77
78
  throw new Error(
78
79
  "Pop-up blocked by browser. The user must allow pop-ups for this site."
79
80
  );
80
81
  return !0;
81
82
  }
82
- if (r === "__rename_site") {
83
+ if (n === "__rename_site") {
83
84
  if (!t.renameSite)
84
85
  throw new Error("renameSite not configured");
85
- const [i] = d;
86
- return await t.renameSite(e, i), !0;
86
+ const [r] = d;
87
+ return await t.renameSite(e, r), !0;
87
88
  }
88
- if (r === "__save_site") {
89
+ if (n === "__save_site") {
89
90
  if (!t.saveSite)
90
91
  throw new Error("saveSite not configured");
91
92
  return await t.saveSite(e);
92
93
  }
93
- const s = t.getPlaygroundClient(e);
94
- if (!s)
94
+ const i = t.getPlaygroundClient(e);
95
+ if (!i)
95
96
  throw new Error(`No active client for site: ${e}`);
96
- const c = E(s)[r];
97
+ const c = h(i)[n];
97
98
  if (typeof c != "function")
98
- throw new Error(`Unknown method: ${r}`);
99
+ throw new Error(`Unknown method: ${n}`);
99
100
  return await c(...d);
100
101
  }
101
102
  export {
package/client.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"client.js","sources":["../../../../packages/playground/mcp/src/bridge-client.ts"],"sourcesContent":["import type { PlaygroundClient } from '@wp-playground/remote';\nimport { createToolClient } from './tools/tool-executors';\nimport type { ToolClient } from './tools/tool-executors';\n\n/**\n * Shared configuration for the MCP bridge client and WebMCP.\n *\n * Both transports need the same callbacks to interact with\n * the Playground site list and active client.\n */\nexport interface PlaygroundConfig {\n\tgetSites: () => Array<{\n\t\tslug: string;\n\t\tname: string;\n\t\tstorage: string;\n\t\tisActive: boolean;\n\t}>;\n\tgetPlaygroundClient: (siteSlug: string) => PlaygroundClient | undefined;\n\trenameSite?: (siteSlug: string, newName: string) => Promise<void>;\n\tsaveSite?: (siteSlug: string) => Promise<{ slug: string; storage: string }>;\n\tonConnect?: () => void;\n}\n\nexport interface McpBridgeHandle {\n\tnotifySitesChanged: () => void;\n\tstop: () => void;\n}\n\nconst RECONNECT_INTERVAL_MS = 5000;\n\nexport function startMcpBridge(\n\tconfig: PlaygroundConfig,\n\tport: number\n): McpBridgeHandle {\n\tconst tabId = crypto.randomUUID();\n\tlet ws: WebSocket | null = null;\n\tlet previousSitesSerialized = '';\n\tlet reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\tlet stopped = false;\n\n\tfunction sendSitesRegistration(socket: WebSocket) {\n\t\tconst sites = config.getSites();\n\t\tconst serialized = JSON.stringify(sites);\n\t\tif (serialized === previousSitesSerialized) {\n\t\t\treturn;\n\t\t}\n\t\tpreviousSitesSerialized = serialized;\n\t\tsocket.send(JSON.stringify({ type: 'register', tabId, sites }));\n\t}\n\n\tasync function connect() {\n\t\ttry {\n\t\t\tconst response = await fetch(\n\t\t\t\t`http://127.0.0.1:${port}/bridge-token`\n\t\t\t);\n\t\t\tif (!response.ok) {\n\t\t\t\tscheduleReconnect();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst { token } = await response.json();\n\t\t\tws = new WebSocket(`ws://127.0.0.1:${port}?token=${token}`);\n\t\t} catch {\n\t\t\tscheduleReconnect();\n\t\t\treturn;\n\t\t}\n\n\t\tws.addEventListener('open', () => {\n\t\t\tpreviousSitesSerialized = '';\n\t\t\tsendSitesRegistration(ws!);\n\t\t\tconfig.onConnect?.();\n\t\t});\n\n\t\tws.addEventListener('message', async (event) => {\n\t\t\tlet message;\n\t\t\ttry {\n\t\t\t\tmessage = JSON.parse(event.data as string);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (message.type !== 'command') {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst { id, method, args, siteSlug } = message;\n\t\t\ttry {\n\t\t\t\tconst value = await handleCommand(\n\t\t\t\t\tconfig,\n\t\t\t\t\tmethod,\n\t\t\t\t\targs || [],\n\t\t\t\t\tsiteSlug\n\t\t\t\t);\n\t\t\t\tif (ws?.readyState === WebSocket.OPEN) {\n\t\t\t\t\tws.send(JSON.stringify({ id, type: 'response', value }));\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconst errorMsg =\n\t\t\t\t\terror instanceof Error ? error.message : String(error);\n\t\t\t\tif (ws?.readyState === WebSocket.OPEN) {\n\t\t\t\t\tws.send(\n\t\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\ttype: 'response',\n\t\t\t\t\t\t\terror: errorMsg,\n\t\t\t\t\t\t})\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tws.addEventListener('close', () => {\n\t\t\tws = null;\n\t\t\tscheduleReconnect();\n\t\t});\n\n\t\tws.addEventListener('error', () => {\n\t\t\t// Error will be followed by close event,\n\t\t\t// which handles reconnect\n\t\t});\n\t}\n\n\tfunction scheduleReconnect() {\n\t\tif (stopped) {\n\t\t\treturn;\n\t\t}\n\t\treconnectTimer = setTimeout(connect, RECONNECT_INTERVAL_MS);\n\t}\n\n\tconnect();\n\n\treturn {\n\t\tnotifySitesChanged: () => {\n\t\t\tif (ws?.readyState === WebSocket.OPEN) {\n\t\t\t\tsendSitesRegistration(ws);\n\t\t\t}\n\t\t},\n\t\tstop: () => {\n\t\t\tstopped = true;\n\t\t\tif (reconnectTimer !== null) {\n\t\t\t\tclearTimeout(reconnectTimer);\n\t\t\t\treconnectTimer = null;\n\t\t\t}\n\t\t\tif (ws) {\n\t\t\t\tws.close();\n\t\t\t\tws = null;\n\t\t\t}\n\t\t},\n\t};\n}\n\nasync function handleCommand(\n\tconfig: PlaygroundConfig,\n\tmethod: string,\n\targs: unknown[],\n\tsiteSlug: string\n): Promise<unknown> {\n\tif (method === '__open_site') {\n\t\tconst url = new URL(window.location.href);\n\t\turl.searchParams.set('site-slug', siteSlug);\n\t\tconst newWindow = window.open(url.toString(), '_blank');\n\t\tif (!newWindow) {\n\t\t\tthrow new Error(\n\t\t\t\t'Pop-up blocked by browser. The user ' +\n\t\t\t\t\t'must allow pop-ups for this site.'\n\t\t\t);\n\t\t}\n\t\treturn true;\n\t}\n\n\tif (method === '__rename_site') {\n\t\tif (!config.renameSite) {\n\t\t\tthrow new Error('renameSite not configured');\n\t\t}\n\t\tconst [newName] = args as [string];\n\t\tawait config.renameSite(siteSlug, newName);\n\t\treturn true;\n\t}\n\n\tif (method === '__save_site') {\n\t\tif (!config.saveSite) {\n\t\t\tthrow new Error('saveSite not configured');\n\t\t}\n\t\treturn await config.saveSite(siteSlug);\n\t}\n\n\tconst playgroundClient = config.getPlaygroundClient(siteSlug);\n\tif (!playgroundClient) {\n\t\tthrow new Error(`No active client for site: ${siteSlug}`);\n\t}\n\n\tconst client = createToolClient(playgroundClient);\n\tconst fn = client[method as keyof ToolClient];\n\tif (typeof fn !== 'function') {\n\t\tthrow new Error(`Unknown method: ${method}`);\n\t}\n\treturn await (fn as (...a: unknown[]) => Promise<unknown>)(...args);\n}\n"],"names":["RECONNECT_INTERVAL_MS","startMcpBridge","config","port","tabId","ws","previousSitesSerialized","reconnectTimer","stopped","sendSitesRegistration","socket","sites","serialized","connect","response","scheduleReconnect","token","_a","event","message","id","method","args","siteSlug","value","handleCommand","error","errorMsg","url","newName","playgroundClient","fn","createToolClient"],"mappings":"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"}
1
+ {"version":3,"file":"client.js","sources":["../../../../packages/playground/mcp/src/bridge-client.ts"],"sourcesContent":["import type { PlaygroundClient } from '@wp-playground/remote';\nimport { createToolClient } from './tools/tool-executors';\nimport type { ToolClient } from './tools/tool-executors';\n\n/**\n * Shared configuration for the MCP bridge client and WebMCP.\n *\n * Both transports need the same callbacks to interact with\n * the Playground site list and active client.\n */\nexport interface PlaygroundConfig {\n\tgetSites: () => Array<{\n\t\tslug: string;\n\t\tname: string;\n\t\tstorage: string;\n\t\tisActive: boolean;\n\t}>;\n\tgetPlaygroundClient: (siteSlug: string) => PlaygroundClient | undefined;\n\trenameSite?: (siteSlug: string, newName: string) => Promise<void>;\n\tsaveSite?: (siteSlug: string) => Promise<{ slug: string; storage: string }>;\n\tonConnect?: () => void;\n}\n\nexport interface McpBridgeHandle {\n\tnotifySitesChanged: () => void;\n\tstop: () => void;\n}\n\nconst RECONNECT_INTERVAL_MS = 5000;\n\nexport function startMcpBridge(\n\tconfig: PlaygroundConfig,\n\tport: number\n): McpBridgeHandle {\n\tconst tabId = crypto.randomUUID();\n\tlet ws: WebSocket | null = null;\n\tlet previousSitesSerialized = '';\n\tlet reconnectTimer: ReturnType<typeof setTimeout> | null = null;\n\tlet stopped = false;\n\n\tfunction sendSitesRegistration(socket: WebSocket) {\n\t\tconst sites = config.getSites();\n\t\tconst serialized = JSON.stringify(sites);\n\t\tif (serialized === previousSitesSerialized) {\n\t\t\treturn;\n\t\t}\n\t\tpreviousSitesSerialized = serialized;\n\t\tsocket.send(JSON.stringify({ type: 'register', tabId, sites }));\n\t}\n\n\tasync function connect() {\n\t\ttry {\n\t\t\tconst response = await fetch(\n\t\t\t\t`http://127.0.0.1:${port}/bridge-token`\n\t\t\t);\n\t\t\tif (!response.ok) {\n\t\t\t\tscheduleReconnect();\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst { token } = await response.json();\n\t\t\tws = new WebSocket(`ws://127.0.0.1:${port}?token=${token}`);\n\t\t} catch {\n\t\t\tscheduleReconnect();\n\t\t\treturn;\n\t\t}\n\n\t\tws.addEventListener('open', () => {\n\t\t\tpreviousSitesSerialized = '';\n\t\t\tsendSitesRegistration(ws!);\n\t\t\tconfig.onConnect?.();\n\t\t});\n\n\t\tws.addEventListener('message', async (event) => {\n\t\t\tlet message;\n\t\t\ttry {\n\t\t\t\tmessage = JSON.parse(event.data as string);\n\t\t\t} catch {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tif (message.type !== 'command') {\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst { id, method, args, siteSlug } = message;\n\t\t\ttry {\n\t\t\t\tconst value = await handleCommand(\n\t\t\t\t\tconfig,\n\t\t\t\t\tmethod,\n\t\t\t\t\targs || [],\n\t\t\t\t\tsiteSlug,\n\t\t\t\t\tport\n\t\t\t\t);\n\t\t\t\tif (ws?.readyState === WebSocket.OPEN) {\n\t\t\t\t\tws.send(JSON.stringify({ id, type: 'response', value }));\n\t\t\t\t}\n\t\t\t} catch (error) {\n\t\t\t\tconst errorMsg =\n\t\t\t\t\terror instanceof Error ? error.message : String(error);\n\t\t\t\tif (ws?.readyState === WebSocket.OPEN) {\n\t\t\t\t\tws.send(\n\t\t\t\t\t\tJSON.stringify({\n\t\t\t\t\t\t\tid,\n\t\t\t\t\t\t\ttype: 'response',\n\t\t\t\t\t\t\terror: errorMsg,\n\t\t\t\t\t\t})\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\t\t});\n\n\t\tws.addEventListener('close', () => {\n\t\t\tws = null;\n\t\t\tscheduleReconnect();\n\t\t});\n\n\t\tws.addEventListener('error', () => {\n\t\t\t// Error will be followed by close event,\n\t\t\t// which handles reconnect\n\t\t});\n\t}\n\n\tfunction scheduleReconnect() {\n\t\tif (stopped) {\n\t\t\treturn;\n\t\t}\n\t\treconnectTimer = setTimeout(connect, RECONNECT_INTERVAL_MS);\n\t}\n\n\tconnect();\n\n\treturn {\n\t\tnotifySitesChanged: () => {\n\t\t\tif (ws?.readyState === WebSocket.OPEN) {\n\t\t\t\tsendSitesRegistration(ws);\n\t\t\t}\n\t\t},\n\t\tstop: () => {\n\t\t\tstopped = true;\n\t\t\tif (reconnectTimer !== null) {\n\t\t\t\tclearTimeout(reconnectTimer);\n\t\t\t\treconnectTimer = null;\n\t\t\t}\n\t\t\tif (ws) {\n\t\t\t\tws.close();\n\t\t\t\tws = null;\n\t\t\t}\n\t\t},\n\t};\n}\n\nasync function handleCommand(\n\tconfig: PlaygroundConfig,\n\tmethod: string,\n\targs: unknown[],\n\tsiteSlug: string,\n\tport: number\n): Promise<unknown> {\n\tif (method === '__open_site') {\n\t\tconst url = new URL(window.location.href);\n\t\turl.searchParams.set('mcp', 'yes');\n\t\turl.searchParams.set('mcp-port', String(port));\n\t\turl.searchParams.set('site-slug', siteSlug);\n\t\tconst newWindow = window.open(url.toString(), '_blank');\n\t\tif (!newWindow) {\n\t\t\tthrow new Error(\n\t\t\t\t'Pop-up blocked by browser. The user ' +\n\t\t\t\t\t'must allow pop-ups for this site.'\n\t\t\t);\n\t\t}\n\t\treturn true;\n\t}\n\n\tif (method === '__rename_site') {\n\t\tif (!config.renameSite) {\n\t\t\tthrow new Error('renameSite not configured');\n\t\t}\n\t\tconst [newName] = args as [string];\n\t\tawait config.renameSite(siteSlug, newName);\n\t\treturn true;\n\t}\n\n\tif (method === '__save_site') {\n\t\tif (!config.saveSite) {\n\t\t\tthrow new Error('saveSite not configured');\n\t\t}\n\t\treturn await config.saveSite(siteSlug);\n\t}\n\n\tconst playgroundClient = config.getPlaygroundClient(siteSlug);\n\tif (!playgroundClient) {\n\t\tthrow new Error(`No active client for site: ${siteSlug}`);\n\t}\n\n\tconst client = createToolClient(playgroundClient);\n\tconst fn = client[method as keyof ToolClient];\n\tif (typeof fn !== 'function') {\n\t\tthrow new Error(`Unknown method: ${method}`);\n\t}\n\treturn await (fn as (...a: unknown[]) => Promise<unknown>)(...args);\n}\n"],"names":["RECONNECT_INTERVAL_MS","startMcpBridge","config","port","tabId","ws","previousSitesSerialized","reconnectTimer","stopped","sendSitesRegistration","socket","sites","serialized","connect","response","scheduleReconnect","token","_a","event","message","id","method","args","siteSlug","value","handleCommand","error","errorMsg","url","newName","playgroundClient","fn","createToolClient"],"mappings":"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"}