commit 08058dca00d614681c87cf60a233092ca9bdd34a Author: wehub-resource-sync Date: Mon Jul 13 12:37:23 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.claude/skills/release.md b/.claude/skills/release.md new file mode 100644 index 0000000..df7c7b0 --- /dev/null +++ b/.claude/skills/release.md @@ -0,0 +1,84 @@ +--- +name: release +description: Prepare a playwright-mcp release — roll Playwright, bump the version, and open a `chore: mark v0.0.` PR whose body IS the release notes (changes from this repo and upstream microsoft/playwright since the last release). +--- + +# Preparing a Release + +A release is a `chore: mark v0.0.` commit whose **PR body is the release notes**. There is a single PR — the version bump and the notes ship together. Most MCP source lives upstream at `~/playwright/packages/playwright-core/src/tools/` (and `tests/mcp/`), so the notes draw from both this repo and upstream. + +## 1. Roll Playwright + +Follow the "Rolling Playwright" steps in `CLAUDE.md`: run `node roll.js`, branch as `roll-pw-`, run `npm test`, and open a `chore: roll Playwright to ` PR. **Wait for it to merge into `main`** before proceeding. + +## 2. Bump the version + +```bash +git checkout main && git pull +git checkout -b mark-v0.0. +# Bump "version" in package.json, package-lock.json (both occurrences), and server.json (both occurrences) +``` + +Do NOT open the PR yet — its body must be the release notes, so write those first (steps 3–5). + +## 3. Find the exact commit window + +The reliable boundary is the **`gitHead` of each published alpha build**, not a fuzzy date window (date windows double-count commits that land on the boundary day — e.g. a commit written on the previous release's build date but not actually in that build). + +```bash +# Playwright version pinned by the previous release (last "mark v" commit) and by this release +git log --oneline | grep "mark v" | head -1 +git show :package.json | grep '"playwright":' # baseline alpha +grep '"playwright":' package.json # new alpha (already rolled) + +# Resolve each alpha to the exact upstream commit it was built from +npm view playwright-core@ gitHead # e.g. -> 287ad476... +npm view playwright-core@ gitHead # e.g. -> a061d963... +``` + +The window is `..`. + +## 4. Collect changes + +```bash +# Upstream playwright — MCP code path widened to catch tools/extension/dashboard too +cd ~/playwright +git log .. --reverse --pretty='%h %s' -- \ + packages/playwright-core/src/tools/ packages/playwright-core/src/extension/ tests/mcp/ + +# This repo +cd - +git log ..HEAD --oneline +``` + +Filter for `feat(mcp)`, `fix(mcp)`, `feat(extension)`, `fix(extension)`, `feat(aria)` snapshot changes, and dashboard changes. Many extension PRs land in *both* repos because the extension source lives upstream now — prefer the `microsoft/playwright` PR link. Use `git show --stat` to disambiguate ambiguous subjects. + +**Drop:** test-only changes, docs, `chore(deps)`, CLI-daemon internals with no MCP-facing effect, reverted commits, and anything not user-visible. **Verify membership** of boundary commits with `git merge-base --is-ancestor ` — a commit can be listed in the previous release's notes yet not actually be in that build; if it was already announced, do not repeat it. + +## 5. Write `release-notes.md` + +Follow the format from the prior release (`gh pr view --repo microsoft/playwright-mcp --json body -q .body`). **No top-level `#` header** — the PR title is the heading. Sections: `## What's New` (with `### New Tools`, `### Tool Improvements`, optional `### Browser Extension`, `### Dashboard`, `### Other Changes`) then `## Bug Fixes`. Link each entry to its PR (`[#NNNNN](https://github.com/microsoft/playwright/pull/NNNNN)` or the playwright-mcp equivalent). Fold follow-up PRs into the feature they extend (e.g. a `browser_find` enhancement joins the `browser_find` bullet). + +Wording rules: +- Only list things that change user-visible behavior. +- **Do not mention features that are not enabled by default** — confirm with the user before listing experimental flags. + +## 6. Commit, push, open the PR with the notes as its body + +```bash +git commit -am "chore: mark v0.0." +git push -u origin mark-v0.0. +gh pr create --repo microsoft/playwright-mcp --head :mark-v0.0. \ + --base main \ + --title "chore: mark v0.0." \ + --body-file release-notes.md +``` + +The PR body is the release notes verbatim — no `#` header, no filename, nothing else. + +## Pitfalls + +- **Don't open the version-bump PR with a placeholder body** and backfill later — write the notes first so the PR is created with them. +- **Don't use a date window** for the upstream diff — use the `gitHead` range. Dates double-count boundary-day commits. +- **Don't re-announce** a commit that already appeared in the previous release's notes; check ancestry with `git merge-base --is-ancestor`. +- **Don't include a `#` header** in the PR body — GitHub renders the PR title already. diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..4bed909 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainer.schema.json", + "name": "Playwright", + "image": "mcr.microsoft.com/playwright:v1.58.2-noble", + "privileged": true, + "init": true, + "remoteUser": "pwuser", + "features": { + "ghcr.io/devcontainers/features/desktop-lite:1": {}, + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/docker-outside-of-docker:1": {} + }, + "forwardPorts": [ + 6080 + ], + "portsAttributes": { + "6080": { + "label": "noVNC" + } + } +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fdb0a2f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,75 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Use Node.js 20 + uses: actions/setup-node@v5 + with: + node-version: '20' + cache: 'npm' + - run: npm ci + - run: npm run lint + - name: Ensure no changes + run: git diff --exit-code + + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-15, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v5 + - name: Use Node.js 20 + uses: actions/setup-node@v5 + with: + node-version: '20' + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Playwright install + run: npx playwright install --with-deps + - name: Build + run: npm run build + - name: Run playwright-mcp tests + run: npm test + + test_mcp_docker: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - name: Use Node.js 20 + uses: actions/setup-node@v5 + with: + node-version: '20' + cache: 'npm' + - name: Install dependencies + run: npm ci + - name: Playwright install + run: npx playwright install --with-deps chromium + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + - name: Build and push + uses: docker/build-push-action@v7 + with: + tags: playwright-mcp-dev:latest + cache-from: type=gha + cache-to: type=gha,mode=max + load: true + - name: Run tests + shell: bash + run: | + # Used for the Docker tests to share the test-results folder with the container. + umask 0000 + npm run test -- --project=chromium-docker + env: + MCP_IN_DOCKER: 1 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..47949ac --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,155 @@ +name: Publish +on: + workflow_dispatch: + schedule: + - cron: '0 8 * * *' + release: + types: [published] + +jobs: + publish-mcp-canary-npm: + if: github.event.schedule || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # Required for OIDC npm publishing + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: 24 + registry-url: https://registry.npmjs.org/ + + - name: Get current date + id: date + run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT + + - name: Get current version + id: version + run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + + - name: Set canary version + id: canary-version + run: echo "version=${{ steps.version.outputs.version }}-alpha-${{ steps.date.outputs.date }}" >> $GITHUB_OUTPUT + + - name: Update package.json version + run: npm version ${{ steps.canary-version.outputs.version }} --no-git-tag-version + + - run: npm ci + - run: npx playwright install --with-deps + - run: npm run lint + - run: npm run ctest + + - name: Publish to npm with next tag + run: npm publish --tag next + + publish-mcp-release-npm: + if: github.event_name == 'release' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # Required for OIDC npm publishing + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-node@v5 + with: + node-version: 24 + registry-url: https://registry.npmjs.org/ + - run: npm ci + - run: npx playwright install --with-deps + - run: npm run lint + - run: npm run ctest + - run: npm publish + + publish-mcp-release-registry: + # Runs on release (after npm publish succeeds) or via manual workflow_dispatch + # (e.g. to retroactively publish a version, in which case publish-mcp-release-npm + # is skipped — the always() + result check lets this job still run). + if: | + always() && + (github.event_name == 'release' || github.event_name == 'workflow_dispatch') && + needs.publish-mcp-release-npm.result != 'failure' && + needs.publish-mcp-release-npm.result != 'cancelled' + needs: publish-mcp-release-npm + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # Required for GitHub OIDC auth to the MCP Registry + steps: + - uses: actions/checkout@v5 + + - name: Validate server.json version matches package.json + run: | + node -e " + const pkg = require('./package.json'); + const server = require('./server.json'); + const mismatches = []; + if (server.version !== pkg.version) + mismatches.push(\`server.version=\${server.version} != package.version=\${pkg.version}\`); + for (const p of server.packages) { + if (p.version !== pkg.version) + mismatches.push(\`packages[\${p.identifier}].version=\${p.version} != package.version=\${pkg.version}\`); + } + if (mismatches.length) { + console.error('server.json is out of sync with package.json:'); + for (const m of mismatches) console.error(' ' + m); + process.exit(1); + } + " + + - name: Install mcp-publisher + run: | + curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_linux_amd64.tar.gz" \ + | tar xz mcp-publisher + + - name: Authenticate with MCP Registry (GitHub OIDC) + run: ./mcp-publisher login github-oidc + + - name: Publish to MCP Registry + run: ./mcp-publisher publish + + publish-mcp-release-docker: + if: github.event_name == 'release' + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # Needed for OIDC login to Azure + environment: allow-publishing-docker-to-acr + steps: + - uses: actions/checkout@v5 + - name: Set up QEMU # Needed for multi-platform builds (e.g., arm64 on amd64 runner) + uses: docker/setup-qemu-action@v4 + - name: Set up Docker Buildx # Needed for multi-platform builds + uses: docker/setup-buildx-action@v4 + - name: Azure Login via OIDC + uses: azure/login@v3 + with: + client-id: ${{ secrets.AZURE_DOCKER_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_DOCKER_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_DOCKER_SUBSCRIPTION_ID }} + - name: Login to ACR + run: az acr login --name playwright + - name: Build and push Docker image + id: build-push + uses: docker/build-push-action@v7 + with: + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: | + playwright.azurecr.io/public/playwright/mcp:${{ github.event.release.tag_name }} + playwright.azurecr.io/public/playwright/mcp:latest + - uses: oras-project/setup-oras@v2 + - name: Set oras tags + run: | + attach_eol_manifest() { + local image="$1" + local today=$(date -u +'%Y-%m-%d') + # oras is re-using Docker credentials, so we don't need to login. + # Following the advice in https://portal.microsofticm.com/imp/v3/incidents/incident/476783820/summary + oras attach --artifact-type application/vnd.microsoft.artifact.lifecycle --annotation "vnd.microsoft.artifact.lifecycle.end-of-life.date=$today" $image + } + # for each tag, attach the eol manifest + for tag in $(echo ${{ steps.build-push.outputs.metadata['image.name'] }} | tr ',' '\n'); do + attach_eol_manifest $tag + done diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ccb7fd --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +test-results/ +playwright-report/ +.vscode/mcp.json +.idea +.DS_Store +.env +sessions/ diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..28e12e4 --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +**/* +!README.md +!LICENSE +!cli.js +!index.* +!config.d.ts diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8e81165 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,41 @@ +## Commit Convention + +Semantic commit messages: `label(scope): description` + +Labels: `fix`, `feat`, `chore`, `docs`, `test`, `devops` + +```bash +git checkout -b fix-39562 +# ... make changes ... +git add +git commit -m "$(cat <<'EOF' +fix(proxy): handle SOCKS proxy authentication + +Fixes: https://github.com/microsoft/playwright/issues/39562 +EOF +)" +git push origin fix-39562 +gh pr create --repo microsoft/playwright --head username:fix-39562 \ + --title "fix(proxy): handle SOCKS proxy authentication" \ + --body "$(cat <<'EOF' +## Summary +- + +Fixes https://github.com/microsoft/playwright/issues/39562 +EOF +)" +``` + +Never add Co-Authored-By agents in commit message. +Branch naming for issue fixes: `fix-` + +## Rolling Playwright + +1. Run `node roll.js` (or `npm run roll`) to bump `playwright`, `playwright-core`, and `@playwright/test`, refresh `config.d.ts`, and regenerate the README. The script prints the resolved version — use its suffix for the branch name. +2. Create a branch: `git checkout -b roll-pw-`. +3. Run `npm test`. Only proceed if all tests pass. +4. Commit with `chore: roll Playwright to `, push, and open a PR against `microsoft/playwright-mcp` with the same title. + +## Preparing a Release + +See [.claude/skills/release.md](.claude/skills/release.md). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..18125d7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,148 @@ +# Contributing + +## Choosing an Issue + +To maintain project quality and focus, Playwright **requires a corresponding issue** for every contribution, with the exception of minor documentation fixes. + +If you would like to address a bug or feature that isn't currently listed, **please file a new issue first**. This allows the community and maintainers to provide early feedback and facilitates a discussion before you invest time in developing a pull request. + +When submitting an issue, please state clearly if you intend to work on it. Once triaged and approved, the maintainers will determine the best path forward—whether the task should be handled by the **core team**, an **automated agent**, or a **community contributor**. If the issue is assigned to you, you may then proceed with your changes and submit a PR. + +### Submission Policy +To ensure the maintainability of the project, please note the following: + +* **Unsolicited PRs:** Pull requests submitted without a linked issue or prior approval will be closed. +* **Low-Quality AI Contributions:** PRs that do not meet our quality standards or lack human oversight (including low-quality agentic submissions) will be closed without explanation. +* **Approval Required:** Only proceed with a PR once the issue has been officially assigned to you or approved for community contribution. + +## Make a change + +> [!WARNING] +> The core of the Playwright MCP was moved to the [Playwright monorepo](https://github.com/microsoft/playwright). + +Clone 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. + + +```bash +git clone https://github.com/microsoft/playwright +cd playwright +``` + +Install dependencies and run the build in watch mode. +```bash +# install deps and run watch +npm ci +npm run watch +npx playwright install +``` + +Source code for Playwright MCP is located in the Playwright monorepo: + +- [packages/playwright-core/src/tools/mcp](https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/tools/mcp) +- [packages/playwright-core/src/tools/backend](https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/tools/backend) + +```bash +# list source files +ls -la packages/playwright-core/src/tools/mcp +ls -la packages/playwright-core/src/tools/backend +``` + +Coding 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: +```bash +# lint the source base before sending PR +npm run flint +``` + +Comments 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. + +## Add a test + +Playwright 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. + +There 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). + +```bash +# list test files +ls -la tests/mcp +``` + +To run the mcp tests, use + +```bash +# fast path runs all MCP tests in Chromium +npm run ctest-mcp +``` + +```bash +# slow path runs all tests in three browsers +npm run test-mcp +``` + +Since 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). + +Note that tests should be *hermetic*, and not depend on external services. Tests should work on all three platforms: macOS, Linux and Windows. + +## Write a commit message + +Commit messages should follow the [Semantic Commit Messages](https://www.conventionalcommits.org/en/v1.0.0/) format: + +``` +label(namespace): title + +description + +footer +``` + +1. *label* is one of the following: + - `fix` - bug fixes + - `feat` - new features + - `docs` - documentation-only changes + - `test` - test-only changes + - `devops` - changes to the CI or build + - `chore` - everything that doesn't fall under previous categories +2. *namespace* is put in parentheses after label and is optional. Must be lowercase. +3. *title* is a brief summary of changes. +4. *description* is **optional**, new-line separated from title and is in present tense. +5. *footer* is **optional**, new-line separated from *description* and contains "fixes" / "references" attribution to GitHub issues. + +Example: + +``` +feat(trace viewer): network panel filtering + +This patch adds a filtering toolbar to the network panel. + + +Fixes #123, references #234. +``` + +## Send a pull request + +All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. +Make sure to keep your PR (diff) small and readable. If necessary, split your contribution into multiple PRs. +Consult [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more information on using pull requests. + +After a successful code review, one of the maintainers will merge your pull request. Congratulations! + +## More details + +**No new dependencies** + +There 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. + +## Contributor License Agreement + +This project welcomes contributions and suggestions. Most contributions require you to agree to a +Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. + +When you submit a pull request, a CLA bot will automatically determine whether you need to provide +a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions +provided by the bot. You will only need to do this once across all repos using our CLA. + +### Code of Conduct + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a41cbab --- /dev/null +++ b/Dockerfile @@ -0,0 +1,67 @@ +ARG PLAYWRIGHT_BROWSERS_PATH=/ms-playwright + +# ------------------------------ +# Base +# ------------------------------ +# Base stage: Contains only the minimal dependencies required for runtime +# (node_modules and Playwright system dependencies) +FROM node:22-bookworm-slim AS base + +ARG PLAYWRIGHT_BROWSERS_PATH +ENV PLAYWRIGHT_BROWSERS_PATH=${PLAYWRIGHT_BROWSERS_PATH} + +# Set the working directory +WORKDIR /app + +RUN --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-cache \ + --mount=type=bind,source=package.json,target=package.json \ + --mount=type=bind,source=package-lock.json,target=package-lock.json \ + npm ci --omit=dev && \ + # Install system dependencies for playwright + npx -y playwright-core install-deps chromium + +# ------------------------------ +# Builder +# ------------------------------ +FROM base AS builder + +RUN --mount=type=cache,target=/root/.npm,sharing=locked,id=npm-cache \ + --mount=type=bind,source=package.json,target=package.json \ + --mount=type=bind,source=package-lock.json,target=package-lock.json \ + npm ci + +# Copy the rest of the app +COPY *.json *.js *.ts . + +# ------------------------------ +# Browser +# ------------------------------ +# Cache optimization: +# - Browser is downloaded only when node_modules or Playwright system dependencies change +# - Cache is reused when only source code changes +FROM base AS browser + +RUN npx -y playwright-core install --no-shell chromium + +# ------------------------------ +# Runtime +# ------------------------------ +FROM base + +ARG PLAYWRIGHT_BROWSERS_PATH +ARG USERNAME=node +ENV NODE_ENV=production + +# Set the correct ownership for the runtime user on production `node_modules` +RUN chown -R ${USERNAME}:${USERNAME} node_modules + +USER ${USERNAME} + +COPY --from=browser --chown=${USERNAME}:${USERNAME} ${PLAYWRIGHT_BROWSERS_PATH} ${PLAYWRIGHT_BROWSERS_PATH} +COPY --chown=${USERNAME}:${USERNAME} cli.js package.json ./ + +# Current working directory must be writable as MCP may need to create default output dir in it. +WORKDIR /home/${USERNAME} + +# Run in headless and only with chromium (other browsers need more dependencies not included in this image) +ENTRYPOINT ["node", "/app/cli.js", "--headless", "--browser", "chromium", "--no-sandbox"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cefe596 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright (c) Microsoft Corporation. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..de61567 --- /dev/null +++ b/README.md @@ -0,0 +1,1592 @@ +## Playwright MCP + +A 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. + +### Playwright MCP vs Playwright CLI + +This 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. + +- **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.
**Learn more about [Playwright CLI with SKILLS](https://github.com/microsoft/playwright-cli)**. + +- **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. + +### Key Features + +- **Fast and lightweight**. Uses Playwright's accessibility tree, not pixel-based input. +- **LLM-friendly**. No vision models needed, operates purely on structured data. +- **Deterministic tool application**. Avoids ambiguity common with screenshot-based approaches. + +### Requirements +- Node.js 18 or newer +- VS Code, Cursor, Windsurf, Claude Desktop, Goose, Grok, Junie or any other MCP client + + + +### Getting started + +First, install the Playwright MCP server with your client. + +**Standard config** works in most of the tools: + +```js +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + } + } +} +``` + +[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) [Install in VS Code Insiders](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D) + +
+Amp + +Add via the Amp VS Code extension settings screen or by updating your settings.json file: + +```json +"amp.mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + } +} +``` + +**Amp CLI Setup:** + +Add via the `amp mcp add`command below + +```bash +amp mcp add playwright -- npx @playwright/mcp@latest +``` + +
+ +
+Antigravity + +Add via the Antigravity settings or by updating your configuration file: + +```json +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + } + } +} +``` + +
+ +
+Claude Code + +Use the Claude Code CLI to add the Playwright MCP server: + +```bash +claude mcp add playwright npx @playwright/mcp@latest +``` +
+ +
+Claude Desktop + +Follow the MCP install [guide](https://modelcontextprotocol.io/quickstart/user), use the standard config above. + +
+ +
+Cline + +Follow the instruction in the section [Configuring MCP Servers](https://docs.cline.bot/mcp/configuring-mcp-servers) + +**Example: Local Setup** + +Add the following to your [`cline_mcp_settings.json`](https://docs.cline.bot/mcp/configuring-mcp-servers#editing-mcp-settings-files) file: + +```json +{ + "mcpServers": { + "playwright": { + "type": "stdio", + "command": "npx", + "timeout": 30, + "args": [ + "-y", + "@playwright/mcp@latest" + ], + "disabled": false + } + } +} +``` + +
+ +
+Codex + +Use the Codex CLI to add the Playwright MCP server: + +```bash +codex mcp add playwright npx "@playwright/mcp@latest" +``` + +Alternatively, create or edit the configuration file `~/.codex/config.toml` and add: + +```toml +[mcp_servers.playwright] +command = "npx" +args = ["@playwright/mcp@latest"] +``` + +For more information, see the [Codex MCP documentation](https://github.com/openai/codex/blob/main/codex-rs/config.md#mcp_servers). + +
+ +
+Copilot + +Use the Copilot CLI to interactively add the Playwright MCP server: + +```bash +/mcp add +``` + +Alternatively, create or edit the configuration file `~/.copilot/mcp-config.json` and add: + +```json +{ + "mcpServers": { + "playwright": { + "type": "local", + "command": "npx", + "tools": [ + "*" + ], + "args": [ + "@playwright/mcp@latest" + ] + } + } +} +``` + +For more information, see the [Copilot CLI documentation](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli). + +
+ +
+Cursor + +#### Click the button to install: + +[Install in Cursor](https://cursor.com/en/install-mcp?name=Playwright&config=eyJjb21tYW5kIjoibnB4IEBwbGF5d3JpZ2h0L21jcEBsYXRlc3QifQ%3D%3D) + +#### Or install manually: + +Go 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`. + +
+ +
+Factory + +Use the Factory CLI to add the Playwright MCP server: + +```bash +droid mcp add playwright "npx @playwright/mcp@latest" +``` + +Alternatively, type `/mcp` within Factory droid to open an interactive UI for managing MCP servers. + +For more information, see the [Factory MCP documentation](https://docs.factory.ai/cli/configuration/mcp). + +
+ +
+Gemini CLI + +Follow 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. + +
+ +
+Goose + +#### Click the button to install: + +[![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) + +#### Or install manually: + +Go 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". +
+ +
+Grok + +Use the Grok CLI to add the Playwright MCP server: + +```bash +grok mcp add playwright -- npx @playwright/mcp@latest +``` + +Alternatively, create or edit the configuration file `~/.grok/config.toml` and add: + +```toml +[mcp_servers.playwright] +command = "npx" +args = ["@playwright/mcp@latest"] +``` + +For more information, see the [Grok MCP documentation](https://docs.x.ai/build/features/mcp-servers). + +
+ +
+Junie + +To add the Playwright MCP server in Junie CLI: + +1. Type `/mcp` +2. Press `Ctrl+A` to add a new MCP server +3. Select **Playwright** from the list + +Alternatively, add to `.junie/mcp/mcp.json`: + +```json +{ + "mcpServers": { + "Playwright": { + "command": "npx", + "args": [ + "-y", + "@playwright/mcp@latest" + ] + } + } +} +``` + +For more information, see the [Junie MCP configuration documentation](https://junie.jetbrains.com/docs/junie-cli-mcp-configuration.html). + +
+ +
+Kiro + +[![Add to Kiro](https://kiro.dev/images/add-to-kiro.svg)](https://kiro.dev/launch/mcp/add?name=playwright&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22%40playwright%2Fmcp%40latest%22%5D%7D) + +Follow the MCP Servers [documentation](https://kiro.dev/docs/mcp/). For example in `.kiro/settings/mcp.json`: + +```json +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + } + } +} +``` +
+ +
+LM Studio + +#### Click the button to install: + +[![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) + +#### Or install manually: + +Go to `Program` in the right sidebar -> `Install` -> `Edit mcp.json`. Use the standard config above. +
+ +
+opencode + +Follow the MCP Servers [documentation](https://opencode.ai/docs/mcp-servers/). For example in `~/.config/opencode/opencode.json`: + +```json +{ + "$schema": "https://opencode.ai/config.json", + "mcp": { + "playwright": { + "type": "local", + "command": [ + "npx", + "@playwright/mcp@latest" + ], + "enabled": true + } + } +} + +``` +
+ +
+Qodo Gen + +Open [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. + +Click Save. +
+ +
+VS Code + +#### Click the button to install: + +[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) [Install in VS Code Insiders](https://insiders.vscode.dev/redirect?url=vscode-insiders%3Amcp%2Finstall%3F%257B%2522name%2522%253A%2522playwright%2522%252C%2522command%2522%253A%2522npx%2522%252C%2522args%2522%253A%255B%2522%2540playwright%252Fmcp%2540latest%2522%255D%257D) + +#### Or install manually: + +Follow 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: + +```bash +# For VS Code +code --add-mcp '{"name":"playwright","command":"npx","args":["@playwright/mcp@latest"]}' +``` + +After installation, the Playwright MCP server will be available for use with your GitHub Copilot agent in VS Code. +
+ +
+Warp + +Go 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. + +Alternatively, use the slash command `/add-mcp` in the Warp prompt and paste the standard config from above: +```js +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ] + } + } +} +``` + +
+ +
+Windsurf + +Follow Windsurf MCP [documentation](https://docs.windsurf.com/windsurf/cascade/mcp). Use the standard config above. + +
+ +### Configuration + +Playwright MCP server supports following arguments. They can be provided in the JSON configuration above, as a part of the `"args"` list: + + + +| Option | Description | +|--------|-------------| +| --allowed-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.
*env* `PLAYWRIGHT_MCP_ALLOWED_HOSTS` | +| --allowed-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.
*env* `PLAYWRIGHT_MCP_ALLOWED_ORIGINS` | +| --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.
*env* `PLAYWRIGHT_MCP_ALLOW_UNRESTRICTED_FILE_ACCESS` | +| --blocked-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.
*env* `PLAYWRIGHT_MCP_BLOCKED_ORIGINS` | +| --block-service-workers | block service workers
*env* `PLAYWRIGHT_MCP_BLOCK_SERVICE_WORKERS` | +| --browser | browser or chrome channel to use, possible values: chrome, firefox, webkit, msedge.
*env* `PLAYWRIGHT_MCP_BROWSER` | +| --caps | comma-separated list of additional capabilities to enable, possible values: vision, pdf, devtools.
*env* `PLAYWRIGHT_MCP_CAPS` | +| --cdp-endpoint | CDP endpoint to connect to.
*env* `PLAYWRIGHT_MCP_CDP_ENDPOINT` | +| --cdp-header | CDP headers to send with the connect request, multiple can be specified.
*env* `PLAYWRIGHT_MCP_CDP_HEADERS` | +| --cdp-timeout | timeout in milliseconds for connecting to CDP endpoint, defaults to 30000ms
*env* `PLAYWRIGHT_MCP_CDP_TIMEOUT` | +| --codegen | specify the language to use for code generation, possible values: "typescript", "none". Default is "typescript".
*env* `PLAYWRIGHT_MCP_CODEGEN` | +| --config | path to the configuration file.
*env* `PLAYWRIGHT_MCP_CONFIG` | +| --console-level | level of console messages to return: "error", "warning", "info", "debug". Each level includes the messages of more severe levels.
*env* `PLAYWRIGHT_MCP_CONSOLE_LEVEL` | +| --device | device to emulate, for example: "iPhone 15"
*env* `PLAYWRIGHT_MCP_DEVICE` | +| --mobile | emulate a generic mobile device (Pixel 10 for Chromium, iPhone 17 for WebKit). Mobile pages are usually lighter, which saves tokens. Cannot be combined with --device.
*env* `PLAYWRIGHT_MCP_MOBILE` | +| --executable-path | path to the browser executable.
*env* `PLAYWRIGHT_MCP_EXECUTABLE_PATH` | +| --extension | Connect to a running browser instance (Edge/Chrome only). Requires the "Playwright Extension" to be installed.
*env* `PLAYWRIGHT_MCP_EXTENSION` | +| --endpoint | Bound browser endpoint to connect to.
*env* `PLAYWRIGHT_MCP_ENDPOINT` | +| --grant-permissions | List of permissions to grant to the browser context, for example "geolocation", "clipboard-read", "clipboard-write".
*env* `PLAYWRIGHT_MCP_GRANT_PERMISSIONS` | +| --headless | run browser in headless mode, headed by default
*env* `PLAYWRIGHT_MCP_HEADLESS` | +| --host | host to bind server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces.
*env* `PLAYWRIGHT_MCP_HOST` | +| --ignore-https-errors | ignore https errors
*env* `PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS` | +| --init-page | path to TypeScript file to evaluate on Playwright page object
*env* `PLAYWRIGHT_MCP_INIT_PAGE` | +| --init-script | 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.
*env* `PLAYWRIGHT_MCP_INIT_SCRIPT` | +| --isolated | keep the browser profile in memory, do not save it to disk.
*env* `PLAYWRIGHT_MCP_ISOLATED` | +| --image-responses | whether to send image responses to the client. Can be "allow" or "omit", Defaults to "allow".
*env* `PLAYWRIGHT_MCP_IMAGE_RESPONSES` | +| --no-sandbox | disable the sandbox for all process types that are normally sandboxed.
*env* `PLAYWRIGHT_MCP_NO_SANDBOX` | +| --output-dir | path to the directory for output files.
*env* `PLAYWRIGHT_MCP_OUTPUT_DIR` | +| --output-max-size | Threshold for evicting old output files, in bytes.
*env* `PLAYWRIGHT_MCP_OUTPUT_MAX_SIZE` | +| --output-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".
*env* `PLAYWRIGHT_MCP_OUTPUT_MODE` | +| --port | port to listen on for SSE transport.
*env* `PLAYWRIGHT_MCP_PORT` | +| --proxy-bypass | comma-separated domains to bypass proxy, for example ".com,chromium.org,.domain.com"
*env* `PLAYWRIGHT_MCP_PROXY_BYPASS` | +| --proxy-server | specify proxy server, for example "http://myproxy:3128" or "socks5://myproxy:8080"
*env* `PLAYWRIGHT_MCP_PROXY_SERVER` | +| --sandbox | enable the sandbox for all process types that are normally not sandboxed.
*env* `PLAYWRIGHT_MCP_SANDBOX` | +| --save-session | Whether to save the Playwright MCP session into the output directory.
*env* `PLAYWRIGHT_MCP_SAVE_SESSION` | +| --secrets | path to a file containing secrets in the dotenv format
*env* `PLAYWRIGHT_MCP_SECRETS_FILE` | +| --shared-browser-context | reuse the same browser context between all connected HTTP clients.
*env* `PLAYWRIGHT_MCP_SHARED_BROWSER_CONTEXT` | +| --snapshot-mode | when taking snapshots for responses, specifies the mode to use. Can be "full" or "none". Default is "full".
*env* `PLAYWRIGHT_MCP_SNAPSHOT_MODE` | +| --storage-state | path to the storage state file for isolated sessions.
*env* `PLAYWRIGHT_MCP_STORAGE_STATE` | +| --test-id-attribute | specify the attribute to use for test ids, defaults to "data-testid"
*env* `PLAYWRIGHT_MCP_TEST_ID_ATTRIBUTE` | +| --timeout-action | specify action timeout in milliseconds, defaults to 5000ms
*env* `PLAYWRIGHT_MCP_TIMEOUT_ACTION` | +| --timeout-navigation | specify navigation timeout in milliseconds, defaults to 60000ms
*env* `PLAYWRIGHT_MCP_TIMEOUT_NAVIGATION` | +| --user-agent | specify user agent string
*env* `PLAYWRIGHT_MCP_USER_AGENT` | +| --user-data-dir | path to the user data directory. If not specified, a temporary directory will be created.
*env* `PLAYWRIGHT_MCP_USER_DATA_DIR` | +| --viewport-size | specify browser viewport size in pixels, for example "1280x720"
*env* `PLAYWRIGHT_MCP_VIEWPORT_SIZE` | + + + +### User profile + +You 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. + +**Persistent profile** + +All 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. +Persistent profile is located at the following locations and you can override it with the `--user-data-dir` argument. + +```bash +# Windows +%USERPROFILE%\AppData\Local\ms-playwright\mcp-{channel}-{workspace-hash} + +# macOS +- ~/Library/Caches/ms-playwright/mcp-{channel}-{workspace-hash} + +# Linux +- ~/.cache/ms-playwright/mcp-{channel}-{workspace-hash} +``` + +`{workspace-hash}` is derived from the MCP client's workspace root, so different projects get separate profiles automatically. + +> [!IMPORTANT] +> A persistent profile can only be used by one browser instance at a time, so concurrent MCP clients sharing the same workspace will conflict. To run several clients in parallel, start each additional client with `--isolated` or point it at a distinct `--user-data-dir`. + +**Isolated** + +In the isolated mode, each session is started in the isolated profile. Every time you ask MCP to close the browser, +the session is closed and all the storage state for this session is lost. You can provide initial storage state +to the browser via the config's `contextOptions` or via the `--storage-state` argument. Learn more about the storage +state [here](https://playwright.dev/docs/auth). + +```js +{ + "mcpServers": { + "playwright": { + "command": "npx", + "args": [ + "@playwright/mcp@latest", + "--isolated", + "--storage-state={path/to/storage.json}" + ] + } + } +} +``` + +**Browser Extension** + +The Playwright MCP Chrome Extension allows you to connect to existing browser tabs and leverage your logged-in sessions and browser state. See [microsoft/playwright › packages/extension](https://github.com/microsoft/playwright/tree/main/packages/extension#readme) for installation and setup instructions. + +### Initial state + +There are multiple ways to provide the initial state to the browser context or a page. + +For the storage state, you can either: +- Start with a user data directory using the `--user-data-dir` argument. This will persist all browser data between the sessions. +- 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. + +For the page state, you can use: + +- `--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. + +```ts +// init-page.ts +export default async ({ page }) => { + await page.context().grantPermissions(['geolocation']); + await page.context().setGeolocation({ latitude: 37.7749, longitude: -122.4194 }); + await page.setViewportSize({ width: 1280, height: 720 }); +}; +``` + +- `--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. +This is useful for overriding browser APIs or setting up the environment. + +```js +// init-script.js +window.isPlaywrightMCP = true; +``` + +### Configuration file + +The Playwright MCP server can be configured using a JSON configuration file. You can specify the configuration file +using the `--config` command line option: + +```bash +npx @playwright/mcp@latest --config path/to/config.json +``` + +
+Configuration file schema + + + +```typescript +{ + /** + * The browser to use. + */ + browser?: { + /** + * The type of browser to use. + */ + browserName?: 'chromium' | 'firefox' | 'webkit'; + + /** + * Keep the browser profile in memory, do not save it to disk. + */ + isolated?: boolean; + + /** + * Path to a user data directory for browser profile persistence. + * Temporary directory is created by default. + */ + userDataDir?: string; + + /** + * Launch options passed to + * @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context + * + * This is useful for settings options like `channel`, `headless`, `executablePath`, etc. + */ + launchOptions?: playwright.LaunchOptions; + + /** + * Context options for the browser context. + * + * This is useful for settings options like `viewport`. + */ + contextOptions?: playwright.BrowserContextOptions; + + /** + * Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers. + */ + cdpEndpoint?: string; + + /** + * CDP headers to send with the connect request. + */ + cdpHeaders?: Record; + + /** + * Timeout in milliseconds for connecting to CDP endpoint. Defaults to 30000 (30 seconds). Pass 0 to disable timeout. + */ + cdpTimeout?: number; + + /** + * Remote endpoint to connect to an existing Playwright server. May be a + * WebSocket URL string, or a [ConnectOptions] object that mirrors the + * `connectOptions` shape used by the test runner. When passed as an object, + * `exposeNetwork`, `headers`, `slowMo`, and `timeout` are forwarded to the + * underlying connect call. + */ + remoteEndpoint?: string | playwright.ConnectOptions & { endpoint: string }; + + /** + * Paths to TypeScript files to add as initialization scripts for Playwright page. + */ + initPage?: string[]; + + /** + * Paths to JavaScript files to add as initialization scripts. + * The scripts will be evaluated in every page before any of the page's scripts. + */ + initScript?: string[]; + }, + + /** + * Connect to a running browser instance (Edge/Chrome only). If specified, `browser` + * config is ignored. + * Requires the "Playwright Extension" to be installed. + */ + extension?: boolean; + + server?: { + /** + * The port to listen on for SSE or MCP transport. + */ + port?: number; + + /** + * The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces. + */ + host?: string; + + /** + * The hosts this server is allowed to serve from. Defaults to the host server is bound to. + * This is not for CORS, but rather for the DNS rebinding protection. + */ + allowedHosts?: string[]; + }, + + /** + * List of enabled tool capabilities. Possible values: + * - 'core': Core browser automation features. + * - 'pdf': PDF generation and manipulation. + * - 'vision': Coordinate-based interactions. + * - 'devtools': Developer tools features. + */ + capabilities?: ToolCapability[]; + + /** + * Whether to save the Playwright session into the output directory. + */ + saveSession?: boolean; + + /** + * Reuse the same browser context between all connected HTTP clients. + */ + sharedBrowserContext?: boolean; + + /** + * Secrets are used to replace matching plain text in the tool responses to prevent the LLM + * from accidentally getting sensitive data. It is a convenience and not a security feature, + * make sure to always examine information coming in and from the tool on the client. + */ + secrets?: Record; + + /** + * The directory to save output files. + */ + outputDir?: string; + + /** + * Threshold for evicting old output files, in bytes. + */ + outputMaxSize?: number; + + console?: { + /** + * The level of console messages to return. Each level includes the messages of more severe levels. Defaults to "info". + */ + level?: 'error' | 'warning' | 'info' | 'debug'; + }, + + network?: { + /** + * List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked. + * + * Supported formats: + * - Full origin: `https://example.com:8080` - matches only that origin + * - Wildcard port: `http://localhost:*` - matches any port on localhost with http protocol + */ + allowedOrigins?: string[]; + + /** + * List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked. + * + * Supported formats: + * - Full origin: `https://example.com:8080` - matches only that origin + * - Wildcard port: `http://localhost:*` - matches any port on localhost with http protocol + */ + blockedOrigins?: string[]; + }; + + /** + * Specify the attribute to use for test ids, defaults to "data-testid". + */ + testIdAttribute?: string; + + timeouts?: { + /* + * Configures default action timeout: https://playwright.dev/docs/api/class-page#page-set-default-timeout. Defaults to 5000ms. + */ + action?: number; + + /* + * Configures default navigation timeout: https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout. Defaults to 60000ms. + */ + navigation?: number; + + /** + * Configures default expect timeout: https://playwright.dev/docs/test-timeouts#expect-timeout. Defaults to 5000ms. + */ + expect?: number; + }; + + /** + * 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. + */ + imageResponses?: 'allow' | 'omit'; + + snapshot?: { + /** + * When taking snapshots for responses, specifies the mode to use. + */ + mode?: 'full' | 'none'; + }; + + /** + * allowUnrestrictedFileAccess acts as a guardrail to prevent the LLM from accidentally + * wandering outside its intended workspace. It is a convenience defense to catch unintended + * file access, not a secure boundary; a deliberate attempt to reach other directories can be + * easily worked around, so always rely on client-level permissions for true security. + */ + allowUnrestrictedFileAccess?: boolean; + + /** + * Specify the language to use for code generation. + */ + codegen?: 'typescript' | 'none'; +} +``` + + + +
+ +### Standalone MCP server + +When running headed browser on system w/o display or from worker processes of the IDEs, +run the MCP server from environment with the DISPLAY and pass the `--port` flag to enable HTTP transport. + +```bash +npx @playwright/mcp@latest --port 8931 +``` + +And then in MCP client config, set the `url` to the HTTP endpoint: + +```js +{ + "mcpServers": { + "playwright": { + "url": "http://localhost:8931/mcp" + } + } +} +``` + +## Security + +Playwright 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. + +
+Docker + +**NOTE:** The Docker implementation only supports headless chromium at the moment. + +```js +{ + "mcpServers": { + "playwright": { + "command": "docker", + "args": ["run", "-i", "--rm", "--init", "--pull=always", "mcr.microsoft.com/playwright/mcp"] + } + } +} +``` + +Or If you prefer to run the container as a long-lived service instead of letting the MCP client spawn it, use: + +``` +docker run -d -i --rm --init --pull=always \ + --entrypoint node \ + --name playwright \ + -p 8931:8931 \ + mcr.microsoft.com/playwright/mcp \ + /app/cli.js --headless --browser chromium --no-sandbox --port 8931 --host 0.0.0.0 +``` + +The server will listen on host port **8931** and can be reached by any MCP client. + +You can build the Docker image yourself. + +``` +docker build -t mcr.microsoft.com/playwright/mcp . +``` +
+ +
+Programmatic usage + +```js +import http from 'http'; + +import { createConnection } from '@playwright/mcp'; +import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; + +http.createServer(async (req, res) => { + // ... + + // Creates a headless Playwright MCP server with SSE transport + const connection = await createConnection({ browser: { launchOptions: { headless: true } } }); + const transport = new SSEServerTransport('/messages', res); + await connection.connect(transport); + + // ... +}); +``` +
+ +### Tools + + + +
+Core automation + + + +- **browser_click** + - Title: Click + - Description: Perform click on a web page + - Parameters: + - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element + - `target` (string): Exact target element reference from the page snapshot, or a unique element selector + - `doubleClick` (boolean, optional): Whether to perform a double click instead of a single click + - `button` (string, optional): Button to click, defaults to left + - `modifiers` (array, optional): Modifier keys to press + - Read-only: **false** + + + +- **browser_close** + - Title: Close browser + - Description: Close the page + - Parameters: None + - Read-only: **false** + + + +- **browser_console_messages** + - Title: Get console messages + - Description: Returns all console messages + - Parameters: + - `level` (string): Level of the console messages to return. Each level includes the messages of more severe levels. Defaults to "info". + - `all` (boolean, optional): Return all console messages since the beginning of the session, not just since the last navigation. Defaults to false. + - `filename` (string, optional): Filename to save the console messages to. If not provided, messages are returned as text. + - Read-only: **true** + + + +- **browser_drag** + - Title: Drag mouse + - Description: Perform drag and drop between two elements + - Parameters: + - `startElement` (string, optional): Human-readable source element description used to obtain the permission to interact with the element + - `startTarget` (string): Exact target element reference from the page snapshot, or a unique element selector + - `endElement` (string, optional): Human-readable target element description used to obtain the permission to interact with the element + - `endTarget` (string): Exact target element reference from the page snapshot, or a unique element selector + - Read-only: **false** + + + +- **browser_drop** + - Title: Drop files or data onto an element + - Description: Drop files or MIME-typed data onto an element, as if dragged from outside the page. At least one of "paths" or "data" must be provided. + - Parameters: + - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element + - `target` (string): Exact target element reference from the page snapshot, or a unique element selector + - `paths` (array, optional): Absolute paths to files to drop onto the element. + - `data` (object, optional): Data to drop, as a map of MIME type to string value (e.g. {"text/plain": "hello", "text/uri-list": "https://example.com"}). + - Read-only: **false** + + + +- **browser_evaluate** + - Title: Evaluate JavaScript + - Description: Evaluate JavaScript expression on page or element + - Parameters: + - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element + - `target` (string, optional): Exact target element reference from the page snapshot, or a unique element selector + - `function` (string): () => { /* code */ } or (element) => { /* code */ } when element is provided + - `filename` (string, optional): Filename to save the result to. If not provided, result is returned as text. + - Read-only: **false** + + + +- **browser_file_upload** + - Title: Upload files + - Description: Upload one or multiple files + - Parameters: + - `paths` (array, optional): The absolute paths to the files to upload. Can be single file or multiple files. If omitted, file chooser is cancelled. + - Read-only: **false** + + + +- **browser_fill_form** + - Title: Fill form + - Description: Fill multiple form fields + - Parameters: + - `fields` (array): Fields to fill in + - Read-only: **false** + + + +- **browser_find** + - Title: Find in page snapshot + - Description: Search the accessibility snapshot of the current page for text or a regular expression. Returns matching snapshot nodes with a few lines of surrounding context (like search snippets), each shown under its path from the root of the tree, which is cheaper than capturing the whole snapshot when you only need to locate an element and its ref. + - Parameters: + - `text` (string, optional): Plain text to search for in the page snapshot (case-insensitive substring match). Provide either text or regex, not both. + - `regex` (string, optional): Regular expression to search for in the page snapshot. Matching is case-sensitive by default; wrap the pattern in slashes to add flags, e.g. "/error/i" for case-insensitive. Provide either text or regex, not both. + - Read-only: **true** + + + +- **browser_handle_dialog** + - Title: Handle a dialog + - Description: Handle a dialog + - Parameters: + - `accept` (boolean): Whether to accept the dialog. + - `promptText` (string, optional): The text of the prompt in case of a prompt dialog. + - Read-only: **false** + + + +- **browser_hover** + - Title: Hover mouse + - Description: Hover over element on page + - Parameters: + - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element + - `target` (string): Exact target element reference from the page snapshot, or a unique element selector + - Read-only: **false** + + + +- **browser_navigate** + - Title: Navigate to a URL + - Description: Navigate to a URL + - Parameters: + - `url` (string): The URL to navigate to + - Read-only: **false** + + + +- **browser_navigate_back** + - Title: Go back + - Description: Go back to the previous page in the history + - Parameters: None + - Read-only: **false** + + + +- **browser_network_request** + - Title: Show network request details + - Description: Returns full details (headers and body) of a single network request, or a single part if `part` is set. Use the number from browser_network_requests. + - Parameters: + - `index` (integer): 1-based index of the request, as printed by browser_network_requests. + - `part` (string, optional): Return only this part of the request. Omit to return full details. + - `filename` (string, optional): Filename to save the result to. If not provided, output is returned as text. + - Read-only: **true** + + + +- **browser_network_requests** + - Title: List network requests + - Description: Returns a numbered list of network requests since loading the page. Use browser_network_request with the number to get full details. + - Parameters: + - `static` (boolean): Whether to include successful static resources like images, fonts, scripts, etc. Defaults to false. + - `filter` (string, optional): Only return requests whose URL matches this regexp (e.g. "/api/.*user"). + - `filename` (string, optional): Filename to save the network requests to. If not provided, requests are returned as text. + - Read-only: **true** + + + +- **browser_press_key** + - Title: Press a key + - Description: Press a key on the keyboard + - Parameters: + - `key` (string): Name of the key to press or a character to generate, such as `ArrowLeft` or `a` + - Read-only: **false** + + + +- **browser_resize** + - Title: Resize browser window + - Description: Resize the browser window + - Parameters: + - `width` (number): Width of the browser window + - `height` (number): Height of the browser window + - Read-only: **false** + + + +- **browser_run_code_unsafe** + - Title: Run Playwright code (unsafe) + - Description: Run a Playwright code snippet. Unsafe: executes arbitrary JavaScript in the Playwright server process and is RCE-equivalent. + - Parameters: + - `code` (string, optional): 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(); }` + - `filename` (string, optional): Load code from the specified file. If both code and filename are provided, code will be ignored. + - Read-only: **false** + + + +- **browser_select_option** + - Title: Select option + - Description: Select an option in a dropdown + - Parameters: + - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element + - `target` (string): Exact target element reference from the page snapshot, or a unique element selector + - `values` (array): Array of values to select in the dropdown. This can be a single value or multiple values. + - Read-only: **false** + + + +- **browser_snapshot** + - Title: Page snapshot + - Description: Capture accessibility snapshot of the current page, this is better than screenshot + - Parameters: + - `target` (string, optional): Exact target element reference from the page snapshot, or a unique element selector + - `filename` (string, optional): Save snapshot to markdown file instead of returning it in the response. + - `depth` (number, optional): Limit the depth of the snapshot tree + - `boxes` (boolean, optional): Include each element's bounding box as [box=x,y,width,height] in the snapshot. Coordinates are viewport-relative, in CSS pixels (Element.getBoundingClientRect) + - Read-only: **true** + + + +- **browser_take_screenshot** + - Title: Take a screenshot + - Description: Take a screenshot of the current page. You can't perform actions based on the screenshot, use browser_snapshot for actions. + - Parameters: + - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element + - `target` (string, optional): Exact target element reference from the page snapshot, or a unique element selector + - `type` (string): Image format for the screenshot. Default is png. + - `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. + - `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. + - `scale` (string): Image resolution scale. "css" produces a screenshot sized in CSS pixels (smaller, consistent across devices). "device" produces a high-resolution screenshot using device pixels (larger, accounts for the device pixel ratio). Default is css. + - Read-only: **true** + + + +- **browser_type** + - Title: Type text + - Description: Type text into editable element + - Parameters: + - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element + - `target` (string): Exact target element reference from the page snapshot, or a unique element selector + - `text` (string): Text to type into the element + - `submit` (boolean, optional): Whether to submit entered text (press Enter after) + - `slowly` (boolean, optional): Whether to type one character at a time. Useful for triggering key handlers in the page. By default entire text is filled in at once. + - Read-only: **false** + + + +- **browser_wait_for** + - Title: Wait for + - Description: Wait for text to appear or disappear or a specified time to pass + - Parameters: + - `time` (number, optional): The time to wait in seconds + - `text` (string, optional): The text to wait for + - `textGone` (string, optional): The text to wait for to disappear + - Read-only: **false** + +
+ +
+Tab management + + + +- **browser_tabs** + - Title: Manage tabs + - Description: List, create, close, or select a browser tab. + - Parameters: + - `action` (string): Operation to perform + - `index` (number, optional): Tab index, used for close/select. If omitted for close, current tab is closed. + - `url` (string, optional): URL to navigate to in the new tab, used for new. + - Read-only: **false** + +
+ +
+Browser installation + +
+ +
+Configuration (opt-in via --caps=config) + + + +- **browser_get_config** + - Title: Get config + - Description: Get the final resolved config after merging CLI options, environment variables and config file. + - Parameters: None + - Read-only: **true** + +
+ +
+Network (opt-in via --caps=network) + + + +- **browser_network_state_set** + - Title: Set network state + - Description: Sets the browser network state to online or offline. When offline, all network requests will fail. + - Parameters: + - `state` (string): Set to "offline" to simulate offline mode, "online" to restore network connectivity + - Read-only: **false** + + + +- **browser_route** + - Title: Mock network requests + - Description: Set up a route to mock network requests matching a URL pattern + - Parameters: + - `pattern` (string): URL pattern to match (e.g., "**/api/users", "**/*.{png,jpg}") + - `status` (number, optional): HTTP status code to return (default: 200) + - `body` (string, optional): Response body (text or JSON string) + - `contentType` (string, optional): Content-Type header (e.g., "application/json", "text/html") + - `headers` (array, optional): Headers to add in "Name: Value" format + - `removeHeaders` (string, optional): Comma-separated list of header names to remove from request + - Read-only: **false** + + + +- **browser_route_list** + - Title: List network routes + - Description: List all active network routes + - Parameters: None + - Read-only: **true** + + + +- **browser_unroute** + - Title: Remove network routes + - Description: Remove network routes matching a pattern (or all routes if no pattern specified) + - Parameters: + - `pattern` (string, optional): URL pattern to unroute (omit to remove all routes) + - Read-only: **false** + +
+ +
+Storage (opt-in via --caps=storage) + + + +- **browser_cookie_clear** + - Title: Clear cookies + - Description: Clear all cookies + - Parameters: None + - Read-only: **false** + + + +- **browser_cookie_delete** + - Title: Delete cookie + - Description: Delete a specific cookie + - Parameters: + - `name` (string): Cookie name to delete + - Read-only: **false** + + + +- **browser_cookie_get** + - Title: Get cookie + - Description: Get a specific cookie by name + - Parameters: + - `name` (string): Cookie name to get + - Read-only: **true** + + + +- **browser_cookie_list** + - Title: List cookies + - Description: List all cookies (optionally filtered by domain/path) + - Parameters: + - `domain` (string, optional): Filter cookies by domain + - `path` (string, optional): Filter cookies by path + - Read-only: **true** + + + +- **browser_cookie_set** + - Title: Set cookie + - Description: Set a cookie with optional flags (domain, path, expires, httpOnly, secure, sameSite) + - Parameters: + - `name` (string): Cookie name + - `value` (string): Cookie value + - `domain` (string, optional): Cookie domain + - `path` (string, optional): Cookie path + - `expires` (number, optional): Cookie expiration as Unix timestamp + - `httpOnly` (boolean, optional): Whether the cookie is HTTP only + - `secure` (boolean, optional): Whether the cookie is secure + - `sameSite` (string, optional): Cookie SameSite attribute + - Read-only: **false** + + + +- **browser_localstorage_clear** + - Title: Clear localStorage + - Description: Clear all localStorage + - Parameters: None + - Read-only: **false** + + + +- **browser_localstorage_delete** + - Title: Delete localStorage item + - Description: Delete a localStorage item + - Parameters: + - `key` (string): Key to delete + - Read-only: **false** + + + +- **browser_localstorage_get** + - Title: Get localStorage item + - Description: Get a localStorage item by key + - Parameters: + - `key` (string): Key to get + - Read-only: **true** + + + +- **browser_localstorage_list** + - Title: List localStorage + - Description: List all localStorage key-value pairs + - Parameters: None + - Read-only: **true** + + + +- **browser_localstorage_set** + - Title: Set localStorage item + - Description: Set a localStorage item + - Parameters: + - `key` (string): Key to set + - `value` (string): Value to set + - Read-only: **false** + + + +- **browser_sessionstorage_clear** + - Title: Clear sessionStorage + - Description: Clear all sessionStorage + - Parameters: None + - Read-only: **false** + + + +- **browser_sessionstorage_delete** + - Title: Delete sessionStorage item + - Description: Delete a sessionStorage item + - Parameters: + - `key` (string): Key to delete + - Read-only: **false** + + + +- **browser_sessionstorage_get** + - Title: Get sessionStorage item + - Description: Get a sessionStorage item by key + - Parameters: + - `key` (string): Key to get + - Read-only: **true** + + + +- **browser_sessionstorage_list** + - Title: List sessionStorage + - Description: List all sessionStorage key-value pairs + - Parameters: None + - Read-only: **true** + + + +- **browser_sessionstorage_set** + - Title: Set sessionStorage item + - Description: Set a sessionStorage item + - Parameters: + - `key` (string): Key to set + - `value` (string): Value to set + - Read-only: **false** + + + +- **browser_set_storage_state** + - Title: Restore storage state + - Description: Restore storage state (cookies, local storage) from a file. This clears existing cookies and local storage before restoring. + - Parameters: + - `filename` (string): Path to the storage state file to restore from + - Read-only: **false** + + + +- **browser_storage_state** + - Title: Save storage state + - Description: Save storage state (cookies, local storage) to a file for later reuse + - Parameters: + - `filename` (string, optional): File name to save the storage state to. Defaults to `storage-state-{timestamp}.json` if not specified. + - Read-only: **true** + +
+ +
+DevTools (opt-in via --caps=devtools) + + + +- **browser_annotate** + - Title: Annotate the current page + - Description: Open the Playwright Dashboard in annotation mode for the current page and wait for the user to draw annotations. Returns the annotated screenshot, ARIA snapshot, and the list of annotations. + - Parameters: None + - Read-only: **true** + + + +- **browser_hide_highlight** + - Title: Hide element highlight + - Description: Remove a highlight overlay previously added for the element. + - Parameters: + - `element` (string, optional): Human-readable element description used when adding the highlight; must match the value passed to browser_highlight. + - `target` (string, optional): Exact target element reference from the page snapshot, or a unique element selector + - Read-only: **true** + + + +- **browser_highlight** + - Title: Highlight element + - Description: Show a persistent highlight overlay around the element on the page. + - Parameters: + - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element + - `target` (string): Exact target element reference from the page snapshot, or a unique element selector + - `style` (string, optional): Additional inline CSS applied to the highlight overlay, e.g. "outline: 2px dashed red". + - Read-only: **true** + + + +- **browser_resume** + - Title: Resume paused script execution + - Description: Resume script execution after it was paused. When called with step set to true, execution will pause again before the next action. + - Parameters: + - `step` (boolean, optional): When true, execution will pause again before the next action, allowing step-by-step debugging. + - `location` (string, optional): Pause execution at a specific :, e.g. "example.spec.ts:42". + - Read-only: **false** + + + +- **browser_start_tracing** + - Title: Start tracing + - Description: Start trace recording + - Parameters: None + - Read-only: **true** + + + +- **browser_start_video** + - Title: Start video + - Description: Start video recording + - Parameters: + - `filename` (string, optional): Filename to save the video. + - `size` (object, optional): Video size + - Read-only: **true** + + + +- **browser_stop_tracing** + - Title: Stop tracing + - Description: Stop trace recording + - Parameters: None + - Read-only: **true** + + + +- **browser_stop_video** + - Title: Stop video + - Description: Stop video recording + - Parameters: None + - Read-only: **true** + + + +- **browser_video_chapter** + - Title: Video chapter + - Description: Add a chapter marker to the video recording. Shows a full-screen chapter card with blurred backdrop. + - Parameters: + - `title` (string): Chapter title + - `description` (string, optional): Chapter description + - `duration` (number, optional): Duration in milliseconds to show the chapter card + - Read-only: **true** + + + +- **browser_video_hide_actions** + - Title: Hide action overlays + - Description: Stop annotating actions performed on the page. + - Parameters: None + - Read-only: **true** + + + +- **browser_video_show_actions** + - Title: Show action overlays + - Description: Annotate subsequent actions performed on the page with a callout that names the action and highlights the target element. Useful while video recording or screencasting. + - Parameters: + - `duration` (number, optional): How long each action annotation stays on screen, in milliseconds. Defaults to 500. + - `position` (string, optional): Where to place the action title relative to the page. Defaults to top-right. + - `cursor` (string, optional): Cursor decoration for pointer actions. "pointer" (default) animates a mouse pointer from the previous action point to the next one; "none" disables the cursor decoration. + - Read-only: **true** + +
+ +
+Coordinate-based (opt-in via --caps=vision) + + + +- **browser_mouse_click_xy** + - Title: Click + - Description: Click mouse button at a given position + - Parameters: + - `x` (number): X coordinate + - `y` (number): Y coordinate + - `button` (string, optional): Button to click, defaults to left + - `clickCount` (number, optional): Number of clicks, defaults to 1 + - `delay` (number, optional): Time to wait between mouse down and mouse up in milliseconds, defaults to 0 + - Read-only: **false** + + + +- **browser_mouse_down** + - Title: Press mouse down + - Description: Press mouse down + - Parameters: + - `button` (string, optional): Button to press, defaults to left + - Read-only: **false** + + + +- **browser_mouse_drag_xy** + - Title: Drag mouse + - Description: Drag left mouse button to a given position + - Parameters: + - `startX` (number): Start X coordinate + - `startY` (number): Start Y coordinate + - `endX` (number): End X coordinate + - `endY` (number): End Y coordinate + - Read-only: **false** + + + +- **browser_mouse_move_xy** + - Title: Move mouse + - Description: Move mouse to a given position + - Parameters: + - `x` (number): X coordinate + - `y` (number): Y coordinate + - Read-only: **false** + + + +- **browser_mouse_up** + - Title: Press mouse up + - Description: Press mouse up + - Parameters: + - `button` (string, optional): Button to press, defaults to left + - Read-only: **false** + + + +- **browser_mouse_wheel** + - Title: Scroll mouse wheel + - Description: Scroll mouse wheel + - Parameters: + - `deltaX` (number): X delta + - `deltaY` (number): Y delta + - Read-only: **false** + +
+ +
+PDF generation (opt-in via --caps=pdf) + + + +- **browser_pdf_save** + - Title: Save as PDF + - Description: Save page as PDF + - Parameters: + - `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. + - Read-only: **true** + +
+ +
+Test assertions (opt-in via --caps=testing) + + + +- **browser_generate_locator** + - Title: Create locator for element + - Description: Generate locator for the given element to use in tests + - Parameters: + - `element` (string, optional): Human-readable element description used to obtain permission to interact with the element + - `target` (string): Exact target element reference from the page snapshot, or a unique element selector + - Read-only: **true** + + + +- **browser_verify_element_visible** + - Title: Verify element visible + - Description: Verify element is visible on the page + - Parameters: + - `role` (string): ROLE of the element. Can be found in the snapshot like this: `- {ROLE} "Accessible Name":` + - `accessibleName` (string): ACCESSIBLE_NAME of the element. Can be found in the snapshot like this: `- role "{ACCESSIBLE_NAME}"` + - Read-only: **false** + + + +- **browser_verify_list_visible** + - Title: Verify list visible + - Description: Verify list is visible on the page + - Parameters: + - `element` (string): Human-readable list description + - `target` (string): Exact target element reference that points to the list + - `items` (array): Items to verify + - Read-only: **false** + + + +- **browser_verify_text_visible** + - Title: Verify text visible + - Description: Verify text is visible on the page. Prefer browser_verify_element_visible if possible. + - Parameters: + - `text` (string): TEXT to verify. Can be found in the snapshot like this: `- role "Accessible Name": {TEXT}` or like this: `- text: {TEXT}` + - Read-only: **false** + + + +- **browser_verify_value** + - Title: Verify value + - Description: Verify element value + - Parameters: + - `type` (string): Type of the element + - `element` (string): Human-readable element description + - `target` (string): Exact target element reference from the page snapshot + - `value` (string): Value to verify. For checkbox, use "true" or "false". + - Read-only: **false** + +
+ + + diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..01c0dc5 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`microsoft/playwright-mcp` +- 原始仓库:https://github.com/microsoft/playwright-mcp +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..b3c89ef --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,41 @@ + + +## Security + +Microsoft 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). + +If 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. + +## Reporting Security Issues + +**Please do not report security vulnerabilities through public GitHub issues.** + +Instead, 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). + +If 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). + +You 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). + +Please 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: + + * Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.) + * Full paths of source file(s) related to the manifestation of the issue + * The location of the affected source code (tag/branch/commit or direct URL) + * Any special configuration required to reproduce the issue + * Step-by-step instructions to reproduce the issue + * Proof-of-concept or exploit code (if possible) + * Impact of the issue, including how an attacker might exploit the issue + +This information will help us triage your report more quickly. + +If 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. + +## Preferred Languages + +We prefer all communications to be in English. + +## Policy + +Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd). + + diff --git a/cli.js b/cli.js new file mode 100755 index 0000000..f07382b --- /dev/null +++ b/cli.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { program } = require('playwright-core/lib/utilsBundle'); +const { tools, libCli } = require('playwright-core/lib/coreBundle'); + +if (process.argv.includes('install-browser')) { + const argv = process.argv.map(arg => arg === 'install-browser' ? 'install' : arg); + libCli.decorateProgram(program); + void program.parseAsync(argv); + return; +} + +const packageJSON = require('./package.json'); +const p = program.version('Version ' + packageJSON.version).name('Playwright MCP'); +tools.decorateMCPCommand(p, packageJSON.version); + +void program.parseAsync(process.argv); diff --git a/config.d.ts b/config.d.ts new file mode 100644 index 0000000..bd9835c --- /dev/null +++ b/config.d.ts @@ -0,0 +1,239 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type * as playwright from '../../..'; + +export type ToolCapability = + 'config' | + 'core' | + 'core-navigation' | + 'core-tabs' | + 'core-input' | + 'core-install' | + 'network' | + 'pdf' | + 'storage' | + 'testing' | + 'vision' | + 'devtools'; + +export type Config = { + /** + * The browser to use. + */ + browser?: { + /** + * The type of browser to use. + */ + browserName?: 'chromium' | 'firefox' | 'webkit'; + + /** + * Keep the browser profile in memory, do not save it to disk. + */ + isolated?: boolean; + + /** + * Path to a user data directory for browser profile persistence. + * Temporary directory is created by default. + */ + userDataDir?: string; + + /** + * Launch options passed to + * @see https://playwright.dev/docs/api/class-browsertype#browser-type-launch-persistent-context + * + * This is useful for settings options like `channel`, `headless`, `executablePath`, etc. + */ + launchOptions?: playwright.LaunchOptions; + + /** + * Context options for the browser context. + * + * This is useful for settings options like `viewport`. + */ + contextOptions?: playwright.BrowserContextOptions; + + /** + * Chrome DevTools Protocol endpoint to connect to an existing browser instance in case of Chromium family browsers. + */ + cdpEndpoint?: string; + + /** + * CDP headers to send with the connect request. + */ + cdpHeaders?: Record; + + /** + * Timeout in milliseconds for connecting to CDP endpoint. Defaults to 30000 (30 seconds). Pass 0 to disable timeout. + */ + cdpTimeout?: number; + + /** + * Remote endpoint to connect to an existing Playwright server. May be a + * WebSocket URL string, or a [ConnectOptions] object that mirrors the + * `connectOptions` shape used by the test runner. When passed as an object, + * `exposeNetwork`, `headers`, `slowMo`, and `timeout` are forwarded to the + * underlying connect call. + */ + remoteEndpoint?: string | playwright.ConnectOptions & { endpoint: string }; + + /** + * Paths to TypeScript files to add as initialization scripts for Playwright page. + */ + initPage?: string[]; + + /** + * Paths to JavaScript files to add as initialization scripts. + * The scripts will be evaluated in every page before any of the page's scripts. + */ + initScript?: string[]; + }, + + /** + * Connect to a running browser instance (Edge/Chrome only). If specified, `browser` + * config is ignored. + * Requires the "Playwright Extension" to be installed. + */ + extension?: boolean; + + server?: { + /** + * The port to listen on for SSE or MCP transport. + */ + port?: number; + + /** + * The host to bind the server to. Default is localhost. Use 0.0.0.0 to bind to all interfaces. + */ + host?: string; + + /** + * The hosts this server is allowed to serve from. Defaults to the host server is bound to. + * This is not for CORS, but rather for the DNS rebinding protection. + */ + allowedHosts?: string[]; + }, + + /** + * List of enabled tool capabilities. Possible values: + * - 'core': Core browser automation features. + * - 'pdf': PDF generation and manipulation. + * - 'vision': Coordinate-based interactions. + * - 'devtools': Developer tools features. + */ + capabilities?: ToolCapability[]; + + /** + * Whether to save the Playwright session into the output directory. + */ + saveSession?: boolean; + + /** + * Reuse the same browser context between all connected HTTP clients. + */ + sharedBrowserContext?: boolean; + + /** + * Secrets are used to replace matching plain text in the tool responses to prevent the LLM + * from accidentally getting sensitive data. It is a convenience and not a security feature, + * make sure to always examine information coming in and from the tool on the client. + */ + secrets?: Record; + + /** + * The directory to save output files. + */ + outputDir?: string; + + /** + * Threshold for evicting old output files, in bytes. + */ + outputMaxSize?: number; + + console?: { + /** + * The level of console messages to return. Each level includes the messages of more severe levels. Defaults to "info". + */ + level?: 'error' | 'warning' | 'info' | 'debug'; + }, + + network?: { + /** + * List of origins to allow the browser to request. Default is to allow all. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked. + * + * Supported formats: + * - Full origin: `https://example.com:8080` - matches only that origin + * - Wildcard port: `http://localhost:*` - matches any port on localhost with http protocol + */ + allowedOrigins?: string[]; + + /** + * List of origins to block the browser to request. Origins matching both `allowedOrigins` and `blockedOrigins` will be blocked. + * + * Supported formats: + * - Full origin: `https://example.com:8080` - matches only that origin + * - Wildcard port: `http://localhost:*` - matches any port on localhost with http protocol + */ + blockedOrigins?: string[]; + }; + + /** + * Specify the attribute to use for test ids, defaults to "data-testid". + */ + testIdAttribute?: string; + + timeouts?: { + /* + * Configures default action timeout: https://playwright.dev/docs/api/class-page#page-set-default-timeout. Defaults to 5000ms. + */ + action?: number; + + /* + * Configures default navigation timeout: https://playwright.dev/docs/api/class-page#page-set-default-navigation-timeout. Defaults to 60000ms. + */ + navigation?: number; + + /** + * Configures default expect timeout: https://playwright.dev/docs/test-timeouts#expect-timeout. Defaults to 5000ms. + */ + expect?: number; + }; + + /** + * 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. + */ + imageResponses?: 'allow' | 'omit'; + + snapshot?: { + /** + * When taking snapshots for responses, specifies the mode to use. + */ + mode?: 'full' | 'none'; + }; + + /** + * allowUnrestrictedFileAccess acts as a guardrail to prevent the LLM from accidentally + * wandering outside its intended workspace. It is a convenience defense to catch unintended + * file access, not a secure boundary; a deliberate attempt to reach other directories can be + * easily worked around, so always rely on client-level permissions for true security. + */ + allowUnrestrictedFileAccess?: boolean; + + /** + * Specify the language to use for code generation. + */ + codegen?: 'typescript' | 'none'; +}; diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..4a2b679 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,23 @@ +#!/usr/bin/env node +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Server } from '@modelcontextprotocol/sdk/server/index.js'; +import type { Config } from './config'; +import type { BrowserContext } from 'playwright'; + +export declare function createConnection(config?: Config, contextGetter?: () => Promise): Promise; +export {}; diff --git a/index.js b/index.js new file mode 100755 index 0000000..75594c7 --- /dev/null +++ b/index.js @@ -0,0 +1,19 @@ +#!/usr/bin/env node +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const { tools } = require('playwright-core/lib/coreBundle'); +module.exports = { createConnection: tools.createConnection }; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d80afcb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1319 @@ +{ + "name": "@playwright/mcp", + "version": "0.0.78", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@playwright/mcp", + "version": "0.0.78", + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.0-alpha-1783623505000", + "playwright-core": "1.62.0-alpha-1783623505000" + }, + "bin": { + "playwright-mcp": "cli.js" + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2", + "@playwright/test": "1.62.0-alpha-1783623505000", + "@types/node": "^24.3.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.14.tgz", + "integrity": "sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.29.0.tgz", + "integrity": "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@playwright/test": { + "version": "1.62.0-alpha-1783623505000", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.0-alpha-1783623505000.tgz", + "integrity": "sha512-6aj9UWRXnS2amfs+8BHPRqQNTyiq91MF8Pl0UechaJkW0TZfvLxEjvhBnSrs6Lm3jcLmkkv/QTpW5NyslKZpTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.62.0-alpha-1783623505000" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@types/node": { + "version": "24.12.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz", + "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.16.0" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.8.tgz", + "integrity": "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.5.1.tgz", + "integrity": "sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.26", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.26.tgz", + "integrity": "sha512-uyZtpnYxM9CmQ7QsQknM4zN8EftNqhON1qYeIKM0Se67CCEe2c44xyGURwB0axX2fBDu1dqHrHAc1hmNT8ITkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/playwright": { + "version": "1.62.0-alpha-1783623505000", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.0-alpha-1783623505000.tgz", + "integrity": "sha512-6KV9h4PP3hqu4NaGdxxcijWfYh9LJcFI/R2sP4TTC4I5cFo3oRawN0ETlW5MkE3cQEgKhhoj0KUNz4sfpCT0Tg==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.0-alpha-1783623505000" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.0-alpha-1783623505000", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.0-alpha-1783623505000.tgz", + "integrity": "sha512-CPJZdsA/KGT2QQlekiV6Wt+QlQrZHVSZ6oiNtOI/bYYOIVLM8jfKGWTM4zQiyd4UN+40Cq4cA6lxmZHZbtPvJQ==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici-types": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c74c4ae --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "@playwright/mcp", + "version": "0.0.78", + "description": "Playwright Tools for MCP", + "repository": { + "type": "git", + "url": "git+https://github.com/microsoft/playwright-mcp.git" + }, + "homepage": "https://playwright.dev", + "engines": { + "node": ">=18" + }, + "author": { + "name": "Microsoft Corporation" + }, + "license": "Apache-2.0", + "mcpName": "io.github.microsoft/playwright-mcp", + "scripts": { + "lint": "node update-readme.js", + "test": "playwright test", + "ctest": "playwright test --project=chrome", + "ftest": "playwright test --project=firefox", + "wtest": "playwright test --project=webkit", + "dtest": "MCP_IN_DOCKER=1 playwright test --project=chromium-docker", + "build": "echo OK", + "npm-publish": "npm run lint && npm run test && npm publish", + "docker-build": "docker build --no-cache -t playwright-mcp-dev:latest .", + "docker-rm": "docker rm playwright-mcp-dev", + "docker-run": "docker run -it -p 8080:8080 --name playwright-mcp-dev playwright-mcp-dev:latest", + "roll": "node roll.js" + }, + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./index.d.ts", + "default": "./index.js" + } + }, + "dependencies": { + "playwright": "1.62.0-alpha-1783623505000", + "playwright-core": "1.62.0-alpha-1783623505000" + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2", + "@playwright/test": "1.62.0-alpha-1783623505000", + "@types/node": "^24.3.0" + }, + "bin": { + "playwright-mcp": "cli.js" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..2c3abfb --- /dev/null +++ b/playwright.config.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { defineConfig } from '@playwright/test'; + +import type { TestOptions } from './tests/fixtures'; + +export default defineConfig({ + testDir: './tests', + fullyParallel: true, + forbidOnly: !!process.env.CI, + workers: process.env.CI ? 2 : undefined, + reporter: 'list', + projects: [ + { name: 'chrome' }, + ...process.env.MCP_IN_DOCKER ? [{ + name: 'chromium-docker', + grep: /browser_navigate|browser_click/, + use: { + mcpBrowser: 'chromium', + mcpMode: 'docker' as const + } + }] : [], + ], +}); diff --git a/roll.js b/roll.js new file mode 100644 index 0000000..1efde26 --- /dev/null +++ b/roll.js @@ -0,0 +1,49 @@ +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +function copyConfig() { + const src = path.join(__dirname, '..', 'playwright', 'packages', 'playwright-core', 'src', 'tools', 'mcp', 'config.d.ts'); + const dst = path.join(__dirname, 'config.d.ts'); + let content = fs.readFileSync(src, 'utf-8'); + content = content.replace( + "import type * as playwright from 'playwright-core';", + "import type * as playwright from 'playwright';" + ); + fs.writeFileSync(dst, content); + console.log(`Copied config.d.ts from ${src} to ${dst}`); +} + +function updatePlaywrightVersion(version) { + const file = path.join(__dirname, 'package.json'); + const json = JSON.parse(fs.readFileSync(file, 'utf-8')); + let updated = false; + for (const section of ['dependencies', 'devDependencies']) { + for (const pkg of ['@playwright/test', 'playwright', 'playwright-core']) { + if (json[section]?.[pkg]) { + json[section][pkg] = version; + updated = true; + } + } + } + if (updated) { + fs.writeFileSync(file, JSON.stringify(json, null, 2) + '\n'); + console.log(`Updated ${file}`); + } + + execSync('npm install', { cwd: __dirname, stdio: 'inherit' }); +} + +function doRoll(version) { + updatePlaywrightVersion(version); + copyConfig(); + // update readme + execSync('npm run lint', { cwd: __dirname, stdio: 'inherit' }); +} + +let version = process.argv[2]; +if (!version) { + version = execSync('npm info playwright@next version', { encoding: 'utf-8' }).trim(); + console.log(`Using next playwright version: ${version}`); +} +doRoll(version); diff --git a/server.json b/server.json new file mode 100644 index 0000000..e7dec3b --- /dev/null +++ b/server.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.microsoft/playwright-mcp", + "description": "Playwright Tools for MCP", + "repository": { + "url": "https://github.com/microsoft/playwright-mcp", + "source": "github" + }, + "version": "0.0.78", + "packages": [ + { + "registryType": "npm", + "identifier": "@playwright/mcp", + "version": "0.0.78", + "transport": { + "type": "stdio" + } + } + ] +} diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..4645766 --- /dev/null +++ b/src/README.md @@ -0,0 +1,3 @@ +# Where is the source? + +Playwright MCP source code is located in the [Playwright monorepo](https://github.com/microsoft/playwright/blob/main/packages/playwright-core/src/tools/mcp). Please refer to the contributor's guide in [CONTRIBUTING.md](../CONTRIBUTING.md) for more details. diff --git a/tests/capabilities.spec.ts b/tests/capabilities.spec.ts new file mode 100644 index 0000000..c55d96a --- /dev/null +++ b/tests/capabilities.spec.ts @@ -0,0 +1,78 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('test snapshot tool list', async ({ client }) => { + const { tools } = await client.listTools(); + expect(new Set(tools.map(t => t.name))).toEqual(new Set([ + 'browser_click', + 'browser_console_messages', + 'browser_drag', + 'browser_drop', + 'browser_evaluate', + 'browser_file_upload', + 'browser_fill_form', + 'browser_find', + 'browser_handle_dialog', + 'browser_hover', + 'browser_select_option', + 'browser_type', + 'browser_close', + 'browser_navigate_back', + 'browser_navigate', + 'browser_network_request', + 'browser_network_requests', + 'browser_press_key', + 'browser_resize', + 'browser_run_code_unsafe', + 'browser_snapshot', + 'browser_tabs', + 'browser_take_screenshot', + 'browser_wait_for', + ])); +}); + +test('test capabilities (pdf)', async ({ startClient }) => { + const { client } = await startClient({ + args: ['--caps=pdf'], + }); + const { tools } = await client.listTools(); + const toolNames = tools.map(t => t.name); + expect(toolNames).toContain('browser_pdf_save'); +}); + +test('test capabilities (vision)', async ({ startClient }) => { + const { client } = await startClient({ + args: ['--caps=vision'], + }); + const { tools } = await client.listTools(); + const toolNames = tools.map(t => t.name); + expect(toolNames).toContain('browser_mouse_move_xy'); + expect(toolNames).toContain('browser_mouse_click_xy'); + expect(toolNames).toContain('browser_mouse_drag_xy'); +}); + +test('support for legacy --vision option', async ({ startClient }) => { + const { client } = await startClient({ + args: ['--vision'], + }); + const { tools } = await client.listTools(); + const toolNames = tools.map(t => t.name); + expect(toolNames).toContain('browser_mouse_move_xy'); + expect(toolNames).toContain('browser_mouse_click_xy'); + expect(toolNames).toContain('browser_mouse_drag_xy'); +}); diff --git a/tests/cli.spec.ts b/tests/cli.spec.ts new file mode 100644 index 0000000..21653ae --- /dev/null +++ b/tests/cli.spec.ts @@ -0,0 +1,25 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import child_process from 'child_process'; +import path from 'path'; +import { test, expect } from './fixtures'; + +const cliPath = path.resolve(__dirname, '..', 'cli.js'); + +test('install-browser --help', async () => { + const output = child_process.execSync(`node ${cliPath} install-browser --help`, { encoding: 'utf-8' }); + expect(output).toContain('install'); +}); diff --git a/tests/click.spec.ts b/tests/click.spec.ts new file mode 100644 index 0000000..a06cf3c --- /dev/null +++ b/tests/click.spec.ts @@ -0,0 +1,49 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('browser_click', async ({ client, server }) => { + server.setContent('/', ` + Title + + + `, 'text/html'); + + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.PREFIX }, + })).toHaveResponse({ + code: `await page.goto('${server.PREFIX}');`, + snapshot: expect.stringContaining(`- button \"Submit\" [ref=e2]`), + }); + + expect(await client.callTool({ + name: 'browser_click', + arguments: { + element: 'Submit button', + target: 'e2', + }, + })).toHaveResponse({ + code: `await page.getByRole('button', { name: 'Submit' }).click();`, + snapshot: expect.stringContaining(`button "Submit" [active] [ref=e2]`), + }); +}); diff --git a/tests/core.spec.ts b/tests/core.spec.ts new file mode 100644 index 0000000..ba3780a --- /dev/null +++ b/tests/core.spec.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { test, expect } from './fixtures'; + +test('browser_navigate', async ({ client, server }) => { + expect(await client.callTool({ + name: 'browser_navigate', + arguments: { url: server.HELLO_WORLD }, + })).toHaveResponse({ + code: `await page.goto('${server.HELLO_WORLD}');`, + snapshot: expect.stringContaining(`generic [active] [ref=e1]: Hello, world!`), + }); +}); diff --git a/tests/fixtures.ts b/tests/fixtures.ts new file mode 100644 index 0000000..f7fa0d9 --- /dev/null +++ b/tests/fixtures.ts @@ -0,0 +1,310 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import path from 'path'; +import { chromium } from 'playwright'; + +import { test as baseTest, expect as baseExpect } from '@playwright/test'; +import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; +import { Client } from '@modelcontextprotocol/sdk/client/index.js'; +import { ListRootsRequestSchema } from '@modelcontextprotocol/sdk/types.js'; +import { TestServer } from './testserver/index'; + +import type { Config } from '../config'; +import type { BrowserContext } from 'playwright'; +import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js'; +import type { Stream } from 'stream'; + +export type TestOptions = { + mcpArgs: string[] | undefined; + mcpBrowser: string | undefined; + mcpMode: 'docker' | undefined; +}; + +type CDPServer = { + endpoint: string; + start: () => Promise; +}; + +export type StartClient = (options?: { + clientName?: string, + args?: string[], + config?: Config, + roots?: { name: string, uri: string }[], + rootsResponseDelay?: number, + extensionToken?: string, + env?: Record, +}) => Promise<{ client: Client, stderr: () => string }>; + + +type TestFixtures = { + client: Client; + startClient: StartClient; + wsEndpoint: string; + cdpServer: CDPServer; + server: TestServer; + httpsServer: TestServer; + mcpHeadless: boolean; +}; + +type WorkerFixtures = { + _workerServers: { server: TestServer, httpsServer: TestServer }; +}; + +export const test = baseTest.extend({ + + mcpArgs: [undefined, { option: true }], + + client: async ({ startClient }, use) => { + const { client } = await startClient(); + await use(client); + }, + + startClient: async ({ mcpHeadless, mcpBrowser, mcpMode, mcpArgs }, use, testInfo) => { + const clients: Client[] = []; + + await use(async options => { + const cwd = testInfo.outputPath(); + const args: string[] = mcpArgs ?? []; + if (mcpHeadless) + args.push('--headless'); + if (mcpBrowser) + args.push(`--browser=${mcpBrowser}`); + if (options?.args) + args.push(...options.args); + if (options?.config) { + const configFile = testInfo.outputPath('config.json'); + await fs.promises.writeFile(configFile, JSON.stringify(options.config, null, 2)); + args.push(`--config=${path.relative(cwd, configFile)}`); + } + + const client = new Client({ name: options?.clientName ?? 'test', version: '1.0.0' }, options?.roots ? { capabilities: { roots: {} } } : undefined); + if (options?.roots) { + client.setRequestHandler(ListRootsRequestSchema, async request => { + if (options.rootsResponseDelay) + await new Promise(resolve => setTimeout(resolve, options.rootsResponseDelay)); + return { + roots: options.roots, + }; + }); + } + const { transport, stderr } = await createTransport(args, cwd, mcpMode, testInfo.outputPath('ms-playwright'), options?.env ?? {}); + let stderrBuffer = ''; + stderr?.on('data', data => { + if (process.env.PWMCP_DEBUG) + process.stderr.write(data); + stderrBuffer += data.toString(); + }); + clients.push(client); + await client.connect(transport); + await client.ping(); + return { client, stderr: () => stderrBuffer }; + }); + + await Promise.all(clients.map(client => client.close())); + }, + + wsEndpoint: async ({ }, use) => { + const browserServer = await chromium.launchServer(); + await use(browserServer.wsEndpoint()); + await browserServer.close(); + }, + + cdpServer: async ({ mcpBrowser }, use, testInfo) => { + test.skip(!['chrome', 'msedge', 'chromium'].includes(mcpBrowser!), 'CDP is not supported for non-Chromium browsers'); + + let browserContext: BrowserContext | undefined; + const port = 3200 + test.info().parallelIndex; + await use({ + endpoint: `http://localhost:${port}`, + start: async () => { + if (browserContext) + throw new Error('CDP server already exists'); + browserContext = await chromium.launchPersistentContext(testInfo.outputPath('cdp-user-data-dir'), { + channel: mcpBrowser, + headless: true, + args: [ + `--remote-debugging-port=${port}`, + ], + }); + return browserContext; + } + }); + await browserContext?.close(); + }, + + mcpHeadless: async ({ headless }, use) => { + await use(headless); + }, + + mcpBrowser: ['chrome', { option: true }], + + mcpMode: [undefined, { option: true }], + + _workerServers: [async ({ }, use, workerInfo) => { + const port = 8907 + workerInfo.workerIndex * 4; + const server = await TestServer.create(port); + + const httpsPort = port + 1; + const httpsServer = await TestServer.createHTTPS(httpsPort); + + await use({ server, httpsServer }); + + await Promise.all([ + server.stop(), + httpsServer.stop(), + ]); + }, { scope: 'worker' }], + + server: async ({ _workerServers }, use) => { + _workerServers.server.reset(); + await use(_workerServers.server); + }, + + httpsServer: async ({ _workerServers }, use) => { + _workerServers.httpsServer.reset(); + await use(_workerServers.httpsServer); + }, +}); + +async function createTransport(args: string[], cwd: string, mcpMode: TestOptions['mcpMode'], profilesDir: string, env: Record): Promise<{ + transport: Transport, + stderr: Stream | null, +}> { + if (mcpMode === 'docker') { + const relCwd = path.relative(test.info().project.outputDir, cwd); + const dockerCwd = path.posix.join('/app/test-results', relCwd.split(path.sep).join('/')); + const dockerArgs = ['run', '--rm', '-i', '--network=host', '-v', `${test.info().project.outputDir}:/app/test-results`, '-w', dockerCwd]; + const transport = new StdioClientTransport({ + command: 'docker', + args: [...dockerArgs, 'playwright-mcp-dev:latest', ...args], + }); + return { + transport, + stderr: transport.stderr, + }; + } + + const transport = new StdioClientTransport({ + command: 'node', + args: [path.join(__dirname, '../cli.js'), ...args], + cwd, + stderr: 'pipe', + env: { + ...process.env, + DEBUG: process.env.PWMCP_DEBUG ? 'pw:mcp*' : 'pw:mcp:test', + DEBUG_COLORS: '0', + DEBUG_HIDE_DATE: '1', + PWMCP_PROFILES_DIR_FOR_TEST: profilesDir, + ...env, + }, + }); + return { + transport, + stderr: transport.stderr!, + }; +} + +type Response = Awaited>; + +export const expect = baseExpect.extend({ + toHaveResponse(response: Response, object: any) { + const parsed = parseResponse(response, test.info().outputPath()); + const isNot = this.isNot; + try { + if (isNot) + expect(parsed).not.toEqual(expect.objectContaining(object)); + else + expect(parsed).toEqual(expect.objectContaining(object)); + } catch (e: any) { + return { + pass: isNot, + message: () => e.message, + }; + } + return { + pass: !isNot, + message: () => ``, + }; + }, +}); + +export function formatOutput(output: string): string[] { + return output.split('\n').map(line => line.replace(/^pw:mcp:test /, '').replace(/user data dir.*/, 'user data dir').trim()).filter(Boolean); +} + +function parseResponse(response: any, cwd: string) { + const text = response.content[0].text; + const sections = parseSections(text); + + const error = sections.get('Error'); + const result = sections.get('Result'); + const code = sections.get('Ran Playwright code'); + const tabs = sections.get('Open tabs'); + const pageState = sections.get('Page state'); + const snapshotSection = sections.get('Snapshot'); + const consoleMessages = sections.get('New console messages'); + const modalState = sections.get('Modal state'); + const downloads = sections.get('Downloads'); + const codeNoFrame = code?.replace(/^```js\n/, '').replace(/\n```$/, ''); + const isError = response.isError; + const attachments = response.content.slice(1); + + let snapshot: string | undefined; + if (snapshotSection) { + const match = snapshotSection.match(/\[Snapshot\]\(([^)]+)\)/); + if (match) { + try { + snapshot = fs.readFileSync(path.resolve(cwd, match[1]), 'utf-8'); + } catch { + } + } else { + snapshot = snapshotSection.replace(/^```yaml\n?/, '').replace(/\n?```$/, ''); + } + } + + return { + error, + result, + code: codeNoFrame, + tabs, + pageState, + snapshot, + consoleMessages, + modalState, + downloads, + isError, + attachments, + }; +} + +function parseSections(text: string): Map { + const sections = new Map(); + const sectionHeaders = text.split(/^### /m).slice(1); // Remove empty first element + + for (const section of sectionHeaders) { + const firstNewlineIndex = section.indexOf('\n'); + if (firstNewlineIndex === -1) + continue; + + const sectionName = section.substring(0, firstNewlineIndex); + const sectionContent = section.substring(firstNewlineIndex + 1).trim(); + sections.set(sectionName, sectionContent); + } + + return sections; +} diff --git a/tests/library.spec.ts b/tests/library.spec.ts new file mode 100644 index 0000000..754df89 --- /dev/null +++ b/tests/library.spec.ts @@ -0,0 +1,28 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import child_process from 'child_process'; +import fs from 'fs/promises'; +import { test, expect } from './fixtures'; + +test('library can be used from CommonJS', { annotation: { type: 'issue', description: 'https://github.com/microsoft/playwright-mcp/issues/456' } }, async ({}, testInfo) => { + const file = testInfo.outputPath('main.cjs'); + await fs.writeFile(file, ` + import('@playwright/mcp') + .then(playwrightMCP => playwrightMCP.createConnection()) + .then(() => console.log('OK')); + `); + expect(child_process.execSync(`node ${file}`, { encoding: 'utf-8' })).toContain('OK'); +}); diff --git a/tests/testserver/cert.pem b/tests/testserver/cert.pem new file mode 100644 index 0000000..3388ed5 --- /dev/null +++ b/tests/testserver/cert.pem @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIFCjCCAvKgAwIBAgIULU/gkDm8IqC7PG8u3RID0AYyP6gwDQYJKoZIhvcNAQEL +BQAwGjEYMBYGA1UEAwwPcGxheXdyaWdodC10ZXN0MB4XDTIzMDgxMDIyNTc1MFoX +DTMzMDgwNzIyNTc1MFowGjEYMBYGA1UEAwwPcGxheXdyaWdodC10ZXN0MIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArbS99qjKcnHr5G0Zc2xhDaOZnjQv +Fbiqxf/nbXt/7WaqryzpVKu7AT1ainBvuPEo7If9DhVnfF//2pGl0gbU31OU4/mr +ymQmczGEyZvOBDsZhtCif54o5OoO0BjhODNT8OWec9RT87n6RkH58MHlOi8xsPxQ +9n5U1CN/h2DyQF3aRKunEFCgtwPKWSjG+J/TAI9i0aSENXPiR8wjTrjg79s8Ehuj +NN8Wk6rKLU3sepG3GIMID5vLsVa2t9xqn562sP95Ee+Xp2YX3z7oYK99QCJdzacw +alhMHob1GCEKjDyxsD2IFRi7Dysiutfyzy3pMo6NALxFrwKVhWX0L4zVFIsI6JlV +dK8dHmDk0MRSqgB9sWXvEfSTXADEe8rncFSFpFz4Z8RNLmn5YSzQJzokNn41DUCP +dZTlTkcGTqvn5NqoY4sOV8rkFbgmTcqyijV/sebPjxCbJNcNmaSWa9FJ5IjRTpzM +38wLmxn+eKGK68n2JB3P7JP6LtsBShQEpXAF3rFfyNsP1bjquvGZVSjV8w/UwPE4 +kV5eq3j3D4913Zfxvzjp6PEmhStG0EQtIXvx/TRoYpaNWypIgZdbkZQp1HUIQL15 +D2Web4nazP3so1FC3ZgbrJZ2ozoadjLMp49NcSFdh+WRyVKuo0DIqR0zaiAzzf2D +G1q7TLKimM3XBMUCAwEAAaNIMEYwCQYDVR0TBAIwADALBgNVHQ8EBAMCBeAwLAYD +VR0RBCUwI4IJbG9jYWxob3N0hwR/AAABhxAAAAAAAAAAAAAAAAAAAAABMA0GCSqG +SIb3DQEBCwUAA4ICAQAvC5M1JFc21WVSLPvE2iVbt4HmirO3EENdDqs+rTYG5VJG +iE5ZuI6h/LjS5ptTfKovXQKaMr3pwp1pLMd/9q+6ZR1Hs9Z2wF6OZan4sb0uT32Y +1KGlj86QMiiSLdrJ/1Z9JHskHYNCep1ZTsUhGk0qqiNv+G3K2y7ZpvrT/xlnYMth +KLTuSVUwM8BBEPrCRLoXuaEy0LnvMvMVepIfP8tnMIL6zqmj3hXMPe4r4OFV/C5o +XX25bC7GyuPWIRYn2OWP92J1CODZD1rGRoDtmvqrQpHdeX9RYcKH0ZLZoIf5L3Hf +pPUtVkw3QGtjvKeG3b9usxaV9Od2Z08vKKk1PRkXFe8gqaeyicK7YVIOMTSuspAf +JeJEHns6Hg61Exbo7GwdX76xlmQ/Z43E9BPHKgLyZ9WuJ0cysqN4aCyvS9yws9to +ki7iMZqJUsmE2o09n9VaEsX6uQANZtLjI9wf+IgJuueDTNrkzQkhU7pbaPMsSG40 +AgGY/y4BR0H8sbhNnhqtZH7RcXV9VCJoPBAe+YiuXRiXyZHWxwBRyBE3e7g4MKHg +hrWtaWUAs7gbavHwjqgU63iVItDSk7t4fCiEyObjK09AaNf2DjjaSGf8YGza4bNy +BjYinYJ6/eX//gp+abqfocFbBP7D9zRDgMIbVmX/Ey6TghKiLkZOdbzcpO4Wgg== +-----END CERTIFICATE----- diff --git a/tests/testserver/index.ts b/tests/testserver/index.ts new file mode 100644 index 0000000..da77cb5 --- /dev/null +++ b/tests/testserver/index.ts @@ -0,0 +1,168 @@ +/** + * Copyright 2017 Google Inc. All rights reserved. + * Modifications copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs'; +import http from 'http'; +import https from 'https'; +import path from 'path'; +import debug from 'debug'; + +const fulfillSymbol = Symbol('fulfil callback'); +const rejectSymbol = Symbol('reject callback'); + +export class TestServer { + private _server: http.Server; + readonly debugServer: any; + private _routes = new Map any>(); + private _csp = new Map(); + private _extraHeaders = new Map(); + private _requestSubscribers = new Map>(); + readonly PORT: number; + readonly PREFIX: string; + readonly CROSS_PROCESS_PREFIX: string; + readonly HELLO_WORLD: string; + + static async create(port: number): Promise { + const server = new TestServer(port); + await new Promise(x => server._server.once('listening', x)); + return server; + } + + static async createHTTPS(port: number): Promise { + const server = new TestServer(port, { + key: await fs.promises.readFile(path.join(path.dirname(__filename), 'key.pem')), + cert: await fs.promises.readFile(path.join(path.dirname(__filename), 'cert.pem')), + passphrase: 'aaaa', + }); + await new Promise(x => server._server.once('listening', x)); + return server; + } + + constructor(port: number, sslOptions?: object) { + if (sslOptions) + this._server = https.createServer(sslOptions, this._onRequest.bind(this)); + else + this._server = http.createServer(this._onRequest.bind(this)); + this._server.listen(port); + this.debugServer = debug('pw:testserver'); + + const cross_origin = '127.0.0.1'; + const same_origin = 'localhost'; + const protocol = sslOptions ? 'https' : 'http'; + this.PORT = port; + this.PREFIX = `${protocol}://${same_origin}:${port}/`; + this.CROSS_PROCESS_PREFIX = `${protocol}://${cross_origin}:${port}/`; + this.HELLO_WORLD = `${this.PREFIX}hello-world`; + } + + setCSP(path: string, csp: string) { + this._csp.set(path, csp); + } + + setExtraHeaders(path: string, object: Record) { + this._extraHeaders.set(path, object); + } + + async stop() { + this.reset(); + await new Promise(x => this._server.close(x)); + } + + route(path: string, handler: (request: http.IncomingMessage, response: http.ServerResponse) => any) { + this._routes.set(path, handler); + } + + setContent(path: string, content: string, mimeType: string) { + this.route(path, (req, res) => { + res.writeHead(200, { 'Content-Type': mimeType }); + res.end(mimeType === 'text/html' ? `${content}` : content); + }); + } + + redirect(from: string, to: string) { + this.route(from, (req, res) => { + const headers = this._extraHeaders.get(req.url!) || {}; + res.writeHead(302, { ...headers, location: to }); + res.end(); + }); + } + + waitForRequest(path: string): Promise { + let promise = this._requestSubscribers.get(path); + if (promise) + return promise; + let fulfill, reject; + promise = new Promise((f, r) => { + fulfill = f; + reject = r; + }); + promise[fulfillSymbol] = fulfill; + promise[rejectSymbol] = reject; + this._requestSubscribers.set(path, promise); + return promise; + } + + reset() { + this._routes.clear(); + this._csp.clear(); + this._extraHeaders.clear(); + this._server.closeAllConnections(); + const error = new Error('Static Server has been reset'); + for (const subscriber of this._requestSubscribers.values()) + subscriber[rejectSymbol].call(null, error); + this._requestSubscribers.clear(); + + this.setContent('/favicon.ico', '', 'image/x-icon'); + + this.setContent('/', ``, 'text/html'); + + this.setContent('/hello-world', ` + Title + Hello, world! + `, 'text/html'); + } + + _onRequest(request: http.IncomingMessage, response: http.ServerResponse) { + request.on('error', error => { + if ((error as any).code === 'ECONNRESET') + response.end(); + else + throw error; + }); + (request as any).postBody = new Promise(resolve => { + const chunks: Buffer[] = []; + request.on('data', chunk => { + chunks.push(chunk); + }); + request.on('end', () => resolve(Buffer.concat(chunks))); + }); + const path = request.url || '/'; + this.debugServer(`request ${request.method} ${path}`); + // Notify request subscriber. + if (this._requestSubscribers.has(path)) { + this._requestSubscribers.get(path)![fulfillSymbol].call(null, request); + this._requestSubscribers.delete(path); + } + const handler = this._routes.get(path); + if (handler) { + handler.call(null, request, response); + } else { + response.writeHead(404); + response.end(); + } + } +} diff --git a/tests/testserver/key.pem b/tests/testserver/key.pem new file mode 100644 index 0000000..28edf51 --- /dev/null +++ b/tests/testserver/key.pem @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCttL32qMpycevk +bRlzbGENo5meNC8VuKrF/+dte3/tZqqvLOlUq7sBPVqKcG+48Sjsh/0OFWd8X//a +kaXSBtTfU5Tj+avKZCZzMYTJm84EOxmG0KJ/nijk6g7QGOE4M1Pw5Z5z1FPzufpG +QfnwweU6LzGw/FD2flTUI3+HYPJAXdpEq6cQUKC3A8pZKMb4n9MAj2LRpIQ1c+JH +zCNOuODv2zwSG6M03xaTqsotTex6kbcYgwgPm8uxVra33Gqfnraw/3kR75enZhff +Puhgr31AIl3NpzBqWEwehvUYIQqMPLGwPYgVGLsPKyK61/LPLekyjo0AvEWvApWF +ZfQvjNUUiwjomVV0rx0eYOTQxFKqAH2xZe8R9JNcAMR7yudwVIWkXPhnxE0uaflh +LNAnOiQ2fjUNQI91lOVORwZOq+fk2qhjiw5XyuQVuCZNyrKKNX+x5s+PEJsk1w2Z +pJZr0UnkiNFOnMzfzAubGf54oYrryfYkHc/sk/ou2wFKFASlcAXesV/I2w/VuOq6 +8ZlVKNXzD9TA8TiRXl6rePcPj3Xdl/G/OOno8SaFK0bQRC0he/H9NGhilo1bKkiB +l1uRlCnUdQhAvXkPZZ5vidrM/eyjUULdmBuslnajOhp2Msynj01xIV2H5ZHJUq6j +QMipHTNqIDPN/YMbWrtMsqKYzdcExQIDAQABAoICAGqXttpdyZ1g+vg5WpzRrNzJ +v8KtExepMmI+Hq24U1BC6AqG7MfgeejQ1XaOeIBsvEgpSsgRqmdQIZjmN3Mibg59 +I6ih1SFlQ5L8mBd/XHSML6Xi8VSOoVmXp29bVRk/pgr1XL6HVN0DCumCIvXyhc+m +lj+dFbGs5DEpd2CDxSRqcz4gd2wzjevAj7MWqsJ2kOyPEHzFD7wdWIXmZuQv3xhQ +2BPkkcon+5qx+07BupOcR1brUU8Cs4QnSgiZYXSB2GnU215+P/mhVJTR7ZcnGRz5 ++cXxCmy3sj4pYs1juS1FMWSM3azUeDVeqvks+vrXmXpEr5H79mbmlwo8/hMPwNDO +07HRZwa8T01aT9EYVm0lIOYjMF/2f6j6cu2apJtjXICOksR2HefRBVXQirOxRHma +9XAYfNkZ/2164ZbgFmJv9khFnegPEuth9tLVdFIeGSmsG0aX9tH63zGT2NROyyLc +QXPqsDl2CxCYPRs2oiGkM9dnfP1wAOp96sq42GIuN7ykfqfRnwAIvvnLKvyCq1vR +pIno3CIX6vnzt+1/Hrmv13b0L6pJPitpXwKWHv9zJKBTpN8HEzP3Qmth2Ef60/7/ +CBo1PVTd1A6zcU7816flg7SCY+Vk+OxVHV3dGBIIqN9SfrQ8BPcOl6FNV5Anbrnv +CpSw+LzH9n5xympDnk0BAoIBAQDjenvDfCnrNVeqx8+sYaYey4/WPVLXOQhREvRY +oOtX9eqlNSi20+Wl+iuXmyj8wdHrDET7rfjCbpDQ7u105yzLw4gy4qIRDKZ1nE45 +YX+tm8mZgBqRnTp0DoGOArqmp3IKXJtUYmpbTz9tOfY7Usb1o1epb4winEB+Pl+8 +mgXOEo8xvWBzKeRA7tE73V64Mwbvbo9Ff2EguhXweQP29yBkEjT4iViayuHUmyPt +hOVSMj2oFQuQGPdhAk7nUXojSGK/Zas/AGpH9CHH9De0h4m08vd3oM4vj0HwzgjU +Co9aRa9SAH7EiaocOTcjDRPxWdZPHhxmrVRIYlF0MNmOAkXJAoIBAQDDfEqu4sNi +pq74VXVatQqhzCILZo+o48bdgEjF7mF99mqPj8rwIDrEoEriDK861kenLc3vWKRY +5wh1iX3S896re9kUMoxx6p4heYTcsOJ9BbkcpT8bJPZx9gBJb4jJENeVf1exf6sG +RhFnulpzReRRaUjX2yAkyUPfc8YcUt+Nalrg+2W0fzeLCUpABCAcj2B1Vv7qRZHj +oEtlCV5Nz+iMhrwIa16g9c8wGt5DZb4PI+VIJ6EYkdsjhgqIF0T/wDq9/habGBPo +mHN+/DX3hCJWN2QgoVGJskHGt0zDMgiEgXfLZ2Grl02vQtq+mW2O2vGVeUd9Y5Ew +RUiY4bSRTrUdAoIBAHxL1wiP9c/By+9TUtScXssA681ioLtdPIAgXUd4VmAvzVEM +ZPzRd/BjbCJg89p4hZ1rjN4Ax6ZmB9dCVpnEH6QPaYJ0d53dTa+CAvQzpDJWp6eq +adobEW+M5ZmVQCwD3rpus6k+RWMzQDMMstDjgDeEU0gP3YCj5FGW/3TsrDNXzMqe +8e67ey9Hzyho43K+3xFBViPhYE8jnw1Q8quliRtlH3CWi8W5CgDD7LPCJBPvw+Tt +6u2H1tQ5EKgwyw4wZVSz1wiLz4cVjMfXWADa9pHbGQFS6pbuLlfIHObQBliLLysd +ficiGcNmOAx8/uKn9gQxLc+k8iLDJkLY1mdUMpECggEAJLl87k37ltTpmg2z9k58 +qNjIrIugAYKJIaOwCD84YYmhi0bgQSxM3hOe/ciUQuFupKGeRpDIj0sX87zYvoDC +HEUwCvNUHzKMco15wFwasJIarJ7+tALFqbMlaqZhdCSN27AIsXfikVMogewoge9n +bUPyQ1sPNtn4vknptfh7tv18BTg1aytbK+ua31vnDHaDEIg/a5OWTMUYZOrVpJii +f4PwX0SMioCjY84oY1EB26ZKtLt9MDh2ir3rzJVSiRl776WEaa6kTtYVHI4VNWLF +cJ0HWnnz74JliQd2jFUh9IK+FqBdYPcTyREuNxBr3KKVMBeQrqW96OubL913JrU6 +oQKCAQEA0yzORUouT0yleWs7RmzBlT9OLD/3cBYJMf/r1F8z8OQjB8fU1jKbO1Cs +q4l+o9FmI+eHkgc3xbEG0hahOFWm/hTTli9vzksxurgdawZELThRkK33uTU9pKla +Okqx3Ru/iMOW2+DQUx9UB+jK+hSAgq4gGqLeJVyaBerIdLQLlvqxrwSxjvvj+wJC +Y66mgRzdCi6VDF1vV0knCrQHK6tRwcPozu/k4zjJzvdbMJnKEy2S7Vh6vO8lEPJm +MQtaHPpmz+F4z14b9unNIiSbHO60Q4O+BwIBCzxApQQbFg63vBLYYwEMRd7hh92s +ZkZVSOEp+sYBf/tmptlKr49nO+dTjQ== +-----END PRIVATE KEY----- diff --git a/tests/testserver/san.cnf b/tests/testserver/san.cnf new file mode 100644 index 0000000..2f4864b --- /dev/null +++ b/tests/testserver/san.cnf @@ -0,0 +1,19 @@ +# openssl req -new -x509 -days 3650 -key key.pem -out cert.pem -config san.cnf -extensions v3_req + +[req] +distinguished_name = req_distinguished_name +req_extensions = v3_req +prompt = no + +[req_distinguished_name] +CN = playwright-test + +[v3_req] +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +subjectAltName = @alt_names + +[alt_names] +DNS.1 = localhost +IP.1 = 127.0.0.1 +IP.2 = ::1 diff --git a/update-readme.js b/update-readme.js new file mode 100644 index 0000000..6d5f557 --- /dev/null +++ b/update-readme.js @@ -0,0 +1,251 @@ +#!/usr/bin/env node +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +// @ts-check + +const fs = require('fs') +const path = require('path') +const { execSync } = require('child_process'); + +const { tools } = require('playwright-core/lib/coreBundle'); + +const capabilities = /** @type {Record} */ ({ + 'core-navigation': 'Core automation', + 'core': 'Core automation', + 'core-tabs': 'Tab management', + 'core-input': 'Core automation', + 'core-install': 'Browser installation', + 'config': 'Configuration', + 'network': 'Network', + 'storage': 'Storage', + 'devtools': 'DevTools', + 'vision': 'Coordinate-based', + 'pdf': 'PDF generation', + 'testing': 'Test assertions', +}); + +const knownCapabilities = new Set(Object.keys(capabilities)); +const unknownCapabilities = [...new Set(tools.browserTools.map(tool => tool.capability))].filter(cap => !knownCapabilities.has(cap)); +if (unknownCapabilities.length) + throw new Error(`Unknown tool capabilities: ${unknownCapabilities.join(', ')}. Please update the capabilities map in ${path.basename(__filename)}.`); + +/** @type {Record} */ +const toolsByCapability = {}; +for (const capability of Object.keys(capabilities)) { + const title = capabilityTitle(capability); + let filteredTools = tools.browserTools.filter(tool => tool.capability === capability && !tool.skillOnly); + filteredTools = (toolsByCapability[title] || []).concat(filteredTools); + toolsByCapability[title] = filteredTools; +} +for (const [, tools] of Object.entries(toolsByCapability)) + tools.sort((a, b) => a.schema.name.localeCompare(b.schema.name)); + +/** + * @param {string} capability + * @returns {string} + */ +function capabilityTitle(capability) { + const title = capabilities[capability]; + return capability.startsWith('core') ? title : `${title} (opt-in via --caps=${capability})`; +} + +/** + * @param {any} tool + * @returns {string[]} + */ +function formatToolForReadme(tool) { + const lines = /** @type {string[]} */ ([]); + lines.push(``); + lines.push(``); + lines.push(`- **${tool.name}**`); + lines.push(` - Title: ${tool.title}`); + lines.push(` - Description: ${tool.description}`); + + const inputSchema = /** @type {any} */ (tool.inputSchema ? tool.inputSchema.toJSONSchema() : {}); + const requiredParams = inputSchema.required || []; + if (inputSchema.properties && Object.keys(inputSchema.properties).length) { + lines.push(` - Parameters:`); + Object.entries(inputSchema.properties).forEach(([name, param]) => { + const optional = !requiredParams.includes(name); + const meta = /** @type {string[]} */ ([]); + if (param.type) + meta.push(param.type); + if (optional) + meta.push('optional'); + lines.push(` - \`${name}\` ${meta.length ? `(${meta.join(', ')})` : ''}: ${param.description}`); + }); + } else { + lines.push(` - Parameters: None`); + } + lines.push(` - Read-only: **${tool.type === 'readOnly'}**`); + lines.push(''); + return lines; +} + +/** + * @param {string} content + * @param {string} startMarker + * @param {string} endMarker + * @param {string[]} generatedLines + * @returns {Promise} + */ +async function updateSection(content, startMarker, endMarker, generatedLines) { + const startMarkerIndex = content.indexOf(startMarker); + const endMarkerIndex = content.indexOf(endMarker); + if (startMarkerIndex === -1 || endMarkerIndex === -1) + throw new Error('Markers for generated section not found in README'); + + return [ + content.slice(0, startMarkerIndex + startMarker.length), + '', + generatedLines.join('\n'), + '', + content.slice(endMarkerIndex), + ].join('\n'); +} + +/** + * @param {string} content + * @returns {Promise} + */ +async function updateTools(content) { + console.log('Loading tool information from compiled modules...'); + + const generatedLines = /** @type {string[]} */ ([]); + for (const [capability, tools] of Object.entries(toolsByCapability)) { + console.log('Updating tools for capability:', capability); + generatedLines.push(`
\n${capability}`); + generatedLines.push(''); + for (const tool of tools) + generatedLines.push(...formatToolForReadme(tool.schema)); + generatedLines.push(`
`); + generatedLines.push(''); + } + + const startMarker = ``; + const endMarker = ``; + return updateSection(content, startMarker, endMarker, generatedLines); +} + +/** + * @param {string} prefix + * @returns {string} + */ +function optionEnvName(prefix) { + if (prefix === 'secrets') + return 'PLAYWRIGHT_MCP_SECRETS_FILE'; + if (prefix === 'cdp-header') + return 'PLAYWRIGHT_MCP_CDP_HEADERS'; + return `PLAYWRIGHT_MCP_` + prefix.toUpperCase().replace(/-/g, '_'); +} + +/** + * @param {string} content + * @returns {Promise} + */ +async function updateOptions(content) { + console.log('Listing options...'); + execSync('node cli.js --help > help.txt'); + const output = fs.readFileSync('help.txt'); + fs.unlinkSync('help.txt'); + const lines = output.toString().split('\n'); + const firstLine = lines.findIndex(line => line.includes('--version')); + lines.splice(0, firstLine + 1); + const lastLine = lines.findIndex(line => line.includes('--help')); + lines.splice(lastLine); + + /** + * @type {{ name: string, value: string }[]} + */ + const options = []; + for (let line of lines) { + if (line.startsWith(' --')) { + const l = line.substring(' --'.length); + const gapIndex = l.indexOf(' '); + const name = l.substring(0, gapIndex).trim(); + const value = l.substring(gapIndex).trim(); + options.push({ name, value }); + } else { + const value = line.trim(); + options[options.length - 1].value += ' ' + value; + } + } + + const table = []; + table.push(`| Option | Description |`); + table.push(`|--------|-------------|`); + for (const option of options) { + const prefix = option.name.split(' ')[0]; + const envName = optionEnvName(prefix); + table.push(`| --${option.name} | ${option.value}
*env* \`${envName}\` |`); + } + + if (process.env.PRINT_ENV) { + const envTable = []; + envTable.push(`| Environment |`); + envTable.push(`|-------------|`); + for (const option of options) { + const prefix = option.name.split(' ')[0]; + const envName = optionEnvName(prefix); + envTable.push(`| \`${envName}\` ${option.value} |`); + } + console.log(envTable.join('\n')); + } + + const startMarker = ``; + const endMarker = ``; + return updateSection(content, startMarker, endMarker, table); +} + +/** + * @param {string} content + * @returns {Promise} + */ +async function updateConfig(content) { + console.log('Updating config schema from config.d.ts...'); + const configPath = path.join(__dirname, 'config.d.ts'); + const configContent = await fs.promises.readFile(configPath, 'utf-8'); + + // Extract the Config type definition + const configTypeMatch = configContent.match(/export type Config = (\{[\s\S]*?\n\});/); + if (!configTypeMatch) + throw new Error('Config type not found in config.d.ts'); + + const configType = configTypeMatch[1]; // Use capture group to get just the object definition + + const startMarker = ``; + const endMarker = ``; + return updateSection(content, startMarker, endMarker, [ + '```typescript', + configType, + '```', + ]); +} + +async function updateReadme() { + const readmePath = path.join(__dirname, 'README.md'); + const readmeContent = await fs.promises.readFile(readmePath, 'utf-8'); + const withTools = await updateTools(readmeContent); + const withOptions = await updateOptions(withTools); + const withConfig = await updateConfig(withOptions); + await fs.promises.writeFile(readmePath, withConfig, 'utf-8'); + console.log('README updated successfully'); +} + +updateReadme().catch(err => { + console.error('Error updating README:', err); + process.exit(1); +});