@nimbus21.ai/chrome-devtools-mcp 0.17.6 → 0.17.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.
@@ -72,9 +72,18 @@ export class McpContext {
72
72
  // Idle page reaper: tracks last activity time per page.
73
73
  static PAGE_IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
74
74
  static IDLE_CHECK_INTERVAL_MS = 60 * 1000; // 60 seconds
75
+ static #INTERNAL_URL_PREFIXES = [
76
+ 'about:',
77
+ 'chrome://',
78
+ 'chrome-extension://',
79
+ 'chrome-untrusted://',
80
+ 'devtools://',
81
+ ];
75
82
  #pageLastActivity = new Map();
76
83
  #idleReaperTimer;
77
84
  #onIdleBrowserEmpty;
85
+ // Tracks when we first noticed only internal pages remaining.
86
+ #onlyInternalPagesSince;
78
87
  #uniqueBackendNodeIdToMcpId = new Map();
79
88
  constructor(browser, logger, options, locatorClass) {
80
89
  this.browser = browser;
@@ -114,6 +123,8 @@ export class McpContext {
114
123
  */
115
124
  touchPage(page) {
116
125
  this.#pageLastActivity.set(page, Date.now());
126
+ // A user is interacting with a page — reset the internal-only timer.
127
+ this.#onlyInternalPagesSince = undefined;
117
128
  }
118
129
  /**
119
130
  * Record activity on all currently tracked pages.
@@ -149,6 +160,10 @@ export class McpContext {
149
160
  this.#idleReaperTimer = undefined;
150
161
  }
151
162
  }
163
+ static #isInternalPage(page) {
164
+ const url = page.url();
165
+ return _a.#INTERNAL_URL_PREFIXES.some(prefix => url.startsWith(prefix));
166
+ }
152
167
  async #reapIdlePages() {
153
168
  const now = Date.now();
154
169
  const idleTimeout = _a.PAGE_IDLE_TIMEOUT_MS;
@@ -160,8 +175,21 @@ export class McpContext {
160
175
  catch {
161
176
  return; // Browser may be disconnected.
162
177
  }
178
+ // Also get ALL pages from the browser (including those filtered by
179
+ // createPagesSnapshot) so we can detect internal-only state.
180
+ let allBrowserPages;
181
+ try {
182
+ allBrowserPages = await this.browser.pages(true);
183
+ }
184
+ catch {
185
+ return;
186
+ }
163
187
  const pagesToClose = [];
164
188
  for (const page of pages) {
189
+ // Skip internal pages — they can't be closed and will respawn.
190
+ if (_a.#isInternalPage(page)) {
191
+ continue;
192
+ }
165
193
  const lastActivity = this.#pageLastActivity.get(page);
166
194
  // Pages without a recorded timestamp get one now (first seen).
167
195
  if (lastActivity === undefined) {
@@ -172,10 +200,7 @@ export class McpContext {
172
200
  pagesToClose.push(page);
173
201
  }
174
202
  }
175
- if (pagesToClose.length === 0) {
176
- return;
177
- }
178
- // Close idle pages. If ALL pages are idle, close them all.
203
+ // Close idle user pages.
179
204
  for (const page of pagesToClose) {
180
205
  try {
181
206
  this.logger(`Closing idle page ${this.#pageIdMap.get(page)}: ${page.url()} (idle ${Math.round((now - (this.#pageLastActivity.get(page) ?? now)) / 1000)}s)`);
@@ -186,16 +211,39 @@ export class McpContext {
186
211
  // Page may already be closed.
187
212
  }
188
213
  }
189
- // Refresh after closing.
214
+ // Re-check all browser pages to determine if only internal pages remain.
190
215
  try {
191
- pages = await this.createPagesSnapshot();
216
+ allBrowserPages = await this.browser.pages(true);
192
217
  }
193
218
  catch {
194
- pages = [];
219
+ allBrowserPages = [];
220
+ }
221
+ const hasUserPages = allBrowserPages.some(p => !p.isClosed() && !_a.#isInternalPage(p));
222
+ if (hasUserPages) {
223
+ // User pages still exist — reset the internal-only timer.
224
+ this.#onlyInternalPagesSince = undefined;
225
+ return;
226
+ }
227
+ // Only internal pages (or no pages) remain.
228
+ if (!this.#onIdleBrowserEmpty) {
229
+ return;
195
230
  }
196
- if (pages.length === 0 && this.#onIdleBrowserEmpty) {
231
+ if (allBrowserPages.length === 0) {
232
+ // Truly empty — kill immediately.
197
233
  this.logger('All pages closed by idle reaper, triggering browser cleanup');
198
234
  this.#onIdleBrowserEmpty();
235
+ return;
236
+ }
237
+ // Internal pages only — start or check the grace timer.
238
+ if (this.#onlyInternalPagesSince === undefined) {
239
+ this.#onlyInternalPagesSince = now;
240
+ this.logger(`Only internal pages remain (${allBrowserPages.length}), starting ${idleTimeout / 1000}s grace period`);
241
+ return;
242
+ }
243
+ if (now - this.#onlyInternalPagesSince > idleTimeout) {
244
+ this.logger(`Only internal pages for ${Math.round((now - this.#onlyInternalPagesSince) / 1000)}s, triggering browser cleanup`);
245
+ this.#onlyInternalPagesSince = undefined;
246
+ this.#onIdleBrowserEmpty();
199
247
  }
200
248
  }
201
249
  static async from(browser, logger, opts,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nimbus21.ai/chrome-devtools-mcp",
3
- "version": "0.17.6",
3
+ "version": "0.17.7",
4
4
  "description": "MCP server for Chrome DevTools with stealth mode support",
5
5
  "type": "module",
6
6
  "bin": "./build/src/index.js",