Skip to content

Commit dd3c143

Browse files
nroscinoNicholas RoscinoLightning00Blade
authored
feat: add request and response body (#267)
This PR adds the request and response bodies into the context if they are available and in a readable format. --------- Co-authored-by: Nicholas Roscino <nroscino@chromium.org> Co-authored-by: Nikolay Vitkov <34244704+Lightning00Blade@users.noreply.github.com>
1 parent 0c3b019 commit dd3c143

File tree

5 files changed

+299
-8
lines changed

5 files changed

+299
-8
lines changed

src/McpResponse.ts

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import type {ResourceType} from 'puppeteer-core';
1212
import {formatConsoleEvent} from './formatters/consoleFormatter.js';
1313
import {
1414
getFormattedHeaderValue,
15+
getFormattedResponseBody,
16+
getFormattedRequestBody,
1517
getShortDescriptionForRequest,
1618
getStatusFromRequest,
1719
} from './formatters/networkFormatter.js';
@@ -21,10 +23,16 @@ import {handleDialog} from './tools/pages.js';
2123
import type {ImageContentData, Response} from './tools/ToolDefinition.js';
2224
import {paginate, type PaginationOptions} from './utils/pagination.js';
2325

26+
interface NetworkRequestData {
27+
networkRequestUrl: string;
28+
requestBody?: string;
29+
responseBody?: string;
30+
}
31+
2432
export class McpResponse implements Response {
2533
#includePages = false;
2634
#includeSnapshot = false;
27-
#attachedNetworkRequestUrl?: string;
35+
#attachedNetworkRequestData?: NetworkRequestData;
2836
#includeConsoleData = false;
2937
#textResponseLines: string[] = [];
3038
#formattedConsoleData?: string[];
@@ -74,7 +82,9 @@ export class McpResponse implements Response {
7482
}
7583

7684
attachNetworkRequest(url: string): void {
77-
this.#attachedNetworkRequestUrl = url;
85+
this.#attachedNetworkRequestData = {
86+
networkRequestUrl: url,
87+
};
7888
}
7989

8090
get includePages(): boolean {
@@ -89,7 +99,7 @@ export class McpResponse implements Response {
8999
return this.#includeConsoleData;
90100
}
91101
get attachedNetworkRequestUrl(): string | undefined {
92-
return this.#attachedNetworkRequestUrl;
102+
return this.#attachedNetworkRequestData?.networkRequestUrl;
93103
}
94104
get networkRequestsPageIdx(): number | undefined {
95105
return this.#networkRequestsOptions?.pagination?.pageIdx;
@@ -127,6 +137,22 @@ export class McpResponse implements Response {
127137
}
128138

129139
let formattedConsoleMessages: string[];
140+
141+
if (this.#attachedNetworkRequestData?.networkRequestUrl) {
142+
const request = context.getNetworkRequestByUrl(
143+
this.#attachedNetworkRequestData.networkRequestUrl,
144+
);
145+
146+
this.#attachedNetworkRequestData.requestBody =
147+
await getFormattedRequestBody(request);
148+
149+
const response = request.response();
150+
if (response) {
151+
this.#attachedNetworkRequestData.responseBody =
152+
await getFormattedResponseBody(response);
153+
}
154+
}
155+
130156
if (this.#includeConsoleData) {
131157
const consoleMessages = context.getConsoleData();
132158
if (consoleMessages) {
@@ -274,10 +300,11 @@ Call ${handleDialog.name} to handle it before continuing.`);
274300

275301
#getIncludeNetworkRequestsData(context: McpContext): string[] {
276302
const response: string[] = [];
277-
const url = this.#attachedNetworkRequestUrl;
303+
const url = this.#attachedNetworkRequestData?.networkRequestUrl;
278304
if (!url) {
279305
return response;
280306
}
307+
281308
const httpRequest = context.getNetworkRequestByUrl(url);
282309
response.push(`## Request ${httpRequest.url()}`);
283310
response.push(`Status: ${getStatusFromRequest(httpRequest)}`);
@@ -286,6 +313,11 @@ Call ${handleDialog.name} to handle it before continuing.`);
286313
response.push(line);
287314
}
288315

316+
if (this.#attachedNetworkRequestData?.requestBody) {
317+
response.push(`### Request Body`);
318+
response.push(this.#attachedNetworkRequestData.requestBody);
319+
}
320+
289321
const httpResponse = httpRequest.response();
290322
if (httpResponse) {
291323
response.push(`### Response Headers`);
@@ -294,6 +326,11 @@ Call ${handleDialog.name} to handle it before continuing.`);
294326
}
295327
}
296328

329+
if (this.#attachedNetworkRequestData?.responseBody) {
330+
response.push(`### Response Body`);
331+
response.push(this.#attachedNetworkRequestData.responseBody);
332+
}
333+
297334
const httpFailure = httpRequest.failure();
298335
if (httpFailure) {
299336
response.push(`### Request failed with`);

src/formatters/networkFormatter.ts

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import type {HTTPRequest} from 'puppeteer-core';
7+
import {isUtf8} from 'node:buffer';
8+
9+
import type {HTTPRequest, HTTPResponse} from 'puppeteer-core';
10+
11+
const BODY_CONTEXT_SIZE_LIMIT = 10000;
812

913
export function getShortDescriptionForRequest(request: HTTPRequest): string {
1014
return `${request.url()} ${request.method()} ${getStatusFromRequest(request)}`;
@@ -37,3 +41,61 @@ export function getFormattedHeaderValue(
3741
}
3842
return response;
3943
}
44+
45+
export async function getFormattedResponseBody(
46+
httpResponse: HTTPResponse,
47+
sizeLimit = BODY_CONTEXT_SIZE_LIMIT,
48+
): Promise<string | undefined> {
49+
try {
50+
const responseBuffer = await httpResponse.buffer();
51+
52+
if (isUtf8(responseBuffer)) {
53+
const responseAsTest = responseBuffer.toString('utf-8');
54+
55+
if (responseAsTest.length === 0) {
56+
return `<empty response>`;
57+
}
58+
59+
return `${getSizeLimitedString(responseAsTest, sizeLimit)}`;
60+
}
61+
62+
return `<binary data>`;
63+
} catch {
64+
// buffer() call might fail with CDP exception, in this case we don't print anything in the context
65+
return;
66+
}
67+
}
68+
69+
export async function getFormattedRequestBody(
70+
httpRequest: HTTPRequest,
71+
sizeLimit: number = BODY_CONTEXT_SIZE_LIMIT,
72+
): Promise<string | undefined> {
73+
if (httpRequest.hasPostData()) {
74+
const data = httpRequest.postData();
75+
76+
if (data) {
77+
return `${getSizeLimitedString(data, sizeLimit)}`;
78+
}
79+
80+
try {
81+
const fetchData = await httpRequest.fetchPostData();
82+
83+
if (fetchData) {
84+
return `${getSizeLimitedString(fetchData, sizeLimit)}`;
85+
}
86+
} catch {
87+
// fetchPostData() call might fail with CDP exception, in this case we don't print anything in the context
88+
return;
89+
}
90+
}
91+
92+
return;
93+
}
94+
95+
function getSizeLimitedString(text: string, sizeLimit: number) {
96+
if (text.length > sizeLimit) {
97+
return `${text.substring(0, sizeLimit) + '... <truncated>'}`;
98+
}
99+
100+
return `${text}`;
101+
}

tests/McpResponse.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import assert from 'node:assert';
77
import {describe, it} from 'node:test';
88

9-
import {getMockRequest, html, withBrowser} from './utils.js';
9+
import {getMockRequest, getMockResponse, html, withBrowser} from './utils.js';
1010

1111
describe('McpResponse', () => {
1212
it('list pages', async () => {
@@ -202,6 +202,51 @@ http://example.com GET [pending]`,
202202
});
203203
});
204204

205+
it('add network request when attached with POST data', async () => {
206+
await withBrowser(async (response, context) => {
207+
response.setIncludeNetworkRequests(true);
208+
const httpResponse = getMockResponse();
209+
httpResponse.buffer = () => {
210+
return Promise.resolve(Buffer.from(JSON.stringify({response: 'body'})));
211+
};
212+
httpResponse.headers = () => {
213+
return {
214+
'Content-Type': 'application/json',
215+
};
216+
};
217+
const request = getMockRequest({
218+
method: 'POST',
219+
hasPostData: true,
220+
postData: JSON.stringify({request: 'body'}),
221+
response: httpResponse,
222+
});
223+
context.getNetworkRequests = () => {
224+
return [request];
225+
};
226+
response.attachNetworkRequest(request.url());
227+
228+
const result = await response.handle('test', context);
229+
230+
assert.strictEqual(
231+
result[0].text,
232+
`# test response
233+
## Request http://example.com
234+
Status: [success - 200]
235+
### Request Headers
236+
- content-size:10
237+
### Request Body
238+
${JSON.stringify({request: 'body'})}
239+
### Response Headers
240+
- Content-Type:application/json
241+
### Response Body
242+
${JSON.stringify({response: 'body'})}
243+
## Network requests
244+
Showing 1-1 of 1 (Page 1 of 1).
245+
http://example.com POST [success - 200]`,
246+
);
247+
});
248+
});
249+
205250
it('add network request when attached', async () => {
206251
await withBrowser(async (response, context) => {
207252
response.setIncludeNetworkRequests(true);

0 commit comments

Comments
 (0)