Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -498,11 +498,12 @@ If you run into any issues, checkout our [troubleshooting guide](./docs/troubles
- [`type_text`](docs/tool-reference.md#type_text)
- [`upload_file`](docs/tool-reference.md#upload_file)
- [`click_at`](docs/tool-reference.md#click_at)
- **Navigation automation** (6 tools)
- **Navigation automation** (7 tools)
- [`close_page`](docs/tool-reference.md#close_page)
- [`list_pages`](docs/tool-reference.md#list_pages)
- [`navigate_page`](docs/tool-reference.md#navigate_page)
- [`new_page`](docs/tool-reference.md#new_page)
- [`select_foreground_page`](docs/tool-reference.md#select_foreground_page)
- [`select_page`](docs/tool-reference.md#select_page)
- [`wait_for`](docs/tool-reference.md#wait_for)
- **Emulation** (2 tools)
Expand Down
11 changes: 10 additions & 1 deletion docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,12 @@
- [`type_text`](#type_text)
- [`upload_file`](#upload_file)
- [`click_at`](#click_at)
- **[Navigation automation](#navigation-automation)** (6 tools)
- **[Navigation automation](#navigation-automation)** (7 tools)
- [`close_page`](#close_page)
- [`list_pages`](#list_pages)
- [`navigate_page`](#navigate_page)
- [`new_page`](#new_page)
- [`select_foreground_page`](#select_foreground_page)
- [`select_page`](#select_page)
- [`wait_for`](#wait_for)
- **[Emulation](#emulation)** (2 tools)
Expand Down Expand Up @@ -230,6 +231,14 @@

---

### `select_foreground_page`

**Description:** Detect and select the browser tab currently visible to the user. This queries every open page for its visibility state and selects the foreground tab. Useful when connecting to an existing browser where the MCP-selected page may differ from the tab the user is actually viewing.

**Parameters:** None

---

### `select_page`

**Description:** Select a page as a context for future tool calls.
Expand Down
38 changes: 38 additions & 0 deletions src/McpContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,44 @@ export class McpContext implements Context {
newPage.updateTimeouts();
}

/**
* Finds the page whose tab is currently in the foreground (visible to the
* user) by querying `document.visibilityState` on every known page. If
* exactly one page reports `"visible"`, it is selected and returned.
*
* Returns `undefined` when no visible page can be determined (e.g. all pages
* are hidden, multiple pages report visible, or evaluation fails).
*/
async selectForegroundPage(): Promise<{id: number; url: string} | undefined> {
const pages = Array.from(this.#mcpPages.values());
if (pages.length === 0) {
return undefined;
}

const results = await Promise.allSettled(
pages.map(async mcpPage => {
const visible = await mcpPage.pptrPage.evaluate(
() => document.visibilityState === 'visible',
);
return {mcpPage, visible};
}),
);

const visiblePages: McpPage[] = [];
for (const result of results) {
if (result.status === 'fulfilled' && result.value.visible) {
visiblePages.push(result.value.mcpPage);
}
}

if (visiblePages.length === 1) {
this.selectPage(visiblePages[0]);
return {id: visiblePages[0].id, url: visiblePages[0].pptrPage.url()};
}

return undefined;
}

/**
* Returns details about the last page snapshot automatically replacing the
* selection because the selected page disappeared from the page list, or
Expand Down
6 changes: 6 additions & 0 deletions src/bin/chrome-devtools-cli-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,12 @@ export const commands: Commands = {
category: 'Debugging',
args: {},
},
select_foreground_page: {
description:
'Detect and select the browser tab currently visible to the user. This queries every open page for its visibility state and selects the foreground tab. Useful when connecting to an existing browser where the MCP-selected page may differ from the tab the user is actually viewing.',
category: 'Navigation automation',
args: {},
},
select_page: {
description: 'Select a page as a context for future tool calls.',
category: 'Navigation automation',
Expand Down
4 changes: 4 additions & 0 deletions src/telemetry/tool_call_metrics.json
Original file line number Diff line number Diff line change
Expand Up @@ -884,5 +884,9 @@
"argType": "number"
}
]
},
{
"name": "select_foreground_page",
"args": []
}
]
1 change: 1 addition & 0 deletions src/tools/ToolDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ export type Context = Readonly<{
): Promise<ContextPage>;
closePage(pageId: number): Promise<void>;
selectPage(page: ContextPage): void;
selectForegroundPage(): Promise<{id: number; url: string} | undefined>;
saveTemporaryFile(
data: Uint8Array<ArrayBufferLike>,
filename: string,
Expand Down
31 changes: 31 additions & 0 deletions src/tools/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,37 @@ export const selectPage = defineTool({
},
});

export const selectForegroundPage = defineTool({
name: 'select_foreground_page',
description:
`Detect and select the browser tab currently visible to the user. ` +
`This queries every open page for its visibility state and selects the ` +
`foreground tab. Useful when connecting to an existing browser where the ` +
`MCP-selected page may differ from the tab the user is actually viewing.`,
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: true,
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: [],
handler: async (_request, response, context) => {
const result = await context.selectForegroundPage();
if (result) {
response.appendResponseLine(
`Selected page ${result.id} (${result.url}) as the foreground tab.`,
);
} else {
response.appendResponseLine(
`Could not determine a single foreground tab. Use select_page to choose manually.`,
);
}
response.setIncludePages(true);
response.setListThirdPartyDeveloperTools();
response.setListWebMcpTools();
},
});

export const closePage = defineTool({
name: 'close_page',
description: `Closes the page by its index. The last open page cannot be closed.`,
Expand Down
44 changes: 44 additions & 0 deletions tests/tools/pages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
newPage,
closePage,
selectPage,
selectForegroundPage,
navigatePage,
resizePage,
handleDialog,
Expand Down Expand Up @@ -624,6 +625,49 @@ describe('pages', () => {
});
});
});
describe('select_foreground_page', () => {
it('selects the foreground tab when one page is visible', async () => {
await withMcpContext(async (response, context) => {
// Open a second page.
const page2 = await context.newPage();
assert.strictEqual(context.getSelectedMcpPage(), page2);

// Bring the first page to the front so it becomes the visible tab.
await context.getPageById(1).pptrPage.bringToFront();

// select_foreground_page should detect and select page 1.
await selectForegroundPage.handler({params: {}}, response, context);
assert.strictEqual(context.getSelectedMcpPage().id, 1);
assert.ok(response.includePages);
});
});

it('reports failure when visibility cannot be determined', async () => {
await withMcpContext(async (response, context) => {
// Force the visibility probe to fail on the only page so that no
// foreground tab can be determined. This deterministically exercises
// the "undefined" branch regardless of the browser's real visibility
// state.
const page = context.getSelectedMcpPage();
sinon
.stub(page.pptrPage, 'evaluate')
.rejects(new Error('evaluate unavailable'));

await selectForegroundPage.handler({params: {}}, response, context);

const result = await response.handle('select_foreground_page', context);
const textContent = result.content.find(c => c.type === 'text') as {
type: 'text';
text: string;
};
assert.ok(textContent);
// Must be the failure message specifically. The success message also
// contains the word "foreground", so asserting on that alone would
// pass even when the tool selected a page.
assert.ok(textContent.text.includes('Could not determine'));
});
});
});
describe('navigate_page', () => {
it('navigates to correct page', async () => {
await withMcpContext(async (response, context) => {
Expand Down