m365connector 0.3.8 → 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.8",
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,6 +73,79 @@ function requestText(url, options = {}) {
70
73
  });
71
74
  }
72
75
 
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
+
73
149
  export function findEdgeExecutable() {
74
150
  if (process.env.M365C_EDGE_PATH) {
75
151
  return expandHome(process.env.M365C_EDGE_PATH);
@@ -122,6 +198,41 @@ function normalizeHeaderMap(headers = {}) {
122
198
  return out;
123
199
  }
124
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
+
125
236
  export class CdpPage extends EventEmitter {
126
237
  constructor(session, target) {
127
238
  super();
@@ -290,12 +401,13 @@ export class CdpPage extends EventEmitter {
290
401
  }
291
402
 
292
403
  async bringToFront(options = {}) {
293
- if (!this.session.allowBrowserFocus && !options.force) {
404
+ if (!this.session.shouldBringBrowserToFront(options)) {
294
405
  this.session.activePageId = this.id;
295
- return;
406
+ return false;
296
407
  }
297
408
  await this.send("Page.bringToFront").catch(() => {});
298
409
  this.session.activePageId = this.id;
410
+ return true;
299
411
  }
300
412
 
301
413
  async clickAt(x, y) {
@@ -389,11 +501,40 @@ export class BrowserSession extends EventEmitter {
389
501
  this.edgePath = findEdgeExecutable();
390
502
  this.logger = options.logger || console;
391
503
  this.allowBrowserFocus = options.allowBrowserFocus ?? process.env.M365C_ALLOW_BROWSER_FOCUS === "1";
504
+ this.neverFocusBrowser = options.neverFocusBrowser ?? process.env.M365C_NEVER_FOCUS_BROWSER === "1";
392
505
  this.process = null;
393
506
  this.pages = new Map();
394
507
  this.activePageId = null;
395
508
  }
396
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
+
397
538
  async start() {
398
539
  if (await this.#isDebugEndpointReady()) {
399
540
  return;
@@ -465,10 +606,23 @@ export class BrowserSession extends EventEmitter {
465
606
 
466
607
  async createPage(url = "about:blank", options = {}) {
467
608
  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
- );
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
+ }
472
626
  const page = new CdpPage(this, target);
473
627
  this.pages.set(page.id, page);
474
628
  await page.connect();
@@ -479,11 +633,33 @@ export class BrowserSession extends EventEmitter {
479
633
  this.emit("bearer", event);
480
634
  });
481
635
  if (options.active) {
482
- await page.bringToFront({ force: true });
636
+ await page.bringToFront();
637
+ }
638
+ if (!this.shouldBringBrowserToFront(options)) {
639
+ await this.restoreFrontmostApp(previousFrontmostApp);
483
640
  }
484
641
  return page;
485
642
  }
486
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
+
487
663
  async getPage(id) {
488
664
  if (this.pages.has(id)) {
489
665
  return this.pages.get(id);
@@ -571,11 +747,15 @@ export function installChromeShim(browserSession) {
571
747
  if (!page) {
572
748
  throw new Error(`Tab not found: ${tabId}`);
573
749
  }
750
+ const previousFrontmostApp = await browserSession.captureFrontmostApp();
574
751
  if (updateProperties.url) {
575
752
  await page.navigate(updateProperties.url);
576
753
  }
577
754
  if (updateProperties.active) {
578
- await page.bringToFront({ force: true });
755
+ await page.bringToFront();
756
+ }
757
+ if (!updateProperties.active || !browserSession.shouldBringBrowserToFront()) {
758
+ await browserSession.restoreFrontmostApp(previousFrontmostApp);
579
759
  }
580
760
  return { id: page.id, url: page.url, status: page.loaded ? "complete" : "loading" };
581
761
  },
@@ -626,6 +806,12 @@ export function installChromeShim(browserSession) {
626
806
  }
627
807
  return results;
628
808
  }
809
+ },
810
+ m365connector: {
811
+ async requestUserInteraction(tabId, reason) {
812
+ const page = await browserSession.getPage(tabId);
813
+ return browserSession.requestUserInteraction(page, reason);
814
+ }
629
815
  }
630
816
  };
631
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