mcp-youtube-intelligence 0.2.3 → 0.2.5
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 +6 -1
- package/build/analyze_channel.js +55 -14
- package/build/index.js +6 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,7 +60,12 @@ Add to your Claude Desktop / MCP client config:
|
|
|
60
60
|
**`analyze_channel`** — Extract intelligence from a YouTube channel
|
|
61
61
|
|
|
62
62
|
```
|
|
63
|
-
channel_url: YouTube channel URL or @handle
|
|
63
|
+
channel_url: YouTube channel URL or @handle. Accepts:
|
|
64
|
+
- @handle (e.g. @fireship)
|
|
65
|
+
- full URL with handle (e.g. youtube.com/@fireship)
|
|
66
|
+
- /channel/UC... URL
|
|
67
|
+
- bare 24-character UC... channel ID
|
|
68
|
+
- legacy /c/name or /user/name URL
|
|
64
69
|
max_videos: Number of recent videos to analyze (default: 5, max: 50)
|
|
65
70
|
```
|
|
66
71
|
|
package/build/analyze_channel.js
CHANGED
|
@@ -130,7 +130,23 @@ export async function resolveChannel(input, apiKey) {
|
|
|
130
130
|
else {
|
|
131
131
|
// custom_url — fall back to search (costs 100 quota units); rare path
|
|
132
132
|
const searchUrl = `${YT_API_BASE}/search?part=snippet&type=channel&q=${encodeURIComponent(parsed.value)}&maxResults=1&key=${apiKey}`;
|
|
133
|
-
|
|
133
|
+
// Same one-shot tolerance as the channels-API lookup below: a thrown rejection
|
|
134
|
+
// is as transient as a 5xx, and this path previously had neither — leaving the
|
|
135
|
+
// most quota-expensive of the three call sites the least tolerant of a blip.
|
|
136
|
+
const fetchSearch = () => fetchWithTimeout(searchUrl, {}, "YouTube search API");
|
|
137
|
+
let searchResp;
|
|
138
|
+
try {
|
|
139
|
+
searchResp = await fetchSearch();
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
searchResp = undefined;
|
|
143
|
+
}
|
|
144
|
+
if (searchResp === undefined) {
|
|
145
|
+
searchResp = await fetchSearch();
|
|
146
|
+
}
|
|
147
|
+
else if (!searchResp.ok && searchResp.status >= 500) {
|
|
148
|
+
searchResp = await fetchSearch();
|
|
149
|
+
}
|
|
134
150
|
if (!searchResp.ok)
|
|
135
151
|
throw new Error(`YouTube search API error ${searchResp.status}: ${await searchResp.text()}`);
|
|
136
152
|
const searchData = await parseJsonResponse(searchResp, "YouTube search API");
|
|
@@ -193,7 +209,25 @@ export async function getRecentVideoIds(uploadsPlaylistId, maxResults, apiKey) {
|
|
|
193
209
|
`&playlistId=${encodeURIComponent(uploadsPlaylistId)}` +
|
|
194
210
|
`&maxResults=${maxResults}` +
|
|
195
211
|
`&key=${apiKey}`;
|
|
196
|
-
|
|
212
|
+
// Same one-shot tolerance as resolveChannel's channels-API lookup and the Apify
|
|
213
|
+
// calls: a thrown rejection (connection reset, DNS blip) is as transient as a
|
|
214
|
+
// 5xx. This is the second external call every analyze_channel run makes, and
|
|
215
|
+
// previously it had no tolerance at all — a single blip failed the whole tool
|
|
216
|
+
// call immediately.
|
|
217
|
+
const fetchPlaylistItems = () => fetchWithTimeout(apiUrl, {}, "YouTube playlistItems API");
|
|
218
|
+
let resp;
|
|
219
|
+
try {
|
|
220
|
+
resp = await fetchPlaylistItems();
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
resp = undefined;
|
|
224
|
+
}
|
|
225
|
+
if (resp === undefined) {
|
|
226
|
+
resp = await fetchPlaylistItems();
|
|
227
|
+
}
|
|
228
|
+
else if (!resp.ok && resp.status >= 500) {
|
|
229
|
+
resp = await fetchPlaylistItems();
|
|
230
|
+
}
|
|
197
231
|
if (!resp.ok)
|
|
198
232
|
throw new Error(`YouTube playlistItems API error ${resp.status}: ${await resp.text()}`);
|
|
199
233
|
const data = await parseJsonResponse(resp, "YouTube playlistItems API");
|
|
@@ -227,10 +261,10 @@ export async function runApifyTranscriptScraper(videoUrls, token) {
|
|
|
227
261
|
if (runResp === undefined) {
|
|
228
262
|
runResp = await startRun();
|
|
229
263
|
}
|
|
230
|
-
else if (!runResp.ok && runResp.status >= 500) {
|
|
231
|
-
// Retry once on a transient 5xx
|
|
232
|
-
//
|
|
233
|
-
//
|
|
264
|
+
else if (!runResp.ok && (runResp.status >= 500 || runResp.status === 429)) {
|
|
265
|
+
// Retry once on a transient 5xx, or a 429 — Apify returns 429 when the account's
|
|
266
|
+
// concurrent-actor-run limit is hit, which clears within seconds, unlike a bad
|
|
267
|
+
// token or malformed request. Same tolerance already given to Gemini below.
|
|
234
268
|
runResp = await startRun();
|
|
235
269
|
}
|
|
236
270
|
if (!runResp.ok) {
|
|
@@ -255,11 +289,13 @@ export async function runApifyTranscriptScraper(videoUrls, token) {
|
|
|
255
289
|
continue;
|
|
256
290
|
}
|
|
257
291
|
if (!statusResp.ok) {
|
|
258
|
-
// 4xx
|
|
259
|
-
|
|
292
|
+
// Most 4xx are permanent (invalid run ID, bad token) — fail fast instead of
|
|
293
|
+
// polling to timeout. 429 is the exception: Apify's own rate limit, which the
|
|
294
|
+
// next scheduled poll (APIFY_POLL_INTERVAL_MS later) already backs off past.
|
|
295
|
+
if (statusResp.status >= 400 && statusResp.status < 500 && statusResp.status !== 429) {
|
|
260
296
|
throw new Error(`Apify poll error ${statusResp.status} for run ${runId}: ${await statusResp.text()}`);
|
|
261
297
|
}
|
|
262
|
-
continue; // 5xx or network transient — keep polling
|
|
298
|
+
continue; // 5xx, 429, or network transient — keep polling
|
|
263
299
|
}
|
|
264
300
|
let statusBody;
|
|
265
301
|
try {
|
|
@@ -295,7 +331,7 @@ export async function runApifyTranscriptScraper(videoUrls, token) {
|
|
|
295
331
|
if (itemsResp === undefined) {
|
|
296
332
|
itemsResp = await fetchItems();
|
|
297
333
|
}
|
|
298
|
-
else if (!itemsResp.ok && itemsResp.status >= 500) {
|
|
334
|
+
else if (!itemsResp.ok && (itemsResp.status >= 500 || itemsResp.status === 429)) {
|
|
299
335
|
itemsResp = await fetchItems();
|
|
300
336
|
}
|
|
301
337
|
if (!itemsResp.ok)
|
|
@@ -454,7 +490,12 @@ ${truncated}`;
|
|
|
454
490
|
parsed = JSON.parse(text);
|
|
455
491
|
}
|
|
456
492
|
catch {
|
|
457
|
-
|
|
493
|
+
// Counts toward the same "every attempted video produced nothing usable"
|
|
494
|
+
// check below as an HTTP-level failure — an all-malformed-JSON response is
|
|
495
|
+
// just as much a Gemini outage as an all-5xx one, and previously only the
|
|
496
|
+
// latter was surfaced as a thrown error instead of a silent empty success.
|
|
497
|
+
lastRequestError = `Gemini returned unparseable JSON for video ${videoId}`;
|
|
498
|
+
console.warn(`[analyze_channel] ${lastRequestError}, skipping`);
|
|
458
499
|
continue;
|
|
459
500
|
}
|
|
460
501
|
results.push({
|
|
@@ -471,9 +512,9 @@ ${truncated}`;
|
|
|
471
512
|
console.warn(`[analyze_channel] Gemini request failed for video ${videoId}, skipping: ${lastRequestError}`);
|
|
472
513
|
}
|
|
473
514
|
}
|
|
474
|
-
// Every attempted video failed at the HTTP level
|
|
475
|
-
// failure so analyzeChannel's note reports a
|
|
476
|
-
// silently claiming success with zero structured topics.
|
|
515
|
+
// Every attempted video failed, whether at the HTTP level or via unparseable
|
|
516
|
+
// JSON — surface it as a total failure so analyzeChannel's note reports a
|
|
517
|
+
// Gemini outage instead of silently claiming success with zero structured topics.
|
|
477
518
|
if (attempted > 0 && results.length === 0 && lastRequestError) {
|
|
478
519
|
throw new Error(lastRequestError);
|
|
479
520
|
}
|
package/build/index.js
CHANGED
|
@@ -13,6 +13,10 @@ import { z } from "zod";
|
|
|
13
13
|
import { analyzeChannel } from "./analyze_channel.js";
|
|
14
14
|
const require = createRequire(import.meta.url);
|
|
15
15
|
const { version: packageVersion } = require("../package.json");
|
|
16
|
+
if (process.argv.includes("--version") || process.argv.includes("-v")) {
|
|
17
|
+
console.log(packageVersion);
|
|
18
|
+
process.exit(0);
|
|
19
|
+
}
|
|
16
20
|
if (process.argv.includes("--help") || process.argv.includes("-h")) {
|
|
17
21
|
console.log(`mcp-youtube-intelligence v${packageVersion} — MCP server for YouTube channel intelligence
|
|
18
22
|
|
|
@@ -20,7 +24,8 @@ Usage:
|
|
|
20
24
|
mcp-youtube-intelligence [options]
|
|
21
25
|
|
|
22
26
|
Options:
|
|
23
|
-
--help, -h
|
|
27
|
+
--help, -h Show this help message
|
|
28
|
+
--version, -v Print the installed version
|
|
24
29
|
|
|
25
30
|
Tools provided:
|
|
26
31
|
analyze_channel Extract transcripts, topics, and competitive signals from a YouTube channel`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-youtube-intelligence",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
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",
|