├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── feature_request.md
├── pull_request_template.md
└── workflows
│ └── docker-build-push.yml
├── .gitignore
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yml
├── icon.svg
├── package-lock.json
├── package.json
├── src
├── config
│ ├── args.ts
│ └── index.ts
├── index.ts
├── server.ts
├── services
│ ├── browserService.ts
│ └── webContentProcessor.ts
├── tools
│ ├── fetchUrl.ts
│ ├── fetchUrls.ts
│ └── index.ts
├── transports
│ ├── http.ts
│ ├── index.ts
│ ├── stdio.ts
│ └── types.ts
├── types
│ └── index.ts
└── utils
│ └── logger.ts
└── tsconfig.json
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: "[BUG] - Short description of the bug"
5 | labels: bug
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Describe the bug**
11 | A clear and concise description of what the bug is.
12 |
13 | **To Reproduce**
14 | Steps to reproduce the behavior:
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Environment (please complete the following information):**
27 | - OS: [e.g. iOS, Windows, Linux]
28 | - Browser [e.g. chrome, safari]
29 | - Version [e.g. 22]
30 |
31 | **Additional context**
32 | Add any other context about the problem here.
33 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: "[FEATURE] - Short description of the feature"
5 | labels: enhancement
6 | assignees: ''
7 |
8 | ---
9 |
10 | **Is your feature request related to a problem? Please describe.**
11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
12 |
13 | **Describe the solution you'd like**
14 | A clear and concise description of what you want to happen.
15 |
16 | **Describe alternatives you've considered**
17 | A clear and concise description of any alternative solutions or features you've considered.
18 |
19 | **Additional context**
20 | Add any other context or screenshots about the feature request here.
21 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Pull Request Checklist
2 |
3 | - [ ] **Linked Issue:** (Link to the issue this PR resolves)
4 | - [ ] **Sufficiently Tested:** (Describe the tests you added or modified to ensure the code works)
5 | - [ ] **Documentation Updated:** (Describe if documentation needs to be updated)
6 | - [ ] **Ready for Review:** (Check this box when all checklist items are complete and PR is ready for review)
7 |
8 | ## Description
9 |
10 | Please briefly describe the changes in your Pull Request, explaining why these changes are needed and how they address the linked issue.
11 |
12 | ## Related Commits
13 |
14 | If this Pull Request is related to specific commits, please link them here.
--------------------------------------------------------------------------------
/.github/workflows/docker-build-push.yml:
--------------------------------------------------------------------------------
1 | name: Build and Push Docker Image
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - master
8 | tags:
9 | - "v*"
10 |
11 | # Allow manual triggering
12 | workflow_dispatch:
13 |
14 | env:
15 | REGISTRY: ghcr.io
16 | IMAGE_NAME: ${{ github.repository }}
17 |
18 | jobs:
19 | build-and-push:
20 | runs-on: ubuntu-latest
21 | permissions:
22 | contents: read
23 | packages: write
24 |
25 | steps:
26 | - name: Checkout repository
27 | uses: actions/checkout@v3
28 |
29 | - name: Set up Docker Buildx
30 | uses: docker/setup-buildx-action@v2
31 |
32 | - name: Log in to GitHub Container Registry
33 | uses: docker/login-action@v2
34 | with:
35 | registry: ${{ env.REGISTRY }}
36 | username: ${{ github.actor }}
37 | password: ${{ secrets.GITHUB_TOKEN }}
38 |
39 | - name: Extract metadata (tags, labels) for Docker
40 | id: meta
41 | uses: docker/metadata-action@v4
42 | with:
43 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
44 | tags: |
45 | type=semver,pattern={{version}}
46 | type=semver,pattern={{major}}.{{minor}}
47 | type=ref,event=branch
48 | type=sha,format=short
49 | type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') || github.ref == format('refs/heads/{0}', 'master') }}
50 |
51 | - name: Build and push Docker image
52 | uses: docker/build-push-action@v4
53 | with:
54 | context: .
55 | push: true
56 | platforms: linux/amd64,linux/arm64
57 | tags: ${{ steps.meta.outputs.tags }}
58 | labels: ${{ steps.meta.outputs.labels }}
59 | cache-from: type=gha
60 | cache-to: type=gha,mode=max
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | build/
3 | *.log
4 | .env*
5 | lightpanda
6 | .history/
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | We as members, contributors, and leaders pledge to make participation in our
6 | community a harassment-free experience for everyone, regardless of age, body
7 | size, visible or invisible disability, ethnicity, sex characteristics, gender
8 | identity and expression, level of experience, education, socio-economic status,
9 | nationality, personal appearance, race, religion, or sexual identity
10 | and orientation.
11 |
12 | We pledge to act and interact in ways that contribute to an open, welcoming,
13 | diverse, inclusive, and healthy community.
14 |
15 | ## Our Standards
16 |
17 | Examples of behavior that contributes to a positive environment for our
18 | community include:
19 |
20 | * Demonstrating empathy and kindness toward other people
21 | * Being respectful of differing opinions, viewpoints, and experiences
22 | * Giving and gracefully accepting constructive feedback
23 | * Accepting responsibility and apologizing to those affected by our mistakes,
24 | and learning from the experience
25 | * Focusing on what is best not just for us as individuals, but for the
26 | overall community
27 |
28 | Examples of unacceptable behavior include:
29 |
30 | * The use of sexualized language or imagery, and sexual attention or
31 | advances of any kind
32 | * Trolling, insulting or derogatory comments, and personal or political attacks
33 | * Public or private harassment
34 | * Publishing others' private information, such as a physical or email
35 | address, without their explicit permission
36 | * Other conduct which could reasonably be considered inappropriate in a
37 | professional setting
38 |
39 | ## Enforcement Responsibilities
40 |
41 | Community leaders are responsible for clarifying and enforcing our standards of
42 | acceptable behavior and will take appropriate and fair corrective action in
43 | response to any behavior that they deem inappropriate, threatening, offensive,
44 | or harmful.
45 |
46 | Community leaders have the right and responsibility to remove, edit, or reject
47 | comments, commits, code, wiki edits, issues, and other contributions that are
48 | not aligned to this Code of Conduct, and will communicate reasons for moderation
49 | decisions when they are reasonable to do so.
50 |
51 | ## Scope
52 |
53 | This Code of Conduct applies within all community spaces, and also applies when
54 | an individual is officially representing the community in public spaces.
55 | Examples of representing our community include using an official e-mail address,
56 | posting via an official social media account, or acting as an appointed
57 | representative at an online or offline event.
58 |
59 | ## Enforcement
60 |
61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
62 | reported to the community leaders responsible for enforcement at JaegerCode@gmail.com.
63 | All complaints will be reviewed and investigated promptly and fairly.
64 |
65 | All community leaders are obligated to respect the privacy and security of the
66 | reporter of any incident.
67 |
68 | ## Enforcement Guidelines
69 |
70 | Community leaders will follow these Community Impact Guidelines in determining
71 | the consequences for any action they deem in violation of this Code of Conduct:
72 |
73 | ### 1. Correction
74 |
75 | **Community Impact**: Use of inappropriate language or other behavior deemed
76 | unprofessional or unwelcome in the community.
77 |
78 | **Consequence**: A private, written warning from community leaders, providing
79 | clarity around the nature of the violation and explanation of why the behavior
80 | was inappropriate. A public apology may be requested.
81 |
82 | ### 2. Warning
83 |
84 | **Community Impact**: A violation through a single incident or series of
85 | incidents.
86 |
87 | **Consequence**: A warning with consequences for continued behavior. No
88 | interaction with the person for a specified period of time. This includes
89 | avoiding interaction in community spaces as well as external channels like
90 | social media. Violating these terms may lead to temporary or permanent ban.
91 |
92 | ### 3. Temporary Ban
93 |
94 | **Community Impact**: A serious violation of community standards, including
95 | sustained inappropriate behavior.
96 |
97 | **Consequence**: A temporary ban from any sort of interaction or public
98 | communication with the community for a specified period of time. No public or
99 | private interaction with the person is allowed during this period. Violating
100 | these terms may lead to permanent ban.
101 |
102 | ### 4. Permanent Ban
103 |
104 | **Community Impact**: Demonstrating a pattern of violation of community
105 | standards, including sustained inappropriate behavior, harassment of an
106 | individual, or aggression toward or disparagement of classes of individuals.
107 |
108 | **Consequence**: A permanent ban from any sort of public interaction within
109 | the community.
110 |
111 | ## Attribution
112 |
113 | This Code of Conduct is adapted from the [Contributor Covenant][homepage],
114 | version 2.1, available at
115 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html](https://www.contributor-covenant.org/version/2/1/code_of_conduct.html).
116 |
117 | [homepage]: https://www.contributor-covenant.org
118 |
119 | For answers to common questions about this code of conduct, see the FAQ at
120 | [https://www.contributor-covenant.org/faq](https://www.contributor-covenant.org/faq).
121 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing to Fetcher MCP
2 |
3 | We welcome contributions to Fetcher MCP! By participating in this project, you agree to abide by our [Code of Conduct](CODE_OF_CONDUCT.md).
4 |
5 | ## How to Contribute
6 |
7 | 1. **Fork the repository** on GitHub.
8 | 2. **Clone your fork** to your local machine.
9 | 3. **Set up your development environment** (see [Development Setup](#development-setup)).
10 | 4. **Create a new branch** for your contribution.
11 | 5. **Make your changes** and commit them with clear, concise commit messages.
12 | 6. **Push your branch** to your fork on GitHub.
13 | 7. **Submit a pull request** to the main repository.
14 |
15 | ## Development Setup
16 |
17 | * Install [Node.js](https://nodejs.org/) and npm.
18 | * Run `npm install` to install dependencies.
19 | * Run `npm run build` to build the project.
20 |
21 | ## Code Style Guide
22 |
23 | * Follow the [JavaScript Standard Style](https://standardjs.com/).
24 | * Write clear, concise, and well-commented code.
25 | * Test your code thoroughly.
26 |
27 | ## Submitting Issues
28 |
29 | * Use the issue templates provided.
30 | * Provide detailed description of the issue, including steps to reproduce it.
31 | * If possible, suggest a solution or workaround.
32 |
33 | ## Submitting Pull Requests
34 |
35 | * Follow the pull request template.
36 | * Ensure your code is well-tested and passes all checks.
37 | * Write clear and concise commit messages.
38 | * Explain the purpose of your pull request and the changes you've made.
39 |
40 | ## Code of Conduct
41 |
42 | Please review and follow our [Code of Conduct](CODE_OF_CONDUCT.md).
43 |
44 | Thank you for your contributions!
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # Build stage
2 | FROM --platform=$BUILDPLATFORM node:22-slim AS builder
3 |
4 | WORKDIR /app
5 |
6 | # Copy dependency files first to leverage caching
7 | COPY package*.json ./
8 |
9 | RUN npm ci
10 |
11 | # Copy source code and configuration files
12 | COPY tsconfig.json ./
13 | COPY src/ ./src/
14 |
15 | # Build the project
16 | RUN npm run build
17 |
18 | # Runtime stage
19 | FROM --platform=$TARGETPLATFORM node:22-slim AS runner
20 |
21 | # Install system dependencies required for runtime
22 | RUN apt-get update && apt-get install -y \
23 | wget \
24 | gnupg \
25 | ca-certificates \
26 | && rm -rf /var/lib/apt/lists/*
27 |
28 | WORKDIR /app
29 |
30 | # Copy only production dependencies
31 | COPY --from=builder /app/build ./build
32 | COPY package*.json ./
33 | RUN npm ci --only=production
34 |
35 | # Install Playwright browsers (ensure headless shell is installed)
36 | RUN npx playwright install --with-deps chromium
37 |
38 | # Expose port
39 | EXPOSE 3000
40 |
41 | # Startup command
42 | CMD ["node", "build/index.js", "--log", "--transport=http", "--host=0.0.0.0", "--port=3000"]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Sat Naing
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | # Fetcher MCP
6 |
7 | MCP server for fetch web page content using Playwright headless browser.
8 |
9 | ## Advantages
10 |
11 | - **JavaScript Support**: Unlike traditional web scrapers, Fetcher MCP uses Playwright to execute JavaScript, making it capable of handling dynamic web content and modern web applications.
12 |
13 | - **Intelligent Content Extraction**: Built-in Readability algorithm automatically extracts the main content from web pages, removing ads, navigation, and other non-essential elements.
14 |
15 | - **Flexible Output Format**: Supports both HTML and Markdown output formats, making it easy to integrate with various downstream applications.
16 |
17 | - **Parallel Processing**: The `fetch_urls` tool enables concurrent fetching of multiple URLs, significantly improving efficiency for batch operations.
18 |
19 | - **Resource Optimization**: Automatically blocks unnecessary resources (images, stylesheets, fonts, media) to reduce bandwidth usage and improve performance.
20 |
21 | - **Robust Error Handling**: Comprehensive error handling and logging ensure reliable operation even when dealing with problematic web pages.
22 |
23 | - **Configurable Parameters**: Fine-grained control over timeouts, content extraction, and output formatting to suit different use cases.
24 |
25 | ## Quick Start
26 |
27 | Run directly with npx:
28 |
29 | ```bash
30 | npx -y fetcher-mcp
31 | ```
32 |
33 | First time setup - install the required browser by running the following command in your terminal:
34 |
35 | ```bash
36 | npx playwright install chromium
37 | ```
38 |
39 | ### HTTP and SSE Transport
40 |
41 | Use the `--transport=http` parameter to start both Streamable HTTP endpoint and SSE endpoint services simultaneously:
42 |
43 | ```bash
44 | npx -y fetcher-mcp --log --transport=http --host=0.0.0.0 --port=3000
45 | ```
46 |
47 | After startup, the server provides the following endpoints:
48 |
49 | - `/mcp` - Streamable HTTP endpoint (modern MCP protocol)
50 | - `/sse` - SSE endpoint (legacy MCP protocol)
51 |
52 | Clients can choose which method to connect based on their needs.
53 |
54 | ### Debug Mode
55 |
56 | Run with the `--debug` option to show the browser window for debugging:
57 |
58 | ```bash
59 | npx -y fetcher-mcp --debug
60 | ```
61 |
62 | ## Configuration MCP
63 |
64 | Configure this MCP server in Claude Desktop:
65 |
66 | On MacOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
67 |
68 | On Windows: `%APPDATA%/Claude/claude_desktop_config.json`
69 |
70 | ```json
71 | {
72 | "mcpServers": {
73 | "fetcher": {
74 | "command": "npx",
75 | "args": ["-y", "fetcher-mcp"]
76 | }
77 | }
78 | }
79 | ```
80 |
81 | ## Docker Deployment
82 |
83 | ### Running with Docker
84 |
85 | ```bash
86 | docker run -p 3000:3000 ghcr.io/jae-jae/fetcher-mcp:latest
87 | ```
88 |
89 | ### Deploying with Docker Compose
90 |
91 | Create a `docker-compose.yml` file:
92 |
93 | ```yaml
94 | version: "3.8"
95 |
96 | services:
97 | fetcher-mcp:
98 | image: ghcr.io/jae-jae/fetcher-mcp:latest
99 | container_name: fetcher-mcp
100 | restart: unless-stopped
101 | ports:
102 | - "3000:3000"
103 | environment:
104 | - NODE_ENV=production
105 | # Using host network mode on Linux hosts can improve browser access efficiency
106 | # network_mode: "host"
107 | volumes:
108 | # For Playwright, may need to share certain system paths
109 | - /tmp:/tmp
110 | # Health check
111 | healthcheck:
112 | test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"]
113 | interval: 30s
114 | timeout: 10s
115 | retries: 3
116 | ```
117 |
118 | Then run:
119 |
120 | ```bash
121 | docker-compose up -d
122 | ```
123 |
124 | ## Features
125 |
126 | - `fetch_url` - Retrieve web page content from a specified URL
127 | - Uses Playwright headless browser to parse JavaScript
128 | - Supports intelligent extraction of main content and conversion to Markdown
129 | - Supports the following parameters:
130 | - `url`: The URL of the web page to fetch (required parameter)
131 | - `timeout`: Page loading timeout in milliseconds, default is 30000 (30 seconds)
132 | - `waitUntil`: Specifies when navigation is considered complete, options: 'load', 'domcontentloaded', 'networkidle', 'commit', default is 'load'
133 | - `extractContent`: Whether to intelligently extract the main content, default is true
134 | - `maxLength`: Maximum length of returned content (in characters), default is no limit
135 | - `returnHtml`: Whether to return HTML content instead of Markdown, default is false
136 | - `waitForNavigation`: Whether to wait for additional navigation after initial page load (useful for sites with anti-bot verification), default is false
137 | - `navigationTimeout`: Maximum time to wait for additional navigation in milliseconds, default is 10000 (10 seconds)
138 | - `disableMedia`: Whether to disable media resources (images, stylesheets, fonts, media), default is true
139 | - `debug`: Whether to enable debug mode (showing browser window), overrides the --debug command line flag if specified
140 |
141 | - `fetch_urls` - Batch retrieve web page content from multiple URLs in parallel
142 | - Uses multi-tab parallel fetching for improved performance
143 | - Returns combined results with clear separation between webpages
144 | - Supports the following parameters:
145 | - `urls`: Array of URLs to fetch (required parameter)
146 | - Other parameters are the same as `fetch_url`
147 |
148 | ## Tips
149 |
150 | ### Handling Special Website Scenarios
151 |
152 | #### Dealing with Anti-Crawler Mechanisms
153 | - **Wait for Complete Loading**: For websites using CAPTCHA, redirects, or other verification mechanisms, include in your prompt:
154 | ```
155 | Please wait for the page to fully load
156 | ```
157 | This will use the `waitForNavigation: true` parameter.
158 |
159 | - **Increase Timeout Duration**: For websites that load slowly:
160 | ```
161 | Please set the page loading timeout to 60 seconds
162 | ```
163 | This adjusts both `timeout` and `navigationTimeout` parameters accordingly.
164 |
165 | #### Content Retrieval Adjustments
166 | - **Preserve Original HTML Structure**: When content extraction might fail:
167 | ```
168 | Please preserve the original HTML content
169 | ```
170 | Sets `extractContent: false` and `returnHtml: true`.
171 |
172 | - **Fetch Complete Page Content**: When extracted content is too limited:
173 | ```
174 | Please fetch the complete webpage content instead of just the main content
175 | ```
176 | Sets `extractContent: false`.
177 |
178 | - **Return Content as HTML**: When HTML format is needed instead of default Markdown:
179 | ```
180 | Please return the content in HTML format
181 | ```
182 | Sets `returnHtml: true`.
183 |
184 | ### Debugging and Authentication
185 |
186 | #### Enabling Debug Mode
187 | - **Dynamic Debug Activation**: To display the browser window during a specific fetch operation:
188 | ```
189 | Please enable debug mode for this fetch operation
190 | ```
191 | This sets `debug: true` even if the server was started without the `--debug` flag.
192 |
193 | #### Using Custom Cookies for Authentication
194 | - **Manual Login**: To login using your own credentials:
195 | ```
196 | Please run in debug mode so I can manually log in to the website
197 | ```
198 | Sets `debug: true` or uses the `--debug` flag, keeping the browser window open for manual login.
199 |
200 | - **Interacting with Debug Browser**: When debug mode is enabled:
201 | 1. The browser window remains open
202 | 2. You can manually log into the website using your credentials
203 | 3. After login is complete, content will be fetched with your authenticated session
204 |
205 | - **Enable Debug for Specific Requests**: Even if the server is already running, you can enable debug mode for a specific request:
206 | ```
207 | Please enable debug mode for this authentication step
208 | ```
209 | Sets `debug: true` for this specific request only, opening the browser window for manual login.
210 |
211 | ## Development
212 |
213 | ### Install Dependencies
214 |
215 | ```bash
216 | npm install
217 | ```
218 |
219 | ### Install Playwright Browser
220 |
221 | Install the browsers needed for Playwright:
222 |
223 | ```bash
224 | npm run install-browser
225 | ```
226 |
227 | ### Build the Server
228 |
229 | ```bash
230 | npm run build
231 | ```
232 |
233 | ## Debugging
234 |
235 | Use MCP Inspector for debugging:
236 |
237 | ```bash
238 | npm run inspector
239 | ```
240 |
241 | You can also enable visible browser mode for debugging:
242 |
243 | ```bash
244 | node build/index.js --debug
245 | ```
246 |
247 | ## Related Projects
248 |
249 | - [g-search-mcp](https://github.com/jae-jae/g-search-mcp): A powerful MCP server for Google search that enables parallel searching with multiple keywords simultaneously. Perfect for batch search operations and data collection.
250 |
251 | ## License
252 |
253 | Licensed under the [MIT License](https://choosealicense.com/licenses/mit/)
254 |
255 | [](https://dartnode.com "Powered by DartNode - Free VPS for Open Source")
256 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 |
3 | services:
4 | fetcher-mcp:
5 | image: ghcr.io/jae-jae/fetcher-mcp:latest
6 | container_name: fetcher-mcp
7 | restart: unless-stopped
8 | ports:
9 | - "3000:3000"
10 | environment:
11 | - NODE_ENV=production
12 | # Using host network mode can improve browser access efficiency
13 | # network_mode: "host" # Use on Linux hosts for better performance
14 | volumes:
15 | # For Playwright, may need to share certain system paths
16 | - /tmp:/tmp
17 | # Health check
18 | healthcheck:
19 | test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"]
20 | interval: 30s
21 | timeout: 10s
22 | retries: 3
23 |
--------------------------------------------------------------------------------
/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fetcher-mcp",
3 | "version": "0.3.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "fetcher-mcp",
9 | "version": "0.3.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "@modelcontextprotocol/sdk": "^1.10.2",
13 | "@mozilla/readability": "^0.5.0",
14 | "express": "^4.18.2",
15 | "jsdom": "^24.0.0",
16 | "playwright": "^1.42.1",
17 | "turndown": "^7.1.2"
18 | },
19 | "bin": {
20 | "fetcher-mcp": "build/index.js"
21 | },
22 | "devDependencies": {
23 | "@types/express": "^4.17.21",
24 | "@types/jsdom": "^21.1.6",
25 | "@types/node": "^20.17.24",
26 | "@types/turndown": "^5.0.4",
27 | "typescript": "^5.3.3"
28 | }
29 | },
30 | "node_modules/@asamuzakjp/css-color": {
31 | "version": "3.1.1",
32 | "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.1.1.tgz",
33 | "integrity": "sha512-hpRD68SV2OMcZCsrbdkccTw5FXjNDLo5OuqSHyHZfwweGsDWZwDJ2+gONyNAbazZclobMirACLw0lk8WVxIqxA==",
34 | "license": "MIT",
35 | "dependencies": {
36 | "@csstools/css-calc": "^2.1.2",
37 | "@csstools/css-color-parser": "^3.0.8",
38 | "@csstools/css-parser-algorithms": "^3.0.4",
39 | "@csstools/css-tokenizer": "^3.0.3",
40 | "lru-cache": "^10.4.3"
41 | }
42 | },
43 | "node_modules/@csstools/color-helpers": {
44 | "version": "5.0.2",
45 | "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
46 | "integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==",
47 | "funding": [
48 | {
49 | "type": "github",
50 | "url": "https://github.com/sponsors/csstools"
51 | },
52 | {
53 | "type": "opencollective",
54 | "url": "https://opencollective.com/csstools"
55 | }
56 | ],
57 | "license": "MIT-0",
58 | "engines": {
59 | "node": ">=18"
60 | }
61 | },
62 | "node_modules/@csstools/css-calc": {
63 | "version": "2.1.2",
64 | "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.2.tgz",
65 | "integrity": "sha512-TklMyb3uBB28b5uQdxjReG4L80NxAqgrECqLZFQbyLekwwlcDDS8r3f07DKqeo8C4926Br0gf/ZDe17Zv4wIuw==",
66 | "funding": [
67 | {
68 | "type": "github",
69 | "url": "https://github.com/sponsors/csstools"
70 | },
71 | {
72 | "type": "opencollective",
73 | "url": "https://opencollective.com/csstools"
74 | }
75 | ],
76 | "license": "MIT",
77 | "engines": {
78 | "node": ">=18"
79 | },
80 | "peerDependencies": {
81 | "@csstools/css-parser-algorithms": "^3.0.4",
82 | "@csstools/css-tokenizer": "^3.0.3"
83 | }
84 | },
85 | "node_modules/@csstools/css-color-parser": {
86 | "version": "3.0.8",
87 | "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.8.tgz",
88 | "integrity": "sha512-pdwotQjCCnRPuNi06jFuP68cykU1f3ZWExLe/8MQ1LOs8Xq+fTkYgd+2V8mWUWMrOn9iS2HftPVaMZDaXzGbhQ==",
89 | "funding": [
90 | {
91 | "type": "github",
92 | "url": "https://github.com/sponsors/csstools"
93 | },
94 | {
95 | "type": "opencollective",
96 | "url": "https://opencollective.com/csstools"
97 | }
98 | ],
99 | "license": "MIT",
100 | "dependencies": {
101 | "@csstools/color-helpers": "^5.0.2",
102 | "@csstools/css-calc": "^2.1.2"
103 | },
104 | "engines": {
105 | "node": ">=18"
106 | },
107 | "peerDependencies": {
108 | "@csstools/css-parser-algorithms": "^3.0.4",
109 | "@csstools/css-tokenizer": "^3.0.3"
110 | }
111 | },
112 | "node_modules/@csstools/css-parser-algorithms": {
113 | "version": "3.0.4",
114 | "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz",
115 | "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==",
116 | "funding": [
117 | {
118 | "type": "github",
119 | "url": "https://github.com/sponsors/csstools"
120 | },
121 | {
122 | "type": "opencollective",
123 | "url": "https://opencollective.com/csstools"
124 | }
125 | ],
126 | "license": "MIT",
127 | "engines": {
128 | "node": ">=18"
129 | },
130 | "peerDependencies": {
131 | "@csstools/css-tokenizer": "^3.0.3"
132 | }
133 | },
134 | "node_modules/@csstools/css-tokenizer": {
135 | "version": "3.0.3",
136 | "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz",
137 | "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==",
138 | "funding": [
139 | {
140 | "type": "github",
141 | "url": "https://github.com/sponsors/csstools"
142 | },
143 | {
144 | "type": "opencollective",
145 | "url": "https://opencollective.com/csstools"
146 | }
147 | ],
148 | "license": "MIT",
149 | "engines": {
150 | "node": ">=18"
151 | }
152 | },
153 | "node_modules/@mixmark-io/domino": {
154 | "version": "2.2.0",
155 | "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
156 | "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
157 | "license": "BSD-2-Clause"
158 | },
159 | "node_modules/@modelcontextprotocol/sdk": {
160 | "version": "1.10.2",
161 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz",
162 | "integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==",
163 | "license": "MIT",
164 | "dependencies": {
165 | "content-type": "^1.0.5",
166 | "cors": "^2.8.5",
167 | "cross-spawn": "^7.0.3",
168 | "eventsource": "^3.0.2",
169 | "express": "^5.0.1",
170 | "express-rate-limit": "^7.5.0",
171 | "pkce-challenge": "^5.0.0",
172 | "raw-body": "^3.0.0",
173 | "zod": "^3.23.8",
174 | "zod-to-json-schema": "^3.24.1"
175 | },
176 | "engines": {
177 | "node": ">=18"
178 | }
179 | },
180 | "node_modules/@modelcontextprotocol/sdk/node_modules/accepts": {
181 | "version": "2.0.0",
182 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
183 | "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
184 | "license": "MIT",
185 | "dependencies": {
186 | "mime-types": "^3.0.0",
187 | "negotiator": "^1.0.0"
188 | },
189 | "engines": {
190 | "node": ">= 0.6"
191 | }
192 | },
193 | "node_modules/@modelcontextprotocol/sdk/node_modules/body-parser": {
194 | "version": "2.2.0",
195 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
196 | "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
197 | "license": "MIT",
198 | "dependencies": {
199 | "bytes": "^3.1.2",
200 | "content-type": "^1.0.5",
201 | "debug": "^4.4.0",
202 | "http-errors": "^2.0.0",
203 | "iconv-lite": "^0.6.3",
204 | "on-finished": "^2.4.1",
205 | "qs": "^6.14.0",
206 | "raw-body": "^3.0.0",
207 | "type-is": "^2.0.0"
208 | },
209 | "engines": {
210 | "node": ">=18"
211 | }
212 | },
213 | "node_modules/@modelcontextprotocol/sdk/node_modules/content-disposition": {
214 | "version": "1.0.0",
215 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
216 | "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
217 | "license": "MIT",
218 | "dependencies": {
219 | "safe-buffer": "5.2.1"
220 | },
221 | "engines": {
222 | "node": ">= 0.6"
223 | }
224 | },
225 | "node_modules/@modelcontextprotocol/sdk/node_modules/cookie-signature": {
226 | "version": "1.2.2",
227 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
228 | "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
229 | "license": "MIT",
230 | "engines": {
231 | "node": ">=6.6.0"
232 | }
233 | },
234 | "node_modules/@modelcontextprotocol/sdk/node_modules/express": {
235 | "version": "5.1.0",
236 | "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
237 | "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
238 | "license": "MIT",
239 | "dependencies": {
240 | "accepts": "^2.0.0",
241 | "body-parser": "^2.2.0",
242 | "content-disposition": "^1.0.0",
243 | "content-type": "^1.0.5",
244 | "cookie": "^0.7.1",
245 | "cookie-signature": "^1.2.1",
246 | "debug": "^4.4.0",
247 | "encodeurl": "^2.0.0",
248 | "escape-html": "^1.0.3",
249 | "etag": "^1.8.1",
250 | "finalhandler": "^2.1.0",
251 | "fresh": "^2.0.0",
252 | "http-errors": "^2.0.0",
253 | "merge-descriptors": "^2.0.0",
254 | "mime-types": "^3.0.0",
255 | "on-finished": "^2.4.1",
256 | "once": "^1.4.0",
257 | "parseurl": "^1.3.3",
258 | "proxy-addr": "^2.0.7",
259 | "qs": "^6.14.0",
260 | "range-parser": "^1.2.1",
261 | "router": "^2.2.0",
262 | "send": "^1.1.0",
263 | "serve-static": "^2.2.0",
264 | "statuses": "^2.0.1",
265 | "type-is": "^2.0.1",
266 | "vary": "^1.1.2"
267 | },
268 | "engines": {
269 | "node": ">= 18"
270 | },
271 | "funding": {
272 | "type": "opencollective",
273 | "url": "https://opencollective.com/express"
274 | }
275 | },
276 | "node_modules/@modelcontextprotocol/sdk/node_modules/finalhandler": {
277 | "version": "2.1.0",
278 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
279 | "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
280 | "license": "MIT",
281 | "dependencies": {
282 | "debug": "^4.4.0",
283 | "encodeurl": "^2.0.0",
284 | "escape-html": "^1.0.3",
285 | "on-finished": "^2.4.1",
286 | "parseurl": "^1.3.3",
287 | "statuses": "^2.0.1"
288 | },
289 | "engines": {
290 | "node": ">= 0.8"
291 | }
292 | },
293 | "node_modules/@modelcontextprotocol/sdk/node_modules/fresh": {
294 | "version": "2.0.0",
295 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
296 | "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
297 | "license": "MIT",
298 | "engines": {
299 | "node": ">= 0.8"
300 | }
301 | },
302 | "node_modules/@modelcontextprotocol/sdk/node_modules/media-typer": {
303 | "version": "1.1.0",
304 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
305 | "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
306 | "license": "MIT",
307 | "engines": {
308 | "node": ">= 0.8"
309 | }
310 | },
311 | "node_modules/@modelcontextprotocol/sdk/node_modules/merge-descriptors": {
312 | "version": "2.0.0",
313 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
314 | "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
315 | "license": "MIT",
316 | "engines": {
317 | "node": ">=18"
318 | },
319 | "funding": {
320 | "url": "https://github.com/sponsors/sindresorhus"
321 | }
322 | },
323 | "node_modules/@modelcontextprotocol/sdk/node_modules/mime-db": {
324 | "version": "1.54.0",
325 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
326 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
327 | "license": "MIT",
328 | "engines": {
329 | "node": ">= 0.6"
330 | }
331 | },
332 | "node_modules/@modelcontextprotocol/sdk/node_modules/mime-types": {
333 | "version": "3.0.1",
334 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
335 | "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
336 | "license": "MIT",
337 | "dependencies": {
338 | "mime-db": "^1.54.0"
339 | },
340 | "engines": {
341 | "node": ">= 0.6"
342 | }
343 | },
344 | "node_modules/@modelcontextprotocol/sdk/node_modules/negotiator": {
345 | "version": "1.0.0",
346 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
347 | "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
348 | "license": "MIT",
349 | "engines": {
350 | "node": ">= 0.6"
351 | }
352 | },
353 | "node_modules/@modelcontextprotocol/sdk/node_modules/qs": {
354 | "version": "6.14.0",
355 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
356 | "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
357 | "license": "BSD-3-Clause",
358 | "dependencies": {
359 | "side-channel": "^1.1.0"
360 | },
361 | "engines": {
362 | "node": ">=0.6"
363 | },
364 | "funding": {
365 | "url": "https://github.com/sponsors/ljharb"
366 | }
367 | },
368 | "node_modules/@modelcontextprotocol/sdk/node_modules/send": {
369 | "version": "1.2.0",
370 | "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
371 | "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
372 | "license": "MIT",
373 | "dependencies": {
374 | "debug": "^4.3.5",
375 | "encodeurl": "^2.0.0",
376 | "escape-html": "^1.0.3",
377 | "etag": "^1.8.1",
378 | "fresh": "^2.0.0",
379 | "http-errors": "^2.0.0",
380 | "mime-types": "^3.0.1",
381 | "ms": "^2.1.3",
382 | "on-finished": "^2.4.1",
383 | "range-parser": "^1.2.1",
384 | "statuses": "^2.0.1"
385 | },
386 | "engines": {
387 | "node": ">= 18"
388 | }
389 | },
390 | "node_modules/@modelcontextprotocol/sdk/node_modules/serve-static": {
391 | "version": "2.2.0",
392 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
393 | "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
394 | "license": "MIT",
395 | "dependencies": {
396 | "encodeurl": "^2.0.0",
397 | "escape-html": "^1.0.3",
398 | "parseurl": "^1.3.3",
399 | "send": "^1.2.0"
400 | },
401 | "engines": {
402 | "node": ">= 18"
403 | }
404 | },
405 | "node_modules/@modelcontextprotocol/sdk/node_modules/type-is": {
406 | "version": "2.0.1",
407 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
408 | "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
409 | "license": "MIT",
410 | "dependencies": {
411 | "content-type": "^1.0.5",
412 | "media-typer": "^1.1.0",
413 | "mime-types": "^3.0.0"
414 | },
415 | "engines": {
416 | "node": ">= 0.6"
417 | }
418 | },
419 | "node_modules/@mozilla/readability": {
420 | "version": "0.5.0",
421 | "resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.5.0.tgz",
422 | "integrity": "sha512-Z+CZ3QaosfFaTqvhQsIktyGrjFjSC0Fa4EMph4mqKnWhmyoGICsV/8QK+8HpXut6zV7zwfWwqDmEjtk1Qf6EgQ==",
423 | "license": "Apache-2.0",
424 | "engines": {
425 | "node": ">=14.0.0"
426 | }
427 | },
428 | "node_modules/@types/body-parser": {
429 | "version": "1.19.5",
430 | "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
431 | "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==",
432 | "dev": true,
433 | "license": "MIT",
434 | "dependencies": {
435 | "@types/connect": "*",
436 | "@types/node": "*"
437 | }
438 | },
439 | "node_modules/@types/connect": {
440 | "version": "3.4.38",
441 | "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz",
442 | "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==",
443 | "dev": true,
444 | "license": "MIT",
445 | "dependencies": {
446 | "@types/node": "*"
447 | }
448 | },
449 | "node_modules/@types/express": {
450 | "version": "4.17.21",
451 | "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz",
452 | "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==",
453 | "dev": true,
454 | "license": "MIT",
455 | "dependencies": {
456 | "@types/body-parser": "*",
457 | "@types/express-serve-static-core": "^4.17.33",
458 | "@types/qs": "*",
459 | "@types/serve-static": "*"
460 | }
461 | },
462 | "node_modules/@types/express-serve-static-core": {
463 | "version": "4.19.6",
464 | "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz",
465 | "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==",
466 | "dev": true,
467 | "license": "MIT",
468 | "dependencies": {
469 | "@types/node": "*",
470 | "@types/qs": "*",
471 | "@types/range-parser": "*",
472 | "@types/send": "*"
473 | }
474 | },
475 | "node_modules/@types/http-errors": {
476 | "version": "2.0.4",
477 | "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz",
478 | "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==",
479 | "dev": true,
480 | "license": "MIT"
481 | },
482 | "node_modules/@types/jsdom": {
483 | "version": "21.1.7",
484 | "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz",
485 | "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==",
486 | "dev": true,
487 | "license": "MIT",
488 | "dependencies": {
489 | "@types/node": "*",
490 | "@types/tough-cookie": "*",
491 | "parse5": "^7.0.0"
492 | }
493 | },
494 | "node_modules/@types/mime": {
495 | "version": "1.3.5",
496 | "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
497 | "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
498 | "dev": true,
499 | "license": "MIT"
500 | },
501 | "node_modules/@types/node": {
502 | "version": "20.17.24",
503 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.24.tgz",
504 | "integrity": "sha512-d7fGCyB96w9BnWQrOsJtpyiSaBcAYYr75bnK6ZRjDbql2cGLj/3GsL5OYmLPNq76l7Gf2q4Rv9J2o6h5CrD9sA==",
505 | "dev": true,
506 | "license": "MIT",
507 | "dependencies": {
508 | "undici-types": "~6.19.2"
509 | }
510 | },
511 | "node_modules/@types/qs": {
512 | "version": "6.9.18",
513 | "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz",
514 | "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==",
515 | "dev": true,
516 | "license": "MIT"
517 | },
518 | "node_modules/@types/range-parser": {
519 | "version": "1.2.7",
520 | "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
521 | "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==",
522 | "dev": true,
523 | "license": "MIT"
524 | },
525 | "node_modules/@types/send": {
526 | "version": "0.17.4",
527 | "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz",
528 | "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==",
529 | "dev": true,
530 | "license": "MIT",
531 | "dependencies": {
532 | "@types/mime": "^1",
533 | "@types/node": "*"
534 | }
535 | },
536 | "node_modules/@types/serve-static": {
537 | "version": "1.15.7",
538 | "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz",
539 | "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==",
540 | "dev": true,
541 | "license": "MIT",
542 | "dependencies": {
543 | "@types/http-errors": "*",
544 | "@types/node": "*",
545 | "@types/send": "*"
546 | }
547 | },
548 | "node_modules/@types/tough-cookie": {
549 | "version": "4.0.5",
550 | "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
551 | "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
552 | "dev": true,
553 | "license": "MIT"
554 | },
555 | "node_modules/@types/turndown": {
556 | "version": "5.0.5",
557 | "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.5.tgz",
558 | "integrity": "sha512-TL2IgGgc7B5j78rIccBtlYAnkuv8nUQqhQc+DSYV5j9Be9XOcm/SKOVRuA47xAVI3680Tk9B1d8flK2GWT2+4w==",
559 | "dev": true,
560 | "license": "MIT"
561 | },
562 | "node_modules/accepts": {
563 | "version": "1.3.8",
564 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
565 | "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
566 | "license": "MIT",
567 | "dependencies": {
568 | "mime-types": "~2.1.34",
569 | "negotiator": "0.6.3"
570 | },
571 | "engines": {
572 | "node": ">= 0.6"
573 | }
574 | },
575 | "node_modules/agent-base": {
576 | "version": "7.1.3",
577 | "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz",
578 | "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==",
579 | "license": "MIT",
580 | "engines": {
581 | "node": ">= 14"
582 | }
583 | },
584 | "node_modules/array-flatten": {
585 | "version": "1.1.1",
586 | "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
587 | "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
588 | "license": "MIT"
589 | },
590 | "node_modules/asynckit": {
591 | "version": "0.4.0",
592 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
593 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
594 | "license": "MIT"
595 | },
596 | "node_modules/body-parser": {
597 | "version": "1.20.3",
598 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
599 | "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
600 | "license": "MIT",
601 | "dependencies": {
602 | "bytes": "3.1.2",
603 | "content-type": "~1.0.5",
604 | "debug": "2.6.9",
605 | "depd": "2.0.0",
606 | "destroy": "1.2.0",
607 | "http-errors": "2.0.0",
608 | "iconv-lite": "0.4.24",
609 | "on-finished": "2.4.1",
610 | "qs": "6.13.0",
611 | "raw-body": "2.5.2",
612 | "type-is": "~1.6.18",
613 | "unpipe": "1.0.0"
614 | },
615 | "engines": {
616 | "node": ">= 0.8",
617 | "npm": "1.2.8000 || >= 1.4.16"
618 | }
619 | },
620 | "node_modules/body-parser/node_modules/debug": {
621 | "version": "2.6.9",
622 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
623 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
624 | "license": "MIT",
625 | "dependencies": {
626 | "ms": "2.0.0"
627 | }
628 | },
629 | "node_modules/body-parser/node_modules/iconv-lite": {
630 | "version": "0.4.24",
631 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
632 | "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
633 | "license": "MIT",
634 | "dependencies": {
635 | "safer-buffer": ">= 2.1.2 < 3"
636 | },
637 | "engines": {
638 | "node": ">=0.10.0"
639 | }
640 | },
641 | "node_modules/body-parser/node_modules/ms": {
642 | "version": "2.0.0",
643 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
644 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
645 | "license": "MIT"
646 | },
647 | "node_modules/body-parser/node_modules/raw-body": {
648 | "version": "2.5.2",
649 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz",
650 | "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==",
651 | "license": "MIT",
652 | "dependencies": {
653 | "bytes": "3.1.2",
654 | "http-errors": "2.0.0",
655 | "iconv-lite": "0.4.24",
656 | "unpipe": "1.0.0"
657 | },
658 | "engines": {
659 | "node": ">= 0.8"
660 | }
661 | },
662 | "node_modules/bytes": {
663 | "version": "3.1.2",
664 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
665 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
666 | "license": "MIT",
667 | "engines": {
668 | "node": ">= 0.8"
669 | }
670 | },
671 | "node_modules/call-bind-apply-helpers": {
672 | "version": "1.0.2",
673 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
674 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
675 | "license": "MIT",
676 | "dependencies": {
677 | "es-errors": "^1.3.0",
678 | "function-bind": "^1.1.2"
679 | },
680 | "engines": {
681 | "node": ">= 0.4"
682 | }
683 | },
684 | "node_modules/call-bound": {
685 | "version": "1.0.4",
686 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
687 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
688 | "license": "MIT",
689 | "dependencies": {
690 | "call-bind-apply-helpers": "^1.0.2",
691 | "get-intrinsic": "^1.3.0"
692 | },
693 | "engines": {
694 | "node": ">= 0.4"
695 | },
696 | "funding": {
697 | "url": "https://github.com/sponsors/ljharb"
698 | }
699 | },
700 | "node_modules/combined-stream": {
701 | "version": "1.0.8",
702 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
703 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
704 | "license": "MIT",
705 | "dependencies": {
706 | "delayed-stream": "~1.0.0"
707 | },
708 | "engines": {
709 | "node": ">= 0.8"
710 | }
711 | },
712 | "node_modules/content-disposition": {
713 | "version": "0.5.4",
714 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
715 | "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
716 | "license": "MIT",
717 | "dependencies": {
718 | "safe-buffer": "5.2.1"
719 | },
720 | "engines": {
721 | "node": ">= 0.6"
722 | }
723 | },
724 | "node_modules/content-type": {
725 | "version": "1.0.5",
726 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
727 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
728 | "license": "MIT",
729 | "engines": {
730 | "node": ">= 0.6"
731 | }
732 | },
733 | "node_modules/cookie": {
734 | "version": "0.7.1",
735 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
736 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
737 | "license": "MIT",
738 | "engines": {
739 | "node": ">= 0.6"
740 | }
741 | },
742 | "node_modules/cookie-signature": {
743 | "version": "1.0.6",
744 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
745 | "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==",
746 | "license": "MIT"
747 | },
748 | "node_modules/cors": {
749 | "version": "2.8.5",
750 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
751 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
752 | "license": "MIT",
753 | "dependencies": {
754 | "object-assign": "^4",
755 | "vary": "^1"
756 | },
757 | "engines": {
758 | "node": ">= 0.10"
759 | }
760 | },
761 | "node_modules/cross-spawn": {
762 | "version": "7.0.6",
763 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
764 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
765 | "license": "MIT",
766 | "dependencies": {
767 | "path-key": "^3.1.0",
768 | "shebang-command": "^2.0.0",
769 | "which": "^2.0.1"
770 | },
771 | "engines": {
772 | "node": ">= 8"
773 | }
774 | },
775 | "node_modules/cssstyle": {
776 | "version": "4.3.0",
777 | "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.3.0.tgz",
778 | "integrity": "sha512-6r0NiY0xizYqfBvWp1G7WXJ06/bZyrk7Dc6PHql82C/pKGUTKu4yAX4Y8JPamb1ob9nBKuxWzCGTRuGwU3yxJQ==",
779 | "license": "MIT",
780 | "dependencies": {
781 | "@asamuzakjp/css-color": "^3.1.1",
782 | "rrweb-cssom": "^0.8.0"
783 | },
784 | "engines": {
785 | "node": ">=18"
786 | }
787 | },
788 | "node_modules/cssstyle/node_modules/rrweb-cssom": {
789 | "version": "0.8.0",
790 | "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
791 | "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
792 | "license": "MIT"
793 | },
794 | "node_modules/data-urls": {
795 | "version": "5.0.0",
796 | "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
797 | "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
798 | "license": "MIT",
799 | "dependencies": {
800 | "whatwg-mimetype": "^4.0.0",
801 | "whatwg-url": "^14.0.0"
802 | },
803 | "engines": {
804 | "node": ">=18"
805 | }
806 | },
807 | "node_modules/debug": {
808 | "version": "4.4.0",
809 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
810 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
811 | "license": "MIT",
812 | "dependencies": {
813 | "ms": "^2.1.3"
814 | },
815 | "engines": {
816 | "node": ">=6.0"
817 | },
818 | "peerDependenciesMeta": {
819 | "supports-color": {
820 | "optional": true
821 | }
822 | }
823 | },
824 | "node_modules/decimal.js": {
825 | "version": "10.5.0",
826 | "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
827 | "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
828 | "license": "MIT"
829 | },
830 | "node_modules/delayed-stream": {
831 | "version": "1.0.0",
832 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
833 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
834 | "license": "MIT",
835 | "engines": {
836 | "node": ">=0.4.0"
837 | }
838 | },
839 | "node_modules/depd": {
840 | "version": "2.0.0",
841 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
842 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
843 | "license": "MIT",
844 | "engines": {
845 | "node": ">= 0.8"
846 | }
847 | },
848 | "node_modules/destroy": {
849 | "version": "1.2.0",
850 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
851 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
852 | "license": "MIT",
853 | "engines": {
854 | "node": ">= 0.8",
855 | "npm": "1.2.8000 || >= 1.4.16"
856 | }
857 | },
858 | "node_modules/dunder-proto": {
859 | "version": "1.0.1",
860 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
861 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
862 | "license": "MIT",
863 | "dependencies": {
864 | "call-bind-apply-helpers": "^1.0.1",
865 | "es-errors": "^1.3.0",
866 | "gopd": "^1.2.0"
867 | },
868 | "engines": {
869 | "node": ">= 0.4"
870 | }
871 | },
872 | "node_modules/ee-first": {
873 | "version": "1.1.1",
874 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
875 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
876 | "license": "MIT"
877 | },
878 | "node_modules/encodeurl": {
879 | "version": "2.0.0",
880 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
881 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
882 | "license": "MIT",
883 | "engines": {
884 | "node": ">= 0.8"
885 | }
886 | },
887 | "node_modules/entities": {
888 | "version": "4.5.0",
889 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
890 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
891 | "license": "BSD-2-Clause",
892 | "engines": {
893 | "node": ">=0.12"
894 | },
895 | "funding": {
896 | "url": "https://github.com/fb55/entities?sponsor=1"
897 | }
898 | },
899 | "node_modules/es-define-property": {
900 | "version": "1.0.1",
901 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
902 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
903 | "license": "MIT",
904 | "engines": {
905 | "node": ">= 0.4"
906 | }
907 | },
908 | "node_modules/es-errors": {
909 | "version": "1.3.0",
910 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
911 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
912 | "license": "MIT",
913 | "engines": {
914 | "node": ">= 0.4"
915 | }
916 | },
917 | "node_modules/es-object-atoms": {
918 | "version": "1.1.1",
919 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
920 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
921 | "license": "MIT",
922 | "dependencies": {
923 | "es-errors": "^1.3.0"
924 | },
925 | "engines": {
926 | "node": ">= 0.4"
927 | }
928 | },
929 | "node_modules/es-set-tostringtag": {
930 | "version": "2.1.0",
931 | "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
932 | "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
933 | "license": "MIT",
934 | "dependencies": {
935 | "es-errors": "^1.3.0",
936 | "get-intrinsic": "^1.2.6",
937 | "has-tostringtag": "^1.0.2",
938 | "hasown": "^2.0.2"
939 | },
940 | "engines": {
941 | "node": ">= 0.4"
942 | }
943 | },
944 | "node_modules/escape-html": {
945 | "version": "1.0.3",
946 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
947 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
948 | "license": "MIT"
949 | },
950 | "node_modules/etag": {
951 | "version": "1.8.1",
952 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
953 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
954 | "license": "MIT",
955 | "engines": {
956 | "node": ">= 0.6"
957 | }
958 | },
959 | "node_modules/eventsource": {
960 | "version": "3.0.6",
961 | "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.6.tgz",
962 | "integrity": "sha512-l19WpE2m9hSuyP06+FbuUUf1G+R0SFLrtQfbRb9PRr+oimOfxQhgGCbVaXg5IvZyyTThJsxh6L/srkMiCeBPDA==",
963 | "license": "MIT",
964 | "dependencies": {
965 | "eventsource-parser": "^3.0.1"
966 | },
967 | "engines": {
968 | "node": ">=18.0.0"
969 | }
970 | },
971 | "node_modules/eventsource-parser": {
972 | "version": "3.0.1",
973 | "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.1.tgz",
974 | "integrity": "sha512-VARTJ9CYeuQYb0pZEPbzi740OWFgpHe7AYJ2WFZVnUDUQp5Dk2yJUgF36YsZ81cOyxT0QxmXD2EQpapAouzWVA==",
975 | "license": "MIT",
976 | "engines": {
977 | "node": ">=18.0.0"
978 | }
979 | },
980 | "node_modules/express": {
981 | "version": "4.21.2",
982 | "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
983 | "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
984 | "license": "MIT",
985 | "dependencies": {
986 | "accepts": "~1.3.8",
987 | "array-flatten": "1.1.1",
988 | "body-parser": "1.20.3",
989 | "content-disposition": "0.5.4",
990 | "content-type": "~1.0.4",
991 | "cookie": "0.7.1",
992 | "cookie-signature": "1.0.6",
993 | "debug": "2.6.9",
994 | "depd": "2.0.0",
995 | "encodeurl": "~2.0.0",
996 | "escape-html": "~1.0.3",
997 | "etag": "~1.8.1",
998 | "finalhandler": "1.3.1",
999 | "fresh": "0.5.2",
1000 | "http-errors": "2.0.0",
1001 | "merge-descriptors": "1.0.3",
1002 | "methods": "~1.1.2",
1003 | "on-finished": "2.4.1",
1004 | "parseurl": "~1.3.3",
1005 | "path-to-regexp": "0.1.12",
1006 | "proxy-addr": "~2.0.7",
1007 | "qs": "6.13.0",
1008 | "range-parser": "~1.2.1",
1009 | "safe-buffer": "5.2.1",
1010 | "send": "0.19.0",
1011 | "serve-static": "1.16.2",
1012 | "setprototypeof": "1.2.0",
1013 | "statuses": "2.0.1",
1014 | "type-is": "~1.6.18",
1015 | "utils-merge": "1.0.1",
1016 | "vary": "~1.1.2"
1017 | },
1018 | "engines": {
1019 | "node": ">= 0.10.0"
1020 | },
1021 | "funding": {
1022 | "type": "opencollective",
1023 | "url": "https://opencollective.com/express"
1024 | }
1025 | },
1026 | "node_modules/express-rate-limit": {
1027 | "version": "7.5.0",
1028 | "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz",
1029 | "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==",
1030 | "license": "MIT",
1031 | "engines": {
1032 | "node": ">= 16"
1033 | },
1034 | "funding": {
1035 | "url": "https://github.com/sponsors/express-rate-limit"
1036 | },
1037 | "peerDependencies": {
1038 | "express": "^4.11 || 5 || ^5.0.0-beta.1"
1039 | }
1040 | },
1041 | "node_modules/express/node_modules/debug": {
1042 | "version": "2.6.9",
1043 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
1044 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
1045 | "license": "MIT",
1046 | "dependencies": {
1047 | "ms": "2.0.0"
1048 | }
1049 | },
1050 | "node_modules/express/node_modules/ms": {
1051 | "version": "2.0.0",
1052 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1053 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
1054 | "license": "MIT"
1055 | },
1056 | "node_modules/finalhandler": {
1057 | "version": "1.3.1",
1058 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
1059 | "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
1060 | "license": "MIT",
1061 | "dependencies": {
1062 | "debug": "2.6.9",
1063 | "encodeurl": "~2.0.0",
1064 | "escape-html": "~1.0.3",
1065 | "on-finished": "2.4.1",
1066 | "parseurl": "~1.3.3",
1067 | "statuses": "2.0.1",
1068 | "unpipe": "~1.0.0"
1069 | },
1070 | "engines": {
1071 | "node": ">= 0.8"
1072 | }
1073 | },
1074 | "node_modules/finalhandler/node_modules/debug": {
1075 | "version": "2.6.9",
1076 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
1077 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
1078 | "license": "MIT",
1079 | "dependencies": {
1080 | "ms": "2.0.0"
1081 | }
1082 | },
1083 | "node_modules/finalhandler/node_modules/ms": {
1084 | "version": "2.0.0",
1085 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1086 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
1087 | "license": "MIT"
1088 | },
1089 | "node_modules/form-data": {
1090 | "version": "4.0.2",
1091 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
1092 | "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
1093 | "license": "MIT",
1094 | "dependencies": {
1095 | "asynckit": "^0.4.0",
1096 | "combined-stream": "^1.0.8",
1097 | "es-set-tostringtag": "^2.1.0",
1098 | "mime-types": "^2.1.12"
1099 | },
1100 | "engines": {
1101 | "node": ">= 6"
1102 | }
1103 | },
1104 | "node_modules/forwarded": {
1105 | "version": "0.2.0",
1106 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
1107 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
1108 | "license": "MIT",
1109 | "engines": {
1110 | "node": ">= 0.6"
1111 | }
1112 | },
1113 | "node_modules/fresh": {
1114 | "version": "0.5.2",
1115 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
1116 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
1117 | "license": "MIT",
1118 | "engines": {
1119 | "node": ">= 0.6"
1120 | }
1121 | },
1122 | "node_modules/fsevents": {
1123 | "version": "2.3.2",
1124 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
1125 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
1126 | "hasInstallScript": true,
1127 | "license": "MIT",
1128 | "optional": true,
1129 | "os": [
1130 | "darwin"
1131 | ],
1132 | "engines": {
1133 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
1134 | }
1135 | },
1136 | "node_modules/function-bind": {
1137 | "version": "1.1.2",
1138 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
1139 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
1140 | "license": "MIT",
1141 | "funding": {
1142 | "url": "https://github.com/sponsors/ljharb"
1143 | }
1144 | },
1145 | "node_modules/get-intrinsic": {
1146 | "version": "1.3.0",
1147 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
1148 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
1149 | "license": "MIT",
1150 | "dependencies": {
1151 | "call-bind-apply-helpers": "^1.0.2",
1152 | "es-define-property": "^1.0.1",
1153 | "es-errors": "^1.3.0",
1154 | "es-object-atoms": "^1.1.1",
1155 | "function-bind": "^1.1.2",
1156 | "get-proto": "^1.0.1",
1157 | "gopd": "^1.2.0",
1158 | "has-symbols": "^1.1.0",
1159 | "hasown": "^2.0.2",
1160 | "math-intrinsics": "^1.1.0"
1161 | },
1162 | "engines": {
1163 | "node": ">= 0.4"
1164 | },
1165 | "funding": {
1166 | "url": "https://github.com/sponsors/ljharb"
1167 | }
1168 | },
1169 | "node_modules/get-proto": {
1170 | "version": "1.0.1",
1171 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
1172 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
1173 | "license": "MIT",
1174 | "dependencies": {
1175 | "dunder-proto": "^1.0.1",
1176 | "es-object-atoms": "^1.0.0"
1177 | },
1178 | "engines": {
1179 | "node": ">= 0.4"
1180 | }
1181 | },
1182 | "node_modules/gopd": {
1183 | "version": "1.2.0",
1184 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
1185 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
1186 | "license": "MIT",
1187 | "engines": {
1188 | "node": ">= 0.4"
1189 | },
1190 | "funding": {
1191 | "url": "https://github.com/sponsors/ljharb"
1192 | }
1193 | },
1194 | "node_modules/has-symbols": {
1195 | "version": "1.1.0",
1196 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
1197 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
1198 | "license": "MIT",
1199 | "engines": {
1200 | "node": ">= 0.4"
1201 | },
1202 | "funding": {
1203 | "url": "https://github.com/sponsors/ljharb"
1204 | }
1205 | },
1206 | "node_modules/has-tostringtag": {
1207 | "version": "1.0.2",
1208 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
1209 | "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
1210 | "license": "MIT",
1211 | "dependencies": {
1212 | "has-symbols": "^1.0.3"
1213 | },
1214 | "engines": {
1215 | "node": ">= 0.4"
1216 | },
1217 | "funding": {
1218 | "url": "https://github.com/sponsors/ljharb"
1219 | }
1220 | },
1221 | "node_modules/hasown": {
1222 | "version": "2.0.2",
1223 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
1224 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1225 | "license": "MIT",
1226 | "dependencies": {
1227 | "function-bind": "^1.1.2"
1228 | },
1229 | "engines": {
1230 | "node": ">= 0.4"
1231 | }
1232 | },
1233 | "node_modules/html-encoding-sniffer": {
1234 | "version": "4.0.0",
1235 | "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
1236 | "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
1237 | "license": "MIT",
1238 | "dependencies": {
1239 | "whatwg-encoding": "^3.1.1"
1240 | },
1241 | "engines": {
1242 | "node": ">=18"
1243 | }
1244 | },
1245 | "node_modules/http-errors": {
1246 | "version": "2.0.0",
1247 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
1248 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==",
1249 | "license": "MIT",
1250 | "dependencies": {
1251 | "depd": "2.0.0",
1252 | "inherits": "2.0.4",
1253 | "setprototypeof": "1.2.0",
1254 | "statuses": "2.0.1",
1255 | "toidentifier": "1.0.1"
1256 | },
1257 | "engines": {
1258 | "node": ">= 0.8"
1259 | }
1260 | },
1261 | "node_modules/http-proxy-agent": {
1262 | "version": "7.0.2",
1263 | "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
1264 | "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
1265 | "license": "MIT",
1266 | "dependencies": {
1267 | "agent-base": "^7.1.0",
1268 | "debug": "^4.3.4"
1269 | },
1270 | "engines": {
1271 | "node": ">= 14"
1272 | }
1273 | },
1274 | "node_modules/https-proxy-agent": {
1275 | "version": "7.0.6",
1276 | "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
1277 | "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
1278 | "license": "MIT",
1279 | "dependencies": {
1280 | "agent-base": "^7.1.2",
1281 | "debug": "4"
1282 | },
1283 | "engines": {
1284 | "node": ">= 14"
1285 | }
1286 | },
1287 | "node_modules/iconv-lite": {
1288 | "version": "0.6.3",
1289 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
1290 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
1291 | "license": "MIT",
1292 | "dependencies": {
1293 | "safer-buffer": ">= 2.1.2 < 3.0.0"
1294 | },
1295 | "engines": {
1296 | "node": ">=0.10.0"
1297 | }
1298 | },
1299 | "node_modules/inherits": {
1300 | "version": "2.0.4",
1301 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
1302 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
1303 | "license": "ISC"
1304 | },
1305 | "node_modules/ipaddr.js": {
1306 | "version": "1.9.1",
1307 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
1308 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
1309 | "license": "MIT",
1310 | "engines": {
1311 | "node": ">= 0.10"
1312 | }
1313 | },
1314 | "node_modules/is-potential-custom-element-name": {
1315 | "version": "1.0.1",
1316 | "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
1317 | "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
1318 | "license": "MIT"
1319 | },
1320 | "node_modules/is-promise": {
1321 | "version": "4.0.0",
1322 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
1323 | "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
1324 | "license": "MIT"
1325 | },
1326 | "node_modules/isexe": {
1327 | "version": "2.0.0",
1328 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
1329 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
1330 | "license": "ISC"
1331 | },
1332 | "node_modules/jsdom": {
1333 | "version": "24.1.3",
1334 | "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz",
1335 | "integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==",
1336 | "license": "MIT",
1337 | "dependencies": {
1338 | "cssstyle": "^4.0.1",
1339 | "data-urls": "^5.0.0",
1340 | "decimal.js": "^10.4.3",
1341 | "form-data": "^4.0.0",
1342 | "html-encoding-sniffer": "^4.0.0",
1343 | "http-proxy-agent": "^7.0.2",
1344 | "https-proxy-agent": "^7.0.5",
1345 | "is-potential-custom-element-name": "^1.0.1",
1346 | "nwsapi": "^2.2.12",
1347 | "parse5": "^7.1.2",
1348 | "rrweb-cssom": "^0.7.1",
1349 | "saxes": "^6.0.0",
1350 | "symbol-tree": "^3.2.4",
1351 | "tough-cookie": "^4.1.4",
1352 | "w3c-xmlserializer": "^5.0.0",
1353 | "webidl-conversions": "^7.0.0",
1354 | "whatwg-encoding": "^3.1.1",
1355 | "whatwg-mimetype": "^4.0.0",
1356 | "whatwg-url": "^14.0.0",
1357 | "ws": "^8.18.0",
1358 | "xml-name-validator": "^5.0.0"
1359 | },
1360 | "engines": {
1361 | "node": ">=18"
1362 | },
1363 | "peerDependencies": {
1364 | "canvas": "^2.11.2"
1365 | },
1366 | "peerDependenciesMeta": {
1367 | "canvas": {
1368 | "optional": true
1369 | }
1370 | }
1371 | },
1372 | "node_modules/lru-cache": {
1373 | "version": "10.4.3",
1374 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
1375 | "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
1376 | "license": "ISC"
1377 | },
1378 | "node_modules/math-intrinsics": {
1379 | "version": "1.1.0",
1380 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
1381 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
1382 | "license": "MIT",
1383 | "engines": {
1384 | "node": ">= 0.4"
1385 | }
1386 | },
1387 | "node_modules/media-typer": {
1388 | "version": "0.3.0",
1389 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
1390 | "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
1391 | "license": "MIT",
1392 | "engines": {
1393 | "node": ">= 0.6"
1394 | }
1395 | },
1396 | "node_modules/merge-descriptors": {
1397 | "version": "1.0.3",
1398 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
1399 | "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
1400 | "license": "MIT",
1401 | "funding": {
1402 | "url": "https://github.com/sponsors/sindresorhus"
1403 | }
1404 | },
1405 | "node_modules/methods": {
1406 | "version": "1.1.2",
1407 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
1408 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
1409 | "license": "MIT",
1410 | "engines": {
1411 | "node": ">= 0.6"
1412 | }
1413 | },
1414 | "node_modules/mime": {
1415 | "version": "1.6.0",
1416 | "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
1417 | "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
1418 | "license": "MIT",
1419 | "bin": {
1420 | "mime": "cli.js"
1421 | },
1422 | "engines": {
1423 | "node": ">=4"
1424 | }
1425 | },
1426 | "node_modules/mime-db": {
1427 | "version": "1.52.0",
1428 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
1429 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
1430 | "license": "MIT",
1431 | "engines": {
1432 | "node": ">= 0.6"
1433 | }
1434 | },
1435 | "node_modules/mime-types": {
1436 | "version": "2.1.35",
1437 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
1438 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
1439 | "license": "MIT",
1440 | "dependencies": {
1441 | "mime-db": "1.52.0"
1442 | },
1443 | "engines": {
1444 | "node": ">= 0.6"
1445 | }
1446 | },
1447 | "node_modules/ms": {
1448 | "version": "2.1.3",
1449 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
1450 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
1451 | "license": "MIT"
1452 | },
1453 | "node_modules/negotiator": {
1454 | "version": "0.6.3",
1455 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
1456 | "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
1457 | "license": "MIT",
1458 | "engines": {
1459 | "node": ">= 0.6"
1460 | }
1461 | },
1462 | "node_modules/nwsapi": {
1463 | "version": "2.2.19",
1464 | "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.19.tgz",
1465 | "integrity": "sha512-94bcyI3RsqiZufXjkr3ltkI86iEl+I7uiHVDtcq9wJUTwYQJ5odHDeSzkkrRzi80jJ8MaeZgqKjH1bAWAFw9bA==",
1466 | "license": "MIT"
1467 | },
1468 | "node_modules/object-assign": {
1469 | "version": "4.1.1",
1470 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
1471 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
1472 | "license": "MIT",
1473 | "engines": {
1474 | "node": ">=0.10.0"
1475 | }
1476 | },
1477 | "node_modules/object-inspect": {
1478 | "version": "1.13.4",
1479 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
1480 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
1481 | "license": "MIT",
1482 | "engines": {
1483 | "node": ">= 0.4"
1484 | },
1485 | "funding": {
1486 | "url": "https://github.com/sponsors/ljharb"
1487 | }
1488 | },
1489 | "node_modules/on-finished": {
1490 | "version": "2.4.1",
1491 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
1492 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
1493 | "license": "MIT",
1494 | "dependencies": {
1495 | "ee-first": "1.1.1"
1496 | },
1497 | "engines": {
1498 | "node": ">= 0.8"
1499 | }
1500 | },
1501 | "node_modules/once": {
1502 | "version": "1.4.0",
1503 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
1504 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
1505 | "license": "ISC",
1506 | "dependencies": {
1507 | "wrappy": "1"
1508 | }
1509 | },
1510 | "node_modules/parse5": {
1511 | "version": "7.2.1",
1512 | "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz",
1513 | "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==",
1514 | "license": "MIT",
1515 | "dependencies": {
1516 | "entities": "^4.5.0"
1517 | },
1518 | "funding": {
1519 | "url": "https://github.com/inikulin/parse5?sponsor=1"
1520 | }
1521 | },
1522 | "node_modules/parseurl": {
1523 | "version": "1.3.3",
1524 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
1525 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
1526 | "license": "MIT",
1527 | "engines": {
1528 | "node": ">= 0.8"
1529 | }
1530 | },
1531 | "node_modules/path-key": {
1532 | "version": "3.1.1",
1533 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
1534 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
1535 | "license": "MIT",
1536 | "engines": {
1537 | "node": ">=8"
1538 | }
1539 | },
1540 | "node_modules/path-to-regexp": {
1541 | "version": "0.1.12",
1542 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
1543 | "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
1544 | "license": "MIT"
1545 | },
1546 | "node_modules/pkce-challenge": {
1547 | "version": "5.0.0",
1548 | "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.0.tgz",
1549 | "integrity": "sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==",
1550 | "license": "MIT",
1551 | "engines": {
1552 | "node": ">=16.20.0"
1553 | }
1554 | },
1555 | "node_modules/playwright": {
1556 | "version": "1.51.1",
1557 | "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz",
1558 | "integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==",
1559 | "license": "Apache-2.0",
1560 | "dependencies": {
1561 | "playwright-core": "1.51.1"
1562 | },
1563 | "bin": {
1564 | "playwright": "cli.js"
1565 | },
1566 | "engines": {
1567 | "node": ">=18"
1568 | },
1569 | "optionalDependencies": {
1570 | "fsevents": "2.3.2"
1571 | }
1572 | },
1573 | "node_modules/playwright-core": {
1574 | "version": "1.51.1",
1575 | "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz",
1576 | "integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==",
1577 | "license": "Apache-2.0",
1578 | "bin": {
1579 | "playwright-core": "cli.js"
1580 | },
1581 | "engines": {
1582 | "node": ">=18"
1583 | }
1584 | },
1585 | "node_modules/proxy-addr": {
1586 | "version": "2.0.7",
1587 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
1588 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
1589 | "license": "MIT",
1590 | "dependencies": {
1591 | "forwarded": "0.2.0",
1592 | "ipaddr.js": "1.9.1"
1593 | },
1594 | "engines": {
1595 | "node": ">= 0.10"
1596 | }
1597 | },
1598 | "node_modules/psl": {
1599 | "version": "1.15.0",
1600 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
1601 | "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
1602 | "license": "MIT",
1603 | "dependencies": {
1604 | "punycode": "^2.3.1"
1605 | },
1606 | "funding": {
1607 | "url": "https://github.com/sponsors/lupomontero"
1608 | }
1609 | },
1610 | "node_modules/punycode": {
1611 | "version": "2.3.1",
1612 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
1613 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
1614 | "license": "MIT",
1615 | "engines": {
1616 | "node": ">=6"
1617 | }
1618 | },
1619 | "node_modules/qs": {
1620 | "version": "6.13.0",
1621 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
1622 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
1623 | "license": "BSD-3-Clause",
1624 | "dependencies": {
1625 | "side-channel": "^1.0.6"
1626 | },
1627 | "engines": {
1628 | "node": ">=0.6"
1629 | },
1630 | "funding": {
1631 | "url": "https://github.com/sponsors/ljharb"
1632 | }
1633 | },
1634 | "node_modules/querystringify": {
1635 | "version": "2.2.0",
1636 | "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
1637 | "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
1638 | "license": "MIT"
1639 | },
1640 | "node_modules/range-parser": {
1641 | "version": "1.2.1",
1642 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
1643 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
1644 | "license": "MIT",
1645 | "engines": {
1646 | "node": ">= 0.6"
1647 | }
1648 | },
1649 | "node_modules/raw-body": {
1650 | "version": "3.0.0",
1651 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz",
1652 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==",
1653 | "license": "MIT",
1654 | "dependencies": {
1655 | "bytes": "3.1.2",
1656 | "http-errors": "2.0.0",
1657 | "iconv-lite": "0.6.3",
1658 | "unpipe": "1.0.0"
1659 | },
1660 | "engines": {
1661 | "node": ">= 0.8"
1662 | }
1663 | },
1664 | "node_modules/requires-port": {
1665 | "version": "1.0.0",
1666 | "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
1667 | "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
1668 | "license": "MIT"
1669 | },
1670 | "node_modules/router": {
1671 | "version": "2.2.0",
1672 | "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
1673 | "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
1674 | "license": "MIT",
1675 | "dependencies": {
1676 | "debug": "^4.4.0",
1677 | "depd": "^2.0.0",
1678 | "is-promise": "^4.0.0",
1679 | "parseurl": "^1.3.3",
1680 | "path-to-regexp": "^8.0.0"
1681 | },
1682 | "engines": {
1683 | "node": ">= 18"
1684 | }
1685 | },
1686 | "node_modules/router/node_modules/path-to-regexp": {
1687 | "version": "8.2.0",
1688 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
1689 | "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
1690 | "license": "MIT",
1691 | "engines": {
1692 | "node": ">=16"
1693 | }
1694 | },
1695 | "node_modules/rrweb-cssom": {
1696 | "version": "0.7.1",
1697 | "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
1698 | "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
1699 | "license": "MIT"
1700 | },
1701 | "node_modules/safe-buffer": {
1702 | "version": "5.2.1",
1703 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1704 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1705 | "funding": [
1706 | {
1707 | "type": "github",
1708 | "url": "https://github.com/sponsors/feross"
1709 | },
1710 | {
1711 | "type": "patreon",
1712 | "url": "https://www.patreon.com/feross"
1713 | },
1714 | {
1715 | "type": "consulting",
1716 | "url": "https://feross.org/support"
1717 | }
1718 | ],
1719 | "license": "MIT"
1720 | },
1721 | "node_modules/safer-buffer": {
1722 | "version": "2.1.2",
1723 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
1724 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
1725 | "license": "MIT"
1726 | },
1727 | "node_modules/saxes": {
1728 | "version": "6.0.0",
1729 | "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
1730 | "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
1731 | "license": "ISC",
1732 | "dependencies": {
1733 | "xmlchars": "^2.2.0"
1734 | },
1735 | "engines": {
1736 | "node": ">=v12.22.7"
1737 | }
1738 | },
1739 | "node_modules/send": {
1740 | "version": "0.19.0",
1741 | "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
1742 | "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
1743 | "license": "MIT",
1744 | "dependencies": {
1745 | "debug": "2.6.9",
1746 | "depd": "2.0.0",
1747 | "destroy": "1.2.0",
1748 | "encodeurl": "~1.0.2",
1749 | "escape-html": "~1.0.3",
1750 | "etag": "~1.8.1",
1751 | "fresh": "0.5.2",
1752 | "http-errors": "2.0.0",
1753 | "mime": "1.6.0",
1754 | "ms": "2.1.3",
1755 | "on-finished": "2.4.1",
1756 | "range-parser": "~1.2.1",
1757 | "statuses": "2.0.1"
1758 | },
1759 | "engines": {
1760 | "node": ">= 0.8.0"
1761 | }
1762 | },
1763 | "node_modules/send/node_modules/debug": {
1764 | "version": "2.6.9",
1765 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
1766 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
1767 | "license": "MIT",
1768 | "dependencies": {
1769 | "ms": "2.0.0"
1770 | }
1771 | },
1772 | "node_modules/send/node_modules/debug/node_modules/ms": {
1773 | "version": "2.0.0",
1774 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
1775 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
1776 | "license": "MIT"
1777 | },
1778 | "node_modules/send/node_modules/encodeurl": {
1779 | "version": "1.0.2",
1780 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
1781 | "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==",
1782 | "license": "MIT",
1783 | "engines": {
1784 | "node": ">= 0.8"
1785 | }
1786 | },
1787 | "node_modules/serve-static": {
1788 | "version": "1.16.2",
1789 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
1790 | "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
1791 | "license": "MIT",
1792 | "dependencies": {
1793 | "encodeurl": "~2.0.0",
1794 | "escape-html": "~1.0.3",
1795 | "parseurl": "~1.3.3",
1796 | "send": "0.19.0"
1797 | },
1798 | "engines": {
1799 | "node": ">= 0.8.0"
1800 | }
1801 | },
1802 | "node_modules/setprototypeof": {
1803 | "version": "1.2.0",
1804 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
1805 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
1806 | "license": "ISC"
1807 | },
1808 | "node_modules/shebang-command": {
1809 | "version": "2.0.0",
1810 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
1811 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
1812 | "license": "MIT",
1813 | "dependencies": {
1814 | "shebang-regex": "^3.0.0"
1815 | },
1816 | "engines": {
1817 | "node": ">=8"
1818 | }
1819 | },
1820 | "node_modules/shebang-regex": {
1821 | "version": "3.0.0",
1822 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
1823 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
1824 | "license": "MIT",
1825 | "engines": {
1826 | "node": ">=8"
1827 | }
1828 | },
1829 | "node_modules/side-channel": {
1830 | "version": "1.1.0",
1831 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
1832 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
1833 | "license": "MIT",
1834 | "dependencies": {
1835 | "es-errors": "^1.3.0",
1836 | "object-inspect": "^1.13.3",
1837 | "side-channel-list": "^1.0.0",
1838 | "side-channel-map": "^1.0.1",
1839 | "side-channel-weakmap": "^1.0.2"
1840 | },
1841 | "engines": {
1842 | "node": ">= 0.4"
1843 | },
1844 | "funding": {
1845 | "url": "https://github.com/sponsors/ljharb"
1846 | }
1847 | },
1848 | "node_modules/side-channel-list": {
1849 | "version": "1.0.0",
1850 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
1851 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
1852 | "license": "MIT",
1853 | "dependencies": {
1854 | "es-errors": "^1.3.0",
1855 | "object-inspect": "^1.13.3"
1856 | },
1857 | "engines": {
1858 | "node": ">= 0.4"
1859 | },
1860 | "funding": {
1861 | "url": "https://github.com/sponsors/ljharb"
1862 | }
1863 | },
1864 | "node_modules/side-channel-map": {
1865 | "version": "1.0.1",
1866 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
1867 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
1868 | "license": "MIT",
1869 | "dependencies": {
1870 | "call-bound": "^1.0.2",
1871 | "es-errors": "^1.3.0",
1872 | "get-intrinsic": "^1.2.5",
1873 | "object-inspect": "^1.13.3"
1874 | },
1875 | "engines": {
1876 | "node": ">= 0.4"
1877 | },
1878 | "funding": {
1879 | "url": "https://github.com/sponsors/ljharb"
1880 | }
1881 | },
1882 | "node_modules/side-channel-weakmap": {
1883 | "version": "1.0.2",
1884 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
1885 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
1886 | "license": "MIT",
1887 | "dependencies": {
1888 | "call-bound": "^1.0.2",
1889 | "es-errors": "^1.3.0",
1890 | "get-intrinsic": "^1.2.5",
1891 | "object-inspect": "^1.13.3",
1892 | "side-channel-map": "^1.0.1"
1893 | },
1894 | "engines": {
1895 | "node": ">= 0.4"
1896 | },
1897 | "funding": {
1898 | "url": "https://github.com/sponsors/ljharb"
1899 | }
1900 | },
1901 | "node_modules/statuses": {
1902 | "version": "2.0.1",
1903 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
1904 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
1905 | "license": "MIT",
1906 | "engines": {
1907 | "node": ">= 0.8"
1908 | }
1909 | },
1910 | "node_modules/symbol-tree": {
1911 | "version": "3.2.4",
1912 | "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
1913 | "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
1914 | "license": "MIT"
1915 | },
1916 | "node_modules/toidentifier": {
1917 | "version": "1.0.1",
1918 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
1919 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
1920 | "license": "MIT",
1921 | "engines": {
1922 | "node": ">=0.6"
1923 | }
1924 | },
1925 | "node_modules/tough-cookie": {
1926 | "version": "4.1.4",
1927 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
1928 | "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
1929 | "license": "BSD-3-Clause",
1930 | "dependencies": {
1931 | "psl": "^1.1.33",
1932 | "punycode": "^2.1.1",
1933 | "universalify": "^0.2.0",
1934 | "url-parse": "^1.5.3"
1935 | },
1936 | "engines": {
1937 | "node": ">=6"
1938 | }
1939 | },
1940 | "node_modules/tr46": {
1941 | "version": "5.1.0",
1942 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.0.tgz",
1943 | "integrity": "sha512-IUWnUK7ADYR5Sl1fZlO1INDUhVhatWl7BtJWsIhwJ0UAK7ilzzIa8uIqOO/aYVWHZPJkKbEL+362wrzoeRF7bw==",
1944 | "license": "MIT",
1945 | "dependencies": {
1946 | "punycode": "^2.3.1"
1947 | },
1948 | "engines": {
1949 | "node": ">=18"
1950 | }
1951 | },
1952 | "node_modules/turndown": {
1953 | "version": "7.2.0",
1954 | "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.0.tgz",
1955 | "integrity": "sha512-eCZGBN4nNNqM9Owkv9HAtWRYfLA4h909E/WGAWWBpmB275ehNhZyk87/Tpvjbp0jjNl9XwCsbe6bm6CqFsgD+A==",
1956 | "license": "MIT",
1957 | "dependencies": {
1958 | "@mixmark-io/domino": "^2.2.0"
1959 | }
1960 | },
1961 | "node_modules/type-is": {
1962 | "version": "1.6.18",
1963 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
1964 | "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
1965 | "license": "MIT",
1966 | "dependencies": {
1967 | "media-typer": "0.3.0",
1968 | "mime-types": "~2.1.24"
1969 | },
1970 | "engines": {
1971 | "node": ">= 0.6"
1972 | }
1973 | },
1974 | "node_modules/typescript": {
1975 | "version": "5.8.2",
1976 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
1977 | "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
1978 | "dev": true,
1979 | "license": "Apache-2.0",
1980 | "bin": {
1981 | "tsc": "bin/tsc",
1982 | "tsserver": "bin/tsserver"
1983 | },
1984 | "engines": {
1985 | "node": ">=14.17"
1986 | }
1987 | },
1988 | "node_modules/undici-types": {
1989 | "version": "6.19.8",
1990 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
1991 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
1992 | "dev": true,
1993 | "license": "MIT"
1994 | },
1995 | "node_modules/universalify": {
1996 | "version": "0.2.0",
1997 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
1998 | "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
1999 | "license": "MIT",
2000 | "engines": {
2001 | "node": ">= 4.0.0"
2002 | }
2003 | },
2004 | "node_modules/unpipe": {
2005 | "version": "1.0.0",
2006 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
2007 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
2008 | "license": "MIT",
2009 | "engines": {
2010 | "node": ">= 0.8"
2011 | }
2012 | },
2013 | "node_modules/url-parse": {
2014 | "version": "1.5.10",
2015 | "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
2016 | "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
2017 | "license": "MIT",
2018 | "dependencies": {
2019 | "querystringify": "^2.1.1",
2020 | "requires-port": "^1.0.0"
2021 | }
2022 | },
2023 | "node_modules/utils-merge": {
2024 | "version": "1.0.1",
2025 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
2026 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
2027 | "license": "MIT",
2028 | "engines": {
2029 | "node": ">= 0.4.0"
2030 | }
2031 | },
2032 | "node_modules/vary": {
2033 | "version": "1.1.2",
2034 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
2035 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
2036 | "license": "MIT",
2037 | "engines": {
2038 | "node": ">= 0.8"
2039 | }
2040 | },
2041 | "node_modules/w3c-xmlserializer": {
2042 | "version": "5.0.0",
2043 | "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
2044 | "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
2045 | "license": "MIT",
2046 | "dependencies": {
2047 | "xml-name-validator": "^5.0.0"
2048 | },
2049 | "engines": {
2050 | "node": ">=18"
2051 | }
2052 | },
2053 | "node_modules/webidl-conversions": {
2054 | "version": "7.0.0",
2055 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
2056 | "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
2057 | "license": "BSD-2-Clause",
2058 | "engines": {
2059 | "node": ">=12"
2060 | }
2061 | },
2062 | "node_modules/whatwg-encoding": {
2063 | "version": "3.1.1",
2064 | "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
2065 | "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
2066 | "license": "MIT",
2067 | "dependencies": {
2068 | "iconv-lite": "0.6.3"
2069 | },
2070 | "engines": {
2071 | "node": ">=18"
2072 | }
2073 | },
2074 | "node_modules/whatwg-mimetype": {
2075 | "version": "4.0.0",
2076 | "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
2077 | "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
2078 | "license": "MIT",
2079 | "engines": {
2080 | "node": ">=18"
2081 | }
2082 | },
2083 | "node_modules/whatwg-url": {
2084 | "version": "14.2.0",
2085 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
2086 | "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
2087 | "license": "MIT",
2088 | "dependencies": {
2089 | "tr46": "^5.1.0",
2090 | "webidl-conversions": "^7.0.0"
2091 | },
2092 | "engines": {
2093 | "node": ">=18"
2094 | }
2095 | },
2096 | "node_modules/which": {
2097 | "version": "2.0.2",
2098 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
2099 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
2100 | "license": "ISC",
2101 | "dependencies": {
2102 | "isexe": "^2.0.0"
2103 | },
2104 | "bin": {
2105 | "node-which": "bin/node-which"
2106 | },
2107 | "engines": {
2108 | "node": ">= 8"
2109 | }
2110 | },
2111 | "node_modules/wrappy": {
2112 | "version": "1.0.2",
2113 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
2114 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
2115 | "license": "ISC"
2116 | },
2117 | "node_modules/ws": {
2118 | "version": "8.18.1",
2119 | "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
2120 | "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
2121 | "license": "MIT",
2122 | "engines": {
2123 | "node": ">=10.0.0"
2124 | },
2125 | "peerDependencies": {
2126 | "bufferutil": "^4.0.1",
2127 | "utf-8-validate": ">=5.0.2"
2128 | },
2129 | "peerDependenciesMeta": {
2130 | "bufferutil": {
2131 | "optional": true
2132 | },
2133 | "utf-8-validate": {
2134 | "optional": true
2135 | }
2136 | }
2137 | },
2138 | "node_modules/xml-name-validator": {
2139 | "version": "5.0.0",
2140 | "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
2141 | "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
2142 | "license": "Apache-2.0",
2143 | "engines": {
2144 | "node": ">=18"
2145 | }
2146 | },
2147 | "node_modules/xmlchars": {
2148 | "version": "2.2.0",
2149 | "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
2150 | "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
2151 | "license": "MIT"
2152 | },
2153 | "node_modules/zod": {
2154 | "version": "3.24.2",
2155 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
2156 | "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
2157 | "license": "MIT",
2158 | "funding": {
2159 | "url": "https://github.com/sponsors/colinhacks"
2160 | }
2161 | },
2162 | "node_modules/zod-to-json-schema": {
2163 | "version": "3.24.5",
2164 | "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz",
2165 | "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==",
2166 | "license": "ISC",
2167 | "peerDependencies": {
2168 | "zod": "^3.24.1"
2169 | }
2170 | }
2171 | }
2172 | }
2173 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fetcher-mcp",
3 | "version": "0.3.0",
4 | "description": "MCP server for fetching web content using Playwright browser",
5 | "private": false,
6 | "type": "module",
7 | "bin": {
8 | "fetcher-mcp": "build/index.js"
9 | },
10 | "files": [
11 | "build",
12 | "icon.svg"
13 | ],
14 | "scripts": {
15 | "build": "tsc && node -e \"require('fs').chmodSync('build/index.js', '755')\"",
16 | "watch": "tsc --watch",
17 | "inspector": "npm run build && npx @modelcontextprotocol/inspector build/index.js --debug",
18 | "install-browser": "npx playwright install chromium"
19 | },
20 | "dependencies": {
21 | "@modelcontextprotocol/sdk": "^1.10.2",
22 | "@mozilla/readability": "^0.5.0",
23 | "express": "^4.18.2",
24 | "jsdom": "^24.0.0",
25 | "playwright": "^1.42.1",
26 | "turndown": "^7.1.2"
27 | },
28 | "devDependencies": {
29 | "@types/express": "^4.17.21",
30 | "@types/jsdom": "^21.1.6",
31 | "@types/node": "^20.17.24",
32 | "@types/turndown": "^5.0.4",
33 | "typescript": "^5.3.3"
34 | },
35 | "main": "index.js",
36 | "keywords": [
37 | "mcp",
38 | "playwright",
39 | "web-scraping",
40 | "readability",
41 | "content-extraction"
42 | ],
43 | "author": "",
44 | "license": "ISC"
45 | }
--------------------------------------------------------------------------------
/src/config/args.ts:
--------------------------------------------------------------------------------
1 | import { TransportConfig } from "../transports/types.js";
2 |
3 | /**
4 | * Parse command line arguments
5 | * @returns Transport configuration object
6 | */
7 | export function parseTransportConfig(): TransportConfig {
8 | const args = process.argv.slice(2);
9 | const config: TransportConfig = {
10 | type: "stdio",
11 | };
12 |
13 | // Parse transport type
14 | const transportArg = args.find((arg) => arg.startsWith("--transport="));
15 | if (transportArg) {
16 | const transportValue = transportArg.split("=")[1].toLowerCase();
17 | if (transportValue === "http") {
18 | config.type = "http";
19 | }
20 | }
21 |
22 | // If HTTP transport, parse port and host
23 | if (config.type === "http") {
24 | // Parse port
25 | const portArg = args.find((arg) => arg.startsWith("--port="));
26 | if (portArg) {
27 | const portValue = parseInt(portArg.split("=")[1], 10);
28 | if (!isNaN(portValue)) {
29 | config.port = portValue;
30 | }
31 | }
32 |
33 | // Parse host
34 | const hostArg = args.find((arg) => arg.startsWith("--host="));
35 | if (hostArg) {
36 | config.host = hostArg.split("=")[1];
37 | }
38 | }
39 |
40 | return config;
41 | }
42 |
43 | /**
44 | * Check debug mode
45 | * @returns Whether debug mode is enabled
46 | */
47 | export function isDebugMode(): boolean {
48 | return process.argv.includes("--debug");
49 | }
50 |
--------------------------------------------------------------------------------
/src/config/index.ts:
--------------------------------------------------------------------------------
1 | import { parseTransportConfig, isDebugMode } from "./args.js";
2 | import { TransportConfig } from "../transports/types.js";
3 |
4 | /**
5 | * Get application configuration
6 | */
7 | export function getConfig(): {
8 | transport: TransportConfig;
9 | debug: boolean;
10 | } {
11 | return {
12 | transport: parseTransportConfig(),
13 | debug: isDebugMode(),
14 | };
15 | }
16 |
17 | export { isDebugMode };
18 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * MCP server based on Playwright headless browser
5 | * Provides web content fetching functionality
6 | * Supports Stdio and HTTP/SSE transport protocols
7 | */
8 |
9 | import { getConfig, isDebugMode } from "./config/index.js";
10 | import { createTransportProvider } from "./transports/index.js";
11 | import { startServer } from "./server.js";
12 | import { logger } from "./utils/logger.js";
13 |
14 | /**
15 | * Start the server
16 | */
17 | async function main() {
18 | logger.info("[Setup] Initializing browser MCP server...");
19 |
20 | if (isDebugMode()) {
21 | logger.warn(
22 | "[Setup] Debug mode enabled, Chrome browser window will be visible"
23 | );
24 | }
25 |
26 | try {
27 | // Get configuration
28 | const config = getConfig();
29 |
30 | // Create transport provider
31 | const transportProvider = createTransportProvider(config.transport);
32 |
33 | // Start server
34 | await startServer(transportProvider);
35 |
36 | logger.info("[Setup] Server started");
37 |
38 | // Print transport information
39 | if (config.transport.type === "http") {
40 | logger.info(
41 | `[Setup] HTTP server running at http://${
42 | config.transport.host || "localhost"
43 | }:${config.transport.port || 3000}`
44 | );
45 | logger.info("[Setup] Available endpoints:");
46 | logger.info(
47 | "[Setup] - /mcp - Streamable HTTP endpoint (modern MCP protocol)"
48 | );
49 | logger.info("[Setup] - /sse - SSE endpoint (legacy MCP protocol)");
50 | } else {
51 | logger.info("[Setup] Using standard input/output (stdio) transport");
52 | }
53 | } catch (error: any) {
54 | logger.error(`[Error] Server error: ${error.message}`);
55 | if (error.stack) {
56 | logger.debug(error.stack);
57 | }
58 | process.exit(1);
59 | }
60 | }
61 |
62 | main();
63 |
--------------------------------------------------------------------------------
/src/server.ts:
--------------------------------------------------------------------------------
1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2 | import {
3 | CallToolRequestSchema,
4 | ListToolsRequestSchema,
5 | } from "@modelcontextprotocol/sdk/types.js";
6 | import { tools, toolHandlers } from "./tools/index.js";
7 | import { TransportProvider } from "./transports/types.js";
8 | import { logger } from "./utils/logger.js";
9 |
10 | /**
11 | * Create MCP server instance
12 | * @returns MCP server instance
13 | */
14 | function createServer() {
15 | const server = new Server(
16 | {
17 | name: "browser-mcp",
18 | version: "0.1.0",
19 | },
20 | {
21 | capabilities: {
22 | tools: {},
23 | },
24 | }
25 | );
26 |
27 | server.setRequestHandler(ListToolsRequestSchema, async () => {
28 | logger.info("[Tools] Listing available tools");
29 | return {
30 | tools,
31 | };
32 | });
33 |
34 | /**
35 | * Handle tool call requests
36 | * Dispatch to the appropriate tool implementation
37 | */
38 | server.setRequestHandler(CallToolRequestSchema, async (request) => {
39 | const toolName = request.params.name;
40 | const handler = toolHandlers[toolName];
41 |
42 | if (!handler) {
43 | throw new Error(`Unknown tool: ${toolName}`);
44 | }
45 |
46 | return handler(request.params.arguments);
47 | });
48 |
49 | return server;
50 | }
51 |
52 | /**
53 | * Set up process signal handlers
54 | * @param transportProvider Transport provider
55 | */
56 | function setupProcessHandlers(transportProvider: TransportProvider): void {
57 | // Handle SIGINT signal (Ctrl+C)
58 | process.on("SIGINT", async () => {
59 | logger.info("[Server] Received SIGINT signal, gracefully shutting down...");
60 | await transportProvider.close();
61 | process.exit(0);
62 | });
63 |
64 | // Handle SIGTERM signal
65 | process.on("SIGTERM", async () => {
66 | logger.info(
67 | "[Server] Received SIGTERM signal, gracefully shutting down..."
68 | );
69 | await transportProvider.close();
70 | process.exit(0);
71 | });
72 |
73 | // Handle uncaught exceptions
74 | process.on("uncaughtException", async (error) => {
75 | logger.error(`[Server] Uncaught exception: ${error.message}`);
76 | if (error.stack) {
77 | logger.error(error.stack);
78 | }
79 | await transportProvider.close();
80 | process.exit(1);
81 | });
82 | }
83 |
84 | /**
85 | * Start MCP server using the specified transport provider
86 | * @param transportProvider Transport provider
87 | */
88 | export async function startServer(
89 | transportProvider: TransportProvider
90 | ): Promise {
91 | try {
92 | const server = createServer();
93 | logger.info("[Server] Starting MCP server...");
94 |
95 | // Connect to transport
96 | await transportProvider.connect(server);
97 |
98 | logger.info("[Server] MCP server started");
99 |
100 | // Set up process termination handlers
101 | setupProcessHandlers(transportProvider);
102 | } catch (error: any) {
103 | logger.error(`[Server] Failed to start MCP server: ${error.message}`);
104 | throw error;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/services/browserService.ts:
--------------------------------------------------------------------------------
1 | import { Browser, BrowserContext, Page, chromium } from "playwright";
2 | import { logger } from "../utils/logger.js";
3 | import { FetchOptions } from "../types/index.js";
4 |
5 | /**
6 | * Service for managing browser instances with anti-detection features
7 | */
8 | export class BrowserService {
9 | private options: FetchOptions;
10 | private isDebugMode: boolean;
11 |
12 | constructor(options: FetchOptions) {
13 | this.options = options;
14 | this.isDebugMode = process.argv.includes("--debug");
15 |
16 | // Debug mode from options takes precedence over command line flag
17 | if (options.debug !== undefined) {
18 | this.isDebugMode = options.debug;
19 | }
20 | }
21 |
22 | /**
23 | * Get whether debug mode is enabled
24 | */
25 | public isInDebugMode(): boolean {
26 | return this.isDebugMode;
27 | }
28 |
29 | /**
30 | * Generate a random user agent string
31 | */
32 | private getRandomUserAgent(): string {
33 | const userAgents = [
34 | // Chrome - Windows
35 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
36 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
37 | // Chrome - Mac
38 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36",
39 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
40 | // Firefox
41 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0",
42 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:123.0) Gecko/20100101 Firefox/123.0",
43 | // Safari
44 | "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.3 Safari/605.1.15",
45 | ];
46 | return userAgents[Math.floor(Math.random() * userAgents.length)];
47 | }
48 |
49 | /**
50 | * Generate a random viewport size
51 | */
52 | private getRandomViewport(): {width: number, height: number} {
53 | const viewports = [
54 | { width: 1920, height: 1080 },
55 | { width: 1366, height: 768 },
56 | { width: 1536, height: 864 },
57 | { width: 1440, height: 900 },
58 | { width: 1280, height: 720 },
59 | ];
60 | return viewports[Math.floor(Math.random() * viewports.length)];
61 | }
62 |
63 | /**
64 | * Setup anti-detection script to evade browser automation detection
65 | */
66 | private async setupAntiDetection(context: BrowserContext): Promise {
67 | await context.addInitScript(() => {
68 | // Override navigator.webdriver
69 | Object.defineProperty(navigator, 'webdriver', {
70 | get: () => false,
71 | });
72 |
73 | // Remove automation fingerprints
74 | delete (window as any).cdc_adoQpoasnfa76pfcZLmcfl_Array;
75 | delete (window as any).cdc_adoQpoasnfa76pfcZLmcfl_Promise;
76 | delete (window as any).cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
77 |
78 | // Add Chrome object for fingerprinting evasion
79 | const chrome = {
80 | runtime: {},
81 | };
82 |
83 | // Add fingerprint characteristics
84 | (window as any).chrome = chrome;
85 |
86 | // Modify screen and navigator properties
87 | Object.defineProperty(screen, 'width', { value: window.innerWidth });
88 | Object.defineProperty(screen, 'height', { value: window.innerHeight });
89 | Object.defineProperty(screen, 'availWidth', { value: window.innerWidth });
90 | Object.defineProperty(screen, 'availHeight', { value: window.innerHeight });
91 |
92 | // Add language features
93 | Object.defineProperty(navigator, 'languages', {
94 | get: () => ['en-US', 'en'],
95 | });
96 |
97 | // Simulate random number of plugins
98 | Object.defineProperty(navigator, 'plugins', {
99 | get: () => {
100 | const plugins = [];
101 | for (let i = 0; i < 5 + Math.floor(Math.random() * 5); i++) {
102 | plugins.push({
103 | name: 'Plugin ' + i,
104 | description: 'Description ' + i,
105 | filename: 'plugin' + i + '.dll',
106 | });
107 | }
108 | return plugins;
109 | },
110 | });
111 | });
112 | }
113 |
114 | /**
115 | * Setup media handling - disable media loading if needed
116 | */
117 | private async setupMediaHandling(context: BrowserContext): Promise {
118 | if (this.options.disableMedia) {
119 | await context.route("**/*", async (route) => {
120 | const resourceType = route.request().resourceType();
121 | if (["image", "stylesheet", "font", "media"].includes(resourceType)) {
122 | await route.abort();
123 | } else {
124 | await route.continue();
125 | }
126 | });
127 | }
128 | }
129 |
130 | /**
131 | * Create a new stealth browser instance
132 | */
133 | public async createBrowser(): Promise {
134 | const viewport = this.getRandomViewport();
135 |
136 | return await chromium.launch({
137 | headless: !this.isDebugMode,
138 | args: [
139 | '--disable-blink-features=AutomationControlled',
140 | '--disable-features=IsolateOrigins,site-per-process',
141 | '--no-sandbox',
142 | '--disable-setuid-sandbox',
143 | '--disable-dev-shm-usage',
144 | '--disable-webgl',
145 | '--disable-infobars',
146 | '--window-size=' + viewport.width + ',' + viewport.height,
147 | '--disable-extensions'
148 | ]
149 | });
150 | }
151 |
152 | /**
153 | * Create a new browser context with stealth configurations
154 | */
155 | public async createContext(browser: Browser): Promise<{ context: BrowserContext, viewport: {width: number, height: number} }> {
156 | const viewport = this.getRandomViewport();
157 |
158 | const context = await browser.newContext({
159 | javaScriptEnabled: true,
160 | ignoreHTTPSErrors: true,
161 | userAgent: this.getRandomUserAgent(),
162 | viewport: viewport,
163 | deviceScaleFactor: Math.random() > 0.5 ? 1 : 2,
164 | isMobile: false,
165 | hasTouch: false,
166 | locale: 'en-US',
167 | timezoneId: 'America/New_York',
168 | colorScheme: 'light',
169 | acceptDownloads: true,
170 | extraHTTPHeaders: {
171 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8',
172 | 'Accept-Language': 'en-US,en;q=0.9',
173 | 'Accept-Encoding': 'gzip, deflate, br',
174 | 'DNT': '1',
175 | 'Connection': 'keep-alive',
176 | 'Upgrade-Insecure-Requests': '1',
177 | 'Sec-Fetch-Dest': 'document',
178 | 'Sec-Fetch-Mode': 'navigate',
179 | 'Sec-Fetch-Site': 'none',
180 | 'Sec-Fetch-User': '?1',
181 | 'Cache-Control': 'max-age=0',
182 | }
183 | });
184 |
185 | // Set up anti-detection measures
186 | await this.setupAntiDetection(context);
187 |
188 | // Configure media handling
189 | await this.setupMediaHandling(context);
190 |
191 | return { context, viewport };
192 | }
193 |
194 | /**
195 | * Create a new page
196 | */
197 | public async createPage(context: BrowserContext, viewport: {width: number, height: number}): Promise {
198 | const page = await context.newPage();
199 | return page;
200 | }
201 |
202 | /**
203 | * Clean up resources
204 | */
205 | public async cleanup(browser: Browser | null, page: Page | null): Promise {
206 | if (!this.isDebugMode) {
207 | if (page) {
208 | await page
209 | .close()
210 | .catch((e) => logger.error(`Failed to close page: ${e.message}`));
211 | }
212 | if (browser) {
213 | await browser
214 | .close()
215 | .catch((e) => logger.error(`Failed to close browser: ${e.message}`));
216 | }
217 | }
218 | }
219 | }
--------------------------------------------------------------------------------
/src/services/webContentProcessor.ts:
--------------------------------------------------------------------------------
1 | import { JSDOM } from "jsdom";
2 | import { Readability } from "@mozilla/readability";
3 | import TurndownService from "turndown";
4 | import { FetchOptions, FetchResult } from "../types/index.js";
5 | import { logger } from "../utils/logger.js";
6 |
7 | export class WebContentProcessor {
8 | private options: FetchOptions;
9 | private logPrefix: string;
10 |
11 | constructor(options: FetchOptions, logPrefix: string = "") {
12 | this.options = options;
13 | this.logPrefix = logPrefix;
14 | }
15 |
16 | async processPageContent(page: any, url: string): Promise {
17 | try {
18 | // Set timeout
19 | page.setDefaultTimeout(this.options.timeout);
20 |
21 | // Navigate to URL
22 | logger.info(`${this.logPrefix} Navigating to URL: ${url}`);
23 | try {
24 | await page.goto(url, {
25 | timeout: this.options.timeout,
26 | waitUntil: this.options.waitUntil,
27 | });
28 | } catch (gotoError: any) {
29 | // If it's a timeout error, try to retrieve page content
30 | if (gotoError.message.includes("Timeout") || gotoError.message.includes("timeout")) {
31 | logger.warn(`${this.logPrefix} Navigation timeout: ${gotoError.message}. Attempting to retrieve content anyway...`);
32 |
33 | // Try to retrieve page content
34 | try {
35 | // Directly get page information without waiting for page stability
36 | const { pageTitle, html } = await this.safelyGetPageInfo(page, url);
37 |
38 | // If content is retrieved, process and return it
39 | if (html && html.trim().length > 0) {
40 | logger.info(`${this.logPrefix} Successfully retrieved content despite timeout, length: ${html.length}`);
41 |
42 | const processedContent = await this.processContent(html, url);
43 | const formattedContent = `Title: ${pageTitle}\nURL: ${url}\nContent:\n\n${processedContent}`;
44 |
45 | return {
46 | success: true,
47 | content: formattedContent,
48 | };
49 | }
50 | } catch (retrieveError: any) {
51 | logger.error(`${this.logPrefix} Failed to retrieve content after timeout: ${retrieveError.message}`);
52 | }
53 | }
54 |
55 | // If unable to retrieve content or it's not a timeout error, continue to throw the original error
56 | throw gotoError;
57 | }
58 |
59 | // Handle possible anti-bot verification and redirection
60 | if (this.options.waitForNavigation) {
61 | logger.info(
62 | `${this.logPrefix} Waiting for possible navigation/redirection...`
63 | );
64 |
65 | try {
66 | // Create a promise to wait for page navigation events
67 | const navigationPromise = page.waitForNavigation({
68 | timeout: this.options.navigationTimeout,
69 | waitUntil: this.options.waitUntil,
70 | });
71 |
72 | // Set a timeout
73 | const timeoutPromise = new Promise((_, reject) => {
74 | setTimeout(() => {
75 | reject(new Error("Navigation timeout"));
76 | }, this.options.navigationTimeout);
77 | });
78 |
79 | // Wait for navigation event or timeout, whichever occurs first
80 | await Promise.race([navigationPromise, timeoutPromise])
81 | .then(() => {
82 | logger.info(
83 | `${this.logPrefix} Page navigated/redirected successfully`
84 | );
85 | })
86 | .catch((e) => {
87 | // If timeout occurs but page may have already loaded, we can continue
88 | logger.warn(
89 | `${this.logPrefix} No navigation occurred or navigation timeout: ${e.message}`
90 | );
91 | });
92 | } catch (navError: any) {
93 | logger.error(
94 | `${this.logPrefix} Error waiting for navigation: ${navError.message}`
95 | );
96 | // Continue processing the page even if there are navigation issues
97 | }
98 | }
99 |
100 | // Wait for the page to stabilize before getting content
101 | await this.ensurePageStability(page);
102 |
103 | // Safely retrieve page title and content
104 | const { pageTitle, html } = await this.safelyGetPageInfo(page, url);
105 |
106 | if (!html) {
107 | logger.warn(`${this.logPrefix} Browser returned empty content`);
108 | return {
109 | success: false,
110 | content: `Title: Error\nURL: ${url}\nContent:\n\nFailed to retrieve web page content: Browser returned empty content `,
111 | error: "Browser returned empty content",
112 | };
113 | }
114 |
115 | logger.info(
116 | `${this.logPrefix} Successfully retrieved web page content, length: ${html.length}`
117 | );
118 |
119 | const processedContent = await this.processContent(html, url);
120 |
121 | // Format the response
122 | const formattedContent = `Title: ${pageTitle}\nURL: ${url}\nContent:\n\n${processedContent}`;
123 |
124 | return {
125 | success: true,
126 | content: formattedContent,
127 | };
128 | } catch (error) {
129 | const errorMessage =
130 | error instanceof Error ? error.message : "Unknown error";
131 | logger.error(`${this.logPrefix} Error: ${errorMessage}`);
132 |
133 | return {
134 | success: false,
135 | content: `Title: Error\nURL: ${url}\nContent:\n\nFailed to retrieve web page content: ${errorMessage} `,
136 | error: errorMessage,
137 | };
138 | }
139 | }
140 |
141 | // Added method: Ensure page stability
142 | private async ensurePageStability(page: any): Promise {
143 | try {
144 | // Check if there are ongoing network requests or navigation
145 | await page.waitForFunction(
146 | () => {
147 | return window.document.readyState === 'complete';
148 | },
149 | { timeout: this.options.timeout }
150 | );
151 |
152 | // Wait an extra short time to ensure page stability
153 | await page.waitForTimeout(500);
154 |
155 | logger.info(`${this.logPrefix} Page has stabilized`);
156 | } catch (error) {
157 | logger.warn(`${this.logPrefix} Error ensuring page stability: ${error instanceof Error ? error.message : String(error)}`);
158 | }
159 | }
160 |
161 | // Added method: Safely get page information (title and HTML content)
162 | private async safelyGetPageInfo(page: any, url: string, retries = 3): Promise<{pageTitle: string, html: string}> {
163 | let pageTitle = "Untitled";
164 | let html = "";
165 | let attempt = 0;
166 |
167 | while (attempt < retries) {
168 | try {
169 | attempt++;
170 |
171 | // Get page title
172 | pageTitle = await page.title();
173 | logger.info(`${this.logPrefix} Page title: ${pageTitle}`);
174 |
175 | // Get HTML content
176 | html = await page.content();
177 |
178 | // If successfully retrieved, exit the loop
179 | return { pageTitle, html };
180 |
181 | } catch (error) {
182 | const errorMessage = error instanceof Error ? error.message : String(error);
183 |
184 | // Check if it's an "execution context was destroyed" error
185 | if (errorMessage.includes("Execution context was destroyed") && attempt < retries) {
186 | logger.warn(`${this.logPrefix} Context destroyed, waiting for navigation to complete (attempt ${attempt}/${retries})...`);
187 |
188 | // Wait for page to stabilize
189 | await new Promise(resolve => setTimeout(resolve, 1000));
190 | await this.ensurePageStability(page);
191 |
192 | // If it's the last retry attempt, log the error but continue
193 | if (attempt === retries) {
194 | logger.error(`${this.logPrefix} Failed to get page info after ${retries} attempts`);
195 | }
196 | } else {
197 | // Other errors, log and rethrow
198 | logger.error(`${this.logPrefix} Error getting page info: ${errorMessage}`);
199 | throw error;
200 | }
201 | }
202 | }
203 |
204 | return { pageTitle, html };
205 | }
206 |
207 | private async processContent(html: string, url: string): Promise {
208 | let contentToProcess = html;
209 |
210 | // Extract main content if needed
211 | if (this.options.extractContent) {
212 | logger.info(`${this.logPrefix} Extracting main content`);
213 | const dom = new JSDOM(html, { url });
214 | const reader = new Readability(dom.window.document);
215 | const article = reader.parse();
216 |
217 | if (!article) {
218 | logger.warn(
219 | `${this.logPrefix} Could not extract main content, will use full HTML`
220 | );
221 | } else {
222 | contentToProcess = article.content;
223 | logger.info(
224 | `${this.logPrefix} Successfully extracted main content, length: ${contentToProcess.length}`
225 | );
226 | }
227 | }
228 |
229 | // Convert to markdown if needed
230 | let processedContent = contentToProcess;
231 | if (!this.options.returnHtml) {
232 | logger.info(`${this.logPrefix} Converting to Markdown`);
233 | const turndownService = new TurndownService();
234 | processedContent = turndownService.turndown(contentToProcess);
235 | logger.info(
236 | `${this.logPrefix} Successfully converted to Markdown, length: ${processedContent.length}`
237 | );
238 | }
239 |
240 | // Truncate if needed
241 | if (
242 | this.options.maxLength > 0 &&
243 | processedContent.length > this.options.maxLength
244 | ) {
245 | logger.info(
246 | `${this.logPrefix} Content exceeds maximum length, will truncate to ${this.options.maxLength} characters`
247 | );
248 | processedContent = processedContent.substring(0, this.options.maxLength);
249 | }
250 |
251 | return processedContent;
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/src/tools/fetchUrl.ts:
--------------------------------------------------------------------------------
1 | import { Browser, Page } from "playwright";
2 | import { WebContentProcessor } from "../services/webContentProcessor.js";
3 | import { BrowserService } from "../services/browserService.js";
4 | import { FetchOptions } from "../types/index.js";
5 | import { logger } from "../utils/logger.js";
6 |
7 | /**
8 | * Tool definition for fetch_url
9 | */
10 | export const fetchUrlTool = {
11 | name: "fetch_url",
12 | description: "Retrieve web page content from a specified URL",
13 | inputSchema: {
14 | type: "object",
15 | properties: {
16 | url: {
17 | type: "string",
18 | description: "URL to fetch. Make sure to include the schema (http:// or https:// if not defined, preferring https for most cases)",
19 | },
20 | timeout: {
21 | type: "number",
22 | description:
23 | "Page loading timeout in milliseconds, default is 30000 (30 seconds)",
24 | },
25 | waitUntil: {
26 | type: "string",
27 | description:
28 | "Specifies when navigation is considered complete, options: 'load', 'domcontentloaded', 'networkidle', 'commit', default is 'load'",
29 | },
30 | extractContent: {
31 | type: "boolean",
32 | description:
33 | "Whether to intelligently extract the main content, default is true",
34 | },
35 | maxLength: {
36 | type: "number",
37 | description:
38 | "Maximum length of returned content (in characters), default is no limit",
39 | },
40 | returnHtml: {
41 | type: "boolean",
42 | description:
43 | "Whether to return HTML content instead of Markdown, default is false",
44 | },
45 | waitForNavigation: {
46 | type: "boolean",
47 | description:
48 | "Whether to wait for additional navigation after initial page load (useful for sites with anti-bot verification), default is false",
49 | },
50 | navigationTimeout: {
51 | type: "number",
52 | description:
53 | "Maximum time to wait for additional navigation in milliseconds, default is 10000 (10 seconds)",
54 | },
55 | disableMedia: {
56 | type: "boolean",
57 | description:
58 | "Whether to disable media resources (images, stylesheets, fonts, media), default is true",
59 | },
60 | debug: {
61 | type: "boolean",
62 | description:
63 | "Whether to enable debug mode (showing browser window), overrides the --debug command line flag if specified",
64 | },
65 | },
66 | required: ["url"],
67 | },
68 | };
69 |
70 | /**
71 | * Implementation of the fetch_url tool
72 | */
73 | export async function fetchUrl(args: any) {
74 | const url = String(args?.url || "");
75 | if (!url) {
76 | logger.error(`URL parameter missing`);
77 | throw new Error("URL parameter is required");
78 | }
79 |
80 | const options: FetchOptions = {
81 | timeout: Number(args?.timeout) || 30000,
82 | waitUntil: String(args?.waitUntil || "load") as
83 | | "load"
84 | | "domcontentloaded"
85 | | "networkidle"
86 | | "commit",
87 | extractContent: args?.extractContent !== false,
88 | maxLength: Number(args?.maxLength) || 0,
89 | returnHtml: args?.returnHtml === true,
90 | waitForNavigation: args?.waitForNavigation === true,
91 | navigationTimeout: Number(args?.navigationTimeout) || 10000,
92 | disableMedia: args?.disableMedia !== false,
93 | debug: args?.debug,
94 | };
95 |
96 | // Create browser service
97 | const browserService = new BrowserService(options);
98 |
99 | // Create content processor
100 | const processor = new WebContentProcessor(options, "[FetchURL]");
101 | let browser: Browser | null = null;
102 | let page: Page | null = null;
103 |
104 | if (browserService.isInDebugMode()) {
105 | logger.debug(`Debug mode enabled for URL: ${url}`);
106 | }
107 |
108 | try {
109 | // Create a stealth browser with anti-detection measures
110 | browser = await browserService.createBrowser();
111 |
112 | // Create a stealth browser context
113 | const { context, viewport } = await browserService.createContext(browser);
114 |
115 | // Create a new page with human-like behavior
116 | page = await browserService.createPage(context, viewport);
117 |
118 | // Process page content
119 | const result = await processor.processPageContent(page, url);
120 |
121 | return {
122 | content: [{ type: "text", text: result.content }],
123 | };
124 | } finally {
125 | // Clean up resources
126 | await browserService.cleanup(browser, page);
127 |
128 | if (browserService.isInDebugMode()) {
129 | logger.debug(`Browser and page kept open for debugging. URL: ${url}`);
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/src/tools/fetchUrls.ts:
--------------------------------------------------------------------------------
1 | import { Browser, Page } from "playwright";
2 | import { WebContentProcessor } from "../services/webContentProcessor.js";
3 | import { BrowserService } from "../services/browserService.js";
4 | import { FetchOptions, FetchResult } from "../types/index.js";
5 | import { logger } from "../utils/logger.js";
6 |
7 | /**
8 | * Tool definition for fetch_urls
9 | */
10 | export const fetchUrlsTool = {
11 | name: "fetch_urls",
12 | description: "Retrieve web page content from multiple specified URLs",
13 | inputSchema: {
14 | type: "object",
15 | properties: {
16 | urls: {
17 | type: "array",
18 | items: {
19 | type: "string",
20 | },
21 | description: "Array of URLs to fetch",
22 | },
23 | timeout: {
24 | type: "number",
25 | description:
26 | "Page loading timeout in milliseconds, default is 30000 (30 seconds)",
27 | },
28 | waitUntil: {
29 | type: "string",
30 | description:
31 | "Specifies when navigation is considered complete, options: 'load', 'domcontentloaded', 'networkidle', 'commit', default is 'load'",
32 | },
33 | extractContent: {
34 | type: "boolean",
35 | description:
36 | "Whether to intelligently extract the main content, default is true",
37 | },
38 | maxLength: {
39 | type: "number",
40 | description:
41 | "Maximum length of returned content (in characters), default is no limit",
42 | },
43 | returnHtml: {
44 | type: "boolean",
45 | description:
46 | "Whether to return HTML content instead of Markdown, default is false",
47 | },
48 | waitForNavigation: {
49 | type: "boolean",
50 | description:
51 | "Whether to wait for additional navigation after initial page load (useful for sites with anti-bot verification), default is false",
52 | },
53 | navigationTimeout: {
54 | type: "number",
55 | description:
56 | "Maximum time to wait for additional navigation in milliseconds, default is 10000 (10 seconds)",
57 | },
58 | disableMedia: {
59 | type: "boolean",
60 | description:
61 | "Whether to disable media resources (images, stylesheets, fonts, media), default is true",
62 | },
63 | debug: {
64 | type: "boolean",
65 | description:
66 | "Whether to enable debug mode (showing browser window), overrides the --debug command line flag if specified",
67 | },
68 | },
69 | required: ["urls"],
70 | },
71 | };
72 |
73 | /**
74 | * Implementation of the fetch_urls tool
75 | */
76 | export async function fetchUrls(args: any) {
77 | const urls = (args?.urls as string[]) || [];
78 | if (!urls || !Array.isArray(urls) || urls.length === 0) {
79 | throw new Error("URLs parameter is required and must be a non-empty array");
80 | }
81 |
82 | const options: FetchOptions = {
83 | timeout: Number(args?.timeout) || 30000,
84 | waitUntil: String(args?.waitUntil || "load") as
85 | | "load"
86 | | "domcontentloaded"
87 | | "networkidle"
88 | | "commit",
89 | extractContent: args?.extractContent !== false,
90 | maxLength: Number(args?.maxLength) || 0,
91 | returnHtml: args?.returnHtml === true,
92 | waitForNavigation: args?.waitForNavigation === true,
93 | navigationTimeout: Number(args?.navigationTimeout) || 10000,
94 | disableMedia: args?.disableMedia !== false,
95 | debug: args?.debug,
96 | };
97 |
98 | // Create browser service
99 | const browserService = new BrowserService(options);
100 |
101 | if (browserService.isInDebugMode()) {
102 | logger.debug(`Debug mode enabled for URLs: ${urls.join(", ")}`);
103 | }
104 |
105 | let browser: Browser | null = null;
106 | try {
107 | // Create a stealth browser with anti-detection measures
108 | browser = await browserService.createBrowser();
109 |
110 | // Create a stealth browser context
111 | const { context, viewport } = await browserService.createContext(browser);
112 |
113 | const processor = new WebContentProcessor(options, "[FetchURLs]");
114 |
115 | const results = await Promise.all(
116 | urls.map(async (url, index) => {
117 | // Create a new page with human-like behavior
118 | const page = await browserService.createPage(context, viewport);
119 |
120 | try {
121 | const result = await processor.processPageContent(page, url);
122 | return { index, ...result } as FetchResult;
123 | } finally {
124 | if (!browserService.isInDebugMode()) {
125 | await page
126 | .close()
127 | .catch((e) => logger.error(`Failed to close page: ${e.message}`));
128 | } else {
129 | logger.debug(`Page kept open for debugging. URL: ${url}`);
130 | }
131 | }
132 | })
133 | );
134 |
135 | results.sort((a, b) => (a.index || 0) - (b.index || 0));
136 | const combinedResults = results
137 | .map(
138 | (result, i) =>
139 | `[webpage ${i + 1} begin]\n${result.content}\n[webpage ${i + 1} end]`
140 | )
141 | .join("\n\n");
142 |
143 | return {
144 | content: [{ type: "text", text: combinedResults }],
145 | };
146 | } finally {
147 | // Clean up browser resources
148 | if (!browserService.isInDebugMode()) {
149 | if (browser)
150 | await browser
151 | .close()
152 | .catch((e) => logger.error(`Failed to close browser: ${e.message}`));
153 | } else {
154 | logger.debug(`Browser kept open for debugging. URLs: ${urls.join(", ")}`);
155 | }
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/tools/index.ts:
--------------------------------------------------------------------------------
1 | import { fetchUrlTool, fetchUrl } from './fetchUrl.js';
2 | import { fetchUrlsTool, fetchUrls } from './fetchUrls.js';
3 |
4 | // Export tool definitions
5 | export const tools = [
6 | fetchUrlTool,
7 | fetchUrlsTool
8 | ];
9 |
10 | // Export tool implementations
11 | export const toolHandlers = {
12 | [fetchUrlTool.name]: fetchUrl,
13 | [fetchUrlsTool.name]: fetchUrls
14 | };
--------------------------------------------------------------------------------
/src/transports/http.ts:
--------------------------------------------------------------------------------
1 | import express, { Request, Response } from "express";
2 | import { randomUUID } from "node:crypto";
3 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4 | import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
5 | import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
6 | import { TransportProvider } from "./types.js";
7 | import { logger } from "../utils/logger.js";
8 |
9 | /**
10 | * Check if a request is an initialization request
11 | */
12 | function isInitializeRequest(body: any): boolean {
13 | return body?.method === "initialize" && body?.jsonrpc === "2.0";
14 | }
15 |
16 | /**
17 | * HTTP Transport Provider implementation
18 | * Handles HTTP and SSE communication protocols
19 | */
20 | export class HttpTransportProvider implements TransportProvider {
21 | private app: express.Application;
22 | private server: any; // HTTP server instance
23 | private transports: {
24 | streamable: Record;
25 | sse: Record;
26 | };
27 |
28 | /**
29 | * Create HTTP Transport Provider
30 | * @param host Host address
31 | * @param port Port number
32 | */
33 | constructor(private host: string = "localhost", private port: number = 3000) {
34 | this.app = express();
35 | this.app.use(express.json());
36 |
37 | this.transports = {
38 | streamable: {},
39 | sse: {},
40 | };
41 | }
42 |
43 | /**
44 | * Connect server to HTTP transport
45 | * @param server MCP server instance
46 | */
47 | async connect(server: Server): Promise {
48 | logger.info(
49 | `[Transport] Connecting server using HTTP transport, listening on ${this.host}:${this.port}`
50 | );
51 |
52 | // Initialize Express routes
53 | this.setupRoutes(server);
54 |
55 | // Start HTTP server
56 | return new Promise((resolve) => {
57 | this.server = this.app.listen(this.port, this.host, () => {
58 | logger.info(
59 | `[Transport] HTTP server started, access at http://${this.host}:${this.port}`
60 | );
61 | resolve();
62 | });
63 | });
64 | }
65 |
66 | /**
67 | * Close HTTP transport connection
68 | */
69 | async close(): Promise {
70 | // Close all transports
71 | Object.values(this.transports.streamable).forEach((transport) => {
72 | try {
73 | transport.close();
74 | } catch (err) {
75 | logger.error(
76 | `[Transport] Failed to close Streamable HTTP transport: ${err}`
77 | );
78 | }
79 | });
80 |
81 | Object.values(this.transports.sse).forEach((transport) => {
82 | try {
83 | transport.close();
84 | } catch (err) {
85 | logger.error(`[Transport] Failed to close SSE transport: ${err}`);
86 | }
87 | });
88 |
89 | // Close HTTP server
90 | if (this.server) {
91 | return new Promise((resolve, reject) => {
92 | this.server.close((err: Error) => {
93 | if (err) {
94 | logger.error(`[Transport] Failed to close HTTP server: ${err}`);
95 | reject(err);
96 | } else {
97 | logger.info("[Transport] HTTP server closed");
98 | resolve();
99 | }
100 | });
101 | });
102 | }
103 |
104 | return Promise.resolve();
105 | }
106 |
107 | /**
108 | * Set up Express routes
109 | * @param mcpServer MCP server instance
110 | */
111 | private setupRoutes(mcpServer: Server): void {
112 | // Streamable HTTP endpoint (modern MCP clients)
113 | this.app.post("/mcp", async (req: Request, res: Response) => {
114 | await this.handleStreamableHttpRequest(mcpServer, req, res);
115 | });
116 |
117 | // Handle GET requests (server-to-client notifications)
118 | this.app.get("/mcp", async (req: Request, res: Response) => {
119 | await this.handleSessionRequest(req, res);
120 | });
121 |
122 | // Handle DELETE requests (session termination)
123 | this.app.delete("/mcp", async (req: Request, res: Response) => {
124 | await this.handleSessionRequest(req, res);
125 | });
126 |
127 | // SSE endpoint (legacy MCP clients)
128 | this.app.get("/sse", async (req: Request, res: Response) => {
129 | await this.handleSseRequest(mcpServer, req, res);
130 | });
131 |
132 | // SSE message endpoint (legacy MCP clients)
133 | this.app.post("/messages", async (req: Request, res: Response) => {
134 | await this.handleSseMessageRequest(req, res);
135 | });
136 |
137 | // Root path response
138 | this.app.get("/", (_req: Request, res: Response) => {
139 | res.send(`
140 |
141 | MCP Browser Server
142 |
143 | MCP Browser Server is running
144 | This is an MCP server providing browser automation functionality.
145 | Supported endpoints:
146 |
147 | /mcp
- Streamable HTTP endpoint (modern MCP protocol)
148 | /sse
- SSE endpoint (legacy MCP protocol)
149 |
150 |
151 |
152 | `);
153 | });
154 | }
155 |
156 | /**
157 | * Handle Streamable HTTP requests
158 | */
159 | private async handleStreamableHttpRequest(
160 | server: Server,
161 | req: Request,
162 | res: Response
163 | ): Promise {
164 | // Check for existing session ID
165 | const sessionId = req.headers["mcp-session-id"] as string | undefined;
166 | let transport: StreamableHTTPServerTransport;
167 |
168 | try {
169 | if (sessionId && this.transports.streamable[sessionId]) {
170 | // Reuse existing transport
171 | logger.debug(`[Transport] Reusing existing session ID: ${sessionId}`);
172 | transport = this.transports.streamable[sessionId];
173 | } else if (!sessionId && isInitializeRequest(req.body)) {
174 | // Initialize new transport
175 | logger.debug("[Transport] Initializing new StreamableHTTP transport");
176 | transport = new StreamableHTTPServerTransport({
177 | sessionIdGenerator: () => randomUUID(),
178 | onsessioninitialized: (sessionId: string) => {
179 | logger.debug(
180 | `[Transport] StreamableHTTP session initialized: ${sessionId}`
181 | );
182 | this.transports.streamable[sessionId] = transport;
183 | },
184 | });
185 |
186 | // Clean up transport
187 | transport.onclose = () => {
188 | if (transport.sessionId) {
189 | logger.debug(
190 | `[Transport] Closing StreamableHTTP session: ${transport.sessionId}`
191 | );
192 | delete this.transports.streamable[transport.sessionId];
193 | }
194 | };
195 |
196 | // Connect to MCP server
197 | await server.connect(transport);
198 | } else {
199 | // Invalid request
200 | logger.error(
201 | "[Transport] Invalid request: No session ID and not an initialization request"
202 | );
203 | res.status(400).json({
204 | jsonrpc: "2.0",
205 | error: {
206 | code: -32000,
207 | message: "Bad Request: No valid session ID provided",
208 | },
209 | id: null,
210 | });
211 | return;
212 | }
213 |
214 | // Handle request
215 | await transport.handleRequest(req, res, req.body);
216 | } catch (error: any) {
217 | logger.error(
218 | `[Transport] Error handling StreamableHTTP request: ${error.message}`
219 | );
220 | if (!res.headersSent) {
221 | res.status(500).json({
222 | jsonrpc: "2.0",
223 | error: {
224 | code: -32603,
225 | message: `Internal server error: ${error.message}`,
226 | },
227 | id: null,
228 | });
229 | }
230 | }
231 | }
232 |
233 | /**
234 | * Handle session requests (GET/DELETE)
235 | */
236 | private async handleSessionRequest(
237 | req: Request,
238 | res: Response
239 | ): Promise {
240 | const sessionId = req.headers["mcp-session-id"] as string | undefined;
241 | if (!sessionId || !this.transports.streamable[sessionId]) {
242 | logger.error(`[Transport] Invalid or missing session ID: ${sessionId}`);
243 | res.status(400).send("Invalid or missing session ID");
244 | return;
245 | }
246 |
247 | try {
248 | const transport = this.transports.streamable[sessionId];
249 | await transport.handleRequest(req, res);
250 | } catch (error: any) {
251 | logger.error(
252 | `[Transport] Error handling session request: ${error.message}`
253 | );
254 | if (!res.headersSent) {
255 | res.status(500).send(`Internal server error: ${error.message}`);
256 | }
257 | }
258 | }
259 |
260 | /**
261 | * Handle SSE requests (legacy clients)
262 | */
263 | private async handleSseRequest(
264 | server: Server,
265 | req: Request,
266 | res: Response
267 | ): Promise {
268 | try {
269 | logger.debug("[Transport] Initializing SSE transport");
270 | const transport = new SSEServerTransport("/messages", res);
271 | this.transports.sse[transport.sessionId] = transport;
272 |
273 | res.on("close", () => {
274 | logger.debug(`[Transport] Closing SSE session: ${transport.sessionId}`);
275 | delete this.transports.sse[transport.sessionId];
276 | });
277 |
278 | await server.connect(transport);
279 | } catch (error: any) {
280 | logger.error(`[Transport] Error handling SSE request: ${error.message}`);
281 | if (!res.headersSent) {
282 | res.status(500).send(`Internal server error: ${error.message}`);
283 | }
284 | }
285 | }
286 |
287 | /**
288 | * Handle SSE message requests (legacy clients)
289 | */
290 | private async handleSseMessageRequest(
291 | req: Request,
292 | res: Response
293 | ): Promise {
294 | const sessionId = req.query.sessionId as string;
295 | const transport = sessionId ? this.transports.sse[sessionId] : undefined;
296 |
297 | if (transport) {
298 | try {
299 | await transport.handlePostMessage(req, res, req.body);
300 | } catch (error: any) {
301 | logger.error(
302 | `[Transport] Error handling SSE message request: ${error.message}`
303 | );
304 | if (!res.headersSent) {
305 | res.status(500).send(`Internal server error: ${error.message}`);
306 | }
307 | }
308 | } else {
309 | logger.error(
310 | `[Transport] No transport found for session ID: ${sessionId}`
311 | );
312 | res.status(400).send("No transport found for session ID");
313 | }
314 | }
315 | }
316 |
--------------------------------------------------------------------------------
/src/transports/index.ts:
--------------------------------------------------------------------------------
1 | import { TransportConfig, TransportProvider } from "./types.js";
2 | import { StdioTransportProvider } from "./stdio.js";
3 | import { HttpTransportProvider } from "./http.js";
4 | import { logger } from "../utils/logger.js";
5 |
6 | /**
7 | * Factory function to create transport providers
8 | * @param config Transport configuration
9 | * @returns Transport provider instance
10 | */
11 | export function createTransportProvider(
12 | config: TransportConfig
13 | ): TransportProvider {
14 | logger.info(`[Transport] Creating ${config.type} transport provider`);
15 |
16 | if (config.type === "http") {
17 | return new HttpTransportProvider(
18 | config.host || "localhost",
19 | config.port || 3000
20 | );
21 | }
22 |
23 | // Default to Stdio transport
24 | return new StdioTransportProvider();
25 | }
26 |
27 | export * from "./types.js";
28 |
--------------------------------------------------------------------------------
/src/transports/stdio.ts:
--------------------------------------------------------------------------------
1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3 | import { TransportProvider } from "./types.js";
4 | import { logger } from "../utils/logger.js";
5 |
6 | /**
7 | * Stdio Transport Provider implementation
8 | * Handles MCP communication via standard input/output
9 | */
10 | export class StdioTransportProvider implements TransportProvider {
11 | private transport: StdioServerTransport | null = null;
12 |
13 | /**
14 | * Connect server to Stdio transport
15 | * @param server MCP server instance
16 | */
17 | async connect(server: Server): Promise {
18 | logger.info("[Transport] Connecting server using Stdio transport");
19 | this.transport = new StdioServerTransport();
20 | await server.connect(this.transport);
21 | logger.info("[Transport] Stdio transport connected");
22 | }
23 |
24 | /**
25 | * Close Stdio transport connection
26 | */
27 | async close(): Promise {
28 | if (this.transport) {
29 | logger.info("[Transport] Closing Stdio transport");
30 | this.transport.close();
31 | this.transport = null;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/transports/types.ts:
--------------------------------------------------------------------------------
1 | import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2 | import { Response, Request } from "express";
3 |
4 | /**
5 | * Transport configuration interface
6 | */
7 | export interface TransportConfig {
8 | type: "stdio" | "http";
9 | port?: number;
10 | host?: string;
11 | }
12 |
13 | /**
14 | * Transport provider interface
15 | * Defines common methods for creating and connecting transports
16 | */
17 | export interface TransportProvider {
18 | /**
19 | * Connect server to transport layer
20 | * @param server MCP server instance
21 | */
22 | connect(server: Server): Promise;
23 |
24 | /**
25 | * Close transport connection
26 | */
27 | close(): Promise;
28 | }
29 |
30 | /**
31 | * HTTP transport session
32 | */
33 | export interface HttpSession {
34 | sessionId: string;
35 | transport: any; // Actual type depends on implementation
36 | }
37 |
38 | /**
39 | * SSE transport request handler interface
40 | */
41 | export interface SseRequestHandler {
42 | handleRequest(req: Request, res: Response, body?: any): Promise;
43 | handlePostMessage(req: Request, res: Response, body?: any): Promise;
44 | }
45 |
--------------------------------------------------------------------------------
/src/types/index.ts:
--------------------------------------------------------------------------------
1 | export interface FetchOptions {
2 | timeout: number;
3 | waitUntil: 'load' | 'domcontentloaded' | 'networkidle' | 'commit';
4 | extractContent: boolean;
5 | maxLength: number;
6 | returnHtml: boolean;
7 | waitForNavigation: boolean;
8 | navigationTimeout: number;
9 | disableMedia: boolean;
10 | debug?: boolean;
11 | }
12 |
13 | export interface FetchResult {
14 | success: boolean;
15 | content: string;
16 | error?: string;
17 | index?: number;
18 | }
--------------------------------------------------------------------------------
/src/utils/logger.ts:
--------------------------------------------------------------------------------
1 | class Logger {
2 | private logMode: boolean;
3 |
4 | constructor(options: { logMode?: boolean } = {}) {
5 | this.logMode = options.logMode || false;
6 | }
7 |
8 | private log(level: string, message: string) {
9 | if (!this.logMode) return;
10 |
11 | const timestamp = new Date().toISOString();
12 | const logMessage = `${timestamp} [${level}] ${message}`;
13 | console.error(logMessage);
14 | }
15 |
16 | info(message: string) {
17 | this.log("INFO", message);
18 | }
19 |
20 | warn(message: string) {
21 | this.log("WARN", message);
22 | }
23 |
24 | error(message: string) {
25 | this.log("ERROR", message);
26 | }
27 |
28 | debug(message: string) {
29 | this.log("DEBUG", message);
30 | }
31 | }
32 |
33 | // Create default logger instance
34 | export const logger = new Logger({
35 | logMode: process.argv.includes("--log"),
36 | });
37 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2022",
4 | "module": "Node16",
5 | "moduleResolution": "Node16",
6 | "outDir": "./build",
7 | "rootDir": "./src",
8 | "strict": true,
9 | "esModuleInterop": true,
10 | "skipLibCheck": true,
11 | "forceConsistentCasingInFileNames": true
12 | },
13 | "include": [
14 | "src/**/*"
15 | ],
16 | "exclude": [
17 | "node_modules"
18 | ]
19 | }
--------------------------------------------------------------------------------