diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 84debad..d311f55 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -41,7 +41,7 @@ jobs: - name: Import and register tools run: | python - <<'PY' - import asyncio, sys + import sys from importlib.metadata import version from selenium_mcp import server @@ -49,7 +49,12 @@ jobs: print("seleniumboot-mcp:", version("seleniumboot-mcp")) print("mcp:", version("mcp")) - tools = asyncio.run(server.list_tools()) + # Assert against the registry itself, not by calling the request + # handler. The handler's signature belongs to the mcp SDK and changed + # in 2.0 (it now takes a context + typed params); ALL_TOOLS is ours + # and is what the handler returns. Checking the registry keeps this + # test measuring the package rather than the SDK's calling convention. + tools = server.ALL_TOOLS handlers = server.TOOL_HANDLERS names = {t.name for t in tools} diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c24c6f..3721669 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,37 @@ All notable changes to **seleniumboot-mcp** are documented here. This project adheres to [Semantic Versioning](https://semver.org/). +## [0.5.0] + +### Changed +- **Migrated to the `mcp` 2.0 SDK.** 2.0.0 removed the `@server.list_tools()` / + `@server.call_tool()` decorators this server registered its 85 tools with; + they are replaced by `on_list_tools` / `on_call_tool` callbacks passed to the + `Server` constructor, taking a `ServerRequestContext` plus typed request + params and returning `ListToolsResult` / `CallToolResult`. This completes the + work 0.4.2 deferred when it pinned the SDK away from 2.x. (#2) +- **The `mcp` dependency is now `>=2.0.0`.** The temporary `<2.0.0` ceiling from + 0.4.2 is gone; 1.x is no longer supported, because the 1.x calling convention + no longer exists in the code. An upper bound returns when `mcp` 3.0 appears — + Dependabot now watches for exactly that. +- **The server reports its own version** in the MCP handshake, read from the + installed distribution metadata rather than duplicated in source. + +**No behaviour change on the wire.** Tool names, input schemas, response +content, and error strings are byte-for-byte what 0.4.x emitted; the migration +was verified as a migration, not shipped alongside other changes. Verified +against the published artifact in a clean environment: 85/85 tools registered +with a handler each, entry point exits cleanly, protocol version `2025-03-26` +still negotiates, and the 68-test `selenium-mcp-test` Java suite passes in full +over real stdio JSON-RPC. + +### Infrastructure +- **PyPI publishing now uses Trusted Publishing (OIDC)** instead of a stored API + token. The token had been returning 403 since ~2026-07-01, so 0.4.0 through + 0.4.2 were each published by hand — a manual workaround that hid the breakage. +- **Dependabot watches the dependencies.** The next upstream major arrives as a + pull request rather than as a bug report. That is the whole lesson of #2. + ## [0.4.2] ### Fixed diff --git a/pyproject.toml b/pyproject.toml index d86658e..87df1c1 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "seleniumboot-mcp" -version = "0.4.2" +version = "0.5.0" description = "A Python MCP server for Selenium WebDriver — 85 tools for browser automation, alerts, frames, shadow DOM, cookies, mobile emulation, self-healing locators, and codegen for Java/C#/Python/Playwright/CI pipelines" readme = "README.md" requires-python = ">=3.10" @@ -21,10 +21,14 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] dependencies = [ - # Upper bound is load-bearing: mcp 2.0.0 removed the Server.list_tools / - # Server.call_tool decorators this server registers its tools with (#2). - # Do not relax it without migrating server.py to the 2.0 API. - "mcp>=1.0.0,<2.0.0", + # server.py was migrated to the mcp 2.0 API in 0.5.0 — the decorators this + # server used to register with (Server.list_tools / Server.call_tool) were + # removed in 2.0.0 and are gone for good (#2). 1.x is therefore no longer + # supported: the floor, not a ceiling, is what's load-bearing now. + # An upper bound goes back the moment mcp 3.0 is on the horizon — + # Dependabot opens that PR (.github/dependabot.yml); do not merge it + # without running the selenium-mcp-test gate first. + "mcp>=2.0.0", "selenium>=4.6.0", ] diff --git a/src/selenium_mcp/server.py b/src/selenium_mcp/server.py index a4dcb60..b0eecb1 100755 --- a/src/selenium_mcp/server.py +++ b/src/selenium_mcp/server.py @@ -7,10 +7,18 @@ import asyncio import logging import tempfile +from importlib.metadata import PackageNotFoundError, version as _pkg_version from pathlib import Path -from mcp.server import Server +from mcp.server import Server, ServerRequestContext from mcp.server.stdio import stdio_server -from mcp.types import Tool, TextContent, ImageContent +from mcp.types import ( + CallToolRequestParams, + CallToolResult, + ImageContent, + ListToolsResult, + PaginatedRequestParams, + TextContent, +) from selenium_mcp.tools.browser_tools import BrowserTools from selenium_mcp.tools.element_tools import ElementTools @@ -26,6 +34,13 @@ ) log = logging.getLogger(__name__) +try: + # Single source of truth is pyproject.toml; read it back off the installed + # distribution rather than duplicating the number in the source. + __version__ = _pkg_version("seleniumboot-mcp") +except PackageNotFoundError: # running from a source tree, not an install + __version__ = "0.0.0.dev0" + SERVER_INSTRUCTIONS = """\ Selenium Boot MCP — real-browser automation plus test-code generation. @@ -65,8 +80,6 @@ more real elements first, then regenerate — do not pad the test by hand. """ -app = Server("selenium-mcp", instructions=SERVER_INSTRUCTIONS) - browser = BrowserTools() element = ElementTools(browser) assertion = AssertionTools(browser) @@ -87,26 +100,57 @@ } -@app.list_tools() -async def list_tools() -> list[Tool]: - return ALL_TOOLS +# mcp 2.0 replaced the @app.list_tools() / @app.call_tool() decorators with +# constructor callbacks. The handlers now take a ServerRequestContext plus typed +# request params, and return Result models instead of bare content lists. +# +# The wire output is deliberately unchanged from 0.4.x — same content, same +# text, same error strings. A migration that also changes behaviour cannot be +# verified as a migration. (Returning is_error=True on the exception path is +# the semantically correct thing and is a follow-up, not part of this change.) + +async def list_tools( + ctx: ServerRequestContext[None], + params: PaginatedRequestParams | None, +) -> ListToolsResult: + return ListToolsResult(tools=ALL_TOOLS) -@app.call_tool() -async def call_tool(name: str, arguments: dict) -> list[TextContent]: + +async def call_tool( + ctx: ServerRequestContext[None], + params: CallToolRequestParams, +) -> CallToolResult: + name = params.name + arguments = params.arguments or {} log.info(f"Tool called: {name} | args: {arguments}") handler = TOOL_HANDLERS.get(name) if not handler: - return [TextContent(type="text", text=f"Unknown tool: {name}")] + return CallToolResult( + content=[TextContent(type="text", text=f"Unknown tool: {name}")] + ) try: result = await handler(arguments) if isinstance(result, str) and result.startswith("screenshot:base64:"): b64 = result[len("screenshot:base64:"):] - return [ImageContent(type="image", data=b64, mimeType="image/png")] - return [TextContent(type="text", text=result)] + return CallToolResult( + content=[ImageContent(type="image", data=b64, mimeType="image/png")] + ) + return CallToolResult(content=[TextContent(type="text", text=result)]) except Exception as e: log.error(f"Tool error [{name}]: {e}") - return [TextContent(type="text", text=f"Error: {str(e)}")] + return CallToolResult( + content=[TextContent(type="text", text=f"Error: {str(e)}")] + ) + + +app = Server( + "selenium-mcp", + version=__version__, + instructions=SERVER_INSTRUCTIONS, + on_list_tools=list_tools, + on_call_tool=call_tool, +) async def main():