feat(mcp): target DOM tools by tab
- Add server-side search filtering to the MCP tabs_list tool. - Let MCP DOM query, click, and type tools operate on an explicit tab_id. - Use tab-scoped DOM eval fallbacks when the SDK active-tab helpers are not enough. - Cover tab filtering and tab-targeted DOM operations in MCP tests. - Bump package and extension versions to 0.16.8 and refresh the lockfile.
This commit is contained in:
@@ -8,6 +8,7 @@ from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Literal
|
||||
@@ -68,12 +69,21 @@ def create_server(*, client_factory: ClientFactory = BrowserCLI, tool_prefix: st
|
||||
|
||||
@mcp.tool(name=tool_name("tabs_list", prefix))
|
||||
def tabs_list(
|
||||
search: str | None = None,
|
||||
browser: str | None = None,
|
||||
remote: str | None = None,
|
||||
key: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""List current tabs. Optionally target a browser alias or authenticated remote."""
|
||||
return structured(_client(client_factory, browser, remote, key).tabs.list())
|
||||
"""List current tabs, optionally filtering by URL or title substring."""
|
||||
tabs = structured(_client(client_factory, browser, remote, key).tabs.list())
|
||||
if not search:
|
||||
return tabs
|
||||
needle = search.lower()
|
||||
return [
|
||||
tab for tab in tabs
|
||||
if needle in str(tab.get("url") or "").lower()
|
||||
or needle in str(tab.get("title") or "").lower()
|
||||
]
|
||||
|
||||
@mcp.tool(name=tool_name("tabs_open", prefix))
|
||||
def tabs_open(
|
||||
@@ -155,35 +165,80 @@ def create_server(*, client_factory: ClientFactory = BrowserCLI, tool_prefix: st
|
||||
@mcp.tool(name=tool_name("dom_query", prefix))
|
||||
def dom_query(
|
||||
selector: str,
|
||||
tab_id: int | None = None,
|
||||
browser: str | None = None,
|
||||
remote: str | None = None,
|
||||
key: str | None = None,
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Return elements matching a CSS selector on the active page."""
|
||||
return structured(_client(client_factory, browser, remote, key).dom.query(selector))
|
||||
"""Return elements matching a CSS selector on the active page or tab_id."""
|
||||
client = _client(client_factory, browser, remote, key)
|
||||
if tab_id is None:
|
||||
return structured(client.dom.query(selector))
|
||||
code = f"""
|
||||
(() => {{
|
||||
const selector = {json.dumps(selector)};
|
||||
return [...document.querySelectorAll(selector)].map(el => ({{
|
||||
tag: el.tagName,
|
||||
text: (el.innerText || el.textContent || '').replace(/\\s+/g, ' ').trim(),
|
||||
attrs: Object.fromEntries([...el.attributes].map(attr => [attr.name, attr.value])),
|
||||
}}));
|
||||
}})()
|
||||
"""
|
||||
return structured(client.dom.eval(code, tab_id))
|
||||
|
||||
@mcp.tool(name=tool_name("dom_click", prefix))
|
||||
def dom_click(
|
||||
selector: str,
|
||||
tab_id: int | None = None,
|
||||
browser: str | None = None,
|
||||
remote: str | None = None,
|
||||
key: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Click the first matching element, then return current active-page info."""
|
||||
"""Click the first matching element, then return current page info."""
|
||||
client = _client(client_factory, browser, remote, key)
|
||||
client.dom.click(selector)
|
||||
return structured(client.page.info())
|
||||
if tab_id is None:
|
||||
client.dom.click(selector)
|
||||
return structured(client.page.info())
|
||||
code = f"""
|
||||
(() => {{
|
||||
const selector = {json.dumps(selector)};
|
||||
const el = document.querySelector(selector);
|
||||
if (!el) throw new Error(`No element found for selector: ${{selector}}`);
|
||||
el.click();
|
||||
return true;
|
||||
}})()
|
||||
"""
|
||||
client.dom.eval(code, tab_id)
|
||||
return structured(client.page.info(tab_id))
|
||||
|
||||
@mcp.tool(name=tool_name("dom_type", prefix))
|
||||
def dom_type(
|
||||
selector: str,
|
||||
text: str,
|
||||
tab_id: int | None = None,
|
||||
browser: str | None = None,
|
||||
remote: str | None = None,
|
||||
key: str | None = None,
|
||||
) -> dict[str, bool]:
|
||||
"""Type text into the first element matching a CSS selector."""
|
||||
_client(client_factory, browser, remote, key).dom.type(selector, text)
|
||||
client = _client(client_factory, browser, remote, key)
|
||||
if tab_id is None:
|
||||
client.dom.type(selector, text)
|
||||
return {"typed": True}
|
||||
code = f"""
|
||||
(() => {{
|
||||
const selector = {json.dumps(selector)};
|
||||
const text = {json.dumps(text)};
|
||||
const el = document.querySelector(selector);
|
||||
if (!el) throw new Error(`No element found for selector: ${{selector}}`);
|
||||
el.focus();
|
||||
el.value = text;
|
||||
el.dispatchEvent(new InputEvent('input', {{ bubbles: true, inputType: 'insertText', data: text }}));
|
||||
el.dispatchEvent(new Event('change', {{ bubbles: true }}));
|
||||
return true;
|
||||
}})()
|
||||
"""
|
||||
client.dom.eval(code, tab_id)
|
||||
return {"typed": True}
|
||||
|
||||
@mcp.tool(name=tool_name("screenshot", prefix), structured_output=False)
|
||||
|
||||
Reference in New Issue
Block a user