Skip to content

Commit 6824a06

Browse files
committed
feat: phoenix builder can run phoenix apis via MCP now to verify things
1 parent 17bd15d commit 6824a06

File tree

4 files changed

+105
-1
lines changed

4 files changed

+105
-1
lines changed

phoenix-builder-mcp/mcp-tools.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,39 @@ export function registerTools(server, processManager, wsControlServer, phoenixDe
193193
}
194194
);
195195

196+
server.tool(
197+
"exec_js",
198+
"Execute JavaScript in the Phoenix Code browser runtime and return the result. " +
199+
"Code runs async in the page context with access to: " +
200+
"$ (jQuery) for DOM queries/clicks, " +
201+
"brackets.test.CommandManager, brackets.test.EditorManager, brackets.test.ProjectManager, " +
202+
"brackets.test.DocumentManager, brackets.test.FileSystem, brackets.test.FileUtils, " +
203+
"and 50+ other modules on brackets.test.* — " +
204+
"supports await.",
205+
{
206+
code: z.string().describe("JavaScript code to execute in Phoenix"),
207+
instance: z.string().optional().describe("Target a specific Phoenix instance by name (e.g. 'Phoenix-a3f2'). Required when multiple instances are connected.")
208+
},
209+
async ({ code, instance }) => {
210+
try {
211+
const result = await wsControlServer.requestExecJs(code, instance);
212+
return {
213+
content: [{
214+
type: "text",
215+
text: result !== undefined ? String(result) : "(undefined)"
216+
}]
217+
};
218+
} catch (err) {
219+
return {
220+
content: [{
221+
type: "text",
222+
text: JSON.stringify({ error: err.message })
223+
}]
224+
};
225+
}
226+
}
227+
);
228+
196229
server.tool(
197230
"get_phoenix_status",
198231
"Check the status of the Phoenix process and WebSocket connection.",

phoenix-builder-mcp/ws-control-server.js

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,19 @@ export function createWSControlServer(port) {
7878
break;
7979
}
8080

81+
case "exec_js_response": {
82+
const pending5 = pendingRequests.get(msg.id);
83+
if (pending5) {
84+
pendingRequests.delete(msg.id);
85+
if (msg.error) {
86+
pending5.reject(new Error(msg.error));
87+
} else {
88+
pending5.resolve(msg.result);
89+
}
90+
}
91+
break;
92+
}
93+
8194
case "reload_response": {
8295
const pending3 = pendingRequests.get(msg.id);
8396
if (pending3) {
@@ -282,6 +295,41 @@ export function createWSControlServer(port) {
282295
});
283296
}
284297

298+
function requestExecJs(code, instanceName) {
299+
return new Promise((resolve, reject) => {
300+
const resolved = _resolveClient(instanceName);
301+
if (resolved.error) {
302+
reject(new Error(resolved.error));
303+
return;
304+
}
305+
306+
const { client } = resolved;
307+
if (client.ws.readyState !== 1) {
308+
reject(new Error("Phoenix client \"" + resolved.name + "\" is not connected"));
309+
return;
310+
}
311+
312+
const id = ++requestIdCounter;
313+
const timeout = setTimeout(() => {
314+
pendingRequests.delete(id);
315+
reject(new Error("exec_js request timed out (30s)"));
316+
}, 30000);
317+
318+
pendingRequests.set(id, {
319+
resolve: (data) => {
320+
clearTimeout(timeout);
321+
resolve(data);
322+
},
323+
reject: (err) => {
324+
clearTimeout(timeout);
325+
reject(err);
326+
}
327+
});
328+
329+
client.ws.send(JSON.stringify({ type: "exec_js_request", id, code }));
330+
});
331+
}
332+
285333
function getBrowserLogs(sinceLast, instanceName) {
286334
const resolved = _resolveClient(instanceName);
287335
if (resolved.error) {
@@ -332,6 +380,7 @@ export function createWSControlServer(port) {
332380
requestScreenshot,
333381
requestReload,
334382
requestLogs,
383+
requestExecJs,
335384
getBrowserLogs,
336385
clearBrowserLogs,
337386
isClientConnected,

src/brackets.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,9 @@ define(function (require, exports, module) {
328328
ViewCommandHandlers.restoreFontSize();
329329
ProjectManager.getStartupProjectPath().then((initialProjectPath)=>{
330330
ProjectManager.openProject(initialProjectPath).always(function () {
331-
_initTest();
331+
if (Phoenix.isTestWindow || window._phoenixBuilder) {
332+
_initTest();
333+
}
332334

333335
// If this is the first launch, and we have an index.html file in the project folder (which should be
334336
// the samples folder on first launch), open it automatically. (We explicitly check for the

src/phoenix-builder/phoenix-builder-boot.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,26 @@
329329
}, 100);
330330
});
331331

332+
// --- Register built-in handler for exec_js_request ---
333+
// Evaluates arbitrary JS in the page context and returns the result.
334+
registerHandler("exec_js_request", function (msg) {
335+
const AsyncFunction = (async function () {}).constructor;
336+
const fn = new AsyncFunction(msg.code);
337+
fn().then(function (result) {
338+
_sendMessage({
339+
type: "exec_js_response",
340+
id: msg.id,
341+
result: _serializeArg(result)
342+
});
343+
}).catch(function (err) {
344+
_sendMessage({
345+
type: "exec_js_response",
346+
id: msg.id,
347+
error: (err && err.stack) || (err && err.message) || String(err)
348+
});
349+
});
350+
});
351+
332352
// --- Expose API for AMD module ---
333353
window._phoenixBuilder = {
334354
connect: connect,

0 commit comments

Comments
 (0)