@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.
- package/README.md +120 -10
- package/build/src/McpContext.js +69 -5
- package/build/src/browser.js +157 -51
- package/build/src/cli.js +11 -5
- package/build/src/formatters/IssueFormatter.js +190 -0
- package/build/src/main.js +83 -2
- package/build/src/telemetry/clearcut-logger.js +102 -0
- package/build/src/telemetry/flag-utils.js +45 -0
- package/build/src/telemetry/metric-utils.js +14 -0
- package/build/src/telemetry/persistence.js +53 -0
- package/build/src/telemetry/types.js +33 -0
- package/build/src/telemetry/watchdog/clearcut-sender.js +201 -0
- package/build/src/telemetry/watchdog/main.js +127 -0
- package/build/src/telemetry/watchdog-client.js +60 -0
- package/build/src/third_party/devtools-formatter-worker.js +7 -0
- package/build/src/third_party/index.js +1 -1
- package/build/src/tools/browser.js +92 -0
- package/build/src/tools/extension.js +31 -0
- package/build/src/tools/extensions.js +79 -0
- package/build/src/tools/input.js +6 -1
- package/build/src/tools/pages.js +7 -1
- package/build/src/tools/script.js +31 -4
- package/build/src/tools/tools.js +4 -0
- package/build/src/transports/CDPClientTransport.js +184 -0
- package/build/src/transports/WebMCPBridgeScript.js +11 -2
- package/build/src/utils/ExtensionRegistry.js +35 -0
- package/build/src/utils/string.js +36 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/Base64.js +20 -2
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/Debouncer.js +8 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/Gzip.js +11 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/Object.js +6 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/ParsedURL.js +3 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/ResourceType.js +6 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/Revealer.js +0 -5
- package/build/vendor/chrome-devtools-frontend/front_end/core/common/Settings.js +18 -8
- package/build/vendor/chrome-devtools-frontend/front_end/core/host/AidaClient.js +24 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/host/InspectorFrontendHostStub.js +11 -3
- package/build/vendor/chrome-devtools-frontend/front_end/core/host/ResourceLoader.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/host/UserMetrics.js +27 -20
- package/build/vendor/chrome-devtools-frontend/front_end/core/i18n/collect-ui-strings.js +7 -8
- package/build/vendor/chrome-devtools-frontend/front_end/core/i18n/generate-locales-js.js +4 -5
- package/build/vendor/chrome-devtools-frontend/front_end/core/platform/ArrayUtilities.js +10 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/platform/StringUtilities.js +63 -12
- package/build/vendor/chrome-devtools-frontend/front_end/core/protocol_client/CDPConnection.js +1 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/protocol_client/InspectorBackend.js +4 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/root/ExperimentNames.js +30 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/root/root.js +2 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/AnimationModel.js +0 -4
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSMatchedStyles.js +69 -9
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSMetadata.js +6 -6
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSModel.js +28 -13
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSProperty.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CSSPropertyParserMatchers.js +6 -0
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/ConsoleModel.js +0 -2
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/CookieModel.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/DOMModel.js +170 -13
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/DebuggerModel.js +5 -39
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/HeapProfilerModel.js +8 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/NetworkManager.js +20 -5
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/NetworkRequest.js +12 -21
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/OverlayModel.js +19 -6
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/RehydratingConnection.js +5 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/ResourceTreeModel.js +8 -5
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/SourceMap.js +15 -10
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/SourceMapManager.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/SourceMapScopesInfo.js +13 -27
- package/build/vendor/chrome-devtools-frontend/front_end/core/sdk/Target.js +3 -1
- package/build/vendor/chrome-devtools-frontend/front_end/generated/ARIAProperties.js +1 -7
- package/build/vendor/chrome-devtools-frontend/front_end/generated/Deprecation.js +1 -16
- package/build/vendor/chrome-devtools-frontend/front_end/generated/InspectorBackendCommands.js +82 -22
- package/build/vendor/chrome-devtools-frontend/front_end/generated/SupportedCSSProperties.js +265 -123
- package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.js +2 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js +10 -16
- package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js +97 -26
- package/build/vendor/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AICallTree.js +35 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/annotations/AnnotationRepository.js +163 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/annotations/AnnotationType.js +10 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/annotations/annotations.js +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/bindings/CompilerScriptMapping.js +5 -3
- package/build/vendor/chrome-devtools-frontend/front_end/models/bindings/DebuggerLanguagePlugins.js +29 -58
- package/build/vendor/chrome-devtools-frontend/front_end/models/bindings/DebuggerWorkspaceBinding.js +7 -45
- package/build/vendor/chrome-devtools-frontend/front_end/models/emulation/DeviceModeModel.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/emulation/EmulatedDevices.js +14 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/formatter/FormatterWorkerPool.js +8 -5
- package/build/vendor/chrome-devtools-frontend/front_end/models/greendev/Prototypes.js +33 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/greendev/greendev.js +4 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/ContrastCheckTrigger.js +2 -2
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/CookieIssue.js +0 -21
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/CorsIssue.js +1 -38
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/IssueAggregator.js +8 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/IssuesManager.js +6 -12
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/PermissionElementIssue.js +243 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementActivationDisabled.md +7 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementActivationDisabledWithOccluder.md +9 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementActivationDisabledWithOccluderParent.md +9 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementCspFrameAncestorsMissing.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementFencedFrameDisallowed.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementFontSizeTooLarge.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementFontSizeTooSmall.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementGeolocationDeprecated.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInsetBoxShadowUnsupported.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidDisplayStyle.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidSizeValue.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidType.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementInvalidTypeActivation.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementLowContrast.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementNonOpaqueColor.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementPaddingBottomUnsupported.md +6 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementPaddingRightUnsupported.md +6 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementPermissionsPolicyBlocked.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementRegistrationFailed.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementRequestInProgress.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementSecurityChecksFailed.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementTypeNotSupported.md +5 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/permissionElementUntrustedEvent.md +7 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/issues_manager.js +2 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/logs/NetworkLog.js +0 -8
- package/build/vendor/chrome-devtools-frontend/front_end/models/source_map_scopes/NamesResolver.js +4 -8
- package/build/vendor/chrome-devtools-frontend/front_end/models/stack_trace/StackTrace.js +30 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceImpl.js +70 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/stack_trace/StackTraceModel.js +82 -30
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/EventsSerializer.js +10 -2
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/LanternComputationData.js +2 -2
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/ModelImpl.js +0 -3
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/Processor.js +18 -19
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/Styles.js +12 -4
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/extras/Initiators.js +46 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/extras/TraceTree.js +4 -3
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/extras/extras.js +1 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/LargestImagePaintHandler.js +2 -2
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/LayoutShiftsHandler.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/MetaHandler.js +6 -0
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/NetworkRequestsHandler.js +10 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/handlers/PageLoadMetricsHandler.js +44 -27
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/helpers/Timing.js +9 -2
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/Common.js +1 -6
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/LCPBreakdown.js +2 -2
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/LCPDiscovery.js +2 -4
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/NetworkDependencyTree.js +3 -2
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/insights/RenderBlocking.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/models/trace/types/TraceEvents.js +33 -11
- package/build/vendor/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec/package/src/decode/decode.js +51 -18
- package/build/vendor/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec/package/src/encode/encoder.js +1 -1
- package/build/vendor/chrome-devtools-frontend/front_end/third_party/source-map-scopes-codec/package/src/scopes.js +4 -0
- package/build/vendor/chrome-devtools-frontend/mcp/HostBindings.js +4 -0
- package/build/vendor/chrome-devtools-frontend/mcp/mcp.js +4 -0
- package/package.json +28 -21
- package/build/vendor/chrome-devtools-frontend/front_end/models/issues_manager/descriptions/SameSiteInvalidSameParty.md +0 -8
- 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
|
-
|
|
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 = {}));
|