[
  {
    "path": ".devcontainer/devcontainer.json",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json\",\n  \"name\": \"Playwright\",\n  \"image\": \"mcr.microsoft.com/playwright:v1.58.2-noble\",\n  \"privileged\": true,\n  \"init\": true,\n  \"remoteUser\": \"pwuser\",\n  \"features\": {\n    \"ghcr.io/devcontainers/features/desktop-lite:1\": {},\n    \"ghcr.io/devcontainers/features/github-cli:1\": {},\n    \"ghcr.io/devcontainers/features/docker-outside-of-docker:1\": {}\n  },\n  \"forwardPorts\": [\n    6080\n  ],\n  \"portsAttributes\": {\n    \"6080\": {\n      \"label\": \"noVNC\"\n    }\n  }\n}"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\nenv:\n  PWMCP_DEBUG: '1'\n  PWDEBUGIMPL: '1'\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n    - name: Use Node.js 20\n      uses: actions/setup-node@v4\n      with:\n        node-version: '20'\n        cache: 'npm'\n    - run: npm ci\n    - run: npm run lint\n    - name: Ensure no changes\n      run: git diff --exit-code\n\n  test:\n    strategy:\n      fail-fast: false\n      matrix:\n        os: [ubuntu-latest, macos-15, windows-latest]\n    runs-on: ${{ matrix.os }}\n    steps:\n    - uses: actions/checkout@v4\n    - name: Use Node.js 20\n      uses: actions/setup-node@v4\n      with:\n        node-version: '20'\n        cache: 'npm'\n    - name: Install dependencies\n      run: npm ci\n    - name: Playwright install\n      run: npx playwright install --with-deps\n    - name: Build\n      run: npm run build\n    - name: Run playwright-mcp tests\n      id: test-mcp\n      run: npm run test --workspace=packages/playwright-mcp\n      continue-on-error: true\n    - name: Run extension tests\n      id: test-extension\n      if: matrix.os == 'macos-15'\n      run: npm run test --workspace=packages/extension\n      continue-on-error: true\n    - name: Check test results\n      if: steps.test-mcp.outcome == 'failure' || steps.test-extension.outcome == 'failure'\n      run: exit 1\n\n  test_mcp_docker:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v4\n    - name: Use Node.js 20\n      uses: actions/setup-node@v4\n      with:\n        node-version: '20'\n        cache: 'npm'\n    - name: Install dependencies\n      run: npm ci\n    - name: Playwright install\n      run: npx playwright install --with-deps chromium\n    - name: Set up Docker Buildx\n      uses: docker/setup-buildx-action@v3\n    - name: Build and push\n      uses: docker/build-push-action@v6\n      with:\n        tags: playwright-mcp-dev:latest\n        cache-from: type=gha\n        cache-to: type=gha,mode=max\n        load: true\n    - name: Run tests\n      shell: bash\n      run: |\n        # Used for the Docker tests to share the test-results folder with the container.\n        umask 0000\n        npm run test -- --project=chromium-docker\n      working-directory: ./packages/playwright-mcp\n      env:\n        MCP_IN_DOCKER: 1\n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "content": "name: Publish\non:\n  workflow_dispatch:\n  schedule:\n    - cron: '0 8 * * *'\n  release:\n    types: [published]\n\njobs:\n  publish-mcp-canary-npm:\n    if: github.event.schedule || github.event_name == 'workflow_dispatch'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      id-token: write  # Required for OIDC npm publishing\n    steps:\n      - uses: actions/checkout@v5\n      - uses: actions/setup-node@v5\n        with:\n          node-version: 20\n          registry-url: https://registry.npmjs.org/\n      # Ensure npm 11.5.1 or later is installed (for OIDC npm publishing)\n      - name: Update npm\n        run: npm install -g npm@latest\n\n      - name: Get current date\n        id: date\n        run: echo \"date=$(date +'%Y-%m-%d')\" >> $GITHUB_OUTPUT\n\n      - name: Get current version\n        id: version\n        run: echo \"version=$(node -p \"require('./package.json').version\")\" >> $GITHUB_OUTPUT\n\n      - name: Set canary version\n        id: canary-version\n        run: echo \"version=${{ steps.version.outputs.version }}-alpha-${{ steps.date.outputs.date }}\" >> $GITHUB_OUTPUT\n\n      - name: Update package.json version\n        run: |\n          npm version ${{ steps.canary-version.outputs.version }} --no-git-tag-version\n        working-directory: ./packages/playwright-mcp\n\n      - run: npm ci\n      - run: npx playwright install --with-deps\n      - run: npm run lint\n      - run: npm run ctest\n        working-directory: ./packages/playwright-mcp\n\n      - name: Publish to npm with next tag\n        run: npm publish --tag next\n        working-directory: ./packages/playwright-mcp\n\n  publish-mcp-release-npm:\n    if: github.event_name == 'release'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      id-token: write  # Required for OIDC npm publishing\n    steps:\n      - uses: actions/checkout@v5\n      - uses: actions/setup-node@v5\n        with:\n          node-version: 20\n          registry-url: https://registry.npmjs.org/\n      # Ensure npm 11.5.1 or later is installed (for OIDC npm publishing)\n      - name: Update npm\n        run: npm install -g npm@latest\n      - run: npm ci\n      - run: npx playwright install --with-deps\n      - run: npm run lint\n      - run: npm run ctest\n        working-directory: ./packages/playwright-mcp\n      - run: npm publish\n        working-directory: ./packages/playwright-mcp\n\n  publish-mcp-release-docker:\n    if: github.event_name == 'release'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      id-token: write # Needed for OIDC login to Azure\n    environment: allow-publishing-docker-to-acr\n    steps:\n      - uses: actions/checkout@v5\n      - name: Set up QEMU # Needed for multi-platform builds (e.g., arm64 on amd64 runner)\n        uses: docker/setup-qemu-action@v3\n      - name: Set up Docker Buildx # Needed for multi-platform builds\n        uses: docker/setup-buildx-action@v3\n      - name: Azure Login via OIDC\n        uses: azure/login@v2\n        with:\n          client-id: ${{ secrets.AZURE_DOCKER_CLIENT_ID }}\n          tenant-id: ${{ secrets.AZURE_DOCKER_TENANT_ID }}\n          subscription-id: ${{ secrets.AZURE_DOCKER_SUBSCRIPTION_ID }}\n      - name: Login to ACR\n        run: az acr login --name playwright\n      - name: Build and push Docker image\n        id: build-push\n        uses: docker/build-push-action@v6\n        with:\n          file: ./Dockerfile\n          platforms: linux/amd64,linux/arm64\n          push: true\n          tags: |\n            playwright.azurecr.io/public/playwright/mcp:${{ github.event.release.tag_name }}\n            playwright.azurecr.io/public/playwright/mcp:latest\n      - uses: oras-project/setup-oras@v1\n      - name: Set oras tags\n        run: |\n          attach_eol_manifest() {\n            local image=\"$1\"\n            local today=$(date -u +'%Y-%m-%d')\n            # oras is re-using Docker credentials, so we don't need to login.\n            # Following the advice in https://portal.microsofticm.com/imp/v3/incidents/incident/476783820/summary\n            oras attach --artifact-type application/vnd.microsoft.artifact.lifecycle --annotation \"vnd.microsoft.artifact.lifecycle.end-of-life.date=$today\" $image\n          }\n          # for each tag, attach the eol manifest\n          for tag in $(echo ${{ steps.build-push.outputs.metadata['image.name'] }} | tr ',' '\\n'); do\n            attach_eol_manifest $tag\n          done\n\n  package-release-extension:\n    if: github.event_name == 'release'\n    runs-on: ubuntu-latest\n    permissions:\n      contents: write # Needed to upload release assets\n    steps:\n      - uses: actions/checkout@v5\n      - uses: actions/setup-node@v5\n        with:\n          node-version: 20\n          cache: 'npm'\n      - name: Install extension dependencies\n        run: npm ci\n      - name: Build extension\n        working-directory: ./packages/extension\n        run: npm run build\n        env:\n          SET_EXTENSION_PUBLIC_KEY_IN_MANIFEST: 1\n      - name: Get extension version\n        id: get-version\n        working-directory: ./packages/extension\n        run: echo \"version=$(node -p \"require('./package.json').version\")\" >> $GITHUB_OUTPUT\n      - name: Package extension\n        working-directory: ./packages/extension\n        run: |\n          cd dist\n          zip -r ../playwright-mcp-extension-${{ steps.get-version.outputs.version }}.zip .\n          cd ..\n      - name: Upload extension to release\n        env:\n           GITHUB_TOKEN: ${{ github.token }}\n        run: |\n            gh release upload ${{github.event.release.tag_name}} ./packages/extension/playwright-mcp-extension-${{ steps.get-version.outputs.version }}.zip\n"
  },
  {
    "path": ".gitignore",
    "content": "node_modules/\ntest-results/\nplaywright-report/\n.vscode/mcp.json\n.idea\n.DS_Store\n.env\nsessions/\n"
  },
  {
    "path": "CLAUDE.md",
    "content": "## Commit Convention\n\nSemantic commit messages: `label(scope): description`\n\nLabels: `fix`, `feat`, `chore`, `docs`, `test`, `devops`\n\n```bash\ngit checkout -b fix-39562\n# ... make changes ...\ngit add <changed-files>\ngit commit -m \"$(cat <<'EOF'\nfix(proxy): handle SOCKS proxy authentication\n\nFixes: https://github.com/microsoft/playwright/issues/39562\nEOF\n)\"\ngit push origin fix-39562\ngh pr create --repo microsoft/playwright --head username:fix-39562 \\\n  --title \"fix(proxy): handle SOCKS proxy authentication\" \\\n  --body \"$(cat <<'EOF'\n## Summary\n- <describe the change very! briefly>\n\nFixes https://github.com/microsoft/playwright/issues/39562\nEOF\n)\"\n```\n\nNever add Co-Authored-By agents in commit message.\nBranch naming for issue fixes: `fix-<issue-number>`\n\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\n\n## Choose an issue\n\nPlaywright MCP **requires an issue** for every contribution, except for minor documentation updates.\n\nIf you are passionate about a bug/feature, but cannot find an issue describing it, **file an issue first**. This will\nfacilitate the discussion, and you might get some early feedback from project maintainers before spending your time on\ncreating a pull request.\n\n## Make a change\n\n> [!WARNING]\n> The core of the Playwright MCP was moved to the [Playwright monorepo](https://github.com/microsoft/playwright).\n\nClone the Playwright repository. If you plan to send a pull request, it might be better to [fork the repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo) first.\n\n\n```bash\ngit clone https://github.com/microsoft/playwright\ncd playwright\n```\n\nInstall dependencies and run the build in watch mode.\n```bash\n# install deps and run watch\nnpm ci\nnpm run watch\nnpx playwright install\n```\n\nSource code for Playwright MCP is located at [packages/playwright/src/mcp](https://github.com/microsoft/playwright/blob/main/packages/playwright/src/mcp).\n\n```bash\n# list source files\nls -la packages/playwright/src/mcp\n```\n\nCoding style is fully defined in [eslint.config.mjs](https://github.com/microsoft/playwright/blob/main/eslint.config.mjs). Before creating a pull request, or at any moment during development, run linter to check all kinds of things:\n```bash\n# lint the source base before sending PR\nnpm run flint\n```\n\nComments should have an explicit purpose and should improve readability rather than hinder it. If the code would not be understood without comments, consider re-writing the code to make it self-explanatory.\n\n## Add a test\n\nPlaywright requires a test for the new or modified functionality. An exception would be a pure refactoring, but chances are you are doing more than that.\n\nThere are multiple [test suites](https://github.com/microsoft/playwright/blob/main/tests) in Playwright that will be executed on the CI. Tests for Playwright MCP are located at [tests/mcp](https://github.com/microsoft/playwright/blob/main/tests/mcp).\n\n```bash\n# list test files\nls -la tests/mcp\n```\n\nTo run the mcp tests, use\n\n```bash\n# fast path runs all MCP tests in Chromium\nnpm run mcp-ctest\n```\n\n```bash\n# slow path runs all tests in three browsers\nnpm run mcp-test\n```\n\nSince Playwright tests are using Playwright under the hood, everything from our documentation applies, for example [this guide on running and debugging tests](https://playwright.dev/docs/running-tests#running-tests).\n\nNote that tests should be *hermetic*, and not depend on external services. Tests should work on all three platforms: macOS, Linux and Windows.\n\n## Write a commit message\n\nCommit messages should follow the [Semantic Commit Messages](https://www.conventionalcommits.org/en/v1.0.0/) format:\n\n```\nlabel(namespace): title\n\ndescription\n\nfooter\n```\n\n1. *label* is one of the following:\n    - `fix` - bug fixes\n    - `feat` - new features\n    - `docs` - documentation-only changes\n    - `test` - test-only changes\n    - `devops` - changes to the CI or build\n    - `chore` - everything that doesn't fall under previous categories\n2. *namespace* is put in parentheses after label and is optional. Must be lowercase.\n3. *title* is a brief summary of changes.\n4. *description* is **optional**, new-line separated from title and is in present tense.\n5. *footer* is **optional**, new-line separated from *description* and contains \"fixes\" / \"references\" attribution to GitHub issues.\n\nExample:\n\n```\nfeat(trace viewer): network panel filtering\n\nThis patch adds a filtering toolbar to the network panel.\n<link to a screenshot>\n\nFixes #123, references #234.\n```\n\n## Send a pull request\n\nAll submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose.\nMake sure to keep your PR (diff) small and readable. If necessary, split your contribution into multiple PRs.\nConsult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests.\n\nAfter a successful code review, one of the maintainers will merge your pull request. Congratulations!\n\n## More details\n\n**No new dependencies**\n\nThere is a very high bar for new dependencies, including updating to a new version of an existing dependency. We recommend to explicitly discuss this in an issue and get a green light from a maintainer, before creating a pull request that updates dependencies.\n\n## Contributor License Agreement\n\nThis project welcomes contributions and suggestions.  Most contributions require you to agree to a\nContributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us\nthe rights to use your contribution. For details, visit https://cla.opensource.microsoft.com.\n\nWhen you submit a pull request, a CLA bot will automatically determine whether you need to provide\na CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions\nprovided by the bot. You will only need to do this once across all repos using our CLA.\n\n### Code of Conduct\n\nThis project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).\nFor more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or\ncontact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments.\n"
  },
  {
    "path": "Dockerfile",
    "content": "ARG PLAYWRIGHT_BROWSERS_PATH=/ms-playwright\n\n# ------------------------------\n# Base\n# ------------------------------\n# Base stage: Contains only the minimal dependencies required for runtime\n# (node_modules and Playwright system dependencies)\nFROM node:22-bookworm-slim AS base\n\nARG PLAYWRIGHT_BROWSERS_PATH\nENV PLAYWRIGHT_BROWSERS_PATH=${PLAYWRIGHT_BROWSERS_PATH}\n\n# Set the working directory\nWORKDIR /app\n\nRUN --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-cache \\\n    --mount=type=bind,source=package.json,target=package.json \\\n    --mount=type=bind,source=package-lock.json,target=package-lock.json \\\n    --mount=type=bind,source=packages/playwright-mcp/package.json,target=packages/playwright-mcp/package.json \\\n  npm ci --omit=dev && \\\n  # Install system dependencies for playwright\n  npx -y playwright-core install-deps chromium\n\n# ------------------------------\n# Builder\n# ------------------------------\nFROM base AS builder\n\nRUN --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-cache \\\n    --mount=type=bind,source=package.json,target=package.json \\\n    --mount=type=bind,source=package-lock.json,target=package-lock.json \\\n    --mount=type=bind,source=packages/playwright-mcp/package.json,target=packages/playwright-mcp/package.json \\\n  npm ci\n\n# Copy the rest of the app\nCOPY packages/playwright-mcp/*.json packages/playwright-mcp/*.js packages/playwright-mcp/*.ts .\n\n# ------------------------------\n# Browser\n# ------------------------------\n# Cache optimization:\n# - Browser is downloaded only when node_modules or Playwright system dependencies change\n# - Cache is reused when only source code changes\nFROM base AS browser\n\nRUN npx -y playwright-core install --no-shell chromium\n\n# ------------------------------\n# Runtime\n# ------------------------------\nFROM base\n\nARG PLAYWRIGHT_BROWSERS_PATH\nARG USERNAME=node\nENV NODE_ENV=production\nENV PLAYWRIGHT_MCP_OUTPUT_DIR=/tmp/playwright-output\n\n# Set the correct ownership for the runtime user on production `node_modules`\nRUN chown -R ${USERNAME}:${USERNAME} node_modules\n\nUSER ${USERNAME}\n\nCOPY --from=browser --chown=${USERNAME}:${USERNAME} ${PLAYWRIGHT_BROWSERS_PATH} ${PLAYWRIGHT_BROWSERS_PATH}\nCOPY --chown=${USERNAME}:${USERNAME} packages/playwright-mcp/cli.js packages/playwright-mcp/package.json ./\n\n# Run in headless and only with chromium (other browsers need more dependencies not included in this image)\nENTRYPOINT [\"node\", \"cli.js\", \"--headless\", \"--browser\", \"chromium\", \"--no-sandbox\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\r\n                           Version 2.0, January 2004\r\n                        http://www.apache.org/licenses/\r\n\r\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\r\n\r\n   1. Definitions.\r\n\r\n      \"License\" shall mean the terms and conditions for use, reproduction,\r\n      and distribution as defined by Sections 1 through 9 of this document.\r\n\r\n      \"Licensor\" shall mean the copyright owner or entity authorized by\r\n      the copyright owner that is granting the License.\r\n\r\n      \"Legal Entity\" shall mean the union of the acting entity and all\r\n      other entities that control, are controlled by, or are under common\r\n      control with that entity. For the purposes of this definition,\r\n      \"control\" means (i) the power, direct or indirect, to cause the\r\n      direction or management of such entity, whether by contract or\r\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\r\n      outstanding shares, or (iii) beneficial ownership of such entity.\r\n\r\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\r\n      exercising permissions granted by this License.\r\n\r\n      \"Source\" form shall mean the preferred form for making modifications,\r\n      including but not limited to software source code, documentation\r\n      source, and configuration files.\r\n\r\n      \"Object\" form shall mean any form resulting from mechanical\r\n      transformation or translation of a Source form, including but\r\n      not limited to compiled object code, generated documentation,\r\n      and conversions to other media types.\r\n\r\n      \"Work\" shall mean the work of authorship, whether in Source or\r\n      Object form, made available under the License, as indicated by a\r\n      copyright notice that is included in or attached to the work\r\n      (an example is provided in the Appendix below).\r\n\r\n      \"Derivative Works\" shall mean any work, whether in Source or Object\r\n      form, that is based on (or derived from) the Work and for which the\r\n      editorial revisions, annotations, elaborations, or other modifications\r\n      represent, as a whole, an original work of authorship. For the purposes\r\n      of this License, Derivative Works shall not include works that remain\r\n      separable from, or merely link (or bind by name) to the interfaces of,\r\n      the Work and Derivative Works thereof.\r\n\r\n      \"Contribution\" shall mean any work of authorship, including\r\n      the original version of the Work and any modifications or additions\r\n      to that Work or Derivative Works thereof, that is intentionally\r\n      submitted to Licensor for inclusion in the Work by the copyright owner\r\n      or by an individual or Legal Entity authorized to submit on behalf of\r\n      the copyright owner. For the purposes of this definition, \"submitted\"\r\n      means any form of electronic, verbal, or written communication sent\r\n      to the Licensor or its representatives, including but not limited to\r\n      communication on electronic mailing lists, source code control systems,\r\n      and issue tracking systems that are managed by, or on behalf of, the\r\n      Licensor for the purpose of discussing and improving the Work, but\r\n      excluding communication that is conspicuously marked or otherwise\r\n      designated in writing by the copyright owner as \"Not a Contribution.\"\r\n\r\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\r\n      on behalf of whom a Contribution has been received by Licensor and\r\n      subsequently incorporated within the Work.\r\n\r\n   2. Grant of Copyright License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      copyright license to reproduce, prepare Derivative Works of,\r\n      publicly display, publicly perform, sublicense, and distribute the\r\n      Work and such Derivative Works in Source or Object form.\r\n\r\n   3. Grant of Patent License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      (except as stated in this section) patent license to make, have made,\r\n      use, offer to sell, sell, import, and otherwise transfer the Work,\r\n      where such license applies only to those patent claims licensable\r\n      by such Contributor that are necessarily infringed by their\r\n      Contribution(s) alone or by combination of their Contribution(s)\r\n      with the Work to which such Contribution(s) was submitted. If You\r\n      institute patent litigation against any entity (including a\r\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\r\n      or a Contribution incorporated within the Work constitutes direct\r\n      or contributory patent infringement, then any patent licenses\r\n      granted to You under this License for that Work shall terminate\r\n      as of the date such litigation is filed.\r\n\r\n   4. Redistribution. You may reproduce and distribute copies of the\r\n      Work or Derivative Works thereof in any medium, with or without\r\n      modifications, and in Source or Object form, provided that You\r\n      meet the following conditions:\r\n\r\n      (a) You must give any other recipients of the Work or\r\n          Derivative Works a copy of this License; and\r\n\r\n      (b) You must cause any modified files to carry prominent notices\r\n          stating that You changed the files; and\r\n\r\n      (c) You must retain, in the Source form of any Derivative Works\r\n          that You distribute, all copyright, patent, trademark, and\r\n          attribution notices from the Source form of the Work,\r\n          excluding those notices that do not pertain to any part of\r\n          the Derivative Works; and\r\n\r\n      (d) If the Work includes a \"NOTICE\" text file as part of its\r\n          distribution, then any Derivative Works that You distribute must\r\n          include a readable copy of the attribution notices contained\r\n          within such NOTICE file, excluding those notices that do not\r\n          pertain to any part of the Derivative Works, in at least one\r\n          of the following places: within a NOTICE text file distributed\r\n          as part of the Derivative Works; within the Source form or\r\n          documentation, if provided along with the Derivative Works; or,\r\n          within a display generated by the Derivative Works, if and\r\n          wherever such third-party notices normally appear. The contents\r\n          of the NOTICE file are for informational purposes only and\r\n          do not modify the License. You may add Your own attribution\r\n          notices within Derivative Works that You distribute, alongside\r\n          or as an addendum to the NOTICE text from the Work, provided\r\n          that such additional attribution notices cannot be construed\r\n          as modifying the License.\r\n\r\n      You may add Your own copyright statement to Your modifications and\r\n      may provide additional or different license terms and conditions\r\n      for use, reproduction, or distribution of Your modifications, or\r\n      for any such Derivative Works as a whole, provided Your use,\r\n      reproduction, and distribution of the Work otherwise complies with\r\n      the conditions stated in this License.\r\n\r\n   5. Submission of Contributions. Unless You explicitly state otherwise,\r\n      any Contribution intentionally submitted for inclusion in the Work\r\n      by You to the Licensor shall be under the terms and conditions of\r\n      this License, without any additional terms or conditions.\r\n      Notwithstanding the above, nothing herein shall supersede or modify\r\n      the terms of any separate license agreement you may have executed\r\n      with Licensor regarding such Contributions.\r\n\r\n   6. Trademarks. This License does not grant permission to use the trade\r\n      names, trademarks, service marks, or product names of the Licensor,\r\n      except as required for reasonable and customary use in describing the\r\n      origin of the Work and reproducing the content of the NOTICE file.\r\n\r\n   7. Disclaimer of Warranty. Unless required by applicable law or\r\n      agreed to in writing, Licensor provides the Work (and each\r\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\r\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\r\n      implied, including, without limitation, any warranties or conditions\r\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\r\n      PARTICULAR PURPOSE. You are solely responsible for determining the\r\n      appropriateness of using or redistributing the Work and assume any\r\n      risks associated with Your exercise of permissions under this License.\r\n\r\n   8. Limitation of Liability. In no event and under no legal theory,\r\n      whether in tort (including negligence), contract, or otherwise,\r\n      unless required by applicable law (such as deliberate and grossly\r\n      negligent acts) or agreed to in writing, shall any Contributor be\r\n      liable to You for damages, including any direct, indirect, special,\r\n      incidental, or consequential damages of any character arising as a\r\n      result of this License or out of the use or inability to use the\r\n      Work (including but not limited to damages for loss of goodwill,\r\n      work stoppage, computer failure or malfunction, or any and all\r\n      other commercial damages or losses), even if such Contributor\r\n      has been advised of the possibility of such damages.\r\n\r\n   9. Accepting Warranty or Additional Liability. While redistributing\r\n      the Work or Derivative Works thereof, You may choose to offer,\r\n      and charge a fee for, acceptance of support, warranty, indemnity,\r\n      or other liability obligations and/or rights consistent with this\r\n      License. However, in accepting such obligations, You may act only\r\n      on Your own behalf and on Your sole responsibility, not on behalf\r\n      of any other Contributor, and only if You agree to indemnify,\r\n      defend, and hold each Contributor harmless for any liability\r\n      incurred by, or claims asserted against, such Contributor by reason\r\n      of your accepting any such warranty or additional liability.\r\n\r\n   END OF TERMS AND CONDITIONS\r\n\r\n   APPENDIX: How to apply the Apache License to your work.\r\n\r\n      To apply the Apache License to your work, attach the following\r\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\r\n      replaced with your own identifying information. (Don't include\r\n      the brackets!)  The text should be enclosed in the appropriate\r\n      comment syntax for the file format. We also recommend that a\r\n      file or class name and description of purpose be included on the\r\n      same \"printed page\" as the copyright notice for easier\r\n      identification within third-party archives.\r\n\r\n   Copyright (c) Microsoft Corporation.\r\n\r\n   Licensed under the Apache License, Version 2.0 (the \"License\");\r\n   you may not use this file except in compliance with the License.\r\n   You may obtain a copy of the License at\r\n\r\n       http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n   Unless required by applicable law or agreed to in writing, software\r\n   distributed under the License is distributed on an \"AS IS\" BASIS,\r\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n   See the License for the specific language governing permissions and\r\n   limitations under the License.\r\n"
  },
  {
    "path": "README.md",
    "content": "## Playwright MCP\n\nA Model Context Protocol (MCP) server that provides browser automation capabilities using [Playwright](https://playwright.dev). This server enables LLMs to interact with web pages through structured accessibility snapshots, bypassing the need for screenshots or visually-tuned models.\n\n### Playwright MCP vs Playwright CLI\n\nThis package provides MCP interface into Playwright. If you are using a **coding agent**, you might benefit from using the [CLI+SKILLS](https://github.com/microsoft/playwright-cli) instead.\n\n- **CLI**: Modern **coding agents** increasingly favor CLI–based workflows exposed as SKILLs over MCP because CLI invocations are more token-efficient: they avoid loading large tool schemas and verbose accessibility trees into the model context, allowing agents to act through concise, purpose-built commands. This makes CLI + SKILLs better suited for high-throughput coding agents that must balance browser automation with large codebases, tests, and reasoning within limited context windows.<br>**Learn more about [Playwright CLI with SKILLS](https://github.com/microsoft/playwright-cli)**.\n\n- **MCP**: MCP remains relevant for specialized agentic loops that benefit from persistent state, rich introspection, and iterative reasoning over page structure, such as exploratory automation, self-healing tests, or long-running autonomous workflows where maintaining continuous browser context outweighs token cost concerns.\n\n### Key Features\n\n- **Fast and lightweight**. Uses Playwright's accessibility tree, not pixel-based input.\n- **LLM-friendly**. No vision models needed, operates purely on structured data.\n- **Deterministic tool application**. Avoids ambiguity common with screenshot-based approaches.\n\n### Requirements\n- Node.js 18 or newer\n- VS Code, Cursor, Windsurf, Claude Desktop, Goose or any other MCP client\n\n<!--\n// Generate using:\nnode utils/generate-links.js\n-->\n\n### Getting started\n\nFirst, install the Playwright MCP server with your client.\n\n**Standard config** works in most of the tools:\n\n```js\n{\n  \"mcpServers\": {\n    \"playwright\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@playwright/mcp@latest\"\n      ]\n    }\n  }\n}\n```\n\n[<img src=\"https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF\" alt=\"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%2540playwright%252Fmcp%2540latest%2522%255D%257D) [<img alt=\"Install in VS Code Insiders\" src=\"https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5\">](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%2540playwright%252Fmcp%2540latest%2522%255D%257D)\n\n<details>\n<summary>Amp</summary>\n\nAdd via the Amp VS Code extension settings screen or by updating your settings.json file:\n\n```json\n\"amp.mcpServers\": {\n  \"playwright\": {\n    \"command\": \"npx\",\n    \"args\": [\n      \"@playwright/mcp@latest\"\n    ]\n  }\n}\n```\n\n**Amp CLI Setup:**\n\nAdd via the `amp mcp add`command below\n\n```bash\namp mcp add playwright -- npx @playwright/mcp@latest\n```\n\n</details>\n\n<details>\n<summary>Antigravity</summary>\n\nAdd via the Antigravity settings or by updating your configuration file:\n\n```json\n{\n  \"mcpServers\": {\n    \"playwright\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@playwright/mcp@latest\"\n      ]\n    }\n  }\n}\n```\n\n</details>\n\n<details>\n<summary>Claude Code</summary>\n\nUse the Claude Code CLI to add the Playwright MCP server:\n\n```bash\nclaude mcp add playwright npx @playwright/mcp@latest\n```\n</details>\n\n<details>\n<summary>Claude Desktop</summary>\n\nFollow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user), use the standard config above.\n\n</details>\n\n<details>\n<summary>Cline</summary>\n\nFollow the instruction in the section [Configuring MCP Servers](https://docs.cline.bot/mcp/configuring-mcp-servers)\n\n**Example: Local Setup**\n\nAdd the following to your [`cline_mcp_settings.json`](https://docs.cline.bot/mcp/configuring-mcp-servers#editing-mcp-settings-files) file:\n\n```json\n{\n  \"mcpServers\": {\n    \"playwright\": {\n      \"type\": \"stdio\",\n      \"command\": \"npx\",\n      \"timeout\": 30,\n      \"args\": [\n        \"-y\",\n        \"@playwright/mcp@latest\"\n      ],\n      \"disabled\": false\n    }\n  }\n}\n```\n\n</details>\n\n<details>\n<summary>Codex</summary>\n\nUse the Codex CLI to add the Playwright MCP server:\n\n```bash\ncodex mcp add playwright npx \"@playwright/mcp@latest\"\n```\n\nAlternatively, create or edit the configuration file `~/.codex/config.toml` and add:\n\n```toml\n[mcp_servers.playwright]\ncommand = \"npx\"\nargs = [\"@playwright/mcp@latest\"]\n```\n\nFor more information, see the [Codex MCP documentation](https://github.com/openai/codex/blob/main/codex-rs/config.md#mcp_servers).\n\n</details>\n\n<details>\n<summary>Copilot</summary>\n\nUse the Copilot CLI to interactively add the Playwright MCP server:\n\n```bash\n/mcp add\n```\n\nAlternatively, create or edit the configuration file `~/.copilot/mcp-config.json` and add:\n\n```json\n{\n  \"mcpServers\": {\n    \"playwright\": {\n      \"type\": \"local\",\n      \"command\": \"npx\",\n      \"tools\": [\n        \"*\"\n      ],\n      \"args\": [\n        \"@playwright/mcp@latest\"\n      ]\n    }\n  }\n}\n```\n\nFor more information, see the [Copilot CLI documentation](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli).\n\n</details>\n\n<details>\n<summary>Cursor</summary>\n\n#### Click the button to install:\n\n[<img src=\"https://cursor.com/deeplink/mcp-install-dark.svg\" alt=\"Install in Cursor\">](https://cursor.com/en/install-mcp?name=Playwright&config=eyJjb21tYW5kIjoibnB4IEBwbGF5d3JpZ2h0L21jcEBsYXRlc3QifQ%3D%3D)\n\n#### Or install manually:\n\nGo to `Cursor Settings` -> `MCP` -> `Add new MCP Server`. Name to your liking, use `command` type with the command `npx @playwright/mcp@latest`. You can also verify config or add command like arguments via clicking `Edit`.\n\n</details>\n\n<details>\n<summary>Factory</summary>\n\nUse the Factory CLI to add the Playwright MCP server:\n\n```bash\ndroid mcp add playwright \"npx @playwright/mcp@latest\"\n```\n\nAlternatively, type `/mcp` within Factory droid to open an interactive UI for managing MCP servers.\n\nFor more information, see the [Factory MCP documentation](https://docs.factory.ai/cli/configuration/mcp).\n\n</details>\n\n<details>\n<summary>Gemini CLI</summary>\n\nFollow the MCP install [guide](https://github.com/google-gemini/gemini-cli/blob/main/docs/tools/mcp-server.md#configure-the-mcp-server-in-settingsjson), use the standard config above.\n\n</details>\n\n<details>\n<summary>Goose</summary>\n\n#### Click the button to install:\n\n[![Install in Goose](https://block.github.io/goose/img/extension-install-dark.svg)](https://block.github.io/goose/extension?cmd=npx&arg=%40playwright%2Fmcp%40latest&id=playwright&name=Playwright&description=Interact%20with%20web%20pages%20through%20structured%20accessibility%20snapshots%20using%20Playwright)\n\n#### Or install manually:\n\nGo to `Advanced settings` -> `Extensions` -> `Add custom extension`. Name to your liking, use type `STDIO`, and set the `command` to `npx @playwright/mcp`. Click \"Add Extension\".\n</details>\n\n<details>\n<summary>Kiro</summary>\n\nFollow the MCP Servers [documentation](https://kiro.dev/docs/mcp/). For example in `.kiro/settings/mcp.json`:\n\n```json\n{\n  \"mcpServers\": {\n    \"playwright\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@playwright/mcp@latest\"\n      ]\n    }\n  }\n}\n```\n</details>\n\n<details>\n<summary>LM Studio</summary>\n\n#### Click the button to install:\n\n[![Add MCP Server playwright to LM Studio](https://files.lmstudio.ai/deeplink/mcp-install-light.svg)](https://lmstudio.ai/install-mcp?name=playwright&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyJAcGxheXdyaWdodC9tY3BAbGF0ZXN0Il19)\n\n#### Or install manually:\n\nGo to `Program` in the right sidebar -> `Install` -> `Edit mcp.json`. Use the standard config above.\n</details>\n\n<details>\n<summary>opencode</summary>\n\nFollow the MCP Servers [documentation](https://opencode.ai/docs/mcp-servers/). For example in `~/.config/opencode/opencode.json`:\n\n```json\n{\n  \"$schema\": \"https://opencode.ai/config.json\",\n  \"mcp\": {\n    \"playwright\": {\n      \"type\": \"local\",\n      \"command\": [\n        \"npx\",\n        \"@playwright/mcp@latest\"\n      ],\n      \"enabled\": true\n    }\n  }\n}\n\n```\n</details>\n\n<details>\n<summary>Qodo Gen</summary>\n\nOpen [Qodo Gen](https://docs.qodo.ai/qodo-documentation/qodo-gen) chat panel in VSCode or IntelliJ → Connect more tools → + Add new MCP → Paste the standard config above.\n\nClick <code>Save</code>.\n</details>\n\n<details>\n<summary>VS Code</summary>\n\n#### Click the button to install:\n\n[<img src=\"https://img.shields.io/badge/VS_Code-VS_Code?style=flat-square&label=Install%20Server&color=0098FF\" alt=\"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%2540playwright%252Fmcp%2540latest%2522%255D%257D) [<img alt=\"Install in VS Code Insiders\" src=\"https://img.shields.io/badge/VS_Code_Insiders-VS_Code_Insiders?style=flat-square&label=Install%20Server&color=24bfa5\">](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%2540playwright%252Fmcp%2540latest%2522%255D%257D)\n\n#### Or install manually:\n\nFollow the MCP install [guide](https://code.visualstudio.com/docs/copilot/chat/mcp-servers#_add-an-mcp-server), use the standard config above. You can also install the Playwright MCP server using the VS Code CLI:\n\n```bash\n# For VS Code\ncode --add-mcp '{\"name\":\"playwright\",\"command\":\"npx\",\"args\":[\"@playwright/mcp@latest\"]}'\n```\n\nAfter installation, the Playwright MCP server will be available for use with your GitHub Copilot agent in VS Code.\n</details>\n\n<details>\n<summary>Warp</summary>\n\nGo to `Settings` -> `AI` -> `Manage MCP Servers` -> `+ Add` to [add an MCP Server](https://docs.warp.dev/knowledge-and-collaboration/mcp#adding-an-mcp-server). Use the standard config above.\n\nAlternatively, use the slash command `/add-mcp` in the Warp prompt and paste the standard config from above:\n```js\n{\n  \"mcpServers\": {\n    \"playwright\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@playwright/mcp@latest\"\n      ]\n    }\n  }\n}\n```\n\n</details>\n\n<details>\n<summary>Windsurf</summary>\n\nFollow Windsurf MCP [documentation](https://docs.windsurf.com/windsurf/cascade/mcp). Use the standard config above.\n\n</details>\n\n### Configuration\n\nPlaywright MCP server supports following arguments. They can be provided in the JSON configuration above, as a part of the `\"args\"` list:\n\n<!--- Options generated by update-readme.js -->\n\n| Option | Description |\n|--------|-------------|\n| --allowed-hosts <hosts...> | comma-separated list of hosts this server is allowed to serve from. Defaults to the host the server is bound to. Pass '*' to disable the host check.<br>*env* `PLAYWRIGHT_MCP_ALLOWED_HOSTS` |\n| --allowed-origins <origins> | semicolon-separated list of TRUSTED origins to allow the browser to request. Default is to allow all. Important: *does not* serve as a security boundary and *does not* affect redirects.<br>*env* `PLAYWRIGHT_MCP_ALLOWED_ORIGINS` |\n| --allow-unrestricted-file-access | allow access to files outside of the workspace roots. Also allows unrestricted access to file:// URLs. By default access to file system is restricted to workspace root directories (or cwd if no roots are configured) only, and navigation to file:// URLs is blocked.<br>*env* `PLAYWRIGHT_MCP_ALLOW_UNRESTRICTED_FILE_ACCESS` |\n| --blocked-origins <origins> | semicolon-separated list of origins to block the browser from requesting. Blocklist is evaluated before allowlist. If used without the allowlist, requests not matching the blocklist are still allowed. Important: *does not* serve as a security boundary and *does not* affect redirects.<br>*env* `PLAYWRIGHT_MCP_BLOCKED_ORIGINS` |\n| --block-service-workers | block service workers<br>*env* `PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS` |\n| --browser <browser> | browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.<br>*env* `PLAYWRIGHT_MCP_BROWSER` |\n| --caps <caps> | comma-separated list of additional capabilities to enable, possible values: vision, pdf, devtools.<br>*env* `PLAYWRIGHT_MCP_CAPS` |\n| --cdp-endpoint <endpoint> | CDP endpoint to connect to.<br>*env* `PLAYWRIGHT_MCP_CDP_ENDPOINT` |\n| --cdp-header <headers...> | CDP headers to send with the connect request, multiple can be specified.<br>*env* `PLAYWRIGHT_MCP_CDP_HEADER` |\n| --cdp-timeout <timeout> | timeout in milliseconds for connecting to CDP endpoint, defaults to 30000ms<br>*env* `PLAYWRIGHT_MCP_CDP_TIMEOUT` |\n| --codegen <lang> | specify the language to use for code generation, possible values: \"typescript\", \"none\". Default is \"typescript\".<br>*env* `PLAYWRIGHT_MCP_CODEGEN` |\n| --config <path> | path to the configuration file.<br>*env* `PLAYWRIGHT_MCP_CONFIG` |\n| --console-level <level> | level of console messages to return: \"error\", \"warning\", \"info\", \"debug\". Each level includes the messages of more severe levels.<br>*env* `PLAYWRIGHT_MCP_CONSOLE_LEVEL` |\n| --device <device> | device to emulate, for example: \"iPhone 15\"<br>*env* `PLAYWRIGHT_MCP_DEVICE` |\n| --executable-path <path> | path to the browser executable.<br>*env* `PLAYWRIGHT_MCP_EXECUTABLE_PATH` |\n| --extension | Connect to a running browser instance (Edge/Chrome only). Requires the \"Playwright MCP Bridge\" browser extension to be installed.<br>*env* `PLAYWRIGHT_MCP_EXTENSION` |\n| --grant-permissions <permissions...> | List of permissions to grant to the browser context, for example \"geolocation\", \"clipboard-read\", \"clipboard-write\".<br>*env* `PLAYWRIGHT_MCP_GRANT_PERMISSIONS` |\n| --headless | run browser in headless mode, headed by default<br>*env* `PLAYWRIGHT_MCP_HEADLESS` |\n| --host <host> | host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.<br>*env* `PLAYWRIGHT_MCP_HOST` |\n| --ignore-https-errors | ignore https errors<br>*env* `PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS` |\n| --init-page <path...> | path to TypeScript file to evaluate on Playwright page object<br>*env* `PLAYWRIGHT_MCP_INIT_PAGE` |\n| --init-script <path...> | path to JavaScript file to add as an initialization script. The script will be evaluated in every page before any of the page's scripts. Can be specified multiple times.<br>*env* `PLAYWRIGHT_MCP_INIT_SCRIPT` |\n| --isolated | keep the browser profile in memory, do not save it to disk.<br>*env* `PLAYWRIGHT_MCP_ISOLATED` |\n| --image-responses <mode> | whether to send image responses to the client. Can be \"allow\" or \"omit\", Defaults to \"allow\".<br>*env* `PLAYWRIGHT_MCP_IMAGE_RESPONSES` |\n| --no-sandbox | disable the sandbox for all process types that are normally sandboxed.<br>*env* `PLAYWRIGHT_MCP_NO_SANDBOX` |\n| --output-dir <path> | path to the directory for output files.<br>*env* `PLAYWRIGHT_MCP_OUTPUT_DIR` |\n| --output-mode <mode> | whether to save snapshots, console messages, network logs to a file or to the standard output. Can be \"file\" or \"stdout\". Default is \"stdout\".<br>*env* `PLAYWRIGHT_MCP_OUTPUT_MODE` |\n| --port <port> | port to listen on for SSE transport.<br>*env* `PLAYWRIGHT_MCP_PORT` |\n| --proxy-bypass <bypass> | comma-separated domains to bypass proxy, for example \".com,chromium.org,.domain.com\"<br>*env* `PLAYWRIGHT_MCP_PROXY_BYPASS` |\n| --proxy-server <proxy> | specify proxy server, for example \"http://myproxy:3128\" or \"socks5://myproxy:8080\"<br>*env* `PLAYWRIGHT_MCP_PROXY_SERVER` |\n| --sandbox | enable the sandbox for all process types that are normally not sandboxed.<br>*env* `PLAYWRIGHT_MCP_SANDBOX` |\n| --save-session | Whether to save the Playwright MCP session into the output directory.<br>*env* `PLAYWRIGHT_MCP_SAVE_SESSION` |\n| --secrets <path> | path to a file containing secrets in the dotenv format<br>*env* `PLAYWRIGHT_MCP_SECRETS` |\n| --shared-browser-context | reuse the same browser context between all connected HTTP clients.<br>*env* `PLAYWRIGHT_MCP_SHARED_BROWSER_CONTEXT` |\n| --snapshot-mode <mode> | when taking snapshots for responses, specifies the mode to use. Can be \"incremental\", \"full\", or \"none\". Default is incremental.<br>*env* `PLAYWRIGHT_MCP_SNAPSHOT_MODE` |\n| --storage-state <path> | path to the storage state file for isolated sessions.<br>*env* `PLAYWRIGHT_MCP_STORAGE_STATE` |\n| --test-id-attribute <attribute> | specify the attribute to use for test ids, defaults to \"data-testid\"<br>*env* `PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE` |\n| --timeout-action <timeout> | specify action timeout in milliseconds, defaults to 5000ms<br>*env* `PLAYWRIGHT_MCP_TIMEOUT_ACTION` |\n| --timeout-navigation <timeout> | specify navigation timeout in milliseconds, defaults to 60000ms<br>*env* `PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION` |\n| --user-agent <ua string> | specify user agent string<br>*env* `PLAYWRIGHT_MCP_USER_AGENT` |\n| --user-data-dir <path> | path to the user data directory. If not specified, a temporary directory will be created.<br>*env* `PLAYWRIGHT_MCP_USER_DATA_DIR` |\n| --viewport-size <size> | specify browser viewport size in pixels, for example \"1280x720\"<br>*env* `PLAYWRIGHT_MCP_VIEWPORT_SIZE` |\n\n<!--- End of options generated section -->\n\n### User profile\n\nYou can run Playwright MCP with persistent profile like a regular browser (default), in isolated contexts for testing sessions, or connect to your existing browser using the browser extension.\n\n**Persistent profile**\n\nAll the logged in information will be stored in the persistent profile, you can delete it between sessions if you'd like to clear the offline state.\nPersistent profile is located at the following locations and you can override it with the `--user-data-dir` argument.\n\n```bash\n# Windows\n%USERPROFILE%\\AppData\\Local\\ms-playwright\\mcp-{channel}-profile\n\n# macOS\n- ~/Library/Caches/ms-playwright/mcp-{channel}-profile\n\n# Linux\n- ~/.cache/ms-playwright/mcp-{channel}-profile\n```\n\n**Isolated**\n\nIn the isolated mode, each session is started in the isolated profile. Every time you ask MCP to close the browser,\nthe session is closed and all the storage state for this session is lost. You can provide initial storage state\nto the browser via the config's `contextOptions` or via the `--storage-state` argument. Learn more about the storage\nstate [here](https://playwright.dev/docs/auth).\n\n```js\n{\n  \"mcpServers\": {\n    \"playwright\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@playwright/mcp@latest\",\n        \"--isolated\",\n        \"--storage-state={path/to/storage.json}\"\n      ]\n    }\n  }\n}\n```\n\n**Browser Extension**\n\nThe Playwright MCP Chrome Extension allows you to connect to existing browser tabs and leverage your logged-in sessions and browser state. See [packages/extension/README.md](packages/extension/README.md) for installation and setup instructions.\n\n### Initial state\n\nThere are multiple ways to provide the initial state to the browser context or a page.\n\nFor the storage state, you can either:\n- Start with a user data directory using the `--user-data-dir` argument. This will persist all browser data between the sessions.\n- Start with a storage state file using the `--storage-state` argument. This will load cookies and local storage from the file into an isolated browser context.\n\nFor the page state, you can use:\n\n- `--init-page` to point to a TypeScript file that will be evaluated on the Playwright page object. This allows you to run arbitrary code to set up the page.\n\n```ts\n// init-page.ts\nexport default async ({ page }) => {\n  await page.context().grantPermissions(['geolocation']);\n  await page.context().setGeolocation({ latitude: 37.7749, longitude: -122.4194 });\n  await page.setViewportSize({ width: 1280, height: 720 });\n};\n```\n\n- `--init-script` to point to a JavaScript file that will be added as an initialization script. The script will be evaluated in every page before any of the page's scripts.\nThis is useful for overriding browser APIs or setting up the environment.\n\n```js\n// init-script.js\nwindow.isPlaywrightMCP = true;\n```\n\n### Configuration file\n\nThe Playwright MCP server can be configured using a JSON configuration file. You can specify the configuration file\nusing the `--config` command line option:\n\n```bash\nnpx @playwright/mcp@latest --config path/to/config.json\n```\n\n<details>\n<summary>Configuration file schema</summary>\n\n<!--- Config generated by update-readme.js -->\n\n```typescript\n{\n  /**\n   * The browser to use.\n   */\n  browser?: {\n    /**\n     * The type of browser to use.\n     */\n    browserName?: 'chromium' | 'firefox' | 'webkit';\n\n    /**\n     * Keep the browser profile in memory, do not save it to disk.\n     */\n    isolated?: boolean;\n\n    /**\n     * Path to a user data directory for browser profile persistence.\n     * Temporary directory is created by default.\n     */\n    userDataDir?: string;\n\n    /**\n     * Launch options passed to\n     * @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context\n     *\n     * This is useful for settings options like `channel`, `headless`, `executablePath`, etc.\n     */\n    launchOptions?: playwright.LaunchOptions;\n\n    /**\n     * Context options for the browser context.\n     *\n     * This is useful for settings options like `viewport`.\n     */\n    contextOptions?: playwright.BrowserContextOptions;\n\n    /**\n     * Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers.\n     */\n    cdpEndpoint?: string;\n\n    /**\n     * CDP headers to send with the connect request.\n     */\n    cdpHeaders?: Record<string, string>;\n\n    /**\n     * Timeout in milliseconds for connecting to CDP endpoint. Defaults to 30000 (30 seconds). Pass 0 to disable timeout.\n     */\n    cdpTimeout?: number;\n\n    /**\n     * Remote endpoint to connect to an existing Playwright server.\n     */\n    remoteEndpoint?: string;\n\n    /**\n     * Paths to TypeScript files to add as initialization scripts for Playwright page.\n     */\n    initPage?: string[];\n\n    /**\n     * Paths to JavaScript files to add as initialization scripts.\n     * The scripts will be evaluated in every page before any of the page's scripts.\n     */\n    initScript?: string[];\n  },\n\n  /**\n   * Connect to a running browser instance (Edge/Chrome only). If specified, `browser`\n   * config is ignored.\n   * Requires the \"Playwright MCP Bridge\" browser extension to be installed.\n   */\n  extension?: boolean;\n\n  server?: {\n    /**\n     * The port to listen on for SSE or MCP transport.\n     */\n    port?: number;\n\n    /**\n     * The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.\n     */\n    host?: string;\n\n    /**\n     * The hosts this server is allowed to serve from. Defaults to the host server is bound to.\n     * This is not for CORS, but rather for the DNS rebinding protection.\n     */\n    allowedHosts?: string[];\n  },\n\n  /**\n   * List of enabled tool capabilities. Possible values:\n   *   - 'core': Core browser automation features.\n   *   - 'pdf': PDF generation and manipulation.\n   *   - 'vision': Coordinate-based interactions.\n   *   - 'devtools': Developer tools features.\n   */\n  capabilities?: ToolCapability[];\n\n  /**\n   * Whether to save the Playwright session into the output directory.\n   */\n  saveSession?: boolean;\n\n  /**\n   * Reuse the same browser context between all connected HTTP clients.\n   */\n  sharedBrowserContext?: boolean;\n\n  /**\n   * Secrets are used to prevent LLM from getting sensitive data while\n   * automating scenarios such as authentication.\n   * Prefer the browser.contextOptions.storageState over secrets file as a more secure alternative.\n   */\n  secrets?: Record<string, string>;\n\n  /**\n   * The directory to save output files.\n   */\n  outputDir?: string;\n\n  /**\n   * Whether to save snapshots, console messages, network logs and other session logs to a file or to the standard output. Defaults to \"stdout\".\n   */\n  outputMode?: 'file' | 'stdout';\n\n  console?: {\n    /**\n     * The level of console messages to return. Each level includes the messages of more severe levels. Defaults to \"info\".\n     */\n    level?: 'error' | 'warning' | 'info' | 'debug';\n  },\n\n  network?: {\n    /**\n     * List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.\n     *\n     * Supported formats:\n     * - Full origin: `https://example.com:8080` - matches only that origin\n     * - Wildcard port: `http://localhost:*` - matches any port on localhost with http protocol\n     */\n    allowedOrigins?: string[];\n\n    /**\n     * List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.\n     *\n     * Supported formats:\n     * - Full origin: `https://example.com:8080` - matches only that origin\n     * - Wildcard port: `http://localhost:*` - matches any port on localhost with http protocol\n     */\n    blockedOrigins?: string[];\n  };\n\n  /**\n   * Specify the attribute to use for test ids, defaults to \"data-testid\".\n   */\n  testIdAttribute?: string;\n\n  timeouts?: {\n    /*\n     * Configures default action timeout: https://playwright.dev/docs/api/class-page#page-set-default-timeout. Defaults to 5000ms.\n     */\n    action?: number;\n\n    /*\n     * Configures default navigation timeout: https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout. Defaults to 60000ms.\n     */\n    navigation?: number;\n\n    /**\n     * Configures default expect timeout: https://playwright.dev/docs/test-timeouts#expect-timeout. Defaults to 5000ms.\n     */\n    expect?: number;\n  };\n\n  /**\n   * Whether to send image responses to the client. Can be \"allow\", \"omit\", or \"auto\". Defaults to \"auto\", which sends images if the client can display them.\n   */\n  imageResponses?: 'allow' | 'omit';\n\n  snapshot?: {\n    /**\n     * When taking snapshots for responses, specifies the mode to use.\n     */\n    mode?: 'incremental' | 'full' | 'none';\n  };\n\n  /**\n   * Whether to allow file uploads from anywhere on the file system.\n   * By default (false), file uploads are restricted to paths within the MCP roots only.\n   */\n  allowUnrestrictedFileAccess?: boolean;\n\n  /**\n   * Specify the language to use for code generation.\n   */\n  codegen?: 'typescript' | 'none';\n}\n```\n\n<!--- End of config generated section -->\n\n</details>\n\n### Standalone MCP server\n\nWhen running headed browser on system w/o display or from worker processes of the IDEs,\nrun the MCP server from environment with the DISPLAY and pass the `--port` flag to enable HTTP transport.\n\n```bash\nnpx @playwright/mcp@latest --port 8931\n```\n\nAnd then in MCP client config, set the `url` to the HTTP endpoint:\n\n```js\n{\n  \"mcpServers\": {\n    \"playwright\": {\n      \"url\": \"http://localhost:8931/mcp\"\n    }\n  }\n}\n```\n\n## Security\n\nPlaywright MCP is **not** a security boundary. See [MCP Security Best Practices](https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices) for guidance on securing your deployment.\n\n<details>\n<summary><b>Docker</b></summary>\n\n**NOTE:** The Docker implementation only supports headless chromium at the moment.\n\n```js\n{\n  \"mcpServers\": {\n    \"playwright\": {\n      \"command\": \"docker\",\n      \"args\": [\"run\", \"-i\", \"--rm\", \"--init\", \"--pull=always\", \"mcr.microsoft.com/playwright/mcp\"]\n    }\n  }\n}\n```\n\nOr If you prefer to run the container as a long-lived service instead of letting the MCP client spawn it, use:\n\n```\ndocker run -d -i --rm --init --pull=always \\\n  --entrypoint node \\\n  --name playwright \\\n  -p 8931:8931 \\\n  mcr.microsoft.com/playwright/mcp \\\n  cli.js --headless --browser chromium --no-sandbox --port 8931 --host 0.0.0.0\n```\n\nThe server will listen on host port **8931** and can be reached by any MCP client.  \n\nYou can build the Docker image yourself.\n\n```\ndocker build -t mcr.microsoft.com/playwright/mcp .\n```\n</details>\n\n<details>\n<summary><b>Programmatic usage</b></summary>\n\n```js\nimport http from 'http';\n\nimport { createConnection } from '@playwright/mcp';\nimport { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';\n\nhttp.createServer(async (req, res) => {\n  // ...\n\n  // Creates a headless Playwright MCP server with SSE transport\n  const connection = await createConnection({ browser: { launchOptions: { headless: true } } });\n  const transport = new SSEServerTransport('/messages', res);\n  await connection.connect(transport);\n\n  // ...\n});\n```\n</details>\n\n### Tools\n\n<!--- Tools generated by update-readme.js -->\n\n<details>\n<summary><b>Core automation</b></summary>\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_click**\n  - Title: Click\n  - Description: Perform click on a web page\n  - Parameters:\n    - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element\n    - `ref` (string): Exact target element reference from the page snapshot\n    - `selector` (string, optional): CSS or role selector for the target element, when \"ref\" is not available\n    - `doubleClick` (boolean, optional): Whether to perform a double click instead of a single click\n    - `button` (string, optional): Button to click, defaults to left\n    - `modifiers` (array, optional): Modifier keys to press\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_close**\n  - Title: Close browser\n  - Description: Close the page\n  - Parameters: None\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_console_messages**\n  - Title: Get console messages\n  - Description: Returns all console messages\n  - Parameters:\n    - `level` (string): Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to \"info\".\n    - `all` (boolean, optional): Return all console messages since the beginning of the session, not just since the last navigation. Defaults to false.\n    - `filename` (string, optional): Filename to save the console messages to. If not provided, messages are returned as text.\n  - Read-only: **true**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_drag**\n  - Title: Drag mouse\n  - Description: Perform drag and drop between two elements\n  - Parameters:\n    - `startElement` (string): Human-readable source element description used to obtain the permission to interact with the element\n    - `startRef` (string): Exact source element reference from the page snapshot\n    - `startSelector` (string, optional): CSS or role selector for the source element, when ref is not available\n    - `endElement` (string): Human-readable target element description used to obtain the permission to interact with the element\n    - `endRef` (string): Exact target element reference from the page snapshot\n    - `endSelector` (string, optional): CSS or role selector for the target element, when ref is not available\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_evaluate**\n  - Title: Evaluate JavaScript\n  - Description: Evaluate JavaScript expression on page or element\n  - Parameters:\n    - `function` (string): () => { /* code */ } or (element) => { /* code */ } when element is provided\n    - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element\n    - `ref` (string, optional): Exact target element reference from the page snapshot\n    - `selector` (string, optional): CSS or role selector for the target element, when \"ref\" is not available.\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_file_upload**\n  - Title: Upload files\n  - Description: Upload one or multiple files\n  - Parameters:\n    - `paths` (array, optional): The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled.\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_fill_form**\n  - Title: Fill form\n  - Description: Fill multiple form fields\n  - Parameters:\n    - `fields` (array): Fields to fill in\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_handle_dialog**\n  - Title: Handle a dialog\n  - Description: Handle a dialog\n  - Parameters:\n    - `accept` (boolean): Whether to accept the dialog.\n    - `promptText` (string, optional): The text of the prompt in case of a prompt dialog.\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_hover**\n  - Title: Hover mouse\n  - Description: Hover over element on page\n  - Parameters:\n    - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element\n    - `ref` (string): Exact target element reference from the page snapshot\n    - `selector` (string, optional): CSS or role selector for the target element, when \"ref\" is not available\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_navigate**\n  - Title: Navigate to a URL\n  - Description: Navigate to a URL\n  - Parameters:\n    - `url` (string): The URL to navigate to\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_navigate_back**\n  - Title: Go back\n  - Description: Go back to the previous page in the history\n  - Parameters: None\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_network_requests**\n  - Title: List network requests\n  - Description: Returns all network requests since loading the page\n  - Parameters:\n    - `includeStatic` (boolean): Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false.\n    - `filename` (string, optional): Filename to save the network requests to. If not provided, requests are returned as text.\n  - Read-only: **true**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_press_key**\n  - Title: Press a key\n  - Description: Press a key on the keyboard\n  - Parameters:\n    - `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a`\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_resize**\n  - Title: Resize browser window\n  - Description: Resize the browser window\n  - Parameters:\n    - `width` (number): Width of the browser window\n    - `height` (number): Height of the browser window\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_run_code**\n  - Title: Run Playwright code\n  - Description: Run Playwright code snippet\n  - Parameters:\n    - `code` (string): A JavaScript function containing Playwright code to execute. It will be invoked with a single argument, page, which you can use for any page interaction. For example: `async (page) => { await page.getByRole('button', { name: 'Submit' }).click(); return await page.title(); }`\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_select_option**\n  - Title: Select option\n  - Description: Select an option in a dropdown\n  - Parameters:\n    - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element\n    - `ref` (string): Exact target element reference from the page snapshot\n    - `selector` (string, optional): CSS or role selector for the target element, when \"ref\" is not available\n    - `values` (array): Array of values to select in the dropdown. This can be a single value or multiple values.\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_snapshot**\n  - Title: Page snapshot\n  - Description: Capture accessibility snapshot of the current page, this is better than screenshot\n  - Parameters:\n    - `filename` (string, optional): Save snapshot to markdown file instead of returning it in the response.\n    - `selector` (string, optional): Element selector of the root element to capture a partial snapshot instead of the whole page\n  - Read-only: **true**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_take_screenshot**\n  - Title: Take a screenshot\n  - Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions.\n  - Parameters:\n    - `type` (string): Image format for the screenshot. Default is png.\n    - `filename` (string, optional): File name to save the screenshot to. Defaults to `page-{timestamp}.{png|jpeg}` if not specified. Prefer relative file names to stay within the output directory.\n    - `element` (string, optional): Human-readable element description used to obtain permission to screenshot the element. If not provided, the screenshot will be taken of viewport. If element is provided, ref must be provided too.\n    - `ref` (string, optional): Exact target element reference from the page snapshot. If not provided, the screenshot will be taken of viewport. If ref is provided, element must be provided too.\n    - `selector` (string, optional): CSS or role selector for the target element, when \"ref\" is not available.\n    - `fullPage` (boolean, optional): When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Cannot be used with element screenshots.\n  - Read-only: **true**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_type**\n  - Title: Type text\n  - Description: Type text into editable element\n  - Parameters:\n    - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element\n    - `ref` (string): Exact target element reference from the page snapshot\n    - `selector` (string, optional): CSS or role selector for the target element, when \"ref\" is not available\n    - `text` (string): Text to type into the element\n    - `submit` (boolean, optional): Whether to submit entered text (press Enter after)\n    - `slowly` (boolean, optional): Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once.\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_wait_for**\n  - Title: Wait for\n  - Description: Wait for text to appear or disappear or a specified time to pass\n  - Parameters:\n    - `time` (number, optional): The time to wait in seconds\n    - `text` (string, optional): The text to wait for\n    - `textGone` (string, optional): The text to wait for to disappear\n  - Read-only: **false**\n\n</details>\n\n<details>\n<summary><b>Tab management</b></summary>\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_tabs**\n  - Title: Manage tabs\n  - Description: List, create, close, or select a browser tab.\n  - Parameters:\n    - `action` (string): Operation to perform\n    - `index` (number, optional): Tab index, used for close/select. If omitted for close, current tab is closed.\n  - Read-only: **false**\n\n</details>\n\n<details>\n<summary><b>Browser installation</b></summary>\n\n</details>\n\n<details>\n<summary><b>Configuration (opt-in via --caps=config)</b></summary>\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_get_config**\n  - Title: Get config\n  - Description: Get the final resolved config after merging CLI options, environment variables and config file.\n  - Parameters: None\n  - Read-only: **true**\n\n</details>\n\n<details>\n<summary><b>Network (opt-in via --caps=network)</b></summary>\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_network_state_set**\n  - Title: Set network state\n  - Description: Sets the browser network state to online or offline. When offline, all network requests will fail.\n  - Parameters:\n    - `state` (string): Set to \"offline\" to simulate offline mode, \"online\" to restore network connectivity\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_route**\n  - Title: Mock network requests\n  - Description: Set up a route to mock network requests matching a URL pattern\n  - Parameters:\n    - `pattern` (string): URL pattern to match (e.g., \"**/api/users\", \"**/*.{png,jpg}\")\n    - `status` (number, optional): HTTP status code to return (default: 200)\n    - `body` (string, optional): Response body (text or JSON string)\n    - `contentType` (string, optional): Content-Type header (e.g., \"application/json\", \"text/html\")\n    - `headers` (array, optional): Headers to add in \"Name: Value\" format\n    - `removeHeaders` (string, optional): Comma-separated list of header names to remove from request\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_route_list**\n  - Title: List network routes\n  - Description: List all active network routes\n  - Parameters: None\n  - Read-only: **true**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_unroute**\n  - Title: Remove network routes\n  - Description: Remove network routes matching a pattern (or all routes if no pattern specified)\n  - Parameters:\n    - `pattern` (string, optional): URL pattern to unroute (omit to remove all routes)\n  - Read-only: **false**\n\n</details>\n\n<details>\n<summary><b>Storage (opt-in via --caps=storage)</b></summary>\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_cookie_clear**\n  - Title: Clear cookies\n  - Description: Clear all cookies\n  - Parameters: None\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_cookie_delete**\n  - Title: Delete cookie\n  - Description: Delete a specific cookie\n  - Parameters:\n    - `name` (string): Cookie name to delete\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_cookie_get**\n  - Title: Get cookie\n  - Description: Get a specific cookie by name\n  - Parameters:\n    - `name` (string): Cookie name to get\n  - Read-only: **true**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_cookie_list**\n  - Title: List cookies\n  - Description: List all cookies (optionally filtered by domain/path)\n  - Parameters:\n    - `domain` (string, optional): Filter cookies by domain\n    - `path` (string, optional): Filter cookies by path\n  - Read-only: **true**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_cookie_set**\n  - Title: Set cookie\n  - Description: Set a cookie with optional flags (domain, path, expires, httpOnly, secure, sameSite)\n  - Parameters:\n    - `name` (string): Cookie name\n    - `value` (string): Cookie value\n    - `domain` (string, optional): Cookie domain\n    - `path` (string, optional): Cookie path\n    - `expires` (number, optional): Cookie expiration as Unix timestamp\n    - `httpOnly` (boolean, optional): Whether the cookie is HTTP only\n    - `secure` (boolean, optional): Whether the cookie is secure\n    - `sameSite` (string, optional): Cookie SameSite attribute\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_localstorage_clear**\n  - Title: Clear localStorage\n  - Description: Clear all localStorage\n  - Parameters: None\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_localstorage_delete**\n  - Title: Delete localStorage item\n  - Description: Delete a localStorage item\n  - Parameters:\n    - `key` (string): Key to delete\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_localstorage_get**\n  - Title: Get localStorage item\n  - Description: Get a localStorage item by key\n  - Parameters:\n    - `key` (string): Key to get\n  - Read-only: **true**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_localstorage_list**\n  - Title: List localStorage\n  - Description: List all localStorage key-value pairs\n  - Parameters: None\n  - Read-only: **true**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_localstorage_set**\n  - Title: Set localStorage item\n  - Description: Set a localStorage item\n  - Parameters:\n    - `key` (string): Key to set\n    - `value` (string): Value to set\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_sessionstorage_clear**\n  - Title: Clear sessionStorage\n  - Description: Clear all sessionStorage\n  - Parameters: None\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_sessionstorage_delete**\n  - Title: Delete sessionStorage item\n  - Description: Delete a sessionStorage item\n  - Parameters:\n    - `key` (string): Key to delete\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_sessionstorage_get**\n  - Title: Get sessionStorage item\n  - Description: Get a sessionStorage item by key\n  - Parameters:\n    - `key` (string): Key to get\n  - Read-only: **true**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_sessionstorage_list**\n  - Title: List sessionStorage\n  - Description: List all sessionStorage key-value pairs\n  - Parameters: None\n  - Read-only: **true**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_sessionstorage_set**\n  - Title: Set sessionStorage item\n  - Description: Set a sessionStorage item\n  - Parameters:\n    - `key` (string): Key to set\n    - `value` (string): Value to set\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_set_storage_state**\n  - Title: Restore storage state\n  - Description: Restore storage state (cookies, local storage) from a file. This clears existing cookies and local storage before restoring.\n  - Parameters:\n    - `filename` (string): Path to the storage state file to restore from\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_storage_state**\n  - Title: Save storage state\n  - Description: Save storage state (cookies, local storage) to a file for later reuse\n  - Parameters:\n    - `filename` (string, optional): File name to save the storage state to. Defaults to `storage-state-{timestamp}.json` if not specified.\n  - Read-only: **true**\n\n</details>\n\n<details>\n<summary><b>DevTools (opt-in via --caps=devtools)</b></summary>\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_start_tracing**\n  - Title: Start tracing\n  - Description: Start trace recording\n  - Parameters: None\n  - Read-only: **true**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_start_video**\n  - Title: Start video\n  - Description: Start video recording\n  - Parameters:\n    - `size` (object, optional): Video size\n  - Read-only: **true**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_stop_tracing**\n  - Title: Stop tracing\n  - Description: Stop trace recording\n  - Parameters: None\n  - Read-only: **true**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_stop_video**\n  - Title: Stop video\n  - Description: Stop video recording\n  - Parameters:\n    - `filename` (string, optional): Filename to save the video\n  - Read-only: **true**\n\n</details>\n\n<details>\n<summary><b>Coordinate-based (opt-in via --caps=vision)</b></summary>\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_mouse_click_xy**\n  - Title: Click\n  - Description: Click mouse button at a given position\n  - Parameters:\n    - `x` (number): X coordinate\n    - `y` (number): Y coordinate\n    - `button` (string, optional): Button to click, defaults to left\n    - `clickCount` (number, optional): Number of clicks, defaults to 1\n    - `delay` (number, optional): Time to wait between mouse down and mouse up in milliseconds, defaults to 0\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_mouse_down**\n  - Title: Press mouse down\n  - Description: Press mouse down\n  - Parameters:\n    - `button` (string, optional): Button to press, defaults to left\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_mouse_drag_xy**\n  - Title: Drag mouse\n  - Description: Drag left mouse button to a given position\n  - Parameters:\n    - `startX` (number): Start X coordinate\n    - `startY` (number): Start Y coordinate\n    - `endX` (number): End X coordinate\n    - `endY` (number): End Y coordinate\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_mouse_move_xy**\n  - Title: Move mouse\n  - Description: Move mouse to a given position\n  - Parameters:\n    - `x` (number): X coordinate\n    - `y` (number): Y coordinate\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_mouse_up**\n  - Title: Press mouse up\n  - Description: Press mouse up\n  - Parameters:\n    - `button` (string, optional): Button to press, defaults to left\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_mouse_wheel**\n  - Title: Scroll mouse wheel\n  - Description: Scroll mouse wheel\n  - Parameters:\n    - `deltaX` (number): X delta\n    - `deltaY` (number): Y delta\n  - Read-only: **false**\n\n</details>\n\n<details>\n<summary><b>PDF generation (opt-in via --caps=pdf)</b></summary>\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_pdf_save**\n  - Title: Save as PDF\n  - Description: Save page as PDF\n  - Parameters:\n    - `filename` (string, optional): File name to save the pdf to. Defaults to `page-{timestamp}.pdf` if not specified. Prefer relative file names to stay within the output directory.\n  - Read-only: **true**\n\n</details>\n\n<details>\n<summary><b>Test assertions (opt-in via --caps=testing)</b></summary>\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_generate_locator**\n  - Title: Create locator for element\n  - Description: Generate locator for the given element to use in tests\n  - Parameters:\n    - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element\n    - `ref` (string): Exact target element reference from the page snapshot\n    - `selector` (string, optional): CSS or role selector for the target element, when \"ref\" is not available\n  - Read-only: **true**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_verify_element_visible**\n  - Title: Verify element visible\n  - Description: Verify element is visible on the page\n  - Parameters:\n    - `role` (string): ROLE of the element. Can be found in the snapshot like this: `- {ROLE} \"Accessible Name\":`\n    - `accessibleName` (string): ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: `- role \"{ACCESSIBLE_NAME}\"`\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_verify_list_visible**\n  - Title: Verify list visible\n  - Description: Verify list is visible on the page\n  - Parameters:\n    - `element` (string): Human-readable list description\n    - `ref` (string): Exact target element reference that points to the list\n    - `selector` (string, optional): CSS or role selector for the target list, when \"ref\" is not available.\n    - `items` (array): Items to verify\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_verify_text_visible**\n  - Title: Verify text visible\n  - Description: Verify text is visible on the page. Prefer browser_verify_element_visible if possible.\n  - Parameters:\n    - `text` (string): TEXT to verify. Can be found in the snapshot like this: `- role \"Accessible Name\": {TEXT}` or like this: `- text: {TEXT}`\n  - Read-only: **false**\n\n<!-- NOTE: This has been generated via update-readme.js -->\n\n- **browser_verify_value**\n  - Title: Verify value\n  - Description: Verify element value\n  - Parameters:\n    - `type` (string): Type of the element\n    - `element` (string): Human-readable element description\n    - `ref` (string): Exact target element reference from the page snapshot\n    - `selector` (string, optional): CSS or role selector for the target element, when \"ref\" is not available\n    - `value` (string): Value to verify. For checkbox, use \"true\" or \"false\".\n  - Read-only: **false**\n\n</details>\n\n\n<!--- End of tools generated section -->\n"
  },
  {
    "path": "SECURITY.md",
    "content": "<!-- BEGIN MICROSOFT SECURITY.MD V0.0.9 BLOCK -->\n\n## Security\n\nMicrosoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin).\n\nIf you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below.\n\n## Reporting Security Issues\n\n**Please do not report security vulnerabilities through public GitHub issues.**\n\nInstead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report).\n\nIf you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com).  If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp).\n\nYou should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc). \n\nPlease include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:\n\n  * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)\n  * Full paths of source file(s) related to the manifestation of the issue\n  * The location of the affected source code (tag/branch/commit or direct URL)\n  * Any special configuration required to reproduce the issue\n  * Step-by-step instructions to reproduce the issue\n  * Proof-of-concept or exploit code (if possible)\n  * Impact of the issue, including how an attacker might exploit the issue\n\nThis information will help us triage your report more quickly.\n\nIf you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs.\n\n## Preferred Languages\n\nWe prefer all communications to be in English.\n\n## Policy\n\nMicrosoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd).\n\n<!-- END MICROSOFT SECURITY.MD BLOCK -->\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"playwright-mcp-internal\",\n  \"version\": \"0.0.68\",\n  \"private\": true,\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/microsoft/playwright-mcp.git\"\n  },\n  \"homepage\": \"https://playwright.dev\",\n  \"author\": {\n    \"name\": \"Microsoft Corporation\"\n  },\n  \"license\": \"Apache-2.0\",\n  \"scripts\": {\n    \"docker-build\": \"docker build --no-cache -t playwright-mcp-dev:latest .\",\n    \"docker-rm\": \"docker rm playwright-mcp-dev\",\n    \"docker-run\": \"docker run -it -p 8080:8080 --name playwright-mcp-dev playwright-mcp-dev:latest\",\n    \"lint\": \"npm run lint --workspaces\",\n    \"test\": \"npm run test --workspaces\",\n    \"build\": \"npm run build --workspaces\",\n    \"bump\": \"npm version --workspaces --no-git-tag-version\",\n    \"roll\": \"node roll.js\"\n  },\n  \"workspaces\": [\n    \"packages/*\"\n  ],\n  \"devDependencies\": {\n    \"@modelcontextprotocol/sdk\": \"^1.25.2\",\n    \"@playwright/test\": \"1.59.0-alpha-1773608981000\",\n    \"@types/node\": \"^24.3.0\"\n  }\n}\n"
  },
  {
    "path": "packages/extension/.gitignore",
    "content": "dist/\n"
  },
  {
    "path": "packages/extension/README.md",
    "content": "# Playwright MCP Chrome Extension\n\n## Introduction\n\nThe Playwright MCP Chrome Extension allows you to connect to pages in your existing browser and leverage the state of your default user profile. This means the AI assistant can interact with websites where you're already logged in, using your existing cookies, sessions, and browser state, providing a seamless experience without requiring separate authentication or setup.\n\n## Prerequisites\n\n- Chrome/Edge/Chromium browser\n\n## Installation Steps\n\n### Install the Extension\n\nInstall [Playwright MCP Bridge](https://chromewebstore.google.com/detail/playwright-mcp-bridge/mmlmfjhmonkocbjadbfplnigmagldckm) from the Chrome Web Store.\n\n### Configure Playwright MCP server\n\nConfigure Playwright MCP server to connect to the browser using the extension by passing the `--extension` option when running the MCP server:\n\n```json\n{\n  \"mcpServers\": {\n    \"playwright-extension\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@playwright/mcp@latest\",\n        \"--extension\"\n      ]\n    }\n  }\n}\n```\n\n## Usage\n\n### Browser Tab Selection\n\nWhen the LLM interacts with the browser for the first time, it will load a page where you can select which browser tab the LLM will connect to. This allows you to control which specific page the AI assistant will interact with during the session.\n\n### Bypassing the Connection Approval Dialog\n\nBy default, you'll need to approve each connection when the MCP server tries to connect to your browser. To bypass this approval dialog and allow automatic connections, you can use an authentication token.\n\n#### Using Your Unique Authentication Token\n\n1. After installing the extension, click on the extension icon or navigate to the extension's status page\n2. Copy the `PLAYWRIGHT_MCP_EXTENSION_TOKEN` value displayed in the extension UI\n3. Add it to your MCP server configuration:\n\n```json\n{\n  \"mcpServers\": {\n    \"playwright-extension\": {\n      \"command\": \"npx\",\n      \"args\": [\n        \"@playwright/mcp@latest\",\n        \"--extension\"\n      ],\n      \"env\": {\n        \"PLAYWRIGHT_MCP_EXTENSION_TOKEN\": \"your-token-here\"\n      }\n    }\n  }\n}\n```\n\nThis token is unique to your browser profile and provides secure authentication between the MCP server and the extension. Once configured, you won't need to manually approve connections each time.\n\n\n"
  },
  {
    "path": "packages/extension/manifest.json",
    "content": "{\n  \"manifest_version\": 3,\n  \"name\": \"Playwright MCP Bridge\",\n  \"version\": \"0.0.68\",\n  \"description\": \"Share browser tabs with Playwright MCP server\",\n  \"permissions\": [\n    \"debugger\",\n    \"activeTab\",\n    \"tabs\"\n  ],\n  \"host_permissions\": [\n    \"<all_urls>\"\n  ],\n  \"background\": {\n    \"service_worker\": \"lib/background.mjs\",\n    \"type\": \"module\"\n  },\n  \"action\": {\n    \"default_title\": \"Playwright MCP Bridge\",\n    \"default_icon\": {\n      \"16\": \"icons/icon-16.png\",\n      \"32\": \"icons/icon-32.png\",\n      \"48\": \"icons/icon-48.png\",\n      \"128\": \"icons/icon-128.png\"\n    }\n  },\n  \"icons\": {\n    \"16\": \"icons/icon-16.png\",\n    \"32\": \"icons/icon-32.png\",\n    \"48\": \"icons/icon-48.png\",\n    \"128\": \"icons/icon-128.png\"\n  }\n}\n"
  },
  {
    "path": "packages/extension/package.json",
    "content": "{\n  \"name\": \"@playwright/mcp-extension\",\n  \"version\": \"0.0.68\",\n  \"description\": \"Playwright MCP Browser Extension\",\n  \"private\": true,\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/microsoft/playwright-mcp.git\"\n  },\n  \"homepage\": \"https://playwright.dev\",\n  \"engines\": {\n    \"node\": \">=18\"\n  },\n  \"author\": {\n    \"name\": \"Microsoft Corporation\"\n  },\n  \"license\": \"Apache-2.0\",\n  \"scripts\": {\n    \"build\": \"tsc --project . && tsc --project tsconfig.ui.json && vite build && vite build --config vite.sw.config.mts\",\n    \"watch\": \"tsc --watch --project . & tsc --watch --project tsconfig.ui.json & vite build --watch & vite build --watch --config vite.sw.config.mts\",\n    \"test\": \"playwright test\",\n    \"lint\": \"tsc --project .\",\n    \"clean\": \"rm -rf dist\"\n  },\n  \"devDependencies\": {\n    \"@types/chrome\": \"^0.0.315\",\n    \"@types/react\": \"^18.2.66\",\n    \"@types/react-dom\": \"^18.2.22\",\n    \"@vitejs/plugin-react\": \"^4.0.0\",\n    \"minimist\": \"^1.2.5\",\n    \"react\": \"^18.2.0\",\n    \"react-dom\": \"^18.2.0\",\n    \"typescript\": \"^5.8.2\",\n    \"vite\": \"^7.3.1\",\n    \"vite-plugin-static-copy\": \"^3.1.1\"\n  }\n}\n"
  },
  {
    "path": "packages/extension/playwright.config.ts",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { defineConfig } from '@playwright/test';\n\nimport type { TestOptions } from '../playwright-mcp/tests/fixtures';\n\nexport default defineConfig<TestOptions>({\n  testDir: './tests',\n  fullyParallel: true,\n  forbidOnly: !!process.env.CI,\n  retries: process.env.CI ? 2 : 0,\n  workers: process.env.CI ? 1 : undefined,\n  reporter: 'list',\n  projects: [\n    { name: 'chromium', use: { mcpBrowser: 'chromium' } },\n  ],\n});\n"
  },
  {
    "path": "packages/extension/src/background.ts",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { RelayConnection, debugLog } from './relayConnection';\n\ntype PageMessage = {\n  type: 'connectToMCPRelay';\n  mcpRelayUrl: string;\n} | {\n  type: 'getTabs';\n} | {\n  type: 'connectToTab';\n  tabId?: number;\n  windowId?: number;\n  mcpRelayUrl: string;\n} | {\n  type: 'getConnectionStatus';\n} | {\n  type: 'disconnect';\n};\n\nclass TabShareExtension {\n  private _activeConnection: RelayConnection | undefined;\n  private _connectedTabId: number | null = null;\n  private _pendingTabSelection = new Map<number, { connection: RelayConnection, timerId?: number }>();\n\n  constructor() {\n    chrome.tabs.onRemoved.addListener(this._onTabRemoved.bind(this));\n    chrome.tabs.onUpdated.addListener(this._onTabUpdated.bind(this));\n    chrome.tabs.onActivated.addListener(this._onTabActivated.bind(this));\n    chrome.runtime.onMessage.addListener(this._onMessage.bind(this));\n    chrome.action.onClicked.addListener(this._onActionClicked.bind(this));\n  }\n\n  // Promise-based message handling is not supported in Chrome: https://issues.chromium.org/issues/40753031\n  private _onMessage(message: PageMessage, sender: chrome.runtime.MessageSender, sendResponse: (response: any) => void) {\n    switch (message.type) {\n      case 'connectToMCPRelay':\n        this._connectToRelay(sender.tab!.id!, message.mcpRelayUrl).then(\n            () => sendResponse({ success: true }),\n            (error: any) => sendResponse({ success: false, error: error.message }));\n        return true;\n      case 'getTabs':\n        this._getTabs().then(\n            tabs => sendResponse({ success: true, tabs, currentTabId: sender.tab?.id }),\n            (error: any) => sendResponse({ success: false, error: error.message }));\n        return true;\n      case 'connectToTab':\n        const tabId = message.tabId || sender.tab?.id!;\n        const windowId = message.windowId || sender.tab?.windowId!;\n        this._connectTab(sender.tab!.id!, tabId, windowId, message.mcpRelayUrl!).then(\n            () => sendResponse({ success: true }),\n            (error: any) => sendResponse({ success: false, error: error.message }));\n        return true; // Return true to indicate that the response will be sent asynchronously\n      case 'getConnectionStatus':\n        sendResponse({\n          connectedTabId: this._connectedTabId\n        });\n        return false;\n      case 'disconnect':\n        this._disconnect().then(\n            () => sendResponse({ success: true }),\n            (error: any) => sendResponse({ success: false, error: error.message }));\n        return true;\n    }\n    return false;\n  }\n\n  private async _connectToRelay(selectorTabId: number, mcpRelayUrl: string): Promise<void> {\n    try {\n      debugLog(`Connecting to relay at ${mcpRelayUrl}`);\n      const socket = new WebSocket(mcpRelayUrl);\n      await new Promise<void>((resolve, reject) => {\n        socket.onopen = () => resolve();\n        socket.onerror = () => reject(new Error('WebSocket error'));\n        setTimeout(() => reject(new Error('Connection timeout')), 5000);\n      });\n\n      const connection = new RelayConnection(socket);\n      connection.onclose = () => {\n        debugLog('Connection closed');\n        this._pendingTabSelection.delete(selectorTabId);\n        // TODO: show error in the selector tab?\n      };\n      this._pendingTabSelection.set(selectorTabId, { connection });\n      debugLog(`Connected to MCP relay`);\n    } catch (error: any) {\n      const message = `Failed to connect to MCP relay: ${error.message}`;\n      debugLog(message);\n      throw new Error(message);\n    }\n  }\n\n  private async _connectTab(selectorTabId: number, tabId: number, windowId: number, mcpRelayUrl: string): Promise<void> {\n    try {\n      debugLog(`Connecting tab ${tabId} to relay at ${mcpRelayUrl}`);\n      try {\n        this._activeConnection?.close('Another connection is requested');\n      } catch (error: any) {\n        debugLog(`Error closing active connection:`, error);\n      }\n      await this._setConnectedTabId(null);\n\n      this._activeConnection = this._pendingTabSelection.get(selectorTabId)?.connection;\n      if (!this._activeConnection)\n        throw new Error('No active MCP relay connection');\n      this._pendingTabSelection.delete(selectorTabId);\n\n      this._activeConnection.setTabId(tabId);\n      this._activeConnection.onclose = () => {\n        debugLog('MCP connection closed');\n        this._activeConnection = undefined;\n        void this._setConnectedTabId(null);\n      };\n\n      await Promise.all([\n        this._setConnectedTabId(tabId),\n        chrome.tabs.update(tabId, { active: true }),\n        chrome.windows.update(windowId, { focused: true }),\n      ]);\n      debugLog(`Connected to MCP bridge`);\n    } catch (error: any) {\n      await this._setConnectedTabId(null);\n      debugLog(`Failed to connect tab ${tabId}:`, error.message);\n      throw error;\n    }\n  }\n\n  private async _setConnectedTabId(tabId: number | null): Promise<void> {\n    const oldTabId = this._connectedTabId;\n    this._connectedTabId = tabId;\n    if (oldTabId && oldTabId !== tabId)\n      await this._updateBadge(oldTabId, { text: '' });\n    if (tabId)\n      await this._updateBadge(tabId, { text: '✓', color: '#4CAF50', title: 'Connected to MCP client' });\n  }\n\n  private async _updateBadge(tabId: number, { text, color, title }: { text: string; color?: string, title?: string }): Promise<void> {\n    try {\n      await chrome.action.setBadgeText({ tabId, text });\n      await chrome.action.setTitle({ tabId, title: title || '' });\n      if (color)\n        await chrome.action.setBadgeBackgroundColor({ tabId, color });\n    } catch (error: any) {\n      // Ignore errors as the tab may be closed already.\n    }\n  }\n\n  private async _onTabRemoved(tabId: number): Promise<void> {\n    const pendingConnection = this._pendingTabSelection.get(tabId)?.connection;\n    if (pendingConnection) {\n      this._pendingTabSelection.delete(tabId);\n      pendingConnection.close('Browser tab closed');\n      return;\n    }\n    if (this._connectedTabId !== tabId)\n      return;\n    this._activeConnection?.close('Browser tab closed');\n    this._activeConnection = undefined;\n    this._connectedTabId = null;\n  }\n\n  private _onTabActivated(activeInfo: chrome.tabs.TabActiveInfo) {\n    for (const [tabId, pending] of this._pendingTabSelection) {\n      if (tabId === activeInfo.tabId) {\n        if (pending.timerId) {\n          clearTimeout(pending.timerId);\n          pending.timerId = undefined;\n        }\n        continue;\n      }\n      if (!pending.timerId) {\n        pending.timerId = setTimeout(() => {\n          const existed = this._pendingTabSelection.delete(tabId);\n          if (existed) {\n            pending.connection.close('Tab has been inactive for 5 seconds');\n            chrome.tabs.sendMessage(tabId, { type: 'connectionTimeout' });\n          }\n        }, 5000);\n      }\n    }\n  }\n\n  private _onTabUpdated(tabId: number, changeInfo: chrome.tabs.TabChangeInfo, tab: chrome.tabs.Tab) {\n    if (this._connectedTabId === tabId)\n      void this._setConnectedTabId(tabId);\n  }\n\n  private async _getTabs(): Promise<chrome.tabs.Tab[]> {\n    const tabs = await chrome.tabs.query({});\n    return tabs.filter(tab => tab.url && !['chrome:', 'edge:', 'devtools:'].some(scheme => tab.url!.startsWith(scheme)));\n  }\n\n  private async _onActionClicked(): Promise<void> {\n    await chrome.tabs.create({\n      url: chrome.runtime.getURL('status.html'),\n      active: true\n    });\n  }\n\n  private async _disconnect(): Promise<void> {\n    this._activeConnection?.close('User disconnected');\n    this._activeConnection = undefined;\n    await this._setConnectedTabId(null);\n  }\n}\n\nnew TabShareExtension();\n"
  },
  {
    "path": "packages/extension/src/relayConnection.ts",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nexport function debugLog(...args: unknown[]): void {\n  const enabled = true;\n  if (enabled) {\n    // eslint-disable-next-line no-console\n    console.log('[Extension]', ...args);\n  }\n}\n\ntype ProtocolCommand = {\n  id: number;\n  method: string;\n  params?: any;\n};\n\ntype ProtocolResponse = {\n  id?: number;\n  method?: string;\n  params?: any;\n  result?: any;\n  error?: string;\n};\n\nexport class RelayConnection {\n  private _debuggee: chrome.debugger.Debuggee;\n  private _ws: WebSocket;\n  private _eventListener: (source: chrome.debugger.DebuggerSession, method: string, params: any) => void;\n  private _detachListener: (source: chrome.debugger.Debuggee, reason: string) => void;\n  private _tabPromise: Promise<void>;\n  private _tabPromiseResolve!: () => void;\n  private _closed = false;\n\n  onclose?: () => void;\n\n  constructor(ws: WebSocket) {\n    this._debuggee = { };\n    this._tabPromise = new Promise(resolve => this._tabPromiseResolve = resolve);\n    this._ws = ws;\n    this._ws.onmessage = this._onMessage.bind(this);\n    this._ws.onclose = () => this._onClose();\n    // Store listeners for cleanup\n    this._eventListener = this._onDebuggerEvent.bind(this);\n    this._detachListener = this._onDebuggerDetach.bind(this);\n    chrome.debugger.onEvent.addListener(this._eventListener);\n    chrome.debugger.onDetach.addListener(this._detachListener);\n  }\n\n  // Either setTabId or close is called after creating the connection.\n  setTabId(tabId: number): void {\n    this._debuggee = { tabId };\n    this._tabPromiseResolve();\n  }\n\n  close(message: string): void {\n    this._ws.close(1000, message);\n    // ws.onclose is called asynchronously, so we call it here to avoid forwarding\n    // CDP events to the closed connection.\n    this._onClose();\n  }\n\n  private _onClose() {\n    if (this._closed)\n      return;\n    this._closed = true;\n    chrome.debugger.onEvent.removeListener(this._eventListener);\n    chrome.debugger.onDetach.removeListener(this._detachListener);\n    chrome.debugger.detach(this._debuggee).catch(() => {});\n    this.onclose?.();\n  }\n\n  private _onDebuggerEvent(source: chrome.debugger.DebuggerSession, method: string, params: any): void {\n    if (source.tabId !== this._debuggee.tabId)\n      return;\n    debugLog('Forwarding CDP event:', method, params);\n    const sessionId = source.sessionId;\n    this._sendMessage({\n      method: 'forwardCDPEvent',\n      params: {\n        sessionId,\n        method,\n        params,\n      },\n    });\n  }\n\n  private _onDebuggerDetach(source: chrome.debugger.Debuggee, reason: string): void {\n    if (source.tabId !== this._debuggee.tabId)\n      return;\n    this.close(`Debugger detached: ${reason}`);\n    this._debuggee = { };\n  }\n\n  private _onMessage(event: MessageEvent): void {\n    this._onMessageAsync(event).catch(e => debugLog('Error handling message:', e));\n  }\n\n  private async _onMessageAsync(event: MessageEvent): Promise<void> {\n    let message: ProtocolCommand;\n    try {\n      message = JSON.parse(event.data);\n    } catch (error: any) {\n      debugLog('Error parsing message:', error);\n      this._sendError(-32700, `Error parsing message: ${error.message}`);\n      return;\n    }\n\n    debugLog('Received message:', message);\n\n    const response: ProtocolResponse = {\n      id: message.id,\n    };\n    try {\n      response.result = await this._handleCommand(message);\n    } catch (error: any) {\n      debugLog('Error handling command:', error);\n      response.error = error.message;\n    }\n    debugLog('Sending response:', response);\n    this._sendMessage(response);\n  }\n\n  private async _handleCommand(message: ProtocolCommand): Promise<any> {\n    if (message.method === 'attachToTab') {\n      await this._tabPromise;\n      debugLog('Attaching debugger to tab:', this._debuggee);\n      await chrome.debugger.attach(this._debuggee, '1.3');\n      const result: any = await chrome.debugger.sendCommand(this._debuggee, 'Target.getTargetInfo');\n      return {\n        targetInfo: result?.targetInfo,\n      };\n    }\n    if (!this._debuggee.tabId)\n      throw new Error('No tab is connected. Please go to the Playwright MCP extension and select the tab you want to connect to.');\n    if (message.method === 'forwardCDPCommand') {\n      const { sessionId, method, params } = message.params;\n      debugLog('CDP command:', method, params);\n      const debuggerSession: chrome.debugger.DebuggerSession = {\n        ...this._debuggee,\n        sessionId,\n      };\n      // Forward CDP command to chrome.debugger\n      return await chrome.debugger.sendCommand(\n          debuggerSession,\n          method,\n          params\n      );\n    }\n  }\n\n  private _sendError(code: number, message: string): void {\n    this._sendMessage({\n      error: {\n        code,\n        message,\n      },\n    });\n  }\n\n  private _sendMessage(message: any): void {\n    if (this._ws.readyState === WebSocket.OPEN)\n      this._ws.send(JSON.stringify(message));\n  }\n}\n"
  },
  {
    "path": "packages/extension/src/ui/authToken.css",
    "content": "/*\n  Copyright (c) Microsoft Corporation.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License.\n*/\n\n.auth-token-section {\n  margin: 16px 0;\n  padding: 16px;\n  background-color: #f6f8fa;\n  border-radius: 6px;\n}\n\n.auth-token-description {\n  font-size: 12px;\n  color: #656d76;\n  margin-bottom: 12px;\n}\n\n.auth-token-container {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  background-color: #ffffff;\n  padding: 8px;\n}\n\n.auth-token-code {\n  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n  font-size: 12px;\n  color: #1f2328;\n  border: none;\n  flex: 1;\n  padding: 0;\n  word-break: break-all;\n}\n\n.auth-token-refresh {\n  flex: none;\n  height: 24px;\n  width: 24px;\n  border: none;\n  outline: none;\n  color: var(--color-fg-muted);\n  background: transparent;\n  padding: 4px;\n  cursor: pointer;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 4px;\n}\n\n.auth-token-refresh svg {\n  margin: 0;\n}\n\n.auth-token-refresh:not(:disabled):hover {\n  background-color: var(--color-btn-selected-bg);\n}\n\n.auth-token-example-section {\n  margin-top: 16px;\n}\n\n.auth-token-example-toggle {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  background: none;\n  border: none;\n  padding: 8px 0;\n  font-size: 12px;\n  color: #656d76;\n  cursor: pointer;\n  outline: none;\n  text-align: left;\n  width: 100%;\n}\n\n.auth-token-example-toggle:hover {\n  color: #1f2328;\n}\n\n.auth-token-chevron {\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  transform: rotate(-90deg);\n  flex-shrink: 0;\n}\n\n.auth-token-chevron.expanded {\n  transform: rotate(0deg);\n}\n\n.auth-token-chevron svg {\n  width: 12px;\n  height: 12px;\n}\n\n.auth-token-chevron .octicon {\n  margin: 0px;\n}\n\n.auth-token-example-content {\n  margin-top: 12px;\n  padding: 12px 0;\n}\n\n.auth-token-example-description {\n  font-size: 12px;\n  color: #656d76;\n  margin-bottom: 12px;\n}\n\n.auth-token-example-config {\n  display: flex;\n  align-items: flex-start;\n  gap: 8px;\n  background-color: #ffffff;\n  padding: 12px;\n}\n\n.auth-token-example-code {\n  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n  font-size: 11px;\n  color: #1f2328;\n  white-space: pre;\n  flex: 1;\n  line-height: 1.4;\n}\n"
  },
  {
    "path": "packages/extension/src/ui/authToken.tsx",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport React, { useCallback, useState } from 'react';\nimport { CopyToClipboard } from './copyToClipboard';\nimport * as icons from './icons';\nimport './authToken.css';\n\nexport const AuthTokenSection: React.FC<{}> = ({}) => {\n  const [authToken, setAuthToken] = useState<string>(getOrCreateAuthToken);\n  const [isExampleExpanded, setIsExampleExpanded] = useState<boolean>(false);\n\n  const onRegenerateToken = useCallback(() => {\n    const newToken = generateAuthToken();\n    localStorage.setItem('auth-token', newToken);\n    setAuthToken(newToken);\n  }, []);\n\n  const toggleExample = useCallback(() => {\n    setIsExampleExpanded(!isExampleExpanded);\n  }, [isExampleExpanded]);\n\n  return (\n    <div className='auth-token-section'>\n      <div className='auth-token-description'>\n        Set this environment variable to bypass the connection dialog:\n      </div>\n      <div className='auth-token-container'>\n        <code className='auth-token-code'>{authTokenCode(authToken)}</code>\n        <button className='auth-token-refresh' title='Generate new token' aria-label='Generate new token'onClick={onRegenerateToken}>{icons.refresh()}</button>\n        <CopyToClipboard value={authTokenCode(authToken)} />\n      </div>\n\n      <div className='auth-token-example-section'>\n        <button\n          className='auth-token-example-toggle'\n          onClick={toggleExample}\n          aria-expanded={isExampleExpanded}\n          title={isExampleExpanded ? 'Hide example config' : 'Show example config'}\n        >\n          <span className={`auth-token-chevron ${isExampleExpanded ? 'expanded' : ''}`}>\n            {icons.chevronDown()}\n          </span>\n          Example MCP server configuration\n        </button>\n\n        {isExampleExpanded && (\n          <div className='auth-token-example-content'>\n            <div className='auth-token-example-description'>\n              Add this configuration to your MCP client (e.g., VS Code) to connect to the Playwright MCP Bridge:\n            </div>\n            <div className='auth-token-example-config'>\n              <code className='auth-token-example-code'>{exampleConfig(authToken)}</code>\n              <CopyToClipboard value={exampleConfig(authToken)} />\n            </div>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\nfunction authTokenCode(authToken: string) {\n  return `PLAYWRIGHT_MCP_EXTENSION_TOKEN=${authToken}`;\n}\n\nfunction exampleConfig(authToken: string) {\n  return `{\n  \"mcpServers\": {\n    \"playwright\": {\n      \"command\": \"npx\",\n      \"args\": [\"@playwright/mcp@latest\", \"--extension\"],\n      \"env\": {\n        \"PLAYWRIGHT_MCP_EXTENSION_TOKEN\":\n          \"${authToken}\"\n      }\n    }\n  }\n}`;\n}\n\nfunction generateAuthToken(): string {\n  // Generate a cryptographically secure random token\n  const array = new Uint8Array(32);\n  crypto.getRandomValues(array);\n  // Convert to base64 and make it URL-safe\n  return btoa(String.fromCharCode.apply(null, Array.from(array)))\n      .replace(/[+/=]/g, match => {\n        switch (match) {\n          case '+': return '-';\n          case '/': return '_';\n          case '=': return '';\n          default: return match;\n        }\n      });\n}\n\nexport const getOrCreateAuthToken = (): string => {\n  let token = localStorage.getItem('auth-token');\n  if (!token) {\n    token = generateAuthToken();\n    localStorage.setItem('auth-token', token);\n  }\n  return token;\n}\n"
  },
  {
    "path": "packages/extension/src/ui/colors.css",
    "content": "/* The MIT License (MIT)\n\nCopyright (c) 2021 GitHub Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE. */\n\n:root {\n  --color-canvas-default-transparent: rgba(255,255,255,0);\n  --color-marketing-icon-primary: #218bff;\n  --color-marketing-icon-secondary: #54aeff;\n  --color-diff-blob-addition-num-text: #24292f;\n  --color-diff-blob-addition-fg: #24292f;\n  --color-diff-blob-addition-num-bg: #CCFFD8;\n  --color-diff-blob-addition-line-bg: #E6FFEC;\n  --color-diff-blob-addition-word-bg: #ABF2BC;\n  --color-diff-blob-deletion-num-text: #24292f;\n  --color-diff-blob-deletion-fg: #24292f;\n  --color-diff-blob-deletion-num-bg: #FFD7D5;\n  --color-diff-blob-deletion-line-bg: #FFEBE9;\n  --color-diff-blob-deletion-word-bg: rgba(255,129,130,0.4);\n  --color-diff-blob-hunk-num-bg: rgba(84,174,255,0.4);\n  --color-diff-blob-expander-icon: #57606a;\n  --color-diff-blob-selected-line-highlight-mix-blend-mode: multiply;\n  --color-diffstat-deletion-border: rgba(27,31,36,0.15);\n  --color-diffstat-addition-border: rgba(27,31,36,0.15);\n  --color-diffstat-addition-bg: #2da44e;\n  --color-search-keyword-hl: #fff8c5;\n  --color-prettylights-syntax-comment: #6e7781;\n  --color-prettylights-syntax-constant: #0550ae;\n  --color-prettylights-syntax-entity: #8250df;\n  --color-prettylights-syntax-storage-modifier-import: #24292f;\n  --color-prettylights-syntax-entity-tag: #116329;\n  --color-prettylights-syntax-keyword: #cf222e;\n  --color-prettylights-syntax-string: #0a3069;\n  --color-prettylights-syntax-variable: #953800;\n  --color-prettylights-syntax-brackethighlighter-unmatched: #82071e;\n  --color-prettylights-syntax-invalid-illegal-text: #f6f8fa;\n  --color-prettylights-syntax-invalid-illegal-bg: #82071e;\n  --color-prettylights-syntax-carriage-return-text: #f6f8fa;\n  --color-prettylights-syntax-carriage-return-bg: #cf222e;\n  --color-prettylights-syntax-string-regexp: #116329;\n  --color-prettylights-syntax-markup-list: #3b2300;\n  --color-prettylights-syntax-markup-heading: #0550ae;\n  --color-prettylights-syntax-markup-italic: #24292f;\n  --color-prettylights-syntax-markup-bold: #24292f;\n  --color-prettylights-syntax-markup-deleted-text: #82071e;\n  --color-prettylights-syntax-markup-deleted-bg: #FFEBE9;\n  --color-prettylights-syntax-markup-inserted-text: #116329;\n  --color-prettylights-syntax-markup-inserted-bg: #dafbe1;\n  --color-prettylights-syntax-markup-changed-text: #953800;\n  --color-prettylights-syntax-markup-changed-bg: #ffd8b5;\n  --color-prettylights-syntax-markup-ignored-text: #eaeef2;\n  --color-prettylights-syntax-markup-ignored-bg: #0550ae;\n  --color-prettylights-syntax-meta-diff-range: #8250df;\n  --color-prettylights-syntax-brackethighlighter-angle: #57606a;\n  --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;\n  --color-prettylights-syntax-constant-other-reference-link: #0a3069;\n  --color-codemirror-text: #24292f;\n  --color-codemirror-bg: #ffffff;\n  --color-codemirror-gutters-bg: #ffffff;\n  --color-codemirror-guttermarker-text: #ffffff;\n  --color-codemirror-guttermarker-subtle-text: #6e7781;\n  --color-codemirror-linenumber-text: #57606a;\n  --color-codemirror-cursor: #24292f;\n  --color-codemirror-selection-bg: rgba(84,174,255,0.4);\n  --color-codemirror-activeline-bg: rgba(234,238,242,0.5);\n  --color-codemirror-matchingbracket-text: #24292f;\n  --color-codemirror-lines-bg: #ffffff;\n  --color-codemirror-syntax-comment: #24292f;\n  --color-codemirror-syntax-constant: #0550ae;\n  --color-codemirror-syntax-entity: #8250df;\n  --color-codemirror-syntax-keyword: #cf222e;\n  --color-codemirror-syntax-storage: #cf222e;\n  --color-codemirror-syntax-string: #0a3069;\n  --color-codemirror-syntax-support: #0550ae;\n  --color-codemirror-syntax-variable: #953800;\n  --color-checks-bg: #24292f;\n  --color-checks-run-border-width: 0px;\n  --color-checks-container-border-width: 0px;\n  --color-checks-text-primary: #f6f8fa;\n  --color-checks-text-secondary: #8c959f;\n  --color-checks-text-link: #54aeff;\n  --color-checks-btn-icon: #afb8c1;\n  --color-checks-btn-hover-icon: #f6f8fa;\n  --color-checks-btn-hover-bg: rgba(255,255,255,0.125);\n  --color-checks-input-text: #eaeef2;\n  --color-checks-input-placeholder-text: #8c959f;\n  --color-checks-input-focus-text: #8c959f;\n  --color-checks-input-bg: #32383f;\n  --color-checks-input-shadow: none;\n  --color-checks-donut-error: #fa4549;\n  --color-checks-donut-pending: #bf8700;\n  --color-checks-donut-success: #2da44e;\n  --color-checks-donut-neutral: #afb8c1;\n  --color-checks-dropdown-text: #afb8c1;\n  --color-checks-dropdown-bg: #32383f;\n  --color-checks-dropdown-border: #424a53;\n  --color-checks-dropdown-shadow: rgba(27,31,36,0.3);\n  --color-checks-dropdown-hover-text: #f6f8fa;\n  --color-checks-dropdown-hover-bg: #424a53;\n  --color-checks-dropdown-btn-hover-text: #f6f8fa;\n  --color-checks-dropdown-btn-hover-bg: #32383f;\n  --color-checks-scrollbar-thumb-bg: #57606a;\n  --color-checks-header-label-text: #d0d7de;\n  --color-checks-header-label-open-text: #f6f8fa;\n  --color-checks-header-border: #32383f;\n  --color-checks-header-icon: #8c959f;\n  --color-checks-line-text: #d0d7de;\n  --color-checks-line-num-text: rgba(140,149,159,0.75);\n  --color-checks-line-timestamp-text: #8c959f;\n  --color-checks-line-hover-bg: #32383f;\n  --color-checks-line-selected-bg: rgba(33,139,255,0.15);\n  --color-checks-line-selected-num-text: #54aeff;\n  --color-checks-line-dt-fm-text: #24292f;\n  --color-checks-line-dt-fm-bg: #9a6700;\n  --color-checks-gate-bg: rgba(125,78,0,0.15);\n  --color-checks-gate-text: #d0d7de;\n  --color-checks-gate-waiting-text: #afb8c1;\n  --color-checks-step-header-open-bg: #32383f;\n  --color-checks-step-error-text: #ff8182;\n  --color-checks-step-warning-text: #d4a72c;\n  --color-checks-logline-text: #8c959f;\n  --color-checks-logline-num-text: rgba(140,149,159,0.75);\n  --color-checks-logline-debug-text: #c297ff;\n  --color-checks-logline-error-text: #d0d7de;\n  --color-checks-logline-error-num-text: #ff8182;\n  --color-checks-logline-error-bg: rgba(164,14,38,0.15);\n  --color-checks-logline-warning-text: #d0d7de;\n  --color-checks-logline-warning-num-text: #d4a72c;\n  --color-checks-logline-warning-bg: rgba(125,78,0,0.15);\n  --color-checks-logline-command-text: #54aeff;\n  --color-checks-logline-section-text: #4ac26b;\n  --color-checks-ansi-black: #24292f;\n  --color-checks-ansi-black-bright: #32383f;\n  --color-checks-ansi-white: #d0d7de;\n  --color-checks-ansi-white-bright: #d0d7de;\n  --color-checks-ansi-gray: #8c959f;\n  --color-checks-ansi-red: #ff8182;\n  --color-checks-ansi-red-bright: #ffaba8;\n  --color-checks-ansi-green: #4ac26b;\n  --color-checks-ansi-green-bright: #6fdd8b;\n  --color-checks-ansi-yellow: #d4a72c;\n  --color-checks-ansi-yellow-bright: #eac54f;\n  --color-checks-ansi-blue: #54aeff;\n  --color-checks-ansi-blue-bright: #80ccff;\n  --color-checks-ansi-magenta: #c297ff;\n  --color-checks-ansi-magenta-bright: #d8b9ff;\n  --color-checks-ansi-cyan: #76e3ea;\n  --color-checks-ansi-cyan-bright: #b3f0ff;\n  --color-project-header-bg: #24292f;\n  --color-project-sidebar-bg: #ffffff;\n  --color-project-gradient-in: #ffffff;\n  --color-project-gradient-out: rgba(255,255,255,0);\n  --color-mktg-success: rgba(36,146,67,1);\n  --color-mktg-info: rgba(19,119,234,1);\n  --color-mktg-bg-shade-gradient-top: rgba(27,31,36,0.065);\n  --color-mktg-bg-shade-gradient-bottom: rgba(27,31,36,0);\n  --color-mktg-btn-bg-top: hsla(228,82%,66%,1);\n  --color-mktg-btn-bg-bottom: #4969ed;\n  --color-mktg-btn-bg-overlay-top: hsla(228,74%,59%,1);\n  --color-mktg-btn-bg-overlay-bottom: #3355e0;\n  --color-mktg-btn-text: #ffffff;\n  --color-mktg-btn-primary-bg-top: hsla(137,56%,46%,1);\n  --color-mktg-btn-primary-bg-bottom: #2ea44f;\n  --color-mktg-btn-primary-bg-overlay-top: hsla(134,60%,38%,1);\n  --color-mktg-btn-primary-bg-overlay-bottom: #22863a;\n  --color-mktg-btn-primary-text: #ffffff;\n  --color-mktg-btn-enterprise-bg-top: hsla(249,100%,72%,1);\n  --color-mktg-btn-enterprise-bg-bottom: #6f57ff;\n  --color-mktg-btn-enterprise-bg-overlay-top: hsla(248,65%,63%,1);\n  --color-mktg-btn-enterprise-bg-overlay-bottom: #614eda;\n  --color-mktg-btn-enterprise-text: #ffffff;\n  --color-mktg-btn-outline-text: #4969ed;\n  --color-mktg-btn-outline-border: rgba(73,105,237,0.3);\n  --color-mktg-btn-outline-hover-text: #3355e0;\n  --color-mktg-btn-outline-hover-border: rgba(51,85,224,0.5);\n  --color-mktg-btn-outline-focus-border: #4969ed;\n  --color-mktg-btn-outline-focus-border-inset: rgba(73,105,237,0.5);\n  --color-mktg-btn-dark-text: #ffffff;\n  --color-mktg-btn-dark-border: rgba(255,255,255,0.3);\n  --color-mktg-btn-dark-hover-text: #ffffff;\n  --color-mktg-btn-dark-hover-border: rgba(255,255,255,0.5);\n  --color-mktg-btn-dark-focus-border: #ffffff;\n  --color-mktg-btn-dark-focus-border-inset: rgba(255,255,255,0.5);\n  --color-avatar-bg: #ffffff;\n  --color-avatar-border: rgba(27,31,36,0.15);\n  --color-avatar-stack-fade: #afb8c1;\n  --color-avatar-stack-fade-more: #d0d7de;\n  --color-avatar-child-shadow: -2px -2px 0 rgba(255,255,255,0.8);\n  --color-topic-tag-border: rgba(0,0,0,0);\n  --color-select-menu-backdrop-border: rgba(0,0,0,0);\n  --color-select-menu-tap-highlight: rgba(175,184,193,0.5);\n  --color-select-menu-tap-focus-bg: #b6e3ff;\n  --color-overlay-shadow: 0 1px 3px rgba(27,31,36,0.12), 0 8px 24px rgba(66,74,83,0.12);\n  --color-header-text: rgba(255,255,255,0.7);\n  --color-header-bg: #24292f;\n  --color-header-logo: #ffffff;\n  --color-header-search-bg: #24292f;\n  --color-header-search-border: #57606a;\n  --color-sidenav-selected-bg: #ffffff;\n  --color-menu-bg-active: rgba(0,0,0,0);\n  --color-control-transparent-bg-hover: #818b981a;\n  --color-input-disabled-bg: rgba(175,184,193,0.2);\n  --color-timeline-badge-bg: #eaeef2;\n  --color-ansi-black: #24292f;\n  --color-ansi-black-bright: #57606a;\n  --color-ansi-white: #6e7781;\n  --color-ansi-white-bright: #8c959f;\n  --color-ansi-gray: #6e7781;\n  --color-ansi-red: #cf222e;\n  --color-ansi-red-bright: #a40e26;\n  --color-ansi-green: #116329;\n  --color-ansi-green-bright: #1a7f37;\n  --color-ansi-yellow: #4d2d00;\n  --color-ansi-yellow-bright: #633c01;\n  --color-ansi-blue: #0969da;\n  --color-ansi-blue-bright: #218bff;\n  --color-ansi-magenta: #8250df;\n  --color-ansi-magenta-bright: #a475f9;\n  --color-ansi-cyan: #1b7c83;\n  --color-ansi-cyan-bright: #3192aa;\n  --color-btn-text: #24292f;\n  --color-btn-bg: #f6f8fa;\n  --color-btn-border: rgba(27,31,36,0.15);\n  --color-btn-shadow: 0 1px 0 rgba(27,31,36,0.04);\n  --color-btn-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.25);\n  --color-btn-hover-bg: #f3f4f6;\n  --color-btn-hover-border: rgba(27,31,36,0.15);\n  --color-btn-active-bg: hsla(220,14%,93%,1);\n  --color-btn-active-border: rgba(27,31,36,0.15);\n  --color-btn-selected-bg: hsla(220,14%,94%,1);\n  --color-btn-focus-bg: #f6f8fa;\n  --color-btn-focus-border: rgba(27,31,36,0.15);\n  --color-btn-focus-shadow: 0 0 0 3px rgba(9,105,218,0.3);\n  --color-btn-shadow-active: inset 0 0.15em 0.3em rgba(27,31,36,0.15);\n  --color-btn-shadow-input-focus: 0 0 0 0.2em rgba(9,105,218,0.3);\n  --color-btn-counter-bg: rgba(27,31,36,0.08);\n  --color-btn-primary-text: #ffffff;\n  --color-btn-primary-bg: #2da44e;\n  --color-btn-primary-border: rgba(27,31,36,0.15);\n  --color-btn-primary-shadow: 0 1px 0 rgba(27,31,36,0.1);\n  --color-btn-primary-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.03);\n  --color-btn-primary-hover-bg: #2c974b;\n  --color-btn-primary-hover-border: rgba(27,31,36,0.15);\n  --color-btn-primary-selected-bg: hsla(137,55%,36%,1);\n  --color-btn-primary-selected-shadow: inset 0 1px 0 rgba(0,45,17,0.2);\n  --color-btn-primary-disabled-text: rgba(255,255,255,0.8);\n  --color-btn-primary-disabled-bg: #94d3a2;\n  --color-btn-primary-disabled-border: rgba(27,31,36,0.15);\n  --color-btn-primary-focus-bg: #2da44e;\n  --color-btn-primary-focus-border: rgba(27,31,36,0.15);\n  --color-btn-primary-focus-shadow: 0 0 0 3px rgba(45,164,78,0.4);\n  --color-btn-primary-icon: rgba(255,255,255,0.8);\n  --color-btn-primary-counter-bg: rgba(255,255,255,0.2);\n  --color-btn-outline-text: #0969da;\n  --color-btn-outline-hover-text: #ffffff;\n  --color-btn-outline-hover-bg: #0969da;\n  --color-btn-outline-hover-border: rgba(27,31,36,0.15);\n  --color-btn-outline-hover-shadow: 0 1px 0 rgba(27,31,36,0.1);\n  --color-btn-outline-hover-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.03);\n  --color-btn-outline-hover-counter-bg: rgba(255,255,255,0.2);\n  --color-btn-outline-selected-text: #ffffff;\n  --color-btn-outline-selected-bg: hsla(212,92%,42%,1);\n  --color-btn-outline-selected-border: rgba(27,31,36,0.15);\n  --color-btn-outline-selected-shadow: inset 0 1px 0 rgba(0,33,85,0.2);\n  --color-btn-outline-disabled-text: rgba(9,105,218,0.5);\n  --color-btn-outline-disabled-bg: #f6f8fa;\n  --color-btn-outline-disabled-counter-bg: rgba(9,105,218,0.05);\n  --color-btn-outline-focus-border: rgba(27,31,36,0.15);\n  --color-btn-outline-focus-shadow: 0 0 0 3px rgba(5,80,174,0.4);\n  --color-btn-outline-counter-bg: rgba(9,105,218,0.1);\n  --color-btn-danger-text: #cf222e;\n  --color-btn-danger-hover-text: #ffffff;\n  --color-btn-danger-hover-bg: #a40e26;\n  --color-btn-danger-hover-border: rgba(27,31,36,0.15);\n  --color-btn-danger-hover-shadow: 0 1px 0 rgba(27,31,36,0.1);\n  --color-btn-danger-hover-inset-shadow: inset 0 1px 0 rgba(255,255,255,0.03);\n  --color-btn-danger-hover-counter-bg: rgba(255,255,255,0.2);\n  --color-btn-danger-selected-text: #ffffff;\n  --color-btn-danger-selected-bg: hsla(356,72%,44%,1);\n  --color-btn-danger-selected-border: rgba(27,31,36,0.15);\n  --color-btn-danger-selected-shadow: inset 0 1px 0 rgba(76,0,20,0.2);\n  --color-btn-danger-disabled-text: rgba(207,34,46,0.5);\n  --color-btn-danger-disabled-bg: #f6f8fa;\n  --color-btn-danger-disabled-counter-bg: rgba(207,34,46,0.05);\n  --color-btn-danger-focus-border: rgba(27,31,36,0.15);\n  --color-btn-danger-focus-shadow: 0 0 0 3px rgba(164,14,38,0.4);\n  --color-btn-danger-counter-bg: rgba(207,34,46,0.1);\n  --color-btn-danger-icon: #cf222e;\n  --color-btn-danger-hover-icon: #ffffff;\n  --color-underlinenav-icon: #6e7781;\n  --color-underlinenav-border-hover: rgba(175,184,193,0.2);\n  --color-fg-default: #24292f;\n  --color-fg-muted: #57606a;\n  --color-fg-subtle: #6e7781;\n  --color-fg-on-emphasis: #ffffff;\n  --color-canvas-default: #ffffff;\n  --color-canvas-overlay: #ffffff;\n  --color-canvas-inset: #f6f8fa;\n  --color-canvas-subtle: #f6f8fa;\n  --color-border-default: #d0d7de;\n  --color-border-muted: hsla(210,18%,87%,1);\n  --color-border-subtle: rgba(27,31,36,0.15);\n  --color-shadow-small: 0 1px 0 rgba(27,31,36,0.04);\n  --color-shadow-medium: 0 3px 6px rgba(140,149,159,0.15);\n  --color-shadow-large: 0 8px 24px rgba(140,149,159,0.2);\n  --color-shadow-extra-large: 0 12px 28px rgba(140,149,159,0.3);\n  --color-neutral-emphasis-plus: #24292f;\n  --color-neutral-emphasis: #6e7781;\n  --color-neutral-muted: rgba(175,184,193,0.2);\n  --color-neutral-subtle: rgba(234,238,242,0.5);\n  --color-accent-fg: #0969da;\n  --color-accent-emphasis: #0969da;\n  --color-accent-muted: rgba(84,174,255,0.4);\n  --color-accent-subtle: #ddf4ff;\n  --color-success-fg: #1a7f37;\n  --color-success-emphasis: #2da44e;\n  --color-success-muted: rgba(74,194,107,0.4);\n  --color-success-subtle: #dafbe1;\n  --color-attention-fg: #9a6700;\n  --color-attention-emphasis: #bf8700;\n  --color-attention-muted: rgba(212,167,44,0.4);\n  --color-attention-subtle: #fff8c5;\n  --color-severe-fg: #bc4c00;\n  --color-severe-emphasis: #bc4c00;\n  --color-severe-muted: rgba(251,143,68,0.4);\n  --color-severe-subtle: #fff1e5;\n  --color-danger-fg: #cf222e;\n  --color-danger-emphasis: #cf222e;\n  --color-danger-muted: rgba(255,129,130,0.4);\n  --color-danger-subtle: #FFEBE9;\n  --color-done-fg: #8250df;\n  --color-done-emphasis: #8250df;\n  --color-done-muted: rgba(194,151,255,0.4);\n  --color-done-subtle: #fbefff;\n  --color-sponsors-fg: #bf3989;\n  --color-sponsors-emphasis: #bf3989;\n  --color-sponsors-muted: rgba(255,128,200,0.4);\n  --color-sponsors-subtle: #ffeff7;\n  --color-primer-canvas-backdrop: rgba(27,31,36,0.5);\n  --color-primer-canvas-sticky: rgba(255,255,255,0.95);\n  --color-primer-border-active: #FD8C73;\n  --color-primer-border-contrast: rgba(27,31,36,0.1);\n  --color-primer-shadow-highlight: inset 0 1px 0 rgba(255,255,255,0.25);\n  --color-primer-shadow-inset: inset 0 1px 0 rgba(208,215,222,0.2);\n  --color-primer-shadow-focus: 0 0 0 3px rgba(9,105,218,0.3);\n  --color-scale-black: #1b1f24;\n  --color-scale-white: #ffffff;\n  --color-scale-gray-0: #f6f8fa;\n  --color-scale-gray-1: #eaeef2;\n  --color-scale-gray-2: #d0d7de;\n  --color-scale-gray-3: #afb8c1;\n  --color-scale-gray-4: #8c959f;\n  --color-scale-gray-5: #6e7781;\n  --color-scale-gray-6: #57606a;\n  --color-scale-gray-7: #424a53;\n  --color-scale-gray-8: #32383f;\n  --color-scale-gray-9: #24292f;\n  --color-scale-blue-0: #ddf4ff;\n  --color-scale-blue-1: #b6e3ff;\n  --color-scale-blue-2: #80ccff;\n  --color-scale-blue-3: #54aeff;\n  --color-scale-blue-4: #218bff;\n  --color-scale-blue-5: #0969da;\n  --color-scale-blue-6: #0550ae;\n  --color-scale-blue-7: #033d8b;\n  --color-scale-blue-8: #0a3069;\n  --color-scale-blue-9: #002155;\n  --color-scale-green-0: #dafbe1;\n  --color-scale-green-1: #aceebb;\n  --color-scale-green-2: #6fdd8b;\n  --color-scale-green-3: #4ac26b;\n  --color-scale-green-4: #2da44e;\n  --color-scale-green-5: #1a7f37;\n  --color-scale-green-6: #116329;\n  --color-scale-green-7: #044f1e;\n  --color-scale-green-8: #003d16;\n  --color-scale-green-9: #002d11;\n  --color-scale-yellow-0: #fff8c5;\n  --color-scale-yellow-1: #fae17d;\n  --color-scale-yellow-2: #eac54f;\n  --color-scale-yellow-3: #d4a72c;\n  --color-scale-yellow-4: #bf8700;\n  --color-scale-yellow-5: #9a6700;\n  --color-scale-yellow-6: #7d4e00;\n  --color-scale-yellow-7: #633c01;\n  --color-scale-yellow-8: #4d2d00;\n  --color-scale-yellow-9: #3b2300;\n  --color-scale-orange-0: #fff1e5;\n  --color-scale-orange-1: #ffd8b5;\n  --color-scale-orange-2: #ffb77c;\n  --color-scale-orange-3: #fb8f44;\n  --color-scale-orange-4: #e16f24;\n  --color-scale-orange-5: #bc4c00;\n  --color-scale-orange-6: #953800;\n  --color-scale-orange-7: #762c00;\n  --color-scale-orange-8: #5c2200;\n  --color-scale-orange-9: #471700;\n  --color-scale-red-0: #FFEBE9;\n  --color-scale-red-1: #ffcecb;\n  --color-scale-red-2: #ffaba8;\n  --color-scale-red-3: #ff8182;\n  --color-scale-red-4: #fa4549;\n  --color-scale-red-5: #cf222e;\n  --color-scale-red-6: #a40e26;\n  --color-scale-red-7: #82071e;\n  --color-scale-red-8: #660018;\n  --color-scale-red-9: #4c0014;\n  --color-scale-purple-0: #fbefff;\n  --color-scale-purple-1: #ecd8ff;\n  --color-scale-purple-2: #d8b9ff;\n  --color-scale-purple-3: #c297ff;\n  --color-scale-purple-4: #a475f9;\n  --color-scale-purple-5: #8250df;\n  --color-scale-purple-6: #6639ba;\n  --color-scale-purple-7: #512a97;\n  --color-scale-purple-8: #3e1f79;\n  --color-scale-purple-9: #2e1461;\n  --color-scale-pink-0: #ffeff7;\n  --color-scale-pink-1: #ffd3eb;\n  --color-scale-pink-2: #ffadda;\n  --color-scale-pink-3: #ff80c8;\n  --color-scale-pink-4: #e85aad;\n  --color-scale-pink-5: #bf3989;\n  --color-scale-pink-6: #99286e;\n  --color-scale-pink-7: #772057;\n  --color-scale-pink-8: #611347;\n  --color-scale-pink-9: #4d0336;\n  --color-scale-coral-0: #FFF0EB;\n  --color-scale-coral-1: #FFD6CC;\n  --color-scale-coral-2: #FFB4A1;\n  --color-scale-coral-3: #FD8C73;\n  --color-scale-coral-4: #EC6547;\n  --color-scale-coral-5: #C4432B;\n  --color-scale-coral-6: #9E2F1C;\n  --color-scale-coral-7: #801F0F;\n  --color-scale-coral-8: #691105;\n  --color-scale-coral-9: #510901\n}\n\n@media(prefers-color-scheme: dark) {\n  :root {\n    --color-canvas-default-transparent: rgba(13,17,23,0);\n    --color-marketing-icon-primary: #79c0ff;\n    --color-marketing-icon-secondary: #1f6feb;\n    --color-diff-blob-addition-num-text: #c9d1d9;\n    --color-diff-blob-addition-fg: #c9d1d9;\n    --color-diff-blob-addition-num-bg: rgba(63,185,80,0.3);\n    --color-diff-blob-addition-line-bg: rgba(46,160,67,0.15);\n    --color-diff-blob-addition-word-bg: rgba(46,160,67,0.4);\n    --color-diff-blob-deletion-num-text: #c9d1d9;\n    --color-diff-blob-deletion-fg: #c9d1d9;\n    --color-diff-blob-deletion-num-bg: rgba(248,81,73,0.3);\n    --color-diff-blob-deletion-line-bg: rgba(248,81,73,0.15);\n    --color-diff-blob-deletion-word-bg: rgba(248,81,73,0.4);\n    --color-diff-blob-hunk-num-bg: rgba(56,139,253,0.4);\n    --color-diff-blob-expander-icon: #8b949e;\n    --color-diff-blob-selected-line-highlight-mix-blend-mode: screen;\n    --color-diffstat-deletion-border: rgba(240,246,252,0.1);\n    --color-diffstat-addition-border: rgba(240,246,252,0.1);\n    --color-diffstat-addition-bg: #3fb950;\n    --color-search-keyword-hl: rgba(210,153,34,0.4);\n    --color-prettylights-syntax-comment: #8b949e;\n    --color-prettylights-syntax-constant: #79c0ff;\n    --color-prettylights-syntax-entity: #d2a8ff;\n    --color-prettylights-syntax-storage-modifier-import: #c9d1d9;\n    --color-prettylights-syntax-entity-tag: #7ee787;\n    --color-prettylights-syntax-keyword: #ff7b72;\n    --color-prettylights-syntax-string: #a5d6ff;\n    --color-prettylights-syntax-variable: #ffa657;\n    --color-prettylights-syntax-brackethighlighter-unmatched: #f85149;\n    --color-prettylights-syntax-invalid-illegal-text: #f0f6fc;\n    --color-prettylights-syntax-invalid-illegal-bg: #8e1519;\n    --color-prettylights-syntax-carriage-return-text: #f0f6fc;\n    --color-prettylights-syntax-carriage-return-bg: #b62324;\n    --color-prettylights-syntax-string-regexp: #7ee787;\n    --color-prettylights-syntax-markup-list: #f2cc60;\n    --color-prettylights-syntax-markup-heading: #1f6feb;\n    --color-prettylights-syntax-markup-italic: #c9d1d9;\n    --color-prettylights-syntax-markup-bold: #c9d1d9;\n    --color-prettylights-syntax-markup-deleted-text: #ffdcd7;\n    --color-prettylights-syntax-markup-deleted-bg: #67060c;\n    --color-prettylights-syntax-markup-inserted-text: #aff5b4;\n    --color-prettylights-syntax-markup-inserted-bg: #033a16;\n    --color-prettylights-syntax-markup-changed-text: #ffdfb6;\n    --color-prettylights-syntax-markup-changed-bg: #5a1e02;\n    --color-prettylights-syntax-markup-ignored-text: #c9d1d9;\n    --color-prettylights-syntax-markup-ignored-bg: #1158c7;\n    --color-prettylights-syntax-meta-diff-range: #d2a8ff;\n    --color-prettylights-syntax-brackethighlighter-angle: #8b949e;\n    --color-prettylights-syntax-sublimelinter-gutter-mark: #484f58;\n    --color-prettylights-syntax-constant-other-reference-link: #a5d6ff;\n    --color-codemirror-text: #c9d1d9;\n    --color-codemirror-bg: #0d1117;\n    --color-codemirror-gutters-bg: #0d1117;\n    --color-codemirror-guttermarker-text: #0d1117;\n    --color-codemirror-guttermarker-subtle-text: #484f58;\n    --color-codemirror-linenumber-text: #8b949e;\n    --color-codemirror-cursor: #c9d1d9;\n    --color-codemirror-selection-bg: rgba(56,139,253,0.4);\n    --color-codemirror-activeline-bg: rgba(110,118,129,0.1);\n    --color-codemirror-matchingbracket-text: #c9d1d9;\n    --color-codemirror-lines-bg: #0d1117;\n    --color-codemirror-syntax-comment: #8b949e;\n    --color-codemirror-syntax-constant: #79c0ff;\n    --color-codemirror-syntax-entity: #d2a8ff;\n    --color-codemirror-syntax-keyword: #ff7b72;\n    --color-codemirror-syntax-storage: #ff7b72;\n    --color-codemirror-syntax-string: #a5d6ff;\n    --color-codemirror-syntax-support: #79c0ff;\n    --color-codemirror-syntax-variable: #ffa657;\n    --color-checks-bg: #010409;\n    --color-checks-run-border-width: 1px;\n    --color-checks-container-border-width: 1px;\n    --color-checks-text-primary: #c9d1d9;\n    --color-checks-text-secondary: #8b949e;\n    --color-checks-text-link: #58a6ff;\n    --color-checks-btn-icon: #8b949e;\n    --color-checks-btn-hover-icon: #c9d1d9;\n    --color-checks-btn-hover-bg: rgba(110,118,129,0.1);\n    --color-checks-input-text: #8b949e;\n    --color-checks-input-placeholder-text: #484f58;\n    --color-checks-input-focus-text: #c9d1d9;\n    --color-checks-input-bg: #161b22;\n    --color-checks-input-shadow: none;\n    --color-checks-donut-error: #f85149;\n    --color-checks-donut-pending: #d29922;\n    --color-checks-donut-success: #2ea043;\n    --color-checks-donut-neutral: #8b949e;\n    --color-checks-dropdown-text: #c9d1d9;\n    --color-checks-dropdown-bg: #161b22;\n    --color-checks-dropdown-border: #30363d;\n    --color-checks-dropdown-shadow: rgba(1,4,9,0.3);\n    --color-checks-dropdown-hover-text: #c9d1d9;\n    --color-checks-dropdown-hover-bg: rgba(110,118,129,0.1);\n    --color-checks-dropdown-btn-hover-text: #c9d1d9;\n    --color-checks-dropdown-btn-hover-bg: rgba(110,118,129,0.1);\n    --color-checks-scrollbar-thumb-bg: rgba(110,118,129,0.4);\n    --color-checks-header-label-text: #8b949e;\n    --color-checks-header-label-open-text: #c9d1d9;\n    --color-checks-header-border: #21262d;\n    --color-checks-header-icon: #8b949e;\n    --color-checks-line-text: #8b949e;\n    --color-checks-line-num-text: #484f58;\n    --color-checks-line-timestamp-text: #484f58;\n    --color-checks-line-hover-bg: rgba(110,118,129,0.1);\n    --color-checks-line-selected-bg: rgba(56,139,253,0.15);\n    --color-checks-line-selected-num-text: #58a6ff;\n    --color-checks-line-dt-fm-text: #f0f6fc;\n    --color-checks-line-dt-fm-bg: #9e6a03;\n    --color-checks-gate-bg: rgba(187,128,9,0.15);\n    --color-checks-gate-text: #8b949e;\n    --color-checks-gate-waiting-text: #d29922;\n    --color-checks-step-header-open-bg: #161b22;\n    --color-checks-step-error-text: #f85149;\n    --color-checks-step-warning-text: #d29922;\n    --color-checks-logline-text: #8b949e;\n    --color-checks-logline-num-text: #484f58;\n    --color-checks-logline-debug-text: #a371f7;\n    --color-checks-logline-error-text: #8b949e;\n    --color-checks-logline-error-num-text: #484f58;\n    --color-checks-logline-error-bg: rgba(248,81,73,0.15);\n    --color-checks-logline-warning-text: #8b949e;\n    --color-checks-logline-warning-num-text: #d29922;\n    --color-checks-logline-warning-bg: rgba(187,128,9,0.15);\n    --color-checks-logline-command-text: #58a6ff;\n    --color-checks-logline-section-text: #3fb950;\n    --color-checks-ansi-black: #0d1117;\n    --color-checks-ansi-black-bright: #161b22;\n    --color-checks-ansi-white: #b1bac4;\n    --color-checks-ansi-white-bright: #b1bac4;\n    --color-checks-ansi-gray: #6e7681;\n    --color-checks-ansi-red: #ff7b72;\n    --color-checks-ansi-red-bright: #ffa198;\n    --color-checks-ansi-green: #3fb950;\n    --color-checks-ansi-green-bright: #56d364;\n    --color-checks-ansi-yellow: #d29922;\n    --color-checks-ansi-yellow-bright: #e3b341;\n    --color-checks-ansi-blue: #58a6ff;\n    --color-checks-ansi-blue-bright: #79c0ff;\n    --color-checks-ansi-magenta: #bc8cff;\n    --color-checks-ansi-magenta-bright: #d2a8ff;\n    --color-checks-ansi-cyan: #76e3ea;\n    --color-checks-ansi-cyan-bright: #b3f0ff;\n    --color-project-header-bg: #0d1117;\n    --color-project-sidebar-bg: #161b22;\n    --color-project-gradient-in: #161b22;\n    --color-project-gradient-out: rgba(22,27,34,0);\n    --color-mktg-success: rgba(41,147,61,1);\n    --color-mktg-info: rgba(42,123,243,1);\n    --color-mktg-bg-shade-gradient-top: rgba(1,4,9,0.065);\n    --color-mktg-bg-shade-gradient-bottom: rgba(1,4,9,0);\n    --color-mktg-btn-bg-top: hsla(228,82%,66%,1);\n    --color-mktg-btn-bg-bottom: #4969ed;\n    --color-mktg-btn-bg-overlay-top: hsla(228,74%,59%,1);\n    --color-mktg-btn-bg-overlay-bottom: #3355e0;\n    --color-mktg-btn-text: #f0f6fc;\n    --color-mktg-btn-primary-bg-top: hsla(137,56%,46%,1);\n    --color-mktg-btn-primary-bg-bottom: #2ea44f;\n    --color-mktg-btn-primary-bg-overlay-top: hsla(134,60%,38%,1);\n    --color-mktg-btn-primary-bg-overlay-bottom: #22863a;\n    --color-mktg-btn-primary-text: #f0f6fc;\n    --color-mktg-btn-enterprise-bg-top: hsla(249,100%,72%,1);\n    --color-mktg-btn-enterprise-bg-bottom: #6f57ff;\n    --color-mktg-btn-enterprise-bg-overlay-top: hsla(248,65%,63%,1);\n    --color-mktg-btn-enterprise-bg-overlay-bottom: #614eda;\n    --color-mktg-btn-enterprise-text: #f0f6fc;\n    --color-mktg-btn-outline-text: #f0f6fc;\n    --color-mktg-btn-outline-border: rgba(240,246,252,0.3);\n    --color-mktg-btn-outline-hover-text: #f0f6fc;\n    --color-mktg-btn-outline-hover-border: rgba(240,246,252,0.5);\n    --color-mktg-btn-outline-focus-border: #f0f6fc;\n    --color-mktg-btn-outline-focus-border-inset: rgba(240,246,252,0.5);\n    --color-mktg-btn-dark-text: #f0f6fc;\n    --color-mktg-btn-dark-border: rgba(240,246,252,0.3);\n    --color-mktg-btn-dark-hover-text: #f0f6fc;\n    --color-mktg-btn-dark-hover-border: rgba(240,246,252,0.5);\n    --color-mktg-btn-dark-focus-border: #f0f6fc;\n    --color-mktg-btn-dark-focus-border-inset: rgba(240,246,252,0.5);\n    --color-avatar-bg: rgba(240,246,252,0.1);\n    --color-avatar-border: rgba(240,246,252,0.1);\n    --color-avatar-stack-fade: #30363d;\n    --color-avatar-stack-fade-more: #21262d;\n    --color-avatar-child-shadow: -2px -2px 0 #0d1117;\n    --color-topic-tag-border: rgba(0,0,0,0);\n    --color-select-menu-backdrop-border: #484f58;\n    --color-select-menu-tap-highlight: rgba(48,54,61,0.5);\n    --color-select-menu-tap-focus-bg: #0c2d6b;\n    --color-overlay-shadow: 0 0 0 1px #30363d, 0 16px 32px rgba(1,4,9,0.85);\n    --color-header-text: rgba(240,246,252,0.7);\n    --color-header-bg: #161b22;\n    --color-header-logo: #f0f6fc;\n    --color-header-search-bg: #0d1117;\n    --color-header-search-border: #30363d;\n    --color-sidenav-selected-bg: #21262d;\n    --color-menu-bg-active: #161b22;\n    --color-control-transparent-bg-hover: #656c7633;\n    --color-input-disabled-bg: rgba(110,118,129,0);\n    --color-timeline-badge-bg: #21262d;\n    --color-ansi-black: #484f58;\n    --color-ansi-black-bright: #6e7681;\n    --color-ansi-white: #b1bac4;\n    --color-ansi-white-bright: #f0f6fc;\n    --color-ansi-gray: #6e7681;\n    --color-ansi-red: #ff7b72;\n    --color-ansi-red-bright: #ffa198;\n    --color-ansi-green: #3fb950;\n    --color-ansi-green-bright: #56d364;\n    --color-ansi-yellow: #d29922;\n    --color-ansi-yellow-bright: #e3b341;\n    --color-ansi-blue: #58a6ff;\n    --color-ansi-blue-bright: #79c0ff;\n    --color-ansi-magenta: #bc8cff;\n    --color-ansi-magenta-bright: #d2a8ff;\n    --color-ansi-cyan: #39c5cf;\n    --color-ansi-cyan-bright: #56d4dd;\n    --color-btn-text: #c9d1d9;\n    --color-btn-bg: #21262d;\n    --color-btn-border: rgba(240,246,252,0.1);\n    --color-btn-shadow: 0 0 transparent;\n    --color-btn-inset-shadow: 0 0 transparent;\n    --color-btn-hover-bg: #30363d;\n    --color-btn-hover-border: #8b949e;\n    --color-btn-active-bg: hsla(212,12%,18%,1);\n    --color-btn-active-border: #6e7681;\n    --color-btn-selected-bg: #161b22;\n    --color-btn-focus-bg: #21262d;\n    --color-btn-focus-border: #8b949e;\n    --color-btn-focus-shadow: 0 0 0 3px rgba(139,148,158,0.3);\n    --color-btn-shadow-active: inset 0 0.15em 0.3em rgba(1,4,9,0.15);\n    --color-btn-shadow-input-focus: 0 0 0 0.2em rgba(31,111,235,0.3);\n    --color-btn-counter-bg: #30363d;\n    --color-btn-primary-text: #ffffff;\n    --color-btn-primary-bg: #238636;\n    --color-btn-primary-border: rgba(240,246,252,0.1);\n    --color-btn-primary-shadow: 0 0 transparent;\n    --color-btn-primary-inset-shadow: 0 0 transparent;\n    --color-btn-primary-hover-bg: #2ea043;\n    --color-btn-primary-hover-border: rgba(240,246,252,0.1);\n    --color-btn-primary-selected-bg: #238636;\n    --color-btn-primary-selected-shadow: 0 0 transparent;\n    --color-btn-primary-disabled-text: rgba(240,246,252,0.5);\n    --color-btn-primary-disabled-bg: rgba(35,134,54,0.6);\n    --color-btn-primary-disabled-border: rgba(240,246,252,0.1);\n    --color-btn-primary-focus-bg: #238636;\n    --color-btn-primary-focus-border: rgba(240,246,252,0.1);\n    --color-btn-primary-focus-shadow: 0 0 0 3px rgba(46,164,79,0.4);\n    --color-btn-primary-icon: #f0f6fc;\n    --color-btn-primary-counter-bg: rgba(240,246,252,0.2);\n    --color-btn-outline-text: #58a6ff;\n    --color-btn-outline-hover-text: #58a6ff;\n    --color-btn-outline-hover-bg: #30363d;\n    --color-btn-outline-hover-border: rgba(240,246,252,0.1);\n    --color-btn-outline-hover-shadow: 0 1px 0 rgba(1,4,9,0.1);\n    --color-btn-outline-hover-inset-shadow: inset 0 1px 0 rgba(240,246,252,0.03);\n    --color-btn-outline-hover-counter-bg: rgba(240,246,252,0.2);\n    --color-btn-outline-selected-text: #f0f6fc;\n    --color-btn-outline-selected-bg: #0d419d;\n    --color-btn-outline-selected-border: rgba(240,246,252,0.1);\n    --color-btn-outline-selected-shadow: 0 0 transparent;\n    --color-btn-outline-disabled-text: rgba(88,166,255,0.5);\n    --color-btn-outline-disabled-bg: #0d1117;\n    --color-btn-outline-disabled-counter-bg: rgba(31,111,235,0.05);\n    --color-btn-outline-focus-border: rgba(240,246,252,0.1);\n    --color-btn-outline-focus-shadow: 0 0 0 3px rgba(17,88,199,0.4);\n    --color-btn-outline-counter-bg: rgba(31,111,235,0.1);\n    --color-btn-danger-text: #f85149;\n    --color-btn-danger-hover-text: #f0f6fc;\n    --color-btn-danger-hover-bg: #da3633;\n    --color-btn-danger-hover-border: #f85149;\n    --color-btn-danger-hover-shadow: 0 0 transparent;\n    --color-btn-danger-hover-inset-shadow: 0 0 transparent;\n    --color-btn-danger-hover-icon: #f0f6fc;\n    --color-btn-danger-hover-counter-bg: rgba(255,255,255,0.2);\n    --color-btn-danger-selected-text: #ffffff;\n    --color-btn-danger-selected-bg: #b62324;\n    --color-btn-danger-selected-border: #ff7b72;\n    --color-btn-danger-selected-shadow: 0 0 transparent;\n    --color-btn-danger-disabled-text: rgba(248,81,73,0.5);\n    --color-btn-danger-disabled-bg: #0d1117;\n    --color-btn-danger-disabled-counter-bg: rgba(218,54,51,0.05);\n    --color-btn-danger-focus-border: #f85149;\n    --color-btn-danger-focus-shadow: 0 0 0 3px rgba(248,81,73,0.4);\n    --color-btn-danger-counter-bg: rgba(218,54,51,0.1);\n    --color-btn-danger-icon: #f85149;\n    --color-underlinenav-icon: #484f58;\n    --color-underlinenav-border-hover: rgba(110,118,129,0.4);\n    --color-fg-default: #c9d1d9;\n    --color-fg-muted: #8b949e;\n    --color-fg-subtle: #484f58;\n    --color-fg-on-emphasis: #f0f6fc;\n    --color-canvas-default: #0d1117;\n    --color-canvas-overlay: #161b22;\n    --color-canvas-inset: #010409;\n    --color-canvas-subtle: #161b22;\n    --color-border-default: #30363d;\n    --color-border-muted: #21262d;\n    --color-border-subtle: rgba(240,246,252,0.1);\n    --color-shadow-small: 0 0 transparent;\n    --color-shadow-medium: 0 3px 6px #010409;\n    --color-shadow-large: 0 8px 24px #010409;\n    --color-shadow-extra-large: 0 12px 48px #010409;\n    --color-neutral-emphasis-plus: #6e7681;\n    --color-neutral-emphasis: #6e7681;\n    --color-neutral-muted: rgba(110,118,129,0.4);\n    --color-neutral-subtle: rgba(110,118,129,0.1);\n    --color-accent-fg: #58a6ff;\n    --color-accent-emphasis: #1f6feb;\n    --color-accent-muted: rgba(56,139,253,0.4);\n    --color-accent-subtle: rgba(56,139,253,0.15);\n    --color-success-fg: #3fb950;\n    --color-success-emphasis: #238636;\n    --color-success-muted: rgba(46,160,67,0.4);\n    --color-success-subtle: rgba(46,160,67,0.15);\n    --color-attention-fg: #d29922;\n    --color-attention-emphasis: #9e6a03;\n    --color-attention-muted: rgba(187,128,9,0.4);\n    --color-attention-subtle: rgba(187,128,9,0.15);\n    --color-severe-fg: #db6d28;\n    --color-severe-emphasis: #bd561d;\n    --color-severe-muted: rgba(219,109,40,0.4);\n    --color-severe-subtle: rgba(219,109,40,0.15);\n    --color-danger-fg: #f85149;\n    --color-danger-emphasis: #da3633;\n    --color-danger-muted: rgba(248,81,73,0.4);\n    --color-danger-subtle: rgba(248,81,73,0.15);\n    --color-done-fg: #a371f7;\n    --color-done-emphasis: #8957e5;\n    --color-done-muted: rgba(163,113,247,0.4);\n    --color-done-subtle: rgba(163,113,247,0.15);\n    --color-sponsors-fg: #db61a2;\n    --color-sponsors-emphasis: #bf4b8a;\n    --color-sponsors-muted: rgba(219,97,162,0.4);\n    --color-sponsors-subtle: rgba(219,97,162,0.15);\n    --color-primer-canvas-backdrop: rgba(1,4,9,0.8);\n    --color-primer-canvas-sticky: rgba(13,17,23,0.95);\n    --color-primer-border-active: #F78166;\n    --color-primer-border-contrast: rgba(240,246,252,0.2);\n    --color-primer-shadow-highlight: 0 0 transparent;\n    --color-primer-shadow-inset: 0 0 transparent;\n    --color-primer-shadow-focus: 0 0 0 3px #0c2d6b;\n    --color-scale-black: #010409;\n    --color-scale-white: #f0f6fc;\n    --color-scale-gray-0: #f0f6fc;\n    --color-scale-gray-1: #c9d1d9;\n    --color-scale-gray-2: #b1bac4;\n    --color-scale-gray-3: #8b949e;\n    --color-scale-gray-4: #6e7681;\n    --color-scale-gray-5: #484f58;\n    --color-scale-gray-6: #30363d;\n    --color-scale-gray-7: #21262d;\n    --color-scale-gray-8: #161b22;\n    --color-scale-gray-9: #0d1117;\n    --color-scale-blue-0: #cae8ff;\n    --color-scale-blue-1: #a5d6ff;\n    --color-scale-blue-2: #79c0ff;\n    --color-scale-blue-3: #58a6ff;\n    --color-scale-blue-4: #388bfd;\n    --color-scale-blue-5: #1f6feb;\n    --color-scale-blue-6: #1158c7;\n    --color-scale-blue-7: #0d419d;\n    --color-scale-blue-8: #0c2d6b;\n    --color-scale-blue-9: #051d4d;\n    --color-scale-green-0: #aff5b4;\n    --color-scale-green-1: #7ee787;\n    --color-scale-green-2: #56d364;\n    --color-scale-green-3: #3fb950;\n    --color-scale-green-4: #2ea043;\n    --color-scale-green-5: #238636;\n    --color-scale-green-6: #196c2e;\n    --color-scale-green-7: #0f5323;\n    --color-scale-green-8: #033a16;\n    --color-scale-green-9: #04260f;\n    --color-scale-yellow-0: #f8e3a1;\n    --color-scale-yellow-1: #f2cc60;\n    --color-scale-yellow-2: #e3b341;\n    --color-scale-yellow-3: #d29922;\n    --color-scale-yellow-4: #bb8009;\n    --color-scale-yellow-5: #9e6a03;\n    --color-scale-yellow-6: #845306;\n    --color-scale-yellow-7: #693e00;\n    --color-scale-yellow-8: #4b2900;\n    --color-scale-yellow-9: #341a00;\n    --color-scale-orange-0: #ffdfb6;\n    --color-scale-orange-1: #ffc680;\n    --color-scale-orange-2: #ffa657;\n    --color-scale-orange-3: #f0883e;\n    --color-scale-orange-4: #db6d28;\n    --color-scale-orange-5: #bd561d;\n    --color-scale-orange-6: #9b4215;\n    --color-scale-orange-7: #762d0a;\n    --color-scale-orange-8: #5a1e02;\n    --color-scale-orange-9: #3d1300;\n    --color-scale-red-0: #ffdcd7;\n    --color-scale-red-1: #ffc1ba;\n    --color-scale-red-2: #ffa198;\n    --color-scale-red-3: #ff7b72;\n    --color-scale-red-4: #f85149;\n    --color-scale-red-5: #da3633;\n    --color-scale-red-6: #b62324;\n    --color-scale-red-7: #8e1519;\n    --color-scale-red-8: #67060c;\n    --color-scale-red-9: #490202;\n    --color-scale-purple-0: #eddeff;\n    --color-scale-purple-1: #e2c5ff;\n    --color-scale-purple-2: #d2a8ff;\n    --color-scale-purple-3: #bc8cff;\n    --color-scale-purple-4: #a371f7;\n    --color-scale-purple-5: #8957e5;\n    --color-scale-purple-6: #6e40c9;\n    --color-scale-purple-7: #553098;\n    --color-scale-purple-8: #3c1e70;\n    --color-scale-purple-9: #271052;\n    --color-scale-pink-0: #ffdaec;\n    --color-scale-pink-1: #ffbedd;\n    --color-scale-pink-2: #ff9bce;\n    --color-scale-pink-3: #f778ba;\n    --color-scale-pink-4: #db61a2;\n    --color-scale-pink-5: #bf4b8a;\n    --color-scale-pink-6: #9e3670;\n    --color-scale-pink-7: #7d2457;\n    --color-scale-pink-8: #5e103e;\n    --color-scale-pink-9: #42062a;\n    --color-scale-coral-0: #FFDDD2;\n    --color-scale-coral-1: #FFC2B2;\n    --color-scale-coral-2: #FFA28B;\n    --color-scale-coral-3: #F78166;\n    --color-scale-coral-4: #EA6045;\n    --color-scale-coral-5: #CF462D;\n    --color-scale-coral-6: #AC3220;\n    --color-scale-coral-7: #872012;\n    --color-scale-coral-8: #640D04;\n    --color-scale-coral-9: #460701\n  }\n}\n"
  },
  {
    "path": "packages/extension/src/ui/connect.css",
    "content": "/*\n  Copyright (c) Microsoft Corporation.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License.\n*/\n\nbody {\n  margin: 0;\n  padding: 0;\n}\n\n/* Base styles */\n.app-container {\n  font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Noto Sans\", Helvetica, Arial, sans-serif;\n  background-color: #ffffff;\n  color: #1f2328;\n  margin: 0;\n  padding: 16px;\n  min-height: 100vh;\n  font-size: 14px;\n}\n\n.content-wrapper {\n  max-width: 600px;\n  margin: 0 auto;\n}\n\n/* Status Banner */\n.status-container {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-bottom: 16px;\n  padding-right: 12px;\n}\n\n.status-banner {\n  padding: 12px;\n  font-size: 14px;\n  font-weight: 500;\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  flex: 1;\n}\n\n.status-banner.connected {\n  color: #1f2328;\n}\n\n.status-banner.connected::before {\n  content: \"\\2705\";\n  margin-right: 8px;\n}\n\n.status-banner.error {\n  color: #1f2328;\n}\n\n.status-banner.error::before {\n  content: \"\\274C\";\n  margin-right: 8px;\n}\n\n/* Buttons */\n.button-container {\n  margin-bottom: 16px;\n  display: flex;\n  justify-content: flex-end;\n  padding-right: 12px;\n}\n\n.button {\n  padding: 8px 16px;\n  border-radius: 6px;\n  border: none;\n  font-size: 14px;\n  font-weight: 500;\n  cursor: pointer;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  text-decoration: none;\n  margin-right: 8px;\n  min-width: 90px;\n}\n\n.button.primary {\n  background-color: #f8f9fa;\n  color: #3c4043;\n  border: 1px solid #dadce0;\n}\n\n.button.primary:hover {\n  background-color: #f1f3f4;\n  border-color: #dadce0;\n  box-shadow: 0 1px 2px 0 rgba(60,64,67,.1);\n}\n\n.button.default {\n  background-color: #f6f8fa;\n  color: #24292f;\n}\n\n.button.default:hover {\n  background-color: #f3f4f6;\n}\n\n.button.reject {\n  background-color: #da3633;\n  color: #ffffff;\n  border: 1px solid #da3633;\n}\n\n.button.reject:hover {\n  background-color: #c73836;\n  border-color: #c73836;\n}\n\n/* Tab selection */\n.tab-section-title {\n  padding-left: 12px;\n  font-size: 12px;\n  font-weight: 400;\n  margin-bottom: 12px;\n  color: #656d76;\n}\n\n.tab-item {\n  display: flex;\n  align-items: center;\n  padding: 12px;\n  margin-bottom: 8px;\n  background-color: #ffffff;\n  cursor: pointer;\n  border-radius: 6px;\n  transition: background-color 0.2s ease;\n}\n\n.tab-item:hover {\n  background-color: #f8f9fa;\n}\n\n.tab-item.selected {\n  background-color: #f6f8fa;\n}\n\n.tab-item.disabled {\n  cursor: not-allowed;\n  opacity: 0.5;\n}\n\n.tab-radio {\n  margin-right: 12px;\n  flex-shrink: 0;\n}\n\n.tab-favicon {\n  width: 16px;\n  height: 16px;\n  margin-right: 8px;\n  flex-shrink: 0;\n}\n\n.tab-content {\n  flex: 1;\n  min-width: 0;\n}\n\n.tab-title {\n  font-weight: 500;\n  color: #1f2328;\n  margin-bottom: 2px;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n.tab-url {\n  font-size: 12px;\n  color: #656d76;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n\n/* Link-style button */\n.link-button {\n  background: none;\n  border: none;\n  color: #0066cc;\n  text-decoration: underline;\n  cursor: pointer;\n  padding: 0;\n  font: inherit;\n}\n\n/* Auth token section */\n.auth-token-section {\n  margin: 16px 0;\n  padding: 16px;\n  background-color: #f6f8fa;\n  border-radius: 6px;\n}\n\n.auth-token-description {\n  font-size: 12px;\n  color: #656d76;\n  margin-bottom: 12px;\n}\n\n.auth-token-container {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  background-color: #ffffff;\n  padding: 8px;\n}\n\n.auth-token-code {\n  font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;\n  font-size: 12px;\n  color: #1f2328;\n  border: none;\n  flex: 1;\n  padding: 0;\n  word-break: break-all;\n}\n\n.auth-token-refresh {\n  flex: none;\n  height: 24px;\n  width: 24px;\n  border: none;\n  outline: none;\n  color: var(--color-fg-muted);\n  background: transparent;\n  padding: 4px;\n  cursor: pointer;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 4px;\n}\n\n.auth-token-refresh svg {\n  margin: 0;\n}\n\n.auth-token-refresh:not(:disabled):hover {\n  background-color: var(--color-btn-selected-bg);\n}\n"
  },
  {
    "path": "packages/extension/src/ui/connect.html",
    "content": "<!--\n  Copyright (c) Microsoft Corporation.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License.\n-->\n<!DOCTYPE html>\n<html>\n<head>\n  <title>Playwright MCP extension</title>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n  <link rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"../../icons/icon-32.png\">\n  <link rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"../../icons/icon-16.png\">\n  <link rel=\"stylesheet\" href=\"connect.css\">\n</head>\n<body>\n  <div id=\"root\"></div>\n  <script type=\"module\" src=\"connect.tsx\"></script>\n</body>\n</html> "
  },
  {
    "path": "packages/extension/src/ui/connect.tsx",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport React, { useCallback, useEffect, useState } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { Button, TabItem } from './tabItem';\nimport { AuthTokenSection, getOrCreateAuthToken } from './authToken';\n\nimport type { TabInfo } from './tabItem';\n\ntype Status =\n  | { type: 'connecting'; message: string }\n  | { type: 'connected'; message: string }\n  | { type: 'error'; message: string }\n  | { type: 'error'; versionMismatch: { extensionVersion: string; } };\n\nconst SUPPORTED_PROTOCOL_VERSION = 1;\n\nconst ConnectApp: React.FC = () => {\n  const [tabs, setTabs] = useState<TabInfo[]>([]);\n  const [status, setStatus] = useState<Status | null>(null);\n  const [showButtons, setShowButtons] = useState(true);\n  const [showTabList, setShowTabList] = useState(true);\n  const [clientInfo, setClientInfo] = useState('unknown');\n  const [mcpRelayUrl, setMcpRelayUrl] = useState('');\n  const [newTab, setNewTab] = useState<boolean>(false);\n\n  useEffect(() => {\n    const runAsync = async () => {\n      const params = new URLSearchParams(window.location.search);\n      const relayUrl = params.get('mcpRelayUrl');\n\n      if (!relayUrl) {\n        handleReject('Missing mcpRelayUrl parameter in URL.');\n        return;\n      }\n\n      try {\n        const host = new URL(relayUrl).hostname;\n        if (host !== '127.0.0.1' && host !== '[::1]') {\n          handleReject(`MCP extension only allows loopback connections (127.0.0.1 or [::1]). Received host: ${host}`);\n          return;\n        }\n      } catch (e) {\n        handleReject(`Invalid mcpRelayUrl parameter in URL: ${relayUrl}. ${e}`);\n        return;\n      }\n\n      setMcpRelayUrl(relayUrl);\n\n      try {\n        const client = JSON.parse(params.get('client') || '{}');\n        const info = `${client.name}/${client.version}`;\n        setClientInfo(info);\n        setStatus({\n          type: 'connecting',\n          message: `🎭 Playwright MCP started from  \"${info}\" is trying to connect. Do you want to continue?`\n        });\n      } catch (e) {\n        setStatus({ type: 'error', message: 'Failed to parse client version.' });\n        return;\n      }\n\n      const parsedVersion = parseInt(params.get('protocolVersion') ?? '', 10);\n      const requiredVersion = isNaN(parsedVersion) ? 1 : parsedVersion;\n      if (requiredVersion > SUPPORTED_PROTOCOL_VERSION) {\n        const extensionVersion = chrome.runtime.getManifest().version;\n        setShowButtons(false);\n        setShowTabList(false);\n        setStatus({\n          type: 'error',\n          versionMismatch: {\n            extensionVersion,\n          }\n        });\n        return;\n      }\n\n      const expectedToken = getOrCreateAuthToken();\n      const token = params.get('token');\n      if (token === expectedToken) {\n        await connectToMCPRelay(relayUrl);\n        await handleConnectToTab();\n        return;\n      }\n      if (token) {\n        handleReject('Invalid token provided.');\n        return;\n      }\n\n      await connectToMCPRelay(relayUrl);\n\n      // If this is a browser_navigate command, hide the tab list and show simple allow/reject\n      if (params.get('newTab') === 'true') {\n        setNewTab(true);\n        setShowTabList(false);\n      } else {\n        await loadTabs();\n      }\n    };\n    void runAsync();\n  }, []);\n\n  const handleReject = useCallback((message: string) => {\n    setShowButtons(false);\n    setShowTabList(false);\n    setStatus({ type: 'error', message });\n  }, []);\n\n  const connectToMCPRelay = useCallback(async (mcpRelayUrl: string) => {\n    const response = await chrome.runtime.sendMessage({ type: 'connectToMCPRelay', mcpRelayUrl  });\n    if (!response.success)\n      handleReject(response.error);\n  }, [handleReject]);\n\n  const loadTabs = useCallback(async () => {\n    const response = await chrome.runtime.sendMessage({ type: 'getTabs' });\n    if (response.success)\n      setTabs(response.tabs);\n    else\n      setStatus({ type: 'error', message: 'Failed to load tabs: ' + response.error });\n  }, []);\n\n  const handleConnectToTab = useCallback(async (tab?: TabInfo) => {\n    setShowButtons(false);\n    setShowTabList(false);\n\n    try {\n      const response = await chrome.runtime.sendMessage({\n        type: 'connectToTab',\n        mcpRelayUrl,\n        tabId: tab?.id,\n        windowId: tab?.windowId,\n      });\n\n      if (response?.success) {\n        setStatus({ type: 'connected', message: `MCP client \"${clientInfo}\" connected.` });\n      } else {\n        setStatus({\n          type: 'error',\n          message: response?.error || `MCP client \"${clientInfo}\" failed to connect.`\n        });\n      }\n    } catch (e) {\n      setStatus({\n        type: 'error',\n        message: `MCP client \"${clientInfo}\" failed to connect: ${e}`\n      });\n    }\n  }, [clientInfo, mcpRelayUrl]);\n\n  useEffect(() => {\n    const listener = (message: any) => {\n      if (message.type === 'connectionTimeout')\n        handleReject('Connection timed out.');\n    };\n    chrome.runtime.onMessage.addListener(listener);\n    return () => {\n      chrome.runtime.onMessage.removeListener(listener);\n    };\n  }, [handleReject]);\n\n  return (\n    <div className='app-container'>\n      <div className='content-wrapper'>\n        {status && (\n          <div className='status-container'>\n            <StatusBanner status={status} />\n            {showButtons && (\n              <div className='button-container'>\n                {newTab ? (\n                  <>\n                    <Button variant='primary' onClick={() => handleConnectToTab()}>\n                      Allow\n                    </Button>\n                    <Button variant='reject' onClick={() => handleReject('Connection rejected. This tab can be closed.')}>\n                      Reject\n                    </Button>\n                  </>\n                ) : (\n                  <Button variant='reject' onClick={() => handleReject('Connection rejected. This tab can be closed.')}>\n                    Reject\n                  </Button>\n                )}\n              </div>\n            )}\n          </div>\n        )}\n\n        {status?.type === 'connecting' && (\n          <AuthTokenSection />\n        )}\n\n        {showTabList && (\n          <div>\n            <div className='tab-section-title'>\n              Select page to expose to MCP server:\n            </div>\n            <div>\n              {tabs.map(tab => (\n                <TabItem\n                  key={tab.id}\n                  tab={tab}\n                  button={\n                    <Button variant='primary' onClick={() => handleConnectToTab(tab)}>\n                      Connect\n                    </Button>\n                  }\n                />\n              ))}\n            </div>\n          </div>\n        )}\n      </div>\n    </div>\n  );\n};\n\nconst VersionMismatchError: React.FC<{ extensionVersion: string }> = ({ extensionVersion }) => {\n  const readmeUrl = 'https://github.com/microsoft/playwright-mcp/blob/main/extension/README.md';\n  const latestReleaseUrl = 'https://github.com/microsoft/playwright-mcp/releases/latest';\n  return (\n    <div>\n      Playwright MCP version trying to connect requires newer extension version (current version: {extensionVersion}).{' '}\n      <a href={latestReleaseUrl}>Click here</a> to download latest version of the extension, then drag and drop it into the Chrome Extensions page.{' '}\n      See <a href={readmeUrl} target='_blank' rel='noopener noreferrer'>installation instructions</a> for more details.\n    </div>\n  );\n};\n\nconst StatusBanner: React.FC<{ status: Status }> = ({ status }) => {\n  return (\n    <div className={`status-banner ${status.type}`}>\n      {'versionMismatch' in status ? (\n        <VersionMismatchError\n          extensionVersion={status.versionMismatch.extensionVersion}\n        />\n      ) : (\n        status.message\n      )}\n    </div>\n  );\n};\n\n// Initialize the React app\nconst container = document.getElementById('root');\nif (container) {\n  const root = createRoot(container);\n  root.render(<ConnectApp />);\n}\n"
  },
  {
    "path": "packages/extension/src/ui/copyToClipboard.css",
    "content": "/*\n  Copyright (c) Microsoft Corporation.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License.\n*/\n\n.copy-icon {\n  flex: none;\n  height: 24px;\n  width: 24px;\n  border: none;\n  outline: none;\n  color: var(--color-fg-muted);\n  background: transparent;\n  padding: 4px;\n  cursor: pointer;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  border-radius: 4px;\n}\n\n.copy-icon svg {\n  margin: 0;\n}\n\n.copy-icon:not(:disabled):hover {\n  background-color: var(--color-btn-selected-bg);\n}\n"
  },
  {
    "path": "packages/extension/src/ui/copyToClipboard.tsx",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as React from 'react';\nimport * as icons from './icons';\nimport './copyToClipboard.css';\n\ntype CopyToClipboardProps = {\n  value: string;\n};\n\n/**\n * A copy to clipboard button.\n */\nexport const CopyToClipboard: React.FunctionComponent<CopyToClipboardProps> = ({ value }) => {\n  type IconType = 'copy' | 'check' | 'cross';\n  const [icon, setIcon] = React.useState<IconType>('copy');\n\n  React.useEffect(() => {\n    setIcon('copy');\n  }, [value]);\n\n  React.useEffect(() => {\n    if (icon === 'check') {\n      const timeout = setTimeout(() => {\n        setIcon('copy');\n      }, 3000);\n      return () => clearTimeout(timeout);\n    }\n  }, [icon]);\n\n  const handleCopy = React.useCallback(() => {\n    navigator.clipboard.writeText(value).then(() => {\n      setIcon('check');\n    }, () => {\n      setIcon('cross');\n    });\n  }, [value]);\n  const iconElement = icon === 'check' ? icons.check() : icon === 'cross' ? icons.cross() : icons.copy();\n  return <button className='copy-icon' title='Copy to clipboard' aria-label='Copy to clipboard' onClick={handleCopy}>{iconElement}</button>;\n};\n"
  },
  {
    "path": "packages/extension/src/ui/icons.css",
    "content": "/*\n  Copyright (c) Microsoft Corporation.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License.\n*/\n\n.octicon {\n  display: inline-block;\n  overflow: visible !important;\n  vertical-align: text-bottom;\n  fill: currentColor;\n  margin-right: 7px;\n  flex: none;\n}\n\n.color-icon-success {\n  color: var(--color-success-fg) !important;\n}\n\n.color-text-danger {\n  color: var(--color-danger-fg) !important;\n}\n"
  },
  {
    "path": "packages/extension/src/ui/icons.tsx",
    "content": "/*\n  Copyright (c) Microsoft Corporation.\n\n  Licensed under the Apache License, Version 2.0 (the \"License\");\n  you may not use this file except in compliance with the License.\n  You may obtain a copy of the License at\n\n      http://www.apache.org/licenses/LICENSE-2.0\n\n  Unless required by applicable law or agreed to in writing, software\n  distributed under the License is distributed on an \"AS IS\" BASIS,\n  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n  See the License for the specific language governing permissions and\n  limitations under the License.\n*/\n\nimport './icons.css';\nimport './colors.css';\n\nexport const cross = () => {\n  return <svg className='octicon color-text-danger' viewBox='0 0 16 16' version='1.1' width='16' height='16' aria-hidden='true'>\n    <path fillRule='evenodd' d='M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z'></path>\n  </svg>;\n};\n\nexport const check = () => {\n  return <svg aria-hidden='true' height='16' viewBox='0 0 16 16' version='1.1' width='16' data-view-component='true' className='octicon color-icon-success'>\n    <path fillRule='evenodd' d='M13.78 4.22a.75.75 0 010 1.06l-7.25 7.25a.75.75 0 01-1.06 0L2.22 9.28a.75.75 0 011.06-1.06L6 10.94l6.72-6.72a.75.75 0 011.06 0z'></path>\n  </svg>;\n};\n\nexport const copy = () => {\n  return <svg className='octicon' viewBox='0 0 16 16' width='16' height='16' aria-hidden='true'>\n    <path d='M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z'></path>\n    <path d='M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z'></path>\n  </svg>;\n};\n\nexport const refresh = () => {\n  return <svg className='octicon' viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden='true'>\n    <path d=\"M1.705 8.005a.75.75 0 0 1 .834.656 5.5 5.5 0 0 0 9.592 2.97l-1.204-1.204a.25.25 0 0 1 .177-.427h3.646a.25.25 0 0 1 .25.25v3.646a.25.25 0 0 1-.427.177l-1.38-1.38A7.002 7.002 0 0 1 1.05 8.84a.75.75 0 0 1 .656-.834ZM8 2.5a5.487 5.487 0 0 0-4.131 1.869l1.204 1.204A.25.25 0 0 1 4.896 6H1.25A.25.25 0 0 1 1 5.75V2.104a.25.25 0 0 1 .427-.177l1.38 1.38A7.002 7.002 0 0 1 14.95 7.16a.75.75 0 0 1-1.49.178A5.5 5.5 0 0 0 8 2.5Z\"></path>\n  </svg>;\n};\n\nexport const chevronDown = () => {\n  return <svg className='octicon' viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" aria-hidden='true'>\n    <path d=\"M12.78 5.22a.749.749 0 0 1 0 1.06l-4.25 4.25a.749.749 0 0 1-1.06 0L3.22 6.28a.749.749 0 1 1 1.06-1.06L8 8.939l3.72-3.719a.749.749 0 0 1 1.06 0Z\"></path>\n  </svg>;\n};\n"
  },
  {
    "path": "packages/extension/src/ui/status.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>Playwright MCP Bridge Status</title>\n  <link rel=\"stylesheet\" href=\"connect.css\">\n</head>\n<body>\n  <div id=\"root\"></div>\n  <script src=\"status.tsx\" type=\"module\"></script>\n</body>\n</html>"
  },
  {
    "path": "packages/extension/src/ui/status.tsx",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport React, { useState, useEffect } from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { Button, TabItem  } from './tabItem';\n\nimport type { TabInfo } from './tabItem';\nimport { AuthTokenSection } from './authToken';\n\ninterface ConnectionStatus {\n  isConnected: boolean;\n  connectedTabId: number | null;\n  connectedTab?: TabInfo;\n}\n\nconst StatusApp: React.FC = () => {\n  const [status, setStatus] = useState<ConnectionStatus>({\n    isConnected: false,\n    connectedTabId: null\n  });\n\n  useEffect(() => {\n    void loadStatus();\n  }, []);\n\n  const loadStatus = async () => {\n    // Get current connection status from background script\n    const { connectedTabId } = await chrome.runtime.sendMessage({ type: 'getConnectionStatus' });\n    if (connectedTabId) {\n      const tab = await chrome.tabs.get(connectedTabId);\n      setStatus({\n        isConnected: true,\n        connectedTabId,\n        connectedTab: {\n          id: tab.id!,\n          windowId: tab.windowId!,\n          title: tab.title!,\n          url: tab.url!,\n          favIconUrl: tab.favIconUrl\n        }\n      });\n    } else {\n      setStatus({\n        isConnected: false,\n        connectedTabId: null\n      });\n    }\n  };\n\n  const openConnectedTab = async () => {\n    if (!status.connectedTabId)\n      return;\n    await chrome.tabs.update(status.connectedTabId, { active: true });\n    window.close();\n  };\n\n  const disconnect = async () => {\n    await chrome.runtime.sendMessage({ type: 'disconnect' });\n    window.close();\n  };\n\n  return (\n    <div className='app-container'>\n      <div className='content-wrapper'>\n        {status.isConnected && status.connectedTab ? (\n          <div>\n            <div className='tab-section-title'>\n              Page with connected MCP client:\n            </div>\n            <div>\n              <TabItem\n                tab={status.connectedTab}\n                button={\n                  <Button variant='primary' onClick={disconnect}>\n                    Disconnect\n                  </Button>\n                }\n                onClick={openConnectedTab}\n              />\n            </div>\n          </div>\n        ) : (\n          <div className='status-banner'>\n            No MCP clients are currently connected.\n          </div>\n        )}\n        <AuthTokenSection />\n      </div>\n    </div>\n  );\n};\n\n// Initialize the React app\nconst container = document.getElementById('root');\nif (container) {\n  const root = createRoot(container);\n  root.render(<StatusApp />);\n}\n"
  },
  {
    "path": "packages/extension/src/ui/tabItem.tsx",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport React from 'react';\n\nexport interface TabInfo {\n  id: number;\n  windowId: number;\n  title: string;\n  url: string;\n  favIconUrl?: string;\n}\n\nexport const Button: React.FC<{ variant: 'primary' | 'default' | 'reject'; onClick: () => void; children: React.ReactNode }> = ({\n  variant,\n  onClick,\n  children\n}) => {\n  return (\n    <button className={`button ${variant}`} onClick={onClick}>\n      {children}\n    </button>\n  );\n};\n\n\nexport interface TabItemProps {\n  tab: TabInfo;\n  onClick?: () => void;\n  button?: React.ReactNode;\n}\n\nexport const TabItem: React.FC<TabItemProps> = ({\n  tab,\n  onClick,\n  button\n}) => {\n  return (\n    <div className='tab-item' onClick={onClick} style={onClick ? { cursor: 'pointer' } : undefined}>\n      <img\n        src={tab.favIconUrl || 'data:image/svg+xml,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\"><rect width=\"16\" height=\"16\" fill=\"%23f6f8fa\"/></svg>'}\n        alt=''\n        className='tab-favicon'\n      />\n      <div className='tab-content'>\n        <div className='tab-title'>\n          {tab.title || 'Untitled'}\n        </div>\n        <div className='tab-url'>{tab.url}</div>\n      </div>\n      {button}\n    </div>\n  );\n};\n"
  },
  {
    "path": "packages/extension/src/ui/tsconfig.json",
    "content": "// Help VSCode to find right tsconfig file.\n{\n    \"extends\": \"../../tsconfig.ui.json\"\n}\n"
  },
  {
    "path": "packages/extension/tests/extension.spec.ts",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport fs from 'fs/promises';\nimport path from 'path';\nimport { chromium } from 'playwright';\nimport { spawn } from 'child_process';\nimport { test as base, expect } from '../../playwright-mcp/tests/fixtures';\n\nimport type { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport type { BrowserContext } from 'playwright';\nimport type { StartClient } from '../../playwright-mcp/tests/fixtures';\n\ntype BrowserWithExtension = {\n  userDataDir: string;\n  launch: (mode?: 'disable-extension') => Promise<BrowserContext>;\n};\n\ntype CliResult = {\n  output: string;\n  error: string;\n};\n\ntype TestFixtures = {\n  browserWithExtension: BrowserWithExtension,\n  pathToExtension: string,\n  useShortConnectionTimeout: (timeoutMs: number) => void\n  overrideProtocolVersion: (version: number) => void\n  cli: (...args: string[]) => Promise<CliResult>;\n};\n\nconst extensionPublicKey = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwRsUUO4mmbCi4JpmrIoIw31iVW9+xUJRZ6nSzya17PQkaUPDxe1IpgM+vpd/xB6mJWlJSyE1Lj95c0sbomGfVY1M0zUeKbaRVcAb+/a6m59gNR+ubFlmTX0nK9/8fE2FpRB9D+4N5jyeIPQuASW/0oswI2/ijK7hH5NTRX8gWc/ROMSgUj7rKhTAgBrICt/NsStgDPsxRTPPJnhJ/ViJtM1P5KsSYswE987DPoFnpmkFpq8g1ae0eYbQfXy55ieaacC4QWyJPj3daU2kMfBQw7MXnnk0H/WDxouMOIHnd8MlQxpEMqAihj7KpuONH+MUhuj9HEQo4df6bSaIuQ0b4QIDAQAB';\nconst extensionId = 'mmlmfjhmonkocbjadbfplnigmagldckm';\n\nconst test = base.extend<TestFixtures>({\n  pathToExtension: async ({}, use, testInfo) => {\n    const extensionDir = testInfo.outputPath('extension');\n    const srcDir = path.resolve(__dirname, '../dist');\n    await fs.cp(srcDir, extensionDir, { recursive: true });\n    const manifestPath = path.join(extensionDir, 'manifest.json');\n    const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));\n    // We don't hardcode the key in manifest, but for the tests we set the key field\n    // to ensure that locally installed extension has the same id as the one published\n    // in the store.\n    manifest.key = extensionPublicKey;\n    await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));\n    await use(extensionDir);\n  },\n\n  browserWithExtension: async ({ mcpBrowser, pathToExtension }, use, testInfo) => {\n    // The flags no longer work in Chrome since\n    // https://chromium.googlesource.com/chromium/src/+/290ed8046692651ce76088914750cb659b65fb17%5E%21/chrome/browser/extensions/extension_service.cc?pli=1#\n    test.skip('chromium' !== mcpBrowser, '--load-extension is not supported for official builds of Chromium');\n\n    let browserContext: BrowserContext | undefined;\n    const userDataDir = testInfo.outputPath('extension-user-data-dir');\n    await use({\n      userDataDir,\n      launch: async (mode?: 'disable-extension') => {\n        browserContext = await chromium.launchPersistentContext(userDataDir, {\n          channel: mcpBrowser,\n          // Opening the browser singleton only works in headed.\n          headless: false,\n          // Automation disables singleton browser process behavior, which is necessary for the extension.\n          ignoreDefaultArgs: ['--enable-automation'],\n          args: mode === 'disable-extension' ? [] : [\n            `--disable-extensions-except=${pathToExtension}`,\n            `--load-extension=${pathToExtension}`,\n          ],\n        });\n\n        // for manifest v3:\n        let [serviceWorker] = browserContext.serviceWorkers();\n        if (!serviceWorker)\n          serviceWorker = await browserContext.waitForEvent('serviceworker');\n\n        return browserContext;\n      }\n    });\n    await browserContext?.close();\n\n    // Free up disk space.\n    await fs.rm(userDataDir, { recursive: true, force: true }).catch(() => {});\n  },\n\n  useShortConnectionTimeout: async ({}, use) => {\n    await use((timeoutMs: number) => {\n      process.env.PWMCP_TEST_CONNECTION_TIMEOUT = timeoutMs.toString();\n    });\n    process.env.PWMCP_TEST_CONNECTION_TIMEOUT = undefined;\n  },\n\n  overrideProtocolVersion: async ({}, use) => {\n    await use((version: number) => {\n      process.env.PWMCP_TEST_PROTOCOL_VERSION = version.toString();\n    });\n    process.env.PWMCP_TEST_PROTOCOL_VERSION = undefined;\n  },\n\n  cli: async ({ mcpBrowser }, use, testInfo) => {\n    await use(async (...args: string[]) => {\n      return await runCli(args, { mcpBrowser, testInfo });\n    });\n\n    // Cleanup sessions\n    await runCli(['close-all'], { mcpBrowser, testInfo }).catch(() => {});\n\n    const daemonDir = path.join(testInfo.outputDir, 'daemon');\n    await fs.rm(daemonDir, { recursive: true, force: true }).catch(() => {});\n  },\n});\n\nasync function runCli(\n  args: string[],\n  options: { mcpBrowser?: string, testInfo: any },\n): Promise<CliResult> {\n  const stepTitle = `cli ${args.join(' ')}`;\n\n  return await test.step(stepTitle, async () => {\n    const testInfo = options.testInfo;\n\n    // Path to the terminal CLI\n    const cliPath = path.join(__dirname, '../../../node_modules/playwright/lib/cli/client/program.js');\n\n    return new Promise<CliResult>((resolve, reject) => {\n      let stdout = '';\n      let stderr = '';\n\n      const childProcess = spawn(process.execPath, [cliPath, ...args], {\n        cwd: testInfo.outputPath(),\n        env: {\n          ...process.env,\n          PLAYWRIGHT_DAEMON_INSTALL_DIR: testInfo.outputPath(),\n          PLAYWRIGHT_DAEMON_SESSION_DIR: testInfo.outputPath('daemon'),\n          PLAYWRIGHT_DAEMON_SOCKETS_DIR: path.join(testInfo.project.outputDir, 'daemon-sockets'),\n          PLAYWRIGHT_MCP_BROWSER: options.mcpBrowser,\n          PLAYWRIGHT_MCP_HEADLESS: 'false',\n        },\n        detached: true,\n      });\n\n      childProcess.stdout?.on('data', (data) => {\n        stdout += data.toString();\n      });\n\n      childProcess.stderr?.on('data', (data) => {\n        if (process.env.PWMCP_DEBUG)\n          process.stderr.write(data);\n        stderr += data.toString();\n      });\n\n      childProcess.on('close', async (code) => {\n        await testInfo.attach(stepTitle, { body: stdout, contentType: 'text/plain' });\n        resolve({\n          output: stdout.trim(),\n          error: stderr.trim(),\n        });\n      });\n\n      childProcess.on('error', reject);\n    });\n  });\n}\n\nasync function startWithExtensionFlag(browserWithExtension: BrowserWithExtension, startClient: StartClient): Promise<Client> {\n  const { client } = await startClient({\n    args: [`--extension`],\n    config: {\n      browser: {\n        userDataDir: browserWithExtension.userDataDir,\n      }\n    },\n  });\n  return client;\n}\n\nconst testWithOldExtensionVersion = test.extend({\n  pathToExtension: async ({ pathToExtension }, use, testInfo) => {\n    const manifestPath = path.join(pathToExtension, 'manifest.json');\n    const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));\n    manifest.key = extensionPublicKey;\n    manifest.version = '0.0.1';\n    await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2));\n    await use(pathToExtension);\n  },\n});\n\ntest(`navigate with extension`, async ({ browserWithExtension, startClient, server }) => {\n  const browserContext = await browserWithExtension.launch();\n\n  const client = await startWithExtensionFlag(browserWithExtension, startClient);\n\n  const confirmationPagePromise = browserContext.waitForEvent('page', page => {\n    return page.url().startsWith(`chrome-extension://${extensionId}/connect.html`);\n  });\n\n  const navigateResponse = client.callTool({\n    name: 'browser_navigate',\n    arguments: { url: server.HELLO_WORLD },\n  });\n\n  const selectorPage = await confirmationPagePromise;\n  // For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector\n  await selectorPage.getByRole('button', { name: 'Allow' }).click();\n\n  expect(await navigateResponse).toHaveResponse({\n    snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),\n  });\n});\n\ntest(`snapshot of an existing page`, async ({ browserWithExtension, startClient, server }) => {\n  const browserContext = await browserWithExtension.launch();\n\n  const page = await browserContext.newPage();\n  await page.goto(server.HELLO_WORLD);\n\n  // Another empty page.\n  await browserContext.newPage();\n  expect(browserContext.pages()).toHaveLength(3);\n\n  const client = await startWithExtensionFlag(browserWithExtension, startClient);\n  expect(browserContext.pages()).toHaveLength(3);\n\n  const confirmationPagePromise = browserContext.waitForEvent('page', page => {\n    return page.url().startsWith(`chrome-extension://${extensionId}/connect.html`);\n  });\n\n  const navigateResponse = client.callTool({\n    name: 'browser_snapshot',\n    arguments: { },\n  });\n\n  const selectorPage = await confirmationPagePromise;\n  expect(browserContext.pages()).toHaveLength(4);\n\n  await selectorPage.locator('.tab-item', { hasText: 'Title' }).getByRole('button', { name: 'Connect' }).click();\n\n  expect(await navigateResponse).toHaveResponse({\n    snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),\n  });\n\n  expect(browserContext.pages()).toHaveLength(4);\n});\n\ntest(`extension not installed timeout`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => {\n  useShortConnectionTimeout(100);\n\n  const browserContext = await browserWithExtension.launch();\n\n  const client = await startWithExtensionFlag(browserWithExtension, startClient);\n\n  const confirmationPagePromise = browserContext.waitForEvent('page', page => {\n    return page.url().startsWith(`chrome-extension://${extensionId}/connect.html`);\n  });\n\n  expect(await client.callTool({\n    name: 'browser_navigate',\n    arguments: { url: server.HELLO_WORLD },\n  })).toHaveResponse({\n    error: expect.stringContaining('Extension connection timeout. Make sure the \"Playwright MCP Bridge\" extension is installed.'),\n    isError: true,\n  });\n\n  await confirmationPagePromise;\n});\n\ntestWithOldExtensionVersion(`works with old extension version`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout }) => {\n  useShortConnectionTimeout(500);\n\n  // Prelaunch the browser, so that it is properly closed after the test.\n  const browserContext = await browserWithExtension.launch();\n\n  const client = await startWithExtensionFlag(browserWithExtension, startClient);\n\n  const confirmationPagePromise = browserContext.waitForEvent('page', page => {\n    return page.url().startsWith(`chrome-extension://${extensionId}/connect.html`);\n  });\n\n  const navigateResponse = client.callTool({\n    name: 'browser_navigate',\n    arguments: { url: server.HELLO_WORLD },\n  });\n\n  const selectorPage = await confirmationPagePromise;\n  // For browser_navigate command, the UI shows Allow/Reject buttons instead of tab selector\n  await selectorPage.getByRole('button', { name: 'Allow' }).click();\n\n  expect(await navigateResponse).toHaveResponse({\n    snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),\n  });\n});\n\ntest(`extension needs update`, async ({ browserWithExtension, startClient, server, useShortConnectionTimeout, overrideProtocolVersion }) => {\n  useShortConnectionTimeout(500);\n  overrideProtocolVersion(1000);\n\n  // Prelaunch the browser, so that it is properly closed after the test.\n  const browserContext = await browserWithExtension.launch();\n\n  const client = await startWithExtensionFlag(browserWithExtension, startClient);\n\n  const confirmationPagePromise = browserContext.waitForEvent('page', page => {\n    return page.url().startsWith(`chrome-extension://${extensionId}/connect.html`);\n  });\n\n  const navigateResponse = client.callTool({\n    name: 'browser_navigate',\n    arguments: { url: server.HELLO_WORLD },\n  });\n\n  const confirmationPage = await confirmationPagePromise;\n  await expect(confirmationPage.locator('.status-banner')).toContainText(`Playwright MCP version trying to connect requires newer extension version`);\n\n  expect(await navigateResponse).toHaveResponse({\n    error: expect.stringContaining('Extension connection timeout.'),\n    isError: true,\n  });\n});\n\ntest(`custom executablePath`, async ({ startClient, server, useShortConnectionTimeout }) => {\n  useShortConnectionTimeout(1000);\n\n  const executablePath = test.info().outputPath('echo.sh');\n  await fs.writeFile(executablePath, '#!/bin/bash\\necho \"Custom exec args: $@\" > \"$(dirname \"$0\")/output.txt\"', { mode: 0o755 });\n\n  const { client } = await startClient({\n    args: [`--extension`],\n    config: {\n      browser: {\n        launchOptions: {\n          executablePath,\n        },\n      }\n    },\n  });\n\n  const navigateResponse = await client.callTool({\n    name: 'browser_navigate',\n    arguments: { url: server.HELLO_WORLD },\n  });\n  expect(await navigateResponse).toHaveResponse({\n    error: expect.stringContaining('Extension connection timeout.'),\n    isError: true,\n  });\n  expect(await fs.readFile(test.info().outputPath('output.txt'), 'utf8')).toMatch(new RegExp(`Custom exec args.*chrome-extension://${extensionId}/connect\\\\.html\\\\?`));\n});\n\ntest(`bypass connection dialog with token`, async ({ browserWithExtension, startClient, server }) => {\n  const browserContext = await browserWithExtension.launch();\n\n  const page = await browserContext.newPage();\n  await page.goto(`chrome-extension://${extensionId}/status.html`);\n  const token = await page.locator('.auth-token-code').textContent();\n  const [name, value] = token?.split('=') || [];\n\n  const { client } = await startClient({\n    args: [`--extension`],\n    extensionToken: value,\n    config: {\n      browser: {\n        userDataDir: browserWithExtension.userDataDir,\n      }\n    },\n  });\n\n  const navigateResponse = await client.callTool({\n    name: 'browser_navigate',\n    arguments: { url: server.HELLO_WORLD },\n  });\n\n  expect(await navigateResponse).toHaveResponse({\n    snapshot: expect.stringContaining(`- generic [active] [ref=e1]: Hello, world!`),\n  });\n});\n\ntest.describe('CLI with extension', () => {\n  test('open <url> --extension', async ({ browserWithExtension, cli, server }, testInfo) => {\n    const browserContext = await browserWithExtension.launch();\n\n    // Write config file with userDataDir \n    const configPath = testInfo.outputPath('cli-config.json');\n    await fs.writeFile(configPath, JSON.stringify({\n      browser: {\n        userDataDir: browserWithExtension.userDataDir,\n      }\n    }, null, 2));\n\n    const confirmationPagePromise = browserContext.waitForEvent('page', page => {\n      return page.url().startsWith(`chrome-extension://${extensionId}/connect.html`);\n    });\n\n    // Start the CLI command in the background\n    const cliPromise = cli('open', server.HELLO_WORLD, '--extension', `--config=cli-config.json`);\n\n    // Wait for the confirmation page to appear\n    const confirmationPage = await confirmationPagePromise;\n\n    // Click the Connect button\n    await confirmationPage.locator('.tab-item', { hasText: 'Playwright MCP extension' }).getByRole('button', { name: 'Connect' }).click();\n\n    // Wait for the CLI command to complete\n    const { output } = await cliPromise;\n\n    // Verify the output\n    expect(output).toContain(`### Page`);\n    expect(output).toContain(`- Page URL: ${server.HELLO_WORLD}`);\n    expect(output).toContain(`- Page Title: Title`);\n  });\n});\n"
  },
  {
    "path": "packages/extension/tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"rootDir\": \"src\",\n    \"outDir\": \"./dist/lib\",\n    \"resolveJsonModule\": true,\n    \"types\": [\"chrome\"],\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"react\",\n    \"noEmit\": true\n  },\n  \"include\": [\n    \"src\",\n  ],\n  \"exclude\": [\n    \"src/ui\",\n  ]\n}\n"
  },
  {
    "path": "packages/extension/tsconfig.ui.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"esModuleInterop\": true,\n    \"moduleResolution\": \"node\",\n    \"strict\": true,\n    \"module\": \"ESNext\",\n    \"rootDir\": \"src\",\n    \"outDir\": \"./lib\",\n    \"resolveJsonModule\": true,\n    \"types\": [\"chrome\"],\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"react\",\n    \"noEmit\": true,\n  },\n  \"include\": [\n    \"src/ui\",\n  ],\n}\n"
  },
  {
    "path": "packages/extension/vite.config.mts",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { resolve } from 'path';\nimport { defineConfig } from 'vite';\nimport react from '@vitejs/plugin-react';\nimport { viteStaticCopy } from 'vite-plugin-static-copy';\n\n// Public key matching the Chrome Web Store listing — used to fix the extension ID across installs.\n// Set SET_EXTENSION_PUBLIC_KEY_IN_MANIFEST=1 in release builds to inject it into the manifest.\nconst extensionPublicKey = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwRsUUO4mmbCi4JpmrIoIw31iVW9+xUJRZ6nSzya17PQkaUPDxe1IpgM+vpd/xB6mJWlJSyE1Lj95c0sbomGfVY1M0zUeKbaRVcAb+/a6m59gNR+ubFlmTX0nK9/8fE2FpRB9D+4N5jyeIPQuASW/0oswI2/ijK7hH5NTRX8gWc/ROMSgUj7rKhTAgBrICt/NsStgDPsxRTPPJnhJ/ViJtM1P5KsSYswE987DPoFnpmkFpq8g1ae0eYbQfXy55ieaacC4QWyJPj3daU2kMfBQw7MXnnk0H/WDxouMOIHnd8MlQxpEMqAihj7KpuONH+MUhuj9HEQo4df6bSaIuQ0b4QIDAQAB';\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [\n    react(),\n    viteStaticCopy({\n      targets: [\n        {\n          src: '../../icons/*',\n          dest: 'icons'\n        },\n        {\n          src: '../../manifest.json',\n          dest: '.',\n          ...(!!process.env.SET_EXTENSION_PUBLIC_KEY_IN_MANIFEST ? {\n            transform: (content: string) => {\n              const manifest = JSON.parse(content);\n              manifest.key = extensionPublicKey;\n              return JSON.stringify(manifest, null, 2);\n            }\n          } : {})\n        }\n      ]\n    })\n  ],\n  root: resolve(__dirname, 'src/ui'),\n  build: {\n    outDir: resolve(__dirname, 'dist/'),\n    emptyOutDir: false,\n    minify: false,\n    rollupOptions: {\n      input: ['src/ui/connect.html', 'src/ui/status.html'],\n      output: {\n        manualChunks: undefined,\n        entryFileNames: 'lib/ui/[name].js',\n        chunkFileNames: 'lib/ui/[name].js',\n        assetFileNames: 'lib/ui/[name].[ext]'\n      }\n    }\n  }\n});\n"
  },
  {
    "path": "packages/extension/vite.sw.config.mts",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { resolve } from 'path';\nimport { defineConfig } from 'vite';\n\nexport default defineConfig({\n  build: {\n    lib: {\n      entry: resolve(__dirname, 'src/background.ts'),\n      fileName: 'lib/background',\n      formats: ['es']\n    },\n    outDir: 'dist',\n    emptyOutDir: false,\n    minify: false\n  }\n});\n"
  },
  {
    "path": "packages/playwright-cli-stub/LICENSE",
    "content": "                                 Apache License\r\n                           Version 2.0, January 2004\r\n                        http://www.apache.org/licenses/\r\n\r\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\r\n\r\n   1. Definitions.\r\n\r\n      \"License\" shall mean the terms and conditions for use, reproduction,\r\n      and distribution as defined by Sections 1 through 9 of this document.\r\n\r\n      \"Licensor\" shall mean the copyright owner or entity authorized by\r\n      the copyright owner that is granting the License.\r\n\r\n      \"Legal Entity\" shall mean the union of the acting entity and all\r\n      other entities that control, are controlled by, or are under common\r\n      control with that entity. For the purposes of this definition,\r\n      \"control\" means (i) the power, direct or indirect, to cause the\r\n      direction or management of such entity, whether by contract or\r\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\r\n      outstanding shares, or (iii) beneficial ownership of such entity.\r\n\r\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\r\n      exercising permissions granted by this License.\r\n\r\n      \"Source\" form shall mean the preferred form for making modifications,\r\n      including but not limited to software source code, documentation\r\n      source, and configuration files.\r\n\r\n      \"Object\" form shall mean any form resulting from mechanical\r\n      transformation or translation of a Source form, including but\r\n      not limited to compiled object code, generated documentation,\r\n      and conversions to other media types.\r\n\r\n      \"Work\" shall mean the work of authorship, whether in Source or\r\n      Object form, made available under the License, as indicated by a\r\n      copyright notice that is included in or attached to the work\r\n      (an example is provided in the Appendix below).\r\n\r\n      \"Derivative Works\" shall mean any work, whether in Source or Object\r\n      form, that is based on (or derived from) the Work and for which the\r\n      editorial revisions, annotations, elaborations, or other modifications\r\n      represent, as a whole, an original work of authorship. For the purposes\r\n      of this License, Derivative Works shall not include works that remain\r\n      separable from, or merely link (or bind by name) to the interfaces of,\r\n      the Work and Derivative Works thereof.\r\n\r\n      \"Contribution\" shall mean any work of authorship, including\r\n      the original version of the Work and any modifications or additions\r\n      to that Work or Derivative Works thereof, that is intentionally\r\n      submitted to Licensor for inclusion in the Work by the copyright owner\r\n      or by an individual or Legal Entity authorized to submit on behalf of\r\n      the copyright owner. For the purposes of this definition, \"submitted\"\r\n      means any form of electronic, verbal, or written communication sent\r\n      to the Licensor or its representatives, including but not limited to\r\n      communication on electronic mailing lists, source code control systems,\r\n      and issue tracking systems that are managed by, or on behalf of, the\r\n      Licensor for the purpose of discussing and improving the Work, but\r\n      excluding communication that is conspicuously marked or otherwise\r\n      designated in writing by the copyright owner as \"Not a Contribution.\"\r\n\r\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\r\n      on behalf of whom a Contribution has been received by Licensor and\r\n      subsequently incorporated within the Work.\r\n\r\n   2. Grant of Copyright License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      copyright license to reproduce, prepare Derivative Works of,\r\n      publicly display, publicly perform, sublicense, and distribute the\r\n      Work and such Derivative Works in Source or Object form.\r\n\r\n   3. Grant of Patent License. Subject to the terms and conditions of\r\n      this License, each Contributor hereby grants to You a perpetual,\r\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\r\n      (except as stated in this section) patent license to make, have made,\r\n      use, offer to sell, sell, import, and otherwise transfer the Work,\r\n      where such license applies only to those patent claims licensable\r\n      by such Contributor that are necessarily infringed by their\r\n      Contribution(s) alone or by combination of their Contribution(s)\r\n      with the Work to which such Contribution(s) was submitted. If You\r\n      institute patent litigation against any entity (including a\r\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\r\n      or a Contribution incorporated within the Work constitutes direct\r\n      or contributory patent infringement, then any patent licenses\r\n      granted to You under this License for that Work shall terminate\r\n      as of the date such litigation is filed.\r\n\r\n   4. Redistribution. You may reproduce and distribute copies of the\r\n      Work or Derivative Works thereof in any medium, with or without\r\n      modifications, and in Source or Object form, provided that You\r\n      meet the following conditions:\r\n\r\n      (a) You must give any other recipients of the Work or\r\n          Derivative Works a copy of this License; and\r\n\r\n      (b) You must cause any modified files to carry prominent notices\r\n          stating that You changed the files; and\r\n\r\n      (c) You must retain, in the Source form of any Derivative Works\r\n          that You distribute, all copyright, patent, trademark, and\r\n          attribution notices from the Source form of the Work,\r\n          excluding those notices that do not pertain to any part of\r\n          the Derivative Works; and\r\n\r\n      (d) If the Work includes a \"NOTICE\" text file as part of its\r\n          distribution, then any Derivative Works that You distribute must\r\n          include a readable copy of the attribution notices contained\r\n          within such NOTICE file, excluding those notices that do not\r\n          pertain to any part of the Derivative Works, in at least one\r\n          of the following places: within a NOTICE text file distributed\r\n          as part of the Derivative Works; within the Source form or\r\n          documentation, if provided along with the Derivative Works; or,\r\n          within a display generated by the Derivative Works, if and\r\n          wherever such third-party notices normally appear. The contents\r\n          of the NOTICE file are for informational purposes only and\r\n          do not modify the License. You may add Your own attribution\r\n          notices within Derivative Works that You distribute, alongside\r\n          or as an addendum to the NOTICE text from the Work, provided\r\n          that such additional attribution notices cannot be construed\r\n          as modifying the License.\r\n\r\n      You may add Your own copyright statement to Your modifications and\r\n      may provide additional or different license terms and conditions\r\n      for use, reproduction, or distribution of Your modifications, or\r\n      for any such Derivative Works as a whole, provided Your use,\r\n      reproduction, and distribution of the Work otherwise complies with\r\n      the conditions stated in this License.\r\n\r\n   5. Submission of Contributions. Unless You explicitly state otherwise,\r\n      any Contribution intentionally submitted for inclusion in the Work\r\n      by You to the Licensor shall be under the terms and conditions of\r\n      this License, without any additional terms or conditions.\r\n      Notwithstanding the above, nothing herein shall supersede or modify\r\n      the terms of any separate license agreement you may have executed\r\n      with Licensor regarding such Contributions.\r\n\r\n   6. Trademarks. This License does not grant permission to use the trade\r\n      names, trademarks, service marks, or product names of the Licensor,\r\n      except as required for reasonable and customary use in describing the\r\n      origin of the Work and reproducing the content of the NOTICE file.\r\n\r\n   7. Disclaimer of Warranty. Unless required by applicable law or\r\n      agreed to in writing, Licensor provides the Work (and each\r\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\r\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\r\n      implied, including, without limitation, any warranties or conditions\r\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\r\n      PARTICULAR PURPOSE. You are solely responsible for determining the\r\n      appropriateness of using or redistributing the Work and assume any\r\n      risks associated with Your exercise of permissions under this License.\r\n\r\n   8. Limitation of Liability. In no event and under no legal theory,\r\n      whether in tort (including negligence), contract, or otherwise,\r\n      unless required by applicable law (such as deliberate and grossly\r\n      negligent acts) or agreed to in writing, shall any Contributor be\r\n      liable to You for damages, including any direct, indirect, special,\r\n      incidental, or consequential damages of any character arising as a\r\n      result of this License or out of the use or inability to use the\r\n      Work (including but not limited to damages for loss of goodwill,\r\n      work stoppage, computer failure or malfunction, or any and all\r\n      other commercial damages or losses), even if such Contributor\r\n      has been advised of the possibility of such damages.\r\n\r\n   9. Accepting Warranty or Additional Liability. While redistributing\r\n      the Work or Derivative Works thereof, You may choose to offer,\r\n      and charge a fee for, acceptance of support, warranty, indemnity,\r\n      or other liability obligations and/or rights consistent with this\r\n      License. However, in accepting such obligations, You may act only\r\n      on Your own behalf and on Your sole responsibility, not on behalf\r\n      of any other Contributor, and only if You agree to indemnify,\r\n      defend, and hold each Contributor harmless for any liability\r\n      incurred by, or claims asserted against, such Contributor by reason\r\n      of your accepting any such warranty or additional liability.\r\n\r\n   END OF TERMS AND CONDITIONS\r\n\r\n   APPENDIX: How to apply the Apache License to your work.\r\n\r\n      To apply the Apache License to your work, attach the following\r\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\r\n      replaced with your own identifying information. (Don't include\r\n      the brackets!)  The text should be enclosed in the appropriate\r\n      comment syntax for the file format. We also recommend that a\r\n      file or class name and description of purpose be included on the\r\n      same \"printed page\" as the copyright notice for easier\r\n      identification within third-party archives.\r\n\r\n   Copyright (c) Microsoft Corporation.\r\n\r\n   Licensed under the Apache License, Version 2.0 (the \"License\");\r\n   you may not use this file except in compliance with the License.\r\n   You may obtain a copy of the License at\r\n\r\n       http://www.apache.org/licenses/LICENSE-2.0\r\n\r\n   Unless required by applicable law or agreed to in writing, software\r\n   distributed under the License is distributed on an \"AS IS\" BASIS,\r\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\r\n   See the License for the specific language governing permissions and\r\n   limitations under the License.\r\n"
  },
  {
    "path": "packages/playwright-cli-stub/README.md",
    "content": "# 🎭 Playwright CLI\n\nThis package has moved to @playwright/cli.\n\n```sh\n$ npm i -g @playwright/cli\n```\n"
  },
  {
    "path": "packages/playwright-cli-stub/package.json",
    "content": "{\n  \"name\": \"playwright-cli\",\n  \"version\": \"0.262.0\",\n  \"description\": \"Deprecated package, use @playwright/cli instead.\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/microsoft/playwright-cli.git\"\n  },\n  \"homepage\": \"https://playwright.dev\",\n  \"scripts\": {\n    \"lint\": \"echo OK\",\n    \"build\": \"echo OK\",\n    \"test\": \"echo OK\"\n  },\n  \"author\": {\n    \"name\": \"Microsoft Corporation\"\n  },\n  \"license\": \"Apache-2.0\"\n}\n"
  },
  {
    "path": "packages/playwright-mcp/.gitignore",
    "content": "README.md\nLICENSE\n"
  },
  {
    "path": "packages/playwright-mcp/.npmignore",
    "content": "**/*\n!README.md\n!LICENSE\n!cli.js\n!index.*\n!config.d.ts\n"
  },
  {
    "path": "packages/playwright-mcp/cli.js",
    "content": "#!/usr/bin/env node\n/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst { program } = require('playwright-core/lib/utilsBundle');\nconst { decorateMCPCommand } = require('playwright-core/lib/tools/mcp/program');\n\nif (process.argv.includes('install-browser')) {\n  const argv = process.argv.map(arg => arg === 'install-browser' ? 'install' : arg);\n  const { program: mainProgram } = require('playwright-core/lib/cli/program');\n  mainProgram.parse(argv);\n  return;\n}\n\nconst packageJSON = require('./package.json');\nconst p = program.version('Version ' + packageJSON.version).name('Playwright MCP');\ndecorateMCPCommand(p, packageJSON.version)\n\nvoid program.parseAsync(process.argv);\n"
  },
  {
    "path": "packages/playwright-mcp/config.d.ts",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type * as playwright from 'playwright';\n\nexport type ToolCapability =\n  'config' |\n  'core' |\n  'core-navigation' |\n  'core-tabs' |\n  'core-input' |\n  'core-install' |\n  'network' |\n  'pdf' |\n  'storage' |\n  'testing' |\n  'vision' |\n  'devtools';\n\nexport type Config = {\n  /**\n   * The browser to use.\n   */\n  browser?: {\n    /**\n     * The type of browser to use.\n     */\n    browserName?: 'chromium' | 'firefox' | 'webkit';\n\n    /**\n     * Keep the browser profile in memory, do not save it to disk.\n     */\n    isolated?: boolean;\n\n    /**\n     * Path to a user data directory for browser profile persistence.\n     * Temporary directory is created by default.\n     */\n    userDataDir?: string;\n\n    /**\n     * Launch options passed to\n     * @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context\n     *\n     * This is useful for settings options like `channel`, `headless`, `executablePath`, etc.\n     */\n    launchOptions?: playwright.LaunchOptions;\n\n    /**\n     * Context options for the browser context.\n     *\n     * This is useful for settings options like `viewport`.\n     */\n    contextOptions?: playwright.BrowserContextOptions;\n\n    /**\n     * Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers.\n     */\n    cdpEndpoint?: string;\n\n    /**\n     * CDP headers to send with the connect request.\n     */\n    cdpHeaders?: Record<string, string>;\n\n    /**\n     * Timeout in milliseconds for connecting to CDP endpoint. Defaults to 30000 (30 seconds). Pass 0 to disable timeout.\n     */\n    cdpTimeout?: number;\n\n    /**\n     * Remote endpoint to connect to an existing Playwright server.\n     */\n    remoteEndpoint?: string;\n\n    /**\n     * Paths to TypeScript files to add as initialization scripts for Playwright page.\n     */\n    initPage?: string[];\n\n    /**\n     * Paths to JavaScript files to add as initialization scripts.\n     * The scripts will be evaluated in every page before any of the page's scripts.\n     */\n    initScript?: string[];\n  },\n\n  /**\n   * Connect to a running browser instance (Edge/Chrome only). If specified, `browser`\n   * config is ignored.\n   * Requires the \"Playwright MCP Bridge\" browser extension to be installed.\n   */\n  extension?: boolean;\n\n  server?: {\n    /**\n     * The port to listen on for SSE or MCP transport.\n     */\n    port?: number;\n\n    /**\n     * The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.\n     */\n    host?: string;\n\n    /**\n     * The hosts this server is allowed to serve from. Defaults to the host server is bound to.\n     * This is not for CORS, but rather for the DNS rebinding protection.\n     */\n    allowedHosts?: string[];\n  },\n\n  /**\n   * List of enabled tool capabilities. Possible values:\n   *   - 'core': Core browser automation features.\n   *   - 'pdf': PDF generation and manipulation.\n   *   - 'vision': Coordinate-based interactions.\n   *   - 'devtools': Developer tools features.\n   */\n  capabilities?: ToolCapability[];\n\n  /**\n   * Whether to save the Playwright session into the output directory.\n   */\n  saveSession?: boolean;\n\n  /**\n   * Reuse the same browser context between all connected HTTP clients.\n   */\n  sharedBrowserContext?: boolean;\n\n  /**\n   * Secrets are used to prevent LLM from getting sensitive data while\n   * automating scenarios such as authentication.\n   * Prefer the browser.contextOptions.storageState over secrets file as a more secure alternative.\n   */\n  secrets?: Record<string, string>;\n\n  /**\n   * The directory to save output files.\n   */\n  outputDir?: string;\n\n  /**\n   * Whether to save snapshots, console messages, network logs and other session logs to a file or to the standard output. Defaults to \"stdout\".\n   */\n  outputMode?: 'file' | 'stdout';\n\n  console?: {\n    /**\n     * The level of console messages to return. Each level includes the messages of more severe levels. Defaults to \"info\".\n     */\n    level?: 'error' | 'warning' | 'info' | 'debug';\n  },\n\n  network?: {\n    /**\n     * List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.\n     *\n     * Supported formats:\n     * - Full origin: `https://example.com:8080` - matches only that origin\n     * - Wildcard port: `http://localhost:*` - matches any port on localhost with http protocol\n     */\n    allowedOrigins?: string[];\n\n    /**\n     * List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked.\n     *\n     * Supported formats:\n     * - Full origin: `https://example.com:8080` - matches only that origin\n     * - Wildcard port: `http://localhost:*` - matches any port on localhost with http protocol\n     */\n    blockedOrigins?: string[];\n  };\n\n  /**\n   * Specify the attribute to use for test ids, defaults to \"data-testid\".\n   */\n  testIdAttribute?: string;\n\n  timeouts?: {\n    /*\n     * Configures default action timeout: https://playwright.dev/docs/api/class-page#page-set-default-timeout. Defaults to 5000ms.\n     */\n    action?: number;\n\n    /*\n     * Configures default navigation timeout: https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout. Defaults to 60000ms.\n     */\n    navigation?: number;\n\n    /**\n     * Configures default expect timeout: https://playwright.dev/docs/test-timeouts#expect-timeout. Defaults to 5000ms.\n     */\n    expect?: number;\n  };\n\n  /**\n   * Whether to send image responses to the client. Can be \"allow\", \"omit\", or \"auto\". Defaults to \"auto\", which sends images if the client can display them.\n   */\n  imageResponses?: 'allow' | 'omit';\n\n  snapshot?: {\n    /**\n     * When taking snapshots for responses, specifies the mode to use.\n     */\n    mode?: 'incremental' | 'full' | 'none';\n  };\n\n  /**\n   * Whether to allow file uploads from anywhere on the file system.\n   * By default (false), file uploads are restricted to paths within the MCP roots only.\n   */\n  allowUnrestrictedFileAccess?: boolean;\n\n  /**\n   * Specify the language to use for code generation.\n   */\n  codegen?: 'typescript' | 'none';\n};\n"
  },
  {
    "path": "packages/playwright-mcp/index.d.ts",
    "content": "#!/usr/bin/env node\n/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport type { Config } from './config';\nimport type { BrowserContext } from 'playwright';\n\nexport declare function createConnection(config?: Config, contextGetter?: () => Promise<BrowserContext>): Promise<Server>;\nexport {};\n"
  },
  {
    "path": "packages/playwright-mcp/index.js",
    "content": "#!/usr/bin/env node\n/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst { createConnection } = require('playwright-core/lib/tools/exports');\nmodule.exports = { createConnection };\n"
  },
  {
    "path": "packages/playwright-mcp/package.json",
    "content": "{\n  \"name\": \"@playwright/mcp\",\n  \"version\": \"0.0.68\",\n  \"description\": \"Playwright Tools for MCP\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/microsoft/playwright-mcp.git\"\n  },\n  \"homepage\": \"https://playwright.dev\",\n  \"engines\": {\n    \"node\": \">=18\"\n  },\n  \"author\": {\n    \"name\": \"Microsoft Corporation\"\n  },\n  \"license\": \"Apache-2.0\",\n  \"mcpName\": \"io.github.microsoft/playwright-mcp\",\n  \"scripts\": {\n    \"lint\": \"node update-readme.js\",\n    \"test\": \"playwright test\",\n    \"ctest\": \"playwright test --project=chrome\",\n    \"ftest\": \"playwright test --project=firefox\",\n    \"wtest\": \"playwright test --project=webkit\",\n    \"dtest\": \"MCP_IN_DOCKER=1 playwright test --project=chromium-docker\",\n    \"build\": \"echo OK\",\n    \"npm-publish\": \"npm run lint && npm run test && npm publish\"\n  },\n  \"exports\": {\n    \"./package.json\": \"./package.json\",\n    \".\": {\n      \"types\": \"./index.d.ts\",\n      \"default\": \"./index.js\"\n    }\n  },\n  \"dependencies\": {\n    \"playwright\": \"1.59.0-alpha-1773608981000\",\n    \"playwright-core\": \"1.59.0-alpha-1773608981000\"\n  },\n  \"bin\": {\n    \"playwright-mcp\": \"cli.js\"\n  }\n}\n"
  },
  {
    "path": "packages/playwright-mcp/playwright.config.ts",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { defineConfig } from '@playwright/test';\n\nimport type { TestOptions } from './tests/fixtures';\n\nexport default defineConfig<TestOptions>({\n  testDir: './tests',\n  fullyParallel: true,\n  forbidOnly: !!process.env.CI,\n  workers: process.env.CI ? 2 : undefined,\n  reporter: 'list',\n  projects: [\n    { name: 'chrome' },\n    ...process.env.MCP_IN_DOCKER ? [{\n      name: 'chromium-docker',\n      grep: /browser_navigate|browser_click/,\n      use: {\n        mcpBrowser: 'chromium',\n        mcpMode: 'docker' as const\n      }\n    }] : [],\n  ],\n});\n"
  },
  {
    "path": "packages/playwright-mcp/tests/capabilities.spec.ts",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { test, expect } from './fixtures';\n\ntest('test snapshot tool list', async ({ client }) => {\n  const { tools } = await client.listTools();\n  expect(new Set(tools.map(t => t.name))).toEqual(new Set([\n    'browser_click',\n    'browser_console_messages',\n    'browser_drag',\n    'browser_evaluate',\n    'browser_file_upload',\n    'browser_fill_form',\n    'browser_handle_dialog',\n    'browser_hover',\n    'browser_select_option',\n    'browser_type',\n    'browser_close',\n    'browser_navigate_back',\n    'browser_navigate',\n    'browser_network_requests',\n    'browser_press_key',\n    'browser_resize',\n    'browser_run_code',\n    'browser_snapshot',\n    'browser_tabs',\n    'browser_take_screenshot',\n    'browser_wait_for',\n  ]));\n});\n\ntest('test capabilities (pdf)', async ({ startClient }) => {\n  const { client } = await startClient({\n    args: ['--caps=pdf'],\n  });\n  const { tools } = await client.listTools();\n  const toolNames = tools.map(t => t.name);\n  expect(toolNames).toContain('browser_pdf_save');\n});\n\ntest('test capabilities (vision)', async ({ startClient }) => {\n  const { client } = await startClient({\n    args: ['--caps=vision'],\n  });\n  const { tools } = await client.listTools();\n  const toolNames = tools.map(t => t.name);\n  expect(toolNames).toContain('browser_mouse_move_xy');\n  expect(toolNames).toContain('browser_mouse_click_xy');\n  expect(toolNames).toContain('browser_mouse_drag_xy');\n});\n\ntest('support for legacy --vision option', async ({ startClient }) => {\n  const { client } = await startClient({\n    args: ['--vision'],\n  });\n  const { tools } = await client.listTools();\n  const toolNames = tools.map(t => t.name);\n  expect(toolNames).toContain('browser_mouse_move_xy');\n  expect(toolNames).toContain('browser_mouse_click_xy');\n  expect(toolNames).toContain('browser_mouse_drag_xy');\n});\n"
  },
  {
    "path": "packages/playwright-mcp/tests/cli.spec.ts",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport child_process from 'child_process';\nimport path from 'path';\nimport { test, expect } from './fixtures';\n\nconst cliPath = path.resolve(__dirname, '..', 'cli.js');\n\ntest('install-browser --help', async () => {\n  const output = child_process.execSync(`node ${cliPath} install-browser --help`, { encoding: 'utf-8' });\n  expect(output).toContain('install');\n});\n"
  },
  {
    "path": "packages/playwright-mcp/tests/click.spec.ts",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { test, expect } from './fixtures';\n\ntest('browser_click', async ({ client, server }) => {\n  server.setContent('/', `\n    <title>Title</title>\n    <button>Submit</button>\n    <script>\n      const button = document.querySelector('button');\n      button.addEventListener('click', () => {\n        button.focus(); // without manual focus, webkit focuses body\n      });\n    </script>\n  `, 'text/html');\n\n  expect(await client.callTool({\n    name: 'browser_navigate',\n    arguments: { url: server.PREFIX },\n  })).toHaveResponse({\n    code: `await page.goto('${server.PREFIX}');`,\n    snapshot: expect.stringContaining(`- button \\\"Submit\\\" [ref=e2]`),\n  });\n\n  expect(await client.callTool({\n    name: 'browser_click',\n    arguments: {\n      element: 'Submit button',\n      ref: 'e2',\n    },\n  })).toHaveResponse({\n    code: `await page.getByRole('button', { name: 'Submit' }).click();`,\n    snapshot: expect.stringContaining(`button \"Submit\" [active] [ref=e2]`),\n  });\n});\n"
  },
  {
    "path": "packages/playwright-mcp/tests/core.spec.ts",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { test, expect } from './fixtures';\n\ntest('browser_navigate', async ({ client, server }) => {\n  expect(await client.callTool({\n    name: 'browser_navigate',\n    arguments: { url: server.HELLO_WORLD },\n  })).toHaveResponse({\n    code: `await page.goto('${server.HELLO_WORLD}');`,\n    snapshot: expect.stringContaining(`generic [active] [ref=e1]: Hello, world!`),\n  });\n});\n"
  },
  {
    "path": "packages/playwright-mcp/tests/fixtures.ts",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport fs from 'fs';\nimport path from 'path';\nimport { chromium } from 'playwright';\n\nimport { test as baseTest, expect as baseExpect } from '@playwright/test';\nimport { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';\nimport { Client } from '@modelcontextprotocol/sdk/client/index.js';\nimport { ListRootsRequestSchema } from '@modelcontextprotocol/sdk/types.js';\nimport { TestServer } from './testserver/index';\n\nimport type { Config } from '../config';\nimport type { BrowserContext } from 'playwright';\nimport type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';\nimport type { Stream } from 'stream';\n\nexport type TestOptions = {\n  mcpArgs: string[] | undefined;\n  mcpBrowser: string | undefined;\n  mcpMode: 'docker' | undefined;\n};\n\ntype CDPServer = {\n  endpoint: string;\n  start: () => Promise<BrowserContext>;\n};\n\nexport type StartClient = (options?: {\n  clientName?: string,\n  args?: string[],\n  config?: Config,\n  roots?: { name: string, uri: string }[],\n  rootsResponseDelay?: number,\n  extensionToken?: string,\n}) => Promise<{ client: Client, stderr: () => string }>;\n\n\ntype TestFixtures = {\n  client: Client;\n  startClient: StartClient;\n  wsEndpoint: string;\n  cdpServer: CDPServer;\n  server: TestServer;\n  httpsServer: TestServer;\n  mcpHeadless: boolean;\n};\n\ntype WorkerFixtures = {\n  _workerServers: { server: TestServer, httpsServer: TestServer };\n};\n\nexport const test = baseTest.extend<TestFixtures & TestOptions, WorkerFixtures>({\n\n  mcpArgs: [undefined, { option: true }],\n\n  client: async ({ startClient }, use) => {\n    const { client } = await startClient();\n    await use(client);\n  },\n\n  startClient: async ({ mcpHeadless, mcpBrowser, mcpMode, mcpArgs }, use, testInfo) => {\n    const configDir = path.dirname(test.info().config.configFile!);\n    const clients: Client[] = [];\n\n    await use(async options => {\n      const args: string[] = mcpArgs ?? [];\n      if (process.env.CI && process.platform === 'linux')\n        args.push('--no-sandbox');\n      if (mcpHeadless)\n        args.push('--headless');\n      if (mcpBrowser)\n        args.push(`--browser=${mcpBrowser}`);\n      if (options?.args)\n        args.push(...options.args);\n      if (options?.config) {\n        const configFile = testInfo.outputPath('config.json');\n        await fs.promises.writeFile(configFile, JSON.stringify(options.config, null, 2));\n        args.push(`--config=${path.relative(configDir, configFile)}`);\n      }\n\n      const client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' }, options?.roots ? { capabilities: { roots: {} } } : undefined);\n      if (options?.roots) {\n        client.setRequestHandler(ListRootsRequestSchema, async request => {\n          if (options.rootsResponseDelay)\n            await new Promise(resolve => setTimeout(resolve, options.rootsResponseDelay));\n          return {\n            roots: options.roots,\n          };\n        });\n      }\n      const { transport, stderr } = await createTransport(args, mcpMode, testInfo.outputPath('ms-playwright'), options?.extensionToken);\n      let stderrBuffer = '';\n      stderr?.on('data', data => {\n        if (process.env.PWMCP_DEBUG)\n          process.stderr.write(data);\n        stderrBuffer += data.toString();\n      });\n      clients.push(client);\n      await client.connect(transport);\n      await client.ping();\n      return { client, stderr: () => stderrBuffer };\n    });\n\n    await Promise.all(clients.map(client => client.close()));\n  },\n\n  wsEndpoint: async ({ }, use) => {\n    const browserServer = await chromium.launchServer();\n    await use(browserServer.wsEndpoint());\n    await browserServer.close();\n  },\n\n  cdpServer: async ({ mcpBrowser }, use, testInfo) => {\n    test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser!), 'CDP is not supported for non-Chromium browsers');\n\n    let browserContext: BrowserContext | undefined;\n    const port = 3200 + test.info().parallelIndex;\n    await use({\n      endpoint: `http://localhost:${port}`,\n      start: async () => {\n        if (browserContext)\n          throw new Error('CDP server already exists');\n        browserContext = await chromium.launchPersistentContext(testInfo.outputPath('cdp-user-data-dir'), {\n          channel: mcpBrowser,\n          headless: true,\n          args: [\n            `--remote-debugging-port=${port}`,\n          ],\n        });\n        return browserContext;\n      }\n    });\n    await browserContext?.close();\n  },\n\n  mcpHeadless: async ({ headless }, use) => {\n    await use(headless);\n  },\n\n  mcpBrowser: ['chrome', { option: true }],\n\n  mcpMode: [undefined, { option: true }],\n\n  _workerServers: [async ({ }, use, workerInfo) => {\n    const port = 8907 + workerInfo.workerIndex * 4;\n    const server = await TestServer.create(port);\n\n    const httpsPort = port + 1;\n    const httpsServer = await TestServer.createHTTPS(httpsPort);\n\n    await use({ server, httpsServer });\n\n    await Promise.all([\n      server.stop(),\n      httpsServer.stop(),\n    ]);\n  }, { scope: 'worker' }],\n\n  server: async ({ _workerServers }, use) => {\n    _workerServers.server.reset();\n    await use(_workerServers.server);\n  },\n\n  httpsServer: async ({ _workerServers }, use) => {\n    _workerServers.httpsServer.reset();\n    await use(_workerServers.httpsServer);\n  },\n});\n\nasync function createTransport(args: string[], mcpMode: TestOptions['mcpMode'], profilesDir: string, extensionToken?: string): Promise<{\n  transport: Transport,\n  stderr: Stream | null,\n}> {\n  if (mcpMode === 'docker') {\n    const dockerArgs = ['run', '--rm', '-i', '--network=host', '-v', `${test.info().project.outputDir}:/app/test-results`];\n    const transport = new StdioClientTransport({\n      command: 'docker',\n      args: [...dockerArgs, 'playwright-mcp-dev:latest', ...args],\n    });\n    return {\n      transport,\n      stderr: transport.stderr,\n    };\n  }\n\n  const transport = new StdioClientTransport({\n    command: 'node',\n    args: [path.join(__dirname, '../cli.js'), ...args],\n    cwd: path.dirname(test.info().config.configFile!),\n    stderr: 'pipe',\n    env: {\n      ...process.env,\n      DEBUG: 'pw:mcp:test',\n      DEBUG_COLORS: '0',\n      DEBUG_HIDE_DATE: '1',\n      PWMCP_PROFILES_DIR_FOR_TEST: profilesDir,\n      ...(extensionToken ? { PLAYWRIGHT_MCP_EXTENSION_TOKEN: extensionToken } : {}),\n    },\n  });\n  return {\n    transport,\n    stderr: transport.stderr!,\n  };\n}\n\ntype Response = Awaited<ReturnType<Client['callTool']>>;\n\nexport const expect = baseExpect.extend({\n  toHaveResponse(response: Response, object: any) {\n    const parsed = parseResponse(response);\n    const isNot = this.isNot;\n    try {\n      if (isNot)\n        expect(parsed).not.toEqual(expect.objectContaining(object));\n      else\n        expect(parsed).toEqual(expect.objectContaining(object));\n    } catch (e: any) {\n      return {\n        pass: isNot,\n        message: () => e.message,\n      };\n    }\n    return {\n      pass: !isNot,\n      message: () => ``,\n    };\n  },\n});\n\nexport function formatOutput(output: string): string[] {\n  return output.split('\\n').map(line => line.replace(/^pw:mcp:test /, '').replace(/user data dir.*/, 'user data dir').trim()).filter(Boolean);\n}\n\nfunction parseResponse(response: any) {\n  const text = response.content[0].text;\n  const sections = parseSections(text);\n\n  const error = sections.get('Error');\n  const result = sections.get('Result');\n  const code = sections.get('Ran Playwright code');\n  const tabs = sections.get('Open tabs');\n  const pageState = sections.get('Page state');\n  const snapshot = sections.get('Snapshot');\n  const consoleMessages = sections.get('New console messages');\n  const modalState = sections.get('Modal state');\n  const downloads = sections.get('Downloads');\n  const codeNoFrame = code?.replace(/^```js\\n/, '').replace(/\\n```$/, '');\n  const isError = response.isError;\n  const attachments = response.content.slice(1);\n\n  return {\n    error,\n    result,\n    code: codeNoFrame,\n    tabs,\n    pageState,\n    snapshot,\n    consoleMessages,\n    modalState,\n    downloads,\n    isError,\n    attachments,\n  };\n}\n\nfunction parseSections(text: string): Map<string, string> {\n  const sections = new Map<string, string>();\n  const sectionHeaders = text.split(/^### /m).slice(1); // Remove empty first element\n\n  for (const section of sectionHeaders) {\n    const firstNewlineIndex = section.indexOf('\\n');\n    if (firstNewlineIndex === -1)\n      continue;\n\n    const sectionName = section.substring(0, firstNewlineIndex);\n    const sectionContent = section.substring(firstNewlineIndex + 1).trim();\n    sections.set(sectionName, sectionContent);\n  }\n\n  return sections;\n}\n"
  },
  {
    "path": "packages/playwright-mcp/tests/library.spec.ts",
    "content": "/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport child_process from 'child_process';\nimport fs from 'fs/promises';\nimport { test, expect } from './fixtures';\n\ntest('library can be used from CommonJS', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/456' } }, async ({}, testInfo) => {\n  const file = testInfo.outputPath('main.cjs');\n  await fs.writeFile(file, `\n    import('@playwright/mcp')\n      .then(playwrightMCP => playwrightMCP.createConnection())\n      .then(() => console.log('OK'));\n `);\n  expect(child_process.execSync(`node ${file}`, { encoding: 'utf-8' })).toContain('OK');\n});\n"
  },
  {
    "path": "packages/playwright-mcp/tests/testserver/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIFCjCCAvKgAwIBAgIULU/gkDm8IqC7PG8u3RID0AYyP6gwDQYJKoZIhvcNAQEL\nBQAwGjEYMBYGA1UEAwwPcGxheXdyaWdodC10ZXN0MB4XDTIzMDgxMDIyNTc1MFoX\nDTMzMDgwNzIyNTc1MFowGjEYMBYGA1UEAwwPcGxheXdyaWdodC10ZXN0MIICIjAN\nBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArbS99qjKcnHr5G0Zc2xhDaOZnjQv\nFbiqxf/nbXt/7WaqryzpVKu7AT1ainBvuPEo7If9DhVnfF//2pGl0gbU31OU4/mr\nymQmczGEyZvOBDsZhtCif54o5OoO0BjhODNT8OWec9RT87n6RkH58MHlOi8xsPxQ\n9n5U1CN/h2DyQF3aRKunEFCgtwPKWSjG+J/TAI9i0aSENXPiR8wjTrjg79s8Ehuj\nNN8Wk6rKLU3sepG3GIMID5vLsVa2t9xqn562sP95Ee+Xp2YX3z7oYK99QCJdzacw\nalhMHob1GCEKjDyxsD2IFRi7Dysiutfyzy3pMo6NALxFrwKVhWX0L4zVFIsI6JlV\ndK8dHmDk0MRSqgB9sWXvEfSTXADEe8rncFSFpFz4Z8RNLmn5YSzQJzokNn41DUCP\ndZTlTkcGTqvn5NqoY4sOV8rkFbgmTcqyijV/sebPjxCbJNcNmaSWa9FJ5IjRTpzM\n38wLmxn+eKGK68n2JB3P7JP6LtsBShQEpXAF3rFfyNsP1bjquvGZVSjV8w/UwPE4\nkV5eq3j3D4913Zfxvzjp6PEmhStG0EQtIXvx/TRoYpaNWypIgZdbkZQp1HUIQL15\nD2Web4nazP3so1FC3ZgbrJZ2ozoadjLMp49NcSFdh+WRyVKuo0DIqR0zaiAzzf2D\nG1q7TLKimM3XBMUCAwEAAaNIMEYwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwLAYD\nVR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqG\nSIb3DQEBCwUAA4ICAQAvC5M1JFc21WVSLPvE2iVbt4HmirO3EENdDqs+rTYG5VJG\niE5ZuI6h/LjS5ptTfKovXQKaMr3pwp1pLMd/9q+6ZR1Hs9Z2wF6OZan4sb0uT32Y\n1KGlj86QMiiSLdrJ/1Z9JHskHYNCep1ZTsUhGk0qqiNv+G3K2y7ZpvrT/xlnYMth\nKLTuSVUwM8BBEPrCRLoXuaEy0LnvMvMVepIfP8tnMIL6zqmj3hXMPe4r4OFV/C5o\nXX25bC7GyuPWIRYn2OWP92J1CODZD1rGRoDtmvqrQpHdeX9RYcKH0ZLZoIf5L3Hf\npPUtVkw3QGtjvKeG3b9usxaV9Od2Z08vKKk1PRkXFe8gqaeyicK7YVIOMTSuspAf\nJeJEHns6Hg61Exbo7GwdX76xlmQ/Z43E9BPHKgLyZ9WuJ0cysqN4aCyvS9yws9to\nki7iMZqJUsmE2o09n9VaEsX6uQANZtLjI9wf+IgJuueDTNrkzQkhU7pbaPMsSG40\nAgGY/y4BR0H8sbhNnhqtZH7RcXV9VCJoPBAe+YiuXRiXyZHWxwBRyBE3e7g4MKHg\nhrWtaWUAs7gbavHwjqgU63iVItDSk7t4fCiEyObjK09AaNf2DjjaSGf8YGza4bNy\nBjYinYJ6/eX//gp+abqfocFbBP7D9zRDgMIbVmX/Ey6TghKiLkZOdbzcpO4Wgg==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "packages/playwright-mcp/tests/testserver/index.ts",
    "content": "/**\n * Copyright 2017 Google Inc. All rights reserved.\n * Modifications copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport fs from 'fs';\nimport http from 'http';\nimport https from 'https';\nimport path from 'path';\nimport debug from 'debug';\n\nconst fulfillSymbol = Symbol('fulfil callback');\nconst rejectSymbol = Symbol('reject callback');\n\nexport class TestServer {\n  private _server: http.Server;\n  readonly debugServer: any;\n  private _routes = new Map<string, (request: http.IncomingMessage, response: http.ServerResponse) => any>();\n  private _csp = new Map<string, string>();\n  private _extraHeaders = new Map<string, object>();\n  private _requestSubscribers = new Map<string, Promise<any>>();\n  readonly PORT: number;\n  readonly PREFIX: string;\n  readonly CROSS_PROCESS_PREFIX: string;\n  readonly HELLO_WORLD: string;\n\n  static async create(port: number): Promise<TestServer> {\n    const server = new TestServer(port);\n    await new Promise(x => server._server.once('listening', x));\n    return server;\n  }\n\n  static async createHTTPS(port: number): Promise<TestServer> {\n    const server = new TestServer(port, {\n      key: await fs.promises.readFile(path.join(path.dirname(__filename), 'key.pem')),\n      cert: await fs.promises.readFile(path.join(path.dirname(__filename), 'cert.pem')),\n      passphrase: 'aaaa',\n    });\n    await new Promise(x => server._server.once('listening', x));\n    return server;\n  }\n\n  constructor(port: number, sslOptions?: object) {\n    if (sslOptions)\n      this._server = https.createServer(sslOptions, this._onRequest.bind(this));\n    else\n      this._server = http.createServer(this._onRequest.bind(this));\n    this._server.listen(port);\n    this.debugServer = debug('pw:testserver');\n\n    const cross_origin = '127.0.0.1';\n    const same_origin = 'localhost';\n    const protocol = sslOptions ? 'https' : 'http';\n    this.PORT = port;\n    this.PREFIX = `${protocol}://${same_origin}:${port}/`;\n    this.CROSS_PROCESS_PREFIX = `${protocol}://${cross_origin}:${port}/`;\n    this.HELLO_WORLD = `${this.PREFIX}hello-world`;\n  }\n\n  setCSP(path: string, csp: string) {\n    this._csp.set(path, csp);\n  }\n\n  setExtraHeaders(path: string, object: Record<string, string>) {\n    this._extraHeaders.set(path, object);\n  }\n\n  async stop() {\n    this.reset();\n    await new Promise(x => this._server.close(x));\n  }\n\n  route(path: string, handler: (request: http.IncomingMessage, response: http.ServerResponse) => any) {\n    this._routes.set(path, handler);\n  }\n\n  setContent(path: string, content: string, mimeType: string) {\n    this.route(path, (req, res) => {\n      res.writeHead(200, { 'Content-Type': mimeType });\n      res.end(mimeType === 'text/html' ? `<!DOCTYPE html>${content}` : content);\n    });\n  }\n\n  redirect(from: string, to: string) {\n    this.route(from, (req, res) => {\n      const headers = this._extraHeaders.get(req.url!) || {};\n      res.writeHead(302, { ...headers, location: to });\n      res.end();\n    });\n  }\n\n  waitForRequest(path: string): Promise<http.IncomingMessage> {\n    let promise = this._requestSubscribers.get(path);\n    if (promise)\n      return promise;\n    let fulfill, reject;\n    promise = new Promise((f, r) => {\n      fulfill = f;\n      reject = r;\n    });\n    promise[fulfillSymbol] = fulfill;\n    promise[rejectSymbol] = reject;\n    this._requestSubscribers.set(path, promise);\n    return promise;\n  }\n\n  reset() {\n    this._routes.clear();\n    this._csp.clear();\n    this._extraHeaders.clear();\n    this._server.closeAllConnections();\n    const error = new Error('Static Server has been reset');\n    for (const subscriber of this._requestSubscribers.values())\n      subscriber[rejectSymbol].call(null, error);\n    this._requestSubscribers.clear();\n\n    this.setContent('/favicon.ico', '', 'image/x-icon');\n\n    this.setContent('/', ``, 'text/html');\n\n    this.setContent('/hello-world', `\n      <title>Title</title>\n      <body>Hello, world!</body>\n    `, 'text/html');\n  }\n\n  _onRequest(request: http.IncomingMessage, response: http.ServerResponse) {\n    request.on('error', error => {\n      if ((error as any).code === 'ECONNRESET')\n        response.end();\n      else\n        throw error;\n    });\n    (request as any).postBody = new Promise(resolve => {\n      const chunks: Buffer[] = [];\n      request.on('data', chunk => {\n        chunks.push(chunk);\n      });\n      request.on('end', () => resolve(Buffer.concat(chunks)));\n    });\n    const path = request.url || '/';\n    this.debugServer(`request ${request.method} ${path}`);\n    // Notify request subscriber.\n    if (this._requestSubscribers.has(path)) {\n      this._requestSubscribers.get(path)![fulfillSymbol].call(null, request);\n      this._requestSubscribers.delete(path);\n    }\n    const handler = this._routes.get(path);\n    if (handler) {\n      handler.call(null, request, response);\n    } else {\n      response.writeHead(404);\n      response.end();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/playwright-mcp/tests/testserver/key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCttL32qMpycevk\nbRlzbGENo5meNC8VuKrF/+dte3/tZqqvLOlUq7sBPVqKcG+48Sjsh/0OFWd8X//a\nkaXSBtTfU5Tj+avKZCZzMYTJm84EOxmG0KJ/nijk6g7QGOE4M1Pw5Z5z1FPzufpG\nQfnwweU6LzGw/FD2flTUI3+HYPJAXdpEq6cQUKC3A8pZKMb4n9MAj2LRpIQ1c+JH\nzCNOuODv2zwSG6M03xaTqsotTex6kbcYgwgPm8uxVra33Gqfnraw/3kR75enZhff\nPuhgr31AIl3NpzBqWEwehvUYIQqMPLGwPYgVGLsPKyK61/LPLekyjo0AvEWvApWF\nZfQvjNUUiwjomVV0rx0eYOTQxFKqAH2xZe8R9JNcAMR7yudwVIWkXPhnxE0uaflh\nLNAnOiQ2fjUNQI91lOVORwZOq+fk2qhjiw5XyuQVuCZNyrKKNX+x5s+PEJsk1w2Z\npJZr0UnkiNFOnMzfzAubGf54oYrryfYkHc/sk/ou2wFKFASlcAXesV/I2w/VuOq6\n8ZlVKNXzD9TA8TiRXl6rePcPj3Xdl/G/OOno8SaFK0bQRC0he/H9NGhilo1bKkiB\nl1uRlCnUdQhAvXkPZZ5vidrM/eyjUULdmBuslnajOhp2Msynj01xIV2H5ZHJUq6j\nQMipHTNqIDPN/YMbWrtMsqKYzdcExQIDAQABAoICAGqXttpdyZ1g+vg5WpzRrNzJ\nv8KtExepMmI+Hq24U1BC6AqG7MfgeejQ1XaOeIBsvEgpSsgRqmdQIZjmN3Mibg59\nI6ih1SFlQ5L8mBd/XHSML6Xi8VSOoVmXp29bVRk/pgr1XL6HVN0DCumCIvXyhc+m\nlj+dFbGs5DEpd2CDxSRqcz4gd2wzjevAj7MWqsJ2kOyPEHzFD7wdWIXmZuQv3xhQ\n2BPkkcon+5qx+07BupOcR1brUU8Cs4QnSgiZYXSB2GnU215+P/mhVJTR7ZcnGRz5\n+cXxCmy3sj4pYs1juS1FMWSM3azUeDVeqvks+vrXmXpEr5H79mbmlwo8/hMPwNDO\n07HRZwa8T01aT9EYVm0lIOYjMF/2f6j6cu2apJtjXICOksR2HefRBVXQirOxRHma\n9XAYfNkZ/2164ZbgFmJv9khFnegPEuth9tLVdFIeGSmsG0aX9tH63zGT2NROyyLc\nQXPqsDl2CxCYPRs2oiGkM9dnfP1wAOp96sq42GIuN7ykfqfRnwAIvvnLKvyCq1vR\npIno3CIX6vnzt+1/Hrmv13b0L6pJPitpXwKWHv9zJKBTpN8HEzP3Qmth2Ef60/7/\nCBo1PVTd1A6zcU7816flg7SCY+Vk+OxVHV3dGBIIqN9SfrQ8BPcOl6FNV5Anbrnv\nCpSw+LzH9n5xympDnk0BAoIBAQDjenvDfCnrNVeqx8+sYaYey4/WPVLXOQhREvRY\noOtX9eqlNSi20+Wl+iuXmyj8wdHrDET7rfjCbpDQ7u105yzLw4gy4qIRDKZ1nE45\nYX+tm8mZgBqRnTp0DoGOArqmp3IKXJtUYmpbTz9tOfY7Usb1o1epb4winEB+Pl+8\nmgXOEo8xvWBzKeRA7tE73V64Mwbvbo9Ff2EguhXweQP29yBkEjT4iViayuHUmyPt\nhOVSMj2oFQuQGPdhAk7nUXojSGK/Zas/AGpH9CHH9De0h4m08vd3oM4vj0HwzgjU\nCo9aRa9SAH7EiaocOTcjDRPxWdZPHhxmrVRIYlF0MNmOAkXJAoIBAQDDfEqu4sNi\npq74VXVatQqhzCILZo+o48bdgEjF7mF99mqPj8rwIDrEoEriDK861kenLc3vWKRY\n5wh1iX3S896re9kUMoxx6p4heYTcsOJ9BbkcpT8bJPZx9gBJb4jJENeVf1exf6sG\nRhFnulpzReRRaUjX2yAkyUPfc8YcUt+Nalrg+2W0fzeLCUpABCAcj2B1Vv7qRZHj\noEtlCV5Nz+iMhrwIa16g9c8wGt5DZb4PI+VIJ6EYkdsjhgqIF0T/wDq9/habGBPo\nmHN+/DX3hCJWN2QgoVGJskHGt0zDMgiEgXfLZ2Grl02vQtq+mW2O2vGVeUd9Y5Ew\nRUiY4bSRTrUdAoIBAHxL1wiP9c/By+9TUtScXssA681ioLtdPIAgXUd4VmAvzVEM\nZPzRd/BjbCJg89p4hZ1rjN4Ax6ZmB9dCVpnEH6QPaYJ0d53dTa+CAvQzpDJWp6eq\nadobEW+M5ZmVQCwD3rpus6k+RWMzQDMMstDjgDeEU0gP3YCj5FGW/3TsrDNXzMqe\n8e67ey9Hzyho43K+3xFBViPhYE8jnw1Q8quliRtlH3CWi8W5CgDD7LPCJBPvw+Tt\n6u2H1tQ5EKgwyw4wZVSz1wiLz4cVjMfXWADa9pHbGQFS6pbuLlfIHObQBliLLysd\nficiGcNmOAx8/uKn9gQxLc+k8iLDJkLY1mdUMpECggEAJLl87k37ltTpmg2z9k58\nqNjIrIugAYKJIaOwCD84YYmhi0bgQSxM3hOe/ciUQuFupKGeRpDIj0sX87zYvoDC\nHEUwCvNUHzKMco15wFwasJIarJ7+tALFqbMlaqZhdCSN27AIsXfikVMogewoge9n\nbUPyQ1sPNtn4vknptfh7tv18BTg1aytbK+ua31vnDHaDEIg/a5OWTMUYZOrVpJii\nf4PwX0SMioCjY84oY1EB26ZKtLt9MDh2ir3rzJVSiRl776WEaa6kTtYVHI4VNWLF\ncJ0HWnnz74JliQd2jFUh9IK+FqBdYPcTyREuNxBr3KKVMBeQrqW96OubL913JrU6\noQKCAQEA0yzORUouT0yleWs7RmzBlT9OLD/3cBYJMf/r1F8z8OQjB8fU1jKbO1Cs\nq4l+o9FmI+eHkgc3xbEG0hahOFWm/hTTli9vzksxurgdawZELThRkK33uTU9pKla\nOkqx3Ru/iMOW2+DQUx9UB+jK+hSAgq4gGqLeJVyaBerIdLQLlvqxrwSxjvvj+wJC\nY66mgRzdCi6VDF1vV0knCrQHK6tRwcPozu/k4zjJzvdbMJnKEy2S7Vh6vO8lEPJm\nMQtaHPpmz+F4z14b9unNIiSbHO60Q4O+BwIBCzxApQQbFg63vBLYYwEMRd7hh92s\nZkZVSOEp+sYBf/tmptlKr49nO+dTjQ==\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "packages/playwright-mcp/tests/testserver/san.cnf",
    "content": "# openssl req -new -x509 -days 3650 -key key.pem -out cert.pem -config san.cnf -extensions v3_req\n\n[req]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\nprompt = no\n\n[req_distinguished_name]\nCN = playwright-test\n\n[v3_req]\nbasicConstraints = CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment\nsubjectAltName = @alt_names\n\n[alt_names]\nDNS.1 = localhost\nIP.1 = 127.0.0.1\nIP.2 = ::1\n"
  },
  {
    "path": "packages/playwright-mcp/update-readme.js",
    "content": "#!/usr/bin/env node\n/**\n * Copyright (c) Microsoft Corporation.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n// @ts-check\n\nconst fs = require('fs')\nconst path = require('path')\nconst { execSync } = require('child_process');\n\nconst { browserTools } = require('playwright-core/lib/tools/exports');\n\nconst capabilities = /** @type {Record<string, string>} */ ({\n  'core-navigation': 'Core automation',\n  'core': 'Core automation',\n  'core-tabs': 'Tab management',\n  'core-input': 'Core automation',\n  'core-install': 'Browser installation',\n  'config': 'Configuration',\n  'network': 'Network',\n  'storage': 'Storage',\n  'devtools': 'DevTools',\n  'vision': 'Coordinate-based',\n  'pdf': 'PDF generation',\n  'testing': 'Test assertions',\n});\n\nconst knownCapabilities = new Set(Object.keys(capabilities));\nconst unknownCapabilities = [...new Set(browserTools.map(tool => tool.capability))].filter(cap => !knownCapabilities.has(cap));\nif (unknownCapabilities.length)\n  throw new Error(`Unknown tool capabilities: ${unknownCapabilities.join(', ')}. Please update the capabilities map in ${path.basename(__filename)}.`);\n\n/** @type {Record<string, any[]>} */\nconst toolsByCapability = {};\nfor (const capability of Object.keys(capabilities)) {\n  const title = capabilityTitle(capability);\n  let tools = browserTools.filter(tool => tool.capability === capability && !tool.skillOnly);\n  tools = (toolsByCapability[title] || []).concat(tools);\n  toolsByCapability[title] = tools;\n}\nfor (const [, tools] of Object.entries(toolsByCapability))\n  tools.sort((a, b) => a.schema.name.localeCompare(b.schema.name));\n\n/**\n * @param {string} capability\n * @returns {string}\n */\nfunction capabilityTitle(capability) {\n  const title = capabilities[capability];\n  return capability.startsWith('core') ? title : `${title} (opt-in via --caps=${capability})`;\n}\n\n/**\n * @param {any} tool\n * @returns {string[]}\n */\nfunction formatToolForReadme(tool) {\n  const lines = /** @type {string[]} */ ([]);\n  lines.push(`<!-- NOTE: This has been generated via ${path.basename(__filename)} -->`);\n  lines.push(``);\n  lines.push(`- **${tool.name}**`);\n  lines.push(`  - Title: ${tool.title}`);\n  lines.push(`  - Description: ${tool.description}`);\n\n  const inputSchema = /** @type {any} */ (tool.inputSchema ? tool.inputSchema.toJSONSchema() : {});\n  const requiredParams = inputSchema.required || [];\n  if (inputSchema.properties && Object.keys(inputSchema.properties).length) {\n    lines.push(`  - Parameters:`);\n    Object.entries(inputSchema.properties).forEach(([name, param]) => {\n      const optional = !requiredParams.includes(name);\n      const meta = /** @type {string[]} */ ([]);\n      if (param.type)\n        meta.push(param.type);\n      if (optional)\n        meta.push('optional');\n      lines.push(`    - \\`${name}\\` ${meta.length ? `(${meta.join(', ')})` : ''}: ${param.description}`);\n    });\n  } else {\n    lines.push(`  - Parameters: None`);\n  }\n  lines.push(`  - Read-only: **${tool.type === 'readOnly'}**`);\n  lines.push('');\n  return lines;\n}\n\n/**\n * @param {string} content\n * @param {string} startMarker\n * @param {string} endMarker\n * @param {string[]} generatedLines\n * @returns {Promise<string>}\n */\nasync function updateSection(content, startMarker, endMarker, generatedLines) {\n  const startMarkerIndex = content.indexOf(startMarker);\n  const endMarkerIndex = content.indexOf(endMarker);\n  if (startMarkerIndex === -1 || endMarkerIndex === -1)\n    throw new Error('Markers for generated section not found in README');\n\n  return [\n    content.slice(0, startMarkerIndex + startMarker.length),\n    '',\n    generatedLines.join('\\n'),\n    '',\n    content.slice(endMarkerIndex),\n  ].join('\\n');\n}\n\n/**\n * @param {string} content\n * @returns {Promise<string>}\n */\nasync function updateTools(content) {\n  console.log('Loading tool information from compiled modules...');\n\n  const generatedLines = /** @type {string[]} */ ([]);\n  for (const [capability, tools] of Object.entries(toolsByCapability)) {\n    console.log('Updating tools for capability:', capability);\n    generatedLines.push(`<details>\\n<summary><b>${capability}</b></summary>`);\n    generatedLines.push('');\n    for (const tool of tools)\n      generatedLines.push(...formatToolForReadme(tool.schema));\n    generatedLines.push(`</details>`);\n    generatedLines.push('');\n  }\n\n  const startMarker = `<!--- Tools generated by ${path.basename(__filename)} -->`;\n  const endMarker = `<!--- End of tools generated section -->`;\n  return updateSection(content, startMarker, endMarker, generatedLines);\n}\n\n/**\n * @param {string} content\n * @returns {Promise<string>}\n */\nasync function updateOptions(content) {\n  console.log('Listing options...');\n  execSync('node cli.js --help > help.txt');\n  const output = fs.readFileSync('help.txt');\n  fs.unlinkSync('help.txt');\n  const lines = output.toString().split('\\n');\n  const firstLine = lines.findIndex(line => line.includes('--version'));\n  lines.splice(0, firstLine + 1);\n  const lastLine = lines.findIndex(line => line.includes('--help'));\n  lines.splice(lastLine);\n\n  /**\n   * @type {{ name: string, value: string }[]}\n   */\n  const options = [];\n  for (let line of lines) {\n    if (line.startsWith('  --')) {\n      const l = line.substring('  --'.length);\n      const gapIndex = l.indexOf('  ');\n      const name = l.substring(0, gapIndex).trim();\n      const value = l.substring(gapIndex).trim();\n      options.push({ name, value });\n    } else {\n      const value = line.trim();\n      options[options.length - 1].value += ' ' + value;\n    }\n  }\n\n  const table = [];\n  table.push(`| Option | Description |`);\n  table.push(`|--------|-------------|`);\n  for (const option of options) {\n    const prefix = option.name.split(' ')[0];\n    const envName = `PLAYWRIGHT_MCP_` + prefix.toUpperCase().replace(/-/g, '_');\n    table.push(`| --${option.name} | ${option.value}<br>*env* \\`${envName}\\` |`);\n  }\n\n  if (process.env.PRINT_ENV) {\n    const envTable = [];\n    envTable.push(`| Environment |`);\n    envTable.push(`|-------------|`);\n    for (const option of options) {\n      const prefix = option.name.split(' ')[0];\n      const envName = `PLAYWRIGHT_MCP_` + prefix.toUpperCase().replace(/-/g, '_');\n      envTable.push(`| \\`${envName}\\` ${option.value} |`);\n    }\n    console.log(envTable.join('\\n'));\n  }\n\n  const startMarker = `<!--- Options generated by ${path.basename(__filename)} -->`;\n  const endMarker = `<!--- End of options generated section -->`;\n  return updateSection(content, startMarker, endMarker, table);\n}\n\n/**\n * @param {string} content\n * @returns {Promise<string>}\n */\nasync function updateConfig(content) {\n  console.log('Updating config schema from config.d.ts...');\n  const configPath = path.join(__dirname, 'config.d.ts');\n  const configContent = await fs.promises.readFile(configPath, 'utf-8');\n\n  // Extract the Config type definition\n  const configTypeMatch = configContent.match(/export type Config = (\\{[\\s\\S]*?\\n\\});/);\n  if (!configTypeMatch)\n    throw new Error('Config type not found in config.d.ts');\n\n  const configType = configTypeMatch[1]; // Use capture group to get just the object definition\n\n  const startMarker = `<!--- Config generated by ${path.basename(__filename)} -->`;\n  const endMarker = `<!--- End of config generated section -->`;\n  return updateSection(content, startMarker, endMarker, [\n    '```typescript',\n    configType,\n    '```',\n  ]);\n}\n\n/**\n * @param {string} filePath\n */\nasync function copyToPackage(filePath) {\n  await fs.promises.copyFile(path.join(__dirname, '../../', filePath), path.join(__dirname, filePath));\n  console.log(`${filePath} copied successfully`);\n}\n\nasync function updateReadme() {\n  const readmePath = path.join(__dirname, '../../README.md');\n  const readmeContent = await fs.promises.readFile(readmePath, 'utf-8');\n  const withTools = await updateTools(readmeContent);\n  const withOptions = await updateOptions(withTools);\n  const withConfig = await updateConfig(withOptions);\n  await fs.promises.writeFile(readmePath, withConfig, 'utf-8');\n  console.log('README updated successfully');\n\n  await copyToPackage('README.md');\n  await copyToPackage('LICENSE');\n}\n\nupdateReadme().catch(err => {\n  console.error('Error updating README:', err);\n  process.exit(1);\n});\n"
  },
  {
    "path": "roll.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst { execSync } = require('child_process');\n\nfunction copyConfig() {\n  const src = path.join(__dirname, '..', 'playwright', 'packages', 'playwright-core', 'src', 'tools', 'mcp', 'config.d.ts');\n  const dst = path.join(__dirname, 'packages', 'playwright-mcp', 'config.d.ts');\n  let content = fs.readFileSync(src, 'utf-8');\n  content = content.replace(\n    \"import type * as playwright from 'playwright-core';\",\n    \"import type * as playwright from 'playwright';\"\n  );\n  fs.writeFileSync(dst, content);\n  console.log(`Copied config.d.ts from ${src} to ${dst}`);\n}\n\nfunction updatePlaywrightVersion(version) {\n  const packagesDir = path.join(__dirname, 'packages');\n  const files = [path.join(__dirname, 'package.json')];\n  for (const entry of fs.readdirSync(packagesDir, { withFileTypes: true })) {\n    const pkgJson = path.join(packagesDir, entry.name, 'package.json');\n    if (fs.existsSync(pkgJson))\n      files.push(pkgJson);\n  }\n\n  for (const file of files) {\n    const json = JSON.parse(fs.readFileSync(file, 'utf-8'));\n    let updated = false;\n    for (const section of ['dependencies', 'devDependencies']) {\n      for (const pkg of ['@playwright/test', 'playwright', 'playwright-core']) {\n        if (json[section]?.[pkg]) {\n          json[section][pkg] = version;\n          updated = true;\n        }\n      }\n    }\n    if (updated) {\n      fs.writeFileSync(file, JSON.stringify(json, null, 2) + '\\n');\n      console.log(`Updated ${file}`);\n    }\n  }\n\n  execSync('npm install', { cwd: __dirname, stdio: 'inherit' });\n}\n\nfunction doRoll(version) {\n  updatePlaywrightVersion(version);\n  copyConfig();\n  // update readme\n  execSync('npm run lint', { cwd: __dirname, stdio: 'inherit' });\n}\n\nlet version = process.argv[2];\nif (!version) {\n  version = execSync('npm info playwright@next version', { encoding: 'utf-8' }).trim();\n  console.log(`Using next playwright version: ${version}`);\n}\ndoRoll(version);\n"
  }
]