youtube-knowledge-mcp 2.2.0 → 3.0.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.
Files changed (192) hide show
  1. package/README.md +62 -18
  2. package/dist/harvest-schemas.d.ts +75 -0
  3. package/dist/harvest-schemas.d.ts.map +1 -0
  4. package/dist/harvest-schemas.js +106 -0
  5. package/dist/harvest-schemas.js.map +1 -0
  6. package/dist/registry/harvest-tools.d.ts +10 -0
  7. package/dist/registry/harvest-tools.d.ts.map +1 -0
  8. package/dist/registry/harvest-tools.js +109 -0
  9. package/dist/registry/harvest-tools.js.map +1 -0
  10. package/dist/registry/health-tools.d.ts.map +1 -1
  11. package/dist/registry/health-tools.js +18 -0
  12. package/dist/registry/health-tools.js.map +1 -1
  13. package/dist/registry/index.d.ts.map +1 -1
  14. package/dist/registry/index.js +2 -0
  15. package/dist/registry/index.js.map +1 -1
  16. package/dist/registry/video-tools.js +1 -1
  17. package/dist/registry/video-tools.js.map +1 -1
  18. package/dist/schemas.d.ts +239 -0
  19. package/dist/schemas.d.ts.map +1 -1
  20. package/dist/schemas.js +56 -0
  21. package/dist/schemas.js.map +1 -1
  22. package/dist/thumbnail-schemas.d.ts +28 -28
  23. package/dist/tools/ask-brain.js +1 -1
  24. package/dist/tools/ask-brain.js.map +1 -1
  25. package/dist/tools/brain-info.js +1 -1
  26. package/dist/tools/brain-info.js.map +1 -1
  27. package/dist/tools/check-health.d.ts +59 -0
  28. package/dist/tools/check-health.d.ts.map +1 -1
  29. package/dist/tools/check-health.js +85 -6
  30. package/dist/tools/check-health.js.map +1 -1
  31. package/dist/tools/fetch-channel-thumbnails.d.ts +8 -8
  32. package/dist/tools/fetch-videos.d.ts +15 -4
  33. package/dist/tools/fetch-videos.d.ts.map +1 -1
  34. package/dist/tools/fetch-videos.js +63 -22
  35. package/dist/tools/fetch-videos.js.map +1 -1
  36. package/dist/tools/get-comments.d.ts +238 -6
  37. package/dist/tools/get-comments.d.ts.map +1 -1
  38. package/dist/tools/get-comments.js +85 -21
  39. package/dist/tools/get-comments.js.map +1 -1
  40. package/dist/tools/get-coverage.d.ts +122 -0
  41. package/dist/tools/get-coverage.d.ts.map +1 -0
  42. package/dist/tools/get-coverage.js +105 -0
  43. package/dist/tools/get-coverage.js.map +1 -0
  44. package/dist/tools/harvest-channel.d.ts +92 -0
  45. package/dist/tools/harvest-channel.d.ts.map +1 -0
  46. package/dist/tools/harvest-channel.js +62 -0
  47. package/dist/tools/harvest-channel.js.map +1 -0
  48. package/dist/tools/harvest-comments.d.ts +81 -0
  49. package/dist/tools/harvest-comments.d.ts.map +1 -0
  50. package/dist/tools/harvest-comments.js +115 -0
  51. package/dist/tools/harvest-comments.js.map +1 -0
  52. package/dist/tools/library.js +1 -1
  53. package/dist/tools/library.js.map +1 -1
  54. package/dist/tools/list-channel-thumbnails.d.ts +10 -10
  55. package/dist/tools/list-channel-thumbnails.js +1 -1
  56. package/dist/tools/list-channel-thumbnails.js.map +1 -1
  57. package/dist/tools/list-library.js +1 -1
  58. package/dist/tools/list-library.js.map +1 -1
  59. package/dist/tools/prune-harvest.d.ts +59 -0
  60. package/dist/tools/prune-harvest.d.ts.map +1 -0
  61. package/dist/tools/prune-harvest.js +108 -0
  62. package/dist/tools/prune-harvest.js.map +1 -0
  63. package/dist/tools/query-comments.d.ts +125 -0
  64. package/dist/tools/query-comments.d.ts.map +1 -0
  65. package/dist/tools/query-comments.js +106 -0
  66. package/dist/tools/query-comments.js.map +1 -0
  67. package/dist/tools/query-videos.d.ts +118 -0
  68. package/dist/tools/query-videos.d.ts.map +1 -0
  69. package/dist/tools/query-videos.js +97 -0
  70. package/dist/tools/query-videos.js.map +1 -0
  71. package/dist/tools/repair-store.d.ts +34 -0
  72. package/dist/tools/repair-store.d.ts.map +1 -0
  73. package/dist/tools/repair-store.js +58 -0
  74. package/dist/tools/repair-store.js.map +1 -0
  75. package/dist/tools/search-channels.d.ts.map +1 -1
  76. package/dist/tools/search-channels.js +5 -1
  77. package/dist/tools/search-channels.js.map +1 -1
  78. package/dist/tools/search-videos.js +1 -1
  79. package/dist/tools/search-videos.js.map +1 -1
  80. package/dist/utils/comment-store.d.ts +71 -0
  81. package/dist/utils/comment-store.d.ts.map +1 -0
  82. package/dist/utils/comment-store.js +129 -0
  83. package/dist/utils/comment-store.js.map +1 -0
  84. package/dist/utils/comment-threads.d.ts +51 -0
  85. package/dist/utils/comment-threads.d.ts.map +1 -0
  86. package/dist/utils/comment-threads.js +76 -0
  87. package/dist/utils/comment-threads.js.map +1 -0
  88. package/dist/utils/coverage-text.d.ts +3 -0
  89. package/dist/utils/coverage-text.d.ts.map +1 -0
  90. package/dist/utils/coverage-text.js +62 -0
  91. package/dist/utils/coverage-text.js.map +1 -0
  92. package/dist/utils/coverage.d.ts +57 -0
  93. package/dist/utils/coverage.d.ts.map +1 -0
  94. package/dist/utils/coverage.js +142 -0
  95. package/dist/utils/coverage.js.map +1 -0
  96. package/dist/utils/errors.d.ts +1 -1
  97. package/dist/utils/errors.d.ts.map +1 -1
  98. package/dist/utils/errors.js +9 -1
  99. package/dist/utils/errors.js.map +1 -1
  100. package/dist/utils/format.d.ts +17 -1
  101. package/dist/utils/format.d.ts.map +1 -1
  102. package/dist/utils/format.js +1 -1
  103. package/dist/utils/format.js.map +1 -1
  104. package/dist/utils/harvest-catalog.d.ts +44 -0
  105. package/dist/utils/harvest-catalog.d.ts.map +1 -0
  106. package/dist/utils/harvest-catalog.js +164 -0
  107. package/dist/utils/harvest-catalog.js.map +1 -0
  108. package/dist/utils/harvest-lock.d.ts +3 -0
  109. package/dist/utils/harvest-lock.d.ts.map +1 -0
  110. package/dist/utils/harvest-lock.js +26 -0
  111. package/dist/utils/harvest-lock.js.map +1 -0
  112. package/dist/utils/harvest-receipts.d.ts +14 -0
  113. package/dist/utils/harvest-receipts.d.ts.map +1 -0
  114. package/dist/utils/harvest-receipts.js +110 -0
  115. package/dist/utils/harvest-receipts.js.map +1 -0
  116. package/dist/utils/listing-cursor.d.ts +31 -0
  117. package/dist/utils/listing-cursor.d.ts.map +1 -0
  118. package/dist/utils/listing-cursor.js +39 -0
  119. package/dist/utils/listing-cursor.js.map +1 -0
  120. package/dist/utils/pot-preflight.d.ts +53 -0
  121. package/dist/utils/pot-preflight.d.ts.map +1 -0
  122. package/dist/utils/pot-preflight.js +146 -0
  123. package/dist/utils/pot-preflight.js.map +1 -0
  124. package/dist/utils/sleep.d.ts +9 -0
  125. package/dist/utils/sleep.d.ts.map +1 -0
  126. package/dist/utils/sleep.js +25 -0
  127. package/dist/utils/sleep.js.map +1 -0
  128. package/dist/utils/store-health.d.ts +37 -0
  129. package/dist/utils/store-health.d.ts.map +1 -0
  130. package/dist/utils/store-health.js +92 -0
  131. package/dist/utils/store-health.js.map +1 -0
  132. package/dist/utils/store-migrations.d.ts +33 -0
  133. package/dist/utils/store-migrations.d.ts.map +1 -0
  134. package/dist/utils/store-migrations.js +41 -0
  135. package/dist/utils/store-migrations.js.map +1 -0
  136. package/dist/utils/store-paths.d.ts +33 -0
  137. package/dist/utils/store-paths.d.ts.map +1 -0
  138. package/dist/utils/store-paths.js +48 -0
  139. package/dist/utils/store-paths.js.map +1 -0
  140. package/dist/utils/store-rows.d.ts +22 -0
  141. package/dist/utils/store-rows.d.ts.map +1 -0
  142. package/dist/utils/store-rows.js +62 -0
  143. package/dist/utils/store-rows.js.map +1 -0
  144. package/dist/utils/store-schema.d.ts +16 -0
  145. package/dist/utils/store-schema.d.ts.map +1 -0
  146. package/dist/utils/store-schema.js +168 -0
  147. package/dist/utils/store-schema.js.map +1 -0
  148. package/dist/utils/store.d.ts +34 -0
  149. package/dist/utils/store.d.ts.map +1 -0
  150. package/dist/utils/store.js +157 -0
  151. package/dist/utils/store.js.map +1 -0
  152. package/dist/utils/video-stats-cache.d.ts +16 -0
  153. package/dist/utils/video-stats-cache.d.ts.map +1 -0
  154. package/dist/utils/video-stats-cache.js +81 -0
  155. package/dist/utils/video-stats-cache.js.map +1 -0
  156. package/dist/utils/video-stats.d.ts +186 -0
  157. package/dist/utils/video-stats.d.ts.map +1 -0
  158. package/dist/utils/video-stats.js +168 -0
  159. package/dist/utils/video-stats.js.map +1 -0
  160. package/dist/utils/video-store.d.ts +69 -0
  161. package/dist/utils/video-store.d.ts.map +1 -0
  162. package/dist/utils/video-store.js +77 -0
  163. package/dist/utils/video-store.js.map +1 -0
  164. package/dist/utils/youtube-channel.d.ts +9 -2
  165. package/dist/utils/youtube-channel.d.ts.map +1 -1
  166. package/dist/utils/youtube-channel.js +33 -3
  167. package/dist/utils/youtube-channel.js.map +1 -1
  168. package/dist/utils/youtube-schemas.d.ts +98 -0
  169. package/dist/utils/youtube-schemas.d.ts.map +1 -1
  170. package/dist/utils/youtube-schemas.js +30 -0
  171. package/dist/utils/youtube-schemas.js.map +1 -1
  172. package/dist/utils/youtube-video.d.ts +22 -1
  173. package/dist/utils/youtube-video.d.ts.map +1 -1
  174. package/dist/utils/youtube-video.js +40 -28
  175. package/dist/utils/youtube-video.js.map +1 -1
  176. package/dist/utils/ytdlp-limiter.d.ts +3 -0
  177. package/dist/utils/ytdlp-limiter.d.ts.map +1 -1
  178. package/dist/utils/ytdlp-limiter.js +19 -3
  179. package/dist/utils/ytdlp-limiter.js.map +1 -1
  180. package/dist/utils/ytdlp-pacer.d.ts +64 -0
  181. package/dist/utils/ytdlp-pacer.d.ts.map +1 -0
  182. package/dist/utils/ytdlp-pacer.js +165 -0
  183. package/dist/utils/ytdlp-pacer.js.map +1 -0
  184. package/dist/utils/ytdlp-stream.d.ts +32 -0
  185. package/dist/utils/ytdlp-stream.d.ts.map +1 -0
  186. package/dist/utils/ytdlp-stream.js +61 -0
  187. package/dist/utils/ytdlp-stream.js.map +1 -0
  188. package/dist/utils/ytdlp.d.ts +10 -0
  189. package/dist/utils/ytdlp.d.ts.map +1 -1
  190. package/dist/utils/ytdlp.js +36 -25
  191. package/dist/utils/ytdlp.js.map +1 -1
  192. package/package.json +2 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/youtube-knowledge-mcp.svg)](https://www.npmjs.com/package/youtube-knowledge-mcp)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
- [![Node.js](https://img.shields.io/badge/Node.js-22%2B-green.svg)](https://nodejs.org/)
5
+ [![Node.js](https://img.shields.io/badge/Node.js-22.13%2B-green.svg)](https://nodejs.org/)
6
6
  [![GitHub stars](https://img.shields.io/github/stars/teobouancheau/youtube-knowledge-mcp?style=social)](https://github.com/teobouancheau/youtube-knowledge-mcp)
7
7
 
8
8
  A Model Context Protocol (MCP) server that gives AI assistants the ability to search, analyze, and extract knowledge from YouTube videos. Works with Claude Desktop, Claude Code, Claude.ai, Cursor and any MCP-compatible client.
@@ -40,6 +40,19 @@ Supports both **local** (stdio) and **remote** (Streamable HTTP) transports.
40
40
  - **Read them back**, and **search across all of them** with full-text ranking
41
41
  - **Tag, retag and delete**
42
42
 
43
+ **Extract exhaustively, and prove it** (local mode)
44
+
45
+ - **Catalogue a whole channel** across every tab — videos, shorts, streams,
46
+ releases, podcasts — into a local SQLite store, resumably
47
+ - **Harvest comments with their replies**, every field YouTube returns, and
48
+ search them locally with full-text ranking and real pagination
49
+ - **A completeness receipt on every extraction**: `complete` is true only when
50
+ the server can _prove_ it holds everything in scope, so a partial harvest can
51
+ never be mistaken for a full history
52
+ - **Ask what you actually have** with `get_coverage`, which never touches the
53
+ network — cheap enough to call before every claim
54
+ - **Remove what you hold**, including one author's comments everywhere
55
+
43
56
  **Build a brain for a channel** (local mode)
44
57
 
45
58
  - **Read a whole channel** into a searchable corpus of timestamped passages, in
@@ -60,7 +73,8 @@ Supports both **local** (stdio) and **remote** (Streamable HTTP) transports.
60
73
  - WebVTT parsed by the W3C reference implementation, not a hand-written matcher
61
74
  - Typed, actionable errors — "no captions in en, try: fr, es, de" rather than a
62
75
  wall of yt-dlp stderr
63
- - Timeouts, retry with backoff, and a concurrency limit on every yt-dlp call
76
+ - Timeouts, adaptive backoff and a concurrency limit on every yt-dlp call, with
77
+ cooldowns and a circuit breaker so a long harvest survives being throttled
64
78
  - `check_health` diagnoses missing or outdated yt-dlp and ffmpeg
65
79
  - Bearer auth, a loopback default bind, an allowlist on every URL and image host, and typed error codes on every failure
66
80
  - Structured output on every tool, plus MCP resources, prompts and completions
@@ -68,7 +82,7 @@ Supports both **local** (stdio) and **remote** (Streamable HTTP) transports.
68
82
 
69
83
  ## Prerequisites
70
84
 
71
- - Node.js 22+
85
+ - Node.js 22.13+ — the version where `node:sqlite`, which the harvest store uses, is available without a flag
72
86
  - [yt-dlp](https://github.com/yt-dlp/yt-dlp#installation) — required by every tool.
73
87
  `brew install yt-dlp` (macOS) or `pip install -U yt-dlp`
74
88
  - [ffmpeg](https://ffmpeg.org/) — required for downloads, clip extraction and frame
@@ -221,7 +235,7 @@ a cold start. Any paid plan removes that.
221
235
 
222
236
  ## MCP Tools
223
237
 
224
- 37 tools. The 15 read-only ones work over both transports; the 22 that touch
238
+ 44 tools. The 15 read-only ones work over both transports; the 29 that touch
225
239
  your filesystem are registered only in local (stdio) mode, so a remote
226
240
  deployment cannot reach the host's disk.
227
241
 
@@ -231,19 +245,19 @@ available for this video. Call get_transcript again with one of: fr, es, de.`
231
245
 
232
246
  ### Discovery — remote + local
233
247
 
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 |
248
+ | Tool | Key parameters | Returns |
249
+ | ------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------- |
250
+ | `search_videos` | `query`, `limit` | Matching videos with durations, channels and view counts |
251
+ | `search_channels` | `query`, `limit` | Matching channels with subscriber counts |
252
+ | `fetch_videos` | `url`, `limit`, `cursor` | One page of a playlist or channel, with a cursor. `total` is present only when YouTube states one |
253
+ | `get_video_info` | `video` | Title, channel, duration, views, likes, description, tags |
254
+ | `get_channel_info` | `channel` | Name, handle, subscriber count, description, avatar and banner |
255
+ | `get_playlist_info` | `url` | Title, channel, video count, last updated |
256
+ | `get_chapters` | `video` | Chapter titles with start/end times and deep links |
257
+ | `get_comments` | `video`, `limit`, `includeReplies`, `sort` | Comments with replies and a completeness receipt saying how many of the video's comments this is |
258
+ | `list_formats` | `video` | Available formats grouped by video+audio, video-only, audio-only |
259
+ | `get_thumbnail` | `video`, `channel`, `image`, `quality` | A video thumbnail, channel avatar or banner as an image, with its real size |
260
+ | `check_health` | — | yt-dlp and ffmpeg status, versions, staleness warnings, and which session options are set |
247
261
 
248
262
  ### Transcripts — remote + local
249
263
 
@@ -286,6 +300,35 @@ keyframe-aligned cut. All of these require ffmpeg.
286
300
  | `delete_library_item` | `videoId`, `contentType` | What was deleted |
287
301
  | `rebuild_library_index` | — | Number of notes reindexed |
288
302
 
303
+ ### Exhaustive harvest — local only
304
+
305
+ Extraction that stores what it reads, and proves what it holds. Every tool here
306
+ returns a **completeness receipt**: `complete` is true only when the server can
307
+ prove it has everything in scope, and a consumer must not describe the data as a
308
+ full history while it is false.
309
+
310
+ | Tool | Key parameters | Returns |
311
+ | ------------------ | ---------------------------------------------------------------------- | ----------------------------------------------------------- |
312
+ | `harvest_channel` | `channel`, `maxVideos` | Every video across all tabs, catalogued, with a receipt |
313
+ | `harvest_comments` | `video`, `maxComments`, `sort`, `maxRepliesPerThread` | Comments and replies stored locally, with a receipt |
314
+ | `get_coverage` | `scope`, `incompleteOnly`, `verify`, `limit`, `offset` | What the server can prove it holds, and what it cannot |
315
+ | `query_comments` | `video`, `channel`, `match`, `author`, `threadOf`, `order`, `limit` | Full-text search over stored comments, with real pagination |
316
+ | `query_videos` | `channel`, `match`, `since`, `until`, `withComments`, `order`, `limit` | The catalogued videos, filtered and sorted |
317
+ | `prune_harvest` | `channel`, `video`, `author`, `scope`, `confirm`, `vacuum` | What was removed from the store |
318
+ | `repair_store` | `confirm` | Moves a damaged store aside and starts a fresh one |
319
+
320
+ `get_coverage` never contacts YouTube, so it is cheap enough to call before
321
+ making any claim about the data. Check `anyIncomplete` first.
322
+
323
+ There is no cursor for comments: YouTube exposes none for a live read, so
324
+ `harvest_comments` cannot page. To get more, run it again with a larger
325
+ `maxComments` — rows are upserted on the comment id, so a re-run costs requests
326
+ and never data.
327
+
328
+ `prune_harvest` exists because the store holds personal data at scale: display
329
+ names, channel ids and free-text opinions from people who never used this tool.
330
+ The `author` filter is the per-person erasure path.
331
+
289
332
  ### Channel brains — local only
290
333
 
291
334
  | Tool | Key parameters | Returns |
@@ -392,6 +435,7 @@ All optional.
392
435
  | `MCP_MAX_SESSIONS` | `1000` | Reject new sessions past this many |
393
436
  | `YOUTUBE_MCP_MAX_CONCURRENCY` | `3` | Concurrent yt-dlp processes |
394
437
  | `YOUTUBE_MCP_TRANSCRIPT_TTL_MS` | 30 days | Transcript cache lifetime |
438
+ | `YOUTUBE_MCP_STATS_TTL_MS` | 1 day | Per-video stats cache lifetime |
395
439
  | `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
440
  | `YOUTUBE_MCP_COOKIES_FILE` | unset | A Netscape-format cookies file inside your home directory, as an alternative to a browser |
397
441
  | `YOUTUBE_MCP_PROXY` | unset | An `http`, `https`, `socks4` or `socks5` proxy URL for yt-dlp |
@@ -549,7 +593,7 @@ npm run start:http # Run server (HTTP)
549
593
  npm run validate # Typecheck + lint + format check + test
550
594
  ```
551
595
 
552
- CI runs the same gate on Node 22 and 24 for every push and pull request,
596
+ CI runs the same gate on Node 22.22, 22 and 24 for every push and pull request,
553
597
  then boots the built server as a real MCP client to verify the manifest.
554
598
 
555
599
  ## Contributing
@@ -0,0 +1,75 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * The shapes a completeness receipt is made of.
4
+ *
5
+ * This server exists to extract everything YouTube will serve, and the failure
6
+ * mode that matters is not "we got 3%" — it is "we got 3% and reported 100%".
7
+ * A receipt travels with every extraction result so a consumer can never
8
+ * describe a sample as a full history by accident.
9
+ *
10
+ * The invariants that make `complete` mean something live in
11
+ * `utils/coverage.ts`, which is the only place allowed to build one.
12
+ */
13
+ export declare const harvestScopeSchema: z.ZodEnum<["channel-catalog", "video-details", "video-comments"]>;
14
+ export type HarvestScope = z.infer<typeof harvestScopeSchema>;
15
+ export declare const completenessReasonSchema: z.ZodEnum<["COMPLETE", "CAP_REACHED", "BUDGET_SPENT", "RATE_LIMITED", "CANCELLED", "TIMEOUT", "SOURCE_REFUSED", "COMMENTS_DISABLED", "SOURCE_SILENT", "PARTIAL_ERROR", "NOT_ATTEMPTED"]>;
16
+ export type CompletenessReason = z.infer<typeof completenessReasonSchema>;
17
+ /**
18
+ * Where a total came from.
19
+ *
20
+ * `source-exhausted` is the one value that is not a number YouTube reported:
21
+ * it means the listing ended on its own, which is its own kind of proof.
22
+ */
23
+ export declare const expectedSourceSchema: z.ZodEnum<["youtube:comment_count", "youtube:playlist_count", "source-exhausted"]>;
24
+ export type ExpectedSource = z.infer<typeof expectedSourceSchema>;
25
+ export declare const coverageSchema: z.ZodObject<{
26
+ scope: z.ZodEnum<["channel-catalog", "video-details", "video-comments"]>;
27
+ targetId: z.ZodString;
28
+ complete: z.ZodBoolean;
29
+ reason: z.ZodEnum<["COMPLETE", "CAP_REACHED", "BUDGET_SPENT", "RATE_LIMITED", "CANCELLED", "TIMEOUT", "SOURCE_REFUSED", "COMMENTS_DISABLED", "SOURCE_SILENT", "PARTIAL_ERROR", "NOT_ATTEMPTED"]>;
30
+ have: z.ZodNumber;
31
+ expected: z.ZodOptional<z.ZodNumber>;
32
+ expectedSource: z.ZodOptional<z.ZodEnum<["youtube:comment_count", "youtube:playlist_count", "source-exhausted"]>>;
33
+ ranToExhaustion: z.ZodOptional<z.ZodBoolean>;
34
+ source: z.ZodString;
35
+ harvestedAt: z.ZodString;
36
+ staleAfter: z.ZodOptional<z.ZodString>;
37
+ limitApplied: z.ZodOptional<z.ZodNumber>;
38
+ sortApplied: z.ZodOptional<z.ZodEnum<["top", "new"]>>;
39
+ resumeToken: z.ZodOptional<z.ZodString>;
40
+ note: z.ZodOptional<z.ZodString>;
41
+ }, "strip", z.ZodTypeAny, {
42
+ have: number;
43
+ scope: "channel-catalog" | "video-details" | "video-comments";
44
+ targetId: string;
45
+ complete: boolean;
46
+ reason: "RATE_LIMITED" | "TIMEOUT" | "CANCELLED" | "COMPLETE" | "CAP_REACHED" | "BUDGET_SPENT" | "SOURCE_REFUSED" | "COMMENTS_DISABLED" | "SOURCE_SILENT" | "PARTIAL_ERROR" | "NOT_ATTEMPTED";
47
+ source: string;
48
+ harvestedAt: string;
49
+ expected?: number | undefined;
50
+ note?: string | undefined;
51
+ ranToExhaustion?: boolean | undefined;
52
+ expectedSource?: "youtube:playlist_count" | "source-exhausted" | "youtube:comment_count" | undefined;
53
+ staleAfter?: string | undefined;
54
+ limitApplied?: number | undefined;
55
+ sortApplied?: "top" | "new" | undefined;
56
+ resumeToken?: string | undefined;
57
+ }, {
58
+ have: number;
59
+ scope: "channel-catalog" | "video-details" | "video-comments";
60
+ targetId: string;
61
+ complete: boolean;
62
+ reason: "RATE_LIMITED" | "TIMEOUT" | "CANCELLED" | "COMPLETE" | "CAP_REACHED" | "BUDGET_SPENT" | "SOURCE_REFUSED" | "COMMENTS_DISABLED" | "SOURCE_SILENT" | "PARTIAL_ERROR" | "NOT_ATTEMPTED";
63
+ source: string;
64
+ harvestedAt: string;
65
+ expected?: number | undefined;
66
+ note?: string | undefined;
67
+ ranToExhaustion?: boolean | undefined;
68
+ expectedSource?: "youtube:playlist_count" | "source-exhausted" | "youtube:comment_count" | undefined;
69
+ staleAfter?: string | undefined;
70
+ limitApplied?: number | undefined;
71
+ sortApplied?: "top" | "new" | undefined;
72
+ resumeToken?: string | undefined;
73
+ }>;
74
+ export type Coverage = z.infer<typeof coverageSchema>;
75
+ //# sourceMappingURL=harvest-schemas.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harvest-schemas.d.ts","sourceRoot":"","sources":["../src/harvest-schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;GAUG;AAEH,eAAO,MAAM,kBAAkB,mEAO7B,CAAC;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,kBAAkB,CAAC,CAAC;AAE9D,eAAO,MAAM,wBAAwB,0LAqBnC,CAAC;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAE1E;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,oFAI/B,CAAC;AACH,MAAM,MAAM,cAAc,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,oBAAoB,CAAC,CAAC;AAElE,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA4EzB,CAAC;AACH,MAAM,MAAM,QAAQ,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,cAAc,CAAC,CAAC"}
@@ -0,0 +1,106 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * The shapes a completeness receipt is made of.
4
+ *
5
+ * This server exists to extract everything YouTube will serve, and the failure
6
+ * mode that matters is not "we got 3%" — it is "we got 3% and reported 100%".
7
+ * A receipt travels with every extraction result so a consumer can never
8
+ * describe a sample as a full history by accident.
9
+ *
10
+ * The invariants that make `complete` mean something live in
11
+ * `utils/coverage.ts`, which is the only place allowed to build one.
12
+ */
13
+ export const harvestScopeSchema = z.enum([
14
+ /** Which videos a channel has. */
15
+ 'channel-catalog',
16
+ /** Full per-video metadata snapshots. */
17
+ 'video-details',
18
+ /** Comments and replies for one video. */
19
+ 'video-comments',
20
+ ]);
21
+ export const completenessReasonSchema = z.enum([
22
+ /** Provably everything: the source stated a total and we hold it, or the source ran out. */
23
+ 'COMPLETE',
24
+ /** The caller's own limit stopped it. */
25
+ 'CAP_REACHED',
26
+ /** The wall-clock budget for this call ran out. */
27
+ 'BUDGET_SPENT',
28
+ /** YouTube throttled this client. */
29
+ 'RATE_LIMITED',
30
+ /** The MCP client aborted the request. */
31
+ 'CANCELLED',
32
+ 'TIMEOUT',
33
+ /** BOT_CHECK, LOGIN_REQUIRED, PRIVATE, AGE_GATED or MEMBERS_ONLY. */
34
+ 'SOURCE_REFUSED',
35
+ /** Comments are turned off for this video, so zero is the true total. */
36
+ 'COMMENTS_DISABLED',
37
+ /** The source states no total, so exhaustion is unprovable. */
38
+ 'SOURCE_SILENT',
39
+ /** Some items failed; each cost only itself. */
40
+ 'PARTIAL_ERROR',
41
+ 'NOT_ATTEMPTED',
42
+ ]);
43
+ /**
44
+ * Where a total came from.
45
+ *
46
+ * `source-exhausted` is the one value that is not a number YouTube reported:
47
+ * it means the listing ended on its own, which is its own kind of proof.
48
+ */
49
+ export const expectedSourceSchema = z.enum([
50
+ 'youtube:comment_count',
51
+ 'youtube:playlist_count',
52
+ 'source-exhausted',
53
+ ]);
54
+ export const coverageSchema = z.object({
55
+ scope: harvestScopeSchema,
56
+ targetId: z.string().describe('Channel id for a catalog, video id for details or comments'),
57
+ complete: z
58
+ .boolean()
59
+ .describe('True ONLY when this server can prove it holds every item in scope. False means the ' +
60
+ 'data is a SAMPLE. A consumer must not describe it as a full history while this is false.'),
61
+ reason: completenessReasonSchema.describe('Why complete is what it is. Always present, including when complete is true.'),
62
+ have: z
63
+ .number()
64
+ .int()
65
+ .nonnegative()
66
+ .describe('Items this store actually holds for this target, counted from the store.'),
67
+ expected: z
68
+ .number()
69
+ .int()
70
+ .nonnegative()
71
+ .optional()
72
+ .describe('The total the SOURCE states. Absent when the source states none — never inferred, ' +
73
+ 'never copied from have. Absent with complete:false means "unknown how much is missing".'),
74
+ expectedSource: expectedSourceSchema
75
+ .optional()
76
+ .describe('Where expected came from. Absent if and only if expected is absent.'),
77
+ ranToExhaustion: z
78
+ .boolean()
79
+ .optional()
80
+ .describe("Whether the source's own iteration finished rather than being cut off. Necessary for " +
81
+ 'completeness but not sufficient: a cap also ends iteration.'),
82
+ source: z.string().describe('The exact extraction path, including tool version and flags.'),
83
+ harvestedAt: z.string().describe('ISO-8601 of the last item written for this target'),
84
+ staleAfter: z
85
+ .string()
86
+ .optional()
87
+ .describe('ISO-8601 after which this should be re-checked. Comments accrue, so a complete ' +
88
+ 'receipt goes stale without becoming wrong.'),
89
+ limitApplied: z
90
+ .number()
91
+ .int()
92
+ .positive()
93
+ .optional()
94
+ .describe('The cap the caller asked for. Present if and only if one was applied.'),
95
+ sortApplied: z
96
+ .enum(['top', 'new'])
97
+ .optional()
98
+ .describe('Ordering the source applied. A capped "top" harvest is a BIASED PREFIX of a stream, ' +
99
+ 'not a random sample.'),
100
+ resumeToken: z
101
+ .string()
102
+ .optional()
103
+ .describe('Opaque. Pass to the same tool to continue. Absent if and only if complete.'),
104
+ note: z.string().optional(),
105
+ });
106
+ //# sourceMappingURL=harvest-schemas.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harvest-schemas.js","sourceRoot":"","sources":["../src/harvest-schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB;;;;;;;;;;GAUG;AAEH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,CAAC,IAAI,CAAC;IACvC,kCAAkC;IAClC,iBAAiB;IACjB,yCAAyC;IACzC,eAAe;IACf,0CAA0C;IAC1C,gBAAgB;CACjB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,CAAC,IAAI,CAAC;IAC7C,4FAA4F;IAC5F,UAAU;IACV,yCAAyC;IACzC,aAAa;IACb,mDAAmD;IACnD,cAAc;IACd,qCAAqC;IACrC,cAAc;IACd,0CAA0C;IAC1C,WAAW;IACX,SAAS;IACT,qEAAqE;IACrE,gBAAgB;IAChB,yEAAyE;IACzE,mBAAmB;IACnB,+DAA+D;IAC/D,eAAe;IACf,gDAAgD;IAChD,eAAe;IACf,eAAe;CAChB,CAAC,CAAC;AAGH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC,IAAI,CAAC;IACzC,uBAAuB;IACvB,wBAAwB;IACxB,kBAAkB;CACnB,CAAC,CAAC;AAGH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC,MAAM,CAAC;IACrC,KAAK,EAAE,kBAAkB;IACzB,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,4DAA4D,CAAC;IAE3F,QAAQ,EAAE,CAAC;SACR,OAAO,EAAE;SACT,QAAQ,CACP,qFAAqF;QACnF,0FAA0F,CAC7F;IAEH,MAAM,EAAE,wBAAwB,CAAC,QAAQ,CACvC,8EAA8E,CAC/E;IAED,IAAI,EAAE,CAAC;SACJ,MAAM,EAAE;SACR,GAAG,EAAE;SACL,WAAW,EAAE;SACb,QAAQ,CAAC,0EAA0E,CAAC;IAEvF,QAAQ,EAAE,CAAC;SACR,MAAM,EAAE;SACR,GAAG,EAAE;SACL,WAAW,EAAE;SACb,QAAQ,EAAE;SACV,QAAQ,CACP,oFAAoF;QAClF,yFAAyF,CAC5F;IAEH,cAAc,EAAE,oBAAoB;SACjC,QAAQ,EAAE;SACV,QAAQ,CAAC,qEAAqE,CAAC;IAElF,eAAe,EAAE,CAAC;SACf,OAAO,EAAE;SACT,QAAQ,EAAE;SACV,QAAQ,CACP,uFAAuF;QACrF,6DAA6D,CAChE;IAEH,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,8DAA8D,CAAC;IAE3F,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,mDAAmD,CAAC;IAErF,UAAU,EAAE,CAAC;SACV,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CACP,iFAAiF;QAC/E,4CAA4C,CAC/C;IAEH,YAAY,EAAE,CAAC;SACZ,MAAM,EAAE;SACR,GAAG,EAAE;SACL,QAAQ,EAAE;SACV,QAAQ,EAAE;SACV,QAAQ,CAAC,uEAAuE,CAAC;IAEpF,WAAW,EAAE,CAAC;SACX,IAAI,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;SACpB,QAAQ,EAAE;SACV,QAAQ,CACP,sFAAsF;QACpF,sBAAsB,CACzB;IAEH,WAAW,EAAE,CAAC;SACX,MAAM,EAAE;SACR,QAAQ,EAAE;SACV,QAAQ,CAAC,4EAA4E,CAAC;IAEzF,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAC5B,CAAC,CAAC"}
@@ -0,0 +1,10 @@
1
+ import { type ToolDefinition } from './types.js';
2
+ /**
3
+ * Exhaustive extraction and the receipts that keep it honest.
4
+ *
5
+ * All stdio-only: they write to the local store, which a channel harvest
6
+ * measures in gigabytes, and no remote caller should be able to fill the
7
+ * operator's disk by naming a channel.
8
+ */
9
+ export declare const harvestTools: ToolDefinition[];
10
+ //# sourceMappingURL=harvest-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harvest-tools.d.ts","sourceRoot":"","sources":["../../src/registry/harvest-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAgC7D;;;;;;GAMG;AACH,eAAO,MAAM,YAAY,EAAE,cAAc,EAmGxC,CAAC"}
@@ -0,0 +1,109 @@
1
+ import { defineTool } from './types.js';
2
+ import { harvestChannelSchema, harvestChannelOutputSchema, harvestChannelHandler, } from '../tools/harvest-channel.js';
3
+ import { getCoverageSchema, getCoverageOutputSchema, getCoverageHandler, } from '../tools/get-coverage.js';
4
+ import { harvestCommentsSchema, harvestCommentsOutputSchema, harvestCommentsHandler, } from '../tools/harvest-comments.js';
5
+ import { queryCommentsSchema, queryCommentsOutputSchema, queryCommentsHandler, } from '../tools/query-comments.js';
6
+ import { queryVideosSchema, queryVideosOutputSchema, queryVideosHandler, } from '../tools/query-videos.js';
7
+ import { pruneHarvestSchema, pruneHarvestOutputSchema, pruneHarvestHandler, } from '../tools/prune-harvest.js';
8
+ /**
9
+ * Exhaustive extraction and the receipts that keep it honest.
10
+ *
11
+ * All stdio-only: they write to the local store, which a channel harvest
12
+ * measures in gigabytes, and no remote caller should be able to fill the
13
+ * operator's disk by naming a channel.
14
+ */
15
+ export const harvestTools = [
16
+ defineTool({
17
+ name: 'harvest_channel',
18
+ mode: 'stdio',
19
+ title: 'Harvest Channel Catalogue',
20
+ description: 'Catalogue every video a channel lists, across all of its tabs (videos, shorts, streams, releases, podcasts), into the local store. Returns a completeness receipt saying whether the catalogue is provably whole. Call it again to continue: videos are keyed on their id, so a re-run costs requests and never data.',
21
+ inputSchema: harvestChannelSchema,
22
+ outputSchema: harvestChannelOutputSchema,
23
+ annotations: {
24
+ readOnlyHint: false,
25
+ // Only ever adds and upserts; `have` never decreases. Deleting is
26
+ // prune_harvest's job alone.
27
+ destructiveHint: false,
28
+ idempotentHint: true,
29
+ openWorldHint: true,
30
+ },
31
+ handler: harvestChannelHandler,
32
+ }),
33
+ defineTool({
34
+ name: 'get_coverage',
35
+ mode: 'stdio',
36
+ title: 'Get Harvest Coverage',
37
+ description: 'Report what this server can actually prove it holds: one completeness receipt per harvested target, plus whether ANY of them is incomplete. Reads only the local store and never contacts YouTube, so it is cheap to call before making any claim about the data. Check anyIncomplete before describing anything as a full history.',
38
+ inputSchema: getCoverageSchema,
39
+ outputSchema: getCoverageOutputSchema,
40
+ annotations: {
41
+ readOnlyHint: true,
42
+ destructiveHint: false,
43
+ idempotentHint: true,
44
+ openWorldHint: false,
45
+ },
46
+ handler: getCoverageHandler,
47
+ }),
48
+ defineTool({
49
+ name: 'harvest_comments',
50
+ mode: 'stdio',
51
+ title: 'Harvest Video Comments',
52
+ description: "Extract a video's comments and replies into the local store, with a completeness receipt. There is no comment cursor — an interrupted run keeps nothing for that video, so to get more, run it again with a larger maxComments. Re-running upserts on the comment id, so it costs requests and never data.",
53
+ inputSchema: harvestCommentsSchema,
54
+ outputSchema: harvestCommentsOutputSchema,
55
+ annotations: {
56
+ readOnlyHint: false,
57
+ destructiveHint: false,
58
+ idempotentHint: true,
59
+ openWorldHint: true,
60
+ },
61
+ handler: harvestCommentsHandler,
62
+ }),
63
+ defineTool({
64
+ name: 'query_comments',
65
+ mode: 'stdio',
66
+ title: 'Query Harvested Comments',
67
+ description: 'Search comments already in the local store, with full-text matching, real pagination and thread lookup. Never contacts YouTube. The total counts rows in the store, not the number of comments the videos have — the coverage receipts alongside it say which videos are only partly harvested.',
68
+ inputSchema: queryCommentsSchema,
69
+ outputSchema: queryCommentsOutputSchema,
70
+ annotations: {
71
+ readOnlyHint: true,
72
+ destructiveHint: false,
73
+ idempotentHint: true,
74
+ openWorldHint: false,
75
+ },
76
+ handler: queryCommentsHandler,
77
+ }),
78
+ defineTool({
79
+ name: 'query_videos',
80
+ mode: 'stdio',
81
+ title: 'Query Catalogued Videos',
82
+ description: 'Search the videos already catalogued in the local store, by channel, title, date, duration or how many comments are held. Never contacts YouTube. The coverage receipts alongside the results say which channels are only partly catalogued.',
83
+ inputSchema: queryVideosSchema,
84
+ outputSchema: queryVideosOutputSchema,
85
+ annotations: {
86
+ readOnlyHint: true,
87
+ destructiveHint: false,
88
+ idempotentHint: true,
89
+ openWorldHint: false,
90
+ },
91
+ handler: queryVideosHandler,
92
+ }),
93
+ defineTool({
94
+ name: 'prune_harvest',
95
+ mode: 'stdio',
96
+ title: 'Prune Harvested Data',
97
+ description: "Remove harvested comments, catalogues, or one author's comments everywhere, from the local store. Requires confirm: true — harvested comments cost hours of network that no local cache can replay. Use the author filter to honour an individual's erasure request.",
98
+ inputSchema: pruneHarvestSchema,
99
+ outputSchema: pruneHarvestOutputSchema,
100
+ annotations: {
101
+ readOnlyHint: false,
102
+ destructiveHint: true,
103
+ idempotentHint: true,
104
+ openWorldHint: false,
105
+ },
106
+ handler: pruneHarvestHandler,
107
+ }),
108
+ ];
109
+ //# sourceMappingURL=harvest-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"harvest-tools.js","sourceRoot":"","sources":["../../src/registry/harvest-tools.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"health-tools.d.ts","sourceRoot":"","sources":["../../src/registry/health-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAO7D,gCAAgC;AAChC,eAAO,MAAM,WAAW,EAAE,cAAc,EAiBvC,CAAC"}
1
+ {"version":3,"file":"health-tools.d.ts","sourceRoot":"","sources":["../../src/registry/health-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,YAAY,CAAC;AAY7D,gCAAgC;AAChC,eAAO,MAAM,WAAW,EAAE,cAAc,EAmCvC,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { defineTool } from './types.js';
2
2
  import { checkHealthSchema, checkHealthOutputSchema, checkHealthHandler, } from '../tools/check-health.js';
3
+ import { repairStoreSchema, repairStoreOutputSchema, repairStoreHandler, } from '../tools/repair-store.js';
3
4
  /** Diagnostics. Remote-safe. */
4
5
  export const healthTools = [
5
6
  defineTool({
@@ -17,5 +18,22 @@ export const healthTools = [
17
18
  },
18
19
  handler: checkHealthHandler,
19
20
  }),
21
+ defineTool({
22
+ name: 'repair_store',
23
+ // Local-only: it renames a file in the user's data directory, and every
24
+ // tool that writes to disk is stdio-only.
25
+ mode: 'stdio',
26
+ title: 'Repair Harvested Store',
27
+ description: 'Check the harvested store and, if it is damaged, move it aside and start a fresh one. The damaged file is kept, never deleted. Call this when a harvest tool reports STORE_CORRUPT.',
28
+ inputSchema: repairStoreSchema,
29
+ outputSchema: repairStoreOutputSchema,
30
+ annotations: {
31
+ readOnlyHint: false,
32
+ destructiveHint: true,
33
+ idempotentHint: false,
34
+ openWorldHint: false,
35
+ },
36
+ handler: repairStoreHandler,
37
+ }),
20
38
  ];
21
39
  //# sourceMappingURL=health-tools.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"health-tools.js","sourceRoot":"","sources":["../../src/registry/health-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAuB,MAAM,YAAY,CAAC;AAC7D,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAElC,gCAAgC;AAChC,MAAM,CAAC,MAAM,WAAW,GAAqB;IAC3C,UAAU,CAAC;QACT,IAAI,EAAE,cAAc;QACpB,IAAI,EAAE,KAAK;QACX,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EACT,mMAAmM;QACrM,WAAW,EAAE,iBAAiB;QAC9B,YAAY,EAAE,uBAAuB;QACrC,WAAW,EAAE;YACX,YAAY,EAAE,IAAI;YAClB,eAAe,EAAE,KAAK;YACtB,cAAc,EAAE,IAAI;YACpB,aAAa,EAAE,KAAK;SACrB;QACD,OAAO,EAAE,kBAAkB;KAC5B,CAAC;CACH,CAAC"}
1
+ {"version":3,"file":"health-tools.js","sourceRoot":"","sources":["../../src/registry/health-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAuB,MAAM,YAAY,CAAC;AAC7D,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAElC,gCAAgC;AAChC,MAAM,CAAC,MAAM,WAAW,GAAqB;IAC3C,UAAU,CAAC;QACT,IAAI,EAAE,cAAc;QACpB,IAAI,EAAE,KAAK;QACX,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EACT,mMAAmM;QACrM,WAAW,EAAE,iBAAiB;QAC9B,YAAY,EAAE,uBAAuB;QACrC,WAAW,EAAE;YACX,YAAY,EAAE,IAAI;YAClB,eAAe,EAAE,KAAK;YACtB,cAAc,EAAE,IAAI;YACpB,aAAa,EAAE,KAAK;SACrB;QACD,OAAO,EAAE,kBAAkB;KAC5B,CAAC;IACF,UAAU,CAAC;QACT,IAAI,EAAE,cAAc;QACpB,wEAAwE;QACxE,0CAA0C;QAC1C,IAAI,EAAE,OAAO;QACb,KAAK,EAAE,wBAAwB;QAC/B,WAAW,EACT,qLAAqL;QACvL,WAAW,EAAE,iBAAiB;QAC9B,YAAY,EAAE,uBAAuB;QACrC,WAAW,EAAE;YACX,YAAY,EAAE,KAAK;YACnB,eAAe,EAAE,IAAI;YACrB,cAAc,EAAE,KAAK;YACrB,aAAa,EAAE,KAAK;SACrB;QACD,OAAO,EAAE,kBAAkB;KAC5B,CAAC;CACH,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/registry/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AASzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,YAAY,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3D,sEAAsE;AACtE,eAAO,MAAM,KAAK,EAAE,cAAc,EASjC,CAAC;AAEF,oGAAoG;AACpG,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAI7E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/registry/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAUzE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD,YAAY,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAE3D,sEAAsE;AACtE,eAAO,MAAM,KAAK,EAAE,cAAc,EAUjC,CAAC;AAEF,oGAAoG;AACpG,wBAAgB,aAAa,CAAC,MAAM,EAAE,SAAS,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAI7E"}
@@ -1,3 +1,4 @@
1
+ import { harvestTools } from './harvest-tools.js';
1
2
  import { brainTools } from './brain-tools.js';
2
3
  import { channelTools } from './channel-tools.js';
3
4
  import { healthTools } from './health-tools.js';
@@ -15,6 +16,7 @@ export const TOOLS = [
15
16
  ...libraryTools,
16
17
  ...mediaTools,
17
18
  ...brainTools,
19
+ ...harvestTools,
18
20
  ...thumbnailTools,
19
21
  ];
20
22
  /** Registers the tools the transport may carry: everything on stdio, remote-safe ones over HTTP. */
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/registry/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAK9C,sEAAsE;AACtE,MAAM,CAAC,MAAM,KAAK,GAAqB;IACrC,GAAG,UAAU;IACb,GAAG,YAAY;IACf,GAAG,eAAe;IAClB,GAAG,WAAW;IACd,GAAG,YAAY;IACf,GAAG,UAAU;IACb,GAAG,UAAU;IACb,GAAG,cAAc;CAClB,CAAC;AAEF,oGAAoG;AACpG,MAAM,UAAU,aAAa,CAAC,MAAiB,EAAE,IAAsB;IACrE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,OAAO;YAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC3E,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/registry/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAChD,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC9C,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAK9C,sEAAsE;AACtE,MAAM,CAAC,MAAM,KAAK,GAAqB;IACrC,GAAG,UAAU;IACb,GAAG,YAAY;IACf,GAAG,eAAe;IAClB,GAAG,WAAW;IACd,GAAG,YAAY;IACf,GAAG,UAAU;IACb,GAAG,UAAU;IACb,GAAG,YAAY;IACf,GAAG,cAAc;CAClB,CAAC;AAEF,oGAAoG;AACpG,MAAM,UAAU,aAAa,CAAC,MAAiB,EAAE,IAAsB;IACrE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAI,CAAC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,OAAO;YAAE,IAAI,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;IAC3E,CAAC;AACH,CAAC"}
@@ -71,7 +71,7 @@ export const videoTools = [
71
71
  name: 'get_comments',
72
72
  mode: 'all',
73
73
  title: 'Get YouTube Video Comments',
74
- description: 'Get top comments from a YouTube video sorted by popularity. Returns author, text, like count, and pinned status. Only top-level comments, no replies.',
74
+ description: "Read a sample of a video's comments, with replies when includeReplies is set. Returns a completeness receipt saying how many of the video's comments this is — the real total comes from a separate metadata read, because a comment fetch reports only what it extracted. This is a sample, not a page: YouTube exposes no cursor for a live comment read.",
75
75
  inputSchema: getCommentsSchema,
76
76
  outputSchema: getCommentsOutputSchema,
77
77
  annotations: {
@@ -1 +1 @@
1
- {"version":3,"file":"video-tools.js","sourceRoot":"","sources":["../../src/registry/video-tools.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"video-tools.js","sourceRoot":"","sources":["../../src/registry/video-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAuB,MAAM,YAAY,CAAC;AAC7D,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,iBAAiB,EACjB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,kBAAkB,EAClB,wBAAwB,EACxB,mBAAmB,GACpB,MAAM,2BAA2B,CAAC;AAEnC,iHAAiH;AACjH,MAAM,CAAC,MAAM,UAAU,GAAqB;IAC1C,UAAU,CAAC;QACT,IAAI,EAAE,eAAe;QACrB,IAAI,EAAE,KAAK;QACX,KAAK,EAAE,uBAAuB;QAC9B,WAAW,EACT,qJAAqJ;QACvJ,WAAW,EAAE,kBAAkB;QAC/B,YAAY,EAAE,wBAAwB;QACtC,WAAW,EAAE;YACX,YAAY,EAAE,IAAI;YAClB,eAAe,EAAE,KAAK;YACtB,cAAc,EAAE,IAAI;YACpB,aAAa,EAAE,IAAI;SACpB;QACD,OAAO,EAAE,mBAAmB;KAC7B,CAAC;IACF,UAAU,CAAC;QACT,IAAI,EAAE,cAAc;QACpB,IAAI,EAAE,KAAK;QACX,KAAK,EAAE,sBAAsB;QAC7B,WAAW,EACT,oJAAoJ;QACtJ,WAAW,EAAE,iBAAiB;QAC9B,YAAY,EAAE,uBAAuB;QACrC,WAAW,EAAE;YACX,YAAY,EAAE,IAAI;YAClB,eAAe,EAAE,KAAK;YACtB,cAAc,EAAE,IAAI;YACpB,aAAa,EAAE,IAAI;SACpB;QACD,OAAO,EAAE,kBAAkB;KAC5B,CAAC;IACF,UAAU,CAAC;QACT,IAAI,EAAE,gBAAgB;QACtB,IAAI,EAAE,KAAK;QACX,KAAK,EAAE,wBAAwB;QAC/B,WAAW,EACT,+KAA+K;QACjL,WAAW,EAAE,kBAAkB;QAC/B,YAAY,EAAE,wBAAwB;QACtC,WAAW,EAAE;YACX,YAAY,EAAE,IAAI;YAClB,eAAe,EAAE,KAAK;YACtB,cAAc,EAAE,IAAI;YACpB,aAAa,EAAE,IAAI;SACpB;QACD,OAAO,EAAE,mBAAmB;KAC7B,CAAC;IACF,UAAU,CAAC;QACT,IAAI,EAAE,cAAc;QACpB,IAAI,EAAE,KAAK;QACX,KAAK,EAAE,4BAA4B;QACnC,WAAW,EACT,+KAA+K;QACjL,WAAW,EAAE,iBAAiB;QAC9B,YAAY,EAAE,uBAAuB;QACrC,WAAW,EAAE;YACX,YAAY,EAAE,IAAI;YAClB,eAAe,EAAE,KAAK;YACtB,cAAc,EAAE,IAAI;YACpB,aAAa,EAAE,IAAI;SACpB;QACD,OAAO,EAAE,kBAAkB;KAC5B,CAAC;IACF,UAAU,CAAC;QACT,IAAI,EAAE,cAAc;QACpB,IAAI,EAAE,KAAK;QACX,KAAK,EAAE,4BAA4B;QACnC,WAAW,EACT,6VAA6V;QAC/V,WAAW,EAAE,iBAAiB;QAC9B,YAAY,EAAE,uBAAuB;QACrC,WAAW,EAAE;YACX,YAAY,EAAE,IAAI;YAClB,eAAe,EAAE,KAAK;YACtB,cAAc,EAAE,IAAI;YACpB,aAAa,EAAE,IAAI;SACpB;QACD,OAAO,EAAE,kBAAkB;KAC5B,CAAC;IACF,UAAU,CAAC;QACT,IAAI,EAAE,cAAc;QACpB,IAAI,EAAE,KAAK;QACX,KAAK,EAAE,4BAA4B;QACnC,WAAW,EACT,wLAAwL;QAC1L,WAAW,EAAE,iBAAiB;QAC9B,YAAY,EAAE,uBAAuB;QACrC,WAAW,EAAE;YACX,YAAY,EAAE,IAAI;YAClB,eAAe,EAAE,KAAK;YACtB,cAAc,EAAE,IAAI;YACpB,aAAa,EAAE,IAAI;SACpB;QACD,OAAO,EAAE,kBAAkB;KAC5B,CAAC;CACH,CAAC"}