fetch_transcript
Fetch official YouTube transcript with per-segment timestamps and automatic language detection. Returns error if no captions exist — then use AI ASR transcript. Free.
Instructions
Fetch the existing official transcript (subtitles/captions) of a YouTube video, with per-segment timestamps and language detected. Errors with NO_CAPTIONS if the video has no captions — fall back to transcribe_video in that case to generate one with AI ASR. This call is free.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| video_id | Yes | YouTube video ID (e.g. 'dQw4w9WgXcQ') or full YouTube URL. | |
| lang | No | ISO 639-1 language code to select among multilingual captions (e.g. 'en', 'zh', 'ja'). Omit for the video's default language. | |
| save | No | When true, also save the video to the user's Library in the same call. Bookmarks the meta row and flips has_asr when the transcript was produced by our ASR. Does NOT upload a summary — use save_to_library with kind='summary' or kind='both' for that. |
Implementation Reference
- src/index.js:122-147 (schema)Schema definition for the 'fetch_transcript' tool - defines name, description, annotations, and inputSchema with parameters: video_id (required), lang (optional ISO 639-1 code), save (optional boolean).
{ name: "fetch_transcript", description: "Fetch the existing official transcript (subtitles/captions) of a YouTube video, with per-segment timestamps and language detected. Errors with NO_CAPTIONS if the video has no captions — fall back to transcribe_video in that case to generate one with AI ASR. This call is free.", annotations: { title: "Fetch YouTube Video Transcript", ...ANN.YT_READ }, inputSchema: { type: "object", properties: { video_id: { type: "string", description: "YouTube video ID (e.g. 'dQw4w9WgXcQ') or full YouTube URL.", minLength: 5, }, lang: { type: "string", description: "ISO 639-1 language code to select among multilingual captions (e.g. 'en', 'zh', 'ja'). Omit for the video's default language.", }, save: { type: "boolean", description: "When true, also save the video to the user's Library in the same call. Bookmarks the meta row and flips has_asr when the transcript was produced by our ASR. Does NOT upload a summary — use save_to_library with kind='summary' or kind='both' for that.", }, }, required: ["video_id"], }, - src/index.js:450-462 (handler)The generic CallToolRequestSchema handler that forwards all tool calls (including fetch_transcript) to the upstream API via callUpstream. No specific fetch_transcript handler exists - it uses the generic proxy pattern.
server.setRequestHandler(CallToolRequestSchema, async (request) => { try { return await callUpstream( request.params.name, request.params.arguments || {} ); } catch (err) { return { content: [{ type: "text", text: err.message || String(err) }], isError: true, }; } }); - src/index.js:443-446 (registration)Server instantiation with tools capability. Tools are registered by including them in the TOOLS array (which includes fetch_transcript at index position).
const server = new Server( { name: "subdownload", version: "1.0.0" }, { capabilities: { tools: {} } } ); - src/index.js:408-441 (helper)The callUpstream helper function that forwards tool calls (by name and arguments) to the upstream MCP endpoint at api.subdownload.com/mcp. Used by the CallToolRequestSchema handler for all tools including fetch_transcript.
async function callUpstream(name, args) { if (!API_KEY) { throw new Error( "SUBDOWNLOAD_API_KEY env var is not set. Get one at https://subdownload.com/account, then run with -e SUBDOWNLOAD_API_KEY=<your-key>." ); } const res = await fetch(UPSTREAM_URL, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream", Authorization: `Bearer ${API_KEY}`, }, body: JSON.stringify({ jsonrpc: "2.0", id: Date.now(), method: "tools/call", params: { name, arguments: args }, }), }); const text = await res.text(); let body; try { body = JSON.parse(text); } catch { throw new Error( `Upstream returned non-JSON response (HTTP ${res.status}): ${text.slice(0, 200)}` ); } if (body.error) { throw new Error(body.error.message || JSON.stringify(body.error)); } return body.result; }