Skip to content

[feat]: add support for WebMCP - #2178

Merged
seanmcguire12 merged 15 commits into
mainfrom
experimental-add-webmcp
Jun 10, 2026
Merged

[feat]: add support for WebMCP#2178
seanmcguire12 merged 15 commits into
mainfrom
experimental-add-webmcp

Conversation

@seanmcguire12

@seanmcguire12 seanmcguire12 commented Jun 2, 2026

Copy link
Copy Markdown
Member

why

this PR introduces first class WebMCP support to Stagehand. this means callers can:

  • list tools exposed by a given page via page.listWebMCPTools()
  • invoke the given tools via page.invokeWebMCPTool()

what changed

  • added page.listWebMCPTools() to collect the current WebMCP tool snapshot from WebMCP.toolsAdded
  • added page.invokeWebMCPTool() with frame disambiguation, result promises, cancel support, timeouts, & early response handling
  • added WebMCP public types in packages/core/lib/v3/types/public/page.ts
  • added StagehandUnsupportedBrowserFeatureError, which gets thrown when using a version of chrome that doesn't support WebMCP
  • added flowlogger wrappers for PageListWebMCPTools & PageInvokeWebMCPTool
  • also added a WebMCP example in packages/core/examples/webmcp.ts

behavioural notes:

  • page.invokeWebMCPTool() starts the tool invocation & returns a WebMCPToolInvocation once chrome accepts the request. it does not wait for the tool itself to finish
  • to wait for the actual tool execution, callers must do await invocation.result from the returned WebMCPToolInvocation
  • this keeps the returned invocation usable for follow up actions like invocation.cancel() while the tool is still running
  • this PR only implements WebMCP support for top level frames/pages. it does not currently handle WebMCP tools registered in OOPIFs

test plan

  • added tests in packages/core/tests/unit/page-webmcp.test.ts which cover page.listWebMCPTools() snapshot collection, no stale tool reuse, unsupported browser errors, page.invokeWebMCPTool() frame resolution, duplicate tool names, result routing, early responses, cancel, timeout, & close cleanup
  • updated packages/core/tests/unit/helpers/mockCDPSession.ts such that it now supports CDP event listeners so WebMCP events can be tested without a browser
  • public API tests cover the new WebMCP page types & StagehandUnsupportedBrowserFeatureError

@changeset-bot

changeset-bot Bot commented Jun 2, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 45239ef

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@browserbasehq/stagehand Minor
@browserbasehq/stagehand-evals Patch
@browserbasehq/stagehand-server-v3 Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@seanmcguire12
seanmcguire12 force-pushed the experimental-add-webmcp branch from 5d59d30 to 4809e8f Compare June 4, 2026 21:30
@seanmcguire12
seanmcguire12 force-pushed the experimental-add-webmcp branch from 4809e8f to 8dba822 Compare June 8, 2026 22:08
@seanmcguire12
seanmcguire12 marked this pull request as ready for review June 8, 2026 22:24

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 10 files

Confidence score: 3/5

  • There is a concrete stability risk in packages/core/lib/v3/understudy/page.ts: late responses from timed-out or abandoned WebMCP invocations are buffered indefinitely, which can grow memory usage over time on long-lived pages.
  • Given the clear user-impacting behavior (potential memory leak) and relatively high confidence, this sits above minor-risk territory even though it appears localized to one area.
  • Pay close attention to packages/core/lib/v3/understudy/page.ts - ensure buffered late responses are discarded or cleaned up to prevent unbounded memory growth.
Architecture diagram
sequenceDiagram
    participant Client as Client Code
    participant P as Page (Stagehand)
    participant MainS as Main CDP Session
    participant Chrome as Chrome Browser
    participant WebMCP as WebMCP CDP Domain

    Note over Client,WebMCP: WebMCP Tool Listing Flow

    Client->>P: page.listWebMCPTools({ timeoutMs })
    P->>P: ensureWebMCPEnabled() – register onWebMCPToolResponded listener
    P->>MainS: WebMCP.enable
    MainS->>Chrome: Send CDP "WebMCP.enable"
    Chrome->>WebMCP: Enable WebMCP domain
    WebMCP-->>Chrome: Emit "WebMCP.toolsAdded" events
    Chrome-->>MainS: CDP events
    MainS-->>P: onWebMCPToolResponded NOT triggered (wrong event type)
    alt Tools added within timeout window
        WebMCP->>WebMCP: Emit "WebMCP.toolsAdded" events with tool list
        WebMCP-->>Chrome: "WebMCP.toolsAdded" event
        Chrome-->>MainS: CDP event
        MainS->>P: onToolsAdded callback invoked
        P->>P: Strip debug fields (stackTrace), normalize tools
        P->>P: Wait for quiet window (no new tools for up to 100ms)
    else Timeout reached
        P->>P: Force resolve after timeoutMs
    end
    P->>P: Cleanup: remove onToolsAdded listener
    P-->>Client: Return WebMCPTool[] (current snapshot)

    Note over Client,WebMCP: WebMCP Tool Invocation Flow

    Client->>P: page.invokeWebMCPTool(toolName, input, { frameId?, timeoutMs? })
    P->>P: ensureWebMCPEnabled() – return cached promise if already enabled
    opt frameId not provided
        P->>MainS: WebMCP.enable (if not already enabled)
        MainS->>Chrome: Send CDP "WebMCP.enable"
        Chrome-->>MainS: "WebMCP.toolsAdded" events
        MainS-->>P: Collect tools to resolve frameId for toolName
        alt Tool not found or ambiguous across frames
            P-->>Client: Throw StagehandInvalidArgumentError
        end
    end
    P->>MainS: WebMCP.invokeTool({ frameId, toolName, input })
    MainS->>Chrome: Send CDP "WebMCP.invokeTool"
    Chrome->>WebMCP: Start tool execution
    WebMCP-->>Chrome: "WebMCP.invokeTool" response { invocationId }
    Chrome-->>MainS: CDP response { invocationId }
    MainS-->>P: Response { invocationId }
    P->>P: Create deferred promise for invocation.result
    P->>P: Start timeout timer
    P-->>Client: Return WebMCPToolInvocation { invocationId, result, cancel() }

    Note over Client,Chrome: Tool Execution & Result (async)

    alt Tool completes normally
        WebMCP-->>Chrome: "WebMCP.toolResponded" { invocationId, status: "Completed", output }
        Chrome-->>MainS: CDP event
        MainS->>P: onWebMCPToolResponded callback
        P->>P: Clear timeout, resolve deferred with WebMCPToolResult
    else Tool is canceled
        Client->>P: invocation.cancel()
        P->>MainS: WebMCP.cancelInvocation({ invocationId })
        MainS->>Chrome: Send CDP "WebMCP.cancelInvocation"
        Chrome-->>MainS: ACK
        MainS-->>P: Success
        WebMCP-->>Chrome: "WebMCP.toolResponded" { invocationId, status: "Canceled" }
        Chrome-->>MainS: CDP event
        MainS->>P: onWebMCPToolResponded callback
        P->>P: Resolve deferred with Canceled status
    else Tool errors
        WebMCP-->>Chrome: "WebMCP.toolResponded" { invocationId, status: "Error", errorText, exception }
        Chrome-->>MainS: CDP event
        MainS->>P: onWebMCPToolResponded callback
        P->>P: Resolve deferred with Error status
    else Timeout expires
        P->>P: Timer fires – reject deferred with timeout error
        P->>P: Cleanup pending invocation entry
    end

    Note over P,Chrome: Early Response Handling (response before invokeTool returns)

    alt Response arrives before invokeTool ACK
        WebMCP-->>Chrome: "WebMCP.toolResponded" event
        Chrome-->>MainS: CDP event
        MainS->>P: onWebMCPToolResponded callback
        P->>P: invocation not yet in pending map
        P->>P: Buffer result in bufferedWebMCPResults
        Chrome-->>MainS: "WebMCP.invokeTool" response { invocationId }
        MainS-->>P: Response with invocationId
        P->>P: Check buffer – find buffered result
        P->>P: Resolve deferred immediately (clear timeout)
    end

    Note over P: Page Close / Cleanup

    Client->>P: page.close()
    P->>P: teardownWebMCP()
    P->>P: Remove onWebMCPToolResponded listener
    loop For each pending invocation
        P->>P: Clear timeout, reject deferred with disposal error
    end
    P->>P: Clear buffered results and webMCPEnablePromise
Loading

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread packages/core/lib/v3/understudy/page.ts Outdated
@seanmcguire12

Copy link
Copy Markdown
Member Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review

@seanmcguire12 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 10 files

Confidence score: 3/5

  • There is a concrete regression risk in packages/core/lib/v3/understudy/page.ts: invokeWebMCPTool() appears to map all CDP invocation failures to unsupported-browser-feature errors, which can hide real argument or state problems from users and developers.
  • Given the issue’s medium severity (6/10) and high confidence (9/10), this is more than a cosmetic concern and can impact debugging accuracy and runtime behavior, so merge risk is moderate.
  • Pay close attention to packages/core/lib/v3/understudy/page.ts - error classification in invokeWebMCPTool() should preserve true CDP failure causes instead of collapsing them into a single unsupported-feature path.
Architecture diagram
sequenceDiagram
    participant C as Client App
    participant P as Stagehand Page
    participant CDP as CDP Session (Browser)
    participant CE as CDP Event Bus
    participant W as WebMCP Runtime (Browser)
    
    Note over C,P: Listing WebMCP Tools
    
    C->>P: page.listWebMCPTools({ timeoutMs })
    P->>CDP: WebMCP.enable
    CDP->>CE: Emit WebMCP.toolsAdded
    
    loop For each toolsAdded event
        CE->>P: onToolsAdded(event)
        P->>P: Collect tools in local map
        P->>P: Reset quiet timer on change
    end
    
    alt Quiet window expires (no new tools)
        P->>P: Resolve snapshot collection
    else Timeout reached
        P->>P: Force resolve snapshot
    end
    
    P->>CDP: WebMCP.disable
    P->>P: Remove toolsAdded listener
    P-->>C: Return WebMCPTool[]
    
    Note over C,P: Invoking a WebMCP Tool
    
    C->>P: page.invokeWebMCPTool(toolName, input, { frameId?, timeoutMs? })
    
    alt frameId not provided
        P->>P: Call listWebMCPTools (internal)
        alt Tool found in single frame
            P->>P: Use resolved frameId
        else Tool found in multiple frames
            P-->>C: Throw StagehandInvalidArgumentError
        else Tool not found
            P-->>C: Throw StagehandInvalidArgumentError
        end
    end
    
    P->>CDP: WebMCP.invokeTool({ frameId, toolName, input })
    CDP-->>P: Return { invocationId }
    
    P->>P: Create deferred result promise
    P->>P: Set timeout timer
    
    Note over P: Invocation returned to client<br/>while tool executes
    
    P-->>C: Return WebMCPToolInvocation { invocationId, result, cancel() }
    
    alt Normal completion
        CE->>P: WebMCP.toolResponded({ invocationId, status, output })
        P->>P: Clear timeout, resolve deferred
        C->>P: await invocation.result
        P-->>C: Return WebMCPToolResult
    else Early response (before invoke response)
        CE->>P: WebMCP.toolResponded (arrives early)
        P->>P: Buffer result in pendingWebMCPInvokeToolResponses window
        CDP-->>P: Return { invocationId }
        P->>P: Retrieve buffered result, resolve deferred
        C->>P: await invocation.result
        P-->>C: Return WebMCPToolResult
    else Timeout
        P->>P: Timer fires, reject deferred
        C->>P: await invocation.result
        P-->>C: Reject with timeout error
    else Client calls cancel()
        C->>P: invocation.cancel()
        P->>CDP: WebMCP.cancelInvocation({ invocationId })
        alt Tool responds before cancel
            CE->>P: WebMCP.toolResponded (status: Canceled)
            P->>P: Resolve deferred normally
        end
        P-->>C: void
        C->>P: await invocation.result
        P-->>C: Return WebMCPToolResult (status: Canceled)
    end
    
    Note over P: Cleanup & Browser Support
    
    alt Unsupported browser (WebMCP.enable fails)
        P->>P: Catch error, create StagehandUnsupportedBrowserFeatureError
        P-->>C: Throw error with guidance message
    end
    
    opt Page close with pending invocations
        C->>P: page.close()
        P->>P: Reject all pending deferred promises
        P->>P: Clear all timers and buffers
        P->>CDP: Remove WebMCP.toolResponded listener
    end
Loading

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread packages/core/lib/v3/understudy/page.ts
@seanmcguire12

Copy link
Copy Markdown
Member Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review

@seanmcguire12 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 10 files

Confidence score: 3/5

  • There is a concrete error-handling risk in packages/core/lib/v3/understudy/page.ts: listWebMCPTools currently maps all failures to an unsupported-browser error, which can hide actual runtime or CDP failures.
  • Because this can mislead users and operators about the real root cause, it introduces moderate regression/debuggability risk even though it is localized to one function.
  • Pay close attention to packages/core/lib/v3/understudy/page.ts - error classification should preserve real failure types instead of collapsing them into unsupported-browser responses.
Architecture diagram
sequenceDiagram
    participant Client as External Caller
    participant Page as Stagehand Page
    participant MainSession as Main CDP Session
    participant Chrome as Chrome Browser (WebMCP)

    Note over Client,Chrome: WebMCP API Flow

    Client->>Page: page.listWebMCPTools({timeoutMs})

    Page->>MainSession: ensureWebMCPEnabled() - check if already enabled
    MainSession->>MainSession: Register listener for WebMCP.toolResponded

    Page->>MainSession: WebMCP.enable (triggers browser snapshot)
    MainSession-->>Page: WebMCP.toolsAdded (event with all tools)
    MainSession->>MainSession: Listen for WebMCP.toolsAdded events

    loop Collect tools during quiet window
        MainSession-->>Page: WebMCP.toolsAdded (new/changed tools)
        Page->>Page: Accumulate tools, update quiet timer
    end

    Page->>Page: Return collected WebMCPTool array (no stale cache)

    Note over Client,Chrome: Tool Invocation

    Client->>Page: page.invokeWebMCPTool(toolName, input, {frameId?, timeoutMs?})

    alt FrameId not provided
        Page->>Page: listWebMCPTools() to resolve frameId
        alt Multiple frames have same tool name
            Page-->>Client: StagehandInvalidArgumentError (ambiguous)
        end
    end

    Page->>MainSession: WebMCP.invokeTool({frameId, toolName, input})
    alt Unsupported browser
        MainSession-->>Page: Error (code -32601 or "method not found")
        Page-->>Client: StagehandUnsupportedBrowserFeatureError
    end

    MainSession-->>Page: {invocationId}
    Page-->>Client: WebMCPToolInvocation (immediate, not waiting for tool result)

    Note over Client,Chrome: Caller controls result/cancel lifecycle

    alt Early response arrives before invokeTool returns
        MainSession-->>Page: WebMCP.toolResponded (event arrives during send)
        Page->>Page: Buffer result in bufferedWebMCPResults (pending response > 0)
    end

    par Caller waits for result
        Client->>Page: await invocation.result
        alt Result already buffered
            Page-->>Client: Resolve from bufferedWebMCPResults
        else Normal response
            MainSession-->>Page: WebMCP.toolResponded (status, output, error)
            Page->>Page: Clear timer, delete pending, resolve deferred
            Page-->>Client: WebMCPToolResult
        else Timeout reached
            Page->>Page: Reject deferred with timeout error
            Page-->>Client: StagehandInvalidArgumentError (timeout)
        end
    and Caller cancels
        Client->>Page: await invocation.cancel()
        Page->>MainSession: WebMCP.cancelInvocation({invocationId})
        MainSession->>MainSession: WebMCP.toolResponded with Canceled status
        Page->>Page: Clear timer, delete pending, resolve/reject deferred
    end

    alt Page closes
        Page->>Page: teardownWebMCP()
        Page->>MainSession: Remove WebMCP.toolResponded listener
        loop All pending invocations
            Page->>Page: Reject deferred with disposal error
        end
        Page->>Page: Clear pending maps and buffer
    end
Loading

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread packages/core/lib/v3/understudy/page.ts Outdated
@seanmcguire12

Copy link
Copy Markdown
Member Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review

@seanmcguire12 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 12 files

Confidence score: 3/5

  • There is a concrete API-contract risk in packages/core/lib/v3/understudy/page.ts: success statuses from WebMCP may be returned as "Success" instead of the documented "Completed" value.
  • I’m scoring this as moderate merge risk because the issue is medium severity (6/10) with high confidence (8/10) and can directly affect client-side status handling.
  • Pay close attention to packages/core/lib/v3/understudy/page.ts - normalize WebMCP success states to the public API contract before or immediately after merge.
Architecture diagram
sequenceDiagram
    participant Client as Caller Code
    participant Page as Page (understudy/page.ts)
    participant CDP as CDPSession
    participant Browser as Chrome Browser (WebMCP)
    participant Tools as Page WebMCP Tools
    participant Timer as Timeout Timer

    Note over Client,Browser: WebMCP Tool Listing Flow

    Client->>Page: listWebMCPTools({ timeoutMs })
    Page->>CDP: on("WebMCP.toolsAdded")
    Page->>CDP: on("WebMCP.toolsRemoved")
    Page->>CDP: send("WebMCP.enable")
    CDP->>Browser: Enable WebMCP
    Browser-->>CDP: OK

    Browser-->>CDP: event "WebMCP.toolsAdded"
    CDP-->>Page: onToolsAdded(event)
    Page->>Page: Store tools in local Map

    Browser-->>CDP: event "WebMCP.toolsRemoved"
    CDP-->>Page: onToolsRemoved(event)
    Page->>Page: Remove tools from local Map

    Note over Page: Wait for quiet window or timeout

    alt Tools settled within timeout
        Page->>Page: Resolve with collected tools
    else Timeout reached
        Page->>Page: Resolve with current tools snapshot
    end

    Page->>CDP: off("WebMCP.toolsAdded")
    Page->>CDP: off("WebMCP.toolsRemoved")
    Page-->>Client: WebMCPTool[]

    Note over Client,Browser: WebMCP Tool Invocation Flow

    Client->>Page: invokeWebMCPTool(name, input, { frameId, timeoutMs })
    alt frameId not provided
        Page->>Page: Collect tool snapshot to resolve frame
        alt Tool found in single frame
            Page->>Page: Use that frameId
        else Tool ambiguous across frames
            Page-->>Client: Throw StagehandInvalidArgumentError
        end
    end

    Page->>Page: Create deferred result promise
    Page->>CDP: send("WebMCP.invokeTool", { toolName, input, frameId })
    CDP->>Browser: Invoke tool
    alt Browser does not support WebMCP
        Browser-->>CDP: Error: method not found
        CDP-->>Page: Error
        Page->>Page: Check isWebMCPUnsupportedBrowserError
        Page-->>Client: Throw StagehandUnsupportedBrowserFeatureError
    else Browser accepts
        Browser-->>CDP: { invocationId }
        CDP-->>Page: { invocationId }
        Page->>Page: Setup timeout timer
        Page->>Page: Check buffered results for this invocationId
        alt Response already buffered
            Page->>Page: Resolve deferred with buffered result
        end
        Page-->>Client: WebMCPToolInvocation { invocationId, result, cancel }

        alt Tool completes before timeout
            Browser-->>CDP: event "WebMCP.toolResponded"
            CDP-->>Page: onWebMCPToolResponded(event)
            Page->>Timer: Clear timeout
            Page->>Page: Resolve deferred with result
        else Tool responds quickly before invokeTool returns
            Browser-->>CDP: event "WebMCP.toolResponded"
            CDP-->>Page: Buffer result temporarily
            Page->>Page: On invokeTool response, consume buffer
        else Timeout expires
            Timer-->>Page: Timeout callback
            Page->>Page: Reject deferred with timeout error
            Page-->>Client: invocation.result rejects
        end
    end

    Note over Client,Page: Cancel Flow (from returned invocation)

    Client->>Page: invocation.cancel()
    Page->>CDP: send("WebMCP.cancelInvocation", { invocationId })
    CDP->>Browser: Cancel tool
    Browser-->>CDP: OK
    CDP-->>Page: OK
    Page->>Page: Cleanup pending invocation state
    Page-->>Client: void

    Note over Client,Browser: Error Handling: Unsupported Browser

    alt listWebMCPTools with unsupported browser
        Client->>Page: listWebMCPTools()
        Page->>CDP: send("WebMCP.enable")
        CDP->>Browser: Enable WebMCP
        Browser-->>CDP: Error: Method not found (code -32601)
        CDP-->>Page: Error
        Page->>Page: isWebMCPUnsupportedBrowserError()
        Page-->>Client: Throw StagehandUnsupportedBrowserFeatureError
    end

    Note over Page: Cleanup on page close

    Client->>Page: close()
    Page->>Page: teardownWebMCP()
    Page->>CDP: off("WebMCP.toolResponded")
    Page->>Page: Reject all pending deferreds
    Page->>Page: Clear pending map
Loading

Reply with feedback, questions, or to request a fix.

Fix all with cubic | Re-trigger cubic

Comment thread packages/core/lib/v3/understudy/page.ts
@seanmcguire12

Copy link
Copy Markdown
Member Author

@cubic-dev-ai review

@cubic-dev-ai

cubic-dev-ai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

@cubic-dev-ai review

@seanmcguire12 I have started the AI code review. It will take a few minutes to complete.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 12 files

Confidence score: 5/5

  • Automated review surfaced no issues in the provided summaries.
  • No files require special attention.
Architecture diagram
sequenceDiagram
    participant Client as Caller Code
    participant Page as Page (understudy/page.ts)
    participant CDP as CDPSession
    participant Browser as Chrome Browser (WebMCP)
    participant Tools as Page WebMCP Tools
    participant Timer as Timeout Timer

    Note over Client,Browser: WebMCP Tool Listing Flow

    Client->>Page: listWebMCPTools({ timeoutMs })
    Page->>CDP: on("WebMCP.toolsAdded")
    Page->>CDP: on("WebMCP.toolsRemoved")
    Page->>CDP: send("WebMCP.enable")
    CDP->>Browser: Enable WebMCP
    Browser-->>CDP: OK

    Browser-->>CDP: event "WebMCP.toolsAdded"
    CDP-->>Page: onToolsAdded(event)
    Page->>Page: Store tools in local Map

    Browser-->>CDP: event "WebMCP.toolsRemoved"
    CDP-->>Page: onToolsRemoved(event)
    Page->>Page: Remove tools from local Map

    Note over Page: Wait for quiet window or timeout

    alt Tools settled within timeout
        Page->>Page: Resolve with collected tools
    else Timeout reached
        Page->>Page: Resolve with current tools snapshot
    end

    Page->>CDP: off("WebMCP.toolsAdded")
    Page->>CDP: off("WebMCP.toolsRemoved")
    Page-->>Client: WebMCPTool[]

    Note over Client,Browser: WebMCP Tool Invocation Flow

    Client->>Page: invokeWebMCPTool(name, input, { frameId, timeoutMs })
    alt frameId not provided
        Page->>Page: Collect tool snapshot to resolve frame
        alt Tool found in single frame
            Page->>Page: Use that frameId
        else Tool ambiguous across frames
            Page-->>Client: Throw StagehandInvalidArgumentError
        end
    end

    Page->>Page: Create deferred result promise
    Page->>CDP: send("WebMCP.invokeTool", { toolName, input, frameId })
    CDP->>Browser: Invoke tool
    alt Browser does not support WebMCP
        Browser-->>CDP: Error: method not found
        CDP-->>Page: Error
        Page->>Page: Check isWebMCPUnsupportedBrowserError
        Page-->>Client: Throw StagehandUnsupportedBrowserFeatureError
    else Browser accepts
        Browser-->>CDP: { invocationId }
        CDP-->>Page: { invocationId }
        Page->>Page: Setup timeout timer
        Page->>Page: Check buffered results for this invocationId
        alt Response already buffered
            Page->>Page: Resolve deferred with buffered result
        end
        Page-->>Client: WebMCPToolInvocation { invocationId, result, cancel }

        alt Tool completes before timeout
            Browser-->>CDP: event "WebMCP.toolResponded"
            CDP-->>Page: onWebMCPToolResponded(event)
            Page->>Timer: Clear timeout
            Page->>Page: Resolve deferred with result
        else Tool responds quickly before invokeTool returns
            Browser-->>CDP: event "WebMCP.toolResponded"
            CDP-->>Page: Buffer result temporarily
            Page->>Page: On invokeTool response, consume buffer
        else Timeout expires
            Timer-->>Page: Timeout callback
            Page->>Page: Reject deferred with timeout error
            Page-->>Client: invocation.result rejects
        end
    end

    Note over Client,Page: Cancel Flow (from returned invocation)

    Client->>Page: invocation.cancel()
    Page->>CDP: send("WebMCP.cancelInvocation", { invocationId })
    CDP->>Browser: Cancel tool
    Browser-->>CDP: OK
    CDP-->>Page: OK
    Page->>Page: Cleanup pending invocation state
    Page-->>Client: void

    Note over Client,Browser: Error Handling: Unsupported Browser

    alt listWebMCPTools with unsupported browser
        Client->>Page: listWebMCPTools()
        Page->>CDP: send("WebMCP.enable")
        CDP->>Browser: Enable WebMCP
        Browser-->>CDP: Error: Method not found (code -32601)
        CDP-->>Page: Error
        Page->>Page: isWebMCPUnsupportedBrowserError()
        Page-->>Client: Throw StagehandUnsupportedBrowserFeatureError
    end

    Note over Page: Cleanup on page close

    Client->>Page: close()
    Page->>Page: teardownWebMCP()
    Page->>CDP: off("WebMCP.toolResponded")
    Page->>Page: Reject all pending deferreds
    Page->>Page: Clear pending map
Loading

You've manually re-run cubic several times on this PR. Each manual re-review checks the full PR again and counts toward your usage quota. To preserve your usage limits, we recommend letting cubic automatically review new commits.

Re-trigger cubic

@seanmcguire12
seanmcguire12 merged commit c49a3fc into main Jun 10, 2026
235 checks passed
seanmcguire12 pushed a commit that referenced this pull request Jun 19, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @browserbasehq/stagehand@3.6.0

### Minor Changes

- [#2178](#2178)
[`c49a3fc`](c49a3fc)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add support
for WebMCP

### Patch Changes

- [#2217](#2217)
[`147e310`](147e310)
Thanks [@monadoid](https://github.com/monadoid)! - Add Azure OpenAI
Microsoft Entra ID model auth support.

- [#2231](#2231)
[`cf3603d`](cf3603d)
Thanks [@miguelg719](https://github.com/miguelg719)! - Add
claude-fable-5 support: native structured outputs via the
@ai-sdk/anthropic bump, adaptive thinking (including the new "xhigh"
effort) on the agent path, the API's built-in server-side refusal
fallback to claude-opus-4-8, and auto tool choice for the final done
call on models that reject forced tool use.

- [#2233](#2233)
[`8d7d414`](8d7d414)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Normalize
URLs in `ActCache` key derivation by sorting query parameters before
hashing. Semantically equivalent URLs that differ only in parameter
order (e.g. `?utm_source=email&id=42` vs `?id=42&utm_source=email`) now
hit the cache instead of silently missing. Fragments and duplicate keys
are preserved.

- [#2229](#2229)
[`fd42e65`](fd42e65)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - launch
local browser with --enable-features=WebMCPTesting,DevToolsWebMCPSupport
by default

- [#2220](#2220)
[`a64c6b7`](a64c6b7)
Thanks [@monadoid](https://github.com/monadoid)! - Fix
Stagehand-generated shadow-root XPath resolution so deterministic
actions can target elements inside web components.

- [#2132](#2132)
[`ed3e566`](ed3e566)
Thanks [@miguelg719](https://github.com/miguelg719)! - Add canonical
verifier evidence normalization for screenshots and text signals without
requiring image dependencies in core installs.

- [#2133](#2133)
[`840aac8`](840aac8)
Thanks [@miguelg719](https://github.com/miguelg719)! - Add the
rubric-based verifier engine with normalized public rubric output and
bounded failure-step parsing.

## @browserbasehq/stagehand-evals@2.0.3

### Patch Changes

- Updated dependencies
\[[`147e310`](147e310),
[`cf3603d`](cf3603d),
[`8d7d414`](8d7d414),
[`fd42e65`](fd42e65),
[`a64c6b7`](a64c6b7),
[`c49a3fc`](c49a3fc),
[`ed3e566`](ed3e566),
[`840aac8`](840aac8)]:
    -   @browserbasehq/stagehand@3.6.0

## @browserbasehq/stagehand-server-v3@3.7.1

### Patch Changes

- [#2217](#2217)
[`147e310`](147e310)
Thanks [@monadoid](https://github.com/monadoid)! - Add Azure OpenAI
Microsoft Entra ID model auth support.

- Updated dependencies
\[[`147e310`](147e310),
[`cf3603d`](cf3603d),
[`8d7d414`](8d7d414),
[`fd42e65`](fd42e65),
[`a64c6b7`](a64c6b7),
[`c49a3fc`](c49a3fc),
[`ed3e566`](ed3e566),
[`840aac8`](840aac8)]:
    -   @browserbasehq/stagehand@3.6.0

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
felipeofdev-ai pushed a commit to felipeofdev-ai/stagehand that referenced this pull request Aug 4, 2026
# why

this PR introduces first class WebMCP support to Stagehand. this means
callers can:
- list tools exposed by a given page via `page.listWebMCPTools()` 
- invoke the given tools via `page.invokeWebMCPTool()`

# what changed

- added `page.listWebMCPTools()` to collect the current WebMCP tool
snapshot from `WebMCP.toolsAdded`
- added `page.invokeWebMCPTool()` with frame disambiguation, result
promises, cancel support, timeouts, & early response handling
- added WebMCP public types in
`packages/core/lib/v3/types/public/page.ts`
- added `StagehandUnsupportedBrowserFeatureError`, which gets thrown
when using a version of chrome that doesn't support WebMCP
- added flowlogger wrappers for `PageListWebMCPTools` &
`PageInvokeWebMCPTool`
- also added a WebMCP example in `packages/core/examples/webmcp.ts`


### behavioural notes:

- `page.invokeWebMCPTool()` starts the tool invocation & returns a
`WebMCPToolInvocation` once chrome accepts the request. **_it does not
wait for the tool itself to finish_**
- to wait for the actual tool execution, callers must do `await
invocation.result` from the returned `WebMCPToolInvocation`
- this keeps the returned invocation usable for follow up actions like
`invocation.cancel()` while the tool is still running
- this PR only implements WebMCP support for top level frames/pages. it
does not currently handle WebMCP tools registered in OOPIFs

# test plan

- added tests in `packages/core/tests/unit/page-webmcp.test.ts` which
cover `page.listWebMCPTools()` snapshot collection, no stale tool reuse,
unsupported browser errors, `page.invokeWebMCPTool()` frame resolution,
duplicate tool names, result routing, early responses, cancel, timeout,
& close cleanup
- updated `packages/core/tests/unit/helpers/mockCDPSession.ts` such that
it now supports CDP event listeners so WebMCP events can be tested
without a browser
- public API tests cover the new WebMCP page types &
`StagehandUnsupportedBrowserFeatureError`
felipeofdev-ai pushed a commit to felipeofdev-ai/stagehand that referenced this pull request Aug 4, 2026
This PR was opened by the [Changesets
release](https://github.com/changesets/action) GitHub action. When
you're ready to do a release, you can merge this and the packages will
be published to npm automatically. If you're not ready to do a release
yet, that's fine, whenever you add more changesets to main, this PR will
be updated.


# Releases
## @browserbasehq/stagehand@3.6.0

### Minor Changes

- [browserbase#2178](browserbase#2178)
[`c49a3fc`](browserbase@c49a3fc)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - add support
for WebMCP

### Patch Changes

- [browserbase#2217](browserbase#2217)
[`147e310`](browserbase@147e310)
Thanks [@monadoid](https://github.com/monadoid)! - Add Azure OpenAI
Microsoft Entra ID model auth support.

- [browserbase#2231](browserbase#2231)
[`cf3603d`](browserbase@cf3603d)
Thanks [@miguelg719](https://github.com/miguelg719)! - Add
claude-fable-5 support: native structured outputs via the
@ai-sdk/anthropic bump, adaptive thinking (including the new "xhigh"
effort) on the agent path, the API's built-in server-side refusal
fallback to claude-opus-4-8, and auto tool choice for the final done
call on models that reject forced tool use.

- [browserbase#2233](browserbase#2233)
[`8d7d414`](browserbase@8d7d414)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - Normalize
URLs in `ActCache` key derivation by sorting query parameters before
hashing. Semantically equivalent URLs that differ only in parameter
order (e.g. `?utm_source=email&id=42` vs `?id=42&utm_source=email`) now
hit the cache instead of silently missing. Fragments and duplicate keys
are preserved.

- [browserbase#2229](browserbase#2229)
[`fd42e65`](browserbase@fd42e65)
Thanks [@seanmcguire12](https://github.com/seanmcguire12)! - launch
local browser with --enable-features=WebMCPTesting,DevToolsWebMCPSupport
by default

- [browserbase#2220](browserbase#2220)
[`a64c6b7`](browserbase@a64c6b7)
Thanks [@monadoid](https://github.com/monadoid)! - Fix
Stagehand-generated shadow-root XPath resolution so deterministic
actions can target elements inside web components.

- [browserbase#2132](browserbase#2132)
[`ed3e566`](browserbase@ed3e566)
Thanks [@miguelg719](https://github.com/miguelg719)! - Add canonical
verifier evidence normalization for screenshots and text signals without
requiring image dependencies in core installs.

- [browserbase#2133](browserbase#2133)
[`840aac8`](browserbase@840aac8)
Thanks [@miguelg719](https://github.com/miguelg719)! - Add the
rubric-based verifier engine with normalized public rubric output and
bounded failure-step parsing.

## @browserbasehq/stagehand-evals@2.0.3

### Patch Changes

- Updated dependencies
\[[`147e310`](browserbase@147e310),
[`cf3603d`](browserbase@cf3603d),
[`8d7d414`](browserbase@8d7d414),
[`fd42e65`](browserbase@fd42e65),
[`a64c6b7`](browserbase@a64c6b7),
[`c49a3fc`](browserbase@c49a3fc),
[`ed3e566`](browserbase@ed3e566),
[`840aac8`](browserbase@840aac8)]:
    -   @browserbasehq/stagehand@3.6.0

## @browserbasehq/stagehand-server-v3@3.7.1

### Patch Changes

- [browserbase#2217](browserbase#2217)
[`147e310`](browserbase@147e310)
Thanks [@monadoid](https://github.com/monadoid)! - Add Azure OpenAI
Microsoft Entra ID model auth support.

- Updated dependencies
\[[`147e310`](browserbase@147e310),
[`cf3603d`](browserbase@cf3603d),
[`8d7d414`](browserbase@8d7d414),
[`fd42e65`](browserbase@fd42e65),
[`a64c6b7`](browserbase@a64c6b7),
[`c49a3fc`](browserbase@c49a3fc),
[`ed3e566`](browserbase@ed3e566),
[`840aac8`](browserbase@840aac8)]:
    -   @browserbasehq/stagehand@3.6.0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants