openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
363 lines • 14.6 kB
JavaScript
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { zod } from '../third_party/index.js';
import { logger } from '../utils/logger.js';
import { validateUrl } from '../utils/url.js';
import { ToolCategory } from './categories.js';
import { CLOSE_PAGE_ERROR, definePageTool, defineTool, timeoutSchema, } from './ToolDefinition.js';
export const listPages = defineTool((args) => {
return {
name: 'list_pages',
description: `Get a list of pages${args?.categoryExtensions ? ' including extension service workers' : ''} open in the browser.`,
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: true,
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (_request, response) => {
response.setIncludePages(true);
response.setListThirdPartyDeveloperTools();
response.setListWebMcpTools();
},
};
});
export const selectPage = defineTool((args) => ({
name: 'select_page',
description: `Select a page as a context for future tool calls.`,
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: true,
},
schema: {
pageId: zod
.number()
.describe(`The ID of the page to select. Call ${listPages(args).name} to get available pages.`),
bringToFront: zod
.boolean()
.optional()
.describe('Whether to focus the page and bring it to the top.'),
},
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (request, response, context) => {
const page = context.getPageById(request.params.pageId);
context.selectPage(page);
response.setIncludePages(true);
response.setListThirdPartyDeveloperTools();
response.setListWebMcpTools();
if (request.params.bringToFront) {
await page.pptrPage.bringToFront();
}
},
}));
export const closePage = defineTool(() => ({
name: 'close_page',
description: `Closes the page by its index. The last open page cannot be closed.`,
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: false,
},
schema: {
pageId: zod
.number()
.describe('The ID of the page to close. Call list_pages to list pages.'),
},
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (request, response, context) => {
try {
await context.closePage(request.params.pageId);
}
catch (err) {
if (err.message === CLOSE_PAGE_ERROR) {
response.appendResponseLine(err.message);
}
else {
throw err;
}
}
response.setIncludePages(true);
response.setListThirdPartyDeveloperTools();
},
}));
export const newPage = defineTool((args) => {
return {
name: 'new_page',
description: `Open a new tab and load a URL. Use project URL if not specified otherwise.`,
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: false,
},
schema: {
url: zod.string().describe('URL to load in a new page.'),
background: zod
.boolean()
.optional()
.describe('Whether to open the page in the background without bringing it to the front. Default is false (foreground).'),
isolatedContext: zod
.string()
.optional()
.describe('If specified, the page is created in an isolated browser context with the given name. ' +
'Pages in the same browser context share cookies and storage. ' +
'Pages in different browser contexts are fully isolated (useful for clean-slate testing of cookies and authentication).'),
...timeoutSchema,
},
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (request, response, context) => {
validateUrl(request.params.url, {
javascriptEvaluation: args?.javascriptEvaluation,
categoryExtensions: args?.categoryExtensions,
});
const page = await context.newPage(request.params.background, request.params.isolatedContext);
await page.waitForEventsAfterAction(async () => {
await page.pptrPage.goto(request.params.url, {
timeout: request.params.timeout,
});
}, { timeout: request.params.timeout });
response.setIncludePages(true);
response.setListThirdPartyDeveloperTools();
},
};
});
export const navigatePage = definePageTool((args) => {
return {
name: 'navigate_page',
description: `Go to a URL, or back, forward, or reload. Use project URL if not specified otherwise.`,
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: false,
},
schema: {
type: zod
.enum(['url', 'back', 'forward', 'reload'])
.optional()
.describe('Navigate the page by URL, back or forward in history, or reload.'),
url: zod.string().optional().describe('Target URL (only type=url)'),
ignoreCache: zod
.boolean()
.optional()
.describe('Whether to ignore cache on reload.'),
handleBeforeUnload: zod
.enum(['accept', 'dismiss'])
.optional()
.describe('Whether to auto accept or beforeunload dialogs triggered by this navigation. Default is accept.'),
...(args?.javascriptEvaluation !== false
? {
initScript: zod
.string()
.optional()
.describe('A JavaScript script to be executed on each new document before any other scripts for the next navigation.'),
}
: {}),
...timeoutSchema,
},
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (request, response) => {
const page = request.page;
const options = {
timeout: request.params.timeout,
};
if (!request.params.type && !request.params.url) {
throw new Error('Either URL or a type is required.');
}
if (!request.params.type) {
request.params.type = 'url';
}
if (request.params.url) {
validateUrl(request.params.url, {
javascriptEvaluation: args?.javascriptEvaluation,
categoryExtensions: args?.categoryExtensions,
});
}
let initScriptId;
if (request.params.initScript) {
const { identifier } = await page.pptrPage.evaluateOnNewDocument(request.params.initScript);
initScriptId = identifier;
}
try {
const action = request.params.handleBeforeUnload ?? 'accept';
const result = await page.waitForEventsAfterAction(async () => {
switch (request.params.type) {
case 'url':
if (!request.params.url) {
throw new Error('A URL is required for navigation of type=url.');
}
try {
await page.pptrPage.goto(request.params.url, options);
response.appendResponseLine(`Successfully navigated to ${request.params.url}.`);
}
catch (error) {
response.appendResponseLine(`Unable to navigate in the selected page: ${error.message}.`);
}
break;
case 'back':
try {
await page.pptrPage.goBack(options);
response.appendResponseLine(`Successfully navigated back to ${page.pptrPage.url()}.`);
}
catch (error) {
response.appendResponseLine(`Unable to navigate back in the selected page: ${error.message}.`);
}
break;
case 'forward':
try {
await page.pptrPage.goForward(options);
response.appendResponseLine(`Successfully navigated forward to ${page.pptrPage.url()}.`);
}
catch (error) {
response.appendResponseLine(`Unable to navigate forward in the selected page: ${error.message}.`);
}
break;
case 'reload':
try {
await page.pptrPage.reload({
...options,
ignoreCache: request.params.ignoreCache,
});
response.appendResponseLine(`Successfully reloaded the page.`);
}
catch (error) {
response.appendResponseLine(`Unable to reload the selected page: ${error.message}.`);
}
break;
}
}, {
timeout: request.params.timeout,
handleDialog: { beforeunload: action },
});
if (result.dialogHandled) {
response.appendResponseLine(`${action === 'dismiss' ? 'Dismissed' : 'Accepted'} a beforeunload dialog.`);
page.clearDialog();
}
}
finally {
if (initScriptId) {
await page.pptrPage
.removeScriptToEvaluateOnNewDocument(initScriptId)
.catch(error => {
logger?.(`Failed to remove init script`, error);
});
}
}
response.setIncludePages(true);
response.setListThirdPartyDeveloperTools();
response.setListWebMcpTools();
},
};
});
export const resizePage = definePageTool(() => ({
name: 'resize_page',
description: `Resizes the page's window so that the page has specified dimension`,
annotations: {
category: ToolCategory.EMULATION,
readOnlyHint: false,
},
schema: {
width: zod.number().describe('Page width'),
height: zod.number().describe('Page height'),
},
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (request, response) => {
const page = request.page;
try {
const browser = page.pptrPage.browser();
const windowId = await page.pptrPage.windowId();
const bounds = await browser.getWindowBounds(windowId);
if (bounds.windowState === 'fullscreen') {
// Have to call this twice on Ubuntu when the window is in fullscreen mode.
await browser.setWindowBounds(windowId, { windowState: 'normal' });
await browser.setWindowBounds(windowId, { windowState: 'normal' });
}
else if (bounds.windowState !== 'normal') {
await browser.setWindowBounds(windowId, { windowState: 'normal' });
}
}
catch {
// Window APIs are not supported on all platforms
}
await page.pptrPage.resize({
contentWidth: request.params.width,
contentHeight: request.params.height,
});
response.setIncludePages(true);
},
}));
export const handleDialog = definePageTool(() => ({
name: 'handle_dialog',
description: `If a browser dialog was opened, use this command to handle it`,
annotations: {
category: ToolCategory.INPUT,
readOnlyHint: false,
},
schema: {
action: zod
.enum(['accept', 'dismiss'])
.describe('Whether to dismiss or accept the dialog'),
promptText: zod
.string()
.optional()
.describe('Optional prompt text to enter into the dialog.'),
},
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (request, response) => {
const page = request.page;
const dialog = page.getDialog();
if (!dialog) {
throw new Error('No open dialog found');
}
switch (request.params.action) {
case 'accept': {
try {
await dialog.accept(request.params.promptText);
}
catch (err) {
// Likely already handled by the user outside of MCP.
logger?.(err);
}
response.appendResponseLine('Successfully accepted the dialog');
break;
}
case 'dismiss': {
try {
await dialog.dismiss();
}
catch (err) {
// Likely already handled.
logger?.(err);
}
response.appendResponseLine('Successfully dismissed the dialog');
break;
}
}
page.clearDialog();
response.setIncludePages(true);
},
}));
export const getTabId = definePageTool(() => ({
name: 'get_tab_id',
description: `Get the tab ID of the page`,
annotations: {
category: ToolCategory.NAVIGATION,
readOnlyHint: true,
conditions: ['experimentalInteropTools'],
},
schema: {},
blockedByDialog: false,
verifyFilesSchema: {},
handler: async (request, response) => {
const page = request.page;
const tabId = page.pptrPage._tabId;
response.setTabId(tabId);
response.appendResponseLine(`Tab ID: ${tabId}`);
},
}));
//# sourceMappingURL=pages.js.map