@mcp-b/chrome-devtools-mcp 2.3.0 → 2.3.1-beta.20260528050333
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/package.json +1 -1
- package/build/src/DevToolsConnectionAdapter.js +0 -70
- package/build/src/DevtoolsUtils.js +0 -290
- package/build/src/McpContext.js +0 -687
- package/build/src/McpPage.js +0 -95
- package/build/src/McpResponse.js +0 -588
- package/build/src/Mutex.js +0 -37
- package/build/src/PageCollector.js +0 -308
- package/build/src/SlimMcpResponse.js +0 -18
- package/build/src/WaitForHelper.js +0 -135
- package/build/src/bin/chrome-devtools-cli-options.js +0 -651
- package/build/src/bin/chrome-devtools-mcp-cli-options.js +0 -317
- package/build/src/bin/chrome-devtools-mcp-main.js +0 -35
- package/build/src/bin/chrome-devtools-mcp.js +0 -21
- package/build/src/bin/chrome-devtools.js +0 -185
- package/build/src/bin/cliDefinitions.js +0 -615
- package/build/src/browser.js +0 -198
- package/build/src/daemon/client.js +0 -152
- package/build/src/daemon/daemon.js +0 -206
- package/build/src/daemon/types.js +0 -6
- package/build/src/daemon/utils.js +0 -108
- package/build/src/formatters/ConsoleFormatter.js +0 -234
- package/build/src/formatters/IssueFormatter.js +0 -192
- package/build/src/formatters/NetworkFormatter.js +0 -215
- package/build/src/formatters/SnapshotFormatter.js +0 -131
- package/build/src/index.js +0 -202
- package/build/src/issue-descriptions.js +0 -39
- package/build/src/logger.js +0 -36
- package/build/src/polyfill.js +0 -7
- package/build/src/telemetry/ClearcutLogger.js +0 -102
- package/build/src/telemetry/WatchdogClient.js +0 -60
- package/build/src/telemetry/flagUtils.js +0 -45
- package/build/src/telemetry/metricUtils.js +0 -14
- package/build/src/telemetry/persistence.js +0 -53
- package/build/src/telemetry/types.js +0 -33
- package/build/src/telemetry/watchdog/ClearcutSender.js +0 -203
- package/build/src/telemetry/watchdog/main.js +0 -127
- package/build/src/third_party/devtools-formatter-worker.js +0 -7
- package/build/src/third_party/index.js +0 -26
- package/build/src/third_party/lighthouse-devtools-mcp-bundle.js +0 -54183
- package/build/src/tools/ToolDefinition.js +0 -72
- package/build/src/tools/categories.js +0 -24
- package/build/src/tools/console.js +0 -85
- package/build/src/tools/emulation.js +0 -55
- package/build/src/tools/extensions.js +0 -96
- package/build/src/tools/input.js +0 -368
- package/build/src/tools/lighthouse.js +0 -123
- package/build/src/tools/memory.js +0 -28
- package/build/src/tools/network.js +0 -120
- package/build/src/tools/pages.js +0 -319
- package/build/src/tools/performance.js +0 -190
- package/build/src/tools/screencast.js +0 -79
- package/build/src/tools/screenshot.js +0 -84
- package/build/src/tools/script.js +0 -119
- package/build/src/tools/slim/tools.js +0 -81
- package/build/src/tools/snapshot.js +0 -56
- package/build/src/tools/tools.js +0 -52
- package/build/src/tools/webmcp.js +0 -416
- package/build/src/trace-processing/parse.js +0 -84
- package/build/src/types.js +0 -6
- package/build/src/utils/ExtensionRegistry.js +0 -35
- package/build/src/utils/files.js +0 -19
- package/build/src/utils/keyboard.js +0 -296
- package/build/src/utils/pagination.js +0 -49
- package/build/src/utils/string.js +0 -36
- package/build/src/utils/types.js +0 -6
- package/build/src/version.js +0 -9
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2025 Google LLC
|
|
4
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
-
*/
|
|
6
|
-
export class SnapshotFormatter {
|
|
7
|
-
#snapshot;
|
|
8
|
-
constructor(snapshot) {
|
|
9
|
-
this.#snapshot = snapshot;
|
|
10
|
-
}
|
|
11
|
-
toString() {
|
|
12
|
-
const chunks = [];
|
|
13
|
-
const root = this.#snapshot.root;
|
|
14
|
-
// Top-level content of the snapshot.
|
|
15
|
-
if (this.#snapshot.verbose &&
|
|
16
|
-
this.#snapshot.hasSelectedElement &&
|
|
17
|
-
!this.#snapshot.selectedElementUid) {
|
|
18
|
-
chunks.push(`Note: there is a selected element in the DevTools Elements panel but it is not included into the current a11y tree snapshot.
|
|
19
|
-
Get a verbose snapshot to include all elements if you are interested in the selected element.\n\n`);
|
|
20
|
-
}
|
|
21
|
-
chunks.push(this.#formatNode(root, 0));
|
|
22
|
-
return chunks.join('');
|
|
23
|
-
}
|
|
24
|
-
toJSON() {
|
|
25
|
-
return this.#nodeToJSON(this.#snapshot.root);
|
|
26
|
-
}
|
|
27
|
-
#formatNode(node, depth = 0) {
|
|
28
|
-
const chunks = [];
|
|
29
|
-
const attributes = this.#getAttributes(node);
|
|
30
|
-
const line = ' '.repeat(depth * 2) +
|
|
31
|
-
attributes.join(' ') +
|
|
32
|
-
(node.id === this.#snapshot.selectedElementUid
|
|
33
|
-
? ' [selected in the DevTools Elements panel]'
|
|
34
|
-
: '') +
|
|
35
|
-
'\n';
|
|
36
|
-
chunks.push(line);
|
|
37
|
-
for (const child of node.children) {
|
|
38
|
-
chunks.push(this.#formatNode(child, depth + 1));
|
|
39
|
-
}
|
|
40
|
-
return chunks.join('');
|
|
41
|
-
}
|
|
42
|
-
#nodeToJSON(node) {
|
|
43
|
-
const rawAttrs = this.#getAttributesMap(node);
|
|
44
|
-
const children = node.children.map((child) => this.#nodeToJSON(child));
|
|
45
|
-
const result = structuredClone(rawAttrs);
|
|
46
|
-
if (children.length > 0) {
|
|
47
|
-
result.children = children;
|
|
48
|
-
}
|
|
49
|
-
return result;
|
|
50
|
-
}
|
|
51
|
-
#getAttributes(serializedAXNodeRoot) {
|
|
52
|
-
const attributes = [`uid=${serializedAXNodeRoot.id}`];
|
|
53
|
-
if (serializedAXNodeRoot.role) {
|
|
54
|
-
attributes.push(serializedAXNodeRoot.role === 'none' ? 'ignored' : serializedAXNodeRoot.role);
|
|
55
|
-
}
|
|
56
|
-
if (serializedAXNodeRoot.name) {
|
|
57
|
-
attributes.push(`"${serializedAXNodeRoot.name}"`);
|
|
58
|
-
}
|
|
59
|
-
const simpleAttrs = this.#getAttributesMap(serializedAXNodeRoot, /* excludeSpecial */ true);
|
|
60
|
-
for (const attr of Object.keys(serializedAXNodeRoot).sort()) {
|
|
61
|
-
if (excludedAttributes.has(attr)) {
|
|
62
|
-
continue;
|
|
63
|
-
}
|
|
64
|
-
const mapped = booleanPropertyMap[attr];
|
|
65
|
-
if (mapped && simpleAttrs[mapped]) {
|
|
66
|
-
attributes.push(mapped);
|
|
67
|
-
}
|
|
68
|
-
const val = simpleAttrs[attr];
|
|
69
|
-
if (val === true) {
|
|
70
|
-
attributes.push(attr);
|
|
71
|
-
}
|
|
72
|
-
else if (typeof val === 'string' || typeof val === 'number') {
|
|
73
|
-
attributes.push(`${attr}="${val}"`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return attributes;
|
|
77
|
-
}
|
|
78
|
-
#getAttributesMap(node, excludeSpecial = false) {
|
|
79
|
-
const result = {};
|
|
80
|
-
if (!excludeSpecial) {
|
|
81
|
-
result.id = node.id;
|
|
82
|
-
if (node.role) {
|
|
83
|
-
result.role = node.role;
|
|
84
|
-
}
|
|
85
|
-
if (node.name) {
|
|
86
|
-
result.name = node.name;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
// Re-implementing the exact logic from original function for #getAttributes to be safe:
|
|
90
|
-
return {
|
|
91
|
-
...result,
|
|
92
|
-
...this.#extractedAttributes(node),
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
#extractedAttributes(node) {
|
|
96
|
-
const result = {};
|
|
97
|
-
for (const attr of Object.keys(node).sort()) {
|
|
98
|
-
if (excludedAttributes.has(attr)) {
|
|
99
|
-
continue;
|
|
100
|
-
}
|
|
101
|
-
const value = node[attr];
|
|
102
|
-
if (typeof value === 'boolean') {
|
|
103
|
-
if (booleanPropertyMap[attr]) {
|
|
104
|
-
result[booleanPropertyMap[attr]] = true;
|
|
105
|
-
}
|
|
106
|
-
if (value) {
|
|
107
|
-
result[attr] = true;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
else if (typeof value === 'string' || typeof value === 'number') {
|
|
111
|
-
result[attr] = value;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
return result;
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
const booleanPropertyMap = {
|
|
118
|
-
disabled: 'disableable',
|
|
119
|
-
expanded: 'expandable',
|
|
120
|
-
focused: 'focusable',
|
|
121
|
-
selected: 'selectable',
|
|
122
|
-
};
|
|
123
|
-
const excludedAttributes = new Set([
|
|
124
|
-
'id',
|
|
125
|
-
'role',
|
|
126
|
-
'name',
|
|
127
|
-
'elementHandle',
|
|
128
|
-
'children',
|
|
129
|
-
'backendNodeId',
|
|
130
|
-
'loaderId',
|
|
131
|
-
]);
|
package/build/src/index.js
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2026 Google LLC
|
|
4
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
-
*/
|
|
6
|
-
import { ensureBrowserConnected, ensureBrowserLaunched } from './browser.js';
|
|
7
|
-
import { loadIssueDescriptions } from './issue-descriptions.js';
|
|
8
|
-
import { logger } from './logger.js';
|
|
9
|
-
import { McpContext } from './McpContext.js';
|
|
10
|
-
import { McpResponse } from './McpResponse.js';
|
|
11
|
-
import { Mutex } from './Mutex.js';
|
|
12
|
-
import { SlimMcpResponse } from './SlimMcpResponse.js';
|
|
13
|
-
import { ClearcutLogger } from './telemetry/ClearcutLogger.js';
|
|
14
|
-
import { bucketizeLatency } from './telemetry/metricUtils.js';
|
|
15
|
-
import { McpServer, SetLevelRequestSchema } from './third_party/index.js';
|
|
16
|
-
import { ToolCategory } from './tools/categories.js';
|
|
17
|
-
import { pageIdSchema } from './tools/ToolDefinition.js';
|
|
18
|
-
import { createTools } from './tools/tools.js';
|
|
19
|
-
import { VERSION } from './version.js';
|
|
20
|
-
export async function createMcpServer(serverArgs, options) {
|
|
21
|
-
let clearcutLogger;
|
|
22
|
-
if (serverArgs.usageStatistics) {
|
|
23
|
-
clearcutLogger = new ClearcutLogger({
|
|
24
|
-
logFile: serverArgs.logFile,
|
|
25
|
-
appVersion: VERSION,
|
|
26
|
-
clearcutEndpoint: serverArgs.clearcutEndpoint,
|
|
27
|
-
clearcutForceFlushIntervalMs: serverArgs.clearcutForceFlushIntervalMs,
|
|
28
|
-
clearcutIncludePidHeader: serverArgs.clearcutIncludePidHeader,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
const server = new McpServer({
|
|
32
|
-
name: 'chrome_devtools',
|
|
33
|
-
title: 'Chrome DevTools MCP server',
|
|
34
|
-
version: VERSION,
|
|
35
|
-
}, { capabilities: { logging: {} } });
|
|
36
|
-
server.server.setRequestHandler(SetLevelRequestSchema, () => {
|
|
37
|
-
return {};
|
|
38
|
-
});
|
|
39
|
-
let context;
|
|
40
|
-
async function getContext() {
|
|
41
|
-
const chromeArgs = (serverArgs.chromeArg ?? []).map(String);
|
|
42
|
-
const ignoreDefaultChromeArgs = (serverArgs.ignoreDefaultChromeArg ?? []).map(String);
|
|
43
|
-
if (serverArgs.proxyServer) {
|
|
44
|
-
chromeArgs.push(`--proxy-server=${serverArgs.proxyServer}`);
|
|
45
|
-
}
|
|
46
|
-
const devtools = serverArgs.experimentalDevtools ?? false;
|
|
47
|
-
const browser = serverArgs.browserUrl || serverArgs.wsEndpoint || serverArgs.autoConnect
|
|
48
|
-
? await ensureBrowserConnected({
|
|
49
|
-
browserURL: serverArgs.browserUrl,
|
|
50
|
-
wsEndpoint: serverArgs.wsEndpoint,
|
|
51
|
-
wsHeaders: serverArgs.wsHeaders,
|
|
52
|
-
// Important: only pass channel, if autoConnect is true.
|
|
53
|
-
channel: serverArgs.autoConnect ? serverArgs.channel : undefined,
|
|
54
|
-
userDataDir: serverArgs.userDataDir,
|
|
55
|
-
devtools,
|
|
56
|
-
})
|
|
57
|
-
: await ensureBrowserLaunched({
|
|
58
|
-
headless: serverArgs.headless,
|
|
59
|
-
executablePath: serverArgs.executablePath,
|
|
60
|
-
channel: serverArgs.channel,
|
|
61
|
-
isolated: serverArgs.isolated ?? false,
|
|
62
|
-
userDataDir: serverArgs.userDataDir,
|
|
63
|
-
logFile: options.logFile,
|
|
64
|
-
viewport: serverArgs.viewport,
|
|
65
|
-
chromeArgs,
|
|
66
|
-
ignoreDefaultChromeArgs,
|
|
67
|
-
acceptInsecureCerts: serverArgs.acceptInsecureCerts,
|
|
68
|
-
devtools,
|
|
69
|
-
enableExtensions: serverArgs.categoryExtensions,
|
|
70
|
-
viaCli: serverArgs.viaCli,
|
|
71
|
-
});
|
|
72
|
-
if (context?.browser !== browser) {
|
|
73
|
-
context = await McpContext.from(browser, logger, {
|
|
74
|
-
experimentalDevToolsDebugging: devtools,
|
|
75
|
-
experimentalIncludeAllPages: serverArgs.experimentalIncludeAllPages,
|
|
76
|
-
performanceCrux: serverArgs.performanceCrux,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
return context;
|
|
80
|
-
}
|
|
81
|
-
const toolMutex = new Mutex();
|
|
82
|
-
function registerTool(tool) {
|
|
83
|
-
if (tool.annotations.category === ToolCategory.EMULATION &&
|
|
84
|
-
serverArgs.categoryEmulation === false) {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
if (tool.annotations.category === ToolCategory.PERFORMANCE &&
|
|
88
|
-
serverArgs.categoryPerformance === false) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (tool.annotations.category === ToolCategory.NETWORK &&
|
|
92
|
-
serverArgs.categoryNetwork === false) {
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
if (tool.annotations.category === ToolCategory.EXTENSIONS && !serverArgs.categoryExtensions) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (tool.annotations.conditions?.includes('computerVision') && !serverArgs.experimentalVision) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (tool.annotations.conditions?.includes('experimentalInteropTools') &&
|
|
102
|
-
!serverArgs.experimentalInteropTools) {
|
|
103
|
-
return;
|
|
104
|
-
}
|
|
105
|
-
if (tool.annotations.conditions?.includes('screencast') && !serverArgs.experimentalScreencast) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
const schema = 'pageScoped' in tool &&
|
|
109
|
-
tool.pageScoped &&
|
|
110
|
-
serverArgs.experimentalPageIdRouting &&
|
|
111
|
-
!serverArgs.slim
|
|
112
|
-
? { ...tool.schema, ...pageIdSchema }
|
|
113
|
-
: tool.schema;
|
|
114
|
-
server.registerTool(tool.name, {
|
|
115
|
-
description: tool.description,
|
|
116
|
-
inputSchema: schema,
|
|
117
|
-
annotations: tool.annotations,
|
|
118
|
-
}, async (params) => {
|
|
119
|
-
const guard = await toolMutex.acquire();
|
|
120
|
-
const startTime = Date.now();
|
|
121
|
-
let success = false;
|
|
122
|
-
try {
|
|
123
|
-
logger(`${tool.name} request: ${JSON.stringify(params, null, ' ')}`);
|
|
124
|
-
const context = await getContext();
|
|
125
|
-
logger(`${tool.name} context: resolved`);
|
|
126
|
-
await context.detectOpenDevToolsWindows();
|
|
127
|
-
const response = serverArgs.slim
|
|
128
|
-
? new SlimMcpResponse(serverArgs)
|
|
129
|
-
: new McpResponse(serverArgs);
|
|
130
|
-
if ('pageScoped' in tool && tool.pageScoped) {
|
|
131
|
-
const page = serverArgs.experimentalPageIdRouting && params.pageId && !serverArgs.slim
|
|
132
|
-
? context.getPageById(params.pageId)
|
|
133
|
-
: context.getSelectedMcpPage();
|
|
134
|
-
response.setPage(page);
|
|
135
|
-
await tool.handler({
|
|
136
|
-
params,
|
|
137
|
-
page,
|
|
138
|
-
}, response, context);
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
await tool.handler(
|
|
142
|
-
// @ts-expect-error types do not match.
|
|
143
|
-
{
|
|
144
|
-
params,
|
|
145
|
-
}, response, context);
|
|
146
|
-
}
|
|
147
|
-
const { content, structuredContent } = await response.handle(tool.name, context);
|
|
148
|
-
const result = {
|
|
149
|
-
content,
|
|
150
|
-
};
|
|
151
|
-
success = true;
|
|
152
|
-
if (serverArgs.experimentalStructuredContent) {
|
|
153
|
-
result.structuredContent = structuredContent;
|
|
154
|
-
}
|
|
155
|
-
return result;
|
|
156
|
-
}
|
|
157
|
-
catch (err) {
|
|
158
|
-
logger(`${tool.name} error:`, err, err?.stack);
|
|
159
|
-
let errorText = err && 'message' in err ? err.message : String(err);
|
|
160
|
-
if ('cause' in err && err.cause) {
|
|
161
|
-
errorText += `\nCause: ${err.cause.message}`;
|
|
162
|
-
}
|
|
163
|
-
return {
|
|
164
|
-
content: [
|
|
165
|
-
{
|
|
166
|
-
type: 'text',
|
|
167
|
-
text: errorText,
|
|
168
|
-
},
|
|
169
|
-
],
|
|
170
|
-
isError: true,
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
finally {
|
|
174
|
-
void clearcutLogger?.logToolInvocation({
|
|
175
|
-
toolName: tool.name,
|
|
176
|
-
success,
|
|
177
|
-
latencyMs: bucketizeLatency(Date.now() - startTime),
|
|
178
|
-
});
|
|
179
|
-
guard.dispose();
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
const tools = createTools(serverArgs);
|
|
184
|
-
for (const tool of tools) {
|
|
185
|
-
registerTool(tool);
|
|
186
|
-
}
|
|
187
|
-
await loadIssueDescriptions();
|
|
188
|
-
return { server, clearcutLogger };
|
|
189
|
-
}
|
|
190
|
-
export const logDisclaimers = (args) => {
|
|
191
|
-
console.error(`chrome-devtools-mcp exposes content of the browser instance to the MCP clients allowing them to inspect,
|
|
192
|
-
debug, and modify any data in the browser or DevTools.
|
|
193
|
-
Avoid sharing sensitive or personal information that you do not want to share with MCP clients.`);
|
|
194
|
-
if (!args.slim && args.performanceCrux) {
|
|
195
|
-
console.error(`Performance tools may send trace URLs to the Google CrUX API to fetch real-user experience data. To disable, run with --no-performance-crux.`);
|
|
196
|
-
}
|
|
197
|
-
if (!args.slim && args.usageStatistics) {
|
|
198
|
-
console.error(`
|
|
199
|
-
Google collects usage statistics to improve Chrome DevTools MCP. To opt-out, run with --no-usage-statistics.
|
|
200
|
-
For more details, visit: https://github.com/ChromeDevTools/chrome-devtools-mcp#usage-statistics`);
|
|
201
|
-
}
|
|
202
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2025 Google LLC
|
|
4
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
-
*/
|
|
6
|
-
import * as fs from 'node:fs';
|
|
7
|
-
import * as path from 'node:path';
|
|
8
|
-
const DESCRIPTIONS_PATH = path.join(import.meta.dirname, 'third_party/issue-descriptions');
|
|
9
|
-
let issueDescriptions = {};
|
|
10
|
-
/**
|
|
11
|
-
* Reads all issue descriptions from the filesystem into memory.
|
|
12
|
-
*/
|
|
13
|
-
export async function loadIssueDescriptions() {
|
|
14
|
-
if (Object.keys(issueDescriptions).length > 0) {
|
|
15
|
-
return;
|
|
16
|
-
}
|
|
17
|
-
const files = await fs.promises.readdir(DESCRIPTIONS_PATH);
|
|
18
|
-
const descriptions = {};
|
|
19
|
-
for (const file of files) {
|
|
20
|
-
if (!file.endsWith('.md')) {
|
|
21
|
-
continue;
|
|
22
|
-
}
|
|
23
|
-
const content = await fs.promises.readFile(path.join(DESCRIPTIONS_PATH, file), 'utf-8');
|
|
24
|
-
descriptions[file] = content;
|
|
25
|
-
}
|
|
26
|
-
issueDescriptions = descriptions;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Gets an issue description from the in-memory cache.
|
|
30
|
-
* @param fileName The file name of the issue description.
|
|
31
|
-
* @returns The description of the issue, or null if it doesn't exist.
|
|
32
|
-
*/
|
|
33
|
-
export function getIssueDescription(fileName) {
|
|
34
|
-
return issueDescriptions[fileName] ?? null;
|
|
35
|
-
}
|
|
36
|
-
export const ISSUE_UTILS = {
|
|
37
|
-
loadIssueDescriptions,
|
|
38
|
-
getIssueDescription,
|
|
39
|
-
};
|
package/build/src/logger.js
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license
|
|
3
|
-
* Copyright 2025 Google LLC
|
|
4
|
-
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
-
*/
|
|
6
|
-
import fs from 'node:fs';
|
|
7
|
-
import { debug } from './third_party/index.js';
|
|
8
|
-
const mcpDebugNamespace = 'mcp:log';
|
|
9
|
-
const namespacesToEnable = [
|
|
10
|
-
mcpDebugNamespace,
|
|
11
|
-
...(process.env['DEBUG'] ? [process.env['DEBUG']] : []),
|
|
12
|
-
];
|
|
13
|
-
export function saveLogsToFile(fileName) {
|
|
14
|
-
// Enable overrides everything so we need to add them
|
|
15
|
-
debug.enable(namespacesToEnable.join(','));
|
|
16
|
-
const logFile = fs.createWriteStream(fileName, { flags: 'a+' });
|
|
17
|
-
debug.log = function (...chunks) {
|
|
18
|
-
logFile.write(`${chunks.join(' ')}\n`);
|
|
19
|
-
};
|
|
20
|
-
logFile.on('error', function (error) {
|
|
21
|
-
console.error(`Error when opening/writing to log file: ${error.message}`);
|
|
22
|
-
logFile.end();
|
|
23
|
-
process.exit(1);
|
|
24
|
-
});
|
|
25
|
-
return logFile;
|
|
26
|
-
}
|
|
27
|
-
export function flushLogs(logFile, timeoutMs = 2000) {
|
|
28
|
-
return new Promise((resolve, reject) => {
|
|
29
|
-
const timeout = setTimeout(reject, timeoutMs);
|
|
30
|
-
logFile.end(() => {
|
|
31
|
-
clearTimeout(timeout);
|
|
32
|
-
resolve();
|
|
33
|
-
});
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
export const logger = debug(mcpDebugNamespace);
|
package/build/src/polyfill.js
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
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 './WatchdogClient.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
|
-
}
|
|
@@ -1,60 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
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
|
-
}
|