diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4b3e8de..53f32cd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,7 @@ repos: - id: mypy args: [] additional_dependencies: - - "mcp>=1.9" + - "mcp>=2" - "youtube-transcript-api>=1.1.0" - "beautifulsoup4>=4.13.3" - "humanize>=4.13" diff --git a/pyproject.toml b/pyproject.toml index 74c5fed..7396efe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,7 @@ classifiers = [ dependencies = [ "beautifulsoup4>=4.13.3", "humanize>=4.13", - "mcp>=1.9", + "mcp>=2", "pydantic>=2.10.6", "requests>=2.32.3", "rich-click>=1.8.8", diff --git a/src/mcp_youtube_transcript/__init__.py b/src/mcp_youtube_transcript/__init__.py index 980c189..4cbd3f4 100644 --- a/src/mcp_youtube_transcript/__init__.py +++ b/src/mcp_youtube_transcript/__init__.py @@ -20,8 +20,7 @@ import requests from bs4 import BeautifulSoup from mcp import ServerSession -from mcp.server import FastMCP -from mcp.server.fastmcp import Context +from mcp.server.mcpserver import MCPServer, Context from pydantic import Field, BaseModel, AwareDatetime from youtube_transcript_api import YouTubeTranscriptApi, FetchedTranscriptSnippet from youtube_transcript_api.proxies import WebshareProxyConfig, GenericProxyConfig, ProxyConfig @@ -37,7 +36,7 @@ class AppContext: @asynccontextmanager -async def _app_lifespan(_server: FastMCP, proxy_config: ProxyConfig | None) -> AsyncIterator[AppContext]: +async def _app_lifespan(_server: MCPServer, proxy_config: ProxyConfig | None) -> AsyncIterator[AppContext]: # Prepare YoutubeDL params with proxy support ytdlp_params: dict[str, Any] = {"quiet": True} ytdlp_params.update(_proxy_config_to_ytdlp_params(proxy_config)) @@ -180,7 +179,7 @@ def server( webshare_proxy_password: str | None = None, http_proxy: str | None = None, https_proxy: str | None = None, -) -> FastMCP: +) -> MCPServer: """Initializes the MCP server.""" proxy_config: ProxyConfig | None = None @@ -189,7 +188,7 @@ def server( elif http_proxy or https_proxy: proxy_config = GenericProxyConfig(http_proxy, https_proxy) - mcp = FastMCP("Youtube Transcript", lifespan=partial(_app_lifespan, proxy_config=proxy_config)) + mcp = MCPServer("Youtube Transcript", lifespan=partial(_app_lifespan, proxy_config=proxy_config)) @mcp.tool() async def get_transcript( diff --git a/tests/test_mcp.py b/tests/test_mcp.py index f7edea1..2740c4a 100644 --- a/tests/test_mcp.py +++ b/tests/test_mcp.py @@ -74,7 +74,7 @@ async def test_get_transcript(mcp_client_session: ClientSession) -> None: transcript = Transcript.model_validate_json(res.content[0].text) assert transcript == expect - assert not res.isError + assert not res.is_error @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skipping this test on CI") @@ -97,7 +97,7 @@ async def test_get_transcript_with_language(mcp_client_session: ClientSession) - transcript = Transcript.model_validate_json(res.content[0].text) assert transcript == expect - assert not res.isError + assert not res.is_error @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skipping this test on CI") @@ -125,7 +125,7 @@ async def test_get_transcript_fallback_language( transcript = Transcript.model_validate_json(res.content[0].text) assert transcript == expect - assert not res.isError + assert not res.is_error @pytest.mark.anyio @@ -133,7 +133,7 @@ async def test_get_transcript_invalid_url(mcp_client_session: ClientSession) -> res = await mcp_client_session.call_tool( "get_transcript", arguments={"url": "https://www.youtube.com/watch?vv=abcdefg"} ) - assert res.isError + assert res.is_error @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skipping this test on CI") @@ -142,7 +142,7 @@ async def test_get_transcript_invalid_url(mcp_client_session: ClientSession) -> @pytest.mark.anyio async def test_get_transcript_not_found(mcp_client_session: ClientSession) -> None: res = await mcp_client_session.call_tool("get_transcript", arguments={"url": "https://www.youtube.com/watch?v=a"}) - assert res.isError + assert res.is_error @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skipping this test on CI") @@ -165,7 +165,7 @@ async def test_get_transcript_with_short_url(mcp_client_session: ClientSession) transcript = Transcript.model_validate_json(res.content[0].text) assert transcript == expect - assert not res.isError + assert not res.is_error @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skipping this test on CI") @@ -187,7 +187,7 @@ async def test_get_transcript_with_response_limit(mcp_client_session_with_respon "get_transcript", arguments={"url": f"https://www.youtube.com/watch?v={video_id}", "next_cursor": cursor}, ) - assert not res.isError + assert not res.is_error assert isinstance(res.content[0], TextContent) t = Transcript.model_validate_json(res.content[0].text) @@ -220,7 +220,7 @@ async def test_get_timed_transcript(mcp_client_session: ClientSession) -> None: transcript = TimedTranscript.model_validate_json(res.content[0].text) assert transcript == expect - assert not res.isError + assert not res.is_error @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skipping this test on CI") @@ -246,7 +246,7 @@ async def test_get_timed_transcript_with_language(mcp_client_session: ClientSess transcript = TimedTranscript.model_validate_json(res.content[0].text) assert transcript == expect - assert not res.isError + assert not res.is_error @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skipping this test on CI") @@ -274,7 +274,7 @@ async def test_get_timed_transcript_fallback_language( transcript = TimedTranscript.model_validate_json(res.content[0].text) assert transcript == expect - assert not res.isError + assert not res.is_error @pytest.mark.anyio @@ -282,7 +282,7 @@ async def test_get_timed_transcript_invalid_url(mcp_client_session: ClientSessio res = await mcp_client_session.call_tool( "get_timed_transcript", arguments={"url": "https://www.youtube.com/watch?vv=abcdefg"} ) - assert res.isError + assert res.is_error @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skipping this test on CI") @@ -293,7 +293,7 @@ async def test_get_timed_transcript_not_found(mcp_client_session: ClientSession) res = await mcp_client_session.call_tool( "get_timed_transcript", arguments={"url": "https://www.youtube.com/watch?v=a"} ) - assert res.isError + assert res.is_error @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skipping this test on CI") @@ -316,7 +316,7 @@ async def test_get_timed_transcript_with_short_url(mcp_client_session: ClientSes transcript = TimedTranscript.model_validate_json(res.content[0].text) assert transcript == expect - assert not res.isError + assert not res.is_error @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skipping this test on CI") @@ -338,7 +338,7 @@ async def test_get_timed_transcript_with_response_limit(mcp_client_session_with_ "get_timed_transcript", arguments={"url": f"https://www.youtube.com/watch?v={video_id}", "next_cursor": cursor}, ) - assert not res.isError + assert not res.is_error assert isinstance(res.content[0], TextContent) t = TimedTranscript.model_validate_json(res.content[0].text) @@ -378,7 +378,7 @@ async def test_get_video_info(mcp_client_session: ClientSession) -> None: info = VideoInfo.model_validate_json(res.content[0].text, strict=True) assert info == expect - assert not res.isError + assert not res.is_error @pytest.mark.skipif(os.getenv("CI") == "true", reason="Skipping this test on CI") @@ -394,7 +394,7 @@ async def test_get_available_languages(mcp_client_session: ClientSession) -> Non "get_available_languages", arguments={"url": f"https://www.youtube.com/watch?v={video_id}"}, ) - assert not res.isError + assert not res.is_error langs = [r.text for r in res.content if isinstance(r, TextContent)] assert langs == expect diff --git a/uv.lock b/uv.lock index 732fe0e..dce4ac4 100644 --- a/uv.lock +++ b/uv.lock @@ -425,19 +425,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] -[[package]] -name = "httpcore" -version = "1.0.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "certifi" }, - { name = "h11" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, -] - [[package]] name = "httpcore2" version = "2.5.0" @@ -451,30 +438,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/a1/7564199d1a8728fe737b0a72e5b3f8d92dfe085a74ddf7cdd83bce5f206d/httpcore2-2.5.0-py3-none-any.whl", hash = "sha256:5ce35188de461d31e8d000bfb8ef8bf22c6c16587a211e5571deaa5e9bdf842a", size = 80330, upload-time = "2026-06-25T14:16:53.634Z" }, ] -[[package]] -name = "httpx" -version = "0.28.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "anyio" }, - { name = "certifi" }, - { name = "httpcore" }, - { name = "idna" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, -] - -[[package]] -name = "httpx-sse" -version = "0.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, -] - [[package]] name = "httpx2" version = "2.5.0" @@ -569,15 +532,15 @@ wheels = [ [[package]] name = "mcp" -version = "1.28.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, + { name = "httpx2" }, { name = "jsonschema" }, + { name = "mcp-types" }, + { name = "opentelemetry-api" }, { name = "pydantic" }, - { name = "pydantic-settings" }, { name = "pyjwt", extra = ["crypto"] }, { name = "python-multipart" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, @@ -587,9 +550,22 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/33/32d4dff2c95bb5d897c3ef4c83649a08996b17b58f0a326d2495d4c81179/mcp-2.0.0.tar.gz", hash = "sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728", size = 1662284, upload-time = "2026-07-28T13:45:32.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/72/7d7897418912c1d12e87556630dfb7bf0eac71160e9bef8b447960804ee3/mcp-2.0.0-py3-none-any.whl", hash = "sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6", size = 349980, upload-time = "2026-07-28T13:45:28.853Z" }, +] + +[[package]] +name = "mcp-types" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/56/9b8e1c152f61f6c6b07c4b5896c88c7d0ae90bac6ee6306f852fcc5c1eb0/mcp_types-2.0.0.tar.gz", hash = "sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379", size = 66632, upload-time = "2026-07-28T13:45:33.804Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4c/c78d78c3d52b0ac594ad7cc8ef5972adfe070e3597a8a4c6ce0cd39196ea/mcp_types-2.0.0-py3-none-any.whl", hash = "sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0", size = 69649, upload-time = "2026-07-28T13:45:30.713Z" }, ] [[package]] @@ -622,7 +598,7 @@ dev = [ requires-dist = [ { name = "beautifulsoup4", specifier = ">=4.13.3" }, { name = "humanize", specifier = ">=4.13" }, - { name = "mcp", specifier = ">=1.9" }, + { name = "mcp", specifier = ">=2" }, { name = "pydantic", specifier = ">=2.10.6" }, { name = "requests", specifier = ">=2.32.3" }, { name = "rich-click", specifier = ">=1.8.8" }, @@ -659,6 +635,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, +] + [[package]] name = "packaging" version = "26.2"