Skip to content

Commit a44f4f2

Browse files
authored
Merge branch 'main' into screenshot-tool-enhancement
2 parents 411cf6c + ee35f20 commit a44f4f2

24 files changed

Lines changed: 1186 additions & 109 deletions

docs/tool-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@
258258
**Parameters:**
259259

260260
- **reqid** (number) _(optional)_: The reqid of the network request. If omitted returns the currently selected request in the DevTools Network panel.
261+
- **requestFilePath** (string) _(optional)_: The absolute or relative path to save the request body to. If omitted, the body is returned inline.
262+
- **responseFilePath** (string) _(optional)_: The absolute or relative path to save the response body to. If omitted, the body is returned inline.
261263

262264
---
263265

scripts/test.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const nodeArgs = [
5353
'spec',
5454
'--test-force-exit',
5555
'--test',
56-
'--test-timeout=30000',
56+
'--test-timeout=60000',
5757
...flags,
5858
...files,
5959
];

src/McpResponse.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ export class McpResponse implements Response {
3434
#includePages = false;
3535
#snapshotParams?: SnapshotParams;
3636
#attachedNetworkRequestId?: number;
37+
#attachedNetworkRequestOptions?: {
38+
requestFilePath?: string;
39+
responseFilePath?: string;
40+
};
3741
#attachedConsoleMessageId?: number;
3842
#textResponseLines: string[] = [];
3943
#images: ImageContentData[] = [];
@@ -125,8 +129,12 @@ export class McpResponse implements Response {
125129
};
126130
}
127131

128-
attachNetworkRequest(reqid: number): void {
132+
attachNetworkRequest(
133+
reqid: number,
134+
options?: {requestFilePath?: string; responseFilePath?: string},
135+
): void {
129136
this.#attachedNetworkRequestId = reqid;
137+
this.#attachedNetworkRequestOptions = options;
130138
}
131139

132140
attachConsoleMessage(msgid: number): void {
@@ -218,6 +226,9 @@ export class McpResponse implements Response {
218226
requestId: this.#attachedNetworkRequestId,
219227
requestIdResolver: req => context.getNetworkRequestStableId(req),
220228
fetchData: true,
229+
requestFilePath: this.#attachedNetworkRequestOptions?.requestFilePath,
230+
responseFilePath: this.#attachedNetworkRequestOptions?.responseFilePath,
231+
saveFile: (data, filename) => context.saveFile(data, filename),
221232
});
222233
detailedNetworkRequest = formatter;
223234
}
@@ -361,6 +372,7 @@ export class McpResponse implements Response {
361372
context.getNetworkRequestStableId(request) ===
362373
this.#networkRequestsOptions?.networkRequestIdInDevToolsUI,
363374
fetchData: false,
375+
saveFile: (data, filename) => context.saveFile(data, filename),
364376
}),
365377
),
366378
);

src/formatters/NetworkFormatter.ts

Lines changed: 77 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,30 @@ export interface NetworkFormatterOptions {
1515
selectedInDevToolsUI?: boolean;
1616
requestIdResolver?: (request: HTTPRequest) => number | string;
1717
fetchData?: boolean;
18+
requestFilePath?: string;
19+
responseFilePath?: string;
20+
saveFile?: (
21+
data: Uint8Array<ArrayBufferLike>,
22+
filename: string,
23+
) => Promise<{filename: string}>;
1824
}
1925

2026
export class NetworkFormatter {
2127
#request: HTTPRequest;
2228
#options: NetworkFormatterOptions;
2329
#requestBody?: string;
2430
#responseBody?: string;
31+
#requestBodyFilePath?: string;
32+
#responseBodyFilePath?: string;
2533

26-
private constructor(
27-
request: HTTPRequest,
28-
options: NetworkFormatterOptions = {},
29-
) {
34+
private constructor(request: HTTPRequest, options: NetworkFormatterOptions) {
3035
this.#request = request;
3136
this.#options = options;
3237
}
3338

3439
static async from(
3540
request: HTTPRequest,
36-
options: NetworkFormatterOptions = {},
41+
options: NetworkFormatterOptions,
3742
): Promise<NetworkFormatter> {
3843
const instance = new NetworkFormatter(request, options);
3944
if (options.fetchData) {
@@ -47,15 +52,40 @@ export class NetworkFormatter {
4752
if (this.#request.hasPostData()) {
4853
const data = this.#request.postData();
4954
if (data) {
50-
this.#requestBody = getSizeLimitedString(data, BODY_CONTEXT_SIZE_LIMIT);
55+
if (this.#options.requestFilePath) {
56+
if (!this.#options.saveFile) {
57+
throw new Error('saveFile is not provided');
58+
}
59+
await this.#options.saveFile(
60+
Buffer.from(data),
61+
this.#options.requestFilePath,
62+
);
63+
this.#requestBodyFilePath = this.#options.requestFilePath;
64+
} else {
65+
this.#requestBody = getSizeLimitedString(
66+
data,
67+
BODY_CONTEXT_SIZE_LIMIT,
68+
);
69+
}
5170
} else {
5271
try {
5372
const fetchData = await this.#request.fetchPostData();
5473
if (fetchData) {
55-
this.#requestBody = getSizeLimitedString(
56-
fetchData,
57-
BODY_CONTEXT_SIZE_LIMIT,
58-
);
74+
if (this.#options.requestFilePath) {
75+
if (!this.#options.saveFile) {
76+
throw new Error('saveFile is not provided');
77+
}
78+
await this.#options.saveFile(
79+
Buffer.from(fetchData),
80+
this.#options.requestFilePath,
81+
);
82+
this.#requestBodyFilePath = this.#options.requestFilePath;
83+
} else {
84+
this.#requestBody = getSizeLimitedString(
85+
fetchData,
86+
BODY_CONTEXT_SIZE_LIMIT,
87+
);
88+
}
5989
}
6090
} catch {
6191
this.#requestBody = '<not available anymore>';
@@ -66,10 +96,17 @@ export class NetworkFormatter {
6696
// Load Response Body
6797
const response = this.#request.response();
6898
if (response) {
69-
this.#responseBody = await this.#getFormattedResponseBody(
70-
response,
71-
BODY_CONTEXT_SIZE_LIMIT,
72-
);
99+
if (this.#options.responseFilePath) {
100+
this.#responseBodyFilePath = await this.#saveResponseBodyToFile(
101+
response,
102+
this.#options.responseFilePath,
103+
);
104+
} else {
105+
this.#responseBody = await this.#getFormattedResponseBody(
106+
response,
107+
BODY_CONTEXT_SIZE_LIMIT,
108+
);
109+
}
73110
}
74111
}
75112

@@ -90,6 +127,9 @@ export class NetworkFormatter {
90127
if (this.#requestBody) {
91128
response.push(`### Request Body`);
92129
response.push(this.#requestBody);
130+
} else if (this.#requestBodyFilePath) {
131+
response.push(`### Request Body`);
132+
response.push(`Saved to ${this.#requestBodyFilePath}.`);
93133
}
94134

95135
const httpResponse = this.#request.response();
@@ -105,6 +145,9 @@ export class NetworkFormatter {
105145
if (this.#responseBody) {
106146
response.push(`### Response Body`);
107147
response.push(this.#responseBody);
148+
} else if (this.#responseBodyFilePath) {
149+
response.push(`### Response Body`);
150+
response.push(`Saved to ${this.#responseBodyFilePath}.`);
108151
}
109152

110153
const httpFailure = this.#request.failure();
@@ -124,6 +167,7 @@ export class NetworkFormatter {
124167
// We create a temporary synchronous instance just for toString
125168
const formatter = new NetworkFormatter(request, {
126169
requestId: id,
170+
saveFile: this.#options.saveFile,
127171
});
128172
response.push(`${' '.repeat(indent)}${formatter.toString()}`);
129173
indent++;
@@ -150,6 +194,7 @@ export class NetworkFormatter {
150194
: undefined;
151195
const formatter = new NetworkFormatter(request, {
152196
requestId: id,
197+
saveFile: this.#options.saveFile,
153198
});
154199
return formatter.toJSON();
155200
});
@@ -158,8 +203,10 @@ export class NetworkFormatter {
158203
...this.toJSON(),
159204
requestHeaders: this.#request.headers(),
160205
requestBody: this.#requestBody,
206+
requestBodyFilePath: this.#requestBodyFilePath,
161207
responseHeaders: this.#request.response()?.headers(),
162208
responseBody: this.#responseBody,
209+
responseBodyFilePath: this.#responseBodyFilePath,
163210
failure: this.#request.failure()?.errorText,
164211
redirectChain: formattedRedirectChain.length
165212
? formattedRedirectChain
@@ -215,6 +262,22 @@ export class NetworkFormatter {
215262
return '<not available anymore>';
216263
}
217264
}
265+
266+
async #saveResponseBodyToFile(
267+
httpResponse: HTTPResponse,
268+
filePath: string,
269+
): Promise<string> {
270+
try {
271+
const responseBuffer = await httpResponse.buffer();
272+
if (!this.#options.saveFile) {
273+
throw new Error('saveFile is not provided');
274+
}
275+
await this.#options.saveFile(responseBuffer, filePath);
276+
return filePath;
277+
} catch {
278+
return '<not available anymore>';
279+
}
280+
}
218281
}
219282

220283
function getSizeLimitedString(text: string, sizeLimit: number) {

src/logger.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,17 @@ export function saveLogsToFile(fileName: string): fs.WriteStream {
3131
return logFile;
3232
}
3333

34+
export function flushLogs(
35+
logFile: fs.WriteStream,
36+
timeoutMs = 2000,
37+
): Promise<void> {
38+
return new Promise((resolve, reject) => {
39+
const timeout = setTimeout(reject, timeoutMs);
40+
logFile.end(() => {
41+
clearTimeout(timeout);
42+
resolve();
43+
});
44+
});
45+
}
46+
3447
export const logger = debug(mcpDebugNamespace);

src/main.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ export const args = parseArguments(VERSION);
3838
const logFile = args.logFile ? saveLogsToFile(args.logFile) : undefined;
3939
let clearcutLogger: ClearcutLogger | undefined;
4040
if (args.usageStatistics) {
41-
clearcutLogger = new ClearcutLogger();
41+
clearcutLogger = new ClearcutLogger({
42+
logFile: args.logFile,
43+
appVersion: VERSION,
44+
});
4245
}
4346

4447
process.on('unhandledRejection', (reason, promise) => {

src/telemetry/clearcut-logger.ts

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,42 +4,75 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import process from 'node:process';
8+
79
import {logger} from '../logger.js';
810

9-
import {ClearcutSender} from './clearcut-sender.js';
1011
import type {LocalState, Persistence} from './persistence.js';
1112
import {FilePersistence} from './persistence.js';
12-
import type {FlagUsage} from './types.js';
13+
import {type FlagUsage, WatchdogMessageType, OsType} from './types.js';
14+
import {WatchdogClient} from './watchdog-client.js';
1315

1416
const MS_PER_DAY = 24 * 60 * 60 * 1000;
1517

18+
function detectOsType(): OsType {
19+
switch (process.platform) {
20+
case 'win32':
21+
return OsType.OS_TYPE_WINDOWS;
22+
case 'darwin':
23+
return OsType.OS_TYPE_MACOS;
24+
case 'linux':
25+
return OsType.OS_TYPE_LINUX;
26+
default:
27+
return OsType.OS_TYPE_UNSPECIFIED;
28+
}
29+
}
30+
1631
export class ClearcutLogger {
1732
#persistence: Persistence;
18-
#sender: ClearcutSender;
33+
#watchdog: WatchdogClient;
1934

20-
constructor(options?: {persistence?: Persistence; sender?: ClearcutSender}) {
21-
this.#persistence = options?.persistence ?? new FilePersistence();
22-
this.#sender = options?.sender ?? new ClearcutSender();
35+
constructor(options: {
36+
appVersion: string;
37+
logFile?: string;
38+
persistence?: Persistence;
39+
watchdogClient?: WatchdogClient;
40+
}) {
41+
this.#persistence = options.persistence ?? new FilePersistence();
42+
this.#watchdog =
43+
options.watchdogClient ??
44+
new WatchdogClient({
45+
parentPid: process.pid,
46+
appVersion: options.appVersion,
47+
osType: detectOsType(),
48+
logFile: options.logFile,
49+
});
2350
}
2451

2552
async logToolInvocation(args: {
2653
toolName: string;
2754
success: boolean;
2855
latencyMs: number;
2956
}): Promise<void> {
30-
await this.#sender.send({
31-
tool_invocation: {
32-
tool_name: args.toolName,
33-
success: args.success,
34-
latency_ms: args.latencyMs,
57+
this.#watchdog.send({
58+
type: WatchdogMessageType.LOG_EVENT,
59+
payload: {
60+
tool_invocation: {
61+
tool_name: args.toolName,
62+
success: args.success,
63+
latency_ms: args.latencyMs,
64+
},
3565
},
3666
});
3767
}
3868

3969
async logServerStart(flagUsage: FlagUsage): Promise<void> {
40-
await this.#sender.send({
41-
server_start: {
42-
flag_usage: flagUsage,
70+
this.#watchdog.send({
71+
type: WatchdogMessageType.LOG_EVENT,
72+
payload: {
73+
server_start: {
74+
flag_usage: flagUsage,
75+
},
4376
},
4477
});
4578
}
@@ -57,13 +90,15 @@ export class ClearcutLogger {
5790
daysSince = Math.ceil(diffTime / MS_PER_DAY);
5891
}
5992

60-
await this.#sender.send({
61-
daily_active: {
62-
days_since_last_active: daysSince,
93+
this.#watchdog.send({
94+
type: WatchdogMessageType.LOG_EVENT,
95+
payload: {
96+
daily_active: {
97+
days_since_last_active: daysSince,
98+
},
6399
},
64100
});
65101

66-
// Update persistence
67102
state.lastActive = new Date().toISOString();
68103
await this.#persistence.saveState(state);
69104
}

src/telemetry/clearcut-sender.ts

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)