#!/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('fs') const path = require('path') const { execSync } = require('child_process'); const { browserTools } = require('playwright/lib/mcp/browser/tools'); const capabilities = { 'core-navigation': 'Core automation', 'core': 'Core automation', 'core-tabs': 'Tab management', 'core-input': 'Core automation', 'core-install': 'Browser installation', 'vision': 'Coordinate-based (opt-in via --caps=vision)', 'pdf': 'PDF generation (opt-in via --caps=pdf)', 'testing': 'Test assertions (opt-in via --caps=testing)', 'tracing': 'Tracing (opt-in via --caps=tracing)', }; /** @type {Record} */ const toolsByCapability = {}; for (const [capability, title] of Object.entries(capabilities)) { let tools = browserTools.filter(tool => tool.capability === capability && !tool.skillOnly); tools = (toolsByCapability[title] || []).concat(tools); toolsByCapability[title] = tools; } for (const [, tools] of Object.entries(toolsByCapability)) tools.sort((a, b) => a.schema.name.localeCompare(b.schema.name)); /** * @param {any} tool * @returns {string[]} */ function formatToolForReadme(tool) { const lines = /** @type {string[]} */ ([]); lines.push(``); lines.push(``); lines.push(`- **${tool.name}**`); lines.push(` - Title: ${tool.title}`); lines.push(` - Description: ${tool.description}`); const inputSchema = /** @type {any} */ (tool.inputSchema ? tool.inputSchema.toJSONSchema() : {}); const requiredParams = inputSchema.required || []; if (inputSchema.properties && Object.keys(inputSchema.properties).length) { lines.push(` - Parameters:`); Object.entries(inputSchema.properties).forEach(([name, param]) => { const optional = !requiredParams.includes(name); const meta = /** @type {string[]} */ ([]); if (param.type) meta.push(param.type); if (optional) meta.push('optional'); lines.push(` - \`${name}\` ${meta.length ? `(${meta.join(', ')})` : ''}: ${param.description}`); }); } else { lines.push(` - Parameters: None`); } lines.push(` - Read-only: **${tool.type === 'readOnly'}**`); lines.push(''); return lines; } /** * @param {string} content * @param {string} startMarker * @param {string} endMarker * @param {string[]} generatedLines * @returns {Promise} */ async function updateSection(content, startMarker, endMarker, generatedLines) { const startMarkerIndex = content.indexOf(startMarker); const endMarkerIndex = content.indexOf(endMarker); if (startMarkerIndex === -1 || endMarkerIndex === -1) throw new Error('Markers for generated section not found in README'); return [ content.slice(0, startMarkerIndex + startMarker.length), '', generatedLines.join('\n'), '', content.slice(endMarkerIndex), ].join('\n'); } /** * @param {string} content * @returns {Promise} */ async function updateTools(content) { console.log('Loading tool information from compiled modules...'); const generatedLines = /** @type {string[]} */ ([]); for (const [capability, tools] of Object.entries(toolsByCapability)) { console.log('Updating tools for capability:', capability); generatedLines.push(`
\n${capability}`); generatedLines.push(''); for (const tool of tools) generatedLines.push(...formatToolForReadme(tool.schema)); generatedLines.push(`
`); generatedLines.push(''); } const startMarker = ``; const endMarker = ``; return updateSection(content, startMarker, endMarker, generatedLines); } /** * @param {string} content * @returns {Promise} */ async function updateOptions(content) { console.log('Listing options...'); execSync('node cli.js --help > help.txt'); const output = fs.readFileSync('help.txt'); fs.unlinkSync('help.txt'); const lines = output.toString().split('\n'); const firstLine = lines.findIndex(line => line.includes('--version')); lines.splice(0, firstLine + 1); const lastLine = lines.findIndex(line => line.includes('--help')); lines.splice(lastLine); /** * @type {{ name: string, value: string }[]} */ const options = []; for (let line of lines) { if (line.startsWith(' --')) { const l = line.substring(' --'.length); const gapIndex = l.indexOf(' '); const name = l.substring(0, gapIndex).trim(); const value = l.substring(gapIndex).trim(); options.push({ name, value }); } else { const value = line.trim(); options[options.length - 1].value += ' ' + value; } } const table = []; table.push(`| Option | Description |`); table.push(`|--------|-------------|`); for (const option of options) { const prefix = option.name.split(' ')[0]; const envName = `PLAYWRIGHT_MCP_` + prefix.toUpperCase().replace(/-/g, '_'); table.push(`| --${option.name} | ${option.value}
*env* \`${envName}\` |`); } if (process.env.PRINT_ENV) { const envTable = []; envTable.push(`| Environment |`); envTable.push(`|-------------|`); for (const option of options) { const prefix = option.name.split(' ')[0]; const envName = `PLAYWRIGHT_MCP_` + prefix.toUpperCase().replace(/-/g, '_'); envTable.push(`| \`${envName}\` ${option.value} |`); } console.log(envTable.join('\n')); } const startMarker = ``; const endMarker = ``; return updateSection(content, startMarker, endMarker, table); } /** * @param {string} content * @returns {Promise} */ async function updateConfig(content) { console.log('Updating config schema from config.d.ts...'); const configPath = path.join(__dirname, 'config.d.ts'); const configContent = await fs.promises.readFile(configPath, 'utf-8'); // Extract the Config type definition const configTypeMatch = configContent.match(/export type Config = (\{[\s\S]*?\n\});/); if (!configTypeMatch) throw new Error('Config type not found in config.d.ts'); const configType = configTypeMatch[1]; // Use capture group to get just the object definition const startMarker = ``; const endMarker = ``; return updateSection(content, startMarker, endMarker, [ '```typescript', configType, '```', ]); } /** * @param {string} filePath */ async function copyToPackage(filePath) { await fs.promises.copyFile(path.join(__dirname, '../../', filePath), path.join(__dirname, filePath)); console.log(`${filePath} copied successfully`); } async function updateReadme() { const readmePath = path.join(__dirname, '../../README.md'); const readmeContent = await fs.promises.readFile(readmePath, 'utf-8'); const withTools = await updateTools(readmeContent); const withOptions = await updateOptions(withTools); const withConfig = await updateConfig(withOptions); await fs.promises.writeFile(readmePath, withConfig, 'utf-8'); console.log('README updated successfully'); await copyToPackage('README.md'); await copyToPackage('LICENSE'); } updateReadme().catch(err => { console.error('Error updating README:', err); process.exit(1); });