Skip to content

Commit 3e4b776

Browse files
committed
feat: phoenix builder mcp to get browser console logs
1 parent 021a6d0 commit 3e4b776

9 files changed

Lines changed: 571 additions & 385 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@
1111
- Brace style: (`if (x) {`), single-line blocks allowed.
1212
- Always use curly braces for `if`/`else`/`for`/`while`.
1313
- No trailing whitespace.
14+
- Use `const` and `let` instead of `var`.

phoenix-builder-mcp/mcp-tools.js

Lines changed: 17 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -89,40 +89,27 @@ export function registerTools(server, processManager, wsControlServer, phoenixDe
8989

9090
server.tool(
9191
"get_browser_console_logs",
92-
"Get console logs forwarded from the Phoenix browser runtime via WebSocket. By default returns new logs since last call; set clear=true to get all logs and clear the buffer.",
92+
"Get console logs captured from the Phoenix browser runtime from boot time. Fetches the full retained log buffer directly from the browser instance.",
9393
{
94-
clear: z.boolean().default(false).describe("If true, return all logs and clear the buffer. If false, return only new logs since last read."),
9594
instance: z.string().optional().describe("Target a specific Phoenix instance by name (e.g. 'Phoenix-a3f2'). Required when multiple instances are connected.")
9695
},
97-
async ({ clear, instance }) => {
98-
let logs;
99-
if (clear) {
100-
logs = wsControlServer.getBrowserLogs(false, instance);
101-
if (logs && logs.error) {
102-
return {
103-
content: [{ type: "text", text: JSON.stringify(logs) }]
104-
};
105-
}
106-
const clearResult = wsControlServer.clearBrowserLogs(instance);
107-
if (clearResult && clearResult.error) {
108-
return {
109-
content: [{ type: "text", text: JSON.stringify(clearResult) }]
110-
};
111-
}
112-
} else {
113-
logs = wsControlServer.getBrowserLogs(true, instance);
114-
if (logs && logs.error) {
115-
return {
116-
content: [{ type: "text", text: JSON.stringify(logs) }]
117-
};
118-
}
96+
async ({ instance }) => {
97+
try {
98+
const logs = await wsControlServer.requestLogs(instance);
99+
return {
100+
content: [{
101+
type: "text",
102+
text: JSON.stringify(logs.length > 0 ? logs : "(no browser logs)")
103+
}]
104+
};
105+
} catch (err) {
106+
return {
107+
content: [{
108+
type: "text",
109+
text: JSON.stringify({ error: err.message })
110+
}]
111+
};
119112
}
120-
return {
121-
content: [{
122-
type: "text",
123-
text: JSON.stringify(logs.length > 0 ? logs : "(no browser logs)")
124-
}]
125-
};
126113
}
127114
);
128115

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

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,26 @@ export function createWSControlServer(port) {
2727
clientName = msg.name || ("Unknown-" + (++unknownCounter));
2828

2929
// If same name reconnects (e.g. tab reload), close old connection
30+
// but preserve the existing log buffer so logs survive across reloads
3031
const existing = clients.get(clientName);
3132
if (existing) {
3233
try {
3334
existing.ws.close(1000, "Replaced by new connection");
3435
} catch {
3536
// ignore
3637
}
38+
clients.set(clientName, {
39+
ws: ws,
40+
logs: existing.logs,
41+
isAlive: true
42+
});
43+
} else {
44+
clients.set(clientName, {
45+
ws: ws,
46+
logs: new LogBuffer(),
47+
isAlive: true
48+
});
3749
}
38-
39-
clients.set(clientName, {
40-
ws: ws,
41-
logs: new LogBuffer(),
42-
isAlive: true
43-
});
4450
break;
4551
}
4652

@@ -63,6 +69,15 @@ export function createWSControlServer(port) {
6369
break;
6470
}
6571

72+
case "get_logs_response": {
73+
const pending4 = pendingRequests.get(msg.id);
74+
if (pending4) {
75+
pendingRequests.delete(msg.id);
76+
pending4.resolve(msg.entries || []);
77+
}
78+
break;
79+
}
80+
6681
case "reload_response": {
6782
const pending3 = pendingRequests.get(msg.id);
6883
if (pending3) {
@@ -232,6 +247,41 @@ export function createWSControlServer(port) {
232247
});
233248
}
234249

250+
function requestLogs(instanceName) {
251+
return new Promise((resolve, reject) => {
252+
const resolved = _resolveClient(instanceName);
253+
if (resolved.error) {
254+
reject(new Error(resolved.error));
255+
return;
256+
}
257+
258+
const { client } = resolved;
259+
if (client.ws.readyState !== 1) {
260+
reject(new Error("Phoenix client \"" + resolved.name + "\" is not connected"));
261+
return;
262+
}
263+
264+
const id = ++requestIdCounter;
265+
const timeout = setTimeout(() => {
266+
pendingRequests.delete(id);
267+
reject(new Error("Log request timed out (10s)"));
268+
}, 10000);
269+
270+
pendingRequests.set(id, {
271+
resolve: (data) => {
272+
clearTimeout(timeout);
273+
resolve(data);
274+
},
275+
reject: (err) => {
276+
clearTimeout(timeout);
277+
reject(err);
278+
}
279+
});
280+
281+
client.ws.send(JSON.stringify({ type: "get_logs_request", id }));
282+
});
283+
}
284+
235285
function getBrowserLogs(sinceLast, instanceName) {
236286
const resolved = _resolveClient(instanceName);
237287
if (resolved.error) {
@@ -281,6 +331,7 @@ export function createWSControlServer(port) {
281331
return {
282332
requestScreenshot,
283333
requestReload,
334+
requestLogs,
284335
getBrowserLogs,
285336
clearBrowserLogs,
286337
isClientConnected,

src/extensions/default/DebugCommands/main.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
*
2020
*/
2121

22-
/*globals path, logger, Phoenix*/
22+
/*globals path, logger, Phoenix, AppConfig*/
2323
/*jslint regexp: true */
2424

2525
define(function (require, exports, module) {
@@ -828,7 +828,9 @@ define(function (require, exports, module) {
828828
diagnosticsSubmenu.addMenuItem(DEBUG_RUN_UNIT_TESTS);
829829
CommandManager.register(Strings.CMD_BUILD_TESTS, DEBUG_BUILD_TESTS, TestBuilder.toggleTestBuilder);
830830
diagnosticsSubmenu.addMenuItem(DEBUG_BUILD_TESTS);
831-
diagnosticsSubmenu.addMenuItem("debug.phoenixBuilderConnect");
831+
if (AppConfig.config.environment === "dev") {
832+
diagnosticsSubmenu.addMenuItem("debug.phoenixBuilderConnect");
833+
}
832834
diagnosticsSubmenu.addMenuDivider();
833835
diagnosticsSubmenu.addMenuItem(DEBUG_ENABLE_LOGGING);
834836
diagnosticsSubmenu.addMenuItem(DEBUG_ENABLE_PHNODE_INSPECTOR, undefined, undefined, undefined, {

src/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@
481481
<!-- load bugsnag error reporter as soon as cache handling code is done-->
482482
<script src="thirdparty/bugsnag.min.js"></script>
483483
<script src="appConfig.js"></script>
484+
<script src="phoenix-builder/phoenix-builder-boot.js"></script>
484485
<script type="module">
485486
import BugsnagPerformance from "./thirdparty/bugsnag-performance.min.js";
486487
window.BugsnagPerformance = BugsnagPerformance;

src/phoenix-builder/builder-connect-dialog.html

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,42 @@
11
<div class="phoenix-builder-connect modal">
22
<div class="modal-header">
33
<h1 class="dialog-title">Phoenix Builder MCP</h1>
4+
<ul class="nav nav-tabs no-focus" style="margin-bottom: 0;">
5+
<li class="active"><a href="#builder-settings" data-toggle="tab">Settings</a></li>
6+
<li><a href="#builder-logs" data-toggle="tab">Console Logs</a></li>
7+
</ul>
48
</div>
5-
<div class="modal-body">
6-
<p class="dialog-message">Connect to the Phoenix Builder MCP server for automated testing with Claude Code.</p>
7-
<div style="margin-bottom: 10px; padding: 6px 10px; background: var(--list-highlight-bg); border-radius: 4px;">
8-
<span style="font-weight: bold;">Instance:</span>
9-
<code style="margin-left: 4px;">{{instanceName}}</code>
10-
</div>
11-
<div style="margin-bottom: 10px;">
12-
<label>
13-
<input type="checkbox" class="builder-enable" {{#enabled}}checked{{/enabled}}>
14-
Enable Connection
15-
</label>
16-
</div>
17-
<div style="margin-bottom: 10px;">
18-
<label>WebSocket URL</label>
19-
<input type="text" class="builder-url" value="{{url}}" style="width: 100%; margin-top: 4px;">
20-
</div>
21-
<div style="margin-bottom: 10px;">
22-
<span class="builder-status" style="font-weight: bold;">
23-
{{#connected}}
24-
<span style="color: green;">&#9679; Connected</span>
25-
{{/connected}}
26-
{{^connected}}
27-
<span style="color: red;">&#9679; Disconnected</span>
28-
{{/connected}}
29-
</span>
30-
</div>
31-
<details style="margin-top: 10px; padding: 8px; border: 1px solid var(--item-border-color); border-radius: 4px;">
32-
<summary style="cursor: pointer; font-weight: bold;">Claude Code Configuration</summary>
33-
<p style="margin-top: 8px;">Add the following to your Claude Code MCP settings (.claude/settings.json):</p>
34-
<pre class="builder-config-code" style="padding: 8px; border: 1px solid var(--item-border-color); border-radius: 4px; overflow-x: auto; font-size: 12px; cursor: pointer;" title="Click to copy">{
9+
<div class="modal-body tab-content">
10+
<div id="builder-settings" class="tab-pane active">
11+
<p class="dialog-message">Connect to the Phoenix Builder MCP server for automated testing with Claude Code.</p>
12+
<div style="margin-bottom: 10px; padding: 6px 10px; background: var(--list-highlight-bg); border-radius: 4px;">
13+
<span style="font-weight: bold;">Instance:</span>
14+
<code style="margin-left: 4px;">{{instanceName}}</code>
15+
</div>
16+
<div style="margin-bottom: 10px;">
17+
<label>
18+
<input type="checkbox" class="builder-enable" {{#enabled}}checked{{/enabled}}>
19+
Enable Connection
20+
</label>
21+
</div>
22+
<div style="margin-bottom: 10px;">
23+
<label>WebSocket URL</label>
24+
<input type="text" class="builder-url" value="{{url}}" style="width: 100%; margin-top: 4px;">
25+
</div>
26+
<div style="margin-bottom: 10px;">
27+
<span class="builder-status" style="font-weight: bold;">
28+
{{#connected}}
29+
<span style="color: green;">&#9679; Connected</span>
30+
{{/connected}}
31+
{{^connected}}
32+
<span style="color: red;">&#9679; Disconnected</span>
33+
{{/connected}}
34+
</span>
35+
</div>
36+
<details style="margin-top: 10px; padding: 8px; border: 1px solid var(--item-border-color); border-radius: 4px;">
37+
<summary style="cursor: pointer; font-weight: bold;">Claude Code Configuration</summary>
38+
<p style="margin-top: 8px;">Add the following to your Claude Code MCP settings (.claude/settings.json):</p>
39+
<pre class="builder-config-code" style="padding: 8px; border: 1px solid var(--item-border-color); border-radius: 4px; overflow-x: auto; font-size: 12px; cursor: pointer;" title="Click to copy">{
3540
"mcpServers": {
3641
"phoenix-builder": {
3742
"command": "node",
@@ -42,10 +47,20 @@ <h1 class="dialog-title">Phoenix Builder MCP</h1>
4247
}
4348
}
4449
}</pre>
45-
<p style="font-size: 12px; opacity: 0.7;">
46-
Replace <code>&lt;path-to-phoenix&gt;</code> and <code>&lt;path-to-phoenix-desktop&gt;</code> with actual paths. Restart Claude Code after adding the config.
47-
</p>
48-
</details>
50+
<p style="font-size: 12px; opacity: 0.7;">
51+
Replace <code>&lt;path-to-phoenix&gt;</code> and <code>&lt;path-to-phoenix-desktop&gt;</code> with actual paths. Restart Claude Code after adding the config.
52+
</p>
53+
</details>
54+
</div>
55+
<div id="builder-logs" class="tab-pane">
56+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
57+
<span class="builder-log-count" style="font-size: 12px; opacity: 0.7;">0 entries</span>
58+
<button class="btn btn-small builder-log-refresh" style="margin-left: auto;">Refresh</button>
59+
</div>
60+
<div class="builder-log-container" style="overflow-y: auto; max-height: 350px; font-family: monospace; font-size: 12px; border: 1px solid var(--item-border-color); border-radius: 4px; padding: 4px;">
61+
<span style="opacity: 0.5;">No logs captured yet.</span>
62+
</div>
63+
</div>
4964
</div>
5065
<div class="modal-footer">
5166
<button class="dialog-button btn primary" data-button-id="ok">Done</button>

0 commit comments

Comments
 (0)