@mcp-b/chrome-devtools-mcp 1.6.2 → 1.7.0-canary.20260214192802
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 +8 -8
- package/build/src/McpContext.js +139 -24
- package/build/src/cli.js +5 -4
- package/build/src/formatters/IssueFormatter.js +190 -0
- package/build/src/main.js +33 -0
- package/build/src/telemetry/clearcut-logger.js +102 -0
- package/build/src/telemetry/flag-utils.js +45 -0
- package/build/src/telemetry/metric-utils.js +14 -0
- package/build/src/telemetry/persistence.js +53 -0
- package/build/src/telemetry/types.js +33 -0
- package/build/src/telemetry/watchdog/clearcut-sender.js +201 -0
- package/build/src/telemetry/watchdog/main.js +127 -0
- package/build/src/telemetry/watchdog-client.js +60 -0
- package/build/src/third_party/devtools-formatter-worker.js +7 -0
- package/build/src/third_party/index.js +1 -1
- package/build/src/tools/WebMCPToolHub.js +10 -3
- package/build/src/tools/extensions.js +79 -0
- package/build/src/tools/webmcp.js +23 -1
- package/build/src/transports/WebMCPClientTransport.js +71 -23
- package/build/src/utils/ExtensionRegistry.js +35 -0
- package/build/src/utils/string.js +36 -0
- package/package.json +15 -15
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](./docs/tool-reference.md)
|
|
9
9
|
[](https://developer.chrome.com/docs/devtools/)
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
**[WebMCP Documentation](https://docs.mcp-b.ai)** | **[Quick Start](https://docs.mcp-b.ai/quickstart)** | **[Connecting Agents](https://docs.mcp-b.ai/connecting-agents)** | **[Chrome DevTools Quickstart](https://github.com/WebMCP-org/chrome-devtools-quickstart)**
|
|
12
12
|
|
|
13
13
|
**@mcp-b/chrome-devtools-mcp** lets AI coding agents like Claude, Gemini, Cursor, and Copilot control and inspect a live Chrome browser via the Model Context Protocol (MCP). Get performance insights, debug network requests, take screenshots, and interact with website-specific MCP tools through WebMCP integration.
|
|
14
14
|
|
|
@@ -48,13 +48,13 @@ This fork adds **WebMCP integration** - the ability to call MCP tools that are r
|
|
|
48
48
|
|
|
49
49
|
| Feature | Chrome DevTools MCP | @mcp-b/chrome-devtools-mcp |
|
|
50
50
|
|---------|--------------------|-----------------------------|
|
|
51
|
-
| Browser automation |
|
|
52
|
-
| Performance analysis |
|
|
53
|
-
| Network inspection |
|
|
54
|
-
| Screenshot/snapshot |
|
|
55
|
-
| **Call website MCP tools** |
|
|
56
|
-
| **List website MCP tools** |
|
|
57
|
-
| **AI-driven tool development** |
|
|
51
|
+
| Browser automation | Yes | Yes |
|
|
52
|
+
| Performance analysis | Yes | Yes |
|
|
53
|
+
| Network inspection | Yes | Yes |
|
|
54
|
+
| Screenshot/snapshot | Yes | Yes |
|
|
55
|
+
| **Call website MCP tools** | No | Yes |
|
|
56
|
+
| **List website MCP tools** | No | Yes |
|
|
57
|
+
| **AI-driven tool development** | No | Yes |
|
|
58
58
|
|
|
59
59
|
The key addition is automatic WebMCP tool discovery and registration. When you visit a page with [@mcp-b/global](https://www.npmjs.com/package/@mcp-b/global), its tools are automatically registered as first-class MCP tools that your AI agent can call directly.
|
|
60
60
|
|
package/build/src/McpContext.js
CHANGED
|
@@ -167,8 +167,10 @@ export class McpContext {
|
|
|
167
167
|
}
|
|
168
168
|
await page.evaluate(WEB_MCP_BRIDGE_SCRIPT);
|
|
169
169
|
}
|
|
170
|
-
catch {
|
|
171
|
-
// Page might not be ready or accessible
|
|
170
|
+
catch (err) {
|
|
171
|
+
// Page might not be ready or accessible - log for debugging
|
|
172
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
173
|
+
this.logger(`Bridge injection skipped for ${page.url()}: ${message}`);
|
|
172
174
|
}
|
|
173
175
|
}
|
|
174
176
|
this.logger('WebMCP bridge injected into existing pages');
|
|
@@ -191,7 +193,10 @@ export class McpContext {
|
|
|
191
193
|
this.#toolHub = hub;
|
|
192
194
|
// Trigger auto-detection for all existing pages asynchronously
|
|
193
195
|
this.#setupWebMCPAutoDetectionForAllPages().catch(err => {
|
|
194
|
-
|
|
196
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
197
|
+
this.logger(`WebMCP auto-detection setup failed: ${message}`);
|
|
198
|
+
// Note: This is non-fatal - individual page connections may still work
|
|
199
|
+
// when explicitly requested via getWebMCPClient()
|
|
195
200
|
});
|
|
196
201
|
}
|
|
197
202
|
/**
|
|
@@ -202,11 +207,23 @@ export class McpContext {
|
|
|
202
207
|
}
|
|
203
208
|
/**
|
|
204
209
|
* Get or create a browser-level CDP session for window operations.
|
|
210
|
+
* Recreates the session if it has become invalid.
|
|
205
211
|
*/
|
|
206
212
|
async #getBrowserCdpSession() {
|
|
207
|
-
if (
|
|
208
|
-
|
|
213
|
+
if (this.#browserCdpSession) {
|
|
214
|
+
// Verify session is still valid by attempting a simple operation
|
|
215
|
+
try {
|
|
216
|
+
await this.#browserCdpSession.send('Browser.getVersion');
|
|
217
|
+
return this.#browserCdpSession;
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
// Session is stale, recreate it
|
|
221
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
222
|
+
this.logger(`Browser CDP session stale (${message}), recreating...`);
|
|
223
|
+
this.#browserCdpSession = undefined;
|
|
224
|
+
}
|
|
209
225
|
}
|
|
226
|
+
this.#browserCdpSession = await this.browser.target().createCDPSession();
|
|
210
227
|
return this.#browserCdpSession;
|
|
211
228
|
}
|
|
212
229
|
/**
|
|
@@ -235,6 +252,44 @@ export class McpContext {
|
|
|
235
252
|
getSessionWindowId() {
|
|
236
253
|
return this.#sessionWindowId;
|
|
237
254
|
}
|
|
255
|
+
/**
|
|
256
|
+
* Close all pages in the session's window.
|
|
257
|
+
* Called during cleanup when the MCP server is shutting down.
|
|
258
|
+
*/
|
|
259
|
+
async closeSessionWindow() {
|
|
260
|
+
if (this.#sessionWindowId === undefined) {
|
|
261
|
+
this.logger('No session window to close');
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
this.logger(`Closing session window ${this.#sessionWindowId}...`);
|
|
265
|
+
// Get all pages in this session's window and close them
|
|
266
|
+
const pagesToClose = [...this.#pages];
|
|
267
|
+
for (const page of pagesToClose) {
|
|
268
|
+
try {
|
|
269
|
+
if (!page.isClosed()) {
|
|
270
|
+
await page.close();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
// Ignore errors during cleanup - page might already be closed
|
|
275
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
276
|
+
this.logger(`Error closing page during cleanup: ${message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
// Clean up the CDP session
|
|
280
|
+
if (this.#browserCdpSession) {
|
|
281
|
+
try {
|
|
282
|
+
await this.#browserCdpSession.detach();
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
// Detach errors during cleanup are expected - log for debugging
|
|
286
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
287
|
+
this.logger(`CDP detach during cleanup: ${message}`);
|
|
288
|
+
}
|
|
289
|
+
this.#browserCdpSession = undefined;
|
|
290
|
+
}
|
|
291
|
+
this.logger('Session window closed');
|
|
292
|
+
}
|
|
238
293
|
/**
|
|
239
294
|
* Set up automatic WebMCP detection for a page.
|
|
240
295
|
* This installs listeners that detect WebMCP after navigation and sync tools.
|
|
@@ -270,7 +325,17 @@ export class McpContext {
|
|
|
270
325
|
// Immediately try to connect - no polling needed
|
|
271
326
|
// If no WebMCP, connection will timeout gracefully
|
|
272
327
|
this.#tryConnectWebMCP(page).catch(err => {
|
|
273
|
-
|
|
328
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
329
|
+
// Differentiate expected timeouts from unexpected errors
|
|
330
|
+
const isExpected = message.includes('timeout') ||
|
|
331
|
+
message.includes('WebMCP not detected') ||
|
|
332
|
+
message.includes('server did not respond');
|
|
333
|
+
if (isExpected) {
|
|
334
|
+
this.logger(`No WebMCP detected after navigation: ${page.url()}`);
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
this.logger(`Unexpected WebMCP connection error for ${page.url()}: ${message}`);
|
|
338
|
+
}
|
|
274
339
|
});
|
|
275
340
|
};
|
|
276
341
|
page.on('framenavigated', onFrameNavigated);
|
|
@@ -324,7 +389,17 @@ export class McpContext {
|
|
|
324
389
|
this.#setupWebMCPAutoDetection(page);
|
|
325
390
|
// Try to connect immediately (don't await - run in parallel for all pages)
|
|
326
391
|
this.#tryConnectWebMCP(page).catch(err => {
|
|
327
|
-
|
|
392
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
393
|
+
// Differentiate expected timeouts from unexpected errors
|
|
394
|
+
const isExpected = message.includes('timeout') ||
|
|
395
|
+
message.includes('WebMCP not detected') ||
|
|
396
|
+
message.includes('server did not respond');
|
|
397
|
+
if (isExpected) {
|
|
398
|
+
this.logger(`No WebMCP on page during initial scan: ${page.url()}`);
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
this.logger(`Unexpected error during initial WebMCP scan for ${page.url()}: ${message}`);
|
|
402
|
+
}
|
|
328
403
|
});
|
|
329
404
|
}
|
|
330
405
|
}
|
|
@@ -412,8 +487,10 @@ export class McpContext {
|
|
|
412
487
|
await existingPage.bringToFront();
|
|
413
488
|
}
|
|
414
489
|
}
|
|
415
|
-
catch {
|
|
490
|
+
catch (err) {
|
|
416
491
|
// Best effort - focus might fail if page is closing
|
|
492
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
493
|
+
this.logger(`Window focus failed (non-critical): ${message}`);
|
|
417
494
|
}
|
|
418
495
|
}
|
|
419
496
|
const page = await this.browser.newPage();
|
|
@@ -427,8 +504,10 @@ export class McpContext {
|
|
|
427
504
|
`Tab may not be visible in list_pages.`);
|
|
428
505
|
}
|
|
429
506
|
}
|
|
430
|
-
catch {
|
|
507
|
+
catch (err) {
|
|
431
508
|
// Failed to get windowId - page might be in an unexpected state
|
|
509
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
510
|
+
this.logger(`Window ID check failed (non-critical): ${message}`);
|
|
432
511
|
}
|
|
433
512
|
}
|
|
434
513
|
await this.createPagesSnapshot();
|
|
@@ -631,6 +710,10 @@ export class McpContext {
|
|
|
631
710
|
*/
|
|
632
711
|
async createPagesSnapshot() {
|
|
633
712
|
const allPages = await this.browser.pages(this.#options.experimentalIncludeAllPages);
|
|
713
|
+
this.logger(`createPagesSnapshot: found ${allPages.length} total pages`);
|
|
714
|
+
for (const page of allPages) {
|
|
715
|
+
this.logger(` - ${page.url()}`);
|
|
716
|
+
}
|
|
634
717
|
// First filter: DevTools pages (unless experimental mode is enabled)
|
|
635
718
|
let filteredPages = allPages.filter(page => {
|
|
636
719
|
return (this.#options.experimentalDevToolsDebugging ||
|
|
@@ -642,23 +725,50 @@ export class McpContext {
|
|
|
642
725
|
const windowFilteredPages = [];
|
|
643
726
|
// Check window IDs in parallel for better performance
|
|
644
727
|
const windowIdResults = await Promise.allSettled(filteredPages.map(async (page) => {
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
728
|
+
// Try to get windowId with retry for pages that might be in transitional state
|
|
729
|
+
let windowId = null;
|
|
730
|
+
let lastError = null;
|
|
731
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
732
|
+
try {
|
|
733
|
+
windowId = await this.getWindowIdForPage(page);
|
|
734
|
+
break; // Success, exit retry loop
|
|
735
|
+
}
|
|
736
|
+
catch (err) {
|
|
737
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
738
|
+
if (attempt < 2) {
|
|
739
|
+
// Wait before retry (50ms, then 100ms)
|
|
740
|
+
await new Promise(r => setTimeout(r, 50 * (attempt + 1)));
|
|
741
|
+
}
|
|
742
|
+
}
|
|
648
743
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
return null;
|
|
744
|
+
if (windowId !== null) {
|
|
745
|
+
return { page, windowId, url: page.url() };
|
|
652
746
|
}
|
|
747
|
+
// All retries failed
|
|
748
|
+
this.logger(`Failed to get windowId for page ${page.url()} after 3 attempts: ${lastError}`);
|
|
749
|
+
return null;
|
|
653
750
|
}));
|
|
654
751
|
for (const result of windowIdResults) {
|
|
655
|
-
if (result.status === 'fulfilled' &&
|
|
656
|
-
result.value
|
|
657
|
-
|
|
658
|
-
|
|
752
|
+
if (result.status === 'fulfilled' && result.value) {
|
|
753
|
+
if (result.value.windowId === this.#sessionWindowId) {
|
|
754
|
+
windowFilteredPages.push(result.value.page);
|
|
755
|
+
}
|
|
756
|
+
else {
|
|
757
|
+
this.logger(`Excluding page ${result.value.url} (windowId ${result.value.windowId} != session ${this.#sessionWindowId})`);
|
|
758
|
+
}
|
|
659
759
|
}
|
|
660
760
|
}
|
|
661
761
|
filteredPages = windowFilteredPages;
|
|
762
|
+
this.logger(`Window filter: ${windowFilteredPages.length} pages in session window ${this.#sessionWindowId}`);
|
|
763
|
+
// Always include the explicitly selected page even if window filtering excluded it
|
|
764
|
+
// This handles edge cases where windowId lookup fails after cross-origin navigation
|
|
765
|
+
if (this.#pageExplicitlySelected &&
|
|
766
|
+
this.#selectedPage &&
|
|
767
|
+
!this.#selectedPage.isClosed() &&
|
|
768
|
+
!filteredPages.includes(this.#selectedPage)) {
|
|
769
|
+
this.logger(`Re-adding explicitly selected page that was filtered out: ${this.#selectedPage.url()}`);
|
|
770
|
+
filteredPages.unshift(this.#selectedPage);
|
|
771
|
+
}
|
|
662
772
|
}
|
|
663
773
|
this.#pages = filteredPages;
|
|
664
774
|
// Only auto-select pages[0] if:
|
|
@@ -709,7 +819,8 @@ export class McpContext {
|
|
|
709
819
|
}
|
|
710
820
|
}
|
|
711
821
|
catch (error) {
|
|
712
|
-
|
|
822
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
823
|
+
this.logger(`DevTools detection failed for ${devToolsPage.url()}: ${message}`);
|
|
713
824
|
}
|
|
714
825
|
}
|
|
715
826
|
}
|
|
@@ -805,7 +916,8 @@ export class McpContext {
|
|
|
805
916
|
try {
|
|
806
917
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'chrome-devtools-mcp-'));
|
|
807
918
|
const filename = path.join(dir, `screenshot.${getExtensionFromMimeType(mimeType)}`);
|
|
808
|
-
|
|
919
|
+
// Use mode 0o600 (owner read/write only) for secure temp file creation
|
|
920
|
+
await fs.writeFile(filename, data, { mode: 0o600 });
|
|
809
921
|
return { filename };
|
|
810
922
|
}
|
|
811
923
|
catch (err) {
|
|
@@ -816,7 +928,8 @@ export class McpContext {
|
|
|
816
928
|
async saveFile(data, filename) {
|
|
817
929
|
try {
|
|
818
930
|
const filePath = path.resolve(filename);
|
|
819
|
-
|
|
931
|
+
// Use mode 0o644 (owner read/write, others read) for user-specified paths
|
|
932
|
+
await fs.writeFile(filePath, data, { mode: 0o644 });
|
|
820
933
|
return { filename };
|
|
821
934
|
}
|
|
822
935
|
catch (err) {
|
|
@@ -879,8 +992,10 @@ export class McpContext {
|
|
|
879
992
|
try {
|
|
880
993
|
await conn.client.close();
|
|
881
994
|
}
|
|
882
|
-
catch {
|
|
883
|
-
//
|
|
995
|
+
catch (err) {
|
|
996
|
+
// Close errors during reconnection are expected - log for debugging
|
|
997
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
998
|
+
this.logger(`WebMCP client close during reconnection: ${message}`);
|
|
884
999
|
}
|
|
885
1000
|
this.#webMCPConnections.delete(targetPage);
|
|
886
1001
|
}
|
package/build/src/cli.js
CHANGED
|
@@ -170,10 +170,11 @@ export function parseArguments(version, argv = process.argv) {
|
|
|
170
170
|
.check(args => {
|
|
171
171
|
// We can't set default in the options else
|
|
172
172
|
// Yargs will complain
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
173
|
+
// Note: Use explicit undefined checks since empty strings are valid falsy values
|
|
174
|
+
if (args.channel === undefined &&
|
|
175
|
+
args.browserUrl === undefined &&
|
|
176
|
+
args.wsEndpoint === undefined &&
|
|
177
|
+
args.executablePath === undefined) {
|
|
177
178
|
args.channel = 'dev';
|
|
178
179
|
}
|
|
179
180
|
return true;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import { ISSUE_UTILS } from '../issue-descriptions.js';
|
|
7
|
+
import { logger } from '../logger.js';
|
|
8
|
+
import { DevTools } from '../third_party/index.js';
|
|
9
|
+
export class IssueFormatter {
|
|
10
|
+
#issue;
|
|
11
|
+
#options;
|
|
12
|
+
constructor(issue, options) {
|
|
13
|
+
this.#issue = issue;
|
|
14
|
+
this.#options = options;
|
|
15
|
+
}
|
|
16
|
+
toString() {
|
|
17
|
+
const title = this.#getTitle();
|
|
18
|
+
const count = this.#issue.getAggregatedIssuesCount();
|
|
19
|
+
const idPart = this.#options.id !== undefined ? `msgid=${this.#options.id} ` : '';
|
|
20
|
+
return `${idPart}[issue] ${title} (count: ${count})`;
|
|
21
|
+
}
|
|
22
|
+
toStringDetailed() {
|
|
23
|
+
const result = [];
|
|
24
|
+
if (this.#options.id !== undefined) {
|
|
25
|
+
result.push(`ID: ${this.#options.id}`);
|
|
26
|
+
}
|
|
27
|
+
const bodyParts = [];
|
|
28
|
+
const description = this.#getDescription();
|
|
29
|
+
let processedMarkdown = description?.trim();
|
|
30
|
+
// Remove heading in order not to conflict with the whole console message response markdown
|
|
31
|
+
if (processedMarkdown?.startsWith('# ')) {
|
|
32
|
+
processedMarkdown = processedMarkdown.substring(2).trimStart();
|
|
33
|
+
}
|
|
34
|
+
if (processedMarkdown) {
|
|
35
|
+
bodyParts.push(processedMarkdown);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
bodyParts.push(this.#getTitle() ?? 'Unknown Issue');
|
|
39
|
+
}
|
|
40
|
+
const links = this.#issue.getDescription()?.links;
|
|
41
|
+
if (links && links.length > 0) {
|
|
42
|
+
bodyParts.push('Learn more:');
|
|
43
|
+
for (const link of links) {
|
|
44
|
+
bodyParts.push(`[${link.linkTitle}](${link.link})`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const affectedResources = this.#getAffectedResources();
|
|
48
|
+
if (affectedResources.length) {
|
|
49
|
+
bodyParts.push('### Affected resources');
|
|
50
|
+
bodyParts.push(...affectedResources.map(item => {
|
|
51
|
+
const details = [];
|
|
52
|
+
if (item.uid) {
|
|
53
|
+
details.push(`uid=${item.uid}`);
|
|
54
|
+
}
|
|
55
|
+
if (item.request) {
|
|
56
|
+
details.push((typeof item.request === 'number' ? `reqid=` : 'url=') +
|
|
57
|
+
item.request);
|
|
58
|
+
}
|
|
59
|
+
if (item.data) {
|
|
60
|
+
details.push(`data=${JSON.stringify(item.data)}`);
|
|
61
|
+
}
|
|
62
|
+
return details.join(' ');
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
result.push(`Message: issue> ${bodyParts.join('\n')}`);
|
|
66
|
+
return result.join('\n');
|
|
67
|
+
}
|
|
68
|
+
toJSON() {
|
|
69
|
+
return {
|
|
70
|
+
type: 'issue',
|
|
71
|
+
title: this.#getTitle(),
|
|
72
|
+
count: this.#issue.getAggregatedIssuesCount(),
|
|
73
|
+
id: this.#options.id,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
toJSONDetailed() {
|
|
77
|
+
return {
|
|
78
|
+
id: this.#options.id,
|
|
79
|
+
type: 'issue',
|
|
80
|
+
title: this.#getTitle(),
|
|
81
|
+
description: this.#getDescription(),
|
|
82
|
+
links: this.#issue.getDescription()?.links,
|
|
83
|
+
affectedResources: this.#getAffectedResources(),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
#getAffectedResources() {
|
|
87
|
+
const issues = this.#issue.getAllIssues();
|
|
88
|
+
const affectedResources = [];
|
|
89
|
+
for (const singleIssue of issues) {
|
|
90
|
+
const details = singleIssue.details();
|
|
91
|
+
if (!details) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
// We send the remaining details as untyped JSON because the DevTools
|
|
95
|
+
// frontend code is currently not re-usable.
|
|
96
|
+
const data = structuredClone(details);
|
|
97
|
+
let uid;
|
|
98
|
+
let request;
|
|
99
|
+
if ('violatingNodeId' in details &&
|
|
100
|
+
details.violatingNodeId &&
|
|
101
|
+
this.#options.elementIdResolver) {
|
|
102
|
+
uid = this.#options.elementIdResolver(details.violatingNodeId);
|
|
103
|
+
delete data.violatingNodeId;
|
|
104
|
+
}
|
|
105
|
+
if ('nodeId' in details &&
|
|
106
|
+
details.nodeId &&
|
|
107
|
+
this.#options.elementIdResolver) {
|
|
108
|
+
uid = this.#options.elementIdResolver(details.nodeId);
|
|
109
|
+
delete data.nodeId;
|
|
110
|
+
}
|
|
111
|
+
if ('documentNodeId' in details &&
|
|
112
|
+
details.documentNodeId &&
|
|
113
|
+
this.#options.elementIdResolver) {
|
|
114
|
+
uid = this.#options.elementIdResolver(details.documentNodeId);
|
|
115
|
+
delete data.documentNodeId;
|
|
116
|
+
}
|
|
117
|
+
if ('request' in details && details.request) {
|
|
118
|
+
request = details.request.url;
|
|
119
|
+
if (details.request.requestId && this.#options.requestIdResolver) {
|
|
120
|
+
const resolvedId = this.#options.requestIdResolver(details.request.requestId);
|
|
121
|
+
if (resolvedId) {
|
|
122
|
+
request = resolvedId;
|
|
123
|
+
const requestData = data.request;
|
|
124
|
+
delete requestData.requestId;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// These fields has no use for the MCP client (redundant or irrelevant).
|
|
129
|
+
delete data.errorType;
|
|
130
|
+
delete data.frameId;
|
|
131
|
+
affectedResources.push({
|
|
132
|
+
uid,
|
|
133
|
+
data: data,
|
|
134
|
+
request,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
return affectedResources;
|
|
138
|
+
}
|
|
139
|
+
isValid() {
|
|
140
|
+
return this.#getTitle() !== undefined;
|
|
141
|
+
}
|
|
142
|
+
// Helper to extract title
|
|
143
|
+
#getTitle() {
|
|
144
|
+
const markdownDescription = this.#issue.getDescription();
|
|
145
|
+
const filename = markdownDescription?.file;
|
|
146
|
+
if (!filename) {
|
|
147
|
+
logger(`no description found for issue:` + this.#issue.code());
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
// We already have the description logic in #getDescription, but title extraction is separate
|
|
151
|
+
// We can reuse the logic or cache it.
|
|
152
|
+
// Ideally we should process markdown once.
|
|
153
|
+
const rawMarkdown = ISSUE_UTILS.getIssueDescription(filename);
|
|
154
|
+
if (!rawMarkdown) {
|
|
155
|
+
logger(`no markdown ${filename} found for issue:` + this.#issue.code());
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
const processedMarkdown = DevTools.MarkdownIssueDescription.substitutePlaceholders(rawMarkdown, markdownDescription?.substitutions);
|
|
160
|
+
const markdownAst = DevTools.Marked.Marked.lexer(processedMarkdown);
|
|
161
|
+
const title = DevTools.MarkdownIssueDescription.findTitleFromMarkdownAst(markdownAst);
|
|
162
|
+
if (!title) {
|
|
163
|
+
logger('cannot read issue title from ' + filename);
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
return title;
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
logger('error parsing markdown for issue ' + this.#issue.code());
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
#getDescription() {
|
|
174
|
+
const markdownDescription = this.#issue.getDescription();
|
|
175
|
+
const filename = markdownDescription?.file;
|
|
176
|
+
if (!filename) {
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
const rawMarkdown = ISSUE_UTILS.getIssueDescription(filename);
|
|
180
|
+
if (!rawMarkdown) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
return DevTools.MarkdownIssueDescription.substitutePlaceholders(rawMarkdown, markdownDescription?.substitutions);
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
package/build/src/main.js
CHANGED
|
@@ -27,6 +27,39 @@ const VERSION = '1.5.6';
|
|
|
27
27
|
process.on('unhandledRejection', (reason, promise) => {
|
|
28
28
|
logger('Unhandled promise rejection', promise, reason);
|
|
29
29
|
});
|
|
30
|
+
/**
|
|
31
|
+
* Cleanup handler for graceful shutdown.
|
|
32
|
+
* Closes the session window when the MCP server is killed.
|
|
33
|
+
*/
|
|
34
|
+
let isCleaningUp = false;
|
|
35
|
+
async function cleanup() {
|
|
36
|
+
if (isCleaningUp)
|
|
37
|
+
return;
|
|
38
|
+
isCleaningUp = true;
|
|
39
|
+
logger('Shutting down MCP server...');
|
|
40
|
+
if (context) {
|
|
41
|
+
try {
|
|
42
|
+
await context.closeSessionWindow();
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
logger('Error during cleanup:', err);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
process.exit(0);
|
|
49
|
+
}
|
|
50
|
+
// Handle various termination signals
|
|
51
|
+
process.on('SIGINT', () => {
|
|
52
|
+
logger('Received SIGINT');
|
|
53
|
+
cleanup();
|
|
54
|
+
});
|
|
55
|
+
process.on('SIGTERM', () => {
|
|
56
|
+
logger('Received SIGTERM');
|
|
57
|
+
cleanup();
|
|
58
|
+
});
|
|
59
|
+
process.on('SIGHUP', () => {
|
|
60
|
+
logger('Received SIGHUP');
|
|
61
|
+
cleanup();
|
|
62
|
+
});
|
|
30
63
|
export const args = parseArguments(VERSION);
|
|
31
64
|
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
|
|
32
65
|
logger(`Starting Chrome DevTools MCP Server v${VERSION}`);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Google LLC
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
import process from 'node:process';
|
|
7
|
+
import { logger } from '../logger.js';
|
|
8
|
+
import { FilePersistence } from './persistence.js';
|
|
9
|
+
import { WatchdogMessageType, OsType } from './types.js';
|
|
10
|
+
import { WatchdogClient } from './watchdog-client.js';
|
|
11
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
12
|
+
function detectOsType() {
|
|
13
|
+
switch (process.platform) {
|
|
14
|
+
case 'win32':
|
|
15
|
+
return OsType.OS_TYPE_WINDOWS;
|
|
16
|
+
case 'darwin':
|
|
17
|
+
return OsType.OS_TYPE_MACOS;
|
|
18
|
+
case 'linux':
|
|
19
|
+
return OsType.OS_TYPE_LINUX;
|
|
20
|
+
default:
|
|
21
|
+
return OsType.OS_TYPE_UNSPECIFIED;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export class ClearcutLogger {
|
|
25
|
+
#persistence;
|
|
26
|
+
#watchdog;
|
|
27
|
+
constructor(options) {
|
|
28
|
+
this.#persistence = options.persistence ?? new FilePersistence();
|
|
29
|
+
this.#watchdog =
|
|
30
|
+
options.watchdogClient ??
|
|
31
|
+
new WatchdogClient({
|
|
32
|
+
parentPid: process.pid,
|
|
33
|
+
appVersion: options.appVersion,
|
|
34
|
+
osType: detectOsType(),
|
|
35
|
+
logFile: options.logFile,
|
|
36
|
+
clearcutEndpoint: options.clearcutEndpoint,
|
|
37
|
+
clearcutForceFlushIntervalMs: options.clearcutForceFlushIntervalMs,
|
|
38
|
+
clearcutIncludePidHeader: options.clearcutIncludePidHeader,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
async logToolInvocation(args) {
|
|
42
|
+
this.#watchdog.send({
|
|
43
|
+
type: WatchdogMessageType.LOG_EVENT,
|
|
44
|
+
payload: {
|
|
45
|
+
tool_invocation: {
|
|
46
|
+
tool_name: args.toolName,
|
|
47
|
+
success: args.success,
|
|
48
|
+
latency_ms: args.latencyMs,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async logServerStart(flagUsage) {
|
|
54
|
+
this.#watchdog.send({
|
|
55
|
+
type: WatchdogMessageType.LOG_EVENT,
|
|
56
|
+
payload: {
|
|
57
|
+
server_start: {
|
|
58
|
+
flag_usage: flagUsage,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
async logDailyActiveIfNeeded() {
|
|
64
|
+
try {
|
|
65
|
+
const state = await this.#persistence.loadState();
|
|
66
|
+
if (this.#shouldLogDailyActive(state)) {
|
|
67
|
+
let daysSince = -1;
|
|
68
|
+
if (state.lastActive) {
|
|
69
|
+
const lastActiveDate = new Date(state.lastActive);
|
|
70
|
+
const now = new Date();
|
|
71
|
+
const diffTime = Math.abs(now.getTime() - lastActiveDate.getTime());
|
|
72
|
+
daysSince = Math.ceil(diffTime / MS_PER_DAY);
|
|
73
|
+
}
|
|
74
|
+
this.#watchdog.send({
|
|
75
|
+
type: WatchdogMessageType.LOG_EVENT,
|
|
76
|
+
payload: {
|
|
77
|
+
daily_active: {
|
|
78
|
+
days_since_last_active: daysSince,
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
state.lastActive = new Date().toISOString();
|
|
83
|
+
await this.#persistence.saveState(state);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
logger('Error in logDailyActiveIfNeeded:', err);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
#shouldLogDailyActive(state) {
|
|
91
|
+
if (!state.lastActive) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
const lastActiveDate = new Date(state.lastActive);
|
|
95
|
+
const now = new Date();
|
|
96
|
+
// Compare UTC dates
|
|
97
|
+
const isSameDay = lastActiveDate.getUTCFullYear() === now.getUTCFullYear() &&
|
|
98
|
+
lastActiveDate.getUTCMonth() === now.getUTCMonth() &&
|
|
99
|
+
lastActiveDate.getUTCDate() === now.getUTCDate();
|
|
100
|
+
return !isSameDay;
|
|
101
|
+
}
|
|
102
|
+
}
|