mcp-youtube-intelligence 0.2.7 → 0.2.8
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 +3 -3
- package/build/analyze_channel.js +30 -17
- package/build/index.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ You need API keys for three services:
|
|
|
25
25
|
|
|
26
26
|
| Variable | Default | Description |
|
|
27
27
|
|----------|---------|-------------|
|
|
28
|
-
| `ANALYZE_CHANNEL_OUTPUT_DIR` | `./output/` | Directory where per-channel JSON analysis artifacts are written |
|
|
28
|
+
| `ANALYZE_CHANNEL_OUTPUT_DIR` | `./output/` | Directory where per-channel JSON analysis artifacts are written. The default is relative to the server's working directory, which your MCP client chooses (some start servers in `/`, where the write is denied). Set an absolute path to be sure: when the write fails the analysis still succeeds, but the result has no `output_path` and the reason goes only to stderr |
|
|
29
29
|
|
|
30
30
|
## Installation
|
|
31
31
|
|
|
@@ -88,9 +88,9 @@ Returns a JSON object:
|
|
|
88
88
|
| `sample_video_ids` | The most recent uploads that were analyzed |
|
|
89
89
|
| `videos_analyzed` | How many videos were fetched from the channel's uploads playlist |
|
|
90
90
|
| `transcripts_available` | How many of those came back with caption content |
|
|
91
|
-
| `topics` | Up to 20 top keywords by frequency across all transcripts. Always computed |
|
|
91
|
+
| `topics` | Up to 20 top keywords by frequency across all transcripts. Always computed. Counts plain English words only: a word with an accented or non-Latin letter is skipped, so a transcript in another language gives few or no keywords here (`topics_structured` still works for it when Gemini is configured) |
|
|
92
92
|
| `topics_structured` | Per video: `theme`, `entities[]`, `tags[]` from Gemini. Empty when Gemini is not configured or fails. Only the first 1,000 words of each transcript are sent, and a video Gemini blocks or returns nothing usable for is left out |
|
|
93
|
-
| `note` | Which analysis mode ran. When Gemini was configured but failed, it also says why |
|
|
93
|
+
| `note` | Which analysis mode ran. When Gemini was configured but failed, or when none of the videos returned a transcript (so there was nothing to analyze), it also says why |
|
|
94
94
|
| `output_path` | Where the JSON artifact was written. Absent if the write failed |
|
|
95
95
|
|
|
96
96
|
**Example prompt:** "Analyze the @fireship YouTube channel and tell me what topics they cover most."
|
package/build/analyze_channel.js
CHANGED
|
@@ -589,7 +589,12 @@ export function extractTopics(transcriptItems, topN = 20) {
|
|
|
589
589
|
else if (typeof snippets === "string") {
|
|
590
590
|
text = snippets;
|
|
591
591
|
}
|
|
592
|
-
|
|
592
|
+
// Split into maximal runs of letters, marks, digits and underscores, then keep only the runs
|
|
593
|
+
// that are plain ASCII words. A `\b[a-z]{2,}\b` match treats every non-ASCII letter as a word
|
|
594
|
+
// boundary, so "Zürich" came out as the topic "rich" and "dzień" as "dzie". For ASCII text the
|
|
595
|
+
// two are identical ("gpt4" and "snake_case" are still skipped); a word with an accented or
|
|
596
|
+
// non-Latin letter is now dropped whole instead of leaving its ASCII fragment behind.
|
|
597
|
+
const words = (text.toLowerCase().match(/[\p{L}\p{M}\p{N}_]+/gu) ?? []).filter((w) => /^[a-z]{2,}$/.test(w));
|
|
593
598
|
for (const word of words) {
|
|
594
599
|
if ((word.length >= 4 || TECH_TERMS.has(word)) && !STOPWORDS.has(word) && !CONTRACTION_STOPWORDS.has(word)) {
|
|
595
600
|
freq[word] = (freq[word] ?? 0) + 1;
|
|
@@ -647,23 +652,31 @@ export async function analyzeChannel(channelInput, maxVideos) {
|
|
|
647
652
|
let geminiConfigured = false;
|
|
648
653
|
let geminiKey;
|
|
649
654
|
let geminiFailure = "";
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
+
// With no transcript text at all there is nothing for Gemini to read, and no key would change
|
|
656
|
+
// that, so skip the call (and the key lookup) rather than report "0 semantic topics via Gemini"
|
|
657
|
+
// or tell the user to set GEMINI_API_KEY for a result it could not improve.
|
|
658
|
+
const nothingToAnalyze = transcriptsAvailable === 0;
|
|
659
|
+
if (!nothingToAnalyze) {
|
|
660
|
+
try {
|
|
661
|
+
geminiKey = getGeminiApiKey();
|
|
662
|
+
geminiConfigured = true;
|
|
663
|
+
topics_structured = await extractTopicsWithLLM(transcriptItems, geminiKey);
|
|
664
|
+
geminiUsed = true;
|
|
665
|
+
}
|
|
666
|
+
catch (err) {
|
|
667
|
+
// Graceful degradation — topics_structured stays empty; topics is the fallback
|
|
668
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
669
|
+
geminiFailure = summarizeFailure(reason, geminiKey);
|
|
670
|
+
console.warn(`[analyze_channel] Gemini extraction unavailable, using keyword fallback: ${reason}`);
|
|
671
|
+
}
|
|
655
672
|
}
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
? `Analyzed ${videoIds.length} video(s), ${transcriptsAvailable} with transcripts — ${topics_structured.length} semantic topics (topics_structured) via Gemini, ${topics.length} keyword topics as supplemental.`
|
|
664
|
-
: geminiConfigured
|
|
665
|
-
? `Analyzed ${videoIds.length} video(s), ${transcriptsAvailable} with transcripts — ${topics.length} keyword topics only (Gemini extraction failed — ${geminiFailure}).`
|
|
666
|
-
: `Analyzed ${videoIds.length} video(s), ${transcriptsAvailable} with transcripts — ${topics.length} keyword topics only (GEMINI_API_KEY not configured). Set GEMINI_API_KEY for richer structured analysis.`;
|
|
673
|
+
const note = nothingToAnalyze
|
|
674
|
+
? `Analyzed ${videoIds.length} video(s), but none returned a transcript (captions disabled or unavailable, or the transcript scraper could not read them), so topics and topics_structured are empty.`
|
|
675
|
+
: geminiUsed
|
|
676
|
+
? `Analyzed ${videoIds.length} video(s), ${transcriptsAvailable} with transcripts — ${topics_structured.length} semantic topics (topics_structured) via Gemini, ${topics.length} keyword topics as supplemental.`
|
|
677
|
+
: geminiConfigured
|
|
678
|
+
? `Analyzed ${videoIds.length} video(s), ${transcriptsAvailable} with transcripts — ${topics.length} keyword topics only (Gemini extraction failed — ${geminiFailure}).`
|
|
679
|
+
: `Analyzed ${videoIds.length} video(s), ${transcriptsAvailable} with transcripts — ${topics.length} keyword topics only (GEMINI_API_KEY not configured). Set GEMINI_API_KEY for richer structured analysis.`;
|
|
667
680
|
const result = {
|
|
668
681
|
channel_id: channel.channelId,
|
|
669
682
|
channel_title: channel.title,
|
package/build/index.js
CHANGED
|
@@ -25,7 +25,7 @@ const server = new McpServer({
|
|
|
25
25
|
});
|
|
26
26
|
server.registerTool("analyze_channel", {
|
|
27
27
|
title: "Analyze YouTube Channel",
|
|
28
|
-
description: "Analyze a YouTube channel and return a JSON object with: channel_id, channel_title, channel_url, sample_video_ids[], videos_analyzed (count of videos fetched from playlist), transcripts_available (count with actual caption content), topics[] (top keyword frequencies across all transcripts), topics_structured[] (per-video semantic analysis — each entry has video_id/theme/entities[]/tags[]), note (which analysis mode ran — and, when Gemini was configured but failed, why), and optional output_path (local artifact path). Requires YOUTUBE_API_KEY and APIFY_TOKEN; set GEMINI_API_KEY for topics_structured semantic analysis (falls back to keyword-only when absent). Supported channel inputs: @handle (e.g. @fireship), youtube.com/@handle URL, /channel/UC... URL, bare 24-char UCxxxxxx ID, or legacy /c/ and /user/ URLs.",
|
|
28
|
+
description: "Analyze a YouTube channel and return a JSON object with: channel_id, channel_title, channel_url, sample_video_ids[], videos_analyzed (count of videos fetched from playlist), transcripts_available (count with actual caption content), topics[] (top keyword frequencies across all transcripts; plain English words only, so a non-English transcript yields few or no keywords — use topics_structured for those), topics_structured[] (per-video semantic analysis — each entry has video_id/theme/entities[]/tags[]), note (which analysis mode ran — and, when Gemini was configured but failed, or no video returned a transcript, why), and optional output_path (local artifact path). Requires YOUTUBE_API_KEY and APIFY_TOKEN; set GEMINI_API_KEY for topics_structured semantic analysis (falls back to keyword-only when absent). Supported channel inputs: @handle (e.g. @fireship), youtube.com/@handle URL, /channel/UC... URL, bare 24-char UCxxxxxx ID, or legacy /c/ and /user/ URLs.",
|
|
29
29
|
inputSchema: {
|
|
30
30
|
channel_url: z.string().describe("YouTube channel URL or @handle (e.g. @fireship, https://www.youtube.com/@fireship, UCxxxxxxx)"),
|
|
31
31
|
max_videos: z.number().int().min(1).max(50).optional().default(5).describe("Number of recent videos to analyze (default 5, max 50)"),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-youtube-intelligence",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.8",
|
|
4
4
|
"description": "MCP server for extracting structured intelligence from YouTube channels and videos — transcripts, topics, competitive signals",
|
|
5
5
|
"mcpName": "io.github.dmitriusan/mcp-youtube-intelligence",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
51
|
-
"zod": "^4.
|
|
51
|
+
"zod": "^4.6.6"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "^22.0.0",
|