mirror of
https://github.com/microsoft/playwright-mcp.git
synced 2025-07-26 08:32:26 +08:00
chore: introduce capabilities argument (#135)
This commit is contained in:
parent
707ebbf4d4
commit
abd56f514b
@ -68,6 +68,7 @@ The Playwright MCP server supports the following command-line options:
|
|||||||
- Chrome channels: `chrome-beta`, `chrome-canary`, `chrome-dev`
|
- Chrome channels: `chrome-beta`, `chrome-canary`, `chrome-dev`
|
||||||
- Edge channels: `msedge-beta`, `msedge-canary`, `msedge-dev`
|
- Edge channels: `msedge-beta`, `msedge-canary`, `msedge-dev`
|
||||||
- Default: `chrome`
|
- Default: `chrome`
|
||||||
|
- `--caps <caps>`: Comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.
|
||||||
- `--cdp-endpoint <endpoint>`: CDP endpoint to connect to
|
- `--cdp-endpoint <endpoint>`: CDP endpoint to connect to
|
||||||
- `--executable-path <path>`: Path to the browser executable
|
- `--executable-path <path>`: Path to the browser executable
|
||||||
- `--headless`: Run browser in headless mode (headed by default)
|
- `--headless`: Run browser in headless mode (headed by default)
|
||||||
@ -299,7 +300,7 @@ server.connect(transport);
|
|||||||
|
|
||||||
### Files and Media
|
### Files and Media
|
||||||
|
|
||||||
- **browser_choose_file**
|
- **browser_file_upload**
|
||||||
- Description: Choose one or multiple files to upload
|
- Description: Choose one or multiple files to upload
|
||||||
- Parameters:
|
- Parameters:
|
||||||
- `paths` (array): The absolute paths to the files to upload. Can be a single file or multiple files.
|
- `paths` (array): The absolute paths to the files to upload. Can be a single file or multiple files.
|
||||||
|
7
index.d.ts
vendored
7
index.d.ts
vendored
@ -18,6 +18,8 @@
|
|||||||
import type { LaunchOptions } from 'playwright';
|
import type { LaunchOptions } from 'playwright';
|
||||||
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
|
|
||||||
|
type ToolCapability = 'core' | 'tabs' | 'pdf' | 'history' | 'wait' | 'files' | 'install';
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
/**
|
/**
|
||||||
* Path to the user data directory.
|
* Path to the user data directory.
|
||||||
@ -35,6 +37,11 @@ type Options = {
|
|||||||
* @default false
|
* @default false
|
||||||
*/
|
*/
|
||||||
vision?: boolean;
|
vision?: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capabilities to enable.
|
||||||
|
*/
|
||||||
|
capabilities?: ToolCapability[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export function createServer(options?: Options): Server;
|
export function createServer(options?: Options): Server;
|
||||||
|
@ -282,7 +282,7 @@ class PageSnapshot {
|
|||||||
results.push('');
|
results.push('');
|
||||||
}
|
}
|
||||||
if (options?.hasFileChooser) {
|
if (options?.hasFileChooser) {
|
||||||
results.push('- There is a file chooser visible that requires browser_choose_file to be called');
|
results.push('- There is a file chooser visible that requires browser_file_upload to be called');
|
||||||
results.push('');
|
results.push('');
|
||||||
}
|
}
|
||||||
results.push(this._text);
|
results.push(this._text);
|
||||||
|
16
src/index.ts
16
src/index.ts
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
import { createServerWithTools } from './server';
|
import { createServerWithTools } from './server';
|
||||||
import common from './tools/common';
|
import common from './tools/common';
|
||||||
import fileChooser from './tools/fileChooser';
|
import files from './tools/files';
|
||||||
import install from './tools/install';
|
import install from './tools/install';
|
||||||
import keyboard from './tools/keyboard';
|
import keyboard from './tools/keyboard';
|
||||||
import navigate from './tools/navigate';
|
import navigate from './tools/navigate';
|
||||||
@ -24,16 +24,16 @@ import pdf from './tools/pdf';
|
|||||||
import snapshot from './tools/snapshot';
|
import snapshot from './tools/snapshot';
|
||||||
import tabs from './tools/tabs';
|
import tabs from './tools/tabs';
|
||||||
import screen from './tools/screen';
|
import screen from './tools/screen';
|
||||||
import { console } from './resources/console';
|
import { console as consoleResource } from './resources/console';
|
||||||
|
|
||||||
import type { Tool } from './tools/tool';
|
import type { Tool, ToolCapability } from './tools/tool';
|
||||||
import type { Resource } from './resources/resource';
|
import type { Resource } from './resources/resource';
|
||||||
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
import type { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
||||||
import type { LaunchOptions } from 'playwright';
|
import type { LaunchOptions } from 'playwright';
|
||||||
|
|
||||||
const snapshotTools: Tool[] = [
|
const snapshotTools: Tool[] = [
|
||||||
...common,
|
...common,
|
||||||
...fileChooser(true),
|
...files(true),
|
||||||
...install,
|
...install,
|
||||||
...keyboard(true),
|
...keyboard(true),
|
||||||
...navigate(true),
|
...navigate(true),
|
||||||
@ -44,7 +44,7 @@ const snapshotTools: Tool[] = [
|
|||||||
|
|
||||||
const screenshotTools: Tool[] = [
|
const screenshotTools: Tool[] = [
|
||||||
...common,
|
...common,
|
||||||
...fileChooser(false),
|
...files(false),
|
||||||
...install,
|
...install,
|
||||||
...keyboard(false),
|
...keyboard(false),
|
||||||
...navigate(false),
|
...navigate(false),
|
||||||
@ -54,7 +54,7 @@ const screenshotTools: Tool[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const resources: Resource[] = [
|
const resources: Resource[] = [
|
||||||
console,
|
consoleResource,
|
||||||
];
|
];
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
@ -63,12 +63,14 @@ type Options = {
|
|||||||
launchOptions?: LaunchOptions;
|
launchOptions?: LaunchOptions;
|
||||||
cdpEndpoint?: string;
|
cdpEndpoint?: string;
|
||||||
vision?: boolean;
|
vision?: boolean;
|
||||||
|
capabilities?: ToolCapability[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const packageJSON = require('../package.json');
|
const packageJSON = require('../package.json');
|
||||||
|
|
||||||
export function createServer(options?: Options): Server {
|
export function createServer(options?: Options): Server {
|
||||||
const tools = options?.vision ? screenshotTools : snapshotTools;
|
const allTools = options?.vision ? screenshotTools : snapshotTools;
|
||||||
|
const tools = allTools.filter(tool => !options?.capabilities || tool.capability === 'core' || options.capabilities.includes(tool.capability));
|
||||||
return createServerWithTools({
|
return createServerWithTools({
|
||||||
name: 'Playwright',
|
name: 'Playwright',
|
||||||
version: packageJSON.version,
|
version: packageJSON.version,
|
||||||
|
@ -29,6 +29,7 @@ import { ServerList } from './server';
|
|||||||
|
|
||||||
import type { LaunchOptions } from 'playwright';
|
import type { LaunchOptions } from 'playwright';
|
||||||
import assert from 'assert';
|
import assert from 'assert';
|
||||||
|
import { ToolCapability } from './tools/tool';
|
||||||
|
|
||||||
const packageJSON = require('../package.json');
|
const packageJSON = require('../package.json');
|
||||||
|
|
||||||
@ -36,6 +37,7 @@ program
|
|||||||
.version('Version ' + packageJSON.version)
|
.version('Version ' + packageJSON.version)
|
||||||
.name(packageJSON.name)
|
.name(packageJSON.name)
|
||||||
.option('--browser <browser>', 'Browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
|
.option('--browser <browser>', 'Browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.')
|
||||||
|
.option('--caps <caps>', 'Comma-separated list of capabilities to enable, possible values: tabs, pdf, history, wait, files, install. Default is all.')
|
||||||
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
|
.option('--cdp-endpoint <endpoint>', 'CDP endpoint to connect to.')
|
||||||
.option('--executable-path <path>', 'Path to the browser executable.')
|
.option('--executable-path <path>', 'Path to the browser executable.')
|
||||||
.option('--headless', 'Run browser in headless mode, headed by default')
|
.option('--headless', 'Run browser in headless mode, headed by default')
|
||||||
@ -85,6 +87,7 @@ program
|
|||||||
launchOptions,
|
launchOptions,
|
||||||
vision: !!options.vision,
|
vision: !!options.vision,
|
||||||
cdpEndpoint: options.cdpEndpoint,
|
cdpEndpoint: options.cdpEndpoint,
|
||||||
|
capabilities: options.caps?.split(',').map((c: string) => c.trim() as ToolCapability),
|
||||||
}));
|
}));
|
||||||
setupExitWatchdog(serverList);
|
setupExitWatchdog(serverList);
|
||||||
|
|
||||||
|
@ -24,6 +24,7 @@ const waitSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const wait: Tool = {
|
const wait: Tool = {
|
||||||
|
capability: 'wait',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_wait',
|
name: 'browser_wait',
|
||||||
description: 'Wait for a specified time in seconds',
|
description: 'Wait for a specified time in seconds',
|
||||||
@ -44,6 +45,7 @@ const wait: Tool = {
|
|||||||
const closeSchema = z.object({});
|
const closeSchema = z.object({});
|
||||||
|
|
||||||
const close: Tool = {
|
const close: Tool = {
|
||||||
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_close',
|
name: 'browser_close',
|
||||||
description: 'Close the page',
|
description: 'Close the page',
|
||||||
|
@ -19,18 +19,19 @@ import { zodToJsonSchema } from 'zod-to-json-schema';
|
|||||||
|
|
||||||
import type { ToolFactory } from './tool';
|
import type { ToolFactory } from './tool';
|
||||||
|
|
||||||
const chooseFileSchema = z.object({
|
const uploadFileSchema = z.object({
|
||||||
paths: z.array(z.string()).describe('The absolute paths to the files to upload. Can be a single file or multiple files.'),
|
paths: z.array(z.string()).describe('The absolute paths to the files to upload. Can be a single file or multiple files.'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const chooseFile: ToolFactory = captureSnapshot => ({
|
const uploadFile: ToolFactory = captureSnapshot => ({
|
||||||
|
capability: 'files',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_choose_file',
|
name: 'browser_file_upload',
|
||||||
description: 'Choose one or multiple files to upload',
|
description: 'Upload one or multiple files',
|
||||||
inputSchema: zodToJsonSchema(chooseFileSchema),
|
inputSchema: zodToJsonSchema(uploadFileSchema),
|
||||||
},
|
},
|
||||||
handle: async (context, params) => {
|
handle: async (context, params) => {
|
||||||
const validatedParams = chooseFileSchema.parse(params);
|
const validatedParams = uploadFileSchema.parse(params);
|
||||||
const tab = context.currentTab();
|
const tab = context.currentTab();
|
||||||
return await tab.runAndWait(async () => {
|
return await tab.runAndWait(async () => {
|
||||||
await tab.submitFileChooser(validatedParams.paths);
|
await tab.submitFileChooser(validatedParams.paths);
|
||||||
@ -43,5 +44,5 @@ const chooseFile: ToolFactory = captureSnapshot => ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export default (captureSnapshot: boolean) => [
|
export default (captureSnapshot: boolean) => [
|
||||||
chooseFile(captureSnapshot),
|
uploadFile(captureSnapshot),
|
||||||
];
|
];
|
@ -23,6 +23,7 @@ import { zodToJsonSchema } from 'zod-to-json-schema';
|
|||||||
import type { Tool } from './tool';
|
import type { Tool } from './tool';
|
||||||
|
|
||||||
const install: Tool = {
|
const install: Tool = {
|
||||||
|
capability: 'install',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_install',
|
name: 'browser_install',
|
||||||
description: 'Install the browser specified in the config. Call this if you get an error about the browser not being installed.',
|
description: 'Install the browser specified in the config. Call this if you get an error about the browser not being installed.',
|
||||||
|
@ -24,6 +24,7 @@ const pressKeySchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const pressKey: ToolFactory = captureSnapshot => ({
|
const pressKey: ToolFactory = captureSnapshot => ({
|
||||||
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_press_key',
|
name: 'browser_press_key',
|
||||||
description: 'Press a key on the keyboard',
|
description: 'Press a key on the keyboard',
|
||||||
|
@ -24,6 +24,7 @@ const navigateSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const navigate: ToolFactory = captureSnapshot => ({
|
const navigate: ToolFactory = captureSnapshot => ({
|
||||||
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_navigate',
|
name: 'browser_navigate',
|
||||||
description: 'Navigate to a URL',
|
description: 'Navigate to a URL',
|
||||||
@ -44,6 +45,7 @@ const navigate: ToolFactory = captureSnapshot => ({
|
|||||||
const goBackSchema = z.object({});
|
const goBackSchema = z.object({});
|
||||||
|
|
||||||
const goBack: ToolFactory = snapshot => ({
|
const goBack: ToolFactory = snapshot => ({
|
||||||
|
capability: 'history',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_navigate_back',
|
name: 'browser_navigate_back',
|
||||||
description: 'Go back to the previous page',
|
description: 'Go back to the previous page',
|
||||||
@ -62,6 +64,7 @@ const goBack: ToolFactory = snapshot => ({
|
|||||||
const goForwardSchema = z.object({});
|
const goForwardSchema = z.object({});
|
||||||
|
|
||||||
const goForward: ToolFactory = snapshot => ({
|
const goForward: ToolFactory = snapshot => ({
|
||||||
|
capability: 'history',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_navigate_forward',
|
name: 'browser_navigate_forward',
|
||||||
description: 'Go forward to the next page',
|
description: 'Go forward to the next page',
|
||||||
|
@ -27,6 +27,7 @@ import type { Tool } from './tool';
|
|||||||
const pdfSchema = z.object({});
|
const pdfSchema = z.object({});
|
||||||
|
|
||||||
const pdf: Tool = {
|
const pdf: Tool = {
|
||||||
|
capability: 'pdf',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_pdf_save',
|
name: 'browser_pdf_save',
|
||||||
description: 'Save page as PDF',
|
description: 'Save page as PDF',
|
||||||
|
@ -20,6 +20,7 @@ import { zodToJsonSchema } from 'zod-to-json-schema';
|
|||||||
import type { Tool } from './tool';
|
import type { Tool } from './tool';
|
||||||
|
|
||||||
const screenshot: Tool = {
|
const screenshot: Tool = {
|
||||||
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_screen_capture',
|
name: 'browser_screen_capture',
|
||||||
description: 'Take a screenshot of the current page',
|
description: 'Take a screenshot of the current page',
|
||||||
@ -45,6 +46,7 @@ const moveMouseSchema = elementSchema.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const moveMouse: Tool = {
|
const moveMouse: Tool = {
|
||||||
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_screen_move_mouse',
|
name: 'browser_screen_move_mouse',
|
||||||
description: 'Move mouse to a given position',
|
description: 'Move mouse to a given position',
|
||||||
@ -67,6 +69,7 @@ const clickSchema = elementSchema.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const click: Tool = {
|
const click: Tool = {
|
||||||
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_screen_click',
|
name: 'browser_screen_click',
|
||||||
description: 'Click left mouse button',
|
description: 'Click left mouse button',
|
||||||
@ -93,6 +96,7 @@ const dragSchema = elementSchema.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const drag: Tool = {
|
const drag: Tool = {
|
||||||
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_screen_drag',
|
name: 'browser_screen_drag',
|
||||||
description: 'Drag left mouse button',
|
description: 'Drag left mouse button',
|
||||||
@ -118,6 +122,7 @@ const typeSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const type: Tool = {
|
const type: Tool = {
|
||||||
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_screen_type',
|
name: 'browser_screen_type',
|
||||||
description: 'Type text',
|
description: 'Type text',
|
||||||
|
@ -21,6 +21,7 @@ import type * as playwright from 'playwright';
|
|||||||
import type { Tool } from './tool';
|
import type { Tool } from './tool';
|
||||||
|
|
||||||
const snapshot: Tool = {
|
const snapshot: Tool = {
|
||||||
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_snapshot',
|
name: 'browser_snapshot',
|
||||||
description: 'Capture accessibility snapshot of the current page, this is better than screenshot',
|
description: 'Capture accessibility snapshot of the current page, this is better than screenshot',
|
||||||
@ -38,6 +39,7 @@ const elementSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const click: Tool = {
|
const click: Tool = {
|
||||||
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_click',
|
name: 'browser_click',
|
||||||
description: 'Perform click on a web page',
|
description: 'Perform click on a web page',
|
||||||
@ -63,6 +65,7 @@ const dragSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const drag: Tool = {
|
const drag: Tool = {
|
||||||
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_drag',
|
name: 'browser_drag',
|
||||||
description: 'Perform drag and drop between two elements',
|
description: 'Perform drag and drop between two elements',
|
||||||
@ -82,6 +85,7 @@ const drag: Tool = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hover: Tool = {
|
const hover: Tool = {
|
||||||
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_hover',
|
name: 'browser_hover',
|
||||||
description: 'Hover over element on page',
|
description: 'Hover over element on page',
|
||||||
@ -106,6 +110,7 @@ const typeSchema = elementSchema.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const type: Tool = {
|
const type: Tool = {
|
||||||
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_type',
|
name: 'browser_type',
|
||||||
description: 'Type text into editable element',
|
description: 'Type text into editable element',
|
||||||
@ -133,6 +138,7 @@ const selectOptionSchema = elementSchema.extend({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const selectOption: Tool = {
|
const selectOption: Tool = {
|
||||||
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_select_option',
|
name: 'browser_select_option',
|
||||||
description: 'Select an option in a dropdown',
|
description: 'Select an option in a dropdown',
|
||||||
@ -155,6 +161,7 @@ const screenshotSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const screenshot: Tool = {
|
const screenshot: Tool = {
|
||||||
|
capability: 'core',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_take_screenshot',
|
name: 'browser_take_screenshot',
|
||||||
description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
|
description: `Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.`,
|
||||||
|
@ -20,6 +20,7 @@ import { zodToJsonSchema } from 'zod-to-json-schema';
|
|||||||
import type { ToolFactory, Tool } from './tool';
|
import type { ToolFactory, Tool } from './tool';
|
||||||
|
|
||||||
const listTabs: Tool = {
|
const listTabs: Tool = {
|
||||||
|
capability: 'tabs',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_tab_list',
|
name: 'browser_tab_list',
|
||||||
description: 'List browser tabs',
|
description: 'List browser tabs',
|
||||||
@ -40,6 +41,7 @@ const selectTabSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const selectTab: ToolFactory = captureSnapshot => ({
|
const selectTab: ToolFactory = captureSnapshot => ({
|
||||||
|
capability: 'tabs',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_tab_select',
|
name: 'browser_tab_select',
|
||||||
description: 'Select a tab by index',
|
description: 'Select a tab by index',
|
||||||
@ -58,6 +60,7 @@ const newTabSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const newTab: Tool = {
|
const newTab: Tool = {
|
||||||
|
capability: 'tabs',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_tab_new',
|
name: 'browser_tab_new',
|
||||||
description: 'Open a new tab',
|
description: 'Open a new tab',
|
||||||
@ -77,6 +80,7 @@ const closeTabSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const closeTab: ToolFactory = captureSnapshot => ({
|
const closeTab: ToolFactory = captureSnapshot => ({
|
||||||
|
capability: 'tabs',
|
||||||
schema: {
|
schema: {
|
||||||
name: 'browser_tab_close',
|
name: 'browser_tab_close',
|
||||||
description: 'Close a tab',
|
description: 'Close a tab',
|
||||||
|
@ -18,6 +18,8 @@ import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types'
|
|||||||
import type { JsonSchema7Type } from 'zod-to-json-schema';
|
import type { JsonSchema7Type } from 'zod-to-json-schema';
|
||||||
import type { Context } from '../context';
|
import type { Context } from '../context';
|
||||||
|
|
||||||
|
export type ToolCapability = 'core' | 'tabs' | 'pdf' | 'history' | 'wait' | 'files' | 'install';
|
||||||
|
|
||||||
export type ToolSchema = {
|
export type ToolSchema = {
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
@ -30,6 +32,7 @@ export type ToolResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export type Tool = {
|
export type Tool = {
|
||||||
|
capability: ToolCapability;
|
||||||
schema: ToolSchema;
|
schema: ToolSchema;
|
||||||
handle: (context: Context, params?: Record<string, any>) => Promise<ToolResult>;
|
handle: (context: Context, params?: Record<string, any>) => Promise<ToolResult>;
|
||||||
};
|
};
|
||||||
|
@ -15,69 +15,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import { spawn } from 'node:child_process';
|
|
||||||
import path from 'node:path';
|
|
||||||
import { test, expect } from './fixtures';
|
import { test, expect } from './fixtures';
|
||||||
|
|
||||||
test('test tool list', async ({ client, visionClient }) => {
|
test('browser_navigate', async ({ client }) => {
|
||||||
const { tools } = await client.listTools();
|
|
||||||
expect(new Set(tools.map(t => t.name))).toEqual(new Set([
|
|
||||||
'browser_click',
|
|
||||||
'browser_drag',
|
|
||||||
'browser_hover',
|
|
||||||
'browser_select_option',
|
|
||||||
'browser_type',
|
|
||||||
'browser_choose_file',
|
|
||||||
'browser_close',
|
|
||||||
'browser_install',
|
|
||||||
'browser_navigate_back',
|
|
||||||
'browser_navigate_forward',
|
|
||||||
'browser_navigate',
|
|
||||||
'browser_pdf_save',
|
|
||||||
'browser_press_key',
|
|
||||||
'browser_snapshot',
|
|
||||||
'browser_tab_close',
|
|
||||||
'browser_tab_list',
|
|
||||||
'browser_tab_new',
|
|
||||||
'browser_tab_select',
|
|
||||||
'browser_take_screenshot',
|
|
||||||
'browser_wait',
|
|
||||||
]));
|
|
||||||
|
|
||||||
const { tools: visionTools } = await visionClient.listTools();
|
|
||||||
expect(new Set(visionTools.map(t => t.name))).toEqual(new Set([
|
|
||||||
'browser_choose_file',
|
|
||||||
'browser_close',
|
|
||||||
'browser_install',
|
|
||||||
'browser_navigate_back',
|
|
||||||
'browser_navigate_forward',
|
|
||||||
'browser_navigate',
|
|
||||||
'browser_pdf_save',
|
|
||||||
'browser_press_key',
|
|
||||||
'browser_screen_capture',
|
|
||||||
'browser_screen_click',
|
|
||||||
'browser_screen_drag',
|
|
||||||
'browser_screen_move_mouse',
|
|
||||||
'browser_screen_type',
|
|
||||||
'browser_tab_close',
|
|
||||||
'browser_tab_list',
|
|
||||||
'browser_tab_new',
|
|
||||||
'browser_tab_select',
|
|
||||||
'browser_wait',
|
|
||||||
]));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('test resources list', async ({ client }) => {
|
|
||||||
const { resources } = await client.listResources();
|
|
||||||
expect(resources).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
uri: 'browser://console',
|
|
||||||
mimeType: 'text/plain',
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('test browser_navigate', async ({ client }) => {
|
|
||||||
expect(await client.callTool({
|
expect(await client.callTool({
|
||||||
name: 'browser_navigate',
|
name: 'browser_navigate',
|
||||||
arguments: {
|
arguments: {
|
||||||
@ -96,7 +36,7 @@ Navigated to data:text/html,<html><title>Title</title><body>Hello, world!</body>
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test browser_click', async ({ client }) => {
|
test('browser_click', async ({ client }) => {
|
||||||
await client.callTool({
|
await client.callTool({
|
||||||
name: 'browser_navigate',
|
name: 'browser_navigate',
|
||||||
arguments: {
|
arguments: {
|
||||||
@ -121,36 +61,8 @@ test('test browser_click', async ({ client }) => {
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('test reopen browser', async ({ client }) => {
|
|
||||||
await client.callTool({
|
|
||||||
name: 'browser_navigate',
|
|
||||||
arguments: {
|
|
||||||
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(await client.callTool({
|
test('browser_select_option', async ({ client }) => {
|
||||||
name: 'browser_close',
|
|
||||||
})).toHaveTextContent('Page closed');
|
|
||||||
|
|
||||||
expect(await client.callTool({
|
|
||||||
name: 'browser_navigate',
|
|
||||||
arguments: {
|
|
||||||
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
|
|
||||||
},
|
|
||||||
})).toHaveTextContent(`
|
|
||||||
Navigated to data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
|
|
||||||
|
|
||||||
- Page URL: data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
|
|
||||||
- Page Title: Title
|
|
||||||
- Page Snapshot
|
|
||||||
\`\`\`yaml
|
|
||||||
- text: Hello, world!
|
|
||||||
\`\`\`
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('single option', async ({ client }) => {
|
|
||||||
await client.callTool({
|
await client.callTool({
|
||||||
name: 'browser_navigate',
|
name: 'browser_navigate',
|
||||||
arguments: {
|
arguments: {
|
||||||
@ -178,7 +90,7 @@ test('single option', async ({ client }) => {
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('multiple option', async ({ client }) => {
|
test('browser_select_option (multiple)', async ({ client }) => {
|
||||||
await client.callTool({
|
await client.callTool({
|
||||||
name: 'browser_navigate',
|
name: 'browser_navigate',
|
||||||
arguments: {
|
arguments: {
|
||||||
@ -207,51 +119,7 @@ test('multiple option', async ({ client }) => {
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('browser://console', async ({ client }) => {
|
test('browser_file_upload', async ({ client }) => {
|
||||||
await client.callTool({
|
|
||||||
name: 'browser_navigate',
|
|
||||||
arguments: {
|
|
||||||
url: 'data:text/html,<html><script>console.log("Hello, world!");console.error("Error"); </script></html>',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const resource = await client.readResource({
|
|
||||||
uri: 'browser://console',
|
|
||||||
});
|
|
||||||
expect(resource.contents).toEqual([{
|
|
||||||
uri: 'browser://console',
|
|
||||||
mimeType: 'text/plain',
|
|
||||||
text: '[LOG] Hello, world!\n[ERROR] Error',
|
|
||||||
}]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('stitched aria frames', async ({ client }) => {
|
|
||||||
expect(await client.callTool({
|
|
||||||
name: 'browser_navigate',
|
|
||||||
arguments: {
|
|
||||||
url: `data:text/html,<h1>Hello</h1><iframe src="data:text/html,<button>World</button><main><iframe src='data:text/html,<p>Nested</p>'></iframe></main>"></iframe><iframe src="data:text/html,<h1>Should be invisible</h1>" style="display: none;"></iframe>`,
|
|
||||||
},
|
|
||||||
})).toContainTextContent(`
|
|
||||||
\`\`\`yaml
|
|
||||||
- heading "Hello" [level=1] [ref=s1e3]
|
|
||||||
- iframe [ref=s1e4]:
|
|
||||||
- button "World" [ref=f1s1e3]
|
|
||||||
- main [ref=f1s1e4]:
|
|
||||||
- iframe [ref=f1s1e5]:
|
|
||||||
- paragraph [ref=f2s1e3]: Nested
|
|
||||||
\`\`\`
|
|
||||||
`);
|
|
||||||
|
|
||||||
expect(await client.callTool({
|
|
||||||
name: 'browser_click',
|
|
||||||
arguments: {
|
|
||||||
element: 'World',
|
|
||||||
ref: 'f1s1e3',
|
|
||||||
},
|
|
||||||
})).toContainTextContent('Clicked "World"');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('browser_choose_file', async ({ client }) => {
|
|
||||||
expect(await client.callTool({
|
expect(await client.callTool({
|
||||||
name: 'browser_navigate',
|
name: 'browser_navigate',
|
||||||
arguments: {
|
arguments: {
|
||||||
@ -265,20 +133,20 @@ test('browser_choose_file', async ({ client }) => {
|
|||||||
element: 'Textbox',
|
element: 'Textbox',
|
||||||
ref: 's1e3',
|
ref: 's1e3',
|
||||||
},
|
},
|
||||||
})).toContainTextContent('There is a file chooser visible that requires browser_choose_file to be called');
|
})).toContainTextContent('There is a file chooser visible that requires browser_file_upload to be called');
|
||||||
|
|
||||||
const filePath = test.info().outputPath('test.txt');
|
const filePath = test.info().outputPath('test.txt');
|
||||||
await fs.writeFile(filePath, 'Hello, world!');
|
await fs.writeFile(filePath, 'Hello, world!');
|
||||||
|
|
||||||
{
|
{
|
||||||
const response = await client.callTool({
|
const response = await client.callTool({
|
||||||
name: 'browser_choose_file',
|
name: 'browser_file_upload',
|
||||||
arguments: {
|
arguments: {
|
||||||
paths: [filePath],
|
paths: [filePath],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response).not.toContainTextContent('There is a file chooser visible that requires browser_choose_file to be called');
|
expect(response).not.toContainTextContent('There is a file chooser visible that requires browser_file_upload to be called');
|
||||||
expect(response).toContainTextContent('textbox [ref=s3e3]: C:\\fakepath\\test.txt');
|
expect(response).toContainTextContent('textbox [ref=s3e3]: C:\\fakepath\\test.txt');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -291,7 +159,7 @@ test('browser_choose_file', async ({ client }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response).toContainTextContent('There is a file chooser visible that requires browser_choose_file to be called');
|
expect(response).toContainTextContent('There is a file chooser visible that requires browser_file_upload to be called');
|
||||||
expect(response).toContainTextContent('button "Button" [ref=s4e4]');
|
expect(response).toContainTextContent('button "Button" [ref=s4e4]');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,89 +172,11 @@ test('browser_choose_file', async ({ client }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(response, 'not submitting browser_choose_file dismisses file chooser').not.toContainTextContent('There is a file chooser visible that requires browser_choose_file to be called');
|
expect(response, 'not submitting browser_file_upload dismisses file chooser').not.toContainTextContent('There is a file chooser visible that requires browser_file_upload to be called');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('sse transport', async () => {
|
test('browser_type', async ({ client }) => {
|
||||||
const cp = spawn('node', [path.join(__dirname, '../cli.js'), '--port', '0'], { stdio: 'pipe' });
|
|
||||||
try {
|
|
||||||
let stdout = '';
|
|
||||||
const url = await new Promise<string>(resolve => cp.stdout?.on('data', data => {
|
|
||||||
stdout += data.toString();
|
|
||||||
const match = stdout.match(/Listening on (http:\/\/.*)/);
|
|
||||||
if (match)
|
|
||||||
resolve(match[1]);
|
|
||||||
}));
|
|
||||||
|
|
||||||
// need dynamic import b/c of some ESM nonsense
|
|
||||||
const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
|
|
||||||
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
|
|
||||||
const transport = new SSEClientTransport(new URL(url));
|
|
||||||
const client = new Client({ name: 'test', version: '1.0.0' });
|
|
||||||
await client.connect(transport);
|
|
||||||
await client.ping();
|
|
||||||
} finally {
|
|
||||||
cp.kill();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('cdp server', async ({ cdpEndpoint, startClient }) => {
|
|
||||||
const client = await startClient({ args: [`--cdp-endpoint=${cdpEndpoint}`] });
|
|
||||||
expect(await client.callTool({
|
|
||||||
name: 'browser_navigate',
|
|
||||||
arguments: {
|
|
||||||
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
|
|
||||||
},
|
|
||||||
})).toHaveTextContent(`
|
|
||||||
Navigated to data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
|
|
||||||
|
|
||||||
- Page URL: data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
|
|
||||||
- Page Title: Title
|
|
||||||
- Page Snapshot
|
|
||||||
\`\`\`yaml
|
|
||||||
- text: Hello, world!
|
|
||||||
\`\`\`
|
|
||||||
`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('save as pdf', async ({ client }) => {
|
|
||||||
expect(await client.callTool({
|
|
||||||
name: 'browser_navigate',
|
|
||||||
arguments: {
|
|
||||||
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
|
|
||||||
},
|
|
||||||
})).toHaveTextContent(`
|
|
||||||
Navigated to data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
|
|
||||||
|
|
||||||
- Page URL: data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
|
|
||||||
- Page Title: Title
|
|
||||||
- Page Snapshot
|
|
||||||
\`\`\`yaml
|
|
||||||
- text: Hello, world!
|
|
||||||
\`\`\`
|
|
||||||
`
|
|
||||||
);
|
|
||||||
|
|
||||||
const response = await client.callTool({
|
|
||||||
name: 'browser_pdf_save',
|
|
||||||
});
|
|
||||||
expect(response).toHaveTextContent(/^Saved as.*page-[^:]+.pdf$/);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('executable path', async ({ startClient }) => {
|
|
||||||
const client = await startClient({ args: [`--executable-path=bogus`] });
|
|
||||||
const response = await client.callTool({
|
|
||||||
name: 'browser_navigate',
|
|
||||||
arguments: {
|
|
||||||
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
expect(response).toContainTextContent(`executable doesn't exist`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fill in text', async ({ client }) => {
|
|
||||||
await client.callTool({
|
await client.callTool({
|
||||||
name: 'browser_navigate',
|
name: 'browser_navigate',
|
||||||
arguments: {
|
arguments: {
|
||||||
@ -412,7 +202,7 @@ test('fill in text', async ({ client }) => {
|
|||||||
}]);
|
}]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('type slowly', async ({ client }) => {
|
test('browser_type (slowly)', async ({ client }) => {
|
||||||
await client.callTool({
|
await client.callTool({
|
||||||
name: 'browser_navigate',
|
name: 'browser_navigate',
|
||||||
arguments: {
|
arguments: {
|
||||||
|
92
tests/capabilities.spec.ts
Normal file
92
tests/capabilities.spec.ts
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from './fixtures';
|
||||||
|
|
||||||
|
test('test snapshot tool list', async ({ client }) => {
|
||||||
|
const { tools } = await client.listTools();
|
||||||
|
expect(new Set(tools.map(t => t.name))).toEqual(new Set([
|
||||||
|
'browser_click',
|
||||||
|
'browser_drag',
|
||||||
|
'browser_file_upload',
|
||||||
|
'browser_hover',
|
||||||
|
'browser_select_option',
|
||||||
|
'browser_type',
|
||||||
|
'browser_close',
|
||||||
|
'browser_install',
|
||||||
|
'browser_navigate_back',
|
||||||
|
'browser_navigate_forward',
|
||||||
|
'browser_navigate',
|
||||||
|
'browser_pdf_save',
|
||||||
|
'browser_press_key',
|
||||||
|
'browser_snapshot',
|
||||||
|
'browser_tab_close',
|
||||||
|
'browser_tab_list',
|
||||||
|
'browser_tab_new',
|
||||||
|
'browser_tab_select',
|
||||||
|
'browser_take_screenshot',
|
||||||
|
'browser_wait',
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test vision tool list', async ({ visionClient }) => {
|
||||||
|
const { tools: visionTools } = await visionClient.listTools();
|
||||||
|
expect(new Set(visionTools.map(t => t.name))).toEqual(new Set([
|
||||||
|
'browser_close',
|
||||||
|
'browser_file_upload',
|
||||||
|
'browser_install',
|
||||||
|
'browser_navigate_back',
|
||||||
|
'browser_navigate_forward',
|
||||||
|
'browser_navigate',
|
||||||
|
'browser_pdf_save',
|
||||||
|
'browser_press_key',
|
||||||
|
'browser_screen_capture',
|
||||||
|
'browser_screen_click',
|
||||||
|
'browser_screen_drag',
|
||||||
|
'browser_screen_move_mouse',
|
||||||
|
'browser_screen_type',
|
||||||
|
'browser_tab_close',
|
||||||
|
'browser_tab_list',
|
||||||
|
'browser_tab_new',
|
||||||
|
'browser_tab_select',
|
||||||
|
'browser_wait',
|
||||||
|
]));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test resources list', async ({ client }) => {
|
||||||
|
const { resources } = await client.listResources();
|
||||||
|
expect(resources).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
uri: 'browser://console',
|
||||||
|
mimeType: 'text/plain',
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('test capabilities', async ({ startClient }) => {
|
||||||
|
const client = await startClient({
|
||||||
|
args: ['--caps="core"'],
|
||||||
|
});
|
||||||
|
const { tools } = await client.listTools();
|
||||||
|
const toolNames = tools.map(t => t.name);
|
||||||
|
expect(toolNames).not.toContain('browser_file_upload');
|
||||||
|
expect(toolNames).not.toContain('browser_pdf_save');
|
||||||
|
expect(toolNames).not.toContain('browser_screen_capture');
|
||||||
|
expect(toolNames).not.toContain('browser_screen_click');
|
||||||
|
expect(toolNames).not.toContain('browser_screen_drag');
|
||||||
|
expect(toolNames).not.toContain('browser_screen_move_mouse');
|
||||||
|
expect(toolNames).not.toContain('browser_screen_type');
|
||||||
|
});
|
37
tests/cdp.spec.ts
Normal file
37
tests/cdp.spec.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from './fixtures';
|
||||||
|
|
||||||
|
test('cdp server', async ({ cdpEndpoint, startClient }) => {
|
||||||
|
const client = await startClient({ args: [`--cdp-endpoint=${cdpEndpoint}`] });
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: {
|
||||||
|
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
|
||||||
|
},
|
||||||
|
})).toHaveTextContent(`
|
||||||
|
Navigated to data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
|
||||||
|
|
||||||
|
- Page URL: data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
|
||||||
|
- Page Title: Title
|
||||||
|
- Page Snapshot
|
||||||
|
\`\`\`yaml
|
||||||
|
- text: Hello, world!
|
||||||
|
\`\`\`
|
||||||
|
`
|
||||||
|
);
|
||||||
|
});
|
35
tests/console.spec.ts
Normal file
35
tests/console.spec.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from './fixtures';
|
||||||
|
|
||||||
|
test('browser://console', async ({ client }) => {
|
||||||
|
await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: {
|
||||||
|
url: 'data:text/html,<html><script>console.log("Hello, world!");console.error("Error"); </script></html>',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const resource = await client.readResource({
|
||||||
|
uri: 'browser://console',
|
||||||
|
});
|
||||||
|
expect(resource.contents).toEqual([{
|
||||||
|
uri: 'browser://console',
|
||||||
|
mimeType: 'text/plain',
|
||||||
|
text: '[LOG] Hello, world!\n[ERROR] Error',
|
||||||
|
}]);
|
||||||
|
});
|
@ -24,7 +24,7 @@ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|||||||
type Fixtures = {
|
type Fixtures = {
|
||||||
client: Client;
|
client: Client;
|
||||||
visionClient: Client;
|
visionClient: Client;
|
||||||
startClient: (options?: { args?: string[], vision?: boolean }) => Promise<Client>;
|
startClient: (options?: { args?: string[] }) => Promise<Client>;
|
||||||
wsEndpoint: string;
|
wsEndpoint: string;
|
||||||
cdpEndpoint: string;
|
cdpEndpoint: string;
|
||||||
};
|
};
|
||||||
@ -36,7 +36,7 @@ export const test = baseTest.extend<Fixtures>({
|
|||||||
},
|
},
|
||||||
|
|
||||||
visionClient: async ({ startClient }, use) => {
|
visionClient: async ({ startClient }, use) => {
|
||||||
await use(await startClient({ vision: true }));
|
await use(await startClient({ args: ['--vision'] }));
|
||||||
},
|
},
|
||||||
|
|
||||||
startClient: async ({ }, use, testInfo) => {
|
startClient: async ({ }, use, testInfo) => {
|
||||||
@ -45,8 +45,6 @@ export const test = baseTest.extend<Fixtures>({
|
|||||||
|
|
||||||
use(async options => {
|
use(async options => {
|
||||||
const args = ['--headless', '--user-data-dir', userDataDir];
|
const args = ['--headless', '--user-data-dir', userDataDir];
|
||||||
if (options?.vision)
|
|
||||||
args.push('--vision');
|
|
||||||
if (options?.args)
|
if (options?.args)
|
||||||
args.push(...options.args);
|
args.push(...options.args);
|
||||||
const transport = new StdioClientTransport({
|
const transport = new StdioClientTransport({
|
||||||
|
43
tests/iframes.spec.ts
Normal file
43
tests/iframes.spec.ts
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from './fixtures';
|
||||||
|
|
||||||
|
test('stitched aria frames', async ({ client }) => {
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: {
|
||||||
|
url: `data:text/html,<h1>Hello</h1><iframe src="data:text/html,<button>World</button><main><iframe src='data:text/html,<p>Nested</p>'></iframe></main>"></iframe><iframe src="data:text/html,<h1>Should be invisible</h1>" style="display: none;"></iframe>`,
|
||||||
|
},
|
||||||
|
})).toContainTextContent(`
|
||||||
|
\`\`\`yaml
|
||||||
|
- heading "Hello" [level=1] [ref=s1e3]
|
||||||
|
- iframe [ref=s1e4]:
|
||||||
|
- button "World" [ref=f1s1e3]
|
||||||
|
- main [ref=f1s1e4]:
|
||||||
|
- iframe [ref=f1s1e5]:
|
||||||
|
- paragraph [ref=f2s1e3]: Nested
|
||||||
|
\`\`\`
|
||||||
|
`);
|
||||||
|
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_click',
|
||||||
|
arguments: {
|
||||||
|
element: 'World',
|
||||||
|
ref: 'f1s1e3',
|
||||||
|
},
|
||||||
|
})).toContainTextContent('Clicked "World"');
|
||||||
|
});
|
57
tests/launch.spec.ts
Normal file
57
tests/launch.spec.ts
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from './fixtures';
|
||||||
|
|
||||||
|
test('test reopen browser', async ({ client }) => {
|
||||||
|
await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: {
|
||||||
|
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_close',
|
||||||
|
})).toHaveTextContent('Page closed');
|
||||||
|
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: {
|
||||||
|
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
|
||||||
|
},
|
||||||
|
})).toHaveTextContent(`
|
||||||
|
Navigated to data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
|
||||||
|
|
||||||
|
- Page URL: data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
|
||||||
|
- Page Title: Title
|
||||||
|
- Page Snapshot
|
||||||
|
\`\`\`yaml
|
||||||
|
- text: Hello, world!
|
||||||
|
\`\`\`
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('executable path', async ({ startClient }) => {
|
||||||
|
const client = await startClient({ args: [`--executable-path=bogus`] });
|
||||||
|
const response = await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: {
|
||||||
|
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(response).toContainTextContent(`executable doesn't exist`);
|
||||||
|
});
|
55
tests/pdf.spec.ts
Normal file
55
tests/pdf.spec.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { test, expect } from './fixtures';
|
||||||
|
|
||||||
|
test('save as pdf unavailable', async ({ startClient }) => {
|
||||||
|
const client = await startClient({ args: ['--caps="no-pdf"'] });
|
||||||
|
await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: {
|
||||||
|
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_pdf_save',
|
||||||
|
})).toHaveTextContent(/Tool \"browser_pdf_save\" not found/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('save as pdf', async ({ client }) => {
|
||||||
|
expect(await client.callTool({
|
||||||
|
name: 'browser_navigate',
|
||||||
|
arguments: {
|
||||||
|
url: 'data:text/html,<html><title>Title</title><body>Hello, world!</body></html>',
|
||||||
|
},
|
||||||
|
})).toHaveTextContent(`
|
||||||
|
Navigated to data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
|
||||||
|
|
||||||
|
- Page URL: data:text/html,<html><title>Title</title><body>Hello, world!</body></html>
|
||||||
|
- Page Title: Title
|
||||||
|
- Page Snapshot
|
||||||
|
\`\`\`yaml
|
||||||
|
- text: Hello, world!
|
||||||
|
\`\`\`
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
const response = await client.callTool({
|
||||||
|
name: 'browser_pdf_save',
|
||||||
|
});
|
||||||
|
expect(response).toHaveTextContent(/^Saved as.*page-[^:]+.pdf$/);
|
||||||
|
});
|
42
tests/sse.spec.ts
Normal file
42
tests/sse.spec.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from 'node:child_process';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { test } from './fixtures';
|
||||||
|
|
||||||
|
test('sse transport', async () => {
|
||||||
|
const cp = spawn('node', [path.join(__dirname, '../cli.js'), '--port', '0'], { stdio: 'pipe' });
|
||||||
|
try {
|
||||||
|
let stdout = '';
|
||||||
|
const url = await new Promise<string>(resolve => cp.stdout?.on('data', data => {
|
||||||
|
stdout += data.toString();
|
||||||
|
const match = stdout.match(/Listening on (http:\/\/.*)/);
|
||||||
|
if (match)
|
||||||
|
resolve(match[1]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// need dynamic import b/c of some ESM nonsense
|
||||||
|
const { SSEClientTransport } = await import('@modelcontextprotocol/sdk/client/sse.js');
|
||||||
|
const { Client } = await import('@modelcontextprotocol/sdk/client/index.js');
|
||||||
|
const transport = new SSEClientTransport(new URL(url));
|
||||||
|
const client = new Client({ name: 'test', version: '1.0.0' });
|
||||||
|
await client.connect(transport);
|
||||||
|
await client.ping();
|
||||||
|
} finally {
|
||||||
|
cp.kill();
|
||||||
|
}
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user