aether-mcp-server 2.0.2 → 2.1.1
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/bridge/debugging.js +133 -0
- package/dist/bridge/inspection.js +302 -0
- package/dist/bridge/interaction.js +586 -0
- package/dist/bridge/navigation.js +146 -0
- package/dist/bridge/session.js +287 -0
- package/dist/cdp-bridge.js +598 -1981
- package/dist/cdp-client.js +232 -366
- package/dist/element-collector.js +198 -0
- package/dist/eval-scripts.js +1024 -0
- package/dist/index.js +16 -28
- package/dist/locator-engine.js +21 -259
- package/dist/logger.js +105 -0
- package/dist/mcp-server.js +59 -0
- package/dist/page-snapshot-cache.js +17 -2
- package/dist/types.js +267 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3,17 +3,16 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
|
|
5
5
|
const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
6
|
-
const ws_server_1 = require("./ws-server");
|
|
7
6
|
const mcp_server_1 = require("./mcp-server");
|
|
8
7
|
const cdp_client_1 = require("./cdp-client");
|
|
9
|
-
const
|
|
10
|
-
const
|
|
8
|
+
const logger_1 = require("./logger");
|
|
9
|
+
const log = (0, logger_1.createLogger)("index");
|
|
11
10
|
// Graceful shutdown handler
|
|
12
11
|
async function shutdown() {
|
|
13
|
-
|
|
12
|
+
log.info("Shutting down...");
|
|
14
13
|
const client = (0, cdp_client_1.getCdpClient)();
|
|
15
14
|
if (client.isConnected()) {
|
|
16
|
-
|
|
15
|
+
log.info("Killing browser...");
|
|
17
16
|
await client.killBrowser();
|
|
18
17
|
}
|
|
19
18
|
process.exit(0);
|
|
@@ -22,11 +21,11 @@ async function shutdown() {
|
|
|
22
21
|
process.on("SIGINT", shutdown);
|
|
23
22
|
process.on("SIGTERM", shutdown);
|
|
24
23
|
process.on("uncaughtException", (err) => {
|
|
25
|
-
|
|
24
|
+
log.error("Uncaught Exception", { error: String(err) });
|
|
26
25
|
shutdown();
|
|
27
26
|
});
|
|
28
27
|
process.on("unhandledRejection", (reason, promise) => {
|
|
29
|
-
|
|
28
|
+
log.error("Unhandled Rejection", { reason: String(reason) });
|
|
30
29
|
shutdown();
|
|
31
30
|
});
|
|
32
31
|
process.on("exit", () => {
|
|
@@ -36,36 +35,25 @@ process.on("exit", () => {
|
|
|
36
35
|
}
|
|
37
36
|
});
|
|
38
37
|
async function main() {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// 1. Kill any stale server on the port
|
|
43
|
-
await (0, ws_server_1.ensurePortAvailable)(WS_PORT);
|
|
44
|
-
// 2. Start WebSocket Server for Extension
|
|
45
|
-
wsServer = (0, ws_server_1.StartWebSocketServer)(WS_PORT);
|
|
46
|
-
console.error("WebSocket server started for extension mode");
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
console.error("CDP mode enabled - no extension needed");
|
|
50
|
-
console.error("To connect to a browser, use the 'launch_browser' or 'connect_browser' tool");
|
|
51
|
-
}
|
|
52
|
-
// 3. Initialize MCP Server
|
|
38
|
+
log.info("Starting Aether MCP Browser Server (CDP mode — no extension needed)...");
|
|
39
|
+
log.info("Use 'launch_browser' or 'connect_browser' tool to connect to a browser.");
|
|
40
|
+
// Initialize MCP Server
|
|
53
41
|
const server = new index_js_1.Server({
|
|
54
|
-
name: "mcp-
|
|
55
|
-
version: "2.
|
|
42
|
+
name: "aether-mcp-server",
|
|
43
|
+
version: "2.1.0",
|
|
56
44
|
}, {
|
|
57
45
|
capabilities: {
|
|
58
46
|
tools: {},
|
|
59
47
|
},
|
|
60
48
|
});
|
|
61
|
-
//
|
|
62
|
-
(0, mcp_server_1.RegisterMcpTools)(server
|
|
63
|
-
//
|
|
49
|
+
// Register Tools
|
|
50
|
+
(0, mcp_server_1.RegisterMcpTools)(server);
|
|
51
|
+
// Connect Transport
|
|
64
52
|
const transport = new stdio_js_1.StdioServerTransport();
|
|
65
53
|
await server.connect(transport);
|
|
66
|
-
|
|
54
|
+
log.info("MCP Server connected via Stdio. Ready.");
|
|
67
55
|
}
|
|
68
56
|
main().catch((err) => {
|
|
69
|
-
|
|
57
|
+
log.error("Fatal Error", { error: String(err) });
|
|
70
58
|
process.exit(1);
|
|
71
59
|
});
|
package/dist/locator-engine.js
CHANGED
|
@@ -9,15 +9,12 @@ class LocatorEngine {
|
|
|
9
9
|
}
|
|
10
10
|
async list(maxElements = 50, includeText = true, withOverlay = false) {
|
|
11
11
|
const capped = Math.max(0, Math.min(Number(maxElements || 50), 200));
|
|
12
|
-
const result = await this.client.evaluate(
|
|
13
|
-
target: "",
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
mode: "list",
|
|
19
|
-
}));
|
|
20
|
-
const candidates = normalizeCandidates(result?.candidates).slice(0, capped);
|
|
12
|
+
const result = await this.client.evaluate(`window.__aetherLocate(${JSON.stringify(JSON.stringify({
|
|
13
|
+
target: "", role: "", selector: "",
|
|
14
|
+
maxCandidates: capped, includeText, mode: "list",
|
|
15
|
+
}))})`);
|
|
16
|
+
const parsed = safeJsonParse(result);
|
|
17
|
+
const candidates = normalizeCandidates(parsed?.candidates).slice(0, capped);
|
|
21
18
|
if (withOverlay && candidates.length > 0) {
|
|
22
19
|
await this.client.getInteractiveElements(true).catch(() => ({ elements: [], somInjected: false }));
|
|
23
20
|
}
|
|
@@ -34,14 +31,11 @@ class LocatorEngine {
|
|
|
34
31
|
return { success: false, message: "target, role, or selector required" };
|
|
35
32
|
}
|
|
36
33
|
while (Date.now() - started < timeout) {
|
|
37
|
-
const
|
|
38
|
-
target,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
includeText: true,
|
|
43
|
-
mode: "resolve",
|
|
44
|
-
})).catch((error) => ({ error: error.message }));
|
|
34
|
+
const resultJson = await this.client.evaluate(`window.__aetherLocate(${JSON.stringify(JSON.stringify({
|
|
35
|
+
target, role, selector: selector,
|
|
36
|
+
maxCandidates, includeText: true, mode: "resolve",
|
|
37
|
+
}))})`).catch((error) => ({ error: error.message }));
|
|
38
|
+
const result = safeJsonParse(resultJson);
|
|
45
39
|
const candidates = normalizeCandidates(result?.candidates);
|
|
46
40
|
const best = candidates[0];
|
|
47
41
|
if (best && (selector || best.score > 0 || role)) {
|
|
@@ -81,246 +75,14 @@ function normalizeCandidates(value) {
|
|
|
81
75
|
function sleep(ms) {
|
|
82
76
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
83
77
|
}
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const interactiveSelector = [
|
|
95
|
-
'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
|
|
96
|
-
'[onclick]', '[role]', '[tabindex]:not([tabindex="-1"])', 'label', 'summary',
|
|
97
|
-
'[contenteditable="true"]', '[aria-label]', '[placeholder]'
|
|
98
|
-
].join(', ');
|
|
99
|
-
|
|
100
|
-
function norm(value) {
|
|
101
|
-
return String(value || '').trim().replace(/\\s+/g, ' ');
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function visible(el) {
|
|
105
|
-
const rect = el.getBoundingClientRect();
|
|
106
|
-
const style = window.getComputedStyle(el);
|
|
107
|
-
return style.display !== 'none' &&
|
|
108
|
-
style.visibility !== 'hidden' &&
|
|
109
|
-
style.opacity !== '0' &&
|
|
110
|
-
rect.width > 0 &&
|
|
111
|
-
rect.height > 0;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function cssPath(el) {
|
|
115
|
-
if (!el || el.nodeType !== Node.ELEMENT_NODE) return '';
|
|
116
|
-
if (el.id) return '#' + CSS.escape(el.id);
|
|
117
|
-
const path = [];
|
|
118
|
-
let node = el;
|
|
119
|
-
while (node && node.nodeType === Node.ELEMENT_NODE && node !== node.ownerDocument.body) {
|
|
120
|
-
let part = node.nodeName.toLowerCase();
|
|
121
|
-
if (node.classList && node.classList.length) {
|
|
122
|
-
part += '.' + Array.from(node.classList).slice(0, 2).map((c) => CSS.escape(c)).join('.');
|
|
123
|
-
}
|
|
124
|
-
const parent = node.parentElement;
|
|
125
|
-
if (parent) {
|
|
126
|
-
const same = Array.from(parent.children).filter((child) => child.nodeName === node.nodeName);
|
|
127
|
-
if (same.length > 1) part += ':nth-of-type(' + (same.indexOf(node) + 1) + ')';
|
|
128
|
-
}
|
|
129
|
-
path.unshift(part);
|
|
130
|
-
node = parent;
|
|
131
|
-
}
|
|
132
|
-
return path.join(' > ');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function xpath(el) {
|
|
136
|
-
const parts = [];
|
|
137
|
-
let node = el;
|
|
138
|
-
while (node && node.nodeType === Node.ELEMENT_NODE) {
|
|
139
|
-
let index = 1;
|
|
140
|
-
let sibling = node.previousElementSibling;
|
|
141
|
-
while (sibling) {
|
|
142
|
-
if (sibling.nodeName === node.nodeName) index++;
|
|
143
|
-
sibling = sibling.previousElementSibling;
|
|
144
|
-
}
|
|
145
|
-
parts.unshift(node.nodeName.toLowerCase() + '[' + index + ']');
|
|
146
|
-
node = node.parentElement;
|
|
147
|
-
}
|
|
148
|
-
return '/' + parts.join('/');
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function inferRole(el) {
|
|
152
|
-
const explicit = (el.getAttribute('role') || '').toLowerCase();
|
|
153
|
-
if (explicit) return explicit;
|
|
154
|
-
const tag = el.tagName.toLowerCase();
|
|
155
|
-
const type = (el.getAttribute('type') || '').toLowerCase();
|
|
156
|
-
if (tag === 'button' || type === 'button' || type === 'submit' || type === 'reset') return 'button';
|
|
157
|
-
if (tag === 'a') return 'link';
|
|
158
|
-
if (tag === 'textarea') return 'textbox';
|
|
159
|
-
if (tag === 'select') return 'combobox';
|
|
160
|
-
if (tag === 'input' && ['checkbox', 'radio'].includes(type)) return type;
|
|
161
|
-
if (tag === 'input') return 'textbox';
|
|
162
|
-
if (el.isContentEditable) return 'textbox';
|
|
163
|
-
if (tag === 'summary') return 'button';
|
|
164
|
-
return tag;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function byId(doc, id) {
|
|
168
|
-
if (!id) return '';
|
|
169
|
-
const el = doc.getElementById(id);
|
|
170
|
-
return el ? norm(el.innerText || el.textContent) : '';
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function labelFor(el) {
|
|
174
|
-
const doc = el.ownerDocument;
|
|
175
|
-
const labelledBy = norm((el.getAttribute('aria-labelledby') || '').split(/\\s+/).map((id) => byId(doc, id)).join(' '));
|
|
176
|
-
if (labelledBy) return labelledBy;
|
|
177
|
-
if (el.id) {
|
|
178
|
-
const direct = doc.querySelector('label[for="' + CSS.escape(el.id) + '"]');
|
|
179
|
-
if (direct) return norm(direct.innerText || direct.textContent);
|
|
180
|
-
}
|
|
181
|
-
const wrapping = el.closest && el.closest('label');
|
|
182
|
-
return wrapping ? norm(wrapping.innerText || wrapping.textContent) : '';
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function textFor(el) {
|
|
186
|
-
return norm(
|
|
187
|
-
el.getAttribute('aria-label') ||
|
|
188
|
-
labelFor(el) ||
|
|
189
|
-
el.getAttribute('placeholder') ||
|
|
190
|
-
el.getAttribute('alt') ||
|
|
191
|
-
el.getAttribute('title') ||
|
|
192
|
-
el.innerText ||
|
|
193
|
-
el.textContent ||
|
|
194
|
-
el.getAttribute('value') ||
|
|
195
|
-
el.getAttribute('name') ||
|
|
196
|
-
''
|
|
197
|
-
);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
function scoreField(field, value, exact, includes) {
|
|
201
|
-
const text = norm(value);
|
|
202
|
-
const lower = text.toLowerCase();
|
|
203
|
-
if (!targetLower) return { score: 0, by: '' };
|
|
204
|
-
if (lower === targetLower) return { score: exact, by: field + ':exact' };
|
|
205
|
-
if (lower.includes(targetLower)) return { score: includes, by: field + ':contains' };
|
|
206
|
-
if (targetLower.includes(lower) && lower.length >= 3) return { score: Math.max(1, includes - 1), by: field + ':contained_by_target' };
|
|
207
|
-
return { score: 0, by: '' };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function scoreCandidate(item) {
|
|
211
|
-
let score = 0;
|
|
212
|
-
let matchedBy = '';
|
|
213
|
-
const fields = [
|
|
214
|
-
['selector', item.selector, 13, 10],
|
|
215
|
-
['xpath', item.xpath, 11, 8],
|
|
216
|
-
['name', item.name, 12, 10],
|
|
217
|
-
['label', item.label, 12, 10],
|
|
218
|
-
['placeholder', item.placeholder, 11, 9],
|
|
219
|
-
['text', item.text, 10, 8],
|
|
220
|
-
['title', item.title, 8, 6],
|
|
221
|
-
['value', item.value, 7, 5]
|
|
222
|
-
];
|
|
223
|
-
for (const field of fields) {
|
|
224
|
-
const match = scoreField(field[0], field[1], field[2], field[3]);
|
|
225
|
-
if (match.score > score) {
|
|
226
|
-
score = match.score;
|
|
227
|
-
matchedBy = match.by;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
if (roleHint) {
|
|
231
|
-
if (item.role === roleHint) score += 5;
|
|
232
|
-
else if (roleHint === 'textbox' && ['input', 'textarea'].includes(item.tag)) score += 3;
|
|
233
|
-
else score -= 3;
|
|
234
|
-
}
|
|
235
|
-
if (!targetLower && roleHint && item.role === roleHint) {
|
|
236
|
-
matchedBy = 'role';
|
|
237
|
-
score = Math.max(score, 5);
|
|
238
|
-
}
|
|
239
|
-
if (item.scope !== 'document') score += 1;
|
|
240
|
-
item.score = score;
|
|
241
|
-
item.matchedBy = matchedBy || (selectorHint ? 'selector' : '');
|
|
242
|
-
return item;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function collectFromRoot(root, framePath, frameOffset, shadowDepth, scope, out) {
|
|
246
|
-
let nodes = [];
|
|
247
|
-
try {
|
|
248
|
-
if (selectorHint) {
|
|
249
|
-
nodes = Array.from(root.querySelectorAll(selectorHint));
|
|
250
|
-
} else {
|
|
251
|
-
nodes = Array.from(root.querySelectorAll(interactiveSelector));
|
|
252
|
-
}
|
|
253
|
-
} catch {
|
|
254
|
-
nodes = [];
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
for (const el of nodes) {
|
|
258
|
-
if (!visible(el)) continue;
|
|
259
|
-
const rect = el.getBoundingClientRect();
|
|
260
|
-
const selector = cssPath(el);
|
|
261
|
-
const role = inferRole(el);
|
|
262
|
-
const label = labelFor(el);
|
|
263
|
-
const item = {
|
|
264
|
-
ref: '',
|
|
265
|
-
selector,
|
|
266
|
-
xpath: xpath(el),
|
|
267
|
-
scope,
|
|
268
|
-
framePath: framePath.slice(),
|
|
269
|
-
shadowDepth,
|
|
270
|
-
tag: el.tagName.toLowerCase(),
|
|
271
|
-
role,
|
|
272
|
-
type: el.getAttribute('type') || '',
|
|
273
|
-
name: norm(el.getAttribute('aria-label') || label || textFor(el) || el.getAttribute('name')),
|
|
274
|
-
text: includeText ? norm(el.innerText || el.textContent).substring(0, 180) : '',
|
|
275
|
-
label,
|
|
276
|
-
placeholder: norm(el.getAttribute('placeholder')),
|
|
277
|
-
title: norm(el.getAttribute('title')),
|
|
278
|
-
value: norm(el.getAttribute('value')),
|
|
279
|
-
score: 0,
|
|
280
|
-
matchedBy: '',
|
|
281
|
-
bounds: {
|
|
282
|
-
x: Math.round(frameOffset.x + rect.left),
|
|
283
|
-
y: Math.round(frameOffset.y + rect.top),
|
|
284
|
-
width: Math.round(rect.width),
|
|
285
|
-
height: Math.round(rect.height)
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
item.ref = item.scope === 'document' && item.framePath.length === 0 && item.shadowDepth === 0
|
|
289
|
-
? 'css:' + item.selector
|
|
290
|
-
: 'point:' + Math.round(item.bounds.x + item.bounds.width / 2) + ',' + Math.round(item.bounds.y + item.bounds.height / 2);
|
|
291
|
-
out.push(scoreCandidate(item));
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const all = [];
|
|
295
|
-
try {
|
|
296
|
-
all.push(...Array.from(root.querySelectorAll('*')));
|
|
297
|
-
} catch {}
|
|
298
|
-
|
|
299
|
-
for (const el of all) {
|
|
300
|
-
if (el.shadowRoot) {
|
|
301
|
-
collectFromRoot(el.shadowRoot, framePath, frameOffset, shadowDepth + 1, 'shadow', out);
|
|
302
|
-
}
|
|
303
|
-
if (el.tagName && el.tagName.toLowerCase() === 'iframe') {
|
|
304
|
-
try {
|
|
305
|
-
const doc = el.contentDocument;
|
|
306
|
-
if (!doc) continue;
|
|
307
|
-
const rect = el.getBoundingClientRect();
|
|
308
|
-
collectFromRoot(doc, framePath.concat([Array.from(el.ownerDocument.querySelectorAll('iframe')).indexOf(el)]), {
|
|
309
|
-
x: frameOffset.x + rect.left,
|
|
310
|
-
y: frameOffset.y + rect.top
|
|
311
|
-
}, shadowDepth, 'frame', out);
|
|
312
|
-
} catch {}
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const candidates = [];
|
|
318
|
-
collectFromRoot(document, [], { x: 0, y: 0 }, 0, 'document', candidates);
|
|
319
|
-
candidates.sort((a, b) => {
|
|
320
|
-
if (mode === 'list') return (a.bounds.y - b.bounds.y) || (a.bounds.x - b.bounds.x);
|
|
321
|
-
return b.score - a.score || a.bounds.y - b.bounds.y || a.bounds.x - b.bounds.x;
|
|
322
|
-
});
|
|
323
|
-
return { candidates: candidates.slice(0, maxCandidates) };
|
|
324
|
-
})()
|
|
325
|
-
`;
|
|
78
|
+
function safeJsonParse(value) {
|
|
79
|
+
if (typeof value === "string") {
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(value);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
326
88
|
}
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Aether structured logger with correlation IDs for agent action tracing.
|
|
4
|
+
* Replaces ad-hoc console.error calls with leveled, structured output.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.rootLogger = void 0;
|
|
8
|
+
exports.setLogLevel = setLogLevel;
|
|
9
|
+
exports.onLog = onLog;
|
|
10
|
+
exports.createLogger = createLogger;
|
|
11
|
+
let globalLogLevel = "info";
|
|
12
|
+
const listeners = [];
|
|
13
|
+
function setLogLevel(level) {
|
|
14
|
+
globalLogLevel = level;
|
|
15
|
+
}
|
|
16
|
+
function onLog(fn) {
|
|
17
|
+
listeners.push(fn);
|
|
18
|
+
return () => {
|
|
19
|
+
const idx = listeners.indexOf(fn);
|
|
20
|
+
if (idx >= 0)
|
|
21
|
+
listeners.splice(idx, 1);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const LEVEL_PRIO = {
|
|
25
|
+
debug: 0,
|
|
26
|
+
info: 1,
|
|
27
|
+
warn: 2,
|
|
28
|
+
error: 3,
|
|
29
|
+
};
|
|
30
|
+
function shouldLog(level) {
|
|
31
|
+
return LEVEL_PRIO[level] >= LEVEL_PRIO[globalLogLevel];
|
|
32
|
+
}
|
|
33
|
+
function emit(entry) {
|
|
34
|
+
if (!shouldLog(entry.level))
|
|
35
|
+
return;
|
|
36
|
+
// Structured stderr output for MCP transport
|
|
37
|
+
const prefix = entry.corrId ? `[${entry.corrId}]` : "";
|
|
38
|
+
const actionTag = entry.action ? ` [${entry.action}]` : "";
|
|
39
|
+
const dataSuffix = entry.data ? ` ${JSON.stringify(entry.data)}` : "";
|
|
40
|
+
const method = entry.level === "error"
|
|
41
|
+
? "error"
|
|
42
|
+
: entry.level === "warn"
|
|
43
|
+
? "warn"
|
|
44
|
+
: "error"; // MCP uses stderr for all logging
|
|
45
|
+
console.error(`[Aether:${entry.level.toUpperCase()}]${prefix}${actionTag} ${entry.msg}${dataSuffix}`);
|
|
46
|
+
for (const fn of listeners) {
|
|
47
|
+
try {
|
|
48
|
+
fn(entry);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Listener errors must not break logging
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
let idCounter = 0;
|
|
56
|
+
function createLogger(corrId) {
|
|
57
|
+
const cid = corrId ?? `req-${++idCounter}-${Date.now().toString(36)}`;
|
|
58
|
+
const log = (level, msg, data, action) => {
|
|
59
|
+
emit({
|
|
60
|
+
ts: new Date().toISOString(),
|
|
61
|
+
level,
|
|
62
|
+
corrId: cid,
|
|
63
|
+
action,
|
|
64
|
+
msg,
|
|
65
|
+
data,
|
|
66
|
+
});
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
corrId: cid,
|
|
70
|
+
debug(msg, data) {
|
|
71
|
+
log("debug", msg, data);
|
|
72
|
+
},
|
|
73
|
+
info(msg, data) {
|
|
74
|
+
log("info", msg, data);
|
|
75
|
+
},
|
|
76
|
+
warn(msg, data) {
|
|
77
|
+
log("warn", msg, data);
|
|
78
|
+
},
|
|
79
|
+
error(msg, data) {
|
|
80
|
+
log("error", msg, data);
|
|
81
|
+
},
|
|
82
|
+
child(action) {
|
|
83
|
+
return {
|
|
84
|
+
corrId: cid,
|
|
85
|
+
debug(msg, data) {
|
|
86
|
+
log("debug", msg, data, action);
|
|
87
|
+
},
|
|
88
|
+
info(msg, data) {
|
|
89
|
+
log("info", msg, data, action);
|
|
90
|
+
},
|
|
91
|
+
warn(msg, data) {
|
|
92
|
+
log("warn", msg, data, action);
|
|
93
|
+
},
|
|
94
|
+
error(msg, data) {
|
|
95
|
+
log("error", msg, data, action);
|
|
96
|
+
},
|
|
97
|
+
child(subAction) {
|
|
98
|
+
return this.child(`${action}/${subAction}`);
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
// Default logger (no correlation ID)
|
|
105
|
+
exports.rootLogger = createLogger("root");
|
package/dist/mcp-server.js
CHANGED
|
@@ -663,6 +663,41 @@ const Tools = [
|
|
|
663
663
|
required: ["fields"]
|
|
664
664
|
}
|
|
665
665
|
},
|
|
666
|
+
{
|
|
667
|
+
name: "get_page_text",
|
|
668
|
+
description: "READ TOOL. Extract clean, readable page content as Markdown (or plain text). Token-cheap alternative to screenshots or full DOM dumps for reading/understanding a page. Scopes to a CSS selector when given, otherwise auto-detects the main content region.",
|
|
669
|
+
inputSchema: {
|
|
670
|
+
type: "object",
|
|
671
|
+
properties: {
|
|
672
|
+
format: { type: "string", enum: ["markdown", "text"], description: "Output format. Default markdown." },
|
|
673
|
+
selector: { type: "string", description: "Optional CSS selector to scope extraction to a region." },
|
|
674
|
+
includeLinks: { type: "boolean", description: "Render anchors as [text](href) in markdown. Default true." },
|
|
675
|
+
maxLength: { type: "number", description: "Max characters returned before truncation. Default 20000." }
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
name: "save_auth_state",
|
|
681
|
+
description: "SESSION TOOL. Export the current browser session (cookies + localStorage + sessionStorage) to a JSON file so a logged-in session can be reused later with load_auth_state. Avoids repeating logins.",
|
|
682
|
+
inputSchema: {
|
|
683
|
+
type: "object",
|
|
684
|
+
properties: {
|
|
685
|
+
path: { type: "string", description: "File path to write the auth state JSON. Defaults to <cwd>/.aether/auth-state.json." },
|
|
686
|
+
origins: { type: "array", items: { type: "string" }, description: "Optional list of origins to capture storage for. Defaults to the current origin." }
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
name: "load_auth_state",
|
|
692
|
+
description: "SESSION TOOL. Restore a previously saved session (cookies + localStorage + sessionStorage) from a JSON file written by save_auth_state. Navigate to the target site first, then load, then reload.",
|
|
693
|
+
inputSchema: {
|
|
694
|
+
type: "object",
|
|
695
|
+
properties: {
|
|
696
|
+
path: { type: "string", description: "File path to read the auth state JSON. Defaults to <cwd>/.aether/auth-state.json." },
|
|
697
|
+
reload: { type: "boolean", description: "Reload the active tab after restoring so storage takes effect. Default true." }
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
},
|
|
666
701
|
{
|
|
667
702
|
name: "page_snapshot",
|
|
668
703
|
description: "Capture page context optimized for LLM consumption. Lightweight by default; opt into screenshots, cookies, accessibility tree, or full DOM snapshot when needed.",
|
|
@@ -1221,6 +1256,30 @@ function RegisterMcpTools(server, wsServer) {
|
|
|
1221
1256
|
}
|
|
1222
1257
|
return { content };
|
|
1223
1258
|
}
|
|
1259
|
+
if (name === "get_page_text") {
|
|
1260
|
+
const result = await bridge.sendCommand("get_page_text", {
|
|
1261
|
+
format: a?.format,
|
|
1262
|
+
selector: a?.selector,
|
|
1263
|
+
includeLinks: a?.includeLinks,
|
|
1264
|
+
maxLength: a?.maxLength,
|
|
1265
|
+
});
|
|
1266
|
+
const header = `Title: ${result.title}\nURL: ${result.url}\nFormat: ${result.format} | ${result.length} chars${result.truncated ? " (truncated)" : ""}`;
|
|
1267
|
+
return { content: [{ type: "text", text: `${header}\n\n${result.text}` }] };
|
|
1268
|
+
}
|
|
1269
|
+
if (name === "save_auth_state") {
|
|
1270
|
+
const result = await bridge.sendCommand("save_auth_state", {
|
|
1271
|
+
path: a?.path,
|
|
1272
|
+
origins: a?.origins,
|
|
1273
|
+
});
|
|
1274
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1275
|
+
}
|
|
1276
|
+
if (name === "load_auth_state") {
|
|
1277
|
+
const result = await bridge.sendCommand("load_auth_state", {
|
|
1278
|
+
path: a?.path,
|
|
1279
|
+
reload: a?.reload,
|
|
1280
|
+
});
|
|
1281
|
+
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
1282
|
+
}
|
|
1224
1283
|
throw new Error(`Unknown tool: ${name}`);
|
|
1225
1284
|
}
|
|
1226
1285
|
catch (error) {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.PageSnapshotCache = void 0;
|
|
4
|
-
const CACHE_TTL_MS =
|
|
4
|
+
const CACHE_TTL_MS = 5000; // 5s — safe because cache auto-invalidates on DOM mutations
|
|
5
5
|
class PageSnapshotCache {
|
|
6
6
|
client;
|
|
7
7
|
locator;
|
|
8
8
|
version = 0;
|
|
9
9
|
cached = null;
|
|
10
|
+
lastUrl = "";
|
|
10
11
|
constructor(client, locator) {
|
|
11
12
|
this.client = client;
|
|
12
13
|
this.locator = locator;
|
|
@@ -15,7 +16,21 @@ class PageSnapshotCache {
|
|
|
15
16
|
invalidate(reason = "manual") {
|
|
16
17
|
this.version++;
|
|
17
18
|
this.cached = null;
|
|
18
|
-
|
|
19
|
+
}
|
|
20
|
+
/** Returns true if the cache is fresh enough to serve. */
|
|
21
|
+
isFresh() {
|
|
22
|
+
const c = this.cached;
|
|
23
|
+
return !!(c && c.version === this.version && (Date.now() - c.createdAt) <= CACHE_TTL_MS);
|
|
24
|
+
}
|
|
25
|
+
/** Get cache metadata for debugging. */
|
|
26
|
+
getCacheInfo() {
|
|
27
|
+
const c = this.cached;
|
|
28
|
+
return {
|
|
29
|
+
version: this.version,
|
|
30
|
+
ageMs: c ? Date.now() - c.createdAt : Infinity,
|
|
31
|
+
fresh: this.isFresh(),
|
|
32
|
+
url: c?.url ?? "",
|
|
33
|
+
};
|
|
19
34
|
}
|
|
20
35
|
async compact(params = {}) {
|
|
21
36
|
const maxElements = Math.max(0, Math.min(Number(params.maxElements ?? 30), 200));
|