@mcp-b/chrome-devtools-mcp 2.0.2 → 2.0.5

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 (149) hide show
  1. package/README.md +120 -10
  2. package/build/src/McpContext.js +69 -5
  3. package/build/src/browser.js +157 -51
  4. package/build/src/cli.js +11 -5
  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/third_party/index.js +1 -1
  17. package/build/src/tools/browser.js +92 -0
  18. package/build/src/tools/extension.js +31 -0
  19. package/build/src/tools/extensions.js +79 -0
  20. package/build/src/tools/input.js +6 -1
  21. package/build/src/tools/pages.js +7 -1
  22. package/build/src/tools/script.js +31 -4
  23. package/build/src/tools/tools.js +4 -0
  24. package/build/src/transports/CDPClientTransport.js +184 -0
  25. package/build/src/transports/WebMCPBridgeScript.js +11 -2
  26. package/build/src/utils/ExtensionRegistry.js +35 -0
  27. package/build/src/utils/string.js +36 -0
  28. package/build/vendor/chrome-devtools-frontend/front_end/core/common/Base64.js +20 -2
  29. package/build/vendor/chrome-devtools-frontend/front_end/core/common/Debouncer.js +8 -1
  30. package/build/vendor/chrome-devtools-frontend/front_end/core/common/Gzip.js +11 -0
  31. package/build/vendor/chrome-devtools-frontend/front_end/core/common/Object.js +6 -1
  32. package/build/vendor/chrome-devtools-frontend/front_end/core/common/ParsedURL.js +3 -0
  33. package/build/vendor/chrome-devtools-frontend/front_end/core/common/ResourceType.js +6 -0
  34. package/build/vendor/chrome-devtools-frontend/front_end/core/common/Revealer.js +0 -5
  35. package/build/vendor/chrome-devtools-frontend/front_end/core/common/Settings.js +18 -8
  36. package/build/vendor/chrome-devtools-frontend/front_end/core/host/AidaClient.js +24 -0
  37. package/build/vendor/chrome-devtools-frontend/front_end/core/host/InspectorFrontendHostStub.js +11 -3
  38. package/build/vendor/chrome-devtools-frontend/front_end/core/host/ResourceLoader.js +1 -1
  39. package/build/vendor/chrome-devtools-frontend/front_end/core/host/UserMetrics.js +27 -20
  40. package/build/vendor/chrome-devtools-frontend/front_end/core/i18n/collect-ui-strings.js +7 -8
  41. package/build/vendor/chrome-devtools-frontend/front_end/core/i18n/generate-locales-js.js +4 -5
  42. package/build/vendor/chrome-devtools-frontend/front_end/core/platform/ArrayUtilities.js +10 -0
  43. package/build/vendor/chrome-devtools-frontend/front_end/core/platform/StringUtilities.js +63 -12
  44. package/build/vendor/chrome-devtools-frontend/front_end/core/protocol_client/CDPConnection.js +1 -0
  45. package/build/vendor/chrome-devtools-frontend/front_end/core/protocol_client/InspectorBackend.js +4 -1
  46. package/build/vendor/chrome-devtools-frontend/front_end/core/root/ExperimentNames.js +30 -0
  47. package/build/vendor/chrome-devtools-frontend/front_end/core/root/root.js +2 -1
  48. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/AnimationModel.js +0 -4
  49. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSMatchedStyles.js +69 -9
  50. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSMetadata.js +6 -6
  51. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSModel.js +28 -13
  52. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSProperty.js +1 -1
  53. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSPropertyParserMatchers.js +6 -0
  54. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/ConsoleModel.js +0 -2
  55. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CookieModel.js +1 -1
  56. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/DOMModel.js +170 -13
  57. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/DebuggerModel.js +5 -39
  58. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/HeapProfilerModel.js +8 -1
  59. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/NetworkManager.js +20 -5
  60. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/NetworkRequest.js +12 -21
  61. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/OverlayModel.js +19 -6
  62. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/RehydratingConnection.js +5 -1
  63. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/ResourceTreeModel.js +8 -5
  64. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/SourceMap.js +15 -10
  65. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/SourceMapManager.js +1 -1
  66. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/SourceMapScopesInfo.js +13 -27
  67. package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/Target.js +3 -1
  68. package/build/vendor/chrome-devtools-frontend/front_end/generated/ARIAProperties.js +1 -7
  69. package/build/vendor/chrome-devtools-frontend/front_end/generated/Deprecation.js +1 -16
  70. package/build/vendor/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +82 -22
  71. package/build/vendor/chrome-devtools-frontend/front_end/generated/SupportedCSSProperties.js +265 -123
  72. package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.js +2 -1
  73. package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js +10 -16
  74. package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js +97 -26
  75. package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AICallTree.js +35 -0
  76. package/build/vendor/chrome-devtools-frontend/front_end/models/annotations/AnnotationRepository.js +163 -0
  77. package/build/vendor/chrome-devtools-frontend/front_end/models/annotations/AnnotationType.js +10 -0
  78. package/build/vendor/chrome-devtools-frontend/front_end/models/annotations/annotations.js +5 -0
  79. package/build/vendor/chrome-devtools-frontend/front_end/models/bindings/CompilerScriptMapping.js +5 -3
  80. package/build/vendor/chrome-devtools-frontend/front_end/models/bindings/DebuggerLanguagePlugins.js +29 -58
  81. package/build/vendor/chrome-devtools-frontend/front_end/models/bindings/DebuggerWorkspaceBinding.js +7 -45
  82. package/build/vendor/chrome-devtools-frontend/front_end/models/emulation/DeviceModeModel.js +1 -1
  83. package/build/vendor/chrome-devtools-frontend/front_end/models/emulation/EmulatedDevices.js +14 -0
  84. package/build/vendor/chrome-devtools-frontend/front_end/models/formatter/FormatterWorkerPool.js +8 -5
  85. package/build/vendor/chrome-devtools-frontend/front_end/models/greendev/Prototypes.js +33 -0
  86. package/build/vendor/chrome-devtools-frontend/front_end/models/greendev/greendev.js +4 -0
  87. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/ContrastCheckTrigger.js +2 -2
  88. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/CookieIssue.js +0 -21
  89. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/CorsIssue.js +1 -38
  90. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/IssueAggregator.js +8 -0
  91. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/IssuesManager.js +6 -12
  92. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/PermissionElementIssue.js +243 -0
  93. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementActivationDisabled.md +7 -0
  94. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementActivationDisabledWithOccluder.md +9 -0
  95. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementActivationDisabledWithOccluderParent.md +9 -0
  96. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementCspFrameAncestorsMissing.md +5 -0
  97. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementFencedFrameDisallowed.md +5 -0
  98. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementFontSizeTooLarge.md +5 -0
  99. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementFontSizeTooSmall.md +5 -0
  100. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementGeolocationDeprecated.md +5 -0
  101. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInsetBoxShadowUnsupported.md +5 -0
  102. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidDisplayStyle.md +5 -0
  103. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidSizeValue.md +5 -0
  104. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidType.md +5 -0
  105. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidTypeActivation.md +5 -0
  106. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementLowContrast.md +5 -0
  107. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementNonOpaqueColor.md +5 -0
  108. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementPaddingBottomUnsupported.md +6 -0
  109. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementPaddingRightUnsupported.md +6 -0
  110. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementPermissionsPolicyBlocked.md +5 -0
  111. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementRegistrationFailed.md +5 -0
  112. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementRequestInProgress.md +5 -0
  113. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementSecurityChecksFailed.md +5 -0
  114. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementTypeNotSupported.md +5 -0
  115. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementUntrustedEvent.md +7 -0
  116. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/issues_manager.js +2 -1
  117. package/build/vendor/chrome-devtools-frontend/front_end/models/logs/NetworkLog.js +0 -8
  118. package/build/vendor/chrome-devtools-frontend/front_end/models/source_map_scopes/NamesResolver.js +4 -8
  119. package/build/vendor/chrome-devtools-frontend/front_end/models/stack_trace/StackTrace.js +30 -1
  120. package/build/vendor/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceImpl.js +70 -1
  121. package/build/vendor/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceModel.js +82 -30
  122. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/EventsSerializer.js +10 -2
  123. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/LanternComputationData.js +2 -2
  124. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/ModelImpl.js +0 -3
  125. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/Processor.js +18 -19
  126. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/Styles.js +12 -4
  127. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/extras/Initiators.js +46 -0
  128. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/extras/TraceTree.js +4 -3
  129. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/extras/extras.js +1 -0
  130. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/LargestImagePaintHandler.js +2 -2
  131. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/LayoutShiftsHandler.js +1 -1
  132. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/MetaHandler.js +6 -0
  133. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/NetworkRequestsHandler.js +10 -1
  134. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/PageLoadMetricsHandler.js +44 -27
  135. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/helpers/Timing.js +9 -2
  136. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/Common.js +1 -6
  137. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/LCPBreakdown.js +2 -2
  138. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/LCPDiscovery.js +2 -4
  139. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/NetworkDependencyTree.js +3 -2
  140. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/RenderBlocking.js +1 -1
  141. package/build/vendor/chrome-devtools-frontend/front_end/models/trace/types/TraceEvents.js +33 -11
  142. package/build/vendor/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec/package/src/decode/decode.js +51 -18
  143. package/build/vendor/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec/package/src/encode/encoder.js +1 -1
  144. package/build/vendor/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec/package/src/scopes.js +4 -0
  145. package/build/vendor/chrome-devtools-frontend/mcp/HostBindings.js +4 -0
  146. package/build/vendor/chrome-devtools-frontend/mcp/mcp.js +4 -0
  147. package/package.json +28 -21
  148. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/SameSiteInvalidSameParty.md +0 -8
  149. package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/SameSiteSamePartyCrossPartyContextSet.md +0 -10
@@ -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
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import process from 'node:process';
7
7
  import './polyfill.js';
8
- import { ensureBrowserConnected, ensureBrowserLaunched } from './browser.js';
8
+ import { connectToNewBrowser, disconnectBrowser, ensureBrowserConnected, ensureBrowserLaunched } from './browser.js';
9
9
  import { parseArguments } from './cli.js';
10
10
  import { loadIssueDescriptions } from './issue-descriptions.js';
11
11
  import { logger, saveLogsToFile } from './logger.js';
@@ -102,6 +102,7 @@ async function getContext() {
102
102
  devtools,
103
103
  channel: undefined,
104
104
  userDataDir: args.userDataDir,
105
+ includeExtensionPages: args.includeExtensionPages,
105
106
  });
106
107
  }
107
108
  // If autoConnect is true, try connecting first, then fall back to launching
@@ -115,6 +116,7 @@ async function getContext() {
115
116
  devtools,
116
117
  channel: args.channel,
117
118
  userDataDir: args.userDataDir,
119
+ includeExtensionPages: args.includeExtensionPages,
118
120
  });
119
121
  logger('Successfully connected to running browser instance');
120
122
  }
@@ -131,6 +133,7 @@ async function getContext() {
131
133
  args: extraArgs,
132
134
  acceptInsecureCerts: args.acceptInsecureCerts,
133
135
  devtools,
136
+ includeExtensionPages: args.includeExtensionPages,
134
137
  });
135
138
  wasLaunched = true;
136
139
  }
@@ -148,6 +151,7 @@ async function getContext() {
148
151
  args: extraArgs,
149
152
  acceptInsecureCerts: args.acceptInsecureCerts,
150
153
  devtools,
154
+ includeExtensionPages: args.includeExtensionPages,
151
155
  });
152
156
  wasLaunched = true;
153
157
  }
@@ -155,6 +159,8 @@ async function getContext() {
155
159
  context = await McpContext.from(browser, logger, {
156
160
  experimentalDevToolsDebugging: devtools,
157
161
  experimentalIncludeAllPages: args.experimentalIncludeAllPages,
162
+ includeExtensionPages: args.includeExtensionPages,
163
+ onReconnect,
158
164
  });
159
165
  if (wasLaunched) {
160
166
  // Fresh browser launch - use the existing default page
@@ -201,6 +207,77 @@ async function getContext() {
201
207
  }
202
208
  return context;
203
209
  }
210
+ /**
211
+ * Reconnect callback invoked by the connect_to_browser tool.
212
+ *
213
+ * Tears down the current browser session and connects to a new browser,
214
+ * rebuilding all infrastructure (McpContext, WebMCPToolHub, session window).
215
+ * Retries connection with exponential backoff for browsers that are still starting.
216
+ */
217
+ async function onReconnect(options) {
218
+ const timeout = options.timeout ?? 30_000;
219
+ const retryInterval = 2_000;
220
+ const devtools = args.experimentalDevtools ?? false;
221
+ // 1. Tear down old session
222
+ if (context) {
223
+ try {
224
+ await context.closeSessionWindow();
225
+ }
226
+ catch (err) {
227
+ logger('Error closing session window during reconnect:', err);
228
+ }
229
+ context.dispose();
230
+ }
231
+ disconnectBrowser();
232
+ // 2. Retry loop to connect to new browser
233
+ const deadline = Date.now() + timeout;
234
+ let lastError;
235
+ while (Date.now() < deadline) {
236
+ try {
237
+ const newBrowser = await connectToNewBrowser({
238
+ browserURL: options.browserURL,
239
+ wsEndpoint: options.wsEndpoint,
240
+ wsHeaders: options.wsHeaders,
241
+ includeExtensionPages: args.includeExtensionPages,
242
+ });
243
+ // 3. Rebuild context
244
+ context = await McpContext.from(newBrowser, logger, {
245
+ experimentalDevToolsDebugging: devtools,
246
+ experimentalIncludeAllPages: args.experimentalIncludeAllPages,
247
+ includeExtensionPages: args.includeExtensionPages,
248
+ onReconnect,
249
+ });
250
+ // 4. Create session window
251
+ const { windowId } = await context.newWindow();
252
+ context.setSessionWindowId(windowId);
253
+ logger(`Reconnect: new session window ${windowId}`);
254
+ // 5. Rebuild tool hub
255
+ const toolHub = new WebMCPToolHub(server, context);
256
+ context.setToolHub(toolHub);
257
+ logger('Reconnect: WebMCPToolHub rebuilt');
258
+ // 6. Build response
259
+ const pages = context.getPages().map((page, index) => ({
260
+ index,
261
+ url: page.url(),
262
+ selected: context.isPageSelected(page),
263
+ }));
264
+ const wsEndpoint = newBrowser.wsEndpoint();
265
+ return { pages, wsEndpoint };
266
+ }
267
+ catch (err) {
268
+ lastError = err instanceof Error ? err : new Error(String(err));
269
+ logger(`Reconnect attempt failed: ${lastError.message}`);
270
+ const remaining = deadline - Date.now();
271
+ if (remaining > retryInterval) {
272
+ await new Promise(resolve => setTimeout(resolve, retryInterval));
273
+ }
274
+ else {
275
+ break;
276
+ }
277
+ }
278
+ }
279
+ throw new Error(`Failed to connect to browser after ${timeout}ms: ${lastError?.message ?? 'unknown error'}`);
280
+ }
204
281
  /**
205
282
  * Log security disclaimers to stderr.
206
283
  *
@@ -251,7 +328,11 @@ function registerTool(tool) {
251
328
  await tool.handler({
252
329
  params,
253
330
  }, response, context);
254
- const content = await response.handle(tool.name, context);
331
+ // Re-fetch context in case the handler swapped it (e.g. connect_to_browser).
332
+ // The local `const context` shadows the module-level `let context`, so we
333
+ // must call getContext() to pick up any reassignment by onReconnect.
334
+ const activeContext = await getContext();
335
+ const content = await response.handle(tool.name, activeContext);
255
336
  const result = {
256
337
  content,
257
338
  };
@@ -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 = {}));