@changgun/youtube-transcript-mcp 0.1.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.
@@ -0,0 +1,41 @@
1
+ import { XMLParser } from "fast-xml-parser";
2
+ export async function fetchTranscript(videoId, lang = "en") {
3
+ // Step 1: HTML에서 INNERTUBE_API_KEY 추출
4
+ const pageRes = await fetch(`https://www.youtube.com/watch?v=${videoId}`, {
5
+ headers: { "Accept-Language": "en-US" },
6
+ });
7
+ const html = await pageRes.text();
8
+ const keyMatch = html.match(/"INNERTUBE_API_KEY":\s*"([a-zA-Z0-9_-]+)"/);
9
+ if (!keyMatch)
10
+ throw new Error("INNERTUBE_API_KEY를 찾을 수 없습니다.");
11
+ // Step 2: player API로 captionTracks 요청
12
+ const playerRes = await fetch(`https://www.youtube.com/youtubei/v1/player?key=${keyMatch[1]}`, {
13
+ method: "POST",
14
+ headers: { "Content-Type": "application/json" },
15
+ body: JSON.stringify({
16
+ context: {
17
+ client: { clientName: "ANDROID", clientVersion: "20.10.38" },
18
+ },
19
+ videoId,
20
+ }),
21
+ });
22
+ const playerData = await playerRes.json();
23
+ const tracks = playerData?.captions?.playerCaptionsTracklistRenderer?.captionTracks ?? [];
24
+ if (tracks.length === 0)
25
+ throw new Error("자막이 없습니다.");
26
+ // 수동 자막 우선, 없으면 ASR
27
+ const manual = tracks.find((t) => t.languageCode === lang && !t.kind);
28
+ const asr = tracks.find((t) => t.languageCode === lang && t.kind === "asr");
29
+ const track = manual ?? asr ?? tracks[0];
30
+ // Step 3: timedtext XML 받아서 파싱
31
+ const xmlRes = await fetch(track.baseUrl);
32
+ const xml = await xmlRes.text();
33
+ const parser = new XMLParser({ ignoreAttributes: false });
34
+ const parsed = parser.parse(xml);
35
+ const paragraphs = parsed?.timedtext?.body?.p ?? [];
36
+ const text = (Array.isArray(paragraphs) ? paragraphs : [paragraphs])
37
+ .map((p) => (typeof p === "string" ? p : p["#text"] ?? ""))
38
+ .filter(Boolean)
39
+ .join(" ");
40
+ return text;
41
+ }
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
5
+ import { fetchTranscript } from "./fetcher.js";
6
+ const server = new Server({ name: "youtube-transcript-mcp", version: "0.1.0" }, { capabilities: { tools: {} } });
7
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
8
+ tools: [
9
+ {
10
+ name: "get_transcript",
11
+ description: "YouTube 영상의 자막(트랜스크립트)을 텍스트로 반환합니다.",
12
+ inputSchema: {
13
+ type: "object",
14
+ properties: {
15
+ videoId: { type: "string", description: "YouTube 영상 ID (예: jNQXAC9IVRw)" },
16
+ lang: { type: "string", description: "언어 코드 (기본값: en)" },
17
+ },
18
+ required: ["videoId"],
19
+ },
20
+ },
21
+ ],
22
+ }));
23
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
24
+ const { videoId, lang = "en" } = request.params.arguments;
25
+ const transcript = await fetchTranscript(videoId, lang);
26
+ return {
27
+ content: [{ type: "text", text: transcript }],
28
+ };
29
+ });
30
+ const transport = new StdioServerTransport();
31
+ await server.connect(transport);
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@changgun/youtube-transcript-mcp",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "youtube-transcript-mcp": "dist/index.js"
7
+ },
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "tsc && chmod +x dist/index.js",
13
+ "dev": "node --loader ts-node/esm src/index.ts"
14
+ },
15
+ "dependencies": {
16
+ "@modelcontextprotocol/sdk": "^1.9.0",
17
+ "fast-xml-parser": "^5.5.9",
18
+ "youtubei.js": "^17.0.1"
19
+ },
20
+ "devDependencies": {
21
+ "@types/node": "^20.0.0",
22
+ "prettier": "^3.8.1",
23
+ "tsx": "^4.21.0",
24
+ "typescript": "^5.6.0"
25
+ }
26
+ }