@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 +102 -0
- package/client.cjs +1 -1
- package/client.cjs.map +1 -1
- package/client.js +42 -41
- package/client.js.map +1 -1
- package/index.cjs +22 -15
- package/index.cjs.map +1 -1
- package/index.js +1433 -1397
- package/index.js.map +1 -1
- package/mcp-server.d.ts +1 -1
- package/package.json +4 -4
- package/tools/tool-definitions.d.ts +1 -1
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
|
|
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":"iIA4BMA,EAAwB,IAEvB,SAASC,EACfC,EACAC,EACkB,CAClB,MAAMC,EAAQ,OAAO,WAAA,EACrB,IAAIC,EAAuB,KACvBC,EAA0B,GAC1BC,EAAuD,KACvDC,EAAU,GAEd,SAASC,EAAsBC,EAAmB,CACjD,MAAMC,EAAQT,EAAO,SAAA,EACfU,EAAa,KAAK,UAAUD,CAAK,EACnCC,IAAeN,IAGnBA,EAA0BM,EAC1BF,EAAO,KAAK,KAAK,UAAU,CAAE,KAAM,WAAY,MAAAN,EAAO,MAAAO,CAAA,CAAO,CAAC,EAC/D,CAEA,eAAeE,GAAU,CACxB,GAAI,CACH,MAAMC,EAAW,MAAM,MACtB,oBAAoBX,CAAI,eAAA,EAEzB,GAAI,CAACW,EAAS,GAAI,CACjBC,EAAA,EACA,MACD,CACA,KAAM,CAAE,MAAAC,CAAA,EAAU,MAAMF,EAAS,KAAA,EACjCT,EAAK,IAAI,UAAU,kBAAkBF,CAAI,UAAUa,CAAK,EAAE,CAC3D,MAAQ,CACPD,EAAA,EACA,MACD,CAEAV,EAAG,iBAAiB,OAAQ,IAAM,OACjCC,EAA0B,GAC1BG,EAAsBJ,CAAG,GACzBY,EAAAf,EAAO,YAAP,MAAAe,EAAA,KAAAf,EACD,CAAC,EAEDG,EAAG,iBAAiB,UAAW,MAAOa,GAAU,CAC/C,IAAIC,EACJ,GAAI,CACHA,EAAU,KAAK,MAAMD,EAAM,IAAc,CAC1C,MAAQ,CACP,MACD,CACA,GAAIC,EAAQ,OAAS,UACpB,OAGD,KAAM,CAAE,GAAAC,EAAI,OAAAC,EAAQ,KAAAC,EAAM,SAAAC,GAAaJ,EACvC,GAAI,CACH,MAAMK,EAAQ,MAAMC,EACnBvB,EACAmB,EACAC,GAAQ,CAAA,EACRC,CAAA,
|
|
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
|
|
2
|
-
const
|
|
3
|
-
function k(t,
|
|
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,
|
|
6
|
-
function
|
|
7
|
-
const
|
|
8
|
-
l !==
|
|
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
|
|
10
|
+
async function r() {
|
|
11
11
|
try {
|
|
12
|
-
const
|
|
13
|
-
`http://127.0.0.1:${
|
|
12
|
+
const o = await fetch(
|
|
13
|
+
`http://127.0.0.1:${n}/bridge-token`
|
|
14
14
|
);
|
|
15
|
-
if (!
|
|
16
|
-
|
|
15
|
+
if (!o.ok) {
|
|
16
|
+
f();
|
|
17
17
|
return;
|
|
18
18
|
}
|
|
19
|
-
const { token:
|
|
20
|
-
e = new WebSocket(`ws://127.0.0.1:${
|
|
19
|
+
const { token: s } = await o.json();
|
|
20
|
+
e = new WebSocket(`ws://127.0.0.1:${n}?token=${s}`);
|
|
21
21
|
} catch {
|
|
22
|
-
|
|
22
|
+
f();
|
|
23
23
|
return;
|
|
24
24
|
}
|
|
25
25
|
e.addEventListener("open", () => {
|
|
26
|
-
var
|
|
27
|
-
|
|
28
|
-
}), e.addEventListener("message", async (
|
|
29
|
-
let
|
|
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
|
-
|
|
31
|
+
s = JSON.parse(o.data);
|
|
32
32
|
} catch {
|
|
33
33
|
return;
|
|
34
34
|
}
|
|
35
|
-
if (
|
|
35
|
+
if (s.type !== "command")
|
|
36
36
|
return;
|
|
37
|
-
const { id: l, method: w, args: y, siteSlug: S } =
|
|
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,
|
|
58
|
+
e = null, f();
|
|
58
59
|
}), e.addEventListener("error", () => {
|
|
59
60
|
});
|
|
60
61
|
}
|
|
61
|
-
function
|
|
62
|
-
|
|
62
|
+
function f() {
|
|
63
|
+
p || (i = setTimeout(r, E));
|
|
63
64
|
}
|
|
64
|
-
return
|
|
65
|
+
return r(), {
|
|
65
66
|
notifySitesChanged: () => {
|
|
66
|
-
(e == null ? void 0 : e.readyState) === WebSocket.OPEN &&
|
|
67
|
+
(e == null ? void 0 : e.readyState) === WebSocket.OPEN && c(e);
|
|
67
68
|
},
|
|
68
69
|
stop: () => {
|
|
69
|
-
|
|
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,
|
|
74
|
-
if (
|
|
75
|
-
const
|
|
76
|
-
if (
|
|
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 (
|
|
83
|
+
if (n === "__rename_site") {
|
|
83
84
|
if (!t.renameSite)
|
|
84
85
|
throw new Error("renameSite not configured");
|
|
85
|
-
const [
|
|
86
|
-
return await t.renameSite(e,
|
|
86
|
+
const [r] = d;
|
|
87
|
+
return await t.renameSite(e, r), !0;
|
|
87
88
|
}
|
|
88
|
-
if (
|
|
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
|
|
94
|
-
if (!
|
|
94
|
+
const i = t.getPlaygroundClient(e);
|
|
95
|
+
if (!i)
|
|
95
96
|
throw new Error(`No active client for site: ${e}`);
|
|
96
|
-
const c =
|
|
97
|
+
const c = h(i)[n];
|
|
97
98
|
if (typeof c != "function")
|
|
98
|
-
throw new Error(`Unknown method: ${
|
|
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":";AA4BA,MAAMA,IAAwB;AAEvB,SAASC,EACfC,GACAC,GACkB;AAClB,QAAMC,IAAQ,OAAO,WAAA;AACrB,MAAIC,IAAuB,MACvBC,IAA0B,IAC1BC,IAAuD,MACvDC,IAAU;AAEd,WAASC,EAAsBC,GAAmB;AACjD,UAAMC,IAAQT,EAAO,SAAA,GACfU,IAAa,KAAK,UAAUD,CAAK;AACvC,IAAIC,MAAeN,MAGnBA,IAA0BM,GAC1BF,EAAO,KAAK,KAAK,UAAU,EAAE,MAAM,YAAY,OAAAN,GAAO,OAAAO,EAAA,CAAO,CAAC;AAAA,EAC/D;AAEA,iBAAeE,IAAU;AACxB,QAAI;AACH,YAAMC,IAAW,MAAM;AAAA,QACtB,oBAAoBX,CAAI;AAAA,MAAA;AAEzB,UAAI,CAACW,EAAS,IAAI;AACjB,QAAAC,EAAA;AACA;AAAA,MACD;AACA,YAAM,EAAE,OAAAC,EAAA,IAAU,MAAMF,EAAS,KAAA;AACjC,MAAAT,IAAK,IAAI,UAAU,kBAAkBF,CAAI,UAAUa,CAAK,EAAE;AAAA,IAC3D,QAAQ;AACP,MAAAD,EAAA;AACA;AAAA,IACD;AAEA,IAAAV,EAAG,iBAAiB,QAAQ,MAAM;;AACjC,MAAAC,IAA0B,IAC1BG,EAAsBJ,CAAG,IACzBY,IAAAf,EAAO,cAAP,QAAAe,EAAA,KAAAf;AAAA,IACD,CAAC,GAEDG,EAAG,iBAAiB,WAAW,OAAOa,MAAU;AAC/C,UAAIC;AACJ,UAAI;AACH,QAAAA,IAAU,KAAK,MAAMD,EAAM,IAAc;AAAA,MAC1C,QAAQ;AACP;AAAA,MACD;AACA,UAAIC,EAAQ,SAAS;AACpB;AAGD,YAAM,EAAE,IAAAC,GAAI,QAAAC,GAAQ,MAAAC,GAAM,UAAAC,MAAaJ;AACvC,UAAI;AACH,cAAMK,IAAQ,MAAMC;AAAA,UACnBvB;AAAA,UACAmB;AAAA,UACAC,KAAQ,CAAA;AAAA,UACRC;AAAA,QAAA;AAED,
|
|
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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"}
|