m365connector 0.3.5 → 0.3.7
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/README.md +29 -103
- package/package.json +4 -3
- package/src/browser-session.js +619 -0
- package/src/index.js +31 -14
- package/src/lib/content-reader.js +2746 -0
- package/src/lib/search-api.js +989 -0
- package/src/lib/vendor/jszip.min.js +13 -0
- package/src/token-providers.js +739 -0
- package/src/tools/search.js +12 -32
- package/src/ws-server.js +0 -203
|
@@ -0,0 +1,619 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
|
+
import { WebSocket } from "ws";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_EDGE_PORT = 52367;
|
|
10
|
+
const DEFAULT_NAVIGATION_TIMEOUT_MS = 30000;
|
|
11
|
+
|
|
12
|
+
function sleep(ms) {
|
|
13
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function expandHome(value) {
|
|
17
|
+
const text = String(value || "");
|
|
18
|
+
if (text === "~") {
|
|
19
|
+
return os.homedir();
|
|
20
|
+
}
|
|
21
|
+
if (text.startsWith("~/")) {
|
|
22
|
+
return path.join(os.homedir(), text.slice(2));
|
|
23
|
+
}
|
|
24
|
+
return text;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function requestJson(url, options = {}) {
|
|
28
|
+
const method = options.method || "GET";
|
|
29
|
+
const timeoutMs = Number(options.timeoutMs || 2000);
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const req = http.request(url, { method, timeout: timeoutMs }, (res) => {
|
|
32
|
+
let data = "";
|
|
33
|
+
res.on("data", (chunk) => { data += chunk; });
|
|
34
|
+
res.on("end", () => {
|
|
35
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
36
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
resolve(data ? JSON.parse(data) : null);
|
|
41
|
+
} catch (err) {
|
|
42
|
+
reject(err);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
req.on("timeout", () => req.destroy(new Error("timeout")));
|
|
47
|
+
req.on("error", reject);
|
|
48
|
+
req.end();
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function requestText(url, options = {}) {
|
|
53
|
+
const method = options.method || "GET";
|
|
54
|
+
const timeoutMs = Number(options.timeoutMs || 2000);
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const req = http.request(url, { method, timeout: timeoutMs }, (res) => {
|
|
57
|
+
let data = "";
|
|
58
|
+
res.on("data", (chunk) => { data += chunk; });
|
|
59
|
+
res.on("end", () => {
|
|
60
|
+
if (res.statusCode < 200 || res.statusCode >= 300) {
|
|
61
|
+
reject(new Error(`HTTP ${res.statusCode}: ${data.slice(0, 200)}`));
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
resolve(data);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
req.on("timeout", () => req.destroy(new Error("timeout")));
|
|
68
|
+
req.on("error", reject);
|
|
69
|
+
req.end();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function findEdgeExecutable() {
|
|
74
|
+
if (process.env.M365C_EDGE_PATH) {
|
|
75
|
+
return expandHome(process.env.M365C_EDGE_PATH);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const candidates = [
|
|
79
|
+
"/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
|
|
80
|
+
path.join(os.homedir(), "Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"),
|
|
81
|
+
"/usr/bin/microsoft-edge",
|
|
82
|
+
"/usr/bin/microsoft-edge-stable",
|
|
83
|
+
"/opt/microsoft/msedge/microsoft-edge"
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
return candidates.find((candidate) => fs.existsSync(candidate)) || "msedge";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function defaultUserDataDir() {
|
|
90
|
+
if (process.env.M365C_EDGE_USER_DATA_DIR) {
|
|
91
|
+
return expandHome(process.env.M365C_EDGE_USER_DATA_DIR);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const phantomwrightProfile = path.join(
|
|
95
|
+
os.homedir(),
|
|
96
|
+
"Library/Caches/phantomwright-cli/daemon/ud-m365"
|
|
97
|
+
);
|
|
98
|
+
if (fs.existsSync(phantomwrightProfile)) {
|
|
99
|
+
return phantomwrightProfile;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return path.join(os.homedir(), ".m365connector", "edge-profile");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function normalizeHeaderMap(headers = {}) {
|
|
106
|
+
const out = {};
|
|
107
|
+
for (const [key, value] of Object.entries(headers || {})) {
|
|
108
|
+
out[String(key).toLowerCase()] = String(value);
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export class CdpPage extends EventEmitter {
|
|
114
|
+
constructor(session, target) {
|
|
115
|
+
super();
|
|
116
|
+
this.session = session;
|
|
117
|
+
this.id = target.id;
|
|
118
|
+
this.target = target;
|
|
119
|
+
this.url = target.url || "";
|
|
120
|
+
this.wsUrl = target.webSocketDebuggerUrl;
|
|
121
|
+
this.ws = null;
|
|
122
|
+
this.nextId = 1;
|
|
123
|
+
this.pending = new Map();
|
|
124
|
+
this.contexts = new Map();
|
|
125
|
+
this.requestUrls = new Map();
|
|
126
|
+
this.mainFrameId = null;
|
|
127
|
+
this.loaded = false;
|
|
128
|
+
this.closed = false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async connect() {
|
|
132
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.ws = new WebSocket(this.wsUrl);
|
|
137
|
+
this.ws.on("message", (raw) => this.#handleMessage(raw));
|
|
138
|
+
this.ws.on("close", () => {
|
|
139
|
+
this.closed = true;
|
|
140
|
+
for (const pending of this.pending.values()) {
|
|
141
|
+
clearTimeout(pending.timeout);
|
|
142
|
+
pending.reject(new Error("CDP page closed"));
|
|
143
|
+
}
|
|
144
|
+
this.pending.clear();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
await new Promise((resolve, reject) => {
|
|
148
|
+
this.ws.once("open", resolve);
|
|
149
|
+
this.ws.once("error", reject);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await this.send("Page.enable");
|
|
153
|
+
await this.send("Runtime.enable");
|
|
154
|
+
await this.send("Network.enable", {
|
|
155
|
+
maxTotalBufferSize: 10000000,
|
|
156
|
+
maxResourceBufferSize: 2000000
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
#handleMessage(raw) {
|
|
161
|
+
let message;
|
|
162
|
+
try {
|
|
163
|
+
message = JSON.parse(String(raw));
|
|
164
|
+
} catch {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (message.id && this.pending.has(message.id)) {
|
|
169
|
+
const pending = this.pending.get(message.id);
|
|
170
|
+
this.pending.delete(message.id);
|
|
171
|
+
clearTimeout(pending.timeout);
|
|
172
|
+
if (message.error) {
|
|
173
|
+
pending.reject(new Error(message.error.message || JSON.stringify(message.error)));
|
|
174
|
+
} else {
|
|
175
|
+
pending.resolve(message.result);
|
|
176
|
+
}
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (message.method === "Runtime.executionContextCreated") {
|
|
181
|
+
const context = message.params?.context;
|
|
182
|
+
const frameId = context?.auxData?.frameId;
|
|
183
|
+
if (context?.id && frameId) {
|
|
184
|
+
this.contexts.set(context.id, {
|
|
185
|
+
id: context.id,
|
|
186
|
+
frameId,
|
|
187
|
+
isDefault: Boolean(context.auxData?.isDefault),
|
|
188
|
+
origin: context.origin || "",
|
|
189
|
+
name: context.name || ""
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (message.method === "Runtime.executionContextDestroyed") {
|
|
196
|
+
this.contexts.delete(message.params?.executionContextId);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (message.method === "Runtime.executionContextsCleared") {
|
|
201
|
+
this.contexts.clear();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (message.method === "Page.frameNavigated") {
|
|
206
|
+
if (!message.params?.frame?.parentId) {
|
|
207
|
+
this.mainFrameId = message.params.frame.id || this.mainFrameId;
|
|
208
|
+
this.url = message.params.frame.url || this.url;
|
|
209
|
+
this.loaded = false;
|
|
210
|
+
}
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (message.method === "Page.loadEventFired") {
|
|
215
|
+
this.loaded = true;
|
|
216
|
+
this.emit("updated", { status: "complete" });
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (message.method === "Network.requestWillBeSent") {
|
|
221
|
+
this.requestUrls.set(message.params?.requestId, message.params?.request?.url || "");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (message.method === "Network.requestWillBeSentExtraInfo") {
|
|
226
|
+
const requestId = message.params?.requestId;
|
|
227
|
+
const headers = normalizeHeaderMap(message.params?.headers || {});
|
|
228
|
+
const authorization = headers.authorization || "";
|
|
229
|
+
if (authorization.startsWith("Bearer ")) {
|
|
230
|
+
this.emit("bearer", {
|
|
231
|
+
token: authorization.slice("Bearer ".length).trim(),
|
|
232
|
+
url: this.requestUrls.get(requestId) || ""
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
send(method, params = {}, timeoutMs = 10000) {
|
|
239
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
240
|
+
return Promise.reject(new Error("CDP page is not connected"));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const id = this.nextId++;
|
|
244
|
+
const payload = { id, method, params };
|
|
245
|
+
return new Promise((resolve, reject) => {
|
|
246
|
+
const timeout = setTimeout(() => {
|
|
247
|
+
this.pending.delete(id);
|
|
248
|
+
reject(new Error(`CDP timeout: ${method}`));
|
|
249
|
+
}, timeoutMs);
|
|
250
|
+
this.pending.set(id, { resolve, reject, timeout });
|
|
251
|
+
this.ws.send(JSON.stringify(payload));
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async navigate(url, timeoutMs = DEFAULT_NAVIGATION_TIMEOUT_MS) {
|
|
256
|
+
this.loaded = false;
|
|
257
|
+
await this.send("Page.navigate", { url });
|
|
258
|
+
await this.waitForLoad(timeoutMs);
|
|
259
|
+
const info = await this.send("Runtime.evaluate", {
|
|
260
|
+
expression: "location.href",
|
|
261
|
+
returnByValue: true
|
|
262
|
+
}).catch(() => null);
|
|
263
|
+
this.url = info?.result?.value || url;
|
|
264
|
+
return this;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async waitForLoad(timeoutMs = DEFAULT_NAVIGATION_TIMEOUT_MS) {
|
|
268
|
+
if (this.loaded) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
await new Promise((resolve) => {
|
|
272
|
+
const timer = setTimeout(resolve, timeoutMs);
|
|
273
|
+
this.once("updated", () => {
|
|
274
|
+
clearTimeout(timer);
|
|
275
|
+
resolve();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async bringToFront(options = {}) {
|
|
281
|
+
if (!this.session.allowBrowserFocus && !options.force) {
|
|
282
|
+
this.session.activePageId = this.id;
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
await this.send("Page.bringToFront").catch(() => {});
|
|
286
|
+
this.session.activePageId = this.id;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async clickAt(x, y) {
|
|
290
|
+
this.session.activePageId = this.id;
|
|
291
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
292
|
+
type: "mouseMoved",
|
|
293
|
+
x,
|
|
294
|
+
y,
|
|
295
|
+
button: "none"
|
|
296
|
+
}).catch(() => {});
|
|
297
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
298
|
+
type: "mousePressed",
|
|
299
|
+
x,
|
|
300
|
+
y,
|
|
301
|
+
button: "left",
|
|
302
|
+
clickCount: 1
|
|
303
|
+
});
|
|
304
|
+
await this.send("Input.dispatchMouseEvent", {
|
|
305
|
+
type: "mouseReleased",
|
|
306
|
+
x,
|
|
307
|
+
y,
|
|
308
|
+
button: "left",
|
|
309
|
+
clickCount: 1
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async evaluateFunction(func, args = [], options = {}) {
|
|
314
|
+
await this.connect();
|
|
315
|
+
const allFrames = Boolean(options.allFrames);
|
|
316
|
+
const expression = `(${func.toString()})(...${JSON.stringify(args || [])})`;
|
|
317
|
+
const contexts = Array.from(this.contexts.values())
|
|
318
|
+
.filter((context) => {
|
|
319
|
+
if (!context.isDefault) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
if (allFrames) {
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
return !this.mainFrameId || context.frameId === this.mainFrameId;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const selectedContexts = contexts.length ? contexts : [null];
|
|
329
|
+
const results = [];
|
|
330
|
+
|
|
331
|
+
for (const context of selectedContexts) {
|
|
332
|
+
try {
|
|
333
|
+
const params = {
|
|
334
|
+
expression,
|
|
335
|
+
returnByValue: true,
|
|
336
|
+
awaitPromise: true
|
|
337
|
+
};
|
|
338
|
+
if (context?.id) {
|
|
339
|
+
params.contextId = context.id;
|
|
340
|
+
}
|
|
341
|
+
const result = await this.send("Runtime.evaluate", params, 8000);
|
|
342
|
+
results.push({ frameId: context?.frameId, result: result?.result?.value });
|
|
343
|
+
} catch (err) {
|
|
344
|
+
results.push({ frameId: context?.frameId, error: String(err?.message || err) });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return results;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async close() {
|
|
352
|
+
if (this.closed) {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
this.closed = true;
|
|
356
|
+
try {
|
|
357
|
+
await requestText(`http://127.0.0.1:${this.session.port}/json/close/${encodeURIComponent(this.id)}`, {
|
|
358
|
+
timeoutMs: 2000
|
|
359
|
+
});
|
|
360
|
+
} catch {
|
|
361
|
+
try {
|
|
362
|
+
this.ws?.close();
|
|
363
|
+
} catch {
|
|
364
|
+
// ignored
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
this.session.pages.delete(this.id);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export class BrowserSession extends EventEmitter {
|
|
372
|
+
constructor(options = {}) {
|
|
373
|
+
super();
|
|
374
|
+
this.port = Number(options.port || process.env.M365C_EDGE_DEBUG_PORT || DEFAULT_EDGE_PORT);
|
|
375
|
+
this.userDataDir = expandHome(options.userDataDir || defaultUserDataDir());
|
|
376
|
+
this.profileDirectory = options.profileDirectory || process.env.M365C_EDGE_PROFILE_DIRECTORY || "Default";
|
|
377
|
+
this.edgePath = findEdgeExecutable();
|
|
378
|
+
this.logger = options.logger || console;
|
|
379
|
+
this.allowBrowserFocus = options.allowBrowserFocus ?? process.env.M365C_ALLOW_BROWSER_FOCUS === "1";
|
|
380
|
+
this.process = null;
|
|
381
|
+
this.pages = new Map();
|
|
382
|
+
this.activePageId = null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async start() {
|
|
386
|
+
if (await this.#isDebugEndpointReady()) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
fs.mkdirSync(this.userDataDir, { recursive: true });
|
|
391
|
+
const args = [
|
|
392
|
+
`--remote-debugging-port=${this.port}`,
|
|
393
|
+
`--user-data-dir=${this.userDataDir}`,
|
|
394
|
+
`--profile-directory=${this.profileDirectory}`,
|
|
395
|
+
"--no-first-run",
|
|
396
|
+
"--no-default-browser-check",
|
|
397
|
+
"--disable-features=CalculateNativeWinOcclusion",
|
|
398
|
+
"about:blank"
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
this.process = spawn(this.edgePath, args, {
|
|
402
|
+
stdio: "ignore",
|
|
403
|
+
detached: true
|
|
404
|
+
});
|
|
405
|
+
this.process.unref();
|
|
406
|
+
this.#moveBrowserToBackground();
|
|
407
|
+
|
|
408
|
+
const deadline = Date.now() + 30000;
|
|
409
|
+
while (Date.now() < deadline) {
|
|
410
|
+
if (await this.#isDebugEndpointReady()) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
await sleep(500);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
throw new Error(
|
|
417
|
+
`Edge CDP endpoint did not start on 127.0.0.1:${this.port}. ` +
|
|
418
|
+
`Set M365C_EDGE_PATH/M365C_EDGE_USER_DATA_DIR if your Edge install or profile is elsewhere.`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
#moveBrowserToBackground() {
|
|
423
|
+
if (this.allowBrowserFocus || process.platform !== "darwin") {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const script = [
|
|
427
|
+
"tell application \"System Events\"",
|
|
428
|
+
" if exists process \"Microsoft Edge\" then",
|
|
429
|
+
" set visible of process \"Microsoft Edge\" to false",
|
|
430
|
+
" end if",
|
|
431
|
+
"end tell"
|
|
432
|
+
].join("\n");
|
|
433
|
+
const child = spawn("osascript", ["-e", script], {
|
|
434
|
+
stdio: "ignore",
|
|
435
|
+
detached: true
|
|
436
|
+
});
|
|
437
|
+
child.unref();
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async #isDebugEndpointReady() {
|
|
441
|
+
try {
|
|
442
|
+
await requestJson(`http://127.0.0.1:${this.port}/json/version`, { timeoutMs: 800 });
|
|
443
|
+
return true;
|
|
444
|
+
} catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
async listTargets() {
|
|
450
|
+
await this.start();
|
|
451
|
+
return requestJson(`http://127.0.0.1:${this.port}/json/list`, { timeoutMs: 2000 });
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
async createPage(url = "about:blank", options = {}) {
|
|
455
|
+
await this.start();
|
|
456
|
+
const target = await requestJson(
|
|
457
|
+
`http://127.0.0.1:${this.port}/json/new?${encodeURIComponent(url)}`,
|
|
458
|
+
{ method: "PUT", timeoutMs: 3000 }
|
|
459
|
+
);
|
|
460
|
+
const page = new CdpPage(this, target);
|
|
461
|
+
this.pages.set(page.id, page);
|
|
462
|
+
await page.connect();
|
|
463
|
+
page.on("updated", (changeInfo) => {
|
|
464
|
+
this.emit("tabUpdated", page.id, changeInfo);
|
|
465
|
+
});
|
|
466
|
+
page.on("bearer", (event) => {
|
|
467
|
+
this.emit("bearer", event);
|
|
468
|
+
});
|
|
469
|
+
if (options.active) {
|
|
470
|
+
await page.bringToFront({ force: true });
|
|
471
|
+
}
|
|
472
|
+
return page;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
async getPage(id) {
|
|
476
|
+
if (this.pages.has(id)) {
|
|
477
|
+
return this.pages.get(id);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const targets = await this.listTargets();
|
|
481
|
+
const target = targets.find((row) => row.id === id);
|
|
482
|
+
if (!target?.webSocketDebuggerUrl) {
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const page = new CdpPage(this, target);
|
|
486
|
+
this.pages.set(page.id, page);
|
|
487
|
+
await page.connect();
|
|
488
|
+
page.on("updated", (changeInfo) => {
|
|
489
|
+
this.emit("tabUpdated", page.id, changeInfo);
|
|
490
|
+
});
|
|
491
|
+
page.on("bearer", (event) => {
|
|
492
|
+
this.emit("bearer", event);
|
|
493
|
+
});
|
|
494
|
+
return page;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
async findPage(predicate) {
|
|
498
|
+
const targets = await this.listTargets();
|
|
499
|
+
const target = targets.find((row) => row.type === "page" && predicate(row));
|
|
500
|
+
if (!target?.id) {
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
return this.getPage(target.id);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async getChildFramePages(parentId) {
|
|
507
|
+
const targets = await this.listTargets();
|
|
508
|
+
const children = [];
|
|
509
|
+
const queue = [parentId];
|
|
510
|
+
const seen = new Set(queue);
|
|
511
|
+
|
|
512
|
+
while (queue.length) {
|
|
513
|
+
const currentParentId = queue.shift();
|
|
514
|
+
for (const target of targets) {
|
|
515
|
+
if (target.type !== "iframe" || target.parentId !== currentParentId || !target.webSocketDebuggerUrl) {
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
if (seen.has(target.id)) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
seen.add(target.id);
|
|
522
|
+
queue.push(target.id);
|
|
523
|
+
const page = await this.getPage(target.id);
|
|
524
|
+
if (page) {
|
|
525
|
+
children.push(page);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
return children;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
export function installChromeShim(browserSession) {
|
|
535
|
+
const listeners = new Set();
|
|
536
|
+
|
|
537
|
+
browserSession.on("tabUpdated", (tabId, changeInfo) => {
|
|
538
|
+
for (const listener of listeners) {
|
|
539
|
+
try {
|
|
540
|
+
listener(tabId, changeInfo);
|
|
541
|
+
} catch {
|
|
542
|
+
// ignored
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
|
|
547
|
+
globalThis.chrome = {
|
|
548
|
+
tabs: {
|
|
549
|
+
async create({ url = "about:blank", active = false } = {}) {
|
|
550
|
+
const page = await browserSession.createPage(url, { active });
|
|
551
|
+
return { id: page.id, url: page.url, status: page.loaded ? "complete" : "loading" };
|
|
552
|
+
},
|
|
553
|
+
async remove(tabId) {
|
|
554
|
+
const page = await browserSession.getPage(tabId);
|
|
555
|
+
await page?.close();
|
|
556
|
+
},
|
|
557
|
+
async update(tabId, updateProperties = {}) {
|
|
558
|
+
const page = await browserSession.getPage(tabId);
|
|
559
|
+
if (!page) {
|
|
560
|
+
throw new Error(`Tab not found: ${tabId}`);
|
|
561
|
+
}
|
|
562
|
+
if (updateProperties.url) {
|
|
563
|
+
await page.navigate(updateProperties.url);
|
|
564
|
+
}
|
|
565
|
+
if (updateProperties.active) {
|
|
566
|
+
await page.bringToFront({ force: true });
|
|
567
|
+
}
|
|
568
|
+
return { id: page.id, url: page.url, status: page.loaded ? "complete" : "loading" };
|
|
569
|
+
},
|
|
570
|
+
async query(queryInfo = {}) {
|
|
571
|
+
if (queryInfo.active) {
|
|
572
|
+
const page = browserSession.activePageId
|
|
573
|
+
? await browserSession.getPage(browserSession.activePageId)
|
|
574
|
+
: null;
|
|
575
|
+
return page ? [{ id: page.id, url: page.url, status: page.loaded ? "complete" : "loading" }] : [];
|
|
576
|
+
}
|
|
577
|
+
const targets = await browserSession.listTargets();
|
|
578
|
+
return targets
|
|
579
|
+
.filter((target) => target.type === "page")
|
|
580
|
+
.map((target) => ({ id: target.id, url: target.url, status: "complete" }));
|
|
581
|
+
},
|
|
582
|
+
async get(tabId) {
|
|
583
|
+
const page = await browserSession.getPage(tabId);
|
|
584
|
+
if (!page) {
|
|
585
|
+
throw new Error(`Tab not found: ${tabId}`);
|
|
586
|
+
}
|
|
587
|
+
return { id: page.id, url: page.url, status: page.loaded ? "complete" : "loading" };
|
|
588
|
+
},
|
|
589
|
+
onUpdated: {
|
|
590
|
+
addListener(listener) {
|
|
591
|
+
listeners.add(listener);
|
|
592
|
+
},
|
|
593
|
+
removeListener(listener) {
|
|
594
|
+
listeners.delete(listener);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
},
|
|
598
|
+
scripting: {
|
|
599
|
+
async executeScript({ target, func, args = [] } = {}) {
|
|
600
|
+
const page = await browserSession.getPage(target?.tabId);
|
|
601
|
+
if (!page) {
|
|
602
|
+
throw new Error(`Tab not found: ${target?.tabId}`);
|
|
603
|
+
}
|
|
604
|
+
const allFrames = Boolean(target?.allFrames);
|
|
605
|
+
const results = await page.evaluateFunction(func, args, { allFrames });
|
|
606
|
+
if (!allFrames) {
|
|
607
|
+
return results;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const framePages = await browserSession.getChildFramePages(page.id);
|
|
611
|
+
for (const framePage of framePages) {
|
|
612
|
+
const frameResults = await framePage.evaluateFunction(func, args, { allFrames: false });
|
|
613
|
+
results.push(...frameResults);
|
|
614
|
+
}
|
|
615
|
+
return results;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
};
|
|
619
|
+
}
|
package/src/index.js
CHANGED
|
@@ -7,7 +7,10 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
|
7
7
|
import { createMcpExpressApp } from "@modelcontextprotocol/sdk/server/express.js";
|
|
8
8
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
9
9
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
10
|
-
import {
|
|
10
|
+
import { SearchApi } from "./lib/search-api.js";
|
|
11
|
+
import { ContentReader } from "./lib/content-reader.js";
|
|
12
|
+
import { BrowserSession, installChromeShim } from "./browser-session.js";
|
|
13
|
+
import { GraphTokenProvider, ServerTokenManager, SubstrateTokenProvider, TOKEN_AUDIENCES } from "./token-providers.js";
|
|
11
14
|
|
|
12
15
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
16
|
const PKG = JSON.parse(fs.readFileSync(path.resolve(__dirname, "..", "package.json"), "utf-8"));
|
|
@@ -18,7 +21,6 @@ const SERVER_INFO = {
|
|
|
18
21
|
};
|
|
19
22
|
|
|
20
23
|
const DEFAULT_MCP_PORT = 52366;
|
|
21
|
-
const DEFAULT_WS_PORT = 52365;
|
|
22
24
|
const SESSION_IDLE_TTL_MS = 30 * 60 * 1000;
|
|
23
25
|
const SESSION_SWEEP_INTERVAL_MS = 5 * 60 * 1000;
|
|
24
26
|
|
|
@@ -282,7 +284,6 @@ function sendJsonRpcError(res, status, code, message) {
|
|
|
282
284
|
|
|
283
285
|
async function main() {
|
|
284
286
|
const mcpPort = parsePort(process.env.M365C_MCP_PORT, "M365C_MCP_PORT", DEFAULT_MCP_PORT);
|
|
285
|
-
const wsPort = parsePort(process.env.M365C_WS_PORT, "M365C_WS_PORT", DEFAULT_WS_PORT);
|
|
286
287
|
|
|
287
288
|
// MCP responses are returned over HTTP; operational logs must go to stderr only.
|
|
288
289
|
const stderrLogger = {
|
|
@@ -294,15 +295,37 @@ async function main() {
|
|
|
294
295
|
const readHandleCache = createReadHandleCache();
|
|
295
296
|
const tempFiles = createTempFileManager(stderrLogger);
|
|
296
297
|
|
|
297
|
-
const
|
|
298
|
-
host: "127.0.0.1",
|
|
299
|
-
port: wsPort,
|
|
298
|
+
const browserSession = new BrowserSession({
|
|
300
299
|
logger: stderrLogger
|
|
301
300
|
});
|
|
302
|
-
|
|
301
|
+
installChromeShim(browserSession);
|
|
302
|
+
|
|
303
|
+
const tokenManager = new ServerTokenManager({
|
|
304
|
+
logger: stderrLogger
|
|
305
|
+
});
|
|
306
|
+
const substrateTokenProvider = new SubstrateTokenProvider(browserSession, tokenManager, {
|
|
307
|
+
logger: stderrLogger
|
|
308
|
+
});
|
|
309
|
+
const graphTokenProvider = new GraphTokenProvider(browserSession, {
|
|
310
|
+
logger: stderrLogger,
|
|
311
|
+
tokenManager
|
|
312
|
+
});
|
|
313
|
+
const searchApi = new SearchApi(tokenManager, {
|
|
314
|
+
onAuthError: () => substrateTokenProvider.refresh()
|
|
315
|
+
});
|
|
316
|
+
const contentReader = new ContentReader(graphTokenProvider, stderrLogger);
|
|
303
317
|
|
|
304
318
|
const toolRegistry = new ToolRegistry();
|
|
305
|
-
const toolContext = {
|
|
319
|
+
const toolContext = {
|
|
320
|
+
readHandleCache,
|
|
321
|
+
tempFiles,
|
|
322
|
+
searchApi,
|
|
323
|
+
contentReader,
|
|
324
|
+
substrateTokenProvider,
|
|
325
|
+
graphTokenProvider,
|
|
326
|
+
tokenManager,
|
|
327
|
+
tokenAudiences: TOKEN_AUDIENCES
|
|
328
|
+
};
|
|
306
329
|
await loadToolModules(toolRegistry, toolContext);
|
|
307
330
|
|
|
308
331
|
const sessions = new Map();
|
|
@@ -404,12 +427,6 @@ async function main() {
|
|
|
404
427
|
}
|
|
405
428
|
}
|
|
406
429
|
|
|
407
|
-
try {
|
|
408
|
-
await ws.stop();
|
|
409
|
-
} catch (err) {
|
|
410
|
-
stderrLogger.warn("[m365connector] failed to stop WebSocket server:", err);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
430
|
tempFiles.cleanup();
|
|
414
431
|
|
|
415
432
|
await new Promise((resolve) => {
|