@mcp-b/chrome-devtools-mcp 1.7.0 → 1.7.1

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.
Files changed (146) hide show
  1. package/README.md +120 -10
  2. package/build/src/McpContext.js +50 -3
  3. package/build/src/browser.js +62 -6
  4. package/build/src/cli.js +6 -1
  5. package/build/src/formatters/IssueFormatter.js +190 -0
  6. package/build/src/main.js +83 -2
  7. package/build/src/telemetry/clearcut-logger.js +102 -0
  8. package/build/src/telemetry/flag-utils.js +45 -0
  9. package/build/src/telemetry/metric-utils.js +14 -0
  10. package/build/src/telemetry/persistence.js +53 -0
  11. package/build/src/telemetry/types.js +33 -0
  12. package/build/src/telemetry/watchdog/clearcut-sender.js +201 -0
  13. package/build/src/telemetry/watchdog/main.js +127 -0
  14. package/build/src/telemetry/watchdog-client.js +60 -0
  15. package/build/src/third_party/devtools-formatter-worker.js +7 -0
  16. package/build/src/tools/browser.js +92 -0
  17. package/build/src/tools/extension.js +31 -0
  18. package/build/src/tools/extensions.js +79 -0
  19. package/build/src/tools/input.js +6 -1
  20. package/build/src/tools/pages.js +0 -1
  21. package/build/src/tools/tools.js +4 -0
  22. package/build/src/transports/WebMCPBridgeScript.js +11 -2
  23. package/build/src/utils/ExtensionRegistry.js +35 -0
  24. package/build/src/utils/string.js +36 -0
  25. package/build/vendor/chrome-devtools-frontend/front_end/core/common/Base64.js +20 -2
  26. package/build/vendor/chrome-devtools-frontend/front_end/core/common/Debouncer.js +8 -1
  27. package/build/vendor/chrome-devtools-frontend/front_end/core/common/Gzip.js +11 -0
  28. package/build/vendor/chrome-devtools-frontend/front_end/core/common/Object.js +6 -1
  29. package/build/vendor/chrome-devtools-frontend/front_end/core/common/ParsedURL.js +3 -0
  30. package/build/vendor/chrome-devtools-frontend/front_end/core/common/ResourceType.js +6 -0
  31. package/build/vendor/chrome-devtools-frontend/front_end/core/common/Revealer.js +0 -5
  32. package/build/vendor/chrome-devtools-frontend/front_end/core/common/Settings.js +18 -8
  33. package/build/vendor/chrome-devtools-frontend/front_end/core/host/AidaClient.js +24 -0
  34. package/build/vendor/chrome-devtools-frontend/front_end/core/host/InspectorFrontendHostStub.js +11 -3
  35. package/build/vendor/chrome-devtools-frontend/front_end/core/host/ResourceLoader.js +1 -1
  36. package/build/vendor/chrome-devtools-frontend/front_end/core/host/UserMetrics.js +27 -20
  37. package/build/vendor/chrome-devtools-frontend/front_end/core/i18n/collect-ui-strings.js +7 -8
  38. package/build/vendor/chrome-devtools-frontend/front_end/core/i18n/generate-locales-js.js +4 -5
  39. package/build/vendor/chrome-devtools-frontend/front_end/core/platform/ArrayUtilities.js +10 -0
  40. package/build/vendor/chrome-devtools-frontend/front_end/core/platform/StringUtilities.js +63 -12
  41. package/build/vendor/chrome-devtools-frontend/front_end/core/protocol_client/CDPConnection.js +1 -0
  42. package/build/vendor/chrome-devtools-frontend/front_end/core/protocol_client/InspectorBackend.js +4 -1
  43. package/build/vendor/chrome-devtools-frontend/front_end/core/root/ExperimentNames.js +30 -0
  44. package/build/vendor/chrome-devtools-frontend/front_end/core/root/root.js +2 -1
  45. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/AnimationModel.js +0 -4
  46. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSMatchedStyles.js +69 -9
  47. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSMetadata.js +6 -6
  48. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSModel.js +28 -13
  49. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSProperty.js +1 -1
  50. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSPropertyParserMatchers.js +6 -0
  51. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/ConsoleModel.js +0 -2
  52. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CookieModel.js +1 -1
  53. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/DOMModel.js +170 -13
  54. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/DebuggerModel.js +5 -39
  55. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/HeapProfilerModel.js +8 -1
  56. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/NetworkManager.js +20 -5
  57. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/NetworkRequest.js +12 -21
  58. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/OverlayModel.js +19 -6
  59. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/RehydratingConnection.js +5 -1
  60. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/ResourceTreeModel.js +8 -5
  61. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/SourceMap.js +15 -10
  62. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/SourceMapManager.js +1 -1
  63. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/SourceMapScopesInfo.js +13 -27
  64. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/Target.js +3 -1
  65. package/build/vendor/chrome-devtools-frontend/front_end/generated/ARIAProperties.js +1 -7
  66. package/build/vendor/chrome-devtools-frontend/front_end/generated/Deprecation.js +1 -16
  67. package/build/vendor/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +82 -22
  68. package/build/vendor/chrome-devtools-frontend/front_end/generated/SupportedCSSProperties.js +265 -123
  69. package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.js +2 -1
  70. package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js +10 -16
  71. package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js +97 -26
  72. package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AICallTree.js +35 -0
  73. package/build/vendor/chrome-devtools-frontend/front_end/models/annotations/AnnotationRepository.js +163 -0
  74. package/build/vendor/chrome-devtools-frontend/front_end/models/annotations/AnnotationType.js +10 -0
  75. package/build/vendor/chrome-devtools-frontend/front_end/models/annotations/annotations.js +5 -0
  76. package/build/vendor/chrome-devtools-frontend/front_end/models/bindings/CompilerScriptMapping.js +5 -3
  77. package/build/vendor/chrome-devtools-frontend/front_end/models/bindings/DebuggerLanguagePlugins.js +29 -58
  78. package/build/vendor/chrome-devtools-frontend/front_end/models/bindings/DebuggerWorkspaceBinding.js +7 -45
  79. package/build/vendor/chrome-devtools-frontend/front_end/models/emulation/DeviceModeModel.js +1 -1
  80. package/build/vendor/chrome-devtools-frontend/front_end/models/emulation/EmulatedDevices.js +14 -0
  81. package/build/vendor/chrome-devtools-frontend/front_end/models/formatter/FormatterWorkerPool.js +8 -5
  82. package/build/vendor/chrome-devtools-frontend/front_end/models/greendev/Prototypes.js +33 -0
  83. package/build/vendor/chrome-devtools-frontend/front_end/models/greendev/greendev.js +4 -0
  84. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/ContrastCheckTrigger.js +2 -2
  85. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/CookieIssue.js +0 -21
  86. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/CorsIssue.js +1 -38
  87. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/IssueAggregator.js +8 -0
  88. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/IssuesManager.js +6 -12
  89. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/PermissionElementIssue.js +243 -0
  90. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementActivationDisabled.md +7 -0
  91. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementActivationDisabledWithOccluder.md +9 -0
  92. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementActivationDisabledWithOccluderParent.md +9 -0
  93. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementCspFrameAncestorsMissing.md +5 -0
  94. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementFencedFrameDisallowed.md +5 -0
  95. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementFontSizeTooLarge.md +5 -0
  96. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementFontSizeTooSmall.md +5 -0
  97. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementGeolocationDeprecated.md +5 -0
  98. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInsetBoxShadowUnsupported.md +5 -0
  99. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidDisplayStyle.md +5 -0
  100. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidSizeValue.md +5 -0
  101. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidType.md +5 -0
  102. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidTypeActivation.md +5 -0
  103. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementLowContrast.md +5 -0
  104. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementNonOpaqueColor.md +5 -0
  105. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementPaddingBottomUnsupported.md +6 -0
  106. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementPaddingRightUnsupported.md +6 -0
  107. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementPermissionsPolicyBlocked.md +5 -0
  108. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementRegistrationFailed.md +5 -0
  109. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementRequestInProgress.md +5 -0
  110. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementSecurityChecksFailed.md +5 -0
  111. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementTypeNotSupported.md +5 -0
  112. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementUntrustedEvent.md +7 -0
  113. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/issues_manager.js +2 -1
  114. package/build/vendor/chrome-devtools-frontend/front_end/models/logs/NetworkLog.js +0 -8
  115. package/build/vendor/chrome-devtools-frontend/front_end/models/source_map_scopes/NamesResolver.js +4 -8
  116. package/build/vendor/chrome-devtools-frontend/front_end/models/stack_trace/StackTrace.js +30 -1
  117. package/build/vendor/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceImpl.js +70 -1
  118. package/build/vendor/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceModel.js +82 -30
  119. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/EventsSerializer.js +10 -2
  120. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/LanternComputationData.js +2 -2
  121. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/ModelImpl.js +0 -3
  122. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/Processor.js +18 -19
  123. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/Styles.js +12 -4
  124. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/extras/Initiators.js +46 -0
  125. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/extras/TraceTree.js +4 -3
  126. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/extras/extras.js +1 -0
  127. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/LargestImagePaintHandler.js +2 -2
  128. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/LayoutShiftsHandler.js +1 -1
  129. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/MetaHandler.js +6 -0
  130. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/NetworkRequestsHandler.js +10 -1
  131. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/PageLoadMetricsHandler.js +44 -27
  132. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/helpers/Timing.js +9 -2
  133. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/Common.js +1 -6
  134. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/LCPBreakdown.js +2 -2
  135. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/LCPDiscovery.js +2 -4
  136. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/NetworkDependencyTree.js +3 -2
  137. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/RenderBlocking.js +1 -1
  138. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/types/TraceEvents.js +33 -11
  139. package/build/vendor/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec/package/src/decode/decode.js +51 -18
  140. package/build/vendor/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec/package/src/encode/encoder.js +1 -1
  141. package/build/vendor/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec/package/src/scopes.js +4 -0
  142. package/build/vendor/chrome-devtools-frontend/mcp/HostBindings.js +4 -0
  143. package/build/vendor/chrome-devtools-frontend/mcp/mcp.js +4 -0
  144. package/package.json +17 -10
  145. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/SameSiteInvalidSameParty.md +0 -8
  146. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/SameSiteSamePartyCrossPartyContextSet.md +0 -10
@@ -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';
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 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
+ export const connectToBrowser = defineTool({
10
+ name: 'connect_to_browser',
11
+ description: 'Connect to a different Chrome browser instance at runtime. ' +
12
+ 'Disconnects from the current browser (without closing it) and attaches to the new one. ' +
13
+ 'Useful for WXT extension development: run `wxt dev`, parse the debug URL from stdout, ' +
14
+ 'then call this tool to attach. Retries automatically if the browser is still starting up.',
15
+ annotations: {
16
+ title: 'Connect to Browser',
17
+ category: ToolCategory.NAVIGATION,
18
+ readOnlyHint: false,
19
+ },
20
+ schema: {
21
+ browserUrl: zod
22
+ .string()
23
+ .optional()
24
+ .describe('HTTP URL to the browser\'s DevTools endpoint, e.g. "http://127.0.0.1:9222". ' +
25
+ 'Exactly one of browserUrl or wsEndpoint is required.'),
26
+ wsEndpoint: zod
27
+ .string()
28
+ .optional()
29
+ .describe('WebSocket URL to connect to, e.g. "ws://127.0.0.1:9222/devtools/browser/...". ' +
30
+ 'Exactly one of browserUrl or wsEndpoint is required.'),
31
+ wsHeaders: zod
32
+ .record(zod.string())
33
+ .optional()
34
+ .describe('Optional HTTP headers for WebSocket connection (only used with wsEndpoint).'),
35
+ timeout: zod
36
+ .number()
37
+ .int()
38
+ .positive()
39
+ .optional()
40
+ .describe('Maximum time in milliseconds to wait for the browser to become available. ' +
41
+ 'Defaults to 30000 (30 seconds). The tool retries every 2 seconds within this window.'),
42
+ },
43
+ handler: async (request, response, context) => {
44
+ const { browserUrl, wsEndpoint, wsHeaders, timeout } = request.params;
45
+ // Validate: exactly one connection method required
46
+ if (!browserUrl && !wsEndpoint) {
47
+ throw new Error('Either browserUrl or wsEndpoint is required. ' +
48
+ 'Example: browserUrl "http://127.0.0.1:9222" or wsEndpoint "ws://127.0.0.1:9222/devtools/browser/..."');
49
+ }
50
+ if (browserUrl && wsEndpoint) {
51
+ throw new Error('Provide only one of browserUrl or wsEndpoint, not both.');
52
+ }
53
+ // Validate URL protocols
54
+ if (browserUrl && !browserUrl.startsWith('http://') && !browserUrl.startsWith('https://')) {
55
+ throw new Error(`Invalid browserUrl: "${browserUrl}". Must start with http:// or https://`);
56
+ }
57
+ if (wsEndpoint && !wsEndpoint.startsWith('ws://') && !wsEndpoint.startsWith('wss://')) {
58
+ throw new Error(`Invalid wsEndpoint: "${wsEndpoint}". Must start with ws:// or wss://`);
59
+ }
60
+ try {
61
+ const result = await context.reconnectBrowser({
62
+ browserURL: browserUrl,
63
+ wsEndpoint,
64
+ wsHeaders,
65
+ timeout,
66
+ });
67
+ response.appendResponseLine(`Connected to browser at ${wsEndpoint ?? browserUrl}`);
68
+ response.appendResponseLine(`WebSocket endpoint: ${result.wsEndpoint}`);
69
+ response.appendResponseLine('');
70
+ response.appendResponseLine('Pages:');
71
+ for (const page of result.pages) {
72
+ const marker = page.selected ? ' (selected)' : '';
73
+ response.appendResponseLine(` [${page.index}] ${page.url}${marker}`);
74
+ }
75
+ }
76
+ catch (err) {
77
+ response.setIsError(true);
78
+ const message = err instanceof Error ? err.message : String(err);
79
+ response.appendResponseLine(`Failed to connect: ${message}`);
80
+ response.appendResponseLine('');
81
+ response.appendResponseLine('Troubleshooting:');
82
+ response.appendResponseLine(' - Ensure Chrome was launched with --remote-debugging-port=<port>');
83
+ response.appendResponseLine(' - Verify the URL is reachable: curl ' + (browserUrl ?? wsEndpoint));
84
+ if (timeout) {
85
+ response.appendResponseLine(' - Try increasing the timeout (current: ' + timeout + 'ms)');
86
+ }
87
+ else {
88
+ response.appendResponseLine(' - Try setting a longer timeout (default is 30s)');
89
+ }
90
+ }
91
+ },
92
+ });
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 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
+ export const evaluateInExtensionWorker = defineTool({
10
+ name: 'evaluate_in_extension_worker',
11
+ description: 'Execute a JavaScript expression in a Chrome extension background service worker. ' +
12
+ 'Useful for inspecting extension state (e.g., McpHub connections, offscreen status) ' +
13
+ 'that is not accessible from page contexts.',
14
+ annotations: {
15
+ category: ToolCategory.DEBUGGING,
16
+ readOnlyHint: false,
17
+ },
18
+ schema: {
19
+ expression: zod
20
+ .string()
21
+ .describe('JavaScript expression to evaluate in the service worker.'),
22
+ extensionId: zod
23
+ .string()
24
+ .optional()
25
+ .describe('Extension ID to target. If omitted, the first extension service worker found is used.'),
26
+ },
27
+ handler: async (request, response, context) => {
28
+ const result = await context.evaluateInExtensionWorker(request.params.expression, request.params.extensionId);
29
+ response.appendResponseLine(JSON.stringify(result, null, 2));
30
+ },
31
+ });
@@ -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
+ });
@@ -103,11 +103,16 @@ async function selectOption(handle, aXNode, value) {
103
103
  throw new Error(`Could not find option with text "${value}"`);
104
104
  }
105
105
  }
106
+ function hasOptionChildren(aXNode) {
107
+ return aXNode.children.some(child => child.role === 'option');
108
+ }
106
109
  async function fillFormElement(uid, value, context) {
107
110
  const handle = await context.getElementByUid(uid);
108
111
  try {
109
112
  const aXNode = context.getAXNodeByUid(uid);
110
- if (aXNode && aXNode.role === 'combobox') {
113
+ // We assume that combobox needs to be handled as select if it has
114
+ // role='combobox' and option children.
115
+ if (aXNode && aXNode.role === 'combobox' && hasOptionChildren(aXNode)) {
111
116
  await selectOption(handle, aXNode, value);
112
117
  }
113
118
  else {
@@ -185,7 +185,6 @@ export const resizePage = defineTool({
185
185
  },
186
186
  handler: async (request, response, context) => {
187
187
  const page = context.getSelectedPage();
188
- // @ts-expect-error internal API for now.
189
188
  await page.resize({
190
189
  contentWidth: request.params.width,
191
190
  contentHeight: request.params.height,
@@ -3,8 +3,10 @@
3
3
  * Copyright 2025 Google LLC
4
4
  * SPDX-License-Identifier: Apache-2.0
5
5
  */
6
+ import * as browserTools from './browser.js';
6
7
  import * as consoleTools from './console.js';
7
8
  import * as emulationTools from './emulation.js';
9
+ import * as extensionTools from './extension.js';
8
10
  import * as inputTools from './input.js';
9
11
  import * as networkTools from './network.js';
10
12
  import * as pagesTools from './pages.js';
@@ -14,8 +16,10 @@ import * as scriptTools from './script.js';
14
16
  import * as snapshotTools from './snapshot.js';
15
17
  import * as webmcpTools from './webmcp.js';
16
18
  const tools = [
19
+ ...Object.values(browserTools),
17
20
  ...Object.values(consoleTools),
18
21
  ...Object.values(emulationTools),
22
+ ...Object.values(extensionTools),
19
23
  ...Object.values(inputTools),
20
24
  ...Object.values(networkTools),
21
25
  ...Object.values(pagesTools),
@@ -100,6 +100,15 @@ export const WEB_MCP_BRIDGE_SCRIPT = `
100
100
  return false;
101
101
  }
102
102
 
103
+ function getTargetOrigin() {
104
+ var origin = window.location && typeof window.location.origin === 'string'
105
+ ? window.location.origin
106
+ : '';
107
+ // about:blank, file://, and sandboxed contexts expose "null" origin.
108
+ // Use "*" so postMessage remains functional in those contexts.
109
+ return origin && origin !== 'null' ? origin : '*';
110
+ }
111
+
103
112
  // Initial check
104
113
  checkWebMCPAvailable();
105
114
 
@@ -125,7 +134,7 @@ export const WEB_MCP_BRIDGE_SCRIPT = `
125
134
  type: 'mcp',
126
135
  direction: 'client-to-server',
127
136
  payload: payload
128
- }, window.location.origin);
137
+ }, getTargetOrigin());
129
138
 
130
139
  return true;
131
140
  } catch (err) {
@@ -169,7 +178,7 @@ export const WEB_MCP_BRIDGE_SCRIPT = `
169
178
  type: 'mcp',
170
179
  direction: 'client-to-server',
171
180
  payload: 'mcp-check-ready'
172
- }, window.location.origin);
181
+ }, getTargetOrigin());
173
182
  },
174
183
 
175
184
  /**
@@ -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
+ }
@@ -29,13 +29,31 @@ export function decode(input) {
29
29
  }
30
30
  return bytes;
31
31
  }
32
+ /**
33
+ * Note: if input can be very large (larger than the max string size), callers should
34
+ * expect this to throw an error.
35
+ */
32
36
  export function encode(input) {
33
37
  return new Promise((resolve, reject) => {
34
38
  const reader = new FileReader();
35
- reader.onerror = () => reject(new Error('failed to convert to base64'));
39
+ reader.onerror = () => reject(new Error('failed to convert to base64: internal error'));
36
40
  reader.onload = () => {
41
+ // The input was too large to encode as a string. The caller should anticipate
42
+ // this and use a workaround. See TimelinePanel.ts innerSaveToFile for an example.
43
+ // For more information, see crbug.com/436482118.
44
+ if (reader.result === '') {
45
+ reject(new Error('failed to convert to base64: input too large to encode as base64 string'));
46
+ return;
47
+ }
48
+ // This string can be very large, so take care to not double memory. `split`
49
+ // was used here before, which always results in new strings in V8. By using
50
+ // slice instead, we leverage the sliced string optimization in V8 and avoid
51
+ // doubling the memory requirement (even if temporarily: that is a potential
52
+ // source of OOM crashes given large enough input, such as is common with
53
+ // Performance traces).
37
54
  const blobAsUrl = reader.result;
38
- const [, base64] = blobAsUrl.split(',', 2);
55
+ const index = blobAsUrl.indexOf(',');
56
+ const base64 = blobAsUrl.slice(index + 1);
39
57
  resolve(base64);
40
58
  };
41
59
  reader.readAsDataURL(new Blob([input]));
@@ -8,7 +8,14 @@ export const debounce = function (func, delay) {
8
8
  let timer;
9
9
  const debounced = (...args) => {
10
10
  clearTimeout(timer);
11
- timer = setTimeout(() => func(...args), delay);
11
+ timer = setTimeout(() => func(...args), testDebounceOverride ? 0 : delay);
12
12
  };
13
13
  return debounced;
14
14
  };
15
+ let testDebounceOverride = false;
16
+ export function enableTestOverride() {
17
+ testDebounceOverride = true;
18
+ }
19
+ export function disableTestOverride() {
20
+ testDebounceOverride = false;
21
+ }
@@ -64,3 +64,14 @@ export function compressStream(stream) {
64
64
  const cs = new CompressionStream('gzip');
65
65
  return stream.pipeThrough(cs);
66
66
  }
67
+ export function createMonitoredStream(stream, onProgress) {
68
+ let bytesRead = 0;
69
+ const progressTransformer = new TransformStream({
70
+ transform(chunk, controller) {
71
+ bytesRead += chunk.byteLength;
72
+ onProgress(bytesRead);
73
+ controller.enqueue(chunk);
74
+ }
75
+ });
76
+ return stream.pipeThrough(progressTransformer);
77
+ }
@@ -57,7 +57,12 @@ export class ObjectWrapper {
57
57
  // new listeners.
58
58
  for (const listener of [...listeners]) {
59
59
  if (!listener.disposed) {
60
- listener.listener.call(listener.thisObject, event);
60
+ try {
61
+ listener.listener.call(listener.thisObject, event);
62
+ }
63
+ catch (err) {
64
+ console.error(`Event listener for ${String(eventType)} throw an error:`, err);
65
+ }
61
66
  }
62
67
  }
63
68
  }
@@ -263,6 +263,9 @@ export class ParsedURL {
263
263
  return '';
264
264
  }
265
265
  static extractName(url) {
266
+ if (url.endsWith('/')) {
267
+ url = url.slice(0, -1);
268
+ }
266
269
  let index = url.lastIndexOf('/');
267
270
  const pathAndQuery = index !== -1 ? url.substr(index + 1) : url;
268
271
  index = pathAndQuery.indexOf('?');
@@ -229,6 +229,12 @@ export class ResourceType {
229
229
  const regex = new RegExp('^application(.*json$|\/json\+.*)');
230
230
  return regex.test(contentType) ? 'application/json' : contentType;
231
231
  }
232
+ /**
233
+ * Checks whether the given MIME type represents JavaScript content.
234
+ */
235
+ static isJavaScriptMimeType(mimeType) {
236
+ return mimeType === 'application/javascript' || mimeType === 'text/javascript';
237
+ }
232
238
  /**
233
239
  * Adds suffixes iff the mimeType is 'text/javascript' to denote whether the JS is minified or from
234
240
  * a source map.