Add optional stateless MCP server for the real browser
Expose a conservative browser-cli tool surface to MCP hosts without duplicating browser-control logic: every tool is a thin adapter over the existing Python SDK, and each call builds a fresh BrowserCLI so the real browser stays the only owner of tab and navigation state. The MCP process resolves BROWSER_CLI_PROFILE, BROWSER_CLI_REMOTE, and BROWSER_CLI_KEY explicitly instead of passing None down, so a pinned server targets one browser rather than fanning listing calls out across every connected browser. Explicit tool arguments still win. Tool names keep a browser_ prefix for hosts that expose raw MCP names, while BROWSER_CLI_MCP_TOOL_PREFIX lets hosts that already namespace by server drop it and avoid names like browser_cli_testing_browser_tabs_list. JavaScript evaluation, raw commands, storage writes, and session import stay unexposed, and Streamable HTTP refuses non-loopback binds because the endpoint has no authentication of its own; remote browsers go through browser-cli's authenticated remote transport instead. MCP stays an optional extra, so normal installs are unaffected.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
"""Tool-name prefixing for the MCP surface.
|
||||
|
||||
Hosts differ in how they namespace MCP tools. Hosts that already prefix tool
|
||||
names with the server name turn the built-in ``browser_`` prefix into stutter
|
||||
(``browser_cli_testing_browser_tabs_list``), while hosts that expose raw names
|
||||
need the prefix to keep ``navigate`` or ``screenshot`` unambiguous. The prefix
|
||||
is therefore configurable per MCP server process.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
TOOL_PREFIX_ENV = "BROWSER_CLI_MCP_TOOL_PREFIX"
|
||||
DEFAULT_TOOL_PREFIX = "browser_"
|
||||
|
||||
_VALID_PREFIX = re.compile(r"\A[a-z][a-z0-9_]*\Z")
|
||||
|
||||
def resolve_tool_prefix(environ: dict[str, str] | None = None) -> str:
|
||||
"""Return the configured tool-name prefix, defaulting to ``browser_``.
|
||||
|
||||
An empty value disables prefixing for hosts that namespace tools themselves.
|
||||
"""
|
||||
configured = (environ if environ is not None else os.environ).get(TOOL_PREFIX_ENV)
|
||||
if configured is None:
|
||||
return DEFAULT_TOOL_PREFIX
|
||||
prefix = configured.strip()
|
||||
if not prefix:
|
||||
return ""
|
||||
if not _VALID_PREFIX.match(prefix):
|
||||
raise ValueError(
|
||||
f"{TOOL_PREFIX_ENV} must be lowercase letters, digits, and underscores starting "
|
||||
f"with a letter, or empty to disable prefixing; got {configured!r}"
|
||||
)
|
||||
return prefix if prefix.endswith("_") else f"{prefix}_"
|
||||
|
||||
def tool_name(base: str, prefix: str) -> str:
|
||||
"""Apply *prefix* to a bare tool name such as ``tabs_list``."""
|
||||
return f"{prefix}{base}"
|
||||
Reference in New Issue
Block a user