-
-
Notifications
You must be signed in to change notification settings - Fork 195
Expand file tree
/
Copy pathmcp-editor-tools.js
More file actions
227 lines (217 loc) · 10.2 KB
/
Copy pathmcp-editor-tools.js
File metadata and controls
227 lines (217 loc) · 10.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
* for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
*
*/
/**
* MCP server factory for exposing Phoenix editor context to Claude Code.
*
* Provides three tools:
* - getEditorState: returns active file, working set, and live preview file
* - takeScreenshot: captures a screenshot of the Phoenix window as base64 PNG
* - execJsInLivePreview: executes JS in the live preview iframe
*
* Uses the Claude Code SDK's in-process MCP server support (createSdkMcpServer / tool).
*/
const { z } = require("zod");
/**
* Create an in-process MCP server exposing editor context tools.
*
* @param {Object} sdkModule - The imported @anthropic-ai/claude-code ESM module
* @param {Object} nodeConnector - The NodeConnector instance for communicating with the browser
* @returns {McpSdkServerConfigWithInstance} MCP server config ready for queryOptions.mcpServers
*/
function createEditorMcpServer(sdkModule, nodeConnector) {
const getEditorStateTool = sdkModule.tool(
"getEditorState",
"Get the current Phoenix editor state: active file, working set (open files), live preview file, " +
"cursor/selection info (current line text with surrounding context, or selected text), " +
"and the currently selected element in the live preview (tag, selector, text preview) if any. " +
"The live preview selected element may differ from the editor cursor — use execJsInLivePreview to inspect it further. " +
"Long lines are trimmed to 200 chars and selections to 10K chars — use the Read tool for full content.",
{},
async function () {
try {
const state = await nodeConnector.execPeer("getEditorState", {});
return {
content: [{ type: "text", text: JSON.stringify(state) }]
};
} catch (err) {
return {
content: [{ type: "text", text: "Error getting editor state: " + err.message }],
isError: true
};
}
}
);
const takeScreenshotTool = sdkModule.tool(
"takeScreenshot",
"Take a screenshot of the Phoenix Code editor window. Returns a PNG image. " +
"Prefer capturing specific regions instead of the full page: " +
"use selector '#panel-live-preview-frame' for the live preview content, " +
"or '.editor-holder' for the code editor area. " +
"Only omit the selector when you need to see the full application layout.",
{ selector: z.string().optional().describe("CSS selector to capture a specific element. Use '#panel-live-preview-frame' for the live preview, '.editor-holder' for the code editor.") },
async function (args) {
try {
const result = await nodeConnector.execPeer("takeScreenshot", {
selector: args.selector || undefined
});
if (result.base64) {
return {
content: [{ type: "image", data: result.base64, mimeType: "image/png" }]
};
}
return {
content: [{ type: "text", text: result.error || "Screenshot failed" }],
isError: true
};
} catch (err) {
return {
content: [{ type: "text", text: "Error taking screenshot: " + err.message }],
isError: true
};
}
}
);
const execJsInLivePreviewTool = sdkModule.tool(
"execJsInLivePreview",
"Execute JavaScript in the live preview iframe (the page being previewed), NOT in Phoenix itself. " +
"Auto-opens the live preview panel if it is not already visible. Code is evaluated via eval() in " +
"the global scope of the previewed page. Note: eval() is synchronous — async/await is NOT supported. " +
"Only available when an HTML file is selected in the live preview — does not work for markdown or " +
"other non-HTML file types. Use this to inspect or manipulate the user's live-previewed web page " +
"(e.g. document.title, DOM queries).",
{ code: z.string().describe("JavaScript code to execute in the live preview iframe") },
async function (args) {
try {
const result = await nodeConnector.execPeer("execJsInLivePreview", {
code: args.code
});
if (result.error) {
return {
content: [{ type: "text", text: "Error: " + result.error }],
isError: true
};
}
return {
content: [{ type: "text", text: result.result || "undefined" }]
};
} catch (err) {
return {
content: [{ type: "text", text: "Error executing JS in live preview: " + err.message }],
isError: true
};
}
}
);
const controlEditorTool = sdkModule.tool(
"controlEditor",
"Control the Phoenix editor: open/close files, navigate to lines, and select text ranges. " +
"Accepts an array of operations to batch multiple actions in one call. " +
"All line and ch (column) parameters are 1-based.\n\n" +
"Operations:\n" +
"- open: Open a file in the active pane. Params: filePath\n" +
"- close: Close a file (force, no save prompt). Params: filePath\n" +
"- openInWorkingSet: Open a file and pin it to the working set. Params: filePath\n" +
"- setSelection: Open a file and select a range. Params: filePath, startLine, startCh, endLine, endCh\n" +
"- setCursorPos: Open a file and set cursor position. Params: filePath, line, ch",
{
operations: z.array(z.object({
operation: z.enum(["open", "close", "openInWorkingSet", "setSelection", "setCursorPos"]),
filePath: z.string().describe("Absolute path to the file"),
startLine: z.number().optional().describe("Start line (1-based) for setSelection"),
startCh: z.number().optional().describe("Start column (1-based) for setSelection"),
endLine: z.number().optional().describe("End line (1-based) for setSelection"),
endCh: z.number().optional().describe("End column (1-based) for setSelection"),
line: z.number().optional().describe("Line number (1-based) for setCursorPos"),
ch: z.number().optional().describe("Column (1-based) for setCursorPos")
})).describe("Array of editor operations to execute sequentially")
},
async function (args) {
const results = [];
let hasError = false;
for (const op of args.operations) {
try {
const result = await nodeConnector.execPeer("controlEditor", op);
results.push(result);
if (!result.success) {
hasError = true;
}
} catch (err) {
results.push({ success: false, error: err.message });
hasError = true;
}
}
return {
content: [{ type: "text", text: JSON.stringify(results) }],
isError: hasError
};
}
);
const resizeLivePreviewTool = sdkModule.tool(
"resizeLivePreview",
"Resize the live preview panel to a specific width for responsive testing. " +
"Provide a width in pixels based on the target device (e.g. 390 for a phone, 768 for a tablet, 1440 for desktop).",
{
width: z.number().describe("Target width in pixels")
},
async function (args) {
try {
const result = await nodeConnector.execPeer("resizeLivePreview", {
width: args.width
});
if (result.error) {
return {
content: [{ type: "text", text: "Error: " + result.error }],
isError: true
};
}
return {
content: [{ type: "text", text: JSON.stringify(result) }]
};
} catch (err) {
return {
content: [{ type: "text", text: "Error resizing live preview: " + err.message }],
isError: true
};
}
}
);
const waitTool = sdkModule.tool(
"wait",
"Wait for a specified number of seconds before continuing. " +
"Useful for waiting after DOM changes, animations, live preview updates, or resize operations " +
"before taking a screenshot or inspecting state. Maximum 60 seconds.",
{
seconds: z.number().min(0.1).max(60).describe("Number of seconds to wait (0.1–60)")
},
async function (args) {
const ms = Math.round(args.seconds * 1000);
await new Promise(function (resolve) { setTimeout(resolve, ms); });
return {
content: [{ type: "text", text: "Waited " + args.seconds + " seconds." }]
};
}
);
return sdkModule.createSdkMcpServer({
name: "phoenix-editor",
tools: [getEditorStateTool, takeScreenshotTool, execJsInLivePreviewTool, controlEditorTool,
resizeLivePreviewTool, waitTool]
});
}
exports.createEditorMcpServer = createEditorMcpServer;