├── .gitattributes ├── .github └── workflows │ ├── docusaurus-gh-pages.yml │ ├── node.js.yml │ └── test.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── docs ├── docs │ ├── ai-courses │ │ ├── AIAgents.mdx │ │ ├── GenAICourse.mdx │ │ ├── MachineLearning.mdx │ │ ├── _category_.json │ │ └── img │ │ │ └── GenAI.png │ ├── img │ │ └── mcp-server.png │ ├── intro.mdx │ ├── local-setup │ │ ├── Debugging.mdx │ │ ├── Installation.mdx │ │ ├── _category_.json │ │ └── img │ │ │ └── mcp-server.png │ ├── playwright-api │ │ ├── Examples.md │ │ ├── Supported-Tools.mdx │ │ ├── _category_.json │ │ └── img │ │ │ ├── api-response.png │ │ │ └── playwright-api.png │ ├── playwright-web │ │ ├── Console-Logging.mdx │ │ ├── Examples.md │ │ ├── Recording-Actions.mdx │ │ ├── Support-of-Cline-Cursor.mdx │ │ ├── Supported-Tools.mdx │ │ ├── _category_.json │ │ └── img │ │ │ ├── console-log.gif │ │ │ ├── mcp-execution.png │ │ │ └── mcp-result.png │ ├── release.mdx │ └── testing-videos │ │ ├── AIAgents.mdx │ │ ├── Bdd.mdx │ │ └── _category_.json ├── docusaurus.config.ts ├── package-lock.json ├── package.json ├── sidebars.ts ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── YouTubeVideoEmbed.tsx │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ ├── index.tsx │ │ └── markdown-page.md ├── static │ ├── .nojekyll │ └── img │ │ ├── EA-Icon.jpg │ │ ├── EA-Icon.svg │ │ ├── docusaurus-social-card.jpg │ │ ├── docusaurus.png │ │ ├── easy-to-use.svg │ │ ├── favicon.ico │ │ ├── logo.svg │ │ ├── node.svg │ │ ├── playwright.svg │ │ ├── undraw_docusaurus_mountain.svg │ │ ├── undraw_docusaurus_react.svg │ │ └── undraw_docusaurus_tree.svg └── tsconfig.json ├── image └── playwright_claude.png ├── jest.config.cjs ├── mcp-config.json ├── package-lock.json ├── package.json ├── run-tests.cjs ├── run-tests.js ├── smithery.yaml ├── src ├── __tests__ │ ├── codegen.test.ts │ ├── toolHandler.test.ts │ ├── tools.test.ts │ └── tools │ │ ├── api │ │ └── requests.test.ts │ │ └── browser │ │ ├── advancedInteraction.test.ts │ │ ├── console.test.ts │ │ ├── goNavigation.test.ts │ │ ├── interaction.test.ts │ │ ├── navigation.test.ts │ │ ├── output.test.ts │ │ ├── screenshot.test.ts │ │ └── visiblePage.test.ts ├── evals │ └── evals.ts ├── index.ts ├── requestHandler.ts ├── toolHandler.ts ├── tools.ts ├── tools │ ├── api │ │ ├── base.ts │ │ ├── index.ts │ │ └── requests.ts │ ├── browser │ │ ├── base.ts │ │ ├── console.ts │ │ ├── index.ts │ │ ├── interaction.ts │ │ ├── navigation.ts │ │ ├── output.ts │ │ ├── response.ts │ │ ├── screenshot.ts │ │ ├── useragent.ts │ │ └── visiblePage.ts │ ├── codegen │ │ ├── generator.ts │ │ ├── index.ts │ │ ├── recorder.ts │ │ └── types.ts │ ├── common │ │ └── types.ts │ └── index.ts └── types.ts ├── test-import.js ├── tsconfig.json └── tsconfig.test.json /.gitattributes: -------------------------------------------------------------------------------- 1 | package-lock.json linguist-generated=true 2 | -------------------------------------------------------------------------------- /.github/workflows/docusaurus-gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docusaurus documentation to GH Pages 2 | 3 | on: 4 | # Runs on pushes targeting the default branch 5 | push: 6 | branches: ["main"] 7 | pull_request: 8 | branches: [ "main" ] 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 14 | permissions: 15 | contents: read 16 | pages: write 17 | id-token: write 18 | 19 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 20 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 21 | concurrency: 22 | group: "pages" 23 | cancel-in-progress: false 24 | 25 | jobs: 26 | # Build job 27 | build: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v4 32 | - name: Install dependencies 33 | working-directory: docs 34 | run: npm install 35 | - name: Build site 36 | working-directory: docs 37 | run: npm run build 38 | - name: Upload artifact 39 | uses: actions/upload-pages-artifact@v3 40 | with: 41 | path: docs/build 42 | 43 | # Deployment job 44 | deploy: 45 | environment: 46 | name: github-pages 47 | url: ${{ steps.deployment.outputs.page_url }} 48 | runs-on: ubuntu-latest 49 | needs: build 50 | steps: 51 | - name: Deploy to GitHub Pages 52 | id: deployment 53 | uses: actions/deploy-pages@v4 54 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x, 20.x, 22.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v4 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm run build --if-present 31 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Set up Node.js 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 22 # Use current LTS version 18 | cache: 'npm' 19 | 20 | - name: Install dependencies 21 | run: npm install 22 | 23 | - name: Build project 24 | run: npm run build 25 | 26 | - name: Run tests with coverage (custom script) 27 | run: node run-tests.cjs 28 | continue-on-error: true 29 | id: custom-test 30 | 31 | - name: Run tests with coverage (npm script) 32 | if: steps.custom-test.outcome == 'failure' 33 | run: npm run test:coverage -- --testMatch="/src/__tests__/**/*.test.ts" 34 | 35 | - name: Upload coverage report 36 | uses: actions/upload-artifact@v4 37 | with: 38 | name: coverage-report 39 | path: coverage/ 40 | if-no-files-found: warn 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | 133 | .DS_Store 134 | .DS_Store? -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile 2 | # Use Node.js image for building the project 3 | FROM node:20-alpine AS builder 4 | 5 | # Set the working directory 6 | WORKDIR /app 7 | 8 | # Copy package.json and package-lock.json 9 | COPY package.json package-lock.json ./ 10 | 11 | # Install dependencies without running scripts to prevent automatic build 12 | RUN npm install --ignore-scripts 13 | 14 | # Copy the entire source directory 15 | COPY src ./src 16 | COPY tsconfig.json ./ 17 | 18 | # Build the project 19 | RUN npm run build 20 | 21 | # Use a minimal Node.js image for running the project 22 | FROM node:20-alpine AS release 23 | 24 | # Set the working directory 25 | WORKDIR /app 26 | 27 | # Copy the built files from the builder stage 28 | COPY --from=builder /app/dist ./dist 29 | COPY --from=builder /app/package.json ./package.json 30 | COPY --from=builder /app/package-lock.json ./package-lock.json 31 | 32 | # Install production dependencies 33 | RUN npm ci --ignore-scripts --omit=dev 34 | 35 | # Set the command to run the server 36 | ENTRYPOINT ["node", "dist/index.js"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 ExecuteAutomation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 9 | 14 | 15 | 16 | 17 | 18 | 19 |
5 | 6 | MseeP.ai Security Assessment Badge 7 | 8 | 10 | 11 | Warp sponsorship 12 | 13 |
MseeP.ai Security AssessmentSpecial thanks to Warp, the AI terminal for developers
20 |
21 |
22 | 23 | # Playwright MCP Server 🎭 24 | 25 | [![smithery badge](https://smithery.ai/badge/@executeautomation/playwright-mcp-server)](https://smithery.ai/server/@executeautomation/playwright-mcp-server) 26 | 27 | A Model Context Protocol server that provides browser automation capabilities using Playwright. This server enables LLMs to interact with web pages, take screenshots, generate test code, web scraps the page and execute JavaScript in a real browser environment. 28 | 29 | mcp-playwright MCP server 30 | 31 | ## Screenshot 32 | ![Playwright + Claude](image/playwright_claude.png) 33 | 34 | ## [Documentation](https://executeautomation.github.io/mcp-playwright/) | [API reference](https://executeautomation.github.io/mcp-playwright/docs/playwright-web/Supported-Tools) 35 | 36 | ## Installation 37 | 38 | You can install the package using either npm, mcp-get, or Smithery: 39 | 40 | Using npm: 41 | ```bash 42 | npm install -g @executeautomation/playwright-mcp-server 43 | ``` 44 | 45 | Using mcp-get: 46 | ```bash 47 | npx @michaellatman/mcp-get@latest install @executeautomation/playwright-mcp-server 48 | ``` 49 | Using Smithery 50 | 51 | To install Playwright MCP for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@executeautomation/playwright-mcp-server): 52 | 53 | ```bash 54 | npx @smithery/cli install @executeautomation/playwright-mcp-server --client claude 55 | ``` 56 | #### Installation in VS Code 57 | 58 | Install the Playwright MCP server in VS Code using one of these buttons: 59 | 60 | 67 | 68 | [Install in VS Code](https://insiders.vscode.dev/redirect?url=vscode%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522%2540executeautomation%252Fplaywright-mcp-server%2522%255D%257D) 69 | [Install in VS Code Insiders](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522-y%2522%252C%2522%2540executeautomation%252Fplaywright-mcp-server%2522%255D%257D) 70 | 71 | Alternatively, you can install the Playwright MCP server using the VS Code CLI: 72 | 73 | ```bash 74 | # For VS Code 75 | code --add-mcp '{"name":"playwright","command":"npx","args":["@executeautomation/playwright-mcp-server"]}' 76 | ``` 77 | 78 | ```bash 79 | # For VS Code Insiders 80 | code-insiders --add-mcp '{"name":"playwright","command":"npx","args":["@executeautomation/playwright-mcp-server"]}' 81 | ``` 82 | 83 | After installation, the ExecuteAutomation Playwright MCP server will be available for use with your GitHub Copilot agent in VS Code. 84 | 85 | ## Configuration to use Playwright Server 86 | Here's the Claude Desktop configuration to use the Playwright server: 87 | 88 | ```json 89 | { 90 | "mcpServers": { 91 | "playwright": { 92 | "command": "npx", 93 | "args": ["-y", "@executeautomation/playwright-mcp-server"] 94 | } 95 | } 96 | } 97 | ``` 98 | 99 | ## Testing 100 | 101 | This project uses Jest for testing. The tests are located in the `src/__tests__` directory. 102 | 103 | ### Running Tests 104 | 105 | You can run the tests using one of the following commands: 106 | 107 | ```bash 108 | # Run tests using the custom script (with coverage) 109 | node run-tests.cjs 110 | 111 | # Run tests using npm scripts 112 | npm test # Run tests without coverage 113 | npm run test:coverage # Run tests with coverage 114 | npm run test:custom # Run tests with custom script (same as node run-tests.cjs) 115 | ``` 116 | 117 | The test coverage report will be generated in the `coverage` directory. 118 | 119 | ### Running evals 120 | 121 | The evals package loads an mcp client that then runs the index.ts file, so there is no need to rebuild between tests. You can load environment variables by prefixing the npx command. Full documentation can be found [here](https://www.mcpevals.io/docs). 122 | 123 | ```bash 124 | OPENAI_API_KEY=your-key npx mcp-eval src/evals/evals.ts src/tools/codegen/index.ts 125 | ``` 126 | 127 | ## Star History 128 | 129 | [![Star History Chart](https://api.star-history.com/svg?repos=executeautomation/mcp-playwright&type=Date)](https://star-history.com/#executeautomation/mcp-playwright&Date) 130 | -------------------------------------------------------------------------------- /docs/docs/ai-courses/AIAgents.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed'; 6 | 7 | # 🧠🤖 Build & Test AI Agents, ChatBots, and RAG with Ollama & Local LLM 8 | 9 |
10 | 11 |
12 | 13 | --- 14 | 15 | :::info 💡 **Note** 16 | All the courses are available on **Udemy**, and they almost always have a **`coupon code`** available. 17 | For discounts, please feel free to reach out at **[karthik@techgeek.co.in](mailto:karthik@techgeek.co.in)**. 18 | 19 | 🎯 **Course Link:** 20 | [Build & Test AI Agents, ChatBots, and RAG with Ollama & Local LLM](https://www.udemy.com/course/build-ai-agent-chatbot-rag-langchain-local-llm/) 21 | ::: 22 | 23 | --- 24 | 25 | ## 📚 **Course Description** 26 | 27 | This course is designed for complete beginners—even if you have **zero knowledge of LangChain**, you’ll learn step-by-step how to build **LLM-based applications** using **local Large Language Models (LLMs)**. 28 | 29 | We’ll go beyond development and dive into **evaluating and testing AI agents**, **RAG applications**, and **chatbots** using **RAGAs** to ensure they deliver **accurate** and **reliable results**, following key industry metrics for **AI performance**. 30 | 31 | --- 32 | 33 | ### 🚀 **What You’ll Learn** 34 | 35 | - **🧠 Fundamentals of LangChain & LangSmith** 36 | Get a solid foundation in building and testing **LLM-based applications**. 37 | 38 | - **💬 Chat Message History in LangChain** 39 | Learn how to store conversation data for **chatbots** and **AI agents**. 40 | 41 | - **⚙️ Running Parallel & Multiple Chains** 42 | Master advanced techniques like **RunnableParallels** to optimize your **LLM workflows**. 43 | 44 | - **🤖 Building Chatbots with LangChain & Streamlit** 45 | Create chatbots with **message history** and an interactive **UI**. 46 | 47 | - **🛠️ Tools & Tool Chains in LLMs** 48 | Understand the power of **Tooling**, **Custom Tools**, and how to build **Tool Chains** for **AI applications**. 49 | 50 | - **🧑‍💻 Creating AI Agents with LangChain** 51 | Implement **AI agents** that can interact dynamically with **RAG applications**. 52 | 53 | - **📚 Implementing RAG with Vector Stores & Local Embeddings** 54 | Develop robust **RAG solutions** with local **LLM embeddings**. 55 | 56 | - **🔧 Using AI Agents & RAG with Tooling** 57 | Learn how to integrate **Tooling** effectively while building **LLM Apps**. 58 | 59 | - **🚦 Optimizing & Debugging AI Applications with LangSmith** 60 | Enhance your **AI models** and **applications** with **LangSmith's debugging** and **optimization tools**. 61 | 62 | - **🧪 Evaluating & Testing LLM Applications with RAGAs** 63 | Apply **hands-on testing strategies** to validate **RAG** and **AI agent** performance. 64 | 65 | - **📊 Real-world Projects & Assessments** 66 | Gain practical experience with **RAGAs** and learn to assess the quality and reliability of **AI solutions**. 67 | 68 | --- 69 | 70 | ## 🎯 **Learning Experience** 71 | 72 | This entire course is taught inside a **Jupyter Notebook** with **Visual Studio**, offering an **interactive**, **guided experience** where you can **run the code seamlessly** and **follow along effortlessly**. 73 | 74 | By the end of this course, you’ll have the **confidence** to **build**, **test**, and **optimize AI-powered applications** with ease! -------------------------------------------------------------------------------- /docs/docs/ai-courses/GenAICourse.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed'; 6 | 7 | # 🤖 Using Generative AI in Software Automation Testing 8 | 9 |
10 | 11 |
12 | --- 13 | 14 | :::info 💡 **Note** 15 | All the courses are available on Udemy, and they almost always have a `coupon code` available. 16 | For discounts, please feel free to reach out at **[karthik@techgeek.co.in](mailto:karthik@techgeek.co.in)**. 17 | 18 | 🎯 **Course Link:** 19 | [Generative AI in Software Automation Testing](https://www.udemy.com/course/generative-ai-in-software-automation-testing/) 20 | ::: 21 | 22 | --- 23 | 24 | ## 📚 **Course Description** 25 | 26 | This course is crafted for everyone, whether you're new to Software Testing or an experienced professional. Unlock the full potential of **Generative AI** and transform your testing process into something **faster**, **smarter**, and **more efficient**. 27 | 28 | ### 🚀 **What You’ll Master** 29 | 30 | - **🧠 Introduction to Generative AI:** 31 | Understand the foundations of Gen AI and its role in Software Testing. 32 | 33 | - **💻 Running Large Language Models (LLMs) Locally:** 34 | Learn how to run models on your machine without paying for external services. 35 | 36 | - **📝 Manual Testing with Gen AI:** 37 | Generate manual test cases, test data, and test requirements using grounded Models with the power of AI and RAG. 38 | 39 | - **🤖 Automated UI Testing:** 40 | Leverage AI to write, refactor, and optimize automated tests for UI applications. 41 | 42 | - **🎭 Playwright UI Testing:** 43 | Use Playwright and AI-driven tools to create smart test scripts and handle complex workflows. 44 | 45 | - **🚫 No-code Automation with TestRigor:** 46 | Create powerful automation suites in plain English, even automating SMS, phone calls, and intricate tables. 47 | 48 | - **🔗 API Testing:** 49 | Harness PostBots and Gen AI to streamline API testing. 50 | 51 | - **🧬 Using Gen AI APIs:** 52 | Add intelligence to your Test Automation code using OpenAI APIs. 53 | 54 | - **📍 Model Context Protocol (MCP):** 55 | Run Playwright tests for UI and APIs by leveraging the power of MCP. 56 | 57 | --- 58 | 59 | By the end of this course, you'll have a deep understanding of how **Generative AI** can supercharge your testing process. With hands-on experience, you'll be able to use **AI-enhanced tools** and **LLMs** to simplify complex testing tasks, making your work smoother and more efficient. 60 | 61 | 62 | -------------------------------------------------------------------------------- /docs/docs/ai-courses/MachineLearning.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed'; 6 | 7 | # 🧠 Understand, Test, and Fine-tune AI Models with Hugging Face 8 | 9 |
10 | 11 |
12 | 13 | --- 14 | 15 | :::info 💡 **Note** 16 | All the courses are available on **Udemy**, and they almost always have a **`coupon code`** available. 17 | For discounts, please feel free to reach out at **[karthik@techgeek.co.in](mailto:karthik@techgeek.co.in)**. 18 | 19 | 🎯 **Course Link:** 20 | [Understand, Test, and Fine-tune AI Models with Hugging Face](https://www.udemy.com/course/ai-with-huggingface/) 21 | ::: 22 | 23 | --- 24 | 25 | ## 📚 **Course Description** 26 | 27 | This course provides a complete journey into **Understanding, Testing, and Fine-tuning AI Models** using the **Hugging Face** library. Whether you are a beginner or an experienced engineer, this course equips you with **hands-on expertise** in every step of the **machine learning pipeline**, from **basic concepts** to **advanced model testing**, **fine-tuning**, and **deployment**. 28 | 29 | --- 30 | 31 | ### 🚀 **What You’ll Learn** 32 | 33 | 1. **📈 Introduction to Machine Learning:** 34 | Lay a strong foundation by exploring key ML concepts and essential terminology. 35 | 36 | 2. **📊 Working with Natural Language Processing (NLP) Libraries:** 37 | Learn how to process, analyze, and derive insights from textual data using popular NLP tools. 38 | 39 | 3. **💡 Deep Dive into the Transformers Library:** 40 | Master Hugging Face’s Transformers, the industry standard for building state-of-the-art **NLP** and **LLM** solutions. 41 | 42 | 4. **🧠 Working with Large Language Models (LLMs):** 43 | Explore multiple methods to interact with and utilize **LLMs** for diverse real-world applications. 44 | 45 | 5. **🧪 Functional Testing of AI Models:** 46 | Ensure your models perform reliably across different scenarios using systematic testing strategies. 47 | 48 | 6. **⚖️ Bias and Fairness Testing:** 49 | Implement techniques to detect and mitigate unintended bias, promoting ethical and fair **AI practices**. 50 | 51 | 7. **📏 Evaluating AI Models:** 52 | Measure performance with robust metrics and refine your models for optimal results. 53 | 54 | 8. **🤖 Working with AI Agents:** 55 | Build, configure, and integrate **intelligent agents** into your workflows. 56 | 57 | 9. **🔬 Fine-tuning and Training AI Models:** 58 | Customize pre-trained models or create your own from scratch to meet specific project requirements. 59 | 60 | --- 61 | 62 | By the end of this course, you’ll gain the **knowledge** and **practical experience** needed to confidently **develop**, **test**, and **optimize** your own **Transformer-based models** and **LLMs**, empowering you to thrive in the rapidly evolving world of **AI**. -------------------------------------------------------------------------------- /docs/docs/ai-courses/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "AI Courses to Learn", 3 | "position": 6, 4 | "collapsed": false, 5 | "link": { 6 | "type": "generated-index", 7 | "description": "AI Courses which helps you learn more on Using it for Testing and Development" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/docs/ai-courses/img/GenAI.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executeautomation/mcp-playwright/ce17a321a24790885e8c65c37241493d5401e782/docs/docs/ai-courses/img/GenAI.png -------------------------------------------------------------------------------- /docs/docs/img/mcp-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executeautomation/mcp-playwright/ce17a321a24790885e8c65c37241493d5401e782/docs/docs/img/mcp-server.png -------------------------------------------------------------------------------- /docs/docs/intro.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed'; 6 | 7 | # Playwright MCP Server 8 | 9 | The **Playwright Model Context Protocol (MCP) server** is a powerful solution for automating Browser and API testing using Playwright. 10 | 11 | With the Playwright MCP server, you can: 12 | - Enable LLMs to interact with web pages in a real browser environment. 13 | - Perform tasks such as executing JavaScript, taking screenshots, and navigating web elements. 14 | - Seamlessly handle API testing to validate endpoints and ensure reliability. 15 | - Test across multiple browser engines including Chromium, Firefox, and WebKit. 16 | 17 | ![Playwright MCP Server](./img/mcp-server.png) 18 | 19 | 20 | ## Installation 21 | You can install Playwright MCP Server package using either **npm**, **mcp-get**, or **Smithery**: 22 | 23 | :::info Playwright MCP Tips 24 | 25 | To get started more quickly on Playwright MCP Server, watch the videos mentioned in the footer of this page under `Docs` 26 | 27 | ::: 28 | 29 | 30 | ### Installing via NPM 31 | To install Playwright MCP for Claude Desktop automatically via Smithery: 32 | 33 | ```bash 34 | npm install -g @executeautomation/playwright-mcp-server 35 | ``` 36 | 37 | ### Installing via Smithery 38 | To install Playwright MCP for Claude Desktop automatically via Smithery: 39 | 40 | ```bash 41 | npx @smithery/cli install @executeautomation/playwright-mcp-server --client claude 42 | ``` 43 | 44 | You can type this command into Command Prompt, Powershell, Terminal, or any other integrated terminal of your code editor. 45 | 46 | ### Installing via MCP-GET 47 | To install Playwright MCP for Claude Desktop automatically via Smithery: 48 | 49 | ```bash 50 | npx @michaellatman/mcp-get@latest install @executeautomation/playwright-mcp-server 51 | ``` 52 | 53 | ### Configuring Playwright MCP in Claude Desktop 54 | Here's the Claude Desktop configuration to use the Playwright MCP server. 55 | 56 | Modify your `claude-desktop-config.json` file as shown below 57 | 58 | ```json 59 | { 60 | "mcpServers": { 61 | "playwright": { 62 | "command": "npx", 63 | "args": ["-y", "@executeautomation/playwright-mcp-server"] 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | ### What is Model Context Protocol 70 | This video should give you an high level overview of what Claude's MCP is and how helpful it will soon become for AI agents 71 | 72 | 73 | -------------------------------------------------------------------------------- /docs/docs/local-setup/Debugging.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed'; 6 | 7 | ## Debugging MCP Server 🚀 8 | 9 | Efficiently debug your **MCP Server** with **MCP Inspector** from Claude! This powerful tool helps you **speed up debugging** and testing for the tools you build for the **Playwright MCP Server**. 10 | 11 | 12 | ## Step 1 : Install the MCP Inspector 13 | To get started, run the following command: 14 | 15 | ```bash 16 | npx @modelcontextprotocol/inspector node dist/index.js 17 | 18 | ``` 19 | 20 | ## Step 2: Navigate to MCP Inspector 21 | 22 | ```bash 23 | http://localhost:5173 🚀 24 | ``` 25 | 26 | 27 | ## Here is the video demonstration 28 | -------------------------------------------------------------------------------- /docs/docs/local-setup/Installation.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | ## Build and Run Playwright MCP Server locally 6 | To build/run Playwright MCP Server in your local machine follow the below steps 7 | 8 | ### Step 1 : Clone Repository 9 | 10 | ```bash 11 | git clone https://github.com/executeautomation/mcp-playwright.git 12 | ``` 13 | 14 | ## Step 2: Install Dependencies 15 | ```bash 16 | npm install 17 | ``` 18 | 19 | ## Step 3: Build Code 20 | ```bash 21 | npm run build 22 | npm link 23 | ``` 24 | 25 | ## Step 4: Configuring Playwright MCP in Claude Desktop 26 | 27 | Modify your `claude-desktop-config.json` file as shown below to work with local playwright mcp server 28 | 29 | ```json 30 | { 31 | "mcpServers": { 32 | "playwright": { 33 | "command": "npx", 34 | "args": [ 35 | "--directory", 36 | "/your-playwright-mcp-server-clone-directory", 37 | "run", 38 | "@executeautomation/playwright-mcp-server" 39 | ] 40 | } 41 | } 42 | } 43 | ``` 44 | 45 | :::warning Important 46 | After modifying the `claude-desktop-config.json` file, you **must** completely close Claude Desktop and **manually terminate** any running processes from **Task Manager** (Windows 10/11). 47 | 48 | ⚠️ If you skip this step, the configuration changes **may not take effect**. 49 | ::: 50 | 51 | ## Reward 52 | If your setup is all correct, you should see Playwright MCP Server pointing your local machine source code 53 | 54 | ![Playwright MCP Server](./img/mcp-server.png) 55 | -------------------------------------------------------------------------------- /docs/docs/local-setup/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Local Development", 3 | "position": 3, 4 | "collapsed": false, 5 | "link": { 6 | "type": "generated-index", 7 | "description": "Understand how to setup Playwright MCP Server to run in your local machine." 8 | } 9 | } -------------------------------------------------------------------------------- /docs/docs/local-setup/img/mcp-server.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executeautomation/mcp-playwright/ce17a321a24790885e8c65c37241493d5401e782/docs/docs/local-setup/img/mcp-server.png -------------------------------------------------------------------------------- /docs/docs/playwright-api/Examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # ⚙️Examples of API automation 6 | 7 | Lets see how we can use the power of Playwright MCP Server to automate API of our application 8 | 9 | ### Scenario 10 | 11 | ```json 12 | // Basic POST request 13 | Perform POST operation for the URL https://api.restful-api.dev/objects with body 14 | { 15 | "name": "Apple MacBook Pro 16", 16 | "data": { 17 | "year": 2024, 18 | "price": 2499, 19 | "CPU model": "M4", 20 | "Hard disk size": "5 TB" 21 | } 22 | } 23 | And verify if the response has createdAt and id property and store the ID in a variable for future reference say variable productID 24 | 25 | // POST request with Bearer token authorization 26 | Perform POST operation for the URL https://api.restful-api.dev/objects with Bearer token "your-token-here" set in the headers 27 | { 28 | 'Content-Type': 'application/json', 29 | 'Authorization': 'Bearer your-token-here' 30 | }, 31 | and body 32 | { 33 | "name": "Secure MacBook Pro", 34 | "data": { 35 | "year": 2024, 36 | "price": 2999, 37 | "CPU model": "M4 Pro", 38 | "Hard disk size": "8 TB", 39 | "security": "enhanced" 40 | } 41 | } 42 | 43 | Perform GET operation for the created ProductID using URL https://api.restful-api.dev/objects/productID and verify the response has properties like Id, name, data 44 | 45 | Perform PUT operation for the created ProductID using URL https://api.restful-api.dev/objects/productID with body { 46 | "name": "Apple MacBook Pro 16", 47 | "data": { 48 | "year": 2025, 49 | "price": 4099, 50 | "CPU model": "M5", 51 | "Hard disk size": "10 TB", 52 | "color": "Titanium" 53 | } 54 | } 55 | 56 | And verify if the response has createdAt and id property 57 | 58 | Perform PATCH operation for the created ProductID using URL https://api.restful-api.dev/objects/productID with body 59 | { 60 | "name": "Apple MacBook Pro 19 (Limited Edition)" 61 | } 62 | And verify if the response has updatedAt property with value Apple MacBook Pro 19 (Limited Edition) 63 | 64 | ``` 65 | 66 | And once the entire test operation completes, we will be presented with the entire details of how the automation did happened. 67 | 68 | ![Playwright MCP Server](./img/playwright-api.png) 69 | 70 | :::tip 71 | You can also see the `Request/Response/StatusCode` from the execution of Playwright MCP Server 72 | 73 | ![Playwright MCP Server](./img/api-response.png) 74 | ::: 75 | -------------------------------------------------------------------------------- /docs/docs/playwright-api/Supported-Tools.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed'; 6 | 7 | 8 | # 🛠️ Supported Tools 9 | 10 | Playwright MCP for API automation has following key features 11 | - Support of GET Request 12 | - Support of POST Request 13 | - Support of PATCH Request 14 | - Support of PUT Request 15 | - Support of DELETE Request 16 | 17 | 18 | 19 | 20 | --- 21 | 22 | :::warning Note 23 | Still the library is not matured enough to support Oauth, Multi-form, Binary input or complex API requests. Please feel free to fork the repo and add the feature with a PR, will can build the library together! 24 | ::: 25 | 26 | ### Playwright_get 27 | Perform a GET operation on any given API request. 28 | 29 | - **Inputs:** 30 | - **`url`** *(string)*: 31 | URL to perform the GET operation. 32 | 33 | - **Response:** 34 | - **`statusCode`** *(string)*: 35 | Status code of the API. 36 | 37 | --- 38 | 39 | ### Playwright_post 40 | Perform a POST operation on any given API request. 41 | 42 | - **Inputs:** 43 | - **`url`** *(string)*: 44 | URL to perform the POST operation. 45 | - **`value`** *(string)*: 46 | Data to include in the body of the POST request. 47 | - **`token`** *(string, optional)*: 48 | Bearer token for authorization. When provided, it will be sent as `Authorization: Bearer ` header. 49 | - **`headers`** *(object, optional)*: 50 | Additional headers to include in the request. Note: Content-Type: application/json is set by default. 51 | 52 | - **Response:** 53 | - **`statusCode`** *(string)*: 54 | Status code of the API. 55 | - **`responseData`** *(string)*: 56 | Response data in JSON format. 57 | 58 | --- 59 | 60 | ### Playwright_put 61 | Perform a PUT operation on any given API request. 62 | 63 | - **Inputs:** 64 | - **`url`** *(string)*: 65 | URL to perform the PUT operation. 66 | - **`value`** *(string)*: 67 | Data to include in the body of the PUT request. 68 | 69 | - **Response:** 70 | - **`statusCode`** *(string)*: 71 | Status code of the API. 72 | - **`responseData`** *(string)*: 73 | Response data in JSON format. 74 | 75 | --- 76 | 77 | ### Playwright_patch 78 | Perform a PATCH operation on any given API request. 79 | 80 | - **Inputs:** 81 | - **`url`** *(string)*: 82 | URL to perform the PATCH operation. 83 | - **`value`** *(string)*: 84 | Data to include in the body of the PATCH request. 85 | 86 | - **Response:** 87 | - **`statusCode`** *(string)*: 88 | Status code of the API. 89 | - **`responseData`** *(string)*: 90 | Response data in JSON format. 91 | 92 | --- 93 | 94 | ### Playwright_delete 95 | Perform a DELETE operation on any given API request. 96 | 97 | - **Inputs:** 98 | - **`url`** *(string)*: 99 | URL to perform the DELETE operation. 100 | 101 | - **Response:** 102 | - **`statusCode`** *(string)*: 103 | Status code of the API. 104 | 105 | ### Upon running the test Claude Desktop will run MCP Server to use above tools 106 | ![Playwright MCP Server](./img/playwright-api.png) -------------------------------------------------------------------------------- /docs/docs/playwright-api/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Playwright API Features", 3 | "position": 5, 4 | "collapsed": false, 5 | "link": { 6 | "type": "generated-index", 7 | "description": "Supported features in Playwright API Testing." 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/docs/playwright-api/img/api-response.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executeautomation/mcp-playwright/ce17a321a24790885e8c65c37241493d5401e782/docs/docs/playwright-api/img/api-response.png -------------------------------------------------------------------------------- /docs/docs/playwright-api/img/playwright-api.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executeautomation/mcp-playwright/ce17a321a24790885e8c65c37241493d5401e782/docs/docs/playwright-api/img/playwright-api.png -------------------------------------------------------------------------------- /docs/docs/playwright-web/Console-Logging.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # 📁 Support of Console Logs 6 | 7 | Playwright MCP Server now supports Console logging of browsers with the power of Playwright. 8 | This feature is especially useful when you want to capture the logs of the browser console while performing any action during development and testing. 9 | 10 | Following logs types are supported 11 | - `log` 12 | - `info` 13 | - `warn` 14 | - `error` 15 | - `debug` 16 | - `all` 17 | 18 | :::tip Usage Example 19 | To invoke `Playwright_console_logs` via MCP Playwright, use the following prompt: 20 | 21 | ```plaintext 22 | Get the console log from the browser whenever you perform any action. 23 | ::: 24 | 25 | --- 26 | :::info 27 | ![Playwright Console Logs ](./img/console-log.gif) 28 | 29 | Demo of how the console logs are captured in Playwright MCP Server 30 | ::: 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/docs/playwright-web/Examples.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # 🌐 Examples of browser automation 6 | Lets see how we can use the power of Playwright MCP Server to automate our browser and do webscrapping 7 | 8 | ### Using Different Browser Types 9 | 10 | Playwright MCP now supports multiple browser engines. You can choose between Chromium (default), Firefox, and WebKit: 11 | 12 | ```bdd 13 | Given I navigate to website "https://example.com" using the "firefox" browser 14 | And I take a screenshot named "firefox-example" 15 | Then I navigate to website "https://example.com" using the "webkit" browser 16 | And I take a screenshot named "webkit-example" 17 | ``` 18 | 19 | When you send these commands to Claude, it will open the website in Firefox first, take a screenshot, then switch to WebKit and take another screenshot, allowing you to compare how different browsers render the same website. 20 | 21 | ### Scenario in BDD Format 22 | ```bdd 23 | Given I navigate to website http://eaapp.somee.com and click login link 24 | And I enter username and password as "admin" and "password" respectively and perform login 25 | Then click the Employee List page 26 | And click "Create New" button and enter realistic employee details to create for Name, Salary, DurationWorked, 27 | Select dropdown for Grade as CLevel and Email. 28 | ``` 29 | 30 | Once I enter the above text in ***Claude Desktop Client*** I should see Claude desktop giving me prompt to perform operation 31 | by opening real browser like this 32 | 33 | ![Playwright MCP Server](./img/mcp-execution.png) 34 | 35 | And once the entire test operation completes, we will be presented with the entire details of how the automation did happened. 36 | 37 | ![Playwright MCP Server](./img/mcp-result.png) 38 | 39 | ### Using Browser History Navigation 40 | 41 | You can navigate through the browser's history using the new navigation controls: 42 | 43 | ```bdd 44 | Given I navigate to website "https://example.com" 45 | When I navigate to website "https://example.com/about" 46 | And I navigate back in browser history 47 | Then the current page should be "https://example.com" 48 | When I navigate forward in browser history 49 | Then the current page should be "https://example.com/about" 50 | ``` 51 | 52 | ### Using Drag and Drop Functionality 53 | 54 | You can drag and drop elements using the new drag tool: 55 | 56 | ```bdd 57 | Given I navigate to website "https://example.com/drag-drop-demo" 58 | When I drag element with id "draggable" to element with id "droppable" 59 | Then I should see confirmation message "Dropped!" 60 | ``` 61 | 62 | ### Using Keyboard Interactions 63 | 64 | You can simulate keyboard presses with the new keyboard tool: 65 | 66 | ```bdd 67 | Given I navigate to website "https://example.com/form" 68 | When I focus on the input field with id "search-box" 69 | And I press the "Enter" key 70 | Then the search results should appear 71 | ``` 72 | 73 | ### Saving Page as PDF 74 | 75 | You can save the current page as a PDF file: 76 | 77 | ```bdd 78 | Given I navigate to website "https://example.com/report" 79 | When I save the current page as a PDF in "/downloads" folder with name "report.pdf" 80 | Then I should see confirmation that the PDF was saved 81 | ``` 82 | 83 | Advanced example with custom options: 84 | 85 | ```bdd 86 | Given I navigate to website "https://example.com/invoice" 87 | When I save the page as PDF with the following settings: 88 | | Setting | Value | 89 | | ----------------- | --------- | 90 | | Output Path | /downloads | 91 | | Filename | invoice.pdf | 92 | | Format | Letter | 93 | | Print Background | true | 94 | | Top Margin | 2cm | 95 | | Right Margin | 1cm | 96 | | Bottom Margin | 2cm | 97 | | Left Margin | 1cm | 98 | Then I should see confirmation that the PDF was saved 99 | ``` 100 | 101 | ### Extracting Page Content 102 | 103 | You can extract visible text content from the page: 104 | 105 | ```bdd 106 | Given I navigate to website "https://example.com/article" 107 | When I extract all visible text from the page 108 | Then I should see the article content in plain text without hidden elements 109 | ``` 110 | 111 | You can also get the complete HTML of the page: 112 | 113 | ```bdd 114 | Given I navigate to website "https://example.com/products" 115 | When I extract the HTML content of the page 116 | Then I should receive the complete HTML structure of the page 117 | ``` 118 | 119 | Example use case for content analysis: 120 | 121 | ```bdd 122 | Given I navigate to website "https://example.com/pricing" 123 | When I extract all visible text from the page 124 | Then I should be able to analyze the text to find pricing information 125 | And I can determine if the "Enterprise" plan mentions "custom pricing" 126 | ``` -------------------------------------------------------------------------------- /docs/docs/playwright-web/Recording-Actions.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed'; 6 | 7 | # 🎥 Recording Actions 8 | 9 | Playwright MCP allows you to record your browser interactions and automatically generate test specifications. This feature helps you create automated tests without writing code manually. 10 | 11 | ## Getting Started 12 | 13 | To start recording your actions and generate test files, you'll need to: 14 | 15 | 1. Start a recording session 16 | 2. Perform your actions using MCP tools 17 | 3. End the session to generate the test file 18 | 19 | ### Example 20 | 21 | Here's a complete example of recording actions and generating a test spec: 22 | 23 | 1. First, start a new recording session by specifying: 24 | - Where to save the generated tests (`tests/generated` directory) 25 | - What to name your tests (using 'LoginTest' as a prefix) 26 | - Whether to include helpful comments in the generated code 27 | 28 | 2. Then perform the test actions: 29 | - Navigate to the Sauce Demo website (https://www.saucedemo.com) 30 | - Enter "standard_user" in the username field 31 | - Enter "secret_sauce" in the password field 32 | - Click the login button 33 | 34 | 3. Finally, end the recording session to generate your test file 35 | 36 | The generated test file will look something like this: 37 | 38 | ```typescript 39 | import { test, expect } from '@playwright/test'; 40 | 41 | test('LoginTest_2024-03-23', async ({ page }) => { 42 | // Navigate to the login page 43 | await page.goto('https://www.saucedemo.com'); 44 | 45 | // Fill in username 46 | await page.fill('#user-name', 'standard_user'); 47 | 48 | // Fill in password 49 | await page.fill('#password', 'secret_sauce'); 50 | 51 | // Click login button 52 | await page.click('#login-button'); 53 | 54 | // Verify successful login 55 | await expect(page).toHaveURL(/.*inventory.html/); 56 | }); 57 | ``` 58 | 59 | ## Configuration Options 60 | 61 | When starting a recording session, you can configure several options: 62 | 63 | - **outputPath**: Directory where the generated test files will be saved 64 | - **testNamePrefix**: Prefix for the generated test names 65 | - **includeComments**: Whether to include descriptive comments in the generated tests 66 | 67 | For example, you might configure your session to: 68 | - Save tests in a 'tests/generated' folder 69 | - Name tests with a 'MyTest' prefix 70 | - Include helpful comments in the generated code 71 | 72 | ## Session Management 73 | 74 | You can manage your recording sessions using these tools: 75 | 76 | - **get_codegen_session**: Retrieve information about a recording session 77 | - **clear_codegen_session**: Clean up a recording session without generating a test 78 | 79 | These tools help you check the status of your recording or clean up if you want to start over without generating a test file. 80 | 81 | ## Best Practices 82 | 83 | 1. **Organize Tests**: Use meaningful test name prefixes to organize your generated tests 84 | 2. **Clean Up**: Always end or clear your sessions after recording 85 | 3. **Verify Actions**: Include verification steps in your recordings 86 | 4. **Maintain Context**: Keep related actions in the same recording session 87 | 5. **Documentation**: Add comments during recording for better test maintainability 88 | 89 | ## Running Generated Tests 90 | 91 | To run your generated tests, use the Playwright test runner: 92 | 93 | ```bash 94 | npx playwright test tests/generated/logintest_*.spec.ts 95 | ``` 96 | 97 | :::tip 98 | You can modify the generated test files to add additional assertions, setup, or teardown code as needed. 99 | ::: -------------------------------------------------------------------------------- /docs/docs/playwright-web/Support-of-Cline-Cursor.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed'; 6 | 7 | # 💻 Support of Cline and Cursor 8 | 9 | Playwright MCP Server now fully supports **Cline** and **Cursor**. 10 | 11 | Below, you will find video demonstrations showing how **Cline** and **Cursor** work with the Playwright MCP Server. 12 | 13 | --- 14 | 15 | ### 🎥 Support of **Cursor** 16 | 17 | Check out the demonstration of how **Cursor** integrates seamlessly with Playwright MCP Server: 18 | 19 | 20 | 21 | --- 22 | 23 | ### 🎥 Support of **Cline** 24 | 25 | Watch the demonstration of **Cline** in action with Playwright MCP Server: 26 | 27 | 28 | 29 | --- -------------------------------------------------------------------------------- /docs/docs/playwright-web/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Playwright Web Features", 3 | "position": 4, 4 | "collapsed": false, 5 | "link": { 6 | "type": "generated-index", 7 | "description": "Supported features in Playwright Web browser automation." 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/docs/playwright-web/img/console-log.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executeautomation/mcp-playwright/ce17a321a24790885e8c65c37241493d5401e782/docs/docs/playwright-web/img/console-log.gif -------------------------------------------------------------------------------- /docs/docs/playwright-web/img/mcp-execution.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executeautomation/mcp-playwright/ce17a321a24790885e8c65c37241493d5401e782/docs/docs/playwright-web/img/mcp-execution.png -------------------------------------------------------------------------------- /docs/docs/playwright-web/img/mcp-result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executeautomation/mcp-playwright/ce17a321a24790885e8c65c37241493d5401e782/docs/docs/playwright-web/img/mcp-result.png -------------------------------------------------------------------------------- /docs/docs/release.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed'; 5 | 6 | # Release Notes 7 | 8 | ## Version 1.0.5 9 | - **Removed SSE (Server-Sent Events) Support**: All SSE-related code, endpoints, and documentation have been fully removed. The server now only supports STDIO transport for communication with clients. 10 | - **Codebase Cleanup**: Removed all references to SseServer, /events endpoint, and related event streaming features from the code and documentation. 11 | - **Version bump**: Incremented version to 1.0.5. 12 | 13 | ## Version 1.0.4 14 | - **New Tool: `playwright_iframe_fill`** 15 | - Fill input fields inside iframes using CSS selectors 16 | 17 | - **New Tool: `playwright_click_and_switch_tab`** 18 | - Click a link and automatically switch to the newly opened browser tab 19 | 20 | - **Improved Error Logging** 21 | - Captures uncaught exceptions and unhandled Promise rejections in browser logs for better debugging 22 | 23 | - **Documentation Updates** 24 | - Added/updated documentation for new tools and improved installation/configuration instructions 25 | 26 | - **Test Coverage** 27 | - Added and updated tests for new tools and features 28 | 29 | ## Version 1.0.3 30 | - **Code Generation Capabilities**: Added new code generation capability 🎭 31 | - `start_codegen_session`: Start a new session to record Playwright actions 32 | - `end_codegen_session`: End a session and generate test file 33 | - `get_codegen_session`: Retrieve information about a session 34 | - `clear_codegen_session`: Clear a session without generating a test 35 | - Ability to record real browser interactions and convert them to reusable Playwright tests 36 | - Support for customizing test output path, test names, and including descriptive comments 37 | - **Enhanced Browser Navigation**: Added new navigation control tools 🧭 38 | - `playwright_go_back`: Navigate back in browser history 39 | - `playwright_go_forward`: Navigate forward in browser history 40 | - **Advanced Interaction**: Added new interaction tools for more complex scenarios 🔄 41 | - `playwright_drag`: Drag elements from one location to another 42 | - `playwright_press_key`: Press keyboard keys with optional element focus 43 | - **Output Capabilities**: Added content export functionality 📄 44 | - `playwright_save_as_pdf`: Save the current page as a PDF file with customizable options 45 | - **Content Extraction**: Added tools for retrieving page content 📝 46 | - `playwright_get_visible_text`: Extract all visible text content from the current page 47 | - `playwright_get_visible_html`: Get the complete HTML content of the current page 48 | - Comprehensive test coverage for all new tools 49 | - Updated documentation with examples and usage detail 50 | 51 | ## Version 1.0.2 52 | - **Multi-Browser Support**: Added support for Firefox and WebKit browsers in addition to Chromium 🌐 53 | - New `browserType` parameter for `playwright_navigate` tool allows specifying browser engine 54 | - Supported browser types: "chromium" (default), "firefox", and "webkit" 55 | - Seamless browser engine switching during automation sessions 56 | - Enhanced test coverage for different browser engines 57 | - Updated documentation with browser-specific examples 58 | 59 | ## Version 1.0.0 60 | - First major release of Playwright MCP Server with the tool structure changes 🚀 61 | - Fixed issue with headless mode in Playwright #62 62 | - Fixed issue Navigation failed: page.goto: Target page, context or browser has been closed #63 63 | - Completed RFC: Refactor handleToolCall for better maintainability #46 64 | - New feature: Optional Bearer Authorization to API POST (Thanks to ***@CopilotMe***) 65 | - Fixed issue Exit process on host close (Thanks to ***@kiracih***) 66 | - New Feature: Three new tools (Thanks to ***@VinceOPS***) 67 | - `playwright_except_response` 68 | - `playwright_assert_response` 69 | 70 | Here is the scenario for the above two tools 71 | ```BDD 72 | Scenario: Logging in requires captcha verification 73 | Given I expect the browser to receive an HTTP response from "**/security/captcha-precheck" 74 | When I enter "some-identifier@test.com" in the input and I submit 75 | Then The browser should have received the HTTP response 76 | And Its body should contain a property "captchaFamily" 77 | ``` 78 | - A new tool `playwright_custom_user_agent` to define a custom user agent. 79 | 80 | 81 | ## Version 0.3.1 82 | - Fixed BROWSER_TOOLS as Playwright_console_logs is not required (Thanks to https://github.com/kfern) 83 | - Added Tests for all the Playwright MCP Server tools (Thanks to https://github.com/kfern) 84 | - Updated documentation with AI Courses 85 | - Gen AI Course [Details here](/docs/ai-courses/AIAgents) 86 | - AI Agents Course [Details here](/docs/ai-courses/AIAgents) 87 | - Machine Learning Course [Details here](/docs/ai-courses/MachineLearning) 88 | 89 | ## Version 0.3.0 90 | - Added support for `Playwright_console_logs` to get the console logs from the browser. Following logs types 91 | are supported.[More Detail available here](/docs/playwright-web/Console-Logging) 92 | - `log` 93 | - `info` 94 | - `warn` 95 | - `error` 96 | - `debug` 97 | - `exception` 98 | - `all` 99 | 100 | 101 | :::tip Usage Example 102 | To invoke `Playwright_console_logs` via MCP Playwright, use the following prompt: 103 | 104 | ```plaintext 105 | Get the console log from the browser whenever you perform any action. 106 | ::: 107 | - Added support for `Playwright_close` to close the browser and release all resources. 108 | 109 | :::tip Usage Example 110 | To invoke `Playwright_close` via MCP Playwright, use the following prompt: 111 | 112 | ```plaintext 113 | Close the browser once the operation is completed. 114 | ::: 115 | 116 | ## Version 0.2.9 117 | - Fixed Screenshot issue with Cline, Cursor and Windows 11 (Reported by @MackDing, @mengjian-github) 118 | 119 | ## Version 0.2.8 120 | - Support of iFrame while running Playwright test via MCP (Supports Cline as well). Thanks to @VinceOPS 121 | - Fixed issue while saving PNG file. Thanks to @BayLee4 122 | - Fixed issue with full page screenshot arguments to be passed to tool, thanks for the report @unipro-LeighMason 123 | - Updated to latest version of Playwright and MCP Server library 124 | 125 | 126 | ## Version 0.2.7 127 | - Fixed the issue with Playwright MCP server not working Cline, VSCode reported in #26, #16 128 | - Fixed issue #28 and now chrome version is updated 129 | - Updated to latest version of Playwright and MCP Server library 130 | 131 | ## Version 0.2.6 132 | - New Documentation site powered by docusaurus hosted in GH-Pages https://executeautomation.github.io/mcp-playwright/ 133 | 134 | --- 135 | 136 | ## Version 0.2.5 137 | 138 | #### API Test Support 139 | - Playwright MCP Server now supports API Testing for 140 | - `GET` request 141 | - `POST` request 142 | - `PUT` request 143 | - `PATCH` request 144 | - `DELETE` request 145 | 146 | 147 | 148 | --- 149 | 150 | ## Version 0.2.4 151 | - Added support for smithery 152 | - Added Support to save Playwright screenshot in local directory, thanks to `@s4l4x` 153 | 154 | --- 155 | 156 | ## Version 0.2.3 157 | - Added quality of life improvement 158 | 159 | --- 160 | -------------------------------------------------------------------------------- /docs/docs/testing-videos/AIAgents.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed'; 6 | 7 | # 🎭 UI + Database Testing 💽 8 | 9 | In this video, we explore API, UI, and Database testing using Playwright MCP Server + SQLite MCP Server in Cursor—where AI agents handle everything for you! No coding required, just seamless automation. 10 | Watch how Cursor + MCP Servers transform testing into a fully automated, hands-free process! 🔥 11 | 12 |
13 | 14 |
15 | 16 | --- 17 | 18 | :::info 💡 **Note** 19 | We will be using following MCP Server Tools 20 | 21 | 1. Playwright MCP server (Which is this tool that you are referring in the documentation) 22 | 2. [SQLite MCP Server](https://github.com/modelcontextprotocol/servers/tree/main/src/sqlite) 23 | ::: 24 | --- 25 | 26 | ## ⌨ **Cursor Rules** 27 | 28 | We will be using Cursor Rules in this demonstration, Cursor Rules help define how AI agents interact with the 29 | code, automate tasks, and follow specific workflows. These rules guide how agents should generate, modify, or test code within the 30 | development environment. 31 | 32 | Here is the Cursor Rules we are using in this demonstration 33 | 34 | ```js title="api-ui-test.mdc" 35 | When I say /test, then perform following 36 | 37 | Navigate to http://localhost:8000/Product/List. 38 | 39 | Create product by clicking Create link . 40 | 41 | Then create a product with some realistic data for Name, Price and Select ProductType as CPU and click create input type with id as Create. 42 | 43 | Check the Database for the created record 44 | 45 | Use the Schema: http://localhost:8001/swagger/v1/swagger.json 46 | 47 | Here is the baseURL of API Base URL: http://localhost:8001/ 48 | 49 | Also check the API which performs the GET operation if the values are correctly retreived for the created product from the above schema 50 | 51 | -------------------------------------------------------------------------------- /docs/docs/testing-videos/Bdd.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | import YouTubeVideoEmbed from '@site/src/components/HomepageFeatures/YouTubeVideoEmbed'; 6 | 7 | # 🎭 BDD Testing with Playwright MCP Server 🥒 8 | 9 | In this video, we explore **Behavior Driven Development (BDD) testing** using **Playwright MCP Server**, without writing a single line of code—just seamless automation! 10 | 11 | Watch how **Cursor + MCP Servers** transform testing into a fully automated, hands-free process! 🔥 12 | 13 |
14 | 15 |
16 | 17 | --- 18 | 19 | :::info 💡 **Note** 20 | We will be using the following **MCP Server Tools**: 21 | 22 | 1. **Playwright MCP Server** (This is the tool referenced in the documentation) 23 | ::: 24 | 25 | --- 26 | 27 | ## 📚 **BDD Scenario for Login** 28 | 29 | ```gherkin 30 | Feature: Login 31 | 32 | Scenario: To Perform login operation in EAApp website 33 | Given I navigate to "https://eaapp.somee.com/" 34 | When I click on "Login" button 35 | And I enter "admin" in "UserName" field 36 | And I enter "password" in "Password" field 37 | And I click on "Login" button 38 | Then I should see the Employee Details menu 39 | ``` 40 | 41 | --- 42 | 43 | ## 📚 **BDD Scenario for Console Logs** 44 | 45 | ```gherkin 46 | Feature: Network Console 47 | 48 | Scenario: To Perform Network Console operation 49 | Given I navigate to amazon.com 50 | And I handle if there is any popup 51 | And I click the All menu from the Amazon website 52 | And I read the network console logs 53 | When I see any error in the network console logs 54 | Then I print them out 55 | And I close the browser 56 | ``` 57 | 58 | --- 59 | 60 | This structured format enhances readability and ensures clarity for developers and testers. 🚀 61 | -------------------------------------------------------------------------------- /docs/docs/testing-videos/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "MCP Testing Videos", 3 | "position": 7, 4 | "collapsed": false, 5 | "link": { 6 | "type": "generated-index", 7 | "description": "AI Testing Videos helps you learn using MCP for Testing and Development" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import {themes as prismThemes} from 'prism-react-renderer'; 2 | import type {Config} from '@docusaurus/types'; 3 | import type * as Preset from '@docusaurus/preset-classic'; 4 | 5 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) 6 | 7 | const config: Config = { 8 | title: 'Playwright MCP Server', 9 | tagline: 'Fastest way to test your APIs and UI in Playwright with AI 🤖', 10 | favicon: 'img/favicon.ico', 11 | 12 | // Set the production url of your site here 13 | url: 'https://executeautomation.github.io/', 14 | // Set the // pathname under which your site is served 15 | // For GitHub pages deployment, it is often '//' 16 | baseUrl: '/mcp-playwright/', 17 | 18 | // GitHub pages deployment config. 19 | // If you aren't using GitHub pages, you don't need these. 20 | organizationName: 'executeautomation', // Usually your GitHub org/user name. 21 | projectName: 'mcp-playwright', // Usually your repo name. 22 | 23 | onBrokenLinks: 'ignore', 24 | onBrokenMarkdownLinks: 'warn', 25 | 26 | // Even if you don't use internationalization, you can use this field to set 27 | // useful metadata like html lang. For example, if your site is Chinese, you 28 | // may want to replace "en" with "zh-Hans". 29 | i18n: { 30 | defaultLocale: 'en', 31 | locales: ['en'], 32 | }, 33 | trailingSlash: false, 34 | deploymentBranch: 'gh-pages', 35 | presets: [ 36 | [ 37 | 'classic', 38 | { 39 | docs: { 40 | sidebarPath: './sidebars.ts', 41 | // Please change this to your repo. 42 | // Remove this to remove the "edit this page" links. 43 | editUrl: 44 | 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/', 45 | }, 46 | theme: { 47 | customCss: './src/css/custom.css', 48 | }, 49 | } satisfies Preset.Options, 50 | ], 51 | ], 52 | 53 | themeConfig: { 54 | // Replace with your project's social card 55 | image: 'img/EA-Icon.svg', 56 | navbar: { 57 | title: 'Playwright MCP Server', 58 | logo: { 59 | alt: 'Playwright MCP Server', 60 | src: 'img/EA-Icon.svg', 61 | }, 62 | items: [ 63 | { 64 | type: 'docSidebar', 65 | sidebarId: 'tutorialSidebar', 66 | position: 'left', 67 | label: 'Tutorial', 68 | }, 69 | { 70 | href: 'https://github.com/executeautomation/mcp-playwright', 71 | label: 'GitHub', 72 | position: 'right', 73 | }, 74 | ], 75 | }, 76 | footer: { 77 | style: 'dark', 78 | links: [ 79 | { 80 | title: 'Docs', 81 | items: [ 82 | { 83 | label: 'Tutorial', 84 | to: '/docs/intro', 85 | }, 86 | { 87 | label: 'Playwright MCP for UI', 88 | href: 'https://youtu.be/8CcgFUE16HM', 89 | }, 90 | { 91 | label: 'Playwright MCP for API', 92 | href: 'https://youtu.be/BYYyoRxCcFE', 93 | }, 94 | ], 95 | }, 96 | { 97 | title: 'Community', 98 | items: [ 99 | { 100 | label: 'Youtube', 101 | href: 'https://youtube.com/executeautomation', 102 | }, 103 | { 104 | label: 'Udemy', 105 | href: 'https://www.udemy.com/user/karthik-kk', 106 | }, 107 | { 108 | label: 'X', 109 | href: 'http://x.com/ExecuteAuto', 110 | }, 111 | ], 112 | } 113 | ], 114 | copyright: `Copyright © ${new Date().getFullYear()} ExecuteAutomation Pvt Ltd.`, 115 | }, 116 | prism: { 117 | theme: prismThemes.github, 118 | darkTheme: prismThemes.dracula, 119 | }, 120 | } satisfies Preset.ThemeConfig, 121 | }; 122 | 123 | export default config; 124 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playwright-mcp-server", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.6.3", 19 | "@docusaurus/preset-classic": "3.6.3", 20 | "@mdx-js/react": "^3.0.0", 21 | "clsx": "^2.0.0", 22 | "gh-pages": "^6.2.0", 23 | "prism-react-renderer": "^2.3.0", 24 | "react": "^18.0.0", 25 | "react-dom": "^18.0.0" 26 | }, 27 | "devDependencies": { 28 | "@docusaurus/module-type-aliases": "3.6.3", 29 | "@docusaurus/tsconfig": "3.6.3", 30 | "@docusaurus/types": "3.6.3", 31 | "typescript": "~5.6.2" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.5%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 3 chrome version", 41 | "last 3 firefox version", 42 | "last 5 safari version" 43 | ] 44 | }, 45 | "engines": { 46 | "node": ">=18.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; 2 | 3 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) 4 | 5 | /** 6 | * Creating a sidebar enables you to: 7 | - create an ordered group of docs 8 | - render a sidebar for each doc of that group 9 | - provide next/previous navigation 10 | 11 | The sidebars can be generated from the filesystem, or explicitly defined here. 12 | 13 | Create as many sidebars as you want. 14 | */ 15 | const sidebars: SidebarsConfig = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | export default sidebars; 34 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/YouTubeVideoEmbed.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function YouTubeVideoEmbed({ videoId }) { 4 | return ( 5 | 13 | ); 14 | } -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Heading from '@theme/Heading'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | Svg: React.ComponentType>; 8 | description: JSX.Element; 9 | }; 10 | 11 | const FeatureList: FeatureItem[] = [ 12 | { 13 | title: 'Easy to Use', 14 | Svg: require('@site/static/img/easy-to-use.svg').default, 15 | description: ( 16 | <> 17 | Playwright MCP Server is easy to use, just change the Claude config file and you are done. 18 | 19 | ), 20 | }, 21 | { 22 | title: 'Test UI and APIs', 23 | Svg: require('@site/static/img/playwright.svg').default, 24 | description: ( 25 | <> 26 | Test both UI and API of your application with plain English text. No code required. 27 | 28 | ), 29 | }, 30 | { 31 | title: 'Powered by NodeJS', 32 | Svg: require('@site/static/img/node.svg').default, 33 | description: ( 34 | <> 35 | Playwright MCP Server is built on top of NodeJS, making it fast and efficient. 36 | 37 | ), 38 | }, 39 | ]; 40 | 41 | function Feature({title, Svg, description}: FeatureItem) { 42 | return ( 43 |
44 |
45 | 46 |
47 |
48 | {title} 49 |

{description}

50 |
51 |
52 | ); 53 | } 54 | 55 | export default function HomepageFeatures(): JSX.Element { 56 | return ( 57 |
58 |
59 |
60 | {FeatureList.map((props, idx) => ( 61 | 62 | ))} 63 |
64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #25c2a0; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /docs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Link from '@docusaurus/Link'; 3 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 4 | import Layout from '@theme/Layout'; 5 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 6 | import Heading from '@theme/Heading'; 7 | 8 | import styles from './index.module.css'; 9 | 10 | function HomepageHeader() { 11 | const {siteConfig} = useDocusaurusContext(); 12 | return ( 13 |
14 |
15 | 16 | {siteConfig.title} 17 | 18 |

{siteConfig.tagline}

19 |
20 | 23 | Playwright MCP Server Tutorial - 5min ⏱️ 24 | 25 |
26 |
27 |
28 | ); 29 | } 30 | 31 | export default function Home(): JSX.Element { 32 | const {siteConfig} = useDocusaurusContext(); 33 | return ( 34 | 37 | 38 |
39 | 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executeautomation/mcp-playwright/ce17a321a24790885e8c65c37241493d5401e782/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/EA-Icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executeautomation/mcp-playwright/ce17a321a24790885e8c65c37241493d5401e782/docs/static/img/EA-Icon.jpg -------------------------------------------------------------------------------- /docs/static/img/EA-Icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /docs/static/img/docusaurus-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executeautomation/mcp-playwright/ce17a321a24790885e8c65c37241493d5401e782/docs/static/img/docusaurus-social-card.jpg -------------------------------------------------------------------------------- /docs/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executeautomation/mcp-playwright/ce17a321a24790885e8c65c37241493d5401e782/docs/static/img/docusaurus.png -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executeautomation/mcp-playwright/ce17a321a24790885e8c65c37241493d5401e782/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/static/img/node.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | }, 7 | "exclude": [".docusaurus", "build"] 8 | } 9 | -------------------------------------------------------------------------------- /image/playwright_claude.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/executeautomation/mcp-playwright/ce17a321a24790885e8c65c37241493d5401e782/image/playwright_claude.png -------------------------------------------------------------------------------- /jest.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | collectCoverage: true, 5 | coverageDirectory: 'coverage', 6 | coverageReporters: ['text', 'lcov'], 7 | collectCoverageFrom: [ 8 | 'src/**/*.ts', 9 | '!src/index.ts', // exclude index.ts 10 | ], 11 | testMatch: [ 12 | '/src/__tests__/**/*.test.ts' 13 | ], 14 | modulePathIgnorePatterns: [ 15 | "/docs/", 16 | "/dist/" 17 | ], 18 | moduleNameMapper: { 19 | "^(.*)\\.js$": "$1" 20 | }, 21 | transform: { 22 | '^.+\\.tsx?$': ['ts-jest', { 23 | useESM: true, 24 | tsconfig: 'tsconfig.test.json' 25 | }], 26 | }, 27 | extensionsToTreatAsEsm: ['.ts'], 28 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], 29 | }; 30 | -------------------------------------------------------------------------------- /mcp-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "mcpServers": { 3 | "playwright": { 4 | "command": "npx", 5 | "args": ["-y", "@executeautomation/playwright-mcp-server"] 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@executeautomation/playwright-mcp-server", 3 | "version": "1.0.5", 4 | "description": "Model Context Protocol servers for Playwright", 5 | "license": "MIT", 6 | "author": "ExecuteAutomation, Ltd (https://executeautomation.com)", 7 | "homepage": "https://executeautomation.github.io/mcp-playwright/", 8 | "bugs": "https://github.com/executeautomation/mcp-playwright/issues", 9 | "types": "dist/index.d.ts", 10 | "type": "module", 11 | "bin": { 12 | "playwright-mcp-server": "dist/index.js" 13 | }, 14 | "files": [ 15 | "dist" 16 | ], 17 | "scripts": { 18 | "build": "tsc && shx chmod +x dist/*.js", 19 | "prepare": "npm run build", 20 | "watch": "tsc --watch", 21 | "test": "jest --testMatch=\"/src/__tests__/**/*.test.ts\"", 22 | "test:coverage": "jest --coverage --testMatch=\"/src/__tests__/**/*.test.ts\"", 23 | "test:custom": "node run-tests.cjs" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/executeautomation/mcp-playwright.git" 28 | }, 29 | "dependencies": { 30 | "@modelcontextprotocol/sdk": "1.11.1", 31 | "@playwright/browser-chromium": "1.52.0", 32 | "@playwright/browser-firefox": "1.52.0", 33 | "@playwright/browser-webkit": "1.52.0", 34 | "@playwright/test": "^1.52.0", 35 | "mcp-evals": "^1.0.18", 36 | "playwright": "1.52.0", 37 | "uuid": "11.1.0" 38 | }, 39 | "keywords": [ 40 | "playwright", 41 | "automation", 42 | "AI", 43 | "Claude MCP" 44 | ], 45 | "devDependencies": { 46 | "@types/jest": "^29.5.14", 47 | "@types/node": "^20.10.5", 48 | "jest": "^29.7.0", 49 | "jest-playwright-preset": "4.0.0", 50 | "shx": "^0.3.4", 51 | "ts-jest": "^29.2.6", 52 | "typescript": "^5.8.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /run-tests.cjs: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | 3 | try { 4 | console.log("Running tests with coverage..."); 5 | execSync('npx jest --no-cache --coverage --testMatch="/src/__tests__/**/*.test.ts"', { stdio: 'inherit' }); 6 | console.log("Tests completed successfully!"); 7 | } catch (error) { 8 | console.error("Error running tests:", error.message); 9 | console.log(error.stdout?.toString() || "No output"); 10 | process.exit(1); 11 | } -------------------------------------------------------------------------------- /run-tests.js: -------------------------------------------------------------------------------- 1 | const { execSync } = require('child_process'); 2 | 3 | try { 4 | console.log('Running tests...'); 5 | const output = execSync('npx jest --no-cache --coverage', { encoding: 'utf8' }); 6 | console.log(output); 7 | } catch (error) { 8 | console.error('Error running tests:', error.message); 9 | console.log(error.stdout); 10 | } -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | properties: {} 9 | commandFunction: 10 | # A function that produces the CLI command to start the MCP on stdio. 11 | |- 12 | (config) => ({command: 'node', args: ['dist/index.js'], env: {}}) -------------------------------------------------------------------------------- /src/__tests__/codegen.test.ts: -------------------------------------------------------------------------------- 1 | import { ActionRecorder } from '../tools/codegen/recorder'; 2 | import { PlaywrightGenerator } from '../tools/codegen/generator'; 3 | import { handleToolCall } from '../toolHandler'; 4 | import * as fs from 'fs/promises'; 5 | import * as path from 'path'; 6 | 7 | jest.mock('../toolHandler'); 8 | const mockedHandleToolCall = jest.mocked(handleToolCall); 9 | 10 | // Test configuration 11 | const TEST_CONFIG = { 12 | OUTPUT_DIR: path.join(__dirname, '../../tests/generated'), 13 | MOCK_SESSION_ID: 'test-session-123' 14 | } as const; 15 | 16 | // Response types 17 | interface ToolResponseContent { 18 | [key: string]: unknown; 19 | type: 'text'; 20 | text: string; 21 | } 22 | 23 | interface ToolResponse { 24 | [key: string]: unknown; 25 | content: ToolResponseContent[]; 26 | isError: boolean; 27 | _meta?: Record; 28 | } 29 | 30 | function createMockResponse(data: unknown): ToolResponse { 31 | return { 32 | content: [{ 33 | type: 'text', 34 | text: JSON.stringify(data) 35 | }], 36 | isError: false, 37 | _meta: {} 38 | }; 39 | } 40 | 41 | function parseJsonResponse(response: unknown): T { 42 | if (!response || typeof response !== 'object' || !('content' in response)) { 43 | throw new Error('Invalid response format'); 44 | } 45 | 46 | const content = (response as { content: unknown[] }).content; 47 | if (!Array.isArray(content) || content.length === 0) { 48 | throw new Error('Invalid response content'); 49 | } 50 | 51 | const textContent = content.find(c => 52 | typeof c === 'object' && 53 | c !== null && 54 | 'type' in c && 55 | (c as { type: string }).type === 'text' && 56 | 'text' in c && 57 | typeof (c as { text: unknown }).text === 'string' 58 | ) as { type: 'text'; text: string } | undefined; 59 | 60 | if (!textContent?.text) { 61 | throw new Error('No text content found in response'); 62 | } 63 | 64 | return JSON.parse(textContent.text) as T; 65 | } 66 | 67 | describe('Code Generation', () => { 68 | beforeAll(async () => { 69 | // Ensure test output directory exists 70 | await fs.mkdir(TEST_CONFIG.OUTPUT_DIR, { recursive: true }); 71 | }); 72 | 73 | afterEach(() => { 74 | // Clear all mocks 75 | jest.clearAllMocks(); 76 | }); 77 | 78 | afterAll(async () => { 79 | // Clean up test files 80 | try { 81 | const files = await fs.readdir(TEST_CONFIG.OUTPUT_DIR); 82 | await Promise.all( 83 | files.map(file => fs.unlink(path.join(TEST_CONFIG.OUTPUT_DIR, file))) 84 | ); 85 | } catch (error) { 86 | console.error('Error cleaning up test files:', error); 87 | } 88 | }); 89 | 90 | describe('Action Recording', () => { 91 | beforeEach(() => { 92 | // Mock session info response 93 | mockedHandleToolCall.mockImplementation(async (name, args, server) => { 94 | if (name === 'get_codegen_session') { 95 | return createMockResponse({ 96 | id: TEST_CONFIG.MOCK_SESSION_ID, 97 | actions: [] 98 | }); 99 | } 100 | return createMockResponse({ success: true }); 101 | }); 102 | }); 103 | 104 | it('should record navigation actions', async () => { 105 | // Setup mock for session info 106 | mockedHandleToolCall.mockImplementation(async (name, args, server) => { 107 | if (name === 'get_codegen_session') { 108 | return createMockResponse({ 109 | id: TEST_CONFIG.MOCK_SESSION_ID, 110 | actions: [{ 111 | toolName: 'playwright_navigate', 112 | params: { url: 'https://example.com' } 113 | }] 114 | }); 115 | } 116 | return createMockResponse({ success: true }); 117 | }); 118 | 119 | await handleToolCall('playwright_navigate', { 120 | url: 'https://example.com' 121 | }, {}); 122 | 123 | const sessionInfo = parseJsonResponse<{ id: string; actions: any[] }>( 124 | await handleToolCall('get_codegen_session', { sessionId: TEST_CONFIG.MOCK_SESSION_ID }, {}) 125 | ); 126 | 127 | expect(sessionInfo.actions).toHaveLength(1); 128 | expect(sessionInfo.actions[0].toolName).toBe('playwright_navigate'); 129 | expect(sessionInfo.actions[0].params).toEqual({ 130 | url: 'https://example.com' 131 | }); 132 | }); 133 | 134 | it('should record multiple actions in sequence', async () => { 135 | // Setup mock for session info 136 | mockedHandleToolCall.mockImplementation(async (name, args, server) => { 137 | if (name === 'get_codegen_session') { 138 | return createMockResponse({ 139 | id: TEST_CONFIG.MOCK_SESSION_ID, 140 | actions: [ 141 | { 142 | toolName: 'playwright_navigate', 143 | params: { url: 'https://example.com' } 144 | }, 145 | { 146 | toolName: 'playwright_click', 147 | params: { selector: '#submit-button' } 148 | }, 149 | { 150 | toolName: 'playwright_fill', 151 | params: { selector: '#search-input', value: 'test query' } 152 | } 153 | ] 154 | }); 155 | } 156 | return createMockResponse({ success: true }); 157 | }); 158 | 159 | await handleToolCall('playwright_navigate', { 160 | url: 'https://example.com' 161 | }, {}); 162 | 163 | await handleToolCall('playwright_click', { 164 | selector: '#submit-button' 165 | }, {}); 166 | 167 | await handleToolCall('playwright_fill', { 168 | selector: '#search-input', 169 | value: 'test query' 170 | }, {}); 171 | 172 | const sessionInfo = parseJsonResponse<{ id: string; actions: any[] }>( 173 | await handleToolCall('get_codegen_session', { sessionId: TEST_CONFIG.MOCK_SESSION_ID }, {}) 174 | ); 175 | 176 | expect(sessionInfo.actions).toHaveLength(3); 177 | expect(sessionInfo.actions.map(a => a.toolName)).toEqual([ 178 | 'playwright_navigate', 179 | 'playwright_click', 180 | 'playwright_fill' 181 | ]); 182 | expect(sessionInfo.actions.map(a => a.params)).toEqual([ 183 | { url: 'https://example.com' }, 184 | { selector: '#submit-button' }, 185 | { selector: '#search-input', value: 'test query' } 186 | ]); 187 | }); 188 | }); 189 | 190 | describe('Test Generation', () => { 191 | it('should generate valid Playwright test code', async () => { 192 | // Setup mock for end session response 193 | mockedHandleToolCall.mockImplementation(async (name, args, server) => { 194 | if (name === 'end_codegen_session') { 195 | return createMockResponse({ 196 | filePath: path.join(TEST_CONFIG.OUTPUT_DIR, 'test.spec.ts'), 197 | testCode: ` 198 | import { test, expect } from '@playwright/test'; 199 | 200 | test('generated test', async ({ page }) => { 201 | await page.goto('https://example.com'); 202 | await page.click('#submit-button'); 203 | await page.fill('#search-input', 'test query'); 204 | }); 205 | ` 206 | }); 207 | } 208 | return createMockResponse({ success: true }); 209 | }); 210 | 211 | // Record actions 212 | await handleToolCall('playwright_navigate', { 213 | url: 'https://example.com' 214 | }, {}); 215 | 216 | await handleToolCall('playwright_click', { 217 | selector: '#submit-button' 218 | }, {}); 219 | 220 | await handleToolCall('playwright_fill', { 221 | selector: '#search-input', 222 | value: 'test query' 223 | }, {}); 224 | 225 | // Generate test 226 | const endResult = await handleToolCall('end_codegen_session', { 227 | sessionId: TEST_CONFIG.MOCK_SESSION_ID 228 | }, {}); 229 | 230 | const { filePath, testCode } = parseJsonResponse<{ filePath: string; testCode: string }>(endResult); 231 | 232 | // Verify test code content 233 | expect(filePath).toBeDefined(); 234 | expect(testCode).toContain('import { test, expect } from \'@playwright/test\''); 235 | expect(testCode).toContain('await page.goto(\'https://example.com\')'); 236 | expect(testCode).toContain('await page.click(\'#submit-button\')'); 237 | expect(testCode).toContain('await page.fill(\'#search-input\', \'test query\')'); 238 | 239 | // Verify mock was called correctly 240 | expect(mockedHandleToolCall).toHaveBeenCalledWith( 241 | 'end_codegen_session', 242 | { sessionId: TEST_CONFIG.MOCK_SESSION_ID }, 243 | {} 244 | ); 245 | }); 246 | }); 247 | }); -------------------------------------------------------------------------------- /src/__tests__/tools.test.ts: -------------------------------------------------------------------------------- 1 | import { createToolDefinitions, BROWSER_TOOLS, API_TOOLS } from '../tools'; 2 | 3 | describe('Tool Definitions', () => { 4 | const toolDefinitions = createToolDefinitions(); 5 | 6 | test('should return an array of tool definitions', () => { 7 | expect(Array.isArray(toolDefinitions)).toBe(true); 8 | expect(toolDefinitions.length).toBeGreaterThan(0); 9 | }); 10 | 11 | test('each tool definition should have required properties', () => { 12 | toolDefinitions.forEach(tool => { 13 | expect(tool).toHaveProperty('name'); 14 | expect(tool).toHaveProperty('description'); 15 | expect(tool).toHaveProperty('inputSchema'); 16 | expect(tool.inputSchema).toHaveProperty('type'); 17 | expect(tool.inputSchema).toHaveProperty('properties'); 18 | }); 19 | }); 20 | 21 | test('BROWSER_TOOLS should contain browser-related tool names', () => { 22 | expect(Array.isArray(BROWSER_TOOLS)).toBe(true); 23 | expect(BROWSER_TOOLS.length).toBeGreaterThan(0); 24 | 25 | BROWSER_TOOLS.forEach(toolName => { 26 | expect(toolDefinitions.some(tool => tool.name === toolName)).toBe(true); 27 | }); 28 | }); 29 | 30 | test('API_TOOLS should contain API-related tool names', () => { 31 | expect(Array.isArray(API_TOOLS)).toBe(true); 32 | expect(API_TOOLS.length).toBeGreaterThan(0); 33 | 34 | API_TOOLS.forEach(toolName => { 35 | expect(toolDefinitions.some(tool => tool.name === toolName)).toBe(true); 36 | }); 37 | }); 38 | 39 | test('should validate navigate tool schema', () => { 40 | const navigateTool = toolDefinitions.find(tool => tool.name === 'playwright_navigate'); 41 | expect(navigateTool).toBeDefined(); 42 | expect(navigateTool!.inputSchema.properties).toHaveProperty('url'); 43 | expect(navigateTool!.inputSchema.properties).toHaveProperty('waitUntil'); 44 | expect(navigateTool!.inputSchema.properties).toHaveProperty('timeout'); 45 | expect(navigateTool!.inputSchema.properties).toHaveProperty('width'); 46 | expect(navigateTool!.inputSchema.properties).toHaveProperty('height'); 47 | expect(navigateTool!.inputSchema.properties).toHaveProperty('headless'); 48 | expect(navigateTool!.inputSchema.required).toEqual(['url']); 49 | }); 50 | 51 | test('should validate go_back tool schema', () => { 52 | const goBackTool = toolDefinitions.find(tool => tool.name === 'playwright_go_back'); 53 | expect(goBackTool).toBeDefined(); 54 | expect(goBackTool!.inputSchema.properties).toEqual({}); 55 | expect(goBackTool!.inputSchema.required).toEqual([]); 56 | }); 57 | 58 | test('should validate go_forward tool schema', () => { 59 | const goForwardTool = toolDefinitions.find(tool => tool.name === 'playwright_go_forward'); 60 | expect(goForwardTool).toBeDefined(); 61 | expect(goForwardTool!.inputSchema.properties).toEqual({}); 62 | expect(goForwardTool!.inputSchema.required).toEqual([]); 63 | }); 64 | 65 | test('should validate drag tool schema', () => { 66 | const dragTool = toolDefinitions.find(tool => tool.name === 'playwright_drag'); 67 | expect(dragTool).toBeDefined(); 68 | expect(dragTool!.inputSchema.properties).toHaveProperty('sourceSelector'); 69 | expect(dragTool!.inputSchema.properties).toHaveProperty('targetSelector'); 70 | expect(dragTool!.inputSchema.required).toEqual(['sourceSelector', 'targetSelector']); 71 | }); 72 | 73 | test('should validate press_key tool schema', () => { 74 | const pressKeyTool = toolDefinitions.find(tool => tool.name === 'playwright_press_key'); 75 | expect(pressKeyTool).toBeDefined(); 76 | expect(pressKeyTool!.inputSchema.properties).toHaveProperty('key'); 77 | expect(pressKeyTool!.inputSchema.properties).toHaveProperty('selector'); 78 | expect(pressKeyTool!.inputSchema.required).toEqual(['key']); 79 | }); 80 | 81 | test('should validate save_as_pdf tool schema', () => { 82 | const saveAsPdfTool = toolDefinitions.find(tool => tool.name === 'playwright_save_as_pdf'); 83 | expect(saveAsPdfTool).toBeDefined(); 84 | expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('outputPath'); 85 | expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('filename'); 86 | expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('format'); 87 | expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('printBackground'); 88 | expect(saveAsPdfTool!.inputSchema.properties).toHaveProperty('margin'); 89 | expect(saveAsPdfTool!.inputSchema.required).toEqual(['outputPath']); 90 | }); 91 | }); -------------------------------------------------------------------------------- /src/__tests__/tools/api/requests.test.ts: -------------------------------------------------------------------------------- 1 | import { GetRequestTool, PostRequestTool, PutRequestTool, PatchRequestTool, DeleteRequestTool } from '../../../tools/api/requests.js'; 2 | import { ToolContext } from '../../../tools/common/types.js'; 3 | import { APIRequestContext } from 'playwright'; 4 | import { jest } from '@jest/globals'; 5 | 6 | // Mock response 7 | const mockStatus200 = jest.fn().mockReturnValue(200); 8 | const mockStatus201 = jest.fn().mockReturnValue(201); 9 | const mockStatus204 = jest.fn().mockReturnValue(204); 10 | const mockText = jest.fn().mockImplementation(() => Promise.resolve('{"success": true}')); 11 | const mockStatusText = jest.fn().mockReturnValue('OK'); 12 | 13 | const mockResponse = { 14 | status: mockStatus200, 15 | statusText: mockStatusText, 16 | text: mockText 17 | }; 18 | 19 | // Mock API context 20 | const mockGet = jest.fn().mockImplementation(() => Promise.resolve(mockResponse)); 21 | const mockPost = jest.fn().mockImplementation(() => Promise.resolve({...mockResponse, status: mockStatus201})); 22 | const mockPut = jest.fn().mockImplementation(() => Promise.resolve(mockResponse)); 23 | const mockPatch = jest.fn().mockImplementation(() => Promise.resolve(mockResponse)); 24 | const mockDelete = jest.fn().mockImplementation(() => Promise.resolve({...mockResponse, status: mockStatus204})); 25 | const mockDispose = jest.fn().mockImplementation(() => Promise.resolve()); 26 | 27 | const mockApiContext = { 28 | get: mockGet, 29 | post: mockPost, 30 | put: mockPut, 31 | patch: mockPatch, 32 | delete: mockDelete, 33 | dispose: mockDispose 34 | } as unknown as APIRequestContext; 35 | 36 | // Mock server 37 | const mockServer = { 38 | sendMessage: jest.fn() 39 | }; 40 | 41 | // Mock context 42 | const mockContext = { 43 | apiContext: mockApiContext, 44 | server: mockServer 45 | } as ToolContext; 46 | 47 | describe('API Request Tools', () => { 48 | let getRequestTool: GetRequestTool; 49 | let postRequestTool: PostRequestTool; 50 | let putRequestTool: PutRequestTool; 51 | let patchRequestTool: PatchRequestTool; 52 | let deleteRequestTool: DeleteRequestTool; 53 | 54 | beforeEach(() => { 55 | jest.clearAllMocks(); 56 | getRequestTool = new GetRequestTool(mockServer); 57 | postRequestTool = new PostRequestTool(mockServer); 58 | putRequestTool = new PutRequestTool(mockServer); 59 | patchRequestTool = new PatchRequestTool(mockServer); 60 | deleteRequestTool = new DeleteRequestTool(mockServer); 61 | }); 62 | 63 | describe('GetRequestTool', () => { 64 | test('should make a GET request', async () => { 65 | const args = { 66 | url: 'https://api.example.com' 67 | }; 68 | 69 | const result = await getRequestTool.execute(args, mockContext); 70 | 71 | expect(mockGet).toHaveBeenCalledWith('https://api.example.com'); 72 | expect(result.isError).toBe(false); 73 | expect(result.content[0].text).toContain('GET request to'); 74 | }); 75 | 76 | test('should handle GET request errors', async () => { 77 | const args = { 78 | url: 'https://api.example.com' 79 | }; 80 | 81 | // Mock a request error 82 | mockGet.mockImplementationOnce(() => Promise.reject(new Error('Request failed'))); 83 | 84 | const result = await getRequestTool.execute(args, mockContext); 85 | 86 | expect(mockGet).toHaveBeenCalledWith('https://api.example.com'); 87 | expect(result.isError).toBe(true); 88 | expect(result.content[0].text).toContain('API operation failed'); 89 | }); 90 | 91 | test('should handle missing API context', async () => { 92 | const args = { 93 | url: 'https://api.example.com' 94 | }; 95 | 96 | const result = await getRequestTool.execute(args, { server: mockServer } as ToolContext); 97 | 98 | expect(mockGet).not.toHaveBeenCalled(); 99 | expect(result.isError).toBe(true); 100 | expect(result.content[0].text).toContain('API context not initialized'); 101 | }); 102 | }); 103 | 104 | describe('PostRequestTool', () => { 105 | test('should make a POST request without token', async () => { 106 | const args = { 107 | url: 'https://api.example.com', 108 | value: '{"data": "test"}' 109 | }; 110 | 111 | const result = await postRequestTool.execute(args, mockContext); 112 | 113 | expect(mockPost).toHaveBeenCalledWith('https://api.example.com', { 114 | data: { data: "test" }, 115 | headers: { 116 | 'Content-Type': 'application/json' 117 | } 118 | }); 119 | expect(result.isError).toBe(false); 120 | expect(result.content[0].text).toContain('POST request to'); 121 | }); 122 | 123 | test('should make a POST request with Bearer token', async () => { 124 | const args = { 125 | url: 'https://api.example.com', 126 | value: '{"data": "test"}', 127 | token: 'test-token' 128 | }; 129 | 130 | const result = await postRequestTool.execute(args, mockContext); 131 | 132 | expect(mockPost).toHaveBeenCalledWith('https://api.example.com', { 133 | data: { data: "test" }, 134 | headers: { 135 | 'Content-Type': 'application/json', 136 | 'Authorization': 'Bearer test-token' 137 | } 138 | }); 139 | expect(result.isError).toBe(false); 140 | expect(result.content[0].text).toContain('POST request to'); 141 | }); 142 | 143 | test('should make a POST request with Bearer token and custom headers', async () => { 144 | const args = { 145 | url: 'https://api.example.com', 146 | value: '{"data": "test"}', 147 | token: 'test-token', 148 | headers: { 149 | 'X-Custom-Header': 'custom-value' 150 | } 151 | }; 152 | 153 | const result = await postRequestTool.execute(args, mockContext); 154 | 155 | expect(mockPost).toHaveBeenCalledWith('https://api.example.com', { 156 | data: { data: "test" }, 157 | headers: { 158 | 'Content-Type': 'application/json', 159 | 'Authorization': 'Bearer test-token', 160 | 'X-Custom-Header': 'custom-value' 161 | } 162 | }); 163 | expect(result.isError).toBe(false); 164 | expect(result.content[0].text).toContain('POST request to'); 165 | }); 166 | }); 167 | 168 | describe('PutRequestTool', () => { 169 | test('should make a PUT request', async () => { 170 | const args = { 171 | url: 'https://api.example.com', 172 | value: '{"data": "test"}' 173 | }; 174 | 175 | const result = await putRequestTool.execute(args, mockContext); 176 | 177 | expect(mockPut).toHaveBeenCalledWith('https://api.example.com', { data: args.value }); 178 | expect(result.isError).toBe(false); 179 | expect(result.content[0].text).toContain('PUT request to'); 180 | }); 181 | }); 182 | 183 | describe('PatchRequestTool', () => { 184 | test('should make a PATCH request', async () => { 185 | const args = { 186 | url: 'https://api.example.com', 187 | value: '{"data": "test"}' 188 | }; 189 | 190 | const result = await patchRequestTool.execute(args, mockContext); 191 | 192 | expect(mockPatch).toHaveBeenCalledWith('https://api.example.com', { data: args.value }); 193 | expect(result.isError).toBe(false); 194 | expect(result.content[0].text).toContain('PATCH request to'); 195 | }); 196 | }); 197 | 198 | describe('DeleteRequestTool', () => { 199 | test('should make a DELETE request', async () => { 200 | const args = { 201 | url: 'https://api.example.com/1' 202 | }; 203 | 204 | const result = await deleteRequestTool.execute(args, mockContext); 205 | 206 | expect(mockDelete).toHaveBeenCalledWith('https://api.example.com/1'); 207 | expect(result.isError).toBe(false); 208 | expect(result.content[0].text).toContain('DELETE request to'); 209 | }); 210 | }); 211 | }); -------------------------------------------------------------------------------- /src/__tests__/tools/browser/advancedInteraction.test.ts: -------------------------------------------------------------------------------- 1 | import { DragTool, PressKeyTool } from '../../../tools/browser/interaction.js'; 2 | import { ToolContext } from '../../../tools/common/types.js'; 3 | import { Page, Browser, ElementHandle } from 'playwright'; 4 | import { jest } from '@jest/globals'; 5 | 6 | // Mock page functions 7 | const mockWaitForSelector = jest.fn(); 8 | const mockMouseMove = jest.fn().mockImplementation(() => Promise.resolve()); 9 | const mockMouseDown = jest.fn().mockImplementation(() => Promise.resolve()); 10 | const mockMouseUp = jest.fn().mockImplementation(() => Promise.resolve()); 11 | const mockKeyboardPress = jest.fn().mockImplementation(() => Promise.resolve()); 12 | const mockFocus = jest.fn().mockImplementation(() => Promise.resolve()); 13 | const mockIsClosed = jest.fn().mockReturnValue(false); 14 | 15 | // Mock element handle 16 | const mockBoundingBox = jest.fn().mockReturnValue({ x: 10, y: 10, width: 100, height: 50 }); 17 | const mockElementHandle = { 18 | boundingBox: mockBoundingBox 19 | } as unknown as ElementHandle; 20 | 21 | // Wait for selector returns element handle 22 | mockWaitForSelector.mockImplementation(() => Promise.resolve(mockElementHandle)); 23 | 24 | // Mock mouse 25 | const mockMouse = { 26 | move: mockMouseMove, 27 | down: mockMouseDown, 28 | up: mockMouseUp 29 | }; 30 | 31 | // Mock keyboard 32 | const mockKeyboard = { 33 | press: mockKeyboardPress 34 | }; 35 | 36 | // Mock the Page object with proper typing 37 | const mockPage = { 38 | waitForSelector: mockWaitForSelector, 39 | mouse: mockMouse, 40 | keyboard: mockKeyboard, 41 | focus: mockFocus, 42 | isClosed: mockIsClosed 43 | } as unknown as Page; 44 | 45 | // Mock the browser 46 | const mockIsConnected = jest.fn().mockReturnValue(true); 47 | const mockBrowser = { 48 | isConnected: mockIsConnected 49 | } as unknown as Browser; 50 | 51 | // Mock the server 52 | const mockServer = { 53 | sendMessage: jest.fn() 54 | }; 55 | 56 | // Mock context 57 | const mockContext = { 58 | page: mockPage, 59 | browser: mockBrowser, 60 | server: mockServer 61 | } as ToolContext; 62 | 63 | describe('Advanced Browser Interaction Tools', () => { 64 | let dragTool: DragTool; 65 | let pressKeyTool: PressKeyTool; 66 | 67 | beforeEach(() => { 68 | jest.clearAllMocks(); 69 | dragTool = new DragTool(mockServer); 70 | pressKeyTool = new PressKeyTool(mockServer); 71 | // Reset browser and page mocks 72 | mockIsConnected.mockReturnValue(true); 73 | mockIsClosed.mockReturnValue(false); 74 | }); 75 | 76 | describe('DragTool', () => { 77 | test('should drag an element to a target location', async () => { 78 | const args = { 79 | sourceSelector: '#source-element', 80 | targetSelector: '#target-element' 81 | }; 82 | 83 | const result = await dragTool.execute(args, mockContext); 84 | 85 | expect(mockWaitForSelector).toHaveBeenCalledWith('#source-element'); 86 | expect(mockWaitForSelector).toHaveBeenCalledWith('#target-element'); 87 | expect(mockBoundingBox).toHaveBeenCalledTimes(2); 88 | expect(mockMouseMove).toHaveBeenCalledWith(60, 35); // Source center (10+100/2, 10+50/2) 89 | expect(mockMouseDown).toHaveBeenCalled(); 90 | expect(mockMouseMove).toHaveBeenCalledWith(60, 35); // Target center (same mock values) 91 | expect(mockMouseUp).toHaveBeenCalled(); 92 | expect(result.isError).toBe(false); 93 | expect(result.content[0].text).toContain('Dragged element from'); 94 | }); 95 | 96 | test('should handle errors when element positions cannot be determined', async () => { 97 | const args = { 98 | sourceSelector: '#source-element', 99 | targetSelector: '#target-element' 100 | }; 101 | 102 | // Mock failure to get bounding box 103 | mockBoundingBox.mockReturnValueOnce(null); 104 | 105 | const result = await dragTool.execute(args, mockContext); 106 | 107 | expect(mockWaitForSelector).toHaveBeenCalledWith('#source-element'); 108 | expect(mockBoundingBox).toHaveBeenCalled(); 109 | expect(mockMouseMove).not.toHaveBeenCalled(); 110 | expect(result.isError).toBe(true); 111 | expect(result.content[0].text).toContain('Could not get element positions'); 112 | }); 113 | 114 | test('should handle drag errors', async () => { 115 | const args = { 116 | sourceSelector: '#source-element', 117 | targetSelector: '#target-element' 118 | }; 119 | 120 | // Mock a mouse operation error 121 | mockMouseDown.mockImplementationOnce(() => Promise.reject(new Error('Mouse operation failed'))); 122 | 123 | const result = await dragTool.execute(args, mockContext); 124 | 125 | expect(mockWaitForSelector).toHaveBeenCalledWith('#source-element'); 126 | expect(mockWaitForSelector).toHaveBeenCalledWith('#target-element'); 127 | expect(mockBoundingBox).toHaveBeenCalled(); 128 | expect(mockMouseMove).toHaveBeenCalled(); 129 | expect(mockMouseDown).toHaveBeenCalled(); 130 | expect(result.isError).toBe(true); 131 | expect(result.content[0].text).toContain('Operation failed'); 132 | }); 133 | 134 | test('should handle missing page', async () => { 135 | const args = { 136 | sourceSelector: '#source-element', 137 | targetSelector: '#target-element' 138 | }; 139 | 140 | const result = await dragTool.execute(args, { server: mockServer } as ToolContext); 141 | 142 | expect(mockWaitForSelector).not.toHaveBeenCalled(); 143 | expect(result.isError).toBe(true); 144 | expect(result.content[0].text).toContain('Browser page not initialized'); 145 | }); 146 | }); 147 | 148 | describe('PressKeyTool', () => { 149 | test('should press a keyboard key', async () => { 150 | const args = { 151 | key: 'Enter' 152 | }; 153 | 154 | const result = await pressKeyTool.execute(args, mockContext); 155 | 156 | expect(mockKeyboardPress).toHaveBeenCalledWith('Enter'); 157 | expect(result.isError).toBe(false); 158 | expect(result.content[0].text).toContain('Pressed key: Enter'); 159 | }); 160 | 161 | test('should focus an element before pressing a key if selector provided', async () => { 162 | const args = { 163 | key: 'Enter', 164 | selector: '#input-field' 165 | }; 166 | 167 | const result = await pressKeyTool.execute(args, mockContext); 168 | 169 | expect(mockWaitForSelector).toHaveBeenCalledWith('#input-field'); 170 | expect(mockFocus).toHaveBeenCalledWith('#input-field'); 171 | expect(mockKeyboardPress).toHaveBeenCalledWith('Enter'); 172 | expect(result.isError).toBe(false); 173 | expect(result.content[0].text).toContain('Pressed key: Enter'); 174 | }); 175 | 176 | test('should handle key press errors', async () => { 177 | const args = { 178 | key: 'Enter' 179 | }; 180 | 181 | // Mock a keyboard operation error 182 | mockKeyboardPress.mockImplementationOnce(() => Promise.reject(new Error('Keyboard operation failed'))); 183 | 184 | const result = await pressKeyTool.execute(args, mockContext); 185 | 186 | expect(mockKeyboardPress).toHaveBeenCalledWith('Enter'); 187 | expect(result.isError).toBe(true); 188 | expect(result.content[0].text).toContain('Operation failed'); 189 | }); 190 | 191 | test('should handle missing page', async () => { 192 | const args = { 193 | key: 'Enter' 194 | }; 195 | 196 | const result = await pressKeyTool.execute(args, { server: mockServer } as ToolContext); 197 | 198 | expect(mockKeyboardPress).not.toHaveBeenCalled(); 199 | expect(result.isError).toBe(true); 200 | expect(result.content[0].text).toContain('Browser page not initialized'); 201 | }); 202 | }); 203 | }); -------------------------------------------------------------------------------- /src/__tests__/tools/browser/console.test.ts: -------------------------------------------------------------------------------- 1 | import { ConsoleLogsTool } from '../../../tools/browser/console.js'; 2 | import { ToolContext } from '../../../tools/common/types.js'; 3 | import { jest } from '@jest/globals'; 4 | 5 | // Mock the server 6 | const mockServer = { 7 | sendMessage: jest.fn() 8 | }; 9 | 10 | // Mock context 11 | const mockContext = { 12 | server: mockServer 13 | } as ToolContext; 14 | 15 | describe('ConsoleLogsTool', () => { 16 | let consoleLogsTool: ConsoleLogsTool; 17 | 18 | beforeEach(() => { 19 | jest.clearAllMocks(); 20 | consoleLogsTool = new ConsoleLogsTool(mockServer); 21 | }); 22 | 23 | test('should register console messages', () => { 24 | consoleLogsTool.registerConsoleMessage('log', 'Test log message'); 25 | consoleLogsTool.registerConsoleMessage('error', 'Test error message'); 26 | consoleLogsTool.registerConsoleMessage('warning', 'Test warning message'); 27 | 28 | const logs = consoleLogsTool.getConsoleLogs(); 29 | expect(logs.length).toBe(3); 30 | expect(logs[0]).toContain('Test log message'); 31 | expect(logs[1]).toContain('Test error message'); 32 | expect(logs[2]).toContain('Test warning message'); 33 | }); 34 | 35 | test('should retrieve console logs with type filter', async () => { 36 | consoleLogsTool.registerConsoleMessage('log', 'Test log message'); 37 | consoleLogsTool.registerConsoleMessage('error', 'Test error message'); 38 | consoleLogsTool.registerConsoleMessage('warning', 'Test warning message'); 39 | 40 | const args = { 41 | type: 'error' 42 | }; 43 | 44 | const result = await consoleLogsTool.execute(args, mockContext); 45 | 46 | expect(result.isError).toBe(false); 47 | expect(result.content[0].text).toContain('Retrieved 1 console log(s)'); 48 | expect(result.content[1].text).toContain('Test error message'); 49 | expect(result.content[1].text).not.toContain('Test log message'); 50 | expect(result.content[1].text).not.toContain('Test warning message'); 51 | }); 52 | 53 | test('should retrieve console logs with search filter', async () => { 54 | consoleLogsTool.registerConsoleMessage('log', 'Test log message'); 55 | consoleLogsTool.registerConsoleMessage('error', 'Test error with [special] characters'); 56 | consoleLogsTool.registerConsoleMessage('warning', 'Another warning message'); 57 | 58 | const args = { 59 | search: 'special' 60 | }; 61 | 62 | const result = await consoleLogsTool.execute(args, mockContext); 63 | 64 | expect(result.isError).toBe(false); 65 | expect(result.content[0].text).toContain('Retrieved 1 console log(s)'); 66 | expect(result.content[1].text).toContain('Test error with [special] characters'); 67 | expect(result.content[1].text).not.toContain('Test log message'); 68 | expect(result.content[1].text).not.toContain('Another warning message'); 69 | }); 70 | 71 | test('should retrieve console logs with limit', async () => { 72 | for (let i = 0; i < 10; i++) { 73 | consoleLogsTool.registerConsoleMessage('log', `Test log message ${i}`); 74 | } 75 | 76 | const args = { 77 | limit: 5 78 | }; 79 | 80 | const result = await consoleLogsTool.execute(args, mockContext); 81 | 82 | expect(result.isError).toBe(false); 83 | expect(result.content[0].text).toContain('Retrieved 5 console log(s)'); 84 | 85 | // The actual implementation might only show the first log in the content 86 | // Just verify that at least one log message is present 87 | const logText = result.content[1].text as string; 88 | expect(logText).toContain('Test log message'); 89 | }); 90 | 91 | test('should clear console logs when requested', async () => { 92 | consoleLogsTool.registerConsoleMessage('log', 'Test log message'); 93 | consoleLogsTool.registerConsoleMessage('error', 'Test error message'); 94 | 95 | const args = { 96 | clear: true 97 | }; 98 | 99 | const result = await consoleLogsTool.execute(args, mockContext); 100 | 101 | expect(result.isError).toBe(false); 102 | expect(result.content[0].text).toContain('Retrieved 2 console log(s)'); 103 | 104 | // Logs should be cleared after retrieval 105 | const logs = consoleLogsTool.getConsoleLogs(); 106 | expect(logs.length).toBe(0); 107 | }); 108 | 109 | test('should handle no logs', async () => { 110 | const args = {}; 111 | 112 | const result = await consoleLogsTool.execute(args, mockContext); 113 | 114 | expect(result.isError).toBe(false); 115 | expect(result.content[0].text).toContain('No console logs matching the criteria'); 116 | }); 117 | }); -------------------------------------------------------------------------------- /src/__tests__/tools/browser/goNavigation.test.ts: -------------------------------------------------------------------------------- 1 | import { GoBackTool, GoForwardTool } from '../../../tools/browser/navigation.js'; 2 | import { ToolContext } from '../../../tools/common/types.js'; 3 | import { Page, Browser } from 'playwright'; 4 | import { jest } from '@jest/globals'; 5 | 6 | // Mock page functions 7 | const mockGoBack = jest.fn().mockImplementation(() => Promise.resolve()); 8 | const mockGoForward = jest.fn().mockImplementation(() => Promise.resolve()); 9 | const mockIsClosed = jest.fn().mockReturnValue(false); 10 | 11 | // Mock the Page object with proper typing 12 | const mockPage = { 13 | goBack: mockGoBack, 14 | goForward: mockGoForward, 15 | isClosed: mockIsClosed 16 | } as unknown as Page; 17 | 18 | // Mock the browser 19 | const mockIsConnected = jest.fn().mockReturnValue(true); 20 | const mockBrowser = { 21 | isConnected: mockIsConnected 22 | } as unknown as Browser; 23 | 24 | // Mock the server 25 | const mockServer = { 26 | sendMessage: jest.fn() 27 | }; 28 | 29 | // Mock context 30 | const mockContext = { 31 | page: mockPage, 32 | browser: mockBrowser, 33 | server: mockServer 34 | } as ToolContext; 35 | 36 | describe('Browser Navigation History Tools', () => { 37 | let goBackTool: GoBackTool; 38 | let goForwardTool: GoForwardTool; 39 | 40 | beforeEach(() => { 41 | jest.clearAllMocks(); 42 | goBackTool = new GoBackTool(mockServer); 43 | goForwardTool = new GoForwardTool(mockServer); 44 | // Reset browser and page mocks 45 | mockIsConnected.mockReturnValue(true); 46 | mockIsClosed.mockReturnValue(false); 47 | }); 48 | 49 | describe('GoBackTool', () => { 50 | test('should navigate back in browser history', async () => { 51 | const args = {}; 52 | 53 | const result = await goBackTool.execute(args, mockContext); 54 | 55 | expect(mockGoBack).toHaveBeenCalled(); 56 | expect(result.isError).toBe(false); 57 | expect(result.content[0].text).toContain('Navigated back'); 58 | }); 59 | 60 | test('should handle navigation back errors', async () => { 61 | const args = {}; 62 | 63 | // Mock a navigation error 64 | mockGoBack.mockImplementationOnce(() => Promise.reject(new Error('Navigation back failed'))); 65 | 66 | const result = await goBackTool.execute(args, mockContext); 67 | 68 | expect(mockGoBack).toHaveBeenCalled(); 69 | expect(result.isError).toBe(true); 70 | expect(result.content[0].text).toContain('Operation failed'); 71 | }); 72 | 73 | test('should handle missing page', async () => { 74 | const args = {}; 75 | 76 | const result = await goBackTool.execute(args, { server: mockServer } as ToolContext); 77 | 78 | expect(mockGoBack).not.toHaveBeenCalled(); 79 | expect(result.isError).toBe(true); 80 | expect(result.content[0].text).toContain('Browser page not initialized'); 81 | }); 82 | }); 83 | 84 | describe('GoForwardTool', () => { 85 | test('should navigate forward in browser history', async () => { 86 | const args = {}; 87 | 88 | const result = await goForwardTool.execute(args, mockContext); 89 | 90 | expect(mockGoForward).toHaveBeenCalled(); 91 | expect(result.isError).toBe(false); 92 | expect(result.content[0].text).toContain('Navigated forward'); 93 | }); 94 | 95 | test('should handle navigation forward errors', async () => { 96 | const args = {}; 97 | 98 | // Mock a navigation error 99 | mockGoForward.mockImplementationOnce(() => Promise.reject(new Error('Navigation forward failed'))); 100 | 101 | const result = await goForwardTool.execute(args, mockContext); 102 | 103 | expect(mockGoForward).toHaveBeenCalled(); 104 | expect(result.isError).toBe(true); 105 | expect(result.content[0].text).toContain('Operation failed'); 106 | }); 107 | 108 | test('should handle missing page', async () => { 109 | const args = {}; 110 | 111 | const result = await goForwardTool.execute(args, { server: mockServer } as ToolContext); 112 | 113 | expect(mockGoForward).not.toHaveBeenCalled(); 114 | expect(result.isError).toBe(true); 115 | expect(result.content[0].text).toContain('Browser page not initialized'); 116 | }); 117 | }); 118 | }); -------------------------------------------------------------------------------- /src/__tests__/tools/browser/navigation.test.ts: -------------------------------------------------------------------------------- 1 | import { NavigationTool } from '../../../tools/browser/navigation.js'; 2 | import { ToolContext } from '../../../tools/common/types.js'; 3 | import { Page, Browser } from 'playwright'; 4 | import { jest } from '@jest/globals'; 5 | 6 | // Mock the Page object 7 | const mockGoto = jest.fn(); 8 | mockGoto.mockImplementation(() => Promise.resolve()); 9 | const mockIsClosed = jest.fn().mockReturnValue(false); 10 | 11 | const mockPage = { 12 | goto: mockGoto, 13 | isClosed: mockIsClosed 14 | } as unknown as Page; 15 | 16 | // Mock the browser 17 | const mockIsConnected = jest.fn().mockReturnValue(true); 18 | const mockBrowser = { 19 | isConnected: mockIsConnected 20 | } as unknown as Browser; 21 | 22 | // Mock the server 23 | const mockServer = { 24 | sendMessage: jest.fn() 25 | }; 26 | 27 | // Mock context 28 | const mockContext = { 29 | page: mockPage, 30 | browser: mockBrowser, 31 | server: mockServer 32 | } as ToolContext; 33 | 34 | describe('NavigationTool', () => { 35 | let navigationTool: NavigationTool; 36 | 37 | beforeEach(() => { 38 | jest.clearAllMocks(); 39 | navigationTool = new NavigationTool(mockServer); 40 | // Reset mocks 41 | mockIsConnected.mockReturnValue(true); 42 | mockIsClosed.mockReturnValue(false); 43 | }); 44 | 45 | test('should navigate to a URL', async () => { 46 | const args = { 47 | url: 'https://example.com', 48 | waitUntil: 'networkidle' 49 | }; 50 | 51 | const result = await navigationTool.execute(args, mockContext); 52 | 53 | expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'networkidle', timeout: 30000 }); 54 | expect(result.isError).toBe(false); 55 | expect(result.content[0].text).toContain('Navigated to'); 56 | }); 57 | 58 | test('should handle navigation with specific browser type', async () => { 59 | const args = { 60 | url: 'https://example.com', 61 | waitUntil: 'networkidle', 62 | browserType: 'firefox' 63 | }; 64 | 65 | const result = await navigationTool.execute(args, mockContext); 66 | 67 | expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'networkidle', timeout: 30000 }); 68 | expect(result.isError).toBe(false); 69 | expect(result.content[0].text).toContain('Navigated to'); 70 | }); 71 | 72 | test('should handle navigation with webkit browser type', async () => { 73 | const args = { 74 | url: 'https://example.com', 75 | browserType: 'webkit' 76 | }; 77 | 78 | const result = await navigationTool.execute(args, mockContext); 79 | 80 | expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'load', timeout: 30000 }); 81 | expect(result.isError).toBe(false); 82 | expect(result.content[0].text).toContain('Navigated to'); 83 | }); 84 | 85 | test('should handle navigation errors', async () => { 86 | const args = { 87 | url: 'https://example.com' 88 | }; 89 | 90 | // Mock a navigation error 91 | mockGoto.mockImplementationOnce(() => Promise.reject(new Error('Navigation failed'))); 92 | 93 | const result = await navigationTool.execute(args, mockContext); 94 | 95 | expect(mockGoto).toHaveBeenCalledWith('https://example.com', { waitUntil: 'load', timeout: 30000 }); 96 | expect(result.isError).toBe(true); 97 | expect(result.content[0].text).toContain('Operation failed'); 98 | }); 99 | 100 | test('should handle missing page', async () => { 101 | const args = { 102 | url: 'https://example.com' 103 | }; 104 | 105 | // Context with browser but without page 106 | const contextWithoutPage = { 107 | browser: mockBrowser, 108 | server: mockServer 109 | } as unknown as ToolContext; 110 | 111 | const result = await navigationTool.execute(args, contextWithoutPage); 112 | 113 | expect(mockGoto).not.toHaveBeenCalled(); 114 | expect(result.isError).toBe(true); 115 | expect(result.content[0].text).toContain('Page is not available'); 116 | }); 117 | 118 | test('should handle disconnected browser', async () => { 119 | const args = { 120 | url: 'https://example.com' 121 | }; 122 | 123 | // Mock disconnected browser 124 | mockIsConnected.mockReturnValueOnce(false); 125 | 126 | const result = await navigationTool.execute(args, mockContext); 127 | 128 | expect(mockGoto).not.toHaveBeenCalled(); 129 | expect(result.isError).toBe(true); 130 | expect(result.content[0].text).toContain('Browser is not connected'); 131 | }); 132 | 133 | test('should handle closed page', async () => { 134 | const args = { 135 | url: 'https://example.com' 136 | }; 137 | 138 | // Mock closed page 139 | mockIsClosed.mockReturnValueOnce(true); 140 | 141 | const result = await navigationTool.execute(args, mockContext); 142 | 143 | expect(mockGoto).not.toHaveBeenCalled(); 144 | expect(result.isError).toBe(true); 145 | expect(result.content[0].text).toContain('Page is not available or has been closed'); 146 | }); 147 | }); -------------------------------------------------------------------------------- /src/__tests__/tools/browser/output.test.ts: -------------------------------------------------------------------------------- 1 | import { SaveAsPdfTool } from '../../../tools/browser/output.js'; 2 | import { ToolContext } from '../../../tools/common/types.js'; 3 | import { Page, Browser } from 'playwright'; 4 | import { jest } from '@jest/globals'; 5 | import * as path from 'path'; 6 | 7 | // Mock path.resolve to test path handling 8 | jest.mock('path', () => ({ 9 | resolve: jest.fn().mockImplementation((dir, file) => `${dir}/${file}`) 10 | })); 11 | 12 | // Mock page functions 13 | const mockPdf = jest.fn().mockImplementation(() => Promise.resolve()); 14 | const mockIsClosed = jest.fn().mockReturnValue(false); 15 | 16 | // Mock the Page object with proper typing 17 | const mockPage = { 18 | pdf: mockPdf, 19 | isClosed: mockIsClosed 20 | } as unknown as Page; 21 | 22 | // Mock the browser 23 | const mockIsConnected = jest.fn().mockReturnValue(true); 24 | const mockBrowser = { 25 | isConnected: mockIsConnected 26 | } as unknown as Browser; 27 | 28 | // Mock the server 29 | const mockServer = { 30 | sendMessage: jest.fn() 31 | }; 32 | 33 | // Mock context 34 | const mockContext = { 35 | page: mockPage, 36 | browser: mockBrowser, 37 | server: mockServer 38 | } as ToolContext; 39 | 40 | describe('Browser Output Tools', () => { 41 | let saveAsPdfTool: SaveAsPdfTool; 42 | 43 | beforeEach(() => { 44 | jest.clearAllMocks(); 45 | saveAsPdfTool = new SaveAsPdfTool(mockServer); 46 | // Reset browser and page mocks 47 | mockIsConnected.mockReturnValue(true); 48 | mockIsClosed.mockReturnValue(false); 49 | }); 50 | 51 | describe('SaveAsPdfTool', () => { 52 | test('should save page as PDF with default options', async () => { 53 | const args = { 54 | outputPath: '/downloads' 55 | }; 56 | 57 | const result = await saveAsPdfTool.execute(args, mockContext); 58 | 59 | expect(mockPdf).toHaveBeenCalledWith({ 60 | path: '/downloads/page.pdf', 61 | format: 'A4', 62 | printBackground: true, 63 | margin: { 64 | top: '1cm', 65 | right: '1cm', 66 | bottom: '1cm', 67 | left: '1cm' 68 | } 69 | }); 70 | expect(result.isError).toBe(false); 71 | expect(result.content[0].text).toContain('Saved page as PDF'); 72 | }); 73 | 74 | test('should save page as PDF with custom options', async () => { 75 | const args = { 76 | outputPath: '/downloads', 77 | filename: 'custom.pdf', 78 | format: 'Letter', 79 | printBackground: false, 80 | margin: { 81 | top: '2cm', 82 | right: '2cm', 83 | bottom: '2cm', 84 | left: '2cm' 85 | } 86 | }; 87 | 88 | const result = await saveAsPdfTool.execute(args, mockContext); 89 | 90 | expect(mockPdf).toHaveBeenCalledWith({ 91 | path: '/downloads/custom.pdf', 92 | format: 'Letter', 93 | printBackground: false, 94 | margin: { 95 | top: '2cm', 96 | right: '2cm', 97 | bottom: '2cm', 98 | left: '2cm' 99 | } 100 | }); 101 | expect(result.isError).toBe(false); 102 | expect(result.content[0].text).toContain('Saved page as PDF'); 103 | }); 104 | 105 | test('should handle PDF generation errors', async () => { 106 | const args = { 107 | outputPath: '/downloads' 108 | }; 109 | 110 | // Mock PDF generation error 111 | mockPdf.mockImplementationOnce(() => Promise.reject(new Error('PDF generation failed'))); 112 | 113 | const result = await saveAsPdfTool.execute(args, mockContext); 114 | 115 | expect(mockPdf).toHaveBeenCalled(); 116 | expect(result.isError).toBe(true); 117 | expect(result.content[0].text).toContain('Operation failed'); 118 | }); 119 | 120 | test('should handle missing page', async () => { 121 | const args = { 122 | outputPath: '/downloads' 123 | }; 124 | 125 | const result = await saveAsPdfTool.execute(args, { server: mockServer } as ToolContext); 126 | 127 | expect(mockPdf).not.toHaveBeenCalled(); 128 | expect(result.isError).toBe(true); 129 | expect(result.content[0].text).toContain('Browser page not initialized'); 130 | }); 131 | }); 132 | }); -------------------------------------------------------------------------------- /src/__tests__/tools/browser/screenshot.test.ts: -------------------------------------------------------------------------------- 1 | import { ScreenshotTool } from '../../../tools/browser/screenshot.js'; 2 | import { ToolContext } from '../../../tools/common/types.js'; 3 | import { Page, Browser } from 'playwright'; 4 | import { jest } from '@jest/globals'; 5 | import fs from 'node:fs'; 6 | import path from 'node:path'; 7 | 8 | // Mock fs module 9 | jest.mock('node:fs', () => ({ 10 | existsSync: jest.fn().mockReturnValue(true), 11 | mkdirSync: jest.fn(), 12 | writeFileSync: jest.fn() 13 | })); 14 | 15 | // Mock the Page object 16 | const mockScreenshot = jest.fn().mockImplementation(() => 17 | Promise.resolve(Buffer.from('mock-screenshot'))); 18 | 19 | const mockLocatorScreenshot = jest.fn().mockImplementation(() => 20 | Promise.resolve(Buffer.from('mock-element-screenshot'))); 21 | 22 | const mockElementHandle = { 23 | screenshot: mockLocatorScreenshot 24 | }; 25 | 26 | const mockElement = jest.fn().mockImplementation(() => Promise.resolve(mockElementHandle)); 27 | 28 | const mockLocator = jest.fn().mockReturnValue({ 29 | screenshot: mockLocatorScreenshot 30 | }); 31 | 32 | const mockIsClosed = jest.fn().mockReturnValue(false); 33 | const mockPage = { 34 | screenshot: mockScreenshot, 35 | locator: mockLocator, 36 | $: mockElement, 37 | isClosed: mockIsClosed 38 | } as unknown as Page; 39 | 40 | // Mock browser 41 | const mockIsConnected = jest.fn().mockReturnValue(true); 42 | const mockBrowser = { 43 | isConnected: mockIsConnected 44 | } as unknown as Browser; 45 | 46 | // Mock the server 47 | const mockServer = { 48 | sendMessage: jest.fn(), 49 | notification: jest.fn() 50 | }; 51 | 52 | // Mock context 53 | const mockContext = { 54 | page: mockPage, 55 | browser: mockBrowser, 56 | server: mockServer 57 | } as ToolContext; 58 | 59 | describe('ScreenshotTool', () => { 60 | let screenshotTool: ScreenshotTool; 61 | 62 | beforeEach(() => { 63 | jest.clearAllMocks(); 64 | screenshotTool = new ScreenshotTool(mockServer); 65 | 66 | // Mock Date to return a consistent value for testing 67 | jest.spyOn(global.Date.prototype, 'toISOString').mockReturnValue('2023-01-01T12:00:00.000Z'); 68 | (fs.existsSync as jest.Mock).mockReturnValue(true); 69 | }); 70 | 71 | afterEach(() => { 72 | jest.restoreAllMocks(); 73 | }); 74 | 75 | test('should take a full page screenshot', async () => { 76 | const args = { 77 | name: 'test-screenshot', 78 | fullPage: true 79 | }; 80 | 81 | // Return a buffer for the screenshot 82 | const screenshotBuffer = Buffer.from('mock-screenshot'); 83 | mockScreenshot.mockImplementationOnce(() => Promise.resolve(screenshotBuffer)); 84 | 85 | const result = await screenshotTool.execute(args, mockContext); 86 | 87 | // Check if screenshot was called with correct options 88 | expect(mockScreenshot).toHaveBeenCalledWith(expect.objectContaining({ 89 | fullPage: true, 90 | type: 'png' 91 | })); 92 | 93 | // Check that the result contains success message 94 | expect(result.isError).toBe(false); 95 | expect(result.content[0].text).toContain('Screenshot saved to'); 96 | }); 97 | 98 | test('should handle element screenshot', async () => { 99 | const args = { 100 | name: 'test-element-screenshot', 101 | selector: '#test-element' 102 | }; 103 | 104 | // Return a buffer for the screenshot 105 | const screenshotBuffer = Buffer.from('mock-element-screenshot'); 106 | mockLocatorScreenshot.mockImplementationOnce(() => Promise.resolve(screenshotBuffer)); 107 | 108 | const result = await screenshotTool.execute(args, mockContext); 109 | 110 | // Check that the result contains success message 111 | expect(result.isError).toBe(false); 112 | expect(result.content[0].text).toContain('Screenshot saved to'); 113 | }); 114 | 115 | test('should handle screenshot errors', async () => { 116 | const args = { 117 | name: 'test-screenshot' 118 | }; 119 | 120 | // Mock a screenshot error 121 | mockScreenshot.mockImplementationOnce(() => Promise.reject(new Error('Screenshot failed'))); 122 | 123 | const result = await screenshotTool.execute(args, mockContext); 124 | 125 | expect(mockScreenshot).toHaveBeenCalled(); 126 | expect(result.isError).toBe(true); 127 | expect(result.content[0].text).toContain('Operation failed'); 128 | }); 129 | 130 | test('should handle missing page', async () => { 131 | const args = { 132 | name: 'test-screenshot' 133 | }; 134 | 135 | // Context without page but with browser 136 | const contextWithoutPage = { 137 | browser: mockBrowser, 138 | server: mockServer 139 | } as unknown as ToolContext; 140 | 141 | const result = await screenshotTool.execute(args, contextWithoutPage); 142 | 143 | expect(mockScreenshot).not.toHaveBeenCalled(); 144 | expect(result.isError).toBe(true); 145 | expect(result.content[0].text).toContain('Browser page not initialized'); 146 | }); 147 | 148 | test('should store screenshots in a map', async () => { 149 | const args = { 150 | name: 'test-screenshot', 151 | storeBase64: true 152 | }; 153 | 154 | // Return a buffer for the screenshot 155 | const screenshotBuffer = Buffer.from('mock-screenshot'); 156 | mockScreenshot.mockImplementationOnce(() => Promise.resolve(screenshotBuffer)); 157 | 158 | await screenshotTool.execute(args, mockContext); 159 | 160 | // Check that the screenshot was stored in the map 161 | const screenshots = screenshotTool.getScreenshots(); 162 | expect(screenshots.has('test-screenshot')).toBe(true); 163 | }); 164 | 165 | test('should take a screenshot with specific browser type', async () => { 166 | const args = { 167 | name: 'browser-type-test', 168 | browserType: 'firefox' 169 | }; 170 | 171 | // Execute with browser type 172 | const result = await screenshotTool.execute(args, mockContext); 173 | 174 | expect(mockScreenshot).toHaveBeenCalled(); 175 | expect(result.isError).toBe(false); 176 | expect(result.content[0].text).toContain('Screenshot saved to'); 177 | }); 178 | }); -------------------------------------------------------------------------------- /src/__tests__/tools/browser/visiblePage.test.ts: -------------------------------------------------------------------------------- 1 | import { VisibleTextTool, VisibleHtmlTool } from '../../../tools/browser/visiblePage.js'; 2 | import { ToolContext } from '../../../tools/common/types.js'; 3 | import { Page, Browser } from 'playwright'; 4 | import { jest } from '@jest/globals'; 5 | 6 | // Mock the Page object 7 | const mockEvaluate = jest.fn(); 8 | const mockContent = jest.fn(); 9 | const mockIsClosed = jest.fn().mockReturnValue(false); 10 | 11 | const mockPage = { 12 | evaluate: mockEvaluate, 13 | content: mockContent, 14 | isClosed: mockIsClosed 15 | } as unknown as Page; 16 | 17 | // Mock the browser 18 | const mockIsConnected = jest.fn().mockReturnValue(true); 19 | const mockBrowser = { 20 | isConnected: mockIsConnected 21 | } as unknown as Browser; 22 | 23 | // Mock the server 24 | const mockServer = { 25 | sendMessage: jest.fn() 26 | }; 27 | 28 | // Mock context 29 | const mockContext = { 30 | page: mockPage, 31 | browser: mockBrowser, 32 | server: mockServer 33 | } as ToolContext; 34 | 35 | describe('VisibleTextTool', () => { 36 | let visibleTextTool: VisibleTextTool; 37 | 38 | beforeEach(() => { 39 | jest.clearAllMocks(); 40 | visibleTextTool = new VisibleTextTool(mockServer); 41 | // Reset mocks 42 | mockIsConnected.mockReturnValue(true); 43 | mockIsClosed.mockReturnValue(false); 44 | mockEvaluate.mockImplementation(() => Promise.resolve('Sample visible text content')); 45 | }); 46 | 47 | test('should retrieve visible text content', async () => { 48 | const args = {}; 49 | 50 | const result = await visibleTextTool.execute(args, mockContext); 51 | 52 | expect(mockEvaluate).toHaveBeenCalled(); 53 | expect(result.isError).toBe(false); 54 | expect(result.content[0].text).toContain('Visible text content'); 55 | expect(result.content[0].text).toContain('Sample visible text content'); 56 | }); 57 | 58 | test('should handle missing page', async () => { 59 | const args = {}; 60 | 61 | // Context with browser but without page 62 | const contextWithoutPage = { 63 | browser: mockBrowser, 64 | server: mockServer 65 | } as unknown as ToolContext; 66 | 67 | const result = await visibleTextTool.execute(args, contextWithoutPage); 68 | 69 | expect(mockEvaluate).not.toHaveBeenCalled(); 70 | expect(result.isError).toBe(true); 71 | expect(result.content[0].text).toContain('Page is not available'); 72 | }); 73 | 74 | test('should handle disconnected browser', async () => { 75 | const args = {}; 76 | 77 | // Mock disconnected browser 78 | mockIsConnected.mockReturnValueOnce(false); 79 | 80 | const result = await visibleTextTool.execute(args, mockContext); 81 | 82 | expect(mockEvaluate).not.toHaveBeenCalled(); 83 | expect(result.isError).toBe(true); 84 | expect(result.content[0].text).toContain('Browser is not connected'); 85 | }); 86 | 87 | test('should handle closed page', async () => { 88 | const args = {}; 89 | 90 | // Mock closed page 91 | mockIsClosed.mockReturnValueOnce(true); 92 | 93 | const result = await visibleTextTool.execute(args, mockContext); 94 | 95 | expect(mockEvaluate).not.toHaveBeenCalled(); 96 | expect(result.isError).toBe(true); 97 | expect(result.content[0].text).toContain('Page is not available or has been closed'); 98 | }); 99 | 100 | test('should handle evaluation errors', async () => { 101 | const args = {}; 102 | 103 | // Mock evaluation error 104 | mockEvaluate.mockImplementationOnce(() => Promise.reject(new Error('Evaluation failed'))); 105 | 106 | const result = await visibleTextTool.execute(args, mockContext); 107 | 108 | expect(mockEvaluate).toHaveBeenCalled(); 109 | expect(result.isError).toBe(true); 110 | expect(result.content[0].text).toContain('Failed to get visible text content'); 111 | expect(result.content[0].text).toContain('Evaluation failed'); 112 | }); 113 | }); 114 | 115 | describe('VisibleHtmlTool', () => { 116 | let visibleHtmlTool: VisibleHtmlTool; 117 | 118 | beforeEach(() => { 119 | jest.clearAllMocks(); 120 | visibleHtmlTool = new VisibleHtmlTool(mockServer); 121 | // Reset mocks 122 | mockIsConnected.mockReturnValue(true); 123 | mockIsClosed.mockReturnValue(false); 124 | mockContent.mockImplementation(() => Promise.resolve('Sample HTML content')); 125 | }); 126 | 127 | test('should retrieve HTML content', async () => { 128 | const args = {}; 129 | 130 | const result = await visibleHtmlTool.execute(args, mockContext); 131 | 132 | expect(mockContent).toHaveBeenCalled(); 133 | expect(result.isError).toBe(false); 134 | expect(result.content[0].text).toContain('HTML content'); 135 | expect(result.content[0].text).toContain('Sample HTML content'); 136 | }); 137 | 138 | test('should handle missing page', async () => { 139 | const args = {}; 140 | 141 | // Context with browser but without page 142 | const contextWithoutPage = { 143 | browser: mockBrowser, 144 | server: mockServer 145 | } as unknown as ToolContext; 146 | 147 | const result = await visibleHtmlTool.execute(args, contextWithoutPage); 148 | 149 | expect(mockContent).not.toHaveBeenCalled(); 150 | expect(result.isError).toBe(true); 151 | expect(result.content[0].text).toContain('Page is not available'); 152 | }); 153 | 154 | test('should handle disconnected browser', async () => { 155 | const args = {}; 156 | 157 | // Mock disconnected browser 158 | mockIsConnected.mockReturnValueOnce(false); 159 | 160 | const result = await visibleHtmlTool.execute(args, mockContext); 161 | 162 | expect(mockContent).not.toHaveBeenCalled(); 163 | expect(result.isError).toBe(true); 164 | expect(result.content[0].text).toContain('Browser is not connected'); 165 | }); 166 | 167 | test('should handle closed page', async () => { 168 | const args = {}; 169 | 170 | // Mock closed page 171 | mockIsClosed.mockReturnValueOnce(true); 172 | 173 | const result = await visibleHtmlTool.execute(args, mockContext); 174 | 175 | expect(mockContent).not.toHaveBeenCalled(); 176 | expect(result.isError).toBe(true); 177 | expect(result.content[0].text).toContain('Page is not available or has been closed'); 178 | }); 179 | 180 | test('should handle content retrieval errors', async () => { 181 | const args = {}; 182 | 183 | // Mock content error 184 | mockContent.mockImplementationOnce(() => Promise.reject(new Error('Content retrieval failed'))); 185 | 186 | const result = await visibleHtmlTool.execute(args, mockContext); 187 | 188 | expect(mockContent).toHaveBeenCalled(); 189 | expect(result.isError).toBe(true); 190 | expect(result.content[0].text).toContain('Failed to get visible HTML content'); 191 | expect(result.content[0].text).toContain('Content retrieval failed'); 192 | }); 193 | }); -------------------------------------------------------------------------------- /src/evals/evals.ts: -------------------------------------------------------------------------------- 1 | //evals.ts 2 | 3 | import { EvalConfig } from 'mcp-evals'; 4 | import { openai } from "@ai-sdk/openai"; 5 | import { grade, EvalFunction } from "mcp-evals"; 6 | 7 | const startCodegenSessionEval: EvalFunction = { 8 | name: 'startCodegenSession Evaluation', 9 | description: 'Evaluates the start codegen session tool', 10 | run: async () => { 11 | const result = await grade(openai("gpt-4"), "Please start a new code generation session with an output path of /my/test/path, a testNamePrefix of MyPrefix, and comments enabled. Confirm the session was created successfully."); 12 | return JSON.parse(result); 13 | } 14 | }; 15 | 16 | const end_codegen_sessionEval: EvalFunction = { 17 | name: 'end_codegen_session Evaluation', 18 | description: 'Evaluates the end_codegen_session tool functionality', 19 | run: async () => { 20 | const result = await grade(openai("gpt-4"), "Please end the code generation session with ID session123 and generate the Playwright test code"); 21 | return JSON.parse(result); 22 | } 23 | }; 24 | 25 | const get_codegen_sessionEval: EvalFunction = { 26 | name: 'get_codegen_session Tool Evaluation', 27 | description: 'Evaluates the retrieval of code generation session details', 28 | run: async () => { 29 | const result = await grade(openai("gpt-4"), "Please retrieve the code generation session details using session ID abc123."); 30 | return JSON.parse(result); 31 | } 32 | }; 33 | 34 | const clearCodegenSessionEval: EvalFunction = { 35 | name: 'clear_codegen_session Evaluation', 36 | description: 'Evaluates the functionality of clearing a code generation session', 37 | run: async () => { 38 | const result = await grade(openai("gpt-4"), "Please clear the code generation session with the ID testSession_123 to verify removal."); 39 | return JSON.parse(result); 40 | } 41 | }; 42 | 43 | const config: EvalConfig = { 44 | model: openai("gpt-4"), 45 | evals: [startCodegenSessionEval, end_codegen_sessionEval, get_codegen_sessionEval, clearCodegenSessionEval] 46 | }; 47 | 48 | export default config; 49 | 50 | export const evals = [startCodegenSessionEval, end_codegen_sessionEval, get_codegen_sessionEval, clearCodegenSessionEval]; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { createToolDefinitions } from "./tools.js"; 6 | import { setupRequestHandlers } from "./requestHandler.js"; 7 | 8 | async function runServer() { 9 | const server = new Server( 10 | { 11 | name: "executeautomation/playwright-mcp-server", 12 | version: "1.0.5", 13 | }, 14 | { 15 | capabilities: { 16 | resources: {}, 17 | tools: {}, 18 | }, 19 | } 20 | ); 21 | 22 | // Create tool definitions 23 | const TOOLS = createToolDefinitions(); 24 | 25 | // Setup request handlers 26 | setupRequestHandlers(server, TOOLS); 27 | 28 | // Graceful shutdown logic 29 | function shutdown() { 30 | console.log('Shutdown signal received'); 31 | process.exit(0); 32 | } 33 | 34 | process.on('SIGINT', shutdown); 35 | process.on('SIGTERM', shutdown); 36 | process.on('exit', shutdown); 37 | process.on('uncaughtException', (err) => { 38 | console.error('Uncaught Exception:', err); 39 | }); 40 | 41 | // Create transport and connect 42 | const transport = new StdioServerTransport(); 43 | await server.connect(transport); 44 | } 45 | 46 | runServer().catch((error) => { 47 | console.error("Fatal error in main():", error); 48 | process.exit(1); 49 | }); -------------------------------------------------------------------------------- /src/requestHandler.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js"; 2 | import { 3 | ListResourcesRequestSchema, 4 | ReadResourceRequestSchema, 5 | ListToolsRequestSchema, 6 | CallToolRequestSchema, 7 | Tool 8 | } from "@modelcontextprotocol/sdk/types.js"; 9 | import { handleToolCall, getConsoleLogs, getScreenshots } from "./toolHandler.js"; 10 | 11 | export function setupRequestHandlers(server: Server, tools: Tool[]) { 12 | // List resources handler 13 | server.setRequestHandler(ListResourcesRequestSchema, async () => ({ 14 | resources: [ 15 | { 16 | uri: "console://logs", 17 | mimeType: "text/plain", 18 | name: "Browser console logs", 19 | }, 20 | ...Array.from(getScreenshots().keys()).map(name => ({ 21 | uri: `screenshot://${name}`, 22 | mimeType: "image/png", 23 | name: `Screenshot: ${name}`, 24 | })), 25 | ], 26 | })); 27 | 28 | // Read resource handler 29 | server.setRequestHandler(ReadResourceRequestSchema, async (request) => { 30 | const uri = request.params.uri.toString(); 31 | 32 | if (uri === "console://logs") { 33 | const logs = getConsoleLogs().join("\n"); 34 | return { 35 | contents: [{ 36 | uri, 37 | mimeType: "text/plain", 38 | text: logs, 39 | }], 40 | }; 41 | } 42 | 43 | if (uri.startsWith("screenshot://")) { 44 | const name = uri.split("://")[1]; 45 | const screenshot = getScreenshots().get(name); 46 | if (screenshot) { 47 | return { 48 | contents: [{ 49 | uri, 50 | mimeType: "image/png", 51 | blob: screenshot, 52 | }], 53 | }; 54 | } 55 | } 56 | 57 | throw new Error(`Resource not found: ${uri}`); 58 | }); 59 | 60 | // List tools handler 61 | server.setRequestHandler(ListToolsRequestSchema, async () => ({ 62 | tools: tools, 63 | })); 64 | 65 | // Call tool handler 66 | server.setRequestHandler(CallToolRequestSchema, async (request) => 67 | handleToolCall(request.params.name, request.params.arguments ?? {}, server) 68 | ); 69 | } -------------------------------------------------------------------------------- /src/tools/api/base.ts: -------------------------------------------------------------------------------- 1 | import type { APIRequestContext } from 'playwright'; 2 | import { ToolHandler, ToolContext, ToolResponse, createErrorResponse } from '../common/types.js'; 3 | 4 | /** 5 | * Base class for all API-based tools 6 | * Provides common functionality and error handling 7 | */ 8 | export abstract class ApiToolBase implements ToolHandler { 9 | protected server: any; 10 | 11 | constructor(server: any) { 12 | this.server = server; 13 | } 14 | 15 | /** 16 | * Main execution method that all tools must implement 17 | */ 18 | abstract execute(args: any, context: ToolContext): Promise; 19 | 20 | /** 21 | * Ensures an API context is available and returns it 22 | * @param context The tool context containing apiContext 23 | * @returns The apiContext or null if not available 24 | */ 25 | protected ensureApiContext(context: ToolContext): APIRequestContext | null { 26 | if (!context.apiContext) { 27 | return null; 28 | } 29 | return context.apiContext; 30 | } 31 | 32 | /** 33 | * Validates that an API context is available and returns an error response if not 34 | * @param context The tool context 35 | * @returns Either null if apiContext is available, or an error response 36 | */ 37 | protected validateApiContextAvailable(context: ToolContext): ToolResponse | null { 38 | if (!this.ensureApiContext(context)) { 39 | return createErrorResponse("API context not initialized"); 40 | } 41 | return null; 42 | } 43 | 44 | /** 45 | * Safely executes an API operation with proper error handling 46 | * @param context The tool context 47 | * @param operation The async operation to perform 48 | * @returns The tool response 49 | */ 50 | protected async safeExecute( 51 | context: ToolContext, 52 | operation: (apiContext: APIRequestContext) => Promise 53 | ): Promise { 54 | const apiError = this.validateApiContextAvailable(context); 55 | if (apiError) return apiError; 56 | 57 | try { 58 | return await operation(context.apiContext!); 59 | } catch (error) { 60 | return createErrorResponse(`API operation failed: ${(error as Error).message}`); 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/tools/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base.js'; 2 | export * from './requests.js'; 3 | 4 | // TODO: Add exports for other API tools as they are implemented -------------------------------------------------------------------------------- /src/tools/api/requests.ts: -------------------------------------------------------------------------------- 1 | import { ApiToolBase } from './base.js'; 2 | import { ToolContext, ToolResponse, createSuccessResponse, createErrorResponse } from '../common/types.js'; 3 | 4 | /** 5 | * Tool for making GET requests 6 | */ 7 | export class GetRequestTool extends ApiToolBase { 8 | /** 9 | * Execute the GET request tool 10 | */ 11 | async execute(args: any, context: ToolContext): Promise { 12 | return this.safeExecute(context, async (apiContext) => { 13 | const response = await apiContext.get(args.url); 14 | 15 | let responseText; 16 | try { 17 | responseText = await response.text(); 18 | } catch (error) { 19 | responseText = "Unable to get response text"; 20 | } 21 | 22 | return createSuccessResponse([ 23 | `GET request to ${args.url}`, 24 | `Status: ${response.status()} ${response.statusText()}`, 25 | `Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}` 26 | ]); 27 | }); 28 | } 29 | } 30 | 31 | /** 32 | * Tool for making POST requests 33 | */ 34 | export class PostRequestTool extends ApiToolBase { 35 | /** 36 | * Execute the POST request tool 37 | */ 38 | async execute(args: any, context: ToolContext): Promise { 39 | return this.safeExecute(context, async (apiContext) => { 40 | // Check if the value is valid JSON if it starts with { or [ 41 | if (args.value && typeof args.value === 'string' && 42 | (args.value.startsWith('{') || args.value.startsWith('['))) { 43 | try { 44 | JSON.parse(args.value); 45 | } catch (error) { 46 | return createErrorResponse(`Failed to parse request body: ${(error as Error).message}`); 47 | } 48 | } 49 | 50 | const response = await apiContext.post(args.url, { 51 | data: typeof args.value === 'string' ? JSON.parse(args.value) : args.value, 52 | headers: { 53 | 'Content-Type': 'application/json', 54 | ...(args.token ? { 'Authorization': `Bearer ${args.token}` } : {}), 55 | ...(args.headers || {}) 56 | } 57 | }); 58 | 59 | let responseText; 60 | try { 61 | responseText = await response.text(); 62 | } catch (error) { 63 | responseText = "Unable to get response text"; 64 | } 65 | 66 | return createSuccessResponse([ 67 | `POST request to ${args.url}`, 68 | `Status: ${response.status()} ${response.statusText()}`, 69 | `Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}` 70 | ]); 71 | }); 72 | } 73 | } 74 | 75 | /** 76 | * Tool for making PUT requests 77 | */ 78 | export class PutRequestTool extends ApiToolBase { 79 | /** 80 | * Execute the PUT request tool 81 | */ 82 | async execute(args: any, context: ToolContext): Promise { 83 | return this.safeExecute(context, async (apiContext) => { 84 | // Check if the value is valid JSON if it starts with { or [ 85 | if (args.value && typeof args.value === 'string' && 86 | (args.value.startsWith('{') || args.value.startsWith('['))) { 87 | try { 88 | JSON.parse(args.value); 89 | } catch (error) { 90 | return createErrorResponse(`Failed to parse request body: ${(error as Error).message}`); 91 | } 92 | } 93 | 94 | const response = await apiContext.put(args.url, { 95 | data: args.value 96 | }); 97 | 98 | let responseText; 99 | try { 100 | responseText = await response.text(); 101 | } catch (error) { 102 | responseText = "Unable to get response text"; 103 | } 104 | 105 | return createSuccessResponse([ 106 | `PUT request to ${args.url}`, 107 | `Status: ${response.status()} ${response.statusText()}`, 108 | `Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}` 109 | ]); 110 | }); 111 | } 112 | } 113 | 114 | /** 115 | * Tool for making PATCH requests 116 | */ 117 | export class PatchRequestTool extends ApiToolBase { 118 | /** 119 | * Execute the PATCH request tool 120 | */ 121 | async execute(args: any, context: ToolContext): Promise { 122 | return this.safeExecute(context, async (apiContext) => { 123 | // Check if the value is valid JSON if it starts with { or [ 124 | if (args.value && typeof args.value === 'string' && 125 | (args.value.startsWith('{') || args.value.startsWith('['))) { 126 | try { 127 | JSON.parse(args.value); 128 | } catch (error) { 129 | return createErrorResponse(`Failed to parse request body: ${(error as Error).message}`); 130 | } 131 | } 132 | 133 | const response = await apiContext.patch(args.url, { 134 | data: args.value 135 | }); 136 | 137 | let responseText; 138 | try { 139 | responseText = await response.text(); 140 | } catch (error) { 141 | responseText = "Unable to get response text"; 142 | } 143 | 144 | return createSuccessResponse([ 145 | `PATCH request to ${args.url}`, 146 | `Status: ${response.status()} ${response.statusText()}`, 147 | `Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}` 148 | ]); 149 | }); 150 | } 151 | } 152 | 153 | /** 154 | * Tool for making DELETE requests 155 | */ 156 | export class DeleteRequestTool extends ApiToolBase { 157 | /** 158 | * Execute the DELETE request tool 159 | */ 160 | async execute(args: any, context: ToolContext): Promise { 161 | return this.safeExecute(context, async (apiContext) => { 162 | const response = await apiContext.delete(args.url); 163 | 164 | let responseText; 165 | try { 166 | responseText = await response.text(); 167 | } catch (error) { 168 | responseText = "Unable to get response text"; 169 | } 170 | 171 | return createSuccessResponse([ 172 | `DELETE request to ${args.url}`, 173 | `Status: ${response.status()} ${response.statusText()}`, 174 | `Response: ${responseText.substring(0, 1000)}${responseText.length > 1000 ? '...' : ''}` 175 | ]); 176 | }); 177 | } 178 | } -------------------------------------------------------------------------------- /src/tools/browser/base.ts: -------------------------------------------------------------------------------- 1 | import type { Browser, Page } from 'playwright'; 2 | import { ToolHandler, ToolContext, ToolResponse, createErrorResponse } from '../common/types.js'; 3 | 4 | /** 5 | * Base class for all browser-based tools 6 | * Provides common functionality and error handling 7 | */ 8 | export abstract class BrowserToolBase implements ToolHandler { 9 | protected server: any; 10 | 11 | constructor(server: any) { 12 | this.server = server; 13 | } 14 | 15 | /** 16 | * Main execution method that all tools must implement 17 | */ 18 | abstract execute(args: any, context: ToolContext): Promise; 19 | 20 | /** 21 | * Ensures a page is available and returns it 22 | * @param context The tool context containing browser and page 23 | * @returns The page or null if not available 24 | */ 25 | protected ensurePage(context: ToolContext): Page | null { 26 | if (!context.page) { 27 | return null; 28 | } 29 | return context.page; 30 | } 31 | 32 | /** 33 | * Validates that a page is available and returns an error response if not 34 | * @param context The tool context 35 | * @returns Either null if page is available, or an error response 36 | */ 37 | protected validatePageAvailable(context: ToolContext): ToolResponse | null { 38 | if (!this.ensurePage(context)) { 39 | return createErrorResponse("Browser page not initialized!"); 40 | } 41 | return null; 42 | } 43 | 44 | /** 45 | * Safely executes a browser operation with proper error handling 46 | * @param context The tool context 47 | * @param operation The async operation to perform 48 | * @returns The tool response 49 | */ 50 | protected async safeExecute( 51 | context: ToolContext, 52 | operation: (page: Page) => Promise 53 | ): Promise { 54 | const pageError = this.validatePageAvailable(context); 55 | if (pageError) return pageError; 56 | 57 | try { 58 | // Verify browser is connected before proceeding 59 | if (context.browser && !context.browser.isConnected()) { 60 | // If browser exists but is disconnected, reset state 61 | const { resetBrowserState } = await import('../../toolHandler.js'); 62 | resetBrowserState(); 63 | return createErrorResponse("Browser is disconnected. Please retry the operation."); 64 | } 65 | 66 | // Check if page is closed 67 | if (context.page.isClosed()) { 68 | return createErrorResponse("Page is closed. Please retry the operation."); 69 | } 70 | 71 | return await operation(context.page!); 72 | } catch (error) { 73 | const errorMessage = (error as Error).message; 74 | 75 | // Check for common browser disconnection errors 76 | if ( 77 | errorMessage.includes("Target page, context or browser has been closed") || 78 | errorMessage.includes("Target closed") || 79 | errorMessage.includes("Browser has been disconnected") || 80 | errorMessage.includes("Protocol error") || 81 | errorMessage.includes("Connection closed") 82 | ) { 83 | // Reset browser state on connection issues 84 | const { resetBrowserState } = await import('../../toolHandler.js'); 85 | resetBrowserState(); 86 | return createErrorResponse(`Browser connection error: ${errorMessage}. Connection has been reset - please retry the operation.`); 87 | } 88 | 89 | return createErrorResponse(`Operation failed: ${errorMessage}`); 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /src/tools/browser/console.ts: -------------------------------------------------------------------------------- 1 | import { BrowserToolBase } from './base.js'; 2 | import { ToolContext, ToolResponse, createSuccessResponse } from '../common/types.js'; 3 | 4 | /** 5 | * Tool for retrieving and filtering console logs from the browser 6 | */ 7 | export class ConsoleLogsTool extends BrowserToolBase { 8 | private consoleLogs: string[] = []; 9 | 10 | /** 11 | * Register a console message 12 | * @param type The type of console message 13 | * @param text The text content of the message 14 | */ 15 | registerConsoleMessage(type: string, text: string): void { 16 | const logEntry = `[${type}] ${text}`; 17 | this.consoleLogs.push(logEntry); 18 | } 19 | 20 | /** 21 | * Execute the console logs tool 22 | */ 23 | async execute(args: any, context: ToolContext): Promise { 24 | // No need to use safeExecute here as we don't need to interact with the page 25 | // We're just filtering and returning logs that are already stored 26 | 27 | let logs = [...this.consoleLogs]; 28 | 29 | // Filter by type if specified 30 | if (args.type && args.type !== 'all') { 31 | logs = logs.filter(log => log.startsWith(`[${args.type}]`)); 32 | } 33 | 34 | // Filter by search text if specified 35 | if (args.search) { 36 | logs = logs.filter(log => log.includes(args.search)); 37 | } 38 | 39 | // Limit the number of logs if specified 40 | if (args.limit && args.limit > 0) { 41 | logs = logs.slice(-args.limit); 42 | } 43 | 44 | // Clear logs if requested 45 | if (args.clear) { 46 | this.consoleLogs = []; 47 | } 48 | 49 | // Format the response 50 | if (logs.length === 0) { 51 | return createSuccessResponse("No console logs matching the criteria"); 52 | } else { 53 | return createSuccessResponse([ 54 | `Retrieved ${logs.length} console log(s):`, 55 | ...logs 56 | ]); 57 | } 58 | } 59 | 60 | /** 61 | * Get all console logs 62 | */ 63 | getConsoleLogs(): string[] { 64 | return this.consoleLogs; 65 | } 66 | 67 | /** 68 | * Clear all console logs 69 | */ 70 | clearConsoleLogs(): void { 71 | this.consoleLogs = []; 72 | } 73 | } -------------------------------------------------------------------------------- /src/tools/browser/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base.js'; 2 | export * from './screenshot.js'; 3 | export * from './navigation.js'; 4 | export * from './console.js'; 5 | export * from './interaction.js'; 6 | export * from './response.js'; 7 | export * from './useragent.js'; 8 | 9 | // TODO: Add exports for other browser tools as they are implemented 10 | // export * from './interaction.js'; -------------------------------------------------------------------------------- /src/tools/browser/interaction.ts: -------------------------------------------------------------------------------- 1 | import { BrowserToolBase } from './base.js'; 2 | import { ToolContext, ToolResponse, createSuccessResponse, createErrorResponse } from '../common/types.js'; 3 | import { setGlobalPage } from '../../toolHandler.js'; 4 | /** 5 | * Tool for clicking elements on the page 6 | */ 7 | export class ClickTool extends BrowserToolBase { 8 | /** 9 | * Execute the click tool 10 | */ 11 | async execute(args: any, context: ToolContext): Promise { 12 | return this.safeExecute(context, async (page) => { 13 | await page.click(args.selector); 14 | return createSuccessResponse(`Clicked element: ${args.selector}`); 15 | }); 16 | } 17 | } 18 | /** 19 | * Tool for clicking a link and switching to the new tab 20 | */ 21 | export class ClickAndSwitchTabTool extends BrowserToolBase { 22 | /** 23 | * Execute the click and switch tab tool 24 | */ 25 | async execute(args: any, context: ToolContext): Promise { 26 | 27 | return this.safeExecute(context, async (page) => { 28 | // Listen for a new tab to open 29 | const [newPage] = await Promise.all([ 30 | //context.browser.waitForEvent('page'), // Wait for a new page (tab) to open 31 | page.context().waitForEvent('page'),// Wait for a new page (tab) to open 32 | page.click(args.selector), // Click the link that opens the new tab 33 | ]); 34 | 35 | // Wait for the new page to load 36 | await newPage.waitForLoadState('domcontentloaded'); 37 | 38 | // Switch control to the new tab 39 | setGlobalPage(newPage); 40 | //page= newPage; // Update the current page to the new tab 41 | //context.page = newPage; 42 | //context.page.bringToFront(); // Bring the new tab to the front 43 | return createSuccessResponse(`Clicked link and switched to new tab: ${newPage.url()}`); 44 | //return createSuccessResponse(`Clicked link and switched to new tab: ${context.page.url()}`); 45 | }); 46 | } 47 | } 48 | /** 49 | * Tool for clicking elements inside iframes 50 | */ 51 | export class IframeClickTool extends BrowserToolBase { 52 | /** 53 | * Execute the iframe click tool 54 | */ 55 | async execute(args: any, context: ToolContext): Promise { 56 | return this.safeExecute(context, async (page) => { 57 | const frame = page.frameLocator(args.iframeSelector); 58 | if (!frame) { 59 | return createErrorResponse(`Iframe not found: ${args.iframeSelector}`); 60 | } 61 | 62 | await frame.locator(args.selector).click(); 63 | return createSuccessResponse(`Clicked element ${args.selector} inside iframe ${args.iframeSelector}`); 64 | }); 65 | } 66 | } 67 | 68 | /** 69 | * Tool for filling elements inside iframes 70 | */ 71 | export class IframeFillTool extends BrowserToolBase { 72 | /** 73 | * Execute the iframe fill tool 74 | */ 75 | async execute(args: any, context: ToolContext): Promise { 76 | return this.safeExecute(context, async (page) => { 77 | const frame = page.frameLocator(args.iframeSelector); 78 | if (!frame) { 79 | return createErrorResponse(`Iframe not found: ${args.iframeSelector}`); 80 | } 81 | 82 | await frame.locator(args.selector).fill(args.value); 83 | return createSuccessResponse(`Filled element ${args.selector} inside iframe ${args.iframeSelector} with: ${args.value}`); 84 | }); 85 | } 86 | } 87 | 88 | /** 89 | * Tool for filling form fields 90 | */ 91 | export class FillTool extends BrowserToolBase { 92 | /** 93 | * Execute the fill tool 94 | */ 95 | async execute(args: any, context: ToolContext): Promise { 96 | return this.safeExecute(context, async (page) => { 97 | await page.waitForSelector(args.selector); 98 | await page.fill(args.selector, args.value); 99 | return createSuccessResponse(`Filled ${args.selector} with: ${args.value}`); 100 | }); 101 | } 102 | } 103 | 104 | /** 105 | * Tool for selecting options from dropdown menus 106 | */ 107 | export class SelectTool extends BrowserToolBase { 108 | /** 109 | * Execute the select tool 110 | */ 111 | async execute(args: any, context: ToolContext): Promise { 112 | return this.safeExecute(context, async (page) => { 113 | await page.waitForSelector(args.selector); 114 | await page.selectOption(args.selector, args.value); 115 | return createSuccessResponse(`Selected ${args.selector} with: ${args.value}`); 116 | }); 117 | } 118 | } 119 | 120 | /** 121 | * Tool for hovering over elements 122 | */ 123 | export class HoverTool extends BrowserToolBase { 124 | /** 125 | * Execute the hover tool 126 | */ 127 | async execute(args: any, context: ToolContext): Promise { 128 | return this.safeExecute(context, async (page) => { 129 | await page.waitForSelector(args.selector); 130 | await page.hover(args.selector); 131 | return createSuccessResponse(`Hovered ${args.selector}`); 132 | }); 133 | } 134 | } 135 | 136 | /** 137 | * Tool for executing JavaScript in the browser 138 | */ 139 | export class EvaluateTool extends BrowserToolBase { 140 | /** 141 | * Execute the evaluate tool 142 | */ 143 | async execute(args: any, context: ToolContext): Promise { 144 | return this.safeExecute(context, async (page) => { 145 | const result = await page.evaluate(args.script); 146 | 147 | // Convert result to string for display 148 | let resultStr: string; 149 | try { 150 | resultStr = JSON.stringify(result, null, 2); 151 | } catch (error) { 152 | resultStr = String(result); 153 | } 154 | 155 | return createSuccessResponse([ 156 | `Executed JavaScript:`, 157 | `${args.script}`, 158 | `Result:`, 159 | `${resultStr}` 160 | ]); 161 | }); 162 | } 163 | } 164 | 165 | /** 166 | * Tool for dragging elements on the page 167 | */ 168 | export class DragTool extends BrowserToolBase { 169 | /** 170 | * Execute the drag tool 171 | */ 172 | async execute(args: any, context: ToolContext): Promise { 173 | return this.safeExecute(context, async (page) => { 174 | const sourceElement = await page.waitForSelector(args.sourceSelector); 175 | const targetElement = await page.waitForSelector(args.targetSelector); 176 | 177 | const sourceBound = await sourceElement.boundingBox(); 178 | const targetBound = await targetElement.boundingBox(); 179 | 180 | if (!sourceBound || !targetBound) { 181 | return createErrorResponse("Could not get element positions for drag operation"); 182 | } 183 | 184 | await page.mouse.move( 185 | sourceBound.x + sourceBound.width / 2, 186 | sourceBound.y + sourceBound.height / 2 187 | ); 188 | await page.mouse.down(); 189 | await page.mouse.move( 190 | targetBound.x + targetBound.width / 2, 191 | targetBound.y + targetBound.height / 2 192 | ); 193 | await page.mouse.up(); 194 | 195 | return createSuccessResponse(`Dragged element from ${args.sourceSelector} to ${args.targetSelector}`); 196 | }); 197 | } 198 | } 199 | 200 | /** 201 | * Tool for pressing keyboard keys 202 | */ 203 | export class PressKeyTool extends BrowserToolBase { 204 | /** 205 | * Execute the key press tool 206 | */ 207 | async execute(args: any, context: ToolContext): Promise { 208 | return this.safeExecute(context, async (page) => { 209 | if (args.selector) { 210 | await page.waitForSelector(args.selector); 211 | await page.focus(args.selector); 212 | } 213 | 214 | await page.keyboard.press(args.key); 215 | return createSuccessResponse(`Pressed key: ${args.key}`); 216 | }); 217 | } 218 | } 219 | 220 | 221 | /** 222 | * Tool for switching browser tabs 223 | */ 224 | // export class SwitchTabTool extends BrowserToolBase { 225 | // /** 226 | // * Switch the tab to the specified index 227 | // */ 228 | // async execute(args: any, context: ToolContext): Promise { 229 | // return this.safeExecute(context, async (page) => { 230 | // const tabs = await browser.page; 231 | 232 | // // Validate the tab index 233 | // const tabIndex = Number(args.index); 234 | // if (isNaN(tabIndex)) { 235 | // return createErrorResponse(`Invalid tab index: ${args.index}. It must be a number.`); 236 | // } 237 | 238 | // if (tabIndex >= 0 && tabIndex < tabs.length) { 239 | // await tabs[tabIndex].bringToFront(); 240 | // return createSuccessResponse(`Switched to tab with index ${tabIndex}`); 241 | // } else { 242 | // return createErrorResponse( 243 | // `Tab index out of range: ${tabIndex}. Available tabs: 0 to ${tabs.length - 1}.` 244 | // ); 245 | // } 246 | // }); 247 | // } 248 | // } -------------------------------------------------------------------------------- /src/tools/browser/navigation.ts: -------------------------------------------------------------------------------- 1 | import { BrowserToolBase } from './base.js'; 2 | import { ToolContext, ToolResponse, createSuccessResponse, createErrorResponse } from '../common/types.js'; 3 | import { resetBrowserState } from '../../toolHandler.js'; 4 | 5 | /** 6 | * Tool for navigating to URLs 7 | */ 8 | export class NavigationTool extends BrowserToolBase { 9 | /** 10 | * Execute the navigation tool 11 | */ 12 | async execute(args: any, context: ToolContext): Promise { 13 | // Check if browser is available 14 | if (!context.browser || !context.browser.isConnected()) { 15 | // If browser is not connected, we need to reset the state to force recreation 16 | resetBrowserState(); 17 | return createErrorResponse( 18 | "Browser is not connected. The connection has been reset - please retry your navigation." 19 | ); 20 | } 21 | 22 | // Check if page is available and not closed 23 | if (!context.page || context.page.isClosed()) { 24 | return createErrorResponse( 25 | "Page is not available or has been closed. Please retry your navigation." 26 | ); 27 | } 28 | 29 | return this.safeExecute(context, async (page) => { 30 | try { 31 | await page.goto(args.url, { 32 | timeout: args.timeout || 30000, 33 | waitUntil: args.waitUntil || "load" 34 | }); 35 | 36 | return createSuccessResponse(`Navigated to ${args.url}`); 37 | } catch (error) { 38 | const errorMessage = (error as Error).message; 39 | 40 | // Check for common disconnection errors 41 | if ( 42 | errorMessage.includes("Target page, context or browser has been closed") || 43 | errorMessage.includes("Target closed") || 44 | errorMessage.includes("Browser has been disconnected") 45 | ) { 46 | // Reset browser state to force recreation on next attempt 47 | resetBrowserState(); 48 | return createErrorResponse( 49 | `Browser connection issue: ${errorMessage}. Connection has been reset - please retry your navigation.` 50 | ); 51 | } 52 | 53 | // For other errors, return the standard error 54 | throw error; 55 | } 56 | }); 57 | } 58 | } 59 | 60 | /** 61 | * Tool for closing the browser 62 | */ 63 | export class CloseBrowserTool extends BrowserToolBase { 64 | /** 65 | * Execute the close browser tool 66 | */ 67 | async execute(args: any, context: ToolContext): Promise { 68 | if (context.browser) { 69 | try { 70 | // Check if browser is still connected 71 | if (context.browser.isConnected()) { 72 | await context.browser.close().catch(error => { 73 | console.error("Error while closing browser:", error); 74 | }); 75 | } else { 76 | console.error("Browser already disconnected, cleaning up state"); 77 | } 78 | } catch (error) { 79 | console.error("Error during browser close operation:", error); 80 | // Continue with resetting state even if close fails 81 | } finally { 82 | // Always reset the global browser and page references 83 | resetBrowserState(); 84 | } 85 | 86 | return createSuccessResponse("Browser closed successfully"); 87 | } 88 | 89 | return createSuccessResponse("No browser instance to close"); 90 | } 91 | } 92 | 93 | /** 94 | * Tool for navigating back in browser history 95 | */ 96 | export class GoBackTool extends BrowserToolBase { 97 | /** 98 | * Execute the go back tool 99 | */ 100 | async execute(args: any, context: ToolContext): Promise { 101 | return this.safeExecute(context, async (page) => { 102 | await page.goBack(); 103 | return createSuccessResponse("Navigated back in browser history"); 104 | }); 105 | } 106 | } 107 | 108 | /** 109 | * Tool for navigating forward in browser history 110 | */ 111 | export class GoForwardTool extends BrowserToolBase { 112 | /** 113 | * Execute the go forward tool 114 | */ 115 | async execute(args: any, context: ToolContext): Promise { 116 | return this.safeExecute(context, async (page) => { 117 | await page.goForward(); 118 | return createSuccessResponse("Navigated forward in browser history"); 119 | }); 120 | } 121 | } -------------------------------------------------------------------------------- /src/tools/browser/output.ts: -------------------------------------------------------------------------------- 1 | import { BrowserToolBase } from './base.js'; 2 | import { ToolContext, ToolResponse, createSuccessResponse, createErrorResponse } from '../common/types.js'; 3 | import * as path from 'path'; 4 | 5 | /** 6 | * Tool for saving page as PDF 7 | */ 8 | export class SaveAsPdfTool extends BrowserToolBase { 9 | /** 10 | * Execute the save as PDF tool 11 | */ 12 | async execute(args: any, context: ToolContext): Promise { 13 | return this.safeExecute(context, async (page) => { 14 | const filename = args.filename || 'page.pdf'; 15 | const options = { 16 | path: path.resolve(args.outputPath || '.', filename), 17 | format: args.format || 'A4', 18 | printBackground: args.printBackground !== false, 19 | margin: args.margin || { 20 | top: '1cm', 21 | right: '1cm', 22 | bottom: '1cm', 23 | left: '1cm' 24 | } 25 | }; 26 | 27 | await page.pdf(options); 28 | return createSuccessResponse(`Saved page as PDF: ${options.path}`); 29 | }); 30 | } 31 | } -------------------------------------------------------------------------------- /src/tools/browser/response.ts: -------------------------------------------------------------------------------- 1 | import type { Response } from 'playwright'; 2 | import { BrowserToolBase } from './base.js'; 3 | import type { ToolContext, ToolResponse } from '../common/types.js'; 4 | import { createSuccessResponse, createErrorResponse } from '../common/types.js'; 5 | 6 | const responsePromises = new Map>(); 7 | 8 | interface ExpectResponseArgs { 9 | id: string; 10 | url: string; 11 | } 12 | 13 | interface AssertResponseArgs { 14 | id: string; 15 | value?: string; 16 | } 17 | 18 | /** 19 | * Tool for setting up response wait operations 20 | */ 21 | export class ExpectResponseTool extends BrowserToolBase { 22 | /** 23 | * Execute the expect response tool 24 | */ 25 | async execute(args: ExpectResponseArgs, context: ToolContext): Promise { 26 | return this.safeExecute(context, async (page) => { 27 | if (!args.id || !args.url) { 28 | return createErrorResponse("Missing required parameters: id and url must be provided"); 29 | } 30 | 31 | const responsePromise = page.waitForResponse(args.url); 32 | responsePromises.set(args.id, responsePromise); 33 | 34 | return createSuccessResponse(`Started waiting for response with ID ${args.id}`); 35 | }); 36 | } 37 | } 38 | 39 | /** 40 | * Tool for asserting and validating responses 41 | */ 42 | export class AssertResponseTool extends BrowserToolBase { 43 | /** 44 | * Execute the assert response tool 45 | */ 46 | async execute(args: AssertResponseArgs, context: ToolContext): Promise { 47 | return this.safeExecute(context, async () => { 48 | if (!args.id) { 49 | return createErrorResponse("Missing required parameter: id must be provided"); 50 | } 51 | 52 | const responsePromise = responsePromises.get(args.id); 53 | if (!responsePromise) { 54 | return createErrorResponse(`No response wait operation found with ID: ${args.id}`); 55 | } 56 | 57 | try { 58 | const response = await responsePromise; 59 | const body = await response.json(); 60 | 61 | if (args.value) { 62 | const bodyStr = JSON.stringify(body); 63 | if (!bodyStr.includes(args.value)) { 64 | const messages = [ 65 | `Response body does not contain expected value: ${args.value}`, 66 | `Actual body: ${bodyStr}` 67 | ]; 68 | return createErrorResponse(messages.join('\n')); 69 | } 70 | } 71 | 72 | const messages = [ 73 | `Response assertion for ID ${args.id} successful`, 74 | `URL: ${response.url()}`, 75 | `Status: ${response.status()}`, 76 | `Body: ${JSON.stringify(body, null, 2)}` 77 | ]; 78 | return createSuccessResponse(messages.join('\n')); 79 | } catch (error) { 80 | return createErrorResponse(`Failed to assert response: ${(error as Error).message}`); 81 | } finally { 82 | responsePromises.delete(args.id); 83 | } 84 | }); 85 | } 86 | } -------------------------------------------------------------------------------- /src/tools/browser/screenshot.ts: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | import * as os from 'node:os'; 4 | import type { Page } from 'playwright'; 5 | import { BrowserToolBase } from './base.js'; 6 | import { ToolContext, ToolResponse, createSuccessResponse } from '../common/types.js'; 7 | 8 | const defaultDownloadsPath = path.join(os.homedir(), 'Downloads'); 9 | 10 | /** 11 | * Tool for taking screenshots of pages or elements 12 | */ 13 | export class ScreenshotTool extends BrowserToolBase { 14 | private screenshots = new Map(); 15 | 16 | /** 17 | * Execute the screenshot tool 18 | */ 19 | async execute(args: any, context: ToolContext): Promise { 20 | return this.safeExecute(context, async (page) => { 21 | const screenshotOptions: any = { 22 | type: args.type || "png", 23 | fullPage: !!args.fullPage 24 | }; 25 | 26 | if (args.selector) { 27 | const element = await page.$(args.selector); 28 | if (!element) { 29 | return { 30 | content: [{ 31 | type: "text", 32 | text: `Element not found: ${args.selector}`, 33 | }], 34 | isError: true 35 | }; 36 | } 37 | screenshotOptions.element = element; 38 | } 39 | 40 | // Generate output path 41 | const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); 42 | const filename = `${args.name || 'screenshot'}-${timestamp}.png`; 43 | const downloadsDir = args.downloadsDir || defaultDownloadsPath; 44 | 45 | if (!fs.existsSync(downloadsDir)) { 46 | fs.mkdirSync(downloadsDir, { recursive: true }); 47 | } 48 | 49 | const outputPath = path.join(downloadsDir, filename); 50 | screenshotOptions.path = outputPath; 51 | 52 | const screenshot = await page.screenshot(screenshotOptions); 53 | const base64Screenshot = screenshot.toString('base64'); 54 | 55 | const messages = [`Screenshot saved to: ${path.relative(process.cwd(), outputPath)}`]; 56 | 57 | // Handle base64 storage 58 | if (args.storeBase64 !== false) { 59 | this.screenshots.set(args.name || 'screenshot', base64Screenshot); 60 | this.server.notification({ 61 | method: "notifications/resources/list_changed", 62 | }); 63 | 64 | messages.push(`Screenshot also stored in memory with name: '${args.name || 'screenshot'}'`); 65 | } 66 | 67 | return createSuccessResponse(messages); 68 | }); 69 | } 70 | 71 | /** 72 | * Get all stored screenshots 73 | */ 74 | getScreenshots(): Map { 75 | return this.screenshots; 76 | } 77 | } -------------------------------------------------------------------------------- /src/tools/browser/useragent.ts: -------------------------------------------------------------------------------- 1 | import { BrowserToolBase } from './base.js'; 2 | import type { ToolContext, ToolResponse } from '../common/types.js'; 3 | import { createSuccessResponse, createErrorResponse } from '../common/types.js'; 4 | 5 | interface CustomUserAgentArgs { 6 | userAgent: string; 7 | } 8 | 9 | /** 10 | * Tool for validating custom User Agent settings 11 | */ 12 | export class CustomUserAgentTool extends BrowserToolBase { 13 | /** 14 | * Execute the custom user agent tool 15 | */ 16 | async execute(args: CustomUserAgentArgs, context: ToolContext): Promise { 17 | return this.safeExecute(context, async (page) => { 18 | if (!args.userAgent) { 19 | return createErrorResponse("Missing required parameter: userAgent must be provided"); 20 | } 21 | 22 | try { 23 | const currentUserAgent = await page.evaluate(() => navigator.userAgent); 24 | 25 | if (currentUserAgent !== args.userAgent) { 26 | const messages = [ 27 | "Page was already initialized with a different User Agent.", 28 | `Requested: ${args.userAgent}`, 29 | `Current: ${currentUserAgent}` 30 | ]; 31 | return createErrorResponse(messages.join('\n')); 32 | } 33 | 34 | return createSuccessResponse("User Agent validation successful"); 35 | } catch (error) { 36 | return createErrorResponse(`Failed to validate User Agent: ${(error as Error).message}`); 37 | } 38 | }); 39 | } 40 | } -------------------------------------------------------------------------------- /src/tools/browser/visiblePage.ts: -------------------------------------------------------------------------------- 1 | import { resetBrowserState } from "../../toolHandler.js"; 2 | import { ToolContext, ToolResponse, createErrorResponse, createSuccessResponse } from "../common/types.js"; 3 | import { BrowserToolBase } from "./base.js"; 4 | 5 | /** 6 | * Tool for getting the visible text content of the current page 7 | */ 8 | export class VisibleTextTool extends BrowserToolBase { 9 | /** 10 | * Execute the visible text page tool 11 | */ 12 | async execute(args: any, context: ToolContext): Promise { 13 | // Check if browser is available 14 | if (!context.browser || !context.browser.isConnected()) { 15 | // If browser is not connected, we need to reset the state to force recreation 16 | resetBrowserState(); 17 | return createErrorResponse( 18 | "Browser is not connected. The connection has been reset - please retry your navigation." 19 | ); 20 | } 21 | 22 | // Check if page is available and not closed 23 | if (!context.page || context.page.isClosed()) { 24 | return createErrorResponse( 25 | "Page is not available or has been closed. Please retry your navigation." 26 | ); 27 | } 28 | return this.safeExecute(context, async (page) => { 29 | try { 30 | const visibleText = await page!.evaluate(() => { 31 | const walker = document.createTreeWalker( 32 | document.body, 33 | NodeFilter.SHOW_TEXT, 34 | { 35 | acceptNode: (node) => { 36 | const style = window.getComputedStyle(node.parentElement!); 37 | return (style.display !== "none" && style.visibility !== "hidden") 38 | ? NodeFilter.FILTER_ACCEPT 39 | : NodeFilter.FILTER_REJECT; 40 | }, 41 | } 42 | ); 43 | let text = ""; 44 | let node; 45 | while ((node = walker.nextNode())) { 46 | const trimmedText = node.textContent?.trim(); 47 | if (trimmedText) { 48 | text += trimmedText + "\n"; 49 | } 50 | } 51 | return text.trim(); 52 | }); 53 | return createSuccessResponse(`Visible text content:\n${visibleText}`); 54 | } catch (error) { 55 | return createErrorResponse(`Failed to get visible text content: ${(error as Error).message}`); 56 | } 57 | }); 58 | } 59 | } 60 | 61 | /** 62 | * Tool for getting the visible HTML content of the current page 63 | */ 64 | export class VisibleHtmlTool extends BrowserToolBase { 65 | /** 66 | * Execute the visible HTML page tool 67 | */ 68 | async execute(args: any, context: ToolContext): Promise { 69 | // Check if browser is available 70 | if (!context.browser || !context.browser.isConnected()) { 71 | // If browser is not connected, we need to reset the state to force recreation 72 | resetBrowserState(); 73 | return createErrorResponse( 74 | "Browser is not connected. The connection has been reset - please retry your navigation." 75 | ); 76 | } 77 | 78 | // Check if page is available and not closed 79 | if (!context.page || context.page.isClosed()) { 80 | return createErrorResponse( 81 | "Page is not available or has been closed. Please retry your navigation." 82 | ); 83 | } 84 | return this.safeExecute(context, async (page) => { 85 | try { 86 | const htmlContent = await page!.content(); 87 | return createSuccessResponse(`HTML content:\n${htmlContent}`); 88 | } catch (error) { 89 | return createErrorResponse(`Failed to get visible HTML content: ${(error as Error).message}`); 90 | } 91 | }); 92 | } 93 | } -------------------------------------------------------------------------------- /src/tools/codegen/generator.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { CodegenAction, CodegenOptions, CodegenResult, CodegenSession, PlaywrightTestCase } from './types.js'; 3 | 4 | export class PlaywrightGenerator { 5 | private static readonly DEFAULT_OPTIONS: Required = { 6 | outputPath: 'tests', 7 | testNamePrefix: 'MCP', 8 | includeComments: true, 9 | }; 10 | 11 | private options: Required; 12 | 13 | constructor(options: CodegenOptions = {}) { 14 | this.validateOptions(options); 15 | this.options = { ...PlaywrightGenerator.DEFAULT_OPTIONS, ...options }; 16 | } 17 | 18 | private validateOptions(options: CodegenOptions): void { 19 | if (options.outputPath && typeof options.outputPath !== 'string') { 20 | throw new Error('outputPath must be a string'); 21 | } 22 | if (options.testNamePrefix && typeof options.testNamePrefix !== 'string') { 23 | throw new Error('testNamePrefix must be a string'); 24 | } 25 | if (options.includeComments !== undefined && typeof options.includeComments !== 'boolean') { 26 | throw new Error('includeComments must be a boolean'); 27 | } 28 | } 29 | 30 | async generateTest(session: CodegenSession): Promise { 31 | if (!session || !Array.isArray(session.actions)) { 32 | throw new Error('Invalid session data'); 33 | } 34 | 35 | const testCase = this.createTestCase(session); 36 | const testCode = this.generateTestCode(testCase); 37 | const filePath = this.getOutputFilePath(session); 38 | 39 | return { 40 | testCode, 41 | filePath, 42 | sessionId: session.id, 43 | }; 44 | } 45 | 46 | private createTestCase(session: CodegenSession): PlaywrightTestCase { 47 | const testCase: PlaywrightTestCase = { 48 | name: `${this.options.testNamePrefix}_${new Date(session.startTime).toISOString().split('T')[0]}`, 49 | steps: [], 50 | imports: new Set(['test', 'expect']), 51 | }; 52 | 53 | for (const action of session.actions) { 54 | const step = this.convertActionToStep(action); 55 | if (step) { 56 | testCase.steps.push(step); 57 | } 58 | } 59 | 60 | return testCase; 61 | } 62 | 63 | private convertActionToStep(action: CodegenAction): string | null { 64 | const { toolName, parameters } = action; 65 | 66 | switch (toolName) { 67 | case 'playwright_navigate': 68 | return this.generateNavigateStep(parameters); 69 | case 'playwright_fill': 70 | return this.generateFillStep(parameters); 71 | case 'playwright_click': 72 | return this.generateClickStep(parameters); 73 | case 'playwright_screenshot': 74 | return this.generateScreenshotStep(parameters); 75 | case 'playwright_expect_response': 76 | return this.generateExpectResponseStep(parameters); 77 | case 'playwright_assert_response': 78 | return this.generateAssertResponseStep(parameters); 79 | case 'playwright_hover': 80 | return this.generateHoverStep(parameters); 81 | case 'playwright_select': 82 | return this.generateSelectStep(parameters); 83 | case 'playwright_custom_user_agent': 84 | return this.generateCustomUserAgentStep(parameters); 85 | default: 86 | console.warn(`Unsupported tool: ${toolName}`); 87 | return null; 88 | } 89 | } 90 | 91 | private generateNavigateStep(parameters: Record): string { 92 | const { url, waitUntil } = parameters; 93 | const options = waitUntil ? `, { waitUntil: '${waitUntil}' }` : ''; 94 | return ` 95 | // Navigate to URL 96 | await page.goto('${url}'${options});`; 97 | } 98 | 99 | private generateFillStep(parameters: Record): string { 100 | const { selector, value } = parameters; 101 | return ` 102 | // Fill input field 103 | await page.fill('${selector}', '${value}');`; 104 | } 105 | 106 | private generateClickStep(parameters: Record): string { 107 | const { selector } = parameters; 108 | return ` 109 | // Click element 110 | await page.click('${selector}');`; 111 | } 112 | 113 | private generateScreenshotStep(parameters: Record): string { 114 | const { name, fullPage = false, path } = parameters; 115 | const options = []; 116 | if (fullPage) options.push('fullPage: true'); 117 | if (path) options.push(`path: '${path}'`); 118 | 119 | const optionsStr = options.length > 0 ? `, { ${options.join(', ')} }` : ''; 120 | return ` 121 | // Take screenshot 122 | await page.screenshot({ path: '${name}.png'${optionsStr} });`; 123 | } 124 | 125 | private generateExpectResponseStep(parameters: Record): string { 126 | const { url, id } = parameters; 127 | return ` 128 | // Wait for response 129 | const ${id}Response = page.waitForResponse('${url}');`; 130 | } 131 | 132 | private generateAssertResponseStep(parameters: Record): string { 133 | const { id, value } = parameters; 134 | const assertion = value 135 | ? `\n const responseText = await ${id}Response.text();\n expect(responseText).toContain('${value}');` 136 | : `\n expect(${id}Response.ok()).toBeTruthy();`; 137 | return ` 138 | // Assert response${assertion}`; 139 | } 140 | 141 | private generateHoverStep(parameters: Record): string { 142 | const { selector } = parameters; 143 | return ` 144 | // Hover over element 145 | await page.hover('${selector}');`; 146 | } 147 | 148 | private generateSelectStep(parameters: Record): string { 149 | const { selector, value } = parameters; 150 | return ` 151 | // Select option 152 | await page.selectOption('${selector}', '${value}');`; 153 | } 154 | 155 | private generateCustomUserAgentStep(parameters: Record): string { 156 | const { userAgent } = parameters; 157 | return ` 158 | // Set custom user agent 159 | await context.setUserAgent('${userAgent}');`; 160 | } 161 | 162 | private generateTestCode(testCase: PlaywrightTestCase): string { 163 | const imports = Array.from(testCase.imports) 164 | .map(imp => `import { ${imp} } from '@playwright/test';`) 165 | .join('\n'); 166 | 167 | return ` 168 | ${imports} 169 | 170 | test('${testCase.name}', async ({ page, context }) => { 171 | ${testCase.steps.join('\n')} 172 | });`; 173 | } 174 | 175 | private getOutputFilePath(session: CodegenSession): string { 176 | if (!session.id) { 177 | throw new Error('Session ID is required'); 178 | } 179 | 180 | const sanitizedPrefix = this.options.testNamePrefix.toLowerCase().replace(/[^a-z0-9_]/g, '_'); 181 | const fileName = `${sanitizedPrefix}_${session.id}.spec.ts`; 182 | return path.resolve(this.options.outputPath, fileName); 183 | } 184 | } -------------------------------------------------------------------------------- /src/tools/codegen/index.ts: -------------------------------------------------------------------------------- 1 | import { Tool } from '../../types.js'; 2 | import { ActionRecorder } from './recorder.js'; 3 | import { PlaywrightGenerator } from './generator.js'; 4 | import { CodegenOptions } from './types.js'; 5 | import * as fs from 'fs/promises'; 6 | import * as path from 'path'; 7 | import type { Browser, Page } from 'playwright'; 8 | 9 | declare global { 10 | var browser: Browser | undefined; 11 | var page: Page | undefined; 12 | } 13 | 14 | // Helper function to get workspace root path 15 | const getWorkspaceRoot = () => { 16 | return process.cwd(); 17 | }; 18 | 19 | const DEFAULT_OPTIONS: Required = { 20 | outputPath: path.join(getWorkspaceRoot(), 'e2e'), 21 | testNamePrefix: 'Test', 22 | includeComments: true 23 | }; 24 | 25 | export const startCodegenSession: Tool = { 26 | name: 'start_codegen_session', 27 | description: 'Start a new code generation session to record MCP tool actions', 28 | parameters: { 29 | type: 'object', 30 | properties: { 31 | options: { 32 | type: 'object', 33 | description: 'Code generation options', 34 | properties: { 35 | outputPath: { type: 'string' }, 36 | testNamePrefix: { type: 'string' }, 37 | includeComments: { type: 'boolean' } 38 | } 39 | } 40 | } 41 | }, 42 | handler: async ({ options = {} }: { options?: CodegenOptions }) => { 43 | try { 44 | // Merge provided options with defaults 45 | const mergedOptions = { ...DEFAULT_OPTIONS, ...options }; 46 | 47 | // Ensure output path is absolute and normalized 48 | const workspaceRoot = getWorkspaceRoot(); 49 | const outputPath = path.isAbsolute(mergedOptions.outputPath) 50 | ? mergedOptions.outputPath 51 | : path.join(workspaceRoot, mergedOptions.outputPath); 52 | 53 | mergedOptions.outputPath = outputPath; 54 | 55 | // Ensure output directory exists 56 | try { 57 | await fs.mkdir(outputPath, { recursive: true }); 58 | } catch (mkdirError: any) { 59 | throw new Error(`Failed to create output directory: ${mkdirError.message}`); 60 | } 61 | 62 | const sessionId = ActionRecorder.getInstance().startSession(); 63 | 64 | // Store options with the session 65 | const recorder = ActionRecorder.getInstance(); 66 | const session = recorder.getSession(sessionId); 67 | if (session) { 68 | session.options = mergedOptions; 69 | } 70 | 71 | return { 72 | sessionId, 73 | options: mergedOptions, 74 | message: `Started codegen session. Tests will be generated in: ${outputPath}` 75 | }; 76 | } catch (error: any) { 77 | throw new Error(`Failed to start codegen session: ${error.message}`); 78 | } 79 | } 80 | }; 81 | 82 | export const endCodegenSession: Tool = { 83 | name: 'end_codegen_session', 84 | description: 'End the current code generation session and generate Playwright test', 85 | parameters: { 86 | type: 'object', 87 | properties: { 88 | sessionId: { 89 | type: 'string', 90 | description: 'ID of the session to end' 91 | } 92 | }, 93 | required: ['sessionId'] 94 | }, 95 | handler: async ({ sessionId }: { sessionId: string }) => { 96 | try { 97 | const recorder = ActionRecorder.getInstance(); 98 | const session = recorder.endSession(sessionId); 99 | 100 | if (!session) { 101 | throw new Error(`Session ${sessionId} not found`); 102 | } 103 | 104 | if (!session.options) { 105 | throw new Error(`Session ${sessionId} has no options configured`); 106 | } 107 | 108 | const generator = new PlaywrightGenerator(session.options); 109 | const result = await generator.generateTest(session); 110 | 111 | // Double check output directory exists 112 | const outputDir = path.dirname(result.filePath); 113 | await fs.mkdir(outputDir, { recursive: true }); 114 | 115 | // Write test file 116 | try { 117 | await fs.writeFile(result.filePath, result.testCode, 'utf-8'); 118 | } catch (writeError: any) { 119 | throw new Error(`Failed to write test file: ${writeError.message}`); 120 | } 121 | 122 | // Close Playwright browser and cleanup 123 | try { 124 | if (global.browser?.isConnected()) { 125 | await global.browser.close(); 126 | } 127 | } catch (browserError: any) { 128 | console.warn('Failed to close browser:', browserError.message); 129 | } finally { 130 | global.browser = undefined; 131 | global.page = undefined; 132 | } 133 | 134 | const absolutePath = path.resolve(result.filePath); 135 | 136 | return { 137 | filePath: absolutePath, 138 | outputDirectory: outputDir, 139 | testCode: result.testCode, 140 | message: `Generated test file at: ${absolutePath}\nOutput directory: ${outputDir}` 141 | }; 142 | } catch (error: any) { 143 | // Ensure browser cleanup even on error 144 | try { 145 | if (global.browser?.isConnected()) { 146 | await global.browser.close(); 147 | } 148 | } catch { 149 | // Ignore cleanup errors 150 | } finally { 151 | global.browser = undefined; 152 | global.page = undefined; 153 | } 154 | 155 | throw new Error(`Failed to end codegen session: ${error.message}`); 156 | } 157 | } 158 | }; 159 | 160 | export const getCodegenSession: Tool = { 161 | name: 'get_codegen_session', 162 | description: 'Get information about a code generation session', 163 | parameters: { 164 | type: 'object', 165 | properties: { 166 | sessionId: { 167 | type: 'string', 168 | description: 'ID of the session to retrieve' 169 | } 170 | }, 171 | required: ['sessionId'] 172 | }, 173 | handler: async ({ sessionId }: { sessionId: string }) => { 174 | const session = ActionRecorder.getInstance().getSession(sessionId); 175 | if (!session) { 176 | throw new Error(`Session ${sessionId} not found`); 177 | } 178 | return session; 179 | } 180 | }; 181 | 182 | export const clearCodegenSession: Tool = { 183 | name: 'clear_codegen_session', 184 | description: 'Clear a code generation session', 185 | parameters: { 186 | type: 'object', 187 | properties: { 188 | sessionId: { 189 | type: 'string', 190 | description: 'ID of the session to clear' 191 | } 192 | }, 193 | required: ['sessionId'] 194 | }, 195 | handler: async ({ sessionId }: { sessionId: string }) => { 196 | const success = ActionRecorder.getInstance().clearSession(sessionId); 197 | if (!success) { 198 | throw new Error(`Session ${sessionId} not found`); 199 | } 200 | return { success }; 201 | } 202 | }; 203 | 204 | export const codegenTools = [ 205 | startCodegenSession, 206 | endCodegenSession, 207 | getCodegenSession, 208 | clearCodegenSession 209 | ]; -------------------------------------------------------------------------------- /src/tools/codegen/recorder.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid'; 2 | import { CodegenAction, CodegenSession } from './types'; 3 | 4 | export class ActionRecorder { 5 | private static instance: ActionRecorder; 6 | private sessions: Map; 7 | private activeSession: string | null; 8 | 9 | private constructor() { 10 | this.sessions = new Map(); 11 | this.activeSession = null; 12 | } 13 | 14 | static getInstance(): ActionRecorder { 15 | if (!ActionRecorder.instance) { 16 | ActionRecorder.instance = new ActionRecorder(); 17 | } 18 | return ActionRecorder.instance; 19 | } 20 | 21 | startSession(): string { 22 | const sessionId = uuidv4(); 23 | this.sessions.set(sessionId, { 24 | id: sessionId, 25 | actions: [], 26 | startTime: Date.now(), 27 | }); 28 | this.activeSession = sessionId; 29 | return sessionId; 30 | } 31 | 32 | endSession(sessionId: string): CodegenSession | null { 33 | const session = this.sessions.get(sessionId); 34 | if (session) { 35 | session.endTime = Date.now(); 36 | if (this.activeSession === sessionId) { 37 | this.activeSession = null; 38 | } 39 | return session; 40 | } 41 | return null; 42 | } 43 | 44 | recordAction(toolName: string, parameters: Record, result?: unknown): void { 45 | if (!this.activeSession) { 46 | return; 47 | } 48 | 49 | const session = this.sessions.get(this.activeSession); 50 | if (!session) { 51 | return; 52 | } 53 | 54 | const action: CodegenAction = { 55 | toolName, 56 | parameters, 57 | timestamp: Date.now(), 58 | result, 59 | }; 60 | 61 | session.actions.push(action); 62 | } 63 | 64 | getSession(sessionId: string): CodegenSession | null { 65 | return this.sessions.get(sessionId) || null; 66 | } 67 | 68 | getActiveSession(): CodegenSession | null { 69 | return this.activeSession ? this.sessions.get(this.activeSession) : null; 70 | } 71 | 72 | clearSession(sessionId: string): boolean { 73 | if (this.activeSession === sessionId) { 74 | this.activeSession = null; 75 | } 76 | return this.sessions.delete(sessionId); 77 | } 78 | } -------------------------------------------------------------------------------- /src/tools/codegen/types.ts: -------------------------------------------------------------------------------- 1 | import { ToolCall } from '../../types.js'; 2 | 3 | export interface CodegenAction { 4 | toolName: string; 5 | parameters: Record; 6 | timestamp: number; 7 | result?: unknown; 8 | } 9 | 10 | export interface CodegenSession { 11 | id: string; 12 | actions: CodegenAction[]; 13 | startTime: number; 14 | endTime?: number; 15 | options?: CodegenOptions; 16 | } 17 | 18 | export interface PlaywrightTestCase { 19 | name: string; 20 | steps: string[]; 21 | imports: Set; 22 | } 23 | 24 | export interface CodegenOptions { 25 | outputPath?: string; 26 | testNamePrefix?: string; 27 | includeComments?: boolean; 28 | } 29 | 30 | export interface CodegenResult { 31 | testCode: string; 32 | filePath: string; 33 | sessionId: string; 34 | } -------------------------------------------------------------------------------- /src/tools/common/types.ts: -------------------------------------------------------------------------------- 1 | import type { CallToolResult, TextContent, ImageContent } from '@modelcontextprotocol/sdk/types.js'; 2 | import type { Page, Browser, APIRequestContext } from 'playwright'; 3 | 4 | // Context for tool execution 5 | export interface ToolContext { 6 | page?: Page; 7 | browser?: Browser; 8 | apiContext?: APIRequestContext; 9 | server?: any; 10 | } 11 | 12 | // Standard response format for all tools 13 | export interface ToolResponse extends CallToolResult { 14 | content: (TextContent | ImageContent)[]; 15 | isError: boolean; 16 | } 17 | 18 | // Interface that all tool implementations must follow 19 | export interface ToolHandler { 20 | execute(args: any, context: ToolContext): Promise; 21 | } 22 | 23 | // Helper functions for creating responses 24 | export function createErrorResponse(message: string): ToolResponse { 25 | return { 26 | content: [{ 27 | type: "text", 28 | text: message 29 | }], 30 | isError: true 31 | }; 32 | } 33 | 34 | export function createSuccessResponse(message: string | string[]): ToolResponse { 35 | const messages = Array.isArray(message) ? message : [message]; 36 | return { 37 | content: messages.map(msg => ({ 38 | type: "text", 39 | text: msg 40 | })), 41 | isError: false 42 | }; 43 | } -------------------------------------------------------------------------------- /src/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common/types.js'; 2 | export * from './browser/index.js'; 3 | export * from './api/index.js'; 4 | 5 | // Tool type constants 6 | export const BROWSER_TOOLS = [ 7 | "playwright_navigate", 8 | "playwright_screenshot", 9 | "playwright_click", 10 | "playwright_iframe_click", 11 | "playwright_iframe_fill", 12 | "playwright_fill", 13 | "playwright_select", 14 | "playwright_hover", 15 | "playwright_evaluate", 16 | "playwright_console_logs", 17 | "playwright_close", 18 | "playwright_get_visible_text", 19 | "playwright_get_visible_html" 20 | ]; 21 | 22 | export const API_TOOLS = [ 23 | "playwright_get", 24 | "playwright_post", 25 | "playwright_put", 26 | "playwright_patch", 27 | "playwright_delete" 28 | ]; -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'; 2 | 3 | export interface Tool { 4 | name: string; 5 | description: string; 6 | parameters: { 7 | type: string; 8 | properties: Record; 9 | required?: string[]; 10 | }; 11 | handler: (args: any) => Promise; 12 | } 13 | 14 | export interface ToolCall { 15 | name: string; 16 | parameters: Record; 17 | result?: CallToolResult; 18 | } -------------------------------------------------------------------------------- /test-import.js: -------------------------------------------------------------------------------- 1 | // test-import.js 2 | import { setupRequestHandlers } from './dist/requestHandler.js'; 3 | import { handleToolCall } from './dist/toolHandler.js'; 4 | 5 | console.log('Imports successful!'); 6 | console.log('setupRequestHandlers:', typeof setupRequestHandlers); 7 | console.log('handleToolCall:', typeof handleToolCall); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "module": "ES2022", 5 | "moduleResolution": "bundler", 6 | "outDir": "./dist", 7 | "strict": true, 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "declaration": true, 11 | "noImplicitAny": false, 12 | "strictNullChecks": false, 13 | "allowJs": true, 14 | "resolveJsonModule": true 15 | }, 16 | "include": ["src/**/*"], 17 | "exclude": ["node_modules", "dist", "src/__tests__"] 18 | } -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitAny": false, 5 | "strictNullChecks": false, 6 | "types": ["jest", "node"] 7 | }, 8 | "include": ["src/**/*", "test/**/*", "src/__tests__/**/*"] 9 | } --------------------------------------------------------------------------------