@mcp-b/chrome-devtools-mcp 1.6.3 → 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 CHANGED
@@ -8,7 +8,7 @@
8
8
  [![27 Tools](https://img.shields.io/badge/MCP_Tools-27-green?style=flat-square)](./docs/tool-reference.md)
9
9
  [![Chrome](https://img.shields.io/badge/Chrome-DevTools-4285F4?style=flat-square&logo=googlechrome)](https://developer.chrome.com/docs/devtools/)
10
10
 
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)**
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
 
@@ -916,7 +916,8 @@ export class McpContext {
916
916
  try {
917
917
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'chrome-devtools-mcp-'));
918
918
  const filename = path.join(dir, `screenshot.${getExtensionFromMimeType(mimeType)}`);
919
- await fs.writeFile(filename, data);
919
+ // Use mode 0o600 (owner read/write only) for secure temp file creation
920
+ await fs.writeFile(filename, data, { mode: 0o600 });
920
921
  return { filename };
921
922
  }
922
923
  catch (err) {
@@ -927,7 +928,8 @@ export class McpContext {
927
928
  async saveFile(data, filename) {
928
929
  try {
929
930
  const filePath = path.resolve(filename);
930
- await fs.writeFile(filePath, data);
931
+ // Use mode 0o644 (owner read/write, others read) for user-specified paths
932
+ await fs.writeFile(filePath, data, { mode: 0o644 });
931
933
  return { filename };
932
934
  }
933
935
  catch (err) {
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
- if (!args.channel &&
174
- !args.browserUrl &&
175
- !args.wsEndpoint &&
176
- !args.executablePath) {
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
+ }
@@ -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
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { toSnakeCase } from '../utils/string.js';
7
+ /**
8
+ * Computes telemetry flag usage from parsed arguments and CLI options.
9
+ *
10
+ * Iterates over the defined CLI options to construct a payload:
11
+ * - Flag names are converted to snake_case (e.g. `browserUrl` -> `browser_url`).
12
+ * - A flag is logged as `{flag_name}_present` if:
13
+ * - It has no default value, OR
14
+ * - The provided value differs from the default value.
15
+ * - Boolean flags are logged with their literal value.
16
+ * - String flags with defined `choices` (Enums) are logged as their uppercase value.
17
+ */
18
+ export function computeFlagUsage(args, options) {
19
+ const usage = {};
20
+ for (const [flagName, config] of Object.entries(options)) {
21
+ const value = args[flagName];
22
+ const snakeCaseName = toSnakeCase(flagName);
23
+ // If there isn't a default value provided for the flag,
24
+ // we're going to log whether it's present on the args user
25
+ // provided or not. If there is a default value, we only log presence
26
+ // if the value differs from the default, implying explicit user intent.
27
+ if (!('default' in config) || value !== config.default) {
28
+ usage[`${snakeCaseName}_present`] = value !== undefined && value !== null;
29
+ }
30
+ if (config.type === 'boolean' && typeof value === 'boolean') {
31
+ // For boolean options, we're going to log the value directly.
32
+ usage[snakeCaseName] = value;
33
+ }
34
+ else if (config.type === 'string' &&
35
+ typeof value === 'string' &&
36
+ 'choices' in config &&
37
+ config.choices) {
38
+ // For enums, log the value as uppercase
39
+ // We're going to have an enum for such flags with choices represented
40
+ // as an `enum` where the keys of the enum will map to the uppercase `choice`.
41
+ usage[snakeCaseName] = `${snakeCaseName}_${value}`.toUpperCase();
42
+ }
43
+ }
44
+ return usage;
45
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ const LATENCY_BUCKETS = [50, 100, 250, 500, 1000, 2500, 5000, 10000];
7
+ export function bucketizeLatency(latencyMs) {
8
+ for (const bucket of LATENCY_BUCKETS) {
9
+ if (latencyMs <= bucket) {
10
+ return bucket;
11
+ }
12
+ }
13
+ return LATENCY_BUCKETS[LATENCY_BUCKETS.length - 1];
14
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import fs from 'node:fs/promises';
7
+ import os from 'node:os';
8
+ import path from 'node:path';
9
+ import process from 'node:process';
10
+ import { logger } from '../logger.js';
11
+ const STATE_FILE_NAME = 'telemetry_state.json';
12
+ function getDataFolder() {
13
+ const homedir = os.homedir();
14
+ const { env } = process;
15
+ const name = 'chrome-devtools-mcp';
16
+ if (process.platform === 'darwin') {
17
+ return path.join(homedir, 'Library', 'Application Support', name);
18
+ }
19
+ if (process.platform === 'win32') {
20
+ const localAppData = env.LOCALAPPDATA || path.join(homedir, 'AppData', 'Local');
21
+ return path.join(localAppData, name, 'Data');
22
+ }
23
+ return path.join(env.XDG_DATA_HOME || path.join(homedir, '.local', 'share'), name);
24
+ }
25
+ export class FilePersistence {
26
+ #dataFolder;
27
+ constructor(dataFolderOverride) {
28
+ this.#dataFolder = dataFolderOverride ?? getDataFolder();
29
+ }
30
+ async loadState() {
31
+ try {
32
+ const filePath = path.join(this.#dataFolder, STATE_FILE_NAME);
33
+ const content = await fs.readFile(filePath, 'utf-8');
34
+ return JSON.parse(content);
35
+ }
36
+ catch {
37
+ return {
38
+ lastActive: '',
39
+ };
40
+ }
41
+ }
42
+ async saveState(state) {
43
+ const filePath = path.join(this.#dataFolder, STATE_FILE_NAME);
44
+ try {
45
+ await fs.mkdir(this.#dataFolder, { recursive: true });
46
+ await fs.writeFile(filePath, JSON.stringify(state, null, 2), 'utf-8');
47
+ }
48
+ catch (error) {
49
+ // Ignore errors during state saving to avoid crashing the server
50
+ logger(`Failed to save telemetry state to ${filePath}:`, error);
51
+ }
52
+ }
53
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ // Enums
7
+ export var OsType;
8
+ (function (OsType) {
9
+ OsType[OsType["OS_TYPE_UNSPECIFIED"] = 0] = "OS_TYPE_UNSPECIFIED";
10
+ OsType[OsType["OS_TYPE_WINDOWS"] = 1] = "OS_TYPE_WINDOWS";
11
+ OsType[OsType["OS_TYPE_MACOS"] = 2] = "OS_TYPE_MACOS";
12
+ OsType[OsType["OS_TYPE_LINUX"] = 3] = "OS_TYPE_LINUX";
13
+ })(OsType || (OsType = {}));
14
+ export var ChromeChannel;
15
+ (function (ChromeChannel) {
16
+ ChromeChannel[ChromeChannel["CHROME_CHANNEL_UNSPECIFIED"] = 0] = "CHROME_CHANNEL_UNSPECIFIED";
17
+ ChromeChannel[ChromeChannel["CHROME_CHANNEL_CANARY"] = 1] = "CHROME_CHANNEL_CANARY";
18
+ ChromeChannel[ChromeChannel["CHROME_CHANNEL_DEV"] = 2] = "CHROME_CHANNEL_DEV";
19
+ ChromeChannel[ChromeChannel["CHROME_CHANNEL_BETA"] = 3] = "CHROME_CHANNEL_BETA";
20
+ ChromeChannel[ChromeChannel["CHROME_CHANNEL_STABLE"] = 4] = "CHROME_CHANNEL_STABLE";
21
+ })(ChromeChannel || (ChromeChannel = {}));
22
+ export var McpClient;
23
+ (function (McpClient) {
24
+ McpClient[McpClient["MCP_CLIENT_UNSPECIFIED"] = 0] = "MCP_CLIENT_UNSPECIFIED";
25
+ McpClient[McpClient["MCP_CLIENT_CLAUDE_CODE"] = 1] = "MCP_CLIENT_CLAUDE_CODE";
26
+ McpClient[McpClient["MCP_CLIENT_GEMINI_CLI"] = 2] = "MCP_CLIENT_GEMINI_CLI";
27
+ })(McpClient || (McpClient = {}));
28
+ // IPC types for messages between the main process and the
29
+ // telemetry watchdog process.
30
+ export var WatchdogMessageType;
31
+ (function (WatchdogMessageType) {
32
+ WatchdogMessageType["LOG_EVENT"] = "log-event";
33
+ })(WatchdogMessageType || (WatchdogMessageType = {}));
@@ -0,0 +1,201 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import crypto from 'node:crypto';
7
+ import { logger } from '../../logger.js';
8
+ const MAX_BUFFER_SIZE = 1000;
9
+ const DEFAULT_CLEARCUT_ENDPOINT = 'https://play.googleapis.com/log?format=json_proto';
10
+ const DEFAULT_FLUSH_INTERVAL_MS = 15 * 60 * 1000;
11
+ const LOG_SOURCE = 2839;
12
+ const CLIENT_TYPE = 47;
13
+ const MIN_RATE_LIMIT_WAIT_MS = 30_000;
14
+ const REQUEST_TIMEOUT_MS = 30_000;
15
+ const SHUTDOWN_TIMEOUT_MS = 5_000;
16
+ const SESSION_ROTATION_INTERVAL_MS = 24 * 60 * 60 * 1000;
17
+ export class ClearcutSender {
18
+ #appVersion;
19
+ #osType;
20
+ #clearcutEndpoint;
21
+ #flushIntervalMs;
22
+ #includePidHeader;
23
+ #sessionId;
24
+ #sessionCreated;
25
+ #buffer = [];
26
+ #flushTimer = null;
27
+ #isFlushing = false;
28
+ #timerStarted = false;
29
+ constructor(config) {
30
+ this.#appVersion = config.appVersion;
31
+ this.#osType = config.osType;
32
+ this.#clearcutEndpoint =
33
+ config.clearcutEndpoint ?? DEFAULT_CLEARCUT_ENDPOINT;
34
+ this.#flushIntervalMs =
35
+ config.forceFlushIntervalMs ?? DEFAULT_FLUSH_INTERVAL_MS;
36
+ this.#includePidHeader = config.includePidHeader ?? false;
37
+ this.#sessionId = crypto.randomUUID();
38
+ this.#sessionCreated = Date.now();
39
+ }
40
+ enqueueEvent(event) {
41
+ if (Date.now() - this.#sessionCreated > SESSION_ROTATION_INTERVAL_MS) {
42
+ this.#sessionId = crypto.randomUUID();
43
+ this.#sessionCreated = Date.now();
44
+ }
45
+ logger('Enqueing telemetry event', JSON.stringify(event, null, 2));
46
+ this.#addToBuffer({
47
+ ...event,
48
+ session_id: this.#sessionId,
49
+ app_version: this.#appVersion,
50
+ os_type: this.#osType,
51
+ });
52
+ if (!this.#timerStarted) {
53
+ this.#timerStarted = true;
54
+ this.#scheduleFlush(this.#flushIntervalMs);
55
+ }
56
+ }
57
+ async sendShutdownEvent() {
58
+ if (this.#flushTimer) {
59
+ clearTimeout(this.#flushTimer);
60
+ this.#flushTimer = null;
61
+ }
62
+ const shutdownEvent = {
63
+ server_shutdown: {},
64
+ };
65
+ this.enqueueEvent(shutdownEvent);
66
+ try {
67
+ await Promise.race([
68
+ this.#finalFlush(),
69
+ new Promise(resolve => setTimeout(resolve, SHUTDOWN_TIMEOUT_MS)),
70
+ ]);
71
+ logger('Final flush completed');
72
+ }
73
+ catch (error) {
74
+ logger('Final flush failed:', error);
75
+ }
76
+ }
77
+ async #flush() {
78
+ if (this.#isFlushing) {
79
+ return;
80
+ }
81
+ if (this.#buffer.length === 0) {
82
+ this.#scheduleFlush(this.#flushIntervalMs);
83
+ return;
84
+ }
85
+ this.#isFlushing = true;
86
+ let nextDelayMs = this.#flushIntervalMs;
87
+ // Optimistically remove events from buffer before sending.
88
+ // This prevents race conditions where a simultaneous #finalFlush would include these same events.
89
+ const eventsToSend = [...this.#buffer];
90
+ this.#buffer = [];
91
+ try {
92
+ const result = await this.#sendBatch(eventsToSend);
93
+ if (result.success) {
94
+ if (result.nextRequestWaitMs !== undefined) {
95
+ nextDelayMs = Math.max(result.nextRequestWaitMs, MIN_RATE_LIMIT_WAIT_MS);
96
+ }
97
+ }
98
+ else if (result.isPermanentError) {
99
+ logger('Permanent error, dropped batch of', eventsToSend.length, 'events');
100
+ }
101
+ else {
102
+ // Transient error: Requeue events at the front of the buffer
103
+ // to maintain order and retry them later.
104
+ this.#buffer = [...eventsToSend, ...this.#buffer];
105
+ }
106
+ }
107
+ catch (error) {
108
+ // Safety catch for unexpected errors, requeue events
109
+ this.#buffer = [...eventsToSend, ...this.#buffer];
110
+ logger('Flush failed unexpectedly:', error);
111
+ }
112
+ finally {
113
+ this.#isFlushing = false;
114
+ this.#scheduleFlush(nextDelayMs);
115
+ }
116
+ }
117
+ #addToBuffer(event) {
118
+ if (this.#buffer.length >= MAX_BUFFER_SIZE) {
119
+ this.#buffer.shift();
120
+ logger('Telemetry buffer overflow: dropped oldest event');
121
+ }
122
+ this.#buffer.push({
123
+ event,
124
+ timestamp: Date.now(),
125
+ });
126
+ }
127
+ #scheduleFlush(delayMs) {
128
+ if (this.#flushTimer) {
129
+ clearTimeout(this.#flushTimer);
130
+ }
131
+ this.#flushTimer = setTimeout(() => {
132
+ this.#flush().catch(err => {
133
+ logger('Flush error:', err);
134
+ });
135
+ }, delayMs);
136
+ }
137
+ async #sendBatch(events) {
138
+ const requestBody = {
139
+ log_source: LOG_SOURCE,
140
+ request_time_ms: Date.now().toString(),
141
+ client_info: {
142
+ client_type: CLIENT_TYPE,
143
+ },
144
+ log_event: events.map(({ event, timestamp }) => ({
145
+ event_time_ms: timestamp.toString(),
146
+ source_extension_json: JSON.stringify(event),
147
+ })),
148
+ };
149
+ const controller = new AbortController();
150
+ const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
151
+ try {
152
+ const response = await fetch(this.#clearcutEndpoint, {
153
+ method: 'POST',
154
+ headers: {
155
+ 'Content-Type': 'application/json',
156
+ // Used in E2E tests to confirm that the watchdog process is killed
157
+ ...(this.#includePidHeader
158
+ ? { 'X-Watchdog-Pid': process.pid.toString() }
159
+ : {}),
160
+ },
161
+ body: JSON.stringify(requestBody),
162
+ signal: controller.signal,
163
+ });
164
+ clearTimeout(timeoutId);
165
+ if (response.ok) {
166
+ const data = (await response.json());
167
+ return {
168
+ success: true,
169
+ nextRequestWaitMs: data.next_request_wait_millis,
170
+ };
171
+ }
172
+ const status = response.status;
173
+ if (status >= 500 || status === 429) {
174
+ return { success: false };
175
+ }
176
+ logger('Telemetry permanent error:', status);
177
+ return { success: false, isPermanentError: true };
178
+ }
179
+ catch {
180
+ clearTimeout(timeoutId);
181
+ return { success: false };
182
+ }
183
+ }
184
+ async #finalFlush() {
185
+ if (this.#buffer.length === 0) {
186
+ return;
187
+ }
188
+ const eventsToSend = [...this.#buffer];
189
+ await this.#sendBatch(eventsToSend);
190
+ }
191
+ stopForTesting() {
192
+ if (this.#flushTimer) {
193
+ clearTimeout(this.#flushTimer);
194
+ this.#flushTimer = null;
195
+ }
196
+ this.#timerStarted = false;
197
+ }
198
+ get bufferSizeForTesting() {
199
+ return this.#buffer.length;
200
+ }
201
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import process from 'node:process';
7
+ import readline from 'node:readline';
8
+ import { parseArgs } from 'node:util';
9
+ import { logger, flushLogs, saveLogsToFile } from '../../logger.js';
10
+ import { WatchdogMessageType } from '../types.js';
11
+ import { ClearcutSender } from './clearcut-sender.js';
12
+ function parseWatchdogArgs() {
13
+ const { values } = parseArgs({
14
+ options: {
15
+ 'parent-pid': { type: 'string' },
16
+ 'app-version': { type: 'string' },
17
+ 'os-type': { type: 'string' },
18
+ 'log-file': { type: 'string' },
19
+ 'clearcut-endpoint': { type: 'string' },
20
+ 'clearcut-force-flush-interval-ms': { type: 'string' },
21
+ 'clearcut-include-pid-header': { type: 'boolean' },
22
+ },
23
+ strict: true,
24
+ });
25
+ // Verify required arguments
26
+ const parentPid = parseInt(values['parent-pid'] ?? '', 10);
27
+ const appVersion = values['app-version'];
28
+ const osType = parseInt(values['os-type'] ?? '', 10);
29
+ if (isNaN(parentPid) || !appVersion || isNaN(osType)) {
30
+ console.error('Invalid arguments provided for watchdog process: ', JSON.stringify({ parentPid, appVersion, osType }));
31
+ process.exit(1);
32
+ }
33
+ // Parse Optional Arguments
34
+ const logFile = values['log-file'];
35
+ const clearcutEndpoint = values['clearcut-endpoint'];
36
+ const clearcutIncludePidHeader = values['clearcut-include-pid-header'];
37
+ let clearcutForceFlushIntervalMs;
38
+ if (values['clearcut-force-flush-interval-ms']) {
39
+ const parsed = parseInt(values['clearcut-force-flush-interval-ms'], 10);
40
+ if (!isNaN(parsed)) {
41
+ clearcutForceFlushIntervalMs = parsed;
42
+ }
43
+ }
44
+ return {
45
+ parentPid,
46
+ appVersion,
47
+ osType,
48
+ logFile,
49
+ clearcutEndpoint,
50
+ clearcutForceFlushIntervalMs,
51
+ clearcutIncludePidHeader,
52
+ };
53
+ }
54
+ function main() {
55
+ const { parentPid, appVersion, osType, logFile, clearcutEndpoint, clearcutForceFlushIntervalMs, clearcutIncludePidHeader, } = parseWatchdogArgs();
56
+ let logStream;
57
+ if (logFile) {
58
+ logStream = saveLogsToFile(logFile);
59
+ }
60
+ const exit = (code) => {
61
+ if (!logStream) {
62
+ process.exit(code);
63
+ }
64
+ void flushLogs(logStream).finally(() => {
65
+ process.exit(code);
66
+ });
67
+ };
68
+ logger('Watchdog started', JSON.stringify({
69
+ pid: process.pid,
70
+ parentPid,
71
+ version: appVersion,
72
+ osType,
73
+ }, null, 2));
74
+ const sender = new ClearcutSender({
75
+ appVersion,
76
+ osType: osType,
77
+ clearcutEndpoint,
78
+ forceFlushIntervalMs: clearcutForceFlushIntervalMs,
79
+ includePidHeader: clearcutIncludePidHeader,
80
+ });
81
+ let isShuttingDown = false;
82
+ function onParentDeath(reason) {
83
+ if (isShuttingDown) {
84
+ return;
85
+ }
86
+ isShuttingDown = true;
87
+ logger(`Parent death detected (${reason}). Sending shutdown event...`);
88
+ sender
89
+ .sendShutdownEvent()
90
+ .then(() => {
91
+ logger('Shutdown event sent. Exiting.');
92
+ exit(0);
93
+ })
94
+ .catch(err => {
95
+ logger('Failed to send shutdown event', err);
96
+ exit(1);
97
+ });
98
+ }
99
+ process.stdin.on('end', () => onParentDeath('stdin end'));
100
+ process.stdin.on('close', () => onParentDeath('stdin close'));
101
+ process.on('disconnect', () => onParentDeath('ipc disconnect'));
102
+ const rl = readline.createInterface({
103
+ input: process.stdin,
104
+ terminal: false,
105
+ });
106
+ rl.on('line', line => {
107
+ try {
108
+ if (!line.trim()) {
109
+ return;
110
+ }
111
+ const msg = JSON.parse(line);
112
+ if (msg.type === WatchdogMessageType.LOG_EVENT && msg.payload) {
113
+ sender.enqueueEvent(msg.payload);
114
+ }
115
+ }
116
+ catch (err) {
117
+ logger('Failed to parse IPC message', err);
118
+ }
119
+ });
120
+ }
121
+ try {
122
+ main();
123
+ }
124
+ catch (err) {
125
+ console.error('Watchdog fatal error:', err);
126
+ process.exit(1);
127
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { spawn } from 'node:child_process';
7
+ import { fileURLToPath } from 'node:url';
8
+ import { logger } from '../logger.js';
9
+ export class WatchdogClient {
10
+ #childProcess;
11
+ constructor(config, options) {
12
+ const watchdogPath = fileURLToPath(new URL('./watchdog/main.js', import.meta.url));
13
+ const args = [
14
+ watchdogPath,
15
+ `--parent-pid=${config.parentPid}`,
16
+ `--app-version=${config.appVersion}`,
17
+ `--os-type=${config.osType}`,
18
+ ];
19
+ if (config.logFile) {
20
+ args.push(`--log-file=${config.logFile}`);
21
+ }
22
+ if (config.clearcutEndpoint) {
23
+ args.push(`--clearcut-endpoint=${config.clearcutEndpoint}`);
24
+ }
25
+ if (config.clearcutForceFlushIntervalMs) {
26
+ args.push(`--clearcut-force-flush-interval-ms=${config.clearcutForceFlushIntervalMs}`);
27
+ }
28
+ if (config.clearcutIncludePidHeader) {
29
+ args.push('--clearcut-include-pid-header');
30
+ }
31
+ const spawner = options?.spawn ?? spawn;
32
+ this.#childProcess = spawner(process.execPath, args, {
33
+ stdio: ['pipe', 'ignore', 'ignore'],
34
+ detached: true,
35
+ });
36
+ this.#childProcess.unref();
37
+ this.#childProcess.on('error', err => {
38
+ logger('Watchdog process error:', err);
39
+ });
40
+ this.#childProcess.on('exit', (code, signal) => {
41
+ logger(`Watchdog exited with code ${code} and signal ${signal}`);
42
+ });
43
+ }
44
+ send(message) {
45
+ if (this.#childProcess.stdin &&
46
+ !this.#childProcess.stdin.destroyed &&
47
+ this.#childProcess.pid) {
48
+ try {
49
+ const line = JSON.stringify(message) + '\n';
50
+ this.#childProcess.stdin.write(line);
51
+ }
52
+ catch (err) {
53
+ logger('Failed to write to watchdog stdin', err);
54
+ }
55
+ }
56
+ else {
57
+ logger('Watchdog stdin not available, dropping message');
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ // eslint-disable-next-line no-restricted-imports
7
+ import '../../vendor/chrome-devtools-frontend/front_end/entrypoints/formatter_worker/formatter_worker-entrypoint.js';
@@ -11,6 +11,6 @@ export { default as debug } from 'debug';
11
11
  export { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
12
  export { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
13
  export { SetLevelRequestSchema, ToolListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
14
- export { z as zod } from 'zod/v4';
14
+ export { z as zod } from 'zod';
15
15
  export { Locator, PredefinedNetworkConditions, CDPSessionEvent, } from 'puppeteer-core';
16
16
  export { default as puppeteer } from 'puppeteer-core';
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { zod } from '../third_party/index.js';
7
+ import { ToolCategory } from './categories.js';
8
+ import { defineTool } from './ToolDefinition.js';
9
+ const EXTENSIONS_CONDITION = 'experimentalExtensionSupport';
10
+ export const installExtension = defineTool({
11
+ name: 'install_extension',
12
+ description: 'Installs a Chrome extension from the given path.',
13
+ annotations: {
14
+ category: ToolCategory.EXTENSIONS,
15
+ readOnlyHint: false,
16
+ conditions: [EXTENSIONS_CONDITION],
17
+ },
18
+ schema: {
19
+ path: zod
20
+ .string()
21
+ .describe('Absolute path to the unpacked extension folder.'),
22
+ },
23
+ handler: async (request, response, context) => {
24
+ const { path } = request.params;
25
+ const id = await context.installExtension(path);
26
+ response.appendResponseLine(`Extension installed. Id: ${id}`);
27
+ },
28
+ });
29
+ export const uninstallExtension = defineTool({
30
+ name: 'uninstall_extension',
31
+ description: 'Uninstalls a Chrome extension by its ID.',
32
+ annotations: {
33
+ category: ToolCategory.EXTENSIONS,
34
+ readOnlyHint: false,
35
+ conditions: [EXTENSIONS_CONDITION],
36
+ },
37
+ schema: {
38
+ id: zod.string().describe('ID of the extension to uninstall.'),
39
+ },
40
+ handler: async (request, response, context) => {
41
+ const { id } = request.params;
42
+ await context.uninstallExtension(id);
43
+ response.appendResponseLine(`Extension uninstalled. Id: ${id}`);
44
+ },
45
+ });
46
+ export const listExtensions = defineTool({
47
+ name: 'list_extensions',
48
+ description: 'Lists all extensions via this server, including their name, ID, version, and enabled status.',
49
+ annotations: {
50
+ category: ToolCategory.EXTENSIONS,
51
+ readOnlyHint: true,
52
+ conditions: [EXTENSIONS_CONDITION],
53
+ },
54
+ schema: {},
55
+ handler: async (_request, response, _context) => {
56
+ response.setListExtensions();
57
+ },
58
+ });
59
+ export const reloadExtension = defineTool({
60
+ name: 'reload_extension',
61
+ description: 'Reloads an unpacked Chrome extension by its ID.',
62
+ annotations: {
63
+ category: ToolCategory.EXTENSIONS,
64
+ readOnlyHint: false,
65
+ conditions: [EXTENSIONS_CONDITION],
66
+ },
67
+ schema: {
68
+ id: zod.string().describe('ID of the extension to reload.'),
69
+ },
70
+ handler: async (request, response, context) => {
71
+ const { id } = request.params;
72
+ const extension = context.getExtension(id);
73
+ if (!extension) {
74
+ throw new Error(`Extension with ID ${id} not found.`);
75
+ }
76
+ await context.installExtension(extension.path);
77
+ response.appendResponseLine('Extension reloaded.');
78
+ },
79
+ });
@@ -0,0 +1,35 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import fs from 'node:fs/promises';
7
+ import path from 'node:path';
8
+ export class ExtensionRegistry {
9
+ #extensions = new Map();
10
+ async registerExtension(id, extensionPath) {
11
+ const manifestPath = path.join(extensionPath, 'manifest.json');
12
+ const manifestContent = await fs.readFile(manifestPath, 'utf-8');
13
+ const manifest = JSON.parse(manifestContent);
14
+ const name = manifest.name ?? 'Unknown';
15
+ const version = manifest.version ?? 'Unknown';
16
+ const extension = {
17
+ id,
18
+ name,
19
+ version,
20
+ isEnabled: true,
21
+ path: extensionPath,
22
+ };
23
+ this.#extensions.set(extension.id, extension);
24
+ return extension;
25
+ }
26
+ remove(id) {
27
+ this.#extensions.delete(id);
28
+ }
29
+ list() {
30
+ return Array.from(this.#extensions.values());
31
+ }
32
+ getById(id) {
33
+ return this.#extensions.get(id);
34
+ }
35
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ /**
7
+ * Converts a given string to snake_case.
8
+ * This function handles camelCase, PascalCase, and acronyms, including transitions between letters and numbers.
9
+ * It uses Unicode-aware regular expressions (`\p{L}`, `\p{N}`, `\p{Lu}`, `\p{Ll}` with the `u` flag)
10
+ * to correctly process letters and numbers from various languages.
11
+ *
12
+ * @param text The input string to convert to snake_case.
13
+ * @returns The snake_case version of the input string.
14
+ */
15
+ export function toSnakeCase(text) {
16
+ if (!text) {
17
+ return '';
18
+ }
19
+ // First, handle case-based transformations to insert underscores correctly.
20
+ // 1. Add underscore between a letter and a number.
21
+ // e.g., "version2" -> "version_2"
22
+ // 2. Add underscore between an uppercase letter sequence and a following uppercase+lowercase sequence.
23
+ // e.g., "APIFlags" -> "API_Flags"
24
+ // 3. Add underscore between a lowercase/number and an uppercase letter.
25
+ // e.g., "lastName" -> "last_Name", "version_2Update" -> "version_2_Update"
26
+ // 4. Replace sequences of non-alphanumeric with a single underscore
27
+ // 5. Remove any leading or trailing underscores.
28
+ const result = text
29
+ .replace(/(\p{L})(\p{N})/gu, '$1_$2') // 1
30
+ .replace(/(\p{Lu}+)(\p{Lu}\p{Ll})/gu, '$1_$2') // 2
31
+ .replace(/(\p{Ll}|\p{N})(\p{Lu})/gu, '$1_$2') // 3
32
+ .toLowerCase()
33
+ .replace(/[^\p{L}\p{N}]+/gu, '_') // 4
34
+ .replace(/^_|_$/g, ''); // 5
35
+ return result;
36
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mcp-b/chrome-devtools-mcp",
3
- "version": "1.6.3",
3
+ "version": "1.7.0-canary.20260214192802",
4
4
  "description": "MCP server for Chrome DevTools with WebMCP integration for connecting to website MCP tools",
5
5
  "keywords": [
6
6
  "mcp",
@@ -48,48 +48,48 @@
48
48
  "!*.tsbuildinfo"
49
49
  ],
50
50
  "dependencies": {
51
- "@composio/json-schema-to-zod": "^0.1.17",
52
- "@modelcontextprotocol/sdk": "1.24.3",
51
+ "@composio/json-schema-to-zod": "^0.1.19",
52
+ "@modelcontextprotocol/sdk": "1.25.2",
53
53
  "core-js": "3.47.0",
54
54
  "debug": "4.4.3",
55
- "esbuild": "^0.24.2",
55
+ "esbuild": "^0.27.2",
56
56
  "puppeteer": "24.32.0",
57
57
  "puppeteer-core": "24.32.0",
58
58
  "yargs": "18.0.0",
59
- "zod": "4.3.5"
59
+ "zod": "3.25.76"
60
60
  },
61
61
  "devDependencies": {
62
62
  "@eslint/js": "^9.35.0",
63
- "@modelcontextprotocol/sdk": "1.24.3",
63
+ "@modelcontextprotocol/sdk": "1.25.2",
64
64
  "@rollup/plugin-commonjs": "^29.0.0",
65
65
  "@rollup/plugin-json": "^6.1.0",
66
66
  "@rollup/plugin-node-resolve": "^16.0.3",
67
- "@stylistic/eslint-plugin": "^5.4.0",
67
+ "@stylistic/eslint-plugin": "^5.7.1",
68
68
  "@types/debug": "^4.1.12",
69
69
  "@types/filesystem": "^0.0.36",
70
70
  "@types/node": "^24.3.3",
71
71
  "@types/sinon": "^21.0.0",
72
72
  "@types/yargs": "^17.0.33",
73
- "@typescript-eslint/eslint-plugin": "^8.43.0",
73
+ "@typescript-eslint/eslint-plugin": "^8.53.1",
74
74
  "@typescript-eslint/parser": "^8.43.0",
75
75
  "chrome-devtools-frontend": "1.0.1550444",
76
76
  "core-js": "3.47.0",
77
77
  "debug": "4.4.3",
78
- "eslint": "^9.35.0",
78
+ "eslint": "^9.39.2",
79
79
  "eslint-import-resolver-typescript": "^4.4.4",
80
80
  "eslint-plugin-import": "^2.32.0",
81
81
  "globals": "^16.4.0",
82
- "prettier": "^3.6.2",
82
+ "prettier": "^3.8.1",
83
83
  "puppeteer": "24.32.0",
84
84
  "puppeteer-core": "24.32.0",
85
- "rollup": "4.53.3",
85
+ "rollup": "4.56.0",
86
86
  "rollup-plugin-cleanup": "^3.2.1",
87
87
  "rollup-plugin-license": "^3.6.0",
88
- "sinon": "^21.0.0",
88
+ "sinon": "^21.0.1",
89
89
  "typescript": "^5.9.2",
90
- "typescript-eslint": "^8.43.0",
90
+ "typescript-eslint": "^8.53.1",
91
91
  "yargs": "18.0.0",
92
- "zod": "4.3.5"
92
+ "zod": "3.25.76"
93
93
  },
94
94
  "engines": {
95
95
  "node": "^20.19.0 || ^22.12.0 || >=23"
@@ -103,7 +103,7 @@
103
103
  "mcpName": "io.github.ChromeDevTools/chrome-devtools-mcp"
104
104
  },
105
105
  "scripts": {
106
- "build": "tsc && node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/post-build.ts",
106
+ "build": "rm -rf build/vendor build/node_modules build/tsconfig.tsbuildinfo && tsc && node --experimental-strip-types --no-warnings=ExperimentalWarning scripts/post-build.ts",
107
107
  "bundle": "npm run clean && npm run build && rollup -c rollup.config.mjs",
108
108
  "check-format": "eslint --cache . && prettier --check --cache .;",
109
109
  "clean": "node -e \"require('fs').rmSync('build', {recursive: true, force: true})\"",