├── .dockerignore ├── .github └── workflows │ ├── issue-manager.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── .npmrc ├── Dockerfile ├── LICENSE ├── README.md ├── assets └── demo.png ├── docs └── KNOWN_ISSUES.md ├── package-lock.json ├── package.json ├── smithery.yaml ├── src ├── index.ts └── youtube.ts └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | .pnpm-store 4 | 5 | # Build output 6 | dist 7 | 8 | # Version control 9 | .git 10 | .gitignore 11 | 12 | # IDE files 13 | .vscode 14 | .idea 15 | *.swp 16 | *.swo 17 | 18 | # Logs 19 | logs 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | pnpm-debug.log* 25 | 26 | # Environment variables 27 | .env 28 | .env.* 29 | 30 | # Test files 31 | coverage 32 | .nyc_output 33 | 34 | # OS files 35 | .DS_Store 36 | Thumbs.db -------------------------------------------------------------------------------- /.github/workflows/issue-manager.yml: -------------------------------------------------------------------------------- 1 | name: Issue Manager 2 | 3 | on: 4 | schedule: 5 | - cron: '0 0 * * *' # Run daily at midnight 6 | issues: 7 | types: [opened, reopened] 8 | 9 | jobs: 10 | close-stale-issues: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | issues: write 14 | steps: 15 | - name: Check for stale issues 16 | uses: actions/stale@v9 17 | with: 18 | repo-token: ${{ secrets.GITHUB_TOKEN }} 19 | stale-issue-message: 'This issue has been automatically closed due to inactivity. If you still need help, please feel free to reopen it.' 20 | stale-issue-label: 'stale' 21 | days-before-stale: 30 22 | days-before-close: 7 23 | exempt-issue-labels: 'pinned,help-wanted' 24 | only-issue-labels: '' 25 | operations-per-run: 30 -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | publish-gpr: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | packages: write 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: actions/setup-node@v4 17 | with: 18 | node-version: '20.x' 19 | registry-url: 'https://npm.pkg.github.com' 20 | scope: '@sinco-lab' 21 | - run: npm ci 22 | - run: npm run build 23 | - run: npm publish 24 | env: 25 | NODE_AUTH_TOKEN: ${{ github.token }} 26 | 27 | publish-npm: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: actions/setup-node@v4 32 | with: 33 | node-version: '20.x' 34 | registry-url: 'https://registry.npmjs.org' 35 | scope: '@sinco-lab' 36 | - run: | 37 | echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc 38 | npm ci 39 | npm run build 40 | npm publish 41 | env: 42 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | build/ 133 | 134 | gcp-oauth.keys.json 135 | .*-server-credentials.json 136 | 137 | # Byte-compiled / optimized / DLL files 138 | __pycache__/ 139 | *.py[cod] 140 | *$py.class 141 | 142 | # C extensions 143 | *.so 144 | 145 | # Distribution / packaging 146 | .Python 147 | build/ 148 | develop-eggs/ 149 | dist/ 150 | downloads/ 151 | eggs/ 152 | .eggs/ 153 | lib/ 154 | lib64/ 155 | parts/ 156 | sdist/ 157 | var/ 158 | wheels/ 159 | share/python-wheels/ 160 | *.egg-info/ 161 | .installed.cfg 162 | *.egg 163 | MANIFEST 164 | 165 | # PyInstaller 166 | # Usually these files are written by a python script from a template 167 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 168 | *.manifest 169 | *.spec 170 | 171 | # Installer logs 172 | pip-log.txt 173 | pip-delete-this-directory.txt 174 | 175 | # Unit test / coverage reports 176 | htmlcov/ 177 | .tox/ 178 | .nox/ 179 | .coverage 180 | .coverage.* 181 | .cache 182 | nosetests.xml 183 | coverage.xml 184 | *.cover 185 | *.py,cover 186 | .hypothesis/ 187 | .pytest_cache/ 188 | cover/ 189 | 190 | # Translations 191 | *.mo 192 | *.pot 193 | 194 | # Django stuff: 195 | *.log 196 | local_settings.py 197 | db.sqlite3 198 | db.sqlite3-journal 199 | 200 | # Flask stuff: 201 | instance/ 202 | .webassets-cache 203 | 204 | # Scrapy stuff: 205 | .scrapy 206 | 207 | # Sphinx documentation 208 | docs/_build/ 209 | 210 | # PyBuilder 211 | .pybuilder/ 212 | target/ 213 | 214 | # Jupyter Notebook 215 | .ipynb_checkpoints 216 | 217 | # IPython 218 | profile_default/ 219 | ipython_config.py 220 | 221 | # pyenv 222 | # For a library or package, you might want to ignore these files since the code is 223 | # intended to run in multiple environments; otherwise, check them in: 224 | # .python-version 225 | 226 | # pipenv 227 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 228 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 229 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 230 | # install all needed dependencies. 231 | #Pipfile.lock 232 | 233 | # poetry 234 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 235 | # This is especially recommended for binary packages to ensure reproducibility, and is more 236 | # commonly ignored for libraries. 237 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 238 | #poetry.lock 239 | 240 | # pdm 241 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 242 | #pdm.lock 243 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 244 | # in version control. 245 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 246 | .pdm.toml 247 | .pdm-python 248 | .pdm-build/ 249 | 250 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 251 | __pypackages__/ 252 | 253 | # Celery stuff 254 | celerybeat-schedule 255 | celerybeat.pid 256 | 257 | # SageMath parsed files 258 | *.sage.py 259 | 260 | # Environments 261 | .env 262 | .venv 263 | env/ 264 | venv/ 265 | ENV/ 266 | env.bak/ 267 | venv.bak/ 268 | 269 | # Spyder project settings 270 | .spyderproject 271 | .spyproject 272 | 273 | # Rope project settings 274 | .ropeproject 275 | 276 | # mkdocs documentation 277 | /site 278 | 279 | # mypy 280 | .mypy_cache/ 281 | .dmypy.json 282 | dmypy.json 283 | 284 | # Pyre type checker 285 | .pyre/ 286 | 287 | # pytype static type analyzer 288 | .pytype/ 289 | 290 | # Cython debug symbols 291 | cython_debug/ 292 | 293 | .DS_Store 294 | 295 | # PyCharm 296 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 297 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 298 | # and can be added to the global gitignore or merged into this file. For a more nuclear 299 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 300 | #.idea/ 301 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Source 2 | src/ 3 | tests/ 4 | 5 | # Config files 6 | .eslintrc.json 7 | .prettierrc 8 | tsconfig.json 9 | .dockerignore 10 | Dockerfile 11 | smithery.yaml 12 | 13 | # Development files 14 | .git/ 15 | .github/ 16 | .vscode/ 17 | .idea/ 18 | *.log 19 | .DS_Store 20 | 21 | # Dependencies 22 | node_modules/ 23 | .pnpm-store/ 24 | 25 | # Test files 26 | coverage/ 27 | .nyc_output/ 28 | 29 | # Environment 30 | .env 31 | .env.* -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | strict-peer-dependencies=false -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build the application 2 | FROM node:18-alpine AS builder 3 | 4 | # Set working directory 5 | WORKDIR /app 6 | 7 | # Copy package files 8 | COPY package.json package-lock.json* ./ 9 | 10 | # Install dependencies 11 | RUN npm ci 12 | 13 | # Copy the rest of the application 14 | COPY . . 15 | 16 | # Build the application 17 | RUN npm run build 18 | 19 | # Stage 2: Create the production image 20 | FROM node:18-alpine AS production 21 | 22 | # Create non-root user 23 | RUN addgroup -S appgroup && adduser -S appuser -G appgroup 24 | 25 | # Set working directory 26 | WORKDIR /app 27 | 28 | # Copy the built files from the builder stage 29 | COPY --from=builder /app/dist /app/dist 30 | COPY --from=builder /app/package.json /app/package-lock.json* ./ 31 | 32 | # Install production dependencies only 33 | RUN npm ci --only=production 34 | 35 | # Change ownership to non-root user 36 | RUN chown -R appuser:appgroup /app 37 | 38 | # Switch to non-root user 39 | USER appuser 40 | 41 | # Specify the default command 42 | ENTRYPOINT ["node", "dist/index.js"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Freddie 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 | # MCP YouTube Transcript Server 2 | 3 | [![smithery badge](https://smithery.ai/badge/@sinco-lab/mcp-youtube-transcript)](https://smithery.ai/server/@sinco-lab/mcp-youtube-transcript) 4 | 5 | A Model Context Protocol server that enables retrieval of transcripts from YouTube videos. This server provides direct access to video transcripts through a simple interface, making it ideal for content analysis and processing. 6 | 7 | 8 | mcp-youtube-transcript 9 | 10 | 11 | ## Table of Contents 12 | - [Features](#features) 13 | - [Getting Started](#getting-started) 14 | - [Prerequisites](#prerequisites) 15 | - [Installation](#installation) 16 | - [Usage](#usage) 17 | - [Basic Configuration](#basic-configuration) 18 | - [Testing](#testing) 19 | - [Troubleshooting and Maintenance](#troubleshooting-and-maintenance) 20 | - [API Reference](#api-reference) 21 | - [Development](#development) 22 | - [Contributing](#contributing) 23 | - [License](#license) 24 | 25 | ## Features 26 | 27 | ✨ Key capabilities: 28 | - Extract transcripts from YouTube videos 29 | - Support for multiple languages 30 | - Format text with continuous or paragraph mode 31 | - Retrieve video titles and metadata 32 | - Automatic paragraph segmentation 33 | - Text normalization and HTML entity decoding 34 | - Robust error handling 35 | - Timestamp and overlap detection 36 | 37 | ## Getting Started 38 | 39 | ### Prerequisites 40 | 41 | - Node.js 18 or higher 42 | 43 | ### Installation 44 | 45 | We provide two installation methods: 46 | 47 | #### Option 1: Manual Configuration (Recommended for Production) 48 | 49 | 1. Create or edit the Claude Desktop configuration file: 50 | - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` 51 | - Windows: `%APPDATA%\Claude\claude_desktop_config.json` 52 | 53 | 2. Add the following configuration: 54 | 55 | ```json 56 | { 57 | "mcpServers": { 58 | "youtube-transcript": { 59 | "command": "npx", 60 | "args": [ 61 | "-y", 62 | "@sinco-lab/mcp-youtube-transcript" 63 | ] 64 | } 65 | } 66 | } 67 | ``` 68 | 69 | Quick setup script for macOS: 70 | 71 | ```bash 72 | # Create directory if it doesn't exist 73 | mkdir -p ~/Library/Application\ Support/Claude 74 | 75 | # Create or update config file 76 | cat > ~/Library/Application\ Support/Claude/claude_desktop_config.json << 'EOL' 77 | { 78 | "mcpServers": { 79 | "youtube-transcript": { 80 | "command": "npx", 81 | "args": [ 82 | "-y", 83 | "@sinco-lab/mcp-youtube-transcript" 84 | ] 85 | } 86 | } 87 | } 88 | EOL 89 | ``` 90 | 91 | #### Option 2: Via Smithery (Development Only) 92 | 93 | ```bash 94 | npx -y @smithery/cli install @sinco-lab/mcp-youtube-transcript --client claude 95 | ``` 96 | 97 | ⚠️ **Note**: This method is not recommended for production use as it relies on Smithery's proxy services. 98 | 99 | ## Usage 100 | 101 | ### Basic Configuration 102 | 103 | To use with Claude Desktop / Cursor / cline, ensure your configuration matches: 104 | 105 | ```json 106 | { 107 | "mcpServers": { 108 | "youtube-transcript": { 109 | "command": "npx", 110 | "args": ["-y", "@sinco-lab/mcp-youtube-transcript"] 111 | } 112 | } 113 | } 114 | ``` 115 | 116 | ### Testing 117 | 118 | #### With Claude App 119 | 120 | 1. Restart the Claude app after installation 121 | 2. Test with a simple command: 122 | ```plaintext 123 | https://www.youtube.com/watch?v=AJpK3YTTKZ4 Summarize this video 124 | ``` 125 | 126 | Example output: 127 | ![Demo](./assets/demo.png) 128 | 129 | #### With MCP Inspector 130 | 131 | ```bash 132 | # Clone and setup 133 | git clone https://github.com/sinco-lab/mcp-youtube-transcript.git 134 | cd mcp-youtube-transcript 135 | npm install 136 | npm run build 137 | 138 | # Launch inspector 139 | npx @modelcontextprotocol/inspector node "dist/index.js" 140 | 141 | # Access http://localhost:6274 and try these commands: 142 | # 1. List Tools: clink `List Tools` 143 | # 2. Test get_transcripts with: 144 | # url: "https://www.youtube.com/watch?v=AJpK3YTTKZ4" 145 | # lang: "en" (optional) 146 | # enableParagraphs: false (optional) 147 | ``` 148 | 149 | ### Troubleshooting and Maintenance 150 | 151 | #### Checking Claude Logs 152 | 153 | To monitor Claude's logs, you can use the following command: 154 | 155 | ```bash 156 | tail -n 20 -f ~/Library/Logs/Claude/mcp*.log 157 | ``` 158 | 159 | This will display the last 20 lines of the log file and continue to show new entries as they are added. 160 | 161 | > **Note**: Claude app automatically prefixes MCP server log files with `mcp-server-`. For example, our server's logs will be written to `mcp-server-youtube-transcript.log`. 162 | 163 | #### Cleaning the `npx` Cache 164 | 165 | If you encounter issues related to the `npx` cache, you can manually clean it using: 166 | 167 | ```bash 168 | rm -rf ~/.npm/_npx 169 | ``` 170 | 171 | This will remove the cached packages and allow you to start fresh. 172 | 173 | ## API Reference 174 | 175 | ### get_transcripts 176 | 177 | Fetches transcripts from YouTube videos. 178 | 179 | **Parameters:** 180 | - `url` (string, required): YouTube video URL or ID 181 | - `lang` (string, optional): Language code (default: "en") 182 | - `enableParagraphs` (boolean, optional): Enable paragraph mode (default: false) 183 | 184 | **Response Format:** 185 | ```json 186 | { 187 | "content": [{ 188 | "type": "text", 189 | "text": "Video title and transcript content", 190 | "metadata": { 191 | "videoId": "video_id", 192 | "title": "video_title", 193 | "language": "transcript_language", 194 | "timestamp": "processing_time", 195 | "charCount": "character_count", 196 | "transcriptCount": "number_of_transcripts", 197 | "totalDuration": "total_duration", 198 | "paragraphsEnabled": "paragraph_mode_status" 199 | } 200 | }] 201 | } 202 | ``` 203 | 204 | ## Development 205 | 206 | ### Project Structure 207 | 208 | ``` 209 | ├── src/ 210 | │ ├── index.ts # Server entry point 211 | │ ├── youtube.ts # YouTube transcript fetching logic 212 | ├── dist/ # Compiled output 213 | └── package.json 214 | ``` 215 | 216 | ### Key Components 217 | 218 | - `YouTubeTranscriptFetcher`: Core transcript fetching functionality 219 | - `YouTubeUtils`: Text processing and utilities 220 | 221 | ### Features and Capabilities 222 | 223 | - **Error Handling:** 224 | - Invalid URLs/IDs 225 | - Unavailable transcripts 226 | - Language availability 227 | - Network errors 228 | - Rate limiting 229 | 230 | - **Text Processing:** 231 | - HTML entity decoding 232 | - Punctuation normalization 233 | - Space normalization 234 | - Smart paragraph detection 235 | 236 | ## Contributing 237 | 238 | We welcome contributions! Please feel free to submit issues and pull requests. 239 | 240 | ## License 241 | 242 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 243 | 244 | ## Related Projects 245 | 246 | - [mcp-servers](https://github.com/modelcontextprotocol/servers) 247 | - [MCP Inspector](https://github.com/modelcontextprotocol/inspector) 248 | -------------------------------------------------------------------------------- /assets/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sinco-lab/mcp-youtube-transcript/78f76285689645f8e71b1f7e81d7245d006be65c/assets/demo.png -------------------------------------------------------------------------------- /docs/KNOWN_ISSUES.md: -------------------------------------------------------------------------------- 1 | # Known Issues 2 | 3 | ## Node.js Version Management with Claude App 4 | 5 | ### Issue Description 6 | 7 | When using nvm (Node Version Manager) with multiple Node.js versions installed, Claude App exhibits specific behavior with node and npx commands. 8 | 9 | #### Current Behavior 10 | - Claude App defaults to using the lowest installed Node.js version 11 | - Full path to node executable works (e.g., `/Users/username/.nvm/versions/node/v18.x.x/bin/node`) 12 | - Full path to npx does not work effectively 13 | 14 | #### Technical Analysis 15 | 16 | 1. Environment Variable Inheritance 17 | - Claude App is built on Electron, which has specific environment variable handling mechanisms 18 | - Electron initializes environment variables before command line flags and app code 19 | - Some environment variables are explicitly controlled by Electron: 20 | - `NODE_OPTIONS`: Limited support, some options are explicitly disallowed 21 | - `ELECTRON_RUN_AS_NODE`: Can be used to run as a normal Node.js process 22 | - The app may have its own environment isolation 23 | 24 | 2. Potential Root Causes 25 | - Electron's environment variable isolation may prevent proper npx path resolution 26 | - The way Electron handles `PATH` and executable resolution might differ from shell behavior 27 | - npx might be trying to use Electron's bundled Node.js version instead of the system one 28 | 29 | #### Solution Found 30 | 31 | 1. Working Configuration: 32 | ```json 33 | { 34 | "mcpServers": { 35 | "youtube-transcript": { 36 | "command": "npx", 37 | "args": ["-y", "@sinco-lab/mcp-youtube-transcript"], 38 | "env": { 39 | "PATH": "/Users/username/.nvm/versions/node/v18.x.x/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" 40 | } 41 | } 42 | } 43 | } 44 | ``` 45 | 46 | 2. Key Findings: 47 | - Using relative command name ("npx") works while full path does not 48 | - PATH environment variable must include complete system paths 49 | - No need for ELECTRON_RUN_AS_NODE when using this approach 50 | 51 | 3. Command Path Resolution Behavior: 52 | - Relative command names (e.g., "npx") work better than absolute paths 53 | - Possible reasons: 54 | - npx's internal Node.js environment dependencies 55 | - Electron's process creation mechanisms 56 | - Shell resolution and environment initialization 57 | - Using PATH allows proper environment setup for npm/npx tools 58 | 59 | 4. Best Practices: 60 | - Use relative command names in the configuration 61 | - Provide complete PATH including all system directories 62 | - Include the desired Node.js version bin directory first in PATH 63 | - Maintain full system paths for maximum compatibility: 64 | ``` 65 | /usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin 66 | ``` 67 | Reasons: 68 | - Different systems may have tools in different locations 69 | - Future dependencies might require additional system tools 70 | - Ensures compatibility across different Unix-like environments 71 | - Prevents potential issues with npm/npx dependencies 72 | 73 | #### Current Status 74 | - Issue Status: Resolved 75 | - Solution: Use relative command name with PATH environment variable 76 | - Impact: Successfully allows using specific Node.js version 77 | 78 | #### Notes 79 | - This solution maintains proper Node.js environment setup 80 | - Works reliably across different Node.js versions 81 | - May need adjustment if system paths change 82 | - Document this approach for future reference 83 | - While minimal PATH might work (e.g., just /bin for sh), full system paths are recommended for better compatibility 84 | 85 | We will keep this document updated if we discover any additional insights or improvements. -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sinco-lab/mcp-youtube-transcript", 3 | "version": "0.0.8", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "@sinco-lab/mcp-youtube-transcript", 9 | "version": "0.0.8", 10 | "license": "MIT", 11 | "dependencies": { 12 | "@modelcontextprotocol/sdk": "1.7.0", 13 | "zod": "^3.24.2" 14 | }, 15 | "bin": { 16 | "mcp-youtube-transcript": "dist/index.js" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^20.11.24", 20 | "typescript": "^5.6.2" 21 | }, 22 | "engines": { 23 | "node": ">=18.0.0" 24 | } 25 | }, 26 | "node_modules/@modelcontextprotocol/sdk": { 27 | "version": "1.7.0", 28 | "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.7.0.tgz", 29 | "integrity": "sha512-IYPe/FLpvF3IZrd/f5p5ffmWhMc3aEMuM2wGJASDqC2Ge7qatVCdbfPx3n/5xFeb19xN0j/911M2AaFuircsWA==", 30 | "dependencies": { 31 | "content-type": "^1.0.5", 32 | "cors": "^2.8.5", 33 | "eventsource": "^3.0.2", 34 | "express": "^5.0.1", 35 | "express-rate-limit": "^7.5.0", 36 | "pkce-challenge": "^4.1.0", 37 | "raw-body": "^3.0.0", 38 | "zod": "^3.23.8", 39 | "zod-to-json-schema": "^3.24.1" 40 | }, 41 | "engines": { 42 | "node": ">=18" 43 | } 44 | }, 45 | "node_modules/@types/node": { 46 | "version": "20.17.27", 47 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.27.tgz", 48 | "integrity": "sha512-U58sbKhDrthHlxHRJw7ZLiLDZGmAUOZUbpw0S6nL27sYUdhvgBLCRu/keSd6qcTsfArd1sRFCCBxzWATGr/0UA==", 49 | "dev": true, 50 | "dependencies": { 51 | "undici-types": "~6.19.2" 52 | } 53 | }, 54 | "node_modules/accepts": { 55 | "version": "2.0.0", 56 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", 57 | "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", 58 | "dependencies": { 59 | "mime-types": "^3.0.0", 60 | "negotiator": "^1.0.0" 61 | }, 62 | "engines": { 63 | "node": ">= 0.6" 64 | } 65 | }, 66 | "node_modules/body-parser": { 67 | "version": "2.1.0", 68 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.1.0.tgz", 69 | "integrity": "sha512-/hPxh61E+ll0Ujp24Ilm64cykicul1ypfwjVttduAiEdtnJFvLePSrIPk+HMImtNv5270wOGCb1Tns2rybMkoQ==", 70 | "dependencies": { 71 | "bytes": "^3.1.2", 72 | "content-type": "^1.0.5", 73 | "debug": "^4.4.0", 74 | "http-errors": "^2.0.0", 75 | "iconv-lite": "^0.5.2", 76 | "on-finished": "^2.4.1", 77 | "qs": "^6.14.0", 78 | "raw-body": "^3.0.0", 79 | "type-is": "^2.0.0" 80 | }, 81 | "engines": { 82 | "node": ">=18" 83 | } 84 | }, 85 | "node_modules/body-parser/node_modules/debug": { 86 | "version": "4.4.0", 87 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 88 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 89 | "dependencies": { 90 | "ms": "^2.1.3" 91 | }, 92 | "engines": { 93 | "node": ">=6.0" 94 | }, 95 | "peerDependenciesMeta": { 96 | "supports-color": { 97 | "optional": true 98 | } 99 | } 100 | }, 101 | "node_modules/body-parser/node_modules/ms": { 102 | "version": "2.1.3", 103 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 104 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 105 | }, 106 | "node_modules/body-parser/node_modules/qs": { 107 | "version": "6.14.0", 108 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", 109 | "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 110 | "dependencies": { 111 | "side-channel": "^1.1.0" 112 | }, 113 | "engines": { 114 | "node": ">=0.6" 115 | }, 116 | "funding": { 117 | "url": "https://github.com/sponsors/ljharb" 118 | } 119 | }, 120 | "node_modules/bytes": { 121 | "version": "3.1.2", 122 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 123 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 124 | "engines": { 125 | "node": ">= 0.8" 126 | } 127 | }, 128 | "node_modules/call-bind-apply-helpers": { 129 | "version": "1.0.2", 130 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 131 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 132 | "dependencies": { 133 | "es-errors": "^1.3.0", 134 | "function-bind": "^1.1.2" 135 | }, 136 | "engines": { 137 | "node": ">= 0.4" 138 | } 139 | }, 140 | "node_modules/call-bound": { 141 | "version": "1.0.4", 142 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 143 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 144 | "dependencies": { 145 | "call-bind-apply-helpers": "^1.0.2", 146 | "get-intrinsic": "^1.3.0" 147 | }, 148 | "engines": { 149 | "node": ">= 0.4" 150 | }, 151 | "funding": { 152 | "url": "https://github.com/sponsors/ljharb" 153 | } 154 | }, 155 | "node_modules/content-disposition": { 156 | "version": "1.0.0", 157 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", 158 | "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", 159 | "dependencies": { 160 | "safe-buffer": "5.2.1" 161 | }, 162 | "engines": { 163 | "node": ">= 0.6" 164 | } 165 | }, 166 | "node_modules/content-type": { 167 | "version": "1.0.5", 168 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 169 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 170 | "engines": { 171 | "node": ">= 0.6" 172 | } 173 | }, 174 | "node_modules/cookie": { 175 | "version": "0.7.1", 176 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", 177 | "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", 178 | "engines": { 179 | "node": ">= 0.6" 180 | } 181 | }, 182 | "node_modules/cookie-signature": { 183 | "version": "1.2.2", 184 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", 185 | "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", 186 | "engines": { 187 | "node": ">=6.6.0" 188 | } 189 | }, 190 | "node_modules/cors": { 191 | "version": "2.8.5", 192 | "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", 193 | "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", 194 | "dependencies": { 195 | "object-assign": "^4", 196 | "vary": "^1" 197 | }, 198 | "engines": { 199 | "node": ">= 0.10" 200 | } 201 | }, 202 | "node_modules/debug": { 203 | "version": "4.3.6", 204 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", 205 | "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", 206 | "dependencies": { 207 | "ms": "2.1.2" 208 | }, 209 | "engines": { 210 | "node": ">=6.0" 211 | }, 212 | "peerDependenciesMeta": { 213 | "supports-color": { 214 | "optional": true 215 | } 216 | } 217 | }, 218 | "node_modules/depd": { 219 | "version": "2.0.0", 220 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 221 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 222 | "engines": { 223 | "node": ">= 0.8" 224 | } 225 | }, 226 | "node_modules/destroy": { 227 | "version": "1.2.0", 228 | "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", 229 | "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", 230 | "engines": { 231 | "node": ">= 0.8", 232 | "npm": "1.2.8000 || >= 1.4.16" 233 | } 234 | }, 235 | "node_modules/dunder-proto": { 236 | "version": "1.0.1", 237 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 238 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 239 | "dependencies": { 240 | "call-bind-apply-helpers": "^1.0.1", 241 | "es-errors": "^1.3.0", 242 | "gopd": "^1.2.0" 243 | }, 244 | "engines": { 245 | "node": ">= 0.4" 246 | } 247 | }, 248 | "node_modules/ee-first": { 249 | "version": "1.1.1", 250 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 251 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" 252 | }, 253 | "node_modules/encodeurl": { 254 | "version": "2.0.0", 255 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 256 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 257 | "engines": { 258 | "node": ">= 0.8" 259 | } 260 | }, 261 | "node_modules/es-define-property": { 262 | "version": "1.0.1", 263 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 264 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 265 | "engines": { 266 | "node": ">= 0.4" 267 | } 268 | }, 269 | "node_modules/es-errors": { 270 | "version": "1.3.0", 271 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 272 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 273 | "engines": { 274 | "node": ">= 0.4" 275 | } 276 | }, 277 | "node_modules/es-object-atoms": { 278 | "version": "1.1.1", 279 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 280 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 281 | "dependencies": { 282 | "es-errors": "^1.3.0" 283 | }, 284 | "engines": { 285 | "node": ">= 0.4" 286 | } 287 | }, 288 | "node_modules/escape-html": { 289 | "version": "1.0.3", 290 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 291 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" 292 | }, 293 | "node_modules/etag": { 294 | "version": "1.8.1", 295 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 296 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 297 | "engines": { 298 | "node": ">= 0.6" 299 | } 300 | }, 301 | "node_modules/eventsource": { 302 | "version": "3.0.5", 303 | "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.5.tgz", 304 | "integrity": "sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==", 305 | "dependencies": { 306 | "eventsource-parser": "^3.0.0" 307 | }, 308 | "engines": { 309 | "node": ">=18.0.0" 310 | } 311 | }, 312 | "node_modules/eventsource-parser": { 313 | "version": "3.0.0", 314 | "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", 315 | "integrity": "sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==", 316 | "engines": { 317 | "node": ">=18.0.0" 318 | } 319 | }, 320 | "node_modules/express": { 321 | "version": "5.0.1", 322 | "resolved": "https://registry.npmjs.org/express/-/express-5.0.1.tgz", 323 | "integrity": "sha512-ORF7g6qGnD+YtUG9yx4DFoqCShNMmUKiXuT5oWMHiOvt/4WFbHC6yCwQMTSBMno7AqntNCAzzcnnjowRkTL9eQ==", 324 | "dependencies": { 325 | "accepts": "^2.0.0", 326 | "body-parser": "^2.0.1", 327 | "content-disposition": "^1.0.0", 328 | "content-type": "~1.0.4", 329 | "cookie": "0.7.1", 330 | "cookie-signature": "^1.2.1", 331 | "debug": "4.3.6", 332 | "depd": "2.0.0", 333 | "encodeurl": "~2.0.0", 334 | "escape-html": "~1.0.3", 335 | "etag": "~1.8.1", 336 | "finalhandler": "^2.0.0", 337 | "fresh": "2.0.0", 338 | "http-errors": "2.0.0", 339 | "merge-descriptors": "^2.0.0", 340 | "methods": "~1.1.2", 341 | "mime-types": "^3.0.0", 342 | "on-finished": "2.4.1", 343 | "once": "1.4.0", 344 | "parseurl": "~1.3.3", 345 | "proxy-addr": "~2.0.7", 346 | "qs": "6.13.0", 347 | "range-parser": "~1.2.1", 348 | "router": "^2.0.0", 349 | "safe-buffer": "5.2.1", 350 | "send": "^1.1.0", 351 | "serve-static": "^2.1.0", 352 | "setprototypeof": "1.2.0", 353 | "statuses": "2.0.1", 354 | "type-is": "^2.0.0", 355 | "utils-merge": "1.0.1", 356 | "vary": "~1.1.2" 357 | }, 358 | "engines": { 359 | "node": ">= 18" 360 | } 361 | }, 362 | "node_modules/express-rate-limit": { 363 | "version": "7.5.0", 364 | "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", 365 | "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", 366 | "engines": { 367 | "node": ">= 16" 368 | }, 369 | "funding": { 370 | "url": "https://github.com/sponsors/express-rate-limit" 371 | }, 372 | "peerDependencies": { 373 | "express": "^4.11 || 5 || ^5.0.0-beta.1" 374 | } 375 | }, 376 | "node_modules/finalhandler": { 377 | "version": "2.1.0", 378 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", 379 | "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", 380 | "dependencies": { 381 | "debug": "^4.4.0", 382 | "encodeurl": "^2.0.0", 383 | "escape-html": "^1.0.3", 384 | "on-finished": "^2.4.1", 385 | "parseurl": "^1.3.3", 386 | "statuses": "^2.0.1" 387 | }, 388 | "engines": { 389 | "node": ">= 0.8" 390 | } 391 | }, 392 | "node_modules/finalhandler/node_modules/debug": { 393 | "version": "4.4.0", 394 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 395 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 396 | "dependencies": { 397 | "ms": "^2.1.3" 398 | }, 399 | "engines": { 400 | "node": ">=6.0" 401 | }, 402 | "peerDependenciesMeta": { 403 | "supports-color": { 404 | "optional": true 405 | } 406 | } 407 | }, 408 | "node_modules/finalhandler/node_modules/ms": { 409 | "version": "2.1.3", 410 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 411 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 412 | }, 413 | "node_modules/forwarded": { 414 | "version": "0.2.0", 415 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 416 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 417 | "engines": { 418 | "node": ">= 0.6" 419 | } 420 | }, 421 | "node_modules/fresh": { 422 | "version": "2.0.0", 423 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", 424 | "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", 425 | "engines": { 426 | "node": ">= 0.8" 427 | } 428 | }, 429 | "node_modules/function-bind": { 430 | "version": "1.1.2", 431 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 432 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 433 | "funding": { 434 | "url": "https://github.com/sponsors/ljharb" 435 | } 436 | }, 437 | "node_modules/get-intrinsic": { 438 | "version": "1.3.0", 439 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 440 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 441 | "dependencies": { 442 | "call-bind-apply-helpers": "^1.0.2", 443 | "es-define-property": "^1.0.1", 444 | "es-errors": "^1.3.0", 445 | "es-object-atoms": "^1.1.1", 446 | "function-bind": "^1.1.2", 447 | "get-proto": "^1.0.1", 448 | "gopd": "^1.2.0", 449 | "has-symbols": "^1.1.0", 450 | "hasown": "^2.0.2", 451 | "math-intrinsics": "^1.1.0" 452 | }, 453 | "engines": { 454 | "node": ">= 0.4" 455 | }, 456 | "funding": { 457 | "url": "https://github.com/sponsors/ljharb" 458 | } 459 | }, 460 | "node_modules/get-proto": { 461 | "version": "1.0.1", 462 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 463 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 464 | "dependencies": { 465 | "dunder-proto": "^1.0.1", 466 | "es-object-atoms": "^1.0.0" 467 | }, 468 | "engines": { 469 | "node": ">= 0.4" 470 | } 471 | }, 472 | "node_modules/gopd": { 473 | "version": "1.2.0", 474 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 475 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 476 | "engines": { 477 | "node": ">= 0.4" 478 | }, 479 | "funding": { 480 | "url": "https://github.com/sponsors/ljharb" 481 | } 482 | }, 483 | "node_modules/has-symbols": { 484 | "version": "1.1.0", 485 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 486 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 487 | "engines": { 488 | "node": ">= 0.4" 489 | }, 490 | "funding": { 491 | "url": "https://github.com/sponsors/ljharb" 492 | } 493 | }, 494 | "node_modules/hasown": { 495 | "version": "2.0.2", 496 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 497 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 498 | "dependencies": { 499 | "function-bind": "^1.1.2" 500 | }, 501 | "engines": { 502 | "node": ">= 0.4" 503 | } 504 | }, 505 | "node_modules/http-errors": { 506 | "version": "2.0.0", 507 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 508 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 509 | "dependencies": { 510 | "depd": "2.0.0", 511 | "inherits": "2.0.4", 512 | "setprototypeof": "1.2.0", 513 | "statuses": "2.0.1", 514 | "toidentifier": "1.0.1" 515 | }, 516 | "engines": { 517 | "node": ">= 0.8" 518 | } 519 | }, 520 | "node_modules/iconv-lite": { 521 | "version": "0.5.2", 522 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.5.2.tgz", 523 | "integrity": "sha512-kERHXvpSaB4aU3eANwidg79K8FlrN77m8G9V+0vOR3HYaRifrlwMEpT7ZBJqLSEIHnEgJTHcWK82wwLwwKwtag==", 524 | "dependencies": { 525 | "safer-buffer": ">= 2.1.2 < 3" 526 | }, 527 | "engines": { 528 | "node": ">=0.10.0" 529 | } 530 | }, 531 | "node_modules/inherits": { 532 | "version": "2.0.4", 533 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 534 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 535 | }, 536 | "node_modules/ipaddr.js": { 537 | "version": "1.9.1", 538 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 539 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 540 | "engines": { 541 | "node": ">= 0.10" 542 | } 543 | }, 544 | "node_modules/is-promise": { 545 | "version": "4.0.0", 546 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", 547 | "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" 548 | }, 549 | "node_modules/math-intrinsics": { 550 | "version": "1.1.0", 551 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 552 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 553 | "engines": { 554 | "node": ">= 0.4" 555 | } 556 | }, 557 | "node_modules/media-typer": { 558 | "version": "1.1.0", 559 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", 560 | "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", 561 | "engines": { 562 | "node": ">= 0.8" 563 | } 564 | }, 565 | "node_modules/merge-descriptors": { 566 | "version": "2.0.0", 567 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", 568 | "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", 569 | "engines": { 570 | "node": ">=18" 571 | }, 572 | "funding": { 573 | "url": "https://github.com/sponsors/sindresorhus" 574 | } 575 | }, 576 | "node_modules/methods": { 577 | "version": "1.1.2", 578 | "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", 579 | "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", 580 | "engines": { 581 | "node": ">= 0.6" 582 | } 583 | }, 584 | "node_modules/mime-db": { 585 | "version": "1.54.0", 586 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 587 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 588 | "engines": { 589 | "node": ">= 0.6" 590 | } 591 | }, 592 | "node_modules/mime-types": { 593 | "version": "3.0.0", 594 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.0.tgz", 595 | "integrity": "sha512-XqoSHeCGjVClAmoGFG3lVFqQFRIrTVw2OH3axRqAcfaw+gHWIfnASS92AV+Rl/mk0MupgZTRHQOjxY6YVnzK5w==", 596 | "dependencies": { 597 | "mime-db": "^1.53.0" 598 | }, 599 | "engines": { 600 | "node": ">= 0.6" 601 | } 602 | }, 603 | "node_modules/ms": { 604 | "version": "2.1.2", 605 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 606 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 607 | }, 608 | "node_modules/negotiator": { 609 | "version": "1.0.0", 610 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", 611 | "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", 612 | "engines": { 613 | "node": ">= 0.6" 614 | } 615 | }, 616 | "node_modules/object-assign": { 617 | "version": "4.1.1", 618 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 619 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", 620 | "engines": { 621 | "node": ">=0.10.0" 622 | } 623 | }, 624 | "node_modules/object-inspect": { 625 | "version": "1.13.4", 626 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 627 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 628 | "engines": { 629 | "node": ">= 0.4" 630 | }, 631 | "funding": { 632 | "url": "https://github.com/sponsors/ljharb" 633 | } 634 | }, 635 | "node_modules/on-finished": { 636 | "version": "2.4.1", 637 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 638 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 639 | "dependencies": { 640 | "ee-first": "1.1.1" 641 | }, 642 | "engines": { 643 | "node": ">= 0.8" 644 | } 645 | }, 646 | "node_modules/once": { 647 | "version": "1.4.0", 648 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 649 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 650 | "dependencies": { 651 | "wrappy": "1" 652 | } 653 | }, 654 | "node_modules/parseurl": { 655 | "version": "1.3.3", 656 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 657 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 658 | "engines": { 659 | "node": ">= 0.8" 660 | } 661 | }, 662 | "node_modules/path-to-regexp": { 663 | "version": "8.2.0", 664 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", 665 | "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", 666 | "engines": { 667 | "node": ">=16" 668 | } 669 | }, 670 | "node_modules/pkce-challenge": { 671 | "version": "4.1.0", 672 | "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-4.1.0.tgz", 673 | "integrity": "sha512-ZBmhE1C9LcPoH9XZSdwiPtbPHZROwAnMy+kIFQVrnMCxY4Cudlz3gBOpzilgc0jOgRaiT3sIWfpMomW2ar2orQ==", 674 | "engines": { 675 | "node": ">=16.20.0" 676 | } 677 | }, 678 | "node_modules/proxy-addr": { 679 | "version": "2.0.7", 680 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 681 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 682 | "dependencies": { 683 | "forwarded": "0.2.0", 684 | "ipaddr.js": "1.9.1" 685 | }, 686 | "engines": { 687 | "node": ">= 0.10" 688 | } 689 | }, 690 | "node_modules/qs": { 691 | "version": "6.13.0", 692 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", 693 | "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", 694 | "dependencies": { 695 | "side-channel": "^1.0.6" 696 | }, 697 | "engines": { 698 | "node": ">=0.6" 699 | }, 700 | "funding": { 701 | "url": "https://github.com/sponsors/ljharb" 702 | } 703 | }, 704 | "node_modules/range-parser": { 705 | "version": "1.2.1", 706 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 707 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 708 | "engines": { 709 | "node": ">= 0.6" 710 | } 711 | }, 712 | "node_modules/raw-body": { 713 | "version": "3.0.0", 714 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", 715 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 716 | "dependencies": { 717 | "bytes": "3.1.2", 718 | "http-errors": "2.0.0", 719 | "iconv-lite": "0.6.3", 720 | "unpipe": "1.0.0" 721 | }, 722 | "engines": { 723 | "node": ">= 0.8" 724 | } 725 | }, 726 | "node_modules/raw-body/node_modules/iconv-lite": { 727 | "version": "0.6.3", 728 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 729 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 730 | "dependencies": { 731 | "safer-buffer": ">= 2.1.2 < 3.0.0" 732 | }, 733 | "engines": { 734 | "node": ">=0.10.0" 735 | } 736 | }, 737 | "node_modules/router": { 738 | "version": "2.1.0", 739 | "resolved": "https://registry.npmjs.org/router/-/router-2.1.0.tgz", 740 | "integrity": "sha512-/m/NSLxeYEgWNtyC+WtNHCF7jbGxOibVWKnn+1Psff4dJGOfoXP+MuC/f2CwSmyiHdOIzYnYFp4W6GxWfekaLA==", 741 | "dependencies": { 742 | "is-promise": "^4.0.0", 743 | "parseurl": "^1.3.3", 744 | "path-to-regexp": "^8.0.0" 745 | }, 746 | "engines": { 747 | "node": ">= 18" 748 | } 749 | }, 750 | "node_modules/safe-buffer": { 751 | "version": "5.2.1", 752 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 753 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 754 | "funding": [ 755 | { 756 | "type": "github", 757 | "url": "https://github.com/sponsors/feross" 758 | }, 759 | { 760 | "type": "patreon", 761 | "url": "https://www.patreon.com/feross" 762 | }, 763 | { 764 | "type": "consulting", 765 | "url": "https://feross.org/support" 766 | } 767 | ] 768 | }, 769 | "node_modules/safer-buffer": { 770 | "version": "2.1.2", 771 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 772 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 773 | }, 774 | "node_modules/send": { 775 | "version": "1.1.0", 776 | "resolved": "https://registry.npmjs.org/send/-/send-1.1.0.tgz", 777 | "integrity": "sha512-v67WcEouB5GxbTWL/4NeToqcZiAWEq90N888fczVArY8A79J0L4FD7vj5hm3eUMua5EpoQ59wa/oovY6TLvRUA==", 778 | "dependencies": { 779 | "debug": "^4.3.5", 780 | "destroy": "^1.2.0", 781 | "encodeurl": "^2.0.0", 782 | "escape-html": "^1.0.3", 783 | "etag": "^1.8.1", 784 | "fresh": "^0.5.2", 785 | "http-errors": "^2.0.0", 786 | "mime-types": "^2.1.35", 787 | "ms": "^2.1.3", 788 | "on-finished": "^2.4.1", 789 | "range-parser": "^1.2.1", 790 | "statuses": "^2.0.1" 791 | }, 792 | "engines": { 793 | "node": ">= 18" 794 | } 795 | }, 796 | "node_modules/send/node_modules/fresh": { 797 | "version": "0.5.2", 798 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", 799 | "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", 800 | "engines": { 801 | "node": ">= 0.6" 802 | } 803 | }, 804 | "node_modules/send/node_modules/mime-db": { 805 | "version": "1.52.0", 806 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 807 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 808 | "engines": { 809 | "node": ">= 0.6" 810 | } 811 | }, 812 | "node_modules/send/node_modules/mime-types": { 813 | "version": "2.1.35", 814 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 815 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 816 | "dependencies": { 817 | "mime-db": "1.52.0" 818 | }, 819 | "engines": { 820 | "node": ">= 0.6" 821 | } 822 | }, 823 | "node_modules/send/node_modules/ms": { 824 | "version": "2.1.3", 825 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 826 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 827 | }, 828 | "node_modules/serve-static": { 829 | "version": "2.1.0", 830 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.1.0.tgz", 831 | "integrity": "sha512-A3We5UfEjG8Z7VkDv6uItWw6HY2bBSBJT1KtVESn6EOoOr2jAxNhxWCLY3jDE2WcuHXByWju74ck3ZgLwL8xmA==", 832 | "dependencies": { 833 | "encodeurl": "^2.0.0", 834 | "escape-html": "^1.0.3", 835 | "parseurl": "^1.3.3", 836 | "send": "^1.0.0" 837 | }, 838 | "engines": { 839 | "node": ">= 18" 840 | } 841 | }, 842 | "node_modules/setprototypeof": { 843 | "version": "1.2.0", 844 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 845 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" 846 | }, 847 | "node_modules/side-channel": { 848 | "version": "1.1.0", 849 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 850 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 851 | "dependencies": { 852 | "es-errors": "^1.3.0", 853 | "object-inspect": "^1.13.3", 854 | "side-channel-list": "^1.0.0", 855 | "side-channel-map": "^1.0.1", 856 | "side-channel-weakmap": "^1.0.2" 857 | }, 858 | "engines": { 859 | "node": ">= 0.4" 860 | }, 861 | "funding": { 862 | "url": "https://github.com/sponsors/ljharb" 863 | } 864 | }, 865 | "node_modules/side-channel-list": { 866 | "version": "1.0.0", 867 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 868 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 869 | "dependencies": { 870 | "es-errors": "^1.3.0", 871 | "object-inspect": "^1.13.3" 872 | }, 873 | "engines": { 874 | "node": ">= 0.4" 875 | }, 876 | "funding": { 877 | "url": "https://github.com/sponsors/ljharb" 878 | } 879 | }, 880 | "node_modules/side-channel-map": { 881 | "version": "1.0.1", 882 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 883 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 884 | "dependencies": { 885 | "call-bound": "^1.0.2", 886 | "es-errors": "^1.3.0", 887 | "get-intrinsic": "^1.2.5", 888 | "object-inspect": "^1.13.3" 889 | }, 890 | "engines": { 891 | "node": ">= 0.4" 892 | }, 893 | "funding": { 894 | "url": "https://github.com/sponsors/ljharb" 895 | } 896 | }, 897 | "node_modules/side-channel-weakmap": { 898 | "version": "1.0.2", 899 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 900 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 901 | "dependencies": { 902 | "call-bound": "^1.0.2", 903 | "es-errors": "^1.3.0", 904 | "get-intrinsic": "^1.2.5", 905 | "object-inspect": "^1.13.3", 906 | "side-channel-map": "^1.0.1" 907 | }, 908 | "engines": { 909 | "node": ">= 0.4" 910 | }, 911 | "funding": { 912 | "url": "https://github.com/sponsors/ljharb" 913 | } 914 | }, 915 | "node_modules/statuses": { 916 | "version": "2.0.1", 917 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 918 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 919 | "engines": { 920 | "node": ">= 0.8" 921 | } 922 | }, 923 | "node_modules/toidentifier": { 924 | "version": "1.0.1", 925 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 926 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 927 | "engines": { 928 | "node": ">=0.6" 929 | } 930 | }, 931 | "node_modules/type-is": { 932 | "version": "2.0.0", 933 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.0.tgz", 934 | "integrity": "sha512-gd0sGezQYCbWSbkZr75mln4YBidWUN60+devscpLF5mtRDUpiaTvKpBNrdaCvel1NdR2k6vclXybU5fBd2i+nw==", 935 | "dependencies": { 936 | "content-type": "^1.0.5", 937 | "media-typer": "^1.1.0", 938 | "mime-types": "^3.0.0" 939 | }, 940 | "engines": { 941 | "node": ">= 0.6" 942 | } 943 | }, 944 | "node_modules/typescript": { 945 | "version": "5.8.2", 946 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", 947 | "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", 948 | "dev": true, 949 | "bin": { 950 | "tsc": "bin/tsc", 951 | "tsserver": "bin/tsserver" 952 | }, 953 | "engines": { 954 | "node": ">=14.17" 955 | } 956 | }, 957 | "node_modules/undici-types": { 958 | "version": "6.19.8", 959 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", 960 | "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", 961 | "dev": true 962 | }, 963 | "node_modules/unpipe": { 964 | "version": "1.0.0", 965 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 966 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 967 | "engines": { 968 | "node": ">= 0.8" 969 | } 970 | }, 971 | "node_modules/utils-merge": { 972 | "version": "1.0.1", 973 | "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", 974 | "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", 975 | "engines": { 976 | "node": ">= 0.4.0" 977 | } 978 | }, 979 | "node_modules/vary": { 980 | "version": "1.1.2", 981 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 982 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 983 | "engines": { 984 | "node": ">= 0.8" 985 | } 986 | }, 987 | "node_modules/wrappy": { 988 | "version": "1.0.2", 989 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 990 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 991 | }, 992 | "node_modules/zod": { 993 | "version": "3.24.2", 994 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", 995 | "integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", 996 | "funding": { 997 | "url": "https://github.com/sponsors/colinhacks" 998 | } 999 | }, 1000 | "node_modules/zod-to-json-schema": { 1001 | "version": "3.24.5", 1002 | "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.5.tgz", 1003 | "integrity": "sha512-/AuWwMP+YqiPbsJx5D6TfgRTc4kTLjsh5SOcd4bLsfUg2RcEXrFMJl1DGgdHy2aCfsIA/cr/1JM0xcB2GZji8g==", 1004 | "peerDependencies": { 1005 | "zod": "^3.24.1" 1006 | } 1007 | } 1008 | } 1009 | } 1010 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sinco-lab/mcp-youtube-transcript", 3 | "version": "0.0.8", 4 | "description": "A server built on the Model Context Protocol (MCP) that enables direct downloading of YouTube video transcripts, supporting AI and video analysis workflows.", 5 | "license": "MIT", 6 | "author": "sinco", 7 | "homepage": "https://github.com/sinco-lab/mcp-youtube-transcript", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/sinco-lab/mcp-youtube-transcript.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/sinco-lab/mcp-youtube-transcript/issues" 14 | }, 15 | "keywords": [ 16 | "mcp", 17 | "youtube", 18 | "transcript", 19 | "subtitles", 20 | "captions", 21 | "video", 22 | "ai", 23 | "claude", 24 | "cursor", 25 | "cline", 26 | "modelcontextprotocol" 27 | ], 28 | "type": "module", 29 | "publishConfig": { 30 | "access": "public" 31 | }, 32 | "main": "dist/index.js", 33 | "module": "dist/index.js", 34 | "bin": { 35 | "mcp-youtube-transcript": "dist/index.js" 36 | }, 37 | "files": [ 38 | "dist", 39 | "README.md", 40 | "LICENSE" 41 | ], 42 | "engines": { 43 | "node": ">=18.0.0" 44 | }, 45 | "scripts": { 46 | "build": "tsc && chmod +x dist/*.js", 47 | "clean": "rm -rf dist", 48 | "prepublishOnly": "npm run clean && npm run build", 49 | "release:patch": "npm version patch && git push origin v$(node -p \"require('./package.json').version\")", 50 | "release:minor": "npm version minor && git push origin v$(node -p \"require('./package.json').version\")", 51 | "release:major": "npm version major && git push origin v$(node -p \"require('./package.json').version\")" 52 | }, 53 | "dependencies": { 54 | "@modelcontextprotocol/sdk": "1.7.0", 55 | "zod": "^3.24.2" 56 | }, 57 | "devDependencies": { 58 | "@types/node": "^20.11.24", 59 | "typescript": "^5.6.2" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- 1 | # Smithery configuration file: https://smithery.ai/docs/config 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | commandFunction: 9 | # A function that produces the CLI command to start the MCP on stdio. 10 | |- 11 | config => ({ command: 'node', args: ['dist/index.js'] }) -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; 4 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; 5 | import { McpError } from "@modelcontextprotocol/sdk/types.js"; 6 | import { YouTubeTranscriptFetcher, YouTubeUtils, YouTubeTranscriptError, TranscriptOptions, Transcript } from './youtube.js'; 7 | import { z } from "zod"; 8 | 9 | class YouTubeTranscriptExtractor { 10 | /** 11 | * Extracts YouTube video ID from various URL formats or direct ID input 12 | */ 13 | extractYoutubeId(input: string): string { 14 | return YouTubeTranscriptFetcher.extractVideoId(input); 15 | } 16 | 17 | /** 18 | * Retrieves transcripts for a given video ID and language 19 | */ 20 | async getTranscripts({ videoID, lang }: TranscriptOptions): Promise<{ transcripts: Transcript[], title: string }> { 21 | try { 22 | const result = await YouTubeTranscriptFetcher.fetchTranscripts(videoID, { lang }); 23 | if (result.transcripts.length === 0) { 24 | throw new YouTubeTranscriptError('No transcripts found'); 25 | } 26 | return result; 27 | } catch (error) { 28 | if (error instanceof YouTubeTranscriptError || error instanceof McpError) { 29 | throw error; 30 | } 31 | throw new YouTubeTranscriptError(`Failed to fetch transcripts: ${(error as Error).message}`); 32 | } 33 | } 34 | } 35 | 36 | class TranscriptServer { 37 | private extractor: YouTubeTranscriptExtractor; 38 | private server: McpServer; 39 | 40 | constructor() { 41 | this.extractor = new YouTubeTranscriptExtractor(); 42 | this.server = new McpServer({ 43 | name: "mcp-youtube-transcript", 44 | version: "0.0.1", 45 | description: "A server built on the Model Context Protocol (MCP) that enables direct downloading of YouTube video transcripts, supporting AI and video analysis workflows." 46 | }); 47 | 48 | this.setupTools(); 49 | this.setupErrorHandling(); 50 | } 51 | 52 | private setupErrorHandling(): void { 53 | process.on('SIGINT', async () => { 54 | await this.stop(); 55 | process.exit(0); 56 | }); 57 | } 58 | 59 | private setupTools(): void { 60 | this.server.tool( 61 | "get_transcripts", 62 | `Extract and process transcripts from a YouTube video.\n\n**Parameters:**\n- \`url\` (string, required): YouTube video URL or ID.\n- \`lang\` (string, optional, default 'en'): Language code for transcripts (e.g. 'en', 'uk', 'ja', 'ru', 'zh').\n- \`enableParagraphs\` (boolean, optional, default false): Enable automatic paragraph breaks.\n\n**IMPORTANT:** If the user does *not* specify a language *code*, **DO NOT** include the \`lang\` parameter in the tool call. Do not guess the language or use parts of the user query as the language code.`, 63 | { 64 | url: z.string().describe("YouTube video URL or ID"), 65 | lang: z.string().default("en").describe("Language code for transcripts, default 'en' (e.g. 'en', 'uk', 'ja', 'ru', 'zh')"), 66 | enableParagraphs: z.boolean().default(false).describe("Enable automatic paragraph breaks, default `false`") 67 | }, 68 | async (input) => { 69 | try { 70 | const videoId = this.extractor.extractYoutubeId(input.url); 71 | console.error(`Processing transcripts for video: ${videoId}`); 72 | 73 | const { transcripts, title } = await this.extractor.getTranscripts({ 74 | videoID: videoId, 75 | lang: input.lang 76 | }); 77 | 78 | // Format text with optional paragraph breaks 79 | const formattedText = YouTubeUtils.formatTranscriptText(transcripts, { 80 | enableParagraphs: input.enableParagraphs 81 | }); 82 | 83 | console.error(`Successfully extracted transcripts for "${title}" (${formattedText.length} chars)`); 84 | 85 | return { 86 | content: [{ 87 | type: "text", 88 | text: `# ${title}\n\n${formattedText}`, 89 | metadata: { 90 | videoId, 91 | title, 92 | language: input.lang, 93 | timestamp: new Date().toISOString(), 94 | charCount: formattedText.length, 95 | transcriptCount: transcripts.length, 96 | totalDuration: YouTubeUtils.calculateTotalDuration(transcripts), 97 | paragraphsEnabled: input.enableParagraphs 98 | } 99 | }] 100 | }; 101 | } catch (error) { 102 | if (error instanceof YouTubeTranscriptError || error instanceof McpError) { 103 | throw error; 104 | } 105 | throw new YouTubeTranscriptError(`Failed to process transcripts: ${(error as Error).message}`); 106 | } 107 | } 108 | ); 109 | } 110 | 111 | async start(): Promise { 112 | const transport = new StdioServerTransport(); 113 | await this.server.connect(transport); 114 | } 115 | 116 | async stop(): Promise { 117 | await this.server.close(); 118 | } 119 | } 120 | 121 | async function main() { 122 | try { 123 | const server = new TranscriptServer(); 124 | await server.start(); 125 | } catch (error) { 126 | console.error('Server error:', error); 127 | process.exit(1); 128 | } 129 | } 130 | 131 | main(); -------------------------------------------------------------------------------- /src/youtube.ts: -------------------------------------------------------------------------------- 1 | import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js"; 2 | 3 | // Types 4 | export interface Transcript { 5 | text: string; // Transcript text 6 | lang?: string; // Language code 7 | timestamp: number; // Start time in seconds 8 | duration: number; // Duration in seconds 9 | } 10 | 11 | export interface TranscriptOptions { 12 | videoID: string; // Video ID or URL 13 | lang?: string; // Language code, default 'en' 14 | } 15 | 16 | // Constants 17 | const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36'; 18 | const ADDITIONAL_HEADERS = { 19 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', 20 | 'Accept-Language': 'en-US,en;q=0.9' 21 | }; 22 | 23 | // Error handling 24 | export class YouTubeTranscriptError extends McpError { 25 | constructor(message: string) { 26 | super(ErrorCode.InternalError, message); 27 | this.name = 'YouTubeTranscriptError'; 28 | } 29 | } 30 | 31 | // Utility functions 32 | export class YouTubeUtils { 33 | /** 34 | * Format time (convert seconds to readable format) 35 | */ 36 | static formatTime(seconds: number): string { 37 | const hours = Math.floor(seconds / 3600); 38 | const minutes = Math.floor((seconds % 3600) / 60); 39 | const secs = Math.floor(seconds % 60); 40 | const ms = Math.floor((seconds % 1) * 1000); 41 | 42 | return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}.${ms.toString().padStart(3, '0')}`; 43 | } 44 | 45 | /** 46 | * Calculate total duration in seconds 47 | */ 48 | static calculateTotalDuration(items: Transcript[]): number { 49 | return items.reduce((acc, item) => Math.max(acc, item.timestamp + item.duration), 0); 50 | } 51 | 52 | /** 53 | * Decode HTML entities 54 | */ 55 | static decodeHTML(text: string): string { 56 | const entities: { [key: string]: string } = { 57 | '&': '&', 58 | '<': '<', 59 | '>': '>', 60 | '"': '"', 61 | ''': "'", 62 | ''': "'", 63 | ''': "'", 64 | '/': '/', 65 | '/': '/', 66 | '/': '/', 67 | ' ': ' ', 68 | ' ': ' ' 69 | }; 70 | 71 | return text.replace(/&[^;]+;/g, match => entities[match] || match).trim(); 72 | } 73 | 74 | /** 75 | * Normalize text formatting (punctuation and spaces) 76 | */ 77 | static normalizeText(text: string): string { 78 | return text 79 | .replace(/\n/g, ' ') 80 | .replace(/\s*\.\s*\.\s*/g, '. ') // Fix multiple dots 81 | .replace(/\s*\.\s+/g, '. ') // Normalize spaces after dots 82 | .replace(/\s+/g, ' ') // Normalize spaces 83 | .replace(/\s+([,.])/g, '$1') // Fix spaces before punctuation 84 | .replace(/\s*\?\s*/g, '? ') // Normalize question marks 85 | .replace(/\s*!\s*/g, '! ') // Normalize exclamation marks 86 | .trim(); 87 | } 88 | 89 | /** 90 | * Format transcript text with optional paragraph breaks 91 | */ 92 | static formatTranscriptText( 93 | transcripts: Transcript[], 94 | options: { 95 | enableParagraphs?: boolean; 96 | timeGapThreshold?: number; 97 | maxSentencesPerParagraph?: number; 98 | } = {} 99 | ): string { 100 | const { 101 | enableParagraphs = false, 102 | timeGapThreshold = 2, 103 | maxSentencesPerParagraph = 5 104 | } = options; 105 | 106 | // Process each transcript text 107 | const processedTranscripts = transcripts 108 | .map(transcript => this.decodeHTML(transcript.text)) 109 | .filter(text => text.length > 0); 110 | 111 | if (!enableParagraphs) { 112 | // Simple concatenation mode with normalized formatting 113 | return this.normalizeText(processedTranscripts.join(' ')); 114 | } 115 | 116 | // Paragraph mode 117 | const paragraphs: string[] = []; 118 | let currentParagraph: string[] = []; 119 | let lastEndTime = 0; 120 | 121 | for (let i = 0; i < transcripts.length; i++) { 122 | const transcript = transcripts[i]; 123 | const text = this.decodeHTML(transcript.text.trim()); 124 | if (!text) continue; 125 | 126 | const timeGap = transcript.timestamp - lastEndTime; 127 | const previousText = currentParagraph[currentParagraph.length - 1] || ''; 128 | 129 | const shouldStartNewParagraph = 130 | timeGap > timeGapThreshold || 131 | (previousText.endsWith('.') && /^[A-Z]/.test(text)) || 132 | currentParagraph.length >= maxSentencesPerParagraph; 133 | 134 | if (shouldStartNewParagraph && currentParagraph.length > 0) { 135 | paragraphs.push(this.normalizeText(currentParagraph.join(' '))); 136 | currentParagraph = []; 137 | } 138 | 139 | currentParagraph.push(text); 140 | lastEndTime = transcript.timestamp + transcript.duration; 141 | } 142 | 143 | if (currentParagraph.length > 0) { 144 | paragraphs.push(this.normalizeText(currentParagraph.join(' '))); 145 | } 146 | 147 | return paragraphs.join('\n\n'); 148 | } 149 | } 150 | 151 | const MAX_RETRIES = 3; 152 | const RETRY_DELAY = 1000; // 1 second 153 | 154 | // Utility function for delay 155 | const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); 156 | 157 | // Rate limit error detection 158 | const isRateLimitError = (html: string): boolean => { 159 | return html.includes('class="g-recaptcha"') || 160 | html.includes('sorry/index') || 161 | html.includes('consent.youtube.com'); 162 | }; 163 | 164 | // Main YouTube functionality 165 | export class YouTubeTranscriptFetcher { 166 | /** 167 | * Fetch video title using oEmbed API 168 | */ 169 | private static async fetchVideoTitle(videoId: string): Promise { 170 | try { 171 | const response = await fetch( 172 | `https://www.youtube.com/oembed?url=http://www.youtube.com/watch?v=${videoId}&format=json` 173 | ); 174 | if (!response.ok) { 175 | throw new Error(`Failed to fetch video title (HTTP ${response.status})`); 176 | } 177 | const data = await response.json(); 178 | return YouTubeUtils.decodeHTML(data.title); 179 | } catch (error) { 180 | console.error(`Failed to fetch video title: ${error}`); 181 | return 'Untitled Video'; 182 | } 183 | } 184 | 185 | private static async fetchWithRetry( 186 | url: string, 187 | options: RequestInit, 188 | retries = MAX_RETRIES 189 | ): Promise { 190 | try { 191 | const response = await fetch(url, options); 192 | if (!response.ok) { 193 | throw new Error(`HTTP error! status: ${response.status}`); 194 | } 195 | return response; 196 | } catch (error) { 197 | if (retries > 0) { 198 | console.warn(`Fetch failed, retrying... (${retries} attempts left)`); 199 | await delay(RETRY_DELAY); 200 | return this.fetchWithRetry(url, options, retries - 1); 201 | } 202 | throw error; 203 | } 204 | } 205 | 206 | /** 207 | * Fetch transcript configuration and content from YouTube video page 208 | */ 209 | private static async fetchTranscriptConfigAndContent(videoId: string, lang?: string): Promise<{ baseUrl: string, languageCode: string, transcripts: Transcript[] }> { 210 | const headers = { 211 | ...ADDITIONAL_HEADERS, 212 | ...(lang && { 'Accept-Language': lang }), 213 | 'User-Agent': USER_AGENT 214 | }; 215 | 216 | try { 217 | const response = await this.fetchWithRetry(`https://www.youtube.com/watch?v=${videoId}`, { headers }); 218 | const html = await response.text(); 219 | 220 | if (isRateLimitError(html)) { 221 | throw new YouTubeTranscriptError( 222 | 'YouTube rate limit detected. This could be due to:\n' + 223 | '1. Too many requests from your IP\n' + 224 | '2. YouTube requiring CAPTCHA verification\n' + 225 | '3. Regional restrictions\n' + 226 | 'Try:\n' + 227 | '- Waiting a few minutes\n' + 228 | '- Using a different IP address\n' + 229 | '- Using a VPN service' 230 | ); 231 | } 232 | 233 | // Debug log for development 234 | if (process.env.NODE_ENV === 'development') { 235 | console.debug('YouTube response length:', html.length); 236 | console.debug('Contains captions:', html.includes('"captions":')); 237 | } 238 | 239 | const splittedHTML = html.split('"captions":'); 240 | 241 | if (splittedHTML.length <= 1) { 242 | // Try alternative parsing method 243 | const captionsMatch = html.match(/"playerCaptionsTracklistRenderer":\s*({[^}]+})/); 244 | if (captionsMatch) { 245 | try { 246 | const captionsData = JSON.parse(captionsMatch[1]); 247 | if (captionsData.captionTracks) { 248 | const tracks = captionsData.captionTracks; 249 | const selectedTrack = lang 250 | ? tracks.find((track: any) => track.languageCode === lang) 251 | : tracks[0]; 252 | 253 | if (selectedTrack) { 254 | return this.fetchTranscriptContent(selectedTrack, lang); 255 | } 256 | } 257 | } catch (e) { 258 | console.error('Failed to parse alternative captions data:', e); 259 | } 260 | } 261 | 262 | if (!html.includes('"playabilityStatus":')) { 263 | throw new YouTubeTranscriptError(`Video ${videoId} is unavailable`); 264 | } 265 | throw new YouTubeTranscriptError(`Could not find transcript data for video ${videoId}. Response size: ${html.length}`); 266 | } 267 | 268 | try { 269 | const transcriptData = JSON.parse(splittedHTML[1].split(',"videoDetails')[0].replace('\n', '')); 270 | const transcripts = transcriptData?.playerCaptionsTracklistRenderer; 271 | 272 | if (!transcripts || !('captionTracks' in transcripts)) { 273 | throw new YouTubeTranscriptError(`No transcripts available for video ${videoId}`); 274 | } 275 | 276 | const tracks = transcripts.captionTracks as { languageCode: string; baseUrl: string }[]; 277 | if (lang && !tracks.some((track) => track.languageCode === lang)) { 278 | const availableLangs = tracks.map((track) => track.languageCode); 279 | throw new YouTubeTranscriptError( 280 | `Language ${lang} not available for video ${videoId}. Available languages: ${availableLangs.join(', ')}` 281 | ); 282 | } 283 | 284 | const selectedTrack = lang 285 | ? tracks.find((track) => track.languageCode === lang) 286 | : tracks[0]; 287 | 288 | if (!selectedTrack) { 289 | throw new YouTubeTranscriptError(`Could not find transcript track for video ${videoId}`); 290 | } 291 | 292 | // Fetch transcript content 293 | const transcriptResponse = await this.fetchTranscriptContent(selectedTrack, lang); 294 | 295 | return { 296 | baseUrl: selectedTrack.baseUrl, 297 | languageCode: selectedTrack.languageCode, 298 | transcripts: transcriptResponse.transcripts.sort((a, b) => a.timestamp - b.timestamp) 299 | }; 300 | } catch (error) { 301 | if (error instanceof YouTubeTranscriptError) { 302 | throw error; 303 | } 304 | throw new YouTubeTranscriptError(`Failed to parse transcript data: ${(error as Error).message}`); 305 | } 306 | } catch (error) { 307 | if (error instanceof YouTubeTranscriptError) { 308 | throw error; 309 | } 310 | throw new YouTubeTranscriptError( 311 | `Failed to fetch transcript data: ${(error as Error).message}\n` + 312 | 'This might be due to network issues or YouTube rate limiting.' 313 | ); 314 | } 315 | } 316 | 317 | /** 318 | * Helper method to fetch transcript content 319 | */ 320 | private static async fetchTranscriptContent( 321 | track: { languageCode: string; baseUrl: string }, 322 | lang?: string 323 | ): Promise<{ baseUrl: string; languageCode: string; transcripts: Transcript[] }> { 324 | const headers = { 325 | ...ADDITIONAL_HEADERS, 326 | ...(lang && { 'Accept-Language': lang }), 327 | 'User-Agent': USER_AGENT, 328 | 'Referer': 'https://www.youtube.com/', 329 | 'Origin': 'https://www.youtube.com' 330 | }; 331 | 332 | try { 333 | const response = await this.fetchWithRetry(track.baseUrl, { headers }); 334 | const xml = await response.text(); 335 | const results: Transcript[] = []; 336 | 337 | // Use regex to parse XML 338 | const regex = /]*>([^<]*)<\/text>/g; 339 | let match; 340 | 341 | while ((match = regex.exec(xml)) !== null) { 342 | const start = parseFloat(match[1]); 343 | const duration = parseFloat(match[2]); 344 | const text = YouTubeUtils.decodeHTML(match[3]); 345 | 346 | if (text.trim()) { 347 | results.push({ 348 | text: text.trim(), 349 | lang: track.languageCode, 350 | timestamp: start, 351 | duration: duration 352 | }); 353 | } 354 | } 355 | 356 | return { 357 | baseUrl: track.baseUrl, 358 | languageCode: track.languageCode, 359 | transcripts: results.sort((a, b) => a.timestamp - b.timestamp) 360 | }; 361 | } catch (error) { 362 | throw new YouTubeTranscriptError( 363 | `Failed to fetch transcript content: ${(error as Error).message}\n` + 364 | 'This might be due to network issues or YouTube rate limiting.' 365 | ); 366 | } 367 | } 368 | 369 | /** 370 | * Extract video ID from YouTube URL or direct ID input 371 | */ 372 | static extractVideoId(input: string): string { 373 | if (!input) { 374 | throw new McpError( 375 | ErrorCode.InvalidParams, 376 | 'YouTube URL or ID is required' 377 | ); 378 | } 379 | 380 | // If input is an 11-digit video ID 381 | if (/^[a-zA-Z0-9_-]{11}$/.test(input)) { 382 | return input; 383 | } 384 | 385 | // Handle URL formats 386 | try { 387 | const url = new URL(input); 388 | if (url.hostname === 'youtu.be') { 389 | return url.pathname.slice(1); 390 | } else if (url.hostname.includes('youtube.com')) { 391 | // Handle shorts URL format 392 | if (url.pathname.startsWith('/shorts/')) { 393 | return url.pathname.slice(8); 394 | } 395 | const videoId = url.searchParams.get('v'); 396 | if (!videoId) { 397 | throw new McpError( 398 | ErrorCode.InvalidParams, 399 | `Invalid YouTube URL: ${input}` 400 | ); 401 | } 402 | return videoId; 403 | } 404 | } catch (error) { 405 | // URL parsing failed, try regex matching 406 | const match = input.match(/(?:youtube\.com\/(?:[^/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?/\s]{11})/); 407 | if (match) { 408 | return match[1]; 409 | } 410 | } 411 | 412 | throw new McpError( 413 | ErrorCode.InvalidParams, 414 | `Could not extract video ID from: ${input}` 415 | ); 416 | } 417 | 418 | /** 419 | * Fetch transcripts and video information 420 | */ 421 | static async fetchTranscripts(videoId: string, config?: { lang?: string }): Promise<{ transcripts: Transcript[], title: string }> { 422 | try { 423 | const identifier = this.extractVideoId(videoId); 424 | const [{ transcripts }, title] = await Promise.all([ 425 | this.fetchTranscriptConfigAndContent(identifier, config?.lang), 426 | this.fetchVideoTitle(identifier) 427 | ]); 428 | 429 | return { transcripts, title }; 430 | } catch (error) { 431 | if (error instanceof YouTubeTranscriptError || error instanceof McpError) { 432 | throw error; 433 | } 434 | throw new YouTubeTranscriptError(`Failed to fetch transcripts: ${(error as Error).message}`); 435 | } 436 | } 437 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2022", 4 | "module": "Node16", 5 | "moduleResolution": "Node16", 6 | "outDir": "./dist", 7 | "rootDir": "./src", 8 | "strict": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "resolveJsonModule": true 13 | }, 14 | "include": ["src/*"], 15 | "exclude": ["node_modules"] 16 | } 17 | --------------------------------------------------------------------------------