m365connector 0.3.7 → 0.3.9

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 CHANGED
@@ -56,6 +56,8 @@ Then add to your MCP client config:
56
56
  | `M365C_EDGE_PATH` | auto-detected | Microsoft Edge executable path |
57
57
  | `M365C_EDGE_USER_DATA_DIR` | phantomwright `ud-m365` profile when present, else `~/.m365connector/edge-profile` | Persistent Edge user data directory |
58
58
  | `M365C_EDGE_PROFILE_DIRECTORY` | `Default` | Edge profile directory inside the user data dir |
59
+ | `M365C_ALLOW_BROWSER_FOCUS` | unset | Set to `1` to allow MCP-driven Edge tabs to come to the foreground for any active-tab request. By default Edge stays in the background unless Microsoft 365 requires user login, MFA, or consent. |
60
+ | `M365C_NEVER_FOCUS_BROWSER` | unset | Set to `1` to prevent MCP-driven Edge tabs from ever coming to the foreground. When user interaction is required, the operation will wait/fail instead of stealing focus. |
59
61
  | `M365C_TOKEN_CACHE_PATH` | `~/.m365connector/token-cache.json` | Local token cache path; stores only current unexpired captured tokens |
60
62
 
61
63
  ## Tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "m365connector",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "MCP server for Microsoft 365 data via Edge CDP",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -3,11 +3,14 @@ import fs from "node:fs";
3
3
  import http from "node:http";
4
4
  import os from "node:os";
5
5
  import path from "node:path";
6
- import { spawn } from "node:child_process";
6
+ import { execFile, spawn } from "node:child_process";
7
+ import { promisify } from "node:util";
7
8
  import { WebSocket } from "ws";
8
9
 
9
10
  const DEFAULT_EDGE_PORT = 52367;
10
11
  const DEFAULT_NAVIGATION_TIMEOUT_MS = 30000;
12
+ const USER_INTERACTION_FOCUS_REASON = "user-interaction";
13
+ const execFileAsync = promisify(execFile);
11
14
 
12
15
  function sleep(ms) {
13
16
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -70,11 +73,96 @@ function requestText(url, options = {}) {
70
73
  });
71
74
  }
72
75
 
73
- function findEdgeExecutable() {
76
+ async function createBackgroundTarget(port, url) {
77
+ const version = await requestJson(`http://127.0.0.1:${port}/json/version`, { timeoutMs: 2000 });
78
+ const wsUrl = version?.webSocketDebuggerUrl;
79
+ if (!wsUrl) {
80
+ throw new Error("Browser CDP websocket URL unavailable");
81
+ }
82
+
83
+ const ws = new WebSocket(wsUrl);
84
+ await new Promise((resolve, reject) => {
85
+ ws.once("open", resolve);
86
+ ws.once("error", reject);
87
+ });
88
+
89
+ try {
90
+ const result = await new Promise((resolve, reject) => {
91
+ const timeout = setTimeout(() => {
92
+ ws.off("message", onMessage);
93
+ reject(new Error("CDP timeout: Target.createTarget"));
94
+ }, 5000);
95
+ const onMessage = (raw) => {
96
+ try {
97
+ const message = JSON.parse(String(raw));
98
+ if (message.id !== 1) {
99
+ return;
100
+ }
101
+ clearTimeout(timeout);
102
+ ws.off("message", onMessage);
103
+ if (message.error) {
104
+ reject(new Error(message.error.message || JSON.stringify(message.error)));
105
+ } else {
106
+ resolve(message.result || {});
107
+ }
108
+ } catch (err) {
109
+ clearTimeout(timeout);
110
+ ws.off("message", onMessage);
111
+ reject(err);
112
+ }
113
+ };
114
+ ws.on("message", onMessage);
115
+ ws.send(JSON.stringify({
116
+ id: 1,
117
+ method: "Target.createTarget",
118
+ params: {
119
+ url,
120
+ background: true
121
+ }
122
+ }));
123
+ });
124
+
125
+ const targetId = result?.targetId;
126
+ if (!targetId) {
127
+ throw new Error("Target.createTarget did not return targetId");
128
+ }
129
+
130
+ const deadline = Date.now() + 5000;
131
+ while (Date.now() < deadline) {
132
+ const targets = await requestJson(`http://127.0.0.1:${port}/json/list`, { timeoutMs: 2000 });
133
+ const target = targets.find((row) => row.id === targetId);
134
+ if (target?.webSocketDebuggerUrl) {
135
+ return target;
136
+ }
137
+ await sleep(100);
138
+ }
139
+ throw new Error(`Target ${targetId} did not appear in /json/list`);
140
+ } finally {
141
+ try {
142
+ ws.close();
143
+ } catch {
144
+ // ignored
145
+ }
146
+ }
147
+ }
148
+
149
+ export function findEdgeExecutable() {
74
150
  if (process.env.M365C_EDGE_PATH) {
75
151
  return expandHome(process.env.M365C_EDGE_PATH);
76
152
  }
77
153
 
154
+ if (process.platform === "win32") {
155
+ const programFiles = process.env["ProgramFiles"] || "C:\\Program Files";
156
+ const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
157
+ const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
158
+ const winCandidates = [
159
+ path.join(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe"),
160
+ path.join(programFiles, "Microsoft", "Edge", "Application", "msedge.exe"),
161
+ path.join(localAppData, "Microsoft", "Edge", "Application", "msedge.exe")
162
+ ];
163
+ return winCandidates.find((candidate) => fs.existsSync(candidate)) || "msedge.exe";
164
+ }
165
+
78
166
  const candidates = [
79
167
  "/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge",
80
168
  path.join(os.homedir(), "Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge"),
@@ -110,6 +198,41 @@ function normalizeHeaderMap(headers = {}) {
110
198
  return out;
111
199
  }
112
200
 
201
+ async function getFrontmostApplicationName() {
202
+ if (process.platform !== "darwin") {
203
+ return null;
204
+ }
205
+ try {
206
+ const { stdout } = await execFileAsync("osascript", [
207
+ "-e",
208
+ 'tell application "System Events" to get name of first application process whose frontmost is true'
209
+ ]);
210
+ const name = stdout.trim();
211
+ return name || null;
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
216
+
217
+ async function restoreFrontmostApplication(name, logger = console) {
218
+ if (process.platform !== "darwin" || !name || name === "Microsoft Edge") {
219
+ return;
220
+ }
221
+ const escapedName = name.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
222
+ const script = [
223
+ 'tell application "System Events"',
224
+ ` if exists process "${escapedName}" then`,
225
+ ` set frontmost of process "${escapedName}" to true`,
226
+ " end if",
227
+ "end tell"
228
+ ].join("\n");
229
+ try {
230
+ await execFileAsync("osascript", ["-e", script], { timeout: 3000 });
231
+ } catch (err) {
232
+ logger.warn?.("[m365connector][browser] failed to restore frontmost application:", err.message);
233
+ }
234
+ }
235
+
113
236
  export class CdpPage extends EventEmitter {
114
237
  constructor(session, target) {
115
238
  super();
@@ -278,12 +401,13 @@ export class CdpPage extends EventEmitter {
278
401
  }
279
402
 
280
403
  async bringToFront(options = {}) {
281
- if (!this.session.allowBrowserFocus && !options.force) {
404
+ if (!this.session.shouldBringBrowserToFront(options)) {
282
405
  this.session.activePageId = this.id;
283
- return;
406
+ return false;
284
407
  }
285
408
  await this.send("Page.bringToFront").catch(() => {});
286
409
  this.session.activePageId = this.id;
410
+ return true;
287
411
  }
288
412
 
289
413
  async clickAt(x, y) {
@@ -377,11 +501,40 @@ export class BrowserSession extends EventEmitter {
377
501
  this.edgePath = findEdgeExecutable();
378
502
  this.logger = options.logger || console;
379
503
  this.allowBrowserFocus = options.allowBrowserFocus ?? process.env.M365C_ALLOW_BROWSER_FOCUS === "1";
504
+ this.neverFocusBrowser = options.neverFocusBrowser ?? process.env.M365C_NEVER_FOCUS_BROWSER === "1";
380
505
  this.process = null;
381
506
  this.pages = new Map();
382
507
  this.activePageId = null;
383
508
  }
384
509
 
510
+ shouldBringBrowserToFront(options = {}) {
511
+ if (this.neverFocusBrowser) {
512
+ return false;
513
+ }
514
+ if (this.allowBrowserFocus) {
515
+ return true;
516
+ }
517
+ return options.force === true && options.reason === USER_INTERACTION_FOCUS_REASON;
518
+ }
519
+
520
+ shouldPreserveFrontmostApp() {
521
+ return process.platform === "darwin" && !this.allowBrowserFocus;
522
+ }
523
+
524
+ async captureFrontmostApp() {
525
+ if (!this.shouldPreserveFrontmostApp()) {
526
+ return null;
527
+ }
528
+ return getFrontmostApplicationName();
529
+ }
530
+
531
+ async restoreFrontmostApp(name) {
532
+ if (!this.shouldPreserveFrontmostApp()) {
533
+ return;
534
+ }
535
+ await restoreFrontmostApplication(name, this.logger);
536
+ }
537
+
385
538
  async start() {
386
539
  if (await this.#isDebugEndpointReady()) {
387
540
  return;
@@ -453,10 +606,23 @@ export class BrowserSession extends EventEmitter {
453
606
 
454
607
  async createPage(url = "about:blank", options = {}) {
455
608
  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
- );
609
+ const previousFrontmostApp = await this.captureFrontmostApp();
610
+ let target = null;
611
+ if (!this.shouldBringBrowserToFront(options)) {
612
+ target = await createBackgroundTarget(this.port, url).catch((err) => {
613
+ this.logger.warn("[m365connector][browser] background target creation failed:", err.message);
614
+ return null;
615
+ });
616
+ }
617
+ if (!target) {
618
+ target = await requestJson(
619
+ `http://127.0.0.1:${this.port}/json/new?${encodeURIComponent(url)}`,
620
+ { method: "PUT", timeoutMs: 3000 }
621
+ );
622
+ }
623
+ if (!this.shouldBringBrowserToFront(options)) {
624
+ await this.restoreFrontmostApp(previousFrontmostApp);
625
+ }
460
626
  const page = new CdpPage(this, target);
461
627
  this.pages.set(page.id, page);
462
628
  await page.connect();
@@ -467,11 +633,33 @@ export class BrowserSession extends EventEmitter {
467
633
  this.emit("bearer", event);
468
634
  });
469
635
  if (options.active) {
470
- await page.bringToFront({ force: true });
636
+ await page.bringToFront();
637
+ }
638
+ if (!this.shouldBringBrowserToFront(options)) {
639
+ await this.restoreFrontmostApp(previousFrontmostApp);
471
640
  }
472
641
  return page;
473
642
  }
474
643
 
644
+ async requestUserInteraction(page, reason = "Microsoft 365 requires user interaction") {
645
+ if (!page) {
646
+ return false;
647
+ }
648
+ const focused = await page.bringToFront({
649
+ force: true,
650
+ reason: USER_INTERACTION_FOCUS_REASON
651
+ });
652
+ if (focused) {
653
+ this.logger.warn("[m365connector][browser] brought Edge to foreground:", reason);
654
+ } else if (this.neverFocusBrowser) {
655
+ this.logger.warn(
656
+ "[m365connector][browser] user interaction required but browser focus is disabled:",
657
+ reason
658
+ );
659
+ }
660
+ return focused;
661
+ }
662
+
475
663
  async getPage(id) {
476
664
  if (this.pages.has(id)) {
477
665
  return this.pages.get(id);
@@ -559,11 +747,15 @@ export function installChromeShim(browserSession) {
559
747
  if (!page) {
560
748
  throw new Error(`Tab not found: ${tabId}`);
561
749
  }
750
+ const previousFrontmostApp = await browserSession.captureFrontmostApp();
562
751
  if (updateProperties.url) {
563
752
  await page.navigate(updateProperties.url);
564
753
  }
565
754
  if (updateProperties.active) {
566
- await page.bringToFront({ force: true });
755
+ await page.bringToFront();
756
+ }
757
+ if (!updateProperties.active || !browserSession.shouldBringBrowserToFront()) {
758
+ await browserSession.restoreFrontmostApp(previousFrontmostApp);
567
759
  }
568
760
  return { id: page.id, url: page.url, status: page.loaded ? "complete" : "loading" };
569
761
  },
@@ -614,6 +806,12 @@ export function installChromeShim(browserSession) {
614
806
  }
615
807
  return results;
616
808
  }
809
+ },
810
+ m365connector: {
811
+ async requestUserInteraction(tabId, reason) {
812
+ const page = await browserSession.getPage(tabId);
813
+ return browserSession.requestUserInteraction(page, reason);
814
+ }
617
815
  }
618
816
  };
619
817
  }
@@ -1,3 +1,8 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+
4
+ const execFileAsync = promisify(execFile);
5
+
1
6
  let jsZipRef = globalThis.JSZip || null;
2
7
  let jsZipLoadPromise = null;
3
8
  let jsZipLoadFailed = false;
@@ -40,6 +45,45 @@ function sleep(ms) {
40
45
  return new Promise((resolve) => setTimeout(resolve, ms));
41
46
  }
42
47
 
48
+ function shouldPreserveFrontmostApp() {
49
+ return process.platform === "darwin" && process.env.M365C_ALLOW_BROWSER_FOCUS !== "1";
50
+ }
51
+
52
+ async function getFrontmostApplicationName() {
53
+ if (!shouldPreserveFrontmostApp()) {
54
+ return null;
55
+ }
56
+ try {
57
+ const { stdout } = await execFileAsync("osascript", [
58
+ "-e",
59
+ 'tell application "System Events" to get name of first application process whose frontmost is true'
60
+ ]);
61
+ const name = stdout.trim();
62
+ return name || null;
63
+ } catch (_err) {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ async function restoreFrontmostApplication(name, logger = console) {
69
+ if (!shouldPreserveFrontmostApp() || !name || name === "Microsoft Edge") {
70
+ return;
71
+ }
72
+ const escapedName = name.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
73
+ const script = [
74
+ 'tell application "System Events"',
75
+ ` if exists process "${escapedName}" then`,
76
+ ` set frontmost of process "${escapedName}" to true`,
77
+ " end if",
78
+ "end tell"
79
+ ].join("\n");
80
+ try {
81
+ await execFileAsync("osascript", ["-e", script], { timeout: 3000 });
82
+ } catch (err) {
83
+ logger.warn?.("[m365connector] failed to restore frontmost application:", err.message);
84
+ }
85
+ }
86
+
43
87
  function pickContext(params) {
44
88
  if (!params) {
45
89
  return null;
@@ -1214,6 +1258,7 @@ export class ContentReader {
1214
1258
 
1215
1259
  async waitForEditorInAnyFrame(tabId, deadline) {
1216
1260
  const pollEnd = Math.min(Date.now() + 60000, deadline);
1261
+ let interactionRequested = false;
1217
1262
 
1218
1263
  while (Date.now() < pollEnd) {
1219
1264
  try {
@@ -1221,6 +1266,23 @@ export class ContentReader {
1221
1266
  target: { tabId, allFrames: true },
1222
1267
  func: () => {
1223
1268
  const host = location.hostname.toLowerCase();
1269
+ const text = document.body?.innerText?.toLowerCase() || "";
1270
+ const path = location.pathname.toLowerCase();
1271
+ if (
1272
+ host === "login.microsoftonline.com"
1273
+ || host === "login.live.com"
1274
+ || path.includes("/common/oauth2")
1275
+ || text.includes("sign in to your account")
1276
+ || text.includes("pick an account")
1277
+ || text.includes("enter password")
1278
+ || text.includes("stay signed in?")
1279
+ || text.includes("verify your identity")
1280
+ || text.includes("approve sign in request")
1281
+ || text.includes("permission requested")
1282
+ || text.includes("permissions requested")
1283
+ ) {
1284
+ return "interaction";
1285
+ }
1224
1286
  if (!host.endsWith(".officeapps.live.com")) {
1225
1287
  return null;
1226
1288
  }
@@ -1239,6 +1301,10 @@ export class ContentReader {
1239
1301
  if (results && results.some((r) => r.result === "ready")) {
1240
1302
  return true;
1241
1303
  }
1304
+ if (!interactionRequested && results && results.some((r) => r.result === "interaction")) {
1305
+ interactionRequested = true;
1306
+ await this.requestUserInteraction(tabId, "Office Online requires Microsoft 365 sign-in or consent");
1307
+ }
1242
1308
  } catch (_err) {
1243
1309
  // Frames may not be ready yet; keep polling.
1244
1310
  }
@@ -1248,6 +1314,18 @@ export class ContentReader {
1248
1314
  return false;
1249
1315
  }
1250
1316
 
1317
+ async requestUserInteraction(tabId, reason) {
1318
+ try {
1319
+ if (chrome.m365connector?.requestUserInteraction) {
1320
+ return await chrome.m365connector.requestUserInteraction(tabId, reason);
1321
+ }
1322
+ } catch (err) {
1323
+ this.logger.warn("[m365connector] failed to request browser user interaction:", err?.message || err);
1324
+ }
1325
+ this.logger.warn("[m365connector] browser user interaction required:", reason);
1326
+ return false;
1327
+ }
1328
+
1251
1329
  /**
1252
1330
  * Scroll through the Word Online document inside the iframe so that all
1253
1331
  * virtually-rendered pages get materialised in the DOM. Returns the number
@@ -1622,14 +1700,15 @@ export class ContentReader {
1622
1700
  const deadline = Date.now() + DEEP_LINK_BUDGET_MS;
1623
1701
  const editUrl = this.buildOfficeEditUrl(webUrl);
1624
1702
  let tabId = null;
1703
+ const previousFrontmostApp = await getFrontmostApplicationName();
1625
1704
 
1626
1705
  this.#deepLinkActive = true;
1627
1706
  try {
1628
- const tab = await chrome.tabs.create({ url: editUrl, active: true });
1707
+ const tab = await chrome.tabs.create({ url: editUrl, active: false });
1629
1708
  tabId = tab.id;
1630
1709
 
1631
1710
  await this.waitForTabLoad(tabId, Math.min(15000, deadline - Date.now()));
1632
- await chrome.tabs.update(tabId, { active: true }).catch(() => null);
1711
+ await chrome.tabs.update(tabId, { active: false }).catch(() => null);
1633
1712
 
1634
1713
  // SharePoint auto-submits the WOPI form into a cross-origin iframe where
1635
1714
  // Word/PowerPoint Online renders. Wait for the editor to become ready in
@@ -1674,6 +1753,7 @@ export class ContentReader {
1674
1753
  // Ignore if tab already closed.
1675
1754
  }
1676
1755
  }
1756
+ await restoreFrontmostApplication(previousFrontmostApp, this.logger);
1677
1757
  }
1678
1758
  }
1679
1759
 
@@ -1686,6 +1766,7 @@ export class ContentReader {
1686
1766
  let tabId = null;
1687
1767
  let previousActiveTabId = null;
1688
1768
  let activatedForDecryption = false;
1769
+ const previousFrontmostApp = await getFrontmostApplicationName();
1689
1770
  try {
1690
1771
  previousActiveTabId = await this.getActiveTabId();
1691
1772
  const tab = await chrome.tabs.create({ url: deepLink, active: false });
@@ -1707,6 +1788,7 @@ export class ContentReader {
1707
1788
  let sawEncryptedWrapper = false;
1708
1789
  let stableContent = null;
1709
1790
  let followedReadMessageUrl = false;
1791
+ let interactionRequested = false;
1710
1792
  const startedAt = Date.now();
1711
1793
  const pollDeadline = () => sawEncryptedWrapper ? IRM_POLL_MS : BASE_POLL_MS;
1712
1794
 
@@ -1734,6 +1816,21 @@ export class ContentReader {
1734
1816
  seen.add(bodyText);
1735
1817
  candidates.push(bodyText);
1736
1818
  }
1819
+ const lowerBodyText = (bodyText || "").toLowerCase();
1820
+ const host = location.hostname.toLowerCase();
1821
+ const userInteractionRequired = Boolean(
1822
+ host === "login.microsoftonline.com"
1823
+ || host === "login.live.com"
1824
+ || location.pathname.toLowerCase().includes("/common/oauth2")
1825
+ || lowerBodyText.includes("sign in to your account")
1826
+ || lowerBodyText.includes("pick an account")
1827
+ || lowerBodyText.includes("enter password")
1828
+ || lowerBodyText.includes("stay signed in?")
1829
+ || lowerBodyText.includes("verify your identity")
1830
+ || lowerBodyText.includes("approve sign in request")
1831
+ || lowerBodyText.includes("permission requested")
1832
+ || lowerBodyText.includes("permissions requested")
1833
+ );
1737
1834
  for (const link of document.querySelectorAll("a[href]")) {
1738
1835
  const label = `${link.innerText || ""} ${link.getAttribute("aria-label") || ""}`.toLowerCase();
1739
1836
  const href = String(link.href || "");
@@ -1741,11 +1838,19 @@ export class ContentReader {
1741
1838
  readMessageUrls.push(href);
1742
1839
  }
1743
1840
  }
1744
- return { candidates, readMessageUrls };
1841
+ return { candidates, readMessageUrls, userInteractionRequired };
1745
1842
  },
1746
1843
  args: [EMAIL_SELECTORS]
1747
1844
  });
1748
1845
 
1846
+ if (
1847
+ !interactionRequested
1848
+ && executions?.some((entry) => entry?.result?.userInteractionRequired)
1849
+ ) {
1850
+ interactionRequested = true;
1851
+ await this.requestUserInteraction(tabId, "Outlook requires Microsoft 365 sign-in or consent");
1852
+ }
1853
+
1749
1854
  const extraction = this.selectEmailExtraction(executions);
1750
1855
  if (extraction.encrypted && extraction.readMessageUrl && !followedReadMessageUrl && tabId !== null) {
1751
1856
  followedReadMessageUrl = true;
@@ -1833,6 +1938,7 @@ export class ContentReader {
1833
1938
  // Best-effort; the original tab may have been closed.
1834
1939
  }
1835
1940
  }
1941
+ await restoreFrontmostApplication(previousFrontmostApp, this.logger);
1836
1942
  }
1837
1943
  }
1838
1944
 
@@ -1917,7 +2023,7 @@ export class ContentReader {
1917
2023
 
1918
2024
  chrome.tabs.onUpdated.addListener(onUpdated);
1919
2025
  timeoutId = setTimeout(finish, timeoutMs);
1920
- Promise.resolve(chrome.tabs.update(tabId, { url, active: true })).catch(finish);
2026
+ Promise.resolve(chrome.tabs.update(tabId, { url, active: false })).catch(finish);
1921
2027
  });
1922
2028
  }
1923
2029
 
@@ -578,6 +578,7 @@ export class GraphTokenProvider {
578
578
  let lastInfo = null;
579
579
  let signInClicked = false;
580
580
  let signInDetectCount = 0;
581
+ let interactionRequested = false;
581
582
  while (Date.now() < deadline) {
582
583
  if (headerToken && this.#acceptToken(headerToken)) {
583
584
  return headerToken;
@@ -625,6 +626,13 @@ export class GraphTokenProvider {
625
626
  "[m365connector][graph] clicked Graph Explorer sign-in:",
626
627
  JSON.stringify(clickResult?.result || {})
627
628
  );
629
+ if (!clickResult?.result?.clicked && !interactionRequested) {
630
+ interactionRequested = true;
631
+ await this.browserSession.requestUserInteraction?.(
632
+ this.page,
633
+ "Graph Explorer sign-in is required"
634
+ );
635
+ }
628
636
  await this.#completeLoginPopup(deadline);
629
637
  await sleep(3000);
630
638
  continue;
@@ -647,6 +655,8 @@ export class GraphTokenProvider {
647
655
 
648
656
  async #completeLoginPopup(deadline) {
649
657
  let accountClicked = false;
658
+ let interactionRequested = false;
659
+ let noAutoResolveCount = 0;
650
660
  while (Date.now() < deadline) {
651
661
  const loginPage = await this.browserSession.findPage((target) => {
652
662
  try {
@@ -669,6 +679,7 @@ export class GraphTokenProvider {
669
679
  const info = result?.result || {};
670
680
  if (info.clicked) {
671
681
  accountClicked = true;
682
+ noAutoResolveCount = 0;
672
683
  const rect = info.rect;
673
684
  if (rect && rect.w > 0 && rect.h > 0) {
674
685
  await loginPage.clickAt(rect.x + rect.w / 2, rect.y + rect.h / 2).catch(() => {});
@@ -676,6 +687,16 @@ export class GraphTokenProvider {
676
687
  this.logger.warn("[m365connector][graph] clicked Microsoft login account:", JSON.stringify(info));
677
688
  await sleep(3000);
678
689
  } else {
690
+ noAutoResolveCount += 1;
691
+ if (!interactionRequested && noAutoResolveCount >= 2) {
692
+ interactionRequested = true;
693
+ await this.browserSession.requestUserInteraction?.(
694
+ loginPage,
695
+ accountClicked
696
+ ? "Microsoft login requires additional verification"
697
+ : "Microsoft login requires user interaction"
698
+ );
699
+ }
679
700
  await sleep(1000);
680
701
  }
681
702