├── .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 | [](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 |
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 | 
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 |
--------------------------------------------------------------------------------