browser-lens-mcp 1.1.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/LICENSE +21 -0
- package/README.md +341 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +25 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/prompts/index.d.ts +4 -0
- package/dist/src/prompts/index.d.ts.map +1 -0
- package/dist/src/prompts/index.js +180 -0
- package/dist/src/prompts/index.js.map +1 -0
- package/dist/src/resources/index.d.ts +4 -0
- package/dist/src/resources/index.d.ts.map +1 -0
- package/dist/src/resources/index.js +64 -0
- package/dist/src/resources/index.js.map +1 -0
- package/dist/src/server.d.ts +4 -0
- package/dist/src/server.d.ts.map +1 -0
- package/dist/src/server.js +15 -0
- package/dist/src/server.js.map +1 -0
- package/dist/src/store/browser-store.d.ts +45 -0
- package/dist/src/store/browser-store.d.ts.map +1 -0
- package/dist/src/store/browser-store.js +240 -0
- package/dist/src/store/browser-store.js.map +1 -0
- package/dist/src/store/types.d.ts +342 -0
- package/dist/src/store/types.d.ts.map +1 -0
- package/dist/src/store/types.js +3 -0
- package/dist/src/store/types.js.map +1 -0
- package/dist/src/tools/index.d.ts +4 -0
- package/dist/src/tools/index.d.ts.map +1 -0
- package/dist/src/tools/index.js +889 -0
- package/dist/src/tools/index.js.map +1 -0
- package/dist/src/transport/connector-script.d.ts +2 -0
- package/dist/src/transport/connector-script.d.ts.map +1 -0
- package/dist/src/transport/connector-script.js +336 -0
- package/dist/src/transport/connector-script.js.map +1 -0
- package/dist/src/transport/http-receiver.d.ts +4 -0
- package/dist/src/transport/http-receiver.d.ts.map +1 -0
- package/dist/src/transport/http-receiver.js +218 -0
- package/dist/src/transport/http-receiver.js.map +1 -0
- package/dist/src/transport/ws-receiver.d.ts +4 -0
- package/dist/src/transport/ws-receiver.d.ts.map +1 -0
- package/dist/src/transport/ws-receiver.js +39 -0
- package/dist/src/transport/ws-receiver.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,889 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
function text(data) {
|
|
3
|
+
return {
|
|
4
|
+
content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
function imageContent(dataUrl, altText) {
|
|
8
|
+
const base64 = dataUrl.replace(/^data:image\/\w+;base64,/, "");
|
|
9
|
+
return {
|
|
10
|
+
content: [
|
|
11
|
+
{
|
|
12
|
+
type: "image",
|
|
13
|
+
data: base64,
|
|
14
|
+
mimeType: "image/png",
|
|
15
|
+
},
|
|
16
|
+
{ type: "text", text: altText },
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
function compareValues(property, expected, actual) {
|
|
21
|
+
if (expected === undefined || expected === null)
|
|
22
|
+
return null;
|
|
23
|
+
const expStr = String(expected).toLowerCase().trim();
|
|
24
|
+
const actStr = actual.toLowerCase().trim();
|
|
25
|
+
if (expStr === actStr)
|
|
26
|
+
return null;
|
|
27
|
+
const expNum = parseFloat(expStr);
|
|
28
|
+
const actNum = parseFloat(actStr);
|
|
29
|
+
if (!isNaN(expNum) && !isNaN(actNum)) {
|
|
30
|
+
const diff = Math.abs(expNum - actNum);
|
|
31
|
+
if (diff <= 1)
|
|
32
|
+
return null;
|
|
33
|
+
const severity = diff > 10 ? "major" : diff > 4 ? "minor" : "info";
|
|
34
|
+
return {
|
|
35
|
+
property,
|
|
36
|
+
expected: String(expected),
|
|
37
|
+
actual,
|
|
38
|
+
severity: severity,
|
|
39
|
+
suggestion: `Change ${property} from ${actual} to ${expected}`,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
property,
|
|
44
|
+
expected: String(expected),
|
|
45
|
+
actual,
|
|
46
|
+
severity: "major",
|
|
47
|
+
suggestion: `Change ${property} from "${actual}" to "${expected}"`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function registerTools(server, store) {
|
|
51
|
+
server.registerTool("get_page_info", {
|
|
52
|
+
title: "Page Info",
|
|
53
|
+
description: "Get connected page URL, viewport size, element count, and data availability summary",
|
|
54
|
+
}, () => text(store.getPageInfo()));
|
|
55
|
+
server.registerTool("get_dom_tree", {
|
|
56
|
+
title: "DOM Tree",
|
|
57
|
+
description: "Get the full DOM tree structure of the connected page with semantic analysis",
|
|
58
|
+
inputSchema: {
|
|
59
|
+
maxDepth: z.number().optional().describe("Max depth to return (default: all)"),
|
|
60
|
+
},
|
|
61
|
+
}, (args) => {
|
|
62
|
+
const dom = store.getDom();
|
|
63
|
+
if (!dom)
|
|
64
|
+
return text({ message: "No DOM data captured. Click the bookmarklet on your app first." });
|
|
65
|
+
const result = {
|
|
66
|
+
url: dom.url,
|
|
67
|
+
title: dom.title,
|
|
68
|
+
totalElements: dom.totalElements,
|
|
69
|
+
viewport: dom.viewport,
|
|
70
|
+
semanticStructure: dom.semanticStructure,
|
|
71
|
+
};
|
|
72
|
+
if (args.maxDepth !== undefined) {
|
|
73
|
+
const trim = (node, depth) => {
|
|
74
|
+
if (depth >= (args.maxDepth ?? 999))
|
|
75
|
+
return { ...node, children: [] };
|
|
76
|
+
const children = node.children ?? [];
|
|
77
|
+
return { ...node, children: children.map((c) => trim(c, depth + 1)) };
|
|
78
|
+
};
|
|
79
|
+
result.rootElement = trim(dom.rootElement, 0);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
result.rootElement = dom.rootElement;
|
|
83
|
+
}
|
|
84
|
+
return text(result);
|
|
85
|
+
});
|
|
86
|
+
server.registerTool("inspect_element", {
|
|
87
|
+
title: "Inspect Element",
|
|
88
|
+
description: "Get full details of a specific element: DOM, computed styles, layout, box model, accessibility",
|
|
89
|
+
inputSchema: {
|
|
90
|
+
selector: z.string().describe("CSS selector of the element to inspect"),
|
|
91
|
+
},
|
|
92
|
+
}, (args) => {
|
|
93
|
+
const el = store.getElement(args.selector);
|
|
94
|
+
if (!el) {
|
|
95
|
+
const found = store.querySelector(args.selector);
|
|
96
|
+
if (found.length > 0) {
|
|
97
|
+
return text({
|
|
98
|
+
message: `Element found in DOM tree but not in detail cache. Found ${found.length} matching element(s).`,
|
|
99
|
+
matches: found.map((f) => ({ selector: f.selector, tagName: f.tagName, id: f.id, classes: f.classNames })),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return text({ error: `Element '${args.selector}' not found. Use query_selector to search.` });
|
|
103
|
+
}
|
|
104
|
+
return text(el);
|
|
105
|
+
});
|
|
106
|
+
server.registerTool("query_selector", {
|
|
107
|
+
title: "Query Selector",
|
|
108
|
+
description: "Search the DOM tree for elements matching a tag name, class, or ID",
|
|
109
|
+
inputSchema: {
|
|
110
|
+
selector: z.string().describe("CSS selector, tag name, .class, or #id"),
|
|
111
|
+
limit: z.number().optional().describe("Max results (default: 20)"),
|
|
112
|
+
},
|
|
113
|
+
}, (args) => {
|
|
114
|
+
const results = store.querySelector(args.selector);
|
|
115
|
+
const limit = args.limit ?? 20;
|
|
116
|
+
return text({
|
|
117
|
+
query: args.selector,
|
|
118
|
+
totalMatches: results.length,
|
|
119
|
+
elements: results.slice(0, limit).map((e) => ({
|
|
120
|
+
selector: e.selector,
|
|
121
|
+
tagName: e.tagName,
|
|
122
|
+
id: e.id,
|
|
123
|
+
classes: e.classNames,
|
|
124
|
+
text: e.textContent.slice(0, 100),
|
|
125
|
+
childCount: e.childCount,
|
|
126
|
+
})),
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
server.registerTool("get_computed_styles", {
|
|
130
|
+
title: "Computed Styles",
|
|
131
|
+
description: "Get all computed CSS styles for a specific element including applied classes and matched rules",
|
|
132
|
+
inputSchema: {
|
|
133
|
+
selector: z.string().describe("CSS selector of the element"),
|
|
134
|
+
properties: z.array(z.string()).optional().describe("Specific CSS properties to return (default: all)"),
|
|
135
|
+
},
|
|
136
|
+
}, (args) => {
|
|
137
|
+
const style = store.getComputedStyle(args.selector);
|
|
138
|
+
if (!style)
|
|
139
|
+
return text({ error: `No style data for '${args.selector}'` });
|
|
140
|
+
if (args.properties) {
|
|
141
|
+
const filtered = {};
|
|
142
|
+
for (const p of args.properties) {
|
|
143
|
+
const camel = p.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
144
|
+
if (style.styles[p])
|
|
145
|
+
filtered[p] = style.styles[p];
|
|
146
|
+
else if (style.styles[camel])
|
|
147
|
+
filtered[camel] = style.styles[camel];
|
|
148
|
+
}
|
|
149
|
+
return text({ selector: args.selector, styles: filtered, appliedClasses: style.appliedClasses });
|
|
150
|
+
}
|
|
151
|
+
return text(style);
|
|
152
|
+
});
|
|
153
|
+
server.registerTool("get_layout_info", {
|
|
154
|
+
title: "Layout Info",
|
|
155
|
+
description: "Get box model, positioning, flex/grid info, and dimensions for an element",
|
|
156
|
+
inputSchema: {
|
|
157
|
+
selector: z.string().describe("CSS selector of the element"),
|
|
158
|
+
},
|
|
159
|
+
}, (args) => {
|
|
160
|
+
const layout = store.getLayout(args.selector);
|
|
161
|
+
if (!layout)
|
|
162
|
+
return text({ error: `No layout data for '${args.selector}'` });
|
|
163
|
+
return text(layout);
|
|
164
|
+
});
|
|
165
|
+
server.registerTool("get_page_screenshot", {
|
|
166
|
+
title: "Page Screenshot",
|
|
167
|
+
description: "Get the latest screenshot of the page viewport as a PNG image",
|
|
168
|
+
}, () => {
|
|
169
|
+
const shot = store.getLatestScreenshot();
|
|
170
|
+
if (!shot)
|
|
171
|
+
return text({ message: "No screenshots captured yet." });
|
|
172
|
+
return imageContent(shot.dataUrl, `Page screenshot (${shot.width}x${shot.height}) captured at ${new Date(shot.timestamp).toISOString()}`);
|
|
173
|
+
});
|
|
174
|
+
server.registerTool("get_all_screenshots", {
|
|
175
|
+
title: "All Screenshots",
|
|
176
|
+
description: "List all captured screenshots with metadata",
|
|
177
|
+
}, () => {
|
|
178
|
+
const shots = store.getScreenshots();
|
|
179
|
+
if (shots.length === 0)
|
|
180
|
+
return text({ message: "No screenshots captured." });
|
|
181
|
+
return text({
|
|
182
|
+
total: shots.length,
|
|
183
|
+
screenshots: shots.map((s, i) => ({
|
|
184
|
+
index: i,
|
|
185
|
+
type: s.type,
|
|
186
|
+
selector: s.selector,
|
|
187
|
+
width: s.width,
|
|
188
|
+
height: s.height,
|
|
189
|
+
timestamp: new Date(s.timestamp).toISOString(),
|
|
190
|
+
hasData: !!s.dataUrl,
|
|
191
|
+
})),
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
server.registerTool("get_css_variables", {
|
|
195
|
+
title: "CSS Variables",
|
|
196
|
+
description: "Get all CSS custom properties (--*) defined in the page with their values",
|
|
197
|
+
inputSchema: {
|
|
198
|
+
search: z.string().optional().describe("Filter variables by name"),
|
|
199
|
+
},
|
|
200
|
+
}, (args) => {
|
|
201
|
+
const vars = store.getCssVariables();
|
|
202
|
+
if (!vars)
|
|
203
|
+
return text({ message: "No CSS variable data captured." });
|
|
204
|
+
if (args.search) {
|
|
205
|
+
const filtered = {};
|
|
206
|
+
const q = args.search.toLowerCase();
|
|
207
|
+
for (const [k, v] of Object.entries(vars.variables)) {
|
|
208
|
+
if (k.toLowerCase().includes(q))
|
|
209
|
+
filtered[k] = v;
|
|
210
|
+
}
|
|
211
|
+
return text({ search: args.search, totalMatches: Object.keys(filtered).length, variables: filtered });
|
|
212
|
+
}
|
|
213
|
+
return text(vars);
|
|
214
|
+
});
|
|
215
|
+
server.registerTool("get_typography", {
|
|
216
|
+
title: "Typography Analysis",
|
|
217
|
+
description: "Analyze all font families, sizes, weights, and line-heights used on the page",
|
|
218
|
+
}, () => {
|
|
219
|
+
const typo = store.getTypography();
|
|
220
|
+
if (!typo)
|
|
221
|
+
return text({ message: "No typography data captured." });
|
|
222
|
+
return text(typo);
|
|
223
|
+
});
|
|
224
|
+
server.registerTool("get_color_palette", {
|
|
225
|
+
title: "Color Palette",
|
|
226
|
+
description: "Extract all colors (text, background, border) used on the page with usage counts",
|
|
227
|
+
}, () => {
|
|
228
|
+
const colors = store.getColors();
|
|
229
|
+
if (!colors)
|
|
230
|
+
return text({ message: "No color data captured." });
|
|
231
|
+
return text(colors);
|
|
232
|
+
});
|
|
233
|
+
server.registerTool("get_accessibility_info", {
|
|
234
|
+
title: "Accessibility Audit",
|
|
235
|
+
description: "Check interactive elements for ARIA labels, roles, alt text, headings, and landmarks",
|
|
236
|
+
}, () => {
|
|
237
|
+
const acc = store.getAccessibility();
|
|
238
|
+
if (!acc)
|
|
239
|
+
return text({ message: "No accessibility data captured." });
|
|
240
|
+
return text(acc);
|
|
241
|
+
});
|
|
242
|
+
server.registerTool("get_responsive_info", {
|
|
243
|
+
title: "Responsive Info",
|
|
244
|
+
description: "Get viewport dimensions, device pixel ratio, active media queries, and breakpoint status",
|
|
245
|
+
}, () => {
|
|
246
|
+
const resp = store.getResponsive();
|
|
247
|
+
if (!resp)
|
|
248
|
+
return text({ message: "No responsive data captured." });
|
|
249
|
+
return text(resp);
|
|
250
|
+
});
|
|
251
|
+
server.registerTool("get_spacing_analysis", {
|
|
252
|
+
title: "Spacing Analysis",
|
|
253
|
+
description: "Analyze margins, paddings, and gaps across elements to find inconsistencies and spacing scale",
|
|
254
|
+
}, () => {
|
|
255
|
+
const spacing = store.getSpacing();
|
|
256
|
+
if (!spacing)
|
|
257
|
+
return text({ message: "No spacing data captured." });
|
|
258
|
+
return text(spacing);
|
|
259
|
+
});
|
|
260
|
+
server.registerTool("get_dom_mutations", {
|
|
261
|
+
title: "DOM Mutations",
|
|
262
|
+
description: "Get recent DOM changes (attribute changes, added/removed nodes)",
|
|
263
|
+
inputSchema: {
|
|
264
|
+
since: z.number().optional().describe("Only mutations after this timestamp"),
|
|
265
|
+
limit: z.number().optional().describe("Max mutations to return (default: 50)"),
|
|
266
|
+
},
|
|
267
|
+
}, (args) => {
|
|
268
|
+
const mutations = store.getMutations(args.since);
|
|
269
|
+
const limit = args.limit ?? 50;
|
|
270
|
+
return text({
|
|
271
|
+
total: mutations.length,
|
|
272
|
+
mutations: mutations.slice(0, limit),
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
server.registerTool("compare_with_figma", {
|
|
276
|
+
title: "Compare with Figma",
|
|
277
|
+
description: "Compare a browser element against Figma design specifications. Provide the expected CSS values from Figma and get a detailed diff report with fix suggestions.",
|
|
278
|
+
inputSchema: {
|
|
279
|
+
selector: z.string().describe("CSS selector of the element to compare"),
|
|
280
|
+
figmaSpec: z.object({
|
|
281
|
+
width: z.number().optional(),
|
|
282
|
+
height: z.number().optional(),
|
|
283
|
+
backgroundColor: z.string().optional(),
|
|
284
|
+
color: z.string().optional(),
|
|
285
|
+
fontSize: z.string().optional(),
|
|
286
|
+
fontWeight: z.string().optional(),
|
|
287
|
+
fontFamily: z.string().optional(),
|
|
288
|
+
lineHeight: z.string().optional(),
|
|
289
|
+
letterSpacing: z.string().optional(),
|
|
290
|
+
borderRadius: z.string().optional(),
|
|
291
|
+
borderWidth: z.string().optional(),
|
|
292
|
+
borderColor: z.string().optional(),
|
|
293
|
+
opacity: z.number().optional(),
|
|
294
|
+
gap: z.string().optional(),
|
|
295
|
+
padding: z.string().optional(),
|
|
296
|
+
margin: z.string().optional(),
|
|
297
|
+
boxShadow: z.string().optional(),
|
|
298
|
+
textAlign: z.string().optional(),
|
|
299
|
+
display: z.string().optional(),
|
|
300
|
+
justifyContent: z.string().optional(),
|
|
301
|
+
alignItems: z.string().optional(),
|
|
302
|
+
}).describe("Expected CSS properties from Figma design"),
|
|
303
|
+
},
|
|
304
|
+
}, (args) => {
|
|
305
|
+
const element = store.getElement(args.selector);
|
|
306
|
+
if (!element)
|
|
307
|
+
return text({ error: `Element '${args.selector}' not found` });
|
|
308
|
+
const styles = element.computedStyle.styles;
|
|
309
|
+
const layout = element.layout;
|
|
310
|
+
const differences = [];
|
|
311
|
+
const spec = args.figmaSpec;
|
|
312
|
+
if (spec.width !== undefined) {
|
|
313
|
+
const d = compareValues("width", spec.width + "px", layout.box.width + "px");
|
|
314
|
+
if (d)
|
|
315
|
+
differences.push(d);
|
|
316
|
+
}
|
|
317
|
+
if (spec.height !== undefined) {
|
|
318
|
+
const d = compareValues("height", spec.height + "px", layout.box.height + "px");
|
|
319
|
+
if (d)
|
|
320
|
+
differences.push(d);
|
|
321
|
+
}
|
|
322
|
+
const styleProps = [
|
|
323
|
+
"backgroundColor", "color", "fontSize", "fontWeight", "fontFamily",
|
|
324
|
+
"lineHeight", "letterSpacing", "borderRadius", "borderWidth",
|
|
325
|
+
"borderColor", "gap", "padding", "margin", "boxShadow", "textAlign",
|
|
326
|
+
"display", "justifyContent", "alignItems",
|
|
327
|
+
];
|
|
328
|
+
for (const prop of styleProps) {
|
|
329
|
+
const expected = spec[prop];
|
|
330
|
+
if (expected === undefined)
|
|
331
|
+
continue;
|
|
332
|
+
const camelProp = prop;
|
|
333
|
+
const actual = styles[camelProp] ?? "";
|
|
334
|
+
const d = compareValues(prop, String(expected), actual);
|
|
335
|
+
if (d)
|
|
336
|
+
differences.push(d);
|
|
337
|
+
}
|
|
338
|
+
if (spec.opacity !== undefined) {
|
|
339
|
+
const d = compareValues("opacity", String(spec.opacity), styles.opacity ?? "1");
|
|
340
|
+
if (d)
|
|
341
|
+
differences.push(d);
|
|
342
|
+
}
|
|
343
|
+
const criticalCount = differences.filter((d) => d.severity === "critical").length;
|
|
344
|
+
const majorCount = differences.filter((d) => d.severity === "major").length;
|
|
345
|
+
const totalDiffs = differences.length;
|
|
346
|
+
const score = totalDiffs === 0 ? 100 : Math.max(0, 100 - criticalCount * 25 - majorCount * 10 - (totalDiffs - criticalCount - majorCount) * 3);
|
|
347
|
+
const status = score >= 95 ? "match" : score >= 80 ? "minor-diff" : score >= 50 ? "major-diff" : "mismatch";
|
|
348
|
+
const suggestions = differences.map((d) => d.suggestion);
|
|
349
|
+
const summary = totalDiffs === 0
|
|
350
|
+
? `Perfect match! Element matches Figma spec exactly.`
|
|
351
|
+
: `Found ${totalDiffs} difference(s): ${criticalCount} critical, ${majorCount} major. Score: ${score}/100. ${status === "minor-diff" ? "Close to Figma spec with minor adjustments needed." : "Significant differences from Figma spec — fixes recommended."}`;
|
|
352
|
+
const result = {
|
|
353
|
+
timestamp: Date.now(),
|
|
354
|
+
selector: args.selector,
|
|
355
|
+
score,
|
|
356
|
+
status,
|
|
357
|
+
differences,
|
|
358
|
+
suggestions,
|
|
359
|
+
summary,
|
|
360
|
+
};
|
|
361
|
+
store.addComparison(result);
|
|
362
|
+
return text(result);
|
|
363
|
+
});
|
|
364
|
+
server.registerTool("get_comparison_history", {
|
|
365
|
+
title: "Comparison History",
|
|
366
|
+
description: "Get all previous Figma comparison results",
|
|
367
|
+
}, () => {
|
|
368
|
+
const comparisons = store.getComparisons();
|
|
369
|
+
if (comparisons.length === 0)
|
|
370
|
+
return text({ message: "No comparisons done yet. Use compare_with_figma first." });
|
|
371
|
+
return text({
|
|
372
|
+
total: comparisons.length,
|
|
373
|
+
comparisons: comparisons.map((c) => ({
|
|
374
|
+
selector: c.selector,
|
|
375
|
+
score: c.score,
|
|
376
|
+
status: c.status,
|
|
377
|
+
diffCount: c.differences.length,
|
|
378
|
+
timestamp: new Date(c.timestamp).toISOString(),
|
|
379
|
+
})),
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
server.registerTool("describe_ui", {
|
|
383
|
+
title: "Describe Page UI",
|
|
384
|
+
description: "Generate a comprehensive AI-friendly description of the current page layout, colors, typography, and interactive elements",
|
|
385
|
+
}, () => {
|
|
386
|
+
const dom = store.getDom();
|
|
387
|
+
const colors = store.getColors();
|
|
388
|
+
const typo = store.getTypography();
|
|
389
|
+
const resp = store.getResponsive();
|
|
390
|
+
const acc = store.getAccessibility();
|
|
391
|
+
if (!dom)
|
|
392
|
+
return text({ message: "No page data. Connect browser first." });
|
|
393
|
+
const lines = [];
|
|
394
|
+
lines.push(`# Page: ${dom.title}`);
|
|
395
|
+
lines.push(`URL: ${dom.url}`);
|
|
396
|
+
lines.push(`Viewport: ${dom.viewport.width}x${dom.viewport.height} (${dom.viewport.devicePixelRatio}x DPR)`);
|
|
397
|
+
lines.push(`Total elements: ${dom.totalElements}`);
|
|
398
|
+
lines.push("");
|
|
399
|
+
if (dom.semanticStructure.length > 0) {
|
|
400
|
+
lines.push("## Page Structure");
|
|
401
|
+
for (const node of dom.semanticStructure) {
|
|
402
|
+
const indent = node.level ? " ".repeat(node.level - 1) : "";
|
|
403
|
+
lines.push(`${indent}- <${node.tag}>${node.label ? ` "${node.label}"` : ""} → ${node.selector}`);
|
|
404
|
+
}
|
|
405
|
+
lines.push("");
|
|
406
|
+
}
|
|
407
|
+
if (colors) {
|
|
408
|
+
lines.push("## Color Scheme");
|
|
409
|
+
lines.push(`Unique colors: ${colors.totalUniqueColors}`);
|
|
410
|
+
const topBg = colors.backgroundColors.slice(0, 3).map((c) => c.hex).join(", ");
|
|
411
|
+
const topText = colors.colors.slice(0, 3).map((c) => c.hex).join(", ");
|
|
412
|
+
if (topBg)
|
|
413
|
+
lines.push(`Primary backgrounds: ${topBg}`);
|
|
414
|
+
if (topText)
|
|
415
|
+
lines.push(`Primary text colors: ${topText}`);
|
|
416
|
+
lines.push("");
|
|
417
|
+
}
|
|
418
|
+
if (typo) {
|
|
419
|
+
lines.push("## Typography");
|
|
420
|
+
for (const f of typo.fonts.slice(0, 5)) {
|
|
421
|
+
lines.push(`- ${f.family} ${f.weight} ${f.size}/${f.lineHeight} (${f.count} uses)`);
|
|
422
|
+
}
|
|
423
|
+
lines.push("");
|
|
424
|
+
}
|
|
425
|
+
if (acc) {
|
|
426
|
+
lines.push("## Accessibility Summary");
|
|
427
|
+
lines.push(`Interactive elements: ${acc.summary.totalInteractive} (${acc.summary.withLabels} labeled, ${acc.summary.withoutLabels} unlabeled)`);
|
|
428
|
+
lines.push(`Images: ${acc.summary.imagesWithAlt} with alt, ${acc.summary.imagesWithoutAlt} without`);
|
|
429
|
+
if (acc.summary.landmarks.length > 0)
|
|
430
|
+
lines.push(`Landmarks: ${acc.summary.landmarks.join(", ")}`);
|
|
431
|
+
if (acc.summary.issues.length > 0) {
|
|
432
|
+
lines.push(`Issues: ${acc.summary.issues.length}`);
|
|
433
|
+
for (const issue of acc.summary.issues.slice(0, 5))
|
|
434
|
+
lines.push(` - ${issue}`);
|
|
435
|
+
}
|
|
436
|
+
lines.push("");
|
|
437
|
+
}
|
|
438
|
+
if (resp) {
|
|
439
|
+
lines.push("## Responsive");
|
|
440
|
+
lines.push(`Active breakpoints: ${resp.activeMediaQueries.join(", ") || "none matched"}`);
|
|
441
|
+
}
|
|
442
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
443
|
+
});
|
|
444
|
+
server.registerTool("suggest_css_fixes", {
|
|
445
|
+
title: "Suggest CSS Fixes",
|
|
446
|
+
description: "Based on the latest Figma comparison, suggest specific CSS changes to match the design",
|
|
447
|
+
inputSchema: {
|
|
448
|
+
selector: z.string().optional().describe("Specific element selector (default: latest comparison)"),
|
|
449
|
+
},
|
|
450
|
+
}, (args) => {
|
|
451
|
+
const comparisons = store.getComparisons();
|
|
452
|
+
if (comparisons.length === 0)
|
|
453
|
+
return text({ message: "No comparisons available. Use compare_with_figma first." });
|
|
454
|
+
const comp = args.selector
|
|
455
|
+
? comparisons.find((c) => c.selector === args.selector)
|
|
456
|
+
: comparisons[comparisons.length - 1];
|
|
457
|
+
if (!comp)
|
|
458
|
+
return text({ error: `No comparison found for '${args.selector}'` });
|
|
459
|
+
if (comp.differences.length === 0) {
|
|
460
|
+
return text({ message: `Element '${comp.selector}' perfectly matches Figma spec. No fixes needed.` });
|
|
461
|
+
}
|
|
462
|
+
const cssLines = [];
|
|
463
|
+
cssLines.push(`/* Fix for ${comp.selector} — Score: ${comp.score}/100 */`);
|
|
464
|
+
cssLines.push(`${comp.selector} {`);
|
|
465
|
+
for (const diff of comp.differences) {
|
|
466
|
+
const cssProp = diff.property.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
|
|
467
|
+
cssLines.push(` ${cssProp}: ${diff.expected}; /* was: ${diff.actual} */`);
|
|
468
|
+
}
|
|
469
|
+
cssLines.push("}");
|
|
470
|
+
return {
|
|
471
|
+
content: [{
|
|
472
|
+
type: "text",
|
|
473
|
+
text: [
|
|
474
|
+
`## CSS Fix for ${comp.selector}`,
|
|
475
|
+
`Score: ${comp.score}/100 (${comp.status})`,
|
|
476
|
+
`Differences: ${comp.differences.length}`,
|
|
477
|
+
"",
|
|
478
|
+
"```css",
|
|
479
|
+
...cssLines,
|
|
480
|
+
"```",
|
|
481
|
+
"",
|
|
482
|
+
"### Changes:",
|
|
483
|
+
...comp.differences.map((d) => `- **${d.property}**: \`${d.actual}\` → \`${d.expected}\` (${d.severity})`),
|
|
484
|
+
].join("\n"),
|
|
485
|
+
}],
|
|
486
|
+
};
|
|
487
|
+
});
|
|
488
|
+
server.registerTool("get_element_hierarchy", {
|
|
489
|
+
title: "Element Hierarchy",
|
|
490
|
+
description: "Show parent-child relationships for a specific element in the DOM tree",
|
|
491
|
+
inputSchema: {
|
|
492
|
+
selector: z.string().describe("CSS selector to find in the tree"),
|
|
493
|
+
},
|
|
494
|
+
}, (args) => {
|
|
495
|
+
const dom = store.getDom();
|
|
496
|
+
if (!dom)
|
|
497
|
+
return text({ message: "No DOM data captured." });
|
|
498
|
+
const path = [];
|
|
499
|
+
const find = (node, trail) => {
|
|
500
|
+
const sel = node.selector;
|
|
501
|
+
const currentTrail = [...trail, sel];
|
|
502
|
+
if (sel === args.selector || node.tagName?.toLowerCase() === args.selector.toLowerCase()) {
|
|
503
|
+
path.push(...currentTrail);
|
|
504
|
+
return true;
|
|
505
|
+
}
|
|
506
|
+
const children = node.children ?? [];
|
|
507
|
+
for (const child of children) {
|
|
508
|
+
if (find(child, currentTrail))
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
return false;
|
|
512
|
+
};
|
|
513
|
+
find(dom.rootElement, []);
|
|
514
|
+
if (path.length === 0)
|
|
515
|
+
return text({ error: `Element '${args.selector}' not found in DOM tree.` });
|
|
516
|
+
return text({
|
|
517
|
+
selector: args.selector,
|
|
518
|
+
depth: path.length,
|
|
519
|
+
hierarchy: path.map((p, i) => ({ depth: i, selector: p })),
|
|
520
|
+
});
|
|
521
|
+
});
|
|
522
|
+
server.registerTool("get_elements_summary", {
|
|
523
|
+
title: "Elements Summary",
|
|
524
|
+
description: "Get a summary of all captured element details with their selectors",
|
|
525
|
+
}, () => {
|
|
526
|
+
const elements = store.getElements();
|
|
527
|
+
const keys = Object.keys(elements);
|
|
528
|
+
if (keys.length === 0)
|
|
529
|
+
return text({ message: "No element details captured." });
|
|
530
|
+
return text({
|
|
531
|
+
totalCaptured: keys.length,
|
|
532
|
+
elements: keys.map((k) => ({
|
|
533
|
+
selector: k,
|
|
534
|
+
tagName: elements[k].snapshot?.tagName,
|
|
535
|
+
display: elements[k].layout?.display,
|
|
536
|
+
size: elements[k].layout ? `${Math.round(elements[k].layout.box.width)}x${Math.round(elements[k].layout.box.height)}` : "unknown",
|
|
537
|
+
classes: elements[k].computedStyle?.appliedClasses?.join(" ") || "",
|
|
538
|
+
})),
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
server.registerTool("clear_data", {
|
|
542
|
+
title: "Clear All Data",
|
|
543
|
+
description: "Clear all captured browser data: DOM, styles, screenshots, comparisons",
|
|
544
|
+
}, () => {
|
|
545
|
+
const count = store.getElementCount();
|
|
546
|
+
store.clear();
|
|
547
|
+
return text({ cleared: count, remaining: 0 });
|
|
548
|
+
});
|
|
549
|
+
server.registerTool("get_connection_status", {
|
|
550
|
+
title: "Connection Status",
|
|
551
|
+
description: "Check if the browser is connected and what data is available. Use this first to verify the bookmarklet is active.",
|
|
552
|
+
}, () => {
|
|
553
|
+
const info = store.getPageInfo();
|
|
554
|
+
const connected = info.hasDom || info.totalElements > 0;
|
|
555
|
+
const httpPort = parseInt(process.env.MCP_BROWSER_LENS_PORT ?? "3202", 10);
|
|
556
|
+
return text({
|
|
557
|
+
connected,
|
|
558
|
+
connectorUrl: `http://localhost:${httpPort}`,
|
|
559
|
+
instructions: connected
|
|
560
|
+
? "Browser connected! Data is available for inspection."
|
|
561
|
+
: "Not connected. Open http://localhost:" + httpPort + " in your browser, drag the bookmarklet to your bookmarks bar, then click it on any page.",
|
|
562
|
+
dataAvailable: {
|
|
563
|
+
dom: info.hasDom,
|
|
564
|
+
elements: info.totalElements,
|
|
565
|
+
screenshots: info.screenshotCount,
|
|
566
|
+
cssVariables: info.hasCssVariables,
|
|
567
|
+
typography: info.hasTypography,
|
|
568
|
+
colors: info.hasColors,
|
|
569
|
+
accessibility: info.hasAccessibility,
|
|
570
|
+
responsive: info.hasResponsive,
|
|
571
|
+
spacing: info.hasSpacing,
|
|
572
|
+
mutations: info.mutationCount,
|
|
573
|
+
comparisons: info.comparisonCount,
|
|
574
|
+
},
|
|
575
|
+
url: info.url,
|
|
576
|
+
title: info.title,
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
server.registerTool("get_full_page_analysis", {
|
|
580
|
+
title: "Full Page Analysis",
|
|
581
|
+
description: "Get a comprehensive analysis of the entire page: structure, design tokens, typography, colors, spacing, accessibility — everything in one call. Best used as a first step after connecting.",
|
|
582
|
+
}, () => {
|
|
583
|
+
const dom = store.getDom();
|
|
584
|
+
if (!dom)
|
|
585
|
+
return text({ message: "No browser connected. Use get_connection_status for setup instructions." });
|
|
586
|
+
const colors = store.getColors();
|
|
587
|
+
const typo = store.getTypography();
|
|
588
|
+
const spacing = store.getSpacing();
|
|
589
|
+
const acc = store.getAccessibility();
|
|
590
|
+
const resp = store.getResponsive();
|
|
591
|
+
const vars = store.getCssVariables();
|
|
592
|
+
const elements = store.getElements();
|
|
593
|
+
const sections = [];
|
|
594
|
+
sections.push(`# Full Page Analysis: ${dom.title}`);
|
|
595
|
+
sections.push(`**URL:** ${dom.url}`);
|
|
596
|
+
sections.push(`**Viewport:** ${dom.viewport.width}x${dom.viewport.height} @${dom.viewport.devicePixelRatio}x`);
|
|
597
|
+
sections.push(`**Total Elements:** ${dom.totalElements}`);
|
|
598
|
+
sections.push(`**Captured Details:** ${Object.keys(elements).length} elements`);
|
|
599
|
+
sections.push("");
|
|
600
|
+
sections.push("## Semantic Structure");
|
|
601
|
+
for (const node of dom.semanticStructure) {
|
|
602
|
+
const prefix = node.level ? " ".repeat(node.level - 1) + "- " : "- ";
|
|
603
|
+
sections.push(`${prefix}<${node.tag}>${node.label ? ` "${node.label}"` : ""} → \`${node.selector}\``);
|
|
604
|
+
}
|
|
605
|
+
sections.push("");
|
|
606
|
+
if (vars) {
|
|
607
|
+
sections.push(`## Design Tokens (${vars.totalCount} CSS variables)`);
|
|
608
|
+
const grouped = {};
|
|
609
|
+
for (const [k, v] of Object.entries(vars.variables)) {
|
|
610
|
+
const category = k.includes("color") || k.includes("bg") || v.startsWith("#") || v.startsWith("rgb") ? "Colors"
|
|
611
|
+
: k.includes("font") || k.includes("size") || k.includes("weight") ? "Typography"
|
|
612
|
+
: k.includes("space") || k.includes("gap") || k.includes("radius") || k.includes("padding") || k.includes("margin") ? "Spacing"
|
|
613
|
+
: "Other";
|
|
614
|
+
if (!grouped[category])
|
|
615
|
+
grouped[category] = [];
|
|
616
|
+
grouped[category].push(`\`${k}\`: ${v}`);
|
|
617
|
+
}
|
|
618
|
+
for (const [cat, items] of Object.entries(grouped)) {
|
|
619
|
+
sections.push(`### ${cat}`);
|
|
620
|
+
for (const item of items.slice(0, 15))
|
|
621
|
+
sections.push(`- ${item}`);
|
|
622
|
+
sections.push("");
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
if (typo) {
|
|
626
|
+
sections.push(`## Typography (${typo.fonts.length} combinations)`);
|
|
627
|
+
for (const f of typo.fonts.slice(0, 8)) {
|
|
628
|
+
sections.push(`- **${f.family}** ${f.weight} ${f.size}/${f.lineHeight} — ${f.count} elements`);
|
|
629
|
+
}
|
|
630
|
+
sections.push("");
|
|
631
|
+
}
|
|
632
|
+
if (colors) {
|
|
633
|
+
sections.push(`## Color Palette (${colors.totalUniqueColors} unique colors)`);
|
|
634
|
+
sections.push("### Text Colors");
|
|
635
|
+
for (const c of colors.colors.slice(0, 5))
|
|
636
|
+
sections.push(`- \`${c.hex}\` — ${c.count} uses`);
|
|
637
|
+
sections.push("### Background Colors");
|
|
638
|
+
for (const c of colors.backgroundColors.slice(0, 5))
|
|
639
|
+
sections.push(`- \`${c.hex}\` — ${c.count} uses`);
|
|
640
|
+
sections.push("### Border Colors");
|
|
641
|
+
for (const c of colors.borderColors.slice(0, 3))
|
|
642
|
+
sections.push(`- \`${c.hex}\` — ${c.count} uses`);
|
|
643
|
+
sections.push("");
|
|
644
|
+
}
|
|
645
|
+
if (spacing) {
|
|
646
|
+
sections.push(`## Spacing Scale`);
|
|
647
|
+
sections.push(`Values in use: ${spacing.spacingScale.join(", ")}`);
|
|
648
|
+
if (spacing.inconsistencies.length > 0) {
|
|
649
|
+
sections.push(`⚠️ ${spacing.inconsistencies.length} inconsistencies detected`);
|
|
650
|
+
}
|
|
651
|
+
sections.push("");
|
|
652
|
+
}
|
|
653
|
+
if (acc) {
|
|
654
|
+
sections.push("## Accessibility");
|
|
655
|
+
sections.push(`- Interactive: ${acc.summary.totalInteractive} (${acc.summary.withLabels} with labels)`);
|
|
656
|
+
sections.push(`- Images: ${acc.summary.imagesWithAlt} with alt / ${acc.summary.imagesWithoutAlt} missing`);
|
|
657
|
+
sections.push(`- Headings: ${Object.entries(acc.summary.headingLevels).map(([k, v]) => `${k}:${v}`).join(", ")}`);
|
|
658
|
+
sections.push(`- Landmarks: ${acc.summary.landmarks.join(", ") || "none"}`);
|
|
659
|
+
if (acc.summary.issues.length > 0) {
|
|
660
|
+
sections.push(`### Issues (${acc.summary.issues.length})`);
|
|
661
|
+
for (const i of acc.summary.issues.slice(0, 10))
|
|
662
|
+
sections.push(`- ${i}`);
|
|
663
|
+
}
|
|
664
|
+
sections.push("");
|
|
665
|
+
}
|
|
666
|
+
if (resp) {
|
|
667
|
+
sections.push("## Responsive");
|
|
668
|
+
sections.push(`Active: ${resp.activeMediaQueries.join(", ") || "no matched breakpoints"}`);
|
|
669
|
+
sections.push(`Scroll: ${resp.viewport.scrollWidth}x${resp.viewport.scrollHeight}`);
|
|
670
|
+
}
|
|
671
|
+
return { content: [{ type: "text", text: sections.join("\n") }] };
|
|
672
|
+
});
|
|
673
|
+
server.registerTool("get_design_tokens", {
|
|
674
|
+
title: "Design Tokens",
|
|
675
|
+
description: "Extract design tokens from the page: CSS variables grouped by category (colors, typography, spacing, breakpoints), font stacks, and color palette as a design system reference.",
|
|
676
|
+
}, () => {
|
|
677
|
+
const vars = store.getCssVariables();
|
|
678
|
+
const colors = store.getColors();
|
|
679
|
+
const typo = store.getTypography();
|
|
680
|
+
const spacing = store.getSpacing();
|
|
681
|
+
if (!vars && !colors && !typo)
|
|
682
|
+
return text({ message: "No design data captured. Connect browser first." });
|
|
683
|
+
const tokens = {};
|
|
684
|
+
if (vars) {
|
|
685
|
+
const colorTokens = {};
|
|
686
|
+
const typographyTokens = {};
|
|
687
|
+
const spacingTokens = {};
|
|
688
|
+
const otherTokens = {};
|
|
689
|
+
for (const [k, v] of Object.entries(vars.variables)) {
|
|
690
|
+
if (k.includes("color") || k.includes("bg") || v.startsWith("#") || v.startsWith("rgb") || v.startsWith("hsl"))
|
|
691
|
+
colorTokens[k] = v;
|
|
692
|
+
else if (k.includes("font") || k.includes("size") || k.includes("weight") || k.includes("line-height"))
|
|
693
|
+
typographyTokens[k] = v;
|
|
694
|
+
else if (k.includes("space") || k.includes("gap") || k.includes("radius") || k.includes("padding") || k.includes("margin"))
|
|
695
|
+
spacingTokens[k] = v;
|
|
696
|
+
else
|
|
697
|
+
otherTokens[k] = v;
|
|
698
|
+
}
|
|
699
|
+
tokens.cssVariables = { colors: colorTokens, typography: typographyTokens, spacing: spacingTokens, other: otherTokens };
|
|
700
|
+
}
|
|
701
|
+
if (colors) {
|
|
702
|
+
tokens.colorPalette = {
|
|
703
|
+
text: colors.colors.slice(0, 10).map((c) => ({ hex: c.hex, count: c.count })),
|
|
704
|
+
background: colors.backgroundColors.slice(0, 10).map((c) => ({ hex: c.hex, count: c.count })),
|
|
705
|
+
border: colors.borderColors.slice(0, 5).map((c) => ({ hex: c.hex, count: c.count })),
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
if (typo) {
|
|
709
|
+
tokens.fontStacks = typo.fonts.slice(0, 10).map((f) => ({
|
|
710
|
+
family: f.family, size: f.size, weight: f.weight, lineHeight: f.lineHeight, count: f.count,
|
|
711
|
+
}));
|
|
712
|
+
}
|
|
713
|
+
if (spacing) {
|
|
714
|
+
tokens.spacingScale = spacing.spacingScale;
|
|
715
|
+
}
|
|
716
|
+
return text(tokens);
|
|
717
|
+
});
|
|
718
|
+
server.registerTool("get_visual_diff_report", {
|
|
719
|
+
title: "Visual Diff Report",
|
|
720
|
+
description: "Generate a comprehensive visual diff report comparing the current UI against all previously compared Figma specs. Shows overall score, per-element results, and prioritized fix list.",
|
|
721
|
+
}, () => {
|
|
722
|
+
const comparisons = store.getComparisons();
|
|
723
|
+
if (comparisons.length === 0)
|
|
724
|
+
return text({ message: "No comparisons done. Use compare_with_figma first." });
|
|
725
|
+
const scores = comparisons.map((c) => c.score);
|
|
726
|
+
const avgScore = Math.round(scores.reduce((a, b) => a + b, 0) / scores.length);
|
|
727
|
+
const passing = comparisons.filter((c) => c.score >= 90).length;
|
|
728
|
+
const failing = comparisons.filter((c) => c.score < 90).length;
|
|
729
|
+
const allDiffs = comparisons.flatMap((c) => c.differences.map((d) => ({ ...d, element: c.selector })));
|
|
730
|
+
const critical = allDiffs.filter((d) => d.severity === "critical");
|
|
731
|
+
const major = allDiffs.filter((d) => d.severity === "major");
|
|
732
|
+
const minor = allDiffs.filter((d) => d.severity === "minor");
|
|
733
|
+
const sections = [];
|
|
734
|
+
sections.push("# Visual Diff Report");
|
|
735
|
+
sections.push(`**Overall Score:** ${avgScore}/100`);
|
|
736
|
+
sections.push(`**Elements Compared:** ${comparisons.length} (${passing} passing, ${failing} failing)`);
|
|
737
|
+
sections.push(`**Total Differences:** ${allDiffs.length} (${critical.length} critical, ${major.length} major, ${minor.length} minor)`);
|
|
738
|
+
sections.push("");
|
|
739
|
+
sections.push("## Per-Element Results");
|
|
740
|
+
for (const c of comparisons.sort((a, b) => a.score - b.score)) {
|
|
741
|
+
const icon = c.score >= 90 ? "✅" : c.score >= 50 ? "⚠️" : "❌";
|
|
742
|
+
sections.push(`${icon} **${c.selector}** — ${c.score}/100 (${c.status})`);
|
|
743
|
+
if (c.differences.length > 0) {
|
|
744
|
+
for (const d of c.differences) {
|
|
745
|
+
sections.push(` - ${d.property}: \`${d.actual}\` → \`${d.expected}\` (${d.severity})`);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
sections.push("");
|
|
750
|
+
if (critical.length + major.length > 0) {
|
|
751
|
+
sections.push("## Priority Fixes");
|
|
752
|
+
for (const d of [...critical, ...major]) {
|
|
753
|
+
sections.push(`- **${d.element}** → ${d.property}: ${d.suggestion}`);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return { content: [{ type: "text", text: sections.join("\n") }] };
|
|
757
|
+
});
|
|
758
|
+
server.registerTool("capture_screenshot_with_analysis", {
|
|
759
|
+
title: "Screenshot with Analysis",
|
|
760
|
+
description: "Get the latest screenshot AND a detailed analysis of what's visible: layout structure, key elements, colors, text content, interactive components. Returns both the image and a text description.",
|
|
761
|
+
}, () => {
|
|
762
|
+
const shot = store.getLatestScreenshot();
|
|
763
|
+
const dom = store.getDom();
|
|
764
|
+
const colors = store.getColors();
|
|
765
|
+
const typo = store.getTypography();
|
|
766
|
+
const elements = store.getElements();
|
|
767
|
+
const acc = store.getAccessibility();
|
|
768
|
+
if (!dom)
|
|
769
|
+
return text({ message: "No browser connected. Use get_connection_status for setup instructions." });
|
|
770
|
+
const analysis = [];
|
|
771
|
+
analysis.push(`# Screenshot Analysis: ${dom.title}`);
|
|
772
|
+
analysis.push(`**URL:** ${dom.url}`);
|
|
773
|
+
analysis.push(`**Viewport:** ${dom.viewport.width}x${dom.viewport.height}`);
|
|
774
|
+
analysis.push("");
|
|
775
|
+
analysis.push("## Page Layout");
|
|
776
|
+
const layoutElements = Object.entries(elements);
|
|
777
|
+
const byDisplay = {};
|
|
778
|
+
for (const [sel, el] of layoutElements) {
|
|
779
|
+
const d = el.layout?.display ?? "block";
|
|
780
|
+
if (!byDisplay[d])
|
|
781
|
+
byDisplay[d] = [];
|
|
782
|
+
byDisplay[d].push(`${sel} (${Math.round(el.layout?.box.width ?? 0)}x${Math.round(el.layout?.box.height ?? 0)})`);
|
|
783
|
+
}
|
|
784
|
+
for (const [display, items] of Object.entries(byDisplay)) {
|
|
785
|
+
if (items.length > 0)
|
|
786
|
+
analysis.push(`- **${display}**: ${items.slice(0, 5).join(", ")}${items.length > 5 ? ` +${items.length - 5} more` : ""}`);
|
|
787
|
+
}
|
|
788
|
+
analysis.push("");
|
|
789
|
+
analysis.push("## Visible Elements");
|
|
790
|
+
for (const [sel, el] of layoutElements.slice(0, 15)) {
|
|
791
|
+
const s = el.computedStyle?.styles ?? {};
|
|
792
|
+
const w = Math.round(el.layout?.box.width ?? 0);
|
|
793
|
+
const h = Math.round(el.layout?.box.height ?? 0);
|
|
794
|
+
const bg = s.backgroundColor && s.backgroundColor !== "rgba(0, 0, 0, 0)" ? ` bg:${s.backgroundColor}` : "";
|
|
795
|
+
const color = s.color ? ` text:${s.color}` : "";
|
|
796
|
+
const font = s.fontSize ? ` ${s.fontSize}` : "";
|
|
797
|
+
analysis.push(`- \`${sel}\` — ${w}x${h}${bg}${color}${font}`);
|
|
798
|
+
}
|
|
799
|
+
analysis.push("");
|
|
800
|
+
if (colors) {
|
|
801
|
+
analysis.push("## Dominant Colors");
|
|
802
|
+
analysis.push(`Backgrounds: ${colors.backgroundColors.slice(0, 3).map((c) => c.hex).join(", ")}`);
|
|
803
|
+
analysis.push(`Text: ${colors.colors.slice(0, 3).map((c) => c.hex).join(", ")}`);
|
|
804
|
+
analysis.push("");
|
|
805
|
+
}
|
|
806
|
+
if (typo) {
|
|
807
|
+
analysis.push("## Text Styles");
|
|
808
|
+
for (const f of typo.fonts.slice(0, 4)) {
|
|
809
|
+
analysis.push(`- ${f.family} ${f.weight} ${f.size} — ${f.count} elements`);
|
|
810
|
+
}
|
|
811
|
+
analysis.push("");
|
|
812
|
+
}
|
|
813
|
+
if (acc && acc.summary.issues.length > 0) {
|
|
814
|
+
analysis.push(`## Issues Spotted (${acc.summary.issues.length})`);
|
|
815
|
+
for (const i of acc.summary.issues.slice(0, 5))
|
|
816
|
+
analysis.push(`- ${i}`);
|
|
817
|
+
}
|
|
818
|
+
const content = [];
|
|
819
|
+
if (shot) {
|
|
820
|
+
const base64 = shot.dataUrl.replace(/^data:image\/\w+;base64,/, "");
|
|
821
|
+
content.push({ type: "image", data: base64, mimeType: "image/png" });
|
|
822
|
+
}
|
|
823
|
+
content.push({ type: "text", text: analysis.join("\n") });
|
|
824
|
+
return { content };
|
|
825
|
+
});
|
|
826
|
+
server.registerTool("inspect_feature_area", {
|
|
827
|
+
title: "Inspect Feature Area",
|
|
828
|
+
description: "Inspect a specific feature area of the page by providing a parent selector. Returns screenshot + all child elements with their styles, layout, and a summary of the feature's visual design.",
|
|
829
|
+
inputSchema: {
|
|
830
|
+
selector: z.string().describe("CSS selector of the feature container (e.g. '.hero-section', '#sidebar', 'nav')"),
|
|
831
|
+
},
|
|
832
|
+
}, (args) => {
|
|
833
|
+
const dom = store.getDom();
|
|
834
|
+
if (!dom)
|
|
835
|
+
return text({ message: "No browser connected." });
|
|
836
|
+
const elements = store.getElements();
|
|
837
|
+
const matching = Object.entries(elements).filter(([sel]) => sel === args.selector || sel.startsWith(args.selector + " ") || sel.startsWith(args.selector + "."));
|
|
838
|
+
if (matching.length === 0) {
|
|
839
|
+
const domResults = store.querySelector(args.selector);
|
|
840
|
+
if (domResults.length > 0) {
|
|
841
|
+
return text({
|
|
842
|
+
message: `Found ${domResults.length} element(s) in DOM tree but no detailed data. Elements nearby in the capture:`,
|
|
843
|
+
domMatches: domResults.slice(0, 5).map((e) => ({ selector: e.selector, tag: e.tagName, children: e.childCount })),
|
|
844
|
+
allCapturedSelectors: Object.keys(elements).slice(0, 20),
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
return text({ error: `No elements matching '${args.selector}'. Available: ${Object.keys(elements).slice(0, 10).join(", ")}` });
|
|
848
|
+
}
|
|
849
|
+
const sections = [];
|
|
850
|
+
sections.push(`# Feature Area: ${args.selector}`);
|
|
851
|
+
sections.push(`**Elements Found:** ${matching.length}`);
|
|
852
|
+
sections.push("");
|
|
853
|
+
for (const [sel, el] of matching.slice(0, 20)) {
|
|
854
|
+
const s = el.computedStyle?.styles ?? {};
|
|
855
|
+
const l = el.layout;
|
|
856
|
+
sections.push(`## \`${sel}\``);
|
|
857
|
+
sections.push(`- **Tag:** ${el.snapshot?.tagName ?? "?"} | **Display:** ${l?.display ?? "?"} | **Size:** ${Math.round(l?.box.width ?? 0)}x${Math.round(l?.box.height ?? 0)}`);
|
|
858
|
+
if (el.snapshot?.textContent)
|
|
859
|
+
sections.push(`- **Text:** "${el.snapshot.textContent.slice(0, 100)}"`);
|
|
860
|
+
const keyStyles = [];
|
|
861
|
+
if (s.backgroundColor && s.backgroundColor !== "rgba(0, 0, 0, 0)")
|
|
862
|
+
keyStyles.push(`bg: ${s.backgroundColor}`);
|
|
863
|
+
if (s.color)
|
|
864
|
+
keyStyles.push(`color: ${s.color}`);
|
|
865
|
+
if (s.fontSize)
|
|
866
|
+
keyStyles.push(`font: ${s.fontSize} ${s.fontWeight ?? ""}`);
|
|
867
|
+
if (s.padding && s.padding !== "0px")
|
|
868
|
+
keyStyles.push(`padding: ${s.padding}`);
|
|
869
|
+
if (s.gap && s.gap !== "normal")
|
|
870
|
+
keyStyles.push(`gap: ${s.gap}`);
|
|
871
|
+
if (s.borderRadius && s.borderRadius !== "0px")
|
|
872
|
+
keyStyles.push(`radius: ${s.borderRadius}`);
|
|
873
|
+
if (keyStyles.length > 0)
|
|
874
|
+
sections.push(`- **Styles:** ${keyStyles.join(" | ")}`);
|
|
875
|
+
if (el.snapshot?.classNames?.length)
|
|
876
|
+
sections.push(`- **Classes:** ${el.snapshot.classNames.join(", ")}`);
|
|
877
|
+
sections.push("");
|
|
878
|
+
}
|
|
879
|
+
const content = [];
|
|
880
|
+
const shot = store.getLatestScreenshot();
|
|
881
|
+
if (shot) {
|
|
882
|
+
const base64 = shot.dataUrl.replace(/^data:image\/\w+;base64,/, "");
|
|
883
|
+
content.push({ type: "image", data: base64, mimeType: "image/png" });
|
|
884
|
+
}
|
|
885
|
+
content.push({ type: "text", text: sections.join("\n") });
|
|
886
|
+
return { content };
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
//# sourceMappingURL=index.js.map
|