@sensaiorg/adapter-chrome 0.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/dist/chrome-adapter.d.ts.map +1 -0
- package/dist/chrome-adapter.js +91 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/tools/chrome-tools.d.ts.map +1 -0
- package/dist/tools/chrome-tools.js +583 -0
- package/dist/tools/chrome-tools.test.d.ts.map +1 -0
- package/dist/tools/chrome-tools.test.js +442 -0
- package/dist/transport/cdp-bridge.d.ts.map +1 -0
- package/dist/transport/cdp-bridge.js +163 -0
- package/package.json +23 -0
- package/src/chrome-adapter.ts +117 -0
- package/src/index.ts +8 -0
- package/src/tools/chrome-tools.test.ts +547 -0
- package/src/tools/chrome-tools.ts +743 -0
- package/src/transport/cdp-bridge.ts +187 -0
- package/tsconfig.json +11 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Chrome MCP Tools — registered when Chrome is connected via CDP.
|
|
4
|
+
*
|
|
5
|
+
* Uses the CdpBridge to send Chrome DevTools Protocol commands
|
|
6
|
+
* and collect console/network events.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.CdpEventCollector = void 0;
|
|
10
|
+
exports.registerChromeTools = registerChromeTools;
|
|
11
|
+
const zod_1 = require("zod");
|
|
12
|
+
/* ── Event collector ─────────────────────────────────────────────── */
|
|
13
|
+
/**
|
|
14
|
+
* Manages CDP domain subscriptions and stores events for later retrieval.
|
|
15
|
+
*/
|
|
16
|
+
class CdpEventCollector {
|
|
17
|
+
consoleLogs = [];
|
|
18
|
+
networkEntries = new Map();
|
|
19
|
+
maxEntries = 500;
|
|
20
|
+
domainsEnabled = false;
|
|
21
|
+
/** Enable Console + Network domains and start collecting events. */
|
|
22
|
+
async enableDomains(bridge) {
|
|
23
|
+
if (this.domainsEnabled)
|
|
24
|
+
return;
|
|
25
|
+
// Listen for CDP events via the bridge
|
|
26
|
+
bridge.onEvent("Runtime.consoleAPICalled", (params) => {
|
|
27
|
+
const p = params;
|
|
28
|
+
const text = (p.args ?? [])
|
|
29
|
+
.map((a) => a.value !== undefined ? String(a.value) : (a.description ?? ""))
|
|
30
|
+
.join(" ");
|
|
31
|
+
const frame = p.stackTrace?.callFrames?.[0];
|
|
32
|
+
this.addConsoleEntry({
|
|
33
|
+
level: p.type ?? "log",
|
|
34
|
+
text,
|
|
35
|
+
timestamp: p.timestamp ?? Date.now(),
|
|
36
|
+
url: frame?.url,
|
|
37
|
+
line: frame?.lineNumber,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
bridge.onEvent("Runtime.exceptionThrown", (params) => {
|
|
41
|
+
const p = params;
|
|
42
|
+
const details = p.exceptionDetails;
|
|
43
|
+
this.addConsoleEntry({
|
|
44
|
+
level: "error",
|
|
45
|
+
text: details?.exception?.description ?? details?.text ?? "Unknown exception",
|
|
46
|
+
timestamp: p.timestamp ?? Date.now(),
|
|
47
|
+
url: details?.url,
|
|
48
|
+
line: details?.lineNumber,
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
bridge.onEvent("Network.requestWillBeSent", (params) => {
|
|
52
|
+
const p = params;
|
|
53
|
+
this.networkEntries.set(p.requestId, {
|
|
54
|
+
requestId: p.requestId,
|
|
55
|
+
method: p.request.method,
|
|
56
|
+
url: p.request.url,
|
|
57
|
+
type: p.type,
|
|
58
|
+
startTime: p.timestamp,
|
|
59
|
+
});
|
|
60
|
+
this.trimNetwork();
|
|
61
|
+
});
|
|
62
|
+
bridge.onEvent("Network.responseReceived", (params) => {
|
|
63
|
+
const p = params;
|
|
64
|
+
const entry = this.networkEntries.get(p.requestId);
|
|
65
|
+
if (entry) {
|
|
66
|
+
entry.status = p.response.status;
|
|
67
|
+
entry.statusText = p.response.statusText;
|
|
68
|
+
entry.endTime = p.timestamp;
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
bridge.onEvent("Network.loadingFinished", (params) => {
|
|
72
|
+
const p = params;
|
|
73
|
+
const entry = this.networkEntries.get(p.requestId);
|
|
74
|
+
if (entry) {
|
|
75
|
+
entry.encodedDataLength = p.encodedDataLength;
|
|
76
|
+
entry.endTime = p.timestamp;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
bridge.onEvent("Network.loadingFailed", (params) => {
|
|
80
|
+
const p = params;
|
|
81
|
+
const entry = this.networkEntries.get(p.requestId);
|
|
82
|
+
if (entry) {
|
|
83
|
+
entry.error = p.errorText;
|
|
84
|
+
entry.endTime = p.timestamp;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// Enable CDP domains
|
|
88
|
+
await bridge.send("Runtime.enable");
|
|
89
|
+
await bridge.send("Network.enable");
|
|
90
|
+
await bridge.send("Page.enable");
|
|
91
|
+
this.domainsEnabled = true;
|
|
92
|
+
}
|
|
93
|
+
getConsoleLogs(maxCount) {
|
|
94
|
+
const limit = maxCount ?? this.consoleLogs.length;
|
|
95
|
+
return this.consoleLogs.slice(-limit);
|
|
96
|
+
}
|
|
97
|
+
getNetworkEntries(maxCount) {
|
|
98
|
+
const all = Array.from(this.networkEntries.values());
|
|
99
|
+
const limit = maxCount ?? all.length;
|
|
100
|
+
return all.slice(-limit);
|
|
101
|
+
}
|
|
102
|
+
addConsoleEntry(entry) {
|
|
103
|
+
this.consoleLogs.push(entry);
|
|
104
|
+
if (this.consoleLogs.length > this.maxEntries) {
|
|
105
|
+
this.consoleLogs.splice(0, this.consoleLogs.length - this.maxEntries);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
trimNetwork() {
|
|
109
|
+
if (this.networkEntries.size > this.maxEntries) {
|
|
110
|
+
const keys = Array.from(this.networkEntries.keys());
|
|
111
|
+
const toRemove = keys.slice(0, keys.length - this.maxEntries);
|
|
112
|
+
for (const k of toRemove) {
|
|
113
|
+
this.networkEntries.delete(k);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
exports.CdpEventCollector = CdpEventCollector;
|
|
119
|
+
/* ── Tool registration ───────────────────────────────────────────── */
|
|
120
|
+
function registerChromeTools(server, bridge, collector, prefix) {
|
|
121
|
+
// ── chrome_diagnose_page ──────────────────────────────────────
|
|
122
|
+
server.tool(`${prefix}diagnose_page`, "Get a comprehensive diagnosis of the current web page: URL, title, console errors, network failures, DOM element counts. START HERE for any Chrome debugging session.", {
|
|
123
|
+
includeNetwork: zod_1.z.boolean().optional().describe("Include recent network requests (default: true)"),
|
|
124
|
+
}, async ({ includeNetwork }) => {
|
|
125
|
+
try {
|
|
126
|
+
const [pageInfo, docResult] = await Promise.all([
|
|
127
|
+
bridge.send("Runtime.evaluate", {
|
|
128
|
+
expression: "JSON.stringify({ url: location.href, title: document.title, readyState: document.readyState, viewport: { width: window.innerWidth, height: window.innerHeight }, cookieCount: document.cookie.split(';').filter(c => c.trim()).length })",
|
|
129
|
+
returnByValue: true,
|
|
130
|
+
}),
|
|
131
|
+
bridge.send("Runtime.evaluate", {
|
|
132
|
+
expression: "JSON.stringify({ elements: document.querySelectorAll('*').length, forms: document.forms.length, images: document.images.length, links: document.links.length, scripts: document.scripts.length })",
|
|
133
|
+
returnByValue: true,
|
|
134
|
+
}),
|
|
135
|
+
]);
|
|
136
|
+
const page = pageInfo.result.value ? JSON.parse(pageInfo.result.value) : {};
|
|
137
|
+
const dom = docResult.result.value ? JSON.parse(docResult.result.value) : {};
|
|
138
|
+
const consoleErrors = collector.getConsoleLogs()
|
|
139
|
+
.filter((e) => e.level === "error" || e.level === "warning")
|
|
140
|
+
.slice(-20);
|
|
141
|
+
const result = {
|
|
142
|
+
page,
|
|
143
|
+
dom,
|
|
144
|
+
consoleErrors: consoleErrors.length,
|
|
145
|
+
recentErrors: consoleErrors,
|
|
146
|
+
};
|
|
147
|
+
if (includeNetwork !== false) {
|
|
148
|
+
const networkEntries = collector.getNetworkEntries();
|
|
149
|
+
const failures = networkEntries.filter((e) => e.error || (e.status && e.status >= 400));
|
|
150
|
+
result.networkSummary = {
|
|
151
|
+
totalRequests: networkEntries.length,
|
|
152
|
+
failedRequests: failures.length,
|
|
153
|
+
failures: failures.slice(-10),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: "text", text: `diagnose_page failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
163
|
+
isError: true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
// ── chrome_take_screenshot ────────────────────────────────────
|
|
168
|
+
server.tool(`${prefix}take_screenshot`, "Capture the current web page as a PNG screenshot via CDP.", {
|
|
169
|
+
fullPage: zod_1.z.boolean().optional().describe("Capture full page including scroll (default: false)"),
|
|
170
|
+
}, async ({ fullPage }) => {
|
|
171
|
+
try {
|
|
172
|
+
let clip;
|
|
173
|
+
if (fullPage) {
|
|
174
|
+
const metrics = await bridge.send("Page.getLayoutMetrics");
|
|
175
|
+
clip = {
|
|
176
|
+
x: 0,
|
|
177
|
+
y: 0,
|
|
178
|
+
width: metrics.contentSize.width,
|
|
179
|
+
height: metrics.contentSize.height,
|
|
180
|
+
scale: 1,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const result = await bridge.send("Page.captureScreenshot", {
|
|
184
|
+
format: "png",
|
|
185
|
+
...(clip ? { clip } : {}),
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
content: [{
|
|
189
|
+
type: "image",
|
|
190
|
+
data: result.data,
|
|
191
|
+
mimeType: "image/png",
|
|
192
|
+
}],
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
return {
|
|
197
|
+
content: [{ type: "text", text: `Screenshot failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
198
|
+
isError: true,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
// ── chrome_get_console_logs ───────────────────────────────────
|
|
203
|
+
server.tool(`${prefix}get_console_logs`, "Get recent console messages (log, warn, error, info) from the web page.", {
|
|
204
|
+
level: zod_1.z.enum(["log", "warn", "error", "info", "debug", "all"]).optional()
|
|
205
|
+
.describe("Filter by log level (default: all)"),
|
|
206
|
+
maxLines: zod_1.z.number().optional().describe("Maximum entries to return (default: 100)"),
|
|
207
|
+
grep: zod_1.z.string().optional().describe("Filter messages by regex pattern"),
|
|
208
|
+
}, async ({ level, maxLines, grep }) => {
|
|
209
|
+
try {
|
|
210
|
+
let entries = collector.getConsoleLogs(maxLines ?? 100);
|
|
211
|
+
if (level && level !== "all") {
|
|
212
|
+
entries = entries.filter((e) => e.level === level);
|
|
213
|
+
}
|
|
214
|
+
if (grep) {
|
|
215
|
+
const regex = new RegExp(grep, "i");
|
|
216
|
+
entries = entries.filter((e) => regex.test(e.text));
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
content: [{
|
|
220
|
+
type: "text",
|
|
221
|
+
text: JSON.stringify({
|
|
222
|
+
totalCollected: collector.getConsoleLogs().length,
|
|
223
|
+
returned: entries.length,
|
|
224
|
+
entries,
|
|
225
|
+
}),
|
|
226
|
+
}],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
catch (err) {
|
|
230
|
+
return {
|
|
231
|
+
content: [{ type: "text", text: `get_console_logs failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
232
|
+
isError: true,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
// ── chrome_get_network ────────────────────────────────────────
|
|
237
|
+
server.tool(`${prefix}get_network`, "Get recent network requests with URL, status, method, timing, and errors.", {
|
|
238
|
+
maxEntries: zod_1.z.number().optional().describe("Maximum entries to return (default: 100)"),
|
|
239
|
+
onlyFailed: zod_1.z.boolean().optional().describe("Only return failed requests (4xx/5xx or network errors)"),
|
|
240
|
+
urlPattern: zod_1.z.string().optional().describe("Filter by URL substring"),
|
|
241
|
+
}, async ({ maxEntries, onlyFailed, urlPattern }) => {
|
|
242
|
+
try {
|
|
243
|
+
let entries = collector.getNetworkEntries(maxEntries ?? 100);
|
|
244
|
+
if (onlyFailed) {
|
|
245
|
+
entries = entries.filter((e) => e.error || (e.status && e.status >= 400));
|
|
246
|
+
}
|
|
247
|
+
if (urlPattern) {
|
|
248
|
+
const lower = urlPattern.toLowerCase();
|
|
249
|
+
entries = entries.filter((e) => e.url.toLowerCase().includes(lower));
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
content: [{
|
|
253
|
+
type: "text",
|
|
254
|
+
text: JSON.stringify({
|
|
255
|
+
totalCollected: collector.getNetworkEntries().length,
|
|
256
|
+
returned: entries.length,
|
|
257
|
+
entries: entries.map((e) => ({
|
|
258
|
+
method: e.method,
|
|
259
|
+
url: e.url,
|
|
260
|
+
status: e.status ?? null,
|
|
261
|
+
statusText: e.statusText ?? null,
|
|
262
|
+
type: e.type ?? null,
|
|
263
|
+
error: e.error ?? null,
|
|
264
|
+
durationMs: e.endTime && e.startTime
|
|
265
|
+
? Math.round((e.endTime - e.startTime) * 1000)
|
|
266
|
+
: null,
|
|
267
|
+
size: e.encodedDataLength ?? null,
|
|
268
|
+
})),
|
|
269
|
+
}),
|
|
270
|
+
}],
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
return {
|
|
275
|
+
content: [{ type: "text", text: `get_network failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
276
|
+
isError: true,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
// ── chrome_evaluate ───────────────────────────────────────────
|
|
281
|
+
server.tool(`${prefix}evaluate`, "Execute JavaScript in the page context and return the result. The expression is evaluated via CDP Runtime.evaluate.", {
|
|
282
|
+
expression: zod_1.z.string().describe("JavaScript expression to evaluate"),
|
|
283
|
+
awaitPromise: zod_1.z.boolean().optional().describe("If true, await the result if it is a Promise (default: false)"),
|
|
284
|
+
}, async ({ expression, awaitPromise }) => {
|
|
285
|
+
try {
|
|
286
|
+
const result = await bridge.send("Runtime.evaluate", {
|
|
287
|
+
expression,
|
|
288
|
+
returnByValue: true,
|
|
289
|
+
awaitPromise: awaitPromise ?? false,
|
|
290
|
+
generatePreview: true,
|
|
291
|
+
});
|
|
292
|
+
if (result.exceptionDetails) {
|
|
293
|
+
const ex = result.exceptionDetails;
|
|
294
|
+
return {
|
|
295
|
+
content: [{
|
|
296
|
+
type: "text",
|
|
297
|
+
text: JSON.stringify({
|
|
298
|
+
error: true,
|
|
299
|
+
message: ex.exception?.description ?? ex.text ?? "Evaluation error",
|
|
300
|
+
}),
|
|
301
|
+
}],
|
|
302
|
+
isError: true,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
const value = result.result.value !== undefined
|
|
306
|
+
? result.result.value
|
|
307
|
+
: result.result.description ?? `[${result.result.type}${result.result.subtype ? `:${result.result.subtype}` : ""}]`;
|
|
308
|
+
return {
|
|
309
|
+
content: [{
|
|
310
|
+
type: "text",
|
|
311
|
+
text: JSON.stringify({ type: result.result.type, value }),
|
|
312
|
+
}],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
return {
|
|
317
|
+
content: [{ type: "text", text: `evaluate failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
318
|
+
isError: true,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
// ── chrome_get_dom ────────────────────────────────────────────
|
|
323
|
+
server.tool(`${prefix}get_dom`, "Get a simplified DOM tree of the visible page. Returns tag names, ids, classes, and truncated text content.", {
|
|
324
|
+
selector: zod_1.z.string().optional().describe("CSS selector to scope the tree (default: 'body')"),
|
|
325
|
+
maxDepth: zod_1.z.number().optional().describe("Maximum depth to traverse (default: 5)"),
|
|
326
|
+
maxNodes: zod_1.z.number().optional().describe("Maximum nodes to return (default: 200)"),
|
|
327
|
+
}, async ({ selector, maxDepth, maxNodes }) => {
|
|
328
|
+
try {
|
|
329
|
+
const sel = selector ?? "body";
|
|
330
|
+
const depth = maxDepth ?? 5;
|
|
331
|
+
const limit = maxNodes ?? 200;
|
|
332
|
+
// Use Runtime.evaluate to traverse the DOM in-page for efficiency
|
|
333
|
+
const expression = `
|
|
334
|
+
(function() {
|
|
335
|
+
const root = document.querySelector(${JSON.stringify(sel)});
|
|
336
|
+
if (!root) return JSON.stringify({ error: "Selector not found: " + ${JSON.stringify(sel)} });
|
|
337
|
+
const nodes = [];
|
|
338
|
+
let count = 0;
|
|
339
|
+
function walk(el, d) {
|
|
340
|
+
if (count >= ${limit} || d > ${depth}) return;
|
|
341
|
+
count++;
|
|
342
|
+
const tag = el.tagName ? el.tagName.toLowerCase() : '#text';
|
|
343
|
+
const node = { tag: tag };
|
|
344
|
+
if (el.id) node.id = el.id;
|
|
345
|
+
if (el.className && typeof el.className === 'string' && el.className.trim())
|
|
346
|
+
node.classes = el.className.trim().split(/\\s+/).slice(0, 5);
|
|
347
|
+
if (el.childNodes.length === 1 && el.childNodes[0].nodeType === 3) {
|
|
348
|
+
const txt = el.textContent.trim();
|
|
349
|
+
if (txt) node.text = txt.slice(0, 100);
|
|
350
|
+
}
|
|
351
|
+
if (el.children && el.children.length > 0) {
|
|
352
|
+
node.children = [];
|
|
353
|
+
for (let i = 0; i < el.children.length; i++) {
|
|
354
|
+
walk(el.children[i], d + 1);
|
|
355
|
+
if (count < ${limit}) node.children.push(nodes[nodes.length - 1]);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
nodes.push(node);
|
|
359
|
+
}
|
|
360
|
+
walk(root, 0);
|
|
361
|
+
return JSON.stringify({ nodeCount: count, tree: nodes[nodes.length - 1] });
|
|
362
|
+
})()
|
|
363
|
+
`;
|
|
364
|
+
const result = await bridge.send("Runtime.evaluate", {
|
|
365
|
+
expression,
|
|
366
|
+
returnByValue: true,
|
|
367
|
+
});
|
|
368
|
+
const parsed = result.result.value ? JSON.parse(result.result.value) : { error: "No result" };
|
|
369
|
+
return {
|
|
370
|
+
content: [{ type: "text", text: JSON.stringify(parsed) }],
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
catch (err) {
|
|
374
|
+
return {
|
|
375
|
+
content: [{ type: "text", text: `get_dom failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
376
|
+
isError: true,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
});
|
|
380
|
+
// ── chrome_click ──────────────────────────────────────────────
|
|
381
|
+
server.tool(`${prefix}click`, "Click on an element by CSS selector or at specific x,y coordinates.", {
|
|
382
|
+
selector: zod_1.z.string().optional().describe("CSS selector of element to click"),
|
|
383
|
+
x: zod_1.z.number().optional().describe("X coordinate to click (used if no selector)"),
|
|
384
|
+
y: zod_1.z.number().optional().describe("Y coordinate to click (used if no selector)"),
|
|
385
|
+
}, async ({ selector, x, y }) => {
|
|
386
|
+
try {
|
|
387
|
+
if (selector) {
|
|
388
|
+
// Use Runtime.evaluate to find element and click it, returning center coordinates
|
|
389
|
+
const result = await bridge.send("Runtime.evaluate", {
|
|
390
|
+
expression: `
|
|
391
|
+
(function() {
|
|
392
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
393
|
+
if (!el) return JSON.stringify({ error: "Element not found: " + ${JSON.stringify(selector)} });
|
|
394
|
+
const rect = el.getBoundingClientRect();
|
|
395
|
+
const cx = Math.round(rect.left + rect.width / 2);
|
|
396
|
+
const cy = Math.round(rect.top + rect.height / 2);
|
|
397
|
+
el.click();
|
|
398
|
+
return JSON.stringify({ ok: true, selector: ${JSON.stringify(selector)}, clicked: { x: cx, y: cy } });
|
|
399
|
+
})()
|
|
400
|
+
`,
|
|
401
|
+
returnByValue: true,
|
|
402
|
+
});
|
|
403
|
+
const parsed = result.result.value ? JSON.parse(result.result.value) : { error: "No result" };
|
|
404
|
+
if (parsed.error) {
|
|
405
|
+
return {
|
|
406
|
+
content: [{ type: "text", text: JSON.stringify(parsed) }],
|
|
407
|
+
isError: true,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
content: [{ type: "text", text: JSON.stringify(parsed) }],
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
if (x !== undefined && y !== undefined) {
|
|
415
|
+
// Use CDP Input.dispatchMouseEvent for coordinate-based click
|
|
416
|
+
await bridge.send("Input.dispatchMouseEvent", {
|
|
417
|
+
type: "mousePressed",
|
|
418
|
+
x,
|
|
419
|
+
y,
|
|
420
|
+
button: "left",
|
|
421
|
+
clickCount: 1,
|
|
422
|
+
});
|
|
423
|
+
await bridge.send("Input.dispatchMouseEvent", {
|
|
424
|
+
type: "mouseReleased",
|
|
425
|
+
x,
|
|
426
|
+
y,
|
|
427
|
+
button: "left",
|
|
428
|
+
clickCount: 1,
|
|
429
|
+
});
|
|
430
|
+
return {
|
|
431
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, clicked: { x, y } }) }],
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
content: [{ type: "text", text: JSON.stringify({ error: "Provide either 'selector' or x/y coordinates" }) }],
|
|
436
|
+
isError: true,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
catch (err) {
|
|
440
|
+
return {
|
|
441
|
+
content: [{ type: "text", text: `click failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
442
|
+
isError: true,
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
// ── chrome_type_text ──────────────────────────────────────────
|
|
447
|
+
server.tool(`${prefix}type_text`, "Type text into the currently focused element or a specific element by selector. Optionally clear existing content first.", {
|
|
448
|
+
text: zod_1.z.string().describe("Text to type"),
|
|
449
|
+
selector: zod_1.z.string().optional().describe("CSS selector to focus before typing"),
|
|
450
|
+
clearFirst: zod_1.z.boolean().optional().describe("Select all and clear before typing (default: false)"),
|
|
451
|
+
}, async ({ text, selector, clearFirst }) => {
|
|
452
|
+
try {
|
|
453
|
+
// Focus the element if selector is provided
|
|
454
|
+
if (selector) {
|
|
455
|
+
await bridge.send("Runtime.evaluate", {
|
|
456
|
+
expression: `
|
|
457
|
+
(function() {
|
|
458
|
+
const el = document.querySelector(${JSON.stringify(selector)});
|
|
459
|
+
if (el) { el.focus(); el.scrollIntoView({ block: 'center' }); }
|
|
460
|
+
})()
|
|
461
|
+
`,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
// Clear existing content if requested
|
|
465
|
+
if (clearFirst) {
|
|
466
|
+
// Select all (Ctrl+A / Cmd+A), then delete
|
|
467
|
+
await bridge.send("Input.dispatchKeyEvent", {
|
|
468
|
+
type: "keyDown",
|
|
469
|
+
key: "a",
|
|
470
|
+
code: "KeyA",
|
|
471
|
+
windowsVirtualKeyCode: 65,
|
|
472
|
+
nativeVirtualKeyCode: 65,
|
|
473
|
+
modifiers: 2, // Ctrl
|
|
474
|
+
});
|
|
475
|
+
await bridge.send("Input.dispatchKeyEvent", {
|
|
476
|
+
type: "keyUp",
|
|
477
|
+
key: "a",
|
|
478
|
+
code: "KeyA",
|
|
479
|
+
windowsVirtualKeyCode: 65,
|
|
480
|
+
nativeVirtualKeyCode: 65,
|
|
481
|
+
modifiers: 2,
|
|
482
|
+
});
|
|
483
|
+
await bridge.send("Input.dispatchKeyEvent", {
|
|
484
|
+
type: "keyDown",
|
|
485
|
+
key: "Backspace",
|
|
486
|
+
code: "Backspace",
|
|
487
|
+
windowsVirtualKeyCode: 8,
|
|
488
|
+
nativeVirtualKeyCode: 8,
|
|
489
|
+
});
|
|
490
|
+
await bridge.send("Input.dispatchKeyEvent", {
|
|
491
|
+
type: "keyUp",
|
|
492
|
+
key: "Backspace",
|
|
493
|
+
code: "Backspace",
|
|
494
|
+
windowsVirtualKeyCode: 8,
|
|
495
|
+
nativeVirtualKeyCode: 8,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
// Type text using insertText for reliable input
|
|
499
|
+
await bridge.send("Input.insertText", { text });
|
|
500
|
+
return {
|
|
501
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, typed: text.length }) }],
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
catch (err) {
|
|
505
|
+
return {
|
|
506
|
+
content: [{ type: "text", text: `type_text failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
507
|
+
isError: true,
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
// ── chrome_navigate ───────────────────────────────────────────
|
|
512
|
+
server.tool(`${prefix}navigate`, "Navigate the current tab to a URL via CDP Page.navigate.", {
|
|
513
|
+
url: zod_1.z.string().describe("URL to navigate to"),
|
|
514
|
+
waitForLoad: zod_1.z.boolean().optional().describe("Wait for page load event (default: true)"),
|
|
515
|
+
}, async ({ url, waitForLoad }) => {
|
|
516
|
+
try {
|
|
517
|
+
const result = await bridge.send("Page.navigate", { url });
|
|
518
|
+
if (result.errorText) {
|
|
519
|
+
return {
|
|
520
|
+
content: [{ type: "text", text: JSON.stringify({ error: result.errorText, url }) }],
|
|
521
|
+
isError: true,
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
// Optionally wait for load
|
|
525
|
+
if (waitForLoad !== false) {
|
|
526
|
+
// Wait up to 30s for the page load by polling readyState
|
|
527
|
+
const deadline = Date.now() + 30_000;
|
|
528
|
+
while (Date.now() < deadline) {
|
|
529
|
+
try {
|
|
530
|
+
const state = await bridge.send("Runtime.evaluate", {
|
|
531
|
+
expression: "document.readyState",
|
|
532
|
+
returnByValue: true,
|
|
533
|
+
});
|
|
534
|
+
if (state.result.value === "complete")
|
|
535
|
+
break;
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
// page may be navigating, retry
|
|
539
|
+
}
|
|
540
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
content: [{ type: "text", text: JSON.stringify({ ok: true, url, frameId: result.frameId }) }],
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
catch (err) {
|
|
548
|
+
return {
|
|
549
|
+
content: [{ type: "text", text: `navigate failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
550
|
+
isError: true,
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
// ── chrome_get_page_info ──────────────────────────────────────
|
|
555
|
+
server.tool(`${prefix}get_page_info`, "Get current page URL, title, viewport size, cookies count, and readyState.", {}, async () => {
|
|
556
|
+
try {
|
|
557
|
+
const result = await bridge.send("Runtime.evaluate", {
|
|
558
|
+
expression: `JSON.stringify({
|
|
559
|
+
url: location.href,
|
|
560
|
+
title: document.title,
|
|
561
|
+
readyState: document.readyState,
|
|
562
|
+
viewport: { width: window.innerWidth, height: window.innerHeight },
|
|
563
|
+
screenSize: { width: screen.width, height: screen.height },
|
|
564
|
+
cookieCount: document.cookie.split(';').filter(c => c.trim()).length,
|
|
565
|
+
localStorage: Object.keys(localStorage).length,
|
|
566
|
+
sessionStorage: Object.keys(sessionStorage).length,
|
|
567
|
+
})`,
|
|
568
|
+
returnByValue: true,
|
|
569
|
+
});
|
|
570
|
+
const info = result.result.value ? JSON.parse(result.result.value) : {};
|
|
571
|
+
return {
|
|
572
|
+
content: [{ type: "text", text: JSON.stringify(info) }],
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
return {
|
|
577
|
+
content: [{ type: "text", text: `get_page_info failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
578
|
+
isError: true,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
//# sourceMappingURL=chrome-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chrome-tools.test.d.ts","sourceRoot":"","sources":["../../src/tools/chrome-tools.test.ts"],"names":[],"mappings":"AAAA;;;;GAIG"}
|