m365connector 0.3.6 → 0.3.8

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