youtube-knowledge-mcp 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -34
- package/dist/brain-schemas.d.ts +10 -10
- package/dist/brain-schemas.d.ts.map +1 -1
- package/dist/brain-schemas.js +0 -4
- package/dist/brain-schemas.js.map +1 -1
- package/dist/http/app.d.ts +16 -0
- package/dist/http/app.d.ts.map +1 -0
- package/dist/http/app.js +176 -0
- package/dist/http/app.js.map +1 -0
- package/dist/http/auth.d.ts +38 -0
- package/dist/http/auth.d.ts.map +1 -0
- package/dist/http/auth.js +79 -0
- package/dist/http/auth.js.map +1 -0
- package/dist/http/config.d.ts +41 -0
- package/dist/http/config.d.ts.map +1 -0
- package/dist/http/config.js +65 -0
- package/dist/http/config.js.map +1 -0
- package/dist/http/rate-limiter.d.ts +24 -0
- package/dist/http/rate-limiter.d.ts.map +1 -0
- package/dist/http/rate-limiter.js +55 -0
- package/dist/http/rate-limiter.js.map +1 -0
- package/dist/http/sessions.d.ts +34 -0
- package/dist/http/sessions.d.ts.map +1 -0
- package/dist/http/sessions.js +62 -0
- package/dist/http/sessions.js.map +1 -0
- package/dist/http.d.ts +6 -48
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +35 -252
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -517
- package/dist/index.js.map +1 -1
- package/dist/prompts-brain.d.ts +4 -0
- package/dist/prompts-brain.d.ts.map +1 -0
- package/dist/prompts-brain.js +48 -0
- package/dist/prompts-brain.js.map +1 -0
- package/dist/prompts-research.d.ts +4 -0
- package/dist/prompts-research.d.ts.map +1 -0
- package/dist/prompts-research.js +74 -0
- package/dist/prompts-research.js.map +1 -0
- package/dist/prompts-shared.d.ts +13 -0
- package/dist/prompts-shared.d.ts.map +1 -0
- package/dist/prompts-shared.js +7 -0
- package/dist/prompts-shared.js.map +1 -0
- package/dist/prompts.d.ts.map +1 -1
- package/dist/prompts.js +5 -100
- package/dist/prompts.js.map +1 -1
- package/dist/registry/brain-tools.d.ts +4 -0
- package/dist/registry/brain-tools.d.ts.map +1 -0
- package/dist/registry/brain-tools.js +104 -0
- package/dist/registry/brain-tools.js.map +1 -0
- package/dist/registry/channel-tools.d.ts +4 -0
- package/dist/registry/channel-tools.d.ts.map +1 -0
- package/dist/registry/channel-tools.js +53 -0
- package/dist/registry/channel-tools.js.map +1 -0
- package/dist/registry/health-tools.d.ts +4 -0
- package/dist/registry/health-tools.d.ts.map +1 -0
- package/dist/registry/health-tools.js +21 -0
- package/dist/registry/health-tools.js.map +1 -0
- package/dist/registry/index.d.ts +8 -0
- package/dist/registry/index.d.ts.map +1 -0
- package/dist/registry/index.js +27 -0
- package/dist/registry/index.js.map +1 -0
- package/dist/registry/library-tools.d.ts +4 -0
- package/dist/registry/library-tools.d.ts.map +1 -0
- package/dist/registry/library-tools.js +116 -0
- package/dist/registry/library-tools.js.map +1 -0
- package/dist/registry/media-tools.d.ts +4 -0
- package/dist/registry/media-tools.d.ts.map +1 -0
- package/dist/registry/media-tools.js +100 -0
- package/dist/registry/media-tools.js.map +1 -0
- package/dist/registry/thumbnail-tools.d.ts +7 -0
- package/dist/registry/thumbnail-tools.d.ts.map +1 -0
- package/dist/registry/thumbnail-tools.js +72 -0
- package/dist/registry/thumbnail-tools.js.map +1 -0
- package/dist/registry/transcript-tools.d.ts +4 -0
- package/dist/registry/transcript-tools.d.ts.map +1 -0
- package/dist/registry/transcript-tools.js +68 -0
- package/dist/registry/transcript-tools.js.map +1 -0
- package/dist/registry/types.d.ts +41 -0
- package/dist/registry/types.d.ts.map +1 -0
- package/dist/registry/types.js +17 -0
- package/dist/registry/types.js.map +1 -0
- package/dist/registry/video-tools.d.ts +4 -0
- package/dist/registry/video-tools.d.ts.map +1 -0
- package/dist/registry/video-tools.js +101 -0
- package/dist/registry/video-tools.js.map +1 -0
- package/dist/resources.d.ts.map +1 -1
- package/dist/resources.js +13 -10
- package/dist/resources.js.map +1 -1
- package/dist/schemas.d.ts +11 -2
- package/dist/schemas.d.ts.map +1 -1
- package/dist/schemas.js +5 -0
- package/dist/schemas.js.map +1 -1
- package/dist/thumbnail-resource.d.ts +8 -0
- package/dist/thumbnail-resource.d.ts.map +1 -0
- package/dist/thumbnail-resource.js +38 -0
- package/dist/thumbnail-resource.js.map +1 -0
- package/dist/thumbnail-schemas.d.ts +512 -0
- package/dist/thumbnail-schemas.d.ts.map +1 -0
- package/dist/thumbnail-schemas.js +95 -0
- package/dist/thumbnail-schemas.js.map +1 -0
- package/dist/tools/batch.js +1 -1
- package/dist/tools/batch.js.map +1 -1
- package/dist/tools/build-brain.js +1 -1
- package/dist/tools/build-brain.js.map +1 -1
- package/dist/tools/check-health.d.ts +2 -0
- package/dist/tools/check-health.d.ts.map +1 -1
- package/dist/tools/check-health.js +9 -0
- package/dist/tools/check-health.js.map +1 -1
- package/dist/tools/clips.d.ts +9 -41
- package/dist/tools/clips.d.ts.map +1 -1
- package/dist/tools/clips.js +52 -128
- package/dist/tools/clips.js.map +1 -1
- package/dist/tools/fetch-channel-thumbnails.d.ts +179 -0
- package/dist/tools/fetch-channel-thumbnails.d.ts.map +1 -0
- package/dist/tools/fetch-channel-thumbnails.js +123 -0
- package/dist/tools/fetch-channel-thumbnails.js.map +1 -0
- package/dist/tools/fetch-videos.d.ts +3 -0
- package/dist/tools/fetch-videos.d.ts.map +1 -1
- package/dist/tools/fetch-videos.js +3 -0
- package/dist/tools/fetch-videos.js.map +1 -1
- package/dist/tools/frames-subtitles.d.ts +44 -0
- package/dist/tools/frames-subtitles.d.ts.map +1 -0
- package/dist/tools/frames-subtitles.js +104 -0
- package/dist/tools/frames-subtitles.js.map +1 -0
- package/dist/tools/get-channel-info.d.ts +2 -0
- package/dist/tools/get-channel-info.d.ts.map +1 -1
- package/dist/tools/get-channel-info.js +8 -1
- package/dist/tools/get-channel-info.js.map +1 -1
- package/dist/tools/get-playlist-info.d.ts.map +1 -1
- package/dist/tools/get-playlist-info.js +1 -0
- package/dist/tools/get-playlist-info.js.map +1 -1
- package/dist/tools/get-thumbnail.d.ts +38 -0
- package/dist/tools/get-thumbnail.d.ts.map +1 -0
- package/dist/tools/get-thumbnail.js +152 -0
- package/dist/tools/get-thumbnail.js.map +1 -0
- package/dist/tools/get-transcript.d.ts.map +1 -1
- package/dist/tools/get-transcript.js +2 -14
- package/dist/tools/get-transcript.js.map +1 -1
- package/dist/tools/library.d.ts.map +1 -1
- package/dist/tools/library.js +13 -3
- package/dist/tools/library.js.map +1 -1
- package/dist/tools/list-channel-thumbnails.d.ts +235 -0
- package/dist/tools/list-channel-thumbnails.d.ts.map +1 -0
- package/dist/tools/list-channel-thumbnails.js +79 -0
- package/dist/tools/list-channel-thumbnails.js.map +1 -0
- package/dist/tools/save-to-library.d.ts.map +1 -1
- package/dist/tools/save-to-library.js +5 -1
- package/dist/tools/save-to-library.js.map +1 -1
- package/dist/tools/search-channels.d.ts +6 -0
- package/dist/tools/search-channels.d.ts.map +1 -1
- package/dist/tools/search-transcript.d.ts.map +1 -1
- package/dist/tools/search-transcript.js +6 -13
- package/dist/tools/search-transcript.js.map +1 -1
- package/dist/tools/search-videos.d.ts +3 -0
- package/dist/tools/search-videos.d.ts.map +1 -1
- package/dist/utils/batches.d.ts +15 -0
- package/dist/utils/batches.d.ts.map +1 -0
- package/dist/utils/batches.js +31 -0
- package/dist/utils/batches.js.map +1 -0
- package/dist/utils/brain-build.d.ts.map +1 -1
- package/dist/utils/brain-build.js +1 -12
- package/dist/utils/brain-build.js.map +1 -1
- package/dist/utils/brain-index.d.ts.map +1 -1
- package/dist/utils/brain-index.js +3 -2
- package/dist/utils/brain-index.js.map +1 -1
- package/dist/utils/brain-lock.d.ts +2 -18
- package/dist/utils/brain-lock.d.ts.map +1 -1
- package/dist/utils/brain-lock.js +4 -80
- package/dist/utils/brain-lock.js.map +1 -1
- package/dist/utils/brain-lookup.d.ts +8 -0
- package/dist/utils/brain-lookup.d.ts.map +1 -1
- package/dist/utils/brain-lookup.js +5 -31
- package/dist/utils/brain-lookup.js.map +1 -1
- package/dist/utils/brain-storage.d.ts.map +1 -1
- package/dist/utils/brain-storage.js +5 -5
- package/dist/utils/brain-storage.js.map +1 -1
- package/dist/utils/caption-probe.d.ts +7 -0
- package/dist/utils/caption-probe.d.ts.map +1 -0
- package/dist/utils/caption-probe.js +36 -0
- package/dist/utils/caption-probe.js.map +1 -0
- package/dist/utils/channel-images.d.ts +16 -0
- package/dist/utils/channel-images.d.ts.map +1 -0
- package/dist/utils/channel-images.js +35 -0
- package/dist/utils/channel-images.js.map +1 -0
- package/dist/utils/channel-lookup.d.ts +5 -0
- package/dist/utils/channel-lookup.d.ts.map +1 -0
- package/dist/utils/channel-lookup.js +37 -0
- package/dist/utils/channel-lookup.js.map +1 -0
- package/dist/utils/chapters.d.ts +11 -0
- package/dist/utils/chapters.d.ts.map +1 -0
- package/dist/utils/chapters.js +24 -0
- package/dist/utils/chapters.js.map +1 -0
- package/dist/utils/clips.d.ts.map +1 -1
- package/dist/utils/clips.js +18 -26
- package/dist/utils/clips.js.map +1 -1
- package/dist/utils/env.d.ts +26 -0
- package/dist/utils/env.d.ts.map +1 -0
- package/dist/utils/env.js +72 -0
- package/dist/utils/env.js.map +1 -0
- package/dist/utils/errors.d.ts +1 -1
- package/dist/utils/errors.d.ts.map +1 -1
- package/dist/utils/errors.js +20 -2
- package/dist/utils/errors.js.map +1 -1
- package/dist/utils/file-lock.d.ts +34 -0
- package/dist/utils/file-lock.d.ts.map +1 -0
- package/dist/utils/file-lock.js +91 -0
- package/dist/utils/file-lock.js.map +1 -0
- package/dist/utils/flat-listing.d.ts +89 -0
- package/dist/utils/flat-listing.d.ts.map +1 -0
- package/dist/utils/flat-listing.js +68 -0
- package/dist/utils/flat-listing.js.map +1 -0
- package/dist/utils/format.d.ts +1 -3
- package/dist/utils/format.d.ts.map +1 -1
- package/dist/utils/format.js +1 -5
- package/dist/utils/format.js.map +1 -1
- package/dist/utils/guard.d.ts +21 -0
- package/dist/utils/guard.d.ts.map +1 -0
- package/dist/utils/guard.js +105 -0
- package/dist/utils/guard.js.map +1 -0
- package/dist/utils/image-dimensions.d.ts +19 -0
- package/dist/utils/image-dimensions.d.ts.map +1 -0
- package/dist/utils/image-dimensions.js +93 -0
- package/dist/utils/image-dimensions.js.map +1 -0
- package/dist/utils/image-fetch.d.ts +31 -0
- package/dist/utils/image-fetch.d.ts.map +1 -0
- package/dist/utils/image-fetch.js +141 -0
- package/dist/utils/image-fetch.js.map +1 -0
- package/dist/utils/json-file.d.ts +14 -5
- package/dist/utils/json-file.d.ts.map +1 -1
- package/dist/utils/json-file.js +24 -9
- package/dist/utils/json-file.js.map +1 -1
- package/dist/utils/paths.d.ts +10 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +14 -0
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/pattern.d.ts +30 -0
- package/dist/utils/pattern.d.ts.map +1 -0
- package/dist/utils/pattern.js +130 -0
- package/dist/utils/pattern.js.map +1 -0
- package/dist/utils/search-index.d.ts.map +1 -1
- package/dist/utils/search-index.js +9 -4
- package/dist/utils/search-index.js.map +1 -1
- package/dist/utils/storage.d.ts.map +1 -1
- package/dist/utils/storage.js +14 -8
- package/dist/utils/storage.js.map +1 -1
- package/dist/utils/subtitles.d.ts +7 -0
- package/dist/utils/subtitles.d.ts.map +1 -0
- package/dist/utils/subtitles.js +30 -0
- package/dist/utils/subtitles.js.map +1 -0
- package/dist/utils/thumbnail-entry.d.ts +19 -0
- package/dist/utils/thumbnail-entry.d.ts.map +1 -0
- package/dist/utils/thumbnail-entry.js +119 -0
- package/dist/utils/thumbnail-entry.js.map +1 -0
- package/dist/utils/thumbnail-fetch.d.ts +37 -0
- package/dist/utils/thumbnail-fetch.d.ts.map +1 -0
- package/dist/utils/thumbnail-fetch.js +89 -0
- package/dist/utils/thumbnail-fetch.js.map +1 -0
- package/dist/utils/thumbnail-ladder.d.ts +44 -0
- package/dist/utils/thumbnail-ladder.d.ts.map +1 -0
- package/dist/utils/thumbnail-ladder.js +68 -0
- package/dist/utils/thumbnail-ladder.js.map +1 -0
- package/dist/utils/thumbnail-listing.d.ts +16 -0
- package/dist/utils/thumbnail-listing.d.ts.map +1 -0
- package/dist/utils/thumbnail-listing.js +26 -0
- package/dist/utils/thumbnail-listing.js.map +1 -0
- package/dist/utils/thumbnail-paths.d.ts +20 -0
- package/dist/utils/thumbnail-paths.d.ts.map +1 -0
- package/dist/utils/thumbnail-paths.js +35 -0
- package/dist/utils/thumbnail-paths.js.map +1 -0
- package/dist/utils/thumbnail-store.d.ts +19 -0
- package/dist/utils/thumbnail-store.d.ts.map +1 -0
- package/dist/utils/thumbnail-store.js +125 -0
- package/dist/utils/thumbnail-store.js.map +1 -0
- package/dist/utils/transcript-cache.d.ts +20 -0
- package/dist/utils/transcript-cache.d.ts.map +1 -0
- package/dist/utils/transcript-cache.js +127 -0
- package/dist/utils/transcript-cache.js.map +1 -0
- package/dist/utils/transcript-search.d.ts +20 -0
- package/dist/utils/transcript-search.d.ts.map +1 -0
- package/dist/utils/transcript-search.js +35 -0
- package/dist/utils/transcript-search.js.map +1 -0
- package/dist/utils/transcript.d.ts +10 -31
- package/dist/utils/transcript.d.ts.map +1 -1
- package/dist/utils/transcript.js +2 -64
- package/dist/utils/transcript.js.map +1 -1
- package/dist/utils/validate-youtube.d.ts +35 -0
- package/dist/utils/validate-youtube.d.ts.map +1 -0
- package/dist/utils/validate-youtube.js +87 -0
- package/dist/utils/validate-youtube.js.map +1 -0
- package/dist/utils/validate.d.ts +11 -4
- package/dist/utils/validate.d.ts.map +1 -1
- package/dist/utils/validate.js +43 -36
- package/dist/utils/validate.js.map +1 -1
- package/dist/utils/version.d.ts +2 -0
- package/dist/utils/version.d.ts.map +1 -0
- package/dist/utils/version.js +19 -0
- package/dist/utils/version.js.map +1 -0
- package/dist/utils/youtube-channel.d.ts +44 -0
- package/dist/utils/youtube-channel.d.ts.map +1 -0
- package/dist/utils/youtube-channel.js +61 -0
- package/dist/utils/youtube-channel.js.map +1 -0
- package/dist/utils/youtube-download.d.ts +27 -0
- package/dist/utils/youtube-download.d.ts.map +1 -0
- package/dist/utils/youtube-download.js +118 -0
- package/dist/utils/youtube-download.js.map +1 -0
- package/dist/utils/youtube-schemas.d.ts +394 -0
- package/dist/utils/youtube-schemas.d.ts.map +1 -0
- package/dist/utils/youtube-schemas.js +118 -0
- package/dist/utils/youtube-schemas.js.map +1 -0
- package/dist/utils/youtube-search.d.ts +14 -0
- package/dist/utils/youtube-search.d.ts.map +1 -0
- package/dist/utils/youtube-search.js +39 -0
- package/dist/utils/youtube-search.js.map +1 -0
- package/dist/utils/youtube-url.d.ts +25 -0
- package/dist/utils/youtube-url.d.ts.map +1 -0
- package/dist/utils/youtube-url.js +75 -0
- package/dist/utils/youtube-url.js.map +1 -0
- package/dist/utils/youtube-video.d.ts +47 -0
- package/dist/utils/youtube-video.d.ts.map +1 -0
- package/dist/utils/youtube-video.js +98 -0
- package/dist/utils/youtube-video.js.map +1 -0
- package/dist/utils/youtube.d.ts +10 -122
- package/dist/utils/youtube.d.ts.map +1 -1
- package/dist/utils/youtube.js +11 -486
- package/dist/utils/youtube.js.map +1 -1
- package/dist/utils/ytdlp-env.d.ts +33 -0
- package/dist/utils/ytdlp-env.d.ts.map +1 -0
- package/dist/utils/ytdlp-env.js +121 -0
- package/dist/utils/ytdlp-env.js.map +1 -0
- package/dist/utils/ytdlp-limiter.d.ts +24 -0
- package/dist/utils/ytdlp-limiter.d.ts.map +1 -0
- package/dist/utils/ytdlp-limiter.js +69 -0
- package/dist/utils/ytdlp-limiter.js.map +1 -0
- package/dist/utils/ytdlp-parse.d.ts +13 -0
- package/dist/utils/ytdlp-parse.d.ts.map +1 -0
- package/dist/utils/ytdlp-parse.js +48 -0
- package/dist/utils/ytdlp-parse.js.map +1 -0
- package/dist/utils/ytdlp.d.ts +13 -18
- package/dist/utils/ytdlp.d.ts.map +1 -1
- package/dist/utils/ytdlp.js +21 -110
- package/dist/utils/ytdlp.js.map +1 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -18,6 +18,7 @@ Supports both **local** (stdio) and **remote** (Streamable HTTP) transports.
|
|
|
18
18
|
- **Search** videos and channels by keyword
|
|
19
19
|
- **Fetch** videos from a playlist or channel
|
|
20
20
|
- **Video, channel and playlist metadata**, chapters, and top comments
|
|
21
|
+
- **Thumbnails**: look at any video's thumbnail or a channel's avatar or banner as an image, or save every thumbnail of a channel to disk
|
|
21
22
|
- **Transcripts with timestamps**, sliced by time range or chapter, and capped so a
|
|
22
23
|
three-hour video cannot flood your context
|
|
23
24
|
- **Search inside a transcript** and get back `?t=` links that open the video at
|
|
@@ -50,6 +51,10 @@ Supports both **local** (stdio) and **remote** (Streamable HTTP) transports.
|
|
|
50
51
|
speaking rate, and the phrases it repeats across videos
|
|
51
52
|
- **Keep a written profile** beside the corpus, grounded in passages you can cite
|
|
52
53
|
|
|
54
|
+
**Study a channel's thumbnails** (local mode)
|
|
55
|
+
|
|
56
|
+
- **Save every thumbnail** of a channel, plus its avatar and banner, resumably, at the largest size YouTube serves, with each image's real pixel size recorded
|
|
57
|
+
|
|
53
58
|
**Built to stay working**
|
|
54
59
|
|
|
55
60
|
- WebVTT parsed by the W3C reference implementation, not a hand-written matcher
|
|
@@ -57,7 +62,9 @@ Supports both **local** (stdio) and **remote** (Streamable HTTP) transports.
|
|
|
57
62
|
wall of yt-dlp stderr
|
|
58
63
|
- Timeouts, retry with backoff, and a concurrency limit on every yt-dlp call
|
|
59
64
|
- `check_health` diagnoses missing or outdated yt-dlp and ffmpeg
|
|
65
|
+
- Bearer auth, a loopback default bind, an allowlist on every URL and image host, and typed error codes on every failure
|
|
60
66
|
- Structured output on every tool, plus MCP resources, prompts and completions
|
|
67
|
+
- An end-to-end suite runs the built server against real yt-dlp over both transports every week
|
|
61
68
|
|
|
62
69
|
## Prerequisites
|
|
63
70
|
|
|
@@ -154,7 +161,11 @@ npm run build
|
|
|
154
161
|
npm run start:http
|
|
155
162
|
```
|
|
156
163
|
|
|
157
|
-
The server listens on `PORT` (default 3000).
|
|
164
|
+
The server listens on `127.0.0.1:PORT` (default 3000). To reach it from another
|
|
165
|
+
machine set `MCP_BIND_HOST=0.0.0.0` together with `MCP_AUTH_TOKEN`; the server
|
|
166
|
+
refuses to start exposed and unauthenticated unless you also set
|
|
167
|
+
`MCP_ALLOW_UNAUTHENTICATED=true`. Behind a reverse proxy, set `MCP_TRUST_PROXY`
|
|
168
|
+
to the number of hops and `MCP_PUBLIC_URL` to the address clients use.
|
|
158
169
|
|
|
159
170
|
#### Docker
|
|
160
171
|
|
|
@@ -210,7 +221,7 @@ a cold start. Any paid plan removes that.
|
|
|
210
221
|
|
|
211
222
|
## MCP Tools
|
|
212
223
|
|
|
213
|
-
|
|
224
|
+
37 tools. The 15 read-only ones work over both transports; the 22 that touch
|
|
214
225
|
your filesystem are registered only in local (stdio) mode, so a remote
|
|
215
226
|
deployment cannot reach the host's disk.
|
|
216
227
|
|
|
@@ -220,18 +231,19 @@ available for this video. Call get_transcript again with one of: fr, es, de.`
|
|
|
220
231
|
|
|
221
232
|
### Discovery — remote + local
|
|
222
233
|
|
|
223
|
-
| Tool | Key parameters
|
|
224
|
-
| ------------------- |
|
|
225
|
-
| `search_videos` | `query`, `limit`
|
|
226
|
-
| `search_channels` | `query`, `limit`
|
|
227
|
-
| `fetch_videos` | `url`, `limit`
|
|
228
|
-
| `get_video_info` | `video`
|
|
229
|
-
| `get_channel_info` | `channel`
|
|
230
|
-
| `get_playlist_info` | `url`
|
|
231
|
-
| `get_chapters` | `video`
|
|
232
|
-
| `get_comments` | `video`, `limit`
|
|
233
|
-
| `list_formats` | `video`
|
|
234
|
-
| `
|
|
234
|
+
| Tool | Key parameters | Returns |
|
|
235
|
+
| ------------------- | -------------------------------------- | ----------------------------------------------------------------------------------------- |
|
|
236
|
+
| `search_videos` | `query`, `limit` | Matching videos with durations, channels and view counts |
|
|
237
|
+
| `search_channels` | `query`, `limit` | Matching channels with subscriber counts |
|
|
238
|
+
| `fetch_videos` | `url`, `limit` | Videos in a playlist or channel, with thumbnail URLs |
|
|
239
|
+
| `get_video_info` | `video` | Title, channel, duration, views, likes, description, tags |
|
|
240
|
+
| `get_channel_info` | `channel` | Name, handle, subscriber count, description, avatar and banner |
|
|
241
|
+
| `get_playlist_info` | `url` | Title, channel, video count, last updated |
|
|
242
|
+
| `get_chapters` | `video` | Chapter titles with start/end times and deep links |
|
|
243
|
+
| `get_comments` | `video`, `limit` | Top-level comments by popularity |
|
|
244
|
+
| `list_formats` | `video` | Available formats grouped by video+audio, video-only, audio-only |
|
|
245
|
+
| `get_thumbnail` | `video`, `channel`, `image`, `quality` | A video thumbnail, channel avatar or banner as an image, with its real size |
|
|
246
|
+
| `check_health` | — | yt-dlp and ffmpeg status, versions, staleness warnings, and which session options are set |
|
|
235
247
|
|
|
236
248
|
### Transcripts — remote + local
|
|
237
249
|
|
|
@@ -289,6 +301,24 @@ keyframe-aligned cut. All of these require ffmpeg.
|
|
|
289
301
|
channel from what is already on disk, so they work offline and cost nothing to
|
|
290
302
|
call.
|
|
291
303
|
|
|
304
|
+
### Channel thumbnails — local only
|
|
305
|
+
|
|
306
|
+
| Tool | Key parameters | Returns |
|
|
307
|
+
| --------------------------- | -------------------------------------------- | --------------------------------------------------------- |
|
|
308
|
+
| `fetch_channel_thumbnails` | `channel`, `maxVideos`, `tabs`, `quality` | Every thumbnail saved to disk, plus the avatar and banner |
|
|
309
|
+
| `list_channel_thumbnails` | `channel`, `tab`, `state`, `limit`, `offset` | Saved entries with paths and decoded sizes |
|
|
310
|
+
| `delete_channel_thumbnails` | `channel` | What was removed |
|
|
311
|
+
|
|
312
|
+
One yt-dlp listing per tab, then the images from YouTube's image hosts
|
|
313
|
+
directly, so a five-hundred-video channel costs a handful of listings and not
|
|
314
|
+
five hundred extractions. For each video the fetch tries the largest image
|
|
315
|
+
YouTube serves (`maxresdefault`, 1280 wide where it exists) and keeps it only if
|
|
316
|
+
it decodes wider than the image the listing offered; otherwise it keeps the
|
|
317
|
+
listed image, and failing that the small `hqdefault`. Every width and height in
|
|
318
|
+
the manifest was decoded from the saved bytes, never read off a URL. Shorts are
|
|
319
|
+
listed with portrait thumbnails and saved under `shorts/`. The run is resumable:
|
|
320
|
+
interrupt it and call again, and only what is missing or truncated is fetched.
|
|
321
|
+
|
|
292
322
|
A brain holds one caption language; pass `language` to read another, and build a
|
|
293
323
|
separate brain per language.
|
|
294
324
|
|
|
@@ -309,16 +339,18 @@ skipped forever as already done.
|
|
|
309
339
|
|
|
310
340
|
Reusable workflows your client can invoke directly: `summarize_video`,
|
|
311
341
|
`extract_skill`, `compare_videos`, `research_topic`, `channel_deep_dive`,
|
|
312
|
-
`clip_from_quote` (find a phrase, then cut the clip around it),
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
342
|
+
`clip_from_quote` (find a phrase, then cut the clip around it),
|
|
343
|
+
`study_thumbnails` (save a channel's thumbnails, look at a sample, describe
|
|
344
|
+
what recurs), and — local only — `review_library`, `create_brain` (build a
|
|
345
|
+
channel's corpus, then write its profile from it) and `ask_creator` (answer a
|
|
346
|
+
question strictly from a brain, with citations).
|
|
316
347
|
|
|
317
348
|
## Resources
|
|
318
349
|
|
|
319
350
|
- `youtube://transcript/{videoId}` — a timestamped transcript, fetched and cached on first read
|
|
320
351
|
- `youtube://library/{videoId}/{summary|skill}` — a saved note (local only, and enumerable)
|
|
321
352
|
- `youtube://brain/{channelId}/{manifest|profile}` — what a channel brain covers, or the profile written from it (local only, and enumerable)
|
|
353
|
+
- `youtube://thumbnails/{channelId}/manifest` — what `fetch_channel_thumbnails` saved for a channel (local only, and enumerable)
|
|
322
354
|
|
|
323
355
|
## Error codes
|
|
324
356
|
|
|
@@ -334,25 +366,50 @@ them, each prefixed with a code and followed by a next step.
|
|
|
334
366
|
| `RATE_LIMITED`, `TIMEOUT` | Transient; retried automatically with backoff before surfacing |
|
|
335
367
|
| `YTDLP_MISSING`, `FFMPEG_MISSING`, `YTDLP_FAILED` | A tooling problem; the message says how to fix it |
|
|
336
368
|
| `INVALID_INPUT` | A bad argument, caught before any network call |
|
|
369
|
+
| `BOT_CHECK` | YouTube asked this address to sign in; set the cookie settings below |
|
|
370
|
+
| `FETCH_FAILED` | An image could not be downloaded from YouTube's image hosts |
|
|
337
371
|
| `CANCELLED` | The client cancelled the request |
|
|
338
372
|
|
|
339
373
|
## Environment variables
|
|
340
374
|
|
|
341
375
|
All optional.
|
|
342
376
|
|
|
343
|
-
| Variable
|
|
344
|
-
|
|
|
345
|
-
| `MCP_AUTH_TOKEN`
|
|
346
|
-
| `MCP_ALLOWED_HOSTS`
|
|
347
|
-
| `MCP_ALLOWED_ORIGINS`
|
|
348
|
-
| `MCP_BIND_HOST`
|
|
349
|
-
| `
|
|
350
|
-
| `
|
|
351
|
-
| `
|
|
352
|
-
| `
|
|
353
|
-
| `
|
|
354
|
-
| `
|
|
355
|
-
| `
|
|
377
|
+
| Variable | Default | Purpose |
|
|
378
|
+
| ---------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
379
|
+
| `MCP_AUTH_TOKEN` | unset | Require this bearer token on the HTTP transport. **Set this if you expose the server beyond localhost.** |
|
|
380
|
+
| `MCP_ALLOWED_HOSTS` | unset | Comma-separated Host allowlist; enables DNS-rebinding protection |
|
|
381
|
+
| `MCP_ALLOWED_ORIGINS` | unset | Comma-separated Origin allowlist |
|
|
382
|
+
| `MCP_BIND_HOST` | `127.0.0.1` | Interface to bind. The Docker image sets `0.0.0.0`. A non-loopback bind refuses to start without `MCP_AUTH_TOKEN` unless `MCP_ALLOW_UNAUTHENTICATED=true` |
|
|
383
|
+
| `MCP_ALLOW_UNAUTHENTICATED` | `false` | Explicit consent to listen on a network interface with no token |
|
|
384
|
+
| `MCP_TRUST_PROXY` | `false` | `true`, `false`, or the number of reverse-proxy hops whose `X-Forwarded-*` headers to believe. Off, forwarded headers are ignored for rate limiting and URLs |
|
|
385
|
+
| `MCP_PUBLIC_URL` | unset | The URL clients reach the server at; used in the OAuth metadata it publishes instead of the request's Host |
|
|
386
|
+
| `MCP_MAX_BODY_BYTES` | `1048576` | Largest request body accepted |
|
|
387
|
+
| `MCP_MODE` | unset | `http` selects the HTTP transport when no `--http` flag is given |
|
|
388
|
+
| `PORT` | `3000` | HTTP port. The Docker image sets `10000`; Render and similar platforms inject their own |
|
|
389
|
+
| `MCP_RATE_LIMIT` | `60` | Requests per window, per client |
|
|
390
|
+
| `MCP_RATE_WINDOW_MS` | `60000` | Rate-limit window |
|
|
391
|
+
| `MCP_SESSION_IDLE_MS` | `1800000` | Close HTTP sessions idle this long |
|
|
392
|
+
| `MCP_MAX_SESSIONS` | `1000` | Reject new sessions past this many |
|
|
393
|
+
| `YOUTUBE_MCP_MAX_CONCURRENCY` | `3` | Concurrent yt-dlp processes |
|
|
394
|
+
| `YOUTUBE_MCP_TRANSCRIPT_TTL_MS` | 30 days | Transcript cache lifetime |
|
|
395
|
+
| `YOUTUBE_MCP_COOKIES_FROM_BROWSER` | unset | Browser whose cookies yt-dlp should read: `brave`, `chrome`, `chromium`, `edge`, `firefox`, `opera`, `safari`, `vivaldi` or `whale`. See Signed-in content |
|
|
396
|
+
| `YOUTUBE_MCP_COOKIES_FILE` | unset | A Netscape-format cookies file inside your home directory, as an alternative to a browser |
|
|
397
|
+
| `YOUTUBE_MCP_PROXY` | unset | An `http`, `https`, `socks4` or `socks5` proxy URL for yt-dlp |
|
|
398
|
+
| `YOUTUBE_MCP_SLEEP_REQUESTS_S` | unset | Seconds yt-dlp sleeps between its own requests, to stay under YouTube's limits |
|
|
399
|
+
|
|
400
|
+
### Signed-in content
|
|
401
|
+
|
|
402
|
+
Some videos need a signed-in session — age-restricted, members-only, private
|
|
403
|
+
ones you have access to — and YouTube sometimes asks an address to prove it is
|
|
404
|
+
not a bot before serving any video at all. Those failures arrive as
|
|
405
|
+
`LOGIN_REQUIRED`, `AGE_GATED` or `BOT_CHECK`, and each names the fix: set
|
|
406
|
+
`YOUTUBE_MCP_COOKIES_FROM_BROWSER` to a browser you are signed in with, or
|
|
407
|
+
`YOUTUBE_MCP_COOKIES_FILE` to a cookies file, and restart the server.
|
|
408
|
+
|
|
409
|
+
Cookies are your account. Use them in local (stdio) mode, keep a cookies file
|
|
410
|
+
readable by you alone, and know that content read this way may be personal.
|
|
411
|
+
The server validates both settings at boot, never logs the file's path or
|
|
412
|
+
contents, and never surfaces yt-dlp's output.
|
|
356
413
|
|
|
357
414
|
## Library Storage
|
|
358
415
|
|
|
@@ -372,6 +429,13 @@ Content is stored in `~/.youtube-knowledge/`:
|
|
|
372
429
|
│ ├── manifest.json # What the brain covers, and where a build stopped
|
|
373
430
|
│ ├── chunks.json # The timestamped passages
|
|
374
431
|
│ └── profile.md # The written account, if one was saved
|
|
432
|
+
├── thumbnails/ # Saved channel thumbnails
|
|
433
|
+
│ └── {channel_id}/
|
|
434
|
+
│ ├── manifest.json # Every image, its decoded size, where it is
|
|
435
|
+
│ ├── channel/ # avatar.* and banner.*
|
|
436
|
+
│ ├── videos/ # {video_id}.jpg (or .png / .webp)
|
|
437
|
+
│ ├── shorts/
|
|
438
|
+
│ └── streams/
|
|
375
439
|
├── downloads/ # Full downloads
|
|
376
440
|
├── clips/ # Extracted clips
|
|
377
441
|
├── frames/ # Captured stills
|
|
@@ -469,6 +533,11 @@ client over an in-memory transport, exercises the library against a real
|
|
|
469
533
|
temporary filesystem, and snapshots the tool manifest so any change to the
|
|
470
534
|
public surface shows up as a reviewable diff.
|
|
471
535
|
|
|
536
|
+
An end-to-end lane (`E2E=1 npm run test:e2e`) drives the built server through
|
|
537
|
+
real MCP clients over both transports against real yt-dlp and the real network,
|
|
538
|
+
in an isolated temporary home. It runs weekly in CI and on every release before
|
|
539
|
+
anything is published; see CONTRIBUTING.md.
|
|
540
|
+
|
|
472
541
|
## Development
|
|
473
542
|
|
|
474
543
|
```bash
|
|
@@ -490,9 +559,11 @@ project layout, coding standards, and how to add a tool.
|
|
|
490
559
|
|
|
491
560
|
## Security
|
|
492
561
|
|
|
493
|
-
The HTTP transport
|
|
494
|
-
|
|
495
|
-
|
|
562
|
+
The HTTP transport binds loopback and refuses to listen on a network interface
|
|
563
|
+
without `MCP_AUTH_TOKEN`. Caller-supplied URLs must be on a YouTube host before
|
|
564
|
+
anything is handed to yt-dlp, every yt-dlp target follows a `--` terminator, and
|
|
565
|
+
ids are validated before they become paths. See [SECURITY.md](SECURITY.md) for
|
|
566
|
+
the deployment checklist and how to report a vulnerability.
|
|
496
567
|
|
|
497
568
|
## License
|
|
498
569
|
|
package/dist/brain-schemas.d.ts
CHANGED
|
@@ -226,6 +226,8 @@ export declare const brainManifestSchema: z.ZodObject<{
|
|
|
226
226
|
subscriberCount: z.ZodNumber;
|
|
227
227
|
channelUrl: z.ZodString;
|
|
228
228
|
description: z.ZodString;
|
|
229
|
+
avatarUrl: z.ZodOptional<z.ZodString>;
|
|
230
|
+
bannerUrl: z.ZodOptional<z.ZodString>;
|
|
229
231
|
}, "strip", z.ZodTypeAny, {
|
|
230
232
|
description: string;
|
|
231
233
|
name: string;
|
|
@@ -233,6 +235,8 @@ export declare const brainManifestSchema: z.ZodObject<{
|
|
|
233
235
|
handle: string;
|
|
234
236
|
subscriberCount: number;
|
|
235
237
|
channelUrl: string;
|
|
238
|
+
avatarUrl?: string | undefined;
|
|
239
|
+
bannerUrl?: string | undefined;
|
|
236
240
|
}, {
|
|
237
241
|
description: string;
|
|
238
242
|
name: string;
|
|
@@ -240,6 +244,8 @@ export declare const brainManifestSchema: z.ZodObject<{
|
|
|
240
244
|
handle: string;
|
|
241
245
|
subscriberCount: number;
|
|
242
246
|
channelUrl: string;
|
|
247
|
+
avatarUrl?: string | undefined;
|
|
248
|
+
bannerUrl?: string | undefined;
|
|
243
249
|
}>;
|
|
244
250
|
/**
|
|
245
251
|
* A brain is built from one caption language. Defaulted rather than required
|
|
@@ -345,6 +351,8 @@ export declare const brainManifestSchema: z.ZodObject<{
|
|
|
345
351
|
handle: string;
|
|
346
352
|
subscriberCount: number;
|
|
347
353
|
channelUrl: string;
|
|
354
|
+
avatarUrl?: string | undefined;
|
|
355
|
+
bannerUrl?: string | undefined;
|
|
348
356
|
};
|
|
349
357
|
version: number;
|
|
350
358
|
videos: Record<string, {
|
|
@@ -391,6 +399,8 @@ export declare const brainManifestSchema: z.ZodObject<{
|
|
|
391
399
|
handle: string;
|
|
392
400
|
subscriberCount: number;
|
|
393
401
|
channelUrl: string;
|
|
402
|
+
avatarUrl?: string | undefined;
|
|
403
|
+
bannerUrl?: string | undefined;
|
|
394
404
|
};
|
|
395
405
|
version: number;
|
|
396
406
|
videos: Record<string, unknown>;
|
|
@@ -489,14 +499,4 @@ export declare const brainSummarySchema: z.ZodObject<{
|
|
|
489
499
|
updatedAt: string;
|
|
490
500
|
hasProfile: boolean;
|
|
491
501
|
}>;
|
|
492
|
-
export declare const brainLockSchema: z.ZodObject<{
|
|
493
|
-
pid: z.ZodNumber;
|
|
494
|
-
startedAt: z.ZodString;
|
|
495
|
-
}, "strip", z.ZodTypeAny, {
|
|
496
|
-
pid: number;
|
|
497
|
-
startedAt: string;
|
|
498
|
-
}, {
|
|
499
|
-
pid: number;
|
|
500
|
-
startedAt: string;
|
|
501
|
-
}>;
|
|
502
502
|
//# sourceMappingURL=brain-schemas.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"brain-schemas.d.ts","sourceRoot":"","sources":["../src/brain-schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;GAMG;AAEH,qDAAqD;AACrD,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAchC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,uDAAuD;AACvD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;EAO3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAG/B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;EAI5B,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;EAGpC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmB3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,4BAA4B;AAC5B,eAAO,MAAM,mBAAmB
|
|
1
|
+
{"version":3,"file":"brain-schemas.d.ts","sourceRoot":"","sources":["../src/brain-schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB;;;;;;GAMG;AAEH,qDAAqD;AACrD,eAAO,MAAM,qBAAqB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAchC,CAAC;AAEH,MAAM,MAAM,eAAe,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,qBAAqB,CAAC,CAAC;AAEpE,uDAAuD;AACvD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;EAO3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,eAAO,MAAM,oBAAoB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAG/B,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;;;;;;;;;;EAI5B,CAAC;AAEH,eAAO,MAAM,yBAAyB;;;;;;;;;EAGpC,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAmB3B,CAAC;AAEH,MAAM,MAAM,UAAU,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAE1D,4BAA4B;AAC5B,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAG9B;;;OAGG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAMH,CAAC;AAEH,MAAM,MAAM,aAAa,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,mBAAmB,CAAC,CAAC;AAEhE,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;EAS7B,CAAC;AAEH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,8BAA8B;AAC9B,eAAO,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAY7B,CAAC"}
|
package/dist/brain-schemas.js
CHANGED
|
@@ -106,8 +106,4 @@ export const brainSummarySchema = z.object({
|
|
|
106
106
|
indexedCount: z.number().int(),
|
|
107
107
|
chunkCount: z.number().int(),
|
|
108
108
|
});
|
|
109
|
-
export const brainLockSchema = z.object({
|
|
110
|
-
pid: z.number().int(),
|
|
111
|
-
startedAt: z.string(),
|
|
112
|
-
});
|
|
113
109
|
//# sourceMappingURL=brain-schemas.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"brain-schemas.js","sourceRoot":"","sources":["../src/brain-schemas.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"brain-schemas.js","sourceRoot":"","sources":["../src/brain-schemas.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Express } from 'express';
|
|
2
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { type HttpConfig } from './config.js';
|
|
4
|
+
import type { RateLimiter } from './rate-limiter.js';
|
|
5
|
+
import type { SessionStore } from './sessions.js';
|
|
6
|
+
/** The Express application: routes, authentication and rate limiting. */
|
|
7
|
+
export interface AppDependencies {
|
|
8
|
+
createServer: (mode: 'http') => McpServer;
|
|
9
|
+
sessions: SessionStore;
|
|
10
|
+
limiter: RateLimiter;
|
|
11
|
+
/** The bound port, known only once listening; read lazily for that reason. */
|
|
12
|
+
port: () => number;
|
|
13
|
+
startedAt: number;
|
|
14
|
+
}
|
|
15
|
+
export declare function buildApp(config: HttpConfig, deps: AppDependencies): Express;
|
|
16
|
+
//# sourceMappingURL=app.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.d.ts","sourceRoot":"","sources":["../../src/http/app.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAmC,MAAM,SAAS,CAAC;AAOxE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAYzE,OAAO,EAAc,KAAK,UAAU,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAElD,yEAAyE;AAEzE,MAAM,WAAW,eAAe;IAC9B,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,SAAS,CAAC;IAC1C,QAAQ,EAAE,YAAY,CAAC;IACvB,OAAO,EAAE,WAAW,CAAC;IACrB,8EAA8E;IAC9E,IAAI,EAAE,MAAM,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,IAAI,EAAE,eAAe,GAAG,OAAO,CAqK3E"}
|
package/dist/http/app.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import express from 'express';
|
|
3
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
4
|
+
import { hostHeaderValidation, localhostHostValidation, } from '@modelcontextprotocol/sdk/server/middleware/hostHeaderValidation.js';
|
|
5
|
+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
|
|
6
|
+
import { runPreflight } from '../utils/preflight.js';
|
|
7
|
+
import { serverVersion } from '../utils/version.js';
|
|
8
|
+
import { bearerToken, clientKey, jsonRpcError, publicBaseUrl, securityHeaders, sessionIdOf, tokenMatches, } from './auth.js';
|
|
9
|
+
import { isLoopback } from './config.js';
|
|
10
|
+
export function buildApp(config, deps) {
|
|
11
|
+
const { createServer, sessions, limiter, port, startedAt } = deps;
|
|
12
|
+
const app = express();
|
|
13
|
+
// `req.ip` honours forwarded headers only when the operator names the proxies.
|
|
14
|
+
app.set('trust proxy', config.trustProxy);
|
|
15
|
+
app.disable('x-powered-by');
|
|
16
|
+
app.use(securityHeaders());
|
|
17
|
+
// Every route, before anything else: an unauthenticated flood is still a flood.
|
|
18
|
+
app.use((req, res, next) => {
|
|
19
|
+
const { allowed, remaining, retryAfter } = limiter.check(clientKey(req));
|
|
20
|
+
res.setHeader('RateLimit-Limit', String(config.rateLimit));
|
|
21
|
+
res.setHeader('RateLimit-Remaining', String(remaining));
|
|
22
|
+
if (allowed) {
|
|
23
|
+
next();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Without Retry-After a client has no idea how long to wait.
|
|
27
|
+
res.setHeader('Retry-After', String(retryAfter));
|
|
28
|
+
jsonRpcError(res, 429, -32000, `Rate limit exceeded. Retry in ${retryAfter}s.`);
|
|
29
|
+
});
|
|
30
|
+
function hasValidToken(req) {
|
|
31
|
+
return (config.authToken === undefined ||
|
|
32
|
+
tokenMatches(bearerToken(req.headers.authorization), config.authToken));
|
|
33
|
+
}
|
|
34
|
+
/** Returns true when the request may proceed; otherwise it has been answered. */
|
|
35
|
+
function authorize(req, res) {
|
|
36
|
+
if (hasValidToken(req))
|
|
37
|
+
return true;
|
|
38
|
+
// RFC 9728: point unauthenticated callers at the resource metadata.
|
|
39
|
+
const metadata = `${publicBaseUrl(config, req, port())}/.well-known/oauth-protected-resource`;
|
|
40
|
+
res.setHeader('WWW-Authenticate', `Bearer realm="youtube-knowledge-mcp", resource_metadata="${metadata}"`);
|
|
41
|
+
jsonRpcError(res, 401, -32001, 'Unauthorized');
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
// Ahead of Host validation on purpose: a container's own HEALTHCHECK and every
|
|
45
|
+
// platform probe arrive over loopback with a Host the allowlist may not name,
|
|
46
|
+
// and a service that 403s its probe never goes live. Unauthenticated callers
|
|
47
|
+
// learn only whether the service is up; versions and counts need the token.
|
|
48
|
+
app.get('/health', (req, res) => {
|
|
49
|
+
void runPreflight().then((report) => {
|
|
50
|
+
const status = report.ok ? 'ok' : 'degraded';
|
|
51
|
+
const body = hasValidToken(req)
|
|
52
|
+
? {
|
|
53
|
+
status,
|
|
54
|
+
version: serverVersion(),
|
|
55
|
+
uptimeSeconds: Math.round((Date.now() - startedAt) / 1000),
|
|
56
|
+
ytDlp: report.ytDlp,
|
|
57
|
+
ffmpeg: report.ffmpeg,
|
|
58
|
+
sessions: sessions.size,
|
|
59
|
+
}
|
|
60
|
+
: { status };
|
|
61
|
+
res.writeHead(report.ok ? 200 : 503, { 'Content-Type': 'application/json' });
|
|
62
|
+
res.end(JSON.stringify(body));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
app.use(express.json({ limit: config.maxBodyBytes }));
|
|
66
|
+
app.use(bodyErrorHandler);
|
|
67
|
+
// The same decision the SDK's createMcpExpressApp makes, made explicitly so a
|
|
68
|
+
// body limit could be configured: an allowlist when one is given, loopback
|
|
69
|
+
// validation when bound to loopback, nothing when bound wide (a token is
|
|
70
|
+
// then required by assertSafeToStart).
|
|
71
|
+
if (config.allowedHosts.length > 0)
|
|
72
|
+
app.use(hostHeaderValidation(config.allowedHosts));
|
|
73
|
+
else if (isLoopback(config.bindHost))
|
|
74
|
+
app.use(localhostHostValidation());
|
|
75
|
+
app.post('/mcp', async (req, res) => {
|
|
76
|
+
if (!authorize(req, res))
|
|
77
|
+
return;
|
|
78
|
+
const sessionId = sessionIdOf(req);
|
|
79
|
+
try {
|
|
80
|
+
const existing = sessionId === undefined ? undefined : sessions.get(sessionId);
|
|
81
|
+
if (existing !== undefined && sessionId !== undefined) {
|
|
82
|
+
sessions.touch(sessionId);
|
|
83
|
+
await existing.transport.handleRequest(req, res, req.body);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (!sessionId && isInitializeRequest(req.body)) {
|
|
87
|
+
await openSession(req, res);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
jsonRpcError(res, 400, -32000, 'Bad Request: No valid session ID provided');
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error('Error handling MCP request:', error);
|
|
94
|
+
jsonRpcError(res, 500, -32603, 'Internal server error');
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
async function openSession(req, res) {
|
|
98
|
+
// Reserved before the first await, so concurrent initializes cannot all
|
|
99
|
+
// pass a size check and overshoot the cap together.
|
|
100
|
+
if (!sessions.reserve()) {
|
|
101
|
+
jsonRpcError(res, 503, -32000, 'Server is at capacity. Try again shortly.');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
const transport = new StreamableHTTPServerTransport({
|
|
105
|
+
sessionIdGenerator: () => randomUUID(),
|
|
106
|
+
// Hosts are checked by the port-agnostic middleware above on every
|
|
107
|
+
// route; the transport's own check would compare the whole Host header,
|
|
108
|
+
// port included, and reject every deployment on a non-default port.
|
|
109
|
+
...(config.allowedOrigins.length > 0
|
|
110
|
+
? { enableDnsRebindingProtection: true, allowedOrigins: config.allowedOrigins }
|
|
111
|
+
: {}),
|
|
112
|
+
onsessioninitialized: (id) => {
|
|
113
|
+
sessions.commit(id, transport);
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
transport.onclose = () => {
|
|
117
|
+
if (transport.sessionId)
|
|
118
|
+
sessions.delete(transport.sessionId);
|
|
119
|
+
};
|
|
120
|
+
try {
|
|
121
|
+
await createServer('http').connect(transport);
|
|
122
|
+
await transport.handleRequest(req, res, req.body);
|
|
123
|
+
}
|
|
124
|
+
finally {
|
|
125
|
+
// The transport carries an id only once the session was initialised, so
|
|
126
|
+
// its absence means the reservation was never turned into a session.
|
|
127
|
+
if (transport.sessionId === undefined)
|
|
128
|
+
sessions.release();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
const bySession = async (req, res) => {
|
|
132
|
+
if (!authorize(req, res))
|
|
133
|
+
return;
|
|
134
|
+
const sessionId = sessionIdOf(req);
|
|
135
|
+
const session = sessionId === undefined ? undefined : sessions.get(sessionId);
|
|
136
|
+
if (sessionId === undefined || session === undefined) {
|
|
137
|
+
jsonRpcError(res, 404, -32001, 'Invalid or missing session ID');
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
sessions.touch(sessionId);
|
|
141
|
+
await session.transport.handleRequest(req, res);
|
|
142
|
+
};
|
|
143
|
+
app.get('/mcp', bySession);
|
|
144
|
+
app.delete('/mcp', bySession);
|
|
145
|
+
// RFC 9728 protected-resource metadata, so OAuth-capable clients can discover
|
|
146
|
+
// how to authenticate instead of guessing.
|
|
147
|
+
app.get('/.well-known/oauth-protected-resource', (req, res) => {
|
|
148
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
149
|
+
res.end(JSON.stringify({
|
|
150
|
+
resource: `${publicBaseUrl(config, req, port())}/mcp`,
|
|
151
|
+
bearer_methods_supported: ['header'],
|
|
152
|
+
resource_documentation: 'https://github.com/teobouancheau/youtube-knowledge-mcp#readme',
|
|
153
|
+
}));
|
|
154
|
+
});
|
|
155
|
+
return app;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Body-parser failures arrive as errors with an HTTP status; Express 5 routes
|
|
159
|
+
* them to the first four-argument middleware. Answered in the JSON-RPC shape
|
|
160
|
+
* every other refusal uses, and never with the parser's own message.
|
|
161
|
+
*/
|
|
162
|
+
function bodyErrorHandler(error, _req, res, next) {
|
|
163
|
+
const status = typeof error === 'object' && error !== null && 'status' in error
|
|
164
|
+
? Number(error.status)
|
|
165
|
+
: Number.NaN;
|
|
166
|
+
if (status === 413) {
|
|
167
|
+
jsonRpcError(res, 413, -32600, 'Request body too large.');
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (status >= 400 && status < 500) {
|
|
171
|
+
jsonRpcError(res, status, -32700, 'Request body could not be parsed.');
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
next(error);
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=app.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"app.js","sourceRoot":"","sources":["../../src/http/app.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Request, RequestHandler, Response } from 'express';
|
|
2
|
+
import type { HttpConfig } from './config.js';
|
|
3
|
+
/** Bearer tokens, client identity, session ids and the JSON-RPC error envelope. */
|
|
4
|
+
/**
|
|
5
|
+
* Compare a presented token against the configured one in constant time, so the
|
|
6
|
+
* endpoint does not leak the token's prefix through response timing.
|
|
7
|
+
*/
|
|
8
|
+
export declare function tokenMatches(presented: string | undefined, expected: string): boolean;
|
|
9
|
+
export declare function bearerToken(header: string | undefined): string | undefined;
|
|
10
|
+
/**
|
|
11
|
+
* The key a client is rate-limited under.
|
|
12
|
+
*
|
|
13
|
+
* `req.ip` is what Express derives after applying the `trust proxy` setting:
|
|
14
|
+
* the socket address by default, and the forwarded client only when the
|
|
15
|
+
* operator has said which proxies to believe. Reading `X-Forwarded-For`
|
|
16
|
+
* directly, as this once did, let any caller mint a fresh quota per request.
|
|
17
|
+
*/
|
|
18
|
+
export declare function clientKey(req: Request): string;
|
|
19
|
+
/**
|
|
20
|
+
* Node parses a repeated header into an array, so this field is genuinely
|
|
21
|
+
* `string | string[] | undefined`. Asserting it was always a string meant a
|
|
22
|
+
* request that sent `Mcp-Session-Id` twice reached `sessions.get()` holding an
|
|
23
|
+
* array, missed every session, and was reported as an invalid session ID.
|
|
24
|
+
* Ambiguous is treated as absent: two session IDs identify no session.
|
|
25
|
+
*/
|
|
26
|
+
export declare function sessionIdOf(req: Request): string | undefined;
|
|
27
|
+
export declare function jsonRpcError(res: Response, status: number, code: number, message: string): void;
|
|
28
|
+
/**
|
|
29
|
+
* The base URL this server publishes in OAuth metadata.
|
|
30
|
+
*
|
|
31
|
+
* Configuration first; the request's own view of itself only when a proxy is
|
|
32
|
+
* trusted to have set it. Reflecting `Host` unconditionally let anyone who
|
|
33
|
+
* could reach the endpoint choose the address other clients were told to use.
|
|
34
|
+
*/
|
|
35
|
+
export declare function publicBaseUrl(config: HttpConfig, req: Request, port: number): string;
|
|
36
|
+
/** Headers every response carries; none of them cost anything for an API. */
|
|
37
|
+
export declare function securityHeaders(): RequestHandler;
|
|
38
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/http/auth.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE9C,mFAAmF;AAEnF;;;GAGG;AACH,wBAAgB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAarF;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAI1E;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAE9C;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAG5D;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAI/F;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAIpF;AAED,6EAA6E;AAC7E,wBAAgB,eAAe,IAAI,cAAc,CAQhD"}
|