跳转至

MCP 服务器参考

Scrapling MCP 服务器通过 Model Context Protocol(MCP)提供十七个数据提取和浏览器操作工具。将 Scrapling 接入 AI 助手和智能体,就能通过对话提取数据,并使用反检测功能。

用下面的命令启动 MCP 服务器:

scrapling mcp

也可以直接导入服务器类:

from scrapling.core.ai import ScraplingMCPServer

server = ScraplingMCPServer()
server.serve(http=False, host="127.0.0.1", port=8000)

要使用自己的 Chromium 兼容浏览器程序,请传入 executable_path:

server = ScraplingMCPServer(executable_path="/path/to/chromium")

响应模型

页面获取和 HTTP 请求工具返回以下结构,每次调用返回一个响应。会话管理工具使用后面的模型。browser_screenshot 将页面或元素的截图和当前 URL 作为图像块和文本块返回。browser_extract 和 browser_actions 返回纯文本。两个网络工具通过后面的模型返回结构化 JSON。browser_evaluate 将结果转为 JSON,放在文本块中返回。

scrapling.core.ai.server.ResponseModel

基类: BaseModel


              flowchart TD
              scrapling.core.ai.server.ResponseModel[ResponseModel]

              

              click scrapling.core.ai.server.ResponseModel href "" "scrapling.core.ai.server.ResponseModel"
            

保存请求响应信息的数据结构。

status class-attribute instance-attribute

status = Field(
    description="The status code returned by the website."
)

content class-attribute instance-attribute

content = Field(
    description="The page content as Markdown, HTML, text, or an AI ARIA snapshot."
)

url class-attribute instance-attribute

url = Field(
    description="The URL given by the user that resulted in this response."
)

网络请求模型

browser_network_requests 返回 NetworkRequestsModel,包含 requests、next_cursor、has_more 和 dropped_count。每个 NetworkRequestInfo 包含 id、url、method、resource_type 和 status。要读取下一页,将 next_cursor 作为 after_id 传入。has_more 表示当前历史记录中是否还有匹配结果。

browser_network_request 返回 NetworkRequestModel,包含 request_id、part、data 和 note。摘要和请求头或响应头是对象。完整保存的内容以字符串返回,即使响应内容是 JSON 也是如此。note 说明内容是否缺失,或头部信息是否不完整。

scrapling.core.ai._network_formatting.NetworkRequestInfo

基类: BaseModel


              flowchart TD
              scrapling.core.ai._network_formatting.NetworkRequestInfo[NetworkRequestInfo]

              

              click scrapling.core.ai._network_formatting.NetworkRequestInfo href "" "scrapling.core.ai._network_formatting.NetworkRequestInfo"
            

已记录浏览器请求的摘要。

model_config class-attribute instance-attribute

model_config = {'populate_by_name': True}

id class-attribute instance-attribute

id = Field(validation_alias=AliasPath("meta", "network_id"))

url instance-attribute

url

method instance-attribute

method

resource_type class-attribute instance-attribute

resource_type = Field(
    validation_alias=AliasPath("meta", "resource_type")
)

status instance-attribute

status

scrapling.core.ai.server.NetworkRequestsModel

基类: BaseModel


              flowchart TD
              scrapling.core.ai.server.NetworkRequestsModel[NetworkRequestsModel]

              

              click scrapling.core.ai.server.NetworkRequestsModel href "" "scrapling.core.ai.server.NetworkRequestsModel"
            

已记录请求的摘要及分页信息。

requests instance-attribute

requests

next_cursor instance-attribute

next_cursor

has_more instance-attribute

has_more

dropped_count instance-attribute

dropped_count

scrapling.core.ai._network_formatting.NetworkRequestModel

基类: BaseModel


              flowchart TD
              scrapling.core.ai._network_formatting.NetworkRequestModel[NetworkRequestModel]

              

              click scrapling.core.ai._network_formatting.NetworkRequestModel href "" "scrapling.core.ai._network_formatting.NetworkRequestModel"
            

已保存请求中某一部分的完整内容,以及可选说明。

request_id instance-attribute

request_id

part instance-attribute

part

data class-attribute instance-attribute

data = None

note class-attribute instance-attribute

note = None

会话模型

用于会话管理的模型类:

scrapling.core.ai.server.SessionInfo

基类: SessionCreatedModel


              flowchart TD
              scrapling.core.ai.server.SessionInfo[SessionInfo]
              scrapling.core.ai.server.SessionCreatedModel[SessionCreatedModel]

                              scrapling.core.ai.server.SessionCreatedModel --> scrapling.core.ai.server.SessionInfo
                


              click scrapling.core.ai.server.SessionInfo href "" "scrapling.core.ai.server.SessionInfo"
              click scrapling.core.ai.server.SessionCreatedModel href "" "scrapling.core.ai.server.SessionCreatedModel"
            

已打开的浏览器会话信息。

created_at class-attribute instance-attribute

created_at = Field(
    description="ISO timestamp of when the session was created."
)

is_alive class-attribute instance-attribute

is_alive = Field(
    description="Whether the session is still alive and usable."
)

settings class-attribute instance-attribute

settings = Field(
    default_factory=dict,
    description="The effective settings this session was created with.",
)

session_id class-attribute instance-attribute

session_id = Field(
    description="The unique identifier of the session."
)

session_type class-attribute instance-attribute

session_type = Field(
    description="The type of the session: 'stealthy' or 'static'."
)

scrapling.core.ai.server.SessionCreatedModel

基类: BaseModel


              flowchart TD
              scrapling.core.ai.server.SessionCreatedModel[SessionCreatedModel]

              

              click scrapling.core.ai.server.SessionCreatedModel href "" "scrapling.core.ai.server.SessionCreatedModel"
            

创建新会话时返回的响应。

session_id class-attribute instance-attribute

session_id = Field(
    description="The unique identifier of the session."
)

session_type class-attribute instance-attribute

session_type = Field(
    description="The type of the session: 'stealthy' or 'static'."
)

scrapling.core.ai.server.SessionClosedModel

基类: BaseModel


              flowchart TD
              scrapling.core.ai.server.SessionClosedModel[SessionClosedModel]

              

              click scrapling.core.ai.server.SessionClosedModel href "" "scrapling.core.ai.server.SessionClosedModel"
            

关闭会话时返回的响应。

session_id class-attribute instance-attribute

session_id = Field(
    description="The unique identifier of the closed session."
)

message class-attribute instance-attribute

message = Field(description='A confirmation message.')

MCP 服务器类

MCP 服务器的主类提供以下工具:

  • make_request:通过 HTTP 获取一个 URL,模拟浏览器,并自动关闭客户端。支持 GET、POST、PUT 和 DELETE。
  • browser_fetch_once:用隐身浏览器获取一个 URL,执行 JavaScript、处理 Cloudflare 验证,然后关闭浏览器。如果之后还要操作页面或查看网络历史记录,请使用持久会话。

  • browser_extract:读取当前页面或元素,输出适合 AI 的结构化视图、HTML、Markdown 或文本,无需重新加载。表单和操作结果会保留。结构化视图包含元素的角色、名称、引用、位置和尺寸。正则表达式搜索返回匹配行、附近内容和父节点。

  • browser_actions:在一次调用中组合鼠标移动、悬停、点击、滚动、填写表单、快捷键、对话框响应和等待。可以接受或关闭提示和确认框,也可以在输入对话框中填写内容。例如,填写搜索框、提交、等待并打开结果,无需每一步都调用一次工具。

    移动和点击支持选择器、结构化视图中的引用或坐标。工具会等待元素出现,并将其滚动到可见区域。支持分步移动、鼠标左键、右键和中键点击、双击,以及嵌套区域内的滚动。

    按住鼠标按钮即可拖动项目或移动滑块。AI 可以在按住按钮的同时查看页面,继续移动以调整位置,然后在目标位置释放按钮。

    用选择器或结构化视图中的引用选择表单字段。可以输入文字、勾选或取消勾选复选框、选择单选按钮,以及按标签选择原生下拉框中的一个或多个选项。可以替换或清空文字,也可以从光标处继续输入。快捷键作用于当前焦点。慢速模式会在字符和操作之间加入随机停顿。

    用选择器或结构化视图中的引用等待内容或加载遮罩达到所需状态。也可以等待页面加载事件、网络空闲或指定时长。

  • browser_evaluate:在当前页面执行 JavaScript,用于自定义数据提取、计算、读取应用状态或修改内容。支持异步脚本,并返回 JSON 结果。

  • browser_observe: 通过当前页面的一小段图像序列,帮助智能体理解运动和变化。
  • browser_network_requests:搜索跨标签页、页面跳转和操作的已完成浏览器请求。查找保存的 API 请求、HTTP 错误和重定向。默认隐藏静态资源,减少无关结果。失败或未完成的请求会被跳过。返回可分页的结构化数据。
  • browser_network_request:以结构化 JSON 读取已保存请求的摘要、头部信息、发送的数据或文本内容,无需再次发送请求。所选部分会完整返回。页面跳转后仍可读取已保存的响应,包括 4xx 和 5xx HTTP 响应。记录完成后才会出现在历史中。读取历史或关闭会话不会等待尚未完成的记录,它们可能在页面跳转或关闭时丢失。字节数据来自 Playwright,可能与服务器发送的原始字节不同。保存的字节是有效 UTF-8 时,使用该编码解码,与 Playwright 一致;否则使用声明的字符集。只读取和保存具有受支持的文本 Content-Type 的响应内容。二进制、缺失或未知类型只保留元数据和头部信息,不读取内容。这不会改变页面加载方式。历史记录有大小限制。可选的说明会指出内容不是文本、超过大小限制或无法读取等情况。请在关闭 MCP 会话前读取需要的数据。
  • browser_fetch:在已打开的隐身浏览器会话中获取页面,并提取 HTML、Markdown、文本或结构化视图。视图可以覆盖整页或单个元素,并包含供后续操作使用的引用、位置和尺寸。

scrapling.core.ai.server.ScraplingMCPServer

ScraplingMCPServer(executable_path=None, auth_token=None)

创建 Scrapling MCP 服务器。

参数 说明
executable_path

为浏览器工具设置兼容 Chromium 的浏览器可执行文件路径,作为全局默认值。此项可选。如未指定,则使用已设置的 SCRAPLING_EXECUTABLE_PATH 环境变量。

类型: Optional[str] 默认值: None

auth_token

可选的共享 token,客户端必须通过 Authorization: Bearer <token> 发送。如未指定,则使用已设置的 SCRAPLING_MCP_AUTH_TOKEN 环境变量。仅适用于 streamable-http 传输方式。

类型: Optional[str] 默认值: None

源代码位于 scrapling/core/ai/server.py
def __init__(self, executable_path: Optional[str] = None, auth_token: Optional[str] = None):
    """Create a Scrapling MCP server.

    :param executable_path: Optional global Chromium-compatible browser executable path for browser tools.
        If omitted, the SCRAPLING_EXECUTABLE_PATH environment variable is used when set.
    :param auth_token: Optional shared token that clients must send as `Authorization: Bearer <token>`.
        If omitted, the SCRAPLING_MCP_AUTH_TOKEN environment variable is used when set. It only applies
        to the streamable-http transport.
    """
    self._sessions: Dict[str, _SessionEntry] = {}
    self._executable_path = executable_path or environ.get(MCP_EXECUTABLE_PATH_ENV) or None
    self._auth_token = auth_token or environ.get(MCP_AUTH_TOKEN_ENV) or None

browser_open async

browser_open(
    session_id=None,
    headless=True,
    real_chrome=False,
    timezone_id=None,
    locale=None,
    useragent=None,
    proxy=None,
    cdp_url=None,
    new_context=True,
    executable_path=None,
    cookies=None,
    hide_canvas=False,
    block_webrtc=False,
    allow_webgl=True,
    additional_args=None,
)

Open a reusable local or remote browser session. Pass per-request options to browser_fetch.

源代码位于 scrapling/core/ai/server.py
async def browser_open(
    self,
    session_id: Annotated[
        Optional[str], Field(description="Custom ID; a random 12-character hex ID if omitted.")
    ] = None,
    headless: Annotated[bool, Field(description="Hide the browser window.")] = True,
    real_chrome: Annotated[bool, Field(description="Use locally installed Chrome.")] = False,
    timezone_id: Annotated[
        str | None, Field(description="Browser timezone; uses the system timezone if omitted.")
    ] = None,
    locale: Annotated[
        str | None,
        Field(description="Browser language, Accept-Language, and formatting locale; system default if omitted."),
    ] = None,
    useragent: Annotated[
        Optional[str],
        Field(description="User-Agent override; otherwise generated in headless mode, native in headful mode."),
    ] = None,
    proxy: Annotated[
        Optional[str | Dict[str, str]],
        Field(description="Proxy URL, or dictionary with server and optional username/password."),
    ] = None,
    cdp_url: Annotated[
        Optional[str],
        Field(
            description="Connect to an existing Chromium browser over CDP; browser launch settings do not apply."
        ),
    ] = None,
    new_context: Annotated[
        bool,
        Field(
            description="With cdp_url, create and close a separate context. False reuses and preserves the default context, ignoring useragent, locale, timezone_id, proxy and additional_args."
        ),
    ] = True,
    executable_path: Annotated[
        Optional[str],
        Field(description="Absolute Chromium-compatible executable path; overrides the server default."),
    ] = None,
    cookies: Annotated[
        Sequence[SetCookieParam] | None, Field(description="Initial cookies as Playwright cookie dictionaries.")
    ] = None,
    hide_canvas: Annotated[bool, Field(description="Add noise to canvas operations.")] = False,
    block_webrtc: Annotated[bool, Field(description="Disable non-proxied WebRTC UDP to reduce IP leaks.")] = False,
    allow_webgl: Annotated[bool, Field(description="Enable WebGL; disabling it can trigger bot detection.")] = True,
    additional_args: Annotated[
        Optional[Dict], Field(description="Browser context options that override Scrapling settings.")
    ] = None,
) -> SessionCreatedModel:
    """Open a reusable local or remote browser session. Pass per-request options to `browser_fetch`."""
    session_id = self._new_session_id(session_id)
    options: Dict[str, Any] = dict(
        proxy=proxy,
        locale=locale,
        cookies=cookies,
        block_ads=True,
        record_requests=True,
        useragent=useragent,
        timezone_id=timezone_id,
        additional_args=additional_args,
    )
    session = (
        AsyncRemoteSession(cdp_url, new_context=new_context, **options)
        if cdp_url is not None
        else AsyncStealthySession(
            headless=headless,
            real_chrome=real_chrome,
            executable_path=self._resolve_executable_path(executable_path),
            hide_canvas=hide_canvas,
            block_webrtc=block_webrtc,
            allow_webgl=allow_webgl,
            **options,
        )
    )
    await session.start()
    return self._register_session(session_id, session, "stealthy")

browser_save_state async

browser_save_state(session_id, path)

将浏览器 Cookie、本地存储 和 IndexedDB 保存为兼容 Playwright 的 JSON。不包含 sessionStorage。

源代码位于 scrapling/core/ai/server.py
async def browser_save_state(
    self,
    session_id: Annotated[str, Field(description="Browser session ID.")],
    path: Annotated[
        NonEmptyString,
        Field(
            description="File on the MCP server host; relative to its working directory. Overwrites the file; parent must exist."
        ),
    ],
) -> str:
    """Save browser cookies, local storage and IndexedDB as Playwright-compatible JSON. Excludes sessionStorage."""
    await self._get_session(session_id, ["stealthy"]).session.save_state(path)
    return "State saved."

browser_load_state async

browser_load_state(session_id, path)

用兼容 Playwright 的 JSON 文件中的数据替换浏览器 Cookie、本地存储 和 IndexedDB。

不更改 sessionStorage 和已打开的标签页。获取或重新加载页面后,才能使用恢复的登录状态。

源代码位于 scrapling/core/ai/server.py
async def browser_load_state(
    self,
    session_id: Annotated[str, Field(description="Browser session ID.")],
    path: Annotated[
        NonEmptyString, Field(description="File on the MCP server host; relative to its working directory.")
    ],
) -> str:
    """Replace browser cookies, local storage and IndexedDB from a Playwright-compatible JSON file.

    Leaves sessionStorage and open tabs unchanged. Fetch or reload pages to use the restored login."""
    await self._get_session(session_id, ["stealthy"]).session.load_state(path)
    return "State loaded."

open_request_session async

open_request_session(
    session_id=None, impersonate="chrome", proxy=None
)

打开供 session_make_request 使用的可复用 HTTP 会话,保留 Cookie、连接和请求设置。

源代码位于 scrapling/core/ai/server.py
async def open_request_session(
    self,
    session_id: Annotated[
        Optional[str], Field(description="Custom ID; a random 12-character hex ID if omitted.")
    ] = None,
    impersonate: Annotated[
        _MCPImpersonateType,
        Field(
            description='Browser/version to impersonate; "chrome" uses the latest supported Chrome profile. Lists pick randomly per attempt; None disables impersonation.'
        ),
    ] = "chrome",
    proxy: Annotated[
        Optional[str], Field(description="Proxy URL for all session requests, optionally including credentials.")
    ] = None,
) -> SessionCreatedModel:
    """Open a reusable HTTP session for `session_make_request`, keeping cookies, connections, and request settings."""
    session_id = self._new_session_id(session_id)
    session = FetcherSession(impersonate=impersonate, proxy=proxy)
    await session.__aenter__()
    return self._register_session(session_id, session, "static")

close_session async

close_session(session_id)

关闭会话并释放其资源。

源代码位于 scrapling/core/ai/server.py
async def close_session(
    self,
    session_id: Annotated[str, Field(description="Session ID to close.")],
) -> SessionClosedModel:
    """Close a session and free its resources."""
    entry = self._sessions.pop(session_id, None)
    if entry is None:
        raise ValueError(f"Session '{session_id}' not found. Use list_sessions to see active sessions.")

    if entry.session_type == "static":
        await entry.session.__aexit__(None, None, None)
    else:
        await entry.session.close()
    return SessionClosedModel(
        session_id=session_id,
        message=f"Session '{session_id}' closed successfully.",
    )

list_sessions async

list_sessions()

列出已注册的会话、它们的状态和实际生效的设置。

源代码位于 scrapling/core/ai/server.py
async def list_sessions(self) -> List[SessionInfo]:
    """List registered sessions, their status, and effective settings."""
    return [
        SessionInfo(
            session_id=sid,
            session_type=entry.session_type,
            created_at=entry.created_at,
            is_alive=entry.session._is_alive,
            settings=_session_settings(entry.session),
        )
        for sid, entry in self._sessions.items()
    ]

browser_network_requests async

browser_network_requests(
    session_id,
    url_pattern=None,
    include_static=False,
    after_id=0,
    limit=50,
)

List completed requests across page changes, with IDs and pagination. Recording starts when the browser session opens and closing does not wait for running captures.

源代码位于 scrapling/core/ai/server.py
async def browser_network_requests(
    self,
    session_id: Annotated[str, Field(description="Browser session ID.")],
    url_pattern: Annotated[Optional[str], Field(description="Filter URLs by regular expression.")] = None,
    include_static: Annotated[bool, Field(description="Include successful non-API requests too.")] = False,
    after_id: Annotated[
        int, Field(ge=0, description="Return requests with a higher ID; use next_cursor for more.")
    ] = 0,
    limit: Annotated[int, Field(ge=1, le=200, description="Maximum requests to return.")] = 50,
) -> NetworkRequestsModel:
    """List completed requests across page changes, with IDs and pagination. Recording starts when the browser session opens and closing does not wait for running captures."""
    network = self._get_session(session_id, ["stealthy"]).session.network
    records = network.search(
        url_pattern=url_pattern, include_static=include_static, after_id=after_id, limit=limit + 1
    )
    shown = records[:limit]
    return NetworkRequestsModel(
        requests=[NetworkRequestInfo.model_validate(record, from_attributes=True) for record in shown],
        next_cursor=shown[-1].meta["network_id"] if shown else max(after_id, network.last_id),
        has_more=len(records) > limit,
        dropped_count=network.dropped_count,
    )

browser_network_request async

browser_network_request(
    session_id, request_id, part="summary"
)

完整读取已保存请求的某一部分,无需重新发送请求,并返回结构化数据和可用性说明。

源代码位于 scrapling/core/ai/server.py
async def browser_network_request(
    self,
    session_id: Annotated[str, Field(description="Browser session ID.")],
    request_id: Annotated[int, Field(ge=1, description="ID from `browser_network_requests`.")],
    part: Annotated[
        NetworkPart, Field(description="Request summary, headers, or text body; incomplete headers are marked.")
    ] = "summary",
) -> NetworkRequestModel:
    """Read a complete saved request part without resending it, with structured data and availability notes."""
    network = self._get_session(session_id, ["stealthy"]).session.network
    record = network.get(request_id)
    if record is None:
        raise ValueError(
            f"Request {request_id} was not found or is no longer retained. Use browser_network_requests."
        )
    return _request_details(record, part)

browser_extract async

browser_extract(
    session_id,
    extraction_type="snapshot",
    target=None,
    depth=None,
    boxes=True,
    search=None,
    main_content_only=True,
)

无需导航,即可以快照、HTML、Markdown 或文本形式读取当前页面或某个元素。返回纯文本;快照搜索会保留引用、祖先路径和附近三行上下文。

源代码位于 scrapling/core/ai/server.py
async def browser_extract(
    self,
    session_id: Annotated[str, Field(description="Browser session ID")],
    extraction_type: Annotated[
        SessionExtractionType, Field(description="Output format; defaults to an AI ARIA snapshot.")
    ] = "snapshot",
    target: Annotated[
        Optional[NonEmptyString],
        Field(description="Playwright selector or aria-ref=<ref> matching one element; omitted reads the page."),
    ] = None,
    depth: Annotated[
        Optional[int],
        Field(description="Snapshot depth before search; unlimited if omitted. Ignored for other formats."),
    ] = None,
    boxes: Annotated[
        bool, Field(description="Include viewport bounding boxes in snapshots; ignored for other formats.")
    ] = True,
    search: Annotated[
        Optional[NonEmptyString],
        Field(description="Snapshot-only Python regex; case-sensitive unless (?i). No matches returns empty text."),
    ] = None,
    main_content_only: Annotated[
        bool, Field(description="Clean body/target content for AI; ignored for snapshots.")
    ] = True,
) -> str:
    """Read the current page or one element as snapshot, HTML, Markdown, or text without navigating.
    Return plain text; snapshot search keeps refs, ancestor paths and three lines of nearby context."""
    pattern = None
    if search is not None:
        if extraction_type != "snapshot":
            raise ValueError("search requires snapshot output.")
        try:
            pattern = re.compile(search)
        except re.error as error:
            raise ValueError(f"Invalid search regex: {error}") from error
    if extraction_type == "snapshot":
        snapshot = await self._browser_snapshot(session_id, css_selector=target, depth=depth, boxes=boxes)
        return _search_snapshot(snapshot, pattern) if pattern is not None else snapshot
    with self._browser_page(session_id) as (_, page):
        content = (
            await page.locator(target).evaluate("(element) => element.outerHTML")
            if target is not None
            else await ResponseFactory._get_async_page_content(page)
        )
        return _CONTROL_CHARS_PATTERN.sub(
            "",
            "".join(
                Convertor._extract_content(
                    Selector(content=content, url=page.url),
                    extraction_type=extraction_type,
                    main_content_only=main_content_only,
                )
            ),
        )

browser_actions async

browser_actions(session_id, actions, slowly=False)

Chain all kinds of browser actions. Do actions without multiple calls or plan ahead to execute a sequence of actions. Run mouse, field, key, dialog, and wait actions on the current page in order. Load waits observe the current document; wait for a result element for delayed navigation. Stops on the first error, identifying its action; partial effects remain. No retries. Element mouse targets auto-wait and scroll into view; coordinates and wheel do not wait for navigation or scrolling. mouse_down holds across calls for observation and further moves; mouse_up releases, including after errors. For drag and drop, move over the target twice before releasing. Cancelled/timed-out clicks attempt release.

源代码位于 scrapling/core/ai/server.py
async def browser_actions(
    self,
    session_id: Annotated[str, Field(description="Browser session ID")],
    actions: Annotated[
        List[BrowserAction],
        Field(
            min_length=1,
            description="Ordered actions; fields need target; move/click need target or viewport (x, y); mouse_down/mouse_up act at the current pointer.",
        ),
    ],
    slowly: Annotated[
        bool,
        Field(
            description="Fresh random delays: 50-150 ms per character, 100-300 ms between all actions, including waits."
        ),
    ] = False,
) -> str:
    """Chain all kinds of browser actions. Do actions without multiple calls or plan ahead to execute a sequence of actions.
    Run mouse, field, key, dialog, and wait actions on the current page in order.
    Load waits observe the current document; wait for a result element for delayed navigation.
    Stops on the first error, identifying its action; partial effects remain. No retries.
    Element mouse targets auto-wait and scroll into view; coordinates and wheel do not wait for navigation or scrolling.
    mouse_down holds across calls for observation and further moves; mouse_up releases, including after errors.
    For drag and drop, move over the target twice before releasing. Cancelled/timed-out clicks attempt release."""
    _validate_actions(actions)
    with self._browser_page(session_id) as (_, page):
        await _run_actions(page, actions, slowly)
    return "Actions completed."

browser_evaluate async

browser_evaluate(
    session_id, expression, arg=None, isolated_context=True
)

在当前页面运行 JavaScript,等待 Promise 完成,并返回 JSON 文本,不附带快照。脚本应返回兼容 JSON 的值;null/undefined 会变为 null。失败时不会撤销脚本已产生的效果。取消操作会停止等待,但可能不会停止脚本。

源代码位于 scrapling/core/ai/server.py
async def browser_evaluate(
    self,
    session_id: Annotated[str, Field(description="Browser session ID")],
    expression: Annotated[NonEmptyString, Field(description="JavaScript expression or function to invoke.")],
    arg: Annotated[
        Optional[Dict[str, Any]],
        Field(description="JSON object passed to the function; use properties for scalar or array values."),
    ] = None,
    isolated_context: Annotated[
        bool,
        Field(
            description="False accesses the page's own JS variables; True uses a separate context with the same DOM."
        ),
    ] = True,
) -> str:
    """Run JavaScript on the current page, await promises, and return JSON text without a snapshot.
    Return JSON-compatible values; null/undefined become null. Script effects are not undone on failure.
    Cancellation stops waiting, but may not stop the script."""
    with self._browser_page(session_id) as (_, page):
        return dumps(
            await page.evaluate(expression, arg=arg, isolated_context=isolated_context),
            ensure_ascii=False,
            allow_nan=False,
            separators=(",", ":"),
        )

browser_screenshot async

browser_screenshot(
    session_id,
    image_type="png",
    full_page=False,
    quality=None,
    timeout=30000,
    selector=None,
    ref=None,
)

无需导航,即可截取当前页面或某个元素;返回图片和当前 URL。截取元素时,会等待元素并将其滚动到可见区域。截图前如需其他等待,请使用 browser_actions。

源代码位于 scrapling/core/ai/server.py
async def browser_screenshot(
    self,
    session_id: Annotated[str, Field(description="Browser session ID")],
    image_type: Annotated[ScreenshotType, Field(description="Image format.")] = "png",
    full_page: Annotated[
        bool, Field(description="Capture the full scrollable page; incompatible with selector/ref.")
    ] = False,
    quality: Annotated[
        Optional[Annotated[int, Field(ge=0, le=100)]], Field(description="JPEG quality, 0-100; invalid for PNG.")
    ] = None,
    timeout: Annotated[
        NonNegativeFiniteFloat, Field(description="Capture limit in milliseconds; 0 disables it.")
    ] = 30000,
    selector: Annotated[
        Optional[NonEmptyString],
        Field(description="Playwright selector matching one element; omit when using ref."),
    ] = None,
    ref: Annotated[
        Optional[NonEmptyString],
        Field(description="Current snapshot element reference; omit both targets for page capture."),
    ] = None,
) -> List[ImageContent | TextContent]:
    """Capture the current page or one element without navigating; return the image and current URL.
    Element capture waits and scrolls into view. Use browser_actions for other waits before capture."""
    if quality is not None and image_type != "jpeg":
        raise ValueError("'quality' is only valid when 'image_type' is 'jpeg'.")
    if selector is not None and ref is not None:
        raise ValueError("Provide either 'selector' or 'ref', not both.")
    if full_page and (selector is not None or ref is not None):
        raise ValueError("'full_page' cannot be combined with 'selector' or 'ref'.")

    with self._browser_page(session_id) as (_, page):
        options: Dict[str, Any] = {"type": image_type, "quality": quality, "timeout": timeout}
        if selector is not None or ref is not None:
            started = monotonic()
            element = await page.locator(selector or f"aria-ref={ref}").element_handle(timeout=timeout)
            try:
                options["timeout"] = max(1, timeout - (monotonic() - started) * 1000) if timeout else 0
                data = await element.screenshot(**options)
            except BaseException as exc:
                try:
                    with CancelScope(shield=True):
                        await element.dispose()
                except Exception as cleanup_error:
                    raise exc from cleanup_error
                raise
            with CancelScope(shield=True):
                await element.dispose()
        else:
            data = await page.screenshot(full_page=full_page, **options)
        return [Image(data=data, format=image_type).to_image_content(), TextContent(type="text", text=page.url)]

browser_observe async

browser_observe(
    session_id, duration_ms=1500, max_frames=12
)

Inspect browser page motion through a short sequence of timestamped viewport images. The page keeps running during and after capture. Use this tool when you get any challenge that require timed actions like games, appearing/disappearing CAPTCHAs, or interacting with any moving elements.

源代码位于 scrapling/core/ai/server.py
async def browser_observe(
    self,
    session_id: Annotated[str, Field(description="Browser session ID")],
    duration_ms: Annotated[int, Field(ge=100, le=5000, description="Observation window in milliseconds.")] = 1500,
    max_frames: Annotated[int, Field(ge=2, le=20, description="Maximum number of sampled frames.")] = 12,
) -> List[ImageContent | TextContent]:
    """Inspect browser page motion through a short sequence of timestamped viewport images. The page keeps running during and after capture.
    Use this tool when you get any challenge that require timed actions like games, appearing/disappearing CAPTCHAs, or interacting with any moving elements.
    """
    with self._browser_page(session_id) as (_, page):
        frames: Dict[int, Dict[str, Any]] = {}
        started = monotonic()

        def capture(frame: Dict[str, Any]) -> None:
            elapsed = (monotonic() - started) * 1000
            if elapsed < duration_ms:
                frames[int(elapsed * max_frames / duration_ms)] = frame

        try:
            await page.screencast.start(on_frame=capture, quality=60, size={"width": 800, "height": 800})
            await sleep(max(0, duration_ms / 1000 - (monotonic() - started)))
        except BaseException as error:
            try:
                await _stop_screencast(page)
            except Exception as cleanup_error:
                raise error from cleanup_error
            raise
        await _stop_screencast(page)
        if not frames:
            raise RuntimeError("No frames were captured. Try browser_screenshot or a longer observation window.")
        return [
            content
            for frame in frames.values()
            for content in (
                TextContent(
                    type="text",
                    text=dumps(
                        {
                            "timestamp_ms": frame["timestamp"],
                            "viewport_width": frame["viewportWidth"],
                            "viewport_height": frame["viewportHeight"],
                        },
                        separators=(",", ":"),
                    ),
                ),
                Image(data=frame["data"], format="jpeg").to_image_content(),
            )
        ]

make_request async staticmethod

make_request(
    url,
    method="GET",
    impersonate="chrome",
    extraction_type="markdown",
    css_selector=None,
    main_content_only=True,
    params=None,
    data=None,
    json=None,
    headers=None,
    cookies=None,
    timeout=30,
    follow_redirects="safe",
    max_redirects=30,
    retries=3,
    retry_delay=1,
    proxy=None,
    proxy_auth=None,
    auth=None,
    verify=True,
    http3=False,
    stealthy_headers=True,
)

发送不执行 JavaScript 的 HTTP 请求。适用于防护程度低到中等的网站。

源代码位于 scrapling/core/ai/server.py
@staticmethod
async def make_request(
    url: Annotated[str, Field(description="URL to fetch.")],
    method: Annotated[SUPPORTED_HTTP_METHODS, Field(description="HTTP method.")] = "GET",
    impersonate: Annotated[
        _MCPImpersonateType,
        Field(
            description='Browser/version to impersonate; "chrome" uses the latest supported Chrome profile. Lists pick randomly per attempt; None disables impersonation.'
        ),
    ] = "chrome",
    extraction_type: Annotated[extraction_types, Field(description="Content output format.")] = "markdown",
    css_selector: Annotated[
        Optional[str], Field(description="Select matching elements after `main_content_only` filtering.")
    ] = None,
    main_content_only: Annotated[
        bool, Field(description="Sanitize <body> content before selection; False uses the full document.")
    ] = True,
    params: Annotated[Optional[Dict], Field(description="Query parameters.")] = None,
    data: Annotated[Optional[Dict[str, str] | str], Field(description="Form or raw body; ignored for GET.")] = None,
    json: Annotated[Optional[Dict | List], Field(description="JSON body; ignored for GET.")] = None,
    headers: Annotated[Optional[Mapping[str, Optional[str]]], Field(description="Request headers.")] = None,
    cookies: Annotated[Optional[Dict[str, str]], Field(description="Request cookies.")] = None,
    timeout: Annotated[Optional[int | float], Field(description="Timeout in seconds.")] = 30,
    follow_redirects: Annotated[
        FollowRedirects,
        Field(
            description='"safe" blocks redirects to private/internal IPs; True allows all; False disables redirects.'
        ),
    ] = "safe",
    max_redirects: Annotated[int, Field(description="Redirect limit; -1 means unlimited.")] = 30,
    retries: Annotated[
        Optional[int], Field(description="Maximum attempts, including the first; None or values below 1 send once.")
    ] = 3,
    retry_delay: Annotated[Optional[int], Field(description="Seconds between retries.")] = 1,
    proxy: Annotated[Optional[str], Field(description="Proxy URL, optionally including credentials.")] = None,
    proxy_auth: Annotated[
        Optional[Dict[str, str]], Field(description='Proxy basic auth: {"username": ..., "password": ...}.')
    ] = None,
    auth: Annotated[
        Optional[Dict[str, str]], Field(description='Basic auth: {"username": ..., "password": ...}.')
    ] = None,
    verify: Annotated[Optional[bool], Field(description="Verify TLS certificates.")] = True,
    http3: Annotated[
        Optional[bool], Field(description="Use HTTP/3; may conflict with browser impersonation.")
    ] = False,
    stealthy_headers: Annotated[
        Optional[bool],
        Field(description="Fill missing browser headers and use a Google referer if none was supplied."),
    ] = True,
) -> ResponseModel:
    """Make an HTTP request without JavaScript. Suitable for low-to-mid protection."""
    normalized_proxy_auth = _normalize_credentials(proxy_auth)
    normalized_auth = _normalize_credentials(auth)

    request_kwargs: Dict[str, Any] = dict(
        auth=normalized_auth,
        proxy=proxy,
        http3=http3,
        verify=verify,
        params=params,
        headers=headers,
        cookies=cookies,
        timeout=timeout,
        retries=retries,
        proxy_auth=normalized_proxy_auth,
        retry_delay=retry_delay,
        impersonate=impersonate,
        max_redirects=max_redirects,
        follow_redirects=follow_redirects,
        stealthy_headers=stealthy_headers,
    )
    if method != "GET":
        request_kwargs.update(data=data, json=json)

    async with FetcherSession() as session:
        page = await getattr(session, method.lower())(url, **request_kwargs)
        return _translate_response(page, extraction_type, css_selector, main_content_only)

browser_fetch_once async

browser_fetch_once(
    url,
    extraction_type="markdown",
    css_selector=None,
    main_content_only=True,
    headless=True,
    google_search=True,
    real_chrome=False,
    wait=0,
    proxy=None,
    timezone_id=None,
    locale=None,
    extra_headers=None,
    useragent=None,
    hide_canvas=False,
    executable_path=None,
    timeout=30000,
    disable_resources=False,
    wait_selector=None,
    cookies=None,
    network_idle=False,
    wait_selector_state="attached",
    block_webrtc=False,
    allow_webgl=True,
    solve_cloudflare=False,
    additional_args=None,
    pierce_shadow=False,
)

通过隐身浏览器获取页面,并执行 JavaScript 渲染。

源代码位于 scrapling/core/ai/server.py
async def browser_fetch_once(
    self,
    url: Annotated[str, Field(description="URL to fetch.")],
    extraction_type: Annotated[extraction_types, Field(description="Content output format.")] = "markdown",
    css_selector: Annotated[
        Optional[str], Field(description="Select matching elements after `main_content_only` filtering.")
    ] = None,
    main_content_only: Annotated[
        bool, Field(description="Sanitize <body> content before selection; False uses the full document.")
    ] = True,
    headless: Annotated[bool, Field(description="Hide the browser window.")] = True,
    google_search: Annotated[
        bool, Field(description="Set a Google referer, overriding any supplied referer.")
    ] = True,
    real_chrome: Annotated[bool, Field(description="Use locally installed Chrome.")] = False,
    wait: Annotated[
        int | float, Field(description="Extra milliseconds after the page is ready, before returning.")
    ] = 0,
    proxy: Annotated[
        Optional[str | Dict[str, str]],
        Field(description="Proxy URL, or dictionary with server and optional username/password."),
    ] = None,
    timezone_id: Annotated[
        str | None, Field(description="Browser timezone; uses the system timezone if omitted.")
    ] = None,
    locale: Annotated[
        str | None,
        Field(description="Browser language, Accept-Language, and formatting locale; system default if omitted."),
    ] = None,
    extra_headers: Annotated[Optional[Dict[str, str]], Field(description="Additional request headers.")] = None,
    useragent: Annotated[
        Optional[str],
        Field(description="User-Agent override; otherwise generated in headless mode, native in headful mode."),
    ] = None,
    hide_canvas: Annotated[bool, Field(description="Add noise to canvas operations.")] = False,
    executable_path: Annotated[
        Optional[str],
        Field(description="Absolute Chromium-compatible executable path; overrides the server default."),
    ] = None,
    timeout: Annotated[
        int | float, Field(description="Navigation and page-operation timeout in milliseconds.")
    ] = 30000,
    disable_resources: Annotated[
        bool,
        Field(
            description="Block font, image, media, beacon, object, imageset, texttrack, websocket, csp_report, and stylesheet requests."
        ),
    ] = False,
    wait_selector: Annotated[
        Optional[str], Field(description="Wait for the first CSS match; continue if the wait fails.")
    ] = None,
    cookies: Annotated[
        Sequence[SetCookieParam] | None, Field(description="Initial cookies as Playwright cookie dictionaries.")
    ] = None,
    network_idle: Annotated[
        bool, Field(description="Try to wait for 500 ms without network activity; continue if the wait fails.")
    ] = False,
    wait_selector_state: Annotated[
        SelectorWaitStates, Field(description="Target state of `wait_selector`.")
    ] = "attached",
    block_webrtc: Annotated[bool, Field(description="Disable non-proxied WebRTC UDP to reduce IP leaks.")] = False,
    allow_webgl: Annotated[bool, Field(description="Enable WebGL; disabling it can trigger bot detection.")] = True,
    solve_cloudflare: Annotated[
        bool, Field(description="Attempt to solve Cloudflare Turnstile/interstitial challenges before returning.")
    ] = False,
    additional_args: Annotated[
        Optional[Dict], Field(description="Browser context options that override Scrapling settings.")
    ] = None,
    pierce_shadow: Annotated[bool, Field(description="Include open Shadow DOM content.")] = False,
) -> ResponseModel:
    """Fetch a page with a stealth browser and JavaScript rendering."""
    async with AsyncStealthySession(
        wait=wait,
        proxy=proxy,
        locale=locale,
        timeout=timeout,
        cookies=cookies,
        headless=headless,
        block_ads=True,
        max_pages=1,
        useragent=useragent,
        timezone_id=timezone_id,
        real_chrome=real_chrome,
        hide_canvas=hide_canvas,
        allow_webgl=allow_webgl,
        network_idle=network_idle,
        pierce_shadow=pierce_shadow,
        block_webrtc=block_webrtc,
        wait_selector=wait_selector,
        google_search=google_search,
        extra_headers=extra_headers,
        executable_path=self._resolve_executable_path(executable_path),
        additional_args=additional_args,
        solve_cloudflare=solve_cloudflare,
        disable_resources=disable_resources,
        wait_selector_state=wait_selector_state,
    ) as session:
        page = await session.fetch(url)

    return _translate_response(page, extraction_type, css_selector, main_content_only)

browser_fetch async

browser_fetch(
    url,
    session_id,
    extraction_type="markdown",
    css_selector=None,
    main_content_only=True,
    wait=0,
    timeout=30000,
    google_search=True,
    network_idle=False,
    load_dom=True,
    disable_resources=False,
    wait_selector=None,
    wait_selector_state="attached",
    extra_headers=None,
    blocked_domains=None,
    solve_cloudflare=False,
    pierce_shadow=False,
)

通过已打开的隐身浏览器会话获取一个 URL。选项仅对本次请求生效。

源代码位于 scrapling/core/ai/server.py
async def browser_fetch(
    self,
    url: Annotated[str, Field(description="URL to fetch.")],
    session_id: Annotated[str, Field(description="Browser session ID.")],
    extraction_type: Annotated[
        SessionExtractionType, Field(description="Content output format; snapshots include element bounding boxes.")
    ] = "markdown",
    css_selector: Annotated[
        Optional[str],
        Field(description="Select after `main_content_only` filtering; snapshots require exactly one match."),
    ] = None,
    main_content_only: Annotated[
        bool,
        Field(description="Sanitize <body> before selection; False uses the full document. Ignored for snapshots."),
    ] = True,
    wait: Annotated[
        int | float, Field(description="Extra milliseconds after the page is ready, before returning.")
    ] = 0,
    timeout: Annotated[
        int | float, Field(description="Navigation and page-operation timeout in milliseconds.")
    ] = 30000,
    google_search: Annotated[
        bool, Field(description="Set a Google referer, overriding any supplied referer.")
    ] = True,
    network_idle: Annotated[
        bool, Field(description="Try to wait for 500 ms without network activity; continue if the wait fails.")
    ] = False,
    load_dom: Annotated[bool, Field(description="Wait for DOMContentLoaded.")] = True,
    disable_resources: Annotated[
        bool,
        Field(
            description="Block font, image, media, beacon, object, imageset, texttrack, websocket, csp_report, and stylesheet requests."
        ),
    ] = False,
    wait_selector: Annotated[
        Optional[str], Field(description="Wait for the first CSS match; continue if the wait fails.")
    ] = None,
    wait_selector_state: Annotated[
        SelectorWaitStates, Field(description="Target state of `wait_selector`.")
    ] = "attached",
    extra_headers: Annotated[Optional[Dict[str, str]], Field(description="Additional request headers.")] = None,
    blocked_domains: Annotated[
        Optional[Set[str]], Field(description="Block requests to these domains and their subdomains.")
    ] = None,
    solve_cloudflare: Annotated[
        bool, Field(description="Attempt to solve Cloudflare Turnstile/interstitial challenges.")
    ] = False,
    pierce_shadow: Annotated[
        bool, Field(description="Include open Shadow DOM content; ignored for snapshots.")
    ] = False,
) -> ResponseModel:
    """Fetch a URL in an open stealthy browser session. Options apply only to this request."""
    entry = self._get_session(session_id, expected_type=["stealthy"])
    fetch_params = dict(
        wait=wait,
        timeout=timeout,
        google_search=google_search,
        network_idle=network_idle,
        pierce_shadow=pierce_shadow,
        load_dom=load_dom,
        disable_resources=disable_resources,
        wait_selector=wait_selector,
        wait_selector_state=wait_selector_state,
        extra_headers=extra_headers,
        blocked_domains=blocked_domains,
        solve_cloudflare=solve_cloudflare,
    )
    page = await entry.session.fetch(
        url, **{name: value for name, value in fetch_params.items() if name in _STEALTH_FETCH_KEYS}
    )
    if extraction_type == "snapshot":
        return ResponseModel(
            status=page.status, content=[await self._browser_snapshot(session_id, css_selector)], url=page.url
        )
    return _translate_response(page, extraction_type, css_selector, main_content_only)

session_make_request async

session_make_request(
    url,
    session_id,
    method="GET",
    extraction_type="markdown",
    css_selector=None,
    main_content_only=True,
    params=None,
    data=None,
    json=None,
    headers=None,
    cookies=None,
    timeout=30,
    follow_redirects="safe",
    max_redirects=30,
    retries=3,
    retry_delay=1,
    auth=None,
    verify=True,
    http3=False,
    stealthy_headers=True,
)

使用会话的 Cookie、连接、浏览器模拟和代理发送 HTTP 请求。选项对本次请求生效。

源代码位于 scrapling/core/ai/server.py
async def session_make_request(
    self,
    url: Annotated[str, Field(description="URL to fetch.")],
    session_id: Annotated[str, Field(description="HTTP session ID from `open_request_session`.")],
    method: Annotated[SUPPORTED_HTTP_METHODS, Field(description="HTTP method.")] = "GET",
    extraction_type: Annotated[extraction_types, Field(description="Content output format.")] = "markdown",
    css_selector: Annotated[
        Optional[str], Field(description="Select matching elements after `main_content_only` filtering.")
    ] = None,
    main_content_only: Annotated[
        bool, Field(description="Sanitize <body> content before selection; False uses the full document.")
    ] = True,
    params: Annotated[Optional[Dict], Field(description="Query parameters.")] = None,
    data: Annotated[Optional[Dict[str, str] | str], Field(description="Form or raw body; ignored for GET.")] = None,
    json: Annotated[Optional[Dict | List], Field(description="JSON body; ignored for GET.")] = None,
    headers: Annotated[Optional[Mapping[str, Optional[str]]], Field(description="Request headers.")] = None,
    cookies: Annotated[Optional[Dict[str, str]], Field(description="Request cookies.")] = None,
    timeout: Annotated[Optional[int | float], Field(description="Timeout in seconds.")] = 30,
    follow_redirects: Annotated[
        FollowRedirects,
        Field(
            description='"safe" blocks redirects to private/internal IPs; True allows all; False disables redirects.'
        ),
    ] = "safe",
    max_redirects: Annotated[int, Field(description="Redirect limit; -1 means unlimited.")] = 30,
    retries: Annotated[
        Optional[int], Field(description="Maximum attempts, including the first; None or values below 1 send once.")
    ] = 3,
    retry_delay: Annotated[Optional[int], Field(description="Seconds between retries.")] = 1,
    auth: Annotated[
        Optional[Dict[str, str]], Field(description='Basic auth: {"username": ..., "password": ...}.')
    ] = None,
    verify: Annotated[Optional[bool], Field(description="Verify TLS certificates.")] = True,
    http3: Annotated[
        Optional[bool], Field(description="Use HTTP/3; may conflict with browser impersonation.")
    ] = False,
    stealthy_headers: Annotated[
        Optional[bool],
        Field(description="Fill missing browser headers and use a Google referer if none was supplied."),
    ] = True,
) -> ResponseModel:
    """Make an HTTP request using the session's cookies, connections, impersonation, and proxy. Options apply to this request."""
    entry = self._get_session(session_id, expected_type=["static"])

    request_kwargs: Dict[str, Any] = dict(
        auth=_normalize_credentials(auth),
        http3=http3,
        verify=verify,
        params=params,
        headers=headers,
        cookies=cookies,
        timeout=timeout,
        retries=retries,
        retry_delay=retry_delay,
        max_redirects=max_redirects,
        follow_redirects=follow_redirects,
        stealthy_headers=stealthy_headers,
    )
    if method != "GET":
        request_kwargs.update(data=data, json=json)

    page = await getattr(entry.session._client, method.lower())(url, **request_kwargs)
    return _translate_response(page, extraction_type, css_selector, main_content_only)

serve

serve(
    http,
    host,
    port,
    allowed_hosts=(),
    allow_unauthenticated=False,
)

启动 MCP 服务器。

参数 说明
http

使用 streamable-http 传输方式提供服务,替代 stdio。

类型: bool

host

启用 http 时绑定的主机地址。

类型: str

port

启用 http 时绑定的端口。

类型: int

allowed_hosts

允许的主机名,设置后会启用 DNS 重绑定防护。

类型: Sequence[str] 默认值: ()

allow_unauthenticated

不使用 token 启动 streamable-http。该传输方式默认要求身份验证,此选项用于明确关闭身份验证。

类型: bool 默认值: False

源代码位于 scrapling/core/ai/server.py
def serve(
    self,
    http: bool,
    host: str,
    port: int,
    allowed_hosts: Sequence[str] = (),
    allow_unauthenticated: bool = False,
):
    """Serve the MCP server.

    :param http: Serve over the streamable-http transport instead of stdio.
    :param host: The host to bind to when `http` is enabled.
    :param port: The port to bind to when `http` is enabled.
    :param allowed_hosts: Host names to accept, which turns on DNS-rebinding protection.
    :param allow_unauthenticated: Start the streamable-http transport without a token. The transport
        requires authentication by default, so this is the explicit opt-out.
    """
    if not http:
        if self._auth_token:
            log.warning(
                "The authentication token only applies to the streamable-http transport, so it's ignored with stdio."
            )
    elif self._auth_token:
        if allow_unauthenticated:
            log.warning(
                "An authentication token was given, so it takes precedence and the server still requires it."
            )
    elif not allow_unauthenticated:
        raise ValueError(
            f"Refusing to serve the MCP server over HTTP without authentication because anyone who can reach "
            f"{host}:{port} would be able to use every tool, including fetching arbitrary URLs from this machine. "
            f"Pass `--auth-token` (or set the {MCP_AUTH_TOKEN_ENV} environment variable) to require a bearer "
            f"token, or `--no-auth` to serve it unauthenticated anyway."
        )
    else:
        log.warning(
            f"The MCP server is running over HTTP without authentication, so anyone who can reach "
            f"{host}:{port} can use every tool, including fetching arbitrary URLs from this machine."
        )

    server = self._build_server(host, port)
    if http:
        server.run(
            transport="streamable-http",
            host=host,
            port=port,
            transport_security=self._transport_security(allowed_hosts),
        )
    else:
        server.run()