├── .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 | Fetcher MCP Icon 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 | [![Powered by DartNode](https://dartnode.com/branding/DN-Open-Source-sm.png)](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 | } --------------------------------------------------------------------------------