devserver passes browser options through args

The browser tool types only its core fields; everything else rides in
`args`. navigate's state and console's level sat at the top level,
where the schema drops them, so errors opened the app without waiting
for networkidle and read every console message rather than errors.
The test's stand-in browser now has the real tool's shape.

Claude-Session: https://claude.ai/code/session_01Dc8DXc3fEQCRHUa6XWK4Ku
This commit is contained in:
2026-09-29 13:06:50 -07:00
parent 450f65932d
commit 1b4cfa4c16
2 changed files with 12 additions and 8 deletions
@@ -208,7 +208,7 @@ class DevserverTool(BaseTool):
# not yet reported a build error. The server's answer stands
# without a browser.
try:
opened = await self._browser("navigate", url=url, state="networkidle")
opened = await self._browser("navigate", url=url, args={"state": "networkidle"})
except Exception as e:
opened = {"error": str(e)}
report = {"server": server.url, "source": "mcp", **await self._call(server, "get_errors", {})}
@@ -216,14 +216,14 @@ class DevserverTool(BaseTool):
report["browser"] = opened["error"]
return report
before = (
(await self._browser("console", level="error")).get("count", 0),
(await self._browser("console", args={"level": "error"})).get("count", 0),
(await self._browser("errors")).get("count", 0),
)
opened = await self._browser("navigate", url=url, state="networkidle")
opened = await self._browser("navigate", url=url, args={"state": "networkidle"})
if opened.get("error"):
raise ToolError("INTERNAL_ERROR", f"browser could not open {url}: {opened['error']}")
overlay = (await self._browser("evaluate", code=OVERLAY)).get("result")
console = await self._browser("console", level="error")
console = await self._browser("console", args={"level": "error"})
thrown = await self._browser("errors")
return {
"server": server.url,
@@ -52,9 +52,12 @@ def registry(pages: list[dict]):
server = FastMCP("test")
calls = []
# The real browser tool types its core fields and takes the rest in `args`;
# this stand-in has the same shape, so a parameter devserver puts in the
# wrong place is a test failure here rather than a value dropped there.
@server.tool(name="browser")
async def browser(action: str, url: str = "", state: str = "", level: str = "", code: str = "") -> dict:
calls.append({k: v for k, v in dict(action=action, url=url, state=state, level=level).items() if v})
async def browser(action: str, url: str = "", code: str = "", args: dict | None = None) -> dict:
calls.append({k: v for k, v in dict(action=action, url=url, args=args).items() if v})
return pages.pop(0)
DevserverTool().register(server)
@@ -71,7 +74,7 @@ async def test_errors_opens_the_app_then_asks_the_servers_mcp(monkeypatch, next_
server, calls = registry([{"success": True}])
out = await run(server, action="errors", port=next_mcp, path="dashboard")
assert out["ok"], out
assert calls == [{"action": "navigate", "url": f"http://localhost:{next_mcp}/dashboard", "state": "networkidle"}]
assert calls == [{"action": "navigate", "url": f"http://localhost:{next_mcp}/dashboard", "args": {"state": "networkidle"}}]
data = out["data"]
assert data["source"] == "mcp"
assert data["result"] == {"sessionErrors": [{"url": "/", "buildError": "./app/page.jsx:3:1 Expected '>'"}]}
@@ -106,7 +109,8 @@ async def test_errors_reads_the_page_without_mcp(monkeypatch):
)
data = (await run(server, action="errors"))["data"]
assert [c["action"] for c in calls] == ["console", "errors", "navigate", "evaluate", "console", "errors"]
assert calls[2]["state"] == "networkidle"
assert calls[2]["args"] == {"state": "networkidle"}
assert calls[0]["args"] == calls[4]["args"] == {"level": "error"}
assert data == {
"server": "http://localhost:5174",
"source": "browser",