Skip to content

Commit ca993a0

Browse files
committed
feat: report navigated URL in action responses
Report the new page URL when an action (click, fill, press_key, etc.) triggers a navigation. The URL is detected by comparing the page URL before and after waitForEventsAfterAction. Fixes #243
1 parent 5aa6437 commit ca993a0

8 files changed

Lines changed: 136 additions & 20 deletions

File tree

src/McpPage.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,15 +121,21 @@ export class McpPage implements ContextPage {
121121
return new WaitForHelper(this.pptrPage, cpuMultiplier, networkMultiplier);
122122
}
123123

124-
waitForEventsAfterAction(
124+
async waitForEventsAfterAction(
125125
action: () => Promise<unknown>,
126126
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
127-
): Promise<void> {
127+
): Promise<{navigatedToUrl?: string}> {
128+
const urlBefore = this.pptrPage.url();
128129
const helper = this.createWaitForHelper(
129130
this.cpuThrottlingRate,
130131
getNetworkMultiplierFromString(this.networkConditions),
131132
);
132-
return helper.waitForEventsAfterAction(action, options);
133+
await helper.waitForEventsAfterAction(action, options);
134+
const urlAfter = this.pptrPage.url();
135+
if (urlAfter !== urlBefore) {
136+
return {navigatedToUrl: urlAfter};
137+
}
138+
return {};
133139
}
134140

135141
dispose(): void {

src/WaitForHelper.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,3 +196,12 @@ export function getNetworkMultiplierFromString(
196196
}
197197
return 1;
198198
}
199+
200+
export function appendNavigatedToUrl(
201+
response: {appendResponseLine(value: string): void},
202+
result: {navigatedToUrl?: string},
203+
): void {
204+
if (result.navigatedToUrl) {
205+
response.appendResponseLine(`Navigated to ${result.navigatedToUrl}`);
206+
}
207+
}

src/tools/ToolDefinition.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ export type ContextPage = Readonly<{
254254
waitForEventsAfterAction(
255255
action: () => Promise<unknown>,
256256
options?: {timeout?: number; handleDialog?: 'accept' | 'dismiss' | string},
257-
): Promise<void>;
257+
): Promise<{navigatedToUrl?: string}>;
258258
getInPageTools(): ToolGroup<InPageToolDefinition> | undefined;
259259
executeInPageTool(
260260
toolName: string,

src/tools/input.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {zod} from '../third_party/index.js';
1010
import type {ElementHandle, KeyInput} from '../third_party/index.js';
1111
import type {TextSnapshotNode} from '../types.js';
1212
import {parseKey} from '../utils/keyboard.js';
13+
import {appendNavigatedToUrl} from '../WaitForHelper.js';
1314

1415
import {ToolCategory} from './categories.js';
1516
import type {ContextPage} from './ToolDefinition.js';
@@ -62,7 +63,7 @@ export const click = definePageTool({
6263
const uid = request.params.uid;
6364
const handle = await request.page.getElementByUid(uid);
6465
try {
65-
await request.page.waitForEventsAfterAction(async () => {
66+
const result = await request.page.waitForEventsAfterAction(async () => {
6667
await handle.asLocator().click({
6768
count: request.params.dblClick ? 2 : 1,
6869
});
@@ -72,6 +73,7 @@ export const click = definePageTool({
7273
? `Successfully double clicked on the element`
7374
: `Successfully clicked on the element`,
7475
);
76+
appendNavigatedToUrl(response, result);
7577
if (request.params.includeSnapshot) {
7678
response.includeSnapshot();
7779
}
@@ -99,7 +101,7 @@ export const clickAt = definePageTool({
99101
},
100102
handler: async (request, response) => {
101103
const page = request.page;
102-
await page.waitForEventsAfterAction(async () => {
104+
const result = await page.waitForEventsAfterAction(async () => {
103105
await page.pptrPage.mouse.click(request.params.x, request.params.y, {
104106
clickCount: request.params.dblClick ? 2 : 1,
105107
});
@@ -109,6 +111,7 @@ export const clickAt = definePageTool({
109111
? `Successfully double clicked at the coordinates`
110112
: `Successfully clicked at the coordinates`,
111113
);
114+
appendNavigatedToUrl(response, result);
112115
if (request.params.includeSnapshot) {
113116
response.includeSnapshot();
114117
}
@@ -134,10 +137,11 @@ export const hover = definePageTool({
134137
const uid = request.params.uid;
135138
const handle = await request.page.getElementByUid(uid);
136139
try {
137-
await request.page.waitForEventsAfterAction(async () => {
140+
const result = await request.page.waitForEventsAfterAction(async () => {
138141
await handle.asLocator().hover();
139142
});
140143
response.appendResponseLine(`Successfully hovered over the element`);
144+
appendNavigatedToUrl(response, result);
141145
if (request.params.includeSnapshot) {
142146
response.includeSnapshot();
143147
}
@@ -235,7 +239,7 @@ export const fill = definePageTool({
235239
},
236240
handler: async (request, response, context) => {
237241
const page = request.page;
238-
await page.waitForEventsAfterAction(async () => {
242+
const result = await page.waitForEventsAfterAction(async () => {
239243
await fillFormElement(
240244
request.params.uid,
241245
request.params.value,
@@ -244,6 +248,7 @@ export const fill = definePageTool({
244248
);
245249
});
246250
response.appendResponseLine(`Successfully filled out the element`);
251+
appendNavigatedToUrl(response, result);
247252
if (request.params.includeSnapshot) {
248253
response.includeSnapshot();
249254
}
@@ -263,7 +268,7 @@ export const typeText = definePageTool({
263268
},
264269
handler: async (request, response) => {
265270
const page = request.page;
266-
await page.waitForEventsAfterAction(async () => {
271+
const result = await page.waitForEventsAfterAction(async () => {
267272
await page.pptrPage.keyboard.type(request.params.text);
268273
if (request.params.submitKey) {
269274
await page.pptrPage.keyboard.press(
@@ -274,6 +279,7 @@ export const typeText = definePageTool({
274279
response.appendResponseLine(
275280
`Typed text "${request.params.text}${request.params.submitKey ? ` + ${request.params.submitKey}` : ''}"`,
276281
);
282+
appendNavigatedToUrl(response, result);
277283
},
278284
});
279285

@@ -295,12 +301,13 @@ export const drag = definePageTool({
295301
);
296302
const toHandle = await request.page.getElementByUid(request.params.to_uid);
297303
try {
298-
await request.page.waitForEventsAfterAction(async () => {
304+
const result = await request.page.waitForEventsAfterAction(async () => {
299305
await fromHandle.drag(toHandle);
300306
await new Promise(resolve => setTimeout(resolve, 50));
301307
await toHandle.drop(fromHandle);
302308
});
303309
response.appendResponseLine(`Successfully dragged an element`);
310+
appendNavigatedToUrl(response, result);
304311
if (request.params.includeSnapshot) {
305312
response.includeSnapshot();
306313
}
@@ -332,17 +339,22 @@ export const fillForm = definePageTool({
332339
},
333340
handler: async (request, response, context) => {
334341
const page = request.page;
342+
let lastResult: {navigatedToUrl?: string} = {};
335343
for (const element of request.params.elements) {
336-
await page.waitForEventsAfterAction(async () => {
344+
const result = await page.waitForEventsAfterAction(async () => {
337345
await fillFormElement(
338346
element.uid,
339347
element.value,
340348
context as McpContext,
341349
page,
342350
);
343351
});
352+
if (result.navigatedToUrl) {
353+
lastResult = result;
354+
}
344355
}
345356
response.appendResponseLine(`Successfully filled out the form`);
357+
appendNavigatedToUrl(response, lastResult);
346358
if (request.params.includeSnapshot) {
347359
response.includeSnapshot();
348360
}
@@ -419,7 +431,7 @@ export const pressKey = definePageTool({
419431
const tokens = parseKey(request.params.key);
420432
const [key, ...modifiers] = tokens;
421433

422-
await page.waitForEventsAfterAction(async () => {
434+
const result = await page.waitForEventsAfterAction(async () => {
423435
for (const modifier of modifiers) {
424436
await page.pptrPage.keyboard.down(modifier);
425437
}
@@ -432,6 +444,7 @@ export const pressKey = definePageTool({
432444
response.appendResponseLine(
433445
`Successfully pressed key: ${request.params.key}`,
434446
);
447+
appendNavigatedToUrl(response, result);
435448
if (request.params.includeSnapshot) {
436449
response.includeSnapshot();
437450
}

src/tools/pages.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ export const newPage = defineTool(args => {
201201
request.params.timeout,
202202
);
203203

204+
response.appendResponseLine(
205+
`Successfully navigated to ${page.pptrPage.url()}.`,
206+
);
204207
response.setIncludePages(true);
205208
response.setListInPageTools();
206209
},
@@ -303,8 +306,9 @@ export const navigatePage = definePageTool(args => {
303306
}
304307
try {
305308
await page.pptrPage.goto(request.params.url, options);
309+
const finalUrl = page.pptrPage.url();
306310
response.appendResponseLine(
307-
`Successfully navigated to ${request.params.url}.`,
311+
`Successfully navigated to ${finalUrl}.`,
308312
);
309313
} catch (error) {
310314
response.appendResponseLine(

src/tools/script.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import {zod} from '../third_party/index.js';
88
import type {Frame, JSHandle, Page, WebWorker} from '../third_party/index.js';
99
import type {ExtensionServiceWorker} from '../types.js';
10+
import {appendNavigatedToUrl} from '../WaitForHelper.js';
1011

1112
import {ToolCategory} from './categories.js';
1213
import type {Context, Response} from './ToolDefinition.js';
@@ -84,12 +85,15 @@ Example with arguments: \`(el) => {
8485
}
8586

8687
const worker = await getWebWorker(context, serviceWorkerId);
87-
await context.getSelectedMcpPage().waitForEventsAfterAction(
88-
async () => {
89-
await performEvaluation(worker, fnString, [], response);
90-
},
91-
{handleDialog: dialogAction ?? 'accept'},
92-
);
88+
const result = await context
89+
.getSelectedMcpPage()
90+
.waitForEventsAfterAction(
91+
async () => {
92+
await performEvaluation(worker, fnString, [], response);
93+
},
94+
{handleDialog: dialogAction ?? 'accept'},
95+
);
96+
appendNavigatedToUrl(response, result);
9397
return;
9498
}
9599

@@ -109,12 +113,13 @@ Example with arguments: \`(el) => {
109113

110114
const evaluatable = await getPageOrFrame(page, frames);
111115

112-
await mcpPage.waitForEventsAfterAction(
116+
const result = await mcpPage.waitForEventsAfterAction(
113117
async () => {
114118
await performEvaluation(evaluatable, fnString, args, response);
115119
},
116120
{handleDialog: dialogAction ?? 'accept'},
117121
);
122+
appendNavigatedToUrl(response, result);
118123
} finally {
119124
void Promise.allSettled(args.map(arg => arg.dispose()));
120125
}

tests/tools/input.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,64 @@ describe('input', () => {
130130
});
131131
});
132132

133+
it('reports navigated URL after click', async () => {
134+
server.addHtmlRoute('/nav-link', html`<a href="/nav-target">Go</a>`);
135+
server.addHtmlRoute('/nav-target', html`<main>Target</main>`);
136+
137+
await withMcpContext(async (response, context) => {
138+
const page = context.getSelectedPptrPage();
139+
await page.goto(server.getRoute('/nav-link'));
140+
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
141+
context.getSelectedMcpPage(),
142+
);
143+
await click.handler(
144+
{
145+
params: {uid: '1_1'},
146+
page: context.getSelectedMcpPage(),
147+
},
148+
response,
149+
context,
150+
);
151+
assert.strictEqual(
152+
response.responseLines[0],
153+
'Successfully clicked on the element',
154+
);
155+
assert.ok(
156+
response.responseLines[1]?.startsWith('Navigated to '),
157+
`Expected "Navigated to" but got: ${response.responseLines[1]}`,
158+
);
159+
assert.ok(
160+
response.responseLines[1]?.includes('/nav-target'),
161+
`Expected URL to contain /nav-target but got: ${response.responseLines[1]}`,
162+
);
163+
});
164+
});
165+
166+
it('does not report navigated URL when no navigation occurs', async () => {
167+
await withMcpContext(async (response, context) => {
168+
const page = context.getSelectedPptrPage();
169+
await page.setContent(
170+
html`<button onclick="this.innerText = 'clicked';">test</button>`,
171+
);
172+
context.getSelectedMcpPage().textSnapshot = await TextSnapshot.create(
173+
context.getSelectedMcpPage(),
174+
);
175+
await click.handler(
176+
{
177+
params: {uid: '1_1'},
178+
page: context.getSelectedMcpPage(),
179+
},
180+
response,
181+
context,
182+
);
183+
assert.strictEqual(response.responseLines.length, 1);
184+
assert.strictEqual(
185+
response.responseLines[0],
186+
'Successfully clicked on the element',
187+
);
188+
});
189+
});
190+
133191
it('waits for stable DOM', async () => {
134192
server.addHtmlRoute(
135193
'/unstable',

tests/tools/pages.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,27 @@ describe('pages', () => {
535535
});
536536
});
537537

538+
it('reports final URL after navigation', async () => {
539+
await withMcpContext(async (response, context) => {
540+
await navigatePage().handler(
541+
{
542+
params: {url: 'data:text/html,<div>Hello</div>'},
543+
page: context.getSelectedMcpPage(),
544+
},
545+
response,
546+
context,
547+
);
548+
assert.ok(
549+
response.responseLines[0]?.startsWith('Successfully navigated to '),
550+
`Expected "Successfully navigated to" but got: ${response.responseLines[0]}`,
551+
);
552+
assert.ok(
553+
response.responseLines[0]?.includes('data:text/html'),
554+
`Expected URL in response but got: ${response.responseLines[0]}`,
555+
);
556+
});
557+
});
558+
538559
it('throws an error if the page was closed not by the MCP server', async () => {
539560
await withMcpContext(async (response, context) => {
540561
const page = await context.newPage();

0 commit comments

Comments
 (0)