diff --git a/src/git/pyproject.toml b/src/git/pyproject.toml index 84459162..85af62fd 100644 --- a/src/git/pyproject.toml +++ b/src/git/pyproject.toml @@ -18,7 +18,7 @@ classifiers = [ dependencies = [ "click>=8.1.7", "gitpython>=3.1.43", - "mcp-python~=0.6.0", + "mcp>=0.6.0", "pydantic>=2.0.0", ] diff --git a/src/git/src/mcp_git/__init__.py b/src/git/src/mcp_git/__init__.py index f925e490..475404f9 100644 --- a/src/git/src/mcp_git/__init__.py +++ b/src/git/src/mcp_git/__init__.py @@ -4,14 +4,13 @@ import anyio import anyio.lowlevel from pathlib import Path from git.types import Sequence -from mcp_python.server import Server -from mcp_python.server.stdio import stdio_server -from mcp_python.types import Tool -from mcp_python.server.types import EmbeddedResource, ImageContent +from mcp.server import Server +from mcp.server.session import ServerSession +from mcp.server.stdio import stdio_server +from mcp.types import TextContent, Tool, EmbeddedResource, ImageContent, ListRootsResult from enum import StrEnum import git from git.objects import Blob, Tree -from mcp_python import ServerSession from pydantic import BaseModel, Field from typing import List, Optional @@ -167,7 +166,7 @@ async def serve(repository: Path | None) -> None: return # Create server - server = Server("git-mcp") + server = Server("mcp-git") @server.list_tools() async def list_tools() -> list[Tool]: @@ -244,7 +243,7 @@ async def serve(repository: Path | None) -> None: "server.request_context.session must be a ServerSession" ) - roots_result = await server.request_context.session.list_roots() + roots_result: ListRootsResult = await server.request_context.session.list_roots() logger.debug(f"Roots result: {roots_result}") repo_paths = [] for root in roots_result.roots: @@ -267,9 +266,10 @@ async def serve(repository: Path | None) -> None: @server.call_tool() async def call_tool( name: str, arguments: dict - ) -> Sequence[str | ImageContent | EmbeddedResource]: + ) -> list[TextContent | ImageContent | EmbeddedResource]: if name == GitTools.LIST_REPOS: - return await list_repos() + result = await list_repos() + return [TextContent(type="text", text=str(r)) for r in result] repo_path = Path(arguments["repo_path"]) repo = git.Repo(repo_path) @@ -277,48 +277,65 @@ async def serve(repository: Path | None) -> None: match name: case GitTools.READ_FILE: return [ - git_read_file( - repo, arguments["file_path"], arguments.get("ref", "HEAD") + TextContent( + type="text", + text=git_read_file( + repo, arguments["file_path"], arguments.get("ref", "HEAD") + ) ) ] case GitTools.LIST_FILES: return [ - str(f) + TextContent(type="text", text=str(f)) for f in git_list_files( repo, arguments.get("path", ""), arguments.get("ref", "HEAD") ) ] case GitTools.FILE_HISTORY: - return git_file_history( - repo, arguments["file_path"], arguments.get("max_entries", 10) - ) + return [ + TextContent(type="text", text=entry) + for entry in git_file_history( + repo, arguments["file_path"], arguments.get("max_entries", 10) + ) + ] case GitTools.COMMIT: result = git_commit(repo, arguments["message"], arguments.get("files")) - return [result] + return [TextContent(type="text", text=result)] case GitTools.SEARCH_CODE: - return git_search_code( - repo, - arguments["query"], - arguments.get("file_pattern", "*"), - arguments.get("ref", "HEAD"), - ) + return [ + TextContent(type="text", text=result) + for result in git_search_code( + repo, + arguments["query"], + arguments.get("file_pattern", "*"), + arguments.get("ref", "HEAD"), + ) + ] case GitTools.GET_DIFF: return [ - git_get_diff( - repo, - arguments["ref1"], - arguments["ref2"], - arguments.get("file_path"), + TextContent( + type="text", + text=git_get_diff( + repo, + arguments["ref1"], + arguments["ref2"], + arguments.get("file_path"), + ) ) ] case GitTools.GET_REPO_STRUCTURE: - return [git_get_repo_structure(repo, arguments.get("ref", "HEAD"))] + return [ + TextContent( + type="text", + text=git_get_repo_structure(repo, arguments.get("ref", "HEAD")) + ) + ] case _: raise ValueError(f"Unknown tool: {name}") @@ -326,7 +343,7 @@ async def serve(repository: Path | None) -> None: # Run the server options = server.create_initialization_options() async with stdio_server() as (read_stream, write_stream): - await server.run(read_stream, write_stream, options) + await server.run(read_stream, write_stream, options, raise_exceptions=True) @click.command() diff --git a/src/git/uv.lock b/src/git/uv.lock index e41e5838..456835c5 100644 --- a/src/git/uv.lock +++ b/src/git/uv.lock @@ -51,9 +51,10 @@ wheels = [ [[package]] name = "colorama" version = "0.4.6" -source = { registry = "https://artifactory.infra.ant.dev/artifactory/api/pypi/pypi-internal/simple" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } wheels = [ - { url = "https://artifactory.infra.ant.dev/artifactory/api/pypi/pypi-internal/colorama/0.4.6/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" }, + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] [[package]] @@ -83,22 +84,23 @@ wheels = [ [[package]] name = "h11" version = "0.14.0" -source = { registry = "https://artifactory.infra.ant.dev/artifactory/api/pypi/pypi-internal/simple" } +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } wheels = [ - { url = "https://artifactory.infra.ant.dev/artifactory/api/pypi/pypi-internal/h11/0.14.0/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761" }, + { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, ] [[package]] name = "httpcore" -version = "1.0.6" +version = "1.0.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "h11" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b6/44/ed0fa6a17845fb033bd885c03e842f08c1b9406c86a2e60ac1ae1b9206a6/httpcore-1.0.6.tar.gz", hash = "sha256:73f6dbd6eb8c21bbf7ef8efad555481853f5f6acdeaff1edb0694289269ee17f", size = 85180 } +sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } wheels = [ - { url = "https://files.pythonhosted.org/packages/06/89/b161908e2f51be56568184aeb4a880fd287178d176fd1c860d2217f41106/httpcore-1.0.6-py3-none-any.whl", hash = "sha256:27b59625743b85577a8c0e10e55b50b5368a4f2cfe8cc7bcfa9cf00829c2682f", size = 78011 }, + { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, ] [[package]] @@ -135,6 +137,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, ] +[[package]] +name = "mcp" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "pydantic" }, + { name = "sse-starlette" }, + { name = "starlette" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/bb/fd56a5c331a6c95a4f2ec907683db3382d30b99b808ef6f46fa4f08a4b74/mcp-0.9.0.tar.gz", hash = "sha256:1d7e3f8d78bf5b37c98a233fce8cebbb86c57d8964d2c3b03cf08cdebd103d9a", size = 78343 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/07/077116e6a23dd0546391f5caa81b4f52938d8a81f2449c55c0b50c0215bf/mcp-0.9.0-py3-none-any.whl", hash = "sha256:e09aca08eadaf0552541aaa71271b44f99a6a5d16e5b1b03c421366f72b51753", size = 31691 }, +] + [[package]] name = "mcp-git" version = "0.1.0" @@ -142,7 +161,7 @@ source = { editable = "." } dependencies = [ { name = "click" }, { name = "gitpython" }, - { name = "mcp-python" }, + { name = "mcp" }, { name = "pydantic" }, ] @@ -155,30 +174,13 @@ dev = [ requires-dist = [ { name = "click", specifier = ">=8.1.7" }, { name = "gitpython", specifier = ">=3.1.43" }, - { name = "mcp-python", specifier = "~=0.6.0" }, + { name = "mcp", specifier = ">=0.6.0" }, { name = "pydantic", specifier = ">=2.0.0" }, ] [package.metadata.requires-dev] dev = [{ name = "ruff", specifier = ">=0.7.3" }] -[[package]] -name = "mcp-python" -version = "0.6.1" -source = { registry = "https://artifactory.infra.ant.dev/artifactory/api/pypi/pypi-internal/simple" } -dependencies = [ - { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, - { name = "pydantic" }, - { name = "sse-starlette" }, - { name = "starlette" }, -] -sdist = { url = "https://artifactory.infra.ant.dev/artifactory/api/pypi/pypi-internal/mcp-python/0.6.1/mcp_python-0.6.1.tar.gz", hash = "sha256:e8a2a6067e36b5790397d678686ffe1642ca1d2249f667d3b9f44bcf3b506b1c" } -wheels = [ - { url = "https://artifactory.infra.ant.dev/artifactory/api/pypi/pypi-internal/mcp-python/0.6.1/mcp_python-0.6.1-py3-none-any.whl", hash = "sha256:812cf7e7da61b6ca5a2498150d152417bdd8519a0ee24a6964442f473599aa8c" }, -] - [[package]] name = "pydantic" version = "2.9.2" @@ -242,27 +244,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.7.3" +version = "0.7.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4b/06/09d1276df977eece383d0ed66052fc24ec4550a61f8fbc0a11200e690496/ruff-0.7.3.tar.gz", hash = "sha256:e1d1ba2e40b6e71a61b063354d04be669ab0d39c352461f3d789cac68b54a313", size = 3243664 } +sdist = { url = "https://files.pythonhosted.org/packages/0b/8b/bc4e0dfa1245b07cf14300e10319b98e958a53ff074c1dd86b35253a8c2a/ruff-0.7.4.tar.gz", hash = "sha256:cd12e35031f5af6b9b93715d8c4f40360070b2041f81273d0527683d5708fce2", size = 3275547 } wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/56/933d433c2489e4642487b835f53dd9ff015fb3d8fa459b09bb2ce42d7c4b/ruff-0.7.3-py3-none-linux_armv6l.whl", hash = "sha256:34f2339dc22687ec7e7002792d1f50712bf84a13d5152e75712ac08be565d344", size = 10372090 }, - { url = "https://files.pythonhosted.org/packages/20/ea/1f0a22a6bcdd3fc26c73f63a025d05bd565901b729d56bcb093c722a6c4c/ruff-0.7.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fb397332a1879b9764a3455a0bb1087bda876c2db8aca3a3cbb67b3dbce8cda0", size = 10190037 }, - { url = "https://files.pythonhosted.org/packages/16/74/aca75666e0d481fe394e76a8647c44ea919087748024924baa1a17371e3e/ruff-0.7.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:37d0b619546103274e7f62643d14e1adcbccb242efda4e4bdb9544d7764782e9", size = 9811998 }, - { url = "https://files.pythonhosted.org/packages/20/a1/cf446a0d7f78ea1f0bd2b9171c11dfe746585c0c4a734b25966121eb4f5d/ruff-0.7.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d59f0c3ee4d1a6787614e7135b72e21024875266101142a09a61439cb6e38a5", size = 10620626 }, - { url = "https://files.pythonhosted.org/packages/cd/c1/82b27d09286ae855f5d03b1ad37cf243f21eb0081732d4d7b0d658d439cb/ruff-0.7.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44eb93c2499a169d49fafd07bc62ac89b1bc800b197e50ff4633aed212569299", size = 10177598 }, - { url = "https://files.pythonhosted.org/packages/b9/42/c0acac22753bf74013d035a5ef6c5c4c40ad4d6686bfb3fda7c6f37d9b37/ruff-0.7.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6d0242ce53f3a576c35ee32d907475a8d569944c0407f91d207c8af5be5dae4e", size = 11171963 }, - { url = "https://files.pythonhosted.org/packages/43/18/bb0befb7fb9121dd9009e6a72eb98e24f1bacb07c6f3ecb55f032ba98aed/ruff-0.7.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6b6224af8b5e09772c2ecb8dc9f3f344c1aa48201c7f07e7315367f6dd90ac29", size = 11856157 }, - { url = "https://files.pythonhosted.org/packages/5e/91/04e98d7d6e32eca9d1372be595f9abc7b7f048795e32eb2edbd8794d50bd/ruff-0.7.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c50f95a82b94421c964fae4c27c0242890a20fe67d203d127e84fbb8013855f5", size = 11440331 }, - { url = "https://files.pythonhosted.org/packages/f5/dc/3fe99f2ce10b76d389041a1b9f99e7066332e479435d4bebcceea16caff5/ruff-0.7.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7f3eff9961b5d2644bcf1616c606e93baa2d6b349e8aa8b035f654df252c8c67", size = 12725354 }, - { url = "https://files.pythonhosted.org/packages/43/7b/1daa712de1c5bc6cbbf9fa60e9c41cc48cda962dc6d2c4f2a224d2c3007e/ruff-0.7.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8963cab06d130c4df2fd52c84e9f10d297826d2e8169ae0c798b6221be1d1d2", size = 11010091 }, - { url = "https://files.pythonhosted.org/packages/b6/db/1227a903587432eb569e57a95b15a4f191a71fe315cde4c0312df7bc85da/ruff-0.7.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:61b46049d6edc0e4317fb14b33bd693245281a3007288b68a3f5b74a22a0746d", size = 10610687 }, - { url = "https://files.pythonhosted.org/packages/db/e2/dc41ee90c3085aadad4da614d310d834f641aaafddf3dfbba08210c616ce/ruff-0.7.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:10ebce7696afe4644e8c1a23b3cf8c0f2193a310c18387c06e583ae9ef284de2", size = 10254843 }, - { url = "https://files.pythonhosted.org/packages/6f/09/5f6cac1c91542bc5bd33d40b4c13b637bf64d7bb29e091dadb01b62527fe/ruff-0.7.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3f36d56326b3aef8eeee150b700e519880d1aab92f471eefdef656fd57492aa2", size = 10730962 }, - { url = "https://files.pythonhosted.org/packages/d3/42/89a4b9a24ef7d00269e24086c417a006f9a3ffeac2c80f2629eb5ce140ee/ruff-0.7.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5d024301109a0007b78d57ab0ba190087b43dce852e552734ebf0b0b85e4fb16", size = 11101907 }, - { url = "https://files.pythonhosted.org/packages/b0/5c/efdb4777686683a8edce94ffd812783bddcd3d2454d38c5ac193fef7c500/ruff-0.7.3-py3-none-win32.whl", hash = "sha256:4ba81a5f0c5478aa61674c5a2194de8b02652f17addf8dfc40c8937e6e7d79fc", size = 8611095 }, - { url = "https://files.pythonhosted.org/packages/bb/b8/28fbc6a4efa50178f973972d1c84b2d0a33cdc731588522ab751ac3da2f5/ruff-0.7.3-py3-none-win_amd64.whl", hash = "sha256:588a9ff2fecf01025ed065fe28809cd5a53b43505f48b69a1ac7707b1b7e4088", size = 9418283 }, - { url = "https://files.pythonhosted.org/packages/3f/77/b587cba6febd5e2003374f37eb89633f79f161e71084f94057c8653b7fb3/ruff-0.7.3-py3-none-win_arm64.whl", hash = "sha256:1713e2c5545863cdbfe2cbce21f69ffaf37b813bfd1fb3b90dc9a6f1963f5a8c", size = 8725228 }, + { url = "https://files.pythonhosted.org/packages/e6/4b/f5094719e254829766b807dadb766841124daba75a37da83e292ae5ad12f/ruff-0.7.4-py3-none-linux_armv6l.whl", hash = "sha256:a4919925e7684a3f18e18243cd6bea7cfb8e968a6eaa8437971f681b7ec51478", size = 10447512 }, + { url = "https://files.pythonhosted.org/packages/9e/1d/3d2d2c9f601cf6044799c5349ff5267467224cefed9b35edf5f1f36486e9/ruff-0.7.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:cfb365c135b830778dda8c04fb7d4280ed0b984e1aec27f574445231e20d6c63", size = 10235436 }, + { url = "https://files.pythonhosted.org/packages/62/83/42a6ec6216ded30b354b13e0e9327ef75a3c147751aaf10443756cb690e9/ruff-0.7.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:63a569b36bc66fbadec5beaa539dd81e0527cb258b94e29e0531ce41bacc1f20", size = 9888936 }, + { url = "https://files.pythonhosted.org/packages/4d/26/e1e54893b13046a6ad05ee9b89ee6f71542ba250f72b4c7a7d17c3dbf73d/ruff-0.7.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d06218747d361d06fd2fdac734e7fa92df36df93035db3dc2ad7aa9852cb109", size = 10697353 }, + { url = "https://files.pythonhosted.org/packages/21/24/98d2e109c4efc02bfef144ec6ea2c3e1217e7ce0cfddda8361d268dfd499/ruff-0.7.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e0cea28d0944f74ebc33e9f934238f15c758841f9f5edd180b5315c203293452", size = 10228078 }, + { url = "https://files.pythonhosted.org/packages/ad/b7/964c75be9bc2945fc3172241b371197bb6d948cc69e28bc4518448c368f3/ruff-0.7.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80094ecd4793c68b2571b128f91754d60f692d64bc0d7272ec9197fdd09bf9ea", size = 11264823 }, + { url = "https://files.pythonhosted.org/packages/12/8d/20abdbf705969914ce40988fe71a554a918deaab62c38ec07483e77866f6/ruff-0.7.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:997512325c6620d1c4c2b15db49ef59543ef9cd0f4aa8065ec2ae5103cedc7e7", size = 11951855 }, + { url = "https://files.pythonhosted.org/packages/b8/fc/6519ce58c57b4edafcdf40920b7273dfbba64fc6ebcaae7b88e4dc1bf0a8/ruff-0.7.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00b4cf3a6b5fad6d1a66e7574d78956bbd09abfd6c8a997798f01f5da3d46a05", size = 11516580 }, + { url = "https://files.pythonhosted.org/packages/37/1a/5ec1844e993e376a86eb2456496831ed91b4398c434d8244f89094758940/ruff-0.7.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7dbdc7d8274e1422722933d1edddfdc65b4336abf0b16dfcb9dedd6e6a517d06", size = 12692057 }, + { url = "https://files.pythonhosted.org/packages/50/90/76867152b0d3c05df29a74bb028413e90f704f0f6701c4801174ba47f959/ruff-0.7.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e92dfb5f00eaedb1501b2f906ccabfd67b2355bdf117fea9719fc99ac2145bc", size = 11085137 }, + { url = "https://files.pythonhosted.org/packages/c8/eb/0a7cb6059ac3555243bd026bb21785bbc812f7bbfa95a36c101bd72b47ae/ruff-0.7.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3bd726099f277d735dc38900b6a8d6cf070f80828877941983a57bca1cd92172", size = 10681243 }, + { url = "https://files.pythonhosted.org/packages/5e/76/2270719dbee0fd35780b05c08a07b7a726c3da9f67d9ae89ef21fc18e2e5/ruff-0.7.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2e32829c429dd081ee5ba39aef436603e5b22335c3d3fff013cd585806a6486a", size = 10319187 }, + { url = "https://files.pythonhosted.org/packages/9f/e5/39100f72f8ba70bec1bd329efc880dea8b6c1765ea1cb9d0c1c5f18b8d7f/ruff-0.7.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:662a63b4971807623f6f90c1fb664613f67cc182dc4d991471c23c541fee62dd", size = 10803715 }, + { url = "https://files.pythonhosted.org/packages/a5/89/40e904784f305fb56850063f70a998a64ebba68796d823dde67e89a24691/ruff-0.7.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:876f5e09eaae3eb76814c1d3b68879891d6fde4824c015d48e7a7da4cf066a3a", size = 11162912 }, + { url = "https://files.pythonhosted.org/packages/8d/1b/dd77503b3875c51e3dbc053fd8367b845ab8b01c9ca6d0c237082732856c/ruff-0.7.4-py3-none-win32.whl", hash = "sha256:75c53f54904be42dd52a548728a5b572344b50d9b2873d13a3f8c5e3b91f5cac", size = 8702767 }, + { url = "https://files.pythonhosted.org/packages/63/76/253ddc3e89e70165bba952ecca424b980b8d3c2598ceb4fc47904f424953/ruff-0.7.4-py3-none-win_amd64.whl", hash = "sha256:745775c7b39f914238ed1f1b0bebed0b9155a17cd8bc0b08d3c87e4703b990d6", size = 9497534 }, + { url = "https://files.pythonhosted.org/packages/aa/70/f8724f31abc0b329ca98b33d73c14020168babcf71b0cba3cded5d9d0e66/ruff-0.7.4-py3-none-win_arm64.whl", hash = "sha256:11bff065102c3ae9d3ea4dc9ecdfe5a5171349cdd0787c1fc64761212fc9cf1f", size = 8851590 }, ] [[package]] @@ -299,14 +301,14 @@ wheels = [ [[package]] name = "starlette" -version = "0.41.2" +version = "0.41.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3e/da/1fb4bdb72ae12b834becd7e1e7e47001d32f91ec0ce8d7bc1b618d9f0bd9/starlette-0.41.2.tar.gz", hash = "sha256:9834fd799d1a87fd346deb76158668cfa0b0d56f85caefe8268e2d97c3468b62", size = 2573867 } +sdist = { url = "https://files.pythonhosted.org/packages/1a/4c/9b5764bd22eec91c4039ef4c55334e9187085da2d8a2df7bd570869aae18/starlette-0.41.3.tar.gz", hash = "sha256:0e4ab3d16522a255be6b28260b938eae2482f98ce5cc934cb08dce8dc3ba5835", size = 2574159 } wheels = [ - { url = "https://files.pythonhosted.org/packages/54/43/f185bfd0ca1d213beb4293bed51d92254df23d8ceaf6c0e17146d508a776/starlette-0.41.2-py3-none-any.whl", hash = "sha256:fbc189474b4731cf30fcef52f18a8d070e3f3b46c6a04c97579e85e6ffca942d", size = 73259 }, + { url = "https://files.pythonhosted.org/packages/96/00/2b325970b3060c7cecebab6d295afe763365822b1306a12eeab198f74323/starlette-0.41.3-py3-none-any.whl", hash = "sha256:44cedb2b7c77a9de33a8b74b2b90e9f50d11fcf25d8270ea525ad71a25374ff7", size = 73225 }, ] [[package]] diff --git a/src/puppeteer/README.md b/src/puppeteer/README.md index 4350f6fb..c6132b1b 100644 --- a/src/puppeteer/README.md +++ b/src/puppeteer/README.md @@ -1,3 +1,53 @@ -# Puppeteer server +# Puppeteer -This MCP server provides **resources** and **tools** for interacting with a browser and web pages, using [Puppeteer](https://pptr.dev/). +A Model Context Protocol server that provides browser automation capabilities using Puppeteer. This server enables LLMs to interact with web pages, take screenshots, and execute JavaScript in a real browser environment. + +## Components + +### Tools + +- **puppeteer_navigate** + - Navigate to any URL in the browser + - Input: `url` (string) + +- **puppeteer_screenshot** + - Capture screenshots of the entire page or specific elements + - Inputs: + - `name` (string, required): Name for the screenshot + - `selector` (string, optional): CSS selector for element to screenshot + - `width` (number, optional, default: 800): Screenshot width + - `height` (number, optional, default: 600): Screenshot height + +- **puppeteer_click** + - Click elements on the page + - Input: `selector` (string): CSS selector for element to click + +- **puppeteer_fill** + - Fill out input fields + - Inputs: + - `selector` (string): CSS selector for input field + - `value` (string): Value to fill + +- **puppeteer_evaluate** + - Execute JavaScript in the browser console + - Input: `script` (string): JavaScript code to execute + +### Resources + +The server provides access to two types of resources: + +1. **Console Logs** (`console://logs`) + - Browser console output in text format + - Includes all console messages from the browser + +2. **Screenshots** (`screenshot://`) + - PNG images of captured screenshots + - Accessible via the screenshot name specified during capture + +## Key Features + +- Browser automation +- Console log monitoring +- Screenshot capabilities +- JavaScript execution +- Basic web interaction (navigation, clicking, form filling) \ No newline at end of file diff --git a/src/puppeteer/index.ts b/src/puppeteer/index.ts index cce6614e..9ad05cbe 100644 --- a/src/puppeteer/index.ts +++ b/src/puppeteer/index.ts @@ -7,8 +7,267 @@ import { ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, + CallToolResult, + TextContent, + ImageContent, + Tool, } from "@modelcontextprotocol/sdk/types.js"; -import puppeteer from "puppeteer"; +import puppeteer, { Browser, Page } from "puppeteer"; + +// Define the tools once to avoid repetition +const TOOLS: Tool[] = [ + { + name: "puppeteer_navigate", + description: "Navigate to a URL", + inputSchema: { + type: "object", + properties: { + url: { type: "string" }, + }, + required: ["url"], + }, + }, + { + name: "puppeteer_screenshot", + description: "Take a screenshot of the current page or a specific element", + inputSchema: { + type: "object", + properties: { + name: { type: "string", description: "Name for the screenshot" }, + selector: { type: "string", description: "CSS selector for element to screenshot" }, + width: { type: "number", description: "Width in pixels (default: 800)" }, + height: { type: "number", description: "Height in pixels (default: 600)" }, + }, + required: ["name"], + }, + }, + { + name: "puppeteer_click", + description: "Click an element on the page", + inputSchema: { + type: "object", + properties: { + selector: { type: "string", description: "CSS selector for element to click" }, + }, + required: ["selector"], + }, + }, + { + name: "puppeteer_fill", + description: "Fill out an input field", + inputSchema: { + type: "object", + properties: { + selector: { type: "string", description: "CSS selector for input field" }, + value: { type: "string", description: "Value to fill" }, + }, + required: ["selector", "value"], + }, + }, + { + name: "puppeteer_evaluate", + description: "Execute JavaScript in the browser console", + inputSchema: { + type: "object", + properties: { + script: { type: "string", description: "JavaScript code to execute" }, + }, + required: ["script"], + }, + }, +]; + +// Global state +let browser: Browser | undefined; +let page: Page | undefined; +const consoleLogs: string[] = []; +const screenshots = new Map(); + +async function ensureBrowser() { + if (!browser) { + browser = await puppeteer.launch({ headless: false }); + const pages = await browser.pages(); + page = pages[0]; + + page.on("console", (msg) => { + const logEntry = `[${msg.type()}] ${msg.text()}`; + consoleLogs.push(logEntry); + server.notification({ + method: "notifications/resources/updated", + params: { uri: "console://logs" }, + }); + }); + } + return page!; +} + +async function handleToolCall(name: string, args: any): Promise<{ toolResult: CallToolResult }> { + const page = await ensureBrowser(); + + switch (name) { + case "puppeteer_navigate": + await page.goto(args.url); + return { + toolResult: { + content: [{ + type: "text", + text: `Navigated to ${args.url}`, + }], + isError: false, + }, + }; + + case "puppeteer_screenshot": { + const width = args.width ?? 800; + const height = args.height ?? 600; + await page.setViewport({ width, height }); + + const screenshot = await (args.selector ? + (await page.$(args.selector))?.screenshot({ encoding: "base64" }) : + page.screenshot({ encoding: "base64", fullPage: false })); + + if (!screenshot) { + return { + toolResult: { + content: [{ + type: "text", + text: args.selector ? `Element not found: ${args.selector}` : "Screenshot failed", + }], + isError: true, + }, + }; + } + + screenshots.set(args.name, screenshot as string); + server.notification({ + method: "notifications/resources/list_changed", + }); + + return { + toolResult: { + content: [ + { + type: "text", + text: `Screenshot '${args.name}' taken at ${width}x${height}`, + } as TextContent, + { + type: "image", + data: screenshot, + mimeType: "image/png", + } as ImageContent, + ], + isError: false, + }, + }; + } + + case "puppeteer_click": + try { + await page.click(args.selector); + return { + toolResult: { + content: [{ + type: "text", + text: `Clicked: ${args.selector}`, + }], + isError: false, + }, + }; + } catch (error) { + return { + toolResult: { + content: [{ + type: "text", + text: `Failed to click ${args.selector}: ${(error as Error).message}`, + }], + isError: true, + }, + }; + } + + case "puppeteer_fill": + try { + await page.waitForSelector(args.selector); + await page.type(args.selector, args.value); + return { + toolResult: { + content: [{ + type: "text", + text: `Filled ${args.selector} with: ${args.value}`, + }], + isError: false, + }, + }; + } catch (error) { + return { + toolResult: { + content: [{ + type: "text", + text: `Failed to fill ${args.selector}: ${(error as Error).message}`, + }], + isError: true, + }, + }; + } + + case "puppeteer_evaluate": + try { + const result = await page.evaluate((script) => { + const logs: string[] = []; + const originalConsole = { ...console }; + + ['log', 'info', 'warn', 'error'].forEach(method => { + (console as any)[method] = (...args: any[]) => { + logs.push(`[${method}] ${args.join(' ')}`); + (originalConsole as any)[method](...args); + }; + }); + + try { + const result = eval(script); + Object.assign(console, originalConsole); + return { result, logs }; + } catch (error) { + Object.assign(console, originalConsole); + throw error; + } + }, args.script); + + return { + toolResult: { + content: [ + { + type: "text", + text: `Execution result:\n${JSON.stringify(result.result, null, 2)}\n\nConsole output:\n${result.logs.join('\n')}`, + }, + ], + isError: false, + }, + }; + } catch (error) { + return { + toolResult: { + content: [{ + type: "text", + text: `Script execution failed: ${(error as Error).message}`, + }], + isError: true, + }, + }; + } + + default: + return { + toolResult: { + content: [{ + type: "text", + text: `Unknown tool: ${name}`, + }], + isError: true, + }, + }; + } +} const server = new Server( { @@ -17,94 +276,70 @@ const server = new Server( }, { capabilities: { - resources: { - listChanged: true, - }, + resources: {}, tools: {}, }, }, ); -let browser: puppeteer.Browser | undefined; -let consoleLogs: string[] = []; -server.setRequestHandler(ListResourcesRequestSchema, async () => { - return { - resources: [ - { - uri: "console://logs", - mimeType: "text/plain", - name: "Browser console logs", - }, - ], - }; -}); +// Setup request handlers +server.setRequestHandler(ListResourcesRequestSchema, async () => ({ + resources: [ + { + uri: "console://logs", + mimeType: "text/plain", + name: "Browser console logs", + }, + ...Array.from(screenshots.keys()).map(name => ({ + uri: `screenshot://${name}`, + mimeType: "image/png", + name: `Screenshot: ${name}`, + })), + ], +})); server.setRequestHandler(ReadResourceRequestSchema, async (request) => { - if (request.params.uri.toString() === "console://logs") { + const uri = request.params.uri.toString(); + + if (uri === "console://logs") { return { - contents: [ - { - uri: "console://logs", - mimeType: "text/plain", - text: consoleLogs.join("\n"), - }, - ], + contents: [{ + uri, + mimeType: "text/plain", + text: consoleLogs.join("\n"), + }], }; } - console.error("Resource not found:", request.params.uri); - throw new Error("Resource not found"); -}); -server.setRequestHandler(ListToolsRequestSchema, async () => { - return { - tools: [ - { - name: "navigate", - description: "Navigate to a URL", - inputSchema: { - type: "object", - properties: { - url: { type: "string" }, - }, - }, - }, - ], - }; -}); - -server.setRequestHandler(CallToolRequestSchema, async (request) => { - if (request.params.name === "navigate") { - const url = request.params.arguments?.url as string; - - if (!browser) { - browser = await puppeteer.launch({ headless: false }); - - const pages = await browser.pages(); - pages[0].on("console", (msg) => { - const logEntry = `[${msg.type()}] ${msg.text()}`; - consoleLogs.push(logEntry); - server.notification({ - method: "notifications/resources/updated", - params: { uri: "console://logs" }, - }); - }); + if (uri.startsWith("screenshot://")) { + const name = uri.split("://")[1]; + const screenshot = screenshots.get(name); + if (screenshot) { + return { + contents: [{ + uri, + mimeType: "image/png", + blob: screenshot, + }], + }; } - - const pages = await browser.pages(); - await pages[0].goto(url); - - return { - content: [{ type: "text", text: `Navigated to ${url}` }], - isError: false, - }; } - throw new Error(`Unknown tool: ${request.params.name}`); + + throw new Error(`Resource not found: ${uri}`); }); +server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: TOOLS, +})); + +server.setRequestHandler(CallToolRequestSchema, async (request) => + handleToolCall(request.params.name, request.params.arguments ?? {}) +); + async function runServer() { const transport = new StdioServerTransport(); await server.connect(transport); } -runServer().catch(console.error); +runServer().catch(console.error); \ No newline at end of file