design-mode-mcp 1.0.0
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/index.js +673 -0
- package/overlay.js +1248 -0
- package/package.json +40 -0
package/index.js
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
|
+
import CDP from "chrome-remote-interface";
|
|
6
|
+
import WebSocket from "ws";
|
|
7
|
+
import { readFileSync, existsSync } from "fs";
|
|
8
|
+
import { dirname, join } from "path";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { homedir } from "os";
|
|
11
|
+
import { z } from "zod";
|
|
12
|
+
|
|
13
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const OVERLAY_PATH = join(__dirname, "overlay.js");
|
|
15
|
+
|
|
16
|
+
// ─── Chrome Port Discovery ───────────────────────────────────
|
|
17
|
+
const CHROME_PROFILE_PATHS = [
|
|
18
|
+
join(homedir(), "Library", "Application Support", "Google", "Chrome", "DevToolsActivePort"),
|
|
19
|
+
join(homedir(), "Library", "Application Support", "Google", "Chrome Canary", "DevToolsActivePort"),
|
|
20
|
+
join(homedir(), ".config", "google-chrome", "DevToolsActivePort"),
|
|
21
|
+
join(homedir(), ".config", "chromium", "DevToolsActivePort"),
|
|
22
|
+
join(homedir(), "AppData", "Local", "Google", "Chrome", "User Data", "DevToolsActivePort"),
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function discoverChrome() {
|
|
26
|
+
for (const filePath of CHROME_PROFILE_PATHS) {
|
|
27
|
+
try {
|
|
28
|
+
if (existsSync(filePath)) {
|
|
29
|
+
const lines = readFileSync(filePath, "utf-8").trim().split("\n");
|
|
30
|
+
const port = parseInt(lines[0], 10);
|
|
31
|
+
const wsPath = lines[1];
|
|
32
|
+
if (port > 0 && port < 65536) return { port, wsPath };
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── Raw WebSocket CDP Client ────────────────────────────────
|
|
40
|
+
// For chrome://inspect connections where HTTP /json endpoints
|
|
41
|
+
// aren't available and direct page WebSocket gets 403.
|
|
42
|
+
// Uses Target.attachToTarget with flatten:true for session-based access.
|
|
43
|
+
|
|
44
|
+
class RawCDPClient {
|
|
45
|
+
constructor(ws, sessionId) {
|
|
46
|
+
this._ws = ws;
|
|
47
|
+
this._sessionId = sessionId;
|
|
48
|
+
this._nextId = 1;
|
|
49
|
+
this._pending = new Map();
|
|
50
|
+
|
|
51
|
+
this._ws.on("message", (raw) => {
|
|
52
|
+
try {
|
|
53
|
+
const msg = JSON.parse(raw.toString());
|
|
54
|
+
if (msg.id && this._pending.has(msg.id)) {
|
|
55
|
+
const { resolve, reject } = this._pending.get(msg.id);
|
|
56
|
+
this._pending.delete(msg.id);
|
|
57
|
+
if (msg.error) reject(new Error(msg.error.message));
|
|
58
|
+
else resolve(msg.result || {});
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// ignore parse errors
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
_send(method, params = {}) {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
const id = this._nextId++;
|
|
69
|
+
const timer = setTimeout(() => {
|
|
70
|
+
if (this._pending.has(id)) {
|
|
71
|
+
this._pending.delete(id);
|
|
72
|
+
reject(new Error(`CDP timeout: ${method}`));
|
|
73
|
+
}
|
|
74
|
+
}, 15000);
|
|
75
|
+
this._pending.set(id, {
|
|
76
|
+
resolve: (v) => { clearTimeout(timer); resolve(v); },
|
|
77
|
+
reject: (e) => { clearTimeout(timer); reject(e); },
|
|
78
|
+
});
|
|
79
|
+
const msg = { id, method, params };
|
|
80
|
+
if (this._sessionId) msg.sessionId = this._sessionId;
|
|
81
|
+
this._ws.send(JSON.stringify(msg));
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Provide the same interface as chrome-remote-interface
|
|
86
|
+
get Runtime() {
|
|
87
|
+
return {
|
|
88
|
+
enable: () => this._send("Runtime.enable"),
|
|
89
|
+
evaluate: (params) => this._send("Runtime.evaluate", params),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
get Page() {
|
|
93
|
+
return {
|
|
94
|
+
enable: () => this._send("Page.enable"),
|
|
95
|
+
navigate: (params) => this._send("Page.navigate", params),
|
|
96
|
+
loadEventFired: () =>
|
|
97
|
+
new Promise((resolve) => {
|
|
98
|
+
const handler = (raw) => {
|
|
99
|
+
try {
|
|
100
|
+
const msg = JSON.parse(raw.toString());
|
|
101
|
+
if (msg.method === "Page.loadEventFired") {
|
|
102
|
+
this._ws.removeListener("message", handler);
|
|
103
|
+
resolve();
|
|
104
|
+
}
|
|
105
|
+
} catch {}
|
|
106
|
+
};
|
|
107
|
+
this._ws.on("message", handler);
|
|
108
|
+
setTimeout(() => {
|
|
109
|
+
this._ws.removeListener("message", handler);
|
|
110
|
+
resolve();
|
|
111
|
+
}, 30000);
|
|
112
|
+
}),
|
|
113
|
+
captureScreenshot: (params) =>
|
|
114
|
+
this._send("Page.captureScreenshot", params),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
get Emulation() {
|
|
118
|
+
return {
|
|
119
|
+
setDeviceMetricsOverride: (params) =>
|
|
120
|
+
this._send("Emulation.setDeviceMetricsOverride", params),
|
|
121
|
+
clearDeviceMetricsOverride: () =>
|
|
122
|
+
this._send("Emulation.clearDeviceMetricsOverride"),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
get Target() {
|
|
126
|
+
return {
|
|
127
|
+
getTargets: () => this._send("Target.getTargets"),
|
|
128
|
+
attachToTarget: (params) =>
|
|
129
|
+
this._send("Target.attachToTarget", params),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
close() {
|
|
134
|
+
try {
|
|
135
|
+
this._ws.close();
|
|
136
|
+
} catch {}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function connectWebSocket(url) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const ws = new WebSocket(url);
|
|
143
|
+
ws.on("open", () => resolve(ws));
|
|
144
|
+
ws.on("error", reject);
|
|
145
|
+
setTimeout(() => reject(new Error("WebSocket timeout")), 5000);
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function connectViaAttach(port, wsPath) {
|
|
150
|
+
// Connect to browser-level WebSocket
|
|
151
|
+
const wsUrl = `ws://127.0.0.1:${port}${wsPath}`;
|
|
152
|
+
const ws = await connectWebSocket(wsUrl);
|
|
153
|
+
const browser = new RawCDPClient(ws, null);
|
|
154
|
+
|
|
155
|
+
// Find a page target
|
|
156
|
+
const { targetInfos } = await browser.Target.getTargets();
|
|
157
|
+
const page = targetInfos.find(
|
|
158
|
+
(t) =>
|
|
159
|
+
t.type === "page" &&
|
|
160
|
+
!t.url.startsWith("chrome") &&
|
|
161
|
+
!t.url.startsWith("devtools") &&
|
|
162
|
+
!t.url.startsWith("about:blank")
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
if (!page) {
|
|
166
|
+
browser.close();
|
|
167
|
+
throw new Error("No suitable page target found");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Attach to the page with flatten:true for session-based messaging
|
|
171
|
+
const { sessionId } = await browser.Target.attachToTarget({
|
|
172
|
+
targetId: page.targetId,
|
|
173
|
+
flatten: true,
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
// Create a session-scoped client
|
|
177
|
+
const pageClient = new RawCDPClient(ws, sessionId);
|
|
178
|
+
await pageClient.Runtime.enable();
|
|
179
|
+
await pageClient.Page.enable();
|
|
180
|
+
return pageClient;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── State ────────────────────────────────────────────────────
|
|
184
|
+
let client = null;
|
|
185
|
+
let isActivated = false;
|
|
186
|
+
|
|
187
|
+
async function getClient() {
|
|
188
|
+
// Check if existing connection still works
|
|
189
|
+
if (client) {
|
|
190
|
+
try {
|
|
191
|
+
await client.Runtime.evaluate({ expression: "1" });
|
|
192
|
+
return client;
|
|
193
|
+
} catch {
|
|
194
|
+
client = null;
|
|
195
|
+
isActivated = false; // page likely changed — overlay is gone
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const discovered = discoverChrome();
|
|
200
|
+
|
|
201
|
+
// Strategy 1: Raw WebSocket + Target.attachToTarget (chrome://inspect method)
|
|
202
|
+
let lastError;
|
|
203
|
+
if (discovered?.wsPath) {
|
|
204
|
+
try {
|
|
205
|
+
client = await connectViaAttach(discovered.port, discovered.wsPath);
|
|
206
|
+
return client;
|
|
207
|
+
} catch (err) {
|
|
208
|
+
lastError = err;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Strategy 2: Standard CDP HTTP (--remote-debugging-port method)
|
|
213
|
+
const ports = [];
|
|
214
|
+
if (process.env.CDP_PORT) ports.push(parseInt(process.env.CDP_PORT, 10));
|
|
215
|
+
if (discovered) ports.push(discovered.port);
|
|
216
|
+
ports.push(9222, 9229, 9333);
|
|
217
|
+
|
|
218
|
+
for (const port of [...new Set(ports)]) {
|
|
219
|
+
try {
|
|
220
|
+
const c = await CDP({ port });
|
|
221
|
+
await c.Runtime.enable();
|
|
222
|
+
await c.Page.enable();
|
|
223
|
+
client = c;
|
|
224
|
+
return client;
|
|
225
|
+
} catch (err) {
|
|
226
|
+
lastError = err;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
throw new Error(
|
|
231
|
+
"Cannot connect to Chrome.\n" +
|
|
232
|
+
"1. Enable remote debugging at chrome://inspect/#remote-debugging\n" +
|
|
233
|
+
"2. Or launch Chrome with: chrome --remote-debugging-port=9222\n" +
|
|
234
|
+
(lastError ? "Last error: " + lastError.message : "")
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function evalInBrowser(expression) {
|
|
239
|
+
const c = await getClient();
|
|
240
|
+
const result = await c.Runtime.evaluate({
|
|
241
|
+
expression,
|
|
242
|
+
returnByValue: true,
|
|
243
|
+
awaitPromise: true,
|
|
244
|
+
});
|
|
245
|
+
if (result.exceptionDetails) {
|
|
246
|
+
throw new Error(
|
|
247
|
+
result.exceptionDetails.exception?.description || "Eval error"
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
return result.result.value;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ─── MCP Server ───────────────────────────────────────────────
|
|
254
|
+
const server = new McpServer({
|
|
255
|
+
name: "design-mode",
|
|
256
|
+
version: "1.0.0",
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// ─── Tool: activate ───────────────────────────────────────────
|
|
260
|
+
server.tool(
|
|
261
|
+
"activate",
|
|
262
|
+
"Inject the Design Mode overlay into the current browser page. Shows hover highlights, click-to-select, annotation panels, and a toolbar.",
|
|
263
|
+
{
|
|
264
|
+
url: z
|
|
265
|
+
.string()
|
|
266
|
+
.optional()
|
|
267
|
+
.describe(
|
|
268
|
+
"Optional URL to navigate to before injecting. If omitted, injects on current page."
|
|
269
|
+
),
|
|
270
|
+
},
|
|
271
|
+
async ({ url }) => {
|
|
272
|
+
try {
|
|
273
|
+
const c = await getClient();
|
|
274
|
+
|
|
275
|
+
if (url) {
|
|
276
|
+
await c.Page.navigate({ url });
|
|
277
|
+
await c.Page.loadEventFired();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!existsSync(OVERLAY_PATH)) {
|
|
281
|
+
return {
|
|
282
|
+
content: [{ type: "text", text: `Overlay script not found at: ${OVERLAY_PATH}` }],
|
|
283
|
+
isError: true,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
const overlayCode = readFileSync(OVERLAY_PATH, "utf-8");
|
|
287
|
+
await c.Runtime.evaluate({
|
|
288
|
+
expression: overlayCode,
|
|
289
|
+
returnByValue: true,
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
isActivated = true;
|
|
293
|
+
|
|
294
|
+
const elementCount = await evalInBrowser(
|
|
295
|
+
"window.__designMode ? window.__designMode.elements.size : 0"
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
content: [
|
|
300
|
+
{
|
|
301
|
+
type: "text",
|
|
302
|
+
text: `Design Mode activated. ${elementCount} elements detected.\n\nControls:\n- Hover to highlight elements with box model visualization\n- Click to select and annotate\n- Shift+click for multi-select\n- Ctrl+Shift+D to toggle visibility\n- Use "Copy to Claude" button to export annotations`,
|
|
303
|
+
},
|
|
304
|
+
],
|
|
305
|
+
};
|
|
306
|
+
} catch (err) {
|
|
307
|
+
return {
|
|
308
|
+
content: [{ type: "text", text: `Failed to activate: ${err.message}` }],
|
|
309
|
+
isError: true,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// ─── Tool: deactivate ─────────────────────────────────────────
|
|
316
|
+
server.tool(
|
|
317
|
+
"deactivate",
|
|
318
|
+
"Remove the Design Mode overlay from the page.",
|
|
319
|
+
{},
|
|
320
|
+
async () => {
|
|
321
|
+
try {
|
|
322
|
+
await evalInBrowser(
|
|
323
|
+
"window.__designMode && window.__designMode._destroy()"
|
|
324
|
+
);
|
|
325
|
+
isActivated = false;
|
|
326
|
+
return {
|
|
327
|
+
content: [{ type: "text", text: "Design Mode deactivated." }],
|
|
328
|
+
};
|
|
329
|
+
} catch (err) {
|
|
330
|
+
return {
|
|
331
|
+
content: [
|
|
332
|
+
{ type: "text", text: `Failed to deactivate: ${err.message}` },
|
|
333
|
+
],
|
|
334
|
+
isError: true,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// ─── Tool: read_annotations ───────────────────────────────────
|
|
341
|
+
server.tool(
|
|
342
|
+
"read_annotations",
|
|
343
|
+
"Read all user annotations from the Design Mode overlay. Returns element selectors, styles, source file info, user comments, AND a cropped screenshot of each annotated element for visual context.",
|
|
344
|
+
{
|
|
345
|
+
include_screenshots: z
|
|
346
|
+
.boolean()
|
|
347
|
+
.optional()
|
|
348
|
+
.describe(
|
|
349
|
+
"Include a cropped screenshot of each annotated element (default: true)"
|
|
350
|
+
),
|
|
351
|
+
},
|
|
352
|
+
async ({ include_screenshots }) => {
|
|
353
|
+
const withScreenshots = include_screenshots !== false;
|
|
354
|
+
try {
|
|
355
|
+
const data = await evalInBrowser(
|
|
356
|
+
"JSON.stringify(window.__designMode ? window.__designMode._dump() : { error: 'Design Mode not active' })"
|
|
357
|
+
);
|
|
358
|
+
const parsed = JSON.parse(data);
|
|
359
|
+
|
|
360
|
+
if (parsed.error) {
|
|
361
|
+
return {
|
|
362
|
+
content: [{ type: "text", text: JSON.stringify(parsed) }],
|
|
363
|
+
isError: true,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const content = [{ type: "text", text: data }];
|
|
368
|
+
|
|
369
|
+
if (withScreenshots && parsed.annotations?.length > 0) {
|
|
370
|
+
const c = await getClient();
|
|
371
|
+
for (const annotation of parsed.annotations) {
|
|
372
|
+
try {
|
|
373
|
+
const rectJson = await evalInBrowser(`
|
|
374
|
+
JSON.stringify((() => {
|
|
375
|
+
const el = document.querySelector(${JSON.stringify(annotation.selector)});
|
|
376
|
+
if (!el) return null;
|
|
377
|
+
const r = el.getBoundingClientRect();
|
|
378
|
+
return { x: Math.max(0, r.left - 8), y: Math.max(0, r.top - 8), width: r.width + 16, height: r.height + 16, scale: window.devicePixelRatio || 1 };
|
|
379
|
+
})())
|
|
380
|
+
`);
|
|
381
|
+
const clip = JSON.parse(rectJson);
|
|
382
|
+
if (clip && clip.width > 0 && clip.height > 0) {
|
|
383
|
+
const { data: imgData } = await c.Page.captureScreenshot({
|
|
384
|
+
format: "png",
|
|
385
|
+
clip: { ...clip, scale: clip.scale },
|
|
386
|
+
});
|
|
387
|
+
content.push({
|
|
388
|
+
type: "text",
|
|
389
|
+
text: `\n--- Screenshot: ${annotation.selector} (comment: "${annotation.comment}") ---`,
|
|
390
|
+
});
|
|
391
|
+
content.push({
|
|
392
|
+
type: "image",
|
|
393
|
+
data: imgData,
|
|
394
|
+
mimeType: "image/png",
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
} catch {
|
|
398
|
+
// Skip screenshot for this element if it fails
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return { content };
|
|
404
|
+
} catch (err) {
|
|
405
|
+
return {
|
|
406
|
+
content: [
|
|
407
|
+
{ type: "text", text: `Failed to read annotations: ${err.message}` },
|
|
408
|
+
],
|
|
409
|
+
isError: true,
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
// ─── Tool: read_element ───────────────────────────────────────
|
|
416
|
+
server.tool(
|
|
417
|
+
"read_element",
|
|
418
|
+
"Read detailed info about a specific element by CSS selector. Returns computed styles, box model, source file mapping, and text content.",
|
|
419
|
+
{
|
|
420
|
+
selector: z.string().describe("CSS selector of the element to inspect"),
|
|
421
|
+
},
|
|
422
|
+
async ({ selector }) => {
|
|
423
|
+
try {
|
|
424
|
+
const data = await evalInBrowser(`
|
|
425
|
+
JSON.stringify((() => {
|
|
426
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
427
|
+
if (!el) return { error: 'Element not found: ${selector}' };
|
|
428
|
+
const cs = window.getComputedStyle(el);
|
|
429
|
+
const rect = el.getBoundingClientRect();
|
|
430
|
+
const sourceInfo = window.__designMode?._getSourceInfo?.(el) || null;
|
|
431
|
+
return {
|
|
432
|
+
tagName: el.tagName.toLowerCase(),
|
|
433
|
+
id: el.id || null,
|
|
434
|
+
classes: el.className && typeof el.className === 'string' ? el.className.trim().split(/\\s+/) : [],
|
|
435
|
+
text: (el.textContent || '').trim().slice(0, 200),
|
|
436
|
+
rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height },
|
|
437
|
+
styles: {
|
|
438
|
+
display: cs.display, position: cs.position,
|
|
439
|
+
width: cs.width, height: cs.height,
|
|
440
|
+
margin: cs.margin, padding: cs.padding,
|
|
441
|
+
color: cs.color, background: cs.backgroundColor,
|
|
442
|
+
fontSize: cs.fontSize, fontWeight: cs.fontWeight,
|
|
443
|
+
borderRadius: cs.borderRadius, gap: cs.gap,
|
|
444
|
+
flexDirection: cs.flexDirection, justifyContent: cs.justifyContent,
|
|
445
|
+
alignItems: cs.alignItems,
|
|
446
|
+
},
|
|
447
|
+
sourceFile: sourceInfo?.fileName || null,
|
|
448
|
+
componentName: sourceInfo?.componentName || null,
|
|
449
|
+
framework: sourceInfo?.framework || null,
|
|
450
|
+
};
|
|
451
|
+
})())
|
|
452
|
+
`);
|
|
453
|
+
return { content: [{ type: "text", text: data }] };
|
|
454
|
+
} catch (err) {
|
|
455
|
+
return {
|
|
456
|
+
content: [
|
|
457
|
+
{ type: "text", text: `Failed to read element: ${err.message}` },
|
|
458
|
+
],
|
|
459
|
+
isError: true,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
// ─── Tool: apply_style ────────────────────────────────────────
|
|
466
|
+
server.tool(
|
|
467
|
+
"apply_style",
|
|
468
|
+
"Apply temporary CSS styles to an element in the browser for visual previewing. Styles are inline and not persisted to source code.",
|
|
469
|
+
{
|
|
470
|
+
selector: z.string().describe("CSS selector of the element"),
|
|
471
|
+
styles: z
|
|
472
|
+
.record(z.string())
|
|
473
|
+
.describe(
|
|
474
|
+
'Object of CSS property-value pairs, e.g. {"fontSize": "20px", "color": "red"}'
|
|
475
|
+
),
|
|
476
|
+
revert: z
|
|
477
|
+
.boolean()
|
|
478
|
+
.optional()
|
|
479
|
+
.describe("If true, revert to original styles instead of applying new ones"),
|
|
480
|
+
},
|
|
481
|
+
async ({ selector, styles, revert }) => {
|
|
482
|
+
try {
|
|
483
|
+
if (revert) {
|
|
484
|
+
const result = await evalInBrowser(`
|
|
485
|
+
(() => {
|
|
486
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
487
|
+
if (!el) return 'Element not found';
|
|
488
|
+
if (el.__dmOriginalStyle !== undefined) {
|
|
489
|
+
el.setAttribute('style', el.__dmOriginalStyle);
|
|
490
|
+
delete el.__dmOriginalStyle;
|
|
491
|
+
return 'Reverted to original styles';
|
|
492
|
+
}
|
|
493
|
+
return 'No original styles stored';
|
|
494
|
+
})()
|
|
495
|
+
`);
|
|
496
|
+
return { content: [{ type: "text", text: result }] };
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const result = await evalInBrowser(`
|
|
500
|
+
(() => {
|
|
501
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
502
|
+
if (!el) return 'Element not found';
|
|
503
|
+
if (!el.__dmOriginalStyle) el.__dmOriginalStyle = el.getAttribute('style') || '';
|
|
504
|
+
Object.assign(el.style, ${JSON.stringify(styles)});
|
|
505
|
+
return 'Styles applied: ' + JSON.stringify(${JSON.stringify(styles)});
|
|
506
|
+
})()
|
|
507
|
+
`);
|
|
508
|
+
return { content: [{ type: "text", text: result }] };
|
|
509
|
+
} catch (err) {
|
|
510
|
+
return {
|
|
511
|
+
content: [
|
|
512
|
+
{ type: "text", text: `Failed to apply styles: ${err.message}` },
|
|
513
|
+
],
|
|
514
|
+
isError: true,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
// ─── Tool: screenshot ─────────────────────────────────────────
|
|
521
|
+
server.tool(
|
|
522
|
+
"screenshot",
|
|
523
|
+
"Take a screenshot of the current page or a specific element. Returns base64-encoded PNG.",
|
|
524
|
+
{
|
|
525
|
+
selector: z
|
|
526
|
+
.string()
|
|
527
|
+
.optional()
|
|
528
|
+
.describe(
|
|
529
|
+
"CSS selector to screenshot a specific element. Omit for full page."
|
|
530
|
+
),
|
|
531
|
+
},
|
|
532
|
+
async ({ selector }) => {
|
|
533
|
+
try {
|
|
534
|
+
const c = await getClient();
|
|
535
|
+
|
|
536
|
+
let clip;
|
|
537
|
+
if (selector) {
|
|
538
|
+
const rect = await evalInBrowser(`
|
|
539
|
+
JSON.stringify((() => {
|
|
540
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
541
|
+
if (!el) return null;
|
|
542
|
+
const r = el.getBoundingClientRect();
|
|
543
|
+
return { x: r.left, y: r.top, width: r.width, height: r.height, scale: window.devicePixelRatio || 1 };
|
|
544
|
+
})())
|
|
545
|
+
`);
|
|
546
|
+
clip = JSON.parse(rect);
|
|
547
|
+
if (!clip) {
|
|
548
|
+
return {
|
|
549
|
+
content: [{ type: "text", text: `Element not found: ${selector}` }],
|
|
550
|
+
isError: true,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const { data } = await c.Page.captureScreenshot({
|
|
556
|
+
format: "png",
|
|
557
|
+
...(clip ? { clip } : {}),
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
return {
|
|
561
|
+
content: [{ type: "image", data, mimeType: "image/png" }],
|
|
562
|
+
};
|
|
563
|
+
} catch (err) {
|
|
564
|
+
return {
|
|
565
|
+
content: [
|
|
566
|
+
{ type: "text", text: `Failed to screenshot: ${err.message}` },
|
|
567
|
+
],
|
|
568
|
+
isError: true,
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
// ─── Tool: resize_viewport ────────────────────────────────────
|
|
575
|
+
server.tool(
|
|
576
|
+
"resize_viewport",
|
|
577
|
+
"Resize the browser viewport to test responsive layouts.",
|
|
578
|
+
{
|
|
579
|
+
width: z.number().describe("Viewport width in pixels"),
|
|
580
|
+
height: z
|
|
581
|
+
.number()
|
|
582
|
+
.optional()
|
|
583
|
+
.describe("Viewport height in pixels (default: 900)"),
|
|
584
|
+
},
|
|
585
|
+
async ({ width, height = 900 }) => {
|
|
586
|
+
try {
|
|
587
|
+
const c = await getClient();
|
|
588
|
+
await c.Emulation.setDeviceMetricsOverride({
|
|
589
|
+
width,
|
|
590
|
+
height,
|
|
591
|
+
deviceScaleFactor: 1,
|
|
592
|
+
mobile: width < 768,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
if (isActivated) {
|
|
596
|
+
await evalInBrowser(
|
|
597
|
+
"window.__designMode && window.__designMode._refresh()"
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return {
|
|
602
|
+
content: [
|
|
603
|
+
{
|
|
604
|
+
type: "text",
|
|
605
|
+
text: `Viewport resized to ${width}x${height}${width < 768 ? " (mobile mode)" : ""}`,
|
|
606
|
+
},
|
|
607
|
+
],
|
|
608
|
+
};
|
|
609
|
+
} catch (err) {
|
|
610
|
+
return {
|
|
611
|
+
content: [
|
|
612
|
+
{ type: "text", text: `Failed to resize: ${err.message}` },
|
|
613
|
+
],
|
|
614
|
+
isError: true,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
);
|
|
619
|
+
|
|
620
|
+
// ─── Tool: reset_viewport ─────────────────────────────────────
|
|
621
|
+
server.tool(
|
|
622
|
+
"reset_viewport",
|
|
623
|
+
"Reset the viewport to the browser's default size.",
|
|
624
|
+
{},
|
|
625
|
+
async () => {
|
|
626
|
+
try {
|
|
627
|
+
const c = await getClient();
|
|
628
|
+
await c.Emulation.clearDeviceMetricsOverride();
|
|
629
|
+
return {
|
|
630
|
+
content: [{ type: "text", text: "Viewport reset to default." }],
|
|
631
|
+
};
|
|
632
|
+
} catch (err) {
|
|
633
|
+
return {
|
|
634
|
+
content: [
|
|
635
|
+
{ type: "text", text: `Failed to reset viewport: ${err.message}` },
|
|
636
|
+
],
|
|
637
|
+
isError: true,
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
|
|
643
|
+
// ─── Tool: eval_js ────────────────────────────────────────────
|
|
644
|
+
server.tool(
|
|
645
|
+
"eval_js",
|
|
646
|
+
"Execute JavaScript in the browser page context. WARNING: Code runs with full page privileges (cookies, localStorage, DOM, network). Use only for debugging or Design Mode interactions — never with untrusted input.",
|
|
647
|
+
{
|
|
648
|
+
code: z.string().describe("JavaScript code to execute in the browser"),
|
|
649
|
+
},
|
|
650
|
+
async ({ code }) => {
|
|
651
|
+
try {
|
|
652
|
+
const result = await evalInBrowser(code);
|
|
653
|
+
return {
|
|
654
|
+
content: [
|
|
655
|
+
{
|
|
656
|
+
type: "text",
|
|
657
|
+
text:
|
|
658
|
+
result !== undefined ? String(result) : "(no return value)",
|
|
659
|
+
},
|
|
660
|
+
],
|
|
661
|
+
};
|
|
662
|
+
} catch (err) {
|
|
663
|
+
return {
|
|
664
|
+
content: [{ type: "text", text: `Eval error: ${err.message}` }],
|
|
665
|
+
isError: true,
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
// ─── Start ────────────────────────────────────────────────────
|
|
672
|
+
const transport = new StdioServerTransport();
|
|
673
|
+
await server.connect(transport);
|