From 0c3792d231f1819f74df3be555120625bdc22b39 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Mon, 21 Apr 2025 20:22:57 +0200 Subject: [PATCH] chore: auto update tools in README (#219) Motivation: Keeping the readme up to date is a manual effort - this keeps it automatically up to date and prevents things like https://github.com/microsoft/playwright-mcp/pull/214 and other consistency errors in the future. --- .github/workflows/ci.yml | 23 ++++- README.md | 129 ++++++++++++++++++++-------- package.json | 1 + utils/update-readme.js | 178 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 291 insertions(+), 40 deletions(-) create mode 100644 utils/update-readme.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4164a3..0a50d14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,25 @@ on: branches: [ main ] jobs: - build-and-test: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Use Node.js 18 + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + - name: Install dependencies + run: npm ci + - run: npm run build + - name: Run ESLint + run: npm run lint + - run: npm run update-readme + - name: Ensure no changes + run: git diff --exit-code + + test: strategy: fail-fast: false matrix: @@ -33,9 +51,6 @@ jobs: if: ${{ matrix.os == 'macos-latest' }} # MS Edge is not preinstalled on macOS runners. run: npx playwright install msedge - - name: Run linting - run: npm run lint - - name: Build run: npm run build diff --git a/README.md b/README.md index 6db3787..bed8642 100644 --- a/README.md +++ b/README.md @@ -168,27 +168,43 @@ transport = new SSEServerTransport("/messages", res); server.connect(transport); ``` + + ### Snapshot-based Interactions + + +- **browser_snapshot** + - Description: Capture accessibility snapshot of the current page, this is better than screenshot + - Parameters: None + + + - **browser_click** - Description: Perform click on a web page - Parameters: - `element` (string): Human-readable element description used to obtain permission to interact with the element - `ref` (string): Exact target element reference from the page snapshot + + +- **browser_drag** + - Description: Perform drag and drop between two elements + - Parameters: + - `startElement` (string): Human-readable source element description used to obtain the permission to interact with the element + - `startRef` (string): Exact source element reference from the page snapshot + - `endElement` (string): Human-readable target element description used to obtain the permission to interact with the element + - `endRef` (string): Exact target element reference from the page snapshot + + + - **browser_hover** - Description: Hover over element on page - Parameters: - `element` (string): Human-readable element description used to obtain permission to interact with the element - `ref` (string): Exact target element reference from the page snapshot -- **browser_drag** - - Description: Perform drag and drop between two elements - - Parameters: - - `startElement` (string): Human-readable source element description used to obtain permission to interact with the element - - `startRef` (string): Exact source element reference from the page snapshot - - `endElement` (string): Human-readable target element description used to obtain permission to interact with the element - - `endRef` (string): Exact target element reference from the page snapshot + - **browser_type** - Description: Type text into editable element @@ -199,6 +215,8 @@ server.connect(transport); - `submit` (boolean, optional): Whether to submit entered text (press Enter after) - `slowly` (boolean, optional): Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once. + + - **browser_select_option** - Description: Select an option in a dropdown - Parameters: @@ -206,17 +224,25 @@ server.connect(transport); - `ref` (string): Exact target element reference from the page snapshot - `values` (array): Array of values to select in the dropdown. This can be a single value or multiple values. -- **browser_snapshot** - - Description: Capture accessibility snapshot of the current page, this is better than screenshot - - Parameters: None + - **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. - Parameters: - `raw` (boolean, optional): Whether to return without compression (in PNG format). Default is false, which returns a JPEG image. + - `element` (string, optional): Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too. + - `ref` (string, optional): Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too. ### Vision-based Interactions + + +- **browser_screen_capture** + - Description: Take a screenshot of the current page + - Parameters: None + + + - **browser_screen_move_mouse** - Description: Move mouse to a given position - Parameters: @@ -224,9 +250,7 @@ server.connect(transport); - `x` (number): X coordinate - `y` (number): Y coordinate -- **browser_screen_capture** - - Description: Take a screenshot of the current page - - Parameters: None + - **browser_screen_click** - Description: Click left mouse button @@ -235,6 +259,8 @@ server.connect(transport); - `x` (number): X coordinate - `y` (number): Y coordinate + + - **browser_screen_drag** - Description: Drag left mouse button - Parameters: @@ -244,33 +270,38 @@ server.connect(transport); - `endX` (number): End X coordinate - `endY` (number): End Y coordinate + + - **browser_screen_type** - Description: Type text - Parameters: - - `text` (string): Text to type + - `text` (string): Text to type into the element - `submit` (boolean, optional): Whether to submit entered text (press Enter after) -- **browser_press_key** - - Description: Press a key on the keyboard - - Parameters: - - `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a` - ### Tab Management + + - **browser_tab_list** - Description: List browser tabs - Parameters: None + + - **browser_tab_new** - Description: Open a new tab - Parameters: - `url` (string, optional): The URL to navigate to in the new tab. If not provided, the new tab will be blank. + + - **browser_tab_select** - Description: Select a tab by index - Parameters: - `index` (number): The index of the tab to select + + - **browser_tab_close** - Description: Close a tab - Parameters: @@ -278,21 +309,29 @@ server.connect(transport); ### Navigation + + - **browser_navigate** - Description: Navigate to a URL - Parameters: - `url` (string): The URL to navigate to + + - **browser_navigate_back** - Description: Go back to the previous page - Parameters: None + + - **browser_navigate_forward** - Description: Go forward to the next page - Parameters: None ### Keyboard + + - **browser_press_key** - Description: Press a key on the keyboard - Parameters: @@ -300,44 +339,62 @@ server.connect(transport); ### Console + + - **browser_console_messages** - Description: Returns all console messages - Parameters: None ### Files and Media + + - **browser_file_upload** - - Description: Choose one or multiple files to upload + - Description: Upload one or multiple files - Parameters: - `paths` (array): The absolute paths to the files to upload. Can be a single file or multiple files. + + - **browser_pdf_save** - Description: Save page as PDF - Parameters: None ### Utilities -- **browser_wait** - - Description: Wait for a specified time in seconds - - Parameters: - - `time` (number): The time to wait in seconds (capped at 10 seconds) - -- **browser_resize** - - Description: Resize the browser window - - Parameters: - - `width` (number): The desired width of the browser window - - `height` (number): The desired height of the browser window - -- **browser_handle_dialog** - - Description: Handle browser dialogs (alert, confirm, prompt) - - Parameters: - - `accept` (boolean): Whether to accept or dismiss the dialog - - `promptText` (string, optional): Text to enter in case of prompt dialogs + - **browser_close** - Description: Close the page - Parameters: None + + +- **browser_wait** + - Description: Wait for a specified time in seconds + - Parameters: + - `time` (number): The time to wait in seconds + + + +- **browser_resize** + - Description: Resize the browser window + - Parameters: + - `width` (number): Width of the browser window + - `height` (number): Height of the browser window + + + - **browser_install** - Description: Install the browser specified in the config. Call this if you get an error about the browser not being installed. - Parameters: None + + + +- **browser_handle_dialog** + - Description: Handle a dialog + - Parameters: + - `accept` (boolean): Whether to accept the dialog. + - `promptText` (string, optional): The text of the prompt in case of a prompt dialog. + + diff --git a/package.json b/package.json index a69c189..1481e32 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "scripts": { "build": "tsc", "lint": "eslint .", + "update-readme": "node utils/update-readme.js", "watch": "tsc --watch", "test": "playwright test", "ctest": "playwright test --project=chrome", diff --git a/utils/update-readme.js b/utils/update-readme.js new file mode 100644 index 0000000..fb76f05 --- /dev/null +++ b/utils/update-readme.js @@ -0,0 +1,178 @@ +#!/usr/bin/env node +/** + * 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. + */ +// @ts-check + +const fs = require('node:fs'); +const path = require('node:path'); + +const commonTools = require('../lib/tools/common').default; +const consoleTools = require('../lib/tools/console').default; +const dialogsTools = require('../lib/tools/dialogs').default; +const filesTools = require('../lib/tools/files').default; +const installTools = require('../lib/tools/install').default; +const keyboardTools = require('../lib/tools/keyboard').default; +const navigateTools = require('../lib/tools/navigate').default; +const pdfTools = require('../lib/tools/pdf').default; +const snapshotTools = require('../lib/tools/snapshot').default; +const tabsTools = require('../lib/tools/tabs').default; +const screenTools = require('../lib/tools/screen').default; + +// Category definitions for tools +const categories = { + 'Snapshot-based Interactions': [ + ...snapshotTools, + ], + 'Vision-based Interactions': [ + ...screenTools + ], + 'Tab Management': [ + ...tabsTools(true), + ], + 'Navigation': [ + ...navigateTools(true), + ], + 'Keyboard': [ + ...keyboardTools(true) + ], + 'Console': [ + ...consoleTools + ], + 'Files and Media': [ + ...filesTools(true), + ...pdfTools + ], + 'Utilities': [ + ...commonTools(true), + ...installTools, + ...dialogsTools(true), + ], +}; + +const kStartMarker = ``; +const kEndMarker = ``; + +/** + * @param {ParsedToolSchema} tool + * @returns {string} + */ +function formatToolForReadme(tool) { + const lines = /** @type {string[]} */ ([]); + lines.push(`\n\n`); + lines.push(`- **${tool.name}**\n`); + lines.push(` - Description: ${tool.description}\n`); + + if (tool.parameters && tool.parameters.length > 0) { + lines.push(` - Parameters:\n`); + tool.parameters.forEach(param => { + const meta = /** @type {string[]} */ ([]); + if (param.type) + meta.push(param.type); + if (param.optional) + meta.push('optional'); + lines.push(` - \`${param.name}\` ${meta.length ? `(${meta.join(', ')})` : ''}: ${param.description}\n`); + }); + } else { + lines.push(` - Parameters: None\n`); + } + + lines.push('\n'); + return lines.join(''); +} + +/** + * @typedef {{ + * name: any; + * description: any; + * parameters: { + * name: string; + * description: string; + * optional: boolean; + * type: string; + * }[]; + *}} ParsedToolSchema + */ + +/** + * @param {import('../src/tools/tool').ToolSchema} schema + * @returns {ParsedToolSchema} + */ +function processToolSchema(schema) { + const inputSchema = /** @type {import('zod-to-json-schema').JsonSchema7ObjectType} */ (schema.inputSchema || {}); + if (inputSchema.type !== 'object') + throw new Error(`Tool ${schema.name} input schema is not an object`); + + // In JSON Schema, properties are considered optional unless listed in the required array + const requiredParams = inputSchema?.required || []; + + const parameters = Object.entries(inputSchema.properties).map(([name, prop]) => { + return { + name, + description: prop.description || '', + optional: !requiredParams.includes(name), + type: /** @type {any} */ (prop).type, + }; + }); + + return { + name: schema.name, + description: schema.description, + parameters + }; +} + +async function updateReadme() { + console.log('Loading tool information from compiled modules...'); + + // Count the tools processed + const totalTools = Object.values(categories).flat().length; + console.log(`Found ${totalTools} tools`); + + const generatedLines = /** @type {string[]} */ ([]); + + for (const [category, categoryTools] of Object.entries(categories)) { + generatedLines.push(`### ${category}\n\n`); + for (const tool of categoryTools) { + const scheme = processToolSchema(tool.schema); + generatedLines.push(formatToolForReadme(scheme)); + } + } + + const readmePath = path.join(__dirname, '..', 'README.md'); + const readmeContent = await fs.promises.readFile(readmePath, 'utf-8'); + const startMarker = readmeContent.indexOf(kStartMarker); + const endMarker = readmeContent.indexOf(kEndMarker); + if (startMarker === -1 || endMarker === -1) + throw new Error('Markers for generated section not found in README'); + + const newReadmeContent = [ + readmeContent.slice(0, startMarker), + kStartMarker + '\n\n', + generatedLines.join(''), + kEndMarker, + readmeContent.slice(endMarker + kEndMarker.length), + ].join(''); + + // Write updated README + await fs.promises.writeFile(readmePath, newReadmeContent, 'utf-8'); + console.log('README updated successfully'); +} + +// Run the update +updateReadme().catch(err => { + console.error('Error updating README:', err); + process.exit(1); +});