Skip to content

Commit 1597f65

Browse files
authored
Merge pull request #37 from jkawamoto/video-info
Add `get_video_info` tool and integrate yt-dlp for video handling
2 parents 1101bfe + 52a3981 commit 1597f65

File tree

5 files changed

+86
-11
lines changed

5 files changed

+86
-11
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,12 @@ Fetches the transcript of a specified YouTube video.
2121
- **lang** *(string, optional)*: The desired language for the transcript. Defaults to `en` if not specified.
2222
- **next_cursor** *(string, optional)*: Cursor to retrieve the next page of the transcript.
2323

24+
### `get_video_info`
25+
Fetches the metadata of a specified YouTube video.
26+
27+
#### Parameters
28+
- **url** *(string)*: The full URL of the YouTube video. This field is required.
29+
2430
## Installation
2531
> [!NOTE]
2632
> You'll need [`uv`](https://docs.astral.sh/uv) installed on your system to use `uvx` command.

pyproject.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ dependencies = [
2929
"requests>=2.32.3",
3030
"rich-click>=1.8.8",
3131
"youtube-transcript-api>=1.1",
32+
"yt-dlp>=2025.8.22",
3233
]
3334

3435
scripts.mcp-youtube-transcript = "mcp_youtube_transcript.cli:main"
@@ -68,3 +69,9 @@ replace = 'version = "{new_version}"'
6869
warn_return_any = true
6970
warn_unused_configs = true
7071
disallow_untyped_defs = true
72+
73+
[[tool.mypy.overrides]]
74+
module = [
75+
"yt_dlp.*",
76+
]
77+
ignore_missing_imports = true

src/mcp_youtube_transcript/__init__.py

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,39 @@
2121
from pydantic import Field, BaseModel
2222
from youtube_transcript_api import YouTubeTranscriptApi
2323
from youtube_transcript_api.proxies import WebshareProxyConfig, GenericProxyConfig, ProxyConfig
24+
from yt_dlp import YoutubeDL
25+
from yt_dlp.extractor.youtube import YoutubeIE
2426

2527

2628
@dataclass(frozen=True)
2729
class AppContext:
2830
http_client: requests.Session
2931
ytt_api: YouTubeTranscriptApi
32+
dlp: YoutubeDL
3033

3134

3235
@asynccontextmanager
3336
async def _app_lifespan(_server: FastMCP, proxy_config: ProxyConfig | None) -> AsyncIterator[AppContext]:
34-
with requests.Session() as http_client:
37+
with requests.Session() as http_client, YoutubeDL(params={"quiet": True}, auto_init=False) as dlp:
3538
ytt_api = YouTubeTranscriptApi(http_client=http_client, proxy_config=proxy_config)
36-
yield AppContext(http_client=http_client, ytt_api=ytt_api)
39+
dlp.add_info_extractor(YoutubeIE())
40+
yield AppContext(http_client=http_client, ytt_api=ytt_api, dlp=dlp)
41+
42+
43+
class Transcript(BaseModel):
44+
"""Transcript of a YouTube video."""
45+
46+
title: str = Field(description="Title of the video")
47+
transcript: str = Field(description="Transcript of the video")
48+
next_cursor: str | None = Field(description="Cursor to retrieve the next page of the transcript", default=None)
49+
50+
51+
class VideoInfo(BaseModel):
52+
"""Video information."""
53+
54+
title: str = Field(description="Title of the video")
55+
description: str = Field(description="Description of the video")
56+
uploader: str = Field(description="Uploader of the video")
3757

3858

3959
@lru_cache
@@ -54,12 +74,10 @@ def _get_transcript(ctx: AppContext, video_id: str, lang: str) -> Tuple[str, lis
5474
return title, [item.text for item in transcripts]
5575

5676

57-
class Transcript(BaseModel):
58-
"""Transcript of a YouTube video."""
59-
60-
title: str = Field(description="Title of the video")
61-
transcript: str = Field(description="Transcript of the video")
62-
next_cursor: str | None = Field(description="Cursor to retrieve the next page of the transcript", default=None)
77+
@lru_cache
78+
def _get_video_info(ctx: AppContext, video_url: str) -> VideoInfo:
79+
res = ctx.dlp.extract_info(video_url, download=False)
80+
return VideoInfo(title=res["title"], description=res["description"], uploader=res["uploader"])
6381

6482

6583
def server(
@@ -111,7 +129,15 @@ async def get_transcript(
111129

112130
return Transcript(title=title, transcript=res[:-1], next_cursor=cursor)
113131

132+
@mcp.tool()
133+
def get_video_info(
134+
ctx: Context[ServerSession, AppContext],
135+
url: str = Field(description="The URL of the YouTube video"),
136+
) -> VideoInfo:
137+
"""Retrieves the video information."""
138+
return _get_video_info(ctx.request_context.lifespan_context, url)
139+
114140
return mcp
115141

116142

117-
__all__: Final = ["server", "Transcript"]
143+
__all__: Final = ["server", "Transcript", "VideoInfo"]

tests/test_mcp.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@
1414
from mcp import StdioServerParameters, stdio_client, ClientSession
1515
from mcp.types import TextContent
1616
from youtube_transcript_api import YouTubeTranscriptApi
17+
import yt_dlp
18+
from yt_dlp.extractor.youtube import YoutubeIE
1719

18-
from mcp_youtube_transcript import Transcript
20+
from mcp_youtube_transcript import Transcript, VideoInfo
1921

2022

2123
def fetch_title(url: str, lang: str) -> str:
@@ -192,3 +194,26 @@ async def test_get_transcript_with_response_limit(mcp_client_session_with_respon
192194

193195
assert t.title == expect.title
194196
assert transcript[:-1] == expect.transcript
197+
198+
199+
@pytest.mark.skipif(os.getenv("CI") == "true", reason="Skipping this test on CI")
200+
@pytest.mark.anyio
201+
async def test_get_video_info(mcp_client_session: ClientSession) -> None:
202+
video_id = "LPZh9BOjkQs"
203+
204+
dlp = yt_dlp.YoutubeDL(params={"quiet": True}, auto_init=False)
205+
dlp.add_info_extractor(YoutubeIE())
206+
dlp_res = dlp.extract_info(f"https://www.youtube.com/watch?v={video_id}", download=False)
207+
expect = VideoInfo(title=dlp_res["title"], description=dlp_res["description"], uploader=dlp_res["uploader"])
208+
209+
res = await mcp_client_session.call_tool(
210+
"get_video_info",
211+
arguments={
212+
"url": f"https://www.youtube.com/watch?v={video_id}",
213+
},
214+
)
215+
assert isinstance(res.content[0], TextContent)
216+
217+
info = VideoInfo.model_validate_json(res.content[0].text)
218+
assert info == expect
219+
assert not res.isError

uv.lock

Lines changed: 12 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)