Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions .github/workflows/smoke.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,20 @@ jobs:
- name: Import and register tools
run: |
python - <<'PY'
import asyncio, sys
import sys
from importlib.metadata import version

from selenium_mcp import server

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}

Expand Down
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
14 changes: 9 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
]

Expand Down
70 changes: 57 additions & 13 deletions src/selenium_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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)
Expand All @@ -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():
Expand Down
Loading