Skip to content

Commit 4427393

Browse files
authored
Add sync and async session WebMCP support (#137)
* Add sync and async session WebMCP support * Bump SDK version to 1.11.0
1 parent 674e14c commit 4427393

18 files changed

Lines changed: 826 additions & 6 deletions

File tree

‎README.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,40 @@ Profile names must match `^[A-Za-z0-9._-]+$`.
3434

3535
`base_url` and `HYPERBROWSER_BASE_URL` accept either `https://host` or `https://host/api`. The client normalizes both to the same control-plane base URL.
3636

37+
## WebMCP page tools
38+
39+
Create a session with `{"enable_web_mcp": True}` (default: false), then navigate
40+
to a page that exposes WebMCP tools using Playwright. With that session open:
41+
42+
```python
43+
discovery = client.sessions.webmcp.list_tools(session.id)
44+
# Select a tool using its name, source page/frame, and input schema.
45+
tool = next(tool for tool in discovery.tools if tool.name == "search")
46+
result = client.sessions.webmcp.invoke(session.id, {
47+
"tool_ref": tool.tool_ref,
48+
"input": {"query": "flights to Tokyo"}, # Match the selected tool's schema.
49+
})
50+
print(result.status, result.output)
51+
```
52+
53+
For long-running tools or forms awaiting human submission, use
54+
`client.sessions.webmcp.start(session_id, params)`, then
55+
`client.sessions.webmcp.get_result(session_id, invocation_id, {"wait_seconds": 30})`
56+
until the status is terminal. `client.sessions.webmcp.cancel(session_id, invocation_id)`
57+
requests best-effort cancellation. Blocking invocation defaults to 60 seconds
58+
(maximum 120); `start` defaults to 300 seconds (maximum 3,600) via `timeout_seconds`.
59+
All five methods also work with `AsyncHyperbrowser` by adding `await`.
60+
61+
Discover tools again after navigation. Invocations never automatically retry;
62+
losing a response can leave an unknown outcome. Handles are ephemeral and
63+
terminal results expire after 10 minutes or earlier eviction. Check result
64+
status and truncation metadata before using output. Tool input and output keys
65+
are not renamed. Request TypedDicts are available in `hyperbrowser.types`, and
66+
Pydantic request/response models in `hyperbrowser.models`.
67+
68+
See the [WebMCP guide](https://hyperbrowser.ai/docs/sessions/webmcp) for complete
69+
examples, cancellation behavior, human submission, and limits.
70+
3771
## Usage
3872

3973
Hyperbrowser 1.0 accepts plain dictionaries for request parameters. Method

‎hyperbrowser/client/managers/async_manager/session.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import warnings
2+
3+
from .webmcp import WebMCPManager
24
from collections.abc import Mapping
35
from typing import IO, List, Optional, Union, overload
46

@@ -68,6 +70,7 @@ class SessionManager:
6870
def __init__(self, client):
6971
self._client = client
7072
self.event_logs = SessionEventLogsManager(client)
73+
self.webmcp = WebMCPManager(client)
7174

7275
async def create(
7376
self,
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from typing import Optional, Union
2+
from urllib.parse import quote
3+
4+
from hyperbrowser.client._request import coerce_request, dump_request
5+
from hyperbrowser.models.webmcp import (
6+
WebMCPInvocation,
7+
WebMCPInvokeParams,
8+
WebMCPInvokeResult,
9+
WebMCPResultParams,
10+
WebMCPStartParams,
11+
WebMCPToolsResponse,
12+
)
13+
from hyperbrowser.types.webmcp import (
14+
WebMCPInvokeParams as WebMCPInvokeParamsDict,
15+
WebMCPResultParams as WebMCPResultParamsDict,
16+
WebMCPStartParams as WebMCPStartParamsDict,
17+
)
18+
19+
20+
class WebMCPManager:
21+
"""Discover and invoke page tools in sessions created with enable_web_mcp=True."""
22+
23+
def __init__(self, client):
24+
self._client = client
25+
26+
def _url(self, session_id: str, suffix: str) -> str:
27+
return self._client._build_url(
28+
f"/session/{quote(session_id, safe='')}/webmcp{suffix}"
29+
)
30+
31+
async def list_tools(self, session_id: str) -> WebMCPToolsResponse:
32+
"""Discover document-bound tools across the session's tabs and frames."""
33+
response = await self._client.transport.get(
34+
self._url(session_id, "/tools"), timeout=max(self._client.timeout, 40)
35+
)
36+
return WebMCPToolsResponse(**response.data)
37+
38+
async def invoke(
39+
self,
40+
session_id: str,
41+
params: Union[WebMCPInvokeParamsDict, WebMCPInvokeParams],
42+
) -> WebMCPInvokeResult:
43+
"""Invoke once and wait. A form can return awaiting_submission with a handle.
44+
45+
This POST is never automatically retried: losing its response can leave the
46+
outcome unknown, and repeating it could duplicate a side effect.
47+
"""
48+
request = coerce_request(params, WebMCPInvokeParams)
49+
response = await self._client.transport.post(
50+
self._url(session_id, "/invoke"),
51+
data=dump_request(request, WebMCPInvokeParams),
52+
timeout=max(self._client.timeout, request.timeout_seconds + 35),
53+
)
54+
return WebMCPInvokeResult(**response.data)
55+
56+
async def start(
57+
self,
58+
session_id: str,
59+
params: Union[WebMCPStartParamsDict, WebMCPStartParams],
60+
) -> WebMCPInvocation:
61+
"""Start once and return a handle; use get_result to observe its outcome."""
62+
response = await self._client.transport.post(
63+
self._url(session_id, "/invocations"),
64+
data=dump_request(params, WebMCPStartParams),
65+
timeout=max(self._client.timeout, 40),
66+
)
67+
return WebMCPInvocation(**response.data)
68+
69+
async def get_result(
70+
self,
71+
session_id: str,
72+
invocation_id: str,
73+
params: Optional[Union[WebMCPResultParamsDict, WebMCPResultParams]] = None,
74+
) -> WebMCPInvocation:
75+
"""Read or long-poll once. Running/awaiting_submission can remain pending.
76+
77+
Handles are in memory. Terminal results expire after 10 minutes and may
78+
be evicted sooner; browser shutdown or receiver restart also loses them.
79+
"""
80+
request = coerce_request(
81+
params if params is not None else {}, WebMCPResultParams
82+
)
83+
response = await self._client.transport.get(
84+
self._url(session_id, f"/invocations/{quote(invocation_id, safe='')}"),
85+
params=dump_request(request, WebMCPResultParams),
86+
timeout=max(self._client.timeout, request.wait_seconds + 20),
87+
)
88+
return WebMCPInvocation(**response.data)
89+
90+
async def cancel(self, session_id: str, invocation_id: str) -> WebMCPInvocation:
91+
"""Request best-effort cancellation. Completion can win; side effects persist."""
92+
response = await self._client.transport.post(
93+
self._url(
94+
session_id, f"/invocations/{quote(invocation_id, safe='')}/cancel"
95+
),
96+
timeout=max(self._client.timeout, 20),
97+
)
98+
return WebMCPInvocation(**response.data)

‎hyperbrowser/client/managers/sync_manager/session.py‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import warnings
2+
3+
from .webmcp import WebMCPManager
24
from collections.abc import Mapping
35
from typing import IO, List, Optional, Union, overload
46

@@ -68,6 +70,7 @@ class SessionManager:
6870
def __init__(self, client):
6971
self._client = client
7072
self.event_logs = SessionEventLogsManager(client)
73+
self.webmcp = WebMCPManager(client)
7174

7275
def create(
7376
self,
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from typing import Optional, Union
2+
from urllib.parse import quote
3+
4+
from hyperbrowser.client._request import coerce_request, dump_request
5+
from hyperbrowser.models.webmcp import (
6+
WebMCPInvocation,
7+
WebMCPInvokeParams,
8+
WebMCPInvokeResult,
9+
WebMCPResultParams,
10+
WebMCPStartParams,
11+
WebMCPToolsResponse,
12+
)
13+
from hyperbrowser.types.webmcp import (
14+
WebMCPInvokeParams as WebMCPInvokeParamsDict,
15+
WebMCPResultParams as WebMCPResultParamsDict,
16+
WebMCPStartParams as WebMCPStartParamsDict,
17+
)
18+
19+
20+
class WebMCPManager:
21+
"""Discover and invoke page tools in sessions created with enable_web_mcp=True."""
22+
23+
def __init__(self, client):
24+
self._client = client
25+
26+
def _url(self, session_id: str, suffix: str) -> str:
27+
return self._client._build_url(
28+
f"/session/{quote(session_id, safe='')}/webmcp{suffix}"
29+
)
30+
31+
def list_tools(self, session_id: str) -> WebMCPToolsResponse:
32+
"""Discover document-bound tools across the session's tabs and frames."""
33+
response = self._client.transport.get(
34+
self._url(session_id, "/tools"), timeout=max(self._client.timeout, 40)
35+
)
36+
return WebMCPToolsResponse(**response.data)
37+
38+
def invoke(
39+
self,
40+
session_id: str,
41+
params: Union[WebMCPInvokeParamsDict, WebMCPInvokeParams],
42+
) -> WebMCPInvokeResult:
43+
"""Invoke once and wait. A form can return awaiting_submission with a handle.
44+
45+
This POST is never automatically retried: losing its response can leave the
46+
outcome unknown, and repeating it could duplicate a side effect.
47+
"""
48+
request = coerce_request(params, WebMCPInvokeParams)
49+
response = self._client.transport.post(
50+
self._url(session_id, "/invoke"),
51+
data=dump_request(request, WebMCPInvokeParams),
52+
timeout=max(self._client.timeout, request.timeout_seconds + 35),
53+
)
54+
return WebMCPInvokeResult(**response.data)
55+
56+
def start(
57+
self,
58+
session_id: str,
59+
params: Union[WebMCPStartParamsDict, WebMCPStartParams],
60+
) -> WebMCPInvocation:
61+
"""Start once and return a handle; use get_result to observe its outcome."""
62+
response = self._client.transport.post(
63+
self._url(session_id, "/invocations"),
64+
data=dump_request(params, WebMCPStartParams),
65+
timeout=max(self._client.timeout, 40),
66+
)
67+
return WebMCPInvocation(**response.data)
68+
69+
def get_result(
70+
self,
71+
session_id: str,
72+
invocation_id: str,
73+
params: Optional[Union[WebMCPResultParamsDict, WebMCPResultParams]] = None,
74+
) -> WebMCPInvocation:
75+
"""Read or long-poll once. Running/awaiting_submission can remain pending.
76+
77+
Handles are in memory. Terminal results expire after 10 minutes and may
78+
be evicted sooner; browser shutdown or receiver restart also loses them.
79+
"""
80+
request = coerce_request(
81+
params if params is not None else {}, WebMCPResultParams
82+
)
83+
response = self._client.transport.get(
84+
self._url(session_id, f"/invocations/{quote(invocation_id, safe='')}"),
85+
params=dump_request(request, WebMCPResultParams),
86+
timeout=max(self._client.timeout, request.wait_seconds + 20),
87+
)
88+
return WebMCPInvocation(**response.data)
89+
90+
def cancel(self, session_id: str, invocation_id: str) -> WebMCPInvocation:
91+
"""Request best-effort cancellation. Completion can win; side effects persist."""
92+
response = self._client.transport.post(
93+
self._url(
94+
session_id, f"/invocations/{quote(invocation_id, safe='')}/cancel"
95+
),
96+
timeout=max(self._client.timeout, 20),
97+
)
98+
return WebMCPInvocation(**response.data)

‎hyperbrowser/models/__init__.py‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,18 @@
1+
from .webmcp import (
2+
WebMCPAnnotations,
3+
WebMCPFrame,
4+
WebMCPSource,
5+
WebMCPTool,
6+
WebMCPToolsResponse,
7+
WebMCPInvokeParams,
8+
WebMCPStartParams,
9+
WebMCPResultParams,
10+
WebMCPInvokeStatus,
11+
WebMCPInvocationStatus,
12+
WebMCPInvokeResult,
13+
WebMCPInvocationError,
14+
WebMCPInvocation,
15+
)
116
from .web.batch_fetch import (
217
StartBatchFetchJobParams,
318
StartBatchFetchJobResponse,
@@ -439,6 +454,20 @@
439454
from .team import TeamCreditInfo
440455

441456
__all__ = [
457+
"WebMCPAnnotations",
458+
"WebMCPFrame",
459+
"WebMCPSource",
460+
"WebMCPTool",
461+
"WebMCPToolsResponse",
462+
"WebMCPInvokeParams",
463+
"WebMCPStartParams",
464+
"WebMCPResultParams",
465+
"WebMCPInvokeStatus",
466+
"WebMCPInvocationStatus",
467+
"WebMCPInvokeResult",
468+
"WebMCPInvocationError",
469+
"WebMCPInvocation",
470+
442471
# consts
443472
"ISO639_1",
444473
"POLLING_ATTEMPTS",

‎hyperbrowser/models/session.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,8 @@ class SessionLaunchState(BaseModel):
129129
populate_by_alias=True,
130130
)
131131

132+
enable_web_mcp: Optional[bool] = Field(default=None, alias="enableWebMcp")
133+
132134
use_ultra_stealth: Optional[bool] = Field(default=None, alias="useUltraStealth")
133135
use_stealth: Optional[bool] = Field(default=None, alias="useStealth")
134136
use_proxy: Optional[bool] = Field(default=None, alias="useProxy")
@@ -411,6 +413,10 @@ class CreateSessionParams(BaseModel):
411413
populate_by_alias=True,
412414
)
413415

416+
enable_web_mcp: Optional[bool] = Field(
417+
default=None, serialization_alias="enableWebMcp"
418+
)
419+
414420
use_ultra_stealth: bool = Field(
415421
default=False, serialization_alias="useUltraStealth"
416422
)

0 commit comments

Comments
 (0)