Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,40 @@ or
yarn add @hyperbrowser/sdk
```

## WebMCP page tools

Create a session with `enableWebMcp: true` (default: false), then navigate to a page
that exposes WebMCP tools using Playwright or Puppeteer. With that session open:

```typescript
const discovery = await client.sessions.webmcp.listTools(session.id);
// Select a tool using its name, source page/frame, and input schema.
const tool = discovery.tools.find((tool) => tool.name === "search");
if (!tool) throw new Error("The page has no search tool");
const result = await client.sessions.webmcp.invoke(session.id, {
toolRef: tool.toolRef,
input: { query: "flights to Tokyo" }, // Match the selected tool's schema.
});
console.log(result.status, result.output);
```

For long-running tools or forms awaiting human submission, use
`sessions.webmcp.start(sessionId, params)`, then
`sessions.webmcp.getResult(sessionId, invocationId, { waitSeconds: 30 })` until the
status is terminal. `sessions.webmcp.cancel(sessionId, invocationId)` requests
best-effort cancellation. Blocking invocation defaults to 60 seconds (maximum
120); `start` defaults to 300 seconds (maximum 3,600) via `timeoutSeconds`.

Discover tools again after navigation. Invocations never automatically retry;
losing a response can leave an unknown outcome. Handles are ephemeral and
terminal results expire after 10 minutes or earlier eviction. Check result
status and truncation metadata before using output. Public types, including
`WebMCPTool`, `WebMCPInvocation`, and the request types, are exported from
`@hyperbrowser/sdk/types`.

See the [WebMCP guide](https://hyperbrowser.ai/docs/sessions/webmcp) for complete
examples, cancellation behavior, human submission, and limits.

## Usage

### Playwright
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@hyperbrowser/sdk",
"version": "1.1.0",
"version": "1.2.0",
"description": "Node SDK for Hyperbrowser API",
"author": "",
"repository": {
Expand Down
3 changes: 3 additions & 0 deletions src/services/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
SessionGetParams,
} from "../types/session";
import { BaseService } from "./base";
import { WebMCPService } from "./webmcp";
import { HyperbrowserError } from "../client";

const CAPTCHA_EVALUATION_REQUEST_TIMEOUT_MS = 185_000;
Expand Down Expand Up @@ -65,10 +66,12 @@ class SessionEventLogsService extends BaseService {

export class SessionsService extends BaseService {
public readonly eventLogs: SessionEventLogsService;
public readonly webmcp: WebMCPService;

constructor(apiKey: string, baseUrl: string, timeout: number) {
super(apiKey, baseUrl, timeout);
this.eventLogs = new SessionEventLogsService(apiKey, baseUrl, timeout);
this.webmcp = new WebMCPService(apiKey, baseUrl, timeout);
}

/**
Expand Down
85 changes: 85 additions & 0 deletions src/services/webmcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { BaseService } from "./base";
import {
WebMCPInvocation,
WebMCPInvokeParams,
WebMCPInvokeResult,
WebMCPResultParams,
WebMCPStartParams,
WebMCPToolsResponse,
} from "../types/webmcp";

function seconds(value: number, min: number, max: number, name: string): number {
if (!Number.isInteger(value) || value < min || value > max) {
throw new RangeError(`${name} must be an integer between ${min} and ${max}`);
}
return value;
}

function invocationBody(params: WebMCPInvokeParams): string {
if (typeof params.toolRef !== "string" || !params.toolRef.trim()) {
throw new TypeError("toolRef must be a non-empty string from listTools");
}
if (
params.input !== undefined &&
(params.input === null || typeof params.input !== "object" || Array.isArray(params.input))
) {
throw new TypeError("input must be a JSON object");
}
return JSON.stringify(params);
}

/** Page tools for sessions created with enableWebMcp: true. */
export class WebMCPService extends BaseService {
private path(sessionId: string): string {
return `/session/${encodeURIComponent(sessionId)}/webmcp`;
}

/** Discover current tools across the session's tabs and frames. */
async listTools(sessionId: string): Promise<WebMCPToolsResponse> {
return this.request(`${this.path(sessionId)}/tools`, {
timeout: Math.max(this.timeout, 40_000),
});
}

/** Invoke once. Transport failures never cause an automatic retry. */
async invoke(sessionId: string, params: WebMCPInvokeParams): Promise<WebMCPInvokeResult> {
const timeout = seconds(params.timeoutSeconds ?? 60, 1, 120, "timeoutSeconds");
return this.request(`${this.path(sessionId)}/invoke`, {
method: "POST",
body: invocationBody(params),
timeout: Math.max(this.timeout, (timeout + 35) * 1000),
});
}

/** Start once and return a handle. A lost response may mean the tool already ran. */
async start(sessionId: string, params: WebMCPStartParams): Promise<WebMCPInvocation> {
seconds(params.timeoutSeconds ?? 300, 1, 3600, "timeoutSeconds");
return this.request(`${this.path(sessionId)}/invocations`, {
method: "POST",
body: invocationBody(params),
timeout: Math.max(this.timeout, 40_000),
});
}

/** Read or long-poll a handle. Pending responses do not cancel or restart the tool. */
async getResult(
sessionId: string,
invocationId: string,
params: WebMCPResultParams = {}
): Promise<WebMCPInvocation> {
const wait = seconds(params.waitSeconds ?? 0, 0, 30, "waitSeconds");
return this.request(
`${this.path(sessionId)}/invocations/${encodeURIComponent(invocationId)}`,
{ timeout: Math.max(this.timeout, (wait + 20) * 1000) },
{ waitSeconds: params.waitSeconds }
);
}

/** Request cancellation; completion can win the race. Read getResult for the outcome. */
async cancel(sessionId: string, invocationId: string): Promise<WebMCPInvocation> {
return this.request(
`${this.path(sessionId)}/invocations/${encodeURIComponent(invocationId)}/cancel`,
{ method: "POST", timeout: Math.max(this.timeout, 20_000) }
);
}
}
1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export { HyperbrowserConfig } from "./config";
export * from "./webmcp";
export {
StartCrawlJobParams,
StartCrawlJobResponse,
Expand Down
3 changes: 3 additions & 0 deletions src/types/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface SessionProfile {
}

export interface SessionLaunchState {
enableWebMcp?: boolean;
useUltraStealth?: boolean;
useStealth?: boolean;
useProxy?: boolean;
Expand Down Expand Up @@ -165,6 +166,8 @@ export interface StartSessionFromSnapshotParams {
}

export interface CreateSessionParams {
/** Enable page-provided WebMCP tools. Defaults to false on the server. */
enableWebMcp?: boolean;
useUltraStealth?: boolean;
useStealth?: boolean;
useProxy?: boolean;
Expand Down
95 changes: 95 additions & 0 deletions src/types/webmcp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/** Page-provided hints; these are not browser-enforced guarantees. */
export interface WebMCPAnnotations {
readOnly: boolean;
untrustedContent: boolean;
consequential: boolean;
autosubmit: boolean;
destructive?: boolean;
idempotent?: boolean;
openWorld?: boolean;
}

export interface WebMCPFrame {
id: string;
url: string;
isMainFrame: boolean;
}

export interface WebMCPSource {
provider: "native" | "polyfill";
tabId: string;
pageUrl: string;
pageTitle: string;
frame: WebMCPFrame;
}

export interface WebMCPTool {
/** Opaque reference to this document. Discover tools again after navigation. */
toolRef: string;
name: string;
title?: string;
description: string;
inputSchema?: Record<string, unknown>;
outputSchema?: Record<string, unknown>;
annotations: WebMCPAnnotations;
declarative: boolean;
/** Native DOM node identifier, when available, in the source target/frame. */
backendNodeId?: number;
source: WebMCPSource;
}

export interface WebMCPToolsResponse {
tools: WebMCPTool[];
nativeSupported: boolean;
truncated: boolean;
}

export interface WebMCPInvokeParams {
toolRef: string;
/** Page-defined keys are passed through unchanged. Defaults to {}. */
input?: Record<string, unknown>;
/** Execution deadline in seconds: 1–120, default 60. */
timeoutSeconds?: number;
}

export interface WebMCPStartParams extends WebMCPInvokeParams {
/** Execution deadline including human submission: 1–3600 seconds, default 300. */
timeoutSeconds?: number;
}

export interface WebMCPResultParams {
/** Wait 0–30 seconds, default 0. A pending response does not cancel the tool. */
waitSeconds?: number;
}

export type WebMCPInvokeStatus = "completed" | "error" | "canceled" | "awaiting_submission";
export type WebMCPInvocationStatus = WebMCPInvokeStatus | "running" | "outcome_unknown";

export interface WebMCPInvokeResult {
invocationId?: string;
status: WebMCPInvokeStatus;
output?: unknown;
errorText?: string;
outputBytes: number;
outputTruncated?: boolean;
outputPreview?: string;
untrustedContent: boolean;
durationMs: number;
}

export interface WebMCPInvocationError {
code: string;
message: string;
}

/** Session-local record; results expire and do not survive receiver restarts. */
export interface WebMCPInvocation {
invocationId: string;
toolRef: string;
status: WebMCPInvocationStatus;
cancellationRequested: boolean;
createdAt: string;
expiresAt?: string;
result?: WebMCPInvokeResult;
error?: WebMCPInvocationError;
}
Loading