aether-mcp-server 2.1.0 → 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/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 WS_PORT = 3009;
10
- const MODE = process.env.AETHER_MODE || "cdp"; // "cdp" or "extension"
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
- console.error("\n[Server] Shutting down...");
12
+ log.info("Shutting down...");
14
13
  const client = (0, cdp_client_1.getCdpClient)();
15
14
  if (client.isConnected()) {
16
- console.error("[Server] Killing browser...");
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
- console.error("[Server] Uncaught Exception:", err);
24
+ log.error("Uncaught Exception", { error: String(err) });
26
25
  shutdown();
27
26
  });
28
27
  process.on("unhandledRejection", (reason, promise) => {
29
- console.error("[Server] Unhandled Rejection at:", promise, "reason:", reason);
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
- console.error(`Starting MCP Browser Server (mode: ${MODE})...`);
40
- let wsServer = null;
41
- if (MODE === "extension") {
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-browser-control",
55
- version: "2.0.0",
42
+ name: "aether-mcp-server",
43
+ version: "2.1.0",
56
44
  }, {
57
45
  capabilities: {
58
46
  tools: {},
59
47
  },
60
48
  });
61
- // 4. Register Tools
62
- (0, mcp_server_1.RegisterMcpTools)(server, wsServer);
63
- // 5. Connect Transport
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
- console.error("MCP Server connected via Stdio");
54
+ log.info("MCP Server connected via Stdio. Ready.");
67
55
  }
68
56
  main().catch((err) => {
69
- console.error("Fatal Error:", err);
57
+ log.error("Fatal Error", { error: String(err) });
70
58
  process.exit(1);
71
59
  });
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LocatorEngine = void 0;
4
- const element_collector_1 = require("./element-collector");
5
4
  const DEFAULT_TIMEOUT = 7000;
6
5
  class LocatorEngine {
7
6
  client;
@@ -10,15 +9,12 @@ class LocatorEngine {
10
9
  }
11
10
  async list(maxElements = 50, includeText = true, withOverlay = false) {
12
11
  const capped = Math.max(0, Math.min(Number(maxElements || 50), 200));
13
- const result = await this.client.evaluate(locatorScript({
14
- target: "",
15
- role: "",
16
- selector: "",
17
- maxCandidates: capped,
18
- includeText,
19
- mode: "list",
20
- }));
21
- 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);
22
18
  if (withOverlay && candidates.length > 0) {
23
19
  await this.client.getInteractiveElements(true).catch(() => ({ elements: [], somInjected: false }));
24
20
  }
@@ -35,14 +31,11 @@ class LocatorEngine {
35
31
  return { success: false, message: "target, role, or selector required" };
36
32
  }
37
33
  while (Date.now() - started < timeout) {
38
- const result = await this.client.evaluate(locatorScript({
39
- target,
40
- role,
41
- selector,
42
- maxCandidates,
43
- includeText: true,
44
- mode: "resolve",
45
- })).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);
46
39
  const candidates = normalizeCandidates(result?.candidates);
47
40
  const best = candidates[0];
48
41
  if (best && (selector || best.score > 0 || role)) {
@@ -82,173 +75,14 @@ function normalizeCandidates(value) {
82
75
  function sleep(ms) {
83
76
  return new Promise((resolve) => setTimeout(resolve, ms));
84
77
  }
85
- function locatorScript(input) {
86
- return `
87
- (function() {
88
- const target = ${JSON.stringify(input.target)};
89
- const targetLower = target.toLowerCase();
90
- const roleHint = ${JSON.stringify(input.role)};
91
- const selectorHint = ${JSON.stringify(input.selector)};
92
- const maxCandidates = ${JSON.stringify(input.maxCandidates)};
93
- const includeText = ${JSON.stringify(input.includeText)};
94
- const mode = ${JSON.stringify(input.mode)};
95
- const interactiveSelector = [
96
- 'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
97
- '[onclick]', '[role]', '[tabindex]:not([tabindex="-1"])', 'label', 'summary',
98
- '[contenteditable="true"]', '[aria-label]', '[placeholder]'
99
- ].join(', ');
100
-
101
- ${element_collector_1.SHARED_DOM_HELPERS}
102
-
103
- // Thin aliases so the rest of this resolver reads naturally while the
104
- // implementations stay shared with every other collector.
105
- const norm = aetherNorm;
106
- const visible = aetherVisible;
107
- const cssPath = aetherStableSelector;
108
- const inferRole = aetherImplicitRole;
109
- const labelFor = aetherLabelFor;
110
- const textFor = aetherAccessibleName;
111
-
112
- function xpath(el) {
113
- const parts = [];
114
- let node = el;
115
- while (node && node.nodeType === Node.ELEMENT_NODE) {
116
- let index = 1;
117
- let sibling = node.previousElementSibling;
118
- while (sibling) {
119
- if (sibling.nodeName === node.nodeName) index++;
120
- sibling = sibling.previousElementSibling;
121
- }
122
- parts.unshift(node.nodeName.toLowerCase() + '[' + index + ']');
123
- node = node.parentElement;
124
- }
125
- return '/' + parts.join('/');
126
- }
127
-
128
- function scoreField(field, value, exact, includes) {
129
- const text = norm(value);
130
- const lower = text.toLowerCase();
131
- if (!targetLower) return { score: 0, by: '' };
132
- if (lower === targetLower) return { score: exact, by: field + ':exact' };
133
- if (lower.includes(targetLower)) return { score: includes, by: field + ':contains' };
134
- if (targetLower.includes(lower) && lower.length >= 3) return { score: Math.max(1, includes - 1), by: field + ':contained_by_target' };
135
- return { score: 0, by: '' };
136
- }
137
-
138
- function scoreCandidate(item) {
139
- let score = 0;
140
- let matchedBy = '';
141
- const fields = [
142
- ['selector', item.selector, 13, 10],
143
- ['xpath', item.xpath, 11, 8],
144
- ['name', item.name, 12, 10],
145
- ['label', item.label, 12, 10],
146
- ['placeholder', item.placeholder, 11, 9],
147
- ['text', item.text, 10, 8],
148
- ['title', item.title, 8, 6],
149
- ['value', item.value, 7, 5]
150
- ];
151
- for (const field of fields) {
152
- const match = scoreField(field[0], field[1], field[2], field[3]);
153
- if (match.score > score) {
154
- score = match.score;
155
- matchedBy = match.by;
156
- }
157
- }
158
- if (roleHint) {
159
- if (item.role === roleHint) score += 5;
160
- else if (roleHint === 'textbox' && ['input', 'textarea'].includes(item.tag)) score += 3;
161
- else score -= 3;
162
- }
163
- if (!targetLower && roleHint && item.role === roleHint) {
164
- matchedBy = 'role';
165
- score = Math.max(score, 5);
166
- }
167
- if (item.scope !== 'document') score += 1;
168
- item.score = score;
169
- item.matchedBy = matchedBy || (selectorHint ? 'selector' : '');
170
- return item;
171
- }
172
-
173
- function collectFromRoot(root, framePath, frameOffset, shadowDepth, scope, out) {
174
- let nodes = [];
175
- try {
176
- if (selectorHint) {
177
- nodes = Array.from(root.querySelectorAll(selectorHint));
178
- } else {
179
- nodes = Array.from(root.querySelectorAll(interactiveSelector));
180
- }
181
- } catch {
182
- nodes = [];
183
- }
184
-
185
- for (const el of nodes) {
186
- if (!visible(el)) continue;
187
- const rect = el.getBoundingClientRect();
188
- const selector = cssPath(el);
189
- const role = inferRole(el);
190
- const label = labelFor(el);
191
- const item = {
192
- ref: '',
193
- selector,
194
- xpath: xpath(el),
195
- scope,
196
- framePath: framePath.slice(),
197
- shadowDepth,
198
- tag: el.tagName.toLowerCase(),
199
- role,
200
- type: el.getAttribute('type') || '',
201
- name: norm(el.getAttribute('aria-label') || label || textFor(el) || el.getAttribute('name')),
202
- text: includeText ? norm(el.innerText || el.textContent).substring(0, 180) : '',
203
- label,
204
- placeholder: norm(el.getAttribute('placeholder')),
205
- title: norm(el.getAttribute('title')),
206
- value: norm(el.getAttribute('value')),
207
- score: 0,
208
- matchedBy: '',
209
- bounds: {
210
- x: Math.round(frameOffset.x + rect.left),
211
- y: Math.round(frameOffset.y + rect.top),
212
- width: Math.round(rect.width),
213
- height: Math.round(rect.height)
214
- }
215
- };
216
- item.ref = item.scope === 'document' && item.framePath.length === 0 && item.shadowDepth === 0
217
- ? 'css:' + item.selector
218
- : 'point:' + Math.round(item.bounds.x + item.bounds.width / 2) + ',' + Math.round(item.bounds.y + item.bounds.height / 2);
219
- out.push(scoreCandidate(item));
220
- }
221
-
222
- const all = [];
223
- try {
224
- all.push(...Array.from(root.querySelectorAll('*')));
225
- } catch {}
226
-
227
- for (const el of all) {
228
- if (el.shadowRoot) {
229
- collectFromRoot(el.shadowRoot, framePath, frameOffset, shadowDepth + 1, 'shadow', out);
230
- }
231
- if (el.tagName && el.tagName.toLowerCase() === 'iframe') {
232
- try {
233
- const doc = el.contentDocument;
234
- if (!doc) continue;
235
- const rect = el.getBoundingClientRect();
236
- collectFromRoot(doc, framePath.concat([Array.from(el.ownerDocument.querySelectorAll('iframe')).indexOf(el)]), {
237
- x: frameOffset.x + rect.left,
238
- y: frameOffset.y + rect.top
239
- }, shadowDepth, 'frame', out);
240
- } catch {}
241
- }
242
- }
243
- }
244
-
245
- const candidates = [];
246
- collectFromRoot(document, [], { x: 0, y: 0 }, 0, 'document', candidates);
247
- candidates.sort((a, b) => {
248
- if (mode === 'list') return (a.bounds.y - b.bounds.y) || (a.bounds.x - b.bounds.x);
249
- return b.score - a.score || a.bounds.y - b.bounds.y || a.bounds.x - b.bounds.x;
250
- });
251
- return { candidates: candidates.slice(0, maxCandidates) };
252
- })()
253
- `;
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;
254
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");
@@ -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 = 1000;
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
- console.error(`[SnapshotCache] invalidated: ${reason}`);
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));