Skip to content

Commit d58c34c

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 dbddb2e commit d58c34c

6 files changed

Lines changed: 146 additions & 23 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<{navigatedUrl?: 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 {navigatedUrl: urlAfter};
137+
}
138+
return {};
133139
}
134140

135141
dispose(): void {

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<{navigatedUrl?: string}>;
258258
getInPageTools(): ToolGroup<InPageToolDefinition> | undefined;
259259
executeInPageTool(
260260
toolName: string,

src/tools/input.ts

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -62,16 +62,21 @@ export const click = definePageTool({
6262
const uid = request.params.uid;
6363
const handle = await request.page.getElementByUid(uid);
6464
try {
65-
await request.page.waitForEventsAfterAction(async () => {
66-
await handle.asLocator().click({
67-
count: request.params.dblClick ? 2 : 1,
68-
});
69-
});
65+
const {navigatedUrl} = await request.page.waitForEventsAfterAction(
66+
async () => {
67+
await handle.asLocator().click({
68+
count: request.params.dblClick ? 2 : 1,
69+
});
70+
},
71+
);
7072
response.appendResponseLine(
7173
request.params.dblClick
7274
? `Successfully double clicked on the element`
7375
: `Successfully clicked on the element`,
7476
);
77+
if (navigatedUrl) {
78+
response.appendResponseLine(`Navigated to ${navigatedUrl}`);
79+
}
7580
if (request.params.includeSnapshot) {
7681
response.includeSnapshot();
7782
}
@@ -99,7 +104,7 @@ export const clickAt = definePageTool({
99104
},
100105
handler: async (request, response) => {
101106
const page = request.page;
102-
await page.waitForEventsAfterAction(async () => {
107+
const {navigatedUrl} = await page.waitForEventsAfterAction(async () => {
103108
await page.pptrPage.mouse.click(request.params.x, request.params.y, {
104109
clickCount: request.params.dblClick ? 2 : 1,
105110
});
@@ -109,6 +114,9 @@ export const clickAt = definePageTool({
109114
? `Successfully double clicked at the coordinates`
110115
: `Successfully clicked at the coordinates`,
111116
);
117+
if (navigatedUrl) {
118+
response.appendResponseLine(`Navigated to ${navigatedUrl}`);
119+
}
112120
if (request.params.includeSnapshot) {
113121
response.includeSnapshot();
114122
}
@@ -134,10 +142,15 @@ export const hover = definePageTool({
134142
const uid = request.params.uid;
135143
const handle = await request.page.getElementByUid(uid);
136144
try {
137-
await request.page.waitForEventsAfterAction(async () => {
138-
await handle.asLocator().hover();
139-
});
145+
const {navigatedUrl} = await request.page.waitForEventsAfterAction(
146+
async () => {
147+
await handle.asLocator().hover();
148+
},
149+
);
140150
response.appendResponseLine(`Successfully hovered over the element`);
151+
if (navigatedUrl) {
152+
response.appendResponseLine(`Navigated to ${navigatedUrl}`);
153+
}
141154
if (request.params.includeSnapshot) {
142155
response.includeSnapshot();
143156
}
@@ -235,7 +248,7 @@ export const fill = definePageTool({
235248
},
236249
handler: async (request, response, context) => {
237250
const page = request.page;
238-
await page.waitForEventsAfterAction(async () => {
251+
const {navigatedUrl} = await page.waitForEventsAfterAction(async () => {
239252
await fillFormElement(
240253
request.params.uid,
241254
request.params.value,
@@ -244,6 +257,9 @@ export const fill = definePageTool({
244257
);
245258
});
246259
response.appendResponseLine(`Successfully filled out the element`);
260+
if (navigatedUrl) {
261+
response.appendResponseLine(`Navigated to ${navigatedUrl}`);
262+
}
247263
if (request.params.includeSnapshot) {
248264
response.includeSnapshot();
249265
}
@@ -263,7 +279,7 @@ export const typeText = definePageTool({
263279
},
264280
handler: async (request, response) => {
265281
const page = request.page;
266-
await page.waitForEventsAfterAction(async () => {
282+
const {navigatedUrl} = await page.waitForEventsAfterAction(async () => {
267283
await page.pptrPage.keyboard.type(request.params.text);
268284
if (request.params.submitKey) {
269285
await page.pptrPage.keyboard.press(
@@ -274,6 +290,9 @@ export const typeText = definePageTool({
274290
response.appendResponseLine(
275291
`Typed text "${request.params.text}${request.params.submitKey ? ` + ${request.params.submitKey}` : ''}"`,
276292
);
293+
if (navigatedUrl) {
294+
response.appendResponseLine(`Navigated to ${navigatedUrl}`);
295+
}
277296
},
278297
});
279298

@@ -295,12 +314,17 @@ export const drag = definePageTool({
295314
);
296315
const toHandle = await request.page.getElementByUid(request.params.to_uid);
297316
try {
298-
await request.page.waitForEventsAfterAction(async () => {
299-
await fromHandle.drag(toHandle);
300-
await new Promise(resolve => setTimeout(resolve, 50));
301-
await toHandle.drop(fromHandle);
302-
});
317+
const {navigatedUrl} = await request.page.waitForEventsAfterAction(
318+
async () => {
319+
await fromHandle.drag(toHandle);
320+
await new Promise(resolve => setTimeout(resolve, 50));
321+
await toHandle.drop(fromHandle);
322+
},
323+
);
303324
response.appendResponseLine(`Successfully dragged an element`);
325+
if (navigatedUrl) {
326+
response.appendResponseLine(`Navigated to ${navigatedUrl}`);
327+
}
304328
if (request.params.includeSnapshot) {
305329
response.includeSnapshot();
306330
}
@@ -332,17 +356,24 @@ export const fillForm = definePageTool({
332356
},
333357
handler: async (request, response, context) => {
334358
const page = request.page;
359+
let lastNavigatedUrl: string | undefined;
335360
for (const element of request.params.elements) {
336-
await page.waitForEventsAfterAction(async () => {
361+
const {navigatedUrl} = await page.waitForEventsAfterAction(async () => {
337362
await fillFormElement(
338363
element.uid,
339364
element.value,
340365
context as McpContext,
341366
page,
342367
);
343368
});
369+
if (navigatedUrl) {
370+
lastNavigatedUrl = navigatedUrl;
371+
}
344372
}
345373
response.appendResponseLine(`Successfully filled out the form`);
374+
if (lastNavigatedUrl) {
375+
response.appendResponseLine(`Navigated to ${lastNavigatedUrl}`);
376+
}
346377
if (request.params.includeSnapshot) {
347378
response.includeSnapshot();
348379
}
@@ -419,7 +450,7 @@ export const pressKey = definePageTool({
419450
const tokens = parseKey(request.params.key);
420451
const [key, ...modifiers] = tokens;
421452

422-
await page.waitForEventsAfterAction(async () => {
453+
const {navigatedUrl} = await page.waitForEventsAfterAction(async () => {
423454
for (const modifier of modifiers) {
424455
await page.pptrPage.keyboard.down(modifier);
425456
}
@@ -432,6 +463,9 @@ export const pressKey = definePageTool({
432463
response.appendResponseLine(
433464
`Successfully pressed key: ${request.params.key}`,
434465
);
466+
if (navigatedUrl) {
467+
response.appendResponseLine(`Navigated to ${navigatedUrl}`);
468+
}
435469
if (request.params.includeSnapshot) {
436470
response.includeSnapshot();
437471
}

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(

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)